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 
14     auto executable = currentProcessBinary();
15 
16     string[] locations = [
17         ".",
18         dirName(executable)
19     ];
20 
21     version (Windows)
22         locations ~= environment["LOCALAPPDATA"];
23     else version (Posix)
24         locations ~= environment["XDG_CONFIG_HOME"].ifThrown("~/.config");
25 
26     foreach (location; locations)
27     {
28         import std.path : buildPath, baseName;
29         import std.file : exists, readText;
30 
31         auto filename = buildPath(location, baseName(executable, ".exe") ~ ".cfg");
32         if (exists(filename))
33         {
34             auto content = readText(filename);
35             readConfigurationImpl(dst, content);
36             return;
37         }
38     }
39 }
40 
41 /**
42     Returns: fully-qualified filesystem path to the currently running executable
43 */
44 private string currentProcessBinary ()
45 {
46     import std.process : thisProcessID;
47 
48     version (Windows)
49     {
50         import core.sys.windows.psapi : GetProcessImageFileNameA;
51         import core.sys.windows.winbase : GetCurrentProcess;
52 
53         char[1024] buffer;
54         auto ln = GetProcessImageFileNameA(GetCurrentProcess(), buffer.ptr, buffer.length);
55         return buffer[0 .. ln].idup;
56     }
57     else version (Posix)
58     {
59         import std.file : readLink;
60         import std.format;
61         return readLink(format("/proc/%s/exe", thisProcessID()));
62     }
63 }
64 
65 private template resolveName (alias Field)
66 {
67     import std.traits;
68 
69     enum resolveName = getUDAs!(Field, CFG)[0].key.length
70         ? getUDAs!(Field, CFG)[0].key
71         : __traits(identifier, Field);
72 }
73 
74 unittest
75 {
76     struct S
77     {
78         @cfg
79         string field1;
80         @cfg("renamed")
81         string field2;
82     }
83 
84     static assert (resolveName!(S.field1) == "field1");
85     static assert (resolveName!(S.field2) == "renamed");
86 }
87 
88 private string[2] extractKV (string line)
89 {
90     import std.algorithm : findSplit;
91     import std.string : strip;
92 
93     auto separated = line.findSplit("=");
94     string key = strip(separated[0]);
95     string value = strip(separated[2]);
96     if (value[0] == '"' && value[$-1] == '"')
97         value = value[1 .. $ -1];
98 
99     return [ key, value ];
100 }
101 
102 unittest
103 {
104     assert(extractKV(" key = value ") == [ "key", "value" ]);
105     assert(extractKV(" key = \"value value\"") == [ "key", "value value" ]);
106 }
107 
108 private void readConfigurationImpl (S) (ref S dst, string src)
109 {
110     import std.traits;
111     import std.algorithm;
112     import std.range;
113     import std.conv;
114 
115     rt: foreach (line; src.splitter("\n"))
116     {
117         if (line.length == 0)
118             continue;
119 
120         auto kv = extractKV(line);
121         bool assign = false;
122 
123         static foreach (Field; getSymbolsByUDA!(S, CFG))
124         {
125             if (kv[0] == resolveName!Field)
126             {
127                 auto pfield = &__traits(getMember, dst, __traits(identifier, Field));
128                 *pfield = to!(typeof(*pfield))(kv[1]);
129                 continue rt;
130             }
131         }
132     }
133 }
134 
135 unittest
136 {
137     struct Config
138     {
139         @cfg
140         string field1;
141         @cfg("alias")
142         int field2;
143         @cfg("with space")
144         string field3;
145         string field4;
146     }
147 
148     Config config;
149 
150     readConfigurationImpl(config, `
151 field1 = value1
152 alias = 42
153 with space = value3
154 field4 = ignored`
155     );
156 
157     assert(config.field1 == "value1");
158     assert(config.field2 == 42);
159     assert(config.field3 == "value3");
160     assert(config.field4 == "");
161 }