vlib/json2/doc_path_coverage_test.v
Rama master · réplica de lectura del repositorio original.
// JSON5 document and path coverage follows the API now provided by json2.
module json2
import os
struct DocCoverageSample {
name string
count int
}
const sample_doc = '{a: {b: [10, 20, 30]}}'
const quoted_path = 'a."b.c"'
const quoted_doc = '{a: {"b.c": 5}}'
fn load_sample() !Doc {
return parse_text(sample_doc, json5: true)!
}
fn test_doc_value_walks_keys_indices_and_quoted_keys() {
d := load_sample()!
assert d.value('a.b[0]').int() == 10
assert d.value('a.b[2]').int() == 30
assert d.value('a').as_array().len == 1
// Measured: an out-of-range index and a missing key both resolve to
// `null` rather than erroring.
assert d.value('a.b[9]') is Null
assert d.value('a.zz') is Null
// A quoted key keeps the dot it contains.
assert parse_text(quoted_doc, json5: true)!.value(quoted_path).int() == 5
}
fn test_doc_value_is_null_when_a_step_meets_the_wrong_shape() {
d := parse_text('{a: [], b: {}}', json5: true)!
assert d.value('a[0]') is Null
assert d.value('b.zz') is Null
// Walking a key into an array, and an index into an object, are both null.
assert d.value('a.zz') is Null
assert d.value('[0]') is Null
}
fn test_doc_value_opt_returns_the_value_when_the_path_resolves() {
d := load_sample()!
assert d.value_opt('a.b[1]')!.int() == 20
// `a` is the inner object, so coercing it to an array yields one element,
// which is that object's only member.
assert d.value_opt('a')!.as_array().len == 1
assert 'b' in d.value_opt('a')!.as_map()
}
fn test_doc_value_opt_reports_a_missing_key_as_a_name_error() {
d := load_sample()!
if value := d.value_opt('a.zz') {
assert false, 'a missing key must error, got `${value.str()}`'
} else {
assert err.msg() == 'json2: no key `a.zz`'
}
}
fn test_doc_get_separates_a_bad_path_from_a_missing_key() {
d := load_sample()!
// Missing and malformed paths are none; a present JSON null remains a value.
if v := d.get('a[x]') {
assert false, 'a malformed path must be none, got `${v.str()}`'
}
assert d.get('a.zz') == none
null_doc := parse_text('{"a": null}')!
if v := null_doc.get('a') {
assert v is Null
} else {
assert false, 'a present null must resolve'
}
if v := d.get('a.b[0]') {
assert v.int() == 10
} else {
assert false, 'a present key must resolve'
}
}
fn test_doc_decode_reads_the_whole_document() {
d := parse_text('{name: "bob", count: 3}', json5: true)!
s := d.decode[DocCoverageSample]()!
assert s.name == 'bob'
assert s.count == 3
}
// ---------------------------------------------------------------------
// parse_path / resolve_path
// ---------------------------------------------------------------------
fn test_path_step_str_round_trips_each_form() {
steps := parse_path('a.b[2].c')!
assert steps.len == 4
assert steps[0].str() == 'a'
assert steps[1].str() == 'b'
assert steps[2].str() == '[2]'
assert steps[3].str() == 'c'
// Measured: an index step carries an empty key and the parsed index,
// which is what `is_index` reports.
assert steps[2].key == ''
assert steps[2].index == 2
assert !steps[0].is_index()
assert steps[2].is_index()
}
fn test_path_step_is_index_is_true_only_for_index_steps() {
steps := parse_path('[0][7]')!
assert steps.len == 2
assert steps[0].is_index()
assert steps[1].is_index()
assert steps[0].index == 0
assert steps[1].index == 7
key_step := PathStep{
key: 'a'
index: -1
}
assert !key_step.is_index()
// A key step is sentinel-marked with index -1, so `is_index` is a
// comparison against that value rather than a separate flag.
zero_key := PathStep{
key: 'a'
index: 0
}
assert zero_key.is_index()
}
fn test_parse_path_keeps_a_dot_inside_a_quoted_key() {
steps := parse_path(quoted_path)!
assert steps.len == 2
assert steps[0].key == 'a'
assert steps[1].key == 'b.c'
assert steps[1].index == -1
// Single quotes work the same way.
assert parse_path("'a b'")![0].key == 'a b'
}
fn test_parse_path_of_an_empty_string_has_no_steps() {
assert parse_path('')!.len == 0
assert parse_path('')!.str() == '[]'
}
fn test_parse_path_rejects_a_bracket_that_is_not_an_index() {
// Each of these is a path the walker cannot read; every one errors rather
// than silently producing a wrong step.
assert path_fails('a[x]')
assert path_fails('a[]')
assert path_fails('a[')
}
fn test_parse_path_rejects_an_unterminated_quote() {
assert path_fails('a."b')
assert path_fails("a.'b")
}
fn test_parse_path_rejects_text_after_a_quoted_key() {
assert path_fails('a."b"c')
assert path_fails("a.'b'c")
}
fn test_parse_path_rejects_a_quote_inside_a_bare_key() {
assert path_fails('a"b')
}
fn test_resolve_path_matches_the_shape_of_each_step() {
nested := parse(sample_doc, json5: true)!
step := parse_path('a.b[1]')!
assert resolve_path(nested, step).int() == 20
// A missing key, an out-of-range index, a key walked into an array and an
// index walked into an object are all `null`.
assert resolve_path(nested, parse_path('a.zz')!) is Null
assert resolve_path(nested, parse_path('a.b[9]')!) is Null
assert resolve_path(nested, parse_path('a.b.zz')!) is Null
assert resolve_path(nested, parse_path('[0]')!) is Null
assert resolve_path(nested, parse_path('a.b')!).as_array().len == 3
assert resolve_path(nested, parse_path('a.b')!).as_array()[2].int() == 30
}
fn test_resolve_path_of_no_steps_returns_the_document_itself() {
nested := parse(sample_doc, json5: true)!
whole := resolve_path(nested, [])
assert whole is map[string]Any
assert 'a' in whole.as_map()
// And a path with no steps applied to an array is the array itself.
assert resolve_path(parse('[1]')!, []).as_array().len == 1
}
fn test_parse_file_reads_a_json5_document_from_disk() {
path := os.join_path(os.vtmp_dir(), 'json5_doc_path_coverage_${os.getpid()}.json5')
os.write_file(path, '{name: "file", count: 12}\n')!
defer { os.rm(path) or {} }
d := parse_file(path, json5: true)!
assert d.value('name').str() == 'file'
assert d.value('count').int() == 12
}
fn test_decode_any_converts_an_already_parsed_tree() {
value := parse('{name: "bob", count: 3}', json5: true)!
s := decode_any[DocCoverageSample](value, json5: true)!
assert s.name == 'bob'
assert s.count == 3
}
fn path_fails(path string) bool {
if steps := parse_path(path) {
assert false, '`${path}` must not parse, got ${steps.len} steps'
}
return true
}