1 module clid.core.parse;
2 
3 import std.stdio : writeln, stderr;
4 import core.stdc.stdlib : exit;
5 import std.traits : hasUDA, getUDAs, hasMember, getSymbolsByUDA;
6 import std.conv : text, to;
7 import std.typecons : Nullable;
8 import std.meta : Alias, AliasSeq;
9 
10 import clid.core.help;
11 import clid.core.util;
12 import clid.attributes;
13 import clid.validate;
14 
15 alias StringConsumer = void delegate(string);
16 
17 private struct ParseState(C)
18 {
19 	bool expectArgument = false;
20 	StringConsumer argumentConsumer = null;
21 	int namelessIndex = 0;
22 	mixin RequireStruct!C requires;
23 }
24 
25 private mixin template RequireStruct(C)
26 {
27 	static foreach (member; getSymbolsByUDA!(C, Required))
28 	{
29 		mixin("bool " ~ member.stringof ~ ";");
30 	}
31 }
32 
33 /**
34  * Parses a range of arguments.
35  * Params: args = The arguments to parse.
36  */
37 Nullable!C parse(C)(string[] args)
38 {
39 	validateStruct!C();
40 	C c;
41 	ParseState!C state;
42 
43 	foreach (string arg; args)
44 	{
45 		if (state.expectArgument == true)
46 		{
47 			state.argumentConsumer(arg);
48 			state.expectArgument = false;
49 		}
50 		else if (arg == "-h" || arg == "--help")
51 		{
52 			printHelp(c);
53 			return Nullable!C();
54 		}
55 		else
56 		{
57 			parseArgument(state, c, arg);
58 		}
59 	}
60 
61 	if (state.expectArgument)
62 	{
63 		stderr.writeln("Incomplete argument");
64 		return Nullable!C();
65 	}
66 	checkRequires(state);
67 	validateConfig(c);
68 
69 	return Nullable!C(c);
70 }
71 
72 private bool checkRequires(C)(ref ParseState!C state)
73 {
74 	bool failed = false;
75 	static foreach (member; __traits(allMembers, state.requires))
76 	{
77 		if (!mixin("state.requires." ~ member))
78 		{
79 			stderr.writeln("Missing required argument --" ~ getUDAs!(value!(C,
80 					member), Parameter)[0].longName);
81 			failed = true;
82 		}
83 	}
84 	return !failed;
85 }
86 
87 private bool parseArgument(C)(ref ParseState!C state, ref C c, string arg)
88 in(arg.length > 0, "Cannot parse empty argument")
89 {
90 	if (arg[0] != '-')
91 		return parseUnnamedArgument(state, c, arg);
92 	else if (arg.length < 2)
93 		return fail("Malformed argument '" ~ arg ~ "'");
94 	else if (arg[0] == '-' && arg[1] != '-')
95 	{
96 		foreach (flag; arg[1 .. $ - 1])
97 		{
98 			if (!parseShortArgument(state, c, flag))
99 				return false;
100 		}
101 		return parseShortArgument(state, c, arg[$ - 1], true);
102 	}
103 	else if (arg.length >= 3 && arg[0] == '-' && arg[1] == '-')
104 	{
105 		return parseLongArgument(state, c, arg[2 .. $]);
106 	}
107 	else
108 		return fail("Malformed argument '" ~ arg ~ "'");
109 }
110 
111 private bool parseShortArgument(C)(ref ParseState!C state, ref C c, dchar flag,
112 		immutable bool allowArgs = false)
113 {
114 	foreach (member; __traits(allMembers, C))
115 	{
116 		static if (hasUDA!(__traits(getMember, c, member), Parameter))
117 		{
118 			if (getUDAs!(__traits(getMember, c, member), Parameter)[0].shortName == flag)
119 			{
120 				static if (is(typeof(__traits(getMember, c, member)) == bool))
121 				{
122 					__traits(getMember, c, member) = true;
123 					return true;
124 				}
125 				else
126 				{
127 					if (allowArgs)
128 					{
129 						import std.conv : to;
130 
131 						state.expectArgument = true;
132 						state.argumentConsumer = (value) {
133 							static if (hasUDAV!(c, member, Required))
134 								mixin("state.requires." ~ member ~ " = true;");
135 							__traits(getMember, c, member) = to!(typeof(__traits(getMember,
136 									c, member)))(value);
137 						};
138 						return true;
139 					}
140 					else
141 						return fail("Illegal argument " ~ flag.text);
142 				}
143 			}
144 		}
145 	}
146 	return fail("Unkown argument " ~ flag.text);
147 }
148 
149 private bool parseLongArgument(C)(ref ParseState!C state, ref C c, string arg)
150 {
151 	foreach (member; __traits(allMembers, C))
152 	{
153 		static if (hasUDA!(__traits(getMember, c, member), Parameter))
154 		{
155 			if (getUDAs!(__traits(getMember, c, member), Parameter)[0].longName == arg)
156 			{
157 				static if (is(typeof(__traits(getMember, c, member)) == bool))
158 				{
159 					__traits(getMember, c, member) = true;
160 					return true;
161 				}
162 				else
163 				{
164 					state.expectArgument = true;
165 					state.argumentConsumer = (value) {
166 						static if (hasUDAV!(c, member, Required))
167 							mixin("state.requires." ~ member ~ " = true;");
168 						__traits(getMember, c, member) = to!(typeof(__traits(getMember, c, member)))(
169 								value);
170 					};
171 					return true;
172 				}
173 			}
174 		}
175 	}
176 	return fail("Illegal argument " ~ arg);
177 }
178 
179 private bool parseUnnamedArgument(C)(ref ParseState!C state, ref C c, string arg)
180 {
181 	auto setters = getSetters!C;
182 
183 	setters[state.namelessIndex++](c, arg);
184 	return false;
185 }
186 
187 private auto getSetters(C)()
188 {
189 	void function(ref C, string)[getUnnamedMembers!(C).length] setters;
190 	static foreach (i, member; getUnnamedMembers!(C))
191 	{
192 		setters[i] = function(ref C c, string arg) {
193 			mixin("c." ~ member ~ " = arg;");
194 		};
195 	}
196 	return setters;
197 }
198 
199 private template getUnnamedMembers(C)
200 {
201 	alias getUnnamedMembers = getUnnamedMembersIn!(C, __traits(allMembers, C));
202 }
203 
204 private template getUnnamedMembersIn(C, members...)
205 {
206 	static if (isUnnamedParameter!(C, members[0]))
207 	{
208 		static if (members.length == 1)
209 			alias getUnnamedMembersIn = AliasSeq!(members[0]);
210 		else
211 			alias getUnnamedMembersIn = AliasSeq!(members[0],
212 					getUnnamedMembersIn!(C, members[1 .. $]));
213 	}
214 	else static if (members.length == 1)
215 		alias getUnnamedMembersIn = AliasSeq!();
216 	else
217 		alias getUnnamedMembersIn = getUnnamedMembersIn!(C, members[1 .. $]);
218 }
219 
220 private bool fail(string message)
221 {
222 	stderr.writeln(message);
223 	return false;
224 }
225 
226 /**
227  * Validates a configuration struct.
228  * Each property should be marked using @Validate!validator.
229  * The validator will be called using the argument used (e.g.: "--help"),
230  * and with the actual value given.
231  * Params: c = The configuration struct to validate.
232  */
233 bool validateConfig(C)(ref C c) // @suppress(dscanner.suspicious.unused_parameter)
234 {
235 	foreach (member; __traits(allMembers, C))
236 	{
237 		static if (hasUDA!(__traits(getMember, C, member), Parameter))
238 		{
239 			Parameter parameter = getUDAs!(__traits(getMember, C, member), Parameter)[0];
240 			foreach (uda; getUDAs!(__traits(getMember, C, member), Validate))
241 			{
242 				static if (is(typeof(__traits(getMember, c, member)) == string))
243 				{
244 					if (__traits(getMember, c, member) !is null)
245 					{
246 						if (uda("--" ~ parameter.longName, __traits(getMember, c, member)) == false)
247 							return false;
248 					}
249 				}
250 				else
251 				{
252 					if (uda("--" ~ parameter.longName, __traits(getMember, c, member)) == false)
253 						return false;
254 				}
255 			}
256 		}
257 	}
258 	return true;
259 }
260 
261 // ======================
262 // Unit tests start here.
263 // ======================
264 
265 unittest
266 {
267 	struct Config
268 	{
269 		@Parameter("foo", 'f')
270 		string value;
271 
272 		@Parameter("num", 'n')
273 		int number;
274 
275 		@Parameter("req", 'r')
276 		@Required int required;
277 
278 		@Parameter("bool", 'b')
279 		bool b;
280 	}
281 
282 	immutable Config config = parse!Config([
283 			"--foo", "a_string", "-b", "-n", "5", "--req", "1"
284 			]);
285 
286 	assert(config.value == "a_string", "String value not read from arguments");
287 	assert(config.number == 5, "Integer value not read from arguments");
288 	assert(config.b == true, "Bool not set from arguments");
289 }
290 
291 unittest
292 {
293 
294 	struct Config
295 	{
296 		@Parameter("bool", 'b')
297 		bool b;
298 
299 		@Parameter("file", 'f') @Validate!doesNotExist string file;
300 	}
301 
302 	immutable Config config = parse!Config([
303 			"-bf", "some-random-file-that-should-not-exist"
304 			]);
305 	config.validateConfig();
306 
307 	assert(config.file == "some-random-file-that-should-not-exist",
308 			"String not read from arguments");
309 	assert(config.b == true, "Bool not set from arguments");
310 }
311 
312 unittest
313 {
314 	struct Config
315 	{
316 		@Parameter() string file;
317 	}
318 
319 	immutable config = parse!Config(["arg"]);
320 
321 	assert(config.file == "arg");
322 }
323 
324 unittest
325 {
326 	struct Config
327 	{
328 		@Parameter() string unnamed;
329 	}
330 
331 	assert(isUnnamedParameter!(Config, "unnamed") == true, "Parameter is unnamed");
332 }
333 
334 unittest
335 {
336 	struct Config
337 	{
338 		@Parameter("named") string named;
339 		@Parameter() string unnamed;
340 	}
341 
342 	assert(getUnnamedMembers!(Config) == AliasSeq!("unnamed"), "Did not find unnamed parameters");
343 }