1 // 
2 // 
3 // 
4 
5 module dini.iniconfig;
6 
7 import std.algorithm  : splitter, joiner;
8 import std.range      : enumerate;
9 import std..string     : indexOf, strip;
10 import std.functional : forward;
11 import std.stdio      : File;
12 import std.conv       : to;
13 
14 import dini.inivalue;
15 
16 struct IniConfigs
17 {
18 public:
19     /// Constructors
20     this(string content) pure
21     {
22         this.add(content);
23     }
24     this(File file)
25     {
26         this.add(file);
27     }
28 
29     /// assignment
30     void opAssign(string content) pure
31     {
32         this.add(content);
33     }
34     void opAssign(File file)
35     {
36         this.add(file);
37     }
38 
39     /// add file
40     void add(File file)
41     {
42         this.add(file.byLine().joiner("\n").to!string);
43     }
44     /// add content
45     void add(string content) pure
46     {
47         size_t npos = size_t(-1);
48 
49         foreach(lineNum, ref line; content.splitter("\n").enumerate(1)) {
50             size_t pos = line.indexOf(';');
51             
52             // remove comment & trim
53             line = ( npos > pos ) ? line = line[0..pos].strip() : line.strip();
54 
55             if( null == line ) {
56                 continue;
57             }
58 
59             // parse key and value
60             pos = line.indexOf('=');
61             if( npos > pos ) {
62                 string key   = line[  0 .. pos  ].strip();
63                 string value = line[ pos+1 .. $ ].strip();
64                 if(value.length > 1 && '"' == value[0] && '"' == value[$-1]) {
65                     value = value[1..$-1];
66                 }
67 
68                 _map[key] = value;
69             } else {
70                 throw new IniConfigsException(lineNum);
71             }
72         }
73     }
74 
75     /// Get ini entry
76     T get(T)(const string key, auto ref T dfltValue, bool noEmpty = true) const
77     {
78         const string* cfg = (key in _map);
79 
80         // if key not exists
81         if(cfg is null) {
82             return dfltValue;
83         }
84 
85         // check if config value string is empty
86         if(noEmpty && null == *cfg) {
87             return dfltValue;
88         }
89 
90         //return cast(T) IniValue(*cfg);
91         return IniValue(*cfg).to!T;
92     }
93 
94     /// Get ini entry
95     T get(T)(auto ref string key) const
96     {
97         const string* cfg = (key in _map);
98 
99         // if key not exists or config value string is empty return default
100         if(!cfg || null == *cfg) {
101             return T.init;
102         }
103         
104         //return cast(T) IniValue(*cfg);
105         return IniValue(*cfg).to!T;
106     }
107 
108     /// Check if ini entry exists
109     bool has()(auto ref string key) const @safe
110     {
111         return (key in _map) !is null;
112     }
113 
114     /// Check if ini configs has not entries
115     bool empty() const @nogc pure @safe
116     {
117         return !_map.length;
118     }
119 
120     /// Returns counts of ini entries
121     size_t size() const @nogc pure @safe
122     {
123         return _map.length;
124     }
125     size_t length() const @nogc pure @safe
126     {
127         return _map.length;
128     }
129 
130 private:
131     string[string] _map;
132 };
133 
134 // Ini parse exception
135 class IniConfigsException : Exception
136 {
137     this(size_t iniLine, string file = __FILE__, size_t line = __LINE__) pure @safe
138     {
139         super("IniConfigs: syntax error in line: " ~ iniLine.to!string, file, line);
140     }
141     this(string msg) pure @safe
142     {
143         super(msg);
144     }
145 }
146 
147 
148 
149 ///////////////////////////////////////////////////////
150 // cd source 
151 // rdmd -unittest -main  dini/iniconfig
152 unittest {
153     import std.math  : approxEqual;
154 
155     string ini = `
156         value1 = 1 ;value1 comment  
157         value2 =     hello world        ;value2 comment
158 
159             value3 =     1.1
160 
161         value3 = 3.1415 ; value3 comment
162 
163         ;value4 comment
164         value4 = 3.14159263358979361680130282241663053355296142399311
165 
166         ;dfgg
167 
168             ; this is a comment      
169         value5=value5
170             value6  =   value6
171         ;   dfgdfgdgd   sdfs    s       
172                     value7= Some text with spaces           
173 
174                     value7+  = " Some text with spaces      "     
175 
176         value8_ = 985642
177         value8=9856428642
178 
179 
180 
181         boolval1 = on
182         boolval2 = 1
183         boolval3 = true
184 
185         boolval4 = off
186         boolval5 = 0
187         boolval6 = false
188         boolval7 = any else
189     `;
190 
191 
192     IniConfigs cfg;
193 
194     assert(cfg.empty());
195     assert(!cfg.size());
196 
197     try {
198         cfg = ini;
199     } catch (IniConfigsException e) {
200         assert(0, e.msg);
201     }
202 
203 
204     assert(!cfg.empty());
205     assert(17 == cfg.size());
206 
207     cfg.add(`
208         value9 = 15
209         value10 =  -15        
210     `);
211 
212     assert(19 == cfg.size());
213 
214     assert(15 == cfg.get!ubyte  ("value9"));
215     assert(15 == cfg.get!ushort ("value9"));
216     assert(15 == cfg.get!uint   ("value9"));
217     assert(15 == cfg.get!ulong  ("value9"));
218 
219     assert(-15 == cfg.get!byte   ("value10"));
220     assert(-15 == cfg.get!int    ("value10"));
221     assert(-15 == cfg.get!short  ("value10"));
222     assert(-15 == cfg.get!long   ("value10"));
223     
224 
225     assert( approxEqual(3.1415, cfg.get!float  ("value3")) );
226     assert( approxEqual(3.1415, cfg.get!double ("value3")) );
227     assert( approxEqual(3.1415, cfg.get!real   ("value3")) );
228 
229     assert( approxEqual(3.14159263358979361680130282241663053355296142399311, cfg.get!float  ("value4"), 1e-20) );
230     assert( approxEqual(3.14159263358979361680130282241663053355296142399311, cfg.get!double ("value4"), 1e-40) );
231     assert( approxEqual(3.14159263358979361680130282241663053355296142399311, cfg.get!real   ("value4"), 1e-60) );
232 
233     
234     const string name1  = "value1";
235     const string name1_ = "value1_";
236     int val1  = cfg.get(name1,  5);
237     int val1_ = cfg.get(name1_, 5);
238 
239     assert(1 == val1);
240     assert(5 == val1_);
241 
242     assert("hello world"   == cfg.get!string("value2",  "default value") );
243     assert("default value" == cfg.get!string("value2+", "default value") );
244 
245     
246     assert("3.1415" == cfg.get!string("value3"));
247     assert(""       == cfg.get!string("value3+"));
248     
249     assert(approxEqual(3.1415, cfg.get("value3",  2.718281828459)));
250     assert(approxEqual(2.71828,cfg.get("value3+", 2.718281828459)));
251     assert(approxEqual(3.14159,cfg.get("value4",  2.718281828459)));
252 
253     assert(cfg.has("value3"));
254     assert(!cfg.has("value3+"));
255 
256     assert("Some text with spaces" == cfg.get!string("value7", "value7"));
257     assert(" Some text with spaces      " == cfg.get!string("value7+"));
258     cfg.add(`empty_str = ""`);
259     assert("" == cfg.get!string("empty_str") );
260 
261 
262     assert(985642     == cfg.get("value8_", 0) );
263     assert(9856428642 == cfg.get("value8", size_t(0)) );
264     assert(9856428642 == cfg.get!size_t("value8", 0) );
265 
266 
267     assert(false == cfg.get("boolval0", false) );
268     assert(true  == cfg.get("boolval1", false) );
269     assert(true  == cfg.get("boolval2", false) );
270     assert(true  == cfg.get("boolval3", false) );
271 
272     assert(false == cfg.get("boolval4", true) );
273     assert(false == cfg.get("boolval5", true) );
274     assert(false == cfg.get("boolval6", true) );
275     assert(false == cfg.get("boolval7", true) );
276 
277     assert(true ==  cfg.get!bool("boolval2") );
278     assert(false == cfg.get!bool("boolval0") );
279 }
280 
281 
282 unittest {
283     
284     import dini.inivalue : IniValue;
285     import std.conv : to;
286 
287     IniConfigs cfg = "value1 = 1";
288 
289     static struct A1 {
290         int a = 5;
291         this(IniValue v)
292         {
293             this.a = v.toString.to!int;
294         }
295     }
296     static struct A2 {
297         int a = 7;
298         void opAssign(IniValue v)
299         {
300             this.a = v.toString.to!int;
301         }
302     }
303 
304     assert( 1 == cfg.get!A1("value1").a );
305     assert( 5 == cfg.get!A1("value1+").a );
306     assert( 1 == cfg.get("value1", A1()).a );
307     assert( 5 == cfg.get("value1+", A1()).a );
308 
309     assert( 1 == cfg.get!A2("value1").a );
310     assert( 7 == cfg.get!A2("value1+").a );
311     assert( 1 == cfg.get("value1", A2()).a );
312     assert( 7 == cfg.get("value1+", A2()).a );
313 
314     //
315     // Indirectly passing:
316     //
317     static struct C {
318         int a = 5;
319     }
320     static struct D {
321         int a;
322         this(IniValue v) {
323             this.a = v.toString.to!int;
324         }
325         this(C c) {
326             this.a = c.a;
327         }
328         C castToC() const {
329             return C(this.a);
330         }
331         alias castToC this;
332     }
333     static struct E {
334         int a;
335         this(IniValue v) {
336             this.a = v.toString.to!int;
337         }
338         C castToC() const {
339             return C(this.a);
340         }
341         alias castToC this;
342     }
343 
344     C c1 = cfg.get!D("value1");
345     C c2 = cfg.get!E("value1");
346     C c3 = cfg.get("value1+", cast(D) C(3));
347 
348     assert( 1 == c1.a );
349     assert( 1 == c2.a );
350     assert( 3 == c3.a );
351 
352 }