1 /****************************************************************************** 2 3 Toolkit to extract values from JSON content of an expected structure. 4 5 Usage example: 6 7 --- 8 9 const content = 10 `{` 11 `"id":"8c97472e-098e-4baa-aa63-4a3f2aab10c6",` 12 `"imp":` 13 `[` 14 `{` 15 `"impid":"7682f6f1-810c-49b0-8388-f91ba4a00c1d",` 16 `"h":480,` 17 `"w":640,` 18 `"btype": [ 1,2,3 ],` 19 `"battr": [ 3,4,5 ]` 20 `}` 21 `],` 22 `"site":` 23 `{` 24 25 `"sid":"1",` 26 `"name":"MySite",` 27 `"pub":"MyPublisher",` 28 `"cat": [ "IAB1", "IAB2" ],` 29 `"page":"http://www.example.com/"` 30 `},` 31 `"user":` 32 `{` 33 `"uid":"45FB778",` 34 `"buyeruid":"100"` 35 `},` 36 `"device":` 37 `{` 38 `"ip":"192.168.0.1",` 39 `"ua":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.30 ` 40 `(KHTML, like Gecko) Chrome/12.0.742.53 Safari/534.30"` 41 `},` 42 `"cud":` 43 `{` 44 `"age":"23",` 45 `"gender":"female"` 46 `}` 47 `}`; 48 49 // Aliases to avoid polluting this example with dozens of "JsonExtractor.". 50 51 alias JsonExtractor.Parser Parser; // actually aliases JsonParserIter 52 alias JsonExtractor.GetField GetField; 53 alias JsonExtractor.GetObject GetObject; 54 alias JsonExtractor.GetArray GetArray; 55 alias JsonExtractor.Main Main; 56 alias JsonExtractor.Type Type; // actually aliases JsonParser.Token 57 58 // Create JSON parser instance. 59 60 Parser json = new Parser; 61 62 // Create one GetField instance for each JSON object field to extract. 63 64 GetField id = new GetField, 65 impid = new GetField, 66 page = new GetField, 67 uid = new GetField, 68 h = new GetField, 69 w = new GetField; 70 71 // Create one GetObject instance for each JSON subobject that contains 72 // fields to extract and pass an associative array of name/GetField 73 // instance pairs to define the fields that should be extracted in this 74 // subobject. 75 76 GetObject site = new GetObject(json, ["page": page]), 77 user = new GetObject(json, ["uid": uid]), 78 // cast needed to prevent array type inference error 79 imp_element = new GetObject(json, ["impid"[]: impid, "w": w, "h": h]); 80 81 82 // Create one IterateArray instance for each JSON array that contains 83 // members to extract. 84 85 GetArray imp = new GetArray(json, [imp_element] 86 (uint i, Type type, cstring value) 87 { 88 // This delegate will be called for each 89 // "imp" array element with i as index. Note 90 // that value is meaningful only if type is 91 // type.String or type.Number. 92 // We are interested in the first array 93 // element only, which we expect to be an 94 // object, so we call imp_element.set() when 95 // i is 0. We return true if we handle the 96 // element or false to make imp skip it. 97 98 bool handled = i == 0; 99 100 if (handled) 101 { 102 if (type == type.BeginObject) 103 { 104 imp_element.set(type); 105 } 106 else throw new Exception 107 ( 108 "\"imp\" array element is not an " 109 "object as expected!" 110 ); 111 } 112 113 return handled; 114 }); 115 116 // Create a Main (GetObject subclass) instance for the main JSON object and 117 // pass the top level getters. 118 119 Main main = new Main(json, ["id"[]: id, "imp": imp, "site": site, 120 "user": user]); 121 122 // Here we go. 123 124 main.parse(content); 125 126 // id.type is now Type.String 127 // id.value is now "8c97472e-098e-4baa-aa63-4a3f2aab10c6" 128 129 // impid.type is now Type.String 130 // impid.value is now "7682f6f1-810c-49b0-8388-f91ba4a00c1d" 131 132 // page.type is now Type.String 133 // page.value is now "http://www.example.com/" 134 135 // uid.type is now Type.String 136 // uid.value is now "45FB778" 137 138 // h.type is now Type.Number 139 // h.value is now "480" 140 141 // w.type is now Type.Number 142 // w.value is now "640" 143 144 --- 145 146 Copyright: 147 Copyright (c) 2009-2016 dunnhumby Germany GmbH. 148 All rights reserved. 149 150 License: 151 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 152 Alternatively, this file may be distributed under the terms of the Tango 153 3-Clause BSD License (see LICENSE_BSD.txt for details). 154 155 ******************************************************************************/ 156 157 module ocean.text.json.JsonExtractor; 158 159 160 import ocean.meta.types.Qualifiers; 161 162 import ocean.text.json.JsonParserIter; 163 import ocean.core.Array; 164 import ocean.core.Enforce : enforce; 165 import ocean.core.Test; 166 import ocean.core.Verify; 167 import ocean.util.ReusableException; 168 169 170 /******************************************************************************* 171 172 Exception which only can be thrown by JsonExtractor 173 174 *******************************************************************************/ 175 176 private class JsonException : ReusableException {} 177 178 179 struct JsonExtractor 180 { 181 static: 182 183 /************************************************************************** 184 185 Type aliases for using code 186 187 **************************************************************************/ 188 189 alias JsonParserIter!(false) Parser; 190 191 alias JsonParserIter!(false).Token Type; 192 193 /*************************************************************************** 194 195 JSON main/top level object getter 196 197 **************************************************************************/ 198 199 class Main : GetObject 200 { 201 /*********************************************************************** 202 203 JSON parser instance 204 205 **********************************************************************/ 206 207 private Parser json; 208 209 /*********************************************************************** 210 211 Constructor, specifies getters for named and unnamed fields. 212 213 If the i-th object field is not named and the i-th instance element 214 in get_indexed_fields is not null, it will be invoked with that 215 field. 216 217 Params: 218 json = JSON parser 219 get_named_fields = list of getters for named fields, 220 associated with field names 221 get_indexed_fields = list of getters for fields without name, 222 may contain null elements to ignore fields. 223 224 **********************************************************************/ 225 226 public this ( Parser json, GetField[cstring] get_named_fields, 227 GetField[] get_indexed_fields ... ) 228 { 229 super(this.json = json, get_named_fields, get_indexed_fields); 230 } 231 232 /*********************************************************************** 233 234 Resets all type/value results and parses content, extracting types 235 and values for the fields to extract. 236 237 Params: 238 content = JSON content to parse 239 240 Returns: 241 true on success or false otherwise. 242 243 Throws: 244 Propagates exceptions thrown in 245 ocean.text.json.JsonParser.parse(). 246 247 **********************************************************************/ 248 249 bool parse ( cstring content ) 250 { 251 super.reset(); 252 253 bool ok = this.json.reset(content); 254 255 if (ok) 256 { 257 super.set(this.json.type); 258 } 259 260 return ok; 261 } 262 } 263 264 /*************************************************************************** 265 266 JSON field getter, extracts type and value of a field. 267 268 **************************************************************************/ 269 270 class GetField 271 { 272 /********************************************************************** 273 274 Field type 275 276 **********************************************************************/ 277 278 Type type; 279 280 /*********************************************************************** 281 282 Field value, meaningful only for certain types, especially 283 Type.String and Type.Number. Corresponds to the value returned by 284 JsonParser.value() for this field. 285 286 **********************************************************************/ 287 288 public cstring value = null; 289 290 /*********************************************************************** 291 292 Sets type and value for the field represented by this instance. 293 294 Params: 295 type = field type 296 value = field value if meaningful (depends on type) 297 298 **********************************************************************/ 299 300 final void set ( Type type, cstring value = null ) 301 { 302 this.type = type; 303 this.value = value; 304 this.set_(); 305 } 306 307 /********************************************************************** 308 309 Resets type and value. 310 311 **********************************************************************/ 312 313 final void reset ( ) 314 { 315 this.type = this.type.init; 316 this.value = null; 317 this.reset_(); 318 } 319 320 /*********************************************************************** 321 322 To be overridden, called when set() has finished. 323 324 **********************************************************************/ 325 326 protected void set_ ( ) { } 327 328 /*********************************************************************** 329 330 To be overridden, called when reset() has finished. 331 332 **********************************************************************/ 333 334 protected void reset_ ( ) { } 335 } 336 337 /************************************************************************** 338 339 JSON object getter, invokes registered field getters with type and value 340 of the corresponding fields in a JSON object. 341 342 **************************************************************************/ 343 344 class GetObject : IterateAggregate 345 { 346 /*********************************************************************** 347 348 If enabled, any unmatched field will result in an exception. 349 350 *********************************************************************/ 351 352 public bool strict; 353 354 /*********************************************************************** 355 356 List of getters for named fields, each associated with the name of a 357 field. 358 359 **********************************************************************/ 360 361 private GetField[cstring] get_named_fields; 362 363 /*********************************************************************** 364 365 List of getters for fields without name, may contain null elements 366 to ignore fields. If the i-th object field is not named and the 367 i-th instance element is not null, it will be invoked with that 368 field. 369 370 **********************************************************************/ 371 372 private GetField[] get_indexed_fields; 373 374 375 /*********************************************************************** 376 377 Thrown as indicator when strict behavior enforcement fails. 378 379 *********************************************************************/ 380 381 private JsonException field_unmatched; 382 383 /*********************************************************************** 384 385 Constructor, specifies getters for named and unnamed fields. 386 387 If the i-th object field is not named and the i-th instance element 388 in get_indexed_fields is not null, it will be invoked with that 389 field. 390 391 Params: 392 json = JSON parser 393 get_named_fields = list of getters for named fields, 394 associated with field names 395 get_indexed_fields = list of getters for fields without name, 396 may contain null elements to ignore fields. 397 398 **********************************************************************/ 399 400 public this ( Parser json, GetField[cstring] get_named_fields, 401 GetField[] get_indexed_fields ... ) 402 { 403 this(json, false, get_named_fields, get_indexed_fields); 404 } 405 406 /*********************************************************************** 407 408 Constructor, specifies getters for named and unnamed fields. 409 410 If the i-th object field is not named and the i-th instance element 411 in get_indexed_fields is not null, it will be invoked with that 412 field. 413 414 Params: 415 json = JSON parser 416 skip_null = should a potential null value be skipped? 417 get_named_fields = list of getters for named fields, 418 associated with field names 419 get_indexed_fields = list of getters for fields without name, 420 may contain null elements to ignore fields. 421 422 **********************************************************************/ 423 424 public this ( Parser json, bool skip_null, 425 GetField[cstring] get_named_fields, 426 GetField[] get_indexed_fields ... ) 427 { 428 super(json, Type.BeginObject, Type.EndObject, skip_null); 429 430 this.field_unmatched = new JsonException(); 431 this.get_named_fields = get_named_fields.rehash; 432 this.get_indexed_fields = get_indexed_fields; 433 } 434 435 /*********************************************************************** 436 437 Add the field to the list of named objects to get. 438 439 Params: 440 name = the name of the field 441 field = the field instance 442 443 **********************************************************************/ 444 445 public void addNamedField ( cstring name, GetField field ) 446 { 447 this.get_named_fields[name] = field; 448 } 449 450 /*********************************************************************** 451 452 Remove a field from the list of named objects to get. 453 454 Params: 455 name = the name of the field 456 457 **********************************************************************/ 458 459 public void removeNamedField ( cstring name ) 460 { 461 this.get_named_fields.remove(name); 462 } 463 464 /*********************************************************************** 465 466 Called by super.reset() to reset all field getters. 467 468 **********************************************************************/ 469 470 protected override void reset_ ( ) 471 { 472 foreach (get_field; this.get_named_fields) 473 { 474 get_field.reset(); 475 } 476 477 foreach (get_field; this.get_indexed_fields) 478 { 479 get_field.reset(); 480 } 481 } 482 483 /*********************************************************************** 484 485 Called by super.reset() to reset all field getters. 486 487 **********************************************************************/ 488 489 protected override void set_ ( ) 490 { 491 super.set_(); 492 493 if (this.strict) 494 { 495 foreach (name, field; this.get_named_fields) 496 { 497 if (field.type == Type.Empty) 498 { 499 throw this.field_unmatched 500 .set("Field '") 501 .append(name) 502 .append("' not found in JSON"); 503 } 504 } 505 506 foreach (i, field; this.get_indexed_fields) 507 { 508 if (field.type == Type.Empty) 509 { 510 throw this.field_unmatched 511 .set("Unnamed field not found in JSON"); 512 } 513 } 514 } 515 } 516 517 /*********************************************************************** 518 519 Picks the field getter responsible for the field corresponding to 520 name, or i if unnamed, and sets its type and value. 521 522 Params: 523 i = field index 524 type = field type 525 name = field name or null if unnamed. 526 value = field value, meaningful only for certain types. 527 528 Returns: 529 true if a getter handled the field or false to skip it. 530 531 **********************************************************************/ 532 533 protected override bool setField ( uint i, Type type, cstring name, cstring value ) 534 { 535 GetField get_field = this.getGetField(i, name); 536 537 bool handle = get_field !is null; 538 539 if (handle) 540 { 541 get_field.set(type, value); 542 } 543 544 return handle; 545 } 546 547 /*********************************************************************** 548 549 Picks the field getter responsible for the field corresponding to 550 name, or i if unnamed. 551 552 Params: 553 i = field index 554 name = field name or null if unnamed 555 556 Returns: 557 GetField instance responsible for the field or null if there is 558 no responsible getter. 559 560 **********************************************************************/ 561 562 private GetField getGetField ( uint i, cstring name ) 563 { 564 GetField* get_field = name? 565 name in this.get_named_fields : 566 (i < this.get_indexed_fields.length)? 567 &this.get_indexed_fields[i] : 568 null; 569 570 return get_field? *get_field : null; 571 } 572 } 573 574 /************************************************************************** 575 576 JSON array getter, invokes a callback delegate with each element in a 577 JSON array. 578 579 **************************************************************************/ 580 581 class GetArray : IterateArray 582 { 583 /*********************************************************************** 584 585 Iteration callback delegate type alias. The delegate must either use 586 an appropriate GetField (or subclass) instance to handle and move 587 the parser to the end of the field or indicate that this field is 588 ignored and unhandled. 589 590 Params: 591 i = element index counter, starts with 0 592 type = element type 593 value = element value, meaningful only for certain types. 594 595 Returns: 596 true if an appropriate GetField (or subclass) instance was used 597 to handle and move the parser to the end of the field or false 598 if the field is ignored and unhandled and should be skipped. 599 600 **********************************************************************/ 601 602 public alias bool delegate ( uint i, Type type, cstring value) IteratorDg; 603 604 /*********************************************************************** 605 606 Iteration callback delegate 607 608 **********************************************************************/ 609 610 private IteratorDg iterator_dg; 611 612 /*********************************************************************** 613 614 List of fields to reset when this.reset is called. 615 616 **********************************************************************/ 617 618 private GetField[] fields_to_reset; 619 620 /*********************************************************************** 621 622 Constructor 623 624 Params: 625 json = JSON parser 626 fields_to_reset = fields to reset when this.reset is called 627 iterator_dg = iteration callback delegate 628 skip_null = should a potential null value be skipped? If 629 false and a null value is found a 630 JsonException will be thrown. 631 632 **********************************************************************/ 633 634 public this ( Parser json, GetField[] fields_to_reset, 635 scope IteratorDg iterator_dg, bool skip_null = false ) 636 { 637 super(json, skip_null); 638 639 this.fields_to_reset = fields_to_reset; 640 641 this.iterator_dg = iterator_dg; 642 } 643 644 /*********************************************************************** 645 646 Invokes the iteration callback delegate. 647 648 Params: 649 i = field index 650 type = field type 651 name = (ignored) 652 value = field value 653 654 Returns: 655 passes through the return value of the delegate. 656 657 **********************************************************************/ 658 659 protected override bool setField ( uint i, Type type, cstring name, 660 cstring value ) 661 { 662 return this.iterator_dg(i, type, value); 663 } 664 665 666 /*********************************************************************** 667 668 Called by super.reset() to reset all field given by fields_to_reset. 669 670 **********************************************************************/ 671 672 protected override void reset_ ( ) 673 { 674 foreach (get_field; this.fields_to_reset) 675 { 676 get_field.reset(); 677 } 678 } 679 } 680 681 /************************************************************************** 682 683 Abstract JSON array iterator. As an alternative to the use of an 684 iteration callback delegate with GetArray one can derive from this 685 class and implement setField(). 686 687 **************************************************************************/ 688 689 abstract class IterateArray : IterateAggregate 690 { 691 /*********************************************************************** 692 693 Constructor 694 695 Params: 696 type = expected parameter type 697 key = parameter name 698 skip_null = should a potential null value be skipped? If false 699 and a null value is found an JsonException will 700 be thrown. 701 702 **********************************************************************/ 703 704 public this ( Parser json, bool skip_null = false ) 705 { 706 super(json, Type.BeginArray, Type.EndArray, skip_null); 707 } 708 } 709 710 /************************************************************************** 711 712 JSON object or array iterator. 713 714 **************************************************************************/ 715 716 abstract class IterateAggregate : GetField 717 { 718 719 /*********************************************************************** 720 721 Skip null value? 722 723 **********************************************************************/ 724 725 private bool skip_null; 726 727 /********************************************************************** 728 729 Start and end token type, usually BeginObject/EndObject or 730 BeginArray/EndArray. 731 732 **********************************************************************/ 733 734 public Type start_type, end_type; 735 736 /*********************************************************************** 737 738 JSON parser instance 739 740 **********************************************************************/ 741 742 private Parser json; 743 744 /*********************************************************************** 745 746 Exception throw to indicate errors during parsing. 747 748 **********************************************************************/ 749 750 protected JsonException exception; 751 752 /*********************************************************************** 753 754 Constructor 755 756 Params: 757 json = JSON parser, can't be null 758 start_type = opening token type of the aggregate this instance 759 iterates over (usually BeginObject or BeginArray) 760 end_type = closing token type of the aggregate this instance 761 iterates over (usually EndObject or EndArray) 762 skip_null = should a potential null value be skipped? If false 763 and a null value is found an AssertException will 764 be thrown. 765 766 **********************************************************************/ 767 768 public this ( Parser json, Type start_type, Type end_type, 769 bool skip_null = false ) 770 { 771 verify(json !is null); 772 this.start_type = start_type; 773 this.end_type = end_type; 774 this.json = json; 775 this.exception = new JsonException(); 776 this.skip_null = skip_null; 777 } 778 779 /*********************************************************************** 780 781 Invoked by super.set() to iterate over the JSON object or array. 782 Expects the type of the current token to be 783 - the start type if this.skip_null is false or 784 - the start type or null if this.skip_null is true. 785 786 Throws: 787 JsonException if the type of the current token is not as 788 expected. 789 790 **********************************************************************/ 791 792 protected override void set_ ( ) 793 { 794 enforce(this.exception, 795 (this.type == this.start_type) || 796 (this.skip_null && this.type == Type.Null), 797 "type mismatch"); 798 799 uint i = 0; 800 801 if (this.json.next()) foreach (type, name, value; this.json) 802 { 803 if (type == this.end_type) 804 { 805 break; 806 } 807 else if (!this.setField(i++, type, name, value)) 808 { 809 this.json.skip(); 810 } 811 } 812 } 813 814 /*********************************************************************** 815 816 Abstract iteration method, must either use an appropriate GetField 817 (or subclass) instance to handle and move the parser to the end of 818 the field or indicate that this field is ignored and unhandled. 819 820 Params: 821 i = element index counter, starts with 0. 822 name = field name or null if the field is unnamed or iterating 823 over an array. 824 type = element type 825 value = element value, meaningful only for certain types. 826 827 Returns: 828 true if an appropriate GetField (or subclass) instance was used 829 to handle and move the parser to the end of the field or false 830 if the field is ignored and unhandled and should be skipped. 831 832 **********************************************************************/ 833 834 abstract protected bool setField ( uint i, Type type, cstring name, 835 cstring value ); 836 } 837 838 /**************************************************************************/ 839 840 unittest 841 { 842 enum content = 843 `{ 844 "id":"8c97472e-098e-4baa-aa63-4a3f2aab10c6", 845 "imp": 846 [ 847 { 848 "impid":"7682f6f1-810c-49b0-8388-f91ba4a00c1d", 849 "h":480, 850 "w":640, 851 "btype": [ 1,2,3 ], 852 "battr": [ 3,4,5 ] 853 }, 854 { 855 "Hello": "World!" 856 }, 857 12345 858 ], 859 "site": 860 { 861 "sid":"1", 862 "name":"MySite", 863 "pub":"MyPublisher", 864 "cat": [ "IAB1", "IAB2" ], 865 "page":"http://www.example.com/" 866 }, 867 "bcat": null, 868 "user": 869 { 870 "uid":"45FB778", 871 "buyeruid":"100" 872 }, 873 "device": 874 { 875 "ip":"192.168.0.1", 876 "ua":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.30 ` 877 ~ `(KHTML, like Gecko) Chrome/12.0.742.53 Safari/534.30" 878 }, 879 "cud": 880 { 881 "age":"23", 882 "gender":"female" 883 } 884 }`; 885 886 auto t = new NamedTest("JsonExtractor"); 887 888 889 scope json = new Parser, 890 id = new GetField, 891 impid = new GetField, 892 page = new GetField, 893 uid = new GetField, 894 h = new GetField, 895 w = new GetField, 896 not = new GetField, 897 site = new GetObject(json, ["page": page]), 898 user = new GetObject(json, ["uid": uid]), 899 imp_element = new GetObject(json, ["impid"[]: impid, 900 "w": w]), 901 bcat = new GetObject(json, true, ["not":not]), 902 imp = new GetArray(json, [imp_element], 903 (uint i, Type type, cstring value) 904 { 905 bool handled = i == 0; 906 907 if (handled) 908 { 909 t.test!("==")(type, 910 type.BeginObject); 911 imp_element.set(type); 912 } 913 914 return handled; 915 }), 916 main = new Main(json, ["id"[]: id, "imp": imp, 917 "site": site, "user": user]); 918 919 imp_element.addNamedField("h", h); 920 921 bool ok = main.parse(content); 922 923 t.test(ok, "parse didn't return true"); 924 925 t.test!("==")(id.type, Type.String); 926 t.test!("==")(id.value, "8c97472e-098e-4baa-aa63-4a3f2aab10c6"[]); 927 928 t.test!("==")(impid.type, Type.String); 929 t.test!("==")(impid.value, "7682f6f1-810c-49b0-8388-f91ba4a00c1d"[]); 930 931 t.test!("==")(page.type, Type.String); 932 t.test!("==")(page.value, "http://www.example.com/"[]); 933 934 t.test!("==")(uid.type, Type.String); 935 t.test!("==")(uid.value, "45FB778"[]); 936 937 t.test!("==")(not.type, Type.Empty); 938 t.test!("==")(not.value, ""[]); 939 940 t.test!("==")(h.type, Type.Number); 941 t.test!("==")(h.value, "480"[]); 942 943 t.test!("==")(w.type, Type.Number); 944 t.test!("==")(w.value, "640"[]); 945 946 imp_element.removeNamedField("h"); 947 h.reset(); 948 949 ok = main.parse(content); 950 951 t.test(ok, "parse didn't return true"[]); 952 953 t.test!("==")(h.type, Type.Empty); 954 t.test!("==")(h.value, ""[]); 955 956 957 ok = main.parse("{}"); 958 959 t.test(ok, "parse didn't return true"[]); 960 961 t.test!("==")(id.value, ""[]); 962 t.test!("==")(id.type, Type.Empty); 963 964 t.test!("==")(impid.value, ""[]); 965 t.test!("==")(impid.type, Type.Empty); 966 967 t.test!("==")(page.value, ""[]); 968 t.test!("==")(page.type, Type.Empty); 969 970 t.test!("==")(uid.value, ""[]); 971 t.test!("==")(uid.type, Type.Empty); 972 973 t.test!("==")(not.type, Type.Empty); 974 t.test!("==")(not.value, ""[]); 975 976 t.test!("==")(h.value, ""[]); 977 t.test!("==")(h.type, Type.Empty); 978 979 t.test!("==")(w.value, ""[]); 980 t.test!("==")(w.type, Type.Empty); 981 982 enum content2 = `{"imp":null}`; 983 984 try 985 { 986 main.parse(content2); 987 t.test(false, "parse didn't throw"[]); 988 } 989 catch (JsonException e) 990 { 991 t.test!("==")(e.message(), "type mismatch"[]); 992 } 993 994 bool fun (uint i, Type type, cstring value) 995 { 996 return false; 997 } 998 999 scope imp2 = new GetArray(json, null, &fun, true), 1000 main2 = new Main(json, ["imp": imp2]); 1001 1002 ok = main2.parse(content2); 1003 1004 t.test(ok, "parse didn't return true"[]); 1005 1006 } 1007 }