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 
73 	return c;
74 }
75 
76 private void checkRequires(C)(ref ParseState!C state)
77 {
78 	bool failed = false;
79 	static foreach (member; __traits(allMembers, state.requires))
80 	{
81 		if (!mixin("state.requires." ~ member))
82 		{
83 			stderr.writeln("Missing required argument --" ~ getUDAs!(Value!(C,
84 					member), Parameter)[0].longName);
85 			failed = true;
86 		}
87 	}
88 	if (failed)
89 		exit(1);
90 }
91 
92 private void parseArgument(C)(ref ParseState!C state, ref C c, string arg)
93 {
94 	if (arg.length < 2)
95 		fail("Malformed argument '" ~ arg ~ "'");
96 	else if (arg[0] == '-' && arg[1] != '-')
97 	{
98 		foreach (flag; arg[1 .. $ - 1])
99 		{
100 			parseShortArgument(state, c, flag);
101 		}
102 		parseShortArgument(state, c, arg[$ - 1], true);
103 	}
104 	else if (arg.length >= 3 && arg[0] == '-' && arg[1] == '-')
105 	{
106 		parseLongArgument(state, c, arg[2 .. $]);
107 	}
108 	else
109 		fail("Malformed argument '" ~ arg ~ "'");
110 }
111 
112 private void parseShortArgument(C)(ref ParseState!C state, ref C c, dchar flag,
113 		immutable bool allowArgs = false)
114 {
115 	foreach (member; __traits(allMembers, C))
116 	{
117 		static if (hasUDA!(__traits(getMember, c, member), Parameter))
118 		{
119 			if (getUDAs!(__traits(getMember, c, member), Parameter)[0].shortName == flag)
120 			{
121 				static if (is(typeof(__traits(getMember, c, member)) == bool))
122 				{
123 					__traits(getMember, c, member) = true;
124 					return;
125 				}
126 				else
127 				{
128 					if (allowArgs)
129 					{
130 						import std.conv : to;
131 
132 						state.expectArgument = true;
133 						state.argumentConsumer = (value) {
134 							static if (hasUDAV!(c, member, Required))
135 								mixin("state.requires." ~ member ~ " = true;");
136 							__traits(getMember, c, member) = to!(typeof(__traits(getMember,
137 									c, member)))(value);
138 						};
139 						return;
140 					}
141 					else
142 						fail("Illegal argument " ~ flag.text);
143 				}
144 			}
145 		}
146 	}
147 	fail("Unkown argument " ~ flag.text);
148 }
149 
150 private void parseLongArgument(C)(ref ParseState!C state, ref C c, string arg)
151 {
152 	foreach (member; __traits(allMembers, C))
153 	{
154 		static if (hasUDA!(__traits(getMember, c, member), Parameter))
155 		{
156 			if (getUDAs!(__traits(getMember, c, member), Parameter)[0].longName == arg)
157 			{
158 				static if (is(typeof(__traits(getMember, c, member)) == bool))
159 				{
160 					__traits(getMember, c, member) = true;
161 					return;
162 				}
163 				else
164 				{
165 					import std.conv : to;
166 
167 					state.expectArgument = true;
168 					state.argumentConsumer = (value) {
169 						static if (hasUDAV!(c, member, Required))
170 							mixin("state.requires." ~ member ~ " = true;");
171 						__traits(getMember, c, member) = to!(typeof(__traits(getMember, c, member)))(
172 								value);
173 					};
174 					return;
175 				}
176 			}
177 		}
178 	}
179 	fail("Illegal argument " ~ arg);
180 }
181 
182 private void fail(string message)
183 {
184 	writeln(message);
185 	exit(1);
186 }
187 
188 /**
189  * Validates a configuration struct.
190  * Each property should be marked using @Validate!validator.
191  * The validator will be called using the argument used (e.g.: "--help"),
192  * and with the actual value given.
193  * Params: c = The configuration struct to validate.
194  */
195 void validateConfig(C)(C c) // @suppress(dscanner.suspicious.unused_parameter)
196 {
197 	foreach (member; __traits(allMembers, C))
198 	{
199 		static if (hasUDA!(__traits(getMember, C, member), Parameter))
200 		{
201 			Parameter parameter = getUDAs!(__traits(getMember, C, member), Parameter)[0];
202 			foreach (uda; getUDAs!(__traits(getMember, C, member), Validate))
203 			{
204 				if (__traits(getMember, c, member) !is null)
205 				{
206 					if (uda("--" ~ parameter.longName, __traits(getMember, c, member)) == false)
207 						exit(1);
208 				}
209 			}
210 		}
211 	}
212 }
213 
214 // ======================
215 // Unit tests start here.
216 // ======================
217 
218 unittest
219 {
220 	struct Config
221 	{
222 		@Parameter("foo", 'f')
223 		string value;
224 
225 		@Parameter("num", 'n')
226 		int number;
227 
228 		@Parameter("req", 'r')
229 		@Required int required;
230 
231 		@Parameter("bool", 'b')
232 		bool b;
233 	}
234 
235 	immutable Config config = parse!Config(["--foo", "a_string", "-b", "-n", "5", "--req", "1"]);
236 	import std.stdio : writeln;
237 
238 	assert(config.value == "a_string", "String value not read from arguments");
239 	assert(config.number == 5, "Integer value not read from arguments");
240 	assert(config.b == true, "Bool not set from arguments");
241 }
242 
243 unittest
244 {
245 
246 	struct Config
247 	{
248 		@Parameter("bool", 'b')
249 		bool b;
250 
251 		@Parameter("file", 'f') @Validate!doesNotExist string file;
252 	}
253 
254 	immutable Config config = parse!Config(["-bf", "some-random-file-that-should-not-exist"]);
255 	config.validateConfig();
256 	import std.stdio : writeln;
257 
258 	assert(config.file == "some-random-file-that-should-not-exist",
259 			"String not read from arguments");
260 	assert(config.b == true, "Bool not set from arguments");
261 }