1 /// Contains everything regarding archives.
2 module jaster.serialise.archive;
3 
4 private
5 {
6     import std.algorithm    : countUntil;
7     import std.exception    : enforce;
8     import std.format       : format;
9     import std.variant      : Algebraic, This;
10     import std.traits       : isNumeric, allSameType;
11 }
12 
13 /// An `Algebraic` which determines the types of data that archives can work with.
14 alias ArchiveValue = Algebraic!
15 (
16     bool,  typeof(null), ubyte[], This[], // Special
17     byte,  short,        int,     long,   // Signed
18     ubyte, ushort,       uint,    ulong,  // Unsigned
19     string,                               // Text
20     float, double                         // Floating point
21 );
22 
23 /++
24  + The base class for an archive.
25  +
26  + Details:
27  +  An archive can be seen as a serialiser/deserialiser for `ArchiveObject`s.
28  +
29  +  Essentially, this class is used to provide the ability to write and read objects into/from a certain format,
30  +  e.g. SDLang, XML, $(I ini) maybe(if you try hard enough), and custom binary formats.
31  +
32  +  The reason that archives exists is primarily so only one serialiser/deserialiser for more complex types (structs/classes) has to be
33  +  written, as `ArchiveObject` is flexible enough to represent mostly everything you'd need to, it is then up to the `Archive` to provide
34  +  the actual format to store it in.
35  +
36  +  For example, the serialiser/deserialiser provided by default could be used to serialise/deserialise to SDLang, XML, binary, etc. without any
37  +  extra effort, since all of those details are left up to the `Archive`, all the serialiser has to do is modify the archive's `Archive.root` to
38  +  what it wants.
39  + ++/
40 abstract class Archive
41 {
42     public abstract
43     {
44         /++
45          + Saves the data in `Archive.root` into memory.
46          +
47          + Notes:
48          +  Since not all archives may use text, but instead binary, no assumptions can be made so
49          +  the return type is a ubyte[].
50          +
51          +  See the helper function `saveToMemoryText` for an easy way to get this data as text.
52          +
53          + Returns:
54          +  The data a byte array.
55          + ++/
56         const(ubyte[]) saveToMemory();
57 
58         /++
59          + Loads the given data and modifies `Archive.root` to represent this data.
60          +
61          + Params:
62          +  data = The data to load.
63          + ++/
64         void loadFromMemory(const ubyte[] data);
65 
66         /++
67          + The root of the archive's data.
68          +
69          + Notes:
70          +  When saving, this is the object that is used as the root of tha data.
71          +
72          +  When loading, this is the object that is the root of the data.
73          +
74          + Returns:
75          +  The root of the archive's data.
76          + ++/
77         @property
78         ArchiveObject root();
79     }
80 
81     // ####################################
82     // # HELPER FUNCS. CAN BE OVERRIDDEN. #
83     // ####################################
84     public
85     {
86         /++
87          + A helpful alternative to `saveToMemory`, where the data
88          + is casted to a `const(char[])`, then passed to `std.utf.validate`,
89          + before being returned.
90          +
91          + Returns:
92          +  The archive's data, validated as valid UTF-8 text.
93          + ++/
94         const(char[]) saveToMemoryText()
95         {
96             import std.utf : validate;
97             auto data = cast(const(char[]))this.saveToMemory();
98             data.validate();
99 
100             return data;
101         }
102 
103         /++
104          + Saves the archive's data to a file.
105          +
106          + Params:
107          +  path = The path to save to.
108          + ++/
109         void saveToFile(in char[] path)
110         {
111             import std.file : write;
112 
113             write(path, this.saveToMemory());
114         }
115 
116         /++
117          + Loads the archive's data from a file.
118          +
119          + Params:
120          +  path = The path to load from.
121          + ++/
122         void loadFromFile(in char[] path)
123         {
124             import std.file : read;
125 
126             return this.loadFromMemory(cast(ubyte[])read(path));
127         }
128     }
129 }
130 
131 /++
132  + This class contains the data about an object in the archive.
133  +
134  + Design:
135  +  This class is modeled after sdlang-d, which feels mostly natural and comfortable to work with.
136  +
137  +  This class should be able to represent most kinds of objects, but will undoubtably be unsuitable for some.
138  +
139  + Values:
140  +  These are `ArchiveValues` that are directly attached to the object.
141  +
142  + Attributes:
143  +  These are named `ArchiveValues` that describe certain features of the object(e.g 'IsHidden=true', 'IsDirectory=false').
144  +  (You can of course, use your own meaning for these).
145  +
146  + Children:
147  +  These are `ArchiveObjects` that are children of the current object.
148  +
149  +  For example, there's the `Archive.root` object which is the root of all of the archive's data,
150  +  and then children would be used to describe more complex objects from the root.
151  +
152  + Issues:
153  +  There is currently no tracking of parentship of objects, so it's possible to have circular
154  +  references.
155  + ++/
156 class ArchiveObject
157 {
158     /// Describes an attribute.
159     struct Attribute
160     {
161         /// The name of the attribute.
162         string name;
163 
164         /// The value of the attribute.
165         ArchiveValue value;
166     }
167 
168     // #####################################
169     // # CTOR, PUBLIC VARS, AND PROPERTIES # 
170     // #####################################
171     public final
172     {
173         /// The name of this object.
174         string          name;
175 
176         /// The attributes of this object.
177         Attribute[]     attributes;
178 
179         /// The values of this object.
180         ArchiveValue[]  values;
181         
182         /// The children of this object.
183         ArchiveObject[] children;
184 
185         /++
186          + Params:
187          +  name = The name of this object.
188          + ++/
189         this(string name = null)
190         {
191             this.name = name;
192         }
193     }
194 
195     // ##############################
196     // # CAN BE OVERIDDEN IF NEEDED #
197     // ##############################
198     public
199     {
200         /++
201          + Sets/Creates an attribute's value.
202          +
203          + Params:
204          +  name   = The name of the attribute.
205          +  attrib = The value of the attribute.
206          + ++/
207         void setAttribute(string name, ArchiveValue attrib)
208         {
209             auto index = this.attributes.countUntil!"a.name == b"(name);
210 
211             if(index == -1)
212                 this.attributes ~= Attribute(name, attrib);
213             else
214                 this.attributes[index].value = attrib;
215         }
216 
217         /++
218          + Adds a value to this object.
219          + 
220          + Params:
221          +  value = The value to add.
222          + ++/
223         void addValue(ArchiveValue value)
224         {
225             this.values ~= value;
226         }
227 
228         /++
229          + Adds a child to this object, it's `ArchiveObject.name` can be used
230          + to retrieve it from `ArchiveObject.getChild`.
231          +
232          + Notes:
233          +  While multiple children with the same name can be added,
234          +  only the first one with the given `ArchiveObject.name` will be used
235          +  via `ArchiveObject.getChild`.
236          +
237          + Params:
238          +  child = The object to add as a child.
239          + ++/
240         void addChild(ArchiveObject child)
241         {
242             assert(child !is null, "The child cannot be null");
243             this.children ~= child;
244         }
245 
246         /++
247          + Gets an attribute by name.
248          +
249          + Params:
250          +  name     = The name of the attribute.
251          +  default_ = The value to return if no attribute with `name` exists.
252          +
253          + Returns:
254          +  Either the attribute called `name`, or `default_`.
255          + ++/
256         ArchiveValue getAttribute(string name, lazy ArchiveValue default_ = ArchiveValue.init)
257         {
258             auto index = this.attributes.countUntil!"a.name == b"(name);
259             return (index == -1) ? default_ : this.attributes[index].value;
260         }
261 
262         /++
263          + Gets a value by it's index.
264          +
265          + Params:
266          +  index    = The index of the value to get.
267          +  default_ = The value to return if the index is out of bounds.
268          +
269          + Returns:
270          +  Either the value at `index`, or `default_` if `index` is out of bounds.
271          + ++/
272         ArchiveValue getValue(size_t index, lazy ArchiveValue default_ = ArchiveValue.init)
273         {
274             return (index >= this.values.length) ? default_ : this.values[index];
275         }
276 
277         /++
278          + Gets an child by name.
279          +
280          + Notes:
281          +  While multiple children with the same name can be added,
282          +  only the first one with the given `ArchiveObject.name` will be used
283          +  via `ArchiveObject.getChild`.
284          +
285          + Params:
286          +  name     = The name of the child.
287          +  default_ = The value to return if no child with `name` exists.
288          +
289          + Returns:
290          +  Either the child called `name`, or `default_`.
291          + ++/
292         ArchiveObject getChild(string name, lazy ArchiveObject default_ = null)
293         {
294             auto index = this.children.countUntil!"a.name == b"(name);
295             return (index == -1) ? default_ : this.children[index];
296         }
297     }
298 
299     // ####################
300     // # HELPER FUNCTIONS #
301     // ####################
302     public final
303     {
304         /++
305          + Helper function to add multiple `ArchiveValue`s.
306          + ++/
307         void addValues(ArchiveValue[] values)
308         {
309             foreach(value; values)
310                 this.addValue(value);
311         }
312 
313         /++
314          + Helper function to set an attribute without having to create an `ArchiveValue`.
315          + ++/
316         void setAttributeAs(T)(string name, T attrib)
317         if(ArchiveValue.allowed!T && !is(T == ArchiveValue))
318         {
319             this.setAttribute(name, ArchiveValue(attrib));
320         }
321 
322         /++
323          + Helper function to add a Value without having to create an `ArchiveValue`.
324          + ++/
325         void addValueAs(T)(T value)
326         if(ArchiveValue.allowed!T && !is(T == ArchiveValue))
327         {
328             this.addValue(ArchiveValue(value));
329         }
330 
331         /++
332          + Helper function to get an attribute/value as a certain type.
333          +
334          + Notes:
335          +  Other than making it easier to transform the `ArchiveValue` into the given type,
336          +  this function performs an extra step.
337          +
338          +  Imagine if the value has a type of `ubyte`, but you want it as a `uint`.
339          +
340          +  Doing `getAttribute("blah").get!uint` would actually give you an error.
341          +
342          +  This function will automatically use the `coerce` function to make sure you'll
343          +  get a `uint` even if the type stored is a `ubyte`.
344          + ++/
345         T getAttributeAs(T)(string name, lazy T default_ = T.init)
346         {
347             return this.convertFromValue!T(this.getAttribute(name, ArchiveValue.init), default_);
348         }
349 
350         /// ditto
351         T getValueAs(T)(size_t index, lazy T default_ = T.init)
352         {
353             return this.convertFromValue!T(this.getValue(index, ArchiveValue.init), default_);
354         }
355 
356         /++
357          + Helper function to get a child, or throw an exception if the child doesn't exist.
358          +
359          + Params:
360          +  name = The name of the child to get.
361          +
362          + Returns:
363          +  The child.
364          + ++/
365         ArchiveObject expectChild(string name)
366         {
367             auto obj = this.getChild(name, null);
368             enforce(obj !is null, "The object '" ~ name ~ "' doesn't exist.");
369 
370             return obj;
371         }
372 
373         /++
374          + Helper function to get a attribute, or throw an exception if the attribute doesn't exist.
375          +
376          + Params:
377          +  name = The name of the attribute to get.
378          +
379          + Returns:
380          +  The attribute.
381          + ++/
382         ArchiveValue expectAttribute(string name)
383         {
384             auto attrib = this.getAttribute(name, ArchiveValue.init);
385             enforce(attrib != ArchiveValue.init, "The attribute '" ~ name ~ "' doesn't exist.");
386 
387             return attrib;
388         }
389 
390         /// ditto
391         T expectAttributeAs(T)(string name)
392         {
393             return this.convertFromValue!T(this.expectAttribute(name), T.init);
394         }
395 
396         /++
397          + Helper function to get a value, or throw an exception if the value doesn't exist.
398          +
399          + Params:
400          +  index = The index of the value to get.
401          +
402          + Returns:
403          +  The value.
404          + ++/
405         ArchiveValue expectValue(size_t index)
406         {
407             auto value = this.getValue(index, ArchiveValue.init);
408             enforce(value != ArchiveValue.init, "The value at index %s doesn't exist. Value count = %s".format(index, this.values.length));
409 
410             return value;
411         }
412 
413         /// ditto
414         T expectValueAs(T)(size_t index)
415         {
416             return this.convertFromValue!T(this.expectValue(index), T.init);
417         }
418     }
419 
420     // ######################
421     // # OPERATOR OVERLOADS #
422     // ######################
423     public final
424     {
425         /// An operator version of `ArchiveObject.expectChild`.
426         ArchiveObject opIndex(string childName)
427         {
428             return this.expectChild(childName);
429         }
430         ///
431         unittest
432         {
433             auto obj = new ArchiveObject();
434             obj.addChild(new ArchiveObject("Foo"));
435 
436             obj["Foo"].addValueAs!int(69);
437             assert(obj["Foo"].getValueAs!int(0) == 69);
438         }
439 
440         /// An operator version of `ArchiveObject.expectChild` that takes multiple child names.
441         ArchiveObject opIndex(Names...)(Names childNames)
442         if(Names.length > 0 && is(Names[0] : const(char)[]) && allSameType!Names)
443         {
444             auto obj = this;
445             foreach(name; childNames)
446                 obj = obj[name];
447 
448             return obj;
449         }
450         ///
451         unittest
452         {
453             auto obj = new ArchiveObject();
454             obj.addChild(new ArchiveObject("Foo"));
455             obj["Foo"].addChild(new ArchiveObject("Bar"));
456             obj["Foo", "Bar"].addChild(new ArchiveObject("Baz"));
457             obj["Foo", "Bar", "Baz"].addValueAs!int(69);
458 
459             assert(obj["Foo", "Bar", "Baz"].getValueAs!int(0) == 69);
460         }
461     }
462 
463     private
464     {
465         T convertFromValue(T)(ArchiveValue value, T default_)
466         {
467             static if(isNumeric!T)
468                 return (value == ArchiveValue.init) ? default_ : value.coerce!T;
469             else
470                 return (value == ArchiveValue.init) ? default_ : value.get!T; 
471         }
472     }
473 }