1 /** 2 * Structured INI 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.sd.sini; 15 16 import std.exception; 17 import std.range; 18 import std.string; 19 import std.traits; 20 21 import ae.utils.meta.binding; 22 import ae.utils.meta.reference; 23 24 struct IniParser(R) 25 { 26 static void setValue(S, Sink)(S[] segments, S value, Sink sink) 27 { 28 if (segments.length == 0) 29 sink.handleString(value); 30 else 31 { 32 struct Reader 33 { 34 // https://d.puremagic.com/issues/show_bug.cgi?id=12318 35 void dummy() {} 36 37 void read(Sink)(Sink sink) 38 { 39 setValue(segments[1..$], value, sink); 40 } 41 } 42 Reader reader; 43 sink.traverse(segments[0], boundFunctorOf!(Reader.read)(&reader)); 44 } 45 } 46 47 static S readSection(R, S, Sink)(ref R r, S[] segments, Sink sink) 48 { 49 if (segments.length) 50 { 51 struct Reader 52 { 53 // https://d.puremagic.com/issues/show_bug.cgi?id=12318 54 void dummy() {} 55 56 S read(Sink)(Sink sink) 57 { 58 return readSection(r, segments[1..$], sink); 59 } 60 } 61 Reader reader; 62 return sink.traverse(segments[0], boundFunctorOf!(Reader.read)(&reader)); 63 } 64 65 while (!r.empty) 66 { 67 auto line = r.front.chomp().stripLeft(); 68 69 scope(success) r.popFront(); 70 if (line.empty) 71 continue; 72 if (line[0] == '#' || line[0] == ';') 73 continue; 74 75 if (line.startsWith('[')) 76 { 77 line = line.stripRight(); 78 enforce(line[$-1] == ']', "Malformed section line (no ']')"); 79 return line[1..$-1]; 80 } 81 82 auto pos = line.indexOf('='); 83 enforce(pos > 0, "Malformed value line (no '=')"); 84 auto name = line[0..pos].strip; 85 segments = name.split("."); 86 enforce(segments.length, "Malformed value line (empty name)"); 87 setValue(segments, line[pos+1..$].strip, sink); 88 } 89 return null; 90 } 91 92 void parseIni(R, Sink)(R r, Sink sink) 93 { 94 auto nextSection = readSection(r, typeof(r.front)[].init, sink); 95 96 while (nextSection) 97 nextSection = readSection(r, nextSection.split("."), sink); 98 } 99 } 100 101 /// Parse a structured INI from a range of lines, into a user-defined struct. 102 T parseIni(T, R)(R r) 103 if (isInputRange!R && isSomeString!(ElementType!R)) 104 { 105 import ae.utils.sd.sd; 106 107 T result; 108 auto parser = IniParser!R(); 109 parser.parseIni(r, deserializer(&result)); 110 return result; 111 } 112 113 unittest 114 { 115 static struct File 116 { 117 struct S 118 { 119 string n1, n2; 120 int[string] a; 121 } 122 S s; 123 } 124 125 auto f = parseIni!File 126 ( 127 q"< 128 s.n1=v1 129 s.a.foo=1 130 [s] 131 n2=v2 132 a.bar=2 133 >".splitLines() 134 ); 135 136 assert(f.s.n1=="v1"); 137 assert(f.s.n2=="v2"); 138 assert(f.s.a==["foo":1, "bar":2]); 139 } 140 141 unittest 142 { 143 import ae.utils.sd.sd; 144 import std.conv; 145 146 static struct Custom 147 { 148 struct Section 149 { 150 string name; 151 string[string] values; 152 } 153 Section[] sections; 154 155 enum isSerializationSink = true; 156 157 auto traverse(Reader)(wstring name, Reader reader) 158 { 159 sections.length++; 160 auto p = §ions[$-1]; 161 p.name = to!string(name); 162 return reader(deserializer(&p.values)); 163 } 164 165 void handleString(S)(S s) { assert(false); } 166 } 167 168 auto c = parseIni!Custom 169 ( 170 q"< 171 [one] 172 a=a 173 [two] 174 b=b 175 >"w.splitLines() 176 ); 177 178 assert(c == Custom([Custom.Section("one", ["a" : "a"]), Custom.Section("two", ["b" : "b"])])); 179 } 180 181 // *************************************************************************** 182 183 /// Convenience function to load a struct from an INI file. 184 /// Returns .init if the file does not exist. 185 S loadIni(S)(string fileName) 186 { 187 S s; 188 189 import std.file; 190 if (fileName.exists) 191 s = fileName 192 .readText() 193 .splitLines() 194 .parseIni!S(); 195 196 return s; 197 }