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