1 /** 2 * LibDJSONcontains functions and classes for reading, parsing, and writing JSON 3 * documents. 4 * 5 * Copyright: (c) 2009 William K. Moore, III (nyphbl8d (at) gmail (dot) com, opticron on freenode) 6 * Authors: William K. Moore, III 7 * License: Boost Software License - Version 1.0 - August 17th, 2003 8 9 Permission is hereby granted, free of charge, to any person or organization 10 obtaining a copy of the software and accompanying documentation covered by 11 this license (the "Software") to use, reproduce, display, distribute, 12 execute, and transmit the Software, and to prepare derivative works of the 13 Software, and to permit third-parties to whom the Software is furnished to 14 do so, all subject to the following: 15 16 The copyright notices in the Software and this entire statement, including 17 the above license grant, this restriction and the following disclaimer, 18 must be included in all copies of the Software, in whole or in part, and 19 all derivative works of the Software, unless such copies or derivative 20 works are solely in the form of machine-executable object code generated by 21 a source language processor. 22 23 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 26 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 27 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 28 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 29 DEALINGS IN THE SOFTWARE. 30 31 * Standards: Attempts to conform to the subset of Javascript required to implement the JSON Specification 32 */ 33 34 /* My TODO list: 35 * implement some kind of XPath like search ability 36 */ 37 module libdjson.json; 38 version(Tango) { 39 import tango.text.Util:isspace=isSpace,stripl=triml,strip=trim,stripr=trimr,find=locatePattern,split,replace=substitute; 40 import tango.text.convert.Integer:tostring=toString,agToULong=toLong; 41 import tango.text.convert.Float:tostring=toString,agToFloat=toFloat; 42 import tango.text.Ascii:icmp=icompare,cmp=compare; 43 import tango.io.Stdout:writef=Stdout; 44 import tango.text.Regex; 45 alias char[] string; 46 string regrep(string input,string pattern,string delegate(string) translator) { 47 string tmpdel(RegExpT!(char) m) { 48 return translator(m.match(0)); 49 } 50 auto rgxp = Regex(pattern,"g"); 51 return rgxp.replaceAll(input,&tmpdel); 52 } 53 } else { 54 version(D_Version2) { 55 import std.conv:to; 56 import std.string:strip,stripr=stripRight,stripl=stripLeft,split,replace,find=indexOf,cmp,icmp; 57 import std.ascii:isspace=isWhite; 58 real agToFloat(string data) { 59 return to!(real)(data); 60 } 61 ulong agToULong(string data) { 62 return to!(ulong)(data); 63 } 64 string tostring(real data) { 65 return to!(string)(data); 66 } 67 string tostring(long data) { 68 return to!(string)(data); 69 } 70 import std.traits : isSomeString, isNumeric; 71 import std.regex; 72 string regrep(string input, string pattern, string delegate(string) translator) { 73 string tmpdel(Captures!(string) m) { 74 return translator(m.hit); 75 } 76 return std.regex.replace!(tmpdel)(input, regex(pattern, "g")); 77 } 78 } else { 79 import std.string:tostring=toString,strip,stripr,stripl,split,replace,find,cmp,icmp; 80 import std.conv:agToULong=toUlong,agToFloat=toReal; 81 import std.ctype:isspace; 82 import std.regexp:sub,RegExp; 83 string regrep(string input,string pattern,string delegate(string) translator) { 84 string tmpdel(RegExp m) { 85 return translator(m.match(0)); 86 } 87 return sub(input,pattern,&tmpdel,"g"); 88 } 89 } 90 import std.stdio:writef; 91 } 92 93 /** 94 * Read an entire string into a JSON tree. 95 * Example: 96 * -------------------------------- 97 * auto root = new JSONObject(); 98 * auto arr = new JSONArray(); 99 * arr ~= new JSONString("da blue teeths!\"\\"); 100 * root["what is that on your ear?"] = arr; 101 * root["my pants"] = new JSONString("are on fire"); 102 * root["i am this many"] = new JSONNumber(10.253); 103 * string jstr = root.toString; 104 * writef("Unit Test libDJSON JSON creation...\n"); 105 * writef("Generated JSON string: ");writef(jstr);writef("\n"); 106 * writef("Regenerated JSON string: ");writef(readJSON(jstr).toString);writef("\n"); 107 * -------------------------------- 108 * Returns: A JSONObject with no name that is the root of the document that was read. 109 * Throws: JSONError on any parsing errors. 110 */ 111 JSONType readJSON(string src) { 112 string pointcpy = src; 113 JSONType root = null; 114 src = src.stripl(); 115 root = parseHelper(src); 116 return root; 117 } 118 119 /// An exception thrown on JSON parsing errors. 120 class JSONError : Exception { 121 // Throws an exception with an error message. 122 this(string msg) { 123 super(msg); 124 } 125 } 126 127 /// This is the interface implemented by all classes that represent JSON objects. 128 abstract class JSONType { 129 override abstract string toString(); 130 abstract string toPrettyString(string indent=null); 131 /// The parse method of this interface should ALWAYS be destructive, removing things from the front of source as it parses. 132 abstract void parse(ref string source); 133 /// Convenience function for casting to JSONObject 134 /// Returns: The casted object or null if the cast fails 135 JSONObject toJSONObject(){return cast(JSONObject)this;} 136 /// Convenience function for casting to JSONArray 137 /// Returns: The casted object or null if the cast fails 138 JSONArray toJSONArray(){return cast(JSONArray)this;} 139 /// Convenience function for casting to JSONString 140 /// Returns: The casted object or null if the cast fails 141 JSONString toJSONString(){return cast(JSONString)this;} 142 /// Convenience function for casting to JSONBoolean 143 /// Returns: The casted object or null if the cast fails 144 JSONBoolean toJSONBoolean(){return cast(JSONBoolean)this;} 145 /// Convenience function for casting to JSONNumber 146 /// Returns: The casted object or null if the cast fails 147 JSONNumber toJSONNumber(){return cast(JSONNumber)this;} 148 /// Convenience function for casting to JSONNull 149 /// Returns: The casted object or null if the cast fails 150 JSONNull toJSONNull(){return cast(JSONNull)this;} 151 /// Associative array index function for objects describing associative array-like attributes. 152 /// Returns: The chosen index or a null reference if the index does not exist. 153 JSONType opIndex(string key) { 154 throw new JSONError(typeof(this).stringof ~" does not support string indexing, check your JSON structure."); 155 } 156 /// Allow foreach over the object with string key and ref value. 157 int opApply(int delegate(ref string,ref JSONType) dg) { 158 throw new JSONError(typeof(this).stringof ~" does not support string index foreach, check your JSON structure."); 159 } 160 /// Array index function for objects describing array-like attributes. 161 /// Returns: The chosen index or a null reference if the index does not exist. 162 JSONType opIndex(int key) { 163 throw new JSONError(typeof(this).stringof ~" does not support integer indexing, check your JSON structure."); 164 } 165 /// Allow foreach over the object with integer key and ref value. 166 int opApply(int delegate(ref ulong,ref JSONType) dg) { 167 throw new JSONError(typeof(this).stringof ~" does not support numeric index foreach, check your JSON structure."); 168 } 169 /// Convenience function for iteration that apply to both AA and array type operations with ref value 170 int opApply(int delegate(ref JSONType) dg) { 171 throw new JSONError(typeof(this).stringof ~" does not support foreach, check your JSON structure."); 172 } 173 /// Allow "in" operator to work as expected for object types without an explicit cast 174 JSONType*opIn_r(string key) { 175 throw new JSONError(typeof(this).stringof ~" does not support opIn, check your JSON structure."); 176 } 177 } 178 /** 179 * JSONObject represents a single JSON object node and has methods for 180 * adding children. All methods that make changes modify this 181 * JSONObject rather than making a copy, unless otherwise noted. Many methods 182 * return a self reference to allow cascaded calls. 183 */ 184 class JSONObject:JSONType { 185 /// Nothing to see here except for the boring constructor, move along. 186 this(){} 187 protected JSONType[string] _children; 188 189 190 version(D_Version2) { 191 /// Operator overload for setting keys in the AA. 192 mixin("void opIndexAssign(T)(T type,string key) if(is(T : JSONType) && !is(T == typeof(null))) { 193 _children[key] = type; 194 } 195 /// ditto 196 void opIndexAssign(T)(T val, string key) if(isSomeString!T && !is(T == typeof(null))) { 197 _children[key] = new JSONString(cast(string)val); 198 } 199 /// ditto 200 void opIndexAssign(T)(T val, string key) if(isNumeric!T) { 201 _children[key] = new JSONNumber(val); 202 } 203 /// ditto 204 void opIndexAssign(T)(T val, string key) if(is(T == bool)) { 205 _children[key] = new JSONBoolean(val); 206 } 207 /// ditto 208 void opIndexAssign(T)(T val, string key) if(is(T == typeof(null))) { 209 _children[key] = new JSONNull(); 210 }"); 211 } else { 212 /// Operator overload for setting keys in the AA. 213 void opIndexAssign(JSONType type,string key) { 214 _children[key] = type; 215 } 216 /// ditto 217 void opIndexAssign(string val, string key) { 218 _children[key] = new JSONString(cast(string)val); 219 } 220 /// ditto 221 void opIndexAssign(bool val, string key) { 222 _children[key] = new JSONBoolean(val); 223 } 224 /// ditto 225 void opIndexAssign(long val, string key) { 226 _children[key] = new JSONNumber(val); 227 } 228 /// ditto 229 void opIndexAssign(real val, string key) { 230 _children[key] = new JSONNumber(val); 231 } 232 /// ditto 233 void opIndexAssign(int val, string key) { 234 _children[key] = new JSONNumber(val); 235 } 236 } 237 238 /// Operator overload for accessing values already in the AA. 239 /// Returns: The child node if it exists, otherwise null. 240 override JSONType opIndex(string key) { 241 return (key in _children)?_children[key]:null; 242 } 243 /// Allow the user to get the number of elements in this object 244 /// Returns: The number of child nodes contained within this JSONObject 245 ulong length() {return _children.length;} 246 /// Operator overload for foreach iteration through the object with values only and allow modification of the reference 247 override int opApply(int delegate(ref JSONType) dg) { 248 int res; 249 foreach(ref child;_children) { 250 res = dg(child); 251 if (res) return res; 252 } 253 return 0; 254 } 255 /// Operator overload for foreach iteration through the object with key and value and allow modification of the reference 256 override int opApply(int delegate(ref string,ref JSONType) dg) { 257 int res; 258 foreach(key,ref child;_children) { 259 res = dg(key,child); 260 if (res) return res; 261 } 262 return 0; 263 } 264 265 /// A method to convert this JSONObject to a user-readable format. 266 /// Returns: A JSON string representing this object and it's contents. 267 override string toString() { 268 string ret; 269 ret ~= "{"; 270 foreach (key,val;_children) { 271 ret ~= "\""~JSONEncode(key)~"\":"~val.toString~","; 272 } 273 // rip off the trailing comma, we don't need it 274 if (_children.length) ret = ret[0..$-1]; 275 ret ~= "}"; 276 return ret; 277 } 278 279 /// A method to convert this JSONObject to a formatted, user-readable format. 280 /// Returns: A JSON string representing this object and it's contents. 281 override string toPrettyString(string indent=null) { 282 string ret; 283 ret ~= "{\n"; 284 foreach (key,val;_children) { 285 ret ~= indent~" \""~JSONEncode(key)~"\":"~val.toPrettyString(indent~" ")~",\n"; 286 } 287 // rip off the trailing comma, we don't need it 288 if (_children.length) ret = ret[0..$-2]~"\n"; 289 ret ~= indent~"}"; 290 return ret; 291 } 292 293 /// This function parses a JSONObject out of a string 294 override void parse(ref string source) { 295 // make sure the first byte is { 296 if (source[0] != '{') throw new JSONError("Missing open brace '{' at start of JSONObject parse: "~source); 297 // rip off the leading { 298 source = stripl(source[1..$]); 299 while (source[0] != '}') { 300 if (source[0] != '"') throw new JSONError("Missing open quote for element key before: "~source); 301 // use JSONString class to help us out here (read, I'm lazy :D) 302 auto jstr = new JSONString(); 303 jstr.parse(source); 304 source = stripl(source); 305 if (source[0] != ':') throw new JSONError("Missing ':' after keystring in object before: "~source); 306 source = stripl(source[1..$]); 307 _children[jstr.get] = parseHelper(source); 308 source = stripl(source); 309 // handle end cases 310 if (source[0] == '}') continue; 311 if (source[0] != ',') throw new JSONError("Missing continuation via ',' or end of JSON object via '}' before "~source); 312 // rip the , in preparation for the next loop 313 source = stripl(source[1..$]); 314 // make sure we don't have a ",}", since I'm assuming it's not allowed 315 if (source[0] == '}') throw new JSONError("Empty array elements (',' followed by '}') are not allowed. Fill the space or remove the comma.\nThis error occurred before: "~source); 316 } 317 // rip off the } and be done with it 318 source = stripl(source[1..$]); 319 } 320 /// Allow "in" operator to work as expected for object types without an explicit cast 321 override JSONType*opIn_r(string key) { 322 return key in _children; 323 } 324 } 325 326 /// JSONArray represents a single JSON array, capable of being heterogenous 327 class JSONArray:JSONType { 328 protected JSONType[] _children; 329 330 version(D_Version2) { 331 mixin(" 332 this(T...)(T args) { 333 foreach(arg; args) { 334 this ~= arg; 335 } 336 } 337 /// Operator overload to allow addition of children 338 void opCatAssign(T)(T child) if(is(T : JSONType)) { 339 _children ~= child; 340 } 341 /// ditto 342 void opCatAssign(T)(T val) if(isSomeString!T && !is(T == typeof(null))) { 343 _children ~= new JSONString(cast(string)val); 344 } 345 /// ditto 346 void opCatAssign(T)(T val) if(isNumeric!T) { 347 _children ~= new JSONNumber(val); 348 } 349 /// ditto 350 void opCatAssign(T)(T val) if(is(T == bool)) { 351 _children ~= new JSONBoolean(val); 352 } 353 /// ditto 354 void opCatAssign(T)(T val) if(is(T == typeof(null))) { 355 _children ~= new JSONNull(); 356 }"); 357 } else { 358 this(){} 359 /// Operator overload to allow addition of children 360 void opCatAssign(JSONType child) { 361 _children ~= child; 362 } 363 /// ditto 364 void opCatAssign(string val) { 365 _children ~= new JSONString(cast(string)val); 366 } 367 /// ditto 368 void opCatAssign(bool val) { 369 _children ~= new JSONBoolean(val); 370 } 371 /// ditto 372 void opCatAssign(long val) { 373 _children ~= new JSONNumber(val); 374 } 375 /// ditto 376 void opCatAssign(real val) { 377 _children ~= new JSONNumber(val); 378 } 379 /// ditto 380 void opCatAssign(int val) { 381 _children ~= new JSONNumber(val); 382 } 383 } 384 385 /// Operator overload to allow access of children 386 /// Returns: The child node if it exists, otherwise null. 387 override JSONType opIndex(int key) { 388 return _children[key]; 389 } 390 /// Allow the user to get the number of elements in this object 391 /// Returns: The number of child nodes contained within this JSONObject 392 ulong length() {return _children.length;} 393 /// Operator overload for foreach iteration through the array with values only and allow modification of the reference 394 override int opApply(int delegate(ref JSONType) dg) { 395 int res; 396 foreach(ref child;_children) { 397 res = dg(child); 398 if (res) return res; 399 } 400 return 0; 401 } 402 /// Operator overload for foreach iteration through the array with key and value and allow modification of the reference 403 override int opApply(int delegate(ref ulong,ref JSONType) dg) { 404 int res; 405 ulong tmp; 406 foreach(key,ref child;_children) { 407 tmp = key; 408 res = dg(tmp,child); 409 if (res) return res; 410 } 411 return 0; 412 } 413 414 /// A method to convert this JSONArray to a user-readable format. 415 /// Returns: A JSON string representing this object and it's contents. 416 override string toString() { 417 string ret; 418 ret ~= "["; 419 foreach (val;_children) { 420 ret ~= val.toString~","; 421 } 422 // rip off the trailing comma, we don't need it 423 if (_children.length) ret = ret[0..$-1]; 424 ret ~= "]"; 425 return ret; 426 } 427 428 /// A method to convert this JSONArray to a formatted, user-readable format. 429 /// Returns: A JSON string representing this object and it's contents. 430 override string toPrettyString(string indent=null) { 431 string ret; 432 ret ~= "[\n"; 433 foreach (val;_children) { 434 ret ~= indent~" "~val.toPrettyString(indent~" ")~",\n"; 435 } 436 // rip off the trailing comma, we don't need it 437 if (_children.length) ret = ret[0..$-2]~"\n"; 438 ret ~= indent~"]"; 439 return ret; 440 } 441 442 /// This function parses a JSONArray out of a string 443 override void parse(ref string source) { 444 if (source[0] != '[') throw new JSONError("Missing open brace '[' at start of JSONArray parse: "~source); 445 // rip off the leading [ 446 source = stripl(source[1..$]); 447 while (source[0] != ']') { 448 _children ~= parseHelper(source); 449 source = stripl(source); 450 // handle end cases 451 if (source[0] == ']') continue; 452 if (source[0] != ',') throw new JSONError("Missing continuation via ',' or end of JSON array via ']' before "~source); 453 // rip the , in preparation for the next loop 454 source = stripl(source[1..$]); 455 // take care of trailing null entries 456 if (source[0] == ']') _children ~= new JSONNull(); 457 } 458 // rip off the ] and be done with it 459 source = stripl(source[1..$]); 460 } 461 } 462 463 /// JSONString represents a JSON string. Internal representation is escaped for faster parsing and JSON generation. 464 class JSONString:JSONType { 465 /// The boring default constructor. 466 this(){} 467 /// The ever so slightly more interesting initializing constructor. 468 this(string data) {set(data);} 469 protected string _data; 470 /// Allow the data to be set so the object can be reused. 471 void set(string data) {_data = JSONEncode(data);} 472 /// Allow the data to be retreived. 473 string get() {return JSONDecode(_data);} 474 475 /// A method to convert this JSONString to a user-readable format. 476 /// Returns: A JSON string representing this object and it's contents. 477 override string toString() { 478 return "\""~_data~"\""; 479 } 480 481 /// A method to convert this JSONString to a formatted, user-readable format. 482 /// Returns: A JSON string representing this object and it's contents. 483 override string toPrettyString(string indent=null) { 484 return toString; 485 } 486 487 /// This function parses a JSONArray out of a string and eats characters as it goes, hence the ref string parameter. 488 override void parse(ref string source) { 489 if (source[0] != '"') throw new JSONError("Missing open quote '\"' at start of JSONArray parse: "~source); 490 // rip off the leading [ 491 source = source[1..$]; 492 // scan to find the closing quote 493 int bscount = 0; 494 int sliceloc = -1; 495 for(int i = 0;i<source.length;i++) { 496 switch(source[i]) { 497 case '\\': 498 bscount++; 499 continue; 500 case '"': 501 // if the count is even, backslashes cancel and we have the end of the string, otherwise cascade 502 if (bscount%2 == 0) { 503 break; 504 } 505 goto default; 506 default: 507 bscount = 0; 508 continue; 509 } 510 // we have reached the terminating case! huzzah! 511 sliceloc = i; 512 break; 513 } 514 // take care of failure to find the end of the string 515 if (sliceloc == -1) throw new JSONError("Unable to find the end of the JSON string starting here: "~source); 516 _data = source[0..sliceloc]; 517 // eat the " that is known to be there 518 source = stripl(source[sliceloc+1..$]); 519 } 520 } 521 522 /// JSONBoolean represents a JSON boolean value. 523 class JSONBoolean:JSONType { 524 /// The boring constructor, again. 525 this(){} 526 /// Only a bit of input for this constructor. 527 this(bool data) {_data = data;} 528 /// Allow setting of the hidden bit. 529 void set(bool data) {_data = data;} 530 /// Allow the bit to be retreived. 531 bool get() {return _data;} 532 protected bool _data; 533 534 /// A method to convert this JSONBoolean to a user-readable format. 535 /// Returns: A JSON string representing this object and it's contents. 536 override string toString() { 537 if (_data) return "true"; 538 return "false"; 539 } 540 541 /// A method to convert this JSONBoolean to a formatted, user-readable format. 542 /// Returns: A JSON string representing this object and it's contents. 543 override string toPrettyString(string indent=null) { 544 return toString; 545 } 546 547 /// This function parses a JSONBoolean out of a string and eats characters as it goes, hence the ref string parameter. 548 override void parse(ref string source) { 549 if (source[0..4] == "true") { 550 source = stripl(source[4..$]); 551 set(true); 552 } else if (source[0..5] == "false") { 553 source = stripl(source[5..$]); 554 set(false); 555 } else throw new JSONError("Could not parse JSON boolean variable from: "~source); 556 } 557 } 558 559 /// JSONNull represents a JSON null value. 560 class JSONNull:JSONType { 561 /// You're forced to use the boring constructor here. 562 this(){} 563 564 /// A method to convert this JSONNull to a user-readable format. 565 /// Returns: "null". Always. Forever. 566 override string toString() { 567 return "null"; 568 } 569 570 /// A method to convert this JSONNull to a formatted, user-readable format. 571 /// Returns: "null". Always. Forever. 572 override string toPrettyString(string indent=null) { 573 return toString; 574 } 575 576 /// This function parses a JSONNull out of a string. Really, it just rips "null" off the beginning of the string and eats whitespace. 577 override void parse(ref string source) in { assert(source[0..4] == "null"); } body { 578 source = stripl(source[4..$]); 579 } 580 } 581 582 /// JSONNumber represents any JSON numeric value. 583 class JSONNumber:JSONType { 584 /// Another boring constructor... 585 this(){} 586 /// ...and its slightly less boring sibling. 587 this(real data) {_data = tostring(data);} 588 this(long data) {_data = tostring(data);} 589 this(int data) {_data = tostring(cast(long)data);} 590 /// Allow setting of the hidden number. 591 void set(real data) {_data = tostring(data);} 592 void set(long data) {_data = tostring(data);} 593 void set(int data) {_data = tostring(cast(long)data);} 594 /// Allow the number to be retreived. 595 real get() {return agToFloat(_data);} 596 long getLong() {return agToULong(_data);} 597 real getReal() {return agToULong(_data);} 598 protected string _data; 599 600 /// A method to convert this JSONNumber to a user-readable format. 601 /// Returns: A JSON string representing this number. 602 override string toString() { 603 return _data; 604 } 605 606 /// A method to convert this JSONNumber to a formatted, user-readable format. 607 /// Returns: A JSON string representing this number. 608 override string toPrettyString(string indent=null) { 609 return toString; 610 } 611 612 /// This function parses a JSONNumber out of a string and eats characters as it goes, hence the ref string parameter. 613 override void parse(ref string source) { 614 // this parser sucks... 615 int i = 0; 616 // check for leading minus sign 617 if (source[i] == '-') i++; 618 // sift through whole numerics 619 if (source[i] == '0') { 620 i++; 621 } else if (source[i] <= '9' && source[i] >= '1') { 622 while (source[i] >= '0' && source[i] <= '9') i++; 623 } else throw new JSONError("A numeric parse error occurred while parsing the numeric beginning at: "~source); 624 // if the next char is not a '.', we know we're done with fractional parts 625 if (source[i] == '.') { 626 i++; 627 while (source[i] >= '0' && source[i] <= '9') i++; 628 } 629 // if the next char is e or E, we're poking at an exponential 630 if (source[i] == 'e' || source[i] == 'E') { 631 i++; 632 if (source[i] == '-' || source[i] == '+') i++; 633 while (source[i] >= '0' && source[i] <= '9') i++; 634 } 635 _data = source[0..i]; 636 source = stripl(source[i..$]); 637 } 638 } 639 640 private JSONType parseHelper(ref string source) { 641 JSONType ret; 642 switch(source[0]) { 643 case '{': 644 ret = new JSONObject(); 645 break; 646 case '[': 647 ret = new JSONArray(); 648 break; 649 case '"': 650 ret = new JSONString(); 651 break; 652 case '-','0','1','2','3','4','5','6','7','8','9': 653 ret = new JSONNumber(); 654 break; 655 default: 656 if (source.length >= 4 && source[0..4] == "null") ret = new JSONNull(); 657 else if ((source.length >= 4 && source[0..4] == "true") || (source.length >= 5 && source[0..5] == "false")) ret = new JSONBoolean(); 658 else if (source.length && source[0] == ',') return new JSONNull(); // no parse here 659 else throw new JSONError("Unable to determine type of next element beginning: "~source); 660 break; 661 } 662 ret.parse(source); 663 return ret; 664 } 665 666 /// Perform JSON escapes on a string 667 /// Returns: A JSON encoded string 668 string JSONEncode(string src) { 669 string tempStr; 670 tempStr = replace(src , "\\", "\\\\"); 671 tempStr = replace(tempStr, "\"", "\\\""); 672 return tempStr; 673 } 674 675 /// Unescape a JSON string 676 /// Returns: A decoded string. 677 string JSONDecode(string src) { 678 string tempStr; 679 tempStr = replace(src , "\\\\", "\\"); 680 tempStr = replace(tempStr, "\\\"", "\""); 681 tempStr = replace(tempStr, "\\/", "/"); 682 tempStr = replace(tempStr, "\\n", "\n"); 683 tempStr = replace(tempStr, "\\r", "\r"); 684 tempStr = replace(tempStr, "\\f", "\f"); 685 tempStr = replace(tempStr, "\\t", "\t"); 686 tempStr = replace(tempStr, "\\b", "\b"); 687 // take care of hex character entities 688 // XXX regex is broken in tango 0.99.9 which means this doesn't work right when numbers enter the mix 689 version(D_Version2) { 690 tempStr = regrep(tempStr,"\\\\u[0-9a-fA-F]{4};",(string m) { 691 auto cnum = m[3..$-1]; 692 dchar dnum = hex2dchar(cnum[1..$]); 693 return quickUTF8(dnum); 694 }); 695 } else { 696 tempStr = regrep(tempStr,"\\u[0-9a-fA-F]{4};",(string m) { 697 auto cnum = m[3..$-1]; 698 dchar dnum = hex2dchar(cnum[1..$]); 699 return quickUTF8(dnum); 700 }); 701 } 702 return tempStr; 703 } 704 705 /// This probably needs documentation. It looks like it converts a dchar to the necessary length string of chars. 706 string quickUTF8(dchar dachar) { 707 char[] ret; 708 if (dachar <= 0x7F) { 709 ret.length = 1; 710 ret[0] = cast(char) dachar; 711 } else if (dachar <= 0x7FF) { 712 ret.length = 2; 713 ret[0] = cast(char)(0xC0 | (dachar >> 6)); 714 ret[1] = cast(char)(0x80 | (dachar & 0x3F)); 715 } else if (dachar <= 0xFFFF) { 716 ret.length = 3; 717 ret[0] = cast(char)(0xE0 | (dachar >> 12)); 718 ret[1] = cast(char)(0x80 | ((dachar >> 6) & 0x3F)); 719 ret[2] = cast(char)(0x80 | (dachar & 0x3F)); 720 } else if (dachar <= 0x10FFFF) { 721 ret.length = 4; 722 ret[0] = cast(char)(0xF0 | (dachar >> 18)); 723 ret[1] = cast(char)(0x80 | ((dachar >> 12) & 0x3F)); 724 ret[2] = cast(char)(0x80 | ((dachar >> 6) & 0x3F)); 725 ret[3] = cast(char)(0x80 | (dachar & 0x3F)); 726 } else { 727 assert(0); 728 } 729 return cast(string)ret; 730 } 731 private dchar hex2dchar (string hex) { 732 dchar res; 733 foreach(digit;hex) { 734 res <<= 4; 735 res |= toHVal(digit); 736 } 737 return res; 738 } 739 740 private dchar toHVal(char digit) { 741 if (digit >= '0' && digit <= '9') { 742 return digit-'0'; 743 } 744 if (digit >= 'a' && digit <= 'f') { 745 return digit-'a'; 746 } 747 if (digit >= 'A' && digit <= 'F') { 748 return digit-'A'; 749 } 750 return 0; 751 } 752 753 unittest { 754 auto root = new JSONObject(); 755 auto arr = new JSONArray(); 756 arr ~= new JSONString("da blue teeths!\"\\"); 757 root["what is that on your ear?"] = arr; 758 root["my pants"] = new JSONString("are on fire"); 759 root["i am this many"] = new JSONNumber(10.253); 760 root["blank"] = new JSONObject(); 761 string jstr = root.toString; 762 writef("Unit Test libDJSON JSON creation...\n"); 763 writef("Generated JSON string: " ~ jstr ~ "\n"); 764 writef("Regenerated JSON string: " ~ readJSON(jstr).toString ~ "\n"); 765 writef("Output using toPrettyString:\n"~root.toPrettyString~"\nEnd pretty output\n"); 766 assert(jstr == readJSON(jstr).toString); 767 writef("Unit Test libDJSON JSON parsing...\n"); 768 jstr = "{\"firstName\": \"John\",\"lastName\": \"Smith\",\"address\": {\"streetAddress\": \"21 2nd Street\",\"city\": \"New York\",\"state\": \"NY\",\"postalCode\": 10021},\"phoneNumbers\": [{ \"type\": \"home\", \"number\": \"212 555-1234\" },{ \"type\": \"fax\", \"number\": \"646 555-4567\" }],\"newSubscription\": false,\"companyName\": null }"; 769 writef("Sample JSON string: " ~ jstr ~ "\n"); 770 jstr = jstr.readJSON().toString; 771 auto tmp = jstr.readJSON(); 772 writef("Parsed JSON string: " ~ jstr ~ "\n"); 773 writef("Output using toPrettyString:\n"~tmp.toPrettyString~"\nEnd pretty output\n"); 774 // ensure that the string doesn't mutate after a second reading, it shouldn't 775 assert(tmp.toString == jstr); 776 // ensure that pretty output still parses properly and doesn't mutate 777 jstr = tmp.toPrettyString; 778 tmp = jstr.readJSON(); 779 assert(tmp.toPrettyString == jstr); 780 writef("Unit Test libDJSON JSON access...\n"); 781 writef("Got first name:" ~ tmp["firstName"].toJSONString.get ~ "\n"); 782 writef("Got last name:" ~ tmp["lastName"].toJSONString.get ~ "\n"); 783 writef("Unit Test libDJSON opApply interface...\n"); 784 foreach(obj;tmp["phoneNumbers"]) { 785 writef("Got " ~ obj["type"].toJSONString.get ~ " phone number:" ~ obj["number"].toJSONString.get ~ "\n"); 786 } 787 foreach(string name,JSONType obj;tmp) { 788 writef("Got element name " ~ name ~ "\n"); 789 } 790 writef("Unit Test libDJSON opIndex interface to ensure breakage where incorrectly used...\n"); 791 try { 792 tmp[5]; 793 assert(false,"An exception should have been thrown on the line above."); 794 } catch (Exception e) {/*shazam! program flow should get here, it is a correct thing*/} 795 writef("Testing alternate base container and empty elements...\n"); 796 assert("[,,]".readJSON().toString == "[null,null,null]"); 797 jstr = 798 " { 799 \"realms\": [ 800 { 801 \"type\": \"pve\", 802 \"queue\": false, 803 \"status\": true, 804 \"population\": \"medium\", 805 \"name\": \"Aerie Peak\", 806 \"slug\": \"aerie-peak\" 807 } 808 ] 809 }"; 810 tmp = jstr.readJSON(); 811 tmp["realms"].toJSONArray().length(); 812 writef("Testing opIn_r functionality...\n"); 813 assert("realms" in tmp); 814 assert(!("bob" in tmp)); 815 816 817 writef("Testing autodetect type opAssign\n"); 818 root = new JSONObject(); 819 root["string"] = "str"; 820 assert(root["string"].toJSONString().get() == "str"); 821 root["int"] = 4; 822 assert(root["int"].toJSONNumber().getLong() == 4); 823 root["float"] = 4.5f; 824 assert(root["float"].toJSONNumber().get() == 4.5f); 825 version(D_Version2) { 826 root["array"] = new JSONArray("foosh", "bar", 1); 827 assert(root["array"].toJSONArray()[0].toJSONString().get == "foosh"); 828 } 829 830 writef("Testing autodetect type opCatAssign\n"); 831 arr = new JSONArray(); 832 arr ~= "foo"; 833 assert(arr[0].toJSONString().get() == "foo"); 834 arr ~= 5; 835 assert(arr[1].toJSONNumber().get() == 5); 836 } 837 838 version(JSON_main) { 839 void main(){} 840 }