1 /++
2     Functions used to generate strings of statements describing the differences
3     (or delta) between two instances of a struct or class of the same type.
4     They can be either assignment statements or assert statements.
5 
6     Example:
7     ---
8     struct Foo
9     {
10         string s;
11         int i;
12         bool b;
13     }
14 
15     Foo altered;
16 
17     altered.s = "some string";
18     altered.i = 42;
19     altered.b = true;
20 
21     Appender!(char[]) sink;
22 
23     // Fill with delta between `Foo.init` and modified `altered`
24     sink.formatDeltaInto!(No.asserts)(Foo.init, altered);
25 
26     assert(sink.data ==
27     `s = "some string";
28     i = 42;
29     b = true;
30     `);
31     sink.clear();
32 
33     // Do the same but prepend the name "altered" to the member names
34     sink.formatDeltaInto!(No.asserts)(Foo.init, altered, 0, "altered");
35 
36     assert(sink.data ==
37     `altered.s = "some string";
38     altered.i = 42;
39     altered.b = true;
40     `);
41     sink.clear();
42 
43     // Generate assert statements instead, for easy copy/pasting into unittest blocks
44     sink.formatDeltaInto!(Yes.asserts)(Foo.init, altered, 0, "altered");
45 
46     assert(sink.data ==
47     `assert((altered.s == "some string"), altered.s);
48     assert((altered.i == 42), altered.i.to!string);
49     assert(altered.b, altered.b.to!string);
50     `);
51     ---
52  +/
53 module lu.deltastrings;
54 
55 private:
56 
57 import std.range.primitives : isOutputRange;
58 import std.traits : isAggregateType;
59 import std.typecons : Flag, No, Yes;
60 
61 public:
62 
63 import lu.uda : Hidden;
64 
65 //@safe:
66 
67 
68 // formatDeltaInto
69 /++
70     Constructs statement lines for each changed field (or the delta) between two
71     instances of a struct and stores them into a passed output sink.
72 
73     Example:
74     ---
75     struct Foo
76     {
77         string s;
78         int i;
79         bool b;
80     }
81 
82     Foo altered;
83 
84     altered.s = "some string";
85     altered.i = 42;
86     altered.b = true;
87 
88     Appender!(char[]) sink;
89     sink.formatDeltaInto!(No.asserts)(Foo.init, altered);
90     ---
91 
92     Params:
93         asserts = Whether or not to build assert statements or assignment statements.
94         sink = Output buffer to write to.
95         before = Original struct object.
96         after = Changed struct object.
97         indents = The number of tabs to indent the lines with.
98         submember = The string name of a recursing symbol, if applicable.
99  +/
100 void formatDeltaInto(Flag!"asserts" asserts = No.asserts, Sink, QualThing)
101     (auto ref Sink sink,
102     auto ref QualThing before,
103     auto ref QualThing after,
104     const uint indents = 0,
105     const string submember = string.init)
106 if (isOutputRange!(Sink, char[]) && isAggregateType!QualThing)
107 {
108     immutable prefix = submember.length ? (submember ~ '.') : string.init;
109 
110     foreach (immutable i, ref member; after.tupleof)
111     {
112         import lu.uda : Hidden;
113         import std.traits : Unqual, hasUDA, isAggregateType, isArray,
114             isSomeFunction, isSomeString, isType;
115 
116         alias T = Unqual!(typeof(member));
117         enum memberstring = __traits(identifier, before.tupleof[i]);
118 
119         static if (hasUDA!(after.tupleof[i], Hidden))
120         {
121             // Member is annotated as Hidden; skip
122             continue;
123         }
124         else static if (isAggregateType!T)
125         {
126             // Recurse
127             sink.formatDeltaInto!asserts(before.tupleof[i], member, indents, prefix ~ memberstring);
128         }
129         else static if (!isType!member && !isSomeFunction!member && !__traits(isTemplate, member))
130         {
131             if (after.tupleof[i] != before.tupleof[i])
132             {
133                 static if (isArray!T && !isSomeString!T)
134                 {
135                     import std.range : ElementEncodingType;
136 
137                     // TODO: Rewrite this to recurse
138                     alias E = ElementEncodingType!T;
139 
140                     static if (isSomeString!E)
141                     {
142                         static if (asserts)
143                         {
144                             enum pattern = "%sassert((%s%s[%d] == \"%s\"), %2$s%3$s[%4$d]);\n";
145                         }
146                         else
147                         {
148                             enum pattern = "%s%s%s[%d] = \"%s\";\n";
149                         }
150                     }
151                     else static if (is(E == char))
152                     {
153                         static if (asserts)
154                         {
155                             enum pattern = "%sassert((%s%s[%d] == '%s'), %2$s%3$s[%4$d].to!string);\n";
156                         }
157                         else
158                         {
159                             enum pattern = "%s%s%s[%d] = '%s';\n";
160                         }
161                     }
162                     else
163                     {
164                         static if (asserts)
165                         {
166                             enum pattern = "%sassert((%s%s[%d] == %s), %2$s%3$s[%4$d].to!string);\n";
167                         }
168                         else
169                         {
170                             enum pattern = "%s%s%s[%d] = %s;\n";
171                         }
172                     }
173                 }
174                 else static if (isSomeString!T)
175                 {
176                     static if (asserts)
177                     {
178                         enum pattern = "%sassert((%s%s == \"%s\"), %2$s%3$s);\n";
179                     }
180                     else
181                     {
182                         enum pattern = "%s%s%s = \"%s\";\n";
183                     }
184                 }
185                 else static if (is(T == char))
186                 {
187                     static if (asserts)
188                     {
189                         enum pattern = "%sassert((%s%s == '%s'), %2$s%3$s.to!string);\n";
190                     }
191                     else
192                     {
193                         enum pattern = "%s%s%s = '%s';\n";
194                     }
195                 }
196                 else static if (is(T == enum))
197                 {
198                     enum typename = Unqual!QualThing.stringof ~ '.' ~ T.stringof;
199 
200                     static if (asserts)
201                     {
202                         immutable pattern = "%sassert((%s%s == " ~ typename ~ ".%s), " ~
203                             "Enum!(" ~ typename ~ ").toString(%2$s%3$s));\n";
204                     }
205                     else
206                     {
207                         immutable pattern = "%s%s%s = " ~ typename ~ ".%s;\n";
208                     }
209                 }
210                 else static if (is(T == bool))
211                 {
212                     static if (asserts)
213                     {
214                         immutable pattern = member ?
215                             "%sassert(%s%s);\n" :
216                             "%sassert(!%s%s);\n";
217                     }
218                     else
219                     {
220                         enum pattern = "%s%s%s = %s;\n";
221                     }
222                 }
223                 else
224                 {
225                     static if (asserts)
226                     {
227                         enum pattern = "%sassert((%s%s == %s), %2$s%3$s.to!string);\n";
228                     }
229                     else
230                     {
231                         enum pattern = "%s%s%s = %s;\n";
232                     }
233                 }
234 
235                 import std.format : formattedWrite;
236                 import std.range : repeat;
237                 import std.string : join;
238 
239                 immutable indentation = "    ".repeat(indents).join;
240 
241                 static if (isSomeString!T)
242                 {
243                     import std.array : replace;
244 
245                     immutable escaped = member
246                         .replace('\\', `\\`)
247                         .replace('"', `\"`);
248 
249                     sink.formattedWrite(pattern, indentation, prefix, memberstring, escaped);
250                 }
251                 else static if (isArray!T)
252                 {
253                     foreach (n, val; member)
254                     {
255                         if (before.tupleof[i][n] == after.tupleof[i][n]) continue;
256                         sink.formattedWrite(pattern, indentation, prefix, memberstring, n, member[n]);
257                     }
258                 }
259                 else
260                 {
261                     sink.formattedWrite(pattern, indentation, prefix, memberstring, member);
262                 }
263             }
264         }
265         else
266         {
267             static assert(0, "Cannot produce deltastrings for type `%s`"
268                 .format(Unqual!QualThing.stringof));
269         }
270     }
271 }
272 
273 ///
274 unittest
275 {
276     import lu.uda : Hidden;
277     import std.array : Appender;
278 
279     Appender!(char[]) sink;
280     sink.reserve(1024);
281 
282     struct Server
283     {
284         string address;
285         ushort port;
286         bool connected;
287     }
288 
289     struct Connection
290     {
291         enum State
292         {
293             unset,
294             disconnected,
295             connected,
296         }
297 
298         State state;
299         string nickname;
300         @Hidden string user;
301         @Hidden string password;
302         Server server;
303     }
304 
305     Connection conn;
306 
307     with (conn)
308     {
309         state = Connection.State.connected;
310         nickname = "NICKNAME";
311         user = "USER";
312         password = "hunter2";
313         server.address = "address.tld";
314         server.port = 1337;
315     }
316 
317     sink.formatDeltaInto!(No.asserts)(Connection.init, conn, 0, "conn");
318 
319     assert(sink.data ==
320 `conn.state = Connection.State.connected;
321 conn.nickname = "NICKNAME";
322 conn.server.address = "address.tld";
323 conn.server.port = 1337;
324 `, '\n' ~ sink.data);
325 
326     sink = typeof(sink).init;
327 
328     sink.formatDeltaInto!(Yes.asserts)(Connection.init, conn, 0, "conn");
329 
330     assert(sink.data ==
331 `assert((conn.state == Connection.State.connected), Enum!(Connection.State).toString(conn.state));
332 assert((conn.nickname == "NICKNAME"), conn.nickname);
333 assert((conn.server.address == "address.tld"), conn.server.address);
334 assert((conn.server.port == 1337), conn.server.port.to!string);
335 `, '\n' ~ sink.data);
336 
337     struct Foo
338     {
339         string s;
340         int i;
341         bool b;
342         char c;
343     }
344 
345     Foo f1;
346     f1.s = "string";
347     f1.i = 42;
348     f1.b = true;
349     f1.c = '$';
350 
351     Foo f2 = f1;
352     f2.s = "yarn";
353     f2.b = false;
354     f2.c = '#';
355 
356     sink = typeof(sink).init;
357 
358     sink.formatDeltaInto!(No.asserts)(f1, f2);
359     assert(sink.data ==
360 `s = "yarn";
361 b = false;
362 c = '#';
363 `, '\n' ~ sink.data);
364 
365     sink = typeof(sink).init;
366 
367     sink.formatDeltaInto!(Yes.asserts)(f1, f2);
368     assert(sink.data ==
369 `assert((s == "yarn"), s);
370 assert(!b);
371 assert((c == '#'), c.to!string);
372 `, '\n' ~ sink.data);
373 
374     sink = typeof(sink).init;
375 
376     {
377         struct S
378         {
379             int i;
380         }
381 
382         class C
383         {
384             string s;
385             bool b;
386             S child;
387         }
388 
389         C c1 = new C;
390         C c2 = new C;
391 
392         c2.s = "harbl";
393         c2.b = true;
394         c2.child.i = 42;
395 
396         sink.formatDeltaInto!(No.asserts)(c1, c2);
397         assert(sink.data ==
398 `s = "harbl";
399 b = true;
400 child.i = 42;
401 `, '\n' ~ sink.data);
402 
403         sink = typeof(sink).init;
404 
405         sink.formatDeltaInto!(Yes.asserts)(c1, c2);
406         assert(sink.data ==
407 `assert((s == "harbl"), s);
408 assert(b);
409 assert((child.i == 42), child.i.to!string);
410 `, '\n' ~ sink.data);
411     }
412     {
413         struct Blah
414         {
415             int[5] arr;
416             string[3] sarr;
417             char[2] carr;
418         }
419 
420         Blah b1;
421         Blah b2;
422         b2.arr = [ 1, 0, 3, 0, 5 ];
423         b2.sarr = [ "hello", string.init, "world" ];
424         b2.carr = [ 'a', char.init ];
425 
426         sink = typeof(sink).init;
427 
428         sink.formatDeltaInto(b1, b2);
429         assert(sink.data ==
430 `arr[0] = 1;
431 arr[2] = 3;
432 arr[4] = 5;
433 sarr[0] = "hello";
434 sarr[2] = "world";
435 carr[0] = 'a';
436 `);
437 
438         sink = typeof(sink).init;
439 
440         sink.formatDeltaInto!(Yes.asserts)(b1, b2);
441         assert(sink.data ==
442 `assert((arr[0] == 1), arr[0].to!string);
443 assert((arr[2] == 3), arr[2].to!string);
444 assert((arr[4] == 5), arr[4].to!string);
445 assert((sarr[0] == "hello"), sarr[0]);
446 assert((sarr[2] == "world"), sarr[2]);
447 assert((carr[0] == 'a'), carr[0].to!string);
448 `);
449     }
450 }