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 }