1 | // Copyright (c) 2021 Lars Pontoppidan. All rights reserved. |
2 | // Use of this source code is governed by an MIT license |
3 | // that can be found in the LICENSE file. |
4 | module toml |
5 | |
6 | import toml.ast |
7 | import toml.input |
8 | import toml.scanner |
9 | import toml.parser |
10 | |
11 | // Null is used in sumtype checks as a "default" value when nothing else is possible. |
12 | pub struct Null { |
13 | } |
14 | |
15 | // decode decodes a TOML `string` into the target type `T`. |
16 | // If `T` has a custom `.from_toml()` method, it will be used instead of the default. |
17 | pub fn decode[T](toml_txt string) !T { |
18 | doc := parse_text(toml_txt)! |
19 | mut typ := T{} |
20 | $for method in T.methods { |
21 | $if method.name == 'from_toml' { |
22 | typ.$method(doc.to_any()) |
23 | return typ |
24 | } |
25 | } |
26 | $if T !is $struct { |
27 | return error('toml.decode: expected struct, found ${T.name}') |
28 | } |
29 | decode_struct[T](doc.to_any(), mut typ) |
30 | return typ |
31 | } |
32 | |
33 | fn decode_struct[T](doc Any, mut typ T) { |
34 | $for field in T.fields { |
35 | value := doc.value(field.name) |
36 | $if field.is_enum { |
37 | typ.$(field.name) = value.int() |
38 | } $else $if field.typ is string { |
39 | typ.$(field.name) = value.string() |
40 | } $else $if field.typ is bool { |
41 | typ.$(field.name) = value.bool() |
42 | } $else $if field.typ is int { |
43 | typ.$(field.name) = value.int() |
44 | } $else $if field.typ is i64 { |
45 | typ.$(field.name) = value.i64() |
46 | } $else $if field.typ is u64 { |
47 | typ.$(field.name) = value.u64() |
48 | } $else $if field.typ is f32 { |
49 | typ.$(field.name) = value.f32() |
50 | } $else $if field.typ is f64 { |
51 | typ.$(field.name) = value.f64() |
52 | } $else $if field.typ is DateTime { |
53 | typ.$(field.name) = value.datetime() |
54 | } $else $if field.typ is Date { |
55 | typ.$(field.name) = value.date() |
56 | } $else $if field.typ is Time { |
57 | typ.$(field.name) = value.time() |
58 | } $else $if field.is_array { |
59 | arr := value.array() |
60 | match typeof(typ.$(field.name)).name { |
61 | '[]string' { typ.$(field.name) = arr.as_strings() } |
62 | '[]int' { typ.$(field.name) = arr.map(it.int()) } |
63 | '[]i64' { typ.$(field.name) = arr.map(it.i64()) } |
64 | '[]u64' { typ.$(field.name) = arr.map(it.u64()) } |
65 | '[]f32' { typ.$(field.name) = arr.map(it.f32()) } |
66 | '[]f64' { typ.$(field.name) = arr.map(it.f64()) } |
67 | '[]bool' { typ.$(field.name) = arr.map(it.bool()) } |
68 | '[]toml.DateTime' { typ.$(field.name) = arr.map(it.datetime()) } |
69 | '[]toml.Date' { typ.$(field.name) = arr.map(it.date()) } |
70 | '[]toml.Time' { typ.$(field.name) = arr.map(it.time()) } |
71 | else {} |
72 | } |
73 | } $else $if field.is_struct { |
74 | mut s := typ.$(field.name) |
75 | decode_struct(value, mut s) |
76 | typ.$(field.name) = s |
77 | } |
78 | } |
79 | } |
80 | |
81 | // encode encodes the type `T` into a TOML string. |
82 | // If `T` has a custom `.to_toml()` method, it will be used instead of the default. |
83 | pub fn encode[T](typ T) string { |
84 | $for method in T.methods { |
85 | $if method.name == 'to_toml' { |
86 | return typ.$method() |
87 | } |
88 | } |
89 | mp := encode_struct[T](typ) |
90 | return mp.to_toml() |
91 | } |
92 | |
93 | fn encode_struct[T](typ T) map[string]Any { |
94 | mut mp := map[string]Any{} |
95 | $for field in T.fields { |
96 | value := typ.$(field.name) |
97 | $if field.is_enum { |
98 | mp[field.name] = Any(int(value)) |
99 | } $else $if field.is_struct { |
100 | mp[field.name] = encode_struct(value) |
101 | } $else $if field.is_array { |
102 | mut arr := []Any{} |
103 | for v in value { |
104 | $if v is Date { |
105 | arr << Any(v) |
106 | } $else $if v is Time { |
107 | arr << Any(v) |
108 | } $else $if v is DateTime { |
109 | arr << Any(v) |
110 | } $else $if v is $struct { |
111 | arr << Any(encode(v)) |
112 | } $else { |
113 | arr << Any(v) |
114 | } |
115 | } |
116 | mp[field.name] = arr |
117 | } $else { |
118 | mp[field.name] = Any(value) |
119 | } |
120 | } |
121 | return mp |
122 | } |
123 | |
124 | // DateTime is the representation of an RFC 3339 datetime string. |
125 | pub struct DateTime { |
126 | datetime string |
127 | } |
128 | |
129 | // str returns the RFC 3339 string representation of the datetime. |
130 | pub fn (dt DateTime) str() string { |
131 | return dt.datetime |
132 | } |
133 | |
134 | // Date is the representation of an RFC 3339 date-only string. |
135 | pub struct Date { |
136 | date string |
137 | } |
138 | |
139 | // str returns the RFC 3339 date-only string representation. |
140 | pub fn (d Date) str() string { |
141 | return d.date |
142 | } |
143 | |
144 | // Time is the representation of an RFC 3339 time-only string. |
145 | pub struct Time { |
146 | time string |
147 | } |
148 | |
149 | // str returns the RFC 3339 time-only string representation. |
150 | pub fn (t Time) str() string { |
151 | return t.time |
152 | } |
153 | |
154 | // Config is used to configure the toml parser. |
155 | // Only one of the fields `text` or `file_path`, is allowed to be set at time of configuration. |
156 | pub struct Config { |
157 | pub: |
158 | text string // TOML text |
159 | file_path string // '/path/to/file.toml' |
160 | parse_comments bool |
161 | } |
162 | |
163 | // Doc is a representation of a TOML document. |
164 | // A document can be constructed from a `string` buffer or from a file path |
165 | pub struct Doc { |
166 | pub: |
167 | ast &ast.Root = unsafe { nil } |
168 | } |
169 | |
170 | // parse_file parses the TOML file in `path`. |
171 | pub fn parse_file(path string) !Doc { |
172 | input_config := input.Config{ |
173 | file_path: path |
174 | } |
175 | scanner_config := scanner.Config{ |
176 | input: input_config |
177 | } |
178 | parser_config := parser.Config{ |
179 | scanner: scanner.new_scanner(scanner_config)! |
180 | } |
181 | mut p := parser.new_parser(parser_config) |
182 | ast_ := p.parse()! |
183 | return Doc{ |
184 | ast: ast_ |
185 | } |
186 | } |
187 | |
188 | // parse_text parses the TOML document provided in `text`. |
189 | pub fn parse_text(text string) !Doc { |
190 | input_config := input.Config{ |
191 | text: text |
192 | } |
193 | scanner_config := scanner.Config{ |
194 | input: input_config |
195 | } |
196 | parser_config := parser.Config{ |
197 | scanner: scanner.new_scanner(scanner_config)! |
198 | } |
199 | mut p := parser.new_parser(parser_config) |
200 | ast_ := p.parse()! |
201 | return Doc{ |
202 | ast: ast_ |
203 | } |
204 | } |
205 | |
206 | // parse_dotted_key converts `key` string to an array of strings. |
207 | // parse_dotted_key preserves strings delimited by both `"` and `'`. |
208 | pub fn parse_dotted_key(key string) ![]string { |
209 | mut out := []string{} |
210 | mut buf := '' |
211 | mut in_string := false |
212 | mut delim := u8(` `) |
213 | for ch in key { |
214 | if ch in [`"`, `'`] { |
215 | if !in_string { |
216 | delim = ch |
217 | } |
218 | in_string = !in_string && ch == delim |
219 | if !in_string { |
220 | if buf != '' && buf != ' ' { |
221 | out << buf |
222 | } |
223 | buf = '' |
224 | delim = ` ` |
225 | } |
226 | continue |
227 | } |
228 | buf += ch.ascii_str() |
229 | if !in_string && ch == `.` { |
230 | if buf != '' && buf != ' ' { |
231 | buf = buf[..buf.len - 1] |
232 | if buf != '' && buf != ' ' { |
233 | out << buf |
234 | } |
235 | } |
236 | buf = '' |
237 | continue |
238 | } |
239 | } |
240 | if buf != '' && buf != ' ' { |
241 | out << buf |
242 | } |
243 | if in_string { |
244 | return error(@FN + |
245 | ': could not parse key, missing closing string delimiter `${delim.ascii_str()}`') |
246 | } |
247 | return out |
248 | } |
249 | |
250 | // parse_array_key converts `key` string to a key and index part. |
251 | fn parse_array_key(key string) (string, int) { |
252 | mut index := -1 |
253 | mut k := key |
254 | if k.contains('[') { |
255 | index = k.all_after('[').all_before(']').int() |
256 | if k.starts_with('[') { |
257 | k = '' // k.all_after(']') |
258 | } else { |
259 | k = k.all_before('[') |
260 | } |
261 | } |
262 | return k, index |
263 | } |
264 | |
265 | // decode decodes a TOML `string` into the target struct type `T`. |
266 | pub fn (d Doc) decode[T]() !T { |
267 | $if T !is $struct { |
268 | return error('Doc.decode: expected struct, found ${T.name}') |
269 | } |
270 | mut typ := T{} |
271 | decode_struct(d.to_any(), mut typ) |
272 | return typ |
273 | } |
274 | |
275 | // to_any converts the `Doc` to toml.Any type. |
276 | pub fn (d Doc) to_any() Any { |
277 | return ast_to_any(d.ast.table) |
278 | } |
279 | |
280 | // reflect returns `T` with `T.<field>`'s value set to the |
281 | // value of any 1st level TOML key by the same name. |
282 | pub fn (d Doc) reflect[T]() T { |
283 | return d.to_any().reflect[T]() |
284 | } |
285 | |
286 | // value queries a value from the TOML document. |
287 | // `key` supports a small query syntax scheme: |
288 | // Maps can be queried in "dotted" form e.g. `a.b.c`. |
289 | // quoted keys are supported as `a."b.c"` or `a.'b.c'`. |
290 | // Arrays can be queried with `a[0].b[1].[2]`. |
291 | pub fn (d Doc) value(key string) Any { |
292 | key_split := parse_dotted_key(key) or { return toml.null } |
293 | return d.value_(d.ast.table, key_split) |
294 | } |
295 | |
296 | pub const null = Any(Null{}) |
297 | |
298 | // value_opt queries a value from the TOML document. Returns an error if the |
299 | // key is not valid or there is no value for the key. |
300 | pub fn (d Doc) value_opt(key string) !Any { |
301 | key_split := parse_dotted_key(key) or { return error('invalid dotted key') } |
302 | x := d.value_(d.ast.table, key_split) |
303 | if x is Null { |
304 | return error('no value for key') |
305 | } |
306 | return x |
307 | } |
308 | |
309 | // value_ returns the value found at `key` in the map `values` as `Any` type. |
310 | fn (d Doc) value_(value ast.Value, key []string) Any { |
311 | if key.len == 0 { |
312 | return toml.null |
313 | } |
314 | mut ast_value := ast.Value(ast.Null{}) |
315 | k, index := parse_array_key(key[0]) |
316 | |
317 | if k == '' { |
318 | a := value as []ast.Value |
319 | ast_value = a[index] or { return toml.null } |
320 | } |
321 | |
322 | if value is map[string]ast.Value { |
323 | ast_value = value[k] or { return toml.null } |
324 | if index > -1 { |
325 | a := ast_value as []ast.Value |
326 | ast_value = a[index] or { return toml.null } |
327 | } |
328 | } |
329 | |
330 | if key.len <= 1 { |
331 | return ast_to_any(ast_value) |
332 | } |
333 | match ast_value { |
334 | map[string]ast.Value, []ast.Value { |
335 | return d.value_(ast_value, key[1..]) |
336 | } |
337 | else { |
338 | return ast_to_any(value) |
339 | } |
340 | } |
341 | } |
342 | |
343 | // ast_to_any converts `from` ast.Value to toml.Any value. |
344 | pub fn ast_to_any(value ast.Value) Any { |
345 | match value { |
346 | ast.Date { |
347 | return Any(Date{value.text}) |
348 | } |
349 | ast.Time { |
350 | return Any(Time{value.text}) |
351 | } |
352 | ast.DateTime { |
353 | return Any(DateTime{value.text}) |
354 | } |
355 | ast.Quoted { |
356 | return Any(value.text) |
357 | } |
358 | ast.Number { |
359 | val_text := value.text |
360 | if val_text == 'inf' || val_text == '+inf' || val_text == '-inf' { |
361 | // NOTE values taken from strconv |
362 | if !val_text.starts_with('-') { |
363 | // strconv.double_plus_infinity |
364 | return Any(u64(0x7FF0000000000000)) |
365 | } else { |
366 | // strconv.double_minus_infinity |
367 | return Any(u64(0xFFF0000000000000)) |
368 | } |
369 | } |
370 | if val_text == 'nan' || val_text == '+nan' || val_text == '-nan' { |
371 | return Any('nan') |
372 | } |
373 | if !val_text.starts_with('0x') |
374 | && (val_text.contains('.') || val_text.to_lower().contains('e')) { |
375 | return Any(value.f64()) |
376 | } |
377 | return Any(value.i64()) |
378 | } |
379 | ast.Bool { |
380 | str := (value as ast.Bool).text |
381 | if str == 'true' { |
382 | return Any(true) |
383 | } |
384 | return Any(false) |
385 | } |
386 | map[string]ast.Value { |
387 | m := (value as map[string]ast.Value) |
388 | mut am := map[string]Any{} |
389 | for k, v in m { |
390 | am[k] = ast_to_any(v) |
391 | } |
392 | return am |
393 | // return d.get_map_value(m, key_split[1..].join('.')) |
394 | } |
395 | []ast.Value { |
396 | a := (value as []ast.Value) |
397 | mut aa := []Any{} |
398 | for val in a { |
399 | aa << ast_to_any(val) |
400 | } |
401 | return aa |
402 | } |
403 | else { |
404 | return toml.null |
405 | } |
406 | } |
407 | |
408 | return toml.null |
409 | // TODO decide this |
410 | // panic(@MOD + '.' + @STRUCT + '.' + @FN + ' can\'t convert "$value"') |
411 | // return Any('') |
412 | } |