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