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 */