nitrpg: Move `nitrpg` to its own repository
[nit.git] / lib / ini / ini.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Handle ini config files.
16 module ini
17
18 # A configuration tree that can read and store data in ini format
19 #
20 # Write example:
21 #
22 # var config = new ConfigTree("config.ini")
23 # config["goo"] = "goo"
24 # config["foo.bar"] = "foobar"
25 # config["foo.baz"] = "foobaz"
26 # config.save
27 # assert config.to_map.length == 3
28 #
29 # Read example:
30 #
31 # config = new ConfigTree("config.ini")
32 # assert config.has_key("foo.bar")
33 # assert config["foo.bar"] == "foobar"
34 class ConfigTree
35 super Writable
36
37 # The ini file used to read/store data
38 var ini_file: String
39
40 init do if ini_file.file_exists then load
41
42 # Get the config value for `key`
43 #
44 # var config = new ConfigTree("config.ini")
45 # assert config["goo"] == "goo"
46 # assert config["foo.bar"] == "foobar"
47 # assert config["foo.baz"] == "foobaz"
48 # assert config["fail.fail"] == null
49 fun [](key: String): nullable String do
50 var node = get_node(key)
51 if node == null then return null
52 return node.value
53 end
54
55 # Get the config values under `key`
56 #
57 # var config = new ConfigTree("config.ini")
58 # var values = config.at("foo")
59 # assert values.has_key("bar")
60 # assert values.has_key("baz")
61 # assert not values.has_key("goo")
62 #
63 # Return null if the key does not exists.
64 #
65 # assert config.at("fail.fail") == null
66 fun at(key: String): nullable Map[String, String] do
67 var node = get_node(key)
68 if node == null then return null
69 var map = new HashMap[String, String]
70 for k, child in node.children do
71 var value = child.value
72 if value == null then continue
73 map[k] = value
74 end
75 return map
76 end
77
78 # Set `value` at `key`
79 #
80 # var config = new ConfigTree("config.ini")
81 # assert config["foo.bar"] == "foobar"
82 # config["foo.bar"] = "baz"
83 # assert config["foo.bar"] == "baz"
84 fun []=(key: String, value: nullable String) do
85 set_node(key, value)
86 end
87
88 # Is `key` in the config?
89 #
90 # var config = new ConfigTree("config.ini")
91 # assert config.has_key("goo")
92 # assert config.has_key("foo.bar")
93 # assert not config.has_key("zoo")
94 fun has_key(key: String): Bool do
95 var parts = key.split(".").reversed
96 var node = get_root(parts.pop)
97 if node == null then return false
98 while not parts.is_empty do
99 node = node.get_child(parts.pop)
100 if node == null then return false
101 end
102 return true
103 end
104
105 # Get `self` as a Map of `key`, `value`
106 #
107 # var config = new ConfigTree("config.ini")
108 # var map = config.to_map
109 # assert map.has_key("goo")
110 # assert map.has_key("foo.bar")
111 # assert map.has_key("foo.baz")
112 # assert map.length == 3
113 fun to_map: Map[String, String] do
114 var map = new HashMap[String, String]
115 for node in leaves do
116 var value = node.value
117 if value == null then continue
118 map[node.key] = value
119 end
120 return map
121 end
122
123 redef fun to_s do return to_map.join(", ", ":")
124
125 # Write `self` in `stream`
126 #
127 # var config = new ConfigTree("config.ini")
128 # var out = new StringWriter
129 # config.write_to(out)
130 # assert out.to_s == """
131 # goo=goo
132 # [foo]
133 # bar=foobar
134 # baz=foobaz
135 # """
136 redef fun write_to(stream) do
137 var todo = new Array[ConfigNode].from(roots.reversed)
138 while not todo.is_empty do
139 var node = todo.pop
140 if node.children.not_empty then
141 todo.add_all node.children.values.to_a.reversed
142 end
143 if node.children.not_empty and node.parent == null then
144 stream.write("[{node.name}]\n")
145 end
146 var value = node.value
147 if value == null then continue
148 var path = node.path
149 if path.length > 1 then path.shift
150 stream.write("{path.join(".")}={value}\n")
151 end
152 end
153
154 # Reload config from file
155 # Done automatically at init
156 #
157 # Example with hierarchical ini file:
158 #
159 # # init file
160 # var str = """
161 # foo.bar=foobar
162 # foo.baz=foobaz
163 # goo=goo"""
164 # str.write_to_file("config1.ini")
165 # # load file
166 # var config = new ConfigTree("config1.ini")
167 # assert config["foo.bar"] == "foobar"
168 #
169 # Example with sections:
170 #
171 # # init file
172 # str = """
173 # goo=goo
174 # [foo]
175 # bar=foobar
176 # baz=foobaz
177 # [boo]
178 # bar=boobar"""
179 # str.write_to_file("config2.ini")
180 # # load file
181 # config = new ConfigTree("config2.ini")
182 # assert config["foo.bar"] == "foobar"
183 # assert config["boo.bar"] == "boobar"
184 #
185 # Example with both hierarchy and section:
186 #
187 # # init file
188 # str = """
189 # goo=goo
190 # [foo]
191 # bar.baz=foobarbaz
192 # [goo.boo]
193 # bar=gooboobar
194 # baz.bar=gooboobazbar"""
195 # str.write_to_file("config3.ini")
196 # # load file
197 # config = new ConfigTree("config3.ini")
198 # assert config["goo"] == "goo"
199 # assert config["foo.bar.baz"] == "foobarbaz"
200 # assert config["goo.boo.bar"] == "gooboobar"
201 # assert config["goo.boo.baz.bar"] == "gooboobazbar"
202 #
203 # Using the array notation
204 #
205 # str = """
206 # foo[]=a
207 # foo[]=b
208 # foo[]=c"""
209 # str.write_to_file("config4.ini")
210 # # load file
211 # config = new ConfigTree("config4.ini")
212 # print config.to_map.join(":", ",")
213 # assert config["foo.0"] == "a"
214 # assert config["foo.1"] == "b"
215 # assert config["foo.2"] == "c"
216 # assert config.at("foo").values.join(",") == "a,b,c"
217 fun load do
218 roots.clear
219 var stream = new FileReader.open(ini_file)
220 var path: nullable String = null
221 var line_number = 0
222 while not stream.eof do
223 var line = stream.read_line
224 line_number += 1
225 if line.is_empty then
226 continue
227 else if line.has_prefix(";") then
228 continue
229 else if line.has_prefix("[") then
230 line = line.trim
231 var key = line.substring(1, line.length - 2)
232 path = key
233 set_node(path, null)
234 else
235 var parts = line.split_once_on("=")
236 if parts.length == 1 then
237 continue
238 end
239 var key = parts[0].trim
240 var val = parts[1].trim
241 if path != null then key = "{path}.{key}"
242 if key.has_suffix("[]") then
243 set_array(key, val)
244 else
245 set_node(key,val)
246 end
247 end
248 end
249 stream.close
250 end
251
252 # Save config to file
253 fun save do write_to_file(ini_file)
254
255 private var roots = new Array[ConfigNode]
256
257 # Append `value` to array at `key`
258 private fun set_array(key: String, value: nullable String) do
259 key = key.substring(0, key.length - 2)
260 var len = 0
261 var node = get_node(key)
262 if node != null then len = node.children.length
263 set_node("{key}.{len.to_s}", value)
264 end
265
266 private fun set_node(key: String, value: nullable String) do
267 var parts = key.split(".").reversed
268 var k = parts.pop
269 var root = get_root(k)
270 if root == null then
271 root = new ConfigNode(k)
272 if parts.is_empty then
273 root.value = value
274 end
275 roots.add root
276 end
277 while not parts.is_empty do
278 k = parts.pop
279 var node = root.get_child(k)
280 if node == null then
281 node = new ConfigNode(k)
282 node.parent = root
283 root.children[node.name] = node
284 end
285 if parts.is_empty then
286 node.value = value
287 end
288 root = node
289 end
290 end
291
292 private fun get_node(key: String): nullable ConfigNode do
293 var parts = key.split(".").reversed
294 var node = get_root(parts.pop)
295 while node != null and not parts.is_empty do
296 node = node.get_child(parts.pop)
297 end
298 return node
299 end
300
301 private fun get_root(name: String): nullable ConfigNode do
302 for root in roots do
303 if root.name == name then return root
304 end
305 return null
306 end
307
308 private fun leaves: Array[ConfigNode] do
309 var res = new Array[ConfigNode]
310 var todo = new Array[ConfigNode]
311 todo.add_all roots
312 while not todo.is_empty do
313 var node = todo.pop
314 if node.children.is_empty then
315 res.add node
316 else
317 todo.add_all node.children.values
318 end
319 end
320 return res
321 end
322 end
323
324 private class ConfigNode
325
326 var parent: nullable ConfigNode = null
327 var children = new HashMap[String, ConfigNode]
328 var name: String is writable
329 var value: nullable String = null
330
331 fun key: String do
332 var parent = self.parent
333 if parent == null then
334 return name
335 end
336 return "{parent.key}.{name}"
337 end
338
339 fun path: Array[String] do
340 var parent = self.parent
341 if parent == null then
342 return [name]
343 end
344 var res = new Array[String].from(parent.path)
345 res.add name
346 return res
347 end
348
349 fun get_child(name: String): nullable ConfigNode do
350 if children.has_key(name) then
351 return children[name]
352 end
353 return null
354 end
355 end