1 /*******************************************************************************
2 
3     Define UDAs that can be applied to a configuration struct
4 
5     This module is stand alone (a leaf module) to allow importing the UDAs
6     without importing the whole configuration parsing code.
7 
8     Copyright:
9         Copyright (c) 2019-2022 BOSAGORA Foundation
10         All rights reserved.
11 
12     License:
13         MIT License. See LICENSE for details.
14 
15 *******************************************************************************/
16 
17 module dub.internal.configy.attributes;
18 
19 import std.traits;
20 
21 /*******************************************************************************
22 
23     An optional parameter with an initial value of `T.init`
24 
25     The config parser automatically recognize non-default initializer,
26     so that the following:
27     ```
28     public struct Config
29     {
30         public string greeting = "Welcome home";
31     }
32     ```
33     Will not error out if `greeting` is not defined in the config file.
34     However, this relies on the initializer of the field (`greeting`) being
35     different from the type initializer (`string.init` is `null`).
36     In some cases, the default value is also the desired initializer, e.g.:
37     ```
38     public struct Config
39     {
40         /// Maximum number of connections. 0 means unlimited.
41         public uint connections_limit = 0;
42     }
43     ```
44     In this case, one can add `@Optional` to the field to inform the parser.
45 
46 *******************************************************************************/
47 
48 public struct Optional {}
49 
50 /*******************************************************************************
51 
52     Inform the config filler that this sequence is to be read as a mapping
53 
54     On some occasions, one might want to read a mapping as an array.
55     One reason to do so may be to provide a better experience to the user,
56     e.g. having to type:
57     ```
58     interfaces:
59       eth0:
60         ip: "192.168.0.1"
61         private: true
62       wlan0:
63         ip: "1.2.3.4"
64     ```
65     Instead of the slightly more verbose:
66     ```
67     interfaces:
68       - name: eth0
69         ip: "192.168.0.1"
70         private: true
71       - name: wlan0
72         ip: "1.2.3.4"
73     ```
74 
75     The former would require to be expressed as an associative arrays.
76     However, one major drawback of associative arrays is that they can't have
77     an initializer, which makes them cumbersome to use in the context of the
78     config filler. To remediate this issue, one may use `@Key("name")`
79     on a field (here, `interfaces`) so that the mapping is flattened
80     to an array. If `name` is `null`, the key will be discarded.
81 
82 *******************************************************************************/
83 
84 public struct Key
85 {
86     ///
87     public string name;
88 }
89 
90 /*******************************************************************************
91 
92     Look up the provided name in the YAML node, instead of the field name.
93 
94     By default, the config filler will look up the field name of a mapping in
95     the YAML node. If this is not desired, an explicit `Name` attribute can
96     be given. This is especially useful for names which are keyword.
97 
98     ```
99     public struct Config
100     {
101         public @Name("delete") bool remove;
102     }
103     ```
104 
105 *******************************************************************************/
106 
107 public struct Name
108 {
109     ///
110     public string name;
111 
112     ///
113     public bool startsWith;
114 }
115 
116 /// Short hand syntax
117 public Name StartsWith(string name) @safe pure nothrow @nogc
118 {
119     return Name(name, true);
120 }
121 
122 /*******************************************************************************
123 
124     A field which carries informations about whether it was set or not
125 
126     Some configurations may need to know which fields were set explicitly while
127     keeping defaults. An example of this is a `struct` where at least one field
128     needs to be set, such as the following:
129     ```
130     public struct ProtoDuration
131     {
132         public @Optional long weeks;
133         public @Optional long days;
134         public @Optional long hours;
135         public @Optional long minutes;
136         public           long seconds = 42;
137         public @Optional long msecs;
138         public @Optional long usecs;
139         public @Optional long hnsecs;
140         public @Optional long nsecs;
141     }
142     ```
143     In this case, it would be impossible to know if any field was explicitly
144     provided. Hence, the struct should be written as:
145     ```
146     public struct ProtoDuration
147     {
148         public SetInfo!long weeks;
149         public SetInfo!long days;
150         public SetInfo!long hours;
151         public SetInfo!long minutes;
152         public SetInfo!long seconds = 42;
153         public SetInfo!long msecs;
154         public SetInfo!long usecs;
155         public SetInfo!long hnsecs;
156         public SetInfo!long nsecs;
157     }
158     ```
159     Note that `SetInfo` implies `Optional`, and supports default values.
160 
161 *******************************************************************************/
162 
163 public struct SetInfo (T)
164 {
165     /***************************************************************************
166 
167         Allow initialization as a field
168 
169         This sets the field as having been set, so that:
170         ```
171         struct Config { SetInfo!Duration timeout; }
172 
173         Config myConf = { timeout: 10.minutes }
174         ```
175         Will behave as if set explicitly. If this behavior is not wanted,
176         pass `false` as second argument:
177         ```
178         Config myConf = { timeout: SetInfo!Duration(10.minutes, false) }
179         ```
180 
181     ***************************************************************************/
182 
183     public this (T initVal, bool isSet = true) @safe pure nothrow @nogc
184     {
185         this.value = initVal;
186         this.set = isSet;
187     }
188 
189     /// Underlying data
190     public T value;
191 
192     ///
193     alias value this;
194 
195     /// Whether this field was set or not
196     public bool set;
197 }
198 
199 /*******************************************************************************
200 
201     Interface that is passed to `fromConfig` hook
202 
203     The `ConfigParser` exposes the raw underlying node (see `node` method),
204     the path within the file (`path` method), and a simple ability to recurse
205     via `parseAs`. This allows to implement complex logic independent of the
206     underlying configuration format.
207 
208 *******************************************************************************/
209 
210 public interface ConfigParser
211 {
212     import dub.internal.configy.backend.node;
213     import dub.internal.configy.fieldref : StructFieldRef;
214     import dub.internal.configy.read : Context, parseField;
215 
216     /// Returns: the node being processed
217     public inout(Node) node () inout @safe pure nothrow @nogc;
218 
219     /// Returns: current location we are parsing
220     public string path () const @safe pure nothrow @nogc;
221 
222     /***************************************************************************
223 
224         Parse this struct as another type
225 
226         This allows implementing union-like behavior, where a `struct` which
227         implements `fromConfig` can parse a simple representation as one type,
228         and one more advanced as another type.
229 
230         Params:
231           OtherType = The type to parse as
232           defaultValue = The instance to use as a default value for fields
233 
234     ***************************************************************************/
235 
236     public final auto parseAs (OtherType)
237         (auto ref OtherType defaultValue = OtherType.init)
238     {
239         alias TypeFieldRef = StructFieldRef!OtherType;
240         return this.node().parseField!(TypeFieldRef)(
241             this.path(), defaultValue, this.context());
242     }
243 
244     /// Internal use only
245     protected const(Context) context () const @safe pure nothrow @nogc;
246 }
247 
248 /*******************************************************************************
249 
250     Specify that a field only accept a limited set of string values.
251 
252     This is similar to how `enum` symbolic names are treated, however the `enum`
253     symbolic names may not contain spaces or special character.
254 
255     Params:
256       Values = Permissible values (case sensitive)
257 
258 *******************************************************************************/
259 
260 public struct Only (string[] Values) {
261     public string value;
262 
263     alias value this;
264 
265     public static Only fromString (scope string str) {
266         import std.algorithm.searching : canFind;
267         import std.exception : enforce;
268         import std.format;
269 
270         enforce(Values.canFind(str),
271             "%s is not a valid value for this field, valid values are: %(%s, %)"
272             .format(str, Values));
273         return Only(str);
274     }
275 }
276 
277 ///
278 unittest {
279     import dub.internal.configy.attributes : Only, Optional;
280     import dub.internal.configy.easy : parseConfigString;
281 
282     static struct CountryConfig {
283         Only!(["France", "Malta", "South Korea"]) country;
284         // Compose with other attributes too
285         @Optional Only!(["citizen", "resident", "alien"]) status;
286     }
287     static struct Config {
288         CountryConfig[] countries;
289     }
290 
291     auto conf = parseConfigString!Config(`countries:
292   - country: France
293     status: citizen
294   - country: Malta
295   - country: South Korea
296     status: alien
297 `, "/dev/null");
298 
299     assert(conf.countries.length == 3);
300     assert(conf.countries[0].country == `France`);
301     assert(conf.countries[0].status  == `citizen`);
302     assert(conf.countries[1].country == `Malta`);
303     assert(conf.countries[1].status  is null);
304     assert(conf.countries[2].country == `South Korea`);
305     assert(conf.countries[2].status  == `alien`);
306 
307     import dub.internal.configy.exceptions : ConfigException;
308 
309     try parseConfigString!Config(`countries:
310   - country: France
311     status: expatriate
312 `, "/etc/config");
313     catch (ConfigException exc)
314         assert(exc.toString() == `/etc/config(3:13): countries[0].status: expatriate is not a valid value for this field, valid values are: "citizen", "resident", "alien"`);
315 }