1 /**
2  * Defines a generic value type for builing and holding JSON documents in memory.
3  *
4  * Copyright: Copyright 2012 - 2015, Sönke Ludwig.
5  * License:   $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
6  * Authors:   Sönke Ludwig
7  * Source:    $(PHOBOSSRC std/data/json/value.d)
8  */
9 module funkwerk.stdx.data.json.value;
10 @safe:
11 
12 ///
13 unittest {
14     // build a simple JSON document
15     auto aa = ["a": JSONValue("hello"), "b": JSONValue(true)];
16     auto obj = JSONValue(aa);
17 
18     // JSONValue behaves almost as the contained native D types
19     assert(obj["a"] == "hello");
20     assert(obj["b"] == true);
21 }
22 
23 import funkwerk.stdx.data.json.foundation;
24 import std.typecons : Nullable;
25 import funkwerk.stdx.data.json.taggedalgebraic;
26 
27 
28 /**
29  * Represents a generic JSON value.
30  *
31  * The $(D JSONValue) type is based on $(D std.variant.Algebraic) and as such
32  * provides the usual binary and unary operators for handling the contained
33  * raw value.
34  *
35  * Raw values can be either $(D null), $(D bool), $(D double), $(D string),
36  * $(D JSONValue[]) or $(D JSONValue[string]).
37 */
38 struct JSONValue
39 {
40     import std.exception : enforce;
41     import funkwerk.stdx.data.json.lexer : JSONToken;
42 
43     /**
44       * Defines the possible types contained in a `JSONValue`
45       */
46     union PayloadUnion {
47         typeof(null) null_; /// A JSON `null` value
48         bool boolean; /// JSON `true` or `false` values
49         double double_; /// The default field for storing numbers
50         long integer; /// Only used if `LexOptions.useLong` was set for parsing
51         WrappedBigInt bigInt; /// Only used if `LexOptions.useBigInt` was set for parsing
52         @disableIndex .string string; /// String value
53         JSONValue[] array; /// Array or JSON values
54         JSONValue[.string] object; /// Dictionary of JSON values (object)
55     }
56 
57     /**
58      * Alias for a $(D TaggedAlgebraic) able to hold all possible JSON
59      * value types.
60      */
61     alias Payload = TaggedAlgebraic!PayloadUnion;
62 
63     /**
64      * Holds the data contained in this value.
65      *
66      * Note that this is available using $(D alias this), so there is usually no
67      * need to access this field directly.
68      */
69     Payload payload;
70 
71     /**
72      * Optional location of the corresponding token in the source document.
73      *
74      * This field will be automatically populated by the JSON parser if location
75      * tracking is enabled.
76      */
77     Location location;
78 
79     ///
80     alias payload this;
81 
82     /**
83      * Constructs a JSONValue from the given raw value.
84      */
85     this(T)(T value, Location loc = Location.init) { payload = Payload(value); location = loc; }
86     /// ditto
87     void opAssign(T)(T value) { payload = value; }
88 
89     /// Tests if the stored value is of a given type.
90     bool hasType(T)() const { return .hasType!T(payload); }
91 
92     /// Tests if the stored value is of kind `Kind.null_`.
93     bool isNull() const { return payload.kind == Kind.null_; }
94 
95     /**
96       * Returns the raw contained value.
97       *
98       * This must only be called if the type of the stored value matches `T`.
99       * Use `.hasType!T` or `.typeID` for that purpose.
100       */
101     ref inout(T) get(T)() inout { return .get!T(payload); }
102 
103     /**
104       * Enables equality comparisons.
105       *
106       * Note that the location is considered token metadata and thus does not
107       * affect the comparison.
108       */
109     bool opEquals(T)(auto ref inout(T) other) inout
110     {
111         import std.traits : Unqual;
112 
113         static if (is(Unqual!T == typeof(null)))
114         {
115             return this.isNull;
116         }
117         else static if (is(Unqual!T == JSONValue))
118         {
119             return this.payload == other.payload;
120         }
121         else
122         {
123             return this.payload == other;
124         }
125     }
126 }
127 
128 /// Shows the basic construction and operations on JSON values.
129 unittest
130 {
131     JSONValue a = 12;
132     JSONValue b = 13;
133 
134     assert(a == 12.0);
135     assert(b == 13.0);
136     assert(a + b == 25.0);
137 
138     auto c = JSONValue([a, b]);
139     assert(c[0] == 12.0);
140     assert(c[1] == 13.0);
141     assert(c[0] == a);
142     assert(c[1] == b);
143 
144     auto d = JSONValue(["a": a, "b": b]);
145     assert(d["a"] == 12.0);
146     assert(d["b"] == 13.0);
147     assert(d["a"] == a);
148     assert(d["b"] == b);
149 }
150 
151 // Unittests for JSONValue equality comparisons
152 unittest
153 {
154     JSONValue nullval = null;
155     assert(nullval.hasType!(typeof(null))());
156     assert(nullval == null);
157     assert(nullval == nullval);
158 
159     JSONValue boolval = true;
160     assert(boolval.hasType!bool());
161     assert(boolval == true);
162     assert(boolval == boolval);
163 
164     JSONValue intval = 22;
165     assert(intval.hasType!long());
166     assert(intval == 22);
167     assert(intval == 22.0);
168     assert(intval == intval);
169 
170     JSONValue longval = 56L;
171     assert(longval.hasType!long());
172     assert(longval == 56);
173     assert(longval == 56.0);
174     assert(longval == longval);
175 
176     assert(intval + longval == 78);
177     assert(intval + longval == intval + longval);
178 
179     JSONValue floatval = 32.0f;
180     assert(floatval.hasType!double());
181     assert(floatval == 32);
182     assert(floatval == 32.0);
183     assert(floatval == floatval);
184 
185     JSONValue doubleval = 63.5;
186     assert(doubleval.hasType!double());
187     assert(doubleval == 63.5);
188     assert(doubleval == doubleval);
189 
190     assert(floatval + doubleval == 95.5);
191     assert(floatval + doubleval == floatval + doubleval);
192     assert(intval + longval + floatval + doubleval == 173.5);
193     assert(intval + longval + floatval + doubleval ==
194            intval + longval + floatval + doubleval);
195 
196     JSONValue strval = "Hello!";
197     assert(strval.hasType!string());
198     assert(strval == "Hello!");
199     assert(strval == strval);
200 
201     auto arrval = JSONValue([floatval, doubleval]);
202     assert(arrval.hasType!(JSONValue[])());
203     assert(arrval == [floatval, doubleval]);
204     assert(arrval == [32.0, 63.5]);
205     assert(arrval[0] == floatval);
206     assert(arrval[0] == 32.0);
207     assert(arrval[1] == doubleval);
208     assert(arrval[1] == 63.5);
209     assert(arrval == arrval);
210 
211     auto objval = JSONValue(["f": floatval, "d": doubleval]);
212     assert(objval.hasType!(JSONValue[string])());
213     assert(objval["f"] == floatval);
214     assert(objval["f"] == 32.0);
215     assert(objval["d"] == doubleval);
216     assert(objval["d"] == 63.5);
217     assert(objval == objval);
218 }
219 
220 
221 /// Proxy structure that stores BigInt as a pointer to save space in JSONValue
222 static struct WrappedBigInt {
223     import std.bigint;
224     private BigInt* _pvalue;
225     ///
226     this(BigInt value) { _pvalue = new BigInt(value); }
227     ///
228     @property ref inout(BigInt) value() inout { return *_pvalue; }
229 }
230 
231 
232 /**
233   * Allows safe access of sub paths of a `JSONValue`.
234   *
235   * Missing intermediate values will not cause an error, but will instead
236   * just cause the final path node to be marked as non-existent. See the
237   * example below for the possbile use cases.
238   */
239 auto opt()(auto ref JSONValue val)
240 {
241     alias C = JSONValue; // this function is generic and could also operate on BSONValue or similar types
242     static struct S(F...) {
243         private {
244             static if (F.length > 0) {
245                 S!(F[0 .. $-1])* _parent;
246                 F[$-1] _field;
247             }
248             else
249             {
250                 C* _container;
251             }
252         }
253 
254         static if (F.length == 0)
255         {
256             this(ref C container)
257             {
258                 () @trusted { _container = &container; } ();
259             }
260         }
261         else
262         {
263             this (ref S!(F[0 .. $-1]) s, F[$-1] field)
264             {
265                 () @trusted { _parent = &s; } ();
266                 _field = field;
267             }
268         }
269 
270         @disable this(); // don't let the reference escape
271 
272         @property bool exists() const { return resolve !is null; }
273 
274         inout(JSONValue) get() inout
275         {
276             auto val = this.resolve();
277             if (val is null)
278                 throw new .Exception("Missing JSON value at "~this.path()~".");
279             return *val;
280         }
281 
282         inout(T) get(T)(T def_value) inout
283         {
284             auto val = resolve();
285             if (val is null || !val.hasType!T)
286                 return def_value;
287             return val.get!T;
288         }
289 
290         alias get this;
291 
292         @property auto opDispatch(string name)()
293         {
294             return S!(F, string)(this, name);
295         }
296 
297         @property void opDispatch(string name, T)(T value)
298         {
299             (*resolveWrite(OptWriteMode.dict))[name] = value;
300         }
301 
302         auto opIndex()(size_t idx)
303         {
304             return S!(F, size_t)(this, idx);
305         }
306 
307         auto opIndex()(string name)
308         {
309             return S!(F, string)(this, name);
310         }
311 
312         auto opIndexAssign(T)(T value, size_t idx)
313         {
314             *(this[idx].resolveWrite(OptWriteMode.any)) = value;
315         }
316 
317         auto opIndexAssign(T)(T value, string name)
318         {
319             (*resolveWrite(OptWriteMode.dict))[name] = value;
320         }
321 
322         private inout(C)* resolve()
323         inout {
324             static if (F.length > 0)
325             {
326                 auto c = this._parent.resolve();
327                 if (!c) return null;
328                 static if (is(F[$-1] : long)) {
329                     if (!c.hasType!(C[])) return null;
330                     if (_field < c.length) return &c.get!(C[])[_field];
331                     return null;
332                 }
333                 else
334                 {
335                     if (!c.hasType!(C[string])) return null;
336                     return this._field in *c;
337                 }
338             }
339             else
340             {
341                 return _container;
342             }
343         }
344 
345         private C* resolveWrite(OptWriteMode mode)
346         {
347             C* v;
348             static if (F.length == 0)
349             {
350                 v = _container;
351             }
352             else
353             {
354                 auto c = _parent.resolveWrite(is(F[$-1] == string) ? OptWriteMode.dict : OptWriteMode.array);
355                 static if (is(F[$-1] == string))
356                 {
357                     v = _field in *c;
358                     if (!v)
359                     {
360                         (*c)[_field] = mode == OptWriteMode.dict ? C(cast(C[string])null) : C(cast(C[])null);
361                         v = _field in *c;
362                     }
363                 }
364                 else
365                 {
366                     import std.conv : to;
367                     if (_field >= c.length)
368                         throw new Exception("Array index "~_field.to!string()~" out of bounds ("~c.length.to!string()~") for "~_parent.path()~".");
369                     v = &c.get!(C[])[_field];
370                 }
371             }
372 
373             final switch (mode)
374             {
375                 case OptWriteMode.dict:
376                     if (!v.hasType!(C[string]))
377                         throw new .Exception(pname()~" is not a dictionary/object. Cannot set a field.");
378                     break;
379                 case OptWriteMode.array:
380                     if (!v.hasType!(C[]))
381                         throw new .Exception(pname()~" is not an array. Cannot set an entry.");
382                     break;
383                 case OptWriteMode.any: break;
384             }
385 
386             return v;
387         }
388 
389         private string path() const
390         {
391             static if (F.length > 0)
392             {
393                 import std.conv : to;
394                 static if (is(F[$-1] == string)) return this._parent.path() ~ "." ~ this._field;
395                 else return this._parent.path() ~ "[" ~ this._field.to!string ~ "]";
396             }
397             else
398             {
399                 return "";
400             }
401         }
402 
403         private string pname() const
404         {
405             static if (F.length > 0) return "Field "~_parent.path();
406             else return "Value";
407         }
408     }
409 
410     return S!()(val);
411 }
412 
413 ///
414 unittest
415 {
416     import std.exception : assertThrown;
417 
418     JSONValue subobj = ["b": JSONValue(1.0), "c": JSONValue(2.0)];
419     JSONValue subarr = [JSONValue(3.0), JSONValue(4.0), JSONValue(null)];
420     JSONValue obj = ["a": subobj, "b": subarr];
421 
422     // access nested fields using member access syntax
423     assert(opt(obj).a.b == 1.0);
424     assert(opt(obj).a.c == 2.0);
425 
426     // get can be used with a default value
427     assert(opt(obj).a.c.get(-1.0) == 2.0); // matched path and type
428     assert(opt(obj).a.c.get(null) == null); // mismatched type -> return default value
429     assert(opt(obj).a.d.get(-1.0) == -1.0); // mismatched path -> return default value
430 
431     // explicit existence check
432     assert(!opt(obj).x.exists);
433     assert(!opt(obj).a.x.y.exists); // works for nested missing paths, too
434 
435     // instead of using member syntax, index syntax can be used
436     assert(opt(obj)["a"]["b"] == 1.0);
437 
438     // integer indices work, too
439     assert(opt(obj).b[0] == 3.0);
440     assert(opt(obj).b[1] == 4.0);
441     assert(opt(obj).b[2].exists);
442     assert(opt(obj).b[2] == null);
443     assert(!opt(obj).b[3].exists);
444 
445     // accessing a missing path throws an exception
446     assertThrown(opt(obj).b[3] == 3);
447 
448     // assignments work, too
449     opt(obj).b[0] = 12;
450     assert(opt(obj).b[0] == 12);
451 
452     // assignments to non-existent paths automatically create all missing parents
453     opt(obj).c.d.opDispatch!"e"( 12);
454     assert(opt(obj).c.d.e == 12);
455 
456     // writing to paths with conflicting types will throw
457     assertThrown(opt(obj).c[2] = 12);
458 
459     // writing out of array bounds will also throw
460     assertThrown(opt(obj).b[10] = 12);
461 }
462 
463 private enum OptWriteMode {
464     dict,
465     array,
466     any
467 }