1 module simpleconfig.file;
2 
3 import simpleconfig.attributes;
4 
5 /// See `simpleconfig.readConfiguration`
6 public void readConfiguration (S) (ref S dst)
7 {
8     static assert (is(S == struct), "Only structs are supported as configuration target");
9 
10     import std.process : environment;
11     import std.exception : ifThrown;
12     import std.path : dirName;
13     import std.file : thisExePath;
14 
15     auto executable = thisExePath();
16 
17     string[] locations = [
18         ".",
19         dirName(executable)
20     ];
21 
22     version (Windows)
23         locations ~= environment["LOCALAPPDATA"];
24     else version (Posix)
25         locations ~= environment["XDG_CONFIG_HOME"].ifThrown("~/.config");
26 
27     foreach (location; locations)
28     {
29         import std.path : buildPath, baseName;
30         import std.file : exists, readText;
31 
32         auto filename = buildPath(location, baseName(executable, ".exe") ~ ".cfg");
33         if (exists(filename))
34         {
35             auto content = readText(filename);
36             readConfigurationImpl(dst, content);
37             return;
38         }
39     }
40 }
41 
42 private template resolveName (alias Field)
43 {
44     import std.traits;
45 
46     enum resolveName = getUDAs!(Field, CFG)[0].key.length
47         ? getUDAs!(Field, CFG)[0].key
48         : __traits(identifier, Field);
49 }
50 
51 unittest
52 {
53     struct S
54     {
55         @cfg
56         string field1;
57         @cfg("renamed")
58         string field2;
59     }
60 
61     static assert (resolveName!(S.field1) == "field1");
62     static assert (resolveName!(S.field2) == "renamed");
63 }
64 
65 private string[2] extractKV (string line)
66 {
67     import std.algorithm : findSplit;
68     import std.string : strip;
69 
70     auto separated = line.findSplit("=");
71     string key = strip(separated[0]);
72     string value = strip(separated[2]);
73     if (value[0] == '"' && value[$-1] == '"')
74         value = value[1 .. $ -1];
75 
76     return [ key, value ];
77 }
78 
79 unittest
80 {
81     assert(extractKV(" key = value ") == [ "key", "value" ]);
82     assert(extractKV(" key = \"value value\"") == [ "key", "value value" ]);
83 }
84 
85 private void readConfigurationImpl (S) (ref S dst, string src)
86 {
87     import std.traits;
88     import std.algorithm;
89     import std.range;
90     import std.conv;
91 
92     rt: foreach (line; src.splitter("\n"))
93     {
94         if (line.length == 0 || line.startsWith("#"))
95             continue;
96 
97         auto kv = extractKV(line);
98         bool assign = false;
99 
100         static foreach (Field; getSymbolsByUDA!(S, CFG))
101         {
102             if (kv[0] == resolveName!Field)
103             {
104                 auto pfield = &__traits(getMember, dst, __traits(identifier, Field));
105                 *pfield = to!(typeof(*pfield))(kv[1]);
106                 continue rt;
107             }
108         }
109     }
110 }
111 
112 unittest
113 {
114     struct Config
115     {
116         @cfg
117         string field1;
118         @cfg("alias")
119         int field2;
120         @cfg("with space")
121         string field3;
122         string field4;
123     }
124 
125     Config config;
126 
127     readConfigurationImpl(config, `
128 # this is a comment
129 field1 = value1
130 alias = 42
131 with space = value3
132 field4 = ignored`
133     );
134 
135     assert(config.field1 == "value1");
136     assert(config.field2 == 42);
137     assert(config.field3 == "value3");
138     assert(config.field4 == "");
139 }