1 module config.setting;
2 
3 import config.config : Config, InconsistentConfigState;
4 import config.util : unsafeCast;
5 
6 import std.traits : isIntegral, isFloatingPoint, isSomeString;
7 import std.typecons : Nullable;
8 
9 /// The type of a Setting
10 enum Type {
11     None, Int, Float, String, Bool, Group, Array, List
12 }
13 
14 /// Integer format used when writing a file
15 enum IntegerFormat {
16     Dec, Hex
17 }
18 
19 /// template that checks if T is a possible scalar candidate
20 enum isScalarCandidate(T) = is(T == bool) || isIntegral!T || isFloatingPoint!T || isSomeString!T;
21 
22 
23 /// returns true if a setting with Type type can hold a value of type T.
24 bool isScalarCompatible(T)(in Type type) pure {
25     switch(type) {
26         case Type.Bool:
27             return is(T == bool);
28         case Type.Int:
29             return isIntegral!T;
30         case Type.Float:
31             return isFloatingPoint!T;
32         case Type.String:
33             return isSomeString!T;
34         default:
35             return false;
36     }
37 }
38 
39 template scalarType(T)
40 if (isScalarCandidate!T)
41 {
42     static if (is(T == bool)) {
43         enum scalarType = Type.Bool;
44     }
45     else static if (isIntegral!T) {
46         enum scalarType = Type.Int;
47     }
48     else static if (isFloatingPoint!T) {
49         enum scalarType = Type.Float;
50     }
51     else static if (isSomeString!T) {
52         enum scalarType = Type.String;
53     }
54 }
55 
56 
57 @property bool isScalar(in Type type)
58 {
59     return cast(int)type > Type.None && cast(int)type < Type.Group;
60 }
61 @property bool isAggregate(in Type type)
62 {
63     return cast(int)type >= Type.Group;
64 }
65 @property bool isNumber(in Type type)
66 {
67     return type == Type.Int || type == Type.Float;
68 }
69 
70 template TypedSetting(Type type)
71 {
72     static if (type == Type.Bool || type == Type.Int || type == Type.Float || type == Type.String)
73     {
74         alias TypedSetting = ScalarSetting;
75     }
76     else static if (type == Type.Array)
77     {
78         alias TypedSetting = ArraySetting;
79     }
80     else static if (type == Type.List)
81     {
82         alias TypedSetting = ListSetting;
83     }
84     else static if (type == Type.Group)
85     {
86         alias TypedSetting = GroupSetting;
87     }
88     else static assert(false);
89 }
90 
91 /// Setting type. Has a name and value which can be of any of Type
92 abstract class Setting {
93 
94     @property string name() const { return _name; }
95     @property Type type() const { return _type; }
96 
97     @property inout(Config) config() inout { return _config; }
98     @property inout(AggregateSetting) parent() inout { return _parent; }
99 
100     inout(Setting) child(in size_t idx) inout { return null; }
101     inout(Setting) child(in string name) inout { return null; }
102     @property inout(Setting)[] children() inout { return []; }
103 
104     bool remove(in size_t idx) { return false; }
105     bool remove(in string path) { return false; }
106 
107     inout(Setting) lookUp(in string path) inout { return null; }
108     Nullable!T lookUpValue(T)(in string path) const {
109         if (auto s = lookUp(path)) {
110             if (auto ss = s.asScalar) {
111                 return Nullable!T(ss.value!T);
112             }
113         }
114         return Nullable!T.init;
115     }
116 
117     @property size_t index() const {
118         import std.algorithm : countUntil;
119         if (!parent) return -1;
120         return countUntil(parent._children, this);
121     }
122 
123     @property bool isGroup() const {
124         return _type == Type.Group;
125     }
126     @property bool isArray() const {
127         return _type == Type.Array;
128     }
129     @property bool isList() const {
130         return _type == Type.List;
131     }
132     @property bool isScalar() const {
133         return _type.isScalar;
134     }
135     @property bool isAggregate() const {
136         return _type.isAggregate;
137     }
138     @property bool isNumber() const {
139         return _type.isNumber;
140     }
141     @property bool isRoot() const {
142         return _parent is null;
143     }
144 
145     @property auto asScalar() inout {
146         return cast(ScalarSetting)this;
147     }
148     @property auto asArray() inout {
149         return cast(ArraySetting)this;
150     }
151     @property auto asList() inout {
152         return cast(ListSetting)this;
153     }
154     @property auto asGroup() inout {
155         return cast(GroupSetting)this;
156     }
157     @property auto as(Type t)() inout {
158         return cast(TypedSetting!t)this;
159     }
160 
161     @property IntegerFormat integerFormat() const {
162         if (!_integerFormat.isNull) return _integerFormat;
163         if (parent) return parent.integerFormat;
164         return config.defaultIntegerFormat;
165     }
166     @property void integerFormat(Nullable!IntegerFormat format) {
167         _integerFormat = format;
168     }
169 
170     package {
171         this(Config config, AggregateSetting parent, string name, Type type) {
172             _config = config;
173             _parent = parent;
174             _name = name;
175             _type = type;
176         }
177     }
178 
179     private {
180         string _name;
181         Type _type;
182         Nullable!IntegerFormat _integerFormat;
183 
184         Config _config;
185         AggregateSetting _parent;
186         string _file;
187         int _lineNumber;
188     }
189 }
190 
191 /// Setting that holds a scalar value that can be one of Bool, Float, Integer or String
192 class ScalarSetting : Setting {
193 
194     @property T value(T)() const
195     if (isScalarCandidate!T)
196     {
197         import std.exception : enforce;
198         import std.conv : to;
199 
200         static if (isIntegral!T)
201         {
202             auto val = _value.get!long;
203             static if (!is(T == long) && T.sizeof<=long.sizeof)
204             {
205                 enforce(val >= T.min && val <= T.max,
206                         "cannot cast "~val.to!string~" to "~T.stringof);
207             }
208             return cast(T)val;
209         }
210         else static if (isFloatingPoint!T)
211         {
212             auto val = _value.get!double;
213             static if (!is(T == double) && T.sizeof<double.sizeof) // should be float unless a half type pops up
214             {
215                 import std.math : abs;
216                 enforce(abs(val) >= T.min_normal && abs(val) <= T.max,
217                         "cannot cast "~val.to!string~" to "~T.stringof);
218             }
219             return cast(T)val;
220         }
221         else static if (isSomeString!T)
222         {
223             import std.conv : to;
224             return (_value.get!string).to!T;
225         }
226         else static if (is(T == bool)) {
227             return _value.get!T;
228         }
229         else static assert (false);
230     }
231 
232     @property void value(T)(T val)
233     if (isScalarCandidate!T)
234     {
235         _type = scalarType!T;
236         static if (isIntegral!T)
237         {
238             static if (!is(T == long) && T.sizeof>=long.sizeof)
239             {
240                 enforce(val >= long.min && val <= long.max,
241                         "cannot cast "~val.to!string~" to long");
242             }
243             _value = cast(long)val;
244         }
245         else static if (isFloatingPoint!T)
246         {
247             static if (T.sizeof>double.sizeof)
248             {
249                 import std.math : abs;
250                 enforce(abs(val) >= double.min_normal && abs(val) <= double.max,
251                         "cannot cast "~val.to!string~" to double");
252             }
253             _value = cast(double)val;
254         }
255         else static if (isSomeString!T)
256         {
257             import std.conv : to;
258             _value = val.to!string;
259         }
260         else static if (is(T == bool))
261         {
262             _value = val;
263         }
264         else static assert(false);
265     }
266 
267 
268     package {
269         this(Config config, AggregateSetting parent, string name, Type type) {
270             assert(type.isScalar);
271             super(config, parent, name, type);
272         }
273     }
274     private {
275         import std.variant : Algebraic;
276 
277         alias Value = Algebraic!(bool, long, double, string);
278         Value _value;
279     }
280 }
281 
282 
283 class AggregateSetting : Setting {
284 
285 
286     override inout(Setting) child(in size_t idx) inout
287     {
288         if (idx >= _children.length) return null;
289         return _children[idx];
290     }
291 
292     override inout(Setting) child(in string name) inout
293     {
294         import std.exception : enforce;
295         enforce(type == Type.Group, "only GroupSetting can have named children");
296         foreach (c; _children) {
297             if (c.name == name) return c;
298         }
299         return null;
300     }
301 
302     override @property inout(Setting)[] children() inout
303     {
304         return _children;
305     }
306 
307     override bool remove(in size_t idx) {
308         import std.algorithm : remove;
309 
310         size_t origLen = _children.length;
311         _children = remove(_children, idx);
312         return origLen != _children.length;
313     }
314 
315     override bool remove(in string path) {
316         import config.config : pathTok;
317         import config.util : findSplitAmong;
318         import std.range : empty;
319 
320         if (path.empty) return false;
321 
322         immutable split = path.findSplitAmong(pathTok);
323 
324         auto s = getChild(split[0]);
325         if (!s) return false;
326 
327         if (!split[2].empty) {
328             return s.remove(split[2]);
329         }
330         else {
331             import std.algorithm : remove;
332             auto origLen = _children.length;
333             _children = remove!(c => c is s)(_children);
334             return _children.length != origLen;
335         }
336     }
337 
338     override inout(Setting) lookUp(in string name) inout {
339         import config.config : pathTok;
340         import config.util : findSplitAmong;
341         import std.range : empty;
342 
343         if (name.empty) return this;
344 
345         immutable split = name.findSplitAmong(pathTok);
346         auto s = getChild(split[0]);
347         if (!split[2].empty && s) return s.lookUp(split[2]);
348         else return s;
349     }
350 
351     package {
352         this(Config config, AggregateSetting parent, string name, Type type) {
353             assert(type.isAggregate);
354             super(config, parent, name, type);
355         }
356 
357         void addChild(Setting child) {
358             assert(child.config is config && child.parent is this);
359             debug {
360                 if (type == Type.Group) {
361                     import std.algorithm : map, canFind;
362                     assert(!_children.map!(c => c.name).canFind(child.name));
363                 }
364             }
365             _children ~= child;
366         }
367 
368         void setChildren(Setting[] children) {
369             import std.algorithm : map, all;
370             import std.exception : enforce;
371             assert(children.map!(c => c.config).all!(cf => cf == config));
372             assert(children.map!(c => c.parent).all!(p => p == this));
373 
374             if (type == Type.Group) {
375                 bool[string] seen;
376                 foreach (c; children) {
377                     enforce(!(c.name in seen),
378                             new InconsistentConfigState("more than one child named \""~c.name~
379                             "\" in GroupSetting \""~name~"\""));
380                     seen[c.name] = true;
381                 }
382             }
383 
384             _children = children;
385         }
386     }
387 
388     private {
389 
390         Setting addChild(string name, Type type) {
391             auto s = config.makeSetting(this, name, type);
392             if (s) _children ~= s;
393             return s;
394         }
395 
396         inout(Setting) getChild(string name) inout {
397             import std.algorithm : find;
398             import std.exception : enforce;
399             import std.range : empty;
400             import std.conv : to;
401 
402             auto square = find(name, '[');
403             auto childName = name[0 .. $-square.length];
404             auto child = child(childName);
405 
406             if (square.empty) return child;
407 
408             assert(square[0] == '[');
409             enforce(square[$-1] == ']');
410 
411             immutable ind = to!size_t(name[1 .. $-1]);
412             return child.child(ind);
413         }
414 
415         Setting[] _children;
416     }
417 }
418 
419 
420 class ArraySetting : AggregateSetting {
421 
422     Setting add(Type type) {
423         import std.exception : enforce;
424         enforce(type.isScalar);
425         enforce(_children.length == 0 || _children[0].type == type);
426 
427         return addChild("", type);
428     }
429 
430     auto add(Type type)() {
431         import std.exception : enforce;
432         static assert(type.isScalar);
433         enforce(_children.length == 0 || _children[0].type == type);
434 
435         return unsafeCast!(TypedSetting!type)(addChild("", type));
436     }
437 
438     ScalarSetting addScalar(T)(in T value)
439     if (isScalarCandidate!T)
440     {
441         import std.exception : enforce;
442         immutable type = scalarType!T;
443         enforce(_children.length == 0 || _children[0].type == type);
444 
445         auto ss = unsafeCast!(ScalarSetting)(addChild("", type));
446         ss.value = value;
447         return ss;
448     }
449 
450     package {
451         this(Config config, AggregateSetting parent, string name) {
452             super(config, parent, name, Type.Array);
453         }
454     }
455 }
456 
457 
458 class ListSetting : AggregateSetting {
459 
460     Setting add(Type type) {
461         return addChild("", type);
462     }
463 
464     auto add(Type type)()
465     {
466         return unsafeCast!(TypedSetting!type)(addChild("", type));
467     }
468 
469     ScalarSetting addScalar(T)(in T value)
470     if (isScalarCandidate!T)
471     {
472         auto ss = unsafeCast!(ScalarSetting)(addChild("", scalarType!T));
473         ss.value = value;
474         return ss;
475     }
476 
477     package {
478         this(Config config, AggregateSetting parent, string name) {
479             super(config, parent, name, Type.List);
480         }
481     }
482 }
483 
484 
485 class GroupSetting : AggregateSetting {
486 
487     Setting add(in string name, in Type type) {
488         import std.exception : enforce;
489         enforce(validateName(name));
490         enforce(!child(name));
491         return addChild(name, type);
492     }
493 
494     auto add(Type type)(in string name)
495     {
496         import std.exception : enforce;
497         enforce(validateName(name));
498         enforce(!child(name));
499         return unsafeCast!(TypedSetting!type)(addChild(name, type));
500     }
501 
502     ScalarSetting addScalar(T)(in string name, in T value)
503     if (isScalarCandidate!T)
504     {
505         import std.exception : enforce;
506         enforce(validateName(name));
507         enforce(!child(name));
508 
509         auto ss = unsafeCast!(ScalarSetting)(addChild(name, scalarType!T));
510         ss.value = value;
511         return ss;
512     }
513 
514     package {
515         this(Config config, AggregateSetting parent, string name) {
516             super(config, parent, name, Type.Group);
517         }
518     }
519 }
520 
521 
522 private:
523 
524 bool validateName(in string name) pure {
525     import std.utf : byDchar;
526     import std.uni : isAlpha;
527     import std.ascii : isDigit;
528     import std.algorithm : canFind;
529 
530     if (name.length == 0) return false;
531 
532     auto dec = name.byDchar;
533 
534     if (!dec.front.isAlpha && dec.front != '*') return false;
535     dec.popFront();
536 
537     foreach(dchar c; dec) {
538         if (!c.isAlpha && !c.isDigit && !"*_-"d.canFind(c)) {
539             return false;
540         }
541     }
542     return true;
543 }