1 /** 2 $(P An $(LINK2 https://github.com/marler8997/mored/wiki/ASON-(Application-Specific-Object-Notation), ASON) parser. 3 ) 4 5 Examples: 6 -------------------------------- 7 // Possible API: 8 serializeToAson(R,T)(R sink, T value); 9 deserializeAson(T,R)(R input); 10 11 Ason parseAson(R)(ref R range, int* line = null); 12 Ason parseAson(string text); 13 14 void writeAson(R, bool compressed)(ref R sink, in Json json, size_t level); 15 16 struct Ason : represents single Ason value 17 struct AsonSerializer : serializer for Ason 18 struct AsonStringSerializer : serializer for a range based plain ASON string 19 20 -------------------------------- 21 Authors: Jonathan Marler, johnnymarler@gmail.com 22 License: use freely for any purpose 23 */ 24 25 module more.ason; 26 27 import std.array; 28 import std..string; 29 import std.range; 30 import std.conv; 31 import std.bitmanip; 32 import std.traits; 33 34 import std.c..string: memmove; 35 36 import more.common; 37 import more.utf8; 38 39 version(unittest_ason) 40 { 41 import std.stdio; 42 } 43 44 enum AsonErrorType { 45 unknown, 46 braceAfterNewline, 47 mixedValuesAndAttributes, 48 } 49 class AsonParseException : Exception 50 { 51 AsonErrorType type; 52 uint lineInAson; 53 this(uint lineInAson, string msg, string file = __FILE__, size_t codeLine = __LINE__) { 54 this(AsonErrorType.unknown, lineInAson, msg, file, codeLine); 55 } 56 this(AsonErrorType errorType, uint lineInAson, string msg, string file = __FILE__, size_t codeLine = __LINE__) { 57 super((lineInAson == 0) ? msg : "line "~to!string(lineInAson)~": "~msg, file, codeLine); 58 this.type = errorType; 59 this.lineInAson = lineInAson; 60 } 61 } 62 63 struct Attribute { 64 const(char)[] namespace; 65 const(char)[] id; 66 const(char)[] value; 67 } 68 69 70 struct Ason { 71 } 72 73 74 /// Embodies all the information about a single tag. 75 /// It does not contain any information about its children because that part of the sdl would not have been parsed yet. 76 /// It is used directly for the StAX/SAX APIs but not for the DOM or Reflection APIs. 77 struct Tag { 78 79 // A bifield of flags used to pass extra options to parseAsonTag. 80 // Used to accept/reject different types of SDL or cause parseAsonTag to 81 // behave differently like preventing it from modifying the sdl text. 82 private ubyte flags; 83 84 /// Normally SDL only allows a tag's attributes to appear after all it's values. 85 /// This flag causes parseAsonTag to allow values/attributes to appear in any order, i.e. 86 /// $(D tag attr="my-value" "another-value" # would be valid) 87 @property @safe bool allowMixedValuesAndAttributes() pure nothrow const { return (flags & 1U) != 0;} 88 @property @safe void allowMixedValuesAndAttributes(bool v) pure nothrow { if (v) flags |= 1U;else flags &= ~1U;} 89 90 /// Causes parseAsonTag to allow a tag's open brace to appear after any number of newlines 91 @property @safe bool allowBraceAfterNewline() pure nothrow const { return (flags & 2U) != 0;} 92 @property @safe void allowBraceAfterNewline(bool v) pure nothrow { if (v) flags |= 2U;else flags &= ~2U;} 93 94 /// Causes parseAsonTag to throw an exception if it finds any number literals 95 /// with postfix letters indicating the type 96 @property @safe bool rejectTypedNumbers() pure nothrow const { return (flags & 4U) != 0;} 97 @property @safe void rejectTypedNumbers(bool v) pure nothrow { if (v) flags |= 4U;else flags &= ~4U;} 98 99 /// Causes parseAsonTag to set the tag name to null instead of "content" for anonymous tags. 100 /// This allows the application to differentiate betweeen "content" tags and anonymous tags. 101 @property @safe bool anonymousTagNameIsNull() pure nothrow const { return (flags & 8U) != 0;} 102 @property @safe void anonymousTagNameIsNull(bool v) pure nothrow { if (v) flags |= 8U;else flags &= ~8U;} 103 104 /// Prevents parseAsonTag from modifying the given sdl text for things such as 105 /// processing escaped strings 106 @property @safe bool preserveAsonText() pure nothrow const { return (flags & 16U) != 0;} 107 @property @safe void preserveAsonText(bool v) pure nothrow { if (v) flags |= 16U;else flags &= ~16U;} 108 109 110 // TODO: maybe add an option to specify that any values accessed should be copied to new buffers 111 // NOTE: Do not add an option to prevent parseAsonTag from throwing exceptions when the input has ended. 112 // It may have been useful for an input buffered object, however, the buffered input object will 113 // need to know when it has a full tag anyway so the sdl will already contain the characters to end the tag. 114 // Or in the case of braces on the next line, if the tag has alot of whitespace until the actual end-of-tag 115 // delimiter, the buffered input reader can insert a semi-colon or open_brace to signify the end of the tag 116 // earlier. 117 118 119 120 /// For now an alias for useStrictAson. Use this function if you want your code to always use 121 /// the default mode whatever it may become. 122 alias useStrictAson useDefaultAson; 123 124 /// This is the default mode. 125 /// $(OL 126 /// $(LI Causes parseAsonTag to throw AsonParseException if a tag's open brace appears after a newline) 127 /// $(LI Causes parseAsonTag to throw AsonParseException if any tag value appears after any tag attribute) 128 /// $(LI Causes parseAsonTag to accept postfix characters after number literals.) 129 /// $(LI Causes parseAsonTag to set anonymous tag names to "content") 130 /// ) 131 void useStrictAson() { 132 this.allowMixedValuesAndAttributes = false; 133 this.allowBraceAfterNewline = false; 134 this.rejectTypedNumbers = false; 135 this.anonymousTagNameIsNull = false; 136 } 137 /// $(OL 138 /// $(LI Causes parseAsonTag to throw AsonParseException if a tag's open brace appears after a newline) 139 /// $(LI Causes parseAsonTag to throw AsonParseException if any tag value appears after any tag attribute) 140 /// $(LI Causes parseAsonTag to accept postfix characters after number literals.) 141 /// $(LI Causes parseAsonTag to set anonymous tag names to "content") 142 /// ) 143 void useLooseAson() { 144 this.allowMixedValuesAndAttributes = true; 145 this.allowBraceAfterNewline = true; 146 this.rejectTypedNumbers = false; 147 this.anonymousTagNameIsNull = false; 148 } 149 /// $(OL 150 /// $(LI Causes parseAsonTag to allow a tag's open brace appears after any number of newlines) 151 /// $(LI Causes parseAsonTag to allow tag values an attributes to mixed in any order) 152 /// $(LI Causes parseAsonTag to throw AsonParseException if a number literal has any postfix characters) 153 /// $(LI Causes parseAsonTag to set anonymous tag names to null) 154 /// ) 155 void useProposedAson() { 156 this.allowMixedValuesAndAttributes = true; 157 this.allowBraceAfterNewline = true; 158 this.rejectTypedNumbers = true; 159 this.anonymousTagNameIsNull = true; 160 } 161 162 163 /// The depth of the tag, all root tags start at depth 0. 164 size_t depth = 0; 165 166 /// The line number of the SDL parser after parsing this tag. 167 uint line = 1; 168 169 /// The namespace of the tag 170 const(char)[] namespace; 171 /// The name of the tag 172 const(char)[] name; 173 /// The values of the tag 174 auto values = appender!(const(char)[][])(); 175 /// The attributes of the tag 176 auto attributes = appender!(Attribute[])(); 177 /// Indicates the tag has an open brace 178 bool hasOpenBrace; 179 180 /+ 181 version(unittest_ason) 182 { 183 // This function is only so unit tests can create Tags to compare 184 // with tags parsed from the parseAsonTag function. This constructor 185 // should never be called in production code 186 this(const(char)[] name, const(char)[][] values...) { 187 auto colonIndex = name.indexOf(':'); 188 if(colonIndex > -1) { 189 this.namespace = name[0..colonIndex]; 190 this.name = name[colonIndex+1..$]; 191 } else { 192 this.namespace.length = 0; 193 this.name = name; 194 } 195 foreach(value; values) { 196 197 const(char)[] attributeNamespace = ""; 198 size_t equalIndex = size_t.max; 199 200 // check if it is an attribute 201 if(value.length && isIDStart(value[0])) { 202 size_t i = 1; 203 while(true) { 204 if(i >= value.length) break; 205 auto c = value[i]; 206 if(!isID(value[i])) { 207 if(c == ':') { 208 if(attributeNamespace.length) throw new Exception("contained 2 colons?"); 209 attributeNamespace = value[0..i]; 210 i++; 211 continue; 212 } 213 if(value[i] == '=') { 214 equalIndex = i; 215 } 216 break; 217 } 218 i++; 219 } 220 } 221 222 if(equalIndex == size_t.max) { 223 this.values.put(value); 224 } else { 225 Attribute a = {attributeNamespace, value[attributeNamespace.length..equalIndex], value[equalIndex+1..$]}; 226 this.attributes.put(a); 227 } 228 229 } 230 } 231 } 232 +/ 233 /// Gets the tag ready to parse a new sdl tree by resetting the depth and the line number. 234 /// It is unnecessary to call this before parsing the first sdl tree but would not be harmful. 235 /// It does not reset the namespace/name/values/attributes because those will 236 /// be reset by the parser on the next call to parseAsonTag when it calls $(D resetForNextTag()). 237 void resetForReuse() { 238 depth = 0; 239 line = 1; 240 } 241 242 /// Resets the tag state to get ready to parse the next tag. 243 /// Should only be called by the parseAsonTag function. 244 /// This will clear the namespace/name/values/attributes and increment the depth if the current tag 245 /// had an open brace. 246 void resetForNextTag() 247 { 248 this.namespace.length = 0; 249 this.name = null; 250 if(hasOpenBrace) { 251 hasOpenBrace = false; 252 this.depth++; 253 } 254 this.values.clear(); 255 this.attributes.clear(); 256 } 257 258 void setNamespace(inout(char)* start, inout(char)* limit) 259 { 260 this.namespace = (cast(const(char)*)start)[0..limit-start]; 261 } 262 void setIsAnonymous() 263 { 264 this.name = anonymousTagNameIsNull ? null : "content"; 265 } 266 void setName(inout(char)* start, inout(char)* limit) 267 { 268 //this.name = (start == limit) ? "content" : (cast(const(char)*)start)[0..limit-start]; 269 this.name = (start == limit) ? null : (cast(const(char)*)start)[0..limit-start]; 270 } 271 bool isAnonymous() { 272 return anonymousTagNameIsNull ? this.name is null : this.name == "content"; 273 } 274 275 /// Returns: true if the tag namespaces/names/values/attributes are 276 /// the same even if the depth/line/options are different. 277 bool opEquals(ref Tag other) { 278 return 279 namespace == other.namespace && 280 name == other.name && 281 values.data == other.values.data && 282 attributes.data == other.attributes.data; 283 } 284 285 /// Returns: A string of the Tag not including it's children. The string will be valid SDL 286 /// by itself but will not include the open brace if it has one. Use toAson for that. 287 string toString() { 288 string str = ""; 289 if(namespace.length) { 290 str ~= namespace; 291 str ~= name; 292 } 293 if(!isAnonymous || (values.data.length == 0 && attributes.data.length == 0)) { 294 str ~= name; 295 } 296 foreach(value; values.data) { 297 str ~= ' '; 298 str ~= value; 299 } 300 foreach(attribute; attributes.data) { 301 str ~= ' '; 302 if(attribute.namespace.length) { 303 str ~= attribute.namespace; 304 str ~= ':'; 305 } 306 str ~= attribute.id; 307 str ~= '='; 308 str ~= attribute.value; 309 } 310 return str; 311 } 312 313 /// Writes the tag as standard SDL to sink. 314 /// It will write the open brace '{' but since the tag does not have a knowledge 315 /// about it's children, its up to the caller to write the close brace '}' after it 316 /// writes the children to the sink. 317 void toAson(S, string indent = " ")(S sink) if(isOutputRange!(S,const(char)[])) { 318 //writefln("[DEBUG] converting to sdl namespace=%s name=%s values=%s attr=%s", 319 //namespace, name, values.data, attributes.data); 320 for(auto i = 0; i < depth; i++) { 321 sink.put(indent); 322 } 323 if(namespace.length) { 324 sink.put(namespace); 325 sink.put(":"); 326 } 327 if(!isAnonymous || (values.data.length == 0 && attributes.data.length == 0)) 328 sink.put(name); 329 foreach(value; values.data) { 330 sink.put(" "); 331 sink.put(value); 332 } 333 foreach(attribute; attributes.data) { 334 sink.put(" "); 335 if(attribute.namespace.length) { 336 sink.put(attribute.namespace); 337 sink.put(":"); 338 } 339 sink.put(attribute.id); 340 sink.put("="); 341 sink.put(attribute.value); 342 } 343 if(hasOpenBrace) { 344 sink.put(" {\n"); 345 } else { 346 sink.put("\n"); 347 } 348 } 349 350 351 352 353 // 354 // User Methods 355 // 356 void throwIsUnknown() { 357 throw new AsonParseException(line, format("unknown tag '%s'", name)); 358 } 359 void throwIsDuplicate() { 360 throw new AsonParseException(line, format("tag '%s' appeared more than once", name)); 361 } 362 void getOneValue(T)(ref T value) { 363 if(values.data.length != 1) { 364 throw new AsonParseException 365 (line,format("tag '%s' %s 1 value but had %s", 366 name, (values.data.length == 0) ? "must have at least" : "can only have", values.data.length)); 367 } 368 369 const(char)[] literal = values.data[0]; 370 371 372 static if( isSomeString!T ) { 373 374 if(!value.empty) throwIsDuplicate(); 375 376 } else static if( isIntegral!T || isFloatingPoint!T ) { 377 378 //if( value != 0 ) throwIsDuplicate(); 379 380 } else { 381 382 } 383 384 if(!sdlLiteralToD!(T)(literal, value)) throw new AsonParseException(line, format("cannot convert '%s' to %s", literal, typeid(T))); 385 } 386 387 void getValues(T, bool allowAppend=false)(ref T[] t, size_t minCount = 1) { 388 if(values.data.length < minCount) throw new AsonParseException(line, format("tag '%s' must have at least %s value(s)", name, minCount)); 389 390 size_t arrayOffset; 391 if(t.ptr is null) { 392 arrayOffset = 0; 393 t = new T[values.data.length]; 394 } else if(allowAppend) { 395 arrayOffset = t.length; 396 t.length += values.data.length; 397 } else throwIsDuplicate(); 398 399 foreach(literal; values.data) { 400 static if( isSomeString!T ) { 401 if(literal[0] != '"') throw new AsonParseException(line, format("tag '%s' must have exactly one string literal but had another literal type", name)); 402 t[arrayOffset++] = literal[1..$-1]; // remove surrounding quotes 403 } else { 404 assert(0, format("Cannot convert sdl literal to D '%s' type", typeid(T))); 405 } 406 } 407 } 408 409 410 void enforceNoValues() { 411 if(values.data.length) throw new AsonParseException(line, format("tag '%s' cannot have any values", name)); 412 } 413 void enforceNoAttributes() { 414 if(attributes.data.length) throw new AsonParseException(line, format("tag '%s' cannot have any attributes", name)); 415 } 416 void enforceNoChildren() { 417 if(hasOpenBrace) throw new AsonParseException(line, format("tag '%s' cannot have any children", name)); 418 } 419 420 421 } 422 423 version = use_lookup_tables; 424 /+ 425 bool isIDStart(dchar c) { 426 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; 427 /* 428 The lookup table doesn't seem to be as fast here, maybe this case I should just compare the ranges 429 version(use_lookup_tables) { 430 return (c < sdlLookup.length) ? ((sdlLookup[c] & idStartFlag) != 0) : false; 431 } else { 432 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; 433 } 434 */ 435 } 436 bool isID(dchar c) { 437 version(use_lookup_tables) { 438 return (c < sdlLookup.length) ? ((sdlLookup[c] & sdlIDFlag) != 0) : false; 439 } else { 440 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' || c == '$'; 441 } 442 } 443 +/ 444 enum tooManyCloseBraces = "too many ending braces '}'"; 445 enum noEndingQuote = "string missing ending quote"; 446 enum invalidBraceFmt = "found '{' on a different line than its tag '%s'. fix the sdl by moving '{' to the same line"; 447 enum mixedValuesAndAttributesFmt = "SDL values cannot appear after attributes, bring '%s' in front of the attributes for tag '%s'"; 448 enum notEnoughCloseBracesFmt = "reached end of ASON but missing %s close brace(s) '}'"; 449 450 451 452 /// Converts literal to the given D type T. 453 /// This is a wrapper arround the $(D sdlLiteralToD) function that returns true on sucess, except 454 /// this function returns the value itself and throws an AsonParseException on error. 455 T sdlLiteralToD(T)(const(char)[] literal) { 456 T value; 457 if(!sdlLiteralToD!(T)(literal, value)) 458 throw new AsonParseException(format("failed to convert '%s' to a %s", literal, typeid(T))); 459 return value; 460 } 461 462 /// Converts literal to the given D type T. 463 /// If isSomeString!T, then it will remove the surrounding quotes if they are present. 464 /// Returns: true on succes, false on failure 465 bool sdlLiteralToD(T)(const(char)[] literal, ref T t) { 466 467 assert(literal.length); 468 469 470 static if( is( T == bool) ) { 471 472 if(literal == "true" || literal == "on" || literal == "1") t = true; 473 if(literal == "false" || literal == "off" || literal == "0") t = false; 474 475 } else static if( isSomeString!T ) { 476 477 if(literal[0] == '"' && literal.length > 1 && literal[$-1] == '"') { 478 t = cast(T)literal[1..$-1]; 479 } else { 480 t = cast(T)literal; 481 } 482 483 } else static if( isIntegral!T || isFloatingPoint!T ) { 484 485 // remove any postfix characters 486 while(true) { 487 char c = literal[$-1]; 488 if(c >= '0' && c <= '9') break; 489 literal.length--; 490 if(literal.length == 0) return false; 491 } 492 493 t = to!T(literal); 494 495 } else { 496 497 t = to!T(literal); 498 499 } 500 501 return true; 502 } 503 504 505 506 507 508 string arrayRange(char min, char max, string initializer) { 509 string initializers = ""; 510 for(char c = min; c < max; c++) { 511 initializers ~= "'"~c~"': "~initializer~",\n"; 512 } 513 initializers ~= "'"~max~"': "~initializer; 514 return initializers; 515 } 516 string rangeInitializers(string[] s...) { 517 if(s.length % 2 != 0) assert(0, "must supply an even number of arguments to rangeInitializers"); 518 string code = "["~rangeInitializersCurrent(s); 519 //assert(0, code); // uncomment to see the code 520 return code; 521 } 522 string rangeInitializersCurrent(string[] s) { 523 string range = s[0]; 524 if(range.length == 3) { 525 return range ~ ":" ~ s[1] ~ rangeInitializersNext(s); 526 } 527 char min = range[1]; 528 char max = range[5]; 529 return arrayRange(min, max, s[1]) ~ rangeInitializersNext(s); 530 } 531 string rangeInitializersNext(string[] s...) { 532 if(s.length <= 2) return "]"; 533 return ","~rangeInitializersCurrent(s[2..$]); 534 } 535 536 537 538 539 enum ubyte controlCharacter = 0x01; 540 enum ubyte whitespace = 0x02; 541 private __gshared ubyte[256] asonLookup = 542 [ 543 ' ' : whitespace, 544 '\t' : whitespace, 545 '\n' : whitespace, 546 '\v' : whitespace, 547 '\f' : whitespace, 548 '\r' : whitespace, 549 550 '{' : controlCharacter, 551 '}' : controlCharacter, 552 '[' : controlCharacter, 553 ']' : controlCharacter, 554 '<' : controlCharacter, 555 '>' : controlCharacter, 556 ';' : controlCharacter, 557 ',' : controlCharacter, 558 '"' : controlCharacter, 559 '\'' : controlCharacter, 560 '\\' : controlCharacter, 561 '#' : controlCharacter, 562 '/' : controlCharacter, 563 '*' : controlCharacter, 564 //'=' : controlCharacter, 565 ]; 566 /+ 567 version(use_lookup_tables) { 568 mixin("private __gshared ubyte[256] asonLookup = "~rangeInitializers 569 ("'_'" , "sdlIDFlag", 570 571 "'a'" , "sdlIDFlag", 572 "'b'" , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 573 "'c'" , "sdlIDFlag", 574 "'d'" , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 575 "'e'" , "sdlIDFlag", 576 "'f'" , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 577 "'g'-'k'", "sdlIDFlag", 578 "'l'" , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 579 "'m'-'z'", "sdlIDFlag", 580 581 "'A'" , "sdlIDFlag", 582 "'B'" , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 583 "'C'" , "sdlIDFlag", 584 "'D'" , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 585 "'E'" , "sdlIDFlag", 586 "'F'" , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 587 "'G'-'K'", "sdlIDFlag", 588 "'L'" , "sdlIDFlag | sdlNumberFlag | sdlNumberPostfixFlag", 589 "'M'-'Z'", "sdlIDFlag", 590 591 "'0'-'9'", "sdlIDFlag | sdlNumberFlag", 592 "'-'" , "sdlIDFlag", 593 "'.'" , "sdlIDFlag | sdlNumberFlag", 594 "'$'" , "sdlIDFlag", 595 )~";"); 596 } 597 +/ 598 /// A convenience function to parse a single tag. 599 /// Calls $(D tag.resetForReuse) and then calls $(D parseAsonTag). 600 /+ 601 void parseOneAsonTag(Tag* tag, char[] sdlText) { 602 tag.resetForReuse(); 603 if(!parseAsonTag(tag, &sdlText)) throw new AsonParseException(tag.line, format("The sdl text '%s' did not contain any tags", sdlText)); 604 } 605 +/ 606 607 608 609 610 struct AsonParser 611 { 612 const char* start; 613 const char* limit; 614 615 this(char[] ason) { 616 this.start = ason.ptr; 617 this.limit = this.start + ason.length; 618 } 619 620 621 } 622 623 version(unittest_ason) unittest 624 { 625 AsonParser parser = AsonParser(setupAsonText("name Joe", false)); 626 627 628 629 630 631 } 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 struct AsonOptions 670 { 671 ubyte flags; 672 673 @property @safe bool preserveAsonText() pure nothrow const { return (flags & 1U) != 0;} 674 @property @safe void perserveAsonText(bool v) pure nothrow { if (v) flags |= 1U;else flags &= ~1U;} 675 676 @property @safe bool copyStrings() pure nothrow const { return (flags & 2U) != 0;} 677 @property @safe void copyStrings(bool v) pure nothrow { if (v) flags |= 2U;else flags &= ~2U;} 678 679 @property @safe bool asonIsList() pure nothrow const { return (flags & 128U) != 0;} 680 @property @safe void asonIsList(bool v) pure nothrow { if (v) flags |= 128U;else flags &= ~128U;} 681 } 682 683 684 685 686 687 688 /+ 689 690 void parseAsonInto(T)(AsonOptions options, string sdl) 691 { 692 options.perserveAsonText = true; 693 parseAsonInto(options, cast(char[])sdl); 694 695 } 696 void parseAsonInto(T)(AsonOptions options, char[] sdl) 697 { 698 699 700 TAG_LOOP: 701 while(walker.pop(depth)) { 702 703 debug writefln("[DEBUG] parseAsonInto: at depth %s tag '%s'", tag.depth, tag.name); 704 705 foreach(memberIndex, copyOfMember; obj.tupleof) { 706 707 alias typeof(T.tupleof[memberIndex]) memberType; 708 enum memberString = T.tupleof[memberIndex].stringof; 709 710 //writefln("[DEBUG] tag '%s' checking member '%s %s'", tag.name, memberType.stringof, memberString); 711 712 alias TypeTuple!(__traits(getAttributes, T.tupleof[memberIndex])) memberAttributes; 713 alias ElementType!(memberType) memberElementType; 714 enum isAppender = is( memberType == Appender!(AppenderElementType!(memberType)[])); 715 716 static if(memberString == "this") { 717 718 mixin(debugAsonReflection(T.stringof, memberString, "ignored because 'this' is always ignored")); 719 720 } else static if(containsFlag!(AsonReflection.ignore, memberAttributes)) { 721 722 mixin(debugAsonReflection(T.stringof, memberString, "ignored from AsonReflection.ignore")); 723 724 } else static if( is( memberType == function) ) { 725 726 mixin(debugAsonReflection(T.stringof, memberString, "ignored because it is a function")); 727 728 } else static if( isAppender || ( !is( memberElementType == void ) && !isSomeString!(memberType) ) ) { 729 730 731 static if(isAppender) { 732 mixin(debugAsonReflection(T.stringof, memberString, "deserialized as a list, specifically an appender")); 733 734 template addValues(string memberName) { 735 void addValues() { 736 auto elementOffset = __traits(getMember, obj, memberString).data.length; 737 __traits(getMember, obj, memberString).reserve(elementOffset + tag.values.data.length); 738 AppenderElementType!(memberType) deserializedValue; 739 740 foreach(value; tag.values.data) { 741 if(!sdlLiteralToD!(AppenderElementType!(memberType))( value, deserializedValue)) { 742 throw new AsonParseException(tag.line, format("failed to convert '%s' to %s for appender %s.%s", 743 value, memberElementType.stringof, T.stringof, memberString) ); 744 } 745 __traits(getMember, obj, memberString).put(deserializedValue); 746 elementOffset++; 747 } 748 } 749 } 750 751 } else { 752 753 mixin(debugAsonReflection(T.stringof, memberString, "deserialized as a list, specifically an array")); 754 755 template addValues(string memberName) { 756 void addValues() { 757 auto elementOffset = __traits(getMember, obj, memberString).length; 758 759 __traits(getMember, obj, memberString).length += tag.values.data.length; 760 foreach(value; tag.values.data) { 761 if(!sdlLiteralToD!(memberElementType)( value, __traits(getMember, obj, memberString)[elementOffset] ) ) { 762 throw new AsonParseException(tag.line, format("failed to convert '%s' to %s for array member %s.%s", 763 value, memberElementType.stringof, T.stringof, memberString) ); 764 } 765 elementOffset++; 766 } 767 } 768 } 769 770 } 771 772 static if(containsFlag!(AsonReflection.onlySingularTags, memberAttributes)) { 773 mixin(debugAsonReflection(T.stringof, memberString, format("onlySingularTags so will not handle tags named '%s'", memberString), true)); 774 } else { 775 776 if(tag.name == memberString) { 777 778 tag.enforceNoAttributes(); 779 780 // 781 // Add tag values to the array 782 // 783 static if( !is( ElementType!(memberElementType) == void ) && !isSomeString!(memberElementType) ) { 784 785 implement("list of arrays"); 786 787 } else static if( isAssociativeArray!(memberElementType)) { 788 789 implement("list of assoc-arrays"); 790 791 } else static if( is ( isNested!( memberType ) ) ) { 792 793 implement("list of functions/structs/classes"); 794 795 } else { 796 797 if(tag.values.data.length > 0) { 798 addValues!(memberString); 799 } 800 801 } 802 803 804 if(tag.hasOpenBrace) { 805 806 size_t arrayDepth = tag.depth + 1; 807 while(walker.pop(arrayDepth)) { 808 809 tag.enforceNoAttributes(); 810 // Check if the tag can be converted to an array element 811 if(!tag.isAnonymous) { 812 throw new AsonParseException(tag.line, format("the child elements of array member %s.%s can only use anonymous tags, but found a tag with name '%s'", 813 T.stringof, memberString, tag.name)); 814 } 815 816 817 static if( !isSomeString!(memberElementType) && isArray!(memberElementType)) { 818 819 implement("using children for list of arrays"); 820 821 } else static if( isAssociativeArray!(memberElementType)) { 822 823 implement("using children for list of assoc-arrays"); 824 825 } else static if( is ( isNested!(memberType) ) ) { 826 827 implement("using children for list of functions/structs/classes"); 828 829 } else { 830 831 if(tag.values.data.length > 0) { 832 833 addValues!(memberString); 834 835 } 836 837 } 838 839 840 } 841 842 } 843 844 continue TAG_LOOP; 845 } 846 } 847 848 static if(containsFlag!(AsonReflection.noSingularTags, memberAttributes) ) { 849 mixin(debugAsonReflection(T.stringof, memberString, "does not handle singular tags", true)); 850 } else { 851 static if(singularName!(T, memberString) is null) { 852 static assert(0, format("Could not determine the singular name for %s.%s because it does not end with an 's'. Use @(AsonSingularName(\"name\") to specify one.", 853 T.stringof, memberString)); 854 } 855 856 mixin(debugAsonReflection(T.stringof, memberString, format("handles singular tags named '%s'", singularName!(T, memberString)), true)); 857 858 859 if(tag.name == singularName!(T, memberString)) { 860 861 tag.enforceNoAttributes(); 862 tag.enforceNoChildren(); 863 864 static if( isArray!(memberElementType) && 865 !isSomeString!(memberElementType) ) { 866 867 implement("singular list of arrays"); 868 869 } else static if( isAssociativeArray!(memberElementType)) { 870 871 implement("singular list of assoc-arrays"); 872 873 } else static if( is ( isNested!(memberType) ) ) { 874 875 implement("singular list of functions/structs/classes"); 876 877 } else { 878 879 static if ( isAppender ) { 880 AppenderElementType!(memberType) value; 881 } else { 882 memberElementType value; 883 } 884 885 tag.getOneValue(value); 886 __traits(getMember, obj, memberString) ~= value; 887 debug writefln("[DEBUG] parseAsonInto: %s.%s was appended with '%s'", T.stringof, memberString, __traits(getMember, obj, memberString)); 888 continue TAG_LOOP; 889 890 } 891 892 } 893 894 } 895 896 // END OF HANDLING OUTPUT RANGES 897 898 } else static if( isAssociativeArray!(memberType)) { 899 900 mixin(debugAsonReflection(T.stringof, memberString, "deserialized as an associative array")); 901 implement("associative arrays"); 902 903 } else static if( is (isNested!(memberType))) { 904 905 mixin(debugAsonReflection(T.stringof, memberString, "deserialized as an object")); 906 implement("sub function/struct/class"); 907 908 } else { 909 910 mixin(debugAsonReflection(T.stringof, memberString, "deserialized as a single value")); 911 912 if(tag.name == memberString) { 913 tag.enforceNoAttributes(); 914 tag.enforceNoChildren(); 915 tag.getOneValue(__traits(getMember, obj, memberString)); 916 debug writefln("[DEBUG] parseAsonInto: set %s.%s to '%s'", T.stringof, memberString, __traits(getMember, obj, memberString)); 917 continue TAG_LOOP; 918 } 919 920 921 } 922 923 } 924 925 tag.throwIsUnknown(); 926 } 927 928 } 929 930 931 932 version(unittest_ason) unittest 933 { 934 mixin(scopedTest!("AsonReflection")); 935 936 void testParseType(T)(bool copyAson, string sdlText, T expectedType) 937 { 938 T parsedType; 939 940 try { 941 942 parseAsonInto!T(parsedType, setupAsonText(sdlText, copyAson)); 943 944 } catch(Exception e) { 945 writefln("the following sdl threw an unexpected exception: %s", sdlText); 946 writeln(e); 947 assert(0); 948 } 949 950 stdout.flush(); 951 if(expectedType != parsedType) { 952 writefln("Expected: %s", expectedType); 953 writefln(" but got: %s", parsedType); 954 assert(0); 955 } 956 957 } 958 959 struct TypeWithAppender 960 { 961 auto values = appender!(int[])(); 962 this(int[] values...) { 963 foreach(value; values) { 964 this.values.put(value); 965 } 966 } 967 } 968 testParseType(false,` 969 values 1 2 3 970 values { 971 4 5 6 7 972 8 9 10 11 973 } 974 value 12 975 value 13 976 `, TypeWithAppender(1,2,3,4,5,6,7,8,9,10,11,12,13)); 977 978 struct PackageInfo 979 { 980 @(AsonReflection.ignore) 981 string ignoreThisMember; 982 983 string name; 984 private string privateName; 985 uint packageID; 986 987 uint[] randomUints; 988 989 string[] authors; 990 991 @(AsonReflection.noSingularTags) 992 string[] sourceFiles; 993 994 995 @(AsonSingularName("dependency")) 996 string[string][] dependencies; 997 998 @(AsonReflection.onlySingularTags) 999 @(AsonSingularName("a-float")) 1000 float[] myFloats; 1001 1002 1003 1004 void reset() { 1005 name = null; 1006 packageID = 0; 1007 randomUints = null; 1008 authors = null; 1009 sourceFiles = null; 1010 dependencies = null; 1011 } 1012 } 1013 1014 1015 1016 testParseType(false, ` 1017 name "vibe-d" 1018 privateName "secret" 1019 packageID 1023 1020 randomUints 1 2 3 4 1021 `, PackageInfo(null, "vibe-d", "secret", 1023, [1,2,3,4])); 1022 1023 testParseType(false, ` 1024 randomUint 1 1025 randomUints 2 3 4 5 1026 randomUints { 1027 99 8291 1028 83992 1029 } 1030 randomUint 9983`, PackageInfo(null, null, null, 0, [1,2,3,4,5,99,8291,83992,9983])); 1031 1032 testParseType(false, ` 1033 authors "Jimbo" 1034 authors "Spencer" "Dylan" 1035 authors { 1036 "Jay" 1037 "Amy" "Steven" 1038 } 1039 author "SingleAuthor" 1040 `, PackageInfo(null, null, null, 0, null, ["Jimbo", "Spencer", "Dylan", "Jay", "Amy", "Steven", "SingleAuthor"])); 1041 1042 1043 testParseType(false,` 1044 a-float 0 1045 // a-float 1 2 # should be an error 1046 // a-float # should be an error 1047 // myFloats 1 2 3 # should be an error 1048 a-float 2.3829 1049 a-float -192 1050 `, PackageInfo(null, null, null, 0, null, null, null, null, [0, 2.3829, -192])); 1051 1052 1053 1054 1055 } 1056 1057 +/ 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 void parseAson(AsonOptions options, string sdl) 1090 { 1091 options.perserveAsonText = true; 1092 parseAson(options, cast(char[])sdl); 1093 } 1094 1095 1096 /// Parses one SDL tag (not including its children) from sdlText saving slices for every 1097 /// name/value/attribute to the given tag struct. 1098 /// This function assumes that sdlText contains at least one full SDL _tag. 1099 /// The only time this function will allocate memory is if the value/attribute appenders 1100 /// in the tag struct are not large enough to hold all the values. 1101 /// Because of this, after the tag values/attributes are populated, it is up to the caller to copy 1102 /// any memory they wish to save unless sdlText is going to persist in memory. 1103 /// Note: this function does not handle the UTF-8 bom because it doesn't make sense to re-check 1104 /// for the BOM after every tag. 1105 /// Params: 1106 /// tag = An address to a Tag structure to save the sdl information. 1107 /// sdlText = An address to the sdl text character array. 1108 /// the function will move the front of the slice foward past 1109 /// any sdl that was parsed. 1110 /// Returns: true if a tag was found, false otherwise 1111 /// Throws: AsonParseException or Utf8Exception 1112 void parseAson(AsonOptions options, char[] sdlText) 1113 { 1114 size_t line = 1; 1115 1116 // developer note: 1117 // whenever reading the next character, the next pointer must be saved to cpos 1118 // if the character could be used later, but if the next is guaranteed to 1119 // be thrown away (such as when skipping till the next newline after a comment) 1120 // then cpos does not need to be saved. 1121 1122 char *next = sdlText.ptr; 1123 char *limit = next + sdlText.length; 1124 1125 size_t depth = 0; 1126 1127 //options.startParser(); // Start the parse 1128 1129 char* cpos; 1130 dchar c; 1131 1132 char[] token; 1133 1134 void enforceNoMoreObjects() { 1135 if(depth > 0) throw new AsonParseException(line, format(notEnoughCloseBracesFmt, depth)); 1136 } 1137 1138 void readNext() 1139 { 1140 cpos = next; 1141 c = decodeUtf8(next, limit); 1142 } 1143 1144 bool isWhitespaceOrControl() 1145 { 1146 return c < asonLookup.length && ( ( (asonLookup[c] & whitespace ) != 0) || 1147 ( (asonLookup[c] & controlCharacter) != 0) ); 1148 } 1149 1150 1151 // Expected State: 1152 // next: points to the next character (could be the newline) 1153 // Return State: 1154 // next: points to the next character after the newline, or at limit 1155 void toNextLine() 1156 { 1157 while(true) { 1158 if(next >= limit) { return; } 1159 c = decodeUtf8(next, limit); // no need to save cpos since c will be thrown away 1160 if(c == '\n') { line++; return; } 1161 } 1162 } 1163 1164 // ExpectedState: 1165 // c/cpos: points to the first character of the potential whitespace/comment 1166 // ReturnState: 1167 // c/cpos: points to the first character after all the whitespace/comments 1168 void skipWhitespaceAndComments() 1169 { 1170 while(true) { 1171 1172 // TODO: maybe use a lookup table here 1173 if(c == ' ' || c == '\t' || c =='\v' || c == '\f' || c == '\r') { 1174 1175 // do nothing (check first as this is the most likely case) 1176 1177 } else if(c == '\n') { 1178 1179 line++; 1180 1181 } else if(c == '#') { 1182 1183 toNextLine(); 1184 1185 } else if(c == '/') { 1186 1187 if(next >= limit) return; 1188 1189 dchar secondChar = decodeUtf8(next, limit); 1190 1191 if(secondChar == '/') { 1192 1193 toNextLine(); 1194 1195 } else if(secondChar == '*') { 1196 1197 1198 MULTILINE_COMMENT_LOOP: 1199 while(next < limit) { 1200 1201 c = decodeUtf8(next, limit); // no need to save cpos since c will be thrown away 1202 if(c == '\n') { 1203 line++; 1204 } else if(c == '*') { 1205 // loop assume c is pointing to a '*' and next is pointing to the next characer 1206 while(next < limit) { 1207 1208 c = decodeUtf8(next, limit); 1209 if(c == '/') break MULTILINE_COMMENT_LOOP; 1210 if(c == '\n') { 1211 line++; 1212 } else if(c != '*') { 1213 break; 1214 } 1215 } 1216 } 1217 } 1218 1219 } else { 1220 1221 return; 1222 1223 } 1224 1225 } else { 1226 1227 return; 1228 1229 } 1230 1231 // Goto next character 1232 if(next >= limit) {cpos = next; break;} 1233 readNext(); 1234 } 1235 1236 return; 1237 } 1238 1239 1240 // Expectd State: 1241 // c/cpos: the first character of a name 1242 // Return State: 1243 // c/cpos: the first non-whitespace character after the name 1244 // token: contains the name 1245 void parseName() 1246 { 1247 auto startOfString = cpos; 1248 1249 if(c == '\'') { 1250 implement("single-quoted strings"); 1251 } else if(c == '"') { 1252 implement("double-quoted strings"); 1253 } else { 1254 1255 while(true) { 1256 if(next >= limit) { 1257 cpos = next; 1258 break; 1259 } 1260 c = decodeUtf8(next, limit); 1261 if(isWhitespaceOrControl()) break; 1262 } 1263 1264 token = startOfString[0..cpos-startOfString]; 1265 1266 } 1267 } 1268 1269 1270 // Expectd State: 1271 // c/cpos: the first character of a name 1272 // next: points to character after cpos 1273 // Return State: 1274 // c/cpos: the first non-whitespace character after the name-value section 1275 void parseNameValues() 1276 { 1277 parseName(); 1278 } 1279 1280 1281 if(next >= limit) return; 1282 readNext(); 1283 skipWhitespaceAndComments(); 1284 if(cpos >= limit) return; 1285 1286 if(options.asonIsList) { 1287 1288 implement("ason root level list"); 1289 1290 } else { 1291 1292 // Skip optional open brace 1293 if(c == '{') { 1294 if(next >= limit) return; // TODO: should this be an error? 1295 readNext(); 1296 skipWhitespaceAndComments(); 1297 } 1298 1299 parseNameValues(); 1300 1301 if(cpos >= limit) return; // TODO: should this be an error? 1302 1303 // Skip optional end brace 1304 if(c == '}') { 1305 if(next >= limit) return; // TODO: should this be an error if no open brace was specified? 1306 skipWhitespaceAndComments(); 1307 } 1308 1309 if(cpos < limit) throw new AsonParseException(line, "expected end of input but found '%s'", cpos[0]); 1310 } 1311 1312 } 1313 1314 version(unittest_ason) 1315 { 1316 char[2048] asonBuffer; 1317 char[asonBuffer.length] asonBuffer2; 1318 char[] setupAsonText(const(char[]) asonText, bool copyAson) 1319 { 1320 if(!copyAson) return cast(char[])asonText; 1321 1322 if(asonText.length >= asonBuffer.length) throw new Exception(format("attempting to copy ason of length %s but asonBuffer is only of length %s", asonText.length, asonBuffer.length)); 1323 asonBuffer[0..asonText.length] = asonText; 1324 return asonBuffer[0..asonText.length]; 1325 } 1326 1327 struct AsonBuffer2Sink 1328 { 1329 size_t offset; 1330 @property 1331 char[] slice() { return asonBuffer2[0..offset]; } 1332 void put(inout(char)[] value) { 1333 asonBuffer2[offset..offset+value.length] = value; 1334 offset += value.length; 1335 } 1336 } 1337 1338 } 1339 1340 version(unittest_ason) unittest 1341 { 1342 AsonOptions options; 1343 1344 void testParseAson(bool reparse = true)(bool copyAson, const(char)[] asonText, /*Ason expectedAson, */size_t line = __LINE__) 1345 { 1346 auto escapedAsonText = escape(asonText); 1347 1348 debug { 1349 static if(reparse) { 1350 writefln("[TEST] testing ason '%s'", escapedAsonText); 1351 } else { 1352 writefln("[TEST] testing ason (regenerated)'%s'", escapedAsonText); 1353 } 1354 } 1355 1356 char[] next = setupAsonText(asonText, copyAson); 1357 1358 //parsedTag.resetForNewAson(); 1359 1360 1361 try { 1362 1363 parseAson(options, next); 1364 /+ 1365 // put the tag into the buffer2 sink to reparse again after 1366 static if(reparse) { 1367 parsedTag.toAson(&buffer2Sink); 1368 previousDepth = parsedTag.depth; 1369 if(parsedTag.hasOpenBrace) previousDepth++; 1370 } 1371 1372 if(parseAsonTag(&parsedTag, &next)) { 1373 writefln("Expected %s tag(s) but got at least one more (depth=%s, name='%s')", 1374 expectedTags.length, parsedTag.depth, parsedTag.name); 1375 writefln("Error: test on line %s", line); 1376 assert(0); 1377 } 1378 +/ 1379 } catch(AsonParseException e) { 1380 writefln("[TEST] this ason threw an unexpected AsonParseException: '%s'", escape(asonText)); 1381 writeln(e); 1382 writefln("Error: test on line %s", line); 1383 assert(0); 1384 } catch(Exception e) { 1385 writefln("[TEST] this ason threw an unexpected Exception: '%s'", escape(asonText)); 1386 writeln(e); 1387 writefln("Error: test on line %s", line); 1388 assert(0); 1389 } 1390 /+ 1391 static if(reparse) { 1392 if(previousDepth != size_t.max) { 1393 while(previousDepth > parsedTag.depth) { 1394 buffer2Sink.put("}"); 1395 previousDepth--; 1396 } 1397 } 1398 1399 if(buffer2Sink.slice != asonText && 1400 (buffer2Sink.slice.length && buffer2Sink.slice[0..$-1] != asonText)) { 1401 testParseAson!false(false, buffer2Sink.slice, expectedTags, line); 1402 } 1403 } 1404 +/ 1405 } 1406 1407 testParseAson(false, "name Johnny"); 1408 testParseAson(false, "{\"name\":\"Johnny\"}"); 1409 testParseAson(false, "{name:\"Johnny\"}"); 1410 testParseAson(false, "{name:Johnny}"); 1411 testParseAson(false, "{name Johnny}"); 1412 } 1413 1414 1415 1416 1417 1418 /+ 1419 1420 version(unittest_ason) unittest 1421 { 1422 //return; // Uncomment to disable these tests 1423 1424 mixin(scopedTest!"AsonParse"); 1425 1426 Tag parsedTag; 1427 1428 void useProposed() { 1429 debug writefln("[TEST] AsonMode: Proposed"); 1430 parsedTag.useProposedAson(); 1431 } 1432 void useStrict() { 1433 debug writefln("[TEST] AsonMode: Strict"); 1434 parsedTag.useStrictAson(); 1435 } 1436 1437 1438 struct AsonTest 1439 { 1440 bool copyAson; 1441 string sdlText; 1442 Tag[] expectedTags; 1443 this(bool copyAson, string sdlText, Tag[] expectedTags...) { 1444 this.copyAson = copyAson; 1445 this.sdlText = sdlText; 1446 this.expectedTags = expectedTags; 1447 } 1448 } 1449 1450 void testParseAson(bool reparse = true)(bool copyAson, const(char)[] sdlText, Tag[] expectedTags...) 1451 { 1452 size_t previousDepth = size_t.max; 1453 AsonBuffer2Sink buffer2Sink; 1454 1455 auto escapedAsonText = escape(sdlText); 1456 1457 debug { 1458 static if(reparse) { 1459 writefln("[TEST] testing sdl : %s", escapedAsonText); 1460 } else { 1461 writefln("[TEST] testing sdl (regenerated): %s", escapedAsonText); 1462 } 1463 } 1464 1465 char[] next = setupAsonText(sdlText, copyAson); 1466 1467 parsedTag.resetForReuse(); 1468 1469 1470 try { 1471 1472 for(auto i = 0; i < expectedTags.length; i++) { 1473 if(!parseAsonTag(&parsedTag, &next)) { 1474 writefln("Expected %s tag(s) but only got %s", expectedTags.length, i); 1475 assert(0); 1476 } 1477 1478 static if(reparse) { 1479 if(previousDepth != size_t.max) { 1480 while(previousDepth > parsedTag.depth) { 1481 buffer2Sink.put("}"); 1482 previousDepth--; 1483 } 1484 } 1485 } 1486 1487 auto expectedTag = expectedTags[i]; 1488 if(parsedTag.namespace != expectedTag.namespace) { 1489 writefln("Error: expected tag namespace '%s' but got '%s'", expectedTag.namespace, parsedTag.namespace); 1490 assert(0); 1491 } 1492 if(parsedTag.name != expectedTag.name) { 1493 writefln("Error: expected tag name '%s' but got '%s'", expectedTag.name, parsedTag.name); 1494 assert(0); 1495 } 1496 //writefln("[DEBUG] expected value '%s', actual values '%s'", expectedTag.values.data, parsedTag.values.data); 1497 if(parsedTag.values.data != expectedTag.values.data) { 1498 writefln("Error: expected tag values '%s' but got '%s'", expectedTag.values.data, parsedTag.values.data); 1499 assert(0); 1500 } 1501 if(parsedTag.attributes.data != expectedTag.attributes.data) { 1502 writefln("Error: expected tag attributes '%s' but got '%s'", expectedTag.attributes.data, parsedTag.attributes.data); 1503 assert(0); 1504 } 1505 1506 // put the tag into the buffer2 sink to reparse again after 1507 static if(reparse) { 1508 parsedTag.toAson(&buffer2Sink); 1509 previousDepth = parsedTag.depth; 1510 if(parsedTag.hasOpenBrace) previousDepth++; 1511 } 1512 } 1513 1514 if(parseAsonTag(&parsedTag, &next)) { 1515 writefln("Expected %s tag(s) but got at least one more (depth=%s, name='%s')", 1516 expectedTags.length, parsedTag.depth, parsedTag.name); 1517 assert(0); 1518 } 1519 1520 } catch(AsonParseException e) { 1521 writefln("[TEST] this sdl threw an unexpected AsonParseException: '%s'", escape(sdlText)); 1522 writeln(e); 1523 assert(0); 1524 } catch(Exception e) { 1525 writefln("[TEST] this sdl threw an unexpected Exception: '%s'", escape(sdlText)); 1526 writeln(e); 1527 assert(0); 1528 } 1529 1530 static if(reparse) { 1531 if(previousDepth != size_t.max) { 1532 while(previousDepth > parsedTag.depth) { 1533 buffer2Sink.put("}"); 1534 previousDepth--; 1535 } 1536 } 1537 1538 if(buffer2Sink.slice != sdlText && 1539 (buffer2Sink.slice.length && buffer2Sink.slice[0..$-1] != sdlText)) { 1540 testParseAson!false(false, buffer2Sink.slice, expectedTags); 1541 } 1542 } 1543 1544 } 1545 1546 void testInvalidAson(bool copyAson, const(char)[] sdlText, AsonErrorType expectedErrorType = AsonErrorType.unknown) { 1547 auto escapedAsonText = escape(sdlText); 1548 debug writefln("[TEST] testing invalid sdl '%s'", escapedAsonText); 1549 1550 AsonErrorType actualErrorType = AsonErrorType.unknown; 1551 1552 char[] next = setupAsonText(sdlText, copyAson); 1553 1554 parsedTag.resetForReuse(); 1555 try { 1556 while(parseAsonTag(&parsedTag, &next)) { } 1557 writefln("Error: invalid sdl was successfully parsed: %s", sdlText); 1558 assert(0); 1559 } catch(AsonParseException e) { 1560 debug writefln("[TEST] got expected error: %s", e.msg); 1561 actualErrorType = e.type; 1562 } catch(Utf8Exception e) { 1563 debug writefln("[TEST] got expected error: %s", e.msg); 1564 } 1565 1566 if(expectedErrorType != AsonErrorType.unknown && 1567 expectedErrorType != actualErrorType) { 1568 writefln("expected error '%s' but got error '%s'", expectedErrorType, actualErrorType); 1569 assert(0); 1570 } 1571 1572 } 1573 1574 testParseAson(false, ""); 1575 testParseAson(false, " "); 1576 testParseAson(false, "\n"); 1577 1578 testParseAson(false, "#Comment"); 1579 testParseAson(false, "#Comment copyright \u00a8"); 1580 testParseAson(false, "#Comment\n"); 1581 testParseAson(false, "#Comment\r\n"); 1582 testParseAson(false, " # Comment\r\n"); 1583 1584 testParseAson(false, " -- Comment\n"); 1585 testParseAson(false, " ------ Comment\n"); 1586 1587 testParseAson(false, " # Comment1 \r\n -- Comment 2"); 1588 1589 1590 testParseAson(false, " // Comment\n"); 1591 testParseAson(false, " //// Comment\n"); 1592 1593 testParseAson(false, "/* a multiline comment \n\r\n\n\n\t hello stuff # -- // */"); 1594 1595 // TODO: test this using the allowBracesAfterNewline option 1596 // testParseAson(false, "tag /*\n\n*/{ child }", Tag("tag"), Tag("child")); 1597 1598 1599 testParseAson(false, "a", Tag("a")); 1600 testParseAson(false, "ab", Tag("ab")); 1601 testParseAson(false, "abc", Tag("abc")); 1602 testParseAson(false, "firsttag", Tag("firsttag")); 1603 testParseAson(false, "funky._-$tag", Tag("funky._-$tag")); 1604 1605 1606 { 1607 auto prefixes = ["", " ", "\t", "--comment\n"]; 1608 foreach(prefix; prefixes) { 1609 testInvalidAson(false, prefix~":"); 1610 } 1611 } 1612 1613 auto namespaces = ["a:", "ab:", "abc:"]; 1614 bool isProposedAson = false; 1615 while(true) { 1616 string tagName; 1617 if(isProposedAson) { 1618 tagName = null; 1619 useProposed(); 1620 } else { 1621 tagName = "content"; 1622 } 1623 foreach(namespace; namespaces) { 1624 testParseAson(false, namespace, Tag(namespace~tagName)); 1625 testParseAson(false, namespace~" ", Tag(namespace~tagName)); 1626 testParseAson(false, namespace~"\t", Tag(namespace~tagName)); 1627 testParseAson(false, namespace~"\n", Tag(namespace~tagName)); 1628 testParseAson(false, namespace~";", Tag(namespace~tagName)); 1629 testParseAson(false, namespace~`"value"`, Tag(namespace~tagName, `"value"`)); 1630 //testParseAson(false, namespace~`attr=null`, Tag(namespace~tagName, "attr=null")); 1631 } 1632 if(isProposedAson) break; 1633 isProposedAson = true; 1634 } 1635 useStrict(); 1636 1637 1638 testParseAson(false, "a:a", Tag("a:a")); 1639 testParseAson(false, "ab:a", Tag("ab:a")); 1640 1641 testParseAson(false, "a:ab", Tag("a:ab")); 1642 testParseAson(false, "ab:ab", Tag("ab:ab")); 1643 1644 testParseAson(false, "html:table", Tag("html:table")); 1645 1646 testParseAson(false, ";", Tag("content")); 1647 testParseAson(false, "myid;", Tag("myid")); 1648 testParseAson(false, "myid; ", Tag("myid")); 1649 testParseAson(false, "myid #comment", Tag("myid")); 1650 testParseAson(false, "myid # comment \n", Tag("myid")); 1651 testParseAson(false, "myid -- comment \n # more comments\n", Tag("myid")); 1652 1653 1654 testParseAson(false, "myid /* multiline comment */", Tag("myid")); 1655 testParseAson(false, "myid /* multiline comment */ ", Tag("myid")); 1656 testParseAson(false, "myid /* multiline comment */\n", Tag("myid")); 1657 testParseAson(false, "myid /* multiline comment \n\n */", Tag("myid")); 1658 testParseAson(false, "myid /* multiline comment **/ \"value\"", Tag("myid", `"value"`)); 1659 testParseAson(false, "myid /* multiline comment \n\n */another-id", Tag("myid"), Tag("another-id")); 1660 testParseAson(false, "myid /* multiline comment */ \"value\"", Tag("myid", `"value"`)); 1661 testParseAson(false, "myid /* multiline comment \n */ \"value\"", Tag("myid"), Tag("content", `"value"`)); 1662 testInvalidAson(false, "myid /* multiline comment \n */ { \n }"); 1663 useProposed(); 1664 testParseAson(false, "myid /* multiline comment */ { \n }", Tag("myid")); 1665 testParseAson(false, "myid /* multiline comment \n */ \"value\"", Tag("myid"), Tag(null, `"value"`)); 1666 useStrict(); 1667 1668 1669 testParseAson(false, "tag1\ntag2", Tag("tag1"), Tag("tag2")); 1670 testParseAson(false, "tag1;tag2\ntag3", Tag("tag1"), Tag("tag2"), Tag("tag3")); 1671 1672 testInvalidAson(false, "myid {"); 1673 testInvalidAson(false, "myid {\n\n"); 1674 1675 testInvalidAson(false, "{}"); 1676 1677 testParseAson(false, "tag1{}", Tag("tag1")); 1678 testParseAson(false, "tag1{}tag2", Tag("tag1"), Tag("tag2")); 1679 testParseAson(false, "tag1{}\ntag2", Tag("tag1"), Tag("tag2")); 1680 1681 testParseAson(false, "tag1{tag1.1}tag2", Tag("tag1"), Tag("tag1.1"), Tag("tag2")); 1682 1683 testParseAson(false, `tag"value"`, Tag("tag", `"value"`)); 1684 1685 1686 // 1687 // Handling the backslash '\' character 1688 // 1689 testInvalidAson(false, "\\"); // slash must in the context of a tag 1690 testInvalidAson(false, `tag \ x`); 1691 1692 testParseAson(false, "tag\\", Tag("tag")); // Make sure this is valid sdl 1693 testParseAson(false, "tag \\ \n \\ \n \"hello\"", Tag("tag", `"hello"`)); 1694 1695 // 1696 // Test the keywords (white box tests trying to attain full code coverage) 1697 // 1698 auto keywords = ["null", "true", "false", "on", "off"]; 1699 1700 foreach(keyword; keywords) { 1701 testParseAson(false, keyword, Tag("content", keyword)); 1702 } 1703 1704 namespaces = ["", "n:", "namespace:"]; 1705 foreach(namespace; namespaces) { 1706 sdlBuffer[0..namespace.length] = namespace; 1707 auto afterTagName = namespace.length + 4; 1708 sdlBuffer[namespace.length..afterTagName] = "tag "; 1709 string expectedTagName = namespace~"tag"; 1710 1711 foreach(keyword; keywords) { 1712 for(auto cutoff = 1; cutoff < keyword.length; cutoff++) { 1713 sdlBuffer[afterTagName..afterTagName+cutoff] = keyword[0..cutoff]; 1714 testInvalidAson(false, sdlBuffer[0..afterTagName+cutoff]); 1715 } 1716 } 1717 auto suffixes = [";", " \t;", "\n", "{}", " \t {\n }"]; 1718 foreach(keyword; keywords) { 1719 auto limit = afterTagName+keyword.length; 1720 1721 sdlBuffer[afterTagName..limit] = keyword; 1722 testParseAson(false, sdlBuffer[0..limit], Tag(expectedTagName, keyword)); 1723 1724 foreach(suffix; suffixes) { 1725 sdlBuffer[limit..limit+suffix.length] = suffix; 1726 testParseAson(false, sdlBuffer[0..limit+suffix.length], Tag(expectedTagName, keyword)); 1727 } 1728 } 1729 foreach(keyword; keywords) { 1730 1731 foreach(attrNamespace; namespaces) { 1732 1733 for(auto cutoff = 1; cutoff <= keyword.length; cutoff++) { 1734 auto limit = afterTagName + attrNamespace.length; 1735 sdlBuffer[afterTagName..limit] = attrNamespace; 1736 limit += cutoff; 1737 sdlBuffer[limit - cutoff..limit] = keyword[0..cutoff]; 1738 sdlBuffer[limit..limit+8] = `="value"`; 1739 testParseAson(false, sdlBuffer[0..limit+8], Tag(expectedTagName, format(`%s%s="value"`, attrNamespace, keyword[0..cutoff]))); 1740 1741 foreach(otherKeyword; keywords) { 1742 sdlBuffer[limit+1..limit+1+otherKeyword.length] = otherKeyword; 1743 testParseAson(false, sdlBuffer[0..limit+1+otherKeyword.length], 1744 Tag(expectedTagName, format("%s%s=%s", attrNamespace, keyword[0..cutoff], otherKeyword))); 1745 } 1746 } 1747 1748 } 1749 1750 } 1751 } 1752 1753 1754 1755 1756 // 1757 // String Literals 1758 // 1759 testParseAson(false, `a "apple"`, Tag("a", `"apple"`)); 1760 testParseAson(false, "a \"pear\"\n", Tag("a", `"pear"`)); 1761 testParseAson(false, "a \"left\"\nb \"right\"", Tag("a", `"left"`), Tag("b", `"right"`)); 1762 testParseAson(false, "a \"cat\"\"dog\"\"bear\"\n", Tag("a", `"cat"`, `"dog"`, `"bear"`)); 1763 testParseAson(false, "a \"tree\";b \"truck\"\n", Tag("a", `"tree"`), Tag("b", `"truck"`)); 1764 1765 // 1766 // Attributes 1767 // 1768 testParseAson(false, "tag attr=null", Tag("tag", "attr=null")); 1769 testParseAson(false, "tag \"val\" attr=null", Tag("tag", `"val"`, "attr=null")); 1770 1771 auto mixedValuesAndAttributesTests = [ 1772 AsonTest(false, "tag attr=null \"val\"", Tag("tag", "attr=null", `"val"`)) ]; 1773 1774 foreach(test; mixedValuesAndAttributesTests) { 1775 testInvalidAson(test.copyAson, test.sdlText, AsonErrorType.mixedValuesAndAttributes); 1776 } 1777 useProposed(); 1778 foreach(test; mixedValuesAndAttributesTests) { 1779 testParseAson(test.copyAson, test.sdlText, test.expectedTags); 1780 } 1781 useStrict(); 1782 1783 // 1784 // Test parsing numbers without extracting them 1785 // 1786 enum numberPostfixes = ["", "l", "L", "f", "F", "d", "D", "bd", "BD"]; 1787 { 1788 enum sdlPostfixes = ["", " ", ";", "\n"]; 1789 1790 auto numbers = ["0", "12", "9876", "5432", /*".1",*/ "0.1", "12.4", /*"1.",*/ "8.04", "123.l"]; 1791 1792 1793 for(size_t negative = 0; negative < 2; negative++) { 1794 string prefix = negative ? "-" : ""; 1795 1796 foreach(postfix; numberPostfixes) { 1797 foreach(number; numbers) { 1798 1799 auto testNumber = prefix~number~postfix; 1800 1801 if(postfix.length) { 1802 useProposed(); 1803 testInvalidAson(false, "tag "~testNumber); 1804 useStrict(); 1805 } 1806 //testInvalidAson(false, "tag "~testNumber~"="); 1807 1808 foreach(sdlPostfix; sdlPostfixes) { 1809 testParseAson(false, "tag "~testNumber~sdlPostfix, Tag("tag", testNumber)); 1810 } 1811 } 1812 } 1813 1814 1815 } 1816 } 1817 1818 // 1819 // Test parsing numbers and extracting them 1820 // 1821 { 1822 for(size_t negative = 0; negative < 2; negative++) { 1823 string prefix = negative ? "-" : ""; 1824 1825 foreach(postfix; numberPostfixes) { 1826 1827 void testNumber(Types...)(ulong expectedValue) { 1828 long expectedSignedValue = negative ? -1 * (cast(long)expectedValue) : cast(long)expectedValue; 1829 1830 foreach(Type; Types) { 1831 if(negative && isUnsigned!Type) continue; 1832 if(expectedSignedValue > Type.max) continue; 1833 static if( is(Type == float) || is(Type == double) || is(Type == real)) { 1834 if(expectedSignedValue < Type.min_normal) continue; 1835 } else { 1836 if(expectedSignedValue < Type.min) continue; 1837 } 1838 1839 debug writefln("[DEBUG] testing %s on %s", typeid(Type), parsedTag.values.data[0]); 1840 Type t; 1841 parsedTag.getOneValue(t); 1842 assert(t == cast(Type) expectedSignedValue, format("Expected (%s) %s but got %s", typeid(Type), expectedSignedValue, t)); 1843 } 1844 } 1845 void testDecimalNumber(Types...)(real expectedValue) { 1846 foreach(Type; Types) { 1847 if(negative && isUnsigned!Type) continue; 1848 if(expectedValue > Type.max) continue; 1849 static if( is(Type == float) || is(Type == double) || is(Type == real)) { 1850 if(expectedValue < Type.min_normal) continue; 1851 } else { 1852 if(expectedValue < Type.min) continue; 1853 } 1854 1855 debug writefln("[DEBUG] testing %s on %s", typeid(Type), parsedTag.values.data[0]); 1856 Type t; 1857 parsedTag.getOneValue(t); 1858 assert(t - cast(Type) expectedValue < .01, format("Expected (%s) %s but got %s", typeid(Type), cast(Type)expectedValue, t)); 1859 } 1860 } 1861 1862 alias testNumber!(byte,ubyte,short,ushort,int,uint,long,ulong,float,double,real) testNumberOnAllTypes; 1863 alias testDecimalNumber!(float,double,real) testDecimalNumberOnAllTypes; 1864 1865 parseOneAsonTag(&parsedTag, cast(char[])"tag "~prefix~"0"~postfix); 1866 testNumberOnAllTypes(0); 1867 1868 parseOneAsonTag(&parsedTag, cast(char[])"tag "~prefix~"1"~postfix); 1869 testNumberOnAllTypes(1); 1870 1871 parseOneAsonTag(&parsedTag, cast(char[])"tag "~prefix~"12"~postfix); 1872 testNumberOnAllTypes(12); 1873 1874 parseOneAsonTag(&parsedTag, cast(char[])"tag "~prefix~"9987"~postfix); 1875 testNumberOnAllTypes(9987); 1876 1877 parseOneAsonTag(&parsedTag, cast(char[])"tag "~prefix~"0.0"~postfix); 1878 testDecimalNumberOnAllTypes(0.0); 1879 1880 parseOneAsonTag(&parsedTag, cast(char[])"tag "~prefix~".1"~postfix); 1881 testDecimalNumberOnAllTypes(0.1); 1882 1883 parseOneAsonTag(&parsedTag, cast(char[])"tag "~prefix~".000001"~postfix); 1884 testDecimalNumberOnAllTypes(0.000001); 1885 1886 parseOneAsonTag(&parsedTag, cast(char[])"tag "~prefix~"100384.999"~postfix); 1887 testDecimalNumberOnAllTypes(100384.999); 1888 1889 parseOneAsonTag(&parsedTag, cast(char[])"tag "~prefix~"3.14159265"~postfix); 1890 testDecimalNumberOnAllTypes(3.14159265); 1891 } 1892 } 1893 1894 } 1895 1896 1897 // 1898 // Children 1899 // 1900 testInvalidAson(false, "{}"); // no line can start with a curly brace 1901 1902 auto braceAfterNewlineTests = [ 1903 AsonTest(false, "tag\n{ child\n}", Tag("tag"), Tag("child")), 1904 AsonTest(false, "colors \"hot\" \n{ yellow\n}", Tag("colors", `"hot"`), Tag("yellow")) ]; 1905 1906 foreach(test; braceAfterNewlineTests) { 1907 testInvalidAson(test.copyAson, test.sdlText, AsonErrorType.braceAfterNewline); 1908 } 1909 useProposed(); 1910 foreach(test; braceAfterNewlineTests) { 1911 testParseAson(test.copyAson, test.sdlText, test.expectedTags); 1912 } 1913 useStrict(); 1914 1915 // 1916 // Odd corner cases for this implementation 1917 // 1918 testParseAson(false, "tag null;", Tag("tag", "null")); 1919 testParseAson(false, "tag null{}", Tag("tag", "null")); 1920 testParseAson(false, "tag true;", Tag("tag", "null")); 1921 testParseAson(false, "tag true{}", Tag("tag", "null")); 1922 testParseAson(false, "tag false;", Tag("tag", "null")); 1923 testParseAson(false, "tag false{}", Tag("tag", "null")); 1924 1925 1926 // TODO: testing using all keywords as namespaces true:id, etc. 1927 testParseAson(false, "tag null:null=\"value\";", Tag("tag", "null:null=\"value\"")); 1928 testParseAson(false, "null", Tag("content", "null")); 1929 1930 1931 1932 // 1933 // Full Parses 1934 // 1935 testParseAson(false, ` 1936 name "joe" 1937 children { 1938 name "jim" 1939 }`, Tag("name", `"joe"`), Tag("children"), Tag("name", `"jim"`)); 1940 1941 testParseAson(false, ` 1942 parent name="jim" { 1943 child "hasToys" name="joey" { 1944 # just a comment here for now 1945 } 1946 }`, Tag("parent", "name=\"jim\""), Tag("child", "name=\"joey\"", `"hasToys"`)); 1947 1948 1949 testParseAson(false,`html:table { 1950 html:tr { 1951 html:th "Name" 1952 html:th "Age" 1953 html:th "Pet" 1954 } 1955 html:tr { 1956 html:td "Brian" 1957 html:td 34 1958 html:td "Puggy" 1959 } 1960 tr { 1961 td "Jackie" 1962 td 27 1963 td null 1964 } 1965 }`, Tag("html:table"), 1966 Tag("html:tr"), 1967 Tag("html:th", `"Name"`), 1968 Tag("html:th", `"Age"`), 1969 Tag("html:th", `"Pet"`), 1970 Tag("html:tr"), 1971 Tag("html:td", `"Brian"`), 1972 Tag("html:td", `34`), 1973 Tag("html:td", `"Puggy"`), 1974 Tag("tr"), 1975 Tag("td", `"Jackie"`), 1976 Tag("td", `27`), 1977 Tag("td", `null`)); 1978 1979 1980 } 1981 +/ 1982 1983 /// Assists in walking an SDL tree which supports the StAX method of parsing. 1984 /// Examples: 1985 /// --- 1986 /// Tag tag; 1987 /// AsonWalker walker = AsonWalker(&tag, sdl); 1988 /// while(walker.pop()) { 1989 /// // use tag to process the current tag 1990 /// 1991 /// auto depth = tag.childrenDepth(); 1992 /// while(walker.pop(depth)) { 1993 /// // process tag again as a child tag 1994 /// } 1995 /// 1996 /// } 1997 /// --- 1998 /+ 1999 struct AsonWalker 2000 { 2001 /// A pointer to the tag structure that will be populated after parsing every tag. 2002 Tag* tag; 2003 2004 // The sdl text that has yet to be parsed. 2005 private char[] sdl; 2006 2007 // Used for when a child walker has popped a parent tag 2008 bool tagAlreadyPopped; 2009 2010 this(Tag* tag, char[] sdl) { 2011 this.tag = tag; 2012 this.sdl = sdl; 2013 } 2014 2015 /// Parses the next tag at the given depth. 2016 /// Returns: true if it parsed a tag at the given depth and false if there are no more 2017 /// tags at the given depth. If it is depth 0 it means the sdl has been fully parsed. 2018 /// Throws: Exception if the current tag has children and they were not parsed 2019 /// and allowSkipChildren is set to false. 2020 bool pop(size_t depth = 0, bool allowSkipChildren = false) { 2021 if(tagAlreadyPopped) { 2022 if(depth < tag.depth) throw new Exception("possible code bug here?"); 2023 if(tag.depth == depth) { 2024 tagAlreadyPopped = false; 2025 return true; 2026 } 2027 } 2028 2029 while(true) { 2030 size_t previousDepth; 2031 const(char)[] previousName; 2032 2033 if(!allowSkipChildren) { 2034 previousDepth = tag.depth; 2035 previousName = tag.name; 2036 } 2037 2038 if(!parseAsonTag(this.tag, &sdl)) { 2039 assert(tag.depth == 0, format("code bug: parseAsonTag returned end of input but tag.depth was %s (not 0)", tag.depth)); 2040 return false; 2041 } 2042 2043 if(this.tag.depth == depth) return true; 2044 2045 // Check if it is the end of this set of children 2046 if(this.tag.depth < depth) { 2047 tagAlreadyPopped = true; 2048 return false; 2049 } 2050 2051 if(!allowSkipChildren) throw new Exception(format("forgot to call children on tag '%s' at depth %s", previousName, previousDepth)); 2052 } 2053 } 2054 2055 public size_t childrenDepth() { return tag.depth + 1; } 2056 2057 2058 } 2059 +/ 2060 2061 /+ 2062 void parseAson(T, bool ignoreUnknown = false)(T t, inout(char)[] sdl) { 2063 inout(char)* start = sdl.ptr; 2064 inout(char)* limit = start + sdl.length; 2065 parseAson!(T)(t, start, limit); 2066 } 2067 void parseAson(T, bool ignoreUnknown = false)(ref T t, const(char)* start, const char* limit) { 2068 Tag tag; 2069 2070 writefln("Parsing sdl struct with the following members:"); 2071 foreach(member; __traits(allMembers, T)) { 2072 writefln(" %s", member); 2073 } 2074 2075 2076 TAG_LOOP: 2077 while(parseAsonTag(&tag, start, limit)) { 2078 2079 writefln("parseAson: (depth %s) tag '%s'%s", tag.depth, tag.name, 2080 tag.hasOpenBrace ? " (has children)" : ""); 2081 2082 2083 foreach(member; __traits(allMembers, T)) { 2084 if(tag.name == member) { 2085 writefln("matched member '%s'", member); 2086 continue TAG_LOOP; 2087 } 2088 } 2089 2090 static if(ignoreUnknown) { 2091 writefln("parseAson: error: no match for tag '%s'", tag.name); 2092 } else { 2093 throw new AsonParseException(tag.line, format("unknown tag '%s'", tag.name)); 2094 } 2095 2096 } 2097 2098 } 2099 +/ 2100 /+ 2101 version(unittest_ason) 2102 { 2103 2104 struct Dependency { 2105 string name; 2106 string version_; 2107 } 2108 // Example of parsing a configuration file 2109 struct Package { 2110 const(char)[] name; 2111 const(char)[] description; 2112 2113 const(char)[][] authors; 2114 auto dependencies = appender!(Dependency[])(); 2115 auto subPackages = appender!(Package[])(); 2116 2117 void reset() { 2118 name = null; 2119 description = null; 2120 authors = null; 2121 dependencies.clear(); 2122 subPackages.clear(); 2123 } 2124 bool opEquals(ref const Package p) { 2125 return 2126 name == p.name && 2127 description == p.description && 2128 authors == p.authors && 2129 dependencies.data == p.dependencies.data && 2130 subPackages.data == p.subPackages.data; 2131 } 2132 void parseAsonPackage(bool copyAson, string sdlText) { 2133 parseAsonPackage(setupAsonText(sdlText, copyAson)); 2134 } 2135 void parseAsonPackage(char[] sdlText) { 2136 Tag tag; 2137 auto sdl = AsonWalker(&tag, sdlText); 2138 while(sdl.pop()) { 2139 2140 debug writefln("[sdl] (depth %s) tag '%s'%s", tag.depth, tag.name, 2141 tag.hasOpenBrace ? "(has children)" : ""); 2142 2143 if(tag.name == "name") { 2144 2145 tag.enforceNoAttributes(); 2146 tag.enforceNoChildren(); 2147 tag.getOneValue(this.name); 2148 2149 } else if(tag.name == "description") { 2150 2151 tag.enforceNoAttributes(); 2152 tag.enforceNoChildren(); 2153 tag.getOneValue(this.description); 2154 2155 } else if(tag.name == "authors") { 2156 2157 if(this.authors !is null) tag.throwIsDuplicate(); 2158 tag.enforceNoAttributes(); 2159 tag.enforceNoChildren(); 2160 tag.getValues(this.authors); 2161 2162 } else tag.throwIsUnknown(); 2163 2164 } 2165 2166 } 2167 } 2168 2169 2170 } 2171 +/ 2172 2173 version(unittest_ason) unittest 2174 { 2175 /+ 2176 mixin(scopedTest!"AsonWalker"); 2177 2178 void testPackage(bool copyAson, string sdlText, ref Package expectedPackage) 2179 { 2180 Package parsedPackage; 2181 2182 parsedPackage.parseAsonPackage(copyAson, sdlText); 2183 2184 if(expectedPackage != parsedPackage) { 2185 writefln("Expected package: %s", expectedPackage); 2186 writefln(" but got package: %s", parsedPackage); 2187 assert(0); 2188 } 2189 } 2190 2191 string sdl; 2192 Package expectedPackage; 2193 2194 expectedPackage = Package("my-package", "an example sdl package", 2195 ["Jonathan", "David", "Amy"]); 2196 2197 testPackage(false, ` 2198 name "my-package" 2199 description "an example sdl package" 2200 authors "Jonathan" "David" "Amy" 2201 `, expectedPackage); 2202 2203 sdl = ` 2204 name "my-package" 2205 description "an example sdl package" 2206 2207 authors "Jonathan" "David" "Amy" 2208 `; 2209 2210 +/ 2211 } 2212 2213 2214 version(unittest_ason) unittest 2215 { 2216 /+ 2217 mixin(scopedTest!"AsonWalkerOnPerson"); 2218 2219 struct Person { 2220 const(char)[] name; 2221 ushort age; 2222 const(char)[][] nicknames; 2223 Person[] children; 2224 void reset() { 2225 name = null; 2226 age = 0; 2227 nicknames = null; 2228 children.clear(); 2229 } 2230 bool opEquals(ref const Person p) { 2231 return 2232 name == p.name && 2233 age == p.age && 2234 nicknames == p.nicknames && 2235 children == p.children; 2236 } 2237 string toString() { 2238 return format("Person(\"%s\", %s, %s, %s)", name, age, nicknames, children); 2239 } 2240 void validate() { 2241 if(name is null) throw new Exception("person is missing the 'name' tag"); 2242 if(age == 0) throw new Exception("person is missing the 'age' tag"); 2243 } 2244 void parseFromAson(ref AsonWalker walker) { 2245 auto tag = walker.tag; 2246 2247 tag.enforceNoValues(); 2248 tag.enforceNoAttributes(); 2249 2250 reset(); 2251 2252 auto childBuilder = appender!(Person[])(); 2253 2254 auto depth = walker.childrenDepth(); 2255 while(walker.pop(depth)) { 2256 2257 //writefln("[sdl] (depth %s) tag '%s'%s", tag.depth, tag.name, 2258 //tag.hasOpenBrace ? "(has children)" : ""); 2259 //stdout.flush(); 2260 2261 if(tag.name == "name") { 2262 2263 tag.enforceNoAttributes(); 2264 tag.enforceNoChildren(); 2265 tag.getOneValue(name); 2266 2267 } else if(tag.name == "age") { 2268 2269 tag.enforceNoAttributes(); 2270 tag.enforceNoChildren(); 2271 tag.getOneValue(age); 2272 2273 } else if(tag.name == "nicknames") { 2274 2275 tag.enforceNoAttributes(); 2276 tag.enforceNoChildren(); 2277 tag.getValues(nicknames); 2278 2279 } else if(tag.name == "child") { 2280 2281 Person child = Person(); 2282 child.parseFromAson(walker); 2283 childBuilder.put(child); 2284 2285 } else tag.throwIsUnknown(); 2286 2287 } 2288 2289 this.children = childBuilder.data.dup; 2290 childBuilder.clear(); 2291 validate(); 2292 } 2293 } 2294 2295 Appender!(Person[]) parsePeople(char[] sdl) { 2296 auto people = appender!(Person[])(); 2297 Person person; 2298 2299 Tag tag; 2300 auto walker = AsonWalker(&tag, sdl); 2301 while(walker.pop()) { 2302 if(tag.name == "person") { 2303 2304 person.parseFromAson(walker); 2305 people.put(person); 2306 2307 } else tag.throwIsUnknown(); 2308 } 2309 2310 return people; 2311 } 2312 2313 void testParsePeople(bool copyAson, string sdlText, Person[] expectedPeople...) 2314 { 2315 Appender!(Person[]) parsedPeople; 2316 try { 2317 2318 parsedPeople = parsePeople(setupAsonText(sdlText, copyAson)); 2319 2320 } catch(Exception e) { 2321 writefln("the following sdl threw an unexpected exception: %s", sdlText); 2322 writeln(e); 2323 assert(0); 2324 } 2325 2326 if(expectedPeople.length != parsedPeople.data.length) { 2327 writefln("Expected: %s", expectedPeople); 2328 writefln(" but got: %s", parsedPeople.data); 2329 assert(0); 2330 } 2331 for(auto i = 0; i < expectedPeople.length; i++) { 2332 Person expectedPerson = expectedPeople[i]; 2333 if(expectedPerson != parsedPeople.data[i]) { 2334 writefln("Expected: %s", expectedPeople); 2335 writefln(" but got: %s", parsedPeople.data); 2336 assert(0); 2337 } 2338 } 2339 2340 } 2341 2342 auto childBuilder = appender!(Person[])(); 2343 2344 2345 childBuilder.clear(); 2346 childBuilder.put(Person("Jack", 6, ["Little Jack"])); 2347 2348 testParsePeople(false, ` 2349 person { 2350 name "Robert" 2351 age 29 2352 nicknames "Bob" "Bobby" 2353 child { 2354 name "Jack" 2355 age 6 2356 nicknames "Little Jack" 2357 } 2358 child { 2359 name "Sally" 2360 age 8 2361 } 2362 }`, Person("Robert", 29, ["Bob", "Bobby"], [Person("Jack", 6, ["Little Jack"]),Person("Sally", 8)])); 2363 2364 +/ 2365 2366 } 2367