v / vlib / toml / toml.v
594 lines · 559 sloc · 13.8 KB · 2b808880f73948e12d8321e107bcab07cd438601
Raw
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.
4module toml
5
6import toml.ast
7import toml.input
8import toml.scanner
9import toml.parser
10
11// Null is used in sumtype checks as a "default" value when nothing else is possible.
12pub 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.
17pub 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
33fn decode_struct[T](doc Any, mut typ T) {
34 $for field in T.fields {
35 mut field_name := field.name
36 mut skip := false
37 for attr in field.attrs {
38 if attr == 'skip' {
39 skip = true
40 break
41 }
42 if attr.starts_with('toml:') {
43 field_name = attr.all_after(':').trim_space()
44 }
45 }
46 value := doc.value(field_name)
47 // only set the field's value when value != null and !skip, else field got it's default value
48 if !skip && value != null {
49 $if field.is_enum {
50 typ.$(field.name) = value.int()
51 } $else $if field.typ is string {
52 typ.$(field.name) = value.string()
53 } $else $if field.typ is bool {
54 typ.$(field.name) = value.bool()
55 } $else $if field.typ is int {
56 typ.$(field.name) = value.int()
57 } $else $if field.typ is i64 {
58 typ.$(field.name) = value.i64()
59 } $else $if field.typ is u64 {
60 typ.$(field.name) = value.u64()
61 } $else $if field.typ is f32 {
62 typ.$(field.name) = value.f32()
63 } $else $if field.typ is f64 {
64 typ.$(field.name) = value.f64()
65 } $else $if field.typ is DateTime {
66 typ.$(field.name) = value.datetime()
67 } $else $if field.typ is Date {
68 typ.$(field.name) = value.date()
69 } $else $if field.typ is Time {
70 typ.$(field.name) = value.time()
71 } $else $if field.typ is Any {
72 typ.$(field.name) = value
73 } $else $if field.is_array {
74 typ.$(field.name) = decode_array(typ.$(field.name), value.array())
75 } $else $if field.is_map {
76 typ.$(field.name) = decode_map(typ.$(field.name), value.as_map())
77 } $else $if field.is_struct {
78 mut s := typ.$(field.name)
79 decode_struct(value, mut s)
80 typ.$(field.name) = s
81 }
82 }
83 }
84}
85
86fn decode_array[T](current []T, values []Any) []T {
87 $if T is string {
88 return values.map(it.string())
89 } $else $if T is bool {
90 return values.map(it.bool())
91 } $else $if T is int {
92 return values.map(it.int())
93 } $else $if T is i64 {
94 return values.map(it.i64())
95 } $else $if T is u64 {
96 return values.map(it.u64())
97 } $else $if T is f32 {
98 return values.map(it.f32())
99 } $else $if T is f64 {
100 return values.map(it.f64())
101 } $else $if T is DateTime {
102 return values.map(it.datetime())
103 } $else $if T is Date {
104 return values.map(it.date())
105 } $else $if T is Time {
106 return values.map(it.time())
107 } $else $if T is Any {
108 return values
109 } $else $if T is $struct {
110 mut decoded := []T{cap: values.len}
111 for value in values {
112 if value is map[string]Any {
113 mut item := T{}
114 decode_struct(value, mut item)
115 decoded << item
116 }
117 }
118 return decoded
119 } $else {
120 return current
121 }
122}
123
124fn decode_map[T](current map[string]T, values map[string]Any) map[string]T {
125 $if T is string {
126 mut decoded := map[string]T{}
127 for key, value in values {
128 decoded[key] = value.string()
129 }
130 return decoded
131 } $else $if T is bool {
132 mut decoded := map[string]T{}
133 for key, value in values {
134 decoded[key] = value.bool()
135 }
136 return decoded
137 } $else $if T is int {
138 mut decoded := map[string]T{}
139 for key, value in values {
140 decoded[key] = value.int()
141 }
142 return decoded
143 } $else $if T is i64 {
144 mut decoded := map[string]T{}
145 for key, value in values {
146 decoded[key] = value.i64()
147 }
148 return decoded
149 } $else $if T is u64 {
150 mut decoded := map[string]T{}
151 for key, value in values {
152 decoded[key] = value.u64()
153 }
154 return decoded
155 } $else $if T is f32 {
156 mut decoded := map[string]T{}
157 for key, value in values {
158 decoded[key] = value.f32()
159 }
160 return decoded
161 } $else $if T is f64 {
162 mut decoded := map[string]T{}
163 for key, value in values {
164 decoded[key] = value.f64()
165 }
166 return decoded
167 } $else $if T is DateTime {
168 mut decoded := map[string]T{}
169 for key, value in values {
170 decoded[key] = value.datetime()
171 }
172 return decoded
173 } $else $if T is Date {
174 mut decoded := map[string]T{}
175 for key, value in values {
176 decoded[key] = value.date()
177 }
178 return decoded
179 } $else $if T is Time {
180 mut decoded := map[string]T{}
181 for key, value in values {
182 decoded[key] = value.time()
183 }
184 return decoded
185 } $else $if T is Any {
186 return values.clone()
187 } $else $if T is $struct {
188 mut decoded := map[string]T{}
189 for key, value in values {
190 if value is map[string]Any {
191 mut item := T{}
192 decode_struct(value, mut item)
193 decoded[key] = item
194 }
195 }
196 return decoded
197 } $else {
198 return current
199 }
200}
201
202// encode encodes the type `T` into a TOML string.
203// If `T` has a custom `.to_toml()` method, it will be used instead of the default.
204pub fn encode[T](typ T) string {
205 $if T is $struct {
206 $for method in T.methods {
207 $if method.name == 'to_toml' {
208 return typ.$method()
209 }
210 }
211 mp := encode_struct[T](typ)
212 return mp.to_toml()
213 } $else {
214 $compile_error('Currently only type `struct` is supported for `T` to encode as TOML')
215 }
216 return ''
217}
218
219fn encode_struct[T](typ T) map[string]Any {
220 mut mp := map[string]Any{}
221 $for field in T.fields {
222 mut skip := false
223 mut field_name := field.name
224 for attr in field.attrs {
225 if attr == 'skip' {
226 skip = true
227 break
228 }
229 if attr.starts_with('toml:') {
230 field_name = attr.all_after(':').trim_space()
231 }
232 }
233 if !skip {
234 mp[field_name] = to_any(typ.$(field.name))
235 }
236 }
237 return mp
238}
239
240fn voidptr_to_toml_string[T](value T) string {
241 ptr := unsafe { voidptr(&value) }
242 return unsafe { '0x${ptr_str(*(&voidptr(ptr)))}' }
243}
244
245fn to_any[T](value T) Any {
246 $if T is $enum {
247 return Any(int(value))
248 } $else $if T is Date {
249 return Any(value)
250 } $else $if T is Time {
251 return Any(value)
252 } $else $if T is Null {
253 return Any(value)
254 } $else $if T is bool {
255 return Any(value)
256 } $else $if T is f32 {
257 return Any(value)
258 } $else $if T is f64 {
259 return Any(value)
260 } $else $if T is i64 {
261 return Any(value)
262 } $else $if T is int {
263 return Any(value)
264 } $else $if T is u64 {
265 return Any(value)
266 } $else $if T is DateTime {
267 return Any(value)
268 } $else $if T is Any {
269 return value
270 } $else $if T is $struct {
271 $for method in T.methods {
272 $if method.name == 'to_toml' {
273 return Any(value.$method())
274 }
275 }
276 return encode_struct(value)
277 } $else $if T is $array {
278 mut arr := []Any{cap: value.len}
279 for v in value {
280 arr << to_any(v)
281 }
282 return arr
283 } $else $if T is $map {
284 mut mmap := map[string]Any{}
285 for key, val in value {
286 mmap['${key}'] = to_any(val)
287 }
288 return mmap
289 } $else {
290 if typeof(value).name == 'voidptr' {
291 return Any(voidptr_to_toml_string(value))
292 }
293 return Any('${value}')
294 }
295}
296
297// DateTime is the representation of an RFC 3339 datetime string.
298pub struct DateTime {
299pub:
300 datetime string
301}
302
303// str returns the RFC 3339 string representation of the datetime.
304pub fn (dt DateTime) str() string {
305 return dt.datetime
306}
307
308// Date is the representation of an RFC 3339 date-only string.
309pub struct Date {
310pub:
311 date string
312}
313
314// str returns the RFC 3339 date-only string representation.
315pub fn (d Date) str() string {
316 return d.date
317}
318
319// Time is the representation of an RFC 3339 time-only string.
320pub struct Time {
321pub:
322 time string
323}
324
325// str returns the RFC 3339 time-only string representation.
326pub fn (t Time) str() string {
327 return t.time
328}
329
330// Config is used to configure the toml parser.
331// Only one of the fields `text` or `file_path`, is allowed to be set at time of configuration.
332pub struct Config {
333pub:
334 text string // TOML text
335 file_path string // '/path/to/file.toml'
336 parse_comments bool
337}
338
339// Doc is a representation of a TOML document.
340// A document can be constructed from a `string` buffer or from a file path
341pub struct Doc {
342pub:
343 ast &ast.Root = unsafe { nil }
344}
345
346// parse_file parses the TOML file in `path`.
347pub fn parse_file(path string) !Doc {
348 input_config := input.Config{
349 file_path: path
350 }
351 scanner_config := scanner.Config{
352 input: input_config
353 }
354 parser_config := parser.Config{
355 scanner: scanner.new_scanner(scanner_config)!
356 }
357 mut p := parser.new_parser(parser_config)
358 ast_ := p.parse()!
359 return Doc{
360 ast: ast_
361 }
362}
363
364// parse_text parses the TOML document provided in `text`.
365pub fn parse_text(text string) !Doc {
366 input_config := input.Config{
367 text: text
368 }
369 scanner_config := scanner.Config{
370 input: input_config
371 }
372 parser_config := parser.Config{
373 scanner: scanner.new_scanner(scanner_config)!
374 }
375 mut p := parser.new_parser(parser_config)
376 ast_ := p.parse()!
377 return Doc{
378 ast: ast_
379 }
380}
381
382// parse_dotted_key converts `key` string to an array of strings.
383// parse_dotted_key preserves strings delimited by both `"` and `'`.
384pub fn parse_dotted_key(key string) ![]string {
385 mut out := []string{}
386 mut buf := ''
387 mut in_string := false
388 mut delim := u8(` `)
389 for ch in key {
390 if ch in [`"`, `'`] {
391 if !in_string {
392 delim = ch
393 }
394 in_string = !in_string && ch == delim
395 if !in_string {
396 if buf != '' && buf != ' ' {
397 out << buf
398 }
399 buf = ''
400 delim = ` `
401 }
402 continue
403 }
404 buf += ch.ascii_str()
405 if !in_string && ch == `.` {
406 if buf != '' && buf != ' ' {
407 buf = buf[..buf.len - 1]
408 if buf != '' && buf != ' ' {
409 out << buf
410 }
411 }
412 buf = ''
413 continue
414 }
415 }
416 if buf != '' && buf != ' ' {
417 out << buf
418 }
419 if in_string {
420 return error(@FN +
421 ': could not parse key, missing closing string delimiter `${delim.ascii_str()}`')
422 }
423 return out
424}
425
426// parse_array_key converts `key` string to a key and index part.
427fn parse_array_key(key string) (string, int) {
428 mut index := -1
429 mut k := key
430 if k.contains('[') {
431 index = k.all_after('[').all_before(']').int()
432 if k.starts_with('[') {
433 k = '' // k.all_after(']')
434 } else {
435 k = k.all_before('[')
436 }
437 }
438 return k, index
439}
440
441// decode decodes a TOML `string` into the target struct type `T`.
442pub fn (d Doc) decode[T]() !T {
443 $if T !is $struct {
444 return error('Doc.decode: expected struct, found ${T.name}')
445 }
446 mut typ := T{}
447 decode_struct(d.to_any(), mut typ)
448 return typ
449}
450
451// to_any converts the `Doc` to toml.Any type.
452pub fn (d Doc) to_any() Any {
453 return ast_to_any(d.ast.table)
454}
455
456// reflect returns `T` with `T.<field>`'s value set to the
457// value of any 1st level TOML key by the same name.
458pub fn (d Doc) reflect[T]() T {
459 return d.to_any().reflect[T]()
460}
461
462// value queries a value from the TOML document.
463// `key` supports a small query syntax scheme:
464// Maps can be queried in "dotted" form e.g. `a.b.c`.
465// quoted keys are supported as `a."b.c"` or `a.'b.c'`.
466// Arrays can be queried with `a[0].b[1].[2]`.
467pub fn (d Doc) value(key string) Any {
468 key_split := parse_dotted_key(key) or { return null }
469 return d.value_(d.ast.table, key_split)
470}
471
472pub const null = Any(Null{})
473
474// value_opt queries a value from the TOML document. Returns an error if the
475// key is not valid or there is no value for the key.
476pub fn (d Doc) value_opt(key string) !Any {
477 key_split := parse_dotted_key(key) or { return error('invalid dotted key') }
478 x := d.value_(d.ast.table, key_split)
479 if x is Null {
480 return error('no value for key')
481 }
482 return x
483}
484
485// value_ returns the value found at `key` in the map `values` as `Any` type.
486fn (d Doc) value_(value ast.Value, key []string) Any {
487 if key.len == 0 {
488 return null
489 }
490 mut ast_value := ast.Value(ast.Null{})
491 k, index := parse_array_key(key[0])
492
493 if k == '' {
494 a := value as []ast.Value
495 ast_value = a[index] or { return null }
496 }
497
498 if value is map[string]ast.Value {
499 ast_value = value[k] or { return null }
500 if index > -1 {
501 a := ast_value as []ast.Value
502 ast_value = a[index] or { return null }
503 }
504 }
505
506 if key.len <= 1 {
507 return ast_to_any(ast_value)
508 }
509 match ast_value {
510 map[string]ast.Value, []ast.Value {
511 return d.value_(ast_value, key[1..])
512 }
513 else {
514 return ast_to_any(value)
515 }
516 }
517}
518
519// ast_to_any converts `from` ast.Value to toml.Any value.
520pub fn ast_to_any(value ast.Value) Any {
521 return ast_to_any_(value)
522}
523
524fn ast_to_any_(value ast.Value) Any {
525 match value {
526 ast.Date {
527 return Any(Date{value.text.clone()})
528 }
529 ast.Time {
530 return Any(Time{value.text.clone()})
531 }
532 ast.DateTime {
533 return Any(DateTime{value.text.clone()})
534 }
535 ast.Quoted {
536 return Any(value.text.clone())
537 }
538 ast.Number {
539 val_text := value.text
540 if val_text == 'inf' || val_text == '+inf' || val_text == '-inf' {
541 // NOTE values taken from strconv
542 if !val_text.starts_with('-') {
543 // strconv.double_plus_infinity
544 return Any(u64(0x7FF0000000000000))
545 } else {
546 // strconv.double_minus_infinity
547 return Any(u64(0xFFF0000000000000))
548 }
549 }
550 if val_text == 'nan' || val_text == '+nan' || val_text == '-nan' {
551 return Any('nan')
552 }
553 if !val_text.starts_with('0x')
554 && (val_text.contains('.') || val_text.to_lower().contains('e')) {
555 return Any(value.f64())
556 }
557 return Any(value.i64())
558 }
559 ast.Bool {
560 str := value.text
561 if str == 'true' {
562 return Any(true)
563 }
564 return Any(false)
565 }
566 map[string]ast.Value {
567 m := (value as map[string]ast.Value)
568 mut am := map[string]Any{}
569 for k, v in m {
570 converted := ast_to_any_(v)
571 am[k] = converted
572 }
573 return am
574 // return d.get_map_value(m, key_split[1..].join('.'))
575 }
576 []ast.Value {
577 a := (value as []ast.Value)
578 mut aa := []Any{cap: a.len}
579 for val in a {
580 converted := ast_to_any_(val)
581 aa << converted
582 }
583 return aa
584 }
585 else {
586 return null
587 }
588 }
589
590 return null
591 // TODO: decide this
592 // panic(@MOD + '.' + @STRUCT + '.' + @FN + ' can\'t convert "${value}"')
593 // return Any('')
594}
595