1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 /// very simple (de)serializer to json-like text format
17 module iv
.txtser
/*is aliced*/;
20 import std
.range
: ElementEncodingType
, isInputRange
, isOutputRange
;
21 import std
.traits
: Unqual
;
26 // ////////////////////////////////////////////////////////////////////////// //
27 public enum SRZIgnore
; /// ignore this field
28 public struct SRZName
{ string name
; } /// rename this field
29 public enum SRZNonDefaultOnly
; /// write only if it has non-default value
32 // ////////////////////////////////////////////////////////////////////////// //
33 template arrayElementType(T
) {
34 private import std
.traits
: isArray
, Unqual
;
35 static if (isArray
!T
) {
36 alias arrayElementType
= arrayElementType
!(typeof(T
.init
[0]));
37 } else static if (is(typeof(T
))) {
38 alias arrayElementType
= Unqual
!(typeof(T
));
40 alias arrayElementType
= Unqual
!T
;
43 static assert(is(arrayElementType
!string
== char));
45 template isSimpleType(T
) {
46 private import std
.traits
: Unqual
;
47 private alias UT
= Unqual
!T
;
48 enum isSimpleType
= __traits(isIntegral
, UT
) ||
__traits(isFloating
, UT
) ||
is(UT
== bool);
51 template isCharType(T
) {
52 private import std
.traits
: Unqual
;
53 private alias UT
= Unqual
!T
;
54 enum isCharType
= is(UT
== char) ||
is(UT
== wchar) ||
is(UT
== dchar);
58 // ////////////////////////////////////////////////////////////////////////// //
60 public void txtser(T
, ST
) (in auto ref T v
, auto ref ST fl
, int indent
=0, bool skipstname
=false)
61 if (!is(T
== class) && (isWriteableStream
!ST || isOutputRange
!(ST
, char)))
65 void xput (const(char)[] s
...) {
66 if (s
.length
== 0) return;
67 static if (isWriteableStream
!ST
) {
70 static if (is(typeof(fl
.put(s
)))) {
73 foreach (char ch
; s
) fl
.put(ch
);
78 void quote (const(char)[] s
) {
79 static immutable string hexd
= "0123456789abcdef";
81 bool goodString
= true;
82 foreach (char ch
; s
) if (ch
< ' ' || ch
== 127 || ch
== '"' || ch
== '\\') { goodString
= false; break; }
89 while (pos
< s
.length
) {
91 while (epos
< s
.length
) {
92 auto ch
= s
.ptr
[epos
];
93 if (ch
< ' ' || ch
== 127 || ch
== '"' || ch
== '\\') break;
100 if (pos
< s
.length
) {
101 auto ch
= s
.ptr
[pos
++];
102 if (ch
< ' ' || ch
== 127 || ch
== '"' || ch
== '\\') {
104 case '\x1b': xput(`\e`); break;
105 case '\r': xput(`\r`); break;
106 case '\n': xput(`\n`); break;
107 case '\t': xput(`\t`); break;
108 case '"': case '\\': xput(`\`); xput(ch
); break;
111 xput(hexd
[(ch
>>4)&0x0f]);
126 foreach (immutable _
; 0..indent
) xput(' ');
129 void serData(T
) (in ref T v
, bool skipstructname
) {
130 alias UT
= arrayElementType
!T
;
131 static if (is(T
: const(char)[])) {
133 static if (is(T
== string
) ||
is(T
== const(char)[])) {
134 if (v
.ptr
is null) xput("null"); else quote(v
);
138 } else static if (is(T
: V
[], V
)) {
143 foreach (immutable idx
, const ref it
; v
) {
144 if (idx
== 0 ||
(v
.length
>= 56 && idx
%42 == 41)) newline();
146 if (idx
!= v
.length
-1) xput(",");
154 } else static if (is(T
: V
[K
], K
, V
)) {
160 foreach (const kv
; v
.byKeyValue
) {
162 serData(kv
.key
, true);
164 serData(kv
.value
, true);
165 if (--len
) xput(",");
173 } else static if (isCharType
!UT
) {
174 import std
.conv
: to
;
175 xput((cast(uint)v
).to
!string
);
176 } else static if (is(UT
== enum)) {
177 bool enumFound
= false;
178 foreach (string fldname
; __traits(allMembers
, UT
)) {
179 if (v
== __traits(getMember
, UT
, fldname
)) {
188 import std
.conv
: to
;
191 } else static if (isSimpleType
!UT
) {
192 import std
.conv
: to
;
194 } else static if (is(UT
== struct)) {
195 import std
.traits
: FieldNameTuple
, getUDAs
, hasUDA
;
196 if (skipstructname
) {
203 bool needComma
= false;
204 foreach (string fldname
; FieldNameTuple
!UT
) {
205 static if (!hasUDA
!(__traits(getMember
, UT
, fldname
), SRZIgnore
)) {
206 enum names
= getUDAs
!(__traits(getMember
, UT
, fldname
), SRZName
);
207 static if (names
.length
) enum xname
= names
[0].name
; else enum xname
= fldname
;
208 static assert(xname
.length
<= 255, "struct '"~UT
.stringof
~"': field name too long: "~xname
);
209 static if (hasUDA
!(__traits(getMember
, UT
, fldname
), SRZNonDefaultOnly
)) {
210 if (__traits(getMember
, v
, fldname
) == __traits(getMember
, v
, fldname
).init
) continue;
212 if (needComma
) xput(",");
216 serData(__traits(getMember
, v
, fldname
), true);
224 static assert(0, "can't serialize type '"~T
.stringof
~"'");
228 serData(v
, skipstname
);
232 // ////////////////////////////////////////////////////////////////////////// //
234 public enum isGoodSerParser(T
) = is(typeof((inout int=0) {
243 const(char)[] s
= t
.expectId
!true();
244 s
= t
.expectId
!false;
245 b
= T
.isGoodIdChar(' ');
247 int d
= T
.digitInBase('1', 10);
249 t
.parseString
!true(delegate (char ch
) {});
250 t
.parseString
!false(delegate (char ch
) {});
255 public struct TxtSerParser(ST
) if (isReadableStream
!ST ||
(isInputRange
!ST
&& is(Unqual
!(ElementEncodingType
!ST
) == char))) {
258 int eotflag
; // 0: not; 1: at peek; -1: at front; -2: done
259 // buffer for identifier reading
262 static if (isReadableStream
!ST
) {
263 enum AsStream
= true;
267 enum AsStream
= false;
275 this() (auto ref ST stream
) {
284 void error (string msg
) { import std
.conv
: to
; throw new Exception(msg
~" around line "~line
.to
!string
~", column "~col
.to
!string
); }
286 @property bool eot () const pure nothrow @safe @nogc { pragma(inline
, true); return (eotflag
< -1); }
289 if (eotflag
< 0) { curch
= peek
= 0; eotflag
= -2; return; }
290 if (curch
== '\n') { ++line
; col
= 1; } else ++col
;
292 if (eotflag
> 0) { eotflag
= -1; peek
= 0; return; }
293 // read next char to `peek`
294 static if (AsStream
) {
295 if (rdpos
>= rdused
) {
296 auto read
= st
.rawRead(rdbuf
[]);
297 if (read
.length
== 0) {
303 rdused
= cast(uint)read
.length
;
305 assert(rdpos
< rdused
);
306 peek
= rdbuf
.ptr
[rdpos
++];
320 if ((curch
== '/' && peek
== '/') || curch
== '#') {
321 while (!eot
&& curch
!= '\n') skipChar();
322 } else if (curch
== '/' && peek
== '*') {
326 if (curch
== '*' && peek
== '/') {
332 } else if (curch
== '/' && peek
== '+') {
337 if (curch
== '+' && peek
== '/') {
340 if (--level
== 0) break;
341 } else if (curch
== '/' && peek
== '+') {
347 } else if (curch
> ' ') {
355 void expectChar (char ch
) {
357 if (eot || curch
!= ch
) error("'"~ch
~"' expected");
361 const(char)[] expectId(bool allowQuoted
=false) () {
364 static if (allowQuoted
) {
365 if (!eot
&& curch
== '"') {
367 while (!eot
&& curch
!= '"') {
368 if (curch
== '\\') error("simple string expected");
369 if (bpos
>= buf
.length
) error("identifier or number too long");
370 buf
.ptr
[bpos
++] = curch
;
373 if (eot || curch
!= '"') error("simple string expected");
378 if (!isGoodIdChar(curch
)) error("identifier or number expected");
379 while (isGoodIdChar(curch
)) {
380 if (bpos
>= buf
.length
) error("identifier or number too long");
387 // `curch` is opening quote
388 void parseString(bool allowEscapes
) (scope void delegate (char ch
) put
) {
389 assert(put
!is null);
390 if (eot
) error("unterminated string");
393 while (!eot
&& curch
!= qch
) {
394 static if (allowEscapes
) if (curch
== '\\') {
396 if (eot
) error("unterminated string");
398 case '0': // oops, octal
400 foreach (immutable _
; 0..4) {
401 if (eot
) error("unterminated string");
402 int dig
= digitInBase(curch
, 10);
404 if (dig
> 7) error("invalid octal escape");
408 if (ucc
> 255) error("invalid octal escape");
411 case '1': .. case '9': // decimal
413 foreach (immutable _
; 0..3) {
414 if (eot
) error("unterminated string");
415 int dig
= digitInBase(curch
, 10);
420 if (ucc
> 255) error("invalid decimal escape");
423 case 'e': put('\x1b'); skipChar(); break;
424 case 'r': put('\r'); skipChar(); break;
425 case 'n': put('\n'); skipChar(); break;
426 case 't': put('\t'); skipChar(); break;
427 case '"': case '\\': case '\'': put(curch
); skipChar(); break;
430 if (eot
) error("unterminated string");
431 if (digitInBase(peek
, 16) < 0) error("invalid hex escape");
432 skipChar(); // skip 'x'
433 if (eot
) error("unterminated string");
434 if (digitInBase(curch
, 16) < 0 ||
digitInBase(peek
, 16) < 0) error("invalid hex escape");
435 put(cast(char)(digitInBase(curch
, 16)*16+digitInBase(peek
, 16)));
441 if (digitInBase(peek
, 16) < 0) error("invalid unicode escape");
443 skipChar(); // skip 'u'
444 foreach (immutable _
; 0..4) {
445 if (eot
) error("unterminated string");
446 if (digitInBase(curch
, 16) < 0) break;
447 ucc
= ucc
*16+digitInBase(curch
, 16);
451 auto len
= utf8Encode(buf
[], cast(dchar)ucc
);
453 if (len
< 0) error("invalid utf-8 escape");
454 foreach (char ch
; buf
[0..len
]) put(ch
);
456 default: error("invalid escape");
464 if (eot || curch
!= qch
) error("unterminated string");
468 static pure nothrow @safe @nogc:
469 bool isGoodIdChar (char ch
) {
470 pragma(inline
, true);
472 (ch
>= '0' && ch
<= '9') ||
473 (ch
>= 'A' && ch
<= 'Z') ||
474 (ch
>= 'a' && ch
<= 'z') ||
475 ch
== '_' || ch
== '-' || ch
== '+' || ch
== '.';
478 int digitInBase (char ch
, int base
) {
479 pragma(inline
, true);
481 base
>= 1 && ch
>= '0' && ch
< '0'+base ? ch
-'0' :
482 base
> 10 && ch
>= 'A' && ch
< 'A'+base
-10 ? ch
-'A'+10 :
483 base
> 10 && ch
>= 'a' && ch
< 'a'+base
-10 ? ch
-'a'+10 :
489 static assert(isGoodSerParser
!(TxtSerParser
!VFile
));
492 // ////////////////////////////////////////////////////////////////////////// //
494 public void txtunser(bool ignoreUnknown
=false, T
, ST
) (out T v
, auto ref ST fl
)
495 if (!is(T
== class) && (isReadableStream
!ST ||
(isInputRange
!ST
&& is(Unqual
!(ElementEncodingType
!ST
) == char))))
497 auto par
= TxtSerParser
!ST(fl
);
498 txtunser
!ignoreUnknown(v
, par
);
503 public void txtunser(bool ignoreUnknown
=false, T
, ST
) (out T v
, auto ref ST par
) if (!is(T
== class) && isGoodSerParser
!ST
) {
504 import std
.traits
: Unqual
;
508 if (par
.curch
== ',') par
.skipChar();
511 static if (ignoreUnknown
) void skipData(bool fieldname
) () {
514 if (par
.curch
== '"' || par
.curch
== '\'') {
515 par
.parseString
!(!fieldname
)(delegate (char ch
) {});
518 static if (!fieldname
) {
520 if (par
.curch
== '[') {
524 if (par
.eot
) par
.error("unterminated array");
525 if (par
.curch
== ']') break;
533 if (par
.curch
== '{') {
537 if (par
.eot
) par
.error("unterminated array");
538 if (par
.curch
== '}') break;
539 skipData
!true(); // field name
550 if (par
.eot ||
!par
.isGoodIdChar(par
.curch
)) par
.error("invalid identifier");
551 while (!par
.eot
&& par
.isGoodIdChar(par
.curch
)) par
.skipChar();
554 void unserData(T
) (out T v
) {
555 if (par
.eot
) par
.error("data expected");
556 static if (is(T
: const(char)[])) {
558 static if (__traits(isStaticArray
, T
)) {
561 if (v
.length
-dpos
< 1) par
.error("value too long");
565 void put (char ch
) { v
~= ch
; }
568 // `null` is empty string
569 if (par
.curch
!= '"' && par
.curch
!= '\'') {
570 if (!par
.isGoodIdChar(par
.curch
)) par
.error("string expected");
572 while (par
.isGoodIdChar(par
.curch
)) {
576 if (ss
!= "null") foreach (char ch
; ss
) put(ch
);
579 assert(par
.curch
== '"' || par
.curch
== '\'');
580 par
.parseString
!true(&put
);
582 } else static if (is(T
: V
[], V
)) {
585 if (par
.curch
== '{') {
587 static if (__traits(isStaticArray
, T
)) {
588 if (v
.length
== 0) par
.error("array too small");
593 } else if (par
.curch
== 'n') {
594 // this should be 'null'
595 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'u') par
.error("'null' expected");
596 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'l') par
.error("'null' expected");
597 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'l') par
.error("'null' expected");
598 par
.skipChar(); if (!par
.eot
&& par
.isGoodIdChar(par
.curch
)) par
.error("'null' expected");
599 static if (__traits(isStaticArray
, T
)) if (v
.length
!= 0) par
.error("static array too big");
602 static if (__traits(isStaticArray
, T
)) {
603 foreach (ref it
; v
) {
605 if (par
.eot || par
.curch
== ']') break;
612 if (par
.eot || par
.curch
== ']') break;
620 } else static if (is(T
: V
[K
], K
, V
)) {
627 if (par
.eot || par
.curch
== '}') break;
632 if (par
.curch
== 'n' && par
.peek
== 'u') {
633 par
.skipChar(); // skip 'n'
634 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'l') par
.error("'null' expected");
635 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'l') par
.error("'null' expected");
636 par
.skipChar(); if (!par
.eot
&& par
.isGoodIdChar(par
.curch
)) par
.error("'null' expected");
637 continue; // skip null value
645 } else static if (isCharType
!T
) {
646 import std
.conv
: to
;
647 auto id
= par
.expectId
;
650 } catch (Exception e
) {
651 par
.error("type conversion error for type '"~T
.stringof
~"' ("~id
.idup
~")");
653 } else static if (isSimpleType
!T
) {
654 import std
.conv
: to
;
655 auto id
= par
.expectId
;
656 // try bool->int conversions
657 static if ((is(T
: ulong) ||
is(T
: real)) && is(typeof((){v
=0;})) && is(typeof((){v
=1;}))) {
659 if (id
== "true") { v
= 1; return; }
660 if (id
== "false") { v
= 0; return; }
664 } catch (Exception e
) {
665 par
.error("type conversion error for type '"~T
.stringof
~"' ("~id
.idup
~")");
667 } else static if (is(T
== struct)) {
669 import std
.traits
: FieldNameTuple
, getUDAs
, hasUDA
;
672 if (par
.curch
!= '{') {
673 auto nm
= par
.expectId
!true();
674 if (nm
!= (Unqual
!T
).stringof
) par
.error("'"~(Unqual
!T
).stringof
~"' struct expected, but got '"~nm
.idup
~"'");
679 ulong[(FieldNameTuple
!T
.length
+ulong.sizeof
-1)/ulong.sizeof
] fldseen
= 0;
681 bool tryField(uint idx
, string fldname
) (const(char)[] name
) {
682 static if (hasUDA
!(__traits(getMember
, T
, fldname
), SRZName
)) {
683 enum names
= getUDAs
!(__traits(getMember
, T
, fldname
), SRZName
);
685 alias tuple(T
...) = T
;
686 enum names
= tuple
!(SRZName(fldname
));
688 foreach (immutable xname
; names
) {
689 if (xname
.name
== name
) {
690 if (fldseen
[idx
/8]&(1UL<<(idx
%8))) throw new Exception(`duplicate field value for '`~fldname
~`'`);
691 fldseen
[idx
/8] |
= 1UL<<(idx
%8);
692 unserData(__traits(getMember
, v
, fldname
));
699 void tryAllFields (const(char)[] name
) {
700 foreach (immutable idx
, string fldname
; FieldNameTuple
!T
) {
701 static if (!hasUDA
!(__traits(getMember
, T
, fldname
), SRZIgnore
)) {
702 if (tryField
!(idx
, fldname
)(name
)) return;
705 static if (ignoreUnknown
) {
708 throw new Exception("unknown field '"~name
.idup
~"'");
712 // let's hope that fields are in order (it is nothing wrong with seeing 'em in wrong order, though)
713 static if (ignoreUnknown
) {
714 while (par
.curch
!= '}') {
715 if (par
.curch
== 0) break;
716 auto name
= par
.expectId
!true();
722 foreach (immutable idx
, string fldname
; FieldNameTuple
!T
) {
723 static if (!hasUDA
!(__traits(getMember
, T
, fldname
), SRZIgnore
)) {
725 if (par
.curch
== '}') break;
726 auto name
= par
.expectId
!true();
728 if (!tryField
!(idx
, fldname
)(name
)) tryAllFields(name
);
742 // ////////////////////////////////////////////////////////////////////////// //
743 private static bool isValidDC (dchar c
) pure nothrow @safe @nogc { pragma(inline
, true); return (c
< 0xD800 ||
(c
> 0xDFFF && c
<= 0x10FFFF)); } /// is given codepoint valid?
745 /// returns -1 on error (out of room in `s`, for example), or bytes taken
746 private int utf8Encode(dchar replacement
='\uFFFD') (char[] s
, dchar c
) pure nothrow @trusted @nogc {
747 static assert(isValidDC(replacement
), "invalid replacement char");
748 if (!isValidDC(c
)) c
= replacement
;
750 if (s
.length
< 1) return -1;
751 s
.ptr
[0] = cast(char)c
;
757 buf
.ptr
[0] = cast(char)(0xC0|
(c
>>6));
758 buf
.ptr
[1] = cast(char)(0x80|
(c
&0x3F));
760 } else if (c
<= 0xFFFF) {
761 buf
.ptr
[0] = cast(char)(0xE0|
(c
>>12));
762 buf
.ptr
[1] = cast(char)(0x80|
((c
>>6)&0x3F));
763 buf
.ptr
[2] = cast(char)(0x80|
(c
&0x3F));
765 } else if (c
<= 0x10FFFF) {
766 buf
.ptr
[0] = cast(char)(0xF0|
(c
>>18));
767 buf
.ptr
[1] = cast(char)(0x80|
((c
>>12)&0x3F));
768 buf
.ptr
[2] = cast(char)(0x80|
((c
>>6)&0x3F));
769 buf
.ptr
[3] = cast(char)(0x80|
(c
&0x3F));
774 if (s
.length
< len
) return -1;
775 s
[0..len
] = buf
[0..len
];
781 // ////////////////////////////////////////////////////////////////////////// //
782 version(egserial_test
) unittest {
783 version(no_vfs
) import std
.stdio
; else { import iv
.vfs
.io
; import iv
.vfs
.streams
; }
785 version(no_vfs
) static struct InRng
{
787 @property char front () { return src
[0]; }
788 @property bool empty () { return (src
.length
== 0); }
789 void popFront () { src
= src
[1..$]; }
792 char[] s2s(T
) (in auto ref T v
) {
795 static struct OutRng
{
797 //void put (const(char)[] ch...) { *dest ~= ch; }
798 void put (char ch
) { *dest
~= ch
; }
800 auto or = OutRng(&res
);
805 auto buf
= MemoryStreamRWRef(res
);
807 auto fl
= wrapStream(buf
);
810 return cast(char[])*buf
.bytes
;
814 // ////////////////////////////////////////////////////////////////////////// //
815 static struct AssemblyInfo
{
818 @SRZIgnore uint ignoreme
;
821 static struct ReplyAsmInfo
{
822 @SRZName("command") @SRZName("xcommand") ubyte cmd
;
823 @SRZName("values") AssemblyInfo
[][2] list
;
825 @SRZNonDefaultOnly bool fbool
;
830 // ////////////////////////////////////////////////////////////////////////// //
834 ri
.list
[0] ~= AssemblyInfo(665, "limbo");
835 ri
.list
[1] ~= AssemblyInfo(69, "pleasure");
837 ri
.dict
["boo"] = 665;
845 xf
.txtunser(InRng(srs
));
847 xf
.txtunser(wrapMemoryRO(srs
));
849 //assert(fl.tell == fl.size);
850 assert(xf
.cmd
== 42);
851 assert(xf
.list
.length
== 2);
852 assert(xf
.list
[0].length
== 1);
853 assert(xf
.list
[1].length
== 1);
854 assert(xf
.list
[0][0].id
== 665);
855 assert(xf
.list
[0][0].name
== "limbo");
856 assert(xf
.list
[1][0].id
== 69);
857 assert(xf
.list
[1][0].name
== "pleasure");
858 assert(xf
.dict
.length
== 2);
859 assert(xf
.dict
["foo"] == 42);
860 assert(xf
.dict
["boo"] == 665);
861 //assert(xf.fbool == true);
862 assert(xf
.fbool
== false);
863 assert(xf
.ext
== "elf");
870 "Alice".txtser(stdout
);
875 import std
.utf
: byChar
;
880 boo
.txtunser("{ n:true }".byChar
);
886 foreach (immutable idx, ref v; n) v = idx;