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