1 /**
2  * The module contains the objects and the properties of control functions
3  *
4  * Copyright: (c) 2015-2016, Milofon Project.
5  * License: Subject to the terms of the BSD license, as written in the included LICENSE.txt file.
6  * Authors: Maksim Galanin
7  */
8 module proped.properties;
9 
10 private
11 {
12     import std.typecons : NullableRef, Nullable;
13     import std.array : split, empty;
14     import std.meta : staticIndexOf, AliasSeq;
15     import std.conv : to;
16     static import std.traits;
17 
18     import proped.uninode;
19     import proped.exception;
20 }
21 
22 alias Types = AliasSeq!(bool, int, long, double, string);
23 alias PropNode = UniNode!(Types);
24 
25 
26 enum DELIMITER_CHAR = '.';
27 enum DEFAULT_FIELD_NAME = "v";
28 
29 
30 template IsValidType(T)
31 {
32     enum IsValidType = staticIndexOf!(T, Types) >= 0 
33         || (std.traits.isArray!T && is(std.traits.ForeachType!T == PropNode))
34         || (std.traits.isAssociativeArray!T && is(std.traits.KeyType!T == string) && is(std.traits.ValueType!T == PropNode));
35 }
36 
37 
38 /**
39  * Class contains methods to manage the properties
40  */
41 struct Properties
42 {
43     private PropNode head;
44 
45 
46     this(T)(T val) if (IsValidType!T)
47     {
48         head = PropNode(val);
49     }
50 
51 
52     this(PropNode node)
53     {
54         head = node;
55     }
56 
57 
58     /**
59      * Getting node in the specified path
60      *
61      * It the node is an object, the we try to find the embedded objects in the specified path
62      *
63      * Params:
64      *
65      * path = The path to the desired site
66      */
67     private NullableRef!PropNode findNode(string path)
68     {
69         auto names = path.split(DELIMITER_CHAR);
70 
71         NullableRef!PropNode findPath(PropNode* node, string[] names)
72         {
73             if(names.length == 0)
74                 return NullableRef!PropNode(node);
75 
76             string name = names[0];
77             if ((*node).isObject)
78                 if (auto chd = name in (*node).toObject)
79                     return findPath(chd, names[1..$]);
80 
81             return NullableRef!PropNode.init;
82         }
83 
84         if (names.length > 0)
85             return findPath(&head, names);
86 
87         return NullableRef!PropNode.init;
88     }
89 
90 
91     /**
92      * Checking for the presence of the node in the specified path
93      *
94      * It the node is an object, the we try to find the embedded objects in the specified path
95      *
96      * Params:
97      *
98      * path = The path to the desired site
99      */
100     bool opBinaryRight(string op)(string path) if ("in" == op)
101     {
102         auto node = findNode(path);
103         return !node.isNull;
104     }
105 
106 
107     /**
108      * Returns the length of an object
109      */
110     ulong length() @property
111     {
112         if (head.isArray)
113             return head.toArray().length;
114         else if (head.isObject)
115             return head.toObject().length;
116         else if (typeid(string) == head.type)
117             return head.length;
118         return 0;
119     }
120 
121 
122     alias opDollar = length;
123 
124 
125     /**
126      * Get the node if the specified path is not a node
127      *
128      * Params:
129      *
130      * path = The path to the desired site
131      *
132      * Example:
133      * ---
134      * node.get!int("foo.bar");
135      * ---
136      */
137     Nullable!T get(T)(string path) if (IsValidType!T)
138     {
139         return doGet!T(findNode(path));
140     }
141 
142 
143     /**
144      * Get the value of the node
145      *
146      * Example
147      * ---
148      * node.get!int;
149      * ---
150      */
151     Nullable!T get(T)() if (IsValidType!T)
152     {
153         return doGet!T(NullableRef!PropNode(&head));
154     }
155 
156 
157     private Nullable!T doGet(T)(NullableRef!PropNode node)
158     {
159         if (!node.isNull)
160         {
161             if (node.get.convertsTo!T)
162                 return Nullable!T(node.get.get!T);
163             else
164                 return Nullable!T.init;
165         }
166         else
167             return Nullable!T.init;
168     }
169 
170 
171     /**
172      * Get the value, otherwise return the default value
173      *
174      * Params:
175      *
176      * alt = Default value
177      *
178      * Example:
179      * ---
180      * getOrElse(1);
181      * ---
182      */
183     T getOrElse(T)(T alt) if (IsValidType!T)
184     {   
185         return doGetOrElse!T(NullableRef!PropNode(&head), alt);
186     }
187 
188 
189     /**
190      * Get the node at the specified path, or return to the default value
191      *
192      * Params:
193      *
194      * path = The path to the desired site
195      * alt  = Default value
196      *
197      * Example:
198      * ---
199      * getOrElse("foo", 1);
200      * ---
201      */
202     T getOrElse(T)(string path, T alt) if (IsValidType!T)
203     {   
204         return doGetOrElse!T(findNode(path), alt);
205     }
206 
207 
208     private T doGetOrElse(T)(NullableRef!PropNode node, T alt) if (IsValidType!T)
209     {
210         if (!node.isNull)
211         {
212             if (node.get.convertsTo!T)
213                 return node.get.get!T;
214             else
215                 return alt;
216         }
217         else
218             return alt;
219     }
220 
221 
222     /**
223      * Check type node
224      */
225     bool isType(T)()
226     {
227         return head.type == typeid(T);
228     }
229 
230 
231     /** 
232      * Get an array of properties on the specified path
233      *
234      * Params:
235      *
236      * path = The path to the desired site
237      *
238      * Example:
239      * ---
240      * getArray("services");
241      * ---
242      */
243     Properties[] getArray(string path)
244     {
245         return doGetArray(findNode(path));
246     }
247 
248 
249     /**
250      * Get an array of properties on the node
251      *
252      * Example
253      * ---
254      * node.getArray();
255      * ---
256      */
257     Properties[] getArray()
258     {
259         return doGetArray(NullableRef!PropNode(&head));
260     }
261 
262 
263     private Properties[] doGetArray(NullableRef!PropNode nodeRef)
264     {
265         Properties[] result;
266 
267         if (nodeRef.isNull)
268             return result;
269 
270         PropNode node = nodeRef.get; 
271 
272         if (node.isArray)
273         {
274             foreach(PropNode ch; node.get!(PropNode[]))
275                 result ~= Properties(ch); 
276         }
277         else
278             result ~= Properties(node); 
279 
280         return result;
281     }
282 
283 
284     /** 
285      * Get an associative array of object properties in the specified path
286      *
287      * Params:
288      *
289      * path = The path to the desired site
290      *
291      * Example:
292      * ---
293      * getObject("services");
294      * ---
295      */
296     Properties[string] getObject(string path)
297     {
298         return doGetObject(findNode(path));
299     }
300 
301 
302     /** 
303      * Get an associative array of object properties in the node
304      *
305      * Params:
306      *
307      * path = The path to the desired site
308      *
309      * Example:
310      * ---
311      * getObject();
312      * ---
313      */
314     Properties[string] getObject()
315     {
316         return doGetObject(NullableRef!PropNode(&head));
317     }
318 
319 
320     private Properties[string] doGetObject(NullableRef!PropNode nodeRef)
321     {
322         Properties[string] result;
323 
324         if (nodeRef.isNull)
325             return result;
326 
327         PropNode node = nodeRef.get;
328         if (!node.isObject)
329         {
330             result[DEFAULT_FIELD_NAME] = Properties(node);
331         }
332         else
333         {
334             foreach(string k, PropNode ch; node.get!(PropNode[string]))
335                 result[k] = Properties(ch); 
336         }
337 
338         return result;
339     }
340 
341 
342     /**
343      * The string representation
344      */
345     string toString()
346     {
347         return head.toString();
348     }
349 
350 
351     /**
352      * Recursive merge properties
353      *
354      * When the merger is not going to existing nodes
355      * If the parameter is an array, it will their concatenation
356      *
357      * Params:
358      *
359      * src = Source properties
360      */
361     Properties opOpAssign(string op)(Properties src) if ("~" == op)
362     {
363         if (src.head.hasNull)
364             return this;
365 
366         if (head.hasNull)
367         {
368             head = src.head;
369             return this;
370         }
371 
372         void mergeNode(ref PropNode dst, ref PropNode src)
373         {
374             if (dst.isObject && src.isObject)
375             {
376                 auto dstMap = dst.toObject;
377                 foreach(string k, PropNode ch; src.toObject)
378                 {
379                     if (auto tg = k in dstMap)
380                         mergeNode(*tg, ch);
381                     else
382                         dstMap[k] = ch;
383                 }
384             }
385             else if (dst.isArray)
386             {
387                 auto dstArr = dst.toArray;
388                 dst = dstArr ~ src;
389             }
390         }
391 
392         mergeNode(head, src.head);
393         return this;
394     }
395 
396 
397     /**
398      * Get a subset of properties in the specified path
399      *
400      * Params:
401      *
402      * path = The path to the desired site
403      */
404     Nullable!Properties sub(string path)
405     {
406         auto node = findNode(path);
407         return node.isNull ? Nullable!Properties.init : Nullable!Properties(Properties(node.get));
408     }
409 
410 
411     /**
412      * Finding and installing a new value in the specified path
413      *
414      * If the specified path object will be the object or an array, it will be thrown
415      * Also, when a situation of lack of necessaty way, a new item will be created
416      *
417      * Params:
418      *
419      * path = The path to the desired site
420      * val  = New value
421      */
422     void set(T)(string path, T val) if (IsValidType!T)
423     {
424         set(path, PropNode(val));
425     }
426 
427 
428     /**
429      * Finding and installing a new value in the specified path
430      *
431      * If the specified path object will be the object or an array, it will be thrown
432      * Also, when a situation of lack of necessaty way, a new item will be created
433      *
434      * Params:
435      *
436      * path = The path to the desired site
437      * val  = New value
438      */
439     void set(string path, PropNode val)
440     {
441         auto names = path.split(DELIMITER_CHAR);
442 
443         PropNode createPath(PropNode terminal, string[] names)
444         {
445             if (names.empty)
446                 return terminal;
447 
448             PropNode[string] map;
449             map[names[0]] = createPath(terminal, names[1..$]);
450             return PropNode(map);
451         }
452 
453         void setNode(ref PropNode node, string[] names)
454         {
455             if (names.empty)
456             {
457                 if (node.isObject || node.isArray)
458                     throw new PropertiesException("The node '" ~ path ~ "' is not a simple type");
459                 node = val;
460                 return;
461             }
462 
463             string name = names[0];
464             if (node.isObject)
465             {
466                 if (auto chd = name in node.toObject)
467                     setNode(*chd, names[1..$]);
468                 else
469                 {
470                     PropNode newNode = createPath(val, names[1..$]);
471                     if (node.length == 0)
472                     {
473                         PropNode[string] map;
474                         map[name] = newNode;
475                         node = map;
476                     }
477                     else
478                         node.toObject[name] = newNode;
479                 }
480             }
481             else
482                 throw new PropertiesException("Failed to set values for the key. The parent node for '" ~ name ~ "' is not object");
483         }
484 
485         if (head.hasNull)
486         {
487             PropNode[string] map;
488             head = PropNode(map);
489         }
490 
491         setNode(head, names);
492     }
493 
494 
495     /**
496      * Installing a new value in the node
497      *
498      * Params:
499      *
500      * val  = New value
501      */
502     void set(T)(T val) if (IsValidType!T)
503     {
504         if (node.isObject || node.isArray)
505             throw new PropertiesException("The node '" ~ path ~ "' is not a simple type");
506         head = val;
507     }
508 
509 
510     /**
511      * Check property is object
512      */
513     bool isObject() @property
514     {
515         return head.isObject;
516     }
517 
518     
519     /**
520      * Check property is array
521      */
522     bool isArray() @property
523     {
524         return head.isArray;
525     }
526 
527 
528     /**
529      * Check property is null
530      */
531     bool isNull() @property
532     {
533         return head.hasNull;
534     }
535 
536     /**
537      * Convert value type to object
538      */
539     void valueToObject()
540     {
541         if (!head.isObject)
542             head = PropNode([DEFAULT_FIELD_NAME: head]);
543     }
544 }
545