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 }