1 /*******************************************************************************
2 
3     Provide the suggested default configuration for applications
4 
5     This module provide the basic tool to quickly get configuration parsing with
6     environment and command-line overrides. It assumes a YAML configuration.
7 
8     Note:
9       This module name is inspired inspired by cURL's 'easy' API.
10 
11 *******************************************************************************/
12 
13 module dub.internal.configy.easy;
14 
15 public import dub.internal.configy.attributes;
16 public import dub.internal.configy.exceptions : ConfigException;
17 public import dub.internal.configy.read;
18 
19 import std.getopt;
20 import std.typecons : Nullable, nullable;
21 
22 /// Command-line arguments
23 public struct CLIArgs
24 {
25     /// Path to the config file
26     public string config_path = "config.yaml";
27 
28     /// Overrides for config options
29     public string[][string] overrides;
30 
31     /// Helper to add items to `overrides`
32     public void overridesHandler (string, string value)
33     {
34         import std.string;
35         const idx = value.indexOf('=');
36         if (idx < 0) return;
37         string k = value[0 .. idx], v = value[idx + 1 .. $];
38         if (auto val = k in this.overrides)
39             (*val) ~= v;
40         else
41             this.overrides[k] = [ v ];
42     }
43 
44     /***************************************************************************
45 
46         Parses the base command line arguments
47 
48         This can be composed with the program argument.
49         For example, consider a program which wants to expose a `--version`
50         switch, the definition could look like this:
51         ---
52         public struct ProgramCLIArgs
53         {
54             public CLIArgs base; // This struct
55 
56             public alias base this; // For convenience
57 
58             public bool version_; // Program-specific part
59         }
60         ---
61         Then, an application-specific configuration routine would be:
62         ---
63         public GetoptResult parse (ref ProgramCLIArgs clargs, ref string[] args)
64         {
65             auto r = clargs.base.parse(args);
66             if (r.helpWanted) return r;
67             return getopt(
68                 args,
69                 "version", "Print the application version, &clargs.version_");
70         }
71         ---
72 
73         Params:
74           args = The command line args to parse (parsed options will be removed)
75           passThrough = Whether to enable `config.passThrough` and
76                         `config.keepEndOfOptions`. `true` by default, to allow
77                         composability. If your program doesn't have other
78                         arguments, pass `false`.
79 
80         Returns:
81           The result of calling `getopt`
82 
83     ***************************************************************************/
84 
85     public GetoptResult parse (ref string[] args, bool passThrough = true)
86     {
87         return getopt(
88             args,
89             // `caseInsensistive` is the default, but we need something
90             // with the same type for the ternary
91             passThrough ? config.keepEndOfOptions : config.caseInsensitive,
92             // Also the default, same reasoning
93             passThrough ? config.passThrough : config.noPassThrough,
94             "config|c",
95                 "Path to the config file. Defaults to: " ~ this.config_path,
96                 &this.config_path,
97 
98             "override|O",
99                 "Override a config file value\n" ~
100                 "Example: -O foo.bar=true -O dns=1.1.1.1 -O dns=2.2.2.2\n" ~
101                 "Array values are additive, other items are set to the last override",
102                 &this.overridesHandler,
103         );
104     }
105 }
106 
107 /*******************************************************************************
108 
109     Attempt to read and deserialize the config file at `path` into the `struct`
110     type `Config` and print any error on failure
111 
112     This 'simple' overload of the more detailed `parseConfigFile` will attempt
113     to deserialize the content of the file at `path` into an instance of
114     `ConfigT`, and return a `Nullable` instance of it.
115     If an error happens, either because the file isn't readable or
116     the configuration has an issue, a message will be printed to `stderr`,
117     with colors if the output is a TTY, and a `null` instance will be returned.
118 
119     The calling code can hence just read a config file via:
120     ```
121     int main ()
122     {
123         auto configN = parseConfigFileSimple!Config("config.yaml");
124         if (configN.isNull()) return 1; // Error path
125         auto config = configN.get();
126         // Rest of the program ...
127     }
128     ```
129     An overload accepting `CLIArgs args` also exists.
130 
131     Params:
132         path = Path of the file to read from
133         args = Command line arguments on which `parse` has been called
134         strict = Whether the parsing should reject unknown keys in the
135                  document, warn, or ignore them (default: `StrictMode.Error`)
136 
137     Returns:
138         An initialized `ConfigT` instance if reading/parsing was successful;
139         a `null` instance otherwise.
140 
141 *******************************************************************************/
142 
143 public Nullable!ConfigT parseConfigFileSimple (ConfigT)
144     (string path, StrictMode strict = StrictMode.Error)
145 {
146     return wrapException(parseConfigFile!(ConfigT)(CLIArgs(path), strict));
147 }
148 
149 /// Ditto
150 public Nullable!ConfigT parseConfigFileSimple (ConfigT)
151     (in CLIArgs args, StrictMode strict = StrictMode.Error)
152 {
153     return wrapException(parseConfigFile!(ConfigT)(args, strict));
154 }
155 
156 /*******************************************************************************
157 
158     Parses the config file or string and returns a `Config` instance.
159 
160     Params:
161         ConfigT = A `struct` type used to drive the deserialization and
162                   validation. This type definition is the most important aspect
163                   of how Configy works.
164 
165         args = command-line arguments (containing the path to the config)
166         path = When parsing a string, the path corresponding to it
167         data = A string containing a valid YAML document to be processed
168         strict = Whether the parsing should reject unknown keys in the
169                  document, warn, or ignore them (default: `StrictMode.Error`)
170 
171     Throws:
172         `ConfigException` if deserializing the configuration into `ConfigT`
173          failed, or an underlying `Exception` if a backend failed (e.g.
174          `path` was not found).
175 
176     Returns:
177         A valid `ConfigT` instance
178 
179 *******************************************************************************/
180 
181 public ConfigT parseConfigFile (ConfigT)
182     (in CLIArgs args, StrictMode strict = StrictMode.Error)
183 {
184     import dub.internal.configy.backend.yaml;
185 
186     auto root = parseFile(args.config_path);
187     return parseConfig!ConfigT(root, strict);
188 }
189 
190 /// ditto
191 public ConfigT parseConfigString (ConfigT)
192     (string data, string path, StrictMode strict = StrictMode.Error)
193 {
194     CLIArgs args = { config_path: path };
195     return parseConfigString!(ConfigT)(data, args, strict);
196 }
197 
198 /// ditto
199 public ConfigT parseConfigString (ConfigT)
200     (string data, in CLIArgs args, StrictMode strict = StrictMode.Error)
201 {
202     import dub.internal.configy.backend.yaml;
203 
204     assert(args.config_path.length, "No config_path provided to parseConfigString");
205     auto root = parseString(data, args.config_path);
206     return parseConfig!ConfigT(root, strict);
207 }