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 }