1 module json.value;
2 
3 private {
4     import std.algorithm;
5     import std.array;
6     import std.ascii;
7     import std.conv;
8     import std.format;
9     import std.range;
10     import std.string;
11     import traits = std.traits;
12     import std.typecons;
13     import std.utf;
14 
15     enum isJsonValue( T ) = is( Unqual!T == JsonValue );
16 }
17 
18 class JsonException : Exception
19 {
20     import std.exception : basicExceptionCtors;
21     mixin basicExceptionCtors;
22 }
23 
24 JsonValue toJson( T )( T value )
25 {
26     return JsonValue( value );
27 }
28 
29 auto jtrue() pure nothrow @property @system
30 {
31     static immutable value = JsonValue( true );
32     return value;
33 }
34 
35 auto jfalse() pure nothrow @property @system
36 {
37     static immutable value = JsonValue( false );
38     return value;
39 }
40 
41 auto jnull() pure nothrow @property @system
42 {
43     static immutable value = JsonValue( null );
44     return value;
45 }
46 
47 alias asJson = toJson;
48 
49 struct JsonValue
50 {
51     enum Type
52     {
53         Null,
54 
55         String,
56         Number,
57         True,
58         False,
59         Array,
60         Object,
61     }
62 
63     private enum NumberType
64     {
65         signed,
66         unsigned,
67         floating,
68     }
69 
70     private {
71         Type _type;
72         NumberType _numType = void;
73 
74         union {
75             dstring stringValue            = void;
76             long signed                    = void;
77             ulong unsigned                 = void;
78             real floating                  = void;
79             JsonValue[] arrayValue         = void;
80             JsonValue[dstring] objectValue = void;
81         }
82     }
83 
84     Type type() const pure nothrow @safe @property
85     {
86         return this._type;
87     }
88 
89     // convenience is* properties
90     bool isObject() const pure nothrow @safe @property
91     {
92         return this.type == Type.Object;
93     }
94 
95     bool isArray() const pure nothrow @safe @property
96     {
97         return this.type == Type.Array;
98     }
99 
100     bool isBool() const pure nothrow @safe @property
101     {
102         return this.type == Type.True || this.type == Type.False;
103     }
104 
105     bool isString() const pure nothrow @safe @property
106     {
107         return this.type == Type.String;
108     }
109 
110     bool isNumber() const pure nothrow @safe @property
111     {
112         return this.type == Type.Number;
113     }
114 
115     bool isSigned() const pure nothrow @safe @property
116     {
117         return this.isNumber && this._numType == NumberType.signed;
118     }
119 
120     bool isUnsigned() const pure nothrow @safe @property
121     {
122         return this.isNumber && this._numType == NumberType.unsigned;
123     }
124 
125     bool isInteger() const pure nothrow @safe @property
126     {
127         return this.isSigned || this.isUnsigned;
128     }
129 
130     bool isFloat() const pure nothrow @safe @property
131     {
132         return this.isNumber && this._numType == NumberType.floating;
133     }
134 
135     bool isNull() const pure nothrow @safe @property
136     {
137         return this.type == Type.Null;
138     }
139 
140     size_t length() const pure @property
141     {
142         this.enforceType!( Type.Array );
143         return this.arrayValue.length;
144     }
145 
146     bool empty() const pure @system @property
147     {
148         return this.length == 0;
149     }
150 
151     JsonValue front() const pure @property
152     {
153         this.enforceType!( Type.Array );
154         return this.arrayValue.front;
155     }
156 
157     JsonValue back() const pure @property
158     {
159         this.enforceType!( Type.Array );
160         return this.arrayValue.back;
161     }
162 
163     this( T )( T value ) if( traits.isSomeString!T )
164     {
165         this.stringValue = value.toUTF32();
166         this( Type.String );
167     }
168 
169     this( T )( T value ) if( traits.isSigned!T && traits.isIntegral!T )
170     {
171         this.signed = value;
172         this._numType = NumberType.signed;
173         this( Type.Number );
174     }
175 
176     this( T )( T value ) if( traits.isUnsigned!T )
177     {
178         this.unsigned = value;
179         this._numType = NumberType.unsigned;
180         this( Type.Number );
181     }
182 
183     this( T )( T value ) if( traits.isFloatingPoint!T )
184     {
185         this.floating = value;
186         this._numType = NumberType.floating;
187         this( Type.Number );
188     }
189 
190     this( bool value )
191     {
192         this( value ? Type.True : Type.False );
193     }
194 
195     this( R )( R r ) if( isForwardRange!R && !traits.isSomeString!R )
196     {
197         foreach( item; r.save() )
198         {
199             static if( isJsonValue!( ElementType!R ) )
200                 arrayValue ~= item;
201             else
202                 arrayValue ~= JsonValue( item );
203         }
204         this( Type.Array );
205     }
206 
207     this( TKey, TValue )( TValue[TKey] assoc ) if( traits.isSomeString!TKey )
208     {
209         foreach( key, value; assoc )
210         {
211             static if( isJsonValue!TValue )
212                 objectValue[key.toUTF32()] = value;
213             else
214                 objectValue[key.toUTF32()] = JsonValue( value );
215         }
216         this( Type.Object );
217     }
218 
219     this( typeof( null ) )
220     {
221         this( Type.Null );
222     }
223 
224     private this( Type type )
225     {
226         this._type = type;
227     }
228 
229     static auto newArray()
230     {
231         return JsonValue( typeof( JsonValue.arrayValue ).init );
232     }
233 
234     static auto newObject()
235     {
236         return JsonValue( typeof( JsonValue.objectValue ).init );
237     }
238 
239     bool hasKey( T )( T key ) const pure nothrow if( traits.isSomeString!T )
240     {
241         return this.type == Type.Object
242              ? ( key.toUTF32() in this.objectValue ) !is null
243              : false;
244     }
245 
246     T get( T, S )( S key, lazy T defaultvalue = T.init ) const pure nothrow if( traits.isSomeString!T )
247     {
248         return this.hasKey( key ) ? this.objectValue[key.toUTF32()].as!T : defaultvalue;
249     }
250 
251     alias to = this.opCast;
252     alias as = this.opCast;
253 
254     string toString()
255     {
256         with( Type )
257         final switch( this.type )
258         {
259             case True: return "true";
260             case False: return "false";
261 
262             case Number:
263             {
264                 with( NumberType )
265                 final switch( this._numType )
266                 {
267                     case signed:   return this.signed.to!string;
268                     case unsigned: return this.unsigned.to!string;
269                     case floating: return this.floating.to!string;
270                 }
271             }
272 
273             case String: return this.stringValue.toUTF8();
274 
275             case Null:
276             case Array:
277             case Object:
278                 return this.typename;
279         }
280     }
281 
282     T toJsonString( T = string )( bool indented = false ) if( traits.isSomeString!T )
283     {
284         return ( indented ? this.toPrettyJsonImpl( 1 ) : this.toPlainJsonImpl() ).to!T;
285     }
286 
287     void popFront()
288     {
289         this.enforceType!( Type.Array );
290         this.arrayValue.popFront();
291     }
292 
293     void popBack()
294     {
295         this.enforceType!( Type.Array );
296         this.arrayValue.popBack();
297     }
298 
299     JsonValue save()
300     {
301         this.enforceType!( Type.Array );
302         return JsonValue( this.arrayValue );
303     }
304 
305     size_t opDollar() const pure @system
306     {
307         return this.length;
308     }
309 
310     JsonValue opSlice( size_t begin, size_t end ) const
311     {
312         this.enforceType!( Type.Array );
313         return JsonValue( this.arrayValue[begin .. end] );
314     }
315 
316     JsonValue opSliceAssign( R )( size_t begin, size_t end, R r ) if( isForwardRange!R )
317     {
318         this.enforceType!( Type.Array );
319         this.arrayValue[begin .. end] = r.save().array;
320         return this;
321     }
322 
323     JsonValue opIndex( size_t i ) const pure
324     {
325         this.enforceType!( Type.Array );
326         return this.arrayValue[i];
327     }
328 
329     JsonValue opIndexAssign( T )( size_t i, T value )
330     {
331         this.enforceType!( Type.Array );
332 
333         static if( isJsonValue!T )
334             return this.arrayValue[i] = value;
335         else
336             return this.arrayValue[i] = JsonValue( value );
337     }
338 
339     JsonValue opIndex( T )( T key ) const pure if( traits.isSomeString!T )
340     {
341         this.enforceType!( Type.Object );
342         return this.objectValue[key.toUTF32()];
343     }
344 
345     JsonValue opIndexAssign( T, U )( T key, U value ) if( traits.isSomeString!T )
346     {
347         this.enforceType!( Type.Object );
348 
349         static if( isJsonValue!U )
350             return this.objectValue[key.toUTF32()] = value;
351         else
352             return this.objectValue[key.toUTF32()] = JsonValue( value );
353     }
354 
355     JsonValue opAssign( T )( T value )
356     {
357         static if( !isJsonValue!T )
358             this = JsonValue( value );
359         else
360             this = value;
361 
362         return this;
363     }
364 
365     JsonValue opOpAssign( string op, T )( T value )
366     {
367         return this = mixin( "this" ~ op ~ "value" );
368     }
369 
370     bool opEquals( T )( auto ref const T value ) const
371     {
372         static if( isJsonValue!T )
373             return this.type == value.type && this.opCmp( value ) == 0;
374         else static if( traits.isSomeString!T )
375             return this.stringValue == value.toUTF32();
376         else
377             return this.opCmp( value ) == 0;
378     }
379 
380     T opCast( T : bool )()
381     {
382         return this.type == Type.True;
383     }
384 
385     T opCast( T )() if( traits.isSigned!T && traits.isIntegral!T )
386     {
387         this.enforceNumType!( NumberType.signed );
388 
389         return cast(T)this.signed;
390     }
391 
392     T opCast( T )() if( traits.isUnsigned!T )
393     {
394         this.enforceNumType!( NumberType.unsigned );
395 
396         return cast(T)this.unsigned;
397     }
398 
399     T opCast( T )() if( traits.isFloatingPoint!T )
400     {
401         this.enforceNumType!( NumberType.floating );
402 
403         return cast(T)this.floating;
404     }
405 
406     T opCast( T )() if( traits.isSomeString!T )
407     {
408         this.enforceType!( Type.String );
409         return this.stringValue.to!T;
410     }
411 
412     T opCast( T )() if( traits.isDynamicArray!T && !traits.isSomeString!T )
413     {
414         alias TElem = ElementType!T;
415 
416         this.enforceType!( Type.Array );
417         return this.arrayValue
418                    .map!( x => x.opCast!TElem )
419                    .array;
420     }
421 
422     T opCast( T )() if( traits.isAssociativeArray!T )
423     {
424         alias TKey   = KeyType!T;
425         alias TValue = ValueType!T;
426 
427         this.enforceType!( Type.Object );
428 
429         T result;
430         foreach( k, v; this.objectValue )
431             result[k.to!TKey] = v.opCast!TValue;
432 
433         return result;
434     }
435 
436     // array foreach
437     int opApply( int delegate( ref JsonValue ) apply )
438     {
439         this.enforceType!( Type.Array );
440 
441         int result;
442         foreach( ref item; this.arrayValue )
443         {
444             result = apply( item );
445             if( result != 0 )
446                 break;
447         }
448 
449         return result;
450     }
451 
452     // object foreach
453     int opApply( int delegate( const ref dstring, ref JsonValue ) apply )
454     {
455         this.enforceType!( Type.Object );
456 
457         int result;
458         foreach( dstring k, ref v; this.objectValue )
459         {
460             result = apply( k, v );
461             if( result != 0 )
462                 break;
463         }
464 
465         return result;
466     }
467 
468     private string typename() const @property
469     {
470         return this.type.to!( string ).toLower();
471     }
472 
473     private void enforceType( Type type )() const pure @safe
474     {
475         enum name    = type.to!( string ).toLower();
476         enum article = type == Type.Object || type == Type.Array ? "an" : "a";
477         enum message = "Value is not %s %s".format( article, name );
478 
479         if( this.type != type )
480             throw new JsonException( message );
481     }
482 
483     private void enforceNumType( NumberType type )() const pure @safe
484     {
485         this.enforceType!( Type.Number );
486         enum name = type.to!string ~ ( type == NumberType.floating ? " point" : "" );
487         enum article = type == NumberType.unsigned ? "an" : "a";
488         enum message = "Value is not %s %s number".format( article, name );
489 
490         if( this.type != Type.Number || this._numType != type )
491             throw new JsonException( message );
492     }
493 
494     private dstring toPlainJsonImpl()
495     {
496         auto writer = appender!dstring;
497 
498         with( Type )
499         final switch( this.type )
500         {
501             case Object:
502             {
503                 writer.put( '{' );
504 
505                 bool first = true;
506                 foreach( k, v; this.objectValue )
507                 {
508                     if( !first ) writer.put( ',' );
509                     writer.formattedWrite( `"%s":%s`, k, v.toPlainJsonImpl() );
510                     if( first ) first = false;
511                 }
512 
513                 writer.put( '}' );
514                 break;
515             }
516 
517             case Array:
518             {
519                 writer.put( '[' );
520 
521                 bool first = true;
522                 foreach( item; this.arrayValue )
523                 {
524                     if( !first ) writer.put( ',' );
525                     writer.put( item.toPlainJsonImpl() );
526                     if( first ) first = false;
527                 }
528 
529                 writer.put( ']' );
530                 break;
531             }
532 
533             case String:
534                 writer.formattedWrite( `"%s"`, this.stringValue );
535                 break;
536 
537             case Number:
538             {
539                 with( NumberType )
540                 final switch( this._numType )
541                 {
542                     case signed:
543                         writer.formattedWrite( "%d", this.signed );
544                         break;
545 
546                     case unsigned:
547                         writer.formattedWrite( "%d", this.unsigned );
548                         break;
549 
550                     case floating:
551                         writer.formattedWrite( "%g", this.floating );
552                         break;
553                 }
554 
555                 break;
556             }
557 
558             case True, False:
559                 writer.put( this.type == True ? "true" : "false" );
560                 break;
561 
562             case Null:
563                 writer.put( "null" );
564                 break;
565         }
566 
567         return writer.data;
568     }
569 
570     private dstring toPrettyJsonImpl( size_t indentLevel )
571     {
572         auto writer = appender!dstring;
573 
574         dstring indent() @property
575         {
576             return ""d.rightJustify( indentLevel * 4 );
577         }
578 
579         with( Type )
580         final switch( this.type )
581         {
582             case Object:
583             {
584                 writer.formattedWrite( "{%s%s", newline, indent );
585 
586                 bool first = true;
587                 foreach( k, v; this.objectValue )
588                 {
589                     if( !first ) writer.formattedWrite( ",%s%s", newline, indent );
590                     writer.formattedWrite( `"%s": %s`, k, v.toPrettyJsonImpl( indentLevel + 1 ) );
591                     if( first ) first = false;
592                 }
593 
594                 --indentLevel;
595                 writer.formattedWrite( "%s%s}", newline, indent );
596                 break;
597             }
598 
599             case Array:
600             {
601                 writer.formattedWrite( "[%s%s", newline, indent );
602 
603                 bool first = true;
604                 foreach( item; this.arrayValue )
605                 {
606                     if( !first ) writer.formattedWrite( ",%s%s", newline, indent );
607                     writer.put( item.toPrettyJsonImpl( indentLevel + 1 ) );
608                     if( first ) first = false;
609                 }
610 
611                 --indentLevel;
612                 writer.formattedWrite( "%s%s]", newline, indent );
613                 break;
614             }
615 
616             case String:
617                 writer.formattedWrite( `"%s"`, this.stringValue );
618                 break;
619 
620             case Number:
621             {
622                 with( NumberType )
623                 final switch( this._numType )
624                 {
625                     case signed:
626                         writer.formattedWrite( "%d", this.signed );
627                         break;
628 
629                     case unsigned:
630                         writer.formattedWrite( "%d", this.unsigned );
631                         break;
632 
633                     case floating:
634                         writer.formattedWrite( "%g", this.floating );
635                         break;
636                 }
637 
638                 break;
639             }
640 
641             case True, False:
642                 writer.put( this.type == True ? "true" : "false" );
643                 break;
644 
645             case Null:
646                 writer.put( "null" );
647                 break;
648         }
649 
650         return writer.data;
651     }
652 }