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 ~= ∝ 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 }