1 /++ 2 Ion Text Deserialization API 3 4 Heavily influenced (and compatible) with upstream Ion implementations (compatible with ion-go) 5 Authors: Harrison Ford 6 +/ 7 module mir.deser.text; 8 import mir.deser.text.readers; 9 import mir.deser.text.skippers; 10 import mir.deser.text.tokenizer; 11 import mir.deser.text.tokens; 12 import mir.ion.symbol_table; 13 import mir.ion.type_code; 14 import mir.ion.tape; 15 import mir.ser.ion; 16 import mir.serde; 17 import mir.format; // Quoted symbol support 18 import mir.ion.internal.data_holder; 19 import mir.ion.internal.stage3 : IonErrorInfo; 20 import mir.bignum.integer; 21 import std.traits : hasUDA, getUDAs; 22 import mir.exception: toMutable; 23 24 private mixin template Stack(T, size_t maxDepth = 1024) 25 { 26 private 27 { 28 immutable maxDepthReachedMsg = "max depth on stack reached"; 29 immutable cannotPopNoElementsMsg = "cannot pop from stack with 0 elements"; 30 version(D_Exceptions) { 31 immutable maxDepthException = new Exception(maxDepthReachedMsg); 32 immutable cannotPopNoElementsException = new Exception(cannotPopNoElementsMsg); 33 } 34 T[maxDepth] stack; 35 size_t stackLength; 36 } 37 38 T peekStack() @safe @nogc pure 39 { 40 if (stackLength == 0) return T.init; 41 return stack[stackLength - 1]; 42 } 43 44 void pushStack(T element) @safe @nogc pure 45 { 46 if (stackLength + 1 > maxDepth) { 47 version(D_Exceptions) 48 throw maxDepthException.toMutable; 49 else 50 assert(0, maxDepthReachedMsg); 51 } 52 stack[stackLength++] = element; 53 } 54 55 T popStackBack() @safe @nogc pure 56 { 57 if (stackLength <= 0) { 58 version (D_Exceptions) 59 throw cannotPopNoElementsException.toMutable; 60 else 61 assert(0, cannotPopNoElementsMsg); 62 } 63 T code = stack[stackLength]; 64 stack[--stackLength] = T.init; 65 return code; 66 } 67 } 68 69 private enum State { 70 beforeAnnotations, 71 beforeFieldName, 72 beforeContainer, 73 afterValue, 74 EOF 75 } 76 77 // UDA 78 private struct S 79 { 80 State state; 81 bool transition = false; 82 bool disableAfterValue = false; 83 } 84 85 /++ 86 Deserializer for the Ion Text format 87 +/ 88 struct IonTextDeserializer(Serializer) 89 { 90 mixin Stack!(IonTypeCode); 91 private Serializer* ser; 92 private State state; 93 private IonTokenizer t; 94 95 /++ 96 Constructor 97 Params: 98 ser = A pointer to a serializer 99 +/ 100 this(Serializer* ser) @safe pure 101 { 102 this.ser = ser; 103 this.state = State.beforeAnnotations; 104 } 105 106 /++ 107 This function starts the deserializing process, and attempts to fully read through 108 the text provided until it reaches the end. 109 Params: 110 text = The text to deserialize 111 +/ 112 void opCall(scope const(char)[] text) @trusted pure 113 { 114 t = IonTokenizer(text); 115 while (!t.isEOF()) 116 { 117 auto ntr = t.nextToken(); 118 assert(ntr, "hit eof when tokenizer says we're not at an EOF??"); 119 120 switch (state) with (State) 121 { 122 case afterValue: 123 case beforeFieldName: 124 case beforeAnnotations: 125 handleState(state); 126 break; 127 default: 128 version(D_Exceptions) 129 throw IonDeserializerErrorCode.unexpectedState.ionDeserializerException; 130 else 131 assert(0, "unexpected state"); 132 } 133 } 134 } 135 136 private: 137 138 static void __bar() 139 { 140 typeof(*typeof(this).init.ser).init.putAnnotation("symbolText"); 141 } 142 143 static assert(__traits(compiles, (){__bar();})); 144 145 // import std.traits: 146 static if (__traits(compiles, ()@nogc{__bar();})) 147 { 148 void handleState(State s) @safe pure @nogc { handleStateImpl(s); } 149 void handleToken(IonTokenType t) @safe pure @nogc { handleTokenImpl(t); } 150 } 151 else 152 { 153 void handleState(State s) @safe pure { handleStateImpl(s); } 154 void handleToken(IonTokenType t) @safe pure { handleTokenImpl(t); } 155 } 156 157 private void handleStateImpl(State s) @safe pure 158 { 159 switch (s) { 160 // Cannot use getSymbolsByUDA as it leads to recursive template instantiations 161 static foreach(member; __traits(allMembers, typeof(this))) { 162 static foreach(registeredState; getUDAs!(__traits(getMember, this, member), S)) 163 static if (!registeredState.transition) { 164 case registeredState.state: 165 __traits(getMember, this, member)(); 166 static if (!registeredState.disableAfterValue) 167 state = handleStateTransition(State.afterValue); 168 return; 169 } 170 } 171 default: { 172 version (D_Exceptions) 173 throw IonDeserializerErrorCode.unexpectedState.ionDeserializerException; 174 else 175 assert(0, "Unexpected state"); 176 } 177 } 178 } 179 180 private State handleStateTransition(State s) @safe pure 181 { 182 switch (s) 183 { 184 static foreach(member; __traits(allMembers, typeof(this))) 185 static foreach(registeredState; getUDAs!(__traits(getMember, this, member), S)) 186 static if (registeredState.transition) { 187 case registeredState.state: 188 return __traits(getMember, this, member); 189 } 190 default: 191 version (D_Exceptions) 192 throw IonDeserializerErrorCode.unexpectedState.ionDeserializerException; 193 else 194 assert(0, "Unexpected state"); 195 } 196 } 197 198 private void handleTokenImpl(IonTokenType t) @safe pure 199 { 200 switch (t) 201 { 202 static foreach(member; __traits(allMembers, typeof(this))) 203 static foreach(registeredToken; getUDAs!(__traits(getMember, this, member), IonTokenType)) { 204 case registeredToken: 205 return __traits(getMember, this, member); 206 } 207 default: 208 version (D_Exceptions) 209 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 210 else 211 assert(0, "Unexpected token"); 212 } 213 } 214 215 /* State / state transition handlers */ 216 217 @S(State.beforeAnnotations) 218 bool handleBeforeAnnotations() @safe pure 219 { 220 switch (t.currentToken) with (IonTokenType) 221 { 222 case TokenString: 223 case TokenLongString: 224 case TokenTimestamp: 225 case TokenBinary: 226 case TokenHex: 227 case TokenNumber: 228 case TokenFloatInf: 229 case TokenFloatMinusInf: 230 case TokenFloatNaN: 231 case TokenSymbolQuoted: 232 case TokenSymbol: 233 case TokenSymbolOperator: 234 case TokenDot: 235 case TokenOpenDoubleBrace: 236 case TokenOpenBrace: 237 case TokenOpenBracket: 238 case TokenOpenParen: 239 handleToken(t.currentToken); 240 return true; 241 case TokenEOF: 242 return false; 243 default: 244 version(D_Exceptions) 245 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 246 else 247 assert(0, "unexpected token"); 248 } 249 } 250 251 // Typically called within a struct 252 @S(State.beforeFieldName, false, true) 253 bool handleBeforeFieldName() @safe pure 254 { 255 switch (t.currentToken) with (IonTokenType) 256 { 257 case TokenCloseBrace: // Simply just return if this is empty 258 return true; 259 case TokenString: 260 case TokenLongString: 261 { 262 // This code is very similar to the string handling code, 263 // but we put the data on a scoped buffer rather then using the serializer 264 // to put a string by parts. 265 auto buf = stringBuf; 266 IonTextString v; 267 if (t.currentToken == TokenString) 268 { 269 v = t.readValue!(TokenString); 270 } 271 else 272 { 273 v = t.readValue!(TokenLongString); 274 } 275 276 buf.put(v.matchedText); 277 while (!v.isFinal) 278 { 279 if (t.currentToken == IonTokenType.TokenString) 280 { 281 v = t.readValue!(IonTokenType.TokenString); 282 } 283 else 284 { 285 v = t.readValue!(IonTokenType.TokenLongString); 286 } 287 buf.put(v.matchedText); 288 } 289 290 // At this point, we should've fully read out the contents of the first long string, 291 // so we should check if there's any long strings following this one. 292 if (t.currentToken == IonTokenType.TokenLongString) 293 { 294 while (true) 295 { 296 char c = t.skipWhitespace(); 297 if (c == '\'') 298 { 299 auto cs = t.peekMax(2); 300 if (cs.length == 2 && cs[0] == '\'' && cs[1] == '\'') 301 { 302 t.skipExactly(2); 303 v = t.readValue!(IonTokenType.TokenLongString); 304 buf.put(v.matchedText); 305 while (!v.isFinal) 306 { 307 v = t.readValue!(IonTokenType.TokenLongString); 308 buf.put(v.matchedText); 309 } 310 } 311 else 312 { 313 t.unread(c); 314 break; 315 } 316 } 317 else 318 { 319 t.unread(c); 320 break; 321 } 322 } 323 } 324 325 ser.putKey(buf.data); 326 if (!t.nextToken()) 327 { 328 version(D_Exceptions) 329 throw IonDeserializerErrorCode.unexpectedEOF.ionDeserializerException; 330 else 331 assert(0, "unexpected end of file"); 332 } 333 if (t.currentToken != TokenColon) 334 { 335 version(D_Exceptions) 336 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 337 else 338 assert(0, "unexpected token"); 339 } 340 state = State.beforeAnnotations; 341 return true; 342 } 343 static foreach(tok; [TokenSymbol, TokenSymbolQuoted]) 344 { 345 case tok: 346 { 347 auto val = t.readValue!(tok); 348 349 static if (tok == TokenSymbol) 350 { 351 if (symbolNeedsQuotes(val.matchedText)) 352 { 353 version(D_Exceptions) 354 throw IonDeserializerErrorCode.requiresQuotes.ionDeserializerException; 355 else 356 assert(0, "unquoted symbol requires quotes"); 357 } 358 ser.putKey(val.matchedText); 359 } else { 360 auto buf = stringBuf; 361 buf.put(val.matchedText); 362 while (!val.isFinal) { 363 val = t.readValue!(tok); 364 buf.put(val.matchedText); 365 } 366 ser.putKey(buf.data); 367 } 368 369 if (!t.nextToken()) 370 { 371 version(D_Exceptions) 372 throw IonDeserializerErrorCode.unexpectedEOF.ionDeserializerException; 373 else 374 assert(0, "unexpected end of file"); 375 } 376 377 if (t.currentToken != TokenColon) 378 { 379 version(D_Exceptions) 380 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 381 else 382 assert(0, "unexpected token"); 383 } 384 state = State.beforeAnnotations; 385 return true; 386 } 387 } 388 389 default: 390 version(D_Exceptions) 391 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 392 else 393 assert(0, "unexpected token"); 394 } 395 } 396 397 @S(State.afterValue, false, true) 398 bool handleAfterValue() @safe @nogc pure 399 { 400 switch (t.currentToken) with (IonTokenType) 401 { 402 case TokenComma: 403 auto top = peekStack(); 404 if (top == IonTypeCode.struct_) 405 { 406 state = State.beforeFieldName; 407 } 408 else if (top == IonTypeCode.list) 409 { 410 state = State.beforeAnnotations; 411 } 412 else 413 { 414 version(D_Exceptions) 415 throw IonDeserializerErrorCode.unexpectedState.ionDeserializerException; 416 else 417 assert(0, "unexpected state"); 418 } 419 return false; 420 case TokenCloseBrace: 421 if (peekStack() == IonTypeCode.struct_) 422 { 423 return true; 424 } 425 goto default; 426 case TokenCloseBracket: 427 if (peekStack() == IonTypeCode.list) 428 { 429 return true; 430 } 431 goto default; 432 case TokenCloseParen: 433 if (peekStack() == IonTypeCode.sexp) 434 { 435 return true; 436 } 437 goto default; 438 default: 439 version(D_Exceptions) 440 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 441 else 442 assert(0, "unexpected token"); 443 } 444 } 445 446 @S(State.afterValue, true) 447 State transitionAfterValue() @safe @nogc pure 448 { 449 switch (peekStack()) with (IonTypeCode) 450 { 451 case list: 452 case struct_: 453 return State.afterValue; 454 case sexp: 455 case null_: 456 return State.beforeAnnotations; 457 default: 458 version(D_Exceptions) 459 throw IonDeserializerErrorCode.unexpectedState.ionDeserializerException; 460 else 461 assert(0, "unexpected state"); 462 } 463 } 464 465 /* Individual token handlers */ 466 467 void onNull() @safe pure 468 { 469 auto cs = t.peekMax(1); 470 // Nulls cannot have any whitespace preceding the dot 471 // This is a workaround, as we skip all whitespace checking for the double-colon 472 if (cs.length == 1 && cs[0] == '.' && !isWhitespace(t.input[t.position - 1 .. t.position][0])) 473 { 474 t.skipOne(); 475 if (!t.nextToken()) 476 { 477 version(D_Exceptions) 478 throw IonDeserializerErrorCode.unexpectedEOF.ionDeserializerException; 479 else 480 assert(0, "unexpected end of file"); 481 } 482 483 if (t.currentToken != IonTokenType.TokenSymbol) 484 { 485 version(D_Exceptions) 486 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 487 else 488 assert(0, "unexpected token"); 489 } 490 auto val = t.readValue!(IonTokenType.TokenSymbol); 491 sw: switch (val.matchedText) 492 { 493 static foreach(v; ["null", "bool", "int", "float", "decimal", 494 "timestamp", "symbol", "string", "blob", 495 "clob", "list", "struct", "sexp"]) 496 { 497 case v: 498 static if (v == "null" || v == "bool" || v == "float" || v == "struct") 499 { 500 mixin ("ser.putNull(IonTypeCode." ~ v ~ "_);"); 501 } 502 else static if (v == "int") 503 { 504 ser.putNull(IonTypeCode.uInt); 505 } 506 else 507 { 508 mixin ("ser.putNull(IonTypeCode." ~ v ~ ");"); 509 } 510 break sw; 511 } 512 default: 513 version(D_Exceptions) 514 throw IonDeserializerErrorCode.invalidNullType.ionDeserializerException; 515 else 516 assert(0, "invalid null type specified"); 517 } 518 } 519 else 520 { 521 ser.putValue(null); 522 } 523 } 524 525 @(IonTokenType.TokenOpenBrace) 526 void onStruct() @safe pure 527 { 528 auto s0 = ser.structBegin(); 529 pushStack(IonTypeCode.struct_); 530 state = State.beforeFieldName; 531 t.finished = true; 532 while (t.nextToken()) 533 { 534 if (t.currentToken == IonTokenType.TokenCloseBrace) 535 { 536 t.finished = true; 537 break; 538 } 539 540 handleState(state); 541 } 542 assert(peekStack() == IonTypeCode.struct_, "XXX: should never happen"); 543 popStackBack(); 544 ser.structEnd(s0); 545 } 546 547 @(IonTokenType.TokenOpenBracket) 548 void onList() @safe pure 549 { 550 auto s0 = ser.listBegin(); 551 pushStack(IonTypeCode.list); 552 state = State.beforeAnnotations; 553 t.finished = true; 554 while (t.nextToken()) 555 { 556 if (t.currentToken == IonTokenType.TokenCloseBracket) 557 { 558 t.finished = true; 559 break; 560 } 561 562 ser.elemBegin; handleState(state); 563 } 564 assert(peekStack() == IonTypeCode.list, "XXX: should never happen"); 565 popStackBack(); 566 ser.listEnd(s0); 567 } 568 569 @(IonTokenType.TokenOpenParen) 570 void onSexp() @safe pure 571 { 572 auto s0 = ser.sexpBegin(); 573 pushStack(IonTypeCode.sexp); 574 state = State.beforeAnnotations; 575 t.finished = true; 576 while (t.nextToken()) 577 { 578 if (t.currentToken == IonTokenType.TokenCloseParen) 579 { 580 t.finished = true; 581 break; 582 } 583 584 ser.sexpElemBegin; handleState(state); 585 } 586 assert(peekStack() == IonTypeCode.sexp, "XXX: should never happen"); 587 popStackBack(); 588 ser.sexpEnd(s0); 589 } 590 591 @(IonTokenType.TokenSymbolOperator) 592 @(IonTokenType.TokenDot) 593 void onSymbolOperator() @safe pure 594 { 595 if (peekStack() != IonTypeCode.sexp) 596 { 597 version(D_Exceptions) 598 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 599 else 600 assert(0, "unexpected token"); 601 } 602 onSymbol(); 603 } 604 605 @(IonTokenType.TokenSymbol) 606 @(IonTokenType.TokenSymbolQuoted) 607 void onSymbol() @safe pure 608 { 609 // The use of a scoped buffer is inevitable, as quoted symbols 610 // may contain UTF code points, which we read out separately 611 auto buf = stringBuf; 612 const(char)[] symbolText; 613 614 if (t.currentToken == IonTokenType.TokenSymbol) 615 { 616 IonTextSymbol val = t.readValue!(IonTokenType.TokenSymbol); 617 buf.put(val.matchedText); 618 } 619 else if (t.currentToken == IonTokenType.TokenSymbolOperator || t.currentToken == IonTokenType.TokenDot) 620 { 621 IonTextSymbolOperator val = t.readValue!(IonTokenType.TokenSymbolOperator); 622 buf.put(val.matchedText); 623 } 624 else if (t.currentToken == IonTokenType.TokenSymbolQuoted) 625 { 626 IonTextQuotedSymbol val = t.readValue!(IonTokenType.TokenSymbolQuoted); 627 buf.put(val.matchedText); 628 while (!val.isFinal) 629 { 630 val = t.readValue!(IonTokenType.TokenSymbolQuoted); 631 buf.put(val.matchedText); 632 } 633 } 634 symbolText = buf.data; 635 636 if (t.isDoubleColon()) 637 { 638 if (t.currentToken == IonTokenType.TokenSymbol && symbolNeedsQuotes(symbolText)) 639 { 640 version(D_Exceptions) 641 throw IonDeserializerErrorCode.requiresQuotes.ionDeserializerException; 642 else 643 assert(0, "unquoted symbol requires quotes"); 644 } 645 else if (t.currentToken == IonTokenType.TokenSymbolOperator) 646 { 647 version(D_Exceptions) 648 throw IonDeserializerErrorCode.requiresQuotes.ionDeserializerException; 649 else 650 assert(0, "unquoted symbol requires quotes"); 651 } 652 // we are an annotation -- special handling is needed here 653 // since we've identified this symbol to be an annotation, 654 // we technically *aren't* finished with reading out the value 655 // and should not default to skipping over the ending mark 656 // rather, we should skip any whitespace and find the next token 657 // (which is ensured to be a double-colon) 658 if (!t.nextToken()) 659 { 660 version(D_Exceptions) 661 throw IonDeserializerErrorCode.unexpectedEOF.ionDeserializerException; 662 else 663 assert(0, "unexpected end of file"); 664 } 665 666 if (t.currentToken != IonTokenType.TokenDoubleColon) 667 { 668 version(D_Exceptions) 669 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 670 else 671 assert(0, "unexpected token"); 672 } 673 674 size_t wrapperStart = ser.annotationWrapperBegin(); 675 ser.putAnnotation(symbolText); 676 677 while (t.nextToken()) 678 { 679 // check if the next token read is a candidate for our annotation array 680 if (t.currentToken == IonTokenType.TokenSymbol || t.currentToken == IonTokenType.TokenSymbolQuoted) 681 { 682 buf.reset; 683 if (t.currentToken == IonTokenType.TokenSymbol) 684 { 685 IonTextSymbol val = t.readValue!(IonTokenType.TokenSymbol); 686 buf.put(val.matchedText); 687 } 688 else if (t.currentToken == IonTokenType.TokenSymbolQuoted) 689 { 690 IonTextQuotedSymbol val = t.readValue!(IonTokenType.TokenSymbolQuoted); 691 buf.put(val.matchedText); 692 while (!val.isFinal) 693 { 694 val = t.readValue!(IonTokenType.TokenSymbolQuoted); 695 buf.put(val.matchedText); 696 } 697 } 698 699 // if the symbol we read is followed by a ::, then that means that 700 // this is not the end of our annotation array sequence 701 if (t.isDoubleColon()) 702 { 703 // set finished to false so we don't skip over values, rather skip over whitespace 704 if (!t.nextToken()) 705 { 706 version(D_Exceptions) 707 throw IonDeserializerErrorCode.unexpectedEOF.ionDeserializerException; 708 else 709 assert(0, "unexpected end of file"); 710 } 711 712 if (t.currentToken != IonTokenType.TokenDoubleColon) 713 { 714 version(D_Exceptions) 715 throw IonDeserializerErrorCode.unexpectedToken.ionDeserializerException; 716 else 717 assert(0, "unexpected token"); 718 } 719 ser.putAnnotation(buf.data); 720 } 721 else 722 { 723 // if not, this is where we end 724 auto arrayStart = ser.annotationsEnd(wrapperStart); 725 ser.putSymbol(buf.data); 726 ser.annotationWrapperEnd(arrayStart, wrapperStart); 727 break; 728 } 729 } 730 else 731 { 732 // if the current token is a value type (a non-symbol), then we should also end the annotation array 733 auto arrayStart = ser.annotationsEnd(wrapperStart); 734 handleToken(t.currentToken); 735 ser.annotationWrapperEnd(arrayStart, wrapperStart); 736 break; 737 } 738 } 739 } 740 else 741 { 742 if (t.currentToken == IonTokenType.TokenSymbol 743 || t.currentToken == IonTokenType.TokenSymbolOperator 744 || t.currentToken == IonTokenType.TokenDot) 745 { 746 switch (symbolText) 747 { 748 case "null": 749 onNull(); 750 break; 751 case "true": 752 ser.putValue(true); 753 break; 754 case "false": 755 ser.putValue(false); 756 break; 757 default: 758 ser.putSymbol(symbolText); 759 break; 760 } 761 } 762 else 763 { 764 ser.putSymbol(symbolText); 765 } 766 } 767 } 768 769 @(IonTokenType.TokenString) 770 @(IonTokenType.TokenLongString) 771 void onString() @safe pure 772 { 773 IonTextString v; 774 if (t.currentToken == IonTokenType.TokenString) 775 { 776 v = t.readValue!(IonTokenType.TokenString); 777 } 778 else 779 { 780 v = t.readValue!(IonTokenType.TokenLongString); 781 } 782 auto s0 = ser.stringBegin; 783 ser.putStringPart(v.matchedText); 784 while (!v.isFinal) 785 { 786 if (t.currentToken == IonTokenType.TokenString) 787 { 788 v = t.readValue!(IonTokenType.TokenString); 789 } 790 else 791 { 792 v = t.readValue!(IonTokenType.TokenLongString); 793 } 794 ser.putStringPart(v.matchedText); 795 } 796 797 // At this point, we should've fully read out the contents of the first long string, 798 // so we should check if there's any long strings following this one. 799 if (t.currentToken == IonTokenType.TokenLongString) 800 { 801 while (true) 802 { 803 char c = t.skipWhitespace(); 804 if (c == '\'') 805 { 806 auto cs = t.peekMax(2); 807 if (cs.length == 2 && cs[0] == '\'' && cs[1] == '\'') 808 { 809 t.skipExactly(2); 810 v = t.readValue!(IonTokenType.TokenLongString); 811 ser.putStringPart(v.matchedText); 812 while (!v.isFinal) 813 { 814 v = t.readValue!(IonTokenType.TokenLongString); 815 ser.putStringPart(v.matchedText); 816 } 817 } 818 else 819 { 820 t.unread(c); 821 break; 822 } 823 } 824 else 825 { 826 t.unread(c); 827 break; 828 } 829 } 830 } 831 832 ser.stringEnd(s0); 833 } 834 835 @(IonTokenType.TokenTimestamp) 836 void onTimestamp() @safe pure 837 { 838 import mir.timestamp : Timestamp; 839 auto v = t.readValue!(IonTokenType.TokenTimestamp); 840 ser.putValue(Timestamp(v.matchedText)); 841 } 842 843 @(IonTokenType.TokenNumber) 844 void onNumber() @safe pure 845 { 846 import mir.bignum.integer; 847 import mir.bignum.decimal; 848 import mir.parse; 849 auto v = t.readValue!(IonTokenType.TokenNumber); 850 851 Decimal!128 dec = void; 852 DecimalExponentKey exponentKey; 853 // special values are handled within the tokenizer and emit different token types 854 // i.e. nan == IonTokenType.TokenFloatNaN, +inf == IonTokenType.TokenFloatInf, etc 855 enum bool allowSpecialValues = false; 856 // Ion spec allows this 857 enum bool allowDotOnBounds = true; 858 enum bool allowDExponent = true; 859 enum bool allowStartingPlus = true; 860 enum bool allowUnderscores = true; 861 enum bool allowLeadingZeros = false; 862 enum bool allowExponent = true; 863 // shouldn't be empty anyways, tokenizer wouldn't allow it 864 enum bool checkEmpty = false; 865 866 if (!dec.fromStringImpl!( 867 char, 868 allowSpecialValues, 869 allowDotOnBounds, 870 allowDExponent, 871 allowStartingPlus, 872 allowUnderscores, 873 allowLeadingZeros, 874 allowExponent, 875 checkEmpty 876 )(v.matchedText, exponentKey)) 877 { 878 goto unexpected_decimal_value; 879 } 880 881 if (exponentKey == DecimalExponentKey.none) 882 { 883 assert(dec.coefficient.coefficients.length != 1 || dec.coefficient.coefficients[0] != 0); 884 dec.coefficient.sign = dec.coefficient.sign && dec.coefficient.coefficients.length != 0; 885 // this is not a FP, so we can discard the exponent 886 ser.putValue(dec.coefficient); 887 } 888 else if ( 889 exponentKey == DecimalExponentKey.d 890 || exponentKey == DecimalExponentKey.D 891 || exponentKey == DecimalExponentKey.dot) 892 { 893 ser.putValue(dec); 894 } 895 else 896 { // floats handle infinity / nan / e / E 897 ser.putValue(cast(double)dec); 898 } 899 900 return; 901 902 unexpected_decimal_value: 903 version(D_Exceptions) 904 throw IonDeserializerErrorCode.unexpectedDecimalValue.ionDeserializerException; 905 else 906 assert(0, "unexpected decimal value"); 907 } 908 909 @(IonTokenType.TokenBinary) 910 void onBinaryNumber() @safe pure 911 { 912 auto v = t.readValue!(IonTokenType.TokenBinary); 913 auto sign = v[0] == '-'; 914 auto val = BigInt!128.fromBinaryString!true(v[2 + sign .. $]); // skip over the negative + 0b 915 val.sign = sign && val.coefficients.length; 916 ser.putValue(val); 917 } 918 919 @(IonTokenType.TokenHex) 920 void onHexNumber() @safe pure 921 { 922 auto v = t.readValue!(IonTokenType.TokenHex); 923 auto sign = v[0] == '-'; 924 auto val = BigInt!128.fromHexString!true(v[2 + sign .. $]); // skip over the 0x 925 val.sign = sign && val.coefficients.length; 926 ser.putValue(val); 927 } 928 929 @(IonTokenType.TokenFloatInf) 930 @(IonTokenType.TokenFloatMinusInf) 931 @(IonTokenType.TokenFloatNaN) 932 void onFloatSpecial() @safe pure 933 { 934 if (t.currentToken == IonTokenType.TokenFloatNaN) 935 { 936 ser.putValue(float.nan); 937 } 938 else if (t.currentToken == IonTokenType.TokenFloatMinusInf) 939 { 940 ser.putValue(-float.infinity); 941 } 942 else 943 { 944 ser.putValue(float.infinity); 945 } 946 } 947 948 @(IonTokenType.TokenOpenDoubleBrace) 949 void onLob() @safe pure 950 { 951 import mir.lob; 952 auto buf = stringBuf; 953 954 char c = t.skipLobWhitespace(); 955 if (c == '"') 956 { 957 IonTextClob clob = t.readClob(); 958 buf.put(clob.matchedText); 959 while (!clob.isFinal) { 960 clob = t.readClob(); 961 buf.put(clob.matchedText); 962 } 963 ser.putValue(Clob(buf.data)); 964 } 965 else if (c == '\'') 966 { 967 if (!t.isTripleQuote) 968 t.unexpectedChar(c); 969 // XXX: ScopedBuffer is unavoidable for the implicit concatenation of long clob values. 970 // Replace when we're able to put in a clob by parts (similar to strings) 971 IonTextClob clob = t.readClob!true(); 972 buf.put(clob.matchedText); 973 while (!clob.isFinal) 974 { 975 clob = t.readClob!true(); 976 buf.put(clob.matchedText); 977 } 978 ser.putValue(Clob(buf.data)); 979 } 980 else 981 { 982 import mir.appender : scopedBuffer; 983 import mir.base64 : decodeBase64; 984 // This is most likely a "blob", and we need every single 985 // character to be read correctly, so we will unread this byte. 986 t.unread(c); 987 auto decoded = scopedBuffer!ubyte; 988 IonTextBlob blob = t.readBlob(); 989 // Since we don't do any whitespace trimming, we need to do that here... 990 foreach(b; blob.matchedText) { 991 if (b.isWhitespace) { 992 continue; 993 } 994 995 buf.put(b); 996 } 997 998 if (buf.data.length % 4 != 0) { 999 version(D_Exceptions) 1000 throw IonDeserializerErrorCode.invalidBase64Length.ionDeserializerException; 1001 else 1002 assert(0, "invalid Base64 length (maybe missing padding?)"); 1003 } 1004 decodeBase64(buf.data, decoded); 1005 ser.putValue(Blob(decoded.data)); 1006 } 1007 t.finished = true; 1008 } 1009 } 1010 1011 /++ 1012 Deserialize an Ion Text value to a D value. 1013 Params: 1014 value = (optional) value to deserialize 1015 text = The text to deserialize 1016 Returns: 1017 The deserialized Ion Text value 1018 +/ 1019 T deserializeText(T)(scope const(char)[] text) 1020 { 1021 import mir.deser.ion; 1022 import mir.ion.conv : text2ion; 1023 import mir.ion.value; 1024 import mir.appender : scopedBuffer; 1025 1026 T value; 1027 deserializeText!T(value, text); 1028 return value; 1029 } 1030 1031 ///ditto 1032 void deserializeText(T)(scope ref T value, scope const(char)[] text) 1033 { 1034 import mir.deser.ion; 1035 import mir.ion.conv : text2ion; 1036 import mir.ion.value; 1037 import mir.appender : scopedBuffer; 1038 1039 auto buf = scopedBuffer!ubyte; 1040 text2ion(text, buf); 1041 return deserializeIon!T(value, buf.data); 1042 } 1043 1044 /// Test struct deserialization 1045 @safe pure 1046 version(mir_ion_parser_test) unittest 1047 { 1048 import mir.ion.value; 1049 static struct Book 1050 { 1051 string title; 1052 bool wouldRecommend; 1053 string description; 1054 uint numberOfNovellas; 1055 double price; 1056 float weight; 1057 string[] tags; 1058 } 1059 1060 static immutable textData = ` 1061 { 1062 "title": "A Hero of Our Time", 1063 "wouldRecommend": true, 1064 "description": "", 1065 "numberOfNovellas": 5, 1066 "price": 7.99, 1067 "weight": 6.88, 1068 "tags": ["russian", "novel", "19th century"] 1069 }`; 1070 1071 Book book = deserializeText!Book(textData); 1072 assert(book.description.length == 0); 1073 assert(book.numberOfNovellas == 5); 1074 assert(book.price == 7.99); 1075 assert(book.tags.length == 3); 1076 assert(book.tags[0] == "russian"); 1077 assert(book.tags[1] == "novel"); 1078 assert(book.tags[2] == "19th century"); 1079 assert(book.title == "A Hero of Our Time"); 1080 assert(book.weight == 6.88f); 1081 assert(book.wouldRecommend); 1082 } 1083 1084 /// Test @nogc struct deserialization 1085 @safe pure @nogc 1086 version(mir_ion_parser_test) unittest 1087 { 1088 import mir.ion.value; 1089 import mir.bignum.decimal; 1090 import mir.small_string; 1091 import mir.small_array; 1092 import mir.conv : to; 1093 static struct Book 1094 { 1095 SmallString!64 title; 1096 bool wouldRecommend; 1097 SmallString!64 description; 1098 uint numberOfNovellas; 1099 Decimal!1 price; 1100 double weight; 1101 SmallArray!(SmallString!(16), 10) tags; 1102 } 1103 1104 static immutable textData = ` 1105 { 1106 "title": "A Hero of Our Time", 1107 "wouldRecommend": true, 1108 "description": "", 1109 "numberOfNovellas": 5, 1110 "price": 7.99, 1111 "weight": 6.88, 1112 "tags": ["russian", "novel", "19th century"] 1113 }`; 1114 1115 Book book = deserializeText!Book(textData); 1116 assert(book.description.length == 0); 1117 assert(book.numberOfNovellas == 5); 1118 assert(book.price.to!double == 7.99); 1119 assert(book.tags.length == 3); 1120 assert(book.tags[0] == "russian"); 1121 assert(book.tags[1] == "novel"); 1122 assert(book.tags[2] == "19th century"); 1123 assert(book.title == "A Hero of Our Time"); 1124 assert(book.weight == 6.88f); 1125 assert(book.wouldRecommend); 1126 } 1127 1128 /// Test that strings are being de-serialized properly 1129 version(mir_ion_parser_test) unittest 1130 { 1131 import mir.test: should; 1132 import mir.ion.stream; 1133 import mir.ion.conv : text2ion; 1134 import mir.ser.text; 1135 void test(const(char)[] ionData, const(char)[] expected) 1136 { 1137 const(char)[] output = ionData.text2ion.IonValueStream.serializeText; 1138 output.should == expected; 1139 } 1140 1141 test(`"hello"`, `"hello"`); 1142 test(`"hello\x20world"`, `"hello world"`); 1143 test(`"hello\u2248world"`, `"hello≈world"`); 1144 test(`"hello\U0001F44Dworld"`, `"hello👍world"`); 1145 } 1146 1147 /// Test that timestamps are de-serialized properly 1148 version(mir_ion_parser_test) unittest 1149 { 1150 import mir.test; 1151 import mir.ion.stream; 1152 import mir.ion.conv : text2ion; 1153 import mir.ion.value : IonTimestamp; 1154 import std.datetime.date : TimeOfDay; 1155 import mir.timestamp : Timestamp; 1156 void test(const(char)[] ionData, Timestamp expected) 1157 { 1158 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1159 Timestamp t = ionValue.get!(IonTimestamp).get; 1160 t.should == expected; 1161 } 1162 } 1163 1164 void testFail(const(char)[] ionData, Timestamp expected) 1165 { 1166 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1167 Timestamp t = ionValue.get!(IonTimestamp).get; 1168 assert(expected != t); 1169 } 1170 } 1171 1172 test("2001-01T", Timestamp(2001, 1)); 1173 test("2001-01-02", Timestamp(2001, 1, 2)); 1174 test("2001-01-02T", Timestamp(2001, 1, 2)); 1175 test("2001-01-02T03:04", Timestamp(2001, 1, 2, 3, 4)); 1176 test("2001-01-02T03:04Z", Timestamp(2001, 1, 2, 3, 4).withOffset(0)); 1177 test("2001-01-02T03:04+00:00", Timestamp(2001, 1, 2, 3, 4).withOffset(0)); 1178 test("2001-01-02T03:05+00:01", Timestamp(2001, 1, 2, 3, 4).withOffset(1)); 1179 test("2001-01-02T05:05+02:01", Timestamp(2001, 1, 2, 3, 4).withOffset(2*60+1)); 1180 test("2001-01-02T03:04:05", Timestamp(2001, 1, 2, 3, 4, 5)); 1181 test("2001-01-02T03:04:05Z", Timestamp(2001, 1, 2, 3, 4, 5).withOffset(0)); 1182 test("2001-01-02T03:04:05+00:00", Timestamp(2001, 1, 2, 3, 4, 5).withOffset(0)); 1183 test("2001-01-02T03:05:05+00:01", Timestamp(2001, 1, 2, 3, 4, 5).withOffset(1)); 1184 test("2001-01-02T05:05:05+02:01", Timestamp(2001, 1, 2, 3, 4, 5).withOffset(2*60+1)); 1185 test("2001-01-02T03:04:05.666", Timestamp(2001, 1, 2, 3, 4, 5, -3, 666)); 1186 test("2001-01-02T03:04:05.666Z", Timestamp(2001, 1, 2, 3, 4, 5, -3, 666).withOffset(0)); 1187 test("2001-01-02T03:04:05.666666Z", Timestamp(2001, 1, 2, 3, 4, 5, -6, 666_666).withOffset(0)); 1188 test("2001-01-02T03:54:05.666+00:50", Timestamp(2001, 1, 2, 3, 4, 5, -3, 666).withOffset(50)); 1189 test("2001-01-02T03:54:05.666666+00:50", Timestamp(2001, 1, 2, 3, 4, 5, -6, 666_666).withOffset(50)); 1190 1191 // Time of day tests 1192 test("03:04", Timestamp(0, 0, 0, 3, 4)); 1193 test("03:04Z", Timestamp(0, 0, 0, 3, 4).withOffset(0)); 1194 test("03:04+00:00", Timestamp(0, 0, 0, 3, 4).withOffset(0)); 1195 test("03:05+00:01", Timestamp(0, 0, 0, 3, 4).withOffset(1)); 1196 test("05:05+02:01", Timestamp(0, 0, 0, 3, 4).withOffset(2*60+1)); 1197 test("03:04:05", Timestamp(0, 0, 0, 3, 4, 5)); 1198 test("03:04:05Z", Timestamp(0, 0, 0, 3, 4, 5).withOffset(0)); 1199 test("03:04:05+00:00", Timestamp(0, 0, 0, 3, 4, 5).withOffset(0)); 1200 test("03:05:05+00:01", Timestamp(0, 0, 0, 3, 4, 5).withOffset(1)); 1201 test("05:05:05+02:01", Timestamp(0, 0, 0, 3, 4, 5).withOffset(2*60+1)); 1202 test("03:04:05.666", Timestamp(0, 0, 0, 3, 4, 5, -3, 666)); 1203 test("03:04:05.666Z", Timestamp(0, 0, 0, 3, 4, 5, -3, 666).withOffset(0)); 1204 test("03:04:05.666666Z", Timestamp(0, 0, 0, 3, 4, 5, -6, 666_666).withOffset(0)); 1205 test("03:54:05.666+00:50", Timestamp(0, 0, 0, 3, 4, 5, -3, 666).withOffset(50)); 1206 test("03:54:05.666666+00:50", Timestamp(0, 0, 0, 3, 4, 5, -6, 666_666).withOffset(50)); 1207 1208 // Mir doesn't like 03:04 only (as technically it's less precise then TimeOfDay)... ugh 1209 test("03:04:05", Timestamp(TimeOfDay(3, 4, 5))); 1210 test("03:04:05Z", Timestamp(TimeOfDay(3, 4, 5)).withOffset(0)); 1211 test("03:04:05+00:00", Timestamp(TimeOfDay(3, 4, 5)).withOffset(0)); 1212 test("03:05:05+00:01", Timestamp(TimeOfDay(3, 4, 5)).withOffset(1)); 1213 test("05:05:05+02:01", Timestamp(TimeOfDay(3, 4, 5)).withOffset(2*60+1)); 1214 1215 testFail("2001-01-02T03:04+00:50", Timestamp(2001, 1, 2, 3, 4)); 1216 testFail("2001-01-02T03:04:05+00:50", Timestamp(2001, 1, 2, 3, 4, 5)); 1217 testFail("2001-01-02T03:04:05.666Z", Timestamp(2001, 1, 2, 3, 4, 5).withOffset(0)); 1218 testFail("2001-01-02T03:54:05.666+00:50", Timestamp(2001, 1, 2, 3, 4, 5)); 1219 1220 // Fake timestamps for Duration encoding 1221 import core.time : weeks, days, hours, minutes, seconds, hnsecs; 1222 test("0005-02-88T07:40:04.9876543", Timestamp(5.weeks + 2.days + 7.hours + 40.minutes + 4.seconds + 9876543.hnsecs)); 1223 test("0005-02-99T07:40:04.9876543", Timestamp(-5.weeks - 2.days - 7.hours - 40.minutes - 4.seconds - 9876543.hnsecs)); 1224 } 1225 1226 /// Test that binary literals are de-serialized properly. 1227 version (mir_ion_parser_test) unittest 1228 { 1229 import mir.ion.value : IonUInt; 1230 import mir.ion.stream; 1231 import mir.ion.conv : text2ion; 1232 void test(const(char)[] ionData, uint val) 1233 { 1234 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1235 auto v = ionValue.get!(IonUInt); 1236 assert(v.get!uint == val); 1237 } 1238 } 1239 1240 test("0b00001", 0b1); 1241 test("0b10101", 0b10101); 1242 test("0b11111", 0b11111); 1243 test("0b111111111111111111111", 0b1111_1111_1111_1111_1111_1); 1244 test("0b1_1111_1111_1111_1111_1111", 0b1_1111_1111_1111_1111_1111); 1245 } 1246 1247 /// Test that signed / unsigned integers are de-serialized properly. 1248 version (mir_ion_parser_test) unittest 1249 { 1250 import mir.ion.value : IonUInt, IonNInt; 1251 import mir.ion.stream; 1252 import mir.ion.conv : text2ion; 1253 void test(const(char)[] ionData, ulong val) 1254 { 1255 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1256 auto v = ionValue.get!(IonUInt); 1257 assert(v.get!ulong == val); 1258 } 1259 } 1260 1261 void testNeg(const(char)[] ionData, ulong val) 1262 { 1263 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1264 auto v = ionValue.get!(IonNInt); 1265 assert(v.get!long == -val); 1266 } 1267 } 1268 1269 test("0xabc_def", 0xabc_def); 1270 test("0xabcdef", 0xabcdef); 1271 test("0xDEADBEEF", 0xDEADBEEF); 1272 test("0xDEADBEEF", 0xDEAD_BEEF); 1273 test("0xDEAD_BEEF", 0xDEAD_BEEF); 1274 test("0xDEAD_BEEF", 0xDEADBEEF); 1275 test("0x0123456789", 0x0123456789); 1276 test("0x0123456789abcdef", 0x0123456789abcdef); 1277 test("0x0123_4567_89ab_cdef", 0x0123_4567_89ab_cdef); 1278 1279 testNeg("-0xabc_def", 0xabc_def); 1280 testNeg("-0xabc_def", 0xabc_def); 1281 testNeg("-0xabcdef", 0xabcdef); 1282 testNeg("-0xDEADBEEF", 0xDEADBEEF); 1283 testNeg("-0xDEADBEEF", 0xDEAD_BEEF); 1284 testNeg("-0xDEAD_BEEF", 0xDEAD_BEEF); 1285 testNeg("-0xDEAD_BEEF", 0xDEADBEEF); 1286 testNeg("-0x0123456789", 0x0123456789); 1287 testNeg("-0x0123456789abcdef", 0x0123456789abcdef); 1288 testNeg("-0x0123_4567_89ab_cdef", 0x0123_4567_89ab_cdef); 1289 } 1290 1291 /// Test that infinity & negative infinity are deserialized properly. 1292 version (mir_ion_parser_test) unittest 1293 { 1294 import mir.test: should; 1295 import mir.ion.value : IonFloat; 1296 import mir.ion.conv : text2ion; 1297 import mir.ion.stream; 1298 void test(const(char)[] ionData, float expected) 1299 { 1300 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1301 auto v = ionValue.get!(IonFloat); 1302 v.get!float.should == expected; 1303 } 1304 } 1305 1306 test("-inf", -float.infinity); 1307 test("+inf", float.infinity); 1308 } 1309 1310 /// Test that NaN is deserialized properly. 1311 version (mir_ion_parser_test) unittest 1312 { 1313 import mir.ion.value; 1314 import mir.ion.conv : text2ion; 1315 import mir.ion.stream; 1316 1317 alias isNaN = x => x != x; 1318 void test(const(char)[] ionData) 1319 { 1320 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1321 auto v = ionValue.get!(IonFloat); 1322 assert(isNaN(v.get!float)); 1323 } 1324 } 1325 1326 test("nan"); 1327 } 1328 1329 /// Test that signed / unsigned integers and decimals and floats are all deserialized properly. 1330 version (mir_ion_parser_test) unittest 1331 { 1332 import mir.test: should; 1333 import mir.ion.value; 1334 import mir.ion.stream; 1335 import mir.ion.conv : text2ion; 1336 void test_uint(const(char)[] ionData, ulong expected) 1337 { 1338 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1339 auto v = ionValue.get!(IonUInt); 1340 v.get!ulong.should == expected; 1341 } 1342 } 1343 1344 void test_nint(const(char)[] ionData, long expected) 1345 { 1346 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1347 auto v = ionValue.get!(IonNInt); 1348 v.get!long.should == expected; 1349 } 1350 } 1351 1352 void test_dec(const(char)[] ionData, double expected) 1353 { 1354 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1355 auto v = ionValue.get!(IonDecimal); 1356 v.get!double.should == expected; 1357 } 1358 } 1359 1360 void test_float(const(char)[] ionData, float expected) 1361 { 1362 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1363 auto v = ionValue.get!(IonFloat); 1364 v.get!float.should == expected; 1365 } 1366 } 1367 1368 test_uint("123", 123); 1369 test_nint("-123", -123); 1370 test_dec("123.123123", 123.123123); 1371 test_dec("123.123123", 123.123123); 1372 test_dec("123.123123d0", 123.123123); 1373 test_dec("123.123123d0", 123.123123); 1374 test_dec("-123.123123", -123.123123); 1375 test_dec("-123.123123d0", -123.123123); 1376 test_dec("18446744073709551615.", 1844_6744_0737_0955_1615.0); 1377 test_dec("-18446744073709551615.", -1844_6744_0737_0955_1615.0); 1378 test_dec("18446744073709551616.", 1844_6744_0737_0955_1616.0); 1379 test_dec("-18446744073709551616.", -1844_6744_0737_0955_1616.0); 1380 test_float("123.456789e-6", 123.456789e-6); 1381 test_float("-123.456789e-6", -123.456789e-6); 1382 } 1383 1384 /// Test that quoted / unquoted symbols are deserialized properly. 1385 version (mir_ion_parser_test) unittest 1386 { 1387 import mir.ion.value; 1388 import mir.ion.conv : text2ion; 1389 import mir.ion.stream; 1390 void test(const(char)[] ionData, string symbol) 1391 { 1392 foreach (symbolTable, val; ionData.text2ion.IonValueStream) { 1393 auto sym = val.get!(IonSymbolID).get; 1394 assert(symbol == symbolTable[sym]); 1395 } 1396 } 1397 1398 test("$0", "$0"); 1399 test("$ion", "$ion"); 1400 test("$ion_1_0", "$ion_1_0"); 1401 test("name", "name"); 1402 test("version", "version"); 1403 test("imports", "imports"); 1404 test("symbols", "symbols"); 1405 test("max_id", "max_id"); 1406 test("$ion_shared_symbol_table", "$ion_shared_symbol_table"); 1407 test("hello", "hello"); 1408 test("world", "world"); 1409 test("'foobaz'", "foobaz"); 1410 test("'👍'", "👍"); 1411 test("' '", " "); 1412 test("'\\U0001F44D'", "👍"); 1413 test("'\\u2248'", "\u2248"); 1414 test("'true'", "true"); 1415 test("'false'", "false"); 1416 test("'nan'", "nan"); 1417 test("'null'", "null"); 1418 } 1419 1420 /// Test that all variations of the "null" value are deserialized properly. 1421 version (mir_ion_parser_test) unittest 1422 { 1423 import mir.ion.value; 1424 import mir.ion.stream; 1425 import mir.ion.conv : text2ion; 1426 void test(const(char)[] ionData, IonTypeCode nullType) 1427 { 1428 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1429 auto v = ionValue.get!(IonNull); 1430 assert(v.code == nullType); 1431 } 1432 } 1433 1434 test("null", IonTypeCode.null_); 1435 test("null.bool", IonTypeCode.bool_); 1436 test("null.int", IonTypeCode.uInt); 1437 test("null.float", IonTypeCode.float_); 1438 test("null.decimal", IonTypeCode.decimal); 1439 test("null.timestamp", IonTypeCode.timestamp); 1440 test("null.symbol", IonTypeCode.symbol); 1441 test("null.string", IonTypeCode..string); 1442 test("null.blob", IonTypeCode.blob); 1443 test("null.clob", IonTypeCode.clob); 1444 test("null.list", IonTypeCode.list); 1445 test("null.struct", IonTypeCode.struct_); 1446 test("null.sexp", IonTypeCode.sexp); 1447 } 1448 1449 /// Test that blobs are getting de-serialized correctly. 1450 version (mir_ion_parser_test) unittest 1451 { 1452 import mir.ion.value; 1453 import mir.ion.stream; 1454 import mir.ion.conv : text2ion; 1455 import mir.lob; 1456 void test(const(char)[] ionData, ubyte[] blobData) 1457 { 1458 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1459 auto v = ionValue.get!(Blob); 1460 assert(v.data == blobData); 1461 } 1462 } 1463 1464 test("{{ SGVsbG8sIHdvcmxkIQ== }}", cast(ubyte[])"Hello, world!"); 1465 test("{{ R29vZCBhZnRlcm5vb24hIPCfkY0= }}", cast(ubyte[])"Good afternoon! 👍"); 1466 } 1467 1468 /// Test that long/short clobs are getting de-serialized correctly. 1469 version (mir_ion_parser_test) unittest 1470 { 1471 import mir.ion.value; 1472 import mir.ion.stream; 1473 import mir.ion.conv : text2ion; 1474 import mir.lob; 1475 void test(const(char)[] ionData, const(char)[] blobData) 1476 { 1477 foreach(symbolTable, scope ionValue; ionData.text2ion.IonValueStream) { 1478 auto v = ionValue.get!(Clob); 1479 assert(v.data == blobData); 1480 } 1481 } 1482 1483 test(`{{ "This is a short clob." }}`, "This is a short clob."); 1484 test(` 1485 {{ 1486 '''This is a long clob,''' 1487 ''' which spans over multiple lines,''' 1488 ''' and can have a theoretically infinite length.''' 1489 }}`, "This is a long clob, which spans over multiple lines, and can have a theoretically infinite length."); 1490 test(`{{ 1491 '''Long clobs can also have their data contained in one value, 1492 but spread out across multiple lines.''' 1493 }}`, "Long clobs can also have their data contained in one value,\n but spread out across multiple lines."); 1494 test(`{{ '''Or, you can have multiple values on the same line,''' ''' like this!'''}}`, 1495 "Or, you can have multiple values on the same line, like this!"); 1496 } 1497 1498 /// Test that structs are getting de-serialized properly 1499 version (mir_ion_parser_test) 1500 unittest 1501 { 1502 import mir.test: should; 1503 import mir.ion.stream; 1504 import mir.ion.conv : text2ion; 1505 import mir.ser.text; 1506 void test(const(char)[] ionData, const(char)[] expected) 1507 { 1508 auto v = ionData.text2ion.IonValueStream.serializeText; 1509 v.should == expected; 1510 } 1511 1512 test(`1`, `1`); 1513 test(`test::1`, `test::1`); 1514 1515 test(`{"test":"world", test: false, 'test': usd::123.456, '''test''': "asdf"}`, 1516 `{test:"world",test:false,test:usd::123.456,test:"asdf"}`); 1517 1518 test(`{'''foo''' 1519 '''bar''': "foobar"}`, 1520 `{foobar:"foobar"}`); 1521 1522 test(`{a: 1, b: 2}`, `{a:1,b:2}`); 1523 1524 test(`{}`, `{}`); 1525 } 1526 1527 /// Test that sexps are getting de-serialized properly. 1528 version (mir_ion_parser_test) unittest 1529 { 1530 import mir.test: should; 1531 import mir.ion.stream; 1532 import mir.ion.conv : text2ion; 1533 import mir.ser.text; 1534 void test(const(char)[] ionData, const(char)[] expected) 1535 { 1536 auto v = ionData.text2ion.IonValueStream.serializeText; 1537 v.should == expected; 1538 } 1539 1540 test(`(this is a sexp list)`, "(this is a sexp list)"); 1541 test(`('+' '++' '+-+' '-++' '-' '--' '---' -3 - 3 '--' 3 '--'3 )`, 1542 "('+' '++' '+-+' '-++' '-' '--' '---' -3 '-' 3 '--' 3 '--' 3)"); 1543 test(`(a_plus_plus_plus_operator::+++ a_3::3)`, `(a_plus_plus_plus_operator::'+++' a_3::3)`); 1544 test(`(& (% -[42, 3]+(2)-))`, `('&' ('%' '-' [42,3] '+' (2) '-'))`); 1545 } 1546 1547 /// Test that arrays are getting de-serialized properly. 1548 version (mir_ion_parser_test) unittest 1549 { 1550 import mir.test: should; 1551 import mir.ion.stream; 1552 import mir.ion.conv : text2ion; 1553 import mir.ser.text; 1554 void test(const(char)[] ionData, const(char)[] expected) 1555 { 1556 auto v = ionData.text2ion.IonValueStream.serializeText; 1557 v.should == expected; 1558 } 1559 1560 test(`[hello, world]`, `[hello,world]`); 1561 test(`[this::is::an::annotated::symbol, this::is::annotated::123.456]`, 1562 `[this::is::an::annotated::symbol,this::is::annotated::123.456]`); 1563 test(`[date::of::birth::0001-01-01T00:00:00.0+00:00, date::of::birth::1970-01-01T]`, 1564 `[date::of::birth::0001-01-01T00:00:00.0Z,date::of::birth::1970-01-01]`); 1565 test(`['hello', "hello", '''hello''', '''hello ''''''world''']`, 1566 `[hello,"hello","hello","hello world"]`); 1567 test(`[0x123_456, 0xF00D_BAD]`, `[1193046,251714477]`); 1568 } 1569 1570 /// Test that annotations work with symbols 1571 version (mir_ion_parser_test) unittest 1572 { 1573 import mir.test: should; 1574 import mir.ion.stream; 1575 import mir.ser.text; 1576 import mir.ion.conv : text2ion; 1577 void test(const(char)[] ionData, const(char)[] expected) 1578 { 1579 auto v = ionData.text2ion.IonValueStream.serializeText; 1580 v.should == expected; 1581 } 1582 1583 test(`'test'::'hello'::'world'`, "test::hello::world"); 1584 test(`foo::bar`, "foo::bar"); 1585 test(`foo::'bar'`, "foo::bar"); 1586 test(`'foo'::bar`, "foo::bar"); 1587 test(`'foo bar'::cash`, "'foo bar'::cash"); 1588 test(`'foo\U0001F44D'::'baz\U0001F44D'`, "'foo\U0001F44D'::'baz\U0001F44D'"); 1589 test(`'\u2248'::'\u2248'`, "'\u2248'::'\u2248'"); 1590 test(`'\u2248'::foo`, "'\u2248'::foo"); 1591 } 1592 1593 /// Test that annotations work with floats 1594 version (mir_ion_parser_test) unittest 1595 { 1596 import mir.test: should; 1597 import mir.ion.stream; 1598 import mir.ion.conv : text2ion; 1599 import mir.ser.text; 1600 void test(const(char)[] ionData, const(char)[] expected) 1601 { 1602 auto v = ionData.text2ion.IonValueStream.serializeText; 1603 v.should == expected; 1604 } 1605 1606 test(`usd::10.50e0`, "usd::10.5"); 1607 test(`'Value is good \U0001F44D'::12.34e0`, "'Value is good \U0001F44D'::12.34"); 1608 test(`'null'::150.00e0`, "'null'::150.0"); 1609 } 1610 1611 /// Test that annotations work with decimals 1612 version (mir_ion_parser_test) unittest 1613 { 1614 import mir.test: should; 1615 import mir.ion.stream; 1616 import mir.ion.conv : text2ion; 1617 import mir.ser.text; 1618 void test(const(char)[] ionData, const(char)[] expected) 1619 { 1620 auto v = ionData.text2ion.IonValueStream.serializeText; 1621 v.should == expected; 1622 } 1623 1624 test(`Types::Speed::MetersPerSecondSquared::9.81`, "Types::Speed::MetersPerSecondSquared::9.81"); 1625 test(`Rate::USD::GBP::12.345`, "Rate::USD::GBP::12.345"); 1626 test(`usd::10.50d0`, "usd::10.50"); 1627 test(`'Value is good \U0001F44D'::12.34d0`, "'Value is good \U0001F44D'::12.34"); 1628 test(`'null'::150.00d0`, "'null'::150.00"); 1629 test(`'Cool'::27182818284590450000000000d-25`, "Cool::2.7182818284590450000000000"); 1630 test(`mass::2.718281828459045d0`, "mass::2.718281828459045"); 1631 test(`weight::0.000000027182818284590450000000000d+8`, "weight::2.7182818284590450000000000"); 1632 test(`coeff::-0.000000027182818284590450000000000d+8`, "coeff::-2.7182818284590450000000000"); 1633 } 1634 1635 /// Test that annotations work with strings 1636 version (mir_ion_parser_test) unittest 1637 { 1638 import mir.test: should; 1639 import mir.ion.stream; 1640 import mir.ion.conv : text2ion; 1641 import mir.ser.text; 1642 void test(const(char)[] ionData, const(char)[] expected) 1643 { 1644 auto v = ionData.text2ion.IonValueStream.serializeText; 1645 v.should == expected; 1646 } 1647 1648 test(`Password::"Super Secure Password"`, `Password::"Super Secure Password"`); 1649 test(`Magic::String::"Hello, world!"`, `Magic::String::"Hello, world!"`); 1650 test(`SSH::PublicKey::'''ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNrMk7QmmmNIusf10CwHQHs6Z9HJIiuknwoqtQLzEPxdMnNHKJexNnfF5QQ2v84BBhVjxvPgSqhdcVMEFy8PrGu44MqhK/cV6BGx430v2FnArWDO+9LUSd+3iwMJVZUQgZGtjSLAkZO+NOSPWZ+W0SODGgUfbNVu35GjVoA2+e1lOINUe22oZPnaD+gpJGUOx7j5JqpCblBZntvZyOjTPl3pc52rIGfxi1TYJnDXjqX76OinZceBzp5Oh0oUTrPbu55ig+b8bd4HtzLWxcqXBCnsw0OAKsAiXfLlBcrgZUsoAP9unrcqsqoJ2qEEumdsPqcpJakpO7/n0lMP6lRdSZ'''`, 1651 `SSH::PublicKey::"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNrMk7QmmmNIusf10CwHQHs6Z9HJIiuknwoqtQLzEPxdMnNHKJexNnfF5QQ2v84BBhVjxvPgSqhdcVMEFy8PrGu44MqhK/cV6BGx430v2FnArWDO+9LUSd+3iwMJVZUQgZGtjSLAkZO+NOSPWZ+W0SODGgUfbNVu35GjVoA2+e1lOINUe22oZPnaD+gpJGUOx7j5JqpCblBZntvZyOjTPl3pc52rIGfxi1TYJnDXjqX76OinZceBzp5Oh0oUTrPbu55ig+b8bd4HtzLWxcqXBCnsw0OAKsAiXfLlBcrgZUsoAP9unrcqsqoJ2qEEumdsPqcpJakpO7/n0lMP6lRdSZ"`); 1652 }