vlib: add x/json2 (#6336)

pull/6349/head
Ned Palacios 2020-09-10 18:05:40 +08:00 committed by GitHub
parent bc8aab4775
commit 368c2a6bf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1068 additions and 0 deletions

View File

@ -0,0 +1,133 @@
> `json2` was named just to avoid any unwanted potential conflicts with the existing codegen tailored for the main `json` module which is powered by CJSON.
An experimental version of the JSON parser written from scratch on V.
## Usage
```v
import x.json2
import http
fn main() {
// Decoding
resp := http.get('https://example.com')?
// raw decode
raw_person := json2.raw_decode(resp.text)?
// Casting `Any` type / Navigating
person := raw_person.as_map()
name := person['name'].str() // Bob
age := person['age'].int() // 19
pi := person['pi'].f64() // 3.14....
// Constructing an `Any` type
mut me := map[string]json2.Any
me['name'] = 'Bob'
me['age'] = 18
mut arr := []json2.Any
arr << 'rock'
arr << 'papers'
arr << json2.null()
arr << 12
me['interests'] = arr
mut pets := map[string]json2.Any
pets['Sam'] = 'Maltese Shitzu'
me['pets'] = pets
// Stringify to JSON
println(me.str())
//{"name":"Bob","age":18,"interests":["rock","papers","scissors",null,12],"pets":{"Sam":"Maltese"}}
// Encode a struct/type to JSON
encoded_json := json2.encode<Person>(person2)
}
```
## Using `decode<T>` and `encode<T>`
> Codegen for this feature is still WIP. You need to manually define the methods before using the module to structs.
In order to use the `decode<T>` and `encode<T>` function, you need to explicitly define two methods: `from_json` and `to_json`. `from_json` accepts a `json2.Any` argument and inside of it you need to map the fields you're going to put into the type. As for `to_json` method, you just need to map the values into `json2.Any` and turn it into a string.
```v
struct Person {
mut:
name string
age int = 20
pets []string
}
fn (mut p Person) from_json(f json2.Any) {
obj := f.as_map()
for k, v in obj {
match k {
'name' { p.name = v.str() }
'age' { p.age = v.int() }
'pets' { p.pets = v.arr().map(it.str()) }
else {}
}
}
}
fn (p Person) to_json() string {
mut obj := map[string]json2.Any
obj['name'] = p.name
obj['age'] = p.age
obj['pets'] = p.pets
return obj.str()
}
fn main() {
resp := os.read_file('./person.json')?
person := json2.decode<Person>(resp)
println(person) // Person{name: 'Bob', age: 28, pets: ['Floof']}
person_json := json2.encode<Person>(person)
println(person_json) // {"name": "Bob", "age": 28, "pets": ["Floof"]}
}
```
## Using struct tags
`x.json2` cannot use struct tags just like when you use the `json` module. However, it emits an `Any` type when decoding so it can be flexible on the way you use it.
### Null Values
`x.json2` have a `null` value for differentiating an undefined value and a null value. Use `is` for verifying the field you're using is a null.
```v
fn (mut p Person) from_json(f json2.Any) {
obj := f.as_map()
if obj['age'] is json2.Null {
// use a default value
p.age = 10
}
}
```
### Custom field names
In `json`, you can specify the field name you're mapping into the struct field by specifying a `json:` tag. In `x.json2`, just simply cast the base field into a map (`as_map()`) and get the value of the field you wish to put into the struct/type.
```v
fn (mut p Person) from_json(f json2.Any) {
obj := f.as_map()
p.name = obj['nickname'].str()
}
```
```v
fn (mut p Person) to_json() string {
obj := f.as_map()
obj['nickname'] = p.name
return obj.str()
}
```
### Undefined Values
Getting undefined values has the same behavior as regular V types. If you're casting a base field into `map[string]json2.Any` and fetch an undefined entry/value, it simply returns empty. As for the `[]json2.Any`, it returns an index error.
## Casting a value to an incompatible type
`x.json2` provides methods for turning `Any` types into usable types. The following list shows the possible outputs when casting a value to an incompatible type.
1. Casting non-array values as array (`arr()`) will return an array with the value as the content.
2. Casting non-map values as map (`as_map()`) will return a map with the value as the content.
3. Casting non-string values to string (`str()`) will return the stringified representation of the value.
4. Casting non-numeric values to int/float (`int()`/`f64()`) will return zero.

View File

@ -0,0 +1,65 @@
// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module json2
// Inserts a string into the map.
pub fn (mut obj map[string]Any) insert_str(key string, val string) {
mut fi := Any{}
fi = val
obj[key] = fi
}
// Inserts an int into the map.
pub fn (mut obj map[string]Any) insert_int(key string, val int) {
obj[key] = Any(val)
}
// Inserts a float into the map.
pub fn (mut obj map[string]Any) insert_f(key string, val f64) {
obj[key] = Any(val)
}
// Inserts a null into the map.
pub fn (mut obj map[string]Any) insert_null(key string) {
obj[key] = Any(Null{})
}
// Inserts a bool into the map.
pub fn (mut obj map[string]Any) insert_bool(key string, val bool) {
obj[key] = Any(val)
}
// Inserts a map into the map.
pub fn (mut obj map[string]Any) insert_map(key string, val map[string]Any) {
obj[key] = Any(val)
}
// Inserts an array into the map.
pub fn (mut obj map[string]Any) insert_arr(key string, val []Any) {
obj[key] = Any(val)
}
// Inserts a string into the array.
pub fn (mut arr []Any) insert_str(val string) {
mut fi := Any{}
fi = val
arr << fi
}
// Inserts an int into the array.
pub fn (mut arr []Any) insert_int(val int) {
arr << Any(val)
}
// Inserts a float into the array.
pub fn (mut arr []Any) insert_f(val f64) {
arr << Any(val)
}
// Inserts a null into the array.
pub fn (mut arr []Any) insert_null() {
arr << Any(Null{})
}
// Inserts a bool into the array.
pub fn (mut arr []Any) insert_bool(val bool) {
arr << Any(val)
}
// Inserts a map into the array.
pub fn (mut arr []Any) insert_map(val map[string]Any) {
arr << Any(val)
}
// Inserts an array into the array.
pub fn (mut arr []Any) insert_arr(val []Any) {
arr << Any(val)
}

View File

@ -0,0 +1,392 @@
// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module json2
import strings
import strconv
import v.scanner
import v.token
import v.util
import v.pref
// `Any` is a sum type that lists the possible types to be decoded and used.
pub type Any = string | int | f64 | any_int | any_float | bool | Null | []Any | map[string]Any
// `Null` struct is a simple representation of the `null` value in JSON.
pub struct Null {}
enum ParseMode {
array
bool
invalid
null
number
object
string
}
const (
formfeed_err = 'formfeed not allowed.'
eof_err = 'reached eof. data not closed properly.'
)
struct Parser {
mut:
scanner &scanner.Scanner
p_tok token.Token
tok token.Token
n_tok token.Token
nn_tok token.Token
mode ParseMode = .invalid
n_level int
}
fn (mut p Parser) next() {
p.p_tok = p.tok
p.tok = p.n_tok
p.n_tok = p.nn_tok
p.nn_tok = p.scanner.scan()
}
fn (p Parser) emit_error(msg string) string {
source := p.scanner.text
cur := p.tok
mut pp := util.imax(0, util.imin(source.len - 1, cur.pos))
if source.len > 0 {
for ; pp >= 0; pp-- {
if source[pp] == `\r` || source[pp] == `\n` {
break
}
}
}
column := util.imax(0, cur.pos - pp + cur.len - 1)
line := cur.line_nr
return '[jisoni] ' + msg + ' (At line $line, column $column)'
}
fn new_parser(srce string) Parser {
mut src := srce
// from v/util/util.v
if src.len >= 3 {
c_text := src.str
unsafe {
if c_text[0] == 0xEF && c_text[1] == 0xBB && c_text[2] == 0xBF {
// skip three BOM bytes
offset_from_begin := 3
src = tos(c_text[offset_from_begin], vstrlen(c_text) - offset_from_begin)
}
}
}
mut p := Parser{
scanner: scanner.new_scanner(src, .parse_comments, &pref.Preferences{}),
}
return p
}
fn check_valid_hex(str string) ?bool {
if str.len != 4 {
return error('Hex string must be 4 characters.')
}
for l in str {
if l.is_hex_digit() { continue }
return error('Provided string is not a hex digit.')
}
return true
}
fn (p Parser) is_formfeed() bool {
prev_tok_pos := p.p_tok.pos + p.p_tok.len - 2
if prev_tok_pos < p.scanner.text.len && p.scanner.text[prev_tok_pos] == 0x0c {
return true
}
return false
}
fn (p Parser) is_singlequote() bool {
src := p.scanner.text
prev_tok_pos := p.p_tok.pos + p.p_tok.len
return src[prev_tok_pos] == `'`
}
fn (mut p Parser) detect_parse_mode() {
src := p.scanner.text
if src.len > 1 && src[0].is_digit() && !src[1].is_digit() {
p.mode == .invalid
return
}
p.tok = p.scanner.scan()
p.n_tok = p.scanner.scan()
p.nn_tok = p.scanner.scan()
if src.len == 1 && p.tok.kind == .string && p.n_tok.kind == .eof {
p.mode == .invalid
return
}
match p.tok.kind {
.lcbr { p.mode = .object }
.lsbr { p.mode = .array }
.number { p.mode = .number }
.key_true, .key_false { p.mode = .bool }
.string { p.mode = .string }
.name {
if p.tok.lit == 'null' {
p.mode = .null
}
}
.minus {
if p.n_tok.kind == .number {
p.mode = .number
}
}
else {}
}
}
fn (mut p Parser) decode_value() ?Any {
mut fi := Any{}
if (p.tok.kind == .lsbr && p.n_tok.kind == .lcbr) || (p.p_tok.kind == p.tok.kind && p.tok.kind == .lsbr) {
p.n_level++
}
if p.n_level == 500 {
return error('Reached maximum nesting level of 500.')
}
match p.tok.kind {
.lsbr {
item := p.decode_array()?
fi = item
}
.lcbr {
item := p.decode_object()?
fi = item
}
.number {
item := p.decode_number()?
fi = item
}
.key_true {
fi = Any(true)
}
.key_false {
fi = Any(false)
}
.name {
if p.tok.lit != 'null' {
return error('Unknown identifier `$p.tok.lit`')
}
fi = Any(Null{})
}
.string {
if p.is_singlequote() {
return error('Strings must be in double-quotes.')
}
item := p.decode_string() or {
return error(err)
}
fi = item
}
else {
if p.tok.kind == .minus && p.n_tok.kind == .number && p.n_tok.pos == p.tok.pos+1 {
p.next()
d_num := p.decode_number() or {
return error(err)
}
p.next()
fi = d_num
return fi
}
return error('[decode_value] Unknown token `$p.tok.lit`')
}
}
p.next()
if p.is_formfeed() {
return error(formfeed_err)
}
return fi
}
fn (mut p Parser) decode_string() ?Any {
mut strwr := strings.new_builder(200)
mut fi := Any{}
for i := 0; i < p.tok.lit.len; i++ {
// s := p.tok.lit[i].str()
// println('$i $s')
if ((i-1 >= 0 && p.tok.lit[i-1] != `/`) || i == 0) && int(p.tok.lit[i]) in [9, 10, 0] {
return error('Character must be escaped with a backslash.')
}
if i == p.tok.lit.len-1 && p.tok.lit[i] == 92 {
return error('Invalid backslash escape.')
}
if i+1 < p.tok.lit.len && p.tok.lit[i] == 92 {
peek := p.tok.lit[i+1]
if peek in [`b`, `f`, `n`, `r`, `t`, `u`, `\\`, `"`, `/`] {
if peek == `u` {
if i+5 < p.tok.lit.len {
codepoint := p.tok.lit[i+2..i+6]
check_valid_hex(codepoint) or {
return error(err)
}
hex_val := strconv.parse_int(codepoint, 16, 0)
strwr.write_b(byte(hex_val))
i += 5
continue
} else {
return error('Incomplete unicode escape.')
}
}
i++
strwr.write_b(p.tok.lit[i])
continue
} else {
return error('Invalid backslash escape.')
}
if peek == 85 {
return error('Unicode endpoints must be in lowercase `u`.')
}
if int(peek) in [9, 229] {
return error('Unicode endpoint not allowed.')
}
}
strwr.write_b(p.tok.lit[i])
}
fi = strwr.str()
return fi
}
fn (mut p Parser) decode_number() ?Any {
src := p.scanner.text
mut tl := p.tok.lit
mut is_fl := false
sep_by_dot := tl.to_lower().split('.')
if tl.starts_with('0x') && tl.all_after('0x').len <= 2 {
return error('Hex numbers should not be less than or equal to two digits.')
}
if src[p.p_tok.pos + p.p_tok.len] == `0` && src[p.p_tok.pos + p.p_tok.len + 1].is_digit() {
return error('Leading zeroes in integers are not allowed.')
}
if tl.starts_with('.') {
return error('Decimals must start with a digit followed by a dot.')
}
if tl.ends_with('+') || tl.ends_with('-') {
return error('Exponents must have a digit before the sign.')
}
if sep_by_dot.len > 1 {
// analyze json number structure
// -[digit][dot][digit][E/e][-/+][digit]
is_fl = true
last := sep_by_dot.last()
if last.starts_with('e') {
return error('Exponents must have a digit before the exponent notation.')
}
}
if p.p_tok.kind == .minus && p.tok.pos == p.p_tok.pos+1 {
tl = '-' + tl
}
return if is_fl { Any(tl.f64()) } else { Any(tl.int()) }
}
fn (mut p Parser) decode_array() ?Any {
mut items := []Any{}
p.next()
for p.tok.kind != .rsbr {
if p.tok.kind == .eof {
return error(eof_err)
}
item := p.decode_value() or {
return error(err)
}
items << item
if p.tok.kind == .comma && p.n_tok.kind !in [.rsbr, .comma] {
p.next()
continue
}
if p.tok.kind == .rsbr {
break
}
return error('Unknown token `$p.tok.lit` when decoding arrays.')
}
return Any(items)
}
fn (mut p Parser) decode_object() ?Any {
mut fields := map[string]Any
mut cur_key := ''
p.next()
for p.tok.kind != .rcbr {
is_key := p.tok.kind == .string && p.n_tok.kind == .colon
// todo
// if p.is_formfeed() {
// return error(formfeed_err)
// }
if p.tok.kind == .eof {
return error(eof_err)
}
if p.is_singlequote() {
return error('Object keys must be in single quotes.')
}
if !is_key {
return error('Invalid token `$p.tok.lit`, expected `string`')
}
cur_key = p.tok.lit
p.next()
p.next()
item := p.decode_value() or {
return error(err)
}
fields[cur_key] = item
if p.tok.kind == .comma && p.n_tok.kind !in [.rcbr, .comma] {
p.next()
continue
}
if p.tok.kind == .rcbr {
break
}
return error('Unknown token `$p.tok.lit` when decoding object.')
}
return Any(fields)
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module json2
import strings
// String representation of the `map[string]Any`.
pub fn (flds map[string]Any) str() string {
mut wr := strings.new_builder(200)
wr.write('{')
mut i := 0
for k, v in flds {
wr.write('"$k":')
if v is string {
wr.write('"' + *v + '"')
} else {
wr.write(v.str())
}
if i < flds.len-1 { wr.write(',') }
i++
}
wr.write('}')
return wr.str()
}
// String representation of the `[]Any`.
pub fn (flds []Any) str() string {
mut wr := strings.new_builder(200)
wr.write('[')
for i, v in flds {
if v is string {
wr.write('"' + *v + '"')
} else {
wr.write(v.str())
}
if i < flds.len-1 { wr.write(',') }
}
wr.write(']')
return wr.str()
}
// String representation of the `Any` type.
pub fn (f Any) str() string {
match f {
string { return *f }
int { return (*f).str() }
f64 { return (*f).str() }
any_int { return (*f).str() }
any_float { return (*f).str() }
bool { return (*f).str() }
map[string]Any { return (*f).str() }
Null { return 'null' }
else {
if f is []Any {
arr := f
return (*arr).str()
}
return ''
}
}
}

View File

@ -0,0 +1,124 @@
// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module json2
pub interface Serializable {
from_json(f Any)
to_json() string
}
// Decodes a JSON string into an `Any` type. Returns an option.
pub fn raw_decode(src string) ?Any {
mut p := new_parser(src)
p.detect_parse_mode()
if p.mode == .invalid {
return error(p.emit_error('Invalid JSON.'))
}
fi := p.decode_value() or {
return error(p.emit_error(err))
}
if p.tok.kind != .eof {
return error(p.emit_error('Unknown token `$p.tok.kind`.'))
}
return fi
}
// A generic function that decodes a JSON string into the target type.
//
// TODO: decode must return an optional generics
pub fn decode<T>(src string) T {
res := raw_decode(src) or {
panic(err)
}
mut typ := T{}
typ.from_json(res)
return typ
}
// A generic function that encodes a type into a JSON string.
pub fn encode<T>(typ T) string {
return typ.to_json()
}
// A simple function that returns `Null` struct. For use on constructing an `Any` object.
pub fn null() Null {
return Null{}
}
// Use `Any` as a map.
pub fn (f Any) as_map() map[string]Any {
mut mp := map[string]Any
match f {
map[string]Any {
return *f
}
string {
mp['0'] = f
return mp
}
int {
mp['0'] = f
return mp
}
bool {
mp['0'] = f
return mp
}
f64 {
mp['0'] = f
return mp
}
Null {
mp['0'] = f
return mp
}
else {
if typeof(f) == 'array_Any' {
arr := f as []Any
for i, fi in arr {
mp[i.str()] = fi
}
return mp
}
return mp
}
}
}
// Use `Any` as an integer.
pub fn (f Any) int() int {
match f {
int { return *f }
f64 { return f.str().int() }
else { return 0 }
}
}
// Use `Any` as a float.
pub fn (f Any) f64() f64 {
match f {
int { return *f }
f64 { return *f }
else { return 0.0 }
}
}
// Use `Any` as an array.
pub fn (f Any) arr() []Any {
if f is []Any {
return *f
}
if f is map[string]Any {
mut arr := []Any{}
mp := *f
for _, v in mp {
arr << v
}
return arr
}
return [f]
}

View File

@ -0,0 +1,295 @@
import x.json2
enum JobTitle {
manager
executive
worker
}
struct Employee {
name string
age int
salary f32
title JobTitle
}
fn (e Employee) to_json() string {
mut mp := map[string]json2.Any
mp['name'] = e.name
mp['age'] = e.age
mp['salary'] = f64(e.salary)
mp['title'] = int(e.title)
/*
$for field in Employee.fields {
d := e.$(field.name)
$if field.Type is JobTitle {
mp[field.name] = json.encode<int>(d)
} $else {
mp[field.name] = d
}
}
*/
return mp.str()
}
fn test_simple() {
x := Employee{'Peter', 28, 95000.5, .worker}
s := json2.encode<Employee>(x)
eprintln('Employee x: $s')
assert s == '{"name":"Peter","age":28,"salary":95000.5,"title":2}'
/*
y := json.decode(Employee, s) or {
assert false
Employee{}
}*/
y := json2.raw_decode(s) or {
assert false
json2.Any{}
}
eprintln('Employee y: $y')
ym := y.as_map()
assert ym['name'].str() == 'Peter'
assert ym['age'].int() == 28
assert ym['salary'].f64() == 95000.5
// assert y['title'] == .worker
assert ym['title'].int() == 2
}
/*
struct User2 {
age int
nums []int
}
struct User {
age int
nums []int
last_name string [json: lastName]
is_registered bool [json: IsRegistered]
typ int [json: 'type']
pets string [raw; json: 'pet_animals']
}
fn test_parse_user() {
s := '{"age": 10, "nums": [1,2,3], "type": 1, "lastName": "Johnson", "IsRegistered": true, "pet_animals": {"name": "Bob", "animal": "Dog"}}'
u2 := json.decode(User2, s) or {
exit(1)
}
println(u2)
u := json.decode(User, s) or {
exit(1)
}
println(u)
assert u.age == 10
assert u.last_name == 'Johnson'
assert u.is_registered == true
assert u.nums.len == 3
assert u.nums[0] == 1
assert u.nums[1] == 2
assert u.nums[2] == 3
assert u.typ == 1
assert u.pets == '{"name":"Bob","animal":"Dog"}'
}
fn test_encode_user() {
usr := User{
age: 10
nums: [1, 2, 3]
last_name: 'Johnson'
is_registered: true
typ: 0
pets: 'foo'
}
expected := '{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"foo"}'
out := json.encode(usr)
println(out)
assert out == expected
}
struct Color {
space string
point string [raw]
}
fn test_raw_json_field() {
color := json.decode(Color, '{"space": "YCbCr", "point": {"Y": 123}}') or {
println('text')
return
}
assert color.point == '{"Y":123}'
assert color.space == 'YCbCr'
}
struct City {
name string
}
struct Country {
cities []City
name string
}
fn test_struct_in_struct() {
country := json.decode(Country, '{ "name": "UK", "cities": [{"name":"London"}, {"name":"Manchester"}]}') or {
assert false
exit(1)
}
assert country.name == 'UK'
assert country.cities.len == 2
assert country.cities[0].name == 'London'
assert country.cities[1].name == 'Manchester'
println(country.cities)
}
*/
fn test_encode_map() {
expected := '{"one":1,"two":2,"three":3,"four":4}'
numbers := {
'one': json2.Any(1)
'two': json2.Any(2)
'three': json2.Any(3)
'four': json2.Any(4)
}
out := numbers.str()
// out := json.encode(numbers)
println(out)
assert out == expected
}
/*
fn test_parse_map() {
expected := {
'one': 1
'two': 2
'three': 3
'four': 4
}
out := json.decode<map[string]int>('{"one":1,"two":2,"three":3,"four":4}') or {
assert false
r := {
'': 0
}
r
}
println(out)
assert out == expected
}
struct Data {
countries []Country
users map[string]User
extra map[string]map[string]int
}
fn test_nested_type() {
data_expected := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":[{"name":"Donlon"},{"name":"Termanches"}],"name":"KU"}],"users":{"Foo":{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"little foo"},"Boo":{"age":20,"nums":[5,3,1],"lastName":"Smith","IsRegistered":false,"type":4,"pet_animals":"little boo"}},"extra":{"2":{"n1":2,"n2":4,"n3":8,"n4":16},"3":{"n1":3,"n2":9,"n3":27,"n4":81}}}'
data := Data{
countries: [
Country{
name: 'UK'
cities: [City{'London'},
City{'Manchester'},
]
},
Country{
name: 'KU'
cities: [City{'Donlon'},
City{'Termanches'},
]
},
]
users: {
'Foo': User{
age: 10
nums: [1, 2, 3]
last_name: 'Johnson'
is_registered: true
typ: 0
pets: 'little foo'
},
'Boo': User{
age: 20
nums: [5, 3, 1]
last_name: 'Smith'
is_registered: false
typ: 4
pets: 'little boo'
}
},
extra: {
'2': {
'n1': 2
'n2': 4
'n3': 8
'n4': 16
},
'3': {
'n1': 3
'n2': 9
'n3': 27
'n4': 81
},
}
}
out := json.encode(data)
println(out)
assert out == data_expected
data2 := json.decode(Data, data_expected) or {
assert false
Data{}
}
assert data2.countries.len == data.countries.len
for i in 0..1 {
assert data2.countries[i].name == data.countries[i].name
assert data2.countries[i].cities.len == data.countries[i].cities.len
for j in 0..1 {
assert data2.countries[i].cities[j].name == data.countries[i].cities[j].name
}
}
for key, user in data.users {
assert data2.users[key].age == user.age
assert data2.users[key].nums == user.nums
assert data2.users[key].last_name == user.last_name
assert data2.users[key].is_registered == user.is_registered
assert data2.users[key].typ == user.typ
// assert data2.users[key].pets == user.pets // TODO FIX
}
for k, v in data.extra {
for k2, v2 in v {
assert data2.extra[k][k2] == v2
}
}
}
fn test_errors() {
invalid_array := fn () {
data := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":{"name":"Donlon"},"name":"KU"}],"users":{"Foo":{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"little foo"},"Boo":{"age":20,"nums":[5,3,1],"lastName":"Smith","IsRegistered":false,"type":4,"pet_animals":"little boo"}},"extra":{"2":{"n1":2,"n2":4,"n3":8,"n4":16},"3":{"n1":3,"n2":9,"n3":27,"n4":81}}}'
json.decode(Data, data) or {
println(err)
assert err.starts_with('Json element is not an array:')
return
}
assert false
}
invalid_object := fn() {
data := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":[{"name":"Donlon"},{"name":"Termanches"}],"name":"KU"}],"users":[{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"little foo"},{"age":20,"nums":[5,3,1],"lastName":"Smith","IsRegistered":false,"type":4,"pet_animals":"little boo"}],"extra":{"2":{"n1":2,"n2":4,"n3":8,"n4":16},"3":{"n1":3,"n2":9,"n3":27,"n4":81}}}'
json.decode(Data, data) or {
println(err)
assert err.starts_with('Json element is not an object:')
return
}
assert false
}
invalid_array()
invalid_object()
}
*/