1 // yamkeys - a runtime configuration management utility.
2 // Copyright © 2013 Nathan M. Swan
3 // Available under the MIT Expat License, see LICENSE file.
4 // Written in the D Programming Language
5 
6 /++
7  + Simple configuration management based on YAML.
8  +
9  + Authors: Nathan M. Swan, nathanmswan@gmail.com
10  + License: MIT (Expat) License
11  + Copyright: Copyright © 2013 Nathan M. Swan
12  +/
13  
14 module yamkeys;
15 
16 private:
17 import std.algorithm;
18 import std.array;
19 import std.file;
20 import std.getopt;
21 import std.string;
22 import std.traits;
23 import std.path;
24 
25 import dyaml.all;
26 
27 public:
28 /// The global configuration variable.
29 __gshared Configuration config;
30 
31 shared static this() {
32     config = new Configuration();
33 }
34 
35 /// Creates code which declares a variable of type T which is loaded with
36 /// configurations by calling config.load,
37 /// as well as a function of the given name which, given config, returns the
38 /// variable.
39 string configure(T, string name = defaultName!T)() {
40     import std.array;
41     import std.string;
42 
43     string code = 
44         `private __gshared TYPE _vcyam_NAME; `~
45         `shared static this() { config.load(_vcyam_NAME, "NAME"); } `~
46         `public @property TYPE NAME(yamkeys.Configuration c) { `~
47             `return _vcyam_NAME; }`;
48 
49     code = replace(code, `TYPE`, T.stringof);
50     code = replace(code, `NAME`, name);
51     
52     return code;
53 }
54 
55 /// The default key name for a type: its lowercased local name.
56 template defaultName(T) {
57     enum defaultName = splitter(T.stringof.toLower(), '.').back;
58 }
59 
60 /// Holds the state for the configuration files.
61 final class Configuration {
62   public:
63     /// Attempts to place the YAML data at the key label into obj.
64     /// This is usually already done by the configure template.
65     void load(T)(ref T obj, string label = defaultName!T) {
66         static if (__traits(compiles, new T()) && is(T == class)) {
67             if (obj is null) {
68                 obj = new T();
69             }
70         }
71 
72         foreach(Node cfg; evaluate(label)) {
73             load!T(obj, cfg);
74         }
75     }
76 
77   private:
78     bool configFolderExists;
79     Node yConfig;
80     Node yLocal;
81 
82     Node emptyYamlMap;
83 
84     string scName = null; Node* scInL=null, scInC=null;
85     string dcName = null; Node* dcInL=null, dcInC=null;
86 
87 
88     this() {
89         import core.runtime;
90         import std.stdio;
91 
92         string[string] mp;
93         emptyYamlMap = Node(mp);
94 
95         bool configFolderExists = "config".exists && "config".isDir;
96         yConfig = getFromYamlFile("config", configFolderExists, "default");
97         yLocal = getFromYamlFile("local", configFolderExists);
98 
99         string[] args = Runtime.args().dup;
100         getopt(args, std.getopt.config.passThrough, "config", &scName);
101         if (scName is null) {
102             if (auto cp = "config" in yLocal) {
103                 scName = cp.as!string;
104             } 
105         }
106 
107         if (auto cp = "default" in yLocal) {
108             dcName = cp.as!string;
109         } else if (auto cp = "default" in yConfig) {
110             dcName = cp.as!string;
111         } else {
112             enum NoDefConf = "No default configuration is specified.";
113             version (Have_vibe_d) {
114                 import vibe.core.log;
115                 logWarn(NoDefConf);
116             } else {
117                 import std.stdio;
118                 stderr.writeln(NoDefConf);
119             }
120         }
121 
122         if (yLocal == emptyYamlMap && scName !is null) {
123             yLocal["config"] = scName;
124             Dumper(configFolderExists ? "config/local.yml" : "local.yml")
125                 .dump(yLocal);
126         }
127 
128         if (scName !is null) {
129             scInL = scName in yLocal;
130             scInC = scName in yConfig;
131         }
132         dcInL = dcName in yLocal;
133         dcInC = dcName in yConfig;
134     }
135 
136     Node getFromYamlFile(string name, bool configFolderExists,
137             string folderName=null)
138     {
139         auto namesToCheck = [name ~ ".yml", name ~ ".yaml"];
140         if (configFolderExists) {
141             alias ds = dirSeparator;
142             if (folderName is null) folderName = name;
143             namesToCheck ~= ["config" ~ ds ~ folderName ~ ".yml",
144                              "config" ~ ds ~ folderName ~ ".yaml"];
145         }
146 
147         foreach(fname; namesToCheck) {
148             if (fname.exists) {
149                 return Loader(fname).load();
150             }
151         }
152         return emptyYamlMap;
153     }
154 
155     Node[] evaluate(string key) {
156         Node[] configs;
157 
158         if (dcInC)
159             if (auto val = key in *dcInC)
160                 configs ~= *val;
161         if (dcInL)
162             if (auto val = key in *dcInL)
163                 configs ~= *val;
164         if (scInC)
165             if (auto val = key in *scInC)
166                 configs ~= *val;
167         if (scInL)
168             if (auto val = key in *scInL)
169                 configs ~= *val;
170         
171         return configs;
172     }
173 
174     void load(T)(ref T obj, Node node) {
175         static if (isBasicType!T && !is(T == enum) || isSomeString!T) {
176             obj = node.get!T;
177         }
178         else static if (isDynamicArray!T) {
179             obj = [];
180             obj.reserve(node.length);
181             foreach(ref Node e; node) {
182                 typeof(obj[0]) obje;
183                 load!(typeof(obje))(obje, e);
184                 obj ~= obje;
185             }
186         }
187         else static if (isAssociativeArray!T) {
188             T map;
189             obj = map;
190             foreach(ref Node k, ref Node v; node) {
191                 KeyType!T objk;
192                 load!(KeyType!T)(objk, k);
193 
194                 ValueType!T objv;
195                 load!(ValueType!T)(objv, v);
196 
197                 obj[objk] = objv;
198             }
199         }
200         else static if (isAggregateType!T) {
201             static if (__traits(compiles, new T()) && is(T == class)) {
202                 if (obj is null) {
203                     obj = new T();
204                 }
205             }
206 
207             alias FT = FieldTypeTuple!T;
208 
209             enum src = ({
210                 string res;
211                 foreach(mb; [__traits(allMembers, T)]) {
212                     res ~= `if (auto mbPtr = "`~mb~`" in node) {`;
213                     string tpof = `typeof(obj.`~mb~`)`;
214                     string loadStmt =
215                         tpof~` t = `~tpof~`.init; `~
216                         `load!(`~tpof~`)`~
217                             `(t, node["`~mb~`"]);`~
218                         `obj.`~mb~` = t;`;
219                     res ~= "
220 static if (__traits(compiles, obj."~mb~"=typeof(obj."~mb~").init)) { 
221 mixin(`"~loadStmt~"`); }
222 }";
223                 }
224                 return res;
225             })();
226 
227             mixin(src);
228         }
229     }
230 }
231 
232 class YamkeysException : Exception {
233     this(string msg, string file=__FILE__, int line=__LINE__, Throwable t=null) {
234         super(msg, file, line, t);
235     }
236 }