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