From 368c2a6bf0c7d99c7aded4c0ed7334b49e03d910 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Thu, 10 Sep 2020 18:05:40 +0800 Subject: [PATCH] vlib: add x/json2 (#6336) --- vlib/x/json2/README.md | 133 +++++++++++++ vlib/x/json2/builder.v | 65 +++++++ vlib/x/json2/decoder.v | 392 ++++++++++++++++++++++++++++++++++++++ vlib/x/json2/encoder.v | 59 ++++++ vlib/x/json2/json2.v | 124 ++++++++++++ vlib/x/json2/json2_test.v | 295 ++++++++++++++++++++++++++++ 6 files changed, 1068 insertions(+) create mode 100644 vlib/x/json2/README.md create mode 100644 vlib/x/json2/builder.v create mode 100644 vlib/x/json2/decoder.v create mode 100644 vlib/x/json2/encoder.v create mode 100644 vlib/x/json2/json2.v create mode 100644 vlib/x/json2/json2_test.v diff --git a/vlib/x/json2/README.md b/vlib/x/json2/README.md new file mode 100644 index 0000000000..9454b5f1c5 --- /dev/null +++ b/vlib/x/json2/README.md @@ -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(person2) +} +``` +## Using `decode` and `encode` +> 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` and `encode` 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(resp) + println(person) // Person{name: 'Bob', age: 28, pets: ['Floof']} + person_json := json2.encode(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. diff --git a/vlib/x/json2/builder.v b/vlib/x/json2/builder.v new file mode 100644 index 0000000000..626c5e41fa --- /dev/null +++ b/vlib/x/json2/builder.v @@ -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) +} diff --git a/vlib/x/json2/decoder.v b/vlib/x/json2/decoder.v new file mode 100644 index 0000000000..0a66261317 --- /dev/null +++ b/vlib/x/json2/decoder.v @@ -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) +} diff --git a/vlib/x/json2/encoder.v b/vlib/x/json2/encoder.v new file mode 100644 index 0000000000..bd32868d23 --- /dev/null +++ b/vlib/x/json2/encoder.v @@ -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 '' + } + } +} diff --git a/vlib/x/json2/json2.v b/vlib/x/json2/json2.v new file mode 100644 index 0000000000..43b0d85bcc --- /dev/null +++ b/vlib/x/json2/json2.v @@ -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(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(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] +} diff --git a/vlib/x/json2/json2_test.v b/vlib/x/json2/json2_test.v new file mode 100644 index 0000000000..2b41d4b718 --- /dev/null +++ b/vlib/x/json2/json2_test.v @@ -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(d) + } $else { + mp[field.name] = d + } + } + */ + + return mp.str() +} + +fn test_simple() { + x := Employee{'Peter', 28, 95000.5, .worker} + s := json2.encode(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('{"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() +} +*/