v/vlib/toml/toml.v

282 lines
7.4 KiB
V

// Copyright (c) 2021 Lars Pontoppidan. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module toml
import toml.ast
import toml.util
import toml.input
import toml.scanner
import toml.parser
import time
import x.json2
import strconv
// Null is used in sumtype checks as a "default" value when nothing else is possible.
pub struct Null {
}
// Config is used to configure the toml parser.
// Only one of the fields `text` or `file_path`, is allowed to be set at time of configuration.
pub struct Config {
pub:
text string // TOML text
file_path string // '/path/to/file.toml'
parse_comments bool
}
// Doc is a representation of a TOML document.
// A document can be constructed from a `string` buffer or from a file path
pub struct Doc {
pub:
ast &ast.Root
}
// parse_file parses the TOML file in `path`.
pub fn parse_file(path string) ?Doc {
input_config := input.Config{
file_path: path
}
scanner_config := scanner.Config{
input: input_config
}
parser_config := parser.Config{
scanner: scanner.new_scanner(scanner_config) ?
}
mut p := parser.new_parser(parser_config)
ast := p.parse() ?
return Doc{
ast: ast
}
}
// parse_text parses the TOML document provided in `text`.
pub fn parse_text(text string) ?Doc {
input_config := input.Config{
text: text
}
scanner_config := scanner.Config{
input: input_config
}
parser_config := parser.Config{
scanner: scanner.new_scanner(scanner_config) ?
}
mut p := parser.new_parser(parser_config)
ast := p.parse() ?
return Doc{
ast: ast
}
}
// parse parses the TOML document provided in `toml`.
// parse automatically try to determine if the type of `toml` is a file or text.
// For explicit parsing of input types see `parse_file` or `parse_text`.
pub fn parse(toml string) ?Doc {
mut input_config := input.auto_config(toml) ?
scanner_config := scanner.Config{
input: input_config
}
parser_config := parser.Config{
scanner: scanner.new_scanner(scanner_config) ?
}
mut p := parser.new_parser(parser_config)
ast := p.parse() ?
return Doc{
ast: ast
}
}
// to_json returns a compact json string of the complete document.
pub fn (d Doc) to_json() string {
return d.ast.to_json()
}
// to_any converts the `Doc` to toml.Any type.
pub fn (d Doc) to_any() Any {
values := d.ast.table as map[string]ast.Value
return d.ast_to_any(values)
}
// value queries a value from the TOML document.
// `key` should be in "dotted" form (`a.b.c`).
// `key` supports quoted keys like `a."b.c"`.
pub fn (d Doc) value(key string) Any {
values := d.ast.table as map[string]ast.Value
key_split := util.parse_dotted_key(key) or { return Any(Null{}) }
return d.value_(values, key_split)
}
// value_ returns the value found at `key` in the map `values` as `Any` type.
fn (d Doc) value_(values map[string]ast.Value, key []string) Any {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, ' getting "${key[0]}"')
value := values[key[0]] or {
return Any(Null{})
// TODO decide this
// panic(@MOD + '.' + @STRUCT + '.' + @FN + ' key "$key[0]" does not exist')
}
// `match` isn't currently very suitable for these types of sum type constructs...
if value is map[string]ast.Value {
if key.len <= 1 {
return d.ast_to_any(value)
}
m := (value as map[string]ast.Value)
return d.value_(m, key[1..])
}
return d.ast_to_any(value)
}
// ast_to_any_value converts `from` ast.Value to toml.Any value.
fn (d Doc) ast_to_any(value ast.Value) Any {
// `match` isn't currently very suitable for further unwrapping sumtypes in the if's...
if value is ast.Date || value is ast.Time || value is ast.DateTime {
mut tim := time.Time{}
if value is ast.Date {
date_str := (value as ast.Date).text
tim = time.parse_rfc3339(date_str) or {
return Any(Null{})
// TODO decide this
// panic(@MOD + '.' + @STRUCT + '.' + @FN +
// ' failed converting "$date_str" to rfc3339: $err')
}
} else if value is ast.Time {
time_str := (value as ast.Time).text
tim = time.parse_rfc3339(time_str) or {
return Any(Null{})
// TODO decide this
// panic(@MOD + '.' + @STRUCT + '.' + @FN +
// ' failed converting "$time_str" to rfc3339: $err')
}
} else {
// value is ast.DateTime
datetime_str := (value as ast.DateTime).text
tim = time.parse_rfc3339(datetime_str) or {
return Any(Null{})
// TODO decide this
// panic(@MOD + '.' + @STRUCT + '.' + @FN +
// ' failed converting "$datetime_str" to rfc3339: $err')
}
}
return Any(tim)
}
match value {
ast.Quoted {
return Any((value as ast.Quoted).text)
}
ast.Number {
if value.text.contains('.') || value.text.to_lower().contains('e') {
return Any(value.text.f64())
}
v := strconv.parse_int(value.text, 0, 0) or { i64(0) }
return Any(v)
}
ast.Bool {
str := (value as ast.Bool).text
if str == 'true' {
return Any(true)
}
return Any(false)
}
map[string]ast.Value {
m := (value as map[string]ast.Value)
mut am := map[string]Any{}
for k, v in m {
am[k] = d.ast_to_any(v)
}
return am
// return d.get_map_value(m, key_split[1..].join('.'))
}
[]ast.Value {
a := (value as []ast.Value)
mut aa := []Any{}
for val in a {
aa << d.ast_to_any(val)
}
return aa
}
else {
return Any(Null{})
}
}
return Any(Null{})
// TODO decide this
// panic(@MOD + '.' + @STRUCT + '.' + @FN + ' can\'t convert "$value"')
// return Any('')
}
// to_burntsushi returns a BurntSushi compatible json string of the complete document.
pub fn (d Doc) to_burntsushi() string {
return d.to_burntsushi_(d.ast.table)
}
// to_burntsushi returns a BurntSushi compatible json string of the complete document.
fn (d Doc) to_burntsushi_(value ast.Value) string {
match value {
ast.Quoted {
// txt := .replace(r'\\','\\').replace(r'\"','"')
json_text := json2.Any(value.text).json_str()
return '{ "type": "string", "value": "$json_text" }'
}
ast.DateTime {
// Normalization for json
json_text := json2.Any(value.text).json_str().to_upper().replace(' ', 'T')
typ := if json_text.ends_with('Z') || json_text.all_after('T').contains('-')
|| json_text.all_after('T').contains('+') {
'datetime'
} else {
'datetime-local'
}
return '{ "type": "$typ", "value": "$json_text" }'
}
ast.Date {
json_text := json2.Any(value.text).json_str()
return '{ "type": "date-local", "value": "$json_text" }'
}
ast.Time {
json_text := json2.Any(value.text).json_str()
return '{ "type": "time-local", "value": "$json_text" }'
}
ast.Bool {
json_text := json2.Any(value.text.bool()).json_str()
return '{ "type": "bool", "value": "$json_text" }'
}
ast.Null {
json_text := json2.Any(value.text).json_str()
return '{ "type": "null", "value": "$json_text" }'
}
ast.Number {
if value.text.contains('.') || value.text.to_lower().contains('e') {
json_text := value.text.f64()
return '{ "type": "float", "value": "$json_text" }'
}
i64_ := strconv.parse_int(value.text, 0, 0) or { i64(0) }
return '{ "type": "integer", "value": "$i64_" }'
}
map[string]ast.Value {
mut str := '{ '
for key, val in value {
json_key := json2.Any(key).json_str()
str += ' "$json_key": ${d.to_burntsushi_(val)},'
// str += d.to_burntsushi_(val, indent+1)
}
str = str.trim_right(',')
str += ' }'
return str
}
[]ast.Value {
mut str := '[ '
for val in value {
str += ' ${d.to_burntsushi_(val)},'
}
str = str.trim_right(',')
str += ' ]\n'
return str
}
}
return '<error>'
}