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 }