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 information 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     Provides a means to convert a field from a `Node` to a complex type
202 
203     When filling the config, it might be useful to store types which are
204     not only simple `string` and integer, such as `URL`, `BigInt`, or any other
205     library type not directly under the user's control.
206 
207     To allow reading those values from the config file, a `Converter` may
208     be used. The converter will tell the `ConfigFiller` how to convert from
209     `Node` to the desired type `T`.
210 
211     If the type is under the user's control, one can also add a constructor
212     accepting a single string, or define the `fromString` method, both of which
213     are tried if no `Converter` is found.
214 
215     For types not under the user's control, there might be different ways
216     to parse the same type within the same struct, or neither the ctor nor
217     the `fromString` method may be defined under that name.
218     The exmaple below uses `parse` in place of `fromString`, for example.
219 
220     ```
221     /// Complex structure representing the age of a person based on its birthday
222     public struct Age
223     {
224         ///
225         public uint birth_year;
226         ///
227         public uint birth_month;
228         ///
229         public uint birth_day;
230 
231         /// Note that this will be picked up automatically if named `fromString`
232         /// but this struct might be a library type.
233         public static Age parse (string value) { /+ Magic +/ }
234     }
235 
236     public struct Person
237     {
238         ///
239         @Converter!Age((Node value) => Age.parse(value.as!string))
240         public Age age;
241     }
242     ```
243 
244     Note that some fields may also be of multiple YAML types, such as DUB's
245     `dependencies`, which is either a simple string (`"vibe-d": "~>1.0 "`),
246     or an in its complex form (`"vibe-d": { "version": "~>1.0" }`).
247     For those use cases, a `Converter` is the best approach.
248 
249     To avoid repeating the field type, a convenience function is provided:
250     ```
251     public struct Age
252     {
253         public uint birth_year;
254         public uint birth_month;
255         public uint birth_day;
256         public static Age parse (string value) { /+ Magic +/ }
257     }
258 
259     public struct Person
260     {
261         /// Here `converter` will deduct the type from the delegate argument,
262         /// and return an instance  of `Converter`. Mind the case.
263         @converter((Node value) => Age.parse(value.as!string))
264         public Age age;
265     }
266     ```
267 
268 *******************************************************************************/
269 
270 public struct Converter (T)
271 {
272     ///
273     public alias ConverterFunc = T function (scope ConfigParser!T context);
274 
275     ///
276     public ConverterFunc converter;
277 }
278 
279 /// Ditto
280 public auto converter (FT) (FT func)
281 {
282     static assert(isFunctionPointer!FT,
283                   "Error: Argument to `converter` should be a function pointer, not: "
284                   ~ FT.stringof);
285 
286     alias RType = ReturnType!FT;
287     static assert(!is(RType == void),
288                   "Error: Converter needs to be of the return type of the field, not `void`");
289     return Converter!RType(func);
290 }
291 
292 public interface ConfigParser (T)
293 {
294     import dub.internal.dyaml.node;
295     import dub.internal.configy.FieldRef : StructFieldRef;
296     import dub.internal.configy.Read : Context, parseField;
297 
298     /// Returns: the node being processed
299     public inout(Node) node () inout @safe pure nothrow @nogc;
300 
301     /// Returns: current location we are parsing
302     public string path () const @safe pure nothrow @nogc;
303 
304     ///
305     public final auto parseAs (OtherType)
306         (auto ref OtherType defaultValue = OtherType.init)
307     {
308         alias TypeFieldRef = StructFieldRef!OtherType;
309         return this.node().parseField!(TypeFieldRef)(
310             this.path(), defaultValue, this.context());
311     }
312 
313     /// Internal use only
314     protected const(Context) context () const @safe pure nothrow @nogc;
315 }