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 }