1 module vision.json.pointer; 2 3 struct JsonPointer 4 { 5 import std.conv : to; 6 import std.typecons : Nullable; 7 import std.string : replace; 8 import std.json; 9 10 string[] path; 11 12 /** 13 * Constructor from string 14 * @throws Exception if error in path 15 */ 16 @safe this(const string path) 17 { 18 import std.algorithm : splitter, map; 19 import std.range : drop; 20 import std.array : array; 21 22 if (path.length > 0 && path[0] != '/') 23 throw new Exception("Incorrect syntax of JsonPointer: " ~ path); 24 25 this.path = path.splitter('/').map!(s => s.replace("~1", "/") 26 .replace("~0", "~").to!string).drop(1).array; 27 } 28 29 /** 30 * Constructor from array of components 31 * @throws Exception if error in path 32 */ 33 @safe this(const string[] path) 34 { 35 this.path = path.dup; 36 } 37 38 /// encode path component, quoting '~' and '/' symbols according to rfc6901 39 @safe static encodeComponent(string component) pure 40 { 41 return component.replace("~", "~0").replace("/", "~1"); 42 } 43 44 /// find element in given document according to path 45 Nullable!(JSONValue*) evaluate(ref JSONValue root) const 46 { 47 return evaluate(&root); 48 } 49 50 /// find element in given document according to path 51 Nullable!(JSONValue*) evaluate(JSONValue* root) const 52 { 53 import std.conv : to, ConvException; 54 import std.string : startsWith; 55 import std.stdio; 56 57 auto cursor = root; 58 59 foreach (component; path) 60 { 61 with (JSON_TYPE) switch (cursor.type) 62 { 63 case OBJECT: 64 if (component !in *cursor) 65 break; 66 cursor = &((*cursor)[component]); 67 continue; 68 case ARRAY: 69 try 70 { 71 int index = component.to!int; 72 if (index < 0 || index >= cursor.array.length || (component.startsWith("0") && component.length>1)) 73 break; 74 cursor = &(cursor.array[index]); 75 continue; 76 } 77 catch (ConvException e) 78 { 79 break; 80 } 81 default: 82 break; 83 } 84 return Nullable!(JSONValue*)(); 85 } 86 return Nullable!(JSONValue*)(cursor); 87 } 88 89 /// Return true for empty path 90 @property bool isRoot() const @safe 91 { 92 return path.length == 0; 93 } 94 95 /// Get path for parent element 96 @property Nullable!JsonPointer parent() const @safe 97 { 98 return isRoot ? Nullable!JsonPointer() : Nullable!JsonPointer(JsonPointer(path[0 .. $ - 1])); 99 } 100 101 /// Get last component of path 102 @property string lastComponent() const @safe 103 { 104 return path[$ - 1]; 105 } 106 107 /// Convert path to string 108 string toString() const @safe 109 { 110 import std.algorithm : map, joiner; 111 import std.range : chain; 112 113 return path.map!(part => chain("/"c, encodeComponent(part))).joiner("").to!string; 114 } 115 116 } 117 118 unittest 119 { 120 import std.json; 121 122 // test exception for incorrect input value 123 try 124 { 125 JsonPointer("a/b/c"); 126 assert(false, "Incorrect pointer syntax must call exception"); 127 } 128 catch (Exception e) 129 { 130 } 131 132 // tests for path parsing 133 assert(JsonPointer("").path == []); 134 assert(JsonPointer("/").path == [""]); 135 assert(JsonPointer("/a/b/c").path == ["a", "b", "c"]); 136 assert(JsonPointer("/a~0a/b~1b/c~01c/d~10d").path == ["a~a", "b/b", "c~1c", "d/0d"]); 137 assert(JsonPointer("/Киррилица, Ё, ЯФЫЖЭЗЮЙ/إنه نحن العرب") 138 .path == ["Киррилица, Ё, ЯФЫЖЭЗЮЙ", "إنه نحن العرب"]); 139 140 assert(JsonPointer("/a/b/c").parent.path == ["a", "b"]); 141 assert(JsonPointer("/a/b/c").lastComponent == "c"); 142 143 // isRoot() 144 assert(JsonPointer("").isRoot); 145 assert(!JsonPointer("/").isRoot); 146 assert(JsonPointer("").parent.isNull); 147 assert(JsonPointer("/").parent.isRoot); 148 149 // toString tests 150 foreach (p; ["/a/b/c", "/a~0a/b~1b/c~01c/d~10d"]) 151 assert(JsonPointer(p).toString == p); 152 153 // test encodeComponent 154 assert(JsonPointer.encodeComponent("a/b~c") == "a~1b~0c"); 155 156 string s = `{ "language": "D", "rating": 3.5, "code": "42", "o": {"p1": 5, "p2": 6}, "a": [1,2,3,4,5] }`; 157 JSONValue j = parseJSON(s); 158 159 // tests for successful requests 160 assert(JsonPointer("/language").evaluate(j).str == "D"); 161 assert(JsonPointer("/rating").evaluate(j).floating == 3.5); 162 assert(JsonPointer("/o/p1").evaluate(j).integer == 5); 163 assert(JsonPointer("/a/3").evaluate(j).integer == 4); 164 165 // tests for failing requests 166 assert(JsonPointer("/nonexistent").evaluate(j).isNull); 167 assert(JsonPointer("/a/b0").evaluate(j).isNull); 168 assert(JsonPointer("/a/00").evaluate(j).isNull); 169 assert(JsonPointer("/a/20").evaluate(j).isNull); 170 assert(JsonPointer("/a/p3").evaluate(j).isNull); 171 assert(JsonPointer("/rating/0").evaluate(j).isNull); 172 173 // fix #6 174 JSONValue arr = parseJSON("[1,2,3,4,5]"); 175 assert(!JsonPointer("/0").evaluate(arr).isNull); 176 177 } 178