1 /++
2 + Module for parsing IRC mode strings.
3 +/
4 module virc.modes;
5 
6 import std.algorithm : among, splitter;
7 import std.range.primitives : isInputRange, isOutputRange;
8 import std.range : put;
9 import std.typecons : Nullable, Tuple;
10 
11 
12 /++
13  + IRC modes. These are settings for channels and users on an IRC network,
14  + responsible for things ranging from user bans, flood control and colour
15  + stripping to registration status.
16  + Consists of a single character and (often) an argument string to go along
17  + with it.
18  +/
19 struct Mode {
20 	///
21 	ModeType type = ModeType.d;
22 	///
23 	char mode;
24 	///
25 	Nullable!string arg;
26 	invariant() {
27 		assert((type != ModeType.d) || arg.isNull);
28 	}
29 	///
30 	auto opEquals(Mode b) const {
31 		return (mode == b.mode);
32 	}
33 	///
34 	auto toHash() const {
35 		return mode.hashOf;
36 	}
37 }
38 @safe pure nothrow /+@nogc+/ unittest {
39 	assert(Mode(ModeType.d, 'a').toHash == Mode(ModeType.d, 'a').toHash);
40 }
41 /++
42 + Mode classification.
43 +/
44 enum ModeType {
45 	///Adds/removes nick/address to a list. always has a parameter.
46 	a,
47 	///Mode that changes a setting and always has a parameter.
48 	b,
49 	///Mode that changes a setting and only has a parameter when set.
50 	c ,
51 	///Mode that changes a setting and never has a parameter.
52 	d
53 }
54 /++
55 + Whether a mode was set or unset.
56 +/
57 enum Change {
58 	///Mode was set.
59 	set,
60 	///Mode was unset.
61 	unset
62 }
63 
64 /++
65 + Full metadata associated with a mode change.
66 +/
67 struct ModeChange {
68 	///
69 	Mode mode;
70 	///
71 	Change change;
72 	void toString(T)(T sink) const if (isOutputRange!(T, const(char))) {
73 		final switch(change) {
74 			case Change.set:
75 				put(sink, '+');
76 				break;
77 			case Change.unset:
78 				put(sink, '-');
79 				break;
80 		}
81 		put(sink, mode.mode);
82 	}
83 }
84 
85 /++
86 + Parse a mode string into individual mode changes.
87 +/
88 auto parseModeString(T)(T input, ModeType[char] channelModeTypes) if (isInputRange!T) {
89 	ModeChange[] changes;
90 	bool unsetMode = false;
91 	auto modeList = input.front;
92 	input.popFront();
93 	foreach (mode; modeList) {
94 		if (mode == '+') {
95 			unsetMode = false;
96 		} else if (mode == '-') {
97 			unsetMode = true;
98 		} else {
99 			if (unsetMode) {
100 				auto modeType = mode in channelModeTypes ? channelModeTypes[mode] : ModeType.d;
101 				if (modeType.among(ModeType.a, ModeType.b)) {
102 					if (input.empty) {
103 						changes = [];
104 						break;
105 					}
106 					auto arg = input.front;
107 					input.popFront();
108 					changes ~= ModeChange(Mode(modeType, mode, Nullable!string(arg)), Change.unset);
109 				} else {
110 					changes ~= ModeChange(Mode(modeType, mode), Change.unset);
111 				}
112 			} else {
113 				auto modeType = mode in channelModeTypes ? channelModeTypes[mode] : ModeType.d;
114 				if (modeType.among(ModeType.a, ModeType.b, ModeType.c)) {
115 					if (input.empty) {
116 						changes = [];
117 						break;
118 					}
119 					auto arg = input.front;
120 					input.popFront();
121 					changes ~= ModeChange(Mode(modeType, mode, Nullable!string(arg)), Change.set);
122 				} else {
123 					changes ~= ModeChange(Mode(modeType, mode), Change.set);
124 				}
125 			}
126 		}
127 	}
128 	return changes;
129 }
130 ///ditto
131 auto parseModeString(string input, ModeType[char] channelModeTypes) {
132 	return parseModeString(input.splitter(" "), channelModeTypes);
133 }
134 ///
135 @safe pure nothrow unittest {
136 	import std.algorithm : canFind, filter, map;
137 	import std.range : empty;
138 	{
139 		const testParsed = parseModeString("+s", null);
140 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 's')));
141 		assert(testParsed.filter!(x => x.change == Change.unset).empty);
142 	}
143 	{
144 		const testParsed = parseModeString("-s", null);
145 		assert(testParsed.filter!(x => x.change == Change.set).empty);
146 		assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's')));
147 	}
148 	{
149 		const testParsed = parseModeString("+s-n", null);
150 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 's')));
151 		assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 'n')));
152 	}
153 	{
154 		const testParsed = parseModeString("-s+n", null);
155 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'n')));
156 		assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's')));
157 	}
158 	{
159 		const testParsed = parseModeString("+kp secret", ['k': ModeType.b]);
160 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'p')));
161 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.b, 'k', Nullable!string("secret"))));
162 	}
163 	{
164 		const testParsed = parseModeString("+kp secret", null);
165 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'p')));
166 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'k')));
167 	}
168 	{
169 		const testParsed = parseModeString("-s+nk secret", ['k': ModeType.b]);
170 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'n')));
171 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.b, 'k', Nullable!string("secret"))));
172 		assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's')));
173 	}
174 	{
175 		const testParsed = parseModeString("-sk+nl secret 4", ['k': ModeType.b, 'l': ModeType.c]);
176 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'n')));
177 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.b, 'l', Nullable!string("4"))));
178 		assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.b, 'k', Nullable!string("secret"))));
179 		assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's')));
180 	}
181 	{
182 		const testParsed = parseModeString("-s+nl 3333", ['l': ModeType.c]);
183 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 'n')));
184 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.c, 'l', Nullable!string("3333"))));
185 		assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 's')));
186 	}
187 	{
188 		const testParsed = parseModeString("+s-nl", ['l': ModeType.c]);
189 		assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.d, 'n')));
190 		assert(testParsed.filter!(x => x.change == Change.unset).map!(x => x.mode).canFind(Mode(ModeType.c, 'l')));
191 		assert(testParsed.filter!(x => x.change == Change.set).map!(x => x.mode).canFind(Mode(ModeType.d, 's')));
192 	}
193 	{
194 		const testParsed = parseModeString("+kp", ['k': ModeType.b]);
195 		assert(testParsed.empty);
196 	}
197 	{
198 		const testParsed = parseModeString("-kp", ['k': ModeType.b]);
199 		assert(testParsed.empty);
200 	}
201 }
202 
203 auto toModeStringLazy(ModeChange[] changes) {
204 	static struct Result {
205 		ModeChange[] changes;
206 		void toString(S)(ref S sink) {
207 			import std.algorithm : joiner, map;
208 			import std.format : formattedWrite;
209 			import std.range : put;
210 			Nullable!Change last;
211 			foreach(change; changes) {
212 				if (last.isNull || (change.change != last)) {
213 					put(sink, change.change == Change.set ? '+' : '-');
214 					last = change.change;
215 				}
216 				put(sink, change.mode.mode);
217 			}
218 			sink.formattedWrite!"%-( %s%)"(changes.map!(x => x.mode.arg).joiner);
219 		}
220 	}
221 	return Result(changes);
222 }
223 
224 @safe pure unittest {
225 	import std.conv : text;
226 	assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.set)]).text == "+s");
227 	assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.unset)]).text == "-s");
228 	assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.set), ModeChange(Mode(ModeType.d, 's'), Change.unset)]).text == "+s-s");
229 	assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.set), ModeChange(Mode(ModeType.b, 'k', Nullable!string("pass")), Change.set)]).text == "+sk pass");
230 	assert(toModeStringLazy([ModeChange(Mode(ModeType.d, 's'), Change.set), ModeChange(Mode(ModeType.b, 'k', Nullable!string("pass")), Change.set), ModeChange(Mode(ModeType.c, 'l', Nullable!string("3")), Change.set)]).text == "+skl pass 3");
231 }
232 
233 string toModeString(ModeChange[] changes) @safe pure {
234 	import std.conv : text;
235 	return changes.toModeStringLazy().text;
236 }