v / vlib / toml
Raw file | 412 loc (382 sloc) | 7.64 KB | Latest commit hash ef5be22f8
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 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.
83pub 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
93fn 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.
125pub struct DateTime {
126 datetime string
127}
128
129// str returns the RFC 3339 string representation of the datetime.
130pub fn (dt DateTime) str() string {
131 return dt.datetime
132}
133
134// Date is the representation of an RFC 3339 date-only string.
135pub struct Date {
136 date string
137}
138
139// str returns the RFC 3339 date-only string representation.
140pub fn (d Date) str() string {
141 return d.date
142}
143
144// Time is the representation of an RFC 3339 time-only string.
145pub struct Time {
146 time string
147}
148
149// str returns the RFC 3339 time-only string representation.
150pub 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.
156pub struct Config {
157pub:
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
165pub struct Doc {
166pub:
167 ast &ast.Root = unsafe { nil }
168}
169
170// parse_file parses the TOML file in `path`.
171pub 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`.
189pub 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 `'`.
208pub 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.
251fn 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`.
266pub 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.
276pub 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.
282pub 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]`.
291pub 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
296pub 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.
300pub 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.
310fn (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.
344pub 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}