1 /** Extensions to std.json.
2  *
3  * Test: dmd -preview=dip1000 -preview=in -vcolumns -I.. -i -debug -unittest -version=integration_test -main -run json.d
4  */
5 module nxt.json;
6 
7 // version = integration_test;
8 
9 import std.digest.sha : SHA1;
10 import std.json : JSONValue, JSONOptions, parseJSON;
11 import nxt.path : FilePath, DirPath, FileName;
12 
13 private alias Hash = SHA1;
14 
15 @safe:
16 
17 /++ Read JSON from file at `path` via `options`, optionally cached in
18 	`cacheDir`. If `cacheDir` is set try to read a cached result inside
19 	`cacheDir`, using binary (de)serialization functions defined in
20     `nxt.serialization`.
21  +/
22 JSONValue readJSON(const FilePath path, in JSONOptions options = JSONOptions.none, in DirPath cacheDir = [], in bool stripComments = false) {
23 	import nxt.debugio : dbg;
24 	import std.file : readText, read, write;
25 	import nxt.path : buildPath;
26 	import nxt.serialization : serializeRaw, deserializeRaw, Status, Format, CodeUnitType;
27 	import std.array : Appender;
28 
29 	alias Sink = Appender!(CodeUnitType[]);
30 
31 	const fmt = Format(false, true);
32 	const text = path.str.readText();
33 
34 	FilePath cachePath;
35 	Sink sink;
36 
37 	if (cacheDir) {
38 		/+ TODO: functionize somehow: +/
39 		Hash hash;
40 		() @trusted { hash.put(cast(ubyte[])text); }();
41 		const digest = hash.finish;
42 		import std.base64 : Base64URLNoPadding;
43 		const base64 = Base64URLNoPadding.encode(digest);
44 		cachePath = cacheDir.buildPath(FileName((base64 ~ ".json-raw").idup));
45 		try {
46 			// dbg("Loading cache from ", cachePath, " ...");
47 			JSONValue json;
48 			() @trusted {
49 				sink = Sink(cast(ubyte[])cachePath.str.read());
50 				assert(sink.deserializeRaw(json, fmt) == Status(Status.Code.successful));
51 			}();
52 			assert(sink.data.length == 0);
53 			return json;
54 		} catch (Exception e) {
55 			// cache reuse failed
56 		}
57 	}
58 
59 	auto json = stripComments ? text.parseJSONWithHashComments(options) : text.parseJSON(options);
60 
61 	if (cacheDir) {
62 		/+ TODO: use `immutable CodeUnitType` in cases where it avoids allocation of array elements in `deserializeRaw` +/
63 		sink.clear();
64 		sink.reserve(text.length); /+ TODO: predict from `text.length` and `fmt` +/
65 		const size_t initialAddrsCapacity = 0; /+ TODO: predict from `text.length` and `fmt` +/
66 		() @trusted {
67 			sink.serializeRaw(json, fmt, initialAddrsCapacity);
68 			// dbg("Saving cache to ", cachePath, " ...");
69 			// dbg(text.length, " => ", sink.data.length);
70 			cachePath.str.write(sink.data);
71 			debug {
72 				JSONValue jsonCopy;
73 				sink.deserializeRaw(jsonCopy, fmt);
74 				if (json != jsonCopy) {
75 					dbg("JSON:\n", json.toPrettyString);
76 					dbg("!=\n");
77 					dbg("JSON copy:\n", jsonCopy.toPrettyString);
78 				}
79 				assert(json == jsonCopy);
80 			}
81 		}();
82 	}
83 	return json;
84 }
85 
86 version (integration_test)
87 @safe unittest {
88 	import std.json : JSONException;
89 	import std.file : dirEntries, SpanMode;
90 	import nxt.file : homeDir, tempDir;
91 	import nxt.path : buildPath, FileName, baseName;
92 	import nxt.stdio : writeln;
93 	DirPath cacheDir = tempDir;
94 	foreach (dent; dirEntries(homeDir.buildPath(DirPath(".dub/packages.all")).str, SpanMode.breadth)) {
95 		const path = FilePath(dent.name);
96 		if (dent.isDir || path.baseName.str != "dub.json")
97 			continue;
98 		try {
99 			path.readJSON(JSONOptions.none, cacheDir);
100 		} catch (JSONException _) {}
101 	}
102 }
103 
104 /++ Parse JSON without its comments.
105  +/
106 JSONValue parseJSONWithHashComments(const(char)[] json, in JSONOptions options = JSONOptions.none) {
107 	import nxt.algorithm.searching : canFind;
108 	if (!json.canFind('#'))
109 		return json.parseJSON(options); // fast path
110 	return json.stripJSONComments.parseJSON(options);
111 }
112 
113 /++ Strip JSON comments from `s`.
114 	Returns: JSON text `s` without its comments, where each comment matches (rx bol (: space '#')).
115  +/
116 private auto stripJSONComments(in char[] s) pure {
117 	import std.algorithm.iteration : filter, joiner;
118 	import std.string : lineSplitter, stripLeft;
119 	import nxt.algorithm.searching : canFind;
120 	return s.lineSplitter.filter!(line => !line.stripLeft(" \t").canFind('#')).joiner;
121 }