1 module jsonwrap;
2 /++ Some extra functions to make easier to work with json
3 ---
4 import jsonwrap;
5 
6 void main()
7 {
8 	import std.exception :assertThrown;
9 
10    // This works with CTFE, too.
11 	auto j = JSOB(
12 		"field1", "value1",
13 		"field2", JSOB(
14 			"subfield1", "value2",
15 			"subfield2", 3,
16 			"subfield3", [1,2,3],
17 		),
18 		"field3", JSAB("mixed", 10, "array", JSOB("obj", 15))
19 	);
20 
21    // Or
22    // auto j = parseJSON(`{"field1" : "value1", "field2" : "..."}`);
23 
24 	// Read will throw on error
25 	assert(j.read!string("/field2/subfield1") == "value2");
26 	assert(j.read!int("/field3/1") == 10);
27 	assert(j.read!int("/field3/3/obj") == 15);
28 	assertThrown(j.read!string("/field2/subfield2")); // Wrong type
29 
30 	// Safe return default value on error
31 	assert(j.safe!string("/field2/subfield2") == string.init);  // subfield2 is a int, wrong type.
32 	assert(j.safe!string("/field2/wrong/path") == string.init);
33 	assert(j.safe!string("/field2/wrong/path", "default") == "default");
34 
35 	// Like safe, but it tries to convert
36 	assert(j.as!string("/field2/subfield1"), "value2");
37 	assert(j.as!string("/field2/subfield2"), "3");
38 
39 	// Check if a key exists
40 	assert(j.exists("/field2/subfield1") == true);
41 	assert(j.exists("/field3/test") == false);
42 
43 	// Remove a key
44 	assert(j.exists("/field2/subfield2") == true);
45 	j.remove("/field2/subfield2");
46 	assert(j.exists("/field2/subfield2") == false);
47 
48   // Add a new value, recreating the whole tree
49   j.put("hello/world/so/deep", "yay!");
50   assert(j.exists("hello/world/so/deep") == true);
51 
52 }
53 ---
54 +/
55 
56 import std.json : JSONValue, JSONType;
57 import std.string : isNumeric, indexOf;
58 import std.typecons : Tuple;
59 import std.conv : to;
60 import std.traits : isIntegral, isSomeString;
61 
62 alias JSOB = JsonObjectBuilder;  /// Shorthand
63 alias JSAB = JsonArrayBuilder;   /// Shorthand
64 
65 deprecated(".get() is deprecated. Use .safe() instead")
66 {
67    alias get = jsonwrap.safe;
68 }
69 
70 /// A simple struct. It is returned by .safe and .as functions
71 struct SafeValue(T)
72 {
73 	@safe @nogc nothrow
74 	this(in bool exists, in bool ok, in T value = T.init)
75 	{
76 		this._exists = exists;
77 		this._ok = ok;
78 		this.value = value;
79 	}
80 
81    // Return true if key is found.
82    @property @safe @nogc nothrow exists() inout { return _exists; }
83 
84    // Return true if value is read without errors
85    @property @safe @nogc nothrow ok() inout { return _ok; }
86 
87 	private bool _exists   = false;
88 	private bool _ok 	     = false;
89 
90    T value = T.init;
91 
92    alias value this;
93 }
94 
95 /// It allows you to read deep values inside a json. If possibile it converts value to type T.
96 pure nothrow
97 SafeValue!T as(T)(in JSONValue json, in string path = "", /* lazy */ in T defaultValue = T.init)
98 {
99    // A way to check if to!T is valid
100    pure
101    void tryConv(T, K)(in K value, ref SafeValue!T result)
102    {
103       static if (__traits(compiles,to!T(value)))
104       {
105          result.value = to!T(value);
106          result._ok = true;
107       }
108       else
109       {
110          result.value = defaultValue;
111          result._ok = false;
112 
113       }
114    }
115 
116    immutable 	splitted = split_json_path(path);
117 	immutable  	isLast	= splitted.remainder.length == 0;
118 	JSONValue 	value;
119 
120    // Split the path passed in tokens and take the first JSONValue
121 	try
122 	{
123 		if (json.type() == JSONType.object) value = json[splitted.token];
124 		else if (json.type() == JSONType.array) value = json[to!size_t(splitted.token)];
125       else value = json;
126    }
127    catch (Exception e) {  return SafeValue!T(false, false, defaultValue); }
128 
129 	immutable type	= value.type();
130 
131 	// The token is a leaf on json, but it's not a leaf on requested path
132 	if (!isLast && type != JSONType.array && type != JSONType.object)
133       return SafeValue!T(false, false, defaultValue);
134 
135 	SafeValue!T result = SafeValue!T(true, true, defaultValue);
136 
137    try
138    {
139    	final switch(type)
140    	{
141    		case JSONType.null_:
142    			result._ok 	= is(T == typeof(null));
143    			break;
144 
145    		case JSONType.false_:
146    			static if (is(T == bool)) result.value = false;
147    			else tryConv!T(false, result);
148    			break;
149 
150    		case JSONType.true_:
151    			static if (is(T == bool)) result.value = true;
152    			else tryConv!T(true, result);
153    			break;
154 
155    		case JSONType.float_:
156             static if (is(T == float) || is(T == double)) result.value = to!T(value.floating());
157             else tryConv!T(value.floating(), result);
158             break;
159 
160    		case JSONType.integer:
161             static if (isIntegral!T) result.value = to!T(value.integer());
162             else tryConv!T(value.integer(), result);
163    			break;
164 
165    		case JSONType.uinteger:
166             static if (isIntegral!T) result.value = to!T(value.uinteger());
167             else tryConv!T(value.uinteger(), result);
168    			break;
169 
170    		case JSONType..string:
171             static if (isSomeString!T) result.value = to!T(value.str());
172             else tryConv!T(value.str(), result);
173    			break;
174 
175          case JSONType.object:
176    			if (isLast)
177    			{
178                // We are on the last token of path and we have a object. If user asks for a JSONValue it's ok.
179    				static if (is(T == JSONValue)) result.value = value.object();
180    				else result._ok = false;
181    			}
182             // Recursion: read next part of path
183    			else return as!T(value, splitted.remainder, defaultValue);
184    			break;
185 
186          // Ricorsivo: richiamo per l'elemento indicizzato con il percorso accorciato
187          case JSONType.array:
188    			if (isLast)
189    			{
190    				// We are on the last token of path and we have an array. If user asks for a JSONValue it's ok.
191    				static if  (is(T == JSONValue)) result.value = value.array();
192    				else result._ok = false;
193    			}
194    			// Recursion: read next part of path
195    			else return as!T(value, splitted.remainder, defaultValue);
196    			break;
197    	}
198 
199    }
200    catch (Exception ce)
201    {
202       // Something goes wrong with conversions. Sorry, we give you back a default value
203       return SafeValue!T(true, false, defaultValue);
204    }
205 
206 	return result;
207 }
208 
209 /// Shortcut. You can write as!null instead of as!(typeof(null))
210 pure nothrow
211 SafeValue!(typeof(null)) as(typeof(null) T)(in JSONValue json, in string path = "")
212 {
213    return as!(typeof(null))(json, path);
214 }
215 
216 unittest
217 {
218 	immutable js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2)));
219 
220 	assert(js.as!(typeof(null))("null").ok == true);
221 	assert(js.as!(typeof(null))("string").ok == false);
222 	assert(js.as!string("/string") == "str");
223 	assert(js.as!string("/obj/int") == "1");
224 	assert(js.as!int("/obj/arr/0") == 1);
225 	assert(js.as!int("/obj/arr/1") == 2);
226 	assert(js.as!float("/obj/float") == 3.0f);
227 	assert(js.as!int("/obj/int/blah").exists == false);
228 	assert(js.as!string("bau").exists == false);
229 	assert(js.as!int("/string").exists == true);
230 	assert(js.as!int("/string").ok == false);
231 }
232 
233 /// Works like as!T but it doesn't convert between types.
234 pure nothrow
235 SafeValue!T safe(T)(in JSONValue json, in string path = "", in T defaultValue = T.init)
236 {
237    alias Ret = SafeValue!T;
238 
239 	immutable 	splitted = split_json_path(path);
240    immutable   isLast 	= splitted.remainder.length == 0;
241    JSONValue   value;
242 
243    // Split the path passed in tokens and take the first JSONValue
244 	try
245    {
246       if (json.type() == JSONType.object) value = json[splitted.token];
247       else if (json.type() == JSONType.array) value = json[to!size_t(splitted.token)];
248       else value = json;
249    }
250    catch (Exception e)
251    {
252 		return Ret(false, false, defaultValue);
253    }
254 
255    immutable type  = value.type();
256 
257    // The token is a leaf on json, but it's not a leaf on requested path
258 	if (!isLast && type != JSONType.array && type != JSONType.object)
259       return Ret(false, false, defaultValue);
260 
261    try
262    {
263       final switch(type)
264       {
265          case JSONType.null_:       static if (is(T == typeof(null))) return Ret(true, true, null); else break;
266          case JSONType.false_:      static if (is(T == bool)) return Ret(true, true, false); else break;
267          case JSONType.true_:       static if (is(T == bool)) return Ret(true, true, true); else break;
268          case JSONType.float_:      static if (is(T == float) || is(T == double)) return Ret(true, true, value.floating()); else break;
269          case JSONType.integer:    static if (isIntegral!T) return Ret(true, true, to!T(value.integer())); else break;
270          case JSONType.uinteger:   static if (isIntegral!T) return Ret(true, true, to!T(value.uinteger())); else break;
271          case JSONType..string:     static if (isSomeString!T) return Ret(true, true, value.str()); else break;
272 
273          case JSONType.object:
274             if (isLast) {
275                // See also: as!T
276                static if (is(T == JSONValue))
277                   return Ret(true, true, JSONValue(value.object));
278                else break;
279             }
280             else return safe!T(value, splitted.remainder, defaultValue);
281 
282          case JSONType.array:
283             if (isLast) {
284                // See also: as!T
285                static if (is(T == JSONValue))
286                   return Ret(true, true, JSONValue(value.array));
287                else break;
288             }
289             else return safe!T(value, splitted.remainder, defaultValue);
290       }
291    }
292    catch (Exception e)
293    {
294       return Ret(true, false, defaultValue);
295    }
296 
297    // Wrong conversion requested.
298    return Ret(true, false, defaultValue);
299 }
300 
301 /// Shortcut. You can write safe!null instead of safe!(typeof(null))
302 pure nothrow
303 SafeValue!(typeof(null)) safe(typeof(null) T)(in JSONValue json, in string path = "")
304 {
305    return safe!(typeof(null))(json, path);
306 }
307 
308 unittest
309 {
310 	immutable js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2)));
311 
312 	assert(js.safe!(typeof(null))("null").ok == true);
313 	assert(js.safe!(typeof(null))("string").ok == false);
314 	assert(js.safe!string("/string") == "str");
315 
316 	assert(js.safe!string("/obj/int").ok == false);
317 	assert(js.safe!string("/obj/int") == string.init);
318 
319 	assert(js.safe!int("/obj/arr/0").ok == false);
320 	assert(js.safe!int("/obj/arr/0") == int.init);
321 
322 	assert(js.safe!int("/obj/arr/1") == 2);
323 	assert(js.safe!float("/obj/float") == 3.0f);
324 	assert(js.safe!int("/obj/int/blah").exists == false);
325 	assert(js.safe!string("bau").exists == false);
326 	assert(js.safe!int("/string").exists == true);
327 	assert(js.safe!int("/string").ok == false);
328 }
329 
330 unittest
331 {
332 	immutable js = JSOB("notnull", 0, "null", null);
333 
334 	assert(js.as!null("/null").ok == true);
335 	assert(js.as!null("/notnull").ok == false);
336 
337 	assert(js.safe!null("/null").ok == true);
338 	assert(js.safe!null("/notnull").ok == false);
339 }
340 
341 /// Works like safe but return T instead of SafeValue!T and throw an exception if something goes wrong (can't convert value or can't find key)
342 pure
343 T read(T)(in JSONValue json, in string path = "")
344 {
345 	auto ret = safe!T(json, path);
346 
347    if (!ret.ok || !ret.exists)
348       throw new Exception("Can't read " ~ path ~ " from json");
349 
350    return ret.value;
351 }
352 
353 unittest
354 {
355    import std.exception: assertThrown;
356    immutable js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2)));
357 
358    assert(js.read!string("string") == "str");
359    assert(js.read!int("/obj/int") == 1);
360    assertThrown(js.read!int("string"));
361    assertThrown(js.read!int("other"));
362 }
363 
364 
365 /// Write a value. It creates missing objects and array (also missing elements)
366 pure
367 ref JSONValue put(T)(return ref JSONValue json, in string path, in T value)
368 {
369    // Take a token from path
370    immutable splitted = split_json_path(path);
371    immutable isLast   = splitted.remainder.length == 0;
372 
373 	enum nullValue = JSONValue(null);
374 
375    // If token is a number, we are trying to write an array.
376    if (isNumeric(splitted.token))
377    {
378       immutable idx = to!size_t(splitted.token);
379 
380       // Are we reading an existing element from an existing array?
381       if (json.type == JSONType.array && json.array.length > idx)
382       {
383          if (!isLast) put!T(json.array[idx], splitted.remainder, value);
384          else json.array[idx] = value;
385       }
386       else
387       {
388          if (json.type != JSONType.array)
389             json = JSONValue[].init;
390 
391          json.array.length = idx+1;
392 
393          if (!isLast) put!T(json.array[idx], splitted.remainder, value);
394          else json.array[idx] = value;
395       }
396    }
397    // If token is *NOT* a number, we are trying to write an object.
398    else
399    {
400       immutable idx = splitted.token;
401 
402       // Are we reading an existing object?
403       if (json.type == JSONType.object)
404       {
405          if (!isLast)
406          {
407             if (idx !in json.object)
408                json.object[idx] = nullValue;
409 
410             put!T(json.object[idx], splitted.remainder, value);
411          }
412          else json.object[idx] = value;
413       }
414       else
415       {
416          json = string[string].init;
417 
418          if (!isLast)
419          {
420             json.object[idx] = nullValue;
421             put!T(json.object[idx], splitted.remainder, value);
422          }
423          else json.object[idx] = value;
424       }
425    }
426 
427    return json;
428 }
429 
430 unittest
431 {
432 	auto js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2)));
433 
434 	js.put("/string", "hello");
435 	js.put("/null/not", 10);
436 	js.put("/obj/arr/3", JSOB);
437 	js.put("hello", "world");
438 
439 	assert(js.safe!string("/string") == "hello");
440 	assert(js.safe!int("/null/not") == 10);
441 	assert(js.safe!null("/obj/arr/2").ok);
442 	assert(js.safe!JSONValue("/obj/arr/3") == JSOB);
443 	assert(js.safe!JSONValue("/obj/arr/3").ok == true);
444 	assert(js.safe!string("hello") == "world");
445 }
446 
447 /// Remove a field (if it exists). It returns the object itself
448 pure
449 ref JSONValue remove(return ref JSONValue json, in string path)
450 {
451    immutable splitted 	= split_json_path(path);
452    immutable isLast  	= splitted.remainder.length == 0;
453 
454    // See above
455    if (isNumeric(splitted.token))
456    {
457       immutable idx = to!size_t(splitted.token);
458 
459       if (json.type == JSONType.array && json.array.length > idx)
460       {
461          if (isLast) json.array = json.array[0..idx] ~ json.array[idx+1 .. $];
462          else  json.array[idx].remove(splitted.remainder);
463       }
464 
465    }
466    else
467    {
468       immutable idx = splitted.token;
469 
470       if (json.type == JSONType.object && idx in json.object)
471       {
472          if (isLast) json.object.remove(idx);
473          else json.object[idx].remove(splitted.remainder);
474       }
475    }
476 
477 
478    return json;
479 }
480 
481 /// Check if a field exists or not
482 pure
483 bool exists(in JSONValue json, in string path)
484 {
485    immutable splitted 	= split_json_path(path);
486    immutable isLast  	= splitted.remainder.length == 0;
487 
488    // See above
489    if (isNumeric(splitted.token))
490    {
491       immutable idx = to!size_t(splitted.token);
492 
493       if (json.type == JSONType.array && json.array.length > idx)
494       {
495          if (isLast) return true;
496          else return json.array[idx].exists(splitted.remainder);
497       }
498 
499    }
500    else
501    {
502       immutable idx = splitted.token;
503 
504       if (json.type == JSONType.object && idx in json.object)
505       {
506          if (isLast) return true;
507          else return json.object[idx].exists(splitted.remainder);
508       }
509    }
510 
511    return false;
512 }
513 
514 unittest
515 {
516 	auto js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2)));
517 
518 	js.put("/string", "hello");
519 	js.put("/null/not", 10);
520 	js.put("/obj/arr/3", JSOB);
521 	js.put("hello", "world");
522 
523 	js.remove("/obj/arr/2");
524 	js.remove("string");
525 
526 	assert(js.exists("/string") == false);
527 	assert(js.exists("/obj/arr/3") == false);
528 	assert(js.exists("/obj/arr/2") == true);
529 	assert(js.safe!JSONValue("/obj/arr/2") == JSOB);
530 }
531 
532 
533 private alias SplitterResult = Tuple!(string, "token", string, "remainder");
534 
535 /// Used to split path like /hello/world in tokens
536 pure nothrow @safe @nogc
537 private SplitterResult split_json_path(in string path)
538 {
539 	immutable idx = path.indexOf('/');
540 
541 	switch (idx)
542 	{
543 		case  0: return split_json_path(path[1..$]);
544 		case -1: return SplitterResult(path, string.init);
545 		default: return SplitterResult(path[0..idx], path[idx+1..$]);
546 	}
547 
548 	assert(0);
549 }
550 
551 /// You can build a json object with JsonObjectBuilder("key", 32, "another_key", "hello", "subobject", JsonObjectBuilder(...));
552 pure
553 JSONValue JsonObjectBuilder(T...)(T vals)
554 {
555    void appendJsonVals(T...)(ref JSONValue value, T vals)
556    {
557       // Appends nothing, recursion ends
558       static if (vals.length == 0) return;
559 
560       // We're working with a tuple (key, value, key, value, ...) so args%2==0 is key and args%2==1 is value
561       else static if (vals.length % 2 == 0)
562       {
563          // Key should be a string!
564          static if (!isSomeString!(typeof(vals[0])))
565             throw new Exception("Wrong param type. Key not valid.");
566 
567 			else value[vals[0]] = vals[1];
568 
569          // Recursion call
570          static if (vals.length > 2)
571             appendJsonVals(value, vals[2..$]);
572 
573       } else throw new Exception("Wrong params. Should be: JsonObjectBuilder(string key1, T1 val1, string key2, T2 val2, ...)");
574    }
575 
576    JSONValue value = string[string].init;
577 
578    static if (vals.length > 0)
579 		appendJsonVals(value, vals);
580 
581    return value;
582 }
583 
584 /// You can build a json array with JsonArrayBuilder("first", 32, "another_element", 2, 23.4, JsonObjectBuilder(...));
585 pure
586 JSONValue JsonArrayBuilder(T...)(T vals)
587 {
588    JSONValue value = JSONValue[].init;
589    value.array.length = vals.length;
590 
591    foreach(idx, v; vals)
592       value[idx] = v;
593 
594    return value;
595 }
596 
597 unittest
598 {
599    {
600       enum js = JSOB("array", JSAB(1,2,"blah"), "subobj", JSOB("int", 1, "string", "str", "array", [1,2,3]));
601       assert(js.safe!int("/array/1") == 2);
602       assert(js.safe!int("/subobj/int") == 1);
603       assert(js.safe!string("/subobj/string") == "str");
604       assert(js.as!string("/subobj/array/2") == "3");
605       assert(js.exists("/subobj/string") == true);
606       assert(js.exists("/subobj/other") == false);
607 
608       // /array/1 it's an integer
609       {
610          // Can't get a string
611          {
612             immutable val = js.safe!string("/array/1", "default");
613             assert(val.exists == true);
614             assert(val.ok == false);
615             assert(val == "default");
616          }
617 
618          // Can read as string
619          {
620             immutable val = js.as!string("/array/1", "default");
621             assert(val.exists == true);
622             assert(val.ok == true);
623             assert(val == "2");
624          }
625       }
626 
627 
628       // This value doesn't exist
629       {
630          immutable val = js.as!string("/subobj/other", "default");
631          assert(val.exists == false);
632          assert(val.ok == false);
633          assert(val == "default");
634       }
635 
636 
637       // Value exists but can't convert to int
638       {
639          immutable val = js.as!int("/array/2", 15);
640          assert(val.exists == true);
641          assert(val.ok == false);
642          assert(val == 15);
643       }
644 
645       // Can't edit an enum, of course
646       assert(__traits(compiles, js.remove("/subobj/string")) == false);
647 
648       // But I can edit a copy
649       JSONValue cp = js;
650       assert(cp == js);
651       assert(cp.toString == js.toString);
652 
653       cp.remove("/subobj/string");
654       assert(cp.exists("/subobj/string") == false);
655       assert(cp.exists("/subobj/int") == true);
656 
657    }
658 }
659 
660 
661 unittest
662 {
663    import std.json : parseJSON;
664 
665    // Standard way
666    JSONValue json = parseJSON(`{"user" : "foo", "address" : {"city" : "venice", "country" : "italy"}, "tags" : ["hello" , 3 , {"key" : "value"}]}`);
667 
668    {
669       string user = json.safe!string("user"); // Read a string from json
670       assert(user == "foo");
671    }
672 
673    {
674       // Read a string, user is a SafeValue!string
675       auto user = json.safe!string("user");
676       assert(user.ok == true);
677       assert(user.exists == true);
678 
679       // This field doesn't exists on json
680       // I can set a default value
681       auto notfound = json.safe!string("blah", "my default value");
682       assert(notfound.ok == false);
683       assert(notfound.exists == false);
684       assert(notfound == "my default value");
685 
686       // This field exists but it's not an int, it's a string
687       auto wrong = json.safe!int("user");
688       assert(wrong.ok == false);
689       assert(wrong.exists == true);
690       assert(wrong == int.init);
691    }
692 
693    {
694       // I can read deep fields
695       assert(json.safe!string("/address/city") == "venice");
696 
697       // also inside an array
698       assert(json.safe!string("/tags/2/key") == "value");
699    }
700 
701    {
702       // Using as!T you can convert field
703       assert(json.as!string("/tags/1") == "3"); // On json "/tags/1" is an int.
704    }
705 
706    {
707       // You can check if a field exists or not
708       assert(json.exists("/address/country") == true);
709 
710       // You can remove it
711       json.remove("/address/country");
712 
713       // It doesn't exists anymore
714       assert(json.exists("/address/country") == false);
715    }
716 
717    {
718       // You can write using put.
719       json.put("/address/country", "italy"); // Restore deleted field
720       json.put("/this/is/a/deep/value", 100); // It create the whole tree
721       json.put("/this/is/an/array/5", "hello"); // Ditto
722 
723       assert(json.safe!int("/this/is/a/deep/value") == 100);
724       assert(json.safe!string("/this/is/an/array/5") == "hello"); // elements 0,1,2,3,4 are nulled
725    }
726 
727    {
728       // A fast way to build object CTFE compatible.
729       // JSOB is an alias for JsonObjectBuilder and JSAB for JsonArrayBuilder
730       JSONValue jv = JSOB
731       (
732          "key", "value",
733          "obj", JSOB("subkey", 3),
734          "array", [1,2,3],
735          "mixed_array", JSAB(1, "hello", 3.0f)
736       );
737 
738       assert(jv.toString == `{"array":[1,2,3],"key":"value","mixed_array":[1,"hello",3.0],"obj":{"subkey":3}}`);
739    }
740 
741    {
742       JSONValue jv = JSOB
743       (
744          "key", "value",
745          "obj", JSOB("subkey", 3),
746          "array", [1,2,3],
747          "mixed_array", JSAB(1, "hello", 3.0f)
748       );
749 
750       foreach(size_t idx, o; jv.safe!JSONValue("/array"))
751       {
752          assert(o.safe!int("/") == idx+1);
753          assert(o.as!float("") == idx+1);
754          assert(o.read!int("/")== idx+1);
755          assert(o.safe!int == idx+1);
756          assert(o.as!float == idx+1);
757          assert(o.read!int == idx+1);
758       }
759    }
760 }
761 
762 /// Append value to array. Array is created if not exists.
763 pure
764 ref JSONValue append(T)(return ref JSONValue json, in string path, in T value)
765 {
766     auto r = json.as!JSONValue(path);
767 
768     if (!r.exists) json.put(path ~ "/0", value);
769     else if (r.value.type != JSONType.array) json.put(path, JSAB(r.value, value));
770     else json.put(path ~ "/" ~ r.array.length.to!string, value);
771 
772     return json;
773 }
774 
775 unittest 
776 {
777     auto j = JSOB("k-1", "v-1", "k-2", 1);
778 
779     j.put("/k-3/0", "v-3-1");
780     j.put("/k-3/1", "v-3-2");
781 
782     j.append("/k-3", "v-3-3");
783     j.append("k-4", "v-4-1");
784     j.append("k-4", "v-4-2");
785     j.append("k-2", "v-2-2");
786 
787     assert(j.toString == `{"k-1":"v-1","k-2":[1,"v-2-2"],"k-3":["v-3-1","v-3-2","v-3-3"],"k-4":["v-4-1","v-4-2"]}`);
788 }
789 
790 /// Parse a string
791 pure
792 ref JSONValue parse(return ref JSONValue json, in string data)
793 {
794     import std.json : parseJSON;
795     json = parseJSON(data);
796     return json;
797 }