1 /++
2 	A declarative file/stream loader/saver. You define structs with a handful of annotations, this read and writes them to/from files.
3 +/
4 module arsd.declarativeloader;
5 
6 import std.range;
7 
8 ///
9 enum BigEndian;
10 ///
11 enum LittleEndian;
12 /// @VariableLength indicates the value is saved in a MIDI like format
13 enum VariableLength;
14 /// @NumBytes!Field or @NumElements!Field controls length of embedded arrays
15 struct NumBytes(alias field) {}
16 /// ditto
17 struct NumElements(alias field) {}
18 /// @Tagged!Field indicates a tagged union. Each struct within should have @Tag(X) which is a value of Field
19 struct Tagged(alias field) {}
20 /// ditto
21 auto Tag(T)(T t) {
22 	return TagStruct!T(t);
23 }
24 /// For example `@presentIf("version >= 2") int addedInVersion2;`
25 struct presentIf { string code; }
26 
27 
28 struct TagStruct(T) { T t; }
29 struct MustBeStruct(T) { T t; }
30 /// The marked field is not in the actual file
31 enum NotSaved;
32 /// Insists the field must be a certain value, like for magic numbers
33 auto MustBe(T)(T t) {
34 	return MustBeStruct!T(t);
35 }
36 
37 static bool fieldSaved(alias a)() {
38 	bool saved;
39 	static if(is(typeof(a.offsetof))) {
40 		saved = true;
41 		static foreach(attr; __traits(getAttributes, a))
42 			static if(is(attr == NotSaved))
43 				saved = false;
44 	}
45 	return saved;
46 }
47 
48 static bool bigEndian(alias a)(bool def) {
49 	bool be = def;
50 	static foreach(attr; __traits(getAttributes, a)) {
51 		static if(is(attr == BigEndian))
52 			be = true;
53 		else static if(is(attr == LittleEndian))
54 			be = false;
55 	}
56 	return be;
57 }
58 
59 static auto getTag(alias a)() {
60 	static foreach(attr; __traits(getAttributes, a)) {
61 		static if(is(typeof(attr) == TagStruct!T, T)) {
62 			return attr.t;
63 		}
64 	}
65 	assert(0);
66 }
67 
68 union N(ty) {
69 	ty member;
70 	ubyte[ty.sizeof] bytes;
71 }
72 
73 static bool fieldPresent(alias field, T)(T t) {
74 	bool p = true;
75 	static foreach(attr; __traits(getAttributes, field)) {
76 		static if(is(typeof(attr) == presentIf)) {
77 			bool p2 = false;
78 			with(t) p2 = mixin(attr.code);
79 			p = p && p2;
80 		}
81 	}
82 	return p;
83 }
84 
85 /// input range of ubytes...
86 int loadFrom(T, Range)(ref T t, auto ref Range r, bool assumeBigEndian = false) {
87 	int bytesConsumed;
88 	string currentItem;
89 
90 	import std.conv;
91 	try {
92 
93 	ubyte next() {
94 		if(r.empty)
95 			throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t));
96 		auto bfr = r.front;
97 		r.popFront;
98 		bytesConsumed++;
99 		return bfr;
100 	}
101 
102 	bool endianness = bigEndian!T(assumeBigEndian);
103 	static foreach(memberName; __traits(allMembers, T)) {{
104 	currentItem = memberName;
105 	static if(is(typeof(__traits(getMember, T, memberName)))) {
106 		alias f = __traits(getMember, T, memberName);
107 		alias ty = typeof(f);
108 		static if(fieldSaved!f)
109 		if(fieldPresent!f(t)) {
110 			endianness = bigEndian!f(endianness);
111 			// FIXME VariableLength
112 			static if(is(ty : ulong) || is(ty : double)) {
113 				N!ty n;
114 				if(endianness) {
115 					foreach(i; 0 .. ty.sizeof) {
116 						version(BigEndian)
117 							n.bytes[i] = next();
118 						else
119 							n.bytes[$ - 1 - i] = next();
120 					}
121 				} else {
122 					foreach(i; 0 .. ty.sizeof) {
123 						version(BigEndian)
124 							n.bytes[$ - 1 - i] = next();
125 						else
126 							n.bytes[i] = next();
127 					}
128 				}
129 
130 				// FIXME: MustBe
131 
132 				__traits(getMember, t, memberName) = n.member;
133 			} else static if(is(ty == struct)) {
134 				bytesConsumed += loadFrom(__traits(getMember, t, memberName), r, endianness);
135 			} else static if(is(ty == union)) {
136 				static foreach(attr; __traits(getAttributes, ty))
137 					static if(is(attr == Tagged!Field, alias Field))
138 						enum tagField = __traits(identifier, Field);
139 				static assert(is(typeof(tagField)), "Unions need a Tagged UDA on the union type (not the member) indicating the field that identifies the union");
140 
141 				auto tag = __traits(getMember, t, tagField);
142 				// find the child of the union matching the tag...
143 				bool found = false;
144 				static foreach(um; __traits(allMembers, ty)) {
145 					if(tag == getTag!(__traits(getMember, ty, um))) {
146 						bytesConsumed += loadFrom(__traits(getMember, __traits(getMember, t, memberName), um), r, endianness);
147 						found = true;
148 					}
149 				}
150 				if(!found) {
151 					import std.format;
152 					throw new Exception(format("found unknown union tag %s at %s", tag, t));
153 				}
154 			} else static if(is(ty == E[], E)) {
155 				static foreach(attr; __traits(getAttributes, f)) {
156 					static if(is(attr == NumBytes!Field, alias Field))
157 						ulong numBytesRemaining = __traits(getMember, t, __traits(identifier, Field));
158 					else static if(is(attr == NumElements!Field, alias Field)) {
159 						ulong numElementsRemaining = __traits(getMember, t, __traits(identifier, Field));
160 					}
161 				}
162 
163 				static if(is(typeof(numBytesRemaining))) {
164 					static if(is(E : const(ubyte)) || is(E : const(char))) {
165 						while(numBytesRemaining) {
166 							__traits(getMember, t, memberName) ~= next;
167 							numBytesRemaining--;
168 						}
169 					} else {
170 						while(numBytesRemaining) {
171 							E piece;
172 							auto by = loadFrom(e, r, endianness);
173 							numBytesRemaining -= by;
174 							bytesConsumed += by;
175 							__traits(getMember, t, memberName) ~= piece;
176 						}
177 					}
178 				} else static if(is(typeof(numElementsRemaining))) {
179 					static if(is(E : const(ubyte)) || is(E : const(char))) {
180 						while(numElementsRemaining) {
181 							__traits(getMember, t, memberName) ~= next;
182 							numElementsRemaining--;
183 						}
184 					} else static if(is(E : const(ushort))) {
185 						while(numElementsRemaining) {
186 							ushort n;
187 							n = next << 8;
188 							n |= next;
189 							// FIXME all of this filth
190 							__traits(getMember, t, memberName) ~= n;
191 							numElementsRemaining--;
192 						}
193 					} else {
194 						while(numElementsRemaining) {
195 							//import std.stdio; writeln(memberName);
196 							E piece;
197 							auto by = loadFrom(piece, r, endianness);
198 							numElementsRemaining--;
199 
200 							// such a filthy hack, needed for Java's mistake though :(
201 							static if(__traits(compiles, piece.takesTwoSlots())) {
202 								if(piece.takesTwoSlots()) {
203 									__traits(getMember, t, memberName) ~= piece;
204 									numElementsRemaining--;
205 								}
206 							}
207 
208 							bytesConsumed += by;
209 							__traits(getMember, t, memberName) ~= piece;
210 						}
211 					}
212 				} else static assert(0, "no way to identify length... " ~ memberName);
213 
214 			} else static assert(0, ty.stringof);
215 		}
216 	}
217 	}}
218 
219 	} catch(Exception e) {
220 		throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t), e.file, e.line, e);
221 	}
222 
223 	return bytesConsumed;
224 }
225 
226 int saveTo(T, Range)(ref T t, ref Range r, bool assumeBigEndian = false) {
227 	int bytesWritten;
228 	string currentItem;
229 
230 	import std.conv;
231 	try {
232 
233 	void write(ubyte b) {
234 		bytesWritten++;
235 		static if(is(Range == ubyte[]))
236 			r ~= b;
237 		else
238 			r.put(b);
239 	}
240 
241 	bool endianness = bigEndian!T(assumeBigEndian);
242 	static foreach(memberName; __traits(allMembers, T)) {{
243 	currentItem = memberName;
244 	static if(is(typeof(__traits(getMember, T, memberName)))) {
245 		alias f = __traits(getMember, T, memberName);
246 		alias ty = typeof(f);
247 		static if(fieldSaved!f)
248 		if(fieldPresent!f(t)) {
249 			endianness = bigEndian!f(endianness);
250 			// FIXME VariableLength
251 			static if(is(ty : ulong) || is(ty : double)) {
252 				N!ty n;
253 				n.member = __traits(getMember, t, memberName);
254 				if(endianness) {
255 					foreach(i; 0 .. ty.sizeof) {
256 						version(BigEndian)
257 							write(n.bytes[i]);
258 						else
259 							write(n.bytes[$ - 1 - i]);
260 					}
261 				} else {
262 					foreach(i; 0 .. ty.sizeof) {
263 						version(BigEndian)
264 							write(n.bytes[$ - 1 - i]);
265 						else
266 							write(n.bytes[i]);
267 					}
268 				}
269 
270 				// FIXME: MustBe
271 			} else static if(is(ty == struct)) {
272 				bytesWritten += saveTo(__traits(getMember, t, memberName), r, endianness);
273 			} else static if(is(ty == union)) {
274 				static foreach(attr; __traits(getAttributes, ty))
275 					static if(is(attr == Tagged!Field, alias Field))
276 						enum tagField = __traits(identifier, Field);
277 				static assert(is(typeof(tagField)), "Unions need a Tagged UDA on the union type (not the member) indicating the field that identifies the union");
278 
279 				auto tag = __traits(getMember, t, tagField);
280 				// find the child of the union matching the tag...
281 				bool found = false;
282 				static foreach(um; __traits(allMembers, ty)) {
283 					if(tag == getTag!(__traits(getMember, ty, um))) {
284 						bytesWritten += saveTo(__traits(getMember, __traits(getMember, t, memberName), um), r, endianness);
285 						found = true;
286 					}
287 				}
288 				if(!found) {
289 					import std.format;
290 					throw new Exception(format("found unknown union tag %s at %s", tag, t));
291 				}
292 			} else static if(is(ty == E[], E)) {
293 
294 				// the numBytesRemaining / numElementsRemaining thing here ASSUMING the
295 				// arrays are already the correct size. the struct itself could invariant that maybe
296 
297 				foreach(item; __traits(getMember, t, memberName)) {
298 					static if(is(typeof(item) == struct)) {
299 						bytesWritten += saveTo(item, r, endianness);
300 					} else {
301 						static struct dummy {
302 							typeof(item) i;
303 						}
304 						dummy d = dummy(item);
305 						bytesWritten += saveTo(d, r, endianness);
306 					}
307 				}
308 
309 			} else static assert(0, ty.stringof);
310 		}
311 	}
312 	}}
313 
314 	} catch(Exception e) {
315 		throw new Exception(T.stringof ~ "." ~ currentItem ~ " save trouble " ~ to!string(t), e.file, e.line, e);
316 	}
317 
318 	return bytesWritten;
319 }
320 
321 unittest {
322 	static struct A {
323 		int a;
324 		@presentIf("a > 5") int b;
325 		int c;
326 		@NumElements!c ubyte[] d;
327 	}
328 
329 	A a;
330 	a.loadFrom(cast(ubyte[]) [1, 1, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 6, 7, 8]);
331 
332 	assert(a.a == 257);
333 	assert(a.b == 7);
334 	assert(a.c == 3);
335 	assert(a.d == [6,7,8]);
336 
337 	a = A.init;
338 
339 	a.loadFrom(cast(ubyte[]) [0, 0, 0, 0, 7, 0, 0, 0,1,2,3,4,5,6,7]);
340 	assert(a.b == 0);
341 	assert(a.c == 7);
342 	assert(a.d == [1,2,3,4,5,6,7]);
343 
344 	a.a = 44;
345 	a.c = 3;
346 	a.d = [5,4,3];
347 
348 	ubyte[] saved;
349 
350 	a.saveTo(saved);
351 
352 	A b;
353 	b.loadFrom(saved);
354 
355 	assert(a == b);
356 }