1 /**
2     DataNode abstraction
3 
4     Copyright: 
5         Copyright © 2020-2026, Inochi2D Project
6     
7     License:
8         $(LINK2 https://github.com/Inochi2D/inochi2d/blob/main/LICENSE, BSD 2-clause License)
9     
10     Authors:
11         Luna Nielsen
12 */
13 module inp.format.node;
14 import inp.format.dict;
15 import inp.format.array;
16 import nulib.collections;
17 import numem.core.traits;
18 import numem.core.meta;
19 import numem.rc;
20 import numem;
21 
22 enum DataNodeType : uint {
23     undefined   = 0,
24     boolean_    = 1,
25     int_        = 2,
26     uint_       = 3,
27     float_      = 4,
28     string_     = 5,
29     array_      = 6,
30     object_     = 7,
31     blob_       = 8,
32 }
33 
34 /**
35     The compile-time $(D DataNodeType) corrosponding to type $(D T).
36 */
37 template dataNodeTypeOf(T) {
38     static if (is(T == bool)) {
39         enum dataNodeTypeOf = DataNodeType.boolean_;
40     } else static if (__traits(isIntegral, T)) {
41         enum dataNodeTypeOf = __traits(isUnsigned, T) ? DataNodeType.uint_ : DataNodeType.int_;
42     } else static if (__traits(isFloating, T)) {
43         enum dataNodeTypeOf = DataNodeType.float_;
44     } else static if (is(T == ubyte[])) {
45         enum dataNodeTypeOf = DataNodeType.blob_;
46     } else static if (is(T : string)) {
47         enum dataNodeTypeOf = DataNodeType.string_;
48     } else static if (is(T == U[], U)) {
49         enum dataNodeTypeOf = DataNodeType.array_;
50     } else static if (is(T == class) || is(T == struct)) {
51         enum dataNodeTypeOf = DataNodeType.object_;
52     } else {
53         enum dataNodeTypeOf = DataNodeType.undefined;
54     }
55 }
56 
57 /**
58     A node containing data for (de)serialization.
59 */
60 struct DataNode {
61 private:
62 @nogc:
63     import nulib.collections.internal.marray : ManagedArray;
64     __gshared DataNode UNDEF = DataNode.init;
65 
66     static
67     union DataNodeStore {
68     @nogc:
69 
70         void* undefined;
71         bool boolean_;
72         long int_; 
73         ulong uint_; 
74         float float_; 
75         string string_;
76         RcArray!DataNode array_; 
77         RcOrderedDictionary!(string, DataNode) object_;
78         ubyte[] blob_;
79     }
80 
81     DataNodeType dataType = DataNodeType.undefined;
82     DataNodeStore dataStore;
83 
84     template isSameType(T) {
85         enum isSameType(U) = is(T == U);
86     }
87 public:
88 
89     /**
90         The type of data stored within the node.
91     */
92     @property DataNodeType type() nothrow pure => dataType;
93 
94     /**
95         Whether the DataNode contains a nil value.
96     */
97     @property bool isNull() nothrow pure => dataType == DataNodeType.undefined;
98 
99     /**
100         Whether the DataNode contains a numeric value.
101     */
102     @property bool isNumber() nothrow pure => dataType >= DataNodeType.int_ && dataType <= DataNodeType.float_;
103 
104     /**
105         Whether the DataNode is an object.
106     */
107     @property bool isObject() nothrow pure => dataType == DataNodeType.object_;
108 
109     /**
110         Whether the DataNode is an array.
111     */
112     @property bool isArray() nothrow pure => dataType == DataNodeType.array_;
113 
114     /**
115         Whether the DataNode is a byte blob.
116     */
117     @property bool isBlob() nothrow pure => dataType == DataNodeType.blob_;
118 
119     /**
120         The text content of the node, or null.
121     */
122     @property string text() nothrow pure => isType(DataNodeType.string_) ? dataStore.string_[] : null;
123 
124     /**
125         A blob of binary data, or null.
126     */
127     @property ubyte[] blob() nothrow pure => isType(DataNodeType.blob_) ? dataStore.blob_[] : null;
128 
129     /**
130         The boolean content of the datanode, or false.
131     */
132     @property bool boolean() nothrow pure => tryCoerce!bool(false);
133 
134     /**
135         The number content of the data node, or NaN
136     */
137     @property float number() nothrow pure => tryCoerce!float(float.nan);
138 
139     /**
140         The key-value pairs in the DataNode object, or null.
141     */
142     @property ref RcOrderedDictionary!(string, DataNode) object() nothrow pure => dataStore.object_;
143 
144     /**
145         The array in the DataNode object, or null.
146     */
147     @property ref RcArray!DataNode array() nothrow pure => dataStore.array_;
148 
149     /// Destructor
150     ~this() @trusted nothrow {
151         switch(dataType) {
152             default:
153                 this.dataType = DataNodeType.undefined;
154                 break;
155             
156             case DataNodeType.string_:
157                 this.dataType = DataNodeType.undefined;
158                 nu_freea(this.dataStore.string_);
159                 break;
160             
161             case DataNodeType.blob_:
162                 this.dataType = DataNodeType.undefined;
163                 nu_freea(this.dataStore.blob_);
164                 break;
165 
166             case DataNodeType.array_:
167                 this.dataType = DataNodeType.undefined;
168                 nogc_trydelete(this.dataStore.array_);
169                 break;
170 
171             case DataNodeType.object_:
172                 this.dataType = DataNodeType.undefined;
173                 nogc_trydelete(this.dataStore.object_);
174                 break;
175         }
176 
177     }
178 
179     /**
180         Creates a new object node.
181     */
182     static DataNode createObject() @trusted nothrow {
183         DataNode v;
184         v.dataType = DataNodeType.object_;
185         v.dataStore.object_ = RcOrderedDictionary!(string, DataNode).make();
186         return v;
187     }
188 
189     /**
190         Creates a new array node.
191     */
192     static DataNode createArray() @trusted nothrow {
193         DataNode v;
194         v.dataType = DataNodeType.array_;
195         v.dataStore.array_ = RcArray!(DataNode).make();
196         return v;
197     }
198     
199     /**
200         Constructs a boolean data node.
201     */
202     this(T)(auto ref T value) @safe nothrow 
203     if (is(T == bool)) {
204         this.dataType = DataNodeType.boolean_;
205         this.dataStore = DataNodeStore(boolean_: cast(bool)value);
206     }
207     
208     /**
209         Constructs a signed integer data node.
210     */
211     this(T)(auto ref T value) @safe nothrow 
212     if (anySatisfy!(isSameType!T, AliasSeq!(byte, short, int, long))) {
213         this.dataType = DataNodeType.int_;
214         this.dataStore = DataNodeStore(int_: cast(long)value);
215     }
216     
217     /**
218         Constructs an unsigned integer data node.
219     */
220     this(T)(auto ref T value) @safe nothrow 
221     if (anySatisfy!(isSameType!T, AliasSeq!(ubyte, ushort, uint, ulong))) {
222         this.dataType = DataNodeType.uint_;
223         this.dataStore = DataNodeStore(uint_: cast(ulong)value);
224     }
225 
226     /**
227         Constructs a floating point data node.
228     */
229     this(T)(auto ref T value) @safe nothrow 
230     if (anySatisfy!(isSameType!T, AliasSeq!(float, double))) {
231         this.dataType = DataNodeType.float_;
232         this.dataStore = DataNodeStore(float_: cast(double)value);
233     }
234 
235     /**
236         Constructs a string data node.
237     */
238     this()(auto ref string value) @safe nothrow {
239         this.dataType = DataNodeType.string_;
240         this.dataStore = DataNodeStore(string_: value.nu_dup());
241     }
242 
243     /**
244         Constructs a string data node.
245     */
246     this()(auto ref ubyte[] value) @safe nothrow {
247         this.dataType = DataNodeType.blob_;
248         this.dataStore = DataNodeStore(blob_: value.nu_dup());
249     }
250 
251     /**
252         Copy constructor
253     */
254     this()(ref return scope typeof(this) rhs) @trusted {
255         this.dataType = rhs.dataType;
256         switch(rhs.dataType) {
257             case DataNodeType.string_:
258                 this.dataStore.string_ = cast(typeof(dataStore.string_))rhs.dataStore.string_.nu_dup();
259                 break;
260 
261             case DataNodeType.blob_:
262                 this.dataStore.blob_ = cast(typeof(dataStore.blob_))rhs.dataStore.blob_.nu_dup();
263                 break;
264             
265             case DataNodeType.array_:
266                 this.dataStore.array_ = cast(typeof(dataStore.array_))rhs.dataStore.array_;
267                 break;
268             
269             case DataNodeType.object_:
270                 this.dataStore.object_ = cast(typeof(dataStore.object_))rhs.dataStore.object_;
271                 break;
272             
273             default:
274                 this.dataStore = cast(typeof(dataStore))rhs.dataStore;
275                 break;
276         }
277     }
278 
279     /**
280         Coerces the value of the DataNode to the given type, if possible.
281     */
282     T tryCoerce(T)(T defaultValue = T.init) nothrow pure {
283         switch(dataType) {
284             case DataNodeType.string_:
285                 static if (is(T == string))
286                     return dataStore.string_;
287                 else
288                     return defaultValue;
289             
290             case DataNodeType.boolean_:
291                 static if (__traits(isScalar, T))
292                     return cast(T)dataStore.boolean_;
293                 else
294                     return defaultValue;
295             
296             case DataNodeType.uint_:
297                 static if (isNumeric!T)
298                     return cast(T)dataStore.uint_;
299                 else
300                     return defaultValue;
301             
302             case DataNodeType.int_:
303                 static if (isNumeric!T)
304                     return cast(T)dataStore.int_;
305                 else
306                     return defaultValue;
307             
308             case DataNodeType.float_:
309                 static if (isNumeric!T)
310                     return cast(T)dataStore.float_;
311                 else
312                     return defaultValue;
313             
314             default:
315                 return defaultValue;
316         }
317     }
318 
319     /**
320         Gets whether this DataNode contains data of the given type.
321 
322         Params:
323             type = The type to check for.
324         
325         Returns:
326             $(D true) if the type of the data in the node matches,
327             $(D false) otherwise.
328     */
329     bool isType(inout(DataNodeType) type) inout nothrow pure => this.dataType == type;
330 
331     /**
332         Length of the node.
333     */
334     @property size_t length() {
335         switch(dataType) {
336             case DataNodeType.array_:
337                 return dataStore.array_.length;
338             case DataNodeType.object_:
339                 return dataStore.object_.length;
340             case DataNodeType.string_:
341                 return dataStore.string_.length;
342             default:
343                 return 0;
344         }
345     }
346 
347     /**
348         Removes the given key from the object.
349 
350         Params:
351             key = The key to remove.
352     */
353     void remove(string key) {
354         if (this.isType(DataNodeType.object_)) {
355             dataStore.object_.remove(key);
356         }
357     }
358 
359     /**
360         Removes the given index from the array.
361 
362         Params:
363             idx = The index to remove.
364     */
365     void remove(size_t idx) {
366         if (this.isType(DataNodeType.array_)) {
367             dataStore.array_.remove(idx);
368         }
369     }
370 
371     /**
372         Adds the given entry into the array.
373 
374         Params:
375             rhs = Value to append
376     */
377     void opOpAssign(string op)(DataNode rhs)
378     if (op == "~") {
379         if (this.isType(DataNodeType.array_)) {
380             dataStore.array_.opOpAssign!op(rhs);
381         }
382     }
383 
384     /**
385         Assigns an element of the object node.
386 
387         Params:
388             key = The key to query.
389             value = The value to set.
390     */
391     void opIndexAssign(T)(auto ref T value, string key) {
392         if (this.isType(DataNodeType.object_)) {
393             static if (is(T == DataNode)) {
394                 dataStore.object_.opIndexAssign(value, key);
395             } else {
396                 dataStore.object_.opIndexAssign(DataNode(value), key);
397             }
398         }
399     }
400 
401     /**
402         Assigns an element of the array node.
403 
404         Params:
405             rhs = The value to set.
406             idx = The idx to set.
407     */
408     void opIndexAssign(T)(auto ref T rhs, size_t idx) {
409         if (this.isType(DataNodeType.array_)) {
410             dataStore.array_.opIndexAssign!T(rhs, idx);
411         }
412     }
413 
414     /**
415         Gets whether the given key is present in the object.
416 
417         Params:
418             key = The key to query.
419 
420         Returns:
421             $(D true) if the object contains a value with the given key,
422             $(D false) otherwise.
423     */
424     inout(DataNode)* opBinaryRight(string op)(string key) inout nothrow
425     if (op == "in") {
426         return this.isType(DataNodeType.object_) ? key in dataStore.object_ : null;
427     }
428 
429     /**
430         Gets the given entry in the object.
431 
432         Params:
433             key = The key to query.
434 
435         Returns:
436             The $(D DataNode) with the given key.
437     */
438     ref DataNode opIndex(string key) {
439         return dataStore.object_.opIndex(key);
440     }
441 
442     /**
443         Gets the given entry in the array.
444 
445         Params:
446             idx = The index to query.
447 
448         Returns:
449             The $(D DataNode) with the given index.
450     */
451     ref DataNode opIndex(size_t idx) {
452         return dataStore.array_.opIndex(idx);
453     }
454 
455     bool opEquals(DataNode other) {
456         if (dataType == DataNodeType.int_ && other.dataType == DataNodeType.uint_) {
457             return dataStore.int_ >= 0 ? cast(uint)dataStore.int_ == other.dataStore.uint_ : false;
458         } else if (dataType == DataNodeType.uint_ && other.dataType == DataNodeType.int_) {
459             return other.dataStore.int_ >= 0 ? dataStore.uint_ == cast(uint)other.dataStore.int_ : false;
460         }
461 
462         if (dataType != other.dataType) {
463             return false;
464         }
465 
466         switch (dataType) {
467             case DataNodeType.undefined:
468                 return dataStore.undefined == other.dataStore.undefined;
469             case DataNodeType.boolean_:
470                 return dataStore.boolean_ == other.dataStore.boolean_;
471             case DataNodeType.int_:
472                 return dataStore.int_ == other.dataStore.int_;
473             case DataNodeType.uint_:
474                 return dataStore.uint_ == other.dataStore.uint_;
475             case DataNodeType.float_:
476                 return dataStore.float_ == other.dataStore.float_;
477             case DataNodeType.string_:
478                 return dataStore.string_ == other.dataStore.string_;
479             // case DataNodeType.array_:
480             //     return dataStore.array_ == other.dataStore.array_;
481             // case DataNodeType.object_:
482             //     return dataStore.object_ == other.dataStore.object_;
483             case DataNodeType.blob_:
484                 return dataStore.blob_ == other.dataStore.blob_;
485             default:
486                 return false;
487         }
488     }
489 
490     bool opEquals(T)(T other) {
491         static if (__traits(isIntegral, T)) {
492             if (dataType == DataNodeType.int_ && dataNodeTypeOf!T == DataNodeType.uint_) {
493                 return dataStore.int_ >= 0 ? cast(uint)dataStore.int_ == other : false;
494             } else if (dataType == DataNodeType.uint_ && dataNodeTypeOf!T == DataNodeType.int_) {
495                 return other >= 0 ? dataStore.uint_ == cast(uint)other : false;
496             }
497         }
498 
499         if (dataType != dataNodeTypeOf!T) {
500             return false;
501         }
502 
503         // TODO: handle array & dictionary
504         static if (dataNodeTypeOf!T == DataNodeType.undefined) {
505             return dataStore.undefined == other;
506         } else static if (dataNodeTypeOf!T == DataNodeType.boolean_) {
507             return dataStore.boolean_ == other;
508         } else static if (dataNodeTypeOf!T == DataNodeType.int_) {
509             return dataStore.int_ == other;
510         } else static if (dataNodeTypeOf!T == DataNodeType.uint_) {
511             return dataStore.uint_ == other;
512         } else static if (dataNodeTypeOf!T == DataNodeType.float_) {
513             return dataStore.float_ == other;
514         } else static if (dataNodeTypeOf!T == DataNodeType.string_) {
515             return dataStore.string_ == other;
516         } else static if (dataNodeTypeOf!T == DataNodeType.blob_) {
517             return dataStore.blob_ == other;
518         } else {
519             return false;
520         }
521     }
522 
523     /**
524         Converts the DataNode to a string.
525     */
526     string toString() const @trusted nothrow {
527         import nulib.string : nstring;
528         import nulib.conv : to;
529         final switch(dataType) {
530             
531             case DataNodeType.string_:
532                 return dataStore.string_;
533             
534             case DataNodeType.boolean_:
535                 return dataStore.boolean_ ? "true" : "false";
536             
537             case DataNodeType.int_:
538                 return to!nstring(dataStore.int_).take();
539             
540             case DataNodeType.uint_:
541                 return to!nstring(dataStore.uint_).take();
542             
543             case DataNodeType.float_:
544                 return to!nstring(dataStore.float_).take();
545             
546             case DataNodeType.array_:
547                 return "<array>";
548             
549             case DataNodeType.object_:
550                 return "<object>";
551             
552             case DataNodeType.blob_:
553                 return "<blob>";
554             
555             case DataNodeType.undefined:
556                 return "<undefined>";
557         }
558     }
559 }
560 
561 /**
562     Gets the type name for a given DataNodeType.
563 
564     Params:
565         type = The data node type.
566 
567     Returns:
568         The name of the type.
569 */
570 string toTypeName(DataNodeType type) @nogc nothrow pure {
571     final switch(type) {
572             
573             case DataNodeType.string_:
574                 return "string";
575             
576             case DataNodeType.boolean_:
577                 return "boolean";
578             
579             case DataNodeType.int_:
580                 return "int32";
581             
582             case DataNodeType.uint_:
583                 return "uint32";
584             
585             case DataNodeType.float_:
586                 return "float32";
587             
588             case DataNodeType.array_:
589                 return "array";
590             
591             case DataNodeType.object_:
592                 return "object";
593             
594             case DataNodeType.blob_:
595                 return "blob";
596             
597             case DataNodeType.undefined:
598                 return "undefined";
599     }
600 }
601 
602 @("Create node.")
603 unittest {
604     DataNode n = DataNode.createObject();
605     n["a"] = 42;
606 
607     assert(n["a"].number == 42);
608     n.remove("a");
609     assert("a" !in n);
610 }