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