1 /**
2 	Defines a string based multi-map with conserved insertion order.
3 
4 	Copyright: © 2012-2014 RejectedSoftware e.K.
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vutil.dictionarylist;
9 
10 import vutil.array : removeFromArrayIdx;
11 import vutil..string : icmp2;
12 import std.exception : enforce;
13 
14 
15 /**
16 	Behaves similar to $(D VALUE[string]) but the insertion order is not changed
17 	and multiple values per key are supported.
18 
19 	This kind of map is used for MIME headers (e.g. for HTTP, see
20 	vibe.inet.message.InetHeaderMap), or for form data
21 	(vibe.inet.webform.FormFields). Note that the map can contain fields with
22 	the same key multiple times if addField is used for insertion. Insertion
23 	order is preserved.
24 
25 	Note that despite case not being relevant for matching keyse, iterating
26 	over the map will yield	the original case of the key that was put in.
27 
28 	Insertion and lookup has O(n) complexity.
29 */
30 struct DictionaryList(VALUE, bool case_sensitive = true, size_t NUM_STATIC_FIELDS = 32) {
31 	import std.typecons : Tuple;
32 
33 	private {
34 		static struct Field { uint keyCheckSum; string key; VALUE value; }
35 		Field[NUM_STATIC_FIELDS] m_fields;
36 		size_t m_fieldCount = 0;
37 		Field[] m_extendedFields;
38 		static char[256] s_keyBuffer;
39 	}
40 	
41 	alias ValueType = VALUE;
42 
43 	struct FieldTuple { string key; ValueType value; }
44 
45 	/** The number of fields present in the map.
46 	*/
47 	@property size_t length() const { return m_fieldCount + m_extendedFields.length; }
48 
49 	/// Supports serialization using vson.serialization.
50 	static DictionaryList fromRepresentation(FieldTuple[] array)
51 	{
52 		DictionaryList ret;
53 		foreach (ref v; array) ret.addField(v.key, v.value);
54 		return ret;
55 	}
56 	/// ditto
57 	FieldTuple[] toRepresentation() {
58 		FieldTuple[] ret;
59 		foreach (k, ref v; this) ret ~= FieldTuple(k, v);
60 		return ret;
61 	}
62 
63 	/** Removes the first field that matches the given key.
64 	*/
65 	void remove(string key)
66 	{
67 		auto keysum = computeCheckSumI(key);
68 		auto idx = getIndex(m_fields[0 .. m_fieldCount], key, keysum);
69 		if( idx >= 0 ){
70 			auto slice = m_fields[0 .. m_fieldCount];
71 			removeFromArrayIdx(slice, idx);
72 			m_fieldCount--;
73 		} else {
74 			idx = getIndex(m_extendedFields, key, keysum);
75 			enforce(idx >= 0);
76 			removeFromArrayIdx(m_extendedFields, idx);
77 		}
78 	}
79 
80 	/** Removes all fields that matches the given key.
81 	*/
82 	void removeAll(string key)
83 	{
84 		auto keysum = computeCheckSumI(key);
85 		for (size_t i = 0; i < m_fieldCount;) {
86 			if (m_fields[i].keyCheckSum == keysum && matches(m_fields[i].key, key)) {
87 				auto slice = m_fields[0 .. m_fieldCount];
88 				removeFromArrayIdx(slice, i);
89 				m_fieldCount--;
90 			} else i++;
91 		}
92 
93 		for (size_t i = 0; i < m_extendedFields.length;) {
94 			if (m_fields[i].keyCheckSum == keysum && matches(m_fields[i].key, key))
95 				removeFromArrayIdx(m_extendedFields, i);
96 			else i++;
97 		}
98 	}
99 
100 	/** Adds a new field to the map.
101 
102 		The new field will be added regardless of any existing fields that
103 		have the same key, possibly resulting in duplicates. Use opIndexAssign
104 		if you want to avoid duplicates.
105 	*/
106 	void addField(string key, ValueType value)
107 	{
108 		auto keysum = computeCheckSumI(key);
109 		if (m_fieldCount < m_fields.length)
110 			m_fields[m_fieldCount++] = Field(keysum, key, value);
111 		else m_extendedFields ~= Field(keysum, key, value);
112 	}
113 
114 	/** Returns the first field that matches the given key.
115 
116 		If no field is found, def_val is returned.
117 	*/
118 	inout(ValueType) get(string key, lazy inout(ValueType) def_val = ValueType.init)
119 	inout {
120 		if (auto pv = key in this) return *pv;
121 		return def_val;
122 	}
123 
124 	/** Returns all values matching the given key.
125 
126 		Note that the version returning an array will allocate for each call.
127 	*/
128 	const(ValueType)[] getAll(string key)
129 	const {
130 		import std.array;
131 		auto ret = appender!(const(ValueType)[])();
132 		getAll(key, (v) { ret.put(v); });
133 		return ret.data;
134 	}
135 	/// ditto
136 	void getAll(string key, scope void delegate(const(ValueType)) del)
137 	const {
138 		uint keysum = computeCheckSumI(key);
139 		foreach (ref f; m_fields[0 .. m_fieldCount]) {
140 			if (f.keyCheckSum != keysum) continue;
141 			if (matches(f.key, key)) del(f.value);
142 		}
143 		foreach (ref f; m_extendedFields) {
144 			if (f.keyCheckSum != keysum) continue;
145 			if (matches(f.key, key)) del(f.value);
146 		}
147 	}
148 
149 	/** Returns the first value matching the given key.
150 	*/
151 	inout(ValueType) opIndex(string key)
152 	inout {
153 		auto pitm = key in this;
154 		enforce(pitm !is null, "Accessing non-existent key '"~key~"'.");
155 		return *pitm;
156 	}
157 	
158 	/** Adds or replaces the given field with a new value.
159 	*/
160 	ValueType opIndexAssign(ValueType val, string key)
161 	{
162 		auto pitm = key in this;
163 		if( pitm ) *pitm = val;
164 		else if( m_fieldCount < m_fields.length ) m_fields[m_fieldCount++] = Field(computeCheckSumI(key), key, val);
165 		else m_extendedFields ~= Field(computeCheckSumI(key), key, val);
166 		return val;
167 	}
168 
169 	/** Returns a pointer to the first field that matches the given key.
170 	*/
171 	inout(ValueType)* opBinaryRight(string op)(string key) inout if(op == "in") {
172 		uint keysum = computeCheckSumI(key);
173 		auto idx = getIndex(m_fields[0 .. m_fieldCount], key, keysum);
174 		if( idx >= 0 ) return &m_fields[idx].value;
175 		idx = getIndex(m_extendedFields, key, keysum);
176 		if( idx >= 0 ) return &m_extendedFields[idx].value;
177 		return null;
178 	}
179 	/// ditto
180 	bool opBinaryRight(string op)(string key) inout if(op == "!in") {
181 		return !(key in this);
182 	}
183 
184 	/** Iterates over all fields, including duplicates.
185 	*/
186 	int opApply(scope int delegate(string key, ref ValueType val) del)
187 	{
188 		foreach (ref kv; m_fields[0 .. m_fieldCount]) {
189 			if (auto ret = del(kv.key, kv.value))
190 				return ret;
191 		}
192 		foreach (ref kv; m_extendedFields) {
193 			if (auto ret = del(kv.key, kv.value))
194 				return ret;
195 		}
196 		return 0;
197 	}
198 
199 	/// ditto
200 	int opApply(scope int delegate(ref ValueType val) del)
201 	{
202 		return this.opApply((string key, ref ValueType val) { return del(val); });
203 	}
204 
205 	/// ditto
206 	int opApply(scope int delegate(string key, ref const(ValueType) val) del) const
207 	{
208 		return (cast() this).opApply(cast(int delegate(string, ref ValueType)) del);
209 	}
210 
211 	/// ditto
212 	int opApply(scope int delegate(ref const(ValueType) val) del) const
213 	{
214 		return (cast() this).opApply(cast(int delegate(ref ValueType)) del);
215 	}
216 
217 	static if (is(typeof({ const(ValueType) v; ValueType w; w = v; }))) {
218 		/** Duplicates the header map.
219 		*/
220 		@property DictionaryList dup()
221 		const {
222 			DictionaryList ret;
223 			ret.m_fields[0 .. m_fieldCount] = m_fields[0 .. m_fieldCount];
224 			ret.m_fieldCount = m_fieldCount;
225 			ret.m_extendedFields = m_extendedFields.dup;
226 			return ret;
227 		}
228 	}
229 
230 	private ptrdiff_t getIndex(in Field[] map, string key, uint keysum)
231 	const {
232 		foreach (i, ref const(Field) entry; map) {
233 			if (entry.keyCheckSum != keysum) continue;
234 			if (matches(entry.key, key)) return i;
235 		}
236 		return -1;
237 	}
238 
239 	private static bool matches(string a, string b)
240 	{
241 		static if (case_sensitive) return a == b;
242 		else return icmp2(a, b) == 0;
243 	}
244 	
245 	// very simple check sum function with a good chance to match
246 	// strings with different case equal
247 	private static uint computeCheckSumI(string s)
248 	@trusted {
249 		uint csum = 0;
250 		immutable(char)* pc = s.ptr, pe = s.ptr + s.length;
251 		for (; pc != pe; pc++) {
252 			static if (case_sensitive) csum ^= *pc;
253 			else csum ^= *pc & 0x1101_1111;
254 			csum = (csum << 1) | (csum >> 31);
255 		}
256 		return csum;
257 	}
258 }
259 
260 unittest {
261 	DictionaryList!(int, true) a;
262 	a.addField("a", 1);
263 	a.addField("a", 2);
264 	assert(a["a"] == 1);
265 	assert(a.getAll("a") == [1, 2]);
266 	a["a"] = 3;
267 	assert(a["a"] == 3);
268 	assert(a.getAll("a") == [3, 2]);
269 	a.removeAll("a");
270 	assert(a.getAll("a").length == 0);
271 	assert(a.get("a", 4) == 4);
272 	a.addField("b", 2);
273 	a.addField("b", 1);
274 	a.remove("b");
275 	assert(a.getAll("b") == [1]);
276 
277 	DictionaryList!(int, false) b;
278 	b.addField("a", 1);
279 	b.addField("A", 2);
280 	assert(b["A"] == 1);
281 	assert(b.getAll("a") == [1, 2]);
282 }
283 
284 /*
285 unittest {
286 	import vson.json;
287 	import vson.serialization;
288 
289 	static assert(isCustomSerializable!(DictionaryList!int));
290 
291 	DictionaryList!(int, false) b;
292 	b.addField("a", 1);
293 	b.addField("A", 2);
294 	auto app = appender!string();
295 	serializeToJson(app, b);
296 	assert(app.data == `[{"key":"a","value":1},{"key":"A","value":2}]`, app.data);
297 
298 	DictionaryList!(int, true, 2) c;
299 	c.addField("a", 1);
300 	c.addField("b", 2);
301 	c.addField("a", 3);
302 	c.remove("b");
303 	auto appc = appender!string();
304 	serializeToJson(appc, c);
305 	assert(appc.data == `[{"key":"a","value":1},{"key":"a","value":3}]`, appc.data);
306 }
307 */