1 /** 
2  * Configuration management
3  *
4  * Configuration entries and
5  * a registry in which to
6  * manage a set of them
7  *
8  * Authors: Tristan Brice Velloza Kildaire (deavmi)
9  */
10 module niknaks.config;
11 
12 import std.string : format;
13 
14 version(unittest)
15 {
16     import std.stdio : writeln;
17 }
18 
19 /** 
20  * A union which expands to
21  * the byte-width of its
22  * biggest member, allowing
23  * us to have space enough
24  * for any one exclusive member
25  * at a time
26  *
27  * See_Also: ConfigEntry
28  */
29 private union ConfigValue
30 {
31     string text;
32     size_t integer;
33     bool flag;
34     string[] textArray;
35 }
36 
37 /** 
38  * The type of the entry
39  */
40 public enum ConfigType
41 {
42     /** 
43      * A string
44      */
45     TEXT,
46 
47     /** 
48      * An integer
49      */
50     NUMERIC,
51 
52     /** 
53      * A boolean
54      */
55     FLAG,
56 
57     /** 
58      * A string array
59      */
60     ARRAY
61 }
62 
63 /** 
64  * An exception thrown when you misuse
65  * a configuration entry
66  */
67 public final class ConfigException : Exception
68 {
69     private this(string msg)
70     {
71         super(msg);
72     }
73 }
74 
75 /** 
76  * A configuration entry which
77  * acts as a typed union and
78  * supports certain fixed types
79  */
80 public struct ConfigEntry
81 {
82     private ConfigValue value;
83     private ConfigType type;
84 
85     /** 
86      * A flag which is used to
87      * know if a value has been
88      * set at all. This helps
89      * with the fact that 
90      * an entry can be constructed
91      * without having a value set
92      */
93     private bool isSet = false;
94 
95     /** 
96      * Ensures a value is set
97      */
98     private void ensureSet()
99     {
100         if(!this.isSet)
101         {
102             throw new ConfigException("This config entry has not yet been set");
103         }
104     }
105 
106     /**
107      * Marks this entry as having
108      * a value set
109      */
110     private void set()
111     {
112         this.isSet = true;
113     }
114 
115     // TODO: Must have an unset flag
116     // @disable
117     // private this();
118     import std.traits : isIntegral;
119 
120     this(EntryType)(EntryType value)
121     if
122     (
123         __traits(isSame, EntryType, string[]) ||
124         __traits(isSame, EntryType, string) ||
125         isIntegral!(EntryType) ||
126         __traits(isSame, EntryType, bool)
127     )
128     {
129         ConfigValue _v;
130         ConfigType _t;
131         static if(__traits(isSame, EntryType, string[]))
132         {
133             _v.textArray = value;
134             _t = ConfigType.ARRAY;
135         }
136         else static if(__traits(isSame, EntryType, string))
137         {
138             _v.text = value;
139             _t = ConfigType.TEXT;
140         }
141         else static if(isIntegral!(EntryType))
142         {
143             _v.integer = cast(size_t)value;
144             _t = ConfigType.NUMERIC;
145         }
146         else static if(__traits(isSame, EntryType, bool))
147         {
148             _v.flag = value;
149             _t = ConfigType.FLAG;
150         }
151 
152         this(_v, _t);
153     }
154 
155     /** 
156      * Constructs a new `ConfigEntry`
157      * with the given value and type
158      *
159      * Params:
160      *   value = the value itself
161      *   type = the value's type
162      */
163     private this(ConfigValue value, ConfigType type)
164     {
165         this.value = value;
166         this.type = type;
167 
168         set();
169     }
170 
171     /** 
172      * Creates a new configuration entry
173      * containing text
174      *
175      * Params:
176      *   text = the text
177      * Returns: a `ConfigEntry`
178      */
179     public static ConfigEntry ofText(string text)
180     {
181         return ConfigEntry(text);
182     }
183 
184     /** 
185      * Creates a new configuration entry
186      * containing an integer
187      *
188      * Params:
189      *   i = the integer
190      * Returns: a `ConfigEntry`
191      */
192     public static ConfigEntry ofNumeric(size_t i)
193     {
194         return ConfigEntry(i);
195     }
196 
197     /** 
198      * Creates a new configuration entry
199      * containing a flag
200      *
201      * Params:
202      *   flag = the flag
203      * Returns: a `ConfigEntry`
204      */
205     public static ConfigEntry ofFlag(bool flag)
206     {
207         return ConfigEntry(flag);
208     }
209 
210     /** 
211      * Creates a new configuration entry
212      * containing a textual array
213      *
214      * Params:
215      *   array = the textual array
216      * Returns: a `ConfigEntry`
217      */
218     public static ConfigEntry ofArray(string[] array)
219     {
220         return ConfigEntry(array);
221     }
222 
223     /** 
224      * Returns the type of the
225      * entry's value
226      *
227      * Returns: a `ConfigType`
228      */
229     public ConfigType getType()
230     {
231         return this.type;
232     }
233 
234     /** 
235      * Ensures the requested type
236      * matches the current type
237      * set
238      *
239      * Params:
240      *   requested = the requested
241      * type
242      * Returns: `true` if the types
243      * are the same, `false` otherwise
244      */
245     private bool ensureTypeMatch0(ConfigType requested)
246     {
247         return getType() == requested;
248     }
249 
250     /** 
251      * A version of the type
252      * matcher but which throws
253      * an exception on type mismatch
254      *
255      * See_Also: `ensureTypeMatch0(ConfigType)`
256      */
257     private void ensureTypeMatch(ConfigType requested)
258     {
259         if(!ensureTypeMatch0(requested))
260         {
261             throw new ConfigException(format("The entry is not of type '%s'", requested));
262         }
263     }
264 
265     /** 
266      * Obtains the numeric value
267      * of this entry
268      *
269      * Returns: an integer
270      * Throws: ConfigException if
271      * the type of the value in this
272      * entry is not numeric
273      */
274     public size_t numeric()
275     {
276         ensureSet;
277         ensureTypeMatch(ConfigType.NUMERIC);
278         return this.value.integer;
279     }
280 
281     /** 
282      * Obtains the textual array
283      * value of this entry
284      *
285      * Returns: a `string[]`
286      * Throws: ConfigException if
287      * the type of the value in this
288      * entry is not a textual array
289      */
290     public string[] array()
291     {
292         ensureSet;
293         ensureTypeMatch(ConfigType. ARRAY);
294         return this.value.textArray;
295     }
296 
297     /** 
298      * See_Also: `array()`
299      */
300     public string[] opSlice()
301     {
302         return array();
303     }
304 
305     /** 
306      * Obtains the flag value
307      * of this entry
308      *
309      * Returns: a `string[]`
310      * Throws: ConfigException if
311      * the type of the value in this
312      * entry is not a flag
313      */
314     public bool flag()
315     {
316         ensureSet;
317         ensureTypeMatch(ConfigType.FLAG);
318         return this.value.flag;
319     }
320 
321     /** 
322      * See_Also: `flag()`
323      */
324     public bool isTrue()
325     {
326         return flag() == true;
327     }
328 
329     /** 
330      * See_Also: `flag()`
331      */
332     public bool isFalse()
333     {
334         return flag() == false;
335     }
336 
337     /** 
338      * Obtains the text value
339      * of this entry
340      *
341      * Returns: a string
342      * Throws: ConfigException if
343      * the type of the value in this
344      * entry is not a string
345      */
346     public string text()
347     {
348         ensureSet;
349         ensureTypeMatch(ConfigType.TEXT);
350         return this.value.text;
351     }
352 
353     /** 
354      * Obtains the value of this
355      * configuration entry dependant
356      * on the requested casting type
357      * and matching that to the supported
358      * types of the configuration entry
359      *
360      * Returns: a value of type `T`
361      */
362     public T opCast(T)()
363     {
364         static if(__traits(isSame, T, bool))
365         {
366             return flag();
367         }
368         else static if(__traits(isSame, T, string))
369         {
370             return text();
371         }
372         else static if(isIntegral!(T))
373         {
374             return cast(T)numeric();
375         }
376         else static if(__traits(isSame, T, string[]))
377         {
378             return array();
379         }
380         else
381         {
382             pragma(msg, "ConfigEntry opCast(): Cannot cast to a type '", T, "'");
383             static assert(false);
384         }
385     }
386 }
387 
388 /**
389  * Tests out using the configuration
390  * entry and its various operator
391  * overloads
392  */
393 unittest
394 {
395     ConfigEntry entry = ConfigEntry.ofArray(["hello", "world"]);
396     assert(entry[] == ["hello", "world"]);
397 
398     entry = ConfigEntry.ofNumeric(1);
399     assert(entry.numeric() == 1);
400 
401     entry = ConfigEntry.ofText("hello");
402     assert(cast(string)entry == "hello");
403 
404     entry = ConfigEntry.ofFlag(true);
405     assert(entry);
406 }
407 
408 /** 
409  * Tests out the erroneous usage of a
410  * configuration entry
411  */
412 unittest
413 {
414     ConfigEntry entry = ConfigEntry.ofText("hello");
415 
416     try
417     {
418         entry[];
419         assert(false);
420     }
421     catch(ConfigException e)
422     {
423         
424     }
425 }
426 
427 /** 
428  * Tests out the erroneous usage of a
429  * configuration entry
430  */
431 unittest
432 {
433     ConfigEntry entry;
434 
435     try
436     {
437         entry[];
438         assert(false);
439     }
440     catch(ConfigException e)
441     {
442         
443     }
444 }
445 
446 /** 
447  * An entry derived from
448  * the `Registry` containing
449  * the name and the configuration
450  * entry itself
451  */
452 public struct RegistryEntry
453 {
454     private string name;
455     private ConfigEntry val;
456 
457     /** 
458      * Constructs a new `RegistryEntry`
459      * with the given name and configuration
460      * entry
461      *
462      * Params:
463      *   name = the name
464      *   entry = the entry itself
465      */
466     this(string name, ConfigEntry entry)
467     {
468         this.name = name;
469         this.val = entry;
470     }
471 
472     /** 
473      * Constructs a new `RegistryEntry`
474      * with the given name and configuration
475      * entry
476      *
477      * Params:
478      *   name = the name
479      *   entry = the entry itself
480      */
481     this(T)(string name, T entry)
482     {
483         this.name = name;
484         this.val = ConfigEntry(entryVal);
485     }
486 
487     /** 
488      * Obtains the entry's name
489      *
490      * Returns: the name
491      */
492     public string getName()
493     {
494         return this.name;
495     }
496 
497     /** 
498      * Obtains the entry itself
499      *
500      * Returns: a `ConfigEntry`
501      */
502     public ConfigEntry getEntry()
503     {
504         return this.val;
505     }
506 
507     /** 
508      * Returns the configugration
509      * entry's type
510      *
511      * See_Also: `ConfigEntry.getType()`
512      * Returns: a `ConfigType`
513      */
514     public ConfigType getType()
515     {
516         return getEntry().getType();
517     }
518 }
519 
520 /** 
521  * An exception thrown when something
522  * goes wrong with your usage of the
523  * `Registry`
524  */
525 public final class RegistryException : Exception
526 {
527     private this(string msg)
528     {
529         super(msg);
530     }
531 }
532 
533 /** 
534  * A registry for managing
535  * multiple mappings of
536  * string-based names to
537  * configuration entries
538  */
539 public struct Registry
540 {
541     private ConfigEntry[string] entries;
542     private bool allowOverwriteEntry;
543 
544     /** 
545      * Creates a new `Registry`
546      * and sets the overwriting policy
547      *
548      * Params:
549      *   allowOverwritingOfEntries = `true`
550      * if you want to allow overwriting of
551      * previously added entries, otherwise
552      * `false`
553      */
554     this(bool allowOverwritingOfEntries)
555     {
556         setAllowOverwrite(allowOverwritingOfEntries);
557     }
558     
559     /** 
560      * Checks if an entry is present
561      *
562      * Params:
563      *   name = the name
564      * Returns: `true` if present,
565      * otherwise `false`
566      */
567     public bool hasEntry(string name)
568     {
569         return getEntry0(name) !is null;
570     }
571 
572     /** 
573      * Ontains a pointer to the configuration
574      * entry at the given key.
575      *
576      * Params:
577      *   name = the key
578      * Returns: a `ConfigEntry*` if found,
579      * otherwise `null`
580      */
581     private ConfigEntry* getEntry0(string name)
582     {
583         ConfigEntry* potEntry = name in this.entries;
584         return potEntry;
585     }
586 
587     /** 
588      * Obtains a pointer to the configuration
589      * entry at the given key. Allowing you
590      * to swap out its contents directly if
591      * you want to.
592      *
593      * Params:
594      *   name = the key
595      * Returns: a `ConfigEntry*` if found,
596      * otherwise `null`
597      */
598     public ConfigEntry* opBinaryRight(string op)(string name)
599     if(op == "in")
600     {
601         return getEntry0(name);
602     }
603 
604     /** 
605      * Obtain a configuration entry
606      * at the given key
607      *
608      * Params:
609      *   name = the key
610      *   entry = the found entry
611      * (if any)
612      * Returns: `true` if found,
613      * otherwise `false` 
614      */
615     public bool getEntry_nothrow(string name, ref ConfigEntry entry)
616     {
617         ConfigEntry* potEntry = getEntry0(name);
618         if(potEntry is null)
619         {
620             return false;
621         }
622 
623         entry = *potEntry;
624         return true;
625     }
626 
627     /** 
628      * Obtain a configuration entry
629      * at the given key
630      *
631      * Params:
632      *   name = the key
633      * Returns: a configuration entry
634      * Throws: RegistryException if
635      * there is no entry at that key
636      */
637     public ConfigEntry opIndex(string name)
638     {
639         ConfigEntry entry;
640         if(!getEntry_nothrow(name, entry))
641         {
642             throw new RegistryException(format("Cannot find an entry by the name of '%s'", name));
643         }
644 
645         return entry;
646     }
647 
648     /** 
649      * Set whether or not the overwriting
650      * of an entry should be allowed
651      *
652      * Params:
653      *   flag = `true` if to allow, `false`
654      * if to deny
655      */
656     public void setAllowOverwrite(bool flag)
657     {
658         this.allowOverwriteEntry = flag;
659     }
660 
661     /** 
662      * Adds a new configuration entry at the
663      * given key and allows you to choose
664      * certain behaviors based on the
665      * existence or non-existence of
666      * an entry at the same key.
667      *
668      * Params:
669      *   name = the name of the entry
670      *   entry = the entry itself
671      *   allowOverWriteNow = if `true`
672      * then if an entry exists already
673      * at that key it will be overwritten,
674      * otherwise an exception will be thrown
675      *   allowSetOnCreation = if there is
676      * no entry at the given key then,
677      * if `true`, an entry will be created,
678      * otherwise an exception will be thrown
679      */
680     private void newEntry(string name, ConfigEntry entry, bool allowOverWriteNow, bool allowSetOnCreation)
681     {
682         // Obtain the address of the value that occupies the value
683         // the key in the map
684         ConfigEntry* entryExist = getEntry0(name);
685 
686         // If something is present but overwiritng is disabled
687         if((entryExist !is null) && !allowOverWriteNow)
688         {
689             throw new RegistryException(format("An entry already exists at '%s' and overwriting is not allowed", name));
690         }
691         // If something is present and overwiring is enabled
692         else if(entryExist !is null)
693         {
694             // Now simply update the data in-place
695             *entryExist = entry;
696         }
697         // If nothing is present but setting-on-creation is enabled
698         else if(allowSetOnCreation)
699         {
700             // Then create the entry
701             this.entries[name] = entry;
702         }
703         // If nothing is present BUT setting-on-creation was NOT allowed
704         else
705         {
706             throw new RegistryException(format("Cannot set-on-creation for entry '%s' as it is not allowed", name));
707         }
708     }
709 
710     /** 
711      * Creates a new entry and adds it
712      *
713      * An exception is thrown if an entry
714      * at that key exists and the policy
715      * for overwriting is to deny
716      *
717      * Params:
718      *   name = the key
719      *   entry = the configuration entry
720      */
721     public void newEntry(string name, ConfigEntry entry)
722     {
723         newEntry(name, entry, this.allowOverwriteEntry, true);
724     }
725 
726     /** 
727      * See_Also: `newEntry(name, ConfigEntry)` 
728      */
729     public void newEntry(string name, int numeric)
730     {
731         newEntry(name, ConfigEntry.ofNumeric(numeric));
732     }
733 
734     /** 
735      * See_Also: `newEntry(name, ConfigEntry)` 
736      */
737     public void newEntry(string name, string text)
738     {
739         newEntry(name, ConfigEntry.ofText(text));
740     }
741 
742     /** 
743      * See_Also: `newEntry(name, ConfigEntry)` 
744      */
745     public void newEntry(string name, bool flag)
746     {
747         newEntry(name, ConfigEntry.ofFlag(flag));
748     }
749 
750     /** 
751      * See_Also: `newEntry(name, ConfigEntry)` 
752      */
753     public void newEntry(string name, string[] array)
754     {
755         newEntry(name, ConfigEntry.ofArray(array));
756     }
757 
758     /** 
759      * Sets the entry at the given name
760      * to the provided entry
761      *
762      * This will throw an exception if
763      * the entry trying to be set does
764      * not yet exist.
765      *
766      * Overwriting will only be allowed
767      * if the policy allows it.
768      *
769      * Params:
770      *   name = the key
771      *   entry = the configuration
772      * entry
773      */
774     public void setEntry(string name, ConfigEntry entry)
775     {
776         newEntry(name, entry, this.allowOverwriteEntry, false);
777     }
778 
779     /** 
780      * Assigns the provided configuration
781      * entry to the provided name
782      *
783      * Take note that using this method
784      * will create the entry if it does
785      * not yet exist.
786      *
787      * It will also ALWAYS allow overwriting.
788      *
789      * Params:
790      *   entry = the entry to add
791      *   name = the name at which to
792      * add the entry
793      */
794     public void opIndexAssign(ConfigEntry entry, string name)
795     {
796         newEntry(name, entry, true, true);
797     }
798 
799     /** 
800      * See_Also: `opIndexAssign(ConfigEntry, string)`
801      */
802     public void opIndexAssign(size_t numeric, string name)
803     {
804         opIndexAssign(ConfigEntry.ofNumeric(numeric), name);
805     }
806 
807     /** 
808      * See_Also: `opIndexAssign(ConfigEntry, string)`
809      */
810     public void opIndexAssign(string entry, string name)
811     {
812         opIndexAssign(ConfigEntry.ofText(entry), name);
813     }
814 
815     /** 
816      * See_Also: `opIndexAssign(ConfigEntry, string)`
817      */
818     public void opIndexAssign(bool flag, string name)
819     {
820         opIndexAssign(ConfigEntry.ofFlag(flag), name);
821     }
822 
823     /** 
824      * See_Also: `opIndexAssign(ConfigEntry, string)`
825      */
826     public void opIndexAssign(string[] array, string name)
827     {
828         opIndexAssign(ConfigEntry.ofArray(array), name);
829     }
830 
831     /** 
832      * Returns all the entries in the
833      * registry as a mapping of their
834      * name to their configuration entry
835      *
836      * See_Also: RegistryEntry
837      * Returns: an array of registry
838      * entries
839      */
840     public RegistryEntry[] opSlice()
841     {
842         RegistryEntry[] entrieS;
843         foreach(string entryName; this.entries.keys())
844         {
845             entrieS ~= RegistryEntry(entryName, this.entries[entryName]);
846         }
847 
848         return entrieS;
849     }
850 }
851 
852 /**
853  * Tests out the working with the
854  * registry in order to manage
855  * a set of named configuration
856  * entries
857  */
858 unittest
859 {
860     Registry reg = Registry(false);
861 
862     // Add an entry
863     reg.newEntry("name", "Tristan");
864 
865     // Check it exists
866     assert(reg.hasEntry("name"));
867 
868     // Adding it again should fail
869     try
870     {
871         reg.newEntry("name", "Tristan2");
872         assert(false);
873     }
874     catch(RegistryException e)
875     {
876 
877     }
878 
879     // Check that the entry still has the right value
880     assert(cast(string)reg["name"] == "Tristan");
881 
882     // // Add a new entry and test its prescence
883     reg["age"] = 24;
884     assert(cast(int)reg["age"]);
885 
886     // // Update it
887     reg["age"] = 25;
888     assert(cast(int)reg["age"] == 25);
889 
890     // Obtain a handle on the configuration
891     // entry, then update it and read it back
892     // to confirm
893     ConfigEntry* ageEntry = "age" in reg;
894     *ageEntry = ConfigEntry.ofNumeric(69_420);
895     assert(cast(int)reg["age"] == 69_420);
896 
897     // Should not be able to set entry it not yet existent
898     try
899     {
900         reg.setEntry("male", ConfigEntry.ofFlag(true));
901         assert(false);
902     }
903     catch(RegistryException e)
904     {
905 
906     }
907 
908     // All entries
909     RegistryEntry[] all = reg[];
910     assert(all.length == 2);
911     writeln(all);
912 }