1 module config.config;
2 
3 import std.typecons : BitFlags, Nullable;
4 import std.range : isOutputRange;
5 
6 import config.setting;
7 
8 
9 /// Options used by Config when writing a file
10 enum Option {
11     AutoConvert                 = 0x01,
12     SemiColonSeparators         = 0x02,
13     ColonAssignmentForGroups    = 0x04,
14     ColonAssignmentForNonGroups = 0x08,
15     OpenBraceOnSeparateLine     = 0x10,
16 }
17 
18 /// Generic libconfig exception
19 class ConfigException : Exception
20 {
21     this(in string msg)
22     {
23         super(msg);
24     }
25 }
26 
27 /// Exception thrown when invalid input is met during parsing
28 class InvalidConfigInput : ConfigException
29 {
30     this(in string input, in string errorMsg)
31     {
32         this.input = input;
33         this.errorMsg = errorMsg;
34         super("Invalid config input. Error:\n"~errorMsg~"\nInput:\n"~prependLineNumbers(input));
35     }
36 
37     /// actual parsing input
38     string input;
39     /// error message, which differ from msg. msg is composed of input and errorMsg
40     string errorMsg;
41 }
42 
43 /// Exception thrown when attempting to build inconsistent configuration.
44 /// e.g: when building ArraySetting with different types of children
45 class InconsistentConfigState : ConfigException
46 {
47     this(in string msg)
48     {
49         super(msg);
50     }
51 }
52 
53 /// Main Config class
54 class Config
55 {
56     import std.stdio : File;
57     import std.ascii : newline;
58 
59     this() {
60         _root = new GroupSetting(this, null, "");
61 
62         _options = BitFlags!Option(
63             Option.SemiColonSeparators |
64             Option.ColonAssignmentForGroups |
65             Option.OpenBraceOnSeparateLine
66         );
67 
68         _tabWidth = 2;
69         _floatPrecision = 2;
70     }
71 
72     @property inout(GroupSetting) root() inout { return _root; }
73 
74     inout(Setting) lookUp(in string name) inout
75     {
76         return root.lookUp(name);
77     }
78 
79     Nullable!T lookUpValue(T)(in string name) const
80     {
81         return root.lookUpValue!T(name);
82     }
83 
84     bool remove(in string path)
85     {
86         return root.remove(path);
87     }
88 
89     @property BitFlags!Option options() const { return _options; }
90     @property void options(BitFlags!Option options) { _options = options; }
91 
92     @property IntegerFormat defaultIntegerFormat() const { return _defaultIntegerFormat; }
93     @property void defaultIntegerFormat(IntegerFormat format) { _defaultIntegerFormat = format; }
94 
95     @property ushort tabWidth() const { return _tabWidth; }
96     @property void tabWidth(ushort val) {
97         import std.algorithm : max;
98         _tabWidth = max(ushort(15), val);
99     }
100 
101     @property ushort floatPrecision() const { return _floatPrecision; }
102     @property void floatPrecision(ushort val) { _floatPrecision = val; }
103 
104 
105     /// Read a config from an opened file.
106     /// The file must be open in text mode (e.g. File(fname, "r"))
107     static Config read(File configFile, in string[] includeDirs=[])
108     {
109         import std.array : join;
110         import std.stdio : KeepTerminator;
111         import std.exception : enforce;
112         import config.util : stripComments, handleIncludeDirs;
113 
114         enforce(configFile.isOpen);
115 
116         immutable config = configFile
117                 .byLineCopy(KeepTerminator.no, "\n")
118                 .stripComments()
119                 .handleIncludeDirs(includeDirs)
120                 .join(newline);
121         return Parser.readConfig(config);
122     }
123 
124     /// Read from configuration string
125     static Config readString(in string configStr, string[] includeDirs=[]) {
126         import std.string : lineSplitter;
127         import std.array : join;
128         import config.util : stripComments, handleIncludeDirs;
129 
130         immutable config = configStr
131                 .lineSplitter()
132                 .stripComments()
133                 .handleIncludeDirs(includeDirs)
134                 .join(newline);
135 
136         return Parser.readConfig(config);
137     }
138 
139     /// read from configuration residing in configFile
140     static Config readFile(in string configFile, string[] includeDirs=[]) {
141         return read(File(configFile, "r"), includeDirs);
142     }
143 
144     /// writes the configuration to a string
145     override string toString()
146     {
147         import std.array : appender;
148         auto app = appender!string();
149         Writer.writeSetting(app, root, 0);
150         return app.data;
151     }
152 
153     /// writes the configuration to the given output range
154     void writeTo(O)(O writer)
155     if (isOutputRange!(O, char))
156     {
157         Writer.writeSetting(writer, root, 0);
158     }
159 
160     package {
161         Setting makeSetting(AggregateSetting parent, string name, Type type) {
162             switch(type) {
163                 case Type.Bool:
164                 case Type.Int:
165                 case Type.Float:
166                 case Type.String:
167                     return new ScalarSetting(this, parent, name, type);
168                 case Type.Array:
169                     return new ArraySetting(this, parent, name);
170                 case Type.List:
171                     return new ListSetting(this, parent, name);
172                 case Type.Group:
173                     return new GroupSetting(this, parent, name);
174                 default:
175                     assert(false, "unsupported Setting type: "~type.stringof);
176             }
177         }
178     }
179 
180     private {
181         GroupSetting _root;
182         BitFlags!Option _options;
183         IntegerFormat _defaultIntegerFormat;
184         ushort _tabWidth;
185         ushort _floatPrecision;
186 
187 
188         struct Parser
189         {
190             import cg = config.grammar;
191 
192             static Config readConfig(in string config)
193             {
194                 import std.exception : enforce;
195 
196                 auto conf = new Config;
197                 auto mainTree = cg.Config(config);
198                 enforce(mainTree.successful, new InvalidConfigInput(config, mainTree.failMsg));
199 
200                 assert(mainTree.children.length == 1);
201                 assert(mainTree.children[0].name == "Config.Document");
202                 auto docTree = mainTree.children[0];
203 
204                 foreach (pt; docTree.children) {
205                     assert(pt.name == "Config.Setting");
206                     conf.root.addChild(parseSetting(conf, conf.root, pt));
207                 }
208 
209                 return conf;
210             }
211 
212             static Setting parseSetting(Config conf, AggregateSetting par, cg.ParseTree pt)
213             {
214                 assert(pt.name == "Config.Setting");
215 
216                 // assertions enforced by grammar
217                 assert(pt.children.length == 2);
218                 assert(pt.children[0].name == "Config.Name");
219                 assert(pt.children[1].name == "Config.Value");
220 
221                 string name = pt.children[0].matches[0];
222                 return parseValue(conf, par, name, pt.children[1]);
223             }
224 
225 
226             static Setting parseValue(Config conf, AggregateSetting par, string name, cg.ParseTree valTree)
227             {
228                 assert(valTree.name == "Config.Value");
229 
230                 valTree = valTree.children[0];
231                 switch (valTree.name) {
232                     case "Config.Scalar":
233                         return parseScalar(conf, par, name, valTree);
234                     case "Config.Array": {
235                         auto s = new ArraySetting(conf, par, name);
236                         auto ss = new Setting[valTree.children.length];
237                         foreach (i, pt; valTree.children) {
238                             ss[i] = parseScalar(conf, s, "", pt);
239                         }
240                         s.setChildren(ss);
241                         return s;
242                     }
243                     case "Config.List": {
244                         auto s = new ListSetting(conf, par, name);
245                         auto ss = new Setting[valTree.children.length];
246                         foreach (i, pt; valTree.children) {
247                             ss[i] = parseValue(conf, s, "", pt);
248                         }
249                         s.setChildren(ss);
250                         return s;
251                     }
252                     case "Config.Group": {
253                         auto s = new GroupSetting(conf, par, name);
254                         auto ss = new Setting[valTree.children.length];
255                         foreach (i, pt; valTree.children) {
256                             ss[i] = parseSetting(conf, s, pt);
257                         }
258                         s.setChildren(ss);
259                         return s;
260                     }
261                     default: assert(false);
262                 }
263             }
264 
265             static Setting parseScalar(Config conf, AggregateSetting par, string name, cg.ParseTree scalTree)
266             {
267                 assert(scalTree.name == "Config.Scalar");
268 
269                 switch (scalTree.children[0].name) {
270                     case "Config.Bool": {
271                         import std.uni : toLower;
272                         ScalarSetting s = new ScalarSetting(conf, par, name, Type.Bool);
273                         if (scalTree.matches[0][0].toLower == 't') {
274                             s.value = true;
275                         }
276                         else {
277                             s.value = false;
278                         }
279                         return s;
280                     }
281                     case "Config.Integer": {
282                         import std.conv : to;
283                         ScalarSetting s = new ScalarSetting(conf, par, name, Type.Int);
284                         if (scalTree.children[0].children[0].name == "Config.Dec") {
285                             s.value = scalTree.matches[0].to!long(10);
286                         }
287                         else {
288                             assert(scalTree.children[0].children[0].name == "Config.Hex");
289                             s.value = scalTree.matches[0].to!long(16);
290                         }
291                         return s;
292                     }
293                     case "Config.Float": {
294                         import std.conv : to;
295                         ScalarSetting s = new ScalarSetting(conf, par, name, Type.Float);
296                         s.value = scalTree.matches[0].to!double();
297                         return s;
298                     }
299                     case "Config.String": {
300                         ScalarSetting s = new ScalarSetting(conf, par, name, Type.String);
301                         s.value = scalTree.matches[0];
302                         return s;
303                     }
304                     default: assert(false);
305                 }
306             }
307 
308             static void validateArrayChildren(Setting[] children) {
309                 import std.algorithm : each;
310                 import std.exception : enforce;
311                 enum seenBool   = 1;
312                 enum seenInt    = 2;
313                 enum seenFloat  = 4;
314                 enum seenString = 8;
315                 static int seen (in Type t) {
316                     switch(t) {
317                         case Type.Bool: return seenBool;
318                         case Type.Int: return seenInt;
319                         case Type.Float: return seenFloat;
320                         case Type.String: return seenString;
321                         default: assert(false);
322                     }
323                 }
324 
325                 int seenFl;
326                 children.each!(c => seenFl |= seen(c.type));
327 
328                 enforce(
329                     seenFl == seenBool || seenFl == seenString || seenFl & (seenInt | seenFloat),
330                     new InconsistentConfigState("Array with different types")
331                 );
332                 if ((seenFl & seenInt)==seenInt && (seenFl & seenFloat)==seenFloat) {
333                     // mix of float and ints, convert all ints to float
334                     foreach(ref c; children) {
335                         if (c.type == Type.Int) {
336                             auto sc = cast(ScalarSetting)c;
337                             immutable val = sc.value!long;
338                             sc.value = cast(double)val;
339                         }
340                     }
341                 }
342             }
343         }
344 
345         struct Writer
346         {
347             import std.range : isOutputRange, repeat, take;
348             import std.range.primitives : put;
349             import std.format : format;
350 
351             static void writeIndent(O)(O output, in int depth, in int width)
352             in {
353                 assert(depth > 1);
354             }
355             body {
356                 put(output, repeat(' ').take((depth-1)*width));
357             }
358 
359             static void writeValue(O)(O output, in Setting setting, in int depth)
360             {
361                 if (setting.isScalar)
362                 {
363                     writeScalarValue(output, setting.asScalar);
364                 }
365                 else if (setting.isArray)
366                 {
367                     writeArrayValue(output, setting.asArray, depth);
368                 }
369                 else if (setting.isList)
370                 {
371                     writeListValue(output, setting.asList, depth);
372                 }
373                 else if (setting.isGroup)
374                 {
375                     writeGroupValue(output, setting.asGroup, depth);
376                 }
377                 else assert(false);
378             }
379 
380             static void writeScalarValue(O)(O output, in ScalarSetting setting)
381             {
382                 switch(setting.type)
383                 {
384                     case Type.Bool: {
385                         put(output, setting.value!bool ? "true" : "false");
386                         break;
387                     }
388                     case Type.Int: {
389                         immutable val = setting.value!long;
390                         string suffix = (val > int.max || val < int.min) ? "L" : "";
391                         string fmt = (setting.integerFormat == IntegerFormat.Hex) ? "0x%X%s" : "%d%s";
392                         put(output, format(fmt, val, suffix));
393                         break;
394                     }
395                     case Type.Float: {
396                         import std.uni : toLower;
397                         import std.algorithm : canFind;
398                         string fval = format("%.*f", setting.config.floatPrecision, setting.value!double);
399                         if (!fval.canFind('.') && !fval.toLower.canFind('e')) fval ~= ".0";
400                         put(output, fval);
401                         break;
402                     }
403                     case Type.String: {
404                         import std.uni : isControl;
405                         put(output, '"');
406                         foreach(c; setting.value!string)
407                         {
408                             switch(c)
409                             {
410                                 case '"':
411                                     put(output, `\"`); break;
412                                 case '\\':
413                                     put(output, `\\`); break;
414                                 case '\n':
415                                     put(output, "\\n"); break;
416                                 case '\r':
417                                     put(output, "\\r"); break;
418                                 case '\f':
419                                     put(output, "\\f"); break;
420                                 case '\t':
421                                     put(output, "\\t"); break;
422                                 default: {
423                                     if (isControl(c))
424                                     {
425                                         put(output, format("\\x%02X", c));
426                                     }
427                                     else
428                                     {
429                                         put(output, c);
430                                     }
431                                     break;
432                                 }
433                             }
434                         }
435                         put(output, '"');
436                         break;
437                     }
438                     default: assert(false);
439                 }
440             }
441 
442             static void writeArrayValue(O)(O output, in ArraySetting setting, int depth)
443             {
444                 put(output, "[ ");
445                 writeListContent(output, setting, depth);
446                 put(output, ']');
447             }
448 
449             static void writeListValue(O)(O output, in ListSetting setting, int depth)
450             {
451                 put(output, "( ");
452                 writeListContent(output, setting, depth);
453                 put(output, ')');
454             }
455 
456             static void writeListContent(O)(O output, in AggregateSetting setting, in int depth)
457             {
458                 auto children = setting.children;
459                 foreach (i, s; children)
460                 {
461                     writeValue(output, s, depth+1);
462                     if (i < children.length-1)  put(output, ',');
463                     put(output, ' ');
464                 }
465             }
466 
467             static void writeGroupValue(O)(O output, in GroupSetting setting, in int depth)
468             {
469                 if (depth > 0)
470                 {
471                     if (setting.config.options & Option.OpenBraceOnSeparateLine)
472                     {
473                         put(output, newline);
474                         if (depth > 1) writeIndent(output, depth, setting.config.tabWidth);
475                     }
476                     put(output, "{"~newline);
477                 }
478 
479                 foreach (s; setting.children)
480                 {
481                     writeSetting(output, s, depth+1);
482                 }
483 
484                 if (depth > 1) writeIndent(output, depth, setting.config.tabWidth);
485                 if (depth > 0) put(output, '}');
486             }
487 
488             static void writeSetting(O)(O output, in Setting setting, in int depth)
489             {
490                 auto config = setting.config;
491                 auto groupAssignChar = (config.options & Option.ColonAssignmentForGroups) ? ':' : '=';
492                 auto nonGroupAssignChar = (config.options & Option.ColonAssignmentForNonGroups) ? ':' : '=';
493 
494                 if (depth > 1) writeIndent(output, depth, config.tabWidth);
495 
496                 if (setting.name.length)
497                 {
498                     put(output, format("%s %s ",
499                         setting.name, setting.isGroup ? groupAssignChar : nonGroupAssignChar
500                     ));
501                 }
502 
503                 writeValue(output, setting, depth);
504 
505                 if (depth > 0)
506                 {
507                     if (config.options & Option.SemiColonSeparators)
508                         put(output, ';');
509                     put(output, newline);
510                 }
511             }
512         }
513     }
514 }
515 
516 
517 
518 package enum pathTok = ".:/";
519 
520 
521 
522 private string prependLineNumbers(string text) {
523     import std.string : splitLines, KeepTerminator;
524     import std.format : format;
525     import std.math : log10;
526 
527     auto lines = text.splitLines(KeepTerminator.yes);
528     immutable width = 1 + cast(int)log10(lines.length);
529     string res;
530     foreach(i, l; lines) {
531         res ~= format("%*s. %s", width, i+1, l);
532     }
533     return res;
534 }