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 }