1 module glued.pathtree;
2 
3 import std.range;
4 import std.algorithm;
5 import std.typecons;
6 import std.traits;
7 import std.meta;
8 
9 import std..string: strip;
10 
11 import optional;
12 
13 /**
14  * Data structure that wraps the actual value of the node with metadata used
15  * for versioning of node values.
16  */
17 interface ValueChain(Data) {
18     ///Version number of the value, counting from 0 (initial value)
19     @property
20     size_t versionNo();
21     
22     ///Payload
23     @property
24     Optional!Data data(); //fixme this should be plain Data, not Optional!Data
25     
26     ///Optional link to previous value with metadata
27     @property
28     Optional!(ValueChain!Data) previousValue();
29     
30     final static class SimpleValueChain: ValueChain!Data {
31         size_t _versionNo;
32         Optional!Data _data; //fixme this should be plain Data, not Optional!Data
33         Optional!(ValueChain!Data) _previousValue;
34         
35         this(size_t v, Optional!Data d, Optional!(ValueChain!Data) p){
36             _versionNo = v;
37             _data = d;
38             _previousValue = p;
39         }
40         
41         @property
42         size_t versionNo(){
43             return _versionNo;
44         }
45         
46         @property
47         Optional!Data data(){
48             return _data;
49         }
50         
51         @property
52         Optional!(ValueChain!Data) previousValue(){
53             return _previousValue;
54         }
55     }
56     
57     static final ValueChain!Data of(size_t v, Optional!Data d, Optional!(ValueChain!Data) p){
58         return new SimpleValueChain(v, d, p);
59     }
60 }
61 
62 /**
63  * ValueChain with actual path under which it is stored. Used when 
64  * $(D_PSYMBOL resolve)ing instead of $(D_PSYMBOL get)ing values, allows
65  * to inspect not only the value obtained from resolution, but also its
66  * origins.
67  */
68 alias ValueChainWithCoordinates(Data) = Tuple!(ValueChain!Data, "valueChain", string, "realPath");
69 
70 class PathTreeNode(Data) {
71     alias Link = ValueChain!Data;
72     alias LinkWithCoordinates = ValueChainWithCoordinates!Data;
73     
74     private string fullPath;
75     private PathTreeNode!Data[string] children;
76     private Optional!(Link) valueChain = no!(Link); //todo rename to valueChain
77     
78     this(string path){
79         fullPath = path;
80     }
81     
82     //
83     // WRITE STACK
84     // impl
85     
86     private void put(string[] pathComponents, Data data){
87         //todo this introduces a silent feature - root can have value, paths can start and end with dots - cover that with tests! or check some conditions in public put
88         pathComponents = pathComponents.filter!(x => !x.strip().empty).array;
89         if (pathComponents.empty){
90             size_t nextVersion = (valueChain.empty ? -1 : valueChain.front().versionNo) + 1;
91             auto prev = valueChain;
92             Link newValueChain = Link.of(nextVersion, data.some, prev);
93             valueChain = newValueChain.some;
94         } else {
95             auto head = pathComponents[0];
96             auto tail = pathComponents[1..$];
97             if (!(head in children)){
98                 string childPath = (this.fullPath.empty ? "" : this.fullPath~".")~head;
99                 children[head] = new PathTreeNode!Data(childPath);
100             }
101             children[head].put(tail, data);
102         }
103     }
104     
105     void put(string path, Data data){
106         put(path.split("."), data);
107     }
108     
109     private Optional!Link getValueChain(string[] pathComponents){
110         if (pathComponents.empty){
111             return valueChain;
112         } else {
113             return pathComponents[0] in children ? 
114                     children[pathComponents[0]].getValueChain(pathComponents[1..$]) : 
115                     no!Link;
116         }
117     }
118     
119     private Optional!LinkWithCoordinates resolveValueChainWithCoordinates(string[] pathComponents, string upToRoot){
120         if (pathComponents.empty){
121             return valueChain.map!(x => LinkWithCoordinates(x, fullPath)).toOptional;
122         }
123         auto deeperResult = pathComponents[0] in children ? 
124                                 children[pathComponents[0]].resolveValueChainWithCoordinates(pathComponents[1..$], upToRoot) : 
125                                 no!LinkWithCoordinates;
126         if (deeperResult.empty && fullPath.startsWith(upToRoot)){
127             return valueChain.map!(x => LinkWithCoordinates(x, fullPath)).toOptional;
128         }
129         return deeperResult;
130     }
131     
132     Optional!Link getValueChain(string path) {
133         return getValueChain(path.split("."));
134     }
135     
136     Optional!LinkWithCoordinates resolveValueChainWithCoordinates(string path, string upToRoot="") {
137         return resolveValueChainWithCoordinates(path.split("."), upToRoot);
138     }
139     
140 }
141 
142 alias PathMapper = string delegate(string); // viewPath -> backendPath
143 //toddo consider ValueMapper(T, T2) = T2 delegate(string, T); //(viewPath, backendValue) -> viewValue
144 alias ValueMapper(T, T2) = T2 delegate(T);
145 
146 interface PathTreeView(Data){
147     Optional!(ValueChain!Data) getValueChain(string path);
148     
149     Optional!(ValueChain!Data) resolveValueChain(string path, string upToRoot="");
150     
151     /**
152      * Returns: $(D_PSYMBOL some) over data stored under this exact $(D_PSYMBOL path); 
153      * empty optional if no value was assigned.
154      */
155     final Optional!Data get(string path) {
156         return getValueChain(path).map!(x => x.data).joiner.toOptional;
157     }
158     
159     /**
160      * Returns: $(D_PSYMBOL some) over version number (counted from 0) of data 
161      * stored under this exact $(D_PSYMBOL path); empty optional if no value was assigned.
162      */
163     final Optional!size_t getVersion(string path) {
164         return getValueChain(path).map!(x => x.versionNo).toOptional;
165     }
166     
167     /**
168      * Returns: $(D_PSYMBOL some) over data stored under this $(D_PSYMBOL path) or 
169      * its closest ancestor; empty optional if no value was assigned anywhere 
170      * from that node up to the root of the tree.
171      */
172     final Optional!Data resolve(string path, string upToRoot="") {
173         return resolveValueChain(path, upToRoot).map!(x => x.data).joiner.toOptional;
174     }
175     
176     final Optional!size_t resolveVersion(string path, string upToRoot="") {
177         return resolveValueChain(path, upToRoot).map!(x => x.versionNo).toOptional;
178     }
179     
180     private static string joinPaths(string a, string b){
181         return cast(string) (a.split(".") ~ b.split(".")).filter!(x => !x.strip.empty).join(".");
182     }
183     
184     final PathTreeView!Data subtree(string from){
185         assert(!from.empty);//todo exception
186         return mapPaths((string s) => joinPaths(from, s));
187     }
188     
189     final PathTreeView!Data mapPaths(PathMapper mapper){
190         class PathView: PathTreeView!Data {
191             private PathTreeView!Data wrapped;
192             private PathMapper foo;
193             
194             this(PathTreeView!Data wrapped, PathMapper foo){
195                 this.wrapped = wrapped;
196                 this.foo = foo;
197             }
198             
199             Optional!(ValueChain!Data) getValueChain(string path){
200                 return wrapped.getValueChain(foo(path));
201             }
202     
203             Optional!(ValueChain!Data) resolveValueChain(string path, string upToRoot=""){
204                 return wrapped.resolveValueChain(foo(path), foo(upToRoot));
205             }
206         }
207         return new PathView(this, mapper);
208     }
209     
210     final PathTreeView!NewData mapValues(NewData)(ValueMapper!(Data, NewData) mapper){
211         alias Mapper = ValueMapper!(Data, NewData);
212         
213         class MappedLink: ValueChain!NewData {
214             private ValueChain!Data wrapped;
215             private Mapper foo;
216             
217             this(ValueChain!Data wrapped, Mapper foo){
218 
219                 this.wrapped = wrapped;
220                 this.foo = foo;
221             }
222             
223             @property
224             size_t versionNo(){
225                 return wrapped.versionNo;
226             }
227             
228             @property
229             Optional!NewData data(){
230                 return wrapped.data.map!(x => foo(x)).toOptional;
231             }
232             
233             @property
234             Optional!(ValueChain!NewData) previousValue(){
235                 return wrapped.previousValue.map!(x => cast(ValueChain!NewData) new MappedLink(x, foo)).toOptional;
236             }
237         }
238     
239         class ValueView: PathTreeView!NewData {
240             private PathTreeView!Data wrapped;
241             private Mapper foo;
242             
243             this(PathTreeView!Data wrapped, Mapper foo){
244                 this.wrapped = wrapped;
245                 this.foo = foo;
246             }
247             
248             Optional!(ValueChain!NewData) getValueChain(string path){
249                 return wrapped.getValueChain(path)
250                     .map!(x => cast(ValueChain!NewData) new MappedLink(x, foo))
251                     .toOptional;
252             }
253     
254             Optional!(ValueChain!NewData) resolveValueChain(string path, string upToRoot=""){
255                 return wrapped.resolveValueChain(path, upToRoot)
256                     .map!(x => cast(ValueChain!NewData) new MappedLink(x, foo))
257                     .toOptional;
258             }
259         }
260         return new ValueView(this, mapper);
261     }
262 }
263 
264 //todo should be an interface
265 abstract class PathTree(Data): PathTreeView!Data {
266     alias Link = ValueChain!Data;
267     alias LinkWithCoordinates = ValueChainWithCoordinates!Data;
268     
269     /**
270      * Assign $(D_PSYMBOL data) to a node specified by $(D_PSYMBOL path). If that node
271      * already had value, replace it with $(D_PSYMBOL data), but increment value 
272      * version and keep reference to previous value.
273      */
274     void put(string path, Data data);
275     
276     Optional!LinkWithCoordinates resolveValueChainWithCoordinates(string path, string upToRoot="");
277     
278     final override Optional!Link resolveValueChain(string path, string upToRoot="") {
279         return resolveValueChainWithCoordinates(path, upToRoot).map!(x => x.valueChain).toOptional;
280     }
281     
282     /**
283      * Returns: real path of the value that would be returned from 
284      * $(D_PSYMBOL resolve(path)) wrapped into optional; empty if aforementioned 
285      * call would result in empty optional.
286      */
287     final Optional!string resolveCoordinates(string path, string upToRoot="") {
288         return resolveValueChainWithCoordinates(path, upToRoot).map!(x => x.realPath).toOptional;
289     }
290     
291     //todo pop(pathComponents) (decrements versionNo, brings back previous val, returns removed value)
292     //todo popToVersion(pathComponents, size_t targetVersion) (pops number of times, returns array of values in order of poping, if targetVersion>current or targetVersion<-1 - exception, if target == current - no-op); this should probably be external function, so we 
293     //can provide default, but impl can optimize it as well
294 }
295 
296 /**
297  * Path tree is a tree where each node has a path assigned. Path is joined with dots
298  * which represent tree hierarchy. Each node can hold an optional value. Values
299  * are overridable, but history of values is kept, so we can analyze version
300  * number for a given node (number of times $(D_PSYMBOL put) was used with that 
301  * node as target) and revert to previous version of that node as well.
302  *
303  * Besides retrieving exact value with $(D_PSYMBOL get), we can also 
304  * $(D_PSYMBOL resolve) a path. In this case if a node specified by path has no 
305  * value, we fall back to parent node (with fallback to grandparent node, etc). 
306  * This mode of retrieving values is very useful when using this structure as 
307  * description of some other hierarchy, e.g. package structure and its related
308  * log levels.
309  */
310 class ConcretePathTree(Data): PathTree!Data {
311     private PathTreeNode!Data root = new PathTreeNode!Data("");
312     
313     override void put(string path, Data data){
314         root.put(path, data);
315     }
316     
317     Optional!Link getValueChain(string path) { //todo untested
318         return root.getValueChain(path);
319     }
320 
321     override Optional!LinkWithCoordinates resolveValueChainWithCoordinates(string path, string upToRoot="") { //todo untested
322         return root.resolveValueChainWithCoordinates(path, upToRoot);
323     }
324     
325 }
326 
327 //todo move to another source set
328 ///basic usage
329 unittest {
330     PathTree!string tree = new ConcretePathTree!string;
331 
332     tree.put("abc.def.g", "x");
333     tree.put("abc.def.g", "y");
334 
335     assert(tree.get("abc.def.g") == "y".some);
336     assert(tree.getVersion("abc.def.g") == 1.some);
337     
338     assert(tree.get("abc.def.g.h") == no!string);
339     assert(tree.getVersion("abc.def.g.h") == no!size_t);
340     
341     assert(tree.resolve("abc.def.g.h") == "y".some);
342     assert(tree.resolveVersion("abc.def.g.h") == 1.some);
343     assert(tree.resolveCoordinates("abc.def.g.h") == "abc.def.g".some);
344     
345     auto mappedPaths = tree.subtree("abc");
346     assert(mappedPaths.get("def.g") == "y".some);
347     assert(mappedPaths.getVersion("def.g") == 1.some);
348     
349     assert(mappedPaths.resolve("def.g.h") == "y".some);
350     assert(mappedPaths.resolveVersion("def.g.h") == 1.some);
351     
352     mappedPaths = mappedPaths.subtree("def");
353     assert(mappedPaths.get("g") == "y".some);
354     assert(mappedPaths.getVersion("g") == 1.some);
355     
356     assert(mappedPaths.resolve("g.h") == "y".some);
357     assert(mappedPaths.resolveVersion("g.h") == 1.some);
358     
359     auto mappedKeys = mappedPaths.mapValues!size_t(x => x.length);
360     assert(mappedKeys.get("g") == 1.some);
361     assert(mappedKeys.getVersion("g") == 1.some);
362     
363     assert(mappedKeys.resolve("g.h") == 1.some);
364     assert(mappedKeys.resolveVersion("g.h") == 1.some);
365 }