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