411 lines
7.8 KiB
V
411 lines
7.8 KiB
V
module yaml
|
|
|
|
import json
|
|
import os
|
|
import strings
|
|
import x.json2
|
|
|
|
// Null is a simple representation of the YAML `null` value.
|
|
pub struct Null {}
|
|
|
|
// null is an instance of `Null`, to ease comparisons with it.
|
|
pub const null = Any(Null{})
|
|
|
|
// Any is the tree representation used by the YAML module.
|
|
pub type Any = []Any | Null | bool | f64 | i64 | int | map[string]Any | string | u64
|
|
|
|
// Doc is a parsed YAML document.
|
|
pub struct Doc {
|
|
pub:
|
|
root Any
|
|
}
|
|
|
|
// parse_file parses the YAML file at `path`.
|
|
pub fn parse_file(path string) !Doc {
|
|
return parse_text(os.read_file(path)!)
|
|
}
|
|
|
|
// parse_text parses the YAML document provided in `text`.
|
|
pub fn parse_text(text string) !Doc {
|
|
mut normalized := text
|
|
if normalized.contains_u8(`\r`) {
|
|
normalized = normalized.replace('\r\n', '\n').replace('\r', '\n')
|
|
}
|
|
if normalized.len >= 3 && normalized[0] == 0xef && normalized[1] == 0xbb
|
|
&& normalized[2] == 0xbf {
|
|
normalized = normalized[3..]
|
|
}
|
|
// `split('\n')` would otherwise turn the canonical trailing line break into
|
|
// a phantom empty last line, which the block-scalar reader then treats as a
|
|
// genuine blank line and over-counts during chomping.
|
|
if normalized.ends_with('\n') {
|
|
normalized = normalized[..normalized.len - 1]
|
|
}
|
|
trimmed := normalized.trim_space()
|
|
if trimmed == '' {
|
|
return Doc{
|
|
root: null
|
|
}
|
|
}
|
|
if trimmed.starts_with('{') || trimmed.starts_with('[') {
|
|
// JSON-superset fast path. `parse_flow_value` already consumes the
|
|
// flow-style grammar that YAML borrows from JSON, so it builds the
|
|
// `yaml.Any` tree directly — no second-pass `from_json2` rebuild.
|
|
// Falls through to the block parser if the body is anything other
|
|
// than a clean flow document.
|
|
if val := parse_flow_value(trimmed) {
|
|
return Doc{
|
|
root: val
|
|
}
|
|
}
|
|
}
|
|
mut parser := Parser{
|
|
lines: normalized.split('\n')
|
|
}
|
|
return Doc{
|
|
root: parser.parse()!
|
|
}
|
|
}
|
|
|
|
// decode decodes YAML text into the target type `T`.
|
|
// The generic encode/decode path uses the main `json` module for field parity.
|
|
pub fn decode[T](yaml_text string) !T {
|
|
doc := parse_text(yaml_text)!
|
|
return doc.decode[T]()
|
|
}
|
|
|
|
// decode_file decodes the YAML file at `path` into the target type `T`.
|
|
pub fn decode_file[T](path string) !T {
|
|
return decode[T](os.read_file(path)!)
|
|
}
|
|
|
|
// encode encodes the value `value` into a YAML string.
|
|
// The generic encode/decode path uses the main `json` module for field parity.
|
|
pub fn encode[T](value T) string {
|
|
json_text := json.encode(value)
|
|
raw := json2.decode[json2.Any](json_text) or { return '' }
|
|
return from_json2(raw).to_yaml()
|
|
}
|
|
|
|
// encode_file encodes `value` as YAML and writes it to `path`.
|
|
pub fn encode_file[T](path string, value T) ! {
|
|
os.write_file(path, encode(value))!
|
|
}
|
|
|
|
// decode decodes the YAML document into the target type `T`. An empty
|
|
// document (parsed as the YAML 1.2 null node) decodes to a default-initialized
|
|
// `T`, matching the common "empty config file = use defaults" idiom.
|
|
pub fn (d Doc) decode[T]() !T {
|
|
if d.root is Null {
|
|
return json.decode(T, '{}')!
|
|
}
|
|
return json.decode(T, d.to_json())!
|
|
}
|
|
|
|
// to_any converts the YAML document to `yaml.Any`.
|
|
pub fn (d Doc) to_any() Any {
|
|
return d.root
|
|
}
|
|
|
|
// to_json converts the YAML document to JSON.
|
|
pub fn (d Doc) to_json() string {
|
|
return d.root.to_json()
|
|
}
|
|
|
|
// to_yaml converts the YAML document back to YAML text.
|
|
pub fn (d Doc) to_yaml() string {
|
|
return d.root.to_yaml()
|
|
}
|
|
|
|
// value queries a value from the YAML document.
|
|
// `key` supports dotted keys and array indexing like `servers[0].host`.
|
|
pub fn (d Doc) value(key string) Any {
|
|
return d.root.value(key)
|
|
}
|
|
|
|
// value_opt queries a value from the YAML document and returns an error when missing.
|
|
pub fn (d Doc) value_opt(key string) !Any {
|
|
return d.root.value_opt(key)
|
|
}
|
|
|
|
// str returns a display-friendly string form of `Any`.
|
|
pub fn (a Any) str() string {
|
|
return a.string()
|
|
}
|
|
|
|
// string returns `Any` as a string when possible, or a YAML representation otherwise.
|
|
pub fn (a Any) string() string {
|
|
return match a {
|
|
string { a }
|
|
bool, f64, i64, int, u64 { a.str() }
|
|
Null { 'null' }
|
|
[]Any, map[string]Any { a.to_yaml() }
|
|
}
|
|
}
|
|
|
|
// int returns `Any` as an `int`.
|
|
pub fn (a Any) int() int {
|
|
return match a {
|
|
int {
|
|
a
|
|
}
|
|
i64 {
|
|
int(a)
|
|
}
|
|
u64 {
|
|
int(a)
|
|
}
|
|
f64 {
|
|
int(a)
|
|
}
|
|
bool {
|
|
if a {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
string {
|
|
a.int()
|
|
}
|
|
else {
|
|
0
|
|
}
|
|
}
|
|
}
|
|
|
|
// i64 returns `Any` as an `i64`.
|
|
pub fn (a Any) i64() i64 {
|
|
return match a {
|
|
i64 {
|
|
a
|
|
}
|
|
int {
|
|
i64(a)
|
|
}
|
|
u64 {
|
|
i64(a)
|
|
}
|
|
f64 {
|
|
i64(a)
|
|
}
|
|
bool {
|
|
if a {
|
|
i64(1)
|
|
} else {
|
|
i64(0)
|
|
}
|
|
}
|
|
string {
|
|
a.i64()
|
|
}
|
|
else {
|
|
i64(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
// u64 returns `Any` as a `u64`.
|
|
pub fn (a Any) u64() u64 {
|
|
return match a {
|
|
u64 {
|
|
a
|
|
}
|
|
int {
|
|
u64(a)
|
|
}
|
|
i64 {
|
|
u64(a)
|
|
}
|
|
f64 {
|
|
u64(a)
|
|
}
|
|
bool {
|
|
if a {
|
|
u64(1)
|
|
} else {
|
|
u64(0)
|
|
}
|
|
}
|
|
string {
|
|
a.u64()
|
|
}
|
|
else {
|
|
u64(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
// f64 returns `Any` as an `f64`.
|
|
pub fn (a Any) f64() f64 {
|
|
return match a {
|
|
f64 {
|
|
a
|
|
}
|
|
int {
|
|
f64(a)
|
|
}
|
|
i64 {
|
|
f64(a)
|
|
}
|
|
u64 {
|
|
f64(a)
|
|
}
|
|
bool {
|
|
if a {
|
|
1.0
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
string {
|
|
a.f64()
|
|
}
|
|
else {
|
|
0.0
|
|
}
|
|
}
|
|
}
|
|
|
|
// bool returns `Any` as a `bool`.
|
|
pub fn (a Any) bool() bool {
|
|
return match a {
|
|
bool {
|
|
a
|
|
}
|
|
int {
|
|
a != 0
|
|
}
|
|
i64 {
|
|
a != 0
|
|
}
|
|
u64 {
|
|
a != 0
|
|
}
|
|
f64 {
|
|
a != 0.0
|
|
}
|
|
string {
|
|
lower := a.to_lower()
|
|
lower in ['true', 'yes', 'on', '1']
|
|
}
|
|
else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
// array returns `Any` as an array.
|
|
pub fn (a Any) array() []Any {
|
|
return match a {
|
|
[]Any {
|
|
a
|
|
}
|
|
map[string]Any {
|
|
mut arr := []Any{cap: a.len}
|
|
for _, value in a {
|
|
arr << value
|
|
}
|
|
arr
|
|
}
|
|
else {
|
|
[a]
|
|
}
|
|
}
|
|
}
|
|
|
|
// as_map returns `Any` as a map.
|
|
pub fn (a Any) as_map() map[string]Any {
|
|
return match a {
|
|
map[string]Any {
|
|
a
|
|
}
|
|
[]Any {
|
|
mut out := map[string]Any{}
|
|
for i, value in a {
|
|
out['${i}'] = value
|
|
}
|
|
out
|
|
}
|
|
else {
|
|
{
|
|
'0': a
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// default_to returns `value` when `a` is `Null`.
|
|
pub fn (a Any) default_to(value Any) Any {
|
|
return match a {
|
|
Null { value }
|
|
else { a }
|
|
}
|
|
}
|
|
|
|
// value queries a value from the current node using dotted keys and array indices.
|
|
pub fn (a Any) value(key string) Any {
|
|
return a.value_opt(key) or { null }
|
|
}
|
|
|
|
// value_opt queries a value from the current node and returns an error when missing.
|
|
// A YAML key whose value is the explicit `null` literal returns that `Null`
|
|
// (it is not treated as missing); only an absent key or a non-traversable
|
|
// path raises an error.
|
|
pub fn (a Any) value_opt(key string) !Any {
|
|
key_split := parse_dotted_key(key) or { return error('yaml: invalid dotted key `${key}`') }
|
|
return a.value_(a, key_split) or { error('yaml: no value for key `${key}`') }
|
|
}
|
|
|
|
// value queries a value from the map.
|
|
pub fn (m map[string]Any) value(key string) Any {
|
|
return Any(m).value(key)
|
|
}
|
|
|
|
// value queries a value from the array.
|
|
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 out := []string{cap: a.len}
|
|
for value in a {
|
|
out << value.string()
|
|
}
|
|
return out
|
|
}
|
|
|
|
// as_strings returns the contents of the map as `map[string]string`.
|
|
pub fn (m map[string]Any) as_strings() map[string]string {
|
|
mut out := map[string]string{}
|
|
for key, value in m {
|
|
out[key] = value.string()
|
|
}
|
|
return out
|
|
}
|
|
|
|
// to_json converts `Any` to JSON.
|
|
pub fn (a Any) to_json() string {
|
|
mut sb := strings.new_builder(256)
|
|
emit_any_as_json(mut sb, a)
|
|
return sb.str()
|
|
}
|
|
|
|
// to_yaml converts `Any` to YAML.
|
|
pub fn (a Any) to_yaml() string {
|
|
mut sb := strings.new_builder(256)
|
|
emit_yaml_any(mut sb, a, 0)
|
|
return sb.str()
|
|
}
|
|
|
|
// to_yaml converts a YAML array to YAML text.
|
|
pub fn (a []Any) to_yaml() string {
|
|
return Any(a).to_yaml()
|
|
}
|
|
|
|
// to_yaml converts a YAML map to YAML text.
|
|
pub fn (m map[string]Any) to_yaml() string {
|
|
return Any(m).to_yaml()
|
|
}
|