Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
## Description
`toml` is a fully fledged [TOML v1.0.0](https://toml.io/en/v1.0.0) compatible parser written in pure V.
The module is tested against the [official compliance tests](https://github.com/toml-lang/compliance).
## Usage
Parsing files or `string`s containing TOML is easy.
Simply import the `toml` module and do:
```v ignore
doc1 := toml.parse_text(<string content>) or { panic(err) }
doc2 := toml.parse_file(<file path>) or { panic(err) }
```
## Example
Here we parse the official [TOML example](https://github.com/toml-lang/toml/blob/3b11f6921da7b6f5db37af039aa021fee450c091/README.md#Example)
and print out some values.
```v
import toml
const toml_text = '# This is a TOML document.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates
[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]'
fn main() {
doc := toml.parse_text(toml_text) or { panic(err) }
title := doc.value('title').string()
println('title: "${title}"')
ip := doc.value('servers.alpha.ip').string()
println('Server IP: "${ip}"')
}
```
## Value retrieval
The `toml` module supports easy retrieval of values from
TOML documents by using a small and simple query syntax
as argument to the `value()` function.
Keys in map entries are denoted by `.` and array entries
uses `[<int>]`. Quoted keys are also supported by using
the delimiters `"` or `'`.
`doc.value('table.array[0].a."b.c"')`
To query for a value that might not be in the document you
can use the `.default_to(...)` function to provide a
default value.
For cases where a default value might not be appropriate or
to check if a value exists you can use `doc.value_opt('query')!`
instead.
```v
import toml
const toml_text = '
val = true
[table]
array = [
{ a = "A" },
{ b = "B" }
]
'
doc := toml.parse_text(toml_text) or { panic(err) }
assert doc.value('val').bool() == true
assert doc.value('table.array[0].a').string() == 'A'
// Provides a default value
assert doc.value('non.existing').default_to(false).bool() == false
// Check if value exist
// doc.value_opt('should.exist') or { ... }
// or
if value := doc.value_opt('table.array[1].b') {
assert value.string() == 'B'
}
// You can pass parts of the TOML document around
// and still use .value()/.value_opt() to get the values
arr := doc.value('table.array')
assert arr.value('[1].b').string() == 'B'
```
### Conversion
Any TOML value can be converted to a V equivalent type.
TOML values are represented as the `toml.Any` sum-type that
can be converted to a V type.
| TOML value | V conversion (via `toml.Any`) |
| -------------------------- | ----------------------------- |
| val = "Basic string" | .string() |
| val = 'Literal string' | .string() |
| val = true | .bool() |
| val = 1979-05-27T07:32:00Z | .datetime() (toml.DateTime) |
| val = 1979-05-27 | .date() (toml.Date) |
| val = 07:32:59 | .time() (toml.Time) |
| val = 3.14 | .f32() / .f64() |
| val = 100 | .int() / .i64() / .u64() |
Read more about values in the [TOML specification](https://toml.io/en/v1.0.0#spec).
## TOML to JSON
The `toml.to` module supports easy serialization of any TOML to JSON.
```v
import toml
import toml.to
const toml_text = '
val = true
[table]
array = [
{ a = "A" },
{ b = "B" }
]
'
doc := toml.parse_text(toml_text) or { panic(err) }
assert to.json(doc) == '{ "val": true, "table": { "array": [ { "a": "A" }, { "b": "B" } ] } }'
```
+516
View File
@@ -0,0 +1,516 @@
// 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
// Pretty much all the same builtin types as the `json2.Any` type plus `DateTime`,`Date`,`Time`
pub type Any = Date
| DateTime
| Null
| Time
| []Any
| bool
| f32
| f64
| i64
| int
| map[string]Any
| string
| u64
// string returns `Any` as a string.
pub fn (a Any) string() string {
match a {
// NOTE if `.clone()` is not used here:
// string { return a as string }
// ... certain call-patterns to this function will cause a memory corruption.
// See `tests/toml_memory_corruption_test.v` for a matching regression test.
string { return (a as string).clone() }
DateTime { return a.str().clone() }
Date { return a.str().clone() }
Time { return a.str().clone() }
else { return a.str().clone() }
}
}
// to_toml returns `Any` as a TOML encoded value.
pub fn (a Any) to_toml() string {
match a {
map[string]Any {
// TODO: more format control?
return a.to_inline_toml()
}
[]Any {
return a.to_toml()
}
bool, f32, f64, i64, int, u64 {
return a.str().clone()
}
// NOTE if `.clone()` is not used here:
// string { return a as string }
// ... certain call-patterns to this function will cause a memory corruption.
// See `tests/toml_memory_corruption_test.v` for a matching regression test.
string {
return '"' + (a as string).clone() + '"'
}
DateTime {
return a.str().clone()
}
Date {
return a.str().clone()
}
Time {
return a.str().clone()
}
else {
return a.str().clone()
}
}
}
// int returns `Any` as an 32-bit integer.
pub fn (a Any) int() int {
match a {
int { return a }
i64, f32, f64, bool { return int(a) }
// time.Time { return int(0) } // TODO
else { return 0 }
}
}
// i64 returns `Any` as a 64-bit integer.
pub fn (a Any) i64() i64 {
match a {
i64 { return a }
int, f32, f64, bool { return i64(a) }
// time.Time { return i64(0) } // TODO
else { return 0 }
}
}
// u64 returns `Any` as a 64-bit unsigned integer.
pub fn (a Any) u64() u64 {
match a {
u64 { return a }
int, i64, f32, f64, bool { return u64(a) }
// time.Time { return u64(0) } // TODO
else { return 0 }
}
}
// f32 returns `Any` as a 32-bit float.
pub fn (a Any) f32() f32 {
match a {
f32 { return a }
int, i64, f64 { return f32(a) }
// time.Time { return f32(0) } // TODO
else { return 0.0 }
}
}
// f64 returns `Any` as a 64-bit float.
pub fn (a Any) f64() f64 {
match a {
f64 { return a }
int, i64, f32 { return f64(a) }
// time.Time { return f64(0) } // TODO
else { return 0.0 }
}
}
// array returns `Any` as an array.
pub fn (a Any) array() []Any {
if a is []Any {
return a
} else if a is map[string]Any {
mut arr := []Any{}
for _, v in a {
arr << v
}
return arr
}
return [a]
}
// as_map returns `Any` as a map (TOML table).
pub fn (a Any) as_map() map[string]Any {
if a is map[string]Any {
return a
} else if a is []Any {
mut mp := map[string]Any{}
for i, fi in a {
mp['${i}'] = fi
}
return mp
}
return {
'0': a
}
}
// bool returns `Any` as a boolean.
pub fn (a Any) bool() bool {
match a {
bool { return a }
string { return a.bool() }
else { return false }
}
}
// date returns `Any` as a `toml.Date` struct.
pub fn (a Any) date() Date {
match a {
// string { } // TODO
// NOTE `.clone()` is to avoid memory corruption see `pub fn (a Any) string() string`
Date { return Date{a.str().clone()} }
else { return Date{''} }
}
}
// time returns `Any` as a `toml.Time` struct.
pub fn (a Any) time() Time {
match a {
// string { } // TODO
// NOTE `.clone()` is to avoid memory corruption see `pub fn (a Any) string() string`
Time { return Time{a.str().clone()} }
else { return Time{''} }
}
}
// datetime returns `Any` as a `toml.DateTime` struct.
pub fn (a Any) datetime() DateTime {
match a {
// string { } // TODO
// NOTE `.clone()` is to avoid memory corruption see `pub fn (a Any) string() string`
DateTime { return DateTime{a.str().clone()} }
else { return DateTime{''} }
}
}
// default_to returns `value` if `a Any` is `Null`.
// This can be used to set default values when retrieving
// values. E.g.: `toml_doc.value('wrong.key').default_to(123).int()`
pub fn (a Any) default_to(value Any) Any {
match a {
Null { return value }
else { return a }
}
}
// value queries a value from the map.
// `key` supports a small query syntax scheme:
// Maps can be queried in "dotted" form e.g. `a.b.c`.
// quoted keys are supported as `a."b.c"` or `a.'b.c'`.
// Arrays can be queried with `a[0].b[1].[2]`.
pub fn (m map[string]Any) value(key string) Any {
return Any(m).value(key)
}
// as_strings returns the contents of the map
// as `map[string]string`
pub fn (m map[string]Any) as_strings() map[string]string {
mut result := map[string]string{}
for k, v in m {
result[k] = v.string()
}
return result
}
// to_toml returns the contents of the map
// as a TOML encoded `string`.
pub fn (m map[string]Any) to_toml() string {
mut toml_text := ''
for k, v in m {
key := if k.contains(' ') { '"${k}"' } else { k }
toml_text += '${key} = ${v.to_toml()}\n'
}
toml_text = toml_text.trim_right('\n')
return toml_text
}
// to_inline_toml returns the contents of the map
// as an inline table encoded TOML `string`.
pub fn (m map[string]Any) to_inline_toml() string {
mut toml_text := '{'
mut i := 1
for k, v in m {
key := if k.contains(' ') { '"${k}"' } else { k }
delimiter := if i < m.len { ',' } else { '' }
toml_text += ' ${key} = ${v.to_toml()}${delimiter}'
i++
}
return toml_text + ' }'
}
// value queries a value from the array.
// `key` supports a small query syntax scheme:
// The array can be queried with `[0].b[1].[2]`.
// Maps can be queried in "dotted" form e.g. `a.b.c`.
// quoted keys are supported as `a."b.c"` or `a.'b.c'`.
pub fn (a []Any) value(key string) Any {
return Any(a).value(key)
}
// as_strings returns the contents of the array
// as `[]string`
pub fn (a []Any) as_strings() []string {
mut sa := []string{}
for any in a {
sa << any.string()
}
return sa
}
// to_toml returns the contents of the array
// as a TOML encoded `string`.
pub fn (a []Any) to_toml() string {
mut toml_text := '[\n'
for any in a {
toml_text += ' ' + any.to_toml() + ',\n'
}
toml_text = toml_text.trim_right(',\n')
return toml_text + '\n]'
}
// value queries a value from the `Any` type.
// `key` supports a small query syntax scheme:
// Maps can be queried in "dotted" form e.g. `a.b.c`.
// quoted keys are supported as `a."b.c"` or `a.'b.c'`.
// Arrays can be queried with `a[0].b[1].[2]`.
pub fn (a Any) value(key string) Any {
key_split := parse_dotted_key(key) or { return null }
return a.value_(a, key_split)
}
// value_opt queries a value from the current element's tree. Returns an error
// if the key is not valid or there is no value for the key.
pub fn (a Any) value_opt(key string) !Any {
key_split := parse_dotted_key(key) or { return error('invalid dotted key') }
x := a.value_(a, key_split)
if x is Null {
return error('no value for key')
}
return x
}
// value_ returns the `Any` value found at `key`.
fn (a Any) value_(value Any, key []string) Any {
if key.len == 0 {
return null
}
mut any_value := null
k, index := parse_array_key(key[0])
if k == '' {
arr := value as []Any
any_value = arr[index] or { return null }
}
if value is map[string]Any {
any_value = value[k] or { return null }
if index > -1 {
arr := any_value as []Any
any_value = arr[index] or { return null }
}
}
if key.len <= 1 {
return any_value
}
match any_value {
map[string]Any, []Any {
return a.value_(any_value, key[1..])
}
else {
return value
}
}
}
// reflect returns `T` with `T.<field>`'s value set to the
// value of any 1st level TOML key by the same name.
pub fn (a Any) reflect[T]() T {
mut reflected := T{}
$for field in T.fields {
mut toml_field_name := field.name
mut skip := false
// Remapping of field names, for example:
// TOML: 'assert = "ok"'
// V: User { asrt string @[toml: 'assert'] }
// User.asrt == 'ok'
for attr in field.attrs {
if attr == 'skip' {
skip = true
break
}
if attr.starts_with('toml:') {
toml_field_name = attr.all_after(':').trim_space()
}
}
value := a.value(toml_field_name)
// only set the field's value when value != null and !skip, else field got it's default value
if !skip && value != null {
$if field.typ is string {
reflected.$(field.name) = value.string()
} $else $if field.typ is bool {
reflected.$(field.name) = value.bool()
} $else $if field.typ is int {
reflected.$(field.name) = value.int()
} $else $if field.typ is f32 {
reflected.$(field.name) = value.f32()
} $else $if field.typ is f64 {
reflected.$(field.name) = value.f64()
} $else $if field.typ is i64 {
reflected.$(field.name) = value.i64()
} $else $if field.typ is u64 {
reflected.$(field.name) = value.u64()
} $else $if field.typ is Any {
reflected.$(field.name) = value
} $else $if field.typ is DateTime {
reflected.$(field.name) = value.datetime()
} $else $if field.typ is Date {
reflected.$(field.name) = value.date()
} $else $if field.typ is Time {
reflected.$(field.name) = value.time()
}
// Arrays of primitive types
$else $if field.typ is []string {
any_array := value.array()
reflected.$(field.name) = any_array.as_strings()
} $else $if field.typ is []bool {
any_array := value.array()
mut arr := []bool{cap: any_array.len}
for any_value in any_array {
arr << any_value.bool()
}
reflected.$(field.name) = arr
} $else $if field.typ is []int {
any_array := value.array()
mut arr := []int{cap: any_array.len}
for any_value in any_array {
arr << any_value.int()
}
reflected.$(field.name) = arr
} $else $if field.typ is []f32 {
any_array := value.array()
mut arr := []f32{cap: any_array.len}
for any_value in any_array {
arr << any_value.f32()
}
reflected.$(field.name) = arr
} $else $if field.typ is []f64 {
any_array := value.array()
mut arr := []f64{cap: any_array.len}
for any_value in any_array {
arr << any_value.f64()
}
reflected.$(field.name) = arr
} $else $if field.typ is []i64 {
any_array := value.array()
mut arr := []i64{cap: any_array.len}
for any_value in any_array {
arr << any_value.i64()
}
reflected.$(field.name) = arr
} $else $if field.typ is []u64 {
any_array := value.array()
mut arr := []u64{cap: any_array.len}
for any_value in any_array {
arr << any_value.u64()
}
reflected.$(field.name) = arr
} $else $if field.typ is []Any {
reflected.$(field.name) = value.array()
} $else $if field.typ is []DateTime {
any_array := value.array()
mut arr := []DateTime{cap: any_array.len}
for any_value in any_array {
arr << any_value.datetime()
}
reflected.$(field.name) = arr
} $else $if field.typ is []Date {
any_array := value.array()
mut arr := []Date{cap: any_array.len}
for any_value in any_array {
arr << any_value.date()
}
reflected.$(field.name) = arr
} $else $if field.typ is []Time {
any_array := value.array()
mut arr := []Time{cap: any_array.len}
for any_value in any_array {
arr << any_value.time()
}
reflected.$(field.name) = arr
}
// String key maps of primitive types
$else $if field.typ is map[string]string {
any_map := value.as_map()
reflected.$(field.name) = any_map.as_strings()
} $else $if field.typ is map[string]bool {
any_map := value.as_map()
mut type_map := map[string]bool{}
for k, any_value in any_map {
type_map[k] = any_value.bool()
}
reflected.$(field.name) = type_map.clone()
} $else $if field.typ is map[string]int {
any_map := value.as_map()
mut type_map := map[string]int{}
for k, any_value in any_map {
type_map[k] = any_value.int()
}
reflected.$(field.name) = type_map.clone()
} $else $if field.typ is map[string]f32 {
any_map := value.as_map()
mut type_map := map[string]f32{}
for k, any_value in any_map {
type_map[k] = any_value.f32()
}
reflected.$(field.name) = type_map.clone()
} $else $if field.typ is map[string]f64 {
any_map := value.as_map()
mut type_map := map[string]f64{}
for k, any_value in any_map {
type_map[k] = any_value.f64()
}
reflected.$(field.name) = type_map.clone()
} $else $if field.typ is map[string]i64 {
any_map := value.as_map()
mut type_map := map[string]i64{}
for k, any_value in any_map {
type_map[k] = any_value.i64()
}
reflected.$(field.name) = type_map.clone()
} $else $if field.typ is map[string]u64 {
any_map := value.as_map()
mut type_map := map[string]u64{}
for k, any_value in any_map {
type_map[k] = any_value.u64()
}
reflected.$(field.name) = type_map.clone()
} $else $if field.typ is map[string]Any {
reflected.$(field.name) = value.as_map()
} $else $if field.typ is map[string]DateTime {
any_map := value.as_map()
mut type_map := map[string]DateTime{}
for k, any_value in any_map {
type_map[k] = any_value.datetime()
}
reflected.$(field.name) = type_map.clone()
} $else $if field.typ is map[string]Date {
any_map := value.as_map()
mut type_map := map[string]Date{}
for k, any_value in any_map {
type_map[k] = any_value.date()
}
reflected.$(field.name) = type_map.clone()
} $else $if field.typ is map[string]Time {
any_map := value.as_map()
mut type_map := map[string]Time{}
for k, any_value in any_map {
type_map[k] = any_value.time()
}
reflected.$(field.name) = type_map.clone()
}
}
}
return reflected
}
+26
View File
@@ -0,0 +1,26 @@
// 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 ast
import toml.input
// Root represents the root structure of any parsed TOML text snippet or file.
@[heap]
pub struct Root {
pub:
input input.Config // User input configuration
pub mut:
comments []Comment
table Value
// errors []errors.Error // all the checker errors in the file
}
// str returns the string representation of the root node.
pub fn (r Root) str() string {
mut s := typeof(r).name + '{\n'
s += ' input: ${r.input}\n'
s += ' table: ${r.table}\n'
s += '}'
return s
}
+267
View File
@@ -0,0 +1,267 @@
// 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 ast
import toml.token
import strconv
// Key is a sumtype representing all types of keys that
// can be found in a TOML document.
pub type Key = Bare | Bool | Null | Number | Quoted
// str returns the string representation of the key. This is implemented
// by all the variants of Key.
pub fn (k Key) str() string {
return k.text.clone()
}
// Value is a sumtype representing all possible value types
// found in a TOML document.
pub type Value = Bool
| Date
| DateTime
| Null
| Number
| Quoted
| Time
| []Value
| map[string]Value
// str outputs the value in JSON-like format for eased
// debugging
pub fn (v Value) str() string {
match v {
Quoted, Date, DateTime, Time {
return '"${v.text}"'
}
Bool, Null, Number {
return v.text
}
map[string]Value {
mut str := '{'
for key, val in v {
str += ' "${key}": ${val},'
}
str = str.trim_right(',')
str += ' }'
return str
}
[]Value {
mut str := '['
for val in v {
str += ' ${val},'
}
str = str.trim_right(',')
str += ' ]'
return str
}
}
}
// DateTimeType is a sumtype representing all possible date types
// found in a TOML document.
pub type DateTimeType = Date | DateTime | Time
// str returns the `string` representation of the `DateTimeType` type.
pub fn (dtt DateTimeType) str() string {
return dtt.text
}
// Comment is the data representation of a TOML comment (`# This is a comment`).
pub struct Comment {
pub:
text string
pos token.Pos
}
// str returns the `string` representation of the `Comment` type.
pub fn (c Comment) str() string {
mut s := typeof(c).name + '{\n'
s += ' text: \'${c.text}\'\n'
s += ' pos: ${c.pos}\n'
s += '}'
return s
}
// Null is used in sumtype checks as a "default" value when nothing else is possible.
pub struct Null {
pub:
text string
pos token.Pos
}
// str returns the `string` representation of the `Null` type
pub fn (n Null) str() string {
return n.text
}
// Quoted is the data representation of a TOML quoted type (`"quoted-key" = "I'm a quoted value"`).
// Quoted types can appear both as keys and values in TOML documents.
pub struct Quoted {
pub mut:
text string
pub:
pos token.Pos
is_multiline bool
quote u8
}
// str returns the `string` representation of the `Quoted` type.
pub fn (q Quoted) str() string {
mut str := typeof(q).name + '{\n'
str += ' text: \'${q.text}\'\n'
str += ' pos: ${q.pos}\n'
str += ' is_multiline: ${q.is_multiline}\n'
str += ' quote: \'${q.quote}\'\n'
str += '}'
return str
}
// Bare is the data representation of a TOML bare type (`bare_key = ...`).
// Bare types can appear only as keys in TOML documents. Otherwise they take the
// form of Bool or Numbers.
pub struct Bare {
pub:
text string
pos token.Pos
}
// str returns the `string` representation of the `Bare` type.
pub fn (b Bare) str() string {
mut str := typeof(b).name + '{\n'
str += ' text: \'${b.text}\'\n'
str += ' pos: ${b.pos}\n'
str += '}'
return str
}
// Bool is the data representation of a TOML boolean type (`... = true`).
// Bool types can appear only as values in TOML documents. Keys named `true` or `false`
// are considered as Bare types.
pub struct Bool {
pub:
text string
pos token.Pos
}
// str returns the `string` representation of the `Bool` type.
pub fn (b Bool) str() string {
mut str := typeof(b).name + '{\n'
str += ' text: \'${b.text}\'\n'
str += ' pos: ${b.pos}\n'
str += '}'
return str
}
// Number is the data representation of a TOML number type (`25 = 5e2`).
// Number types can appear both as keys and values in TOML documents.
// Number can be integers, floats, infinite, NaN - they can have exponents (`5e2`) and be sign prefixed (`+2`).
pub struct Number {
pub:
pos token.Pos
pub mut:
text string
}
// str returns the `string` representation of the `Number` type.
pub fn (n Number) str() string {
mut str := typeof(n).name + '{\n'
str += ' text: \'${n.text}\'\n'
str += ' pos: ${n.pos}\n'
str += '}'
return str
}
// i64 returns the `n Number` as an `i64` value.
pub fn (n Number) i64() i64 {
if n.text.starts_with('0x') {
hex := n.text.all_after('0x').to_upper().replace('_', '')
return strconv.parse_int(hex, 16, 64) or { i64(0) }
} else if n.text.starts_with('0o') {
oct := n.text.all_after('0o').replace('_', '')
return strconv.parse_int(oct, 8, 64) or { i64(0) }
} else if n.text.starts_with('0b') {
bin := n.text.all_after('0b').replace('_', '')
return strconv.parse_int(bin, 2, 64) or { i64(0) }
}
return strconv.parse_int(n.text, 0, 64) or { i64(0) }
}
// f64 returns the `n Number` as an `f64` value.
pub fn (n Number) f64() f64 {
return n.text.replace('_', '').f64()
}
// Date is the data representation of a TOML date type (`YYYY-MM-DD`).
// Date types can appear both as keys and values in TOML documents.
// Keys named like dates e.g. `1980-12-29` are considered Bare key types.
pub struct Date {
pub:
text string
pos token.Pos
}
// str returns the `string` representation of the `Date` type.
pub fn (d Date) str() string {
mut str := typeof(d).name + '{\n'
str += ' text: \'${d.text}\'\n'
str += ' pos: ${d.pos}\n'
str += '}'
return str
}
// Time is the data representation of a TOML time type (`HH:MM:SS.milli`).
// Time types can appear only as values in TOML documents.
pub struct Time {
pub:
text string
offset int
pos token.Pos
}
// str returns the `string` representation of the `Time` type.
pub fn (t Time) str() string {
mut str := typeof(t).name + '{\n'
str += ' text: \'${t.text}\'\n'
str += ' offset: \'${t.offset}\'\n'
str += ' pos: ${t.pos}\n'
str += '}'
return str
}
// DateTime is the data representation of a TOML date-time type (`YYYY-MM-DDTHH:MM:SS.milli`).
// DateTime types can appear only as values in TOML documents.
pub struct DateTime {
pub mut:
text string
pub:
pos token.Pos
date Date
time Time
}
// str returns the `string` representation of the `DateTime` type.
pub fn (dt DateTime) str() string {
mut str := typeof(dt).name + '{\n'
str += ' text: \'${dt.text}\'\n'
str += ' date: \'${dt.date}\'\n'
str += ' time: \'${dt.time}\'\n'
str += ' pos: ${dt.pos}\n'
str += '}'
return str
}
// EOF is the data representation of the end of the TOML document.
pub struct EOF {
pub:
pos token.Pos
}
// str returns the `string` representation of the `EOF` type.
pub fn (e EOF) str() string {
mut str := typeof(e).name + '{\n'
str += ' pos: ${e.pos}\n'
str += '}'
return str
}
+70
View File
@@ -0,0 +1,70 @@
module walker
import toml.ast
// Visitor defines a visit method which is invoked by the walker on each Value node it encounters.
pub interface Visitor {
visit(value &ast.Value) !
}
// Modifier defines a modify method which is invoked by the walker on each Value node it encounters.
pub interface Modifier {
modify(mut value ast.Value) !
}
pub type InspectorFn = fn (value &ast.Value, data voidptr) !
struct Inspector {
inspector_callback InspectorFn = unsafe { nil }
mut:
data voidptr
}
// visit calls the inspector callback on the specified Value node.
pub fn (i &Inspector) visit(value &ast.Value) ! {
i.inspector_callback(value, i.data) or { return err }
}
// inspect traverses and checks the AST Value node on a depth-first order and based on the data given
pub fn inspect(value &ast.Value, data voidptr, inspector_callback InspectorFn) ! {
walk(Inspector{inspector_callback, data}, value)!
}
// walk traverses the AST using the given visitor
@[autofree_bug; manualfree]
pub fn walk(visitor Visitor, value &ast.Value) ! {
if value is map[string]ast.Value {
value_map := value as map[string]ast.Value
for _, val in value_map {
walk(visitor, &val)!
}
}
if value is []ast.Value {
value_array := value as []ast.Value
for val in value_array {
walk(visitor, &val)!
}
} else {
visitor.visit(value)!
}
}
// walk_and_modify traverses the AST using the given modifier and lets the visitor
// modify the contents.
@[autofree_bug; manualfree]
pub fn walk_and_modify(modifier Modifier, mut value ast.Value) ! {
if value is map[string]ast.Value {
mut value_map := value as map[string]ast.Value
for _, mut val in value_map {
walk_and_modify(modifier, mut &val)!
}
}
if value is []ast.Value {
mut value_array := value as []ast.Value
for mut val in value_array {
walk_and_modify(modifier, mut &val)!
}
} else {
modifier.modify(mut value)!
}
}
+665
View File
@@ -0,0 +1,665 @@
// 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 checker
import toml.ast
import toml.ast.walker
import toml.util
import toml.token
import toml.scanner
import encoding.utf8
import time
import strconv
pub const allowed_basic_escape_chars = [`u`, `U`, `b`, `t`, `n`, `f`, `r`, `"`, `\\`]
// utf8_max is the largest inclusive value of the Unicodes scalar value ranges.
const utf8_max = 0x10FFFF
fn allowed_basic_escape_char_list() []u8 {
return [u8(`u`), `U`, `b`, `t`, `n`, `f`, `r`, `"`, `\\`]
}
fn toml_parse_time(s string) !time.Time {
if s.len > 3 && s[2] == `:` {
// complete the partial time, with an arbitrary date:
return time.parse_rfc3339('0001-01-01T' + s)
}
if s.len == 10 {
// complete the partial date, with zero time and zero timezone
return time.parse_rfc3339(s + 'T00:00:00Z')
}
return time.parse_rfc3339(s)!
}
// Checker checks a tree of TOML `ast.Value`'s for common errors.
pub struct Checker {
pub:
scanner &scanner.Scanner = unsafe { nil }
}
// check checks the `ast.Value` and all it's children
// for common errors.
pub fn (c &Checker) check(n &ast.Value) ! {
walker.walk(c, n)!
}
fn (c &Checker) visit(value &ast.Value) ! {
match value {
ast.Bool {
c.check_boolean(value)!
}
ast.Number {
c.check_number(value)!
}
ast.Quoted {
c.check_quoted(value)!
}
ast.DateTime {
c.check_date_time(value)!
}
ast.Date {
c.check_date(value)!
}
ast.Time {
c.check_time(value)!
}
else {}
}
}
// excerpt returns a string of the token's surroundings
fn (c &Checker) excerpt(tp token.Pos) string {
return c.scanner.excerpt(tp.pos, 10)
}
// is_hex_bin_oct_prefixed returns true if `hbo` has either
// of: `0x`, `0o` or `0b` - as a prefix.
// Example: assert is_hex_bin_oct_prefixed('0xAF') == true
// Example: assert is_hex_bin_oct_prefixed('xAF') == false
fn is_hex_bin_oct_prefixed(hbo string) bool {
return hbo.len > 2 && (hbo.starts_with('0x') || hbo.starts_with('0o') || hbo.starts_with('0b'))
}
// has_repeating returns true if `str` has one or more repeating
// `rune` characters provided in `repeats`.
// Example: assert has_repeating('hello__v.', [`.`,`_`]) == true
// Example: assert has_repeating('hello_v.', [`.`,`_`]) == false
fn has_repeating(str string, repeats []rune) bool {
for i, r in str {
if r in repeats && i + 1 < str.len {
if r == str[i + 1] {
return true
}
}
}
return false
}
// check_number returns an error if `num` is not a valid TOML number.
fn (c &Checker) check_number(num ast.Number) ! {
lit := num.text
lit_lower_case := lit.to_lower()
if lit in ['0', '0.0', '+0', '-0', '+0.0', '-0.0', '0e0', '+0e0', '-0e0', '0e00'] {
return
}
if lit.contains('_') {
if lit.starts_with('_') || lit.ends_with('_') {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" can not start or end with `_` in ...${c.excerpt(num.pos)}...')
}
if lit.contains('__') {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" can not have more than one underscore (`_`) in ...${c.excerpt(num.pos)}...')
}
}
mut hex_bin_oct := is_hex_bin_oct_prefixed(lit)
mut is_bin, mut is_oct, mut is_hex := false, false, false
is_float := lit_lower_case.all_before('e').contains('.')
has_exponent_notation := lit_lower_case.contains('e')
float_decimal_index := lit.index_('.')
// mut is_first_digit := u8(lit[0]).is_digit()
mut ascii := u8(lit[0]).ascii_str()
is_sign_prefixed := lit[0] in [`+`, `-`]
mut lit_sans_sign := lit
if is_sign_prefixed { // +/- ...
lit_sans_sign = lit[1..]
hex_bin_oct = is_hex_bin_oct_prefixed(lit_sans_sign)
if hex_bin_oct {
ascii = u8(lit[0]).ascii_str()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" (hex, octal and binary) can not start with `${ascii}` in ...${c.excerpt(num.pos)}...')
}
if lit.len > 1 && lit_sans_sign.starts_with('0') && !lit_sans_sign.starts_with('0.') {
ascii = u8(lit_sans_sign[0]).ascii_str()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" can not start with `${ascii}` in ...${c.excerpt(num.pos)}...')
}
} else {
if !hex_bin_oct {
if !is_float && lit[0] == `0` {
if lit[1] in [`B`, `O`, `X`] {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" only lowercase notation in ...${c.excerpt(num.pos)}...')
}
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" can not start with a zero in ...${c.excerpt(num.pos)}...')
}
if is_float && lit[0] == `0` && float_decimal_index > 1 {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" can not start with a zero in ...${c.excerpt(num.pos)}...')
}
}
}
if has_repeating(lit, [`_`, `.`, `b`, `o`, `x`]) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" can not have ${scanner.digit_extras} as repeating characters in ...${c.excerpt(num.pos)}...')
}
if hex_bin_oct {
is_bin = lit_sans_sign.starts_with('0b')
is_oct = lit_sans_sign.starts_with('0o')
is_hex = lit_sans_sign.starts_with('0x')
lit_sans_sign_and_type_prefix := lit_sans_sign[2..]
if lit_sans_sign_and_type_prefix.starts_with('_')
|| lit_sans_sign_and_type_prefix.ends_with('_') {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" can not start or end with `_` in ...${c.excerpt(num.pos)}...')
}
if is_bin {
if !c.is_valid_binary_literal(lit_sans_sign_and_type_prefix) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid binary number in ...${c.excerpt(num.pos)}...')
}
} else if is_oct {
if !c.is_valid_octal_literal(lit_sans_sign_and_type_prefix) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid octal number in ...${c.excerpt(num.pos)}...')
}
} else {
if !c.is_valid_hex_literal(lit_sans_sign_and_type_prefix) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid hexadecimal number in ...${c.excerpt(num.pos)}...')
}
}
}
if has_exponent_notation {
if lit_lower_case.all_after('e').starts_with('_')
|| lit_lower_case.all_before('e').ends_with('_') {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' the exponent in "${lit}" can not start nor end with an underscore in ...${c.excerpt(num.pos)}...')
}
if lit_lower_case.all_after('e').contains('.') {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" (with exponent) can not have a decimal point in ...${c.excerpt(num.pos)}...')
}
if !is_hex && lit_lower_case.count('e') > 1 {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" (with exponent) can only have one exponent in ...${c.excerpt(num.pos)}...')
}
}
if is_float {
if lit.count('.') > 1 {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" (float) can only have one decimal point in ...${c.excerpt(num.pos)}...')
}
last := lit[lit.len - 1]
if last in scanner.digit_extras {
ascii = u8(last).ascii_str()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" (float) can not end with `${ascii}` in ...${c.excerpt(num.pos)}...')
}
if lit.contains('_.') || lit.contains('._') {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" (float) can not have underscores before or after the decimal point in ...${c.excerpt(num.pos)}...')
}
if lit_lower_case.contains('e.') || lit.contains('.e') {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" (float) can not have decimal points on either side of the exponent notation in ...${c.excerpt(num.pos)}...')
}
// Check if it contains other chars than the allowed
for r in lit {
if r !in [`0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `.`, `e`, `E`, `-`, `+`,
`_`] {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" (float) can not contain `${u8(r).ascii_str()}` in ...${c.excerpt(num.pos)}...')
}
}
} else {
if lit.len > 1 && lit.starts_with('0') && lit[1] !in [`b`, `o`, `x`] {
ascii = u8(lit[0]).ascii_str()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' numbers like "${lit}" can not start with `${ascii}` in ...${c.excerpt(num.pos)}...')
}
}
}
// is_valid_binary_literal returns true if `num` is valid TOML binary literal.
fn (c &Checker) is_valid_binary_literal(num string) bool {
for ch in num {
if ch == `_` {
continue
}
if !(ch >= `0` && ch <= `1`) {
return false
}
}
return true
}
// is_valid_octal_literal returns true if `num` is valid TOML octal literal.
fn (c &Checker) is_valid_octal_literal(num string) bool {
for ch in num {
if ch == `_` {
continue
}
if !(ch >= `0` && ch <= `7`) {
return false
}
}
return true
}
// is_valid_hex_literal returns true if `num` is valid TOML hexadecimal literal.
fn (c &Checker) is_valid_hex_literal(num string) bool {
for ch in num {
if ch == `_` {
continue
}
if !ch.is_hex_digit() {
return false
}
}
return true
}
// check_boolean returns an error if `b` is not a valid TOML boolean.
fn (c &Checker) check_boolean(b ast.Bool) ! {
lit := b.text
if lit in ['true', 'false'] {
return
}
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' boolean values like "${lit}" can only be `true` or `false` literals, not `${lit}` in ...${c.excerpt(b.pos)}...')
}
// check_date_time returns an error if `dt` is not a valid TOML date-time string (RFC 3339).
// See also https://ijmacd.github.io/rfc3339-iso8601 for a more
// visual representation of the RFC 3339 format.
fn (c &Checker) check_date_time(dt ast.DateTime) ! {
lit := dt.text
mut split := []string{}
// RFC 3339 Date-Times can be split via 4 separators (` `, `_`, `T` and `t`).
if lit.to_lower().contains_any(' _t') {
if lit.contains(' ') {
split = lit.split(' ')
} else if lit.contains('_') {
split = lit.split('_')
} else if lit.contains('T') {
split = lit.split('T')
} else if lit.contains('t') {
split = lit.split('t')
}
// Validate the split into date and time parts.
if split.len != 2 {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" contains too many date/time separators in ...${c.excerpt(dt.pos)}...')
}
// Re-use date and time validation code for detailed testing of each part
c.check_date(ast.Date{
text: split[0]
pos: token.Pos{
len: split[0].len
line_nr: dt.pos.line_nr
pos: dt.pos.pos
col: dt.pos.col
}
})!
c.check_time(ast.Time{
text: split[1]
pos: token.Pos{
len: split[1].len
line_nr: dt.pos.line_nr
pos: dt.pos.pos + split[0].len
col: dt.pos.col + split[0].len
}
})!
// Simulate a time offset if it's missing then it can be checked. Already toml supports local time and rfc3339 don't.
mut has_time_offset := false
for ch in lit#[19..] {
if ch in [u8(`-`), `+`, `Z`] {
has_time_offset = true
break
}
}
mut lit_with_offset := lit
if !has_time_offset {
lit_with_offset += 'Z'
}
toml_parse_time(lit_with_offset) or {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid RFC 3339 Date-Time format string "${err}". In ...${c.excerpt(dt.pos)}...')
}
} else {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid RFC 3339 Date-Time format string in ...${c.excerpt(dt.pos)}...')
}
}
// check_time returns an error if `date` is not a valid TOML date string (RFC 3339).
fn (c &Checker) check_date(date ast.Date) ! {
lit := date.text
parts := lit.split('-')
if parts.len != 3 {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid RFC 3339 Date format string in ...${c.excerpt(date.pos)}...')
}
yyyy := parts[0]
if yyyy.len != 4 {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" does not have a valid RFC 3339 year indication in ...${c.excerpt(date.pos)}...')
}
mm := parts[1]
if mm.len != 2 {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" does not have a valid RFC 3339 month indication in ...${c.excerpt(date.pos)}...')
}
dd := parts[2]
if dd.len != 2 {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" does not have a valid RFC 3339 day indication in ...${c.excerpt(date.pos)}...')
}
if mm.int() == 2 {
ddi := dd.int()
if ddi > 28 {
if ddi == 29 {
yyyyi := yyyy.int()
if !(yyyyi % 4 == 0 && (yyyyi % 100 != 0 || yyyyi % 400 == 0)) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid RFC 3339 date: ${yyyy} is not a leap year so February can not have 29 days in it ...${c.excerpt(date.pos)}...')
}
} else {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid RFC 3339 date: February can not have more that 28 or 29 days in it ...${c.excerpt(date.pos)}...')
}
}
}
toml_parse_time(lit) or {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid RFC 3339 Date format string "${err}". In ...${c.excerpt(date.pos)}...')
}
}
// check_time returns an error if `t` is not a valid TOML time string (RFC 3339).
fn (c &Checker) check_time(t ast.Time) ! {
lit := t.text
// Split any offsets from the time
mut offset_splitter := if lit.contains('+') { '+' } else { '-' }
parts := lit.split(offset_splitter)
mut hhmmss := parts[0].all_before('.')
// Check for 2 digits in all fields
mut check_length := 8
if hhmmss.to_upper().ends_with('Z') {
check_length++
}
if hhmmss.len != check_length {
starts_with_zero := hhmmss.starts_with('0')
if !starts_with_zero {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" must be zero prefixed in ...${c.excerpt(t.pos)}...')
}
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid RFC 3339 Time format string in ...${c.excerpt(t.pos)}...')
}
if parts.len > 1 {
// Offset
offset_parts := parts[1].split(':')
if offset_parts.len != 2 {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${parts[1]}" is not a valid RFC 3339 time offset specifier in ...${c.excerpt(t.pos)}...')
}
hh := offset_parts[0].int()
if hh < 0 || hh > 24 {
pos := token.Pos{
...t.pos
pos: t.pos.pos + check_length
}
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${hh}" hour specifier in "${parts[1]}" should be between 00 and 24 in ...${c.excerpt(pos)}...')
}
mm := offset_parts[1].int()
if mm < 0 || mm > 59 {
pos := token.Pos{
...t.pos
pos: t.pos.pos + check_length
}
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${mm}" second specifier in "${parts[1]}" should be between 00 and 59 in ...${c.excerpt(pos)}...')
}
}
// Simulate a time offset if it's missing then it can be checked. Already toml supports local time and rfc3339 don't.
mut has_time_offset := false
for ch in parts[0]#[8..] {
if ch in [u8(`-`), `+`, `Z`] {
has_time_offset = true
break
}
}
mut part_with_offset := parts[0]
if !has_time_offset {
part_with_offset += 'Z'
}
toml_parse_time(part_with_offset) or {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' "${lit}" is not a valid RFC 3339 Time format string "${err}". In ...${c.excerpt(t.pos)}...')
}
}
// check_quoted returns an error if `q` is not a valid quoted TOML string.
pub fn (c &Checker) check_quoted(q ast.Quoted) ! {
lit := q.text
quote := q.quote.ascii_str()
triple_quote := quote + quote + quote
if q.is_multiline && lit.ends_with(triple_quote) && !lit.ends_with('\\' + triple_quote) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' string values like "${lit}" has unbalanced quote literals `${quote}` in ...${c.excerpt(q.pos)}...')
}
c.check_quoted_escapes(q)!
c.check_utf8_validity(q)!
}
// check_quoted_escapes returns an error for any disallowed escape sequences.
// Delimiters in TOML has significant meaning:
// '/''' delimits *literal* strings (WYSIWYG / What-you-see-is-what-you-get)
// "/""" delimits *basic* strings
// Allowed escapes in *basic* strings are:
// \b - backspace (U+0008)
// \t - tab (U+0009)
// \n - linefeed (U+000A)
// \f - form feed (U+000C)
// \r - carriage return (U+000D)
// \" - quote (U+0022)
// \\ - backslash (U+005C)
// \uXXXX - Unicode (U+XXXX)
// \UXXXXXXXX - Unicode (U+XXXXXXXX)
fn (c &Checker) check_quoted_escapes(q ast.Quoted) ! {
// Setup a scanner in stack memory for easier navigation.
mut s := scanner.new_simple_text(q.text)!
// See https://toml.io/en/v1.0.0#string for more info on string types.
is_basic := q.quote == `\"`
contains_newlines := q.text.contains('\n')
for {
ch := s.next()
if ch == scanner.end_of_text {
break
}
ch_byte := u8(ch)
if ch == `\\` {
next_ch := u8(s.at())
if next_ch == `\\` {
s.next()
continue
}
escape := ch_byte.ascii_str() + next_ch.ascii_str()
if is_basic {
if q.is_multiline {
if next_ch == ` ` {
if !contains_newlines {
st := s.state()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' can not escape whitespaces in multi-line strings (`\\ `) at `${escape}` (${st.line_nr},${st.col}) in ...${c.excerpt(q.pos)}...')
}
// Rest of line must only be space chars from this point on
for {
ch_ := s.next()
if ch_ == `\n` {
break
}
if !(ch_ == ` ` || ch_ == `\t`) {
st := s.state()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' invalid character `${u8(ch_).ascii_str()}` after `${escape}` at (${st.line_nr},${st.col}) in ...${c.excerpt(q.pos)}...')
}
}
}
if next_ch in [`\t`, `\r`, `\n`, ` `] {
s.next()
continue
}
}
if next_ch !in allowed_basic_escape_char_list() {
st := s.state()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' unknown basic string escape character `${next_ch.ascii_str()}` in `${escape}` (${st.line_nr},${st.col}) in ...${c.excerpt(q.pos)}...')
}
}
// Check Unicode escapes
if is_basic && escape.to_lower() == '\\u' {
// Long type Unicode (\UXXXXXXXX) is a maximum of 10 chars: '\' + 'U' + 8 hex characters
// we pass in 10 characters from the `u`/`U` which is the longest possible sequence
// of 9 chars plus one extra.
if s.remaining() >= 10 {
pos := s.state().pos
c.check_unicode_escape(s.text[pos..pos + 11]) or {
st := s.state()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' escaped Unicode is invalid. ${err.msg().capitalize()} (${st.line_nr},${st.col}) in ...${c.excerpt(q.pos)}...')
}
} else {
pos := s.state().pos
c.check_unicode_escape(s.text[pos..]) or {
st := s.state()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' escaped Unicode is invalid. ${err.msg().capitalize()} (${st.line_nr},${st.col}) in ...${c.excerpt(q.pos)}...')
}
}
}
}
}
}
// check_utf8_string returns an error if `str` is not valid UTF-8.
fn (c &Checker) check_utf8_validity(q ast.Quoted) ! {
lit := q.text
if !utf8.validate_str(lit) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' the string value "${lit}" is not valid UTF-8 in ...${c.excerpt(q.pos)}...')
}
}
// validate_utf8_codepoint_string returns an error if `str` is not a valid Unicode code point.
// `str` is expected to be a `string` containing *only* hex values.
// Any preludes or prefixes like `0x` could pontentially yield wrong results.
fn validate_utf8_codepoint_string(str string) ! {
int_val := strconv.parse_int(str, 16, 64) or { i64(-1) }
if int_val > utf8_max || int_val < 0 {
return error('Unicode code point `${str}` is outside the valid Unicode scalar value ranges.')
}
// Check if the Unicode value is actually in the valid Unicode scalar value ranges.
// TODO: should probably be transferred / implemented in `utf8.validate(...)` also?
if !((int_val >= 0x0000 && int_val <= 0xD7FF) || (int_val >= 0xE000 && int_val <= 0x10FFFF)) {
return error('Unicode code point `${str}` is not a valid Unicode scalar value.')
}
bytes := str.bytes()
if !utf8.validate(bytes.data, bytes.len) {
return error('Unicode code point `${str}` is not a valid UTF-8 code point.')
}
}
// check_unicode_escape returns an error if `esc_unicode` is not
// a valid Unicode escape sequence. `esc_unicode` is expected to be
// prefixed with either `u` or `U`.
fn (c &Checker) check_unicode_escape(esc_unicode string) ! {
if esc_unicode.len < 5 || !esc_unicode.to_lower().starts_with('u') {
// Makes sure the input to this function is actually valid.
return error('`${esc_unicode}` is not a valid escaped Unicode sequence.')
}
is_long_esc_type := esc_unicode.starts_with('U')
mut sequence := esc_unicode[1..]
hex_digits_len := if is_long_esc_type { 8 } else { 4 }
if sequence.len < hex_digits_len {
return error('Unicode escape sequence `${esc_unicode}` should be at least ${hex_digits_len} in length.')
}
sequence = sequence[..hex_digits_len]
// TODO: not enforced in BurnSushi testsuite??
// if !sequence.is_upper() {
// return error('Unicode escape sequence `${esc_unicode}` is not in all uppercase.')
//}
validate_utf8_codepoint_string(sequence.to_upper())!
if is_long_esc_type {
// Long escape type checks
} else {
// Short escape type checks
}
}
// check_comment returns an error if the contents of `comment` isn't
// a valid TOML comment.
pub fn (c &Checker) check_comment(comment ast.Comment) ! {
lit := comment.text
// Setup a scanner in stack memory for easier navigation.
mut s := scanner.new_simple_text(lit)!
for {
ch := s.next()
if ch == scanner.end_of_text {
break
}
ch_byte := u8(ch)
// Check for carriage return
if ch_byte == 0x0D {
st := s.state()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' carriage return character `${ch_byte.hex()}` is not allowed in comments (${st.line_nr},${st.col}).')
}
// Check for control characters (allow TAB)
if util.is_illegal_ascii_control_character(ch_byte) {
st := s.state()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' control character `${ch_byte.hex()}` is not allowed (${st.line_nr},${st.col}) "${u8(s.at()).ascii_str()}" near ...${s.excerpt(st.pos, 10)}...')
}
}
// Check for bad UTF-8 encoding
if !utf8.validate_str(lit) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' comment "${lit}" is not valid UTF-8 in ...${c.excerpt(comment.pos)}...')
}
}
+243
View File
@@ -0,0 +1,243 @@
// 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 decoder
import toml.ast
import toml.ast.walker
import toml.token
import toml.scanner
import strconv
// utf8_max is the largest inclusive value of the Unicodes scalar value ranges.
const utf8_max = 0x10FFFF
// Decoder decode special sequences in a tree of TOML `ast.Value`'s.
pub struct Decoder {
pub:
scanner &scanner.Scanner = unsafe { nil }
}
// decode decodes certain `ast.Value`'s and all it's children.
pub fn (d Decoder) decode(mut n ast.Value) ! {
walker.walk_and_modify(d, mut n)!
}
fn (d Decoder) modify(mut value ast.Value) ! {
match value {
ast.Quoted {
mut v := &(value as ast.Quoted)
d.decode_quoted(mut v)!
}
ast.Number {
mut v := &(value as ast.Number)
d.decode_number(mut v)!
}
ast.DateTime {
mut v := &(value as ast.DateTime)
d.decode_date_time(mut v)!
}
else {}
}
}
// excerpt returns a string of the token's surroundings
fn (d Decoder) excerpt(tp token.Pos) string {
return d.scanner.excerpt(tp.pos, 10)
}
// decode_quoted returns an error if `q` is not a valid quoted TOML string.
fn (d Decoder) decode_quoted(mut q ast.Quoted) ! {
decode_quoted_escapes(mut q)!
}
// decode_number decodes the `n ast.Number` into valid TOML.
fn (d Decoder) decode_number(mut n ast.Number) ! {
if n.text == '-nan' || n.text == '+nan' {
n.text = 'nan'
}
}
// decode_quoted_escapes returns an error for any disallowed escape sequences.
// Delimiters in TOML has significant meaning:
// '/''' delimits *literal* strings (WYSIWYG / What-you-see-is-what-you-get)
// "/""" delimits *basic* strings
// Allowed escapes in *basic* strings are:
// \b - backspace (U+0008)
// \t - tab (U+0009)
// \n - linefeed (U+000A)
// \f - form feed (U+000C)
// \r - carriage return (U+000D)
// \" - quote (U+0022)
// \\ - backslash (U+005C)
// \uXXXX - Unicode (U+XXXX)
// \UXXXXXXXX - Unicode (U+XXXXXXXX)
pub fn decode_quoted_escapes(mut q ast.Quoted) ! {
// Setup a scanner in stack memory for easier navigation.
mut eat_whitespace := false
// TODO: use string builder
mut decoded_s := ''
// See https://toml.io/en/v1.0.0#string for more info on string types.
is_basic := q.quote == `\"`
if !is_basic {
return
}
mut s := scanner.new_simple_text(q.text)!
q.text = q.text.replace('\\"', '"')
for ch := s.next(); ch != scanner.end_of_text; ch = s.next() {
ch_byte := u8(ch)
if eat_whitespace && ch_byte.is_space() {
continue
}
eat_whitespace = false
if ch == `\\` {
ch_next := s.at()
ch_next_byte := u8(ch_next)
if q.is_multiline {
if ch_next_byte.is_space() {
eat_whitespace = true
continue
}
}
match rune(ch_next) {
`\\`, `"` {
decoded_s += ch_next_byte.ascii_str()
s.next()
continue
}
`n` {
decoded_s += '\n'
s.next()
continue
}
`t` {
decoded_s += '\t'
s.next()
continue
}
`b` {
decoded_s += '\b'
s.next()
continue
}
`r` {
decoded_s += '\r'
s.next()
continue
}
`f` {
decoded_s += '\f'
s.next()
continue
}
else {}
}
escape := ch_byte.ascii_str() + ch_next_byte.ascii_str()
// Decode unicode escapes
if escape.to_lower() == '\\u' {
is_valid_short := u8(s.peek(1)).is_hex_digit() && u8(s.peek(2)).is_hex_digit()
&& u8(s.peek(3)).is_hex_digit() && u8(s.peek(4)).is_hex_digit()
if is_valid_short {
is_valid_long := u8(s.peek(5)).is_hex_digit() && u8(s.peek(6)).is_hex_digit()
&& u8(s.peek(7)).is_hex_digit() && u8(s.peek(8)).is_hex_digit()
// If it's a long type Unicode (\UXXXXXXXX) with a maximum of 10 chars: '\' + 'U' + 8 hex characters
// we pass in 10 characters from the `u`/`U` which is the longest possible sequence
// of 9 chars plus one extra.
// Else it's a short sequence (\uXXXX) with a maximum of 6 chars: '\' + 'U' + 4 hex characters.
mut decoded := ''
mut sequence_length := 0
mut unicode_val := 0
mut slen := if is_valid_long { 10 } else { 6 }
if slen <= s.remaining() {
pos := s.state().pos
sequence := s.text#[pos..pos + slen + 1]
decoded, unicode_val, sequence_length = decode_unicode_escape(sequence) or {
decoded_s += escape
continue
}
if unicode_val > utf8_max || unicode_val < 0 {
decoded_s += escape
continue
}
// Check if the Unicode value is actually in the valid Unicode scalar value ranges.
if !((unicode_val >= 0x0000 && unicode_val <= 0xD7FF)
|| (unicode_val >= 0xE000 && unicode_val <= utf8_max)) {
decoded_s += escape
continue
}
decoded_s += decoded
replacement := s.text[pos..pos + sequence_length + 1]
s.skip_n(replacement.len)
continue
} else {
pos := s.state().pos
sequence := s.text[pos..]
decoded, _, _ = decode_unicode_escape(sequence) or {
decoded_s += escape
continue
}
decoded_s += decoded
s.skip_n(s.text[pos..].len)
continue
}
}
}
}
decoded_s += ch_byte.ascii_str()
}
q.text = decoded_s
}
// decode_unicode_escape decodes the Unicode escape sequence `esc_unicode`.
// The sequence is expected to be prefixed with either `u` or `U`.
// decode_unicode_escape returns the decoded rune as
// a string, it's integer value and it's length.
fn decode_unicode_escape(esc_unicode string) !(string, int, int) {
is_long_esc_type := esc_unicode.starts_with('U')
mut sequence := esc_unicode[1..]
hex_digits_len := if is_long_esc_type { 8 } else { 4 }
mut sequence_len := hex_digits_len
sequence = sequence[..hex_digits_len]
mut unicode_point := sequence
if unicode_point.len < 8 {
unicode_point = '0'.repeat(8 - unicode_point.len) + unicode_point
}
i64_val := strconv.parse_int(unicode_point, 16, 0)!
rn := rune(i64_val)
return '${rn}', int(i64_val), sequence_len
}
// decode_date_time decodes the `dt ast.DateTime`.
fn (d Decoder) decode_date_time(mut dt ast.DateTime) ! {
// Expand milliseconds that are only 1 char
if dt.text.contains('.') {
yymmddhhmmss := dt.text.all_before('.')
rest := dt.text.all_after('.')
z := if rest.contains('Z') { 'Z' } else { '' }
mut ms := rest
mut offset := ''
if rest.contains('+') {
offset = '+' + rest.all_after('+')
ms = rest.all_before('+')
} else if rest.contains('-') {
offset = '-' + rest.all_after('-')
ms = rest.all_before('-')
}
if z != '' {
ms = ms.replace('Z', '')
}
if ms.len > 1 {
return
}
ms = ms + '0'.repeat(4 - ms.len) + z
dt.text = yymmddhhmmss + '.' + ms + offset
}
}
+35
View File
@@ -0,0 +1,35 @@
// 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 input
import os
// Config is used to configure input to the toml module.
// Only one of the fields `text` and `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'
}
// read_input returns either Config.text or the read file contents of Config.file_path
// depending on which one is not empty.
pub fn (c Config) read_input() !string {
if c.file_path != '' && c.text != '' {
return error(@MOD + '.' + @FN +
' ${typeof(c).name} should contain only one of the fields `file_path` OR `text` filled out')
}
if c.file_path == '' && c.text == '' {
// TODO: passing both empty is used *a lot* by `./v vlib/toml/tests/toml_lang_test.v`; investigate why.
return ''
}
if c.text != '' {
return c.text
}
text := os.read_file(c.file_path) or {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' Could not read "${c.file_path}": "${err.msg()}"')
}
return text
}
File diff suppressed because it is too large. Load diff
+741
View File
@@ -0,0 +1,741 @@
// 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 scanner
import toml.input
import toml.token
import toml.util
pub const digit_extras = [`_`, `.`, `x`, `o`, `b`, `e`, `E`]
pub const end_of_text = u32(~0)
// Scanner contains the necessary fields for the state of the scan process.
// the task the scanner does is also referred to as "lexing" or "tokenizing".
// The Scanner methods are based on much of the work in `vlib/strings/textscanner`.
pub struct Scanner {
pub:
config Config
text string // the input TOML text
mut:
col int // current column number (x coordinate)
line_nr int = 1 // current line number (y coordinate)
pos int // current flat/index position in the `text` field
header_len int // Length, how many bytes of header was found
// Quirks
is_left_of_assign bool = true // indicates if the scanner is on the *left* side of an assignment
}
// State is a read-only copy of the scanner's internal state.
// See also `Scanner.state()`.
pub struct State {
pub:
col int // current column number (x coordinate)
line_nr int = 1 // current line number (y coordinate)
pos int // current flat/index position in the `text` field
}
// Config is used to configure a Scanner instance.
// Only one of the fields `text` and `file_path` is allowed to be set at time of configuration.
pub struct Config {
pub:
input input.Config
tokenize_formatting bool = true // if true, generate tokens for `\n`, ` `, `\t`, `\r` etc.
}
// new_scanner returns a new *heap* allocated `Scanner` instance, based on the file in config.input.file_path,
// or based on the text in config.input.text .
pub fn new_scanner(config Config) !&Scanner {
mut s := &Scanner{
config: config
text: config.input.read_input()!
}
return s
}
// new_simple returns a new *stack* allocated `Scanner` instance.
pub fn new_simple(config Config) !Scanner {
return Scanner{
config: config
text: config.input.read_input()!
}
}
// new_simple_text returns a new *stack* allocated `Scanner` instance
// ready for parsing TOML in `text`.
pub fn new_simple_text(text string) !Scanner {
in_config := input.Config{
text: text
}
config := Config{
input: in_config
}
return Scanner{
config: config
text: config.input.read_input()!
}
}
// new_simple_file returns a new *stack* allocated `Scanner` instance
// ready for parsing TOML in file read from `path`.
pub fn new_simple_file(path string) !Scanner {
in_config := input.Config{
file_path: path
}
config := Config{
input: in_config
}
return Scanner{
config: config
text: config.input.read_input()!
}
}
// scan returns the next token from the input.
@[direct_array_access]
pub fn (mut s Scanner) scan() !token.Token {
s.validate_and_skip_headers()!
for {
c := s.next()
byte_c := u8(c)
if c == end_of_text {
s.inc_line_number()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'reached EOF')
return s.new_token(.eof, '', 1)
}
ascii := byte_c.ascii_str()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'current char "${ascii}"')
if byte_c == u8(0x0) {
s.reset()
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' NULL control character `${c.hex()}` is not allowed at (${s.line_nr},${s.col}) "${ascii}" near ...${s.excerpt(s.pos, 5)}...')
}
is_sign := c == `+` || c == `-`
// (+/-)nan & (+/-)inf
peek_1 := s.peek(1)
peek_2 := s.peek(2)
is_nan := c == `n` && s.at() == `a` && peek_1 == `n`
is_inf := !is_nan && c == `i` && s.at() == `n` && peek_1 == `f`
is_signed_nan := is_sign && s.at() == `n` && peek_1 == `a` && peek_2 == `n`
is_signed_inf := !is_signed_nan && is_sign && s.at() == `i` && peek_1 == `n`
&& peek_2 == `f`
if !s.is_left_of_assign && (is_nan || is_inf || is_signed_nan || is_signed_inf) {
num := s.extract_nan_or_inf_number()!
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified a special number "${num}" (${num.len})')
return s.new_token(.number, num, num.len)
}
is_signed_number := is_sign && u8(s.at()).is_digit() && !u8(s.peek(-1)).is_digit()
is_digit := byte_c.is_digit()
if is_digit || is_signed_number {
num := s.extract_number()!
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified a number "${num}" (${num.len})')
return s.new_token(.number, num, num.len)
}
if util.is_key_char(byte_c) {
key := s.extract_key()
if u8(s.peek(1)) != `=` && (key == 'true' || key == 'false') {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified a boolean "${key}" (${key.len})')
return s.new_token(.boolean, key, key.len)
}
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified a bare key "${key}" (${key.len})')
return s.new_token(.bare, key, key.len)
}
match rune(c) {
` `, `\t`, `\n`, `\r` {
if c == `\n` {
s.inc_line_number()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'incremented line nr to ${s.line_nr}')
} else if c == `\r` {
// CR should always be followed by a `\n`
if s.at() != `\n` {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' missing newline/linefeed character after "\\c" carriage return at (${s.line_nr},${s.col}) "${ascii}" near ...${s.excerpt(s.pos, 5)}...')
}
}
// Date-Time in RFC 3339 is allowed to have a space between the date and time in supplement to the 'T'
// so we allow space characters to slip through to the parser if the space is between two digits...
// util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, '"'+u8(s.peek(-1)).ascii_str()+'" < "${ascii}" > "'+u8(s.at()).ascii_str()+'"')
if c == ` ` && u8(s.peek(-1)).is_digit() && u8(s.at()).is_digit() {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified, what could be, a space between a RFC 3339 date and time ("${ascii}") (${ascii.len})')
return s.new_token(token.Kind.whitespace, ascii, ascii.len)
}
if s.config.tokenize_formatting {
mut kind := token.Kind.whitespace
if c == `\t` {
kind = token.Kind.tab
} else if c == `\r` {
kind = token.Kind.cr
} else if c == `\n` {
kind = token.Kind.nl
}
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified formatting character ("${ascii}") (${ascii.len})')
return s.new_token(kind, ascii, ascii.len)
} else {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'skipping " ", "\\t" or "\\n" ("${ascii}") (${ascii.len})')
}
continue
}
`-` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified minus "${ascii}" (${ascii.len})')
return s.new_token(.minus, ascii, ascii.len)
}
`_` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified underscore "${ascii}" (${ascii.len})')
return s.new_token(.underscore, ascii, ascii.len)
}
`+` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified plus "${ascii}" (${ascii.len})')
return s.new_token(.plus, ascii, ascii.len)
}
`=` {
s.is_left_of_assign = false
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified assignment "${ascii}" (${ascii.len})')
return s.new_token(.assign, ascii, ascii.len)
}
`"`, `'` { // ... some string "/'
ident_string := s.extract_string()!
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified quoted string `${ident_string}`')
return s.new_token(.quoted, ident_string, ident_string.len)
}
`#` {
hash := s.ignore_line()!
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified comment hash "${hash}" (${hash.len})')
return s.new_token(.hash, hash, hash.len + 1)
}
`{` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified left curly bracket "${ascii}" (${ascii.len})')
return s.new_token(.lcbr, ascii, ascii.len)
}
`}` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified right curly bracket "${ascii}" (${ascii.len})')
return s.new_token(.rcbr, ascii, ascii.len)
}
`[` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified left square bracket "${ascii}" (${ascii.len})')
return s.new_token(.lsbr, ascii, ascii.len)
}
`]` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified right square bracket "${ascii}" (${ascii.len})')
return s.new_token(.rsbr, ascii, ascii.len)
}
`:` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified colon "${ascii}" (${ascii.len})')
return s.new_token(.colon, ascii, ascii.len)
}
`,` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified comma "${ascii}" (${ascii.len})')
return s.new_token(.comma, ascii, ascii.len)
}
`.` {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified period "${ascii}" (${ascii.len})')
return s.new_token(.period, ascii, ascii.len)
}
else {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' could not scan character `${ascii}` / ${c} at ${s.pos} (${s.line_nr},${s.col}) near ...${s.excerpt(s.pos, 5)}...')
}
}
}
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'unknown character code at ${s.pos} (${s.line_nr},${s.col}) near ...${s.excerpt(s.pos,
5)}...')
return s.new_token(.unknown, '', 0)
}
// free frees all allocated resources.
@[unsafe]
pub fn (mut s Scanner) free() {
unsafe {
s.text.free()
}
}
// remaining returns how many characters remain in the text input.
@[inline]
pub fn (s &Scanner) remaining() int {
return s.text.len - s.pos
}
// next returns the next character code from the input text.
// next returns `end_of_text` if it can't reach the next character.
@[direct_array_access; inline]
pub fn (mut s Scanner) next() u32 {
if s.pos < s.text.len {
opos := s.pos
s.pos++
s.col++
c := s.text[opos]
return c
}
return end_of_text
}
// skip skips one character ahead.
@[inline]
pub fn (mut s Scanner) skip() {
if s.pos + 1 < s.text.len {
s.pos++
s.col++
}
}
// skip_n skips ahead `n` characters.
// If the skip goes out of bounds from the length of `Scanner.text`,
// the scanner position will be sat to the last character possible.
@[inline]
pub fn (mut s Scanner) skip_n(n int) {
s.pos += n
if s.pos > s.text.len {
s.pos = s.text.len
}
s.col = s.pos
}
// at returns the *current* character code from the input text.
// at returns `end_of_text` if it can't get the current character.
// unlike `next()`, `at()` does not change the state of the scanner.
@[direct_array_access; inline]
pub fn (s &Scanner) at() u32 {
if s.pos < s.text.len {
return s.text[s.pos]
}
return end_of_text
}
// at_crlf returns `true` if the scanner is at a `\r` character
// and the next character is a `\n`.
fn (s &Scanner) at_crlf() bool {
return s.at() == `\r` && s.peek(1) == `\n`
}
// peek returns the character code from the input text at position + `n`.
// peek returns `end_of_text` if it can't peek `n` characters ahead.
@[direct_array_access; inline]
pub fn (s &Scanner) peek(n int) u32 {
if s.pos + n < s.text.len {
// Allow peeking back - needed for spaces between date and time in RFC 3339 format :/
if n - 1 < 0 && s.pos + n - 1 >= 0 {
// util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'LOOKING BAAAA-AACK - OOOVER MY SHOOOOULDEEEER "${s.text[s.pos + n-1]}"')
return s.text[s.pos + n - 1]
}
return s.text[s.pos + n]
}
return end_of_text
}
// reset resets the internal state of the scanner.
pub fn (mut s Scanner) reset() {
s.pos = 0
s.col = 0
s.line_nr = 1
s.header_len = 0
}
// new_token returns a new `token.Token`.
@[inline]
fn (mut s Scanner) new_token(kind token.Kind, lit string, len int) token.Token {
// println('new_token(${lit})')
mut col := s.col - len + 1
if s.line_nr == 1 {
col -= s.header_len
}
return token.Token{
kind: kind
lit: lit
col: if col < 1 { 1 } else { col }
line_nr: s.line_nr + 1
pos: s.pos - s.header_len - len + 1
len: len
}
}
// ignore_line forwards the scanner to the end of the current line.
@[direct_array_access; inline]
fn (mut s Scanner) ignore_line() !string {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, ' ignoring until EOL...')
start := s.pos
for c := s.at(); c != end_of_text && c != `\n`; c = s.at() {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping "${u8(c).ascii_str()} / ${c}"')
if s.at_crlf() {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'letting `\\r\\n` slip through')
break
}
s.next()
}
return s.text[start..s.pos]
}
// inc_line_number increases the internal line number.
@[inline]
fn (mut s Scanner) inc_line_number() {
s.col = 0
s.line_nr++
s.is_left_of_assign = true
}
// extract_key parses and returns a TOML key as a string.
@[direct_array_access; inline]
fn (mut s Scanner) extract_key() string {
s.pos--
s.col--
start := s.pos
for s.pos < s.text.len {
c := u8(s.at())
if !(util.is_key_char(c) || c.is_digit() || c in [`_`, `-`]) {
break
}
s.pos++
s.col++
}
key := s.text[start..s.pos]
return key
}
// extract_string collects and returns a string containing
// any bytes recognized as a TOML string.
// TOML strings are everything found between two double or single quotation marks (`"`/`'`).
@[direct_array_access; inline]
fn (mut s Scanner) extract_string() !string {
// extract_string is called when the scanner has already reached
// a byte that is the start of a string so we rewind it to start at the correct
s.pos--
s.col--
quote := u8(s.at())
start := s.pos
mut lit := quote.ascii_str()
is_multiline := s.text[s.pos + 1] == quote && s.text[s.pos + 2] == quote
// Check for escaped multiline quote
if is_multiline {
mls := s.extract_multiline_string()!
return mls
}
for {
s.pos++
s.col++
if s.pos >= s.text.len {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' unfinished single-line string literal `${quote.ascii_str()}` started at ${start} (${s.line_nr},${s.col}) "${u8(s.at()).ascii_str()}" near ...${s.excerpt(s.pos, 5)}...')
}
c := u8(s.at())
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'c: `${c.ascii_str()}` / ${c} (quote type: ${quote}/${quote.ascii_str()})')
// Check for escaped chars
if c == u8(92) {
esc, skip := s.handle_escapes(quote, is_multiline)
lit += esc
if skip > 0 {
s.pos += skip
s.col += skip
continue
}
}
// Check for control characters (allow TAB)
if util.is_illegal_ascii_control_character(c) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' control character `${c.hex()}` is not allowed at ${start} (${s.line_nr},${s.col}) "${u8(s.at()).ascii_str()}" near ...${s.excerpt(s.pos, 5)}...')
}
if c == quote {
s.pos++
s.col++
return lit + quote.ascii_str()
}
lit += c.ascii_str()
// Don't eat multiple lines in single-line mode
if lit.contains('\n') {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' unfinished single-line string literal `${quote.ascii_str()}` started at ${start} (${s.line_nr},${s.col}) "${u8(s.at()).ascii_str()}" near ...${s.excerpt(s.pos, 5)}...')
}
}
return lit
}
// extract_multiline_string collects and returns a string containing
// any bytes recognized as a TOML string.
// TOML strings are everything found between two double or single quotation marks (`"`/`'`).
@[direct_array_access; inline]
fn (mut s Scanner) extract_multiline_string() !string {
// extract_multiline_string is called from extract_string so we know the 3 first
// characters is the quotes
quote := u8(s.at())
start := s.pos
mut lit := quote.ascii_str() + quote.ascii_str() + quote.ascii_str()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'multi-line `${quote.ascii_str()}${s.text[
s.pos + 1].ascii_str()}${s.text[s.pos + 2].ascii_str()}` string started at pos ${start} (${s.line_nr},${s.col}) (quote type: ${quote.ascii_str()} / ${quote})')
s.pos += 2
s.col += 2
for {
s.pos++
s.col++
if s.pos >= s.text.len {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' unfinished multi-line string literal (${quote.ascii_str()}${quote.ascii_str()}${quote.ascii_str()}) started at ${start} (${s.line_nr},${s.col}) "${u8(s.at()).ascii_str()}" near ...${s.excerpt(s.pos, 5)}...')
}
c := u8(s.at())
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'c: `${c.ascii_str()}` / ${c} (quote type: ${quote}/${quote.ascii_str()})')
if c == `\r` && s.peek(1) == `\n` {
continue
}
if c == `\n` {
s.inc_line_number()
lit += c.ascii_str()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'c: `\\n` / ${c}')
continue
}
// Check for escaped chars
if c == u8(92) {
esc, skip := s.handle_escapes(quote, true)
lit += esc
if skip > 0 {
s.pos += skip
s.col += skip
continue
}
}
// Check for control characters (allow TAB)
if util.is_illegal_ascii_control_character(c) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' control character `${c.hex()}` is not allowed at ${start} (${s.line_nr},${s.col}) "${u8(s.at()).ascii_str()}" near ...${s.excerpt(s.pos, 5)}...')
}
if c == quote {
if s.peek(1) == quote && s.peek(2) == quote {
if s.peek(3) == end_of_text {
s.pos += 3
s.col += 3
lit += quote.ascii_str() + quote.ascii_str() + quote.ascii_str()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'returning at ${c.ascii_str()} `${lit}`')
return lit
} else if s.peek(3) != quote {
// lit += c.ascii_str()
// lit += quote.ascii_str()
s.pos += 3
s.col += 3
lit += quote.ascii_str() + quote.ascii_str() + quote.ascii_str()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'returning at ${c.ascii_str()} `${lit}`')
return lit
}
}
}
lit += c.ascii_str()
}
return lit
}
// handle_escapes returns any escape character sequence.
// For escape sequence validation see `Checker.check_quoted_escapes`.
fn (mut s Scanner) handle_escapes(quote u8, is_multiline bool) (string, int) {
c := u8(s.at())
mut lit := c.ascii_str()
is_literal_string := quote == `'`
if !is_literal_string {
if s.peek(1) == `u` && u8(s.peek(2)).is_hex_digit() && u8(s.peek(3)).is_hex_digit()
&& u8(s.peek(4)).is_hex_digit() && u8(s.peek(5)).is_hex_digit() {
lit += s.text[s.pos + 1..s.pos + 6] //.ascii_str()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'gulp escaped unicode `${lit}`')
return lit, 5
} else if s.peek(1) == quote {
if (!is_multiline && s.peek(2) == `\n`)
|| (is_multiline && s.peek(2) == quote && s.peek(3) == quote && s.peek(4) == `\n`) {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'ignore special case escaped `${lit}` at end of string')
return '', 0
}
lit += quote.ascii_str()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'gulp escaped `${lit}`')
return lit, 1
}
}
if is_literal_string {
if s.peek(1) == quote {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'ignore escape `${lit}${u8(s.peek(1)).ascii_str()}` in literal string')
return '', 0
}
}
lit += u8(s.peek(1)).ascii_str()
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'gulp escaped `${lit}`')
return lit, 1
}
// extract_number collects and returns a string containing
// any bytes recognized as a TOML number except for "(+/-)nan" and "(+/-)inf".
// TOML numbers can include digits 0-9 and `_`.
@[direct_array_access; inline]
fn (mut s Scanner) extract_number() !string {
// extract_number is called when the scanner has already reached
// a byte that is a number or +/- - so we rewind it to start at the correct
// position to get the complete number. Even if it's only one digit
s.pos--
s.col--
start := s.pos
mut c := s.at()
is_digit := u8(c).is_digit()
if !(is_digit || c in [`+`, `-`]) {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' ${u8(c).ascii_str()} is not a number at ${s.excerpt(s.pos, 10)}')
}
s.pos++
s.col++
for s.pos < s.text.len {
c = s.at()
// Adjust scanner position to floating point numbers
mut float_precision := 0
if c == `.` {
mut i := 1
for c_ := u8(s.peek(i)); c_ != end_of_text && c_ != `\n`; c_ = u8(s.peek(i)) {
if !c_.is_digit() && c_ != `,` {
float_precision = 0
break
}
float_precision++
i++
}
}
s.pos += float_precision
s.col += float_precision
// Handle signed exponent notation. I.e.: 3e2, 3E2, 3e-2, 3E+2, 3e0, 3.1e2, 3.1E2, -1E-1
if c in [`e`, `E`] && s.peek(1) in [`+`, `-`] && u8(s.peek(2)).is_digit() {
s.pos += 2
s.col += 2
}
c = s.at()
if !(u8(c).is_hex_digit() || c in digit_extras) || (c == `.` && s.is_left_of_assign) {
break
}
s.pos++
s.col++
}
key := s.text[start..s.pos]
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified number "${key}" in range [${start} .. ${s.pos}]')
return key
}
// extract_nan_or_inf_number collects and returns a string containing
// any bytes recognized as infinity or not-a-number TOML numbers.
@[direct_array_access; inline]
fn (mut s Scanner) extract_nan_or_inf_number() !string {
// extract_nan_or_inf_number is called when the scanner has already identified that
// +/- or 'nan'/'inf' bytes is up but we rewind it to start at the correct position
s.pos--
s.col--
start := s.pos
mut c := s.at()
if c !in [`+`, `-`, `n`, `i`] {
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' ${u8(c).ascii_str()} is not a number at ${s.excerpt(s.pos, 10)}')
}
s.pos++
s.col++
for s.pos < s.text.len {
c = s.at()
if c !in [`n`, `a`, `i`, `f`] {
break
}
s.pos++
s.col++
}
key := s.text[start..s.pos]
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN,
'identified special number "${key}" in range [${start} .. ${s.pos}]')
return key
}
// excerpt returns a string excerpt of the input text centered
// at `pos`. The `margin` argument defines how many chacters
// on each side of `pos` is returned
pub fn (s &Scanner) excerpt(pos int, margin int) string {
start := if pos > 0 && pos >= margin { pos - margin } else { 0 }
end := if pos + margin < s.text.len { pos + margin } else { s.text.len }
return s.text[start..end].replace('\n', r'\n')
}
// state returns a read-only view of the scanner's internal state.
pub fn (s &Scanner) state() State {
return State{
col: s.col
line_nr: s.line_nr
pos: s.pos
}
}
fn (mut s Scanner) validate_and_skip_headers() ! {
// UTF-16 / UTF-32 headers (BE/LE)
s.check_utf16_or_32_bom()!
// NICE-TO-HAVE-TODO Check other types of (UTF-?) headers and yield an error. TOML is UTF-8 only.
// Skip optional UTF-8 header, if any.
if s.at() == 0xEF && s.peek(1) == 0xBB && s.peek(2) == 0xBF {
util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping UTF-8 byte order mark (BOM)')
s.header_len = 3
s.skip_n(s.header_len)
}
// Check after we've skipped UTF-8 BOM
s.check_utf16_or_32_bom()!
}
fn (mut s Scanner) check_utf16_or_32_bom() ! {
if (s.at() == 0xFF && s.peek(1) == 0xFE && s.peek(2) == 0x00 && s.peek(3) == 0x00)
|| (s.at() == 0x00 && s.peek(1) == 0x00 && s.peek(2) == 0xFE && s.peek(3) == 0xFF) {
s.header_len = 4
s.skip_n(s.header_len)
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' UTF-32 is not a valid TOML encoding at ${s.pos} (${s.line_nr},${s.col}) near ...${s.excerpt(s.pos, 5)}...')
}
if (s.at() == 0xFE && s.peek(1) == 0xFF) || (s.at() == 0xFF && s.peek(1) == 0xFE) {
s.header_len = 2
s.skip_n(s.header_len)
return error(@MOD + '.' + @STRUCT + '.' + @FN +
' UTF-16 is not a valid TOML encoding at ${s.pos} (${s.line_nr},${s.col}) near ...${s.excerpt(s.pos, 5)}...')
}
}
+95
View File
@@ -0,0 +1,95 @@
import toml.input
import toml.scanner
const scan_input = input.Config{
text: 'abc'
}
fn test_remaining() {
mut s := scanner.new_scanner(input: scan_input) or { panic(err) }
assert s.remaining() == 3
s.next()
s.next()
assert s.remaining() == 1
s.next()
assert s.remaining() == 0
s.next()
s.next()
assert s.remaining() == 0
s.reset()
assert s.remaining() == 3
}
fn test_next() {
mut s := scanner.new_scanner(input: scan_input) or { panic(err) }
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == scanner.end_of_text
assert s.next() == scanner.end_of_text
assert s.next() == scanner.end_of_text
}
fn test_skip() {
mut s := scanner.new_scanner(input: scan_input) or { panic(err) }
assert s.next() == `a`
s.skip()
assert s.next() == `c`
assert s.next() == scanner.end_of_text
}
fn test_skip_n() {
mut s := scanner.new_scanner(input: scan_input) or { panic(err) }
s.skip_n(2)
assert s.next() == `c`
assert s.next() == scanner.end_of_text
}
fn test_at() {
mut s := scanner.new_scanner(input: scan_input) or { panic(err) }
assert s.at() == `a`
assert s.at() == `a`
assert s.at() == `a`
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == scanner.end_of_text
}
fn test_peek() {
mut s := scanner.new_scanner(input: scan_input) or { panic(err) }
assert s.peek(0) == `a`
assert s.peek(1) == `b`
assert s.peek(2) == `c`
assert s.peek(3) == scanner.end_of_text
assert s.peek(4) == scanner.end_of_text
assert s.next() == `a`
assert s.next() == `b`
assert s.next() == `c`
assert s.next() == scanner.end_of_text
}
fn test_reset() {
mut s := scanner.new_scanner(input: scan_input) or { panic(err) }
assert s.next() == `a`
s.next()
s.next()
assert s.next() == scanner.end_of_text
s.reset()
assert s.next() == `a`
}
const multiline_string_input = input.Config{
text: '"""abc\r\ndef\n123"""'
}
fn test_multiline_string() {
mut s := scanner.new_scanner(input: multiline_string_input) or { panic(err) }
tok := s.scan()!
assert tok.kind == .quoted
assert tok.lit.contains('abc')
assert tok.lit.contains('def')
assert tok.lit.contains('123')
}
@@ -0,0 +1,26 @@
import os
import toml
import toml.to
const toml_table_text = '
[[products]]
name = "Hammer"
sku = 738594937
[[products]] # empty table within the array
[[products]]
name = "Nail"
sku = 284758393
color = "gray"'
fn test_tables() {
mut toml_doc := toml.parse_text(toml_table_text) or { panic(err) }
toml_json := to.json(toml_doc)
assert toml_json == os.read_file(
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.out') or { panic(err) }
}
@@ -0,0 +1,32 @@
import os
import toml
import toml.to
const toml_text = '[[albums]]
name = "Born to Run"
[[albums.songs]]
name = "Jungleland"
[[albums.songs]]
name = "Meeting Across the River"
[[albums]]
name = "Born in the USA"
[[albums.songs]]
name = "Glory Days"
[[albums.songs]]
name = "Dancing in the Dark"'
const fprefix = os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))
fn test_nested_array_of_tables() {
mut toml_doc := toml.parse_text(toml_text)!
toml_json := to.json(toml_doc)
eprintln(toml_json)
assert toml_json == os.read_file(fprefix + '.out')!
}
@@ -0,0 +1,23 @@
import os
import toml
import toml.to
const toml_text = '[[a]]
[[a.b]]
[a.b.c]
d = "val0"
[[a.b]]
[a.b.c]
d = "val1"
'
fn test_nested_array_of_tables() {
mut toml_doc := toml.parse_text(toml_text) or { panic(err) }
toml_json := to.json(toml_doc)
eprintln(toml_json)
assert toml_json == os.read_file(
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.out') or { panic(err) }
}
@@ -0,0 +1,18 @@
import os
import toml
import toml.to
fn test_array_of_tables_edge_case_file() {
toml_file :=
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.toml'
toml_doc := toml.parse_file(toml_file) or { panic(err) }
toml_json := to.json(toml_doc)
out_file :=
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.out'
out_file_json := os.read_file(out_file) or { panic(err) }
println(toml_json)
assert toml_json == out_file_json
}
@@ -0,0 +1,15 @@
import os
import toml
import toml.to
const fprefix = os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))
fn test_array_of_tables_edge_case_file() {
toml_doc := toml.parse_file(os.real_path(fprefix + '.toml'))!
toml_json := to.json(toml_doc)
out_file_json := os.read_file(os.real_path(fprefix + '.out'))!
println(toml_json)
assert toml_json == out_file_json
}
+78
View File
@@ -0,0 +1,78 @@
import toml
// Complete text from the example in the README.md:
// https://github.com/toml-lang/toml/blob/3b11f6921da7b6f5db37af039aa021fee450c091/README.md#Example
const toml_text = '# This is a TOML document.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates
[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data=[["gamma","delta"],[1,2]]
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]'
fn test_parse_compact_text() {
toml_doc := toml.parse_text(toml_text) or { panic(err) }
title := toml_doc.value('title')
assert title == toml.Any('TOML Example')
assert title as string == 'TOML Example'
database := toml_doc.value('database') as map[string]toml.Any
db_serv := database['server'] or {
panic('could not access "server" index in "database" variable')
}
assert db_serv as string == '192.168.1.1'
assert toml_doc.value('owner.name') as string == 'Tom Preston-Werner'
assert toml_doc.value('database.server') as string == '192.168.1.1'
database_ports := toml_doc.value('database.ports') as []toml.Any
assert database_ports[0] as i64 == 8000
assert database_ports[1] as i64 == 8001
assert database_ports[2] as i64 == 8002
assert database_ports[0].int() == 8000
assert database_ports[1].int() == 8001
assert database_ports[2].int() == 8002
assert toml_doc.value('database.connection_max') as i64 == 5000
assert toml_doc.value('database.enabled') as bool == true
assert toml_doc.value('servers.alpha.ip').string() == '10.0.0.1'
assert toml_doc.value('servers.alpha.dc').string() == 'eqdc10'
assert toml_doc.value('servers.beta.ip').string() == '10.0.0.2'
assert toml_doc.value('servers.beta.dc').string() == 'eqdc10'
clients_data := (toml_doc.value('clients.data') as []toml.Any)
// dump(clients_data)
// assert false
gamma_delta_array := clients_data[0] as []toml.Any
digits_array := clients_data[1] as []toml.Any
assert gamma_delta_array[0].string() == 'gamma'
assert gamma_delta_array[1].string() == 'delta'
assert digits_array[0].int() == 1
assert digits_array[1].int() == 2
clients_hosts := (toml_doc.value('clients.hosts') as []toml.Any).as_strings()
assert clients_hosts[0] == 'alpha'
assert clients_hosts[1] == 'omega'
}
+23
View File
@@ -0,0 +1,23 @@
import toml
fn test_crlf() {
str_value := 'test string'
mut toml_txt := 'crlf_string = "test string"\r\n
# Comment with CRLF is not allowed'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value := toml_doc.value('crlf_string')
assert value == toml.Any(str_value)
assert value as string == str_value
assert value.string() == str_value
}
fn test_crlf_is_parsable_just_like_lf() {
crlf_content := '# a comment\r\ntitle = "TOML Example"\r\n[database]\r\nserver = "192.168.1.1"\r\nports = [ 8000, 8001, 8002 ]\r\n'
all := [crlf_content, crlf_content.replace('\r\n', '\n')]
for content in all {
res := toml.parse_text(content)!
assert res.value('title') == toml.Any('TOML Example')
assert (res.value('database') as map[string]toml.Any)['server']! == toml.Any('192.168.1.1')
}
}
+72
View File
@@ -0,0 +1,72 @@
import toml
fn test_dates() {
toml_txt := '
# Offset Date-Time
odt1 = 1979-05-27T07:32:00Z
odt2 = 1979-05-27T00:32:00-07:00
odt3 = 1979-05-27T00:32:00.999999-07:00
odt4 = 1979-05-27 07:32:00Z
# Local Date-Time
ldt1 = 1979-05-27T07:32:00
ldt2 = 1979-05-27T00:32:00.999999
# Local Date
ld1 = 1979-05-27
# Local Time
lt1 = 07:32:00
lt2 = 00:32:00.999999
'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
// Re-use vars
mut odt_time := toml.DateTime{'1979-05-27T07:32:00Z'}
mut odt_str := toml_doc.value('odt1').string()
// odt1 test section
assert odt_str == '1979-05-27T07:32:00Z'
odt1 := toml_doc.value('odt1')
assert odt1.datetime() == odt_time
// odt2 test section
odt_time = toml.DateTime{'1979-05-27T00:32:00-07:00'}
odt2 := toml_doc.value('odt2')
assert odt2.datetime() == odt_time
// odt3 test section
odt_time = toml.DateTime{'1979-05-27T00:32:00.999999-07:00'}
odt3 := toml_doc.value('odt3')
assert odt3.datetime() == odt_time
// odt4 test section
odt_time = toml.DateTime{'1979-05-27 07:32:00Z'}
odt4 := toml_doc.value('odt4')
assert odt4.datetime() == odt_time
// ldt1 test section
odt_time = toml.DateTime{'1979-05-27T07:32:00'}
ldt1 := toml_doc.value('ldt1')
assert ldt1.datetime() == odt_time
// ldt2 test section
odt_time = toml.DateTime{'1979-05-27T00:32:00.999999'}
ldt2 := toml_doc.value('ldt2')
assert ldt2.datetime() == odt_time
// ld1 test section
od_time := toml.Date{'1979-05-27'}
ld1 := toml_doc.value('ld1')
assert ld1.date() == od_time
assert ld1.string() == '1979-05-27'
// lt1 test section
mut ot_time := toml.Time{'07:32:00'}
lt1 := toml_doc.value('lt1')
assert lt1.time() == ot_time
assert lt1.string() == '07:32:00'
// lt2 test section
ot_time = toml.Time{'00:32:00.999999'}
lt2 := toml_doc.value('lt2')
assert lt2.time() == ot_time
assert lt2.string() == '00:32:00.999999'
}
@@ -0,0 +1,9 @@
import toml
fn test_default_to() {
default_value := 4321
mut toml_txt := 'var = 1234'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value := toml_doc.value('tar').default_to(default_value).int()
assert value == default_value
}
@@ -0,0 +1,85 @@
import toml
const toml_text = '# This TOML can reflect/decode to a struct
val_string = "test"
val_bool = false
val_int = 456
val_i64 = 4567
val_u64 = 45678
val_f32 = 200.2
val_f64 = 2000.2
val_datetime = 2024-10-05 09:10:20.000
val_date = 2099-09-09
val_time = 22:22:22.222
'
const toml_all_default_text = '# This TOML can reflect/decode to a struct all with default values'
struct Test {
val_string string = 'abcd'
val_bool bool = true
val_int int = 123
val_i64 i64 = 1234
val_u64 u64 = 12345
val_f32 f32 = 100.1
val_f64 f64 = 1000.1
val_datetime toml.DateTime = toml.DateTime{'1980-07-11 21:23:42.123'}
val_date toml.Date = toml.Date{'1977-07-07'}
val_time toml.Time = toml.Time{'11:11:11.111'}
}
fn test_reflect_default_values() {
toml_has_values := toml.parse_text(toml_text) or { panic(err) }
test_has_values := toml_has_values.reflect[Test]()
assert test_has_values.val_string == 'test'
assert test_has_values.val_bool == false
assert test_has_values.val_int == 456
assert test_has_values.val_i64 == 4567
assert test_has_values.val_u64 == 45678
assert test_has_values.val_f32 == 200.2
assert test_has_values.val_f64 == 2000.2
assert test_has_values.val_datetime == toml.DateTime{'2024-10-05 09:10:20.000'}
assert test_has_values.val_date == toml.Date{'2099-09-09'}
assert test_has_values.val_time == toml.Time{'22:22:22.222'}
toml_all_default_values := toml.parse_text(toml_all_default_text) or { panic(err) }
test_all_default_values := toml_all_default_values.reflect[Test]()
assert test_all_default_values.val_string == 'abcd'
assert test_all_default_values.val_bool == true
assert test_all_default_values.val_int == 123
assert test_all_default_values.val_i64 == 1234
assert test_all_default_values.val_u64 == 12345
assert test_all_default_values.val_f32 == 100.1
assert test_all_default_values.val_f64 == 1000.1
assert test_all_default_values.val_datetime == toml.DateTime{'1980-07-11 21:23:42.123'}
assert test_all_default_values.val_date == toml.Date{'1977-07-07'}
assert test_all_default_values.val_time == toml.Time{'11:11:11.111'}
}
fn test_decode_struct_default_values() {
test_has_values := toml.decode[Test](toml_text) or { panic(err) }
assert test_has_values.val_string == 'test'
assert test_has_values.val_bool == false
assert test_has_values.val_int == 456
assert test_has_values.val_i64 == 4567
assert test_has_values.val_u64 == 45678
assert test_has_values.val_f32 == 200.2
assert test_has_values.val_f64 == 2000.2
assert test_has_values.val_datetime == toml.DateTime{'2024-10-05 09:10:20.000'}
assert test_has_values.val_date == toml.Date{'2099-09-09'}
assert test_has_values.val_time == toml.Time{'22:22:22.222'}
test_all_default_values := toml.decode[Test](toml_all_default_text) or { panic(err) }
assert test_all_default_values.val_string == 'abcd'
assert test_all_default_values.val_bool == true
assert test_all_default_values.val_int == 123
assert test_all_default_values.val_i64 == 1234
assert test_all_default_values.val_u64 == 12345
assert test_all_default_values.val_f32 == 100.1
assert test_all_default_values.val_f64 == 1000.1
assert test_all_default_values.val_datetime == toml.DateTime{'1980-07-11 21:23:42.123'}
assert test_all_default_values.val_date == toml.Date{'1977-07-07'}
assert test_all_default_values.val_time == toml.Time{'11:11:11.111'}
}
@@ -0,0 +1,403 @@
import toml
enum JobTitle {
worker
executive
manager
}
struct Pet {
name string
nicknames []string
age u64
income int
height f32
has_furr bool
title JobTitle
address Address
meal_frequency map[string]int
// *¹ Currently it is only possible to decode a single nested struct generically.
// As soon as we decode another nested struct (e.g. within this struct, like `contact` below)
// or only one nested struct within another struct, it results in wrong values or errors.
// Related issue: https://github.com/vlang/v/issues/18110
// contact Contact
}
struct Address {
street string
city string
}
// *¹
/*
struct Contact {
phone string
}*/
struct AnyStruct {
val toml.Any
}
struct Employee {
mut:
name string
age int
salary f32
is_human bool
title JobTitle
}
struct Arrs {
strs []string
bools []bool
ints []int
i64s []i64
u64s []u64
f32s []f32
f64s []f64
dts []toml.DateTime
dates []toml.Date
times []toml.Time
}
// individual because toml.decode[Foo](str)! == foo is false
struct AnyArr {
arr []toml.Any
}
struct Item {
name string
}
struct Doc {
items []Item
}
struct MapDoc {
items map[string]Item
}
fn test_encode_and_decode() {
// *¹
// p := Pet{'Mr. Scratchy McEvilPaws', ['Freddy', 'Fred', 'Charles'], 8, -1, 0.8, true, .manager, Address{'1428 Elm Street', 'Springwood'}, Contact{'123-456-7890'}}
p := Pet{'Mr. Scratchy McEvilPaws', ['Freddy', 'Fred', 'Charles'], 8, -1, 0.8, true, .manager, Address{'1428 Elm Street', 'Springwood'}, {
'bones': 2
'kibble': 5
}}
s := 'name = "Mr. Scratchy McEvilPaws"
nicknames = [
"Freddy",
"Fred",
"Charles"
]
age = 8
income = -1
height = 0.8
has_furr = true
title = 2
address = { street = "1428 Elm Street", city = "Springwood" }
meal_frequency = { bones = 2, kibble = 5 }'
// contact = { phone = "123-456-7890" }' // *¹
assert toml.encode[Pet](p) == s
assert toml.decode[Pet](s)! == p
}
fn test_encode_and_decode_any() {
a := AnyStruct{toml.Any(10)}
s := 'val = 10'
assert toml.encode[AnyStruct](a) == s
assert toml.decode[AnyStruct](s)!.val.int() == 10
}
pub fn (e Employee) to_toml() string {
mut mp := map[string]toml.Any{}
mp['name'] = toml.Any(e.name)
mp['age'] = toml.Any(e.age)
mp['is_human'] = toml.Any(e.is_human)
// Change some values to assert that the custom method is used instead of generic encoding.
mp['salary'] = toml.Any(f32(e.salary) + 5000.0)
mp['title'] = toml.Any(int(e.title) + 1)
return mp.to_toml()
}
pub fn (mut e Employee) from_toml(any toml.Any) {
mp := any.as_map()
e.name = mp['name'] or { toml.Any('') }.string()
e.age = mp['age'] or { toml.Any(0) }.int()
e.is_human = mp['is_human'] or { toml.Any(false) }.bool()
// Change some values to assert that the custom method is used instead of generic decoding.
e.salary = mp['salary'] or { toml.Any(0) }.f32() - 15000.0
e.title = unsafe { JobTitle(mp['title'] or { toml.Any(0) }.int() - 2) }
}
fn test_custom_encode_and_decode() {
x := Employee{'Peter', 28, 95000.5, true, .executive}
s := toml.encode[Employee](x)
eprintln('Employee x: ${s}')
assert s == r'name = "Peter"
age = 28
is_human = true
salary = 100000.5
title = 2'
y := toml.decode[Employee](s) or {
println(err)
assert false
return
}
eprintln('Employee y: ${y}')
assert y.name == 'Peter'
assert y.age == 28
assert y.salary == 85000.5
assert y.is_human == true
assert y.title == .worker
}
struct Example1 {
arr []Problem
}
struct Example2 {
arr []Problem
}
struct Problem {
x int
}
pub fn (example Example1) to_toml() string {
return '[This is Valid]'
}
pub fn (problem Problem) to_toml() string {
return 'a problem'
}
fn test_custom_encode_of_complex_struct() {
assert toml.encode(Example1{}) == '[This is Valid]'
assert toml.encode(Example2{[Problem{}, Problem{}]}) == 'arr = [
"a problem",
"a problem"
]'
}
struct Example3 {
arr_arr [][]Problem
}
struct Example4 {
mp map[string]Problem
}
pub fn (example Example3) to_toml() string {
return '[This is Valid]'
}
pub fn (example Example4) to_toml() string {
return '[This is Valid]'
}
fn test_custom_encode_of_nested_complex_struct() {
assert toml.encode(Example3{}) == '[This is Valid]'
assert toml.encode(Example4{}) == '[This is Valid]'
}
struct Example5 {
mp map[string]Problem
}
fn test_map_encode_of_complex_struct() {
mut mp := map[string]Problem{}
mp['key_one'] = Problem{}
mp['key_two'] = Problem{}
assert toml.encode(Example5{ mp: mp }) == 'mp = { key_one = "a problem", key_two = "a problem" }'
}
struct Example6 {
ptr voidptr
r rune
}
fn test_encode_for_exotic_types() {
assert toml.encode(Example6{ ptr: unsafe { &voidptr(nil) }, r: `🚀` }) == 'ptr = "0x0"\nr = "🚀"'
}
fn test_array_encode_decode() {
a := Arrs{
strs: ['foo', 'bar']
bools: [true, false]
ints: [-1, 2]
i64s: [i64(-2)]
u64s: [u64(123)]
f32s: [f32(1.0), f32(2.5)]
f64s: [100000.5, -123.0]
dts: [toml.DateTime{'1979-05-27T07:32:00Z'}, toml.DateTime{'1979-05-27T07:32:00Z'}]
dates: [toml.Date{'1979-05-27'}, toml.Date{'2022-12-31'}]
times: [toml.Time{'07:32:59'}, toml.Time{'17:32:04'}]
}
s := 'strs = [
"foo",
"bar"
]
bools = [
true,
false
]
ints = [
-1,
2
]
i64s = [
-2
]
u64s = [
123
]
f32s = [
1.0,
2.5
]
f64s = [
100000.5,
-123.0
]
dts = [
1979-05-27T07:32:00Z,
1979-05-27T07:32:00Z
]
dates = [
1979-05-27,
2022-12-31
]
times = [
07:32:59,
17:32:04
]'
assert toml.encode[Arrs](a) == s
assert toml.decode[Arrs](s)! == a
any_a := AnyArr{[toml.Any(10), 20, 30]}
any_s := 'arr = [
10,
20,
30
]'
assert toml.encode[AnyArr](any_a) == any_s
assert toml.decode[AnyArr](any_s)!.arr.map(it.int()) == [10, 20, 30]
}
fn test_array_of_struct_decode() {
doc := Doc{
items: [
Item{
name: 'item #1'
},
Item{
name: 'item #2'
},
Item{
name: 'item #3'
},
]
}
encoded := toml.encode[Doc](doc)
assert toml.decode[Doc](encoded)! == doc
array_of_tables := '[[items]]
name = "item #1"
[[items]]
name = "item #2"
[[items]]
name = "item #3"'
assert toml.decode[Doc](array_of_tables)! == doc
}
fn test_map_of_struct_decode() {
doc := MapDoc{
items: {
'first': Item{
name: 'first one'
}
'second': Item{
name: 'second one'
}
}
}
encoded := toml.encode[MapDoc](doc)
assert toml.decode[MapDoc](encoded)! == doc
table_doc := '[items.first]
name = "first one"
[items.second]
name = "second one"'
assert toml.decode[MapDoc](table_doc)! == doc
}
fn test_decode_doc() {
doc := toml.parse_text('name = "Peter"
age = 28
is_human = true
salary = 100000.5
title = 2')!
e := doc.decode[Employee]()!
assert e.name == 'Peter'
assert e.age == 28
assert e.salary == 100000.5
assert e.is_human == true
assert e.title == .manager
}
fn test_unsupported_type() {
s := 'name = "Peter"'
err_msg := 'toml.decode: expected struct, found '
if _ := toml.decode[string](s) {
assert false
} else {
assert err.msg() == err_msg + 'string'
}
if _ := toml.decode[[]string](s) {
assert false
} else {
assert err.msg() == err_msg + '[]string'
}
if _ := toml.decode[int](s) {
assert false
} else {
assert err.msg() == err_msg + 'int'
}
if _ := toml.decode[[]f32](s) {
assert false
} else {
assert err.msg() == err_msg + '[]f32'
}
// ...
doc := toml.parse_text('name = "Peter"')!
assert doc.value('name').string() == 'Peter'
if _ := doc.decode[string]() {
assert false
} else {
assert err.msg() == 'Doc.decode: expected struct, found string'
}
}
fn test_date_array_decode_with_spaces() {
// test space after yyyy-mm-dd is not date-time separator but white to consume. issue # 25279
s := '
dates = [ 1979-05-27 , 2022-12-31 ]
'
a := Arrs{
dates: [toml.Date{'1979-05-27'}, toml.Date{'2022-12-31'}]
}
assert toml.decode[Arrs](s)! == a
}
@@ -0,0 +1,400 @@
import os
import toml
import toml.ast
import x.json2
const hide_oks = os.getenv('VTEST_HIDE_OK') == '1'
// Can be set to `true` to process tests that stress test the parser
// by having large data amounts - these pass - but slow down the test run
const do_large_files = os.getenv('VTEST_TOML_DO_LARGE_FILES') == '1'
// Can be set to `true` to process tests that triggers a slow conversion
// process that uses `python` to convert from YAML to JSON.
const do_yaml_conversion = os.getenv('VTEST_TOML_DO_YAML_CONVERSION') == '1'
// Instructions for developers:
// The actual tests and data can be obtained by doing:
// `git clone --depth 1 https://github.com/iarna/toml-spec-tests.git vlib/toml/tests/testdata/iarna/toml-test`
// See also the CI toml tests
// Kept for easier handling of future updates to the tests
const valid_exceptions = []string{}
const invalid_exceptions = []string{}
const valid_value_exceptions = [
'values/spec-date-time-3.toml',
'values/spec-date-time-4.toml',
'values/spec-readme-example.toml',
'values/spec-date-time-6.toml',
'values/spec-date-time-5.toml',
'values/spec-date-time-1.toml',
'values/spec-date-time-2.toml',
'values/qa-table-inline-nested-1000.toml',
'values/qa-array-inline-nested-1000.toml',
]
const yaml_value_exceptions = [
'values/spec-float-5.toml', // YAML: "1e6", V: 1000000
'values/spec-float-9.toml', // YAML: "-0e0", V: 0
'values/spec-float-6.toml', // YAML: "-2E-2", V: -0.02
'values/spec-float-4.toml', // YAML: "5e+22", V: 50000000000000004000000
]
const jq = os.find_abs_path_of_executable('jq') or { '' }
const python = os.find_abs_path_of_executable('python') or { '' }
const compare_work_dir_root = os.join_path(os.vtmp_dir(), 'toml_iarna')
// From: https://stackoverflow.com/a/38266731/1904615
const jq_normalize = r'# Apply f to composite entities recursively using keys[], and to atoms
def sorted_walk(f):
. as $in
| if type == "object" then
reduce keys[] as $key
( {}; . + { ($key): ($in[$key] | sorted_walk(f)) } ) | f
elif type == "array" then map( sorted_walk(f) ) | f
else f
end;
def normalize: sorted_walk(if type == "array" then sort else . end);
normalize'
fn run(args []string) !string {
res := os.execute(args.join(' '))
if res.exit_code != 0 {
return error('${args[0]} failed with return code ${res.exit_code}.\n${res.output}')
}
return res.output
}
// test_iarna_toml_spec_tests run though 'testdata/iarna/toml-test/*' if found.
fn test_iarna_toml_spec_tests() {
this_file := @FILE
test_root := os.join_path(os.dir(this_file), 'testdata', 'iarna')
if os.is_dir(test_root) {
valid_test_files := os.walk_ext(os.join_path(test_root, 'values'), '.toml')
println('Testing ${valid_test_files.len} valid TOML files...')
mut valid := 0
mut e := 0
for i, valid_test_file in valid_test_files {
mut relative := valid_test_file.all_after('iarna').trim_left(os.path_separator)
$if windows {
relative = relative.replace('/', '\\')
}
if !do_large_files && valid_test_file.contains('qa-') {
e++
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" LARGE FILE...')
continue
}
if relative in valid_exceptions {
e++
idx := valid_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...')
continue
}
if !hide_oks {
println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
}
toml_doc := toml.parse_file(valid_test_file)!
valid++
}
println('${valid}/${valid_test_files.len} TOML files were parsed correctly')
if valid_exceptions.len > 0 {
println('TODO Skipped parsing of ${e} valid TOML files...')
}
// If the command-line tool `jq` is installed, value tests can be run as well.
if jq != '' {
println('Testing value output of ${valid_test_files.len} valid TOML files using "${jq}"...')
if os.exists(compare_work_dir_root) {
os.rmdir_all(compare_work_dir_root)!
}
os.mkdir_all(compare_work_dir_root)!
jq_normalize_path := os.join_path(compare_work_dir_root, 'normalize.jq')
os.write_file(jq_normalize_path, jq_normalize)!
valid = 0
e = 0
for i, valid_test_file in valid_test_files {
mut relative := valid_test_file.all_after('iarna').trim_left(os.path_separator)
$if windows {
relative = relative.replace('/', '\\')
}
// Skip the file if we know it can't be parsed or we know that the value retrieval needs work.
if relative in valid_exceptions {
e++
idx := valid_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...')
continue
}
if relative in valid_value_exceptions {
e++
idx := valid_value_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID VALUE EXCEPTION [${idx}/${valid_value_exceptions.len}]...')
continue
}
valid_test_file_name := os.file_name(valid_test_file).all_before_last('.')
uses_json_format := os.exists(valid_test_file.all_before_last('.') + '.json')
// Use python to convert the YAML files to json - it yields some inconsistencies
// so we skip some of them
mut converted_from_yaml := false
mut converted_json_path := ''
if !uses_json_format {
$if windows {
println('N/A [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
continue
}
if python == '' {
println('N/A [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
continue
}
if !do_yaml_conversion || relative in yaml_value_exceptions {
e++
idx := yaml_value_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" YAML VALUE EXCEPTION [${idx}/${valid_value_exceptions.len}]...')
continue
}
if !do_large_files && valid_test_file.contains('qa-') {
e++
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" LARGE FILE...')
continue
}
iarna_yaml_path := valid_test_file.all_before_last('.') + '.yaml'
if os.exists(iarna_yaml_path) {
converted_json_path = os.join_path(compare_work_dir_root,
'${valid_test_file_name}.yaml.json')
run([python, '-c',
"'import sys, yaml, json; json.dump(yaml.load(sys.stdin, Loader=yaml.FullLoader), sys.stdout, indent=4)'",
'<', iarna_yaml_path, '>', converted_json_path]) or {
contents := os.read_file(iarna_yaml_path)!
// NOTE there's known errors with the python convention method.
// For now we just ignore them as it's a broken tool - not a wrong test-case.
// Uncomment this print to see/check them.
// eprintln(err.msg() + '\n${contents}')
e++
println('ERR [${i + 1}/${valid_test_files.len}] "${valid_test_file}" EXCEPTION [${e}/${valid_value_exceptions.len}]...')
continue
}
converted_from_yaml = true
}
}
if !hide_oks {
println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
}
toml_doc := toml.parse_file(valid_test_file)!
v_toml_json_path := os.join_path(compare_work_dir_root,
'${valid_test_file_name}.v.json')
iarna_toml_json_path := os.join_path(compare_work_dir_root,
'${valid_test_file_name}.json')
os.write_file(v_toml_json_path, to_iarna(toml_doc.ast.table, converted_from_yaml))!
if converted_json_path == '' {
converted_json_path = valid_test_file.all_before_last('.') + '.json'
}
iarna_json := os.read_file(converted_json_path)!
os.write_file(iarna_toml_json_path, iarna_json)!
v_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', v_toml_json_path]) or {
contents := os.read_file(v_toml_json_path)!
panic(err.msg() + '\n${contents}')
}
cmd := [jq, '-S', '-f "${jq_normalize_path}"', iarna_toml_json_path]
iarna_normalized_json := run(cmd) or {
contents := os.read_file(v_toml_json_path)!
panic(err.msg() + '\n${contents}\n\ncmd: ${cmd.join(' ')}')
}
assert iarna_normalized_json == v_normalized_json
valid++
}
println('${valid}/${valid_test_files.len} TOML files were parsed correctly and value checked')
if valid_value_exceptions.len > 0 {
println('TODO Skipped value checks of ${e} valid TOML files...')
}
}
invalid_test_files := os.walk_ext(os.join_path(test_root, 'errors'), '.toml')
println('Testing ${invalid_test_files.len} invalid TOML files...')
mut invalid := 0
e = 0
for i, invalid_test_file in invalid_test_files {
mut relative := invalid_test_file.all_after('iarna').trim_left(os.path_separator)
$if windows {
relative = relative.replace('/', '\\')
}
if relative in invalid_exceptions {
e++
idx := invalid_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}" INVALID EXCEPTION [${idx}/${invalid_exceptions.len}]...')
continue
}
if !hide_oks {
println('OK [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}"...')
}
if toml_doc := toml.parse_file(invalid_test_file) {
content_that_should_have_failed := os.read_file(invalid_test_file)!
println(' This TOML should have failed:\n${'-'.repeat(40)}\n${content_that_should_have_failed}\n${'-'.repeat(40)}')
assert false
} else {
if !hide_oks {
println(' ${err.msg()}')
}
assert true
}
invalid++
}
println('${invalid}/${invalid_test_files.len} TOML files were parsed correctly')
if invalid_exceptions.len > 0 {
println('TODO Skipped parsing of ${invalid_exceptions.len} invalid TOML files...')
}
} else {
println('No test data directory found in "${test_root}"')
assert true
}
}
// to_iarna_time
fn to_iarna_time(time_str string) string {
if time_str.contains('.') {
date_and_time := time_str.all_before('.')
mut ms := time_str.all_after('.')
z := if ms.contains('Z') { 'Z' } else { '' }
ms = ms.replace('Z', '')
if ms.len > 3 {
ms = ms[..3]
}
return date_and_time + '.' + ms + z
} else {
return time_str + '.000'
}
}
// to_iarna returns a iarna compatible json string converted from the `value` ast.Value.
fn to_iarna(value ast.Value, skip_value_map bool) string {
match value {
ast.Quoted {
json_text := json2.Any(value.text).json_str()
if skip_value_map {
return json_text
}
return '{ "type": "string", "value": ${json_text} }'
}
ast.DateTime {
// Normalization for json
mut 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'
}
// NOTE test suite inconsistency.
// It seems it's implementation specific how time and
// date-time values are represented in detail. For now we follow the toml-lang format
// that expands to 6 digits which is also a valid RFC 3339 representation.
json_text = to_iarna_time(json_text[1..json_text.len - 1])
if skip_value_map {
return json_text
}
return '{ "type": "${typ}", "value": "${json_text}" }'
}
ast.Date {
json_text := json2.Any(value.text).json_str()
if skip_value_map {
return json_text
}
return '{ "type": "date", "value": ${json_text} }'
}
ast.Time {
mut json_text := json2.Any(value.text).json_str()
// Note: Removes the quotes of the encoded JSON string - Ned
json_text = to_iarna_time(json_text[1..json_text.len - 1])
if skip_value_map {
return json_text
}
return '{ "type": "time", "value": "${json_text}" }'
}
ast.Bool {
json_text := json2.Any(value.text.bool()).json_str()
if skip_value_map {
return json_text
}
return '{ "type": "bool", "value": "${json_text}" }'
}
ast.Null {
json_text := json2.Any(value.text).json_str()
if skip_value_map {
return json_text
}
return '{ "type": "null", "value": ${json_text} }'
}
ast.Number {
if value.text.contains('inf') {
mut json_text :=
value.text.replace('inf', '1.7976931348623157e+308') // Inconsistency ???
if skip_value_map {
return '${json_text}'
}
return '{ "type": "float", "value": "${json_text}" }'
}
if value.text.contains('nan') {
mut json_text := 'null'
if skip_value_map {
return '${json_text}'
}
return '{ "type": "float", "value": "${json_text}" }'
}
if !value.text.starts_with('0x')
&& (value.text.contains('.') || value.text.to_lower().contains('e')) {
mut val := '${value.f64()}'.replace('.e+', '.0e') // json notation
if !val.contains('.') && val != '0' { // json notation
val += '.0'
}
if skip_value_map {
return '${val}'
}
return '{ "type": "float", "value": "${val}" }'
}
v := value.i64()
if skip_value_map {
return '${v}'
}
return '{ "type": "integer", "value": "${v}" }'
}
map[string]ast.Value {
mut str := '{ '
for key, val in value {
json_key := json2.Any(key).json_str()
str += ' ${json_key}: ${to_iarna(val, skip_value_map)},'
}
str = str.trim_right(',')
str += ' }'
return str
}
[]ast.Value {
mut str := '[ '
for val in value {
str += ' ${to_iarna(val, skip_value_map)},'
}
str = str.trim_right(',')
str += ' ]\n'
return str
}
}
return '<error>'
}
+16
View File
@@ -0,0 +1,16 @@
import toml
struct Address {
street string
city string
}
fn test_inline() {
a := Address{'Elm Street', 'Springwood'}
mut mp := map[string]toml.Any{}
mp['street'] = toml.Any(a.street)
mp['city'] = toml.Any(a.city)
assert mp.to_inline_toml() == '{ street = "Elm Street", city = "Springwood" }'
}
@@ -0,0 +1,15 @@
import os
import toml
import toml.to
const fprefix = os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))
fn test_parse() {
toml_doc := toml.parse_file(os.real_path(fprefix + '.toml'))!
toml_json := to.json(toml_doc)
println(toml_json)
out_file_json := os.read_file(os.real_path(fprefix + '.out'))!
assert toml_json == out_file_json
}
+18
View File
@@ -0,0 +1,18 @@
import os
import toml
import toml.to
fn test_parse() {
toml_file :=
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.toml'
toml_doc := toml.parse_file(toml_file) or { panic(err) }
toml_json := to.json(toml_doc)
out_file :=
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.out'
out_file_json := os.read_file(out_file) or { panic(err) }
println(toml_json)
assert toml_json == out_file_json
}
+48
View File
@@ -0,0 +1,48 @@
import os
import toml
import toml.to
fn path_by_extension(ext string) string {
return os.join_path(os.dir(@VEXE), 'vlib/toml/tests/testdata/key_test.${ext}')
}
fn test_keys() {
toml_doc := toml.parse_file(path_by_extension('toml'))!
mut value := toml_doc.value('34-11')
assert value.int() == 23
value = toml_doc.value('1.2')
assert value.int() == 3
value = toml_doc.value('34-12.2')
assert value.int() == 42
toml_json := to.json(toml_doc)
out_file_json := os.read_file(path_by_extension('out'))!
println(toml_json)
assert toml_json == out_file_json
if x := toml_doc.value_opt('unknown key') {
assert false
} else {
assert err.msg() == 'no value for key'
}
if x := toml_doc.value_opt("'a") {
assert false
} else {
assert err.msg() == 'invalid dotted key'
}
}
fn test_parse_dotted_key() {
assert toml.parse_dotted_key('')! == []
assert toml.parse_dotted_key('abc')! == ['abc']
assert toml.parse_dotted_key('tube.test."test.test".h."i.j."."k"')! == ['tube', 'test',
'test.test', 'h', 'i.j.', 'k']
if x := toml.parse_dotted_key("'some unclosed string") {
assert false
} else {
assert err.msg().starts_with('parse_dotted_key: could not parse key, missing closing string delimiter')
}
}
@@ -0,0 +1,20 @@
import os
import toml
// Instructions for developers:
// The large (1MB) TOML file can be obtained by doing:
// `wget https://gist.githubusercontent.com/Larpon/89b0e3d94c6903851ff15559e5df7a05/raw/62a1f87a4e37bf157f2e0bfb32d85d840c98e422/large_toml_file_test.toml`
// `mv large_toml_file_test.toml vlib/toml/tests/testdata/`
const toml_file = os.join_path_single(@VEXEROOT,
'vlib/toml/tests/testdata/large_toml_file_test.toml')
fn test_large_file() {
if !os.exists(toml_file) {
eprintln('skipping ${@FILE} since ${toml_file} is missing.')
return
}
println('Testing parsing of large (${os.file_size(toml_file)} bytes) "${toml_file}"...')
doc := toml.parse_file(toml_file) or { panic(err) }
assert true
}
+43
View File
@@ -0,0 +1,43 @@
import toml
const toml_text = '
[db]
enabled = true
[servers]
# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[servers.alpha.tricky]
ip = "10.0.0.100"
[firewall.rules]
block = true
[firewall.rules.limit]
ip = "10.0.0.101"
'
fn test_parse() {
toml_doc := toml.parse_text(toml_text) or { panic(err) }
// dump(toml_doc.ast)
// assert false
assert toml_doc.value('db.enabled').bool()
// TODO: make this work
assert toml_doc.value('servers.alpha.ip').string() == '10.0.0.1'
assert toml_doc.value('servers.alpha.dc').string() == 'eqdc10'
assert toml_doc.value('servers.beta.ip').string() == '10.0.0.2'
assert toml_doc.value('servers.beta.dc').string() == 'eqdc10'
assert toml_doc.value('servers.alpha.tricky.ip').string() == '10.0.0.100'
assert toml_doc.value('firewall.rules.limit.ip').string() == '10.0.0.101'
assert toml_doc.value('firewall.rules.block').bool() == true
}
@@ -0,0 +1,13 @@
// vtest retry: 3
import toml
fn test_quoted_keys() {
str_value := 'V rocks!'
toml_txt := 'a."b.c" = "V rocks!"'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value := toml_doc.value('a."b.c"')
assert value == toml.Any(str_value)
assert value as string == str_value
assert value.string() == str_value
}
@@ -0,0 +1,9 @@
import toml
fn test_quoted_string_crlf() {
toml_txt := 'str1 = """tcc \\\r\nabc \\\r\n123"""'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value := toml_doc.value('str1').string()
assert value == 'tcc abc 123'
}
+108
View File
@@ -0,0 +1,108 @@
import toml
const toml_text = '# This TOML can reflect to a struct
name = "Tom"
age = 45
height = 1.97
birthday = 1980-04-23
strings = [
"v matures",
"like rings",
"spread in the",
"water"
]
bools = [true, false, true, true]
bools_br = [
true,
false,
true,
true
]
floats = [0.0, 1.0, 2.0, 3.0]
floats_br = [
0.0,
1.05,
2.025,
3.5001
]
int_map = {"a" = 0, "b" = 1, "c" = 2, "d" = 3}
[bio]
text = "Tom has done many great things"
years_of_service = 5
[field_remap]
txt = "I am remapped"
uint64 = 100
[config]
data = [ 1, 2, 3 ]
levels = { "info" = 1, "warn" = 2, "critical" = 3 }
'
struct FieldRemap {
text string @[toml: 'txt']
num u64 @[toml: 'uint64']
}
struct Bio {
text string
years_of_service int
}
struct User {
name string
age int
height f64
birthday toml.Date
strings []string
bools []bool
bools_br []bool
floats []f32
floats_br []f32
int_map map[string]int
config toml.Any
mut:
bio Bio
remap FieldRemap
}
fn test_reflect() {
toml_doc := toml.parse_text(toml_text) or { panic(err) }
mut user := toml_doc.reflect[User]()
user.bio = toml_doc.value('bio').reflect[Bio]()
user.remap = toml_doc.value('field_remap').reflect[FieldRemap]()
assert user.name == 'Tom'
assert user.age == 45
assert user.height == 1.97
assert user.birthday.str() == '1980-04-23'
assert user.strings == ['v matures', 'like rings', 'spread in the', 'water']
assert user.bools == [true, false, true, true]
assert user.bools_br == [true, false, true, true]
assert user.floats == [f32(0.0), 1.0, 2.0, 3.0]
assert user.floats_br == [f32(0.0), 1.05, 2.025, 3.5001]
assert user.int_map == {
'a': 0
'b': 1
'c': 2
'd': 3
}
assert user.bio.text == 'Tom has done many great things'
assert user.bio.years_of_service == 5
assert user.remap.text == 'I am remapped'
assert user.remap.num == 100
assert user.config.value('data[0]').int() == 1
assert user.config.value('levels.warn').int() == 2
}
@@ -0,0 +1,26 @@
import toml
fn test_spaced_keys() {
str_value := 'V rocks!'
toml_txt := '
"o" . pq . r = "Yuk"
[[ a . "b.c" ]]
d . e = "V rocks!"
[ tube . test . "test.test" ]
h . "i.j." . "k" = "Cryptic"
'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
mut value := toml_doc.value('a."b.c"[0].d.e')
assert value == toml.Any(str_value)
assert value as string == str_value
assert value.string() == str_value
value = toml_doc.value('"o".pq.r')
assert value.string() == 'Yuk'
value = toml_doc.value('tube.test."test.test".h."i.j."."k"')
assert value.string() == 'Cryptic'
}
+97
View File
@@ -0,0 +1,97 @@
import os
import toml
const toml_multiline_text_1 = 'multi1 = """one"""
multi2 = """one
two"""
multi3 = """
one
two
three"""
multi4 = """
one
two
three
four
"""'
const toml_multiline_text_2 = "multi1 = '''one'''
multi2 = '''one
two'''
multi3 = '''
one
two
three'''
multi4 = '''
one
two
three
four
'''"
const toml_unicode_escapes = r'short = "\u03B4"
long = "\U000003B4"'
fn test_multiline_strings() {
mut toml_doc := toml.parse_text(toml_multiline_text_1) or { panic(err) }
mut value := toml_doc.value('multi1')
assert value.string() == 'one'
value = toml_doc.value('multi2')
assert value.string() == 'one\ntwo'
value = toml_doc.value('multi3')
assert value.string() == 'one\ntwo\nthree'
value = toml_doc.value('multi4')
assert value.string() == 'one\ntwo\nthree\nfour\n'
toml_doc = toml.parse_text(toml_multiline_text_2) or { panic(err) }
value = toml_doc.value('multi1')
assert value.string() == 'one'
value = toml_doc.value('multi2')
assert value.string() == 'one\ntwo'
value = toml_doc.value('multi3')
assert value.string() == 'one\ntwo\nthree'
value = toml_doc.value('multi4')
assert value.string() == 'one\ntwo\nthree\nfour\n'
toml_file :=
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.toml'
toml_doc = toml.parse_file(toml_file) or { panic(err) }
value = toml_doc.value('lit_one')
assert value.string() == "'one quote'"
value = toml_doc.value('lit_two')
assert value.string() == "''two quotes''"
value = toml_doc.value('mismatch1')
assert value.string() == 'aaa' + "'''" + 'bbb'
value = toml_doc.value('mismatch2')
assert value.string() == 'aaa' + '"""' + 'bbb'
}
fn test_unicode_escapes() {
mut toml_doc := toml.parse_text(toml_unicode_escapes) or { panic(err) }
mut value := toml_doc.value('short')
assert value.string() == '\u03B4' // <- This escape is handled by V
value = toml_doc.value('long')
assert value.string() == 'δ' // <- for the long escape we compare with the unicode point
}
fn test_literal_strings() {
toml_file :=
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.toml'
toml_doc := toml.parse_file(toml_file) or { panic(err) }
assert toml_doc.value('lit1').string() == r'\' // '\'
assert toml_doc.value('lit2').string() == r'\\' // '\\'
assert toml_doc.value('lit3').string() == r'\tricky\' // '\tricky\'
// NOTE to Windows users: git is set to use Unix EOLs for all TOML files (*.toml) in the repo.
// See `.gitattributes` in the project root for the rule in action.
// These lines would look like this on Windows:
// assert toml_doc.value('ml_lit1').string() == '\r\n\\'
assert toml_doc.value('ml_lit1').string() == '\\'
assert toml_doc.value('ml_lit2').string() == '\\\n\\'
assert toml_doc.value('ml_lit3').string() == '\\\ntricky\\\n'
}
+89
View File
@@ -0,0 +1,89 @@
import toml
const toml_table_text = 'inline = {a.b = 42}
many.dots.here.dot.dot.dot = {a.b.c = 1, a.b.d = 2}
a = { a.b = 1 }
b = { "a"."b" = 1 }
c = { a . b = 1 }
d = { \'a\' . "b" = 1 }
e = {a.b=1}
[tbl]
a.b.c = {d.e=1}
[tbl.x]
a.b.c = {d.e=1}
[[arr]]
t = {a.b=1}
T = {a.b=1}
[[arr]]
t = {a.b=2}
T = {a.b=2}'
fn test_tables() {
mut toml_doc := toml.parse_text(toml_table_text) or { panic(err) }
mut value := toml_doc.value('inline.a.b')
assert value.int() == 42
value = toml_doc.value('many.dots.here.dot.dot.dot.a.b.c')
assert value.int() == 1
value = toml_doc.value('many.dots.here.dot.dot.dot.a.b.d')
assert value.int() == 2
value = toml_doc.value('a.a.b')
assert value.int() == 1
value = toml_doc.value('b.a.b')
assert value.int() == 1
value = toml_doc.value('c.a.b')
assert value.int() == 1
value = toml_doc.value('d.a.b')
assert value.int() == 1
value = toml_doc.value('e.a.b')
assert value.int() == 1
value = toml_doc.value('tbl.a.b.c.d.e')
assert value.int() == 1
value = toml_doc.value('tbl.x.a.b.c.d.e')
assert value.int() == 1
mut m := toml_doc.value('tbl') as map[string]toml.Any
value = m.value('a.b.c.d.e')
assert value.int() == 1
value = m.value('x.a.b.c.d.e')
assert value.int() == 1
arr := toml_doc.value('arr') as []toml.Any
for i := 0; i < arr.len; i++ {
entry := (arr[i] as map[string]toml.Any)
value = entry.value('t.a.b')
assert value.int() == i + 1
value = entry.value('T.a.b')
assert value.int() == i + 1
}
arr0 := arr[0] as map[string]toml.Any
value = arr0.value('t.a.b')
assert value.int() == 1
value = arr0.value('T.a.b')
assert value.int() == 1
arr1 := arr[1] as map[string]toml.Any
value = arr1.value('t.a.b')
assert value.int() == 2
value = arr1.value('T.a.b')
assert value.int() == 2
}
@@ -0,0 +1,4 @@
iarna/
toml_lang/
toml_rs/
large_toml_file_test.toml
@@ -0,0 +1 @@
{ "products": [ { "name": "Hammer", "sku": 738594937 }, { }, { "name": "Nail", "sku": 284758393, "color": "gray" } ] }
@@ -0,0 +1 @@
{ "albums": [ { "name": "Born to Run", "songs": [ { "name": "Jungleland" }, { "name": "Meeting Across the River" } ] }, { "name": "Born in the USA", "songs": [ { "name": "Glory Days" }, { "name": "Dancing in the Dark" } ] } ] }
@@ -0,0 +1 @@
{ "a": [ { "b": [ { "c": { "d": "val0" } }, { "c": { "d": "val1" } } ] } ] }
@@ -0,0 +1 @@
{ "Toml": [ { "ID": "1", "Index": 0, "Range": [ 0, 1, 2, 3 ], "Greeting": "Hello!", "Name": { "First": "Dolores", "Last": "Alvarado" }, "Friends": [ { "ID": 0, "Name": "Tom" }, { "ID": 1, "Name": "Dollie" } ] }, { "ID": "2", "Index": 1, "Range": [ 0, 1, 2, 3 ], "Greeting": "Hep!", "Name": { "First": "Rush", "Last": "Washington" }, "Friends": [ { "ID": 0, "Name": "Staci" }, { "ID": 1, "Name": "Dollie" } ] } ] }
@@ -0,0 +1,33 @@
[[Toml]]
ID = "1"
Index = 0
Range = [0, 1, 2, 3]
Greeting = "Hello!"
[Toml.Name]
First = "Dolores"
Last = "Alvarado"
[[Toml.Friends]]
ID = 0
Name = "Tom"
[[Toml.Friends]]
ID = 1
Name = "Dollie"
[[Toml]]
ID = "2"
Index = 1
Range = [0, 1, 2, 3]
Greeting = "Hep!"
[Toml.Name]
First = "Rush"
Last = "Washington"
[[Toml.Friends]]
ID = 0
Name = "Staci"
[[Toml.Friends]]
ID = 1
Name = "Dollie"
@@ -0,0 +1 @@
{ "albums": [ { "songs": [ { }, { } ] } ], "artists": [ { "home": { "address": { } } } ] }
@@ -0,0 +1,6 @@
[[ albums ]]
[[ albums.songs ]]
[[ albums.songs ]]
[[ artists ]]
[ artists.home.address ]
@@ -0,0 +1 @@
{ "ひらがな": "ひら" }
@@ -0,0 +1 @@
"ひらがな" = 'ひら'
+1
View File
@@ -0,0 +1 @@
{ "v": true, "animal": { "type": { "name": "pug" } }, "inline": { "a": 4, "b.c": 6, "b": { "c": 7 } }, "db": { "t": true }, "ij": { "a": { "i": 1, "j": 2 }, "b": { "i": "3", "j": "4" } }, "fruit": { "apple": { "color": "red", "taste": { "sweet": true }, "texture": { "smooth": true } } } }
+25
View File
@@ -0,0 +1,25 @@
v = true
animal = { type.name = "pug" }
inline = { "a" = 4, "b.c" = 6, b.c = 7 }
[db]
t = true
[ij]
[ij.a]
i = 1
j = 2
[ij.b]
i = "3"
j = "4"
[fruit]
apple.color = "red"
apple.taste.sweet = true
[fruit.apple.texture]
smooth = true
+1
View File
@@ -0,0 +1 @@
{ "34-11": 23, "1": { "2": 3 }, "34-12": { "2": 42 }, "34-20": { }, "5": { "6": { } }, "34-13": { "2": { } } }
+9
View File
@@ -0,0 +1,9 @@
34-11 = 23 # came out as "34-11 " = 23
1.2 = 3 # came out as "1.2" = 3
34-12.2 = 42 # came out as "34-12.2" = 42
[34-20]
[5.6]
[34-13.2]
+27
View File
@@ -0,0 +1,27 @@
# Make sure that quotes inside multiline strings are allowed, including right
# after the opening '''/""" and before the closing '''/"""
lit_one = ''''one quote''''
lit_two = '''''two quotes'''''
lit_one_space = ''' 'one quote' '''
lit_two_space = ''' ''two quotes'' '''
one = """"one quote""""
two = """""two quotes"""""
one_space = """ "one quote" """
two_space = """ ""two quotes"" """
mismatch1 = """aaa'''bbb"""
mismatch2 = '''aaa"""bbb'''
lit1 = '\'
lit2 = '\\'
lit3 = '\tricky\'
ml_lit1 = '''
\'''
ml_lit2 = '''\
\'''
ml_lit3 = '''\
tricky\
'''
+1
View File
@@ -0,0 +1 @@
{ "title": "TOML Example", "owner": { "name": "Tom Preston-Werner", "dob": "1979-05-27T07:32:00-08:00" }, "database": { "server": "192.168.1.1", "ports": [ 8000, 8001, 8002 ], "connection_max": 5000, "enabled": true }, "servers": { "alpha": { "ip": "10.0.0.1", "dc": "eqdc10" }, "beta": { "ip": "10.0.0.2", "dc": "eqdc10" } }, "clients": { "data": [ [ "gamma", "delta" ], [ 1, 2 ] ], "hosts": [ "alpha", "omega" ] } }
+33
View File
@@ -0,0 +1,33 @@
# This is a TOML document.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates
[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,33 @@
# This is a TOML document with an UTF-8 BOM header.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates
[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
+15
View File
@@ -0,0 +1,15 @@
import toml
struct TestStruct {
foo int
bar bool @[skip]
baz string = 'def' @[toml: barbaz]
}
fn test_toml_attr_encode() {
assert toml.encode(TestStruct{}) == 'foo = 0\nbarbaz = "def"'
}
fn test_toml_attr_decode() {
assert toml.decode[TestStruct]('foo = 0\nbarbaz = "def"')! == TestStruct{}
}
+51
View File
@@ -0,0 +1,51 @@
import os
import toml
import toml.to
import toml.ast
const empty_toml_document = toml.Doc{
ast: &ast.Root(unsafe { nil })
}
const toml_text_with_utf8_bom = os.read_file(os.real_path(os.join_path(os.dir(@FILE), 'testdata',
'toml_with_utf8_bom' + '.toml'))) or { panic(err) }
const toml_text_with_utf16_bom = os.read_file(os.real_path(os.join_path(os.dir(@FILE), 'testdata',
'toml_with_utf16_bom' + '.toml'))) or { panic(err) }
const toml_text_with_utf32_bom = os.read_file(os.real_path(os.join_path(os.dir(@FILE), 'testdata',
'toml_with_utf32_bom' + '.toml'))) or { panic(err) }
fn test_toml_with_bom() {
toml_doc := toml.parse_text(toml_text_with_utf8_bom) or { panic(err) }
toml_json := to.json(toml_doc)
title := toml_doc.value('title')
assert title == toml.Any('TOML Example')
assert title as string == 'TOML Example'
owner := toml_doc.value('owner') as map[string]toml.Any
any_name := owner.value('name')
assert any_name.string() == 'Tom Preston-Werner'
database := toml_doc.value('database') as map[string]toml.Any
db_serv := database['server'] or {
panic('could not access "server" index in "database" variable')
}
assert db_serv as string == '192.168.1.1'
// Re-cycle bad_toml_doc
mut bad_toml_doc := empty_toml_document
bad_toml_doc = toml.parse_text(toml_text_with_utf16_bom) or {
println(' ${err.msg()}')
assert true
empty_toml_document
}
bad_toml_doc = toml.parse_text(toml_text_with_utf32_bom) or {
println(' ${err.msg()}')
assert true
empty_toml_document
}
}
+305
View File
@@ -0,0 +1,305 @@
// Instructions for developers:
// The actual tests and data can be obtained by doing:
// `git clone -n https://github.com/toml-lang/toml-test.git vlib/toml/tests/testdata/toml_lang`
// `git -C vlib/toml/tests/testdata/toml_lang reset --hard 229ce2e
// See also the CI toml tests
import os
import toml
import toml.ast
import x.json2
const test_root = os.join_path(@VEXEROOT, 'vlib/toml/tests/testdata/toml_lang/tests')
const test_files_file = os.join_path(test_root, 'files-toml-1.0.0')
const hide_oks = os.getenv('VTEST_HIDE_OK') == '1'
const no_jq = os.getenv('VNO_JQ') == '1'
// Kept for easier lookup and handling of future updates to the tests.
// NOTE: entries in this list, except `do_not_remove`, are valid TOML that the parser should work with, but currently does not.
const valid_exceptions = [
'do_not_remove',
]
// NOTE: entries in this list, except `do_not_remove`, are tests of invalid TOML that should have the parser fail, but currently does not.
const invalid_exceptions = [
'do_not_remove',
]
const valid_value_exceptions = [
'do_not_remove',
]
const jq_not_equal = [
'do_not_remove',
]
const jq = os.find_abs_path_of_executable('jq') or { '' }
const compare_work_dir_root = os.join_path(os.vtmp_dir(), 'toml_toml_lang')
// From: https://stackoverflow.com/a/38266731/1904615
const jq_normalize = r'# Apply f to composite entities recursively using keys[], and to atoms
def sorted_walk(f):
. as $in
| if type == "object" then
reduce keys[] as $key
( {}; . + { ($key): ($in[$key] | sorted_walk(f)) } ) | f
elif type == "array" then map( sorted_walk(f) ) | f
else f
end;
def normalize: sorted_walk(if type == "array" then sort else . end);
normalize'
fn run(args []string) !string {
res := os.execute(args.join(' '))
if res.exit_code != 0 {
return error('${args[0]} failed with return code ${res.exit_code}.\n${res.output}')
}
return res.output
}
// test_toml_lang_tomltest run though 'testdata/toml_lang/toml-test/*' if found.
fn test_toml_lang_tomltest() {
eprintln('> running ${@LOCATION}')
if !os.is_dir(test_root) {
println('No test data directory found in "${test_root}"')
assert true
return
}
test_files_list := os.read_lines(test_files_file) or {
panic('Could not read "${test_files_file}" with test files list. It should reside in "${test_root}": ${err.msg()}')
}
valid_folder := 'valid'
valid_test_files :=
test_files_list.filter(it.starts_with('valid/') && it.ends_with('.toml')).map(it.replace('\\', '/'))
invalid_folder := 'invalid'
invalid_test_files :=
test_files_list.filter(it.starts_with('invalid/') && it.ends_with('.toml')).map(it.replace('\\', '/'))
assert valid_test_files.len > 0, 'Expected a list of *valid* test files'
assert invalid_test_files.len > 0, 'Expected a list of *invalid* test files'
assert os.is_file(os.join_path(test_root, valid_test_files[0])), 'Expected at least one file from the valid list to be present'
assert os.is_file(os.join_path(test_root, invalid_test_files[0])), 'Expected at least one file from the invalid list to present'
println('\nTesting ${valid_test_files.len} valid TOML files...')
mut valid := 0
mut e := 0
for i, relative_valid_test_file in valid_test_files {
relative := relative_valid_test_file.all_after(valid_folder).trim_left('/')
valid_test_file := os.join_path(test_root, relative_valid_test_file)
if relative in valid_exceptions {
e++
idx := valid_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...')
continue
}
// eprintln('>>> trying to parse: ${valid_test_file} | relative: ${relative}')
toml_doc := toml.parse_file(valid_test_file) or {
eprintln('>>> error while parsing: ${valid_test_file}')
panic(err)
}
valid++
if !hide_oks {
println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
}
}
println('${valid}/${valid_test_files.len} TOML files were parsed correctly')
if valid_exceptions.len > 0 {
println('TODO Skipped parsing of ${valid_exceptions.len} valid TOML files...')
}
// If the command-line tool `jq` is installed, value tests can be run as well.
if jq != '' && !no_jq {
println('\nTesting value output of ${valid_test_files.len} valid TOML files using "${jq}"...')
if os.exists(compare_work_dir_root) {
os.rmdir_all(compare_work_dir_root)!
}
os.mkdir_all(compare_work_dir_root)!
jq_normalize_path := os.join_path(compare_work_dir_root, 'normalize.jq')
os.write_file(jq_normalize_path, jq_normalize)!
valid = 0
e = 0
for i, relative_valid_test_file in valid_test_files {
relative := relative_valid_test_file.all_after(valid_folder).trim_left('/')
valid_test_file := os.join_path(test_root, relative_valid_test_file)
// Skip the file if we know it can't be parsed or we know that the value retrieval needs work.
if relative in valid_exceptions {
e++
idx := valid_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...')
continue
}
if relative in valid_value_exceptions {
e++
idx := valid_value_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID VALUE EXCEPTION [${idx}/${valid_value_exceptions.len}]...')
continue
}
if !hide_oks {
println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
}
toml_doc := toml.parse_file(valid_test_file)!
// eprintln(' relative: ${relative} parsed')
v_toml_json_path := os.join_path(compare_work_dir_root,
os.file_name(valid_test_file).all_before_last('.') + '.v.json')
bs_toml_json_path := os.join_path(compare_work_dir_root,
os.file_name(valid_test_file).all_before_last('.') + '.json')
os.write_file(v_toml_json_path, to_toml_lang(toml_doc.ast.table))!
bs_json := os.read_file(valid_test_file.all_before_last('.') + '.json')!
os.write_file(bs_toml_json_path, bs_json)!
v_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', v_toml_json_path]) or {
contents := os.read_file(v_toml_json_path)!
panic(err.msg() + '\n${contents}')
}
bs_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', bs_toml_json_path]) or {
contents := os.read_file(v_toml_json_path)!
panic(err.msg() + '\n${contents}')
}
if relative in jq_not_equal {
e++
eprintln('>>> skipped: relative: ${relative} in jq_not_equal, bs_normalized_json != bs_normalized_json')
continue
}
if bs_normalized_json != v_normalized_json {
e++
eprintln('>>> error: relative: ${relative}, bs_normalized_json != bs_normalized_json')
continue
}
assert bs_normalized_json == v_normalized_json
valid++
}
} else {
println('> Skipping json conversion tests, since jq: ${jq} | no_jq: ${no_jq}')
}
println('${valid}/${valid_test_files.len} TOML files were parsed correctly and value checked')
if valid_value_exceptions.len > 0 {
println('TODO Skipped value checks of ${valid_value_exceptions.len} valid TOML files...')
}
println('\nTesting ${invalid_test_files.len} invalid TOML files...')
mut invalid := 0
e = 0
for i, relative_invalid_test_file in invalid_test_files {
relative := relative_invalid_test_file.all_after(invalid_folder).trim_left('/')
invalid_test_file := os.join_path(test_root, relative_invalid_test_file)
if relative in invalid_exceptions {
e++
idx := invalid_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}" INVALID EXCEPTION [${idx}/${invalid_exceptions.len}]...')
continue
}
if !hide_oks {
println('OK [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}"...')
}
if toml_doc := toml.parse_file(invalid_test_file) {
content_that_should_have_failed := os.read_file(invalid_test_file)!
println(' This TOML from relative: ${relative}, invalid_test_file: ${invalid_test_file}, should have failed:\n${'-'.repeat(100)}\n${content_that_should_have_failed}\n${'-'.repeat(100)}')
assert false
} else {
if !hide_oks {
println(' ${err.msg()}')
}
assert true
}
invalid++
}
println('${invalid}/${invalid_test_files.len} invalid TOML files correctly had parsing errors')
if invalid_exceptions.len > 0 {
println('TODO Skipped parsing of ${invalid_exceptions.len} invalid TOML files...')
}
}
// to_toml_lang returns a toml-lang compatible json string converted from the `value` ast.Value.
fn to_toml_lang(value ast.Value) string {
match value {
ast.Quoted {
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')
// Note: Since encoding strings in JSON now automatically includes quotes,
// I added a somewhat a workaround by adding an ending quote in order to
// recognize properly the date time type. - Ned
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('inf') || value.text.contains('nan') {
return '{ "type": "float", "value": "${value.text}" }'
}
if !value.text.starts_with('0x')
&& (value.text.contains('.') || value.text.to_lower().contains('e')) {
mut val := '${value.f64()}'.replace('.e+', '.0e') // JSON notation
if !val.contains('.') && val != '0' { // JSON notation
val += '.0'
}
// Since https://github.com/vlang/v/pull/16079 V's string conversion of a zero (0) will
// output "0.0" for float types - the JSON test suite data, however, expects "0" for floats
// The following is a correction for that inconsistency
if val == '0.0' {
val = '0'
}
return '{ "type": "float", "value": "${val}" }'
}
return '{ "type": "integer", "value": "${value.i64()}" }'
}
map[string]ast.Value {
mut str := '{ '
for key, val in value {
json_key := json2.Any(key).json_str()
str += ' ${json_key}: ${to_toml_lang(val)},'
}
str = str.trim_right(',')
str += ' }'
return str
}
[]ast.Value {
mut str := '[ '
for val in value {
str += ' ${to_toml_lang(val)},'
}
str = str.trim_right(',')
str += ' ]\n'
return str
}
}
return '<error>'
}
@@ -0,0 +1,63 @@
// This tests the `toml` module for a known memory corruption.
// The BUG shows below if no string `.clone()` nor any garbage-collection is done...
import os
import toml
const toml_text = os.read_file(os.real_path(os.join_path(os.dir(@FILE), 'testdata', 'toml_test')) +
'.toml') or { panic(err) }
fn test_toml_known_memory_corruption() {
toml_doc := toml.parse_text(toml_text) or { panic(err) }
owner := toml_doc.value('owner') as map[string]toml.Any
any_name := owner.value('name')
// This assert code path will cause the corruption.
assert any_name.string() == 'Tom Preston-Werner'
// This code then triggered the bug before the fix.
// Also see note in toml/any.v in function `pub fn (a Any) string() string`
assert toml_doc.value('owner.name') as string == 'Tom Preston-Werner'
// Repeat the pattern
assert any_name.string() == 'Tom Preston-Werner'
assert toml_doc.value('owner.name') as string == 'Tom Preston-Werner'
}
fn test_toml_known_memory_corruption_2() {
toml_txt := '
# Local Date-Time
ldt1 = 1979-05-27T07:32:00
ldt2 = 1979-05-27T00:32:00.999999
# Local Date
ld1 = 1979-05-27
# Local Time
lt1 = 07:32:00
lt2 = 00:32:00.999999
'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
// ldt1 test section
odt_time := toml.DateTime{'1979-05-27T07:32:00'}
ldt1 := toml_doc.value('ldt1')
assert ldt1.string() == '1979-05-27T07:32:00' // Running this before yielded a double free segfault
assert ldt1.datetime() == odt_time
assert ldt1.string() == '1979-05-27T07:32:00' // Used to yield a memory corruption
// ld1 test section
od_time := toml.Date{'1979-05-27'}
ld1 := toml_doc.value('ld1')
assert ld1.date() == od_time
assert ld1.string() == '1979-05-27' // Used to yield a memory corruption
// lt1 test section
mut ot_time := toml.Time{'07:32:00'}
lt1 := toml_doc.value('lt1')
assert lt1.time() == ot_time
assert lt1.string() == '07:32:00' // Used to yield a memory corruption
// lt2 test section
ot_time = toml.Time{'00:32:00.999999'}
lt2 := toml_doc.value('lt2')
assert lt2.time() == ot_time
assert lt2.string() == '00:32:00.999999' // Used to yield a memory corruption
}
+321
View File
@@ -0,0 +1,321 @@
import os
import toml
import toml.ast
import x.json2
const hide_oks = os.getenv('VTEST_HIDE_OK') == '1'
const no_jq = os.getenv('VNO_JQ') == '1'
// Instructions for developers:
// The actual tests and data can be obtained by doing:
// `git clone -n https://github.com/toml-rs/toml.git vlib/toml/tests/testdata/toml_rs`
// `git -C vlib/toml/tests/testdata/toml_rs reset --hard 9bd454c`
// See also the CI toml tests
// Kept for easier handling of future updates to the tests
const valid_exceptions = [
'valid/example-v0.3.0.toml',
'valid/example-v0.4.0.toml',
'valid/datetime-truncate.toml', // Not considered valid since RFC 3339 doesn't permit > 6 ms digits ??
]
const invalid_exceptions = []string{}
const valid_value_exceptions = [
// These have correct values, and should've passed, but the format of arrays is *mixed* in the JSON ??
'valid/datetime-truncate.toml',
'valid/example2.toml',
'valid/example-v0.4.0.toml',
'valid/example-v0.3.0.toml',
]
// These have correct values, and should've passed as-is, but the format of arrays changes in the JSON ??
// We account for that here
const use_type_2_arrays = [
'valid/table-array-implicit.toml',
'valid/table-array-many.toml',
'valid/table-array-one.toml',
'valid/table-array-nest.toml',
'valid/table-array-nest-no-keys.toml',
]
const tests_folder = os.join_path('crates', 'test-suite', 'tests')
const jq = os.find_abs_path_of_executable('jq') or { '' }
const compare_work_dir_root = os.join_path(os.vtmp_dir(), 'toml_toml_rs')
// From: https://stackoverflow.com/a/38266731/1904615
const jq_normalize = r'# Apply f to composite entities recursively using keys[], and to atoms
def sorted_walk(f):
. as $in
| if type == "object" then
reduce keys[] as $key
( {}; . + { ($key): ($in[$key] | sorted_walk(f)) } ) | f
elif type == "array" then map( sorted_walk(f) ) | f
else f
end;
def normalize: sorted_walk(if type == "array" then sort else . end);
normalize'
fn run(args []string) !string {
res := os.execute(args.join(' '))
if res.exit_code != 0 {
return error('${args[0]} failed with return code ${res.exit_code}.\n${res.output}')
}
return res.output
}
// test_toml_rs_toml_rs run though 'testdata/toml_rs/crates/test-suite/tests/*' if found.
fn test_toml_rs_toml_rs() {
eprintln('> running ${@LOCATION}')
test_root := '${@VEXEROOT}/vlib/toml/tests/testdata/toml_rs/crates/test-suite/tests'
if os.is_dir(test_root) {
valid_test_files := os.walk_ext(os.join_path(test_root, 'valid'), '.toml')
invalid_test_files := os.walk_ext(os.join_path(test_root, 'invalid'), '.toml')
println('Testing ${valid_test_files.len} valid TOML files...')
mut valid := 0
mut e := 0
for i, valid_test_file in valid_test_files {
mut relative := valid_test_file.all_after(tests_folder).trim_left(os.path_separator)
$if windows {
relative = relative.replace('/', '\\')
}
if relative in valid_exceptions {
e++
idx := valid_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...')
continue
}
if !hide_oks {
println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
}
toml_doc := toml.parse_file(valid_test_file)!
valid++
}
println('${valid}/${valid_test_files.len} TOML files were parsed correctly')
if valid_exceptions.len > 0 {
println('TODO Skipped parsing of ${e} valid TOML files...')
}
// If the command-line tool `jq` is installed, value tests can be run as well.
if jq != '' && !no_jq {
println('Testing value output of ${valid_test_files.len} valid TOML files using "${jq}"...')
if os.exists(compare_work_dir_root) {
os.rmdir_all(compare_work_dir_root)!
}
os.mkdir_all(compare_work_dir_root)!
jq_normalize_path := os.join_path(compare_work_dir_root, 'normalize.jq')
os.write_file(jq_normalize_path, jq_normalize)!
valid = 0
e = 0
for i, valid_test_file in valid_test_files {
mut relative := valid_test_file.all_after(tests_folder).trim_left(os.path_separator)
$if windows {
relative = relative.replace('/', '\\')
}
if !os.exists(valid_test_file.all_before_last('.') + '.json') {
println('N/A [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
continue
}
// Skip the file if we know it can't be parsed or we know that the value retrieval needs work.
if relative in valid_exceptions {
e++
idx := valid_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID EXCEPTION [${idx}/${valid_exceptions.len}]...')
continue
}
if relative in valid_value_exceptions {
e++
idx := valid_value_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${valid_test_files.len}] "${valid_test_file}" VALID VALUE EXCEPTION [${idx}/${valid_value_exceptions.len}]...')
continue
}
if !hide_oks {
println('OK [${i + 1}/${valid_test_files.len}] "${valid_test_file}"...')
}
toml_doc := toml.parse_file(valid_test_file)!
v_toml_json_path := os.join_path(compare_work_dir_root,
os.file_name(valid_test_file).all_before_last('.') + '.v.json')
toml_rs_toml_json_path := os.join_path(compare_work_dir_root,
os.file_name(valid_test_file).all_before_last('.') + '.json')
mut array_type := 1
if relative in use_type_2_arrays {
array_type = 2
}
os.write_file(v_toml_json_path, to_toml_rs(toml_doc.ast.table, array_type))!
toml_rs_json := os.read_file(valid_test_file.all_before_last('.') + '.json')!
os.write_file(toml_rs_toml_json_path, toml_rs_json)!
v_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"', v_toml_json_path]) or {
contents := os.read_file(v_toml_json_path)!
panic(err.msg() + '\n${contents}')
}
toml_rs_normalized_json := run([jq, '-S', '-f "${jq_normalize_path}"',
toml_rs_toml_json_path]) or {
contents := os.read_file(v_toml_json_path)!
panic(err.msg() + '\n${contents}')
}
assert toml_rs_normalized_json == v_normalized_json
valid++
}
println('${valid}/${valid_test_files.len} TOML files were parsed correctly and value checked')
if valid_value_exceptions.len > 0 {
println('TODO Skipped value checks of ${e} valid TOML files...')
}
}
println('Testing ${invalid_test_files.len} invalid TOML files...')
mut invalid := 0
e = 0
for i, invalid_test_file in invalid_test_files {
mut relative := invalid_test_file.all_after(tests_folder).trim_left(os.path_separator)
$if windows {
relative = relative.replace('/', '\\')
}
if relative in invalid_exceptions {
e++
idx := invalid_exceptions.index(relative) + 1
println('SKIP [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}" INVALID EXCEPTION [${idx}/${invalid_exceptions.len}]...')
continue
}
if !hide_oks {
println('OK [${i + 1}/${invalid_test_files.len}] "${invalid_test_file}"...')
}
if toml_doc := toml.parse_file(invalid_test_file) {
content_that_should_have_failed := os.read_file(invalid_test_file)!
println(' This TOML should have failed:\n${'-'.repeat(40)}\n${content_that_should_have_failed}\n${'-'.repeat(40)}')
assert false
} else {
if !hide_oks {
println(' ${err.msg()}')
}
assert true
}
invalid++
}
println('${invalid}/${invalid_test_files.len} TOML files were parsed correctly')
if invalid_exceptions.len > 0 {
println('TODO Skipped parsing of ${invalid_exceptions.len} invalid TOML files...')
}
} else {
println('No test data directory found in "${test_root}"')
assert true
}
}
// to_toml_rs_time
fn to_toml_rs_time(time_str string) string {
if time_str.contains('.') {
date_and_time := time_str.all_before('.')
mut ms := time_str.all_after('.')
z := if ms.contains('Z') { 'Z' } else { '' }
ms = ms.replace('Z', '')
if ms.len > 3 {
ms = ms[..3]
}
return date_and_time + '.' + ms + z
} else {
return time_str
}
}
// to_toml_rs returns an toml_rs compatible json string converted from the `value` ast.Value.
fn to_toml_rs(value ast.Value, array_type int) string {
match value {
ast.Quoted {
json_text := json2.Any(value.text).json_str()
return '{ "type": "string", "value": ${json_text} }'
}
ast.DateTime {
// Normalization for json
mut 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'
}
// NOTE test suite inconsistency.
// It seems it's implementation specific how time and
// date-time values are represented in detail. For now we follow the toml-lang format
// that expands to 6 digits which is also a valid RFC 3339 representation.
json_text = to_toml_rs_time(json_text[1..json_text.len - 1])
return '{ "type": "${typ}", "value": "${json_text}" }'
}
ast.Date {
json_text := json2.Any(value.text).json_str()
return '{ "type": "date", "value": ${json_text} }'
}
ast.Time {
mut json_text := json2.Any(value.text).json_str()
json_text = to_toml_rs_time(json_text[1..json_text.len - 1])
return '{ "type": "time", "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 {
text := value.text
if text.contains('inf') || text.contains('nan') {
return '{ "type": "float", "value": ${value.text} }'
}
if !text.starts_with('0x') && (text.contains('.') || text.to_lower().contains('e')) {
mut val := ''
if text.to_lower().contains('e') && !text.contains('-') {
val = '${value.f64():.1f}'
} else {
val = '${value.f64()}'
}
return '{ "type": "float", "value": "${val}" }'
}
return '{ "type": "integer", "value": "${value.i64()}" }'
}
map[string]ast.Value {
mut str := '{ '
for key, val in value {
json_key := json2.Any(key).json_str()
str += ' ${json_key}: ${to_toml_rs(val, array_type)},'
}
str = str.trim_right(',')
str += ' }'
return str
}
[]ast.Value {
mut str := ''
if array_type == 1 {
str = '{ "type": "array", "value": [ '
} else {
str = '[ '
}
for val in value {
str += ' ${to_toml_rs(val, array_type)},'
}
str = str.trim_right(',')
if array_type == 1 {
str += ' ] }\n'
} else {
str += ' ]\n'
}
return str
}
}
return '<error>'
}
+106
View File
@@ -0,0 +1,106 @@
import os
import toml
import toml.to
const toml_text = os.read_file(
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.toml') or { panic(err) }
fn test_toml() {
// File containing the complete text from the example in the official TOML project README.md:
// https://github.com/toml-lang/toml/blob/3b11f6921da7b6f5db37af039aa021fee450c091/README.md#Example
toml_doc := toml.parse_text(toml_text) or { panic(err) }
toml_json := to.json(toml_doc)
// NOTE Kept for easier debugging:
// dump(toml_doc.ast)
// println(toml_json)
// assert false
assert toml_json == os.read_file(
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.out') or { panic(err) }
title := toml_doc.value('title')
assert title == toml.Any('TOML Example')
assert title as string == 'TOML Example'
database := toml_doc.value('database') as map[string]toml.Any
db_serv := database['server'] or {
panic('could not access "server" index in "database" variable')
}
assert db_serv as string == '192.168.1.1'
assert toml_doc.value('owner.name') as string == 'Tom Preston-Werner'
assert toml_doc.value('database.server') as string == '192.168.1.1'
database_ports := toml_doc.value('database.ports') as []toml.Any
assert database_ports[0] as i64 == 8000
assert database_ports[1] as i64 == 8001
assert database_ports[2] as i64 == 8002
assert database_ports[0].int() == 8000
assert database_ports[1].int() == 8001
assert database_ports[2].int() == 8002
assert toml_doc.value('database.connection_max') as i64 == 5000
assert toml_doc.value('database.enabled') as bool == true
assert toml_doc.value('servers.alpha.ip').string() == '10.0.0.1'
assert toml_doc.value('servers.alpha.dc').string() == 'eqdc10'
assert toml_doc.value('servers.beta.ip').string() == '10.0.0.2'
assert toml_doc.value('servers.beta.dc').string() == 'eqdc10'
clients_data := (toml_doc.value('clients.data') as []toml.Any)
// dump(clients_data)
// assert false
gamma_delta_array := clients_data[0] as []toml.Any
digits_array := clients_data[1] as []toml.Any
assert gamma_delta_array[0].string() == 'gamma'
assert gamma_delta_array[1].string() == 'delta'
assert digits_array[0].int() == 1
assert digits_array[1].int() == 2
clients_hosts := (toml_doc.value('clients.hosts') as []toml.Any).as_strings()
assert clients_hosts[0] == 'alpha'
assert clients_hosts[1] == 'omega'
}
fn test_toml_file() {
out_path := os.join_path(os.vtmp_dir(), 'toml_tests')
test_file := os.join_path(out_path, 'toml_example.toml')
os.mkdir_all(out_path) or { assert false }
defer {
os.rmdir_all(out_path) or {}
}
os.write_file(test_file, toml_text) or { assert false }
toml_doc := toml.parse_file(test_file) or { panic(err) }
toml_json := to.json(toml_doc)
// NOTE Kept for easier debugging:
// dump(toml_doc.ast)
// println(toml_json)
// assert false
assert toml_json == os.read_file(
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.out') or { panic(err) }
}
fn test_toml_parse_text() {
toml_doc := toml.parse_text(toml_text) or { panic(err) }
toml_json := to.json(toml_doc)
assert toml_json == os.read_file(
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.out') or { panic(err) }
}
fn test_toml_parse() {
toml_doc := toml.parse_text(toml_text) or { panic(err) }
toml_json := to.json(toml_doc)
assert toml_json == os.read_file(
os.real_path(os.join_path(os.dir(@FILE), 'testdata', os.file_name(@FILE).all_before_last('.'))) +
'.out') or { panic(err) }
}
+123
View File
@@ -0,0 +1,123 @@
import toml
import strconv
fn test_string() {
str_value := 'test string'
toml_txt := 'string = "test string"'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value := toml_doc.value('string')
assert value == toml.Any(str_value)
assert value as string == str_value
assert value.string() == str_value
}
fn test_i64() {
toml_txt := 'i64 = 120'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value := toml_doc.value('i64')
assert value == toml.Any(i64(120))
assert value as i64 == 120
assert value.i64() == i64(120)
}
fn test_bool() {
toml_txt := '
bool_true = true
bool_false = false'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value_true := toml_doc.value('bool_true')
assert value_true == toml.Any(true)
assert value_true as bool == true
assert value_true != toml.Any(false)
assert value_true as bool != false
assert value_true.bool() == true
value_false := toml_doc.value('bool_false')
assert value_false == toml.Any(false)
assert value_false as bool == false
assert value_false != toml.Any(true)
assert value_false as bool != true
assert value_false.bool() == false
}
fn test_bool_key_is_not_value() {
toml_txt := 'true = true
false = false'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value_true := toml_doc.value('true')
assert value_true == toml.Any(true)
assert value_true as bool == true
assert value_true != toml.Any(false)
assert value_true as bool != false
value_false := toml_doc.value('false')
assert value_false == toml.Any(false)
assert value_false as bool == false
assert value_false != toml.Any(true)
assert value_false as bool != true
}
fn test_single_letter_key() {
toml_txt := '[v]
open_sourced = "Jun 22 2019 20:20:28"'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value := toml_doc.value('v.open_sourced').string()
assert value == 'Jun 22 2019 20:20:28'
}
fn test_hex_values() {
// Regression test
// '0xb' is carefully chosen to include the 'b' character that also denotes binary via 0b prefix.
toml_txt := 'hex = 0xb'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value := toml_doc.value('hex')
assert value as i64 == 11
assert value.i64() == 11
}
fn test_comment_as_last_value() {
toml_txt := '
test = 42
# this line has comment as last thing'
toml_doc := toml.parse_text(toml_txt) or { panic(err) }
value := toml_doc.value('test')
assert value as i64 == 42
assert value.i64() == 42
}
fn test_nan_and_inf_values() {
mut toml_doc := toml.parse_text('nan = nan') or { panic(err) }
mut value := toml_doc.value('nan')
assert value.string() == 'nan'
toml_doc = toml.parse_text('nan = nan#comment') or { panic(err) }
value = toml_doc.value('nan')
assert value.string() == 'nan'
toml_doc = toml.parse_text('nan = -nan') or { panic(err) }
value = toml_doc.value('nan')
assert value.string() == 'nan'
toml_doc = toml.parse_text('nan = +nan') or { panic(err) }
value = toml_doc.value('nan')
assert value.string() == 'nan'
toml_doc = toml.parse_text('inf = inf') or { panic(err) }
value = toml_doc.value('inf')
assert value.u64() == strconv.double_plus_infinity
toml_doc = toml.parse_text('inf = +inf') or { panic(err) }
value = toml_doc.value('inf')
assert value.u64() == strconv.double_plus_infinity
toml_doc = toml.parse_text('inf = -inf') or { panic(err) }
value = toml_doc.value('inf')
assert value.u64() == strconv.double_minus_infinity
}
+131
View File
@@ -0,0 +1,131 @@
import toml
import strconv
const toml_text = '
modules = [ "ui", "toml" ]
errors = []
[[themes]]
name = "Dracula"
colors = [ "red", "black", "white" ]
[[themes]]
name = "Lemon"
colors = [
"green",
"yellow",
[ "transparent" ]
]
[[tests]]
id = 1
[[tests]]
id = 2
[values]
nan = nan
inf = inf
test = 2
minus-inf = -inf
[[themes]]
name = "Ice"
colors = [
"blue",
"white"
]
'
const toml_text_2 = "
[defaults]
run.flags = ['-f 1']
[[defaults.env]]
'RUN_PATH' = '\$OUT_PATH'
'RUN_TIME' = 5
'TEST_PATH' = '/tmp/test'
"
const toml_text_3 = '
[[foo.bar]]
baz = 1
bzz = 1
[[foo.bar]]
baz = 2
bzz = 2
'
fn test_value_query_in_array() {
toml_doc := toml.parse_text(toml_text) or { panic(err) }
mut value := toml_doc.value('themes[0].colors[1]').string()
assert value == 'black'
value = toml_doc.value('themes[1].colors[0]').string()
assert value == 'green'
value = toml_doc.value('themes[1].colors[2].[0]').string()
assert value == 'transparent'
value = toml_doc.value('modules[1]').string()
assert value == 'toml'
value = toml_doc.value('errors[11]').default_to('<none>').string()
assert value == '<none>'
value = toml_doc.value('themes[2].colors[0]').string()
assert value == 'blue'
}
fn test_any_value_query() {
toml_doc := toml.parse_text(toml_text) or { panic(err) }
themes := toml_doc.value('themes')
assert themes.value('[0].colors[0]').string() == 'red'
themes_arr := toml_doc.value('themes') as []toml.Any
assert themes_arr[0].value('colors[0]').string() == 'red'
mut any := themes
assert any.value('[1].name').string() == 'Lemon'
any = any.value('[1]')
assert any.value('name').string() == 'Lemon'
any = toml_doc.value('themes').value('[1].colors').value('[1]')
assert any.string() == 'yellow'
any = toml_doc.value('themes[1]').value('colors[1]')
assert any.string() == 'yellow'
any = toml_doc.value('themes[1].colors[0]')
assert any.string() == 'green'
any = toml_doc.value('values')
any = any.value('test')
assert any.int() == 2
}
fn test_inf_and_nan_query() {
toml_doc := toml.parse_text(toml_text) or { panic(err) }
value := toml_doc.value('values.nan').string()
assert value == 'nan'
mut value_u64 := toml_doc.value('values.inf').u64()
assert value_u64 == strconv.double_plus_infinity
value_u64 = toml_doc.value('values.minus-inf').u64()
assert value_u64 == strconv.double_minus_infinity
}
fn test_any_value_query_2() {
toml_doc := toml.parse_text(toml_text_2) or { panic(err) }
defaults := toml_doc.value('defaults')
assert defaults.value('run.flags[0]').string() == '-f 1'
assert defaults.value('env[0].RUN_TIME').int() == 5
}
fn test_any_value_query_for_nested_tables_array() {
toml_doc := toml.parse_text(toml_text_3) or { panic(err) }
items := toml_doc.value('foo.bar').array()
assert items.len == 2
assert items[0].value('baz').int() == 1
assert items[0].value('bzz').int() == 1
assert items[1].value('baz').int() == 2
assert items[1].value('bzz').int() == 2
}
+134
View File
@@ -0,0 +1,134 @@
// 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 to
import toml
import x.json2
type DocOrAny = toml.Any | toml.Doc
// json returns `doa` as a JSON encoded string.
pub fn json(doa DocOrAny) string {
match doa {
toml.Doc {
return doa.ast.table.str()
}
toml.Any {
return any_to_json(doa)
}
}
}
// json returns `a` as a JSON encoded string.
fn any_to_json(a toml.Any) string {
match a {
toml.Null {
return 'null'
}
toml.DateTime {
return json2.Any(toml.Any(a).string()).json_str()
}
toml.Date {
return json2.Any(toml.Any(a).string()).json_str()
}
toml.Time {
return json2.Any(toml.Any(a).string()).json_str()
}
string {
return json2.Any(toml.Any(a).string()).json_str()
}
bool {
return json2.Any(toml.Any(a).bool()).json_str()
}
f32 {
return json2.Any(toml.Any(a).f32()).json_str()
}
f64 {
return json2.Any(toml.Any(a).f64()).json_str()
}
i64 {
return json2.Any(toml.Any(a).i64()).json_str()
}
int {
return json2.Any(toml.Any(a).int()).json_str()
}
u64 {
return json2.Any(toml.Any(a).u64()).json_str()
}
map[string]toml.Any {
mut str := '{'
for key, val in a {
json_key := json2.Any(key)
str += ' ${json_key.json_str()}: ${any_to_json(val)},'
}
str = str.trim_right(',')
str += ' }'
return str
}
[]toml.Any {
mut str := '['
for val in a {
str += ' ${any_to_json(val)},'
}
str = str.trim_right(',')
str += ' ]'
return str
}
}
}
// json_any returns `Any` as a `x.json2.Any` type.
pub fn json_any(a toml.Any) json2.Any {
match a {
toml.Null {
return json2.Null{}
}
toml.DateTime {
return json2.Any(a.str())
}
toml.Date {
return json2.Any(a.str())
}
toml.Time {
return json2.Any(a.str())
}
string {
return json2.Any(a.str())
}
bool {
return json2.Any(bool(a))
}
int {
return json2.Any(int(a))
}
f32 {
return json2.Any(f32(a))
}
f64 {
return json2.Any(f64(a))
}
i64 {
return json2.Any(i64(a))
}
u64 {
return json2.Any(u64(a))
}
map[string]toml.Any {
mut jmap := map[string]json2.Any{}
for key, val in a {
jmap[key] = json_any(val)
}
return jmap
}
[]toml.Any {
mut jarr := []json2.Any{}
for val in a {
jarr << json_any(val)
}
return jarr
}
}
}
+13
View File
@@ -0,0 +1,13 @@
// 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 token
// Position represents a position in a TOML document.
pub struct Pos {
pub:
len int // length of the literal in the source
line_nr int // the line number in the source where the token occurred
pos int // the position of the token in scanner text
col int // the column in the source where the token occurred
}
+53
View File
@@ -0,0 +1,53 @@
// 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 token
// Token holds information about the current scan of bytes.
pub struct Token {
pub:
kind Kind // the token number/enum; for quick comparisons
lit string // literal representation of the token
col int // the column in the source where the token occurred
line_nr int // the line number in the source where the token occurred
pos int // the position of the token in scanner text
len int // length of the literal
}
// Kind represents a logical type of entity found in any given TOML document.
pub enum Kind {
unknown
eof
bare // user
boolean // true or false
number // 123
quoted // 'foo', "foo", """foo""" or '''foo'''
plus // +
minus // -
underscore // _
comma // ,
colon // :
hash // # comment
assign // =
lcbr // {
rcbr // }
lsbr // [
rsbr // ]
nl // \n linefeed / newline character
cr // \r carriage return
tab // \t character
whitespace // ` `
period // .
_end_
}
// pos returns the exact position of a token in the input.
@[inline]
pub fn (tok &Token) pos() Pos {
return Pos{
len: tok.len
line_nr: tok.line_nr - 1
pos: tok.pos
col: tok.col - 1
}
}
+594
View File
@@ -0,0 +1,594 @@
// 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.input
import toml.scanner
import toml.parser
// Null is used in sumtype checks as a "default" value when nothing else is possible.
pub struct Null {
}
// decode decodes a TOML `string` into the target type `T`.
// If `T` has a custom `.from_toml()` method, it will be used instead of the default.
pub fn decode[T](toml_txt string) !T {
doc := parse_text(toml_txt)!
mut typ := T{}
$for method in T.methods {
$if method.name == 'from_toml' {
typ.$method(doc.to_any())
return typ
}
}
$if T !is $struct {
return error('toml.decode: expected struct, found ${T.name}')
}
decode_struct[T](doc.to_any(), mut typ)
return typ
}
fn decode_struct[T](doc Any, mut typ T) {
$for field in T.fields {
mut field_name := field.name
mut skip := false
for attr in field.attrs {
if attr == 'skip' {
skip = true
break
}
if attr.starts_with('toml:') {
field_name = attr.all_after(':').trim_space()
}
}
value := doc.value(field_name)
// only set the field's value when value != null and !skip, else field got it's default value
if !skip && value != null {
$if field.is_enum {
typ.$(field.name) = value.int()
} $else $if field.typ is string {
typ.$(field.name) = value.string()
} $else $if field.typ is bool {
typ.$(field.name) = value.bool()
} $else $if field.typ is int {
typ.$(field.name) = value.int()
} $else $if field.typ is i64 {
typ.$(field.name) = value.i64()
} $else $if field.typ is u64 {
typ.$(field.name) = value.u64()
} $else $if field.typ is f32 {
typ.$(field.name) = value.f32()
} $else $if field.typ is f64 {
typ.$(field.name) = value.f64()
} $else $if field.typ is DateTime {
typ.$(field.name) = value.datetime()
} $else $if field.typ is Date {
typ.$(field.name) = value.date()
} $else $if field.typ is Time {
typ.$(field.name) = value.time()
} $else $if field.typ is Any {
typ.$(field.name) = value
} $else $if field.is_array {
typ.$(field.name) = decode_array(typ.$(field.name), value.array())
} $else $if field.is_map {
typ.$(field.name) = decode_map(typ.$(field.name), value.as_map())
} $else $if field.is_struct {
mut s := typ.$(field.name)
decode_struct(value, mut s)
typ.$(field.name) = s
}
}
}
}
fn decode_array[T](current []T, values []Any) []T {
$if T is string {
return values.map(it.string())
} $else $if T is bool {
return values.map(it.bool())
} $else $if T is int {
return values.map(it.int())
} $else $if T is i64 {
return values.map(it.i64())
} $else $if T is u64 {
return values.map(it.u64())
} $else $if T is f32 {
return values.map(it.f32())
} $else $if T is f64 {
return values.map(it.f64())
} $else $if T is DateTime {
return values.map(it.datetime())
} $else $if T is Date {
return values.map(it.date())
} $else $if T is Time {
return values.map(it.time())
} $else $if T is Any {
return values
} $else $if T is $struct {
mut decoded := []T{cap: values.len}
for value in values {
if value is map[string]Any {
mut item := T{}
decode_struct(value, mut item)
decoded << item
}
}
return decoded
} $else {
return current
}
}
fn decode_map[T](current map[string]T, values map[string]Any) map[string]T {
$if T is string {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.string()
}
return decoded
} $else $if T is bool {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.bool()
}
return decoded
} $else $if T is int {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.int()
}
return decoded
} $else $if T is i64 {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.i64()
}
return decoded
} $else $if T is u64 {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.u64()
}
return decoded
} $else $if T is f32 {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.f32()
}
return decoded
} $else $if T is f64 {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.f64()
}
return decoded
} $else $if T is DateTime {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.datetime()
}
return decoded
} $else $if T is Date {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.date()
}
return decoded
} $else $if T is Time {
mut decoded := map[string]T{}
for key, value in values {
decoded[key] = value.time()
}
return decoded
} $else $if T is Any {
return values.clone()
} $else $if T is $struct {
mut decoded := map[string]T{}
for key, value in values {
if value is map[string]Any {
mut item := T{}
decode_struct(value, mut item)
decoded[key] = item
}
}
return decoded
} $else {
return current
}
}
// encode encodes the type `T` into a TOML string.
// If `T` has a custom `.to_toml()` method, it will be used instead of the default.
pub fn encode[T](typ T) string {
$if T is $struct {
$for method in T.methods {
$if method.name == 'to_toml' {
return typ.$method()
}
}
mp := encode_struct[T](typ)
return mp.to_toml()
} $else {
$compile_error('Currently only type `struct` is supported for `T` to encode as TOML')
}
return ''
}
fn encode_struct[T](typ T) map[string]Any {
mut mp := map[string]Any{}
$for field in T.fields {
mut skip := false
mut field_name := field.name
for attr in field.attrs {
if attr == 'skip' {
skip = true
break
}
if attr.starts_with('toml:') {
field_name = attr.all_after(':').trim_space()
}
}
if !skip {
mp[field_name] = to_any(typ.$(field.name))
}
}
return mp
}
fn voidptr_to_toml_string[T](value T) string {
ptr := unsafe { voidptr(&value) }
return unsafe { '0x${ptr_str(*(&voidptr(ptr)))}' }
}
fn to_any[T](value T) Any {
$if T is $enum {
return Any(int(value))
} $else $if T is Date {
return Any(value)
} $else $if T is Time {
return Any(value)
} $else $if T is Null {
return Any(value)
} $else $if T is bool {
return Any(value)
} $else $if T is f32 {
return Any(value)
} $else $if T is f64 {
return Any(value)
} $else $if T is i64 {
return Any(value)
} $else $if T is int {
return Any(value)
} $else $if T is u64 {
return Any(value)
} $else $if T is DateTime {
return Any(value)
} $else $if T is Any {
return value
} $else $if T is $struct {
$for method in T.methods {
$if method.name == 'to_toml' {
return Any(value.$method())
}
}
return encode_struct(value)
} $else $if T is $array {
mut arr := []Any{cap: value.len}
for v in value {
arr << to_any(v)
}
return arr
} $else $if T is $map {
mut mmap := map[string]Any{}
for key, val in value {
mmap['${key}'] = to_any(val)
}
return mmap
} $else {
if typeof(value).name == 'voidptr' {
return Any(voidptr_to_toml_string(value))
}
return Any('${value}')
}
}
// DateTime is the representation of an RFC 3339 datetime string.
pub struct DateTime {
pub:
datetime string
}
// str returns the RFC 3339 string representation of the datetime.
pub fn (dt DateTime) str() string {
return dt.datetime
}
// Date is the representation of an RFC 3339 date-only string.
pub struct Date {
pub:
date string
}
// str returns the RFC 3339 date-only string representation.
pub fn (d Date) str() string {
return d.date
}
// Time is the representation of an RFC 3339 time-only string.
pub struct Time {
pub:
time string
}
// str returns the RFC 3339 time-only string representation.
pub fn (t Time) str() string {
return t.time
}
// 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 = unsafe { nil }
}
// 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_dotted_key converts `key` string to an array of strings.
// parse_dotted_key preserves strings delimited by both `"` and `'`.
pub fn parse_dotted_key(key string) ![]string {
mut out := []string{}
mut buf := ''
mut in_string := false
mut delim := u8(` `)
for ch in key {
if ch in [`"`, `'`] {
if !in_string {
delim = ch
}
in_string = !in_string && ch == delim
if !in_string {
if buf != '' && buf != ' ' {
out << buf
}
buf = ''
delim = ` `
}
continue
}
buf += ch.ascii_str()
if !in_string && ch == `.` {
if buf != '' && buf != ' ' {
buf = buf[..buf.len - 1]
if buf != '' && buf != ' ' {
out << buf
}
}
buf = ''
continue
}
}
if buf != '' && buf != ' ' {
out << buf
}
if in_string {
return error(@FN +
': could not parse key, missing closing string delimiter `${delim.ascii_str()}`')
}
return out
}
// parse_array_key converts `key` string to a key and index part.
fn parse_array_key(key string) (string, int) {
mut index := -1
mut k := key
if k.contains('[') {
index = k.all_after('[').all_before(']').int()
if k.starts_with('[') {
k = '' // k.all_after(']')
} else {
k = k.all_before('[')
}
}
return k, index
}
// decode decodes a TOML `string` into the target struct type `T`.
pub fn (d Doc) decode[T]() !T {
$if T !is $struct {
return error('Doc.decode: expected struct, found ${T.name}')
}
mut typ := T{}
decode_struct(d.to_any(), mut typ)
return typ
}
// to_any converts the `Doc` to toml.Any type.
pub fn (d Doc) to_any() Any {
return ast_to_any(d.ast.table)
}
// reflect returns `T` with `T.<field>`'s value set to the
// value of any 1st level TOML key by the same name.
pub fn (d Doc) reflect[T]() T {
return d.to_any().reflect[T]()
}
// value queries a value from the TOML document.
// `key` supports a small query syntax scheme:
// Maps can be queried in "dotted" form e.g. `a.b.c`.
// quoted keys are supported as `a."b.c"` or `a.'b.c'`.
// Arrays can be queried with `a[0].b[1].[2]`.
pub fn (d Doc) value(key string) Any {
key_split := parse_dotted_key(key) or { return null }
return d.value_(d.ast.table, key_split)
}
pub const null = Any(Null{})
// value_opt queries a value from the TOML document. Returns an error if the
// key is not valid or there is no value for the key.
pub fn (d Doc) value_opt(key string) !Any {
key_split := parse_dotted_key(key) or { return error('invalid dotted key') }
x := d.value_(d.ast.table, key_split)
if x is Null {
return error('no value for key')
}
return x
}
// value_ returns the value found at `key` in the map `values` as `Any` type.
fn (d Doc) value_(value ast.Value, key []string) Any {
if key.len == 0 {
return null
}
mut ast_value := ast.Value(ast.Null{})
k, index := parse_array_key(key[0])
if k == '' {
a := value as []ast.Value
ast_value = a[index] or { return null }
}
if value is map[string]ast.Value {
ast_value = value[k] or { return null }
if index > -1 {
a := ast_value as []ast.Value
ast_value = a[index] or { return null }
}
}
if key.len <= 1 {
return ast_to_any(ast_value)
}
match ast_value {
map[string]ast.Value, []ast.Value {
return d.value_(ast_value, key[1..])
}
else {
return ast_to_any(value)
}
}
}
// ast_to_any converts `from` ast.Value to toml.Any value.
pub fn ast_to_any(value ast.Value) Any {
return ast_to_any_(value)
}
fn ast_to_any_(value ast.Value) Any {
match value {
ast.Date {
return Any(Date{value.text.clone()})
}
ast.Time {
return Any(Time{value.text.clone()})
}
ast.DateTime {
return Any(DateTime{value.text.clone()})
}
ast.Quoted {
return Any(value.text.clone())
}
ast.Number {
val_text := value.text
if val_text == 'inf' || val_text == '+inf' || val_text == '-inf' {
// NOTE values taken from strconv
if !val_text.starts_with('-') {
// strconv.double_plus_infinity
return Any(u64(0x7FF0000000000000))
} else {
// strconv.double_minus_infinity
return Any(u64(0xFFF0000000000000))
}
}
if val_text == 'nan' || val_text == '+nan' || val_text == '-nan' {
return Any('nan')
}
if !val_text.starts_with('0x')
&& (val_text.contains('.') || val_text.to_lower().contains('e')) {
return Any(value.f64())
}
return Any(value.i64())
}
ast.Bool {
str := value.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 {
converted := ast_to_any_(v)
am[k] = converted
}
return am
// return d.get_map_value(m, key_split[1..].join('.'))
}
[]ast.Value {
a := (value as []ast.Value)
mut aa := []Any{cap: a.len}
for val in a {
converted := ast_to_any_(val)
aa << converted
}
return aa
}
else {
return null
}
}
return null
// TODO: decide this
// panic(@MOD + '.' + @STRUCT + '.' + @FN + ' can\'t convert "${value}"')
// return Any('')
}
+30
View File
@@ -0,0 +1,30 @@
// 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 util
// is_key_char returns true if the given u8 is a valid key character.
@[inline]
pub fn is_key_char(c u8) bool {
return c.is_letter() // || c == `_` || c == `-` <- these are identified when tokenizing
}
// is_ascii_control_character returns true if `byte_char` is an ASCII control character.
@[inline]
pub fn is_ascii_control_character(byte_char u8) bool {
return (byte_char >= 0 && byte_char <= 0x1f) || byte_char == 0x7f
}
// is_illegal_ascii_control_character returns true if a `byte_char` ASCII control character
// is considered "illegal" in TOML .
@[inline]
pub fn is_illegal_ascii_control_character(byte_char u8) bool {
return byte_char != 0x09 && is_ascii_control_character(byte_char)
}
// printdbg is a utility function for displaying a key:pair error message
// when `-d trace_toml` is passed to the compiler.
@[if trace_toml ?]
pub fn printdbg(id string, message string) {
eprintln(id + ' ' + message)
}