1 /++
2 Mutable FGHJ data structure.
3 The representation can be used to compute a difference between JSON object-trees.
4 
5 Copyright: Tamedia Digital, 2016
6 
7 Authors: Ilya Yaroshenko
8 
9 License: BSL-1.0
10 +/
11 module fghj.transform;
12 
13 import fghj.fghj;
14 import fghj.serialization;
15 import std.exception: enforce;
16 
17 /++
18 Object-tree structure for mutable Fghj representation.
19 
20 `FghjNode` can be used to construct and manipulate JSON objects.
21 Each `FghjNode` can represent either a dynamic JSON object (associative array of `FghjNode` nodes) or a FGHJ JSON value.
22 JSON arrays can be represented only as JSON values.
23 +/
24 struct FghjNode
25 {
26     /++
27     Children nodes.
28     +/
29     FghjNode[const(char)[]] children;
30     /++
31     Leaf data.
32     +/
33     Fghj data;
34 
35 pure:
36 
37     /++
38     Returns `true` if the node is leaf.
39     +/
40     bool isLeaf() const @safe pure nothrow @nogc
41     {
42         return cast(bool) data.data.length;
43     }
44 
45     /++
46     Construct `FghjNode` recursively.
47     +/
48     this(Fghj data)
49     {
50         if(data.kind == Fghj.Kind.object)
51         {
52             foreach(kv; data.byKeyValue)
53             {
54                 children[kv.key] = FghjNode(kv.value);
55             }
56         }
57         else
58         {
59             this.data = data;
60             enforce(isLeaf);
61         }
62     }
63 
64     ///
65     ref FghjNode opIndex(scope const(char)[][] keys...) scope return
66     {
67         if(keys.length == 0)
68             return this;
69         auto ret = this;
70         for(;;)
71         {
72             auto ptr = keys[0] in ret.children;
73             enforce(ptr, "FghjNode.opIndex: keys do not exist");
74             keys = keys[1 .. $];
75             if(keys.length == 0)
76                 return *ptr;
77             ret = *ptr;
78         }
79     }
80 
81     ///
82     unittest
83     {
84         import fghj;
85         auto text = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`;
86         auto root = FghjNode(text.parseJson);
87         assert(root["inner", "a"].data == `true`.parseJson);
88     }
89 
90     ///
91     void opIndexAssign(FghjNode value, scope const(char)[][] keys...)
92     {
93         auto root = &this;
94         foreach(key; keys)
95         {
96             L:
97             auto ptr = key in root.children;
98             if(ptr)
99             {
100                 enforce(ptr, "FghjNode.opIndex: keys do not exist");
101                 keys = keys[1 .. $];
102                 root = ptr;
103             }
104             else
105             {
106                 root.children[keys[0]] = FghjNode.init;
107                 goto L;
108             }
109         }
110         *root = value;
111     }
112 
113     ///
114     unittest
115     {
116         import fghj;
117         auto text = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`;
118         auto root = FghjNode(text.parseJson);
119         auto value = FghjNode(`true`.parseJson);
120         root["inner", "g", "u"] = value;
121         assert(root["inner", "g", "u"].data == true);
122     }
123 
124     /++
125     Params:
126         value = default value
127         keys = list of keys
128     Returns: `[keys]` if any and `value` othervise.
129     +/
130     FghjNode get(FghjNode value, in char[][] keys...)
131     {
132         auto ret = this;
133         foreach(key; keys)
134             if(auto ptr = key in ret.children)
135                 ret = *ptr;
136             else
137             {
138                 ret = value;
139                 break;
140             }
141         return ret;
142     }
143 
144     ///
145     unittest
146     {
147         import fghj;
148         auto text = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`;
149         auto root = FghjNode(text.parseJson);
150         auto value = FghjNode(`false`.parseJson);
151         assert(root.get(value, "inner", "a").data == true);
152         assert(root.get(value, "inner", "f").data == false);
153     }
154 
155     /// Serialization primitive
156     void serialize(ref FghjSerializer serializer)
157     {
158         if(isLeaf)
159         {
160             serializer.app.put(cast(const(char)[])data.data);
161             return;
162         }
163         auto state = serializer.structBegin;
164         foreach(key, ref value; children)
165         {
166             serializer.putKey(key);
167             value.serialize(serializer);
168         }
169         serializer.structEnd(state);
170     }
171 
172     ///
173     Fghj opCast(T : Fghj)()
174     {
175         return serializeToFghj(this);
176     }
177 
178     ///
179     unittest
180     {
181         import fghj;
182         auto text = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`;
183         auto root = FghjNode(text.parseJson);
184         import std.stdio;
185         Fghj flat = cast(Fghj) root;
186         assert(flat["inner", "a"] == true);
187     }
188 
189     ///
190     bool opEquals(in FghjNode rhs) const @safe pure nothrow @nogc
191     {
192         if(isLeaf)
193             if(rhs.isLeaf)
194                 return data == rhs.data;
195             else
196                 return false;
197         else
198             if(rhs.isLeaf)
199                 return false;
200             else
201                 return children == rhs.children;
202     }
203 
204     ///
205     unittest
206     {
207         import fghj;
208         auto text = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`;
209         auto root1 = FghjNode(text.parseJson);
210         auto root2= FghjNode(text.parseJson);
211         assert(root1 == root2);
212         assert(root1["inner"].children.remove("b"));
213         assert(root1 != root2);
214     }
215 
216     /// Adds data to the object-tree recursively.
217     void add(Fghj data)
218     {
219         if(data.kind == Fghj.Kind.object)
220         {
221             this.data = Fghj.init;
222             foreach(kv; data.byKeyValue)
223             {
224                 if(auto nodePtr = kv.key in children)
225                 {
226                     nodePtr.add(kv.value);
227                 }
228                 else
229                 {
230                     children[kv.key] = FghjNode(kv.value);
231                 }
232             }
233         }
234         else
235         {
236             this.data = data;
237             children = null;
238         }
239     }
240 
241     ///
242     unittest
243     {
244         import fghj;
245         auto text = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`;
246         auto addition = `{"do":"re","inner":{"a":false,"u":2}}`;
247         auto root = FghjNode(text.parseJson);
248         root.add(addition.parseJson);
249         auto result = `{"do":"re","foo":"bar","inner":{"a":false,"u":2,"b":false,"c":"32323","d":null,"e":{}}}`;
250         assert(root == FghjNode(result.parseJson));
251     }
252 
253     /// Removes keys from the object-tree recursively.
254     void remove(Fghj data)
255     {
256         enforce(children, "FghjNode.remove: fghj data must be a sub-tree");
257         foreach(kv; data.byKeyValue)
258         {
259             if(kv.value.kind == Fghj.Kind.object)
260             {
261                 if(auto nodePtr = kv.key in children)
262                 {
263                     nodePtr.remove(kv.value);
264                 }
265             }
266             else
267             {
268                 children.remove(kv.key);
269             }
270         }
271     }
272 
273     ///
274     unittest
275     {
276         import fghj;
277         auto text = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`;
278         auto rem = `{"do":null,"foo":null,"inner":{"c":null,"e":null}}`;
279         auto root = FghjNode(text.parseJson);
280         root.remove(rem.parseJson);
281         auto result = `{"inner":{"a":true,"b":false,"d":null}}`;
282         assert(root == FghjNode(result.parseJson));
283     }
284 
285     private void removedImpl(ref FghjSerializer serializer, FghjNode node)
286     {
287         import std.exception : enforce;
288         enforce(!isLeaf);
289         enforce(!node.isLeaf);
290         auto state = serializer.structBegin;
291         foreach(key, ref value; children)
292         {
293             auto nodePtr = key in node.children;
294             if(nodePtr && *nodePtr == value)
295                 continue;
296             serializer.putKey(key);
297             if(nodePtr && !nodePtr.isLeaf && !value.isLeaf)
298                 value.removedImpl(serializer, *nodePtr);
299             else
300                 serializer.putValue(null);
301          }
302         serializer.structEnd(state);
303     }
304 
305     /++
306     Returns the subset of the object-tree which is not represented in `node`.
307     If a leaf is represented but has a different value then it will be included
308     in the return value.
309     Returned value has FGHJ format and its leaves are set to `null`.
310     +/
311     Fghj removed(FghjNode node)
312     {
313         auto serializer = fghjSerializer();
314         removedImpl(serializer, node);
315         serializer.flush;
316         return serializer.app.result;
317     }
318 
319     ///
320     unittest
321     {
322         import fghj;
323         auto text1 = `{"inner":{"a":true,"b":false,"d":null}}`;
324         auto text2 = `{"foo":"bar","inner":{"a":false,"b":false,"c":"32323","d":null,"e":{}}}`;
325         auto node1 = FghjNode(text1.parseJson);
326         auto node2 = FghjNode(text2.parseJson);
327         auto diff = FghjNode(node2.removed(node1));
328         assert(diff == FghjNode(`{"foo":null,"inner":{"a":null,"c":null,"e":null}}`.parseJson));
329     }
330 
331     void addedImpl(ref FghjSerializer serializer, FghjNode node)
332     {
333         import std.exception : enforce;
334         enforce(!isLeaf);
335         enforce(!node.isLeaf);
336         auto state = serializer.structBegin;
337         foreach(key, ref value; node.children)
338         {
339             auto nodePtr = key in children;
340             if(nodePtr && *nodePtr == value)
341                 continue;
342             serializer.putKey(key);
343             if(nodePtr && !nodePtr.isLeaf && !value.isLeaf)
344                 nodePtr.addedImpl(serializer, value);
345             else
346                 value.serialize(serializer);
347          }
348         serializer.structEnd(state);
349     }
350 
351     /++
352     Returns the subset of the node which is not represented in the object-tree.
353     If a leaf is represented but has a different value then it will be included
354     in the return value.
355     Returned value has FGHJ format.
356     +/
357     Fghj added(FghjNode node)
358     {
359         auto serializer = fghjSerializer();
360         addedImpl(serializer, node);
361         serializer.flush;
362         return serializer.app.result;
363     }
364 
365     ///
366     unittest
367     {
368         import fghj;
369         auto text1 = `{"foo":"bar","inner":{"a":false,"b":false,"c":"32323","d":null,"e":{}}}`;
370         auto text2 = `{"inner":{"a":true,"b":false,"d":null}}`;
371         auto node1 = FghjNode(text1.parseJson);
372         auto node2 = FghjNode(text2.parseJson);
373         auto diff = FghjNode(node2.added(node1));
374         assert(diff == FghjNode(`{"foo":"bar","inner":{"a":false,"c":"32323","e":{}}}`.parseJson));
375     }
376 }