1 module drecord;
2 
3 import std.meta;
4 import std.traits;
5 import std.conv : to;
6 
7 /++ Add a field and getter to the record
8 + Params:
9 +	type: The type the field will be
10 +	name: Name of the field 
11 +	args: An optional default initialisation lambda (no arguments, returns value) ++/
12 template get(alias type, string name, args...)
13 {
14 	static assert(args.length <= 1, "There may only be 0 or 1 default initialisers");
15 
16 	private alias type_ = type;
17 	private alias name_ = name;
18 	private alias args_ = args;
19 }
20 
21 private mixin template getImpl(alias type, string name, args...)
22 {
23 	static if(args.length == 0)
24 		mixin("protected " ~ type.stringof ~ " " ~ name ~ "_;");
25 	else
26 		mixin("protected " ~ type.stringof ~ " " ~ name ~ "_ = AliasSeq!(args)[0]();");
27 	mixin("public @property auto " ~ name ~ "() { return " ~ name ~ "_; }");
28 }
29 
30 /++ Add a field and appropriate getter and setter to the record
31 + Params:
32 +	type: The type the field will be
33 +	name: Name of the field 
34 +	args: An optional default initialisation lambda (no arguments, returns value) ++/
35 template get_set(alias type, string name, args...)
36 {
37 	static assert(args.length <= 1, "There may only be 0 or 1 default initialisers");
38 
39 	private alias type_ = type;
40 	private alias name_ = name;
41 	private alias args_ = args;
42 }
43 
44 private mixin template get_setImpl(alias type, string name, args...)
45 {
46 	static if(args.length == 0)
47 		mixin("protected " ~ type.stringof ~ " " ~ name ~ "_;");
48 	else
49 		mixin("protected " ~ type.stringof ~ " " ~ name ~ "_ = AliasSeq!(args)[0]();");
50 	mixin("public @property auto " ~ name ~ "() { return " ~ name ~ "_; }");
51 	mixin("public @property void " ~ name ~ "(" ~ type.stringof ~ " nval__) { " ~ name ~ "_ = nval__; }");
52 }
53 
54 /++ Add a field whose value is computed when the record is created.
55 + It is run after `get`, and `get_set` fields have been set.
56 + Params:
57 +	type: Type of the field
58 +	name: Name of the field
59 +	construct: A lambda value that sets this field
60 + Notice: the construct lambda must accept the record as its only parameter
61 +	and return the field value.
62 ++/
63 template get_compute(alias type, string name, alias construct)
64 {
65 	private alias type_ = type;
66 	private alias name_ = name;
67 	private alias construct_ = construct;
68 }
69 
70 private mixin template get_computeImpl(alias type, string name, alias construct)
71 {
72 	private static string generateImpl()
73 	{
74 		string header = "protected " ~ type.stringof ~ " " ~ name ~ "_;" ~ 
75 			"protected @property auto " ~ name ~ "_construct() { return construct(this); }" ~
76 			"public @property auto " ~ name ~ "() { return " ~ name ~ "_; }";
77 		return header;
78 	}
79 	mixin(generateImpl);
80 }
81 
82 /++ Add a property to the record
83 + Params:
84 +	name: Name of the property
85 +	accessor: A lambda of the property body
86 +	args: Types of arguments the property needs ++/
87 template property(string name, alias accessor, args...)
88 {
89 	private alias name_ = name;
90 	private alias accessor_ = accessor;
91 	private alias args_ = AliasSeq!args;
92 }
93 
94 private mixin template propertyImpl(string name, alias accessor, args...)
95 {
96 	private alias seq = AliasSeq!args;
97 	private static string generateImpl()
98 	{
99 		string header = "public @property auto " ~ name ~ "(";
100 		string body_ = "{ return accessor(this, ";
101 		static foreach(i, item; seq)
102 		{
103 			header ~= item.stringof ~ " arg" ~ to!string(i) ~ "__";
104 			body_ ~= "arg" ~ to!string(i) ~ "__";
105 			static if(i < seq.length - 1) 
106 			{
107 				header ~= ", ";
108 				body_ ~= ", ";
109 			}
110 		}
111 		header ~= ") ";
112 		body_ ~= "); }";
113 		return header ~ body_;
114 	}
115 	mixin(generateImpl);
116 }
117 
118 template record(args...)
119 {
120 	private enum isGet(alias T) = __traits(isSame, TemplateOf!T, get);
121 	private enum isGetSet(alias T) = __traits(isSame, TemplateOf!T, get_set);
122 	private enum isProperty(alias T) = __traits(isSame, TemplateOf!T, property);
123 	private enum isGetCompute(alias T) = __traits(isSame, TemplateOf!T, get_compute);
124 
125 	private enum isCtorParam(alias T) = isGet!T || isGetSet!T;
126 	private enum numCtorParam = Filter!(isCtorParam, AliasSeq!args).length;
127 	private enum isField(alias T) = isGet!T || isGetSet!T || isGetCompute!T;
128 	private enum numFields = Filter!(isField, AliasSeq!args).length;
129 
130 	/// Generate a constructor that takes inputs for every field
131 	private static string genCtor()
132 	{
133 		string header = "public this(";
134 		string body_ = "{\n";
135 
136 		static foreach(i, item; Filter!(isCtorParam, AliasSeq!args))
137 		{
138 			header ~= item.type_.stringof ~ " arg" ~ to!string(i) ~ "__";
139 			body_ ~= "\t" ~ item.name_ ~ "_ = arg" ~ to!string(i) ~ "__;\n";
140 
141 			static if(i < numCtorParam - 1)
142 				header ~= ", ";
143 		}
144 		header ~= ")\n";
145 		body_ ~= "constructs; }";
146 		return header ~ body_;
147 	}
148 
149 	/++ Test for equality. Reference types are checked to ensure
150 	+ their references are the same (point to same thing), value types
151 	+ are checked to ensure their values are identical.
152 	++/
153 	private static string genEquals()
154 	{
155 		string res = "import std.math : isClose;\nbool result = true;\n";
156 		static foreach(i, item; Filter!(isField, AliasSeq!args))
157 		{
158 			static if(is(item.type_ == class) ||
159 					  is(item.type_ == interface) ||
160 					  isPointer!(item.type_))
161 			{
162 				res ~= "if(" ~ item.name_ ~ "_ !is otherRec." ~ item.name_ ~ "_) { result = false; }\n";
163 			}
164 			else static if(isFloatingPoint!(item.type_))
165 			{
166 				res ~= "if(!isClose(" ~ item.name_ ~ "_, otherRec." ~ item.name_ ~ "_)) { result = false; }\n";
167 			}
168 			else
169 			{
170 				res ~= "if(" ~ item.name_ ~ "_ != otherRec." ~ item.name_ ~ "_) { result = false; }\n";
171 			}
172 		}
173 		res ~= "return result;";
174 		return res;
175 	}
176 
177 	final class record
178 	{
179 		static foreach(item; AliasSeq!args)
180 		{
181 			static if(isGet!item)
182 				mixin getImpl!(item.type_, item.name_, item.args_);
183 			else static if(isGetSet!item)
184 				mixin get_setImpl!(item.type_, item.name_, item.args_);
185 			else static if(isProperty!item) 
186 				mixin propertyImpl!(item.name_, item.accessor_, item.args_);
187 			else static if(isGetCompute!item)
188 				mixin get_computeImpl!(item.type_, item.name_, item.construct_);
189 			else static assert(false, "Unsupported type. Please ensure types for record are either get!T, get_set!T, property!T");
190 		}
191 
192 		/// Default initialise all fields
193 		this(bool runConstructs = true) 
194 		{
195 			if(runConstructs)
196 			{
197 				constructs;
198 			}
199 		}
200 
201 		private void constructs()
202 		{
203 			static foreach(item; Filter!(isGetCompute, AliasSeq!args))
204 			{
205 				mixin("this." ~ item.name_ ~ "_ = " ~ item.name_ ~ "_construct();");
206 			}
207 		}
208 
209 		/// Explicitly set all fields
210 		mixin(genCtor);
211 
212 		/// Explicitly set certain fields, default initialise the rest
213 		static record create(TNames...)(...)
214 		{
215 			auto r = new record(false);
216 			import core.vararg;
217 			static foreach(item; AliasSeq!TNames)
218 			{
219 				static foreach(b; AliasSeq!args)
220 					static if(isGetCompute!b)
221 						static assert(b.name_ != item, "Cannot set a get_compute property '" ~ item ~ "'");
222 
223 				mixin("r." ~ item ~ "_ = va_arg!(typeof(" ~ item ~ "_))(_argptr);");
224 			}
225 			r.constructs;
226 			return r;
227 		}
228 
229 		/++ Test for equality. Reference types are checked to ensure
230 		+ their references are the same (point to same thing), value types
231 		+ are checked to ensure their values are identical.
232 		++/
233 		override bool opEquals(Object other)
234 		{
235 			if(record otherRec = cast(record)other)
236 			{
237 				mixin(genEquals);
238 			}
239 			else return false;
240 		}
241 
242 		/// Generate a human-readable string. Fields are sampled.
243 		override string toString()
244 		{
245 			string result = "{";
246 
247 			static foreach(i, item; Filter!(isField, AliasSeq!args))
248 			{
249 				result ~= item.name_ ~ " = " ~ to!string(mixin(item.name_ ~ "_"));
250 				static if(i < numFields - 1)
251 					result ~= ", ";
252 			}
253 			result ~= "}";
254 
255 			return result;
256 		}
257 
258 		/// Compute the hash of this record. Algorithm is `result = 31 * previousResult + currentField.toHash`
259 		override nothrow @trusted size_t toHash()
260 		{
261 			size_t result = 0;
262 
263 			static foreach(i, item; Filter!(isField, AliasSeq!args))
264 			{
265 				static if(is(item.type_ == class) || is(item.type_ == interface))
266 					result = result * 31 + cast(size_t)mixin(item.name_ ~ "_.toHash()");
267 				else static if(is(item.type_ == struct))
268 				{
269 					static if(__traits(compiles, () { item.type_().toHash; }()))
270 						result = result * 31 + cast(size_t)mixin(item.name_ ~ "_");
271 				}
272 				else
273 					result = result * 31 + cast(size_t)mixin(item.name_ ~ "_");
274 			}
275 
276 			return result;
277 		}
278 		/++ Create a copy of this record and permit modification of read-only fields. ++/
279 		record duplicate(TNames...)(...)
280 		{
281 			record r = new record;
282 			static foreach(item; Filter!(isField, AliasSeq!args))
283 				mixin("r." ~ item.name_ ~ "_ = this." ~ item.name_ ~ "_;");
284 			import core.vararg;
285 			static foreach(item; AliasSeq!TNames)
286 			{
287 				static foreach(b; AliasSeq!args)
288 					static if(isGetCompute!b)
289 						static assert(b.name_ != item, "Cannot set a get_compute property '" ~ item ~ "'");
290 
291 				mixin("r." ~ item ~ "_ = va_arg!(typeof(" ~ item ~ "_))(_argptr);");
292 			}
293 			r.constructs;
294 			return r;
295 		}
296 	}
297 }
298 
299 unittest
300 {
301 	alias MyRecord = record!(
302 		get!(int, "x"), /// x is an int, can only be set during construction
303 		get_set!(float, "y"), /// y is a float, can be get or set whenever
304 		property!("getDoubleOfX", (r) => r.x * 2), /// a property that returns the double of x
305 		property!("getMultipleOfX", (r, m) => r.x * m, int), /// that takes an argument and multiples x by that value
306 		property!("resetY", (r) => r.y = 0) /// resets y to 0f
307 	); 
308 
309 	auto r = new MyRecord(12, 4.5f); /// sets x, y
310 
311 	assert(r.toString == `{x = 12, y = 4.5}`,);
312 	assert(r.toHash == 376);
313 
314 	assert(r.x == 12);
315 	assert(r.getDoubleOfX == 24);
316 	assert(r.getMultipleOfX(4) == 48);
317 	assert(r.y == 4.5f);
318 	r.resetY;
319 	assert(r.y == 0);
320 	r.y = 13f;
321 	assert(r.y == 13f);
322 
323 	/// Duplicate r, and set x to 17 (we can only do this in ctor, or during duplication)
324 	/// This is equivalent to C#'s "with" syntax for records [0]
325 	r.y = 0;
326 	auto q = r.duplicate!("x")(17); 
327 	assert(q.toString == `{x = 17, y = 0}`);
328 	assert(q != r);
329 	assert(q !is r);
330 
331 	auto b = r.duplicate; // duplicate, don't change any fields
332 	assert(b == r);
333 	assert(b !is r);
334 }
335 
336 unittest
337 {
338 	alias DefaultRecord = record!(
339 		// The third parameter is a lambda which provides default initialisation
340 		get!(int, "x", () => 4), // x is set to 4 by default
341 		get_set!(Object, "o", () => new Object) // o is set to a new Object by default
342 	);
343 
344 	auto r = new DefaultRecord; // run the default initialisers
345 	assert(r.toString == `{x = 4, o = object.Object}`);
346 
347 	auto q = DefaultRecord.create!"x"(9); // run default initialisers, then set x to 9
348 	assert(q.toString == `{x = 9, o = object.Object}`);
349 }
350 
351 version (unittest)
352 {
353 	alias TC3Record = record!(
354 		get!(int, "x", () => 20),
355 		// get_compute lets you compute a field after the rest have been initialised
356 		get_compute!(float, "y", (rec) => rec.x * 2f)
357 	);
358 }
359 unittest
360 {
361 	auto r = new TC3Record;
362 	assert(r.toString == `{x = 20, y = 40}`);
363 	r = new TC3Record(10);
364 	assert(r.toString == `{x = 10, y = 20}`);
365 	r = TC3Record.create!"x"(5);
366 	assert(r.toString == `{x = 5, y = 10}`);
367 	auto q = r.duplicate!"x"(2);
368 	assert(q.toString == `{x = 2, y = 4}`);
369 }