vlib/json2/doc.v
Rama master · réplica de lectura del repositorio original.
module json2
// Doc is a parsed JSON document. Use `value`, `value_opt` or `decode` to read
// from it.
pub struct Doc {
params DecoderOptions
pub:
root Any
}
// new_doc wraps an already parsed `Any` tree.
pub fn new_doc(root Any, params DecoderOptions) Doc {
return Doc{
root: root
params: params
}
}
// to_any returns the document root as a dynamic value.
pub fn (d Doc) to_any() Any {
return d.root
}
// str returns the document rendered in the dialect it was parsed with.
pub fn (d Doc) str() string {
return encode(d.root, json5: d.params.json5)
}
// value queries a value with a small path syntax: dots separate object keys,
// brackets index arrays, and a key that contains a dot can be quoted, as in
// `a."b.c"`. Returns `Null` when the path does not resolve.
pub fn (d Doc) value(key string) Any {
return resolve_path(d.root, parse_path(key) or { return null })
}
// value_opt queries a value and returns an error when the path does not
// resolve, instead of `Null`.
pub fn (d Doc) value_opt(key string) !Any {
steps := parse_path(key) or { return error('invalid path `${key}`') }
return resolve_path_opt(d.root, steps) or { return error('json2: no key `${key}`') }
}
// get queries a value and returns an option, so `none` distinguishes a missing
// key from an explicit `null`.
pub fn (d Doc) get(key string) ?Any {
steps := parse_path(key) or { return none }
return resolve_path_opt(d.root, steps)
}
// decode decodes the document into `T`. It is the method form of the
// package-level `decode` and shares its attribute and hook support.
pub fn (d Doc) decode[T]() !T {
return decode_any[T](d.root, d.params)
}
// reflect sets the fields of `T` from the matching document keys, leaving
// absent fields at their default value. Fields are matched by name or by a
// `@[json: 'name']` attribute. Unlike `decode`, a key that cannot be decoded
// into its field is silently skipped instead of reported, so a partially
// matching document still fills in what it can.
pub fn (d Doc) reflect[T]() T {
mut out := T{}
reflect_into(d.root, mut out, d.params)
return out
}
// PathStep is one element of a parsed lookup path: either an object key or an
// array index.
pub struct PathStep {
pub:
key string
index int
}
// is_index reports whether the step indexes an array.
pub fn (s PathStep) is_index() bool {
return s.index >= 0
}
// str returns the step in path syntax.
pub fn (s PathStep) str() string {
return if s.is_index() { '[${s.index}]' } else { s.key }
}
// parse_path splits a lookup path into steps. Quoted segments keep any dots
// they contain, so `a."b.c"` addresses the key `b.c` inside the object `a`.
//
// The path is walked with an explicit index rather than a state machine, which
// keeps the "flush the pending key" logic in one place.
pub fn parse_path(path string) ![]PathStep {
mut steps := []PathStep{}
runes := path.runes()
mut i := 0
mut key := []rune{}
mut has_key := false
for i < runes.len {
ch := runes[i]
match ch {
`.` {
push_key(mut steps, key, has_key)
key = []rune{}
has_key = false
i++
}
`[` {
push_key(mut steps, key, has_key)
key = []rune{}
has_key = false
mut j := i + 1
mut digits := []rune{}
for j < runes.len && json5_is_digit(int(runes[j])) {
digits << runes[j]
j++
}
if digits.len == 0 || j >= runes.len || runes[j] != `]` {
return error('expected an array index like `[0]` in path `${path}`')
}
steps << PathStep{
key: ''
index: digits.string().int()
}
i = j + 1
}
`'`, `"` {
if has_key {
return error('unexpected quote in path `${path}`')
}
mut j := i + 1
mut quoted := []rune{}
for j < runes.len && runes[j] != ch {
quoted << runes[j]
j++
}
if j >= runes.len {
return error('unterminated quoted key in path `${path}`')
}
steps << PathStep{
key: quoted.string()
index: -1
}
has_key = false
key = []rune{}
i = j + 1
if i < runes.len && runes[i] != `.` && runes[i] != `[` {
return error('unexpected character after a quoted key in path `${path}`')
}
}
else {
key << ch
has_key = true
i++
}
}
}
push_key(mut steps, key, has_key)
return steps
}
// push_key appends a pending object-key step to `mut steps` when there is one.
fn push_key(mut steps []PathStep, key []rune, has_key bool) {
if has_key {
steps << PathStep{
key: key.string()
index: -1
}
}
}
// resolve_path walks steps through value, returning null when a step does not resolve.
pub fn resolve_path(value Any, steps []PathStep) Any {
return resolve_path_opt(value, steps) or { null }
}
// The traversal borrows containers from the document; autofree must not release them.
@[manualfree]
fn resolve_path_opt(value Any, steps []PathStep) ?Any {
mut current := value
for step in steps {
if step.is_index() {
if current is []Any {
if step.index >= current.len {
return none
}
current = current[step.index]
continue
}
return none
}
if current is map[string]Any {
current = current[step.key] or { return none }
} else {
return none
}
}
return current
}
// reflect_into fills the fields of `mut out` from `value`, skipping any key it
// cannot decode.
@[manualfree]
fn reflect_into[T](value Any, mut out T, params DecoderOptions) {
$if T is $struct {
obj := match value {
map[string]Any { value }
else { return }
}
// `$for` unrolls to straight-line code, so the field logic is nested
// `if`s rather than a loop with `continue`.
$for field in T.fields {
key := field_key_name(field.name, field.attrs)
if key != '' {
$if field.is_embed {
$if field.unaliased_typ is $struct {
mut embedded := out.$(field.name)
reflect_into(value, mut embedded, params)
out.$(field.name) = embedded
}
} $else {
// A key that is absent, `null`, or of a kind the field cannot
// hold leaves the field at its default.
if item := obj[key] {
if item !is Null {
mut slot := out.$(field.name)
reflect_value(item, mut slot, params)
out.$(field.name) = slot
}
}
}
}
}
} $else {
reflect_value(value, mut out, params)
}
}
@[manualfree]
fn reflect_value[T](value Any, mut out T, params DecoderOptions) {
if decoded := decode_any[T](value, params) {
out = decoded
}
}
fn field_key_name(name string, attrs []string) string {
info := encoder_field_info(name, attrs)
return if info.is_skip { '' } else { info.key_name }
}