1 /++
2     This module contains functions that in some way or another manipulates
3     struct and class instances, as well as (associative) arrays.
4 
5     Example:
6     ---
7     struct Foo
8     {
9         string nickname;
10         string address;
11     }
12 
13     Foo foo;
14 
15     foo.setMemberByName("nickname", "foobar");
16     foo.setMemberByName("address", "subdomain.address.tld");
17 
18     assert(foo.nickname == "foobar");
19     assert(foo.address == "subdomain.address.tld");
20 
21     foo.replaceMembers("subdomain.address.tld", "foobar");
22     assert(foo.address == "foobar");
23 
24     foo.replaceMembers("foobar", string.init);
25     assert(foo.nickname.length == 0);
26     assert(foo.address.length == 0);
27     ---
28  +/
29 module lu.objmanip;
30 
31 private:
32 
33 import std.traits : isAggregateType, isAssociativeArray, isEqualityComparable, isMutable;
34 import std.typecons : Flag, No, Yes;
35 
36 public:
37 
38 import lu.uda : Separator;
39 
40 
41 // setMemberByName
42 /++
43     Given a struct/class object, sets one of its members by its string name to a
44     specified value. Overload that takes the value as a string and tries to
45     convert it into the target type.
46 
47     It does not currently recurse into other struct/class members.
48 
49     Example:
50     ---
51     struct Foo
52     {
53         string name;
54         int number;
55         bool alive;
56     }
57 
58     Foo foo;
59 
60     foo.setMemberByName("name", "James Bond");
61     foo.setMemberByName("number", "007");
62     foo.setMemberByName("alive", "false");
63 
64     assert(foo.name == "James Bond");
65     assert(foo.number == 7);
66     assert(!foo.alive);
67     ---
68 
69     Params:
70         thing = Reference object whose members to set.
71         memberToSet = String name of the thing's member to set.
72         valueToSet = String contents of the value to set the member to; string
73             even if the member is of a different type.
74 
75     Returns:
76         `true` if a member was found and set, `false` if nothing was done.
77 
78     Throws: [std.conv.ConvException|ConvException] if a string could not be
79         converted into an array, if a passed string could not be converted into
80         a bool, or if [std.conv.to] failed to convert a string into wanted type `T`.
81         [SetMemberException] if an unexpected exception was thrown.
82  +/
83 bool setMemberByName(Thing)
84     (ref Thing thing,
85     const string memberToSet,
86     const string valueToSet)
87 if (isAggregateType!Thing && isMutable!Thing)
88 in (memberToSet.length, "Tried to set member by name but no member string was given")
89 {
90     import lu.string : stripSuffix, stripped, unquoted;
91     import std.conv : ConvException, to;
92 
93     bool success;
94 
95     top:
96     switch (memberToSet)
97     {
98     static foreach (immutable i; 0..thing.tupleof.length)
99     {{
100         alias QualT = typeof(thing.tupleof[i]);
101 
102         static if (!isMutable!QualT)
103         {
104             // Can't set const or immutable, so just ignore and break
105             enum memberstring = __traits(identifier, thing.tupleof[i]);
106 
107             case memberstring:
108                 break top;
109         }
110         else
111         {
112             import lu.traits : isSerialisable;
113             import std.traits : Unqual;
114 
115             alias T = Unqual!QualT;
116 
117             static if (isSerialisable!(thing.tupleof[i]))
118             {
119                 enum memberstring = __traits(identifier, thing.tupleof[i]);
120 
121                 case memberstring:
122                 {
123                     import std.traits : isAggregateType, isArray,
124                         isAssociativeArray, isPointer, isSomeString;
125 
126                     static if (isAggregateType!T)
127                     {
128                         static if (__traits(compiles, { thing.tupleof[i] = string.init; }))
129                         {
130                             thing.tupleof[i] = valueToSet.stripped.unquoted;
131                             success = true;
132                         }
133 
134                         // Else do nothing
135                     }
136                     else static if (!isSomeString!T && isArray!T)
137                     {
138                         import lu.uda : Separator;
139                         import std.algorithm.iteration : splitter;
140                         import std.array : replace;
141                         import std.traits : getUDAs, hasUDA;
142 
143                         thing.tupleof[i].length = 0;
144 
145                         static if (hasUDA!(thing.tupleof[i], Separator))
146                         {
147                             alias separators = getUDAs!(thing.tupleof[i], Separator);
148                         }
149                         else static if ((__VERSION__ >= 2087L) && hasUDA!(thing.tupleof[i], string))
150                         {
151                             alias separators = getUDAs!(thing.tupleof[i], string);
152                         }
153                         else
154                         {
155                             import std.format : format;
156                             static assert(0, "`%s.%s` is missing a `Separator` annotation"
157                                 .format(Thing.stringof, memberstring));
158                         }
159 
160                         enum escapedPlaceholder = "\0\0";  // anything really
161                         enum ephemeralSeparator = "\1\1";  // ditto
162                         enum doubleEphemeral = ephemeralSeparator ~ ephemeralSeparator;
163                         enum doubleEscapePlaceholder = "\2\2";
164 
165                         string values = valueToSet.replace("\\\\", doubleEscapePlaceholder);
166 
167                         foreach (immutable thisSeparator; separators)
168                         {
169                             static if (is(Unqual!(typeof(thisSeparator)) == Separator))
170                             {
171                                 enum escapedSeparator = '\\' ~ thisSeparator.token;
172                                 enum separator = thisSeparator.token;
173                             }
174                             else
175                             {
176                                 enum escapedSeparator = '\\' ~ thisSeparator;
177                                 alias separator = thisSeparator;
178                             }
179 
180                             values = values
181                                 .replace(escapedSeparator, escapedPlaceholder)
182                                 .replace(separator, ephemeralSeparator)
183                                 .replace(escapedPlaceholder, separator);
184                         }
185 
186                         values = values
187                             .replace(doubleEphemeral, ephemeralSeparator)
188                             .replace(doubleEscapePlaceholder, "\\");
189 
190                         auto range = values.splitter(ephemeralSeparator);
191 
192                         foreach (immutable entry; range)
193                         {
194                             if (!entry.length) continue;
195 
196                             try
197                             {
198                                 import std.range : ElementEncodingType;
199 
200                                 thing.tupleof[i] ~= entry
201                                     .stripped
202                                     .unquoted
203                                     .to!(ElementEncodingType!T);
204 
205                                 success = true;
206                             }
207                             catch (ConvException e)
208                             {
209                                 import std.format : format;
210 
211                                 enum pattern = "Could not convert `%s.%s` array " ~
212                                     "entry \"%s\" into `%s` (%s)";
213                                 immutable message = pattern.format(
214                                     Thing.stringof.stripSuffix("Settings"),
215                                     memberToSet,
216                                     entry,
217                                     T.stringof,
218                                     e.msg);
219 
220                                 throw new ConvException(message);
221                             }
222                             catch (Exception e)
223                             {
224                                 import std.format : format;
225 
226                                 enum pattern = "A set-member action failed: %s";
227                                 immutable message = pattern.format(e.msg);
228 
229                                 throw new SetMemberException(message, Thing.stringof,
230                                     memberToSet, values);
231                             }
232                         }
233                     }
234                     else static if (is(T : string))
235                     {
236                         thing.tupleof[i] = valueToSet.stripped.unquoted;
237                         success = true;
238                     }
239                     else static if (isAssociativeArray!T)
240                     {
241                         static if (__traits(compiles, valueToSet.to!T))
242                         {
243                             try
244                             {
245                                 thing.tupleof[i] = valueToSet.stripped.unquoted.to!T;
246                                 success = true;
247                             }
248                             catch (ConvException e)
249                             {
250                                 import std.format : format;
251 
252                                 enum pattern = "Could not convert `%s.%s` text \"%s\" " ~
253                                     "to a `%s` associative array (%s)";
254                                 immutable message = pattern.format(
255                                     Thing.stringof.stripSuffix("Settings"),
256                                     memberToSet,
257                                     valueToSet.stripped.unquoted,
258                                     T.stringof,
259                                     e.msg);
260 
261                                 throw new ConvException(message);
262                             }
263                             catch (Exception e)
264                             {
265                                 import std.format : format;
266 
267                                 enum pattern = "A set-member action failed (AA): %s";
268                                 immutable message = pattern.format(e.msg);
269 
270                                 throw new SetMemberException(message, Thing.stringof,
271                                     memberToSet, valueToSet.stripped.unquoted);
272                             }
273                         }
274                         else
275                         {
276                             // Inconvertible AA, silently ignore
277                         }
278                     }
279                     else static if (isPointer!T)
280                     {
281                         // Ditto for pointers
282                     }
283                     else static if (is(T == bool))
284                     {
285                         import std.uni : toLower;
286 
287                         switch (valueToSet.stripped.unquoted.toLower)
288                         {
289                         case "true":
290                         case "yes":
291                         case "on":
292                         case "1":
293                             thing.tupleof[i] = true;
294                             break;
295 
296                         case "false":
297                         case "no":
298                         case "off":
299                         case "0":
300                             thing.tupleof[i] = false;
301                             break;
302 
303                         default:
304                             import std.format : format;
305 
306                             enum pattern = "Invalid value for setting `%s.%s`: " ~
307                                 `could not convert "%s" to a boolean value`;
308                             immutable message = pattern.format(
309                                 Thing.stringof.stripSuffix("Settings"),
310                                 memberToSet,
311                                 valueToSet);
312 
313                             throw new ConvException(message);
314                         }
315 
316                         success = true;
317                     }
318                     else
319                     {
320                         try
321                         {
322                             static if (is(T == enum))
323                             {
324                                 import lu.conv : Enum;
325 
326                                 immutable asString = valueToSet
327                                     .stripped
328                                     .unquoted;
329                                 thing.tupleof[i] = Enum!T.fromString(asString);
330                             }
331                             else
332                             {
333                                 /*writefln("%s.%s = %s.to!%s", Thing.stringof,
334                                     memberstring, valueToSet, T.stringof);*/
335                                 thing.tupleof[i] = valueToSet
336                                     .stripped
337                                     .unquoted
338                                     .to!T;
339                             }
340 
341                             success = true;
342                         }
343                         catch (ConvException e)
344                         {
345                             import std.format : format;
346 
347                             enum pattern = "Invalid value for setting `%s.%s`: " ~
348                                 "could not convert \"%s\" to `%s` (%s)";
349                             immutable message = pattern.format(
350                                 Thing.stringof.stripSuffix("Settings"),
351                                 memberToSet,
352                                 valueToSet,
353                                 T.stringof,
354                                 e.msg);
355 
356                             throw new ConvException(message);
357                         }
358                         catch (Exception e)
359                         {
360                             import std.format : format;
361 
362                             enum pattern = "A set-member action failed: %s";
363                             immutable message = pattern.format(e.msg);
364 
365                             throw new SetMemberException(message, Thing.stringof,
366                                 memberToSet, valueToSet);
367                         }
368                     }
369                     break top;
370                 }
371             }
372         }
373     }}
374 
375     default:
376         break;
377     }
378 
379     return success;
380 }
381 
382 ///
383 unittest
384 {
385     import lu.uda : Separator;
386     import std.conv : to;
387 
388     struct Foo
389     {
390         string bar;
391         int baz;
392         float* f;
393         string[string] aa;
394 
395         @Separator("|")
396         @Separator(" ")
397         {
398             string[] arr;
399             string[] matey;
400         }
401 
402         @Separator(";;")
403         {
404             string[] parrots;
405             string[] withSpaces;
406         }
407 
408         @Separator(`\o/`)
409         {
410             string[] blurgh;
411         }
412 
413         static if (__VERSION__ >= 2087L)
414         {
415             @(`\o/`)
416             {
417                 int[] blargh;
418             }
419         }
420     }
421 
422     Foo foo;
423     bool success;
424 
425     success = foo.setMemberByName("bar", "asdf fdsa adf");
426     assert(success);
427     assert((foo.bar == "asdf fdsa adf"), foo.bar);
428 
429     success = foo.setMemberByName("baz", "42");
430     assert(success);
431     assert((foo.baz == 42), foo.baz.to!string);
432 
433     success = foo.setMemberByName("aa", `["abc":"def", "ghi":"jkl"]`);
434     assert(success);
435     assert((foo.aa == [ "abc":"def", "ghi":"jkl" ]), foo.aa.to!string);
436 
437     success = foo.setMemberByName("arr", "herp|derp|dirp|darp");
438     assert(success);
439     assert((foo.arr == [ "herp", "derp", "dirp", "darp"]), foo.arr.to!string);
440 
441     success = foo.setMemberByName("arr", "herp derp dirp|darp");
442     assert(success);
443     assert((foo.arr == [ "herp", "derp", "dirp", "darp"]), foo.arr.to!string);
444 
445     success = foo.setMemberByName("matey", "this,should,not,be,separated");
446     assert(success);
447     assert((foo.matey == [ "this,should,not,be,separated" ]), foo.matey.to!string);
448 
449     success = foo.setMemberByName("parrots", "squaawk;;parrot sounds;;repeating");
450     assert(success);
451     assert((foo.parrots == [ "squaawk", "parrot sounds", "repeating"]),
452         foo.parrots.to!string);
453 
454     success = foo.setMemberByName("withSpaces", `         squoonk         ;;"  spaced  ";;" "`);
455     assert(success);
456     assert((foo.withSpaces == [ "squoonk", `  spaced  `, " "]),
457         foo.withSpaces.to!string);
458 
459     success = foo.setMemberByName("invalid", "oekwpo");
460     assert(!success);
461 
462     /*success = foo.setMemberByName("", "true");
463     assert(!success);*/
464 
465     success = foo.setMemberByName("matey", "hirr steff\\ stuff staff\\|stirf hooo");
466     assert(success);
467     assert((foo.matey == [ "hirr", "steff stuff", "staff|stirf", "hooo" ]), foo.matey.to!string);
468 
469     success = foo.setMemberByName("matey", "hirr steff\\\\ stuff staff\\\\|stirf hooo");
470     assert(success);
471     assert((foo.matey == [ "hirr", "steff\\", "stuff", "staff\\", "stirf", "hooo" ]), foo.matey.to!string);
472 
473     success = foo.setMemberByName("matey", "asdf\\ fdsa\\\\ hirr                                steff");
474     assert(success);
475     assert((foo.matey == [ "asdf fdsa\\", "hirr", "steff" ]), foo.matey.to!string);
476 
477     success = foo.setMemberByName("blurgh", "asdf\\\\o/fdsa\\\\\\o/hirr\\o/\\o/\\o/\\o/\\o/\\o/\\o/\\o/steff");
478     assert(success);
479     assert((foo.blurgh == [ "asdf\\o/fdsa\\", "hirr", "steff" ]), foo.blurgh.to!string);
480 
481     static if (__VERSION__ >= 2087L)
482     {
483         success = foo.setMemberByName("blargh", `1\o/2\o/3\o/4\o/5`);
484         assert(success);
485         assert((foo.blargh == [ 1, 2, 3, 4, 5 ]), foo.blargh.to!string);
486     }
487 
488     class C
489     {
490         string abc;
491         int def;
492     }
493 
494     C c = new C;
495 
496     success = c.setMemberByName("abc", "this is abc");
497     assert(success);
498     assert((c.abc == "this is abc"), c.abc);
499 
500     success = c.setMemberByName("def", "42");
501     assert(success);
502     assert((c.def == 42), c.def.to!string);
503 
504     import lu.conv : Enum;
505 
506     enum E { abc, def, ghi }
507 
508     struct S
509     {
510         E e = E.ghi;
511     }
512 
513     S s;
514 
515     assert(s.e == E.ghi);
516     success = s.setMemberByName("e", "def");
517     assert(success);
518     assert((s.e == E.def), Enum!E.toString(s.e));
519 
520     struct StructWithOpAssign
521     {
522         string thing = "init";
523 
524         void opAssign(const string thing)
525         {
526             this.thing = thing;
527         }
528     }
529 
530     StructWithOpAssign assignable;
531     assert((assignable.thing == "init"), assignable.thing);
532     assignable = "new thing";
533     assert((assignable.thing == "new thing"), assignable.thing);
534 
535     struct StructWithAssignableMember
536     {
537         StructWithOpAssign child;
538     }
539 
540     StructWithAssignableMember parent;
541     success = parent.setMemberByName("child", "flerp");
542     assert(success);
543     assert((parent.child.thing == "flerp"), parent.child.thing);
544 
545     class ClassWithOpAssign
546     {
547         string thing = "init";
548 
549         void opAssign(const string thing) //@safe pure nothrow @nogc
550         {
551             this.thing = thing;
552         }
553     }
554 
555     class ClassWithAssignableMember
556     {
557         ClassWithOpAssign child;
558 
559         this()
560         {
561             child = new ClassWithOpAssign;
562         }
563     }
564 
565     ClassWithAssignableMember parent2 = new ClassWithAssignableMember;
566     success = parent2.setMemberByName("child", "flerp");
567     assert(success);
568     assert((parent2.child.thing == "flerp"), parent2.child.thing);
569 }
570 
571 
572 // setMemberByName
573 /++
574     Given a struct/class object, sets one of its members by its string name to a
575     specified value. Overload that takes a value of the same type as the target
576     member, rather than a string to convert. Integer promotion applies.
577 
578     It does not currently recurse into other struct/class members.
579 
580     Example:
581     ---
582     struct Foo
583     {
584         int i;
585         double d;
586     }
587 
588     Foo foo;
589 
590     foo.setMemberByName("i", 42);
591     foo.setMemberByName("d", 3.14);
592 
593     assert(foo.i == 42);
594     assert(foo.d = 3.14);
595     ---
596 
597     Params:
598         thing = Reference object whose members to set.
599         memberToSet = String name of the thing's member to set.
600         valueToSet = Value, of the same type as the target member.
601 
602     Returns:
603         `true` if a member was found and set, `false` if not.
604 
605     Throws: [SetMemberException] if the passed `valueToSet` was not the same type
606         (or implicitly convertible to) the member to set.
607  +/
608 bool setMemberByName(Thing, Val)
609     (ref Thing thing,
610     const string memberToSet,
611     /*const*/ Val valueToSet)
612 if (isAggregateType!Thing && isMutable!Thing && !is(Val : string))
613 in (memberToSet.length, "Tried to set member by name but no member string was given")
614 {
615     bool success;
616 
617     top:
618     switch (memberToSet)
619     {
620     static foreach (immutable i; 0..thing.tupleof.length)
621     {{
622         alias QualT = typeof(thing.tupleof[i]);
623         enum memberstring = __traits(identifier, thing.tupleof[i]);
624 
625         static if (!isMutable!QualT)
626         {
627             // Can't set const or immutable, so just ignore and break
628             case memberstring:
629                 break top;
630         }
631         else
632         {
633             import lu.traits : isSerialisable;
634             import std.traits : Unqual;
635 
636             alias T = Unqual!QualT;
637 
638             static if (isSerialisable!(thing.tupleof[i]))
639             {
640                 case memberstring:
641                 {
642                     static if (is(Val : T))
643                     {
644                         thing.tupleof[i] = valueToSet;
645                         success = true;
646                         break top;
647                     }
648                     else
649                     {
650                         import std.conv : to;
651                         enum message = "A set-member action failed due to type mismatch";
652                         throw new SetMemberException(message, Thing.stringof,
653                             memberToSet, valueToSet.to!string);
654                     }
655                 }
656             }
657         }
658     }}
659 
660     default:
661         break;
662     }
663 
664     return success;
665 }
666 
667 ///
668 unittest
669 {
670     import std.conv : to;
671     import std.exception : assertThrown;
672 
673     struct Foo
674     {
675         string s;
676         int i;
677         bool b;
678         const double d;
679     }
680 
681     Foo foo;
682 
683     bool success;
684 
685     success = foo.setMemberByName("s", "harbl");
686     assert(success);
687     assert((foo.s == "harbl"), foo.s);
688 
689     success = foo.setMemberByName("i", 42);
690     assert(success);
691     assert((foo.i == 42), foo.i.to!string);
692 
693     success = foo.setMemberByName("b", true);
694     assert(success);
695     assert(foo.b);
696 
697     success = foo.setMemberByName("d", 3.14);
698     assert(!success);
699 
700     assertThrown!SetMemberException(foo.setMemberByName("b", 3.14));
701 }
702 
703 
704 @safe:
705 
706 
707 // SetMemberException
708 /++
709     Exception, to be thrown when [setMemberByName] fails for some given reason.
710 
711     It is a normal [object.Exception|Exception] but with attached strings of
712     the type name, name of member and the value that was attempted to set.
713  +/
714 final class SetMemberException : Exception
715 {
716     /// Name of type that was attempted to set the member of.
717     string typeName;
718 
719     /// Name of the member that was attempted to set.
720     string memberToSet;
721 
722     /// String representation of the value that was attempted to assign.
723     string valueToSet;
724 
725     /++
726         Create a new [SetMemberException], without attaching anything.
727      +/
728     this(const string message,
729         const string file = __FILE__,
730         const size_t line = __LINE__,
731         Throwable nextInChain = null) pure nothrow @nogc @safe
732     {
733         super(message, file, line, nextInChain);
734     }
735 
736     /++
737         Create a new [SetMemberException], attaching extra set-member information.
738      +/
739     this(const string message,
740         const string typeName,
741         const string memberToSet,
742         const string valueToSet,
743         const string file = __FILE__,
744         const size_t line = __LINE__,
745         Throwable nextInChain = null) pure nothrow @nogc @safe
746     {
747         super(message, file, line, nextInChain);
748 
749         this.typeName = typeName;
750         this.memberToSet = memberToSet;
751         this.valueToSet = valueToSet;
752     }
753 }
754 
755 
756 // replaceMembers
757 /++
758     Inspects a passed struct or class for members whose values match that of the
759     passed `token`. Matches members are set to a replacement value, which is
760     an optional parameter that defaults to the `.init` value of the token's type.
761 
762     Params:
763         recurse = Whether or not to recurse into aggregate members.
764         thing = Reference to a struct or class whose members to iterate over.
765         token = What value to look for in members, be it a string or an integer
766             or whatever; anything that can be compared to.
767         replacement = What to assign matched values. Defaults to the `.init`
768             of the matched type.
769  +/
770 void replaceMembers(Flag!"recurse" recurse = No.recurse, Thing, Token)
771     (ref Thing thing,
772     Token token,
773     Token replacement = Token.init) pure nothrow @nogc
774 if (isAggregateType!Thing && isMutable!Thing && isEqualityComparable!Token)
775 {
776     import std.range : ElementEncodingType, ElementType;
777     import std.traits : isAggregateType, isArray, isSomeString;
778 
779     foreach (immutable i, ref member; thing.tupleof)
780     {
781         alias T = typeof(member);
782 
783         static if (isAggregateType!T)
784         {
785             static if (recurse)
786             {
787                 // Recurse
788                 member.replaceMembers!recurse(token, replacement);
789             }
790         }
791         else static if (is(T : Token))
792         {
793             if (member == token)
794             {
795                 member = replacement;
796             }
797         }
798         else static if (isArray!T && (is(ElementEncodingType!T : Token) ||
799             is(ElementType!T : Token)))
800         {
801             if ((member.length == 1) && (member[0] == token))
802             {
803                 if (replacement == typeof(replacement).init)
804                 {
805                     member = typeof(member).init;
806                 }
807                 else
808                 {
809                     member[0] = replacement;
810                 }
811             }
812         }
813     }
814 }
815 
816 ///
817 unittest
818 {
819     struct Bar
820     {
821         string s = "content";
822     }
823 
824     struct Foo
825     {
826         Bar b;
827         string s = "more content";
828     }
829 
830     Foo foo1, foo2;
831     foo1.replaceMembers("-");
832     assert(foo1 == foo2);
833 
834     foo2.s = "-";
835     foo2.replaceMembers("-");
836     assert(!foo2.s.length);
837     foo2.b.s = "-";
838     foo2.replaceMembers!(Yes.recurse)("-", "herblp");
839     assert((foo2.b.s == "herblp"), foo2.b.s);
840 
841     Foo foo3;
842     foo3.s = "---";
843     foo3.b.s = "---";
844     foo3.replaceMembers!(No.recurse)("---");
845     assert(!foo3.s.length);
846     assert((foo3.b.s == "---"), foo3.b.s);
847     foo3.replaceMembers!(Yes.recurse)("---");
848     assert(!foo3.b.s.length);
849 
850     class Baz
851     {
852         string barS = "init";
853         string barT = "*";
854         Foo f;
855     }
856 
857     Baz b1 = new Baz;
858     Baz b2 = new Baz;
859 
860     b1.replaceMembers("-");
861     assert((b1.barS == b2.barS), b1.barS);
862     assert((b1.barT == b2.barT), b1.barT);
863 
864     b1.replaceMembers("*");
865     assert(b1.barS.length, b1.barS);
866     assert(!b1.barT.length, b1.barT);
867     assert(b1.f.s.length, b1.f.s);
868 
869     b1.replaceMembers!(Yes.recurse)("more content");
870     assert(!b1.f.s.length, b1.f.s);
871 
872     import std.conv : to;
873 
874     struct Qux
875     {
876         int i = 42;
877     }
878 
879     Qux q;
880 
881     q.replaceMembers("*");
882     assert(q.i == 42);
883 
884     q.replaceMembers(43);
885     assert(q.i == 42);
886 
887     q.replaceMembers(42, 99);
888     assert((q.i == 99), q.i.to!string);
889 
890     struct Flerp
891     {
892         string[] arr;
893     }
894 
895     Flerp flerp;
896     flerp.arr = [ "-" ];
897     assert(flerp.arr.length == 1);
898     flerp.replaceMembers("-");
899     assert(!flerp.arr.length);
900 }
901 
902 
903 // pruneAA
904 /++
905     Iterates an associative array and deletes invalid entries, either if the value
906     is in a default `.init` state or as per the optionally passed predicate.
907 
908     It is supposedly undefined behaviour to remove an associative array's fields
909     when foreaching through it. So far we have been doing a simple mark-sweep
910     garbage collection whenever we encounter this use-case in the code, so why
911     not just make a generic solution instead and deduplicate code?
912 
913     Example:
914     ---
915     auto aa =
916     [
917         "abc" : "def",
918         "ghi" : string.init;
919         "mno" : "123",
920         "pqr" : string.init,
921     ];
922 
923     pruneAA(aa);
924 
925     assert("ghi" !in aa);
926     assert("pqr" !in aa);
927 
928     pruneAA!((entry) => entry.length > 0)(aa);
929 
930     assert("abc" !in aa);
931     assert("mno" !in aa);
932     ---
933 
934     Params:
935         pred = Optional predicate if special logic is needed to determine whether
936             an entry is to be removed or not.
937         aa = The associative array to modify.
938  +/
939 void pruneAA(alias pred = null, AA)(ref AA aa)
940 if (isAssociativeArray!AA && isMutable!AA)
941 {
942     if (!aa.length) return;
943 
944     string[] garbage;
945 
946     // Mark
947     foreach (/*immutable*/ key, value; aa)
948     {
949         static if (!is(typeof(pred) == typeof(null)))
950         {
951             import std.functional : binaryFun, unaryFun;
952 
953             alias unaryPred = unaryFun!pred;
954             alias binaryPred = binaryFun!pred;
955 
956             static if (__traits(compiles, unaryPred(value)))
957             {
958                 if (unaryPred(value)) garbage ~= key;
959             }
960             else static if (__traits(compiles, binaryPred(key, value)))
961             {
962                 if (unaryPred(key, value)) garbage ~= key;
963             }
964             else
965             {
966                 static assert(0, "Unknown predicate type passed to `pruneAA`");
967             }
968         }
969         else
970         {
971             if (value == typeof(value).init)
972             {
973                 garbage ~= key;
974             }
975         }
976     }
977 
978     // Sweep
979     foreach (immutable key; garbage)
980     {
981         aa.remove(key);
982     }
983 }
984 
985 ///
986 unittest
987 {
988     import std.conv : text;
989 
990     {
991         auto aa =
992         [
993             "abc" : "def",
994             "ghi" : "jkl",
995             "mno" : "123",
996             "pqr" : string.init,
997         ];
998 
999         pruneAA!((a) => a == "def")(aa);
1000         assert("abc" !in aa);
1001 
1002         pruneAA!((a,b) => a == "pqr")(aa);
1003         assert("pqr" !in aa);
1004 
1005         pruneAA!`a == "123"`(aa);
1006         assert("mno" !in aa);
1007     }
1008     {
1009         struct Record
1010         {
1011             string name;
1012             int id;
1013         }
1014 
1015         auto aa =
1016         [
1017             "rhubarb" : Record("rhubarb", 100),
1018             "raspberry" : Record("raspberry", 80),
1019             "blueberry" : Record("blueberry", 0),
1020             "apples" : Record("green apples", 60),
1021             "yakisoba"  : Record("yakisoba", 78),
1022             "cabbage" : Record.init,
1023         ];
1024 
1025         pruneAA(aa);
1026         assert("cabbage" !in aa);
1027 
1028         pruneAA!((entry) => entry.id < 80)(aa);
1029         assert("blueberry" !in aa);
1030         assert("apples" !in aa);
1031         assert("yakisoba" !in aa);
1032         assert((aa.length == 2), aa.length.text);
1033     }
1034     {
1035         import std.algorithm.searching : canFind;
1036 
1037         string[][string] aa =
1038         [
1039             "abc" : [ "a", "b", "c" ],
1040             "def" : [ "d", "e", "f" ],
1041             "ghi" : [ "g", "h", "i" ],
1042             "jkl" : [ "j", "k", "l" ],
1043         ];
1044 
1045         pruneAA(aa);
1046         assert((aa.length == 4), aa.length.text);
1047 
1048         pruneAA!((entry) => entry.canFind("a"))(aa);
1049         assert("abc" !in aa);
1050     }
1051 }