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 }