/++ A declarative file/stream loader/saver. You define structs with a handful of annotations, this read and writes them to/from files. +/ module arsd.declarativeloader; import std.range; /// enum BigEndian; /// enum LittleEndian; /// @VariableLength indicates the value is saved in a MIDI like format enum VariableLength; /// @NumBytes!Field or @NumElements!Field controls length of embedded arrays struct NumBytes(alias field) {} /// ditto struct NumElements(alias field) {} /// @Tagged!Field indicates a tagged union. Each struct within should have @Tag(X) which is a value of Field struct Tagged(alias field) {} /// ditto auto Tag(T)(T t) { return TagStruct!T(t); } /// For example `@presentIf("version >= 2") int addedInVersion2;` struct presentIf { string code; } struct TagStruct(T) { T t; } struct MustBeStruct(T) { T t; } /// The marked field is not in the actual file enum NotSaved; /// Insists the field must be a certain value, like for magic numbers auto MustBe(T)(T t) { return MustBeStruct!T(t); } static bool fieldSaved(alias a)() { bool saved; static if(is(typeof(a.offsetof))) { saved = true; static foreach(attr; __traits(getAttributes, a)) static if(is(attr == NotSaved)) saved = false; } return saved; } static bool bigEndian(alias a)(bool def) { bool be = def; static foreach(attr; __traits(getAttributes, a)) { static if(is(attr == BigEndian)) be = true; else static if(is(attr == LittleEndian)) be = false; } return be; } static auto getTag(alias a)() { static foreach(attr; __traits(getAttributes, a)) { static if(is(typeof(attr) == TagStruct!T, T)) { return attr.t; } } assert(0); } union N(ty) { ty member; ubyte[ty.sizeof] bytes; } static bool fieldPresent(alias field, T)(T t) { bool p = true; static foreach(attr; __traits(getAttributes, field)) { static if(is(typeof(attr) == presentIf)) { bool p2 = false; with(t) p2 = mixin(attr.code); p = p && p2; } } return p; } /// input range of ubytes... int loadFrom(T, Range)(ref T t, auto ref Range r, bool assumeBigEndian = false) { int bytesConsumed; string currentItem; import std.conv; try { ubyte next() { if(r.empty) throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t)); auto bfr = r.front; r.popFront; bytesConsumed++; return bfr; } bool endianness = bigEndian!T(assumeBigEndian); static foreach(memberName; __traits(allMembers, T)) {{ currentItem = memberName; static if(is(typeof(__traits(getMember, T, memberName)))) { alias f = __traits(getMember, T, memberName); alias ty = typeof(f); static if(fieldSaved!f) if(fieldPresent!f(t)) { endianness = bigEndian!f(endianness); // FIXME VariableLength static if(is(ty : ulong) || is(ty : double)) { N!ty n; if(endianness) { foreach(i; 0 .. ty.sizeof) { version(BigEndian) n.bytes[i] = next(); else n.bytes[$ - 1 - i] = next(); } } else { foreach(i; 0 .. ty.sizeof) { version(BigEndian) n.bytes[$ - 1 - i] = next(); else n.bytes[i] = next(); } } // FIXME: MustBe __traits(getMember, t, memberName) = n.member; } else static if(is(ty == struct)) { bytesConsumed += loadFrom(__traits(getMember, t, memberName), r, endianness); } else static if(is(ty == union)) { static foreach(attr; __traits(getAttributes, ty)) static if(is(attr == Tagged!Field, alias Field)) enum tagField = __traits(identifier, Field); static assert(is(typeof(tagField)), "Unions need a Tagged UDA on the union type (not the member) indicating the field that identifies the union"); auto tag = __traits(getMember, t, tagField); // find the child of the union matching the tag... bool found = false; static foreach(um; __traits(allMembers, ty)) { if(tag == getTag!(__traits(getMember, ty, um))) { bytesConsumed += loadFrom(__traits(getMember, __traits(getMember, t, memberName), um), r, endianness); found = true; } } if(!found) { import std.format; throw new Exception(format("found unknown union tag %s at %s", tag, t)); } } else static if(is(ty == E[], E)) { static foreach(attr; __traits(getAttributes, f)) { static if(is(attr == NumBytes!Field, alias Field)) ulong numBytesRemaining = __traits(getMember, t, __traits(identifier, Field)); else static if(is(attr == NumElements!Field, alias Field)) { ulong numElementsRemaining = __traits(getMember, t, __traits(identifier, Field)); } } static if(is(typeof(numBytesRemaining))) { static if(is(E : const(ubyte)) || is(E : const(char))) { while(numBytesRemaining) { __traits(getMember, t, memberName) ~= next; numBytesRemaining--; } } else { while(numBytesRemaining) { E piece; auto by = loadFrom(e, r, endianness); numBytesRemaining -= by; bytesConsumed += by; __traits(getMember, t, memberName) ~= piece; } } } else static if(is(typeof(numElementsRemaining))) { static if(is(E : const(ubyte)) || is(E : const(char))) { while(numElementsRemaining) { __traits(getMember, t, memberName) ~= next; numElementsRemaining--; } } else static if(is(E : const(ushort))) { while(numElementsRemaining) { ushort n; n = next << 8; n |= next; // FIXME all of this filth __traits(getMember, t, memberName) ~= n; numElementsRemaining--; } } else { while(numElementsRemaining) { //import std.stdio; writeln(memberName); E piece; auto by = loadFrom(piece, r, endianness); numElementsRemaining--; // such a filthy hack, needed for Java's mistake though :( static if(__traits(compiles, piece.takesTwoSlots())) { if(piece.takesTwoSlots()) { __traits(getMember, t, memberName) ~= piece; numElementsRemaining--; } } bytesConsumed += by; __traits(getMember, t, memberName) ~= piece; } } } else static assert(0, "no way to identify length... " ~ memberName); } else static assert(0, ty.stringof); } } }} } catch(Exception e) { throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t), e.file, e.line, e); } return bytesConsumed; } int saveTo(T, Range)(ref T t, ref Range r, bool assumeBigEndian = false) { int bytesWritten; string currentItem; import std.conv; try { void write(ubyte b) { bytesWritten++; static if(is(Range == ubyte[])) r ~= b; else r.put(b); } bool endianness = bigEndian!T(assumeBigEndian); static foreach(memberName; __traits(allMembers, T)) {{ currentItem = memberName; static if(is(typeof(__traits(getMember, T, memberName)))) { alias f = __traits(getMember, T, memberName); alias ty = typeof(f); static if(fieldSaved!f) if(fieldPresent!f(t)) { endianness = bigEndian!f(endianness); // FIXME VariableLength static if(is(ty : ulong) || is(ty : double)) { N!ty n; n.member = __traits(getMember, t, memberName); if(endianness) { foreach(i; 0 .. ty.sizeof) { version(BigEndian) write(n.bytes[i]); else write(n.bytes[$ - 1 - i]); } } else { foreach(i; 0 .. ty.sizeof) { version(BigEndian) write(n.bytes[$ - 1 - i]); else write(n.bytes[i]); } } // FIXME: MustBe } else static if(is(ty == struct)) { bytesWritten += saveTo(__traits(getMember, t, memberName), r, endianness); } else static if(is(ty == union)) { static foreach(attr; __traits(getAttributes, ty)) static if(is(attr == Tagged!Field, alias Field)) enum tagField = __traits(identifier, Field); static assert(is(typeof(tagField)), "Unions need a Tagged UDA on the union type (not the member) indicating the field that identifies the union"); auto tag = __traits(getMember, t, tagField); // find the child of the union matching the tag... bool found = false; static foreach(um; __traits(allMembers, ty)) { if(tag == getTag!(__traits(getMember, ty, um))) { bytesWritten += saveTo(__traits(getMember, __traits(getMember, t, memberName), um), r, endianness); found = true; } } if(!found) { import std.format; throw new Exception(format("found unknown union tag %s at %s", tag, t)); } } else static if(is(ty == E[], E)) { // the numBytesRemaining / numElementsRemaining thing here ASSUMING the // arrays are already the correct size. the struct itself could invariant that maybe foreach(item; __traits(getMember, t, memberName)) { static if(is(typeof(item) == struct)) { bytesWritten += saveTo(item, r, endianness); } else { static struct dummy { typeof(item) i; } dummy d = dummy(item); bytesWritten += saveTo(d, r, endianness); } } } else static assert(0, ty.stringof); } } }} } catch(Exception e) { throw new Exception(T.stringof ~ "." ~ currentItem ~ " save trouble " ~ to!string(t), e.file, e.line, e); } return bytesWritten; } unittest { static struct A { int a; @presentIf("a > 5") int b; int c; @NumElements!c ubyte[] d; } A a; a.loadFrom(cast(ubyte[]) [1, 1, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 6, 7, 8]); assert(a.a == 257); assert(a.b == 7); assert(a.c == 3); assert(a.d == [6,7,8]); a = A.init; a.loadFrom(cast(ubyte[]) [0, 0, 0, 0, 7, 0, 0, 0,1,2,3,4,5,6,7]); assert(a.b == 0); assert(a.c == 7); assert(a.d == [1,2,3,4,5,6,7]); a.a = 44; a.c = 3; a.d = [5,4,3]; ubyte[] saved; a.saveTo(saved); A b; b.loadFrom(saved); assert(a == b); }