1 module mage.util.properties;
2 
3 import mage;
4 import mage.util.option;
5 import std.typetuple : allSatisfy;
6 import std.traits : isCallable, fullyQualifiedName;
7 
8 class MissingKeyError : core.exception.Error
9 {
10   @safe pure nothrow this(string key, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
11   {
12     super(`Missing key "` ~ key ~ `".`, file, line, next);
13   }
14 }
15 
16 
17 /// Wraps std.variant.Variant
18 struct Property
19 {
20   static import std.variant;
21 
22   alias Callable = Variant delegate();
23 
24   std.variant.Variant value;
25 
26   package bool isCallableValue = false;
27 
28   this(T)(auto ref T value) { this.opAssign(value); }
29 
30   void opAssign(T)(auto ref T value)
31   {
32     this.isCallableValue = isCallable!T;
33 
34     static if(!isCallable!T)
35     {
36       this.value = value;
37     }
38     else
39     {
40       import std.functional : toDelegate, FunctionAttribute, functionAttributes;
41 
42       alias DelType = typeof(toDelegate(value));
43       static assert(!(functionAttributes!DelType & FunctionAttribute.safe) && !(functionAttributes!DelType & FunctionAttribute.trusted),
44                     "@safe and @trusted functions are not supported due to some bug in phobos... Use @system instead for now.");
45       static assert(is(DelType == Callable),
46                     "Expected type `" ~ Callable.stringof ~ "', got `" ~ DelType.stringof ~ "'.");
47 
48       this.value = toDelegate(value);
49     }
50   }
51 
52   /// Shadows $(D std.variant.Variant.get).
53   auto get(T)() if(!is(T == const))
54   {
55     if(this.isCallableValue) {
56       return this.value.get!Callable()().get!T;
57     }
58 
59     return this.value.get!T;
60   }
61 
62   /// Shadows $(D std.variant.Variant.get).
63   auto get(T)() const if(is(T == const))
64   {
65     if(this.isCallableValue) {
66       return this.value.get!(const Callable)()().get!T;
67     }
68 
69     return this.value.get!T;
70   }
71 
72   alias value this;
73 }
74 
75 ///
76 unittest
77 {
78   Property p;
79   assert(!p.isCallableValue);
80   p = 42;
81   assert(!p.isCallableValue);
82   assert(p == 42);
83   assert(p.get!int == 42);
84 
85   p = () @system => Variant(1337);
86   assert(p.isCallableValue);
87   assert(p.get!int() == 1337);
88 
89   auto prop1 = Properties("first");
90   prop1["foo"] = "hello";
91 
92   auto prop2 = Properties("second");
93   prop2["foo"] = () @system => Variant(prop1["foo"].get!string ~ " world!");
94 
95   assert(prop2["foo"].isCallableValue);
96   assert(prop2["foo"].get!string() == "hello world!");
97   prop1["foo"] = "goodbye cruel";
98   assert(prop2["foo"].get!string() == "goodbye cruel world!");
99 }
100 
101 
102 struct Properties
103 {
104   string name = "<anonymous>";
105 
106   /// Dynamic property storage.
107   /// Note: This member is public on purpose.
108   Property[string] props;
109 
110   this(this) {
111     this.props = this.props.dup;
112   }
113 
114   inout(Property)* tryGet(in string key, inout(Property)* otherwise = null) inout
115   {
116     auto prop = key in this.props;
117     return prop ? prop : otherwise;
118   }
119 
120   ref inout(Property) opIndex(in string key) inout
121   {
122     auto prop = this.tryGet(key);
123     if(prop) {
124       return *prop;
125     }
126     throw new MissingKeyError(key);
127   }
128 
129   void opIndexAssign(T)(auto ref T value, string key)
130   {
131     this.props[key] = value;
132   }
133 
134   string toString() const
135   {
136     return "Properties(%s)".format(this.name);
137   }
138 
139   void prettyPrint() const
140   {
141     auto _ = log.Block(this.toString());
142 
143     foreach(key, ref value; this.props) {
144       log.info("%s: %s", key, (cast()value).toString());
145     }
146   }
147 }
148 
149 /// set/get
150 unittest
151 {
152   import std.exception : assertThrown;
153 
154   auto props = Properties("props");
155   props["name"] = "hello";
156   assert(props["name"].get!string() == "hello");
157   props["custom"] = 1337;
158   assert(props["custom"].get!int() == 1337);
159   props["asdfghjkl"] = 42;
160   assert(props["asdfghjkl"].convertsTo!int());
161   assert(props["asdfghjkl"].get!int() == 42);
162   //struct IntValue { int value; }
163   //assert(props["asdfghjkl"].coerce!IntValue().value == 42);
164 
165   assertThrown!MissingKeyError(props["iDontExist"].get!float());
166   assert(props.tryGet("iDontExist", null) is null);
167   {
168     Property fallback;
169     assert(props.tryGet("iDontExist", &fallback) == &fallback);
170   }
171 
172   // Overwrite.
173   props["name"] = "world";
174   assert(props["name"].get!string() == "world");
175 }
176 
177 
178 import std.traits : Unqual;
179 enum isProperties(T) = is(Unqual!T == Properties);
180 
181 
182 /// Linking Properties together linearly (as opposed to hierarchically).
183 struct Environment
184 {
185   Properties*[] env;
186   string name;
187   Environment* internal = null;
188 
189   /// Construct an environment with a name and the given properties.
190   /// Example: Properties p1, p2, p3, p4; Environment("theName", p1, [ &p3, &p4 ], p2);
191   this(Props...)(string name, auto ref Props props)
192   {
193     this.name = name;
194     foreach(ref prop; props) {
195       static if(isInputRange!(typeof(prop)))
196       {
197         foreach(p; prop) {
198           static assert(is(typeof(p) == Properties*), "The array you pass must contain Properties*, not " ~ typeof(p).stringof);
199           this.env ~= p;
200         }
201       }
202       else
203       {
204         static assert(isProperties!(typeof(prop)));
205         this.env ~= &prop;
206       }
207     }
208   }
209 
210   /// Pointer to the first occurrence of `key' in this environment.
211   inout(Property)* first(in string key, lazy Property* otherwise = null) inout
212   {
213     auto all = this.all(key);
214     auto result = all.empty ? otherwise : all.front;
215     return cast(typeof(return))result;
216   }
217 
218   /// Return: A range containing $(D Property*).
219   auto all(in string key)
220   {
221     return this.env.map!(a => a.tryGet(key, null)) // Properties* => Property*
222                    .filter!(a => a !is null);      // Allow no null values.
223   }
224 
225   /// Return: A range containing $(D Property*).
226   auto all(in string key) const
227   {
228     return this.env.map!(a => a.tryGet(key, null)) // Properties* => Property*
229                    .filter!(a => a !is null);      // Allow no null values.
230   }
231 
232   ref inout(Property) opIndex(in string key) inout
233   {
234     auto val = this.first(key);
235     enforce!MissingKeyError(val, `Missing key "%s" in environment "%s".`
236                                  .format(key, this.name));
237     return *val;
238   }
239 
240   /// Set a value in this.env[0].
241   void opIndexAssign(T)(auto ref T value, in string key)
242   {
243     assert(this.env.length > 0, `Environment "%s" is empty!`.format(this.name));
244     (*this.env[0])[key] = value;
245   }
246 
247   string toString() const
248   {
249     return "Environment(%s)".format(this.name);
250   }
251 
252   void prettyPrint() const
253   {
254     auto _ = log.Block(this.toString());
255 
256     foreach(p; this.env)
257     {
258       assert(p);
259       p.prettyPrint();
260     }
261 
262     if(this.internal) {
263       this.internal.prettyPrint();
264     }
265   }
266 }
267 
268 unittest
269 {
270   import std.exception;
271 
272   auto p1 = Properties("p1");
273   auto p2 = Properties("p2");
274   auto p3 = Properties("p3");
275   auto p4 = Properties("p4");
276   auto env = Environment("env", p1, p2, p3, p4);
277   p1["something"] = "hello";
278   p2["something"] = " ";
279   // p3 left empty on purpose.
280   p4["something"] = "world";
281   assert(env.first("something").get!string() == "hello");
282   assert(env.all("something").map!(a => a.get!string()).equal(["hello", " ", "world"]));
283 
284   assert(env.first("foo") is null);
285   assertThrown!MissingKeyError(env["foo"] == 3.1415f);
286   env["foo"] = 3.1415f;
287   assert(env["foo"].get!float == 3.1415f);
288   assert(p1["foo"].get!float == 3.1415f);
289   assert(p2.tryGet("foo") is null);
290   assert(p3.tryGet("foo") is null);
291   assert(p4.tryGet("foo") is null);
292 
293   p3["bar"] = null;
294   assert(env.first("bar") !is null);
295   assert(env["bar"].get!(typeof(null)) is null);
296   assert(p1.tryGet("bar") is null);
297   assert(p2.tryGet("bar") is null);
298   assert(p3.tryGet("bar") !is null);
299   assert(p4.tryGet("bar") is null);
300 }
301 
302 mixin template PropertiesOperators(alias memberName)
303 {
304   inout(Property)* tryGet(string key) inout {
305     return memberName.tryGet(key);
306   }
307 
308   void opIndexAssign(T)(auto ref T value, string key) {
309     memberName[key] = value;
310   }
311 
312   ref inout(Property) opIndex(string key) inout {
313     return memberName[key];
314   }
315 }
316 
317 unittest
318 {
319   static struct Wrapper
320   {
321     Properties props;
322     mixin PropertiesOperators!props;
323   }
324 
325   Wrapper w;
326   w["foo"] = 123;
327   assert(w["foo"].get!int == 123);
328   assert(w["foo"] == 123);
329   assert(w.tryGet("bar") is null);
330   w["bar"] = "hello world";
331   assert(w.tryGet("bar") !is null);
332   assert(*w.tryGet("bar") == "hello world");
333 }