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 moduleocean.text.json.JsonExtractor;
158 159 160 importocean.transition;
161 162 importocean.text.json.JsonParserIter;
163 importocean.core.Array;
164 importocean.core.Enforce : enforce;
165 importocean.core.Test;
166 importocean.core.Verify;
167 importocean.util.ReusableException;
168 169 170 /*******************************************************************************
171 172 Exception which only can be thrown by JsonExtractor
173 174 *******************************************************************************/175 176 privateclassJsonException : ReusableException {}
177 178 179 structJsonExtractor180 {
181 static:
182 183 /**************************************************************************
184 185 Type aliases for using code
186 187 **************************************************************************/188 189 aliasJsonParserIter!(false) Parser;
190 191 aliasJsonParserIter!(false).TokenType;
192 193 /***************************************************************************
194 195 JSON main/top level object getter
196 197 **************************************************************************/198 199 classMain : GetObject200 {
201 /***********************************************************************
202 203 JSON parser instance
204 205 **********************************************************************/206 207 privateParserjson;
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 publicthis ( Parserjson, 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 boolparse ( cstringcontent )
250 {
251 super.reset();
252 253 boolok = this.json.reset(content);
254 255 if (ok)
256 {
257 super.set(this.json.type);
258 }
259 260 returnok;
261 }
262 }
263 264 /***************************************************************************
265 266 JSON field getter, extracts type and value of a field.
267 268 **************************************************************************/269 270 classGetField271 {
272 /**********************************************************************
273 274 Field type
275 276 **********************************************************************/277 278 Typetype;
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 publiccstringvalue = 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 finalvoidset ( Typetype, cstringvalue = 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 finalvoidreset ( )
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 protectedvoidset_ ( ) { }
327 328 /***********************************************************************
329 330 To be overridden, called when reset() has finished.
331 332 **********************************************************************/333 334 protectedvoidreset_ ( ) { }
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 classGetObject : IterateAggregate345 {
346 /***********************************************************************
347 348 If enabled, any unmatched field will result in an exception.
349 350 *********************************************************************/351 352 publicboolstrict;
353 354 /***********************************************************************
355 356 List of getters for named fields, each associated with the name of a
357 field.
358 359 **********************************************************************/360 361 privateGetField[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 privateGetField[] get_indexed_fields;
373 374 375 /***********************************************************************
376 377 Thrown as indicator when strict behavior enforcement fails.
378 379 *********************************************************************/380 381 privateJsonExceptionfield_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 publicthis ( Parserjson, 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 publicthis ( Parserjson, boolskip_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 = newJsonException();
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 publicvoidaddNamedField ( cstringname, GetFieldfield )
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 publicvoidremoveNamedField ( cstringname )
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 protectedoverridevoidreset_ ( )
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 protectedoverridevoidset_ ( )
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 throwthis.field_unmatched500 .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 throwthis.field_unmatched511 .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 protectedoverrideboolsetField ( uinti, Typetype, cstringname, cstringvalue )
534 {
535 GetFieldget_field = this.getGetField(i, name);
536 537 boolhandle = get_field !isnull;
538 539 if (handle)
540 {
541 get_field.set(type, value);
542 }
543 544 returnhandle;
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 privateGetFieldgetGetField ( uinti, cstringname )
563 {
564 GetField* get_field = name?
565 nameinthis.get_named_fields :
566 (i < this.get_indexed_fields.length)?
567 &this.get_indexed_fields[i] :
568 null;
569 570 returnget_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 classGetArray : IterateArray582 {
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 publicaliasbooldelegate ( uinti, Typetype, cstringvalue) IteratorDg;
603 604 /***********************************************************************
605 606 Iteration callback delegate
607 608 **********************************************************************/609 610 privateIteratorDgiterator_dg;
611 612 /***********************************************************************
613 614 List of fields to reset when this.reset is called.
615 616 **********************************************************************/617 618 privateGetField[] 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 publicthis ( Parserjson, GetField[] fields_to_reset,
635 scopeIteratorDgiterator_dg, boolskip_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 protectedoverrideboolsetField ( uinti, Typetype, cstringname,
660 cstringvalue )
661 {
662 returnthis.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 protectedoverridevoidreset_ ( )
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 abstractclassIterateArray : IterateAggregate690 {
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 publicthis ( Parserjson, boolskip_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 abstractclassIterateAggregate : GetField717 {
718 719 /***********************************************************************
720 721 Skip null value?
722 723 **********************************************************************/724 725 privateboolskip_null;
726 727 /**********************************************************************
728 729 Start and end token type, usually BeginObject/EndObject or
730 BeginArray/EndArray.
731 732 **********************************************************************/733 734 publicTypestart_type, end_type;
735 736 /***********************************************************************
737 738 JSON parser instance
739 740 **********************************************************************/741 742 privateParserjson;
743 744 /***********************************************************************
745 746 Exception throw to indicate errors during parsing.
747 748 **********************************************************************/749 750 protectedJsonExceptionexception;
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 publicthis ( Parserjson, Typestart_type, Typeend_type,
769 boolskip_null = false )
770 {
771 verify(json !isnull);
772 this.start_type = start_type;
773 this.end_type = end_type;
774 this.json = json;
775 this.exception = newJsonException();
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 protectedoverridevoidset_ ( )
793 {
794 enforce(this.exception,
795 (this.type == this.start_type) ||
796 (this.skip_null && this.type == Type.Null),
797 "type mismatch");
798 799 uinti = 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 elseif (!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 abstractprotectedboolsetField ( uinti, Typetype, cstringname,
835 cstringvalue );
836 }
837 838 /**************************************************************************/839 840 unittest841 {
842 enumcontent =
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 autot = newNamedTest("JsonExtractor");
887 888 889 scopejson = newParser,
890 id = newGetField,
891 impid = newGetField,
892 page = newGetField,
893 uid = newGetField,
894 h = newGetField,
895 w = newGetField,
896 not = newGetField,
897 site = newGetObject(json, ["page": page]),
898 user = newGetObject(json, ["uid": uid]),
899 imp_element = newGetObject(json, ["impid"[]: impid,
900 "w": w]),
901 bcat = newGetObject(json, true, ["not":not]),
902 imp = newGetArray(json, [imp_element],
903 (uinti, Typetype, cstringvalue)
904 {
905 boolhandled = i == 0;
906 907 if (handled)
908 {
909 t.test!("==")(type,
910 type.BeginObject);
911 imp_element.set(type);
912 }
913 914 returnhandled;
915 }),
916 main = newMain(json, ["id"[]: id, "imp": imp,
917 "site": site, "user": user]);
918 919 imp_element.addNamedField("h", h);
920 921 boolok = 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 enumcontent2 = `{"imp":null}`;
983 984 try985 {
986 main.parse(content2);
987 t.test(false, "parse didn't throw"[]);
988 }
989 catch (JsonExceptione)
990 {
991 t.test!("==")(e.message(), "type mismatch"[]);
992 }
993 994 boolfun (uinti, Typetype, cstringvalue)
995 {
996 returnfalse;
997 }
998 999 scopeimp2 = newGetArray(json, null, &fun, true),
1000 main2 = newMain(json, ["imp": imp2]);
1001 1002 ok = main2.parse(content2);
1003 1004 t.test(ok, "parse didn't return true"[]);
1005 1006 }
1007 }