projects: update some short descriptions
[nit.git] / lib / 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 # REQUIRE: `has_key(key)`
45 #
46 # var config = new ConfigTree("config.ini")
47 # assert config["goo"] == "goo"
48 # assert config["foo.bar"] == "foobar"
49 # assert config["foo.baz"] == "foobaz"
50 fun [](key: String): String do
51 if not has_key(key) then
52 print "error: config key `{key}` not found"
53 abort
54 end
55 var node = get_node(key).as(not null)
56 if node.value == null then
57 print "error: config key `{key}` has no value"
58 abort
59 end
60 return node.value.as(not null)
61 end
62
63 # Get the config values under `key`
64 #
65 # REQUIRE: `has_key(key)`
66 #
67 # var config = new ConfigTree("config.ini")
68 # var values = config.at("foo")
69 # assert values.has_key("bar")
70 # assert values.has_key("baz")
71 # assert not values.has_key("goo")
72 fun at(key: String): Map[String, String] do
73 if not has_key(key) then
74 print "error: config key `{key}` not found"
75 abort
76 end
77 var map = new HashMap[String, String]
78 var node = get_node(key).as(not null)
79 for k, child in node.children do
80 if child.value == null then continue
81 map[k] = child.value.to_s
82 end
83 return map
84 end
85
86 # Set `value` at `key`
87 #
88 # var config = new ConfigTree("config.ini")
89 # assert config["foo.bar"] == "foobar"
90 # config["foo.bar"] = "baz"
91 # assert config["foo.bar"] == "baz"
92 fun []=(key: String, value: nullable String) do
93 set_node(key, value)
94 end
95
96 # Is `key` in the config?
97 #
98 # var config = new ConfigTree("config.ini")
99 # assert config.has_key("goo")
100 # assert config.has_key("foo.bar")
101 # assert not config.has_key("zoo")
102 fun has_key(key: String): Bool do
103 var parts = key.split(".").reversed
104 var node = get_root(parts.pop)
105 if node == null then return false
106 while not parts.is_empty do
107 node = node.get_child(parts.pop)
108 if node == null then return false
109 end
110 return true
111 end
112
113 # Get `self` as a Map of `key`, `value`
114 #
115 # var config = new ConfigTree("config.ini")
116 # var map = config.to_map
117 # assert map.has_key("goo")
118 # assert map.has_key("foo.bar")
119 # assert map.has_key("foo.baz")
120 # assert map.length == 3
121 fun to_map: Map[String, String] do
122 var map = new HashMap[String, String]
123 for node in leaves do
124 if node.value == null then continue
125 map[node.key] = node.value.to_s
126 end
127 return map
128 end
129
130 redef fun to_s do return to_map.join(", ", ":")
131
132 redef fun write_to(stream) do
133 for node in leaves do
134 if node.value == null then continue
135 stream.write("{node.key}={node.value.to_s}\n")
136 end
137 end
138
139 # Reload config from file
140 # Done automatically at init
141 #
142 # Example with hierarchical ini file:
143 #
144 # # init file
145 # var str = """
146 # foo.bar=foobar
147 # foo.baz=foobaz
148 # goo=goo"""
149 # str.write_to_file("config1.ini")
150 # # load file
151 # var config = new ConfigTree("config1.ini")
152 # assert config["foo.bar"] == "foobar"
153 #
154 # Example with sections:
155 #
156 # # init file
157 # str = """
158 # goo=goo
159 # [foo]
160 # bar=foobar
161 # baz=foobaz
162 # [boo]
163 # bar=boobar"""
164 # str.write_to_file("config2.ini")
165 # # load file
166 # config = new ConfigTree("config2.ini")
167 # assert config["foo.bar"] == "foobar"
168 # assert config["boo.bar"] == "boobar"
169 #
170 # Example with both hierarchy and section:
171 #
172 # # init file
173 # str = """
174 # goo=goo
175 # [foo]
176 # bar.baz=foobarbaz
177 # [goo.boo]
178 # bar=gooboobar
179 # baz.bar=gooboobazbar"""
180 # str.write_to_file("config3.ini")
181 # # load file
182 # config = new ConfigTree("config3.ini")
183 # assert config["goo"] == "goo"
184 # assert config["foo.bar.baz"] == "foobarbaz"
185 # assert config["goo.boo.bar"] == "gooboobar"
186 # assert config["goo.boo.baz.bar"] == "gooboobazbar"
187 #
188 # Using the array notation
189 #
190 # str = """
191 # foo[]=a
192 # foo[]=b
193 # foo[]=c"""
194 # str.write_to_file("config4.ini")
195 # # load file
196 # config = new ConfigTree("config4.ini")
197 # print config.to_map.join(":", ",")
198 # assert config["foo.0"] == "a"
199 # assert config["foo.1"] == "b"
200 # assert config["foo.2"] == "c"
201 # assert config.at("foo").values.join(",") == "a,b,c"
202 fun load do
203 roots.clear
204 var stream = new FileReader.open(ini_file)
205 var path: nullable String = null
206 var line_number = 0
207 while not stream.eof do
208 var line = stream.read_line
209 line_number += 1
210 if line.is_empty then
211 continue
212 else if line.has_prefix(";") then
213 continue
214 else if line.has_prefix("[") then
215 line = line.trim
216 var key = line.substring(1, line.length - 2)
217 path = key
218 set_node(path, null)
219 else
220 var parts = line.split("=")
221 assert parts.length > 1 else
222 print "Error: malformed ini at line {line_number}"
223 end
224 var key = parts[0].trim
225 var val = parts[1].trim
226 if path != null then key = "{path}.{key}"
227 if key.has_suffix("[]") then
228 set_array(key, val)
229 else
230 set_node(key,val)
231 end
232 end
233 end
234 stream.close
235 end
236
237 # Save config to file
238 fun save do write_to_file(ini_file)
239
240 private var roots = new Array[ConfigNode]
241
242 # Append `value` to array at `key`
243 private fun set_array(key: String, value: nullable String) do
244 key = key.substring(0, key.length - 2)
245 var len = 0
246 if has_key(key) then
247 len = get_node(key).children.length
248 end
249 set_node("{key}.{len.to_s}", value)
250 end
251
252 private fun set_node(key: String, value: nullable String) do
253 var parts = key.split(".").reversed
254 var k = parts.pop
255 var root = get_root(k)
256 if root == null then
257 root = new ConfigNode(k)
258 if parts.is_empty then
259 root.value = value
260 end
261 roots.add root
262 end
263 while not parts.is_empty do
264 k = parts.pop
265 var node = root.get_child(k)
266 if node == null then
267 node = new ConfigNode(k)
268 node.parent = root
269 root.children[node.name] = node
270 end
271 if parts.is_empty then
272 node.value = value
273 end
274 root = node
275 end
276 end
277
278 private fun get_node(key: String): nullable ConfigNode do
279 var parts = key.split(".").reversed
280 var node = get_root(parts.pop)
281 while not parts.is_empty do
282 node = node.get_child(parts.pop)
283 end
284 return node
285 end
286
287 private fun get_root(name: String): nullable ConfigNode do
288 for root in roots do
289 if root.name == name then return root
290 end
291 return null
292 end
293
294 private fun leaves: Array[ConfigNode] do
295 var res = new Array[ConfigNode]
296 var todo = new Array[ConfigNode]
297 todo.add_all roots
298 while not todo.is_empty do
299 var node = todo.pop
300 if node.children.is_empty then
301 res.add node
302 else
303 todo.add_all node.children.values
304 end
305 end
306 return res
307 end
308 end
309
310 private class ConfigNode
311
312 var parent: nullable ConfigNode = null
313 var children = new HashMap[String, ConfigNode]
314 var name: String is writable
315 var value: nullable String = null
316
317 fun key: String do
318 if parent == null then
319 return name
320 end
321 return "{parent.key}.{name}"
322 end
323
324 fun get_child(name: String): nullable ConfigNode do
325 if children.has_key(name) then
326 return children[name]
327 end
328 return null
329 end
330 end