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, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 /// very simple (de)serializer to json-like text format
18 module iv
.txtser
/*is aliced*/;
21 import std
.range
: ElementEncodingType
, isInputRange
, isOutputRange
;
22 import std
.traits
: Unqual
;
27 // ////////////////////////////////////////////////////////////////////////// //
28 public enum SRZIgnore
; /// ignore this field
29 public struct SRZName
{ string name
; } /// rename this field
30 public enum SRZNonDefaultOnly
; /// write only if it has non-default value
33 // ////////////////////////////////////////////////////////////////////////// //
34 template arrayElementType(T
) {
35 private import std
.traits
: isArray
, Unqual
;
36 static if (isArray
!T
) {
37 alias arrayElementType
= arrayElementType
!(typeof(T
.init
[0]));
38 } else static if (is(typeof(T
))) {
39 alias arrayElementType
= Unqual
!(typeof(T
));
41 alias arrayElementType
= Unqual
!T
;
44 static assert(is(arrayElementType
!string
== char));
46 template isSimpleType(T
) {
47 private import std
.traits
: Unqual
;
48 private alias UT
= Unqual
!T
;
49 enum isSimpleType
= __traits(isIntegral
, UT
) ||
__traits(isFloating
, UT
) ||
is(UT
== bool);
52 template isCharType(T
) {
53 private import std
.traits
: Unqual
;
54 private alias UT
= Unqual
!T
;
55 enum isCharType
= is(UT
== char) ||
is(UT
== wchar) ||
is(UT
== dchar);
59 // ////////////////////////////////////////////////////////////////////////// //
61 public void txtser(T
, ST
) (in auto ref T v
, auto ref ST fl
, int indent
=0, bool skipstname
=false)
62 if (!is(T
== class) && (isWriteableStream
!ST || isOutputRange
!(ST
, char)))
66 void xput (const(char)[] s
...) {
67 if (s
.length
== 0) return;
68 static if (isWriteableStream
!ST
) {
71 static if (is(typeof(fl
.put(s
)))) {
74 foreach (char ch
; s
) fl
.put(ch
);
79 void quote (const(char)[] s
) {
80 static immutable string hexd
= "0123456789abcdef";
82 bool goodString
= true;
83 foreach (char ch
; s
) if (ch
< ' ' || ch
== 127 || ch
== '"' || ch
== '\\') { goodString
= false; break; }
90 while (pos
< s
.length
) {
92 while (epos
< s
.length
) {
93 auto ch
= s
.ptr
[epos
];
94 if (ch
< ' ' || ch
== 127 || ch
== '"' || ch
== '\\') break;
101 if (pos
< s
.length
) {
102 auto ch
= s
.ptr
[pos
++];
103 if (ch
< ' ' || ch
== 127 || ch
== '"' || ch
== '\\') {
105 case '\x1b': xput(`\e`); break;
106 case '\r': xput(`\r`); break;
107 case '\n': xput(`\n`); break;
108 case '\t': xput(`\t`); break;
109 case '"': case '\\': xput(`\`); xput(ch
); break;
112 xput(hexd
[(ch
>>4)&0x0f]);
127 foreach (immutable _
; 0..indent
) xput(' ');
130 void serData(T
) (in ref T v
, bool skipstructname
) {
131 alias UT
= arrayElementType
!T
;
132 static if (is(T
: const(char)[])) {
134 static if (is(T
== string
) ||
is(T
== const(char)[])) {
135 if (v
.ptr
is null) xput("null"); else quote(v
);
139 } else static if (is(T
: V
[], V
)) {
144 foreach (immutable idx
, const ref it
; v
) {
145 if (idx
== 0 ||
(v
.length
>= 56 && idx
%42 == 41)) newline();
147 if (idx
!= v
.length
-1) xput(",");
155 } else static if (is(T
: V
[K
], K
, V
)) {
161 foreach (const kv
; v
.byKeyValue
) {
163 serData(kv
.key
, true);
165 serData(kv
.value
, true);
166 if (--len
) xput(",");
174 } else static if (isCharType
!UT
) {
175 import std
.conv
: to
;
176 xput((cast(uint)v
).to
!string
);
177 } else static if (is(UT
== enum)) {
178 bool enumFound
= false;
179 foreach (string fldname
; __traits(allMembers
, UT
)) {
180 if (v
== __traits(getMember
, UT
, fldname
)) {
189 import std
.conv
: to
;
192 } else static if (isSimpleType
!UT
) {
193 import std
.conv
: to
;
195 } else static if (is(UT
== struct)) {
196 import std
.traits
: FieldNameTuple
, getUDAs
, hasUDA
;
197 if (skipstructname
) {
204 bool needComma
= false;
205 foreach (string fldname
; FieldNameTuple
!UT
) {
206 static if (!hasUDA
!(__traits(getMember
, UT
, fldname
), SRZIgnore
)) {
207 enum names
= getUDAs
!(__traits(getMember
, UT
, fldname
), SRZName
);
208 static if (names
.length
) enum xname
= names
[0].name
; else enum xname
= fldname
;
209 static assert(xname
.length
<= 255, "struct '"~UT
.stringof
~"': field name too long: "~xname
);
210 static if (hasUDA
!(__traits(getMember
, UT
, fldname
), SRZNonDefaultOnly
)) {
211 if (__traits(getMember
, v
, fldname
) == __traits(getMember
, v
, fldname
).init
) continue;
213 if (needComma
) xput(",");
217 serData(__traits(getMember
, v
, fldname
), true);
225 static assert(0, "can't serialize type '"~T
.stringof
~"'");
229 serData(v
, skipstname
);
233 // ////////////////////////////////////////////////////////////////////////// //
235 public enum isGoodSerParser(T
) = is(typeof((inout int=0) {
244 const(char)[] s
= t
.expectId
!true();
245 s
= t
.expectId
!false;
246 b
= T
.isGoodIdChar(' ');
248 int d
= T
.digitInBase('1', 10);
250 t
.parseString
!true(delegate (char ch
) {});
251 t
.parseString
!false(delegate (char ch
) {});
256 public struct TxtSerParser(ST
) if (isReadableStream
!ST ||
(isInputRange
!ST
&& is(Unqual
!(ElementEncodingType
!ST
) == char))) {
259 int eotflag
; // 0: not; 1: at peek; -1: at front; -2: done
260 // buffer for identifier reading
263 static if (isReadableStream
!ST
) {
264 enum AsStream
= true;
268 enum AsStream
= false;
276 this() (auto ref ST stream
) {
285 void error (string msg
) { import std
.conv
: to
; throw new Exception(msg
~" around line "~line
.to
!string
~", column "~col
.to
!string
); }
287 @property bool eot () const pure nothrow @safe @nogc { pragma(inline
, true); return (eotflag
< -1); }
290 if (eotflag
< 0) { curch
= peek
= 0; eotflag
= -2; return; }
291 if (curch
== '\n') { ++line
; col
= 1; } else ++col
;
293 if (eotflag
> 0) { eotflag
= -1; peek
= 0; return; }
294 // read next char to `peek`
295 static if (AsStream
) {
296 if (rdpos
>= rdused
) {
297 auto read
= st
.rawRead(rdbuf
[]);
298 if (read
.length
== 0) {
304 rdused
= cast(uint)read
.length
;
306 assert(rdpos
< rdused
);
307 peek
= rdbuf
.ptr
[rdpos
++];
321 if ((curch
== '/' && peek
== '/') || curch
== '#') {
322 while (!eot
&& curch
!= '\n') skipChar();
323 } else if (curch
== '/' && peek
== '*') {
327 if (curch
== '*' && peek
== '/') {
333 } else if (curch
== '/' && peek
== '+') {
338 if (curch
== '+' && peek
== '/') {
341 if (--level
== 0) break;
342 } else if (curch
== '/' && peek
== '+') {
348 } else if (curch
> ' ') {
356 void expectChar (char ch
) {
358 if (eot || curch
!= ch
) error("'"~ch
~"' expected");
362 const(char)[] expectId(bool allowQuoted
=false) () {
365 static if (allowQuoted
) {
366 if (!eot
&& curch
== '"') {
368 while (!eot
&& curch
!= '"') {
369 if (curch
== '\\') error("simple string expected");
370 if (bpos
>= buf
.length
) error("identifier or number too long");
371 buf
.ptr
[bpos
++] = curch
;
374 if (eot || curch
!= '"') error("simple string expected");
379 if (!isGoodIdChar(curch
)) error("identifier or number expected");
380 while (isGoodIdChar(curch
)) {
381 if (bpos
>= buf
.length
) error("identifier or number too long");
388 // `curch` is opening quote
389 void parseString(bool allowEscapes
) (scope void delegate (char ch
) put
) {
390 assert(put
!is null);
391 if (eot
) error("unterminated string");
394 while (!eot
&& curch
!= qch
) {
395 static if (allowEscapes
) if (curch
== '\\') {
397 if (eot
) error("unterminated string");
399 case '0': // oops, octal
401 foreach (immutable _
; 0..4) {
402 if (eot
) error("unterminated string");
403 int dig
= digitInBase(curch
, 10);
405 if (dig
> 7) error("invalid octal escape");
409 if (ucc
> 255) error("invalid octal escape");
412 case '1': .. case '9': // decimal
414 foreach (immutable _
; 0..3) {
415 if (eot
) error("unterminated string");
416 int dig
= digitInBase(curch
, 10);
421 if (ucc
> 255) error("invalid decimal escape");
424 case 'e': put('\x1b'); skipChar(); break;
425 case 'r': put('\r'); skipChar(); break;
426 case 'n': put('\n'); skipChar(); break;
427 case 't': put('\t'); skipChar(); break;
428 case '"': case '\\': case '\'': put(curch
); skipChar(); break;
431 if (eot
) error("unterminated string");
432 if (digitInBase(peek
, 16) < 0) error("invalid hex escape");
433 skipChar(); // skip 'x'
434 if (eot
) error("unterminated string");
435 if (digitInBase(curch
, 16) < 0 ||
digitInBase(peek
, 16) < 0) error("invalid hex escape");
436 put(cast(char)(digitInBase(curch
, 16)*16+digitInBase(peek
, 16)));
442 if (digitInBase(peek
, 16) < 0) error("invalid unicode escape");
444 skipChar(); // skip 'u'
445 foreach (immutable _
; 0..4) {
446 if (eot
) error("unterminated string");
447 if (digitInBase(curch
, 16) < 0) break;
448 ucc
= ucc
*16+digitInBase(curch
, 16);
452 auto len
= utf8Encode(buf
[], cast(dchar)ucc
);
454 if (len
< 0) error("invalid utf-8 escape");
455 foreach (char ch
; buf
[0..len
]) put(ch
);
457 default: error("invalid escape");
465 if (eot || curch
!= qch
) error("unterminated string");
469 static pure nothrow @safe @nogc:
470 bool isGoodIdChar (char ch
) {
471 pragma(inline
, true);
473 (ch
>= '0' && ch
<= '9') ||
474 (ch
>= 'A' && ch
<= 'Z') ||
475 (ch
>= 'a' && ch
<= 'z') ||
476 ch
== '_' || ch
== '-' || ch
== '+' || ch
== '.';
479 int digitInBase (char ch
, int base
) {
480 pragma(inline
, true);
482 base
>= 1 && ch
>= '0' && ch
< '0'+base ? ch
-'0' :
483 base
> 10 && ch
>= 'A' && ch
< 'A'+base
-10 ? ch
-'A'+10 :
484 base
> 10 && ch
>= 'a' && ch
< 'a'+base
-10 ? ch
-'a'+10 :
490 static assert(isGoodSerParser
!(TxtSerParser
!VFile
));
493 // ////////////////////////////////////////////////////////////////////////// //
495 public void txtunser(bool ignoreUnknown
=false, T
, ST
) (out T v
, auto ref ST fl
)
496 if (!is(T
== class) && (isReadableStream
!ST ||
(isInputRange
!ST
&& is(Unqual
!(ElementEncodingType
!ST
) == char))))
498 auto par
= TxtSerParser
!ST(fl
);
499 txtunser
!ignoreUnknown(v
, par
);
504 public void txtunser(bool ignoreUnknown
=false, T
, ST
) (out T v
, auto ref ST par
) if (!is(T
== class) && isGoodSerParser
!ST
) {
505 import std
.traits
: Unqual
;
509 if (par
.curch
== ',') par
.skipChar();
512 static if (ignoreUnknown
) void skipData(bool fieldname
) () {
515 if (par
.curch
== '"' || par
.curch
== '\'') {
516 par
.parseString
!(!fieldname
)(delegate (char ch
) {});
519 static if (!fieldname
) {
521 if (par
.curch
== '[') {
525 if (par
.eot
) par
.error("unterminated array");
526 if (par
.curch
== ']') break;
534 if (par
.curch
== '{') {
538 if (par
.eot
) par
.error("unterminated array");
539 if (par
.curch
== '}') break;
540 skipData
!true(); // field name
551 if (par
.eot ||
!par
.isGoodIdChar(par
.curch
)) par
.error("invalid identifier");
552 while (!par
.eot
&& par
.isGoodIdChar(par
.curch
)) par
.skipChar();
555 void unserData(T
) (out T v
) {
556 if (par
.eot
) par
.error("data expected");
557 static if (is(T
: const(char)[])) {
559 static if (__traits(isStaticArray
, T
)) {
562 if (v
.length
-dpos
< 1) par
.error("value too long");
566 void put (char ch
) { v
~= ch
; }
569 // `null` is empty string
570 if (par
.curch
!= '"' && par
.curch
!= '\'') {
571 if (!par
.isGoodIdChar(par
.curch
)) par
.error("string expected");
573 while (par
.isGoodIdChar(par
.curch
)) {
577 if (ss
!= "null") foreach (char ch
; ss
) put(ch
);
580 assert(par
.curch
== '"' || par
.curch
== '\'');
581 par
.parseString
!true(&put
);
583 } else static if (is(T
: V
[], V
)) {
586 if (par
.curch
== '{') {
588 static if (__traits(isStaticArray
, T
)) {
589 if (v
.length
== 0) par
.error("array too small");
594 } else if (par
.curch
== 'n') {
595 // this should be 'null'
596 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'u') par
.error("'null' expected");
597 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'l') par
.error("'null' expected");
598 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'l') par
.error("'null' expected");
599 par
.skipChar(); if (!par
.eot
&& par
.isGoodIdChar(par
.curch
)) par
.error("'null' expected");
600 static if (__traits(isStaticArray
, T
)) if (v
.length
!= 0) par
.error("static array too big");
603 static if (__traits(isStaticArray
, T
)) {
604 foreach (ref it
; v
) {
606 if (par
.eot || par
.curch
== ']') break;
613 if (par
.eot || par
.curch
== ']') break;
621 } else static if (is(T
: V
[K
], K
, V
)) {
628 if (par
.eot || par
.curch
== '}') break;
633 if (par
.curch
== 'n' && par
.peek
== 'u') {
634 par
.skipChar(); // skip 'n'
635 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'l') par
.error("'null' expected");
636 par
.skipChar(); if (!par
.eot
&& par
.curch
!= 'l') par
.error("'null' expected");
637 par
.skipChar(); if (!par
.eot
&& par
.isGoodIdChar(par
.curch
)) par
.error("'null' expected");
638 continue; // skip null value
646 } else static if (isCharType
!T
) {
647 import std
.conv
: to
;
648 auto id
= par
.expectId
;
651 } catch (Exception e
) {
652 par
.error("type conversion error for type '"~T
.stringof
~"' ("~id
.idup
~")");
654 } else static if (isSimpleType
!T
) {
655 import std
.conv
: to
;
656 auto id
= par
.expectId
;
657 // try bool->int conversions
658 static if ((is(T
: ulong) ||
is(T
: real)) && is(typeof((){v
=0;})) && is(typeof((){v
=1;}))) {
660 if (id
== "true") { v
= 1; return; }
661 if (id
== "false") { v
= 0; return; }
665 } catch (Exception e
) {
666 par
.error("type conversion error for type '"~T
.stringof
~"' ("~id
.idup
~")");
668 } else static if (is(T
== struct)) {
670 import std
.traits
: FieldNameTuple
, getUDAs
, hasUDA
;
673 if (par
.curch
!= '{') {
674 auto nm
= par
.expectId
!true();
675 if (nm
!= (Unqual
!T
).stringof
) par
.error("'"~(Unqual
!T
).stringof
~"' struct expected, but got '"~nm
.idup
~"'");
680 ulong[(FieldNameTuple
!T
.length
+ulong.sizeof
-1)/ulong.sizeof
] fldseen
= 0;
682 bool tryField(uint idx
, string fldname
) (const(char)[] name
) {
683 static if (hasUDA
!(__traits(getMember
, T
, fldname
), SRZName
)) {
684 enum names
= getUDAs
!(__traits(getMember
, T
, fldname
), SRZName
);
686 alias tuple(T
...) = T
;
687 enum names
= tuple
!(SRZName(fldname
));
689 foreach (immutable xname
; names
) {
690 if (xname
.name
== name
) {
691 if (fldseen
[idx
/8]&(1UL<<(idx
%8))) throw new Exception(`duplicate field value for '`~fldname
~`'`);
692 fldseen
[idx
/8] |
= 1UL<<(idx
%8);
693 unserData(__traits(getMember
, v
, fldname
));
700 void tryAllFields (const(char)[] name
) {
701 foreach (immutable idx
, string fldname
; FieldNameTuple
!T
) {
702 static if (!hasUDA
!(__traits(getMember
, T
, fldname
), SRZIgnore
)) {
703 if (tryField
!(idx
, fldname
)(name
)) return;
706 static if (ignoreUnknown
) {
709 throw new Exception("unknown field '"~name
.idup
~"'");
713 // let's hope that fields are in order (it is nothing wrong with seeing 'em in wrong order, though)
714 static if (ignoreUnknown
) {
715 while (par
.curch
!= '}') {
716 if (par
.curch
== 0) break;
717 auto name
= par
.expectId
!true();
723 foreach (immutable idx
, string fldname
; FieldNameTuple
!T
) {
724 static if (!hasUDA
!(__traits(getMember
, T
, fldname
), SRZIgnore
)) {
726 if (par
.curch
== '}') break;
727 auto name
= par
.expectId
!true();
729 if (!tryField
!(idx
, fldname
)(name
)) tryAllFields(name
);
743 // ////////////////////////////////////////////////////////////////////////// //
744 private static bool isValidDC (dchar c
) pure nothrow @safe @nogc { pragma(inline
, true); return (c
< 0xD800 ||
(c
> 0xDFFF && c
<= 0x10FFFF)); } /// is given codepoint valid?
746 /// returns -1 on error (out of room in `s`, for example), or bytes taken
747 private int utf8Encode(dchar replacement
='\uFFFD') (char[] s
, dchar c
) pure nothrow @trusted @nogc {
748 static assert(isValidDC(replacement
), "invalid replacement char");
749 if (!isValidDC(c
)) c
= replacement
;
751 if (s
.length
< 1) return -1;
752 s
.ptr
[0] = cast(char)c
;
758 buf
.ptr
[0] = cast(char)(0xC0|
(c
>>6));
759 buf
.ptr
[1] = cast(char)(0x80|
(c
&0x3F));
761 } else if (c
<= 0xFFFF) {
762 buf
.ptr
[0] = cast(char)(0xE0|
(c
>>12));
763 buf
.ptr
[1] = cast(char)(0x80|
((c
>>6)&0x3F));
764 buf
.ptr
[2] = cast(char)(0x80|
(c
&0x3F));
766 } else if (c
<= 0x10FFFF) {
767 buf
.ptr
[0] = cast(char)(0xF0|
(c
>>18));
768 buf
.ptr
[1] = cast(char)(0x80|
((c
>>12)&0x3F));
769 buf
.ptr
[2] = cast(char)(0x80|
((c
>>6)&0x3F));
770 buf
.ptr
[3] = cast(char)(0x80|
(c
&0x3F));
775 if (s
.length
< len
) return -1;
776 s
[0..len
] = buf
[0..len
];
782 // ////////////////////////////////////////////////////////////////////////// //
783 version(egserial_test
) unittest {
784 version(no_vfs
) import std
.stdio
; else { import iv
.vfs
.io
; import iv
.vfs
.streams
; }
786 version(no_vfs
) static struct InRng
{
788 @property char front () { return src
[0]; }
789 @property bool empty () { return (src
.length
== 0); }
790 void popFront () { src
= src
[1..$]; }
793 char[] s2s(T
) (in auto ref T v
) {
796 static struct OutRng
{
798 //void put (const(char)[] ch...) { *dest ~= ch; }
799 void put (char ch
) { *dest
~= ch
; }
801 auto or = OutRng(&res
);
806 auto buf
= MemoryStreamRWRef(res
);
808 auto fl
= wrapStream(buf
);
811 return cast(char[])*buf
.bytes
;
815 // ////////////////////////////////////////////////////////////////////////// //
816 static struct AssemblyInfo
{
819 @SRZIgnore uint ignoreme
;
822 static struct ReplyAsmInfo
{
823 @SRZName("command") @SRZName("xcommand") ubyte cmd
;
824 @SRZName("values") AssemblyInfo
[][2] list
;
826 @SRZNonDefaultOnly bool fbool
;
831 // ////////////////////////////////////////////////////////////////////////// //
835 ri
.list
[0] ~= AssemblyInfo(665, "limbo");
836 ri
.list
[1] ~= AssemblyInfo(69, "pleasure");
838 ri
.dict
["boo"] = 665;
846 xf
.txtunser(InRng(srs
));
848 xf
.txtunser(wrapMemoryRO(srs
));
850 //assert(fl.tell == fl.size);
851 assert(xf
.cmd
== 42);
852 assert(xf
.list
.length
== 2);
853 assert(xf
.list
[0].length
== 1);
854 assert(xf
.list
[1].length
== 1);
855 assert(xf
.list
[0][0].id
== 665);
856 assert(xf
.list
[0][0].name
== "limbo");
857 assert(xf
.list
[1][0].id
== 69);
858 assert(xf
.list
[1][0].name
== "pleasure");
859 assert(xf
.dict
.length
== 2);
860 assert(xf
.dict
["foo"] == 42);
861 assert(xf
.dict
["boo"] == 665);
862 //assert(xf.fbool == true);
863 assert(xf
.fbool
== false);
864 assert(xf
.ext
== "elf");
871 "Alice".txtser(stdout
);
876 import std
.utf
: byChar
;
881 boo
.txtunser("{ n:true }".byChar
);
887 foreach (immutable idx, ref v; n) v = idx;