1 /// This module provides a utility class to store different type values inside a single field. 2 /// This can for example be used to model inheritance. 3 module mongoschema.variant; 4 5 import std.meta; 6 import std.traits; 7 import std.variant; 8 9 import vibe.data.bson; 10 11 import mongoschema; 12 13 private enum bool distinctFieldNames(names...) = __traits(compiles, { 14 static foreach (__name; names) 15 static if (is(typeof(__name) : string)) 16 mixin("enum int " ~ __name ~ " = 0;"); 17 else 18 mixin("enum int " ~ __name.stringof ~ " = 0;"); 19 }); 20 21 /// Represents a data type which can hold different kinds of values but always exactly one or none at a time. 22 /// Types is a list of types the variant can hold. By default type IDs are assigned from the stringof value which is the type name without module name. 23 /// You can pass custom type names by passing a string following the type. 24 /// Those will affect the type value in the serialized bson and the convenience access function names. 25 /// Serializes the Bson as `{"type": "T", "value": "my value here"}` 26 final struct SchemaVariant(Specs...) if (distinctFieldNames!(Specs)) 27 { 28 // Parse (type,name) pairs (FieldSpecs) out of the specified 29 // arguments. Some fields would have name, others not. 30 private template parseSpecs(Specs...) 31 { 32 static if (Specs.length == 0) 33 { 34 alias parseSpecs = AliasSeq!(); 35 } 36 else static if (is(Specs[0])) 37 { 38 static if (is(typeof(Specs[1]) : string)) 39 { 40 alias parseSpecs = AliasSeq!(FieldSpec!(Specs[0 .. 2]), parseSpecs!(Specs[2 .. $])); 41 } 42 else 43 { 44 alias parseSpecs = AliasSeq!(FieldSpec!(Specs[0]), parseSpecs!(Specs[1 .. $])); 45 } 46 } 47 else 48 { 49 static assert(0, 50 "Attempted to instantiate Variant with an invalid argument: " ~ Specs[0].stringof); 51 } 52 } 53 54 private template specTypes(Specs...) 55 { 56 static if (Specs.length == 0) 57 { 58 alias specTypes = AliasSeq!(); 59 } 60 else static if (is(Specs[0])) 61 { 62 static if (is(typeof(Specs[1]) : string)) 63 { 64 alias specTypes = AliasSeq!(Specs[0], specTypes!(Specs[2 .. $])); 65 } 66 else 67 { 68 alias specTypes = AliasSeq!(Specs[0], specTypes!(Specs[1 .. $])); 69 } 70 } 71 else 72 { 73 static assert(0, 74 "Attempted to instantiate Variant with an invalid argument: " ~ Specs[0].stringof); 75 } 76 } 77 78 private template FieldSpec(T, string s = T.stringof) 79 { 80 alias Type = T; 81 alias name = s; 82 } 83 84 alias Fields = parseSpecs!Specs; 85 alias Types = specTypes!Specs; 86 87 template typeIndex(T) 88 { 89 enum hasType = staticIndexOf!(T, Types); 90 } 91 92 template hasType(T) 93 { 94 enum hasType = staticIndexOf!(T, Types) != -1; 95 } 96 97 public: 98 Algebraic!Types value; 99 100 this(T)(T value) @trusted 101 { 102 this.value = value; 103 } 104 105 static foreach (Field; Fields) 106 mixin("Field.Type " ~ Field.name 107 ~ "() @trusted { checkType!(Field.Type); return value.get!(Field.Type); }"); 108 109 void checkType(T)() 110 { 111 if (!isType!T) 112 throw new Exception("Attempted to access " ~ type ~ " field as " ~ T.stringof); 113 } 114 115 bool isType(T)() @trusted 116 { 117 return value.type == typeid(T); 118 } 119 120 string type() 121 { 122 if (!value.hasValue) 123 return null; 124 125 static foreach (Field; Fields) 126 if (isType!(Field.Type)) 127 return Field.name; 128 129 assert(false, "Checked all possible types of variant but none of them matched?!"); 130 } 131 132 void opAssign(T)(T value) @trusted if (hasType!T) 133 { 134 this.value = value; 135 } 136 137 static Bson toBson(SchemaVariant!Specs value) 138 { 139 if (!value.value.hasValue) 140 return Bson.init; 141 142 static foreach (Field; Fields) 143 if (value.isType!(Field.Type)) 144 return Bson([ 145 "type": Bson(Field.name), 146 "value": toSchemaBson((() @trusted => value.value.get!(Field.Type))()) 147 ]); 148 149 assert(false, "Checked all possible types of variant but none of them matched?!"); 150 } 151 152 static SchemaVariant!Specs fromBson(Bson bson) 153 { 154 if (bson.type != Bson.Type.object) 155 return SchemaVariant!Specs.init; 156 auto type = "type" in bson.get!(Bson[string]); 157 if (!type || type.type != Bson.Type..string) 158 throw new Exception( 159 "Malformed " ~ SchemaVariant!Specs.stringof ~ " bson, missing or invalid type argument"); 160 161 switch (type.get!string) 162 { 163 static foreach (i, Field; Fields) 164 { 165 case Field.name: 166 return SchemaVariant!Specs(fromSchemaBson!(Field.Type)(bson["value"])); 167 } 168 default: 169 throw new Exception("Invalid " ~ SchemaVariant!Specs.stringof ~ " type " ~ type.get!string); 170 } 171 } 172 } 173 174 unittest 175 { 176 struct Foo 177 { 178 int x = 3; 179 } 180 181 struct Bar 182 { 183 string y = "bar"; 184 } 185 186 SchemaVariant!(Foo, Bar) var1; 187 assert(typeof(var1).toBson(var1) == Bson.init); 188 var1 = Foo(); 189 assert(typeof(var1).toBson(var1) == Bson([ 190 "type": Bson("Foo"), 191 "value": Bson(["x": Bson(3)]) 192 ])); 193 assert(var1.type == "Foo"); 194 var1 = Bar(); 195 assert(typeof(var1).toBson(var1) == Bson([ 196 "type": Bson("Bar"), 197 "value": Bson(["y": Bson("bar")]) 198 ])); 199 assert(var1.type == "Bar"); 200 201 var1 = typeof(var1).fromBson(Bson([ 202 "type": Bson("Foo"), 203 "value": Bson(["x": Bson(4)]) 204 ])); 205 assert(var1.type == "Foo"); 206 assert(var1.Foo == Foo(4)); 207 208 var1 = typeof(var1).fromBson(Bson([ 209 "type": Bson("Bar"), 210 "value": Bson(["y": Bson("barf")]) 211 ])); 212 assert(var1.type == "Bar"); 213 assert(var1.Bar == Bar("barf")); 214 215 SchemaVariant!(Foo, "foo", Bar, "bar") var2; 216 assert(typeof(var2).toBson(var2) == Bson.init); 217 var2 = Foo(); 218 assert(var2.type == "foo"); 219 assert(var2.foo == Foo()); 220 assert(typeof(var2).toBson(var2) == Bson([ 221 "type": Bson("foo"), 222 "value": Bson(["x": Bson(3)]) 223 ])); 224 }