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 = &sections[$-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 }