lib/ini: fixes old style inits
[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 Streamable
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 children = roots
104 var parts = key.split(".").reversed
105 var node = get_root(parts.pop)
106 if node == null then return false
107 while not parts.is_empty do
108 node = node.get_child(parts.pop)
109 if node == null then return false
110 end
111 return true
112 end
113
114 # Get `self` as a Map of `key`, `value`
115 #
116 # var config = new ConfigTree("config.ini")
117 # var map = config.to_map
118 # assert map.has_key("goo")
119 # assert map.has_key("foo.bar")
120 # assert map.has_key("foo.baz")
121 # assert map.length == 3
122 fun to_map: Map[String, String] do
123 var map = new HashMap[String, String]
124 for node in leaves do
125 if node.value == null then continue
126 map[node.key] = node.value.to_s
127 end
128 return map
129 end
130
131 redef fun to_s do return to_map.join(", ", ":")
132
133 redef fun write_to(stream) do
134 for node in leaves do
135 if node.value == null then continue
136 stream.write("{node.key}={node.value.to_s}\n")
137 end
138 end
139
140 # Reload config from file
141 # Done automatically at init
142 #
143 # Example with hierarchical ini file:
144 #
145 # # init file
146 # var str = """
147 # foo.bar=foobar
148 # foo.baz=foobaz
149 # goo=goo"""
150 # str.write_to_file("config1.ini")
151 # # load file
152 # var config = new ConfigTree("config1.ini")
153 # assert config["foo.bar"] == "foobar"
154 #
155 # Example with sections:
156 #
157 # # init file
158 # str = """
159 # goo=goo
160 # [foo]
161 # bar=foobar
162 # baz=foobaz
163 # [boo]
164 # bar=boobar"""
165 # str.write_to_file("config2.ini")
166 # # load file
167 # config = new ConfigTree("config2.ini")
168 # assert config["foo.bar"] == "foobar"
169 # assert config["boo.bar"] == "boobar"
170 #
171 # Example with both hierarchy and section:
172 #
173 # # init file
174 # str = """
175 # goo=goo
176 # [foo]
177 # bar.baz=foobarbaz
178 # [goo.boo]
179 # bar=gooboobar
180 # baz.bar=gooboobazbar"""
181 # str.write_to_file("config3.ini")
182 # # load file
183 # config = new ConfigTree("config3.ini")
184 # assert config["goo"] == "goo"
185 # assert config["foo.bar.baz"] == "foobarbaz"
186 # assert config["goo.boo.bar"] == "gooboobar"
187 # assert config["goo.boo.baz.bar"] == "gooboobazbar"
188 fun load do
189 roots.clear
190 var stream = new IFStream.open(ini_file)
191 var path: nullable String = null
192 while not stream.eof do
193 var line = stream.read_line
194 if line.is_empty then
195 continue
196 else if line.has_prefix(";") then
197 continue
198 else if line.has_prefix("[") then
199 line = line.trim
200 var key = line.substring(1, line.length - 2)
201 path = key
202 set_node(path, null)
203 else
204 var parts = line.split("=")
205 var key = parts[0].trim
206 var val = parts[1].trim
207 if path == null then
208 set_node(key, val)
209 else
210 set_node("{path}.{key}", val)
211 end
212 end
213 end
214 stream.close
215 end
216
217 # Save config to file
218 fun save do write_to_file(ini_file)
219
220 private var roots = new Array[ConfigNode]
221
222 private fun set_node(key: String, value: nullable String) do
223 var children = roots
224 var parts = key.split(".").reversed
225 var k = parts.pop
226 var root = get_root(k)
227 if root == null then
228 root = new ConfigNode(k)
229 if parts.is_empty then
230 root.value = value
231 end
232 roots.add root
233 end
234 while not parts.is_empty do
235 k = parts.pop
236 var node = root.get_child(k)
237 if node == null then
238 node = new ConfigNode(k)
239 node.parent = root
240 root.children[node.name] = node
241 end
242 if parts.is_empty then
243 node.value = value
244 end
245 root = node
246 end
247 end
248
249 private fun get_node(key: String): nullable ConfigNode do
250 var children = roots
251 var parts = key.split(".").reversed
252 var node = get_root(parts.pop)
253 while not parts.is_empty do
254 node = node.get_child(parts.pop)
255 end
256 return node
257 end
258
259 private fun get_root(name: String): nullable ConfigNode do
260 for root in roots do
261 if root.name == name then return root
262 end
263 return null
264 end
265
266 private fun leaves: Array[ConfigNode] do
267 var res = new Array[ConfigNode]
268 var todo = new Array[ConfigNode]
269 todo.add_all roots
270 while not todo.is_empty do
271 var node = todo.pop
272 if node.children.is_empty then
273 res.add node
274 else
275 todo.add_all node.children.values
276 end
277 end
278 return res
279 end
280 end
281
282 private class ConfigNode
283
284 var parent: nullable ConfigNode = null
285 var children = new HashMap[String, ConfigNode]
286 var name: String is writable
287 var value: nullable String = null
288
289 fun key: String do
290 if parent == null then
291 return name
292 end
293 return "{parent.key}.{name}"
294 end
295
296 fun get_child(name: String): nullable ConfigNode do
297 if children.has_key(name) then
298 return children[name]
299 end
300 return null
301 end
302 end
303