1 /**
2  * Command framework for IRC bots.
3  *
4  * Groups of commands are bundled as _command sets,
5  * which are defined by classes deriving from $(MREF CommandSet).
6  *
7  * All _command sets implement the $(MREF ICommandSet) interface,
8  * which presents basic operations for _command sets.
9  *
10  * Commands are represented by the $(MREF Command) struct.
11  *
12  * See_Also:
13  *    $(MREF CommandSet)
14  */
15 module diggler.command;
16 
17 import std.array;
18 import std.exception;
19 import std.traits;
20 
21 import diggler.attribute;
22 import diggler.bot;
23 import diggler.context;
24 import diggler.util;
25 
26 import irc.client;
27 
28 /// Can be thrown by command implementations to signal a problem
29 /// with the command arguments.
30 class CommandArgumentException : Exception
31 {
32 	import irc.util : ExceptionConstructor;
33 	mixin ExceptionConstructor!();
34 }
35 
36 /// Represents a single command.
37 struct Command
38 {
39 	string name;
40 	string[] aliases;
41 	string usage;
42 
43 	static struct ParameterInfo
44 	{
45 		string name;
46 		string typeName;
47 		string defaultArgument; // null when this parameter has no default argument
48 		string displayName;
49 
50 		static immutable angleBrackets = "<>";
51 		static immutable squareBrackets = "[]";
52 
53 		this(string name, string typeName, bool optional, string defaultArgument, bool variadic)
54 		{
55 			this.name = name;
56 			this.typeName = typeName;
57 			this.defaultArgument = defaultArgument;
58 
59 			auto brackets = optional? squareBrackets : angleBrackets;
60 
61 			auto app = appender!string();
62 
63 			app ~= brackets[0];
64 			app ~= name;
65 
66 			if(variadic)
67 				app ~= "...";
68 
69 			auto isString = typeName == "string";
70 
71 			if(defaultArgument)
72 			{
73 				app ~= " = ";
74 
75 				if(isString)
76 					app ~= '\"';
77 
78 				app ~= defaultArgument;
79 
80 				if(isString)
81 					app ~= '\"';
82 			}
83 
84 			if(!isString)
85 			{
86 				app ~= " (";
87 				app ~= typeName;
88 				app ~= ")";
89 			}
90 
91 			app ~= brackets[1];
92 
93 			this.displayName = app.data;
94 		}
95 	}
96 
97 	ParameterInfo[] parameterInfo;
98 	bool adminOnly = false;
99 	bool channelOnly = false;
100 	bool variadic = false;
101 
102 	package:
103 	void delegate(string strArgs) handler;
104 
105 	static Command create(alias handler, T)(T dg) if(is(T == delegate))
106 	{
107 		alias RetType = ReturnType!T;
108 		static immutable primaryName = __traits(identifier, handler);
109 
110 		static assert(is(RetType == void),
111 			format("return type of command handler `%s` must be `void`, not `%s`",
112 				fullyQualifiedName!handler,
113 				RetType.stringof));
114 
115 		alias Args = FillableParameterTypeTuple!T;
116 		alias defaultArgs = ParameterDefaultValueTuple!handler;
117 		enum isVariadic = variadicFunctionStyle!handler == Variadic.typesafe;
118 
119 		void handleCommand(string strArgs) // TODO: code size reduction potential here
120 		{
121 			import std.algorithm : findSplitBefore;
122 			import std.uni : isWhite; // TODO
123 			import std..string : strip, stripLeft;
124 
125 			strArgs = strArgs.strip();
126 
127 			Args args;
128 
129 			enum firstDefaultArg = computeFirstDefaultArg!defaultArgs;
130 			enum hasDefaultArgs = firstDefaultArg != -1;
131 
132 			static immutable expectedMoreArgsMsg = hasDefaultArgs || isVariadic?
133 				`got %s argument(s), expected at least %s for command "%s"` :
134 				`got %s argument(s), expected %s for command "%s"`;
135 
136 			foreach(i, ref arg; args)
137 			{
138 				alias Arg = typeof(arg);
139 				enum isLastArgument = i == args.length - 1;
140 
141 				static if(is(Arg == string[]))
142 				{
143 					static assert(isLastArgument, fullyQualifiedName!handler ~ `: string[] parameter can only appear at the end of the parameter list`);
144 					static assert(isVariadic, fullyQualifiedName!handler ~ `: string[] parameter must be variadic`);
145 				}
146 				else
147 					static assert(isValidCommandParameterType!Arg,
148 						format("parameter #%s of command handler `%s` is of unsupported type `%s`",
149 							i + 1,
150 							fullyQualifiedName!handler,
151 							Arg.stringof));
152 
153 				alias defaultArg = defaultArgs[i];
154 
155 				static if(is(defaultArg == void))
156 				{
157 					static if(!(isLastArgument && isVariadic)) // Variadic arguments can be empty
158 						enforceEx!CommandArgumentException(!strArgs.empty,
159 							format(expectedMoreArgsMsg,
160 								i,
161 								hasDefaultArgs? firstDefaultArg : args.length - isVariadic,
162 								primaryName));
163 				}
164 				else
165 				{
166 					if(strArgs.empty)
167 					{
168 						arg = defaultArg;
169 					}
170 				}
171 
172 				if(!strArgs.empty)
173 				{
174 					auto result = strArgs.findSplitBefore(" ");
175 
176 					auto tail = result[1].stripLeft();
177 
178 					static if(isLastArgument &&
179 						(is(Arg : const(char)[]) || is(Arg : const(char[])[])))
180 					{
181 						auto rawArg = strArgs;
182 						strArgs = null;
183 					}
184 					else
185 					{
186 						auto rawArg = result[0];
187 						strArgs = tail;
188 					}
189 
190 					arg = parseCommandArgument!Arg(rawArg, primaryName, i + 1);
191 				}
192 			}
193 
194 			enforceEx!CommandArgumentException(strArgs.empty,
195 				format(`too many arguments to command "%s", expected %s`,
196 					primaryName,
197 					args.length));
198 
199 			dg(args);
200 		}
201 
202 		Command cmd;
203 		cmd.name = primaryName;
204 
205 		static if(isVariadic)
206 			cmd.variadic = true;
207 
208 		static if(hasAttribute!(handler, .aliases))
209 			cmd.aliases = getAttribute!(handler, .aliases).value;
210 
211 		static if(hasAttribute!(handler, .usage))
212 			cmd.usage = getAttribute!(handler, .usage).value;
213 
214 		static if(hasAttribute!(handler, .admin))
215 			cmd.adminOnly = true;
216 
217 		static if(hasAttribute!(handler, .channelOnly))
218 			cmd.channelOnly = true;
219 
220 		foreach(i, paramName; ParameterIdentifierTuple!handler)
221 		{
222 			import std.conv : to;
223 
224 			alias defaultArg = defaultArgs[i];
225 			static if(is(defaultArg == void))
226 			{
227 				bool optional = false;
228 				string defaultValue = null;
229 			}
230 			else
231 			{
232 				bool optional = true;
233 				static if(isSomeString!(typeof(defaultArg)))
234 				{
235 					string defaultValue = defaultArg.ptr? to!string(defaultArg) : null;
236 				}
237 				else
238 					string defaultValue = to!string(defaultArg);
239 			}
240 
241 			bool vararg = isVariadic && i == Args.length - 1;
242 			cmd.parameterInfo ~= ParameterInfo(paramName, commandParameterTypeName!(Args[i]), optional, defaultValue, vararg);
243 		}
244 
245 		cmd.handler = &handleCommand;
246 
247 		return cmd;
248 	}
249 }
250 
251 // TODO: Simplify?
252 private template computeFirstDefaultArgImpl(int count, args...)
253 {
254 	static if(args.length > 0 && is(args[0] == void))
255 	{
256 		enum computeFirstDefaultArgImpl = computeFirstDefaultArgImpl!(count, args[1 .. $]) + 1;
257 	}
258 	else
259 		enum computeFirstDefaultArgImpl = count;
260 }
261 
262 private template computeFirstDefaultArg(args...)
263 {
264 	private enum result = computeFirstDefaultArgImpl!(0, args);
265 	enum computeFirstDefaultArg = result == args.length? -1 : result;
266 }
267 
268 private template commandParameterTypeName(T)
269 {
270 	static if(is(T : const(char)[]) || is(T== string[]))
271 		enum commandParameterTypeName = "string";
272 	else static if(isIntegral!T)
273 	{
274 		static if(isSigned!T)
275 			enum commandParameterTypeName = "integer";
276 		else
277 			enum commandParameterTypeName = "positive integer";
278 	}
279 	else static if(isFloatingPoint!T)
280 		enum commandParameterTypeName = "number";
281 	else static if(is(T == dchar))
282 		enum commandParameterTypeName = "character";
283 	else
284 		static assert(false);
285 }
286 
287 private template isValidCommandParameterType(T)
288 {
289 	enum isValidCommandParameterType =
290 		(!is(T == char[]) && is(T : const(char)[])) ||
291 		isIntegral!T ||
292 		isFloatingPoint!T ||
293 		is(T == dchar);
294 }
295 
296 private T parseCommandArgument(T)(string strArg, string cmdName, size_t argNum)
297 {
298 	import std.conv : ConvException, parse;
299 
300 	auto makeError(Exception cause)
301 	{
302 		auto msg = format("expected " ~ commandParameterTypeName!T ~ " for argument #%s of command \"%s\", not `%s`",
303 			argNum,
304 			cmdName,
305 			strArg);
306 
307 		return new CommandArgumentException(msg, __FILE__, __LINE__, cause);
308 	}
309 
310 	auto parsedArg = strArg;
311 	T arg;
312 	try arg = parsedArg.parse!T();
313 	catch(ConvException e)
314 	{
315 		throw makeError(e);
316 	}
317 
318 	enforce(parsedArg.empty, makeError(null));
319 
320 	return arg;
321 }
322 
323 private T parseCommandArgument(T : const(char)[])(string strArg, string cmdName, size_t argNum)
324 {
325 	return strArg;
326 }
327 
328 private T parseCommandArgument(T : const(char[])[])(string strArg, string cmdName, size_t argNum)
329 {
330 	import std.array : split;
331 	return strArg.split();
332 }
333 
334 /**
335  * Basic interface of all command sets.
336  */
337 interface ICommandSet
338 {
339 	/// Human-readable categorical name for the
340 	/// commands in the set.
341 	/// See_Also: $(DPREF attribute, _category)
342 	string category() @property @safe pure nothrow;
343 
344 	/**
345 	 * Context for the currently executing command.
346 	 * See_Also:
347 	 *    $(DPREF _context, Context)
348 	 */
349 	ref Context context() @property @safe pure nothrow;
350 
351 	/**
352 	 * Add the command cmd to the command set.
353 	 */
354 	void add(ref Command cmd);
355 
356 	/**
357 	 * Lookup a command in this command set by one of its names.
358 	 */
359 	Command* getCommand(in char[] cmdName);
360 
361 	/// Sorted list of the primary names of all commands in the
362 	/// command set.
363 	string[] commandNames() @property @safe;
364 }
365 
366 /**
367  * Base class for command sets.
368  *
369  * Commands are implemented by adding public, non-static methods
370  * to the derived class. Non-public or static methods of the derived
371  * class are ignored, as well as methods with the
372  * $(DPREF attribute, ignore) attribute.
373  *
374  * The name of the method becomes the primary name
375  * through which the command is invoked in chat. Other names may be added
376  * by tagging the method with the $(D @aliases) ($(DPREF attribute, aliases))
377  * command attribute. When the command is invoked through one of its
378  * names, the method is called.
379  *
380  * Commands are invoked by sending a message to a channel the bot
381  * is a member of, where the message starts with the bot's command prefix
382  * followed by the name of the command to invoke. Whitespace-separated words
383  * following the command name are parsed as arguments to the command.
384  *
385  * The arguments to the chat command map one-to-one to the parameters
386  * of the method. The method's allowed parameter types are:
387  * const or immutable UTF-8 strings, integers and floating point numbers.
388  *
389  * If the method's last parameter type is a string, then it is passed all the
390  * text passed in chat after the previous arguments, including whitespace.
391  *
392  * If the method's last parameter is an array of strings, then the method
393  * must also be marked typesafe-variadic; the array is filled with all
394  * whitespace-separated arguments passed after arguments to preceding
395  * parameters. If no such arguments are passed, the array is empty.
396  *
397  * Parameters may have default arguments. If a command invocation does not
398  * pass an argument to a parameter with a default argument, the default
399  * argument is used instead.
400  *
401  * If an argument is not passed to a parameter without a default argument,
402  * or a non-integer is passed to an integer parameter or a non-number is
403  * passed to a floating point parameter, then the bot replies with
404  * an error message and the command method is not called.
405  *
406  * See $(DPMODULE attribute) for a list of attributes that can be attached
407  * to command methods to alter the behaviour of the command.
408  *
409  * This type subtypes a context object ($(DPREF context, Context)) that
410  * provides contextual operations and information for
411  * use by command method implementations.
412  *
413  * Params:
414  *    T = type with command implementation methods. Must be the derived class
415  */
416 abstract class CommandSet(T) : ICommandSet
417 {
418 	private:
419 	Command[string] _commands;
420 	string[] _commandNames; // TODO: Not a static, immutable property because of template bugs (2.063)
421 	Context _context;
422 
423 	final void registerCommands(T cmdSet)
424 	{
425 		foreach(memberName; __traits(derivedMembers, T))
426 		{
427 			static if(
428 				memberName != "__ctor" &&
429 				memberName != "__dtor" &&
430 				__traits(compiles, __traits(getMember, T, memberName)) && // ahem...
431 				__traits(getProtection, __traits(getMember, T, memberName)) == "public" &&
432 				isSomeFunction!(mixin("T." ~ memberName)) &&
433 				!__traits(isStaticFunction, mixin("T." ~ memberName)) &&
434 				!hasAttribute!(mixin("T." ~ memberName), ignore))
435 			{
436 				auto dg = &mixin("cmdSet." ~ memberName);
437 				auto cmd = Command.create!(mixin("T." ~ memberName))(dg);
438 				add(cmd);
439 			}
440 		}
441 	}
442 
443 	public:
444 	this()
445 	{
446 		auto cmdSet = enforce(cast(T)this);
447 		registerCommands(cmdSet);
448 	}
449 
450 	// See CommandContext
451 	//alias context this;
452 
453 	override: // Implement ICommandSet
454 	ref Context context()
455 	{
456 		return _context;
457 	}
458 
459 	string category()
460 	{
461 		static if(hasAttribute!(T, .category))
462 			return getAttribute!(T, .category).value;
463 		else
464 			return null;
465 	}
466 
467 	void add(ref Command cmd)
468 	{
469 		import std.range : assumeSorted, chain;
470 		import irc.util : values;
471 
472 		foreach(name; values(cmd.name).chain(cmd.aliases))
473 			_commands[name] = cmd;
474 
475 		auto pos = _commandNames.assumeSorted().lowerBound(cmd.name).length;
476 		_commandNames.insertInPlace(pos, cmd.name);
477 	}
478 
479 	Command* getCommand(in char[] cmdName)
480 	{
481 		return cmdName in _commands;
482 	}
483 
484 	string[] commandNames()
485 	{
486 		return _commandNames;
487 	}
488 }
489 
490 /**
491  * Temporary workaround for compiler bugs as of DMD front-end version 2.063.
492  * This mixin template must be mixed into deriviate classes of $(MREF CommandSet).
493  */
494 // TODO: Temporary, figure out various bugs
495 mixin template CommandContext()
496 {
497 	alias context this;
498 }