From 6c634086b091fe1cb4dc1da3203719e13a2db7d2 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sun, 29 Nov 2020 21:54:45 +0800 Subject: [PATCH] json2: decode fn returns `?T`; add new tests (#6933) --- vlib/x/json2/README.md | 60 ++++++++++---- vlib/x/json2/any_test.v | 131 ++++++++++++++++++++++++++++++ vlib/x/json2/builder.v | 13 +++ vlib/x/json2/decoder.v | 156 +++++++++++++++++------------------- vlib/x/json2/decoder_test.v | 61 ++++++++++++++ vlib/x/json2/encoder.v | 60 ++++++++++---- vlib/x/json2/json2.v | 89 +++++++------------- vlib/x/json2/json2_test.v | 139 +++++++++++++++++++++++--------- 8 files changed, 498 insertions(+), 211 deletions(-) create mode 100644 vlib/x/json2/any_test.v create mode 100644 vlib/x/json2/decoder_test.v diff --git a/vlib/x/json2/README.md b/vlib/x/json2/README.md index 77ac1efe50..e540f158a5 100644 --- a/vlib/x/json2/README.md +++ b/vlib/x/json2/README.md @@ -1,7 +1,7 @@ -> `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. +> The name `json2` was chosen 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. +`x.json2` is an experimental JSON parser written from scratch on V. ## Usage ```v oksyntax @@ -29,7 +29,7 @@ fn main() { mut arr := []json2.Any arr << 'rock' arr << 'papers' - arr << json2.null() + arr << json2.null arr << 12 me['interests'] = arr @@ -40,7 +40,12 @@ fn main() { // Stringify to JSON println(me.str()) - //{"name":"Bob","age":18,"interests":["rock","papers","scissors",null,12],"pets":{"Sam":"Maltese"}} + //{ + // "name":"Bob", + // "age":18, + // "interests":["rock","papers","scissors",null,12], + // "pets":{"Sam":"Maltese"} + //} // Encode a struct/type to JSON encoded_json := json2.encode(person2) @@ -86,7 +91,7 @@ fn (p Person) to_json() string { fn main() { resp := os.read_file('./person.json')? - person := json2.decode(resp) + 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"]} @@ -94,12 +99,36 @@ fn main() { ``` ## 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. +`x.json2` can access and use the struct field tags similar to the +`json` module by using the comp-time `$for` for structs. + +```v ignore +fn (mut p Person) from_json(f json2.Any) { + mp := an.as_map() + mut js_field_name := '' + $for field in Person.fields { + js_field_name = field.name + + for attr in field.attrs { + if attr.starts_with('json:') { + js_field_name = attr.all_after('json:').trim_left(' ') + break + } + } + + match field.name { + 'name' { p.name = mp[js_field_name].str() } + 'age' { u.age = mp[js_field_name].int() } + 'pets' { u.pets = mp[js_field_name].arr().map(it.str()) } + else {} + } + } +} +``` ### 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. +`x.json2` has a separate `null` type for differentiating an undefined value and a null value. +To verify that the field you're accessing is a `null`, use ` is json2.Null`. ```v ignore fn (mut p Person) from_json(f json2.Any) { @@ -112,9 +141,8 @@ fn (mut p Person) from_json(f json2.Any) { ``` ### 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. +Aside from using struct tags, you can also just simply cast the base field into a map (`as_map()`) +and access the field you wish to put into the struct/type. ```v ignore fn (mut p Person) from_json(f json2.Any) { @@ -142,6 +170,6 @@ The following list shows the possible outputs when casting a value to an incompa 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. +3. Casting non-string values to string (`str()`) will return the +JSON string representation of the value. +4. Casting non-numeric values to int/float (`int()`/`i64()`/`f32()`/`f64()`) will return zero. diff --git a/vlib/x/json2/any_test.v b/vlib/x/json2/any_test.v new file mode 100644 index 0000000000..5e042af05f --- /dev/null +++ b/vlib/x/json2/any_test.v @@ -0,0 +1,131 @@ +import x.json2 + +const ( + sample_data = { + 'int': json2.Any(int(1)) + 'i64': json2.Any(i64(128)) + 'f32': json2.Any(f32(2.0)) + 'f64': json2.Any(f64(1.283)) + 'bool': json2.Any(false) + 'str': json2.Any('test') + 'null': json2.Any(json2.null) + 'arr': json2.Any([json2.Any('lol')]) + 'obj': json2.Any({ + 'foo': json2.Any(10) + }) + } +) + +fn is_null(f json2.Any) bool { + match f { + json2.Null { return true } + else { return false } + } +} + +fn test_f32() { + // valid conversions + assert sample_data['int'].f32() == 1.0 + assert sample_data['i64'].f32() == 128.0 + assert sample_data['f32'].f32() == 2.0 + assert sample_data['f64'].f32() == 1.2829999923706055 + // invalid conversions + assert sample_data['bool'].f32() == 0.0 + assert sample_data['str'].f32() == 0.0 + assert sample_data['null'].f32() == 0.0 + assert sample_data['arr'].f32() == 0.0 + assert sample_data['obj'].f32() == 0.0 +} + +fn test_f64() { + // valid conversions + assert sample_data['int'].f64() == 1.0 + assert sample_data['i64'].f64() == 128.0 + assert sample_data['f32'].f64() == 2.0 + assert sample_data['f64'].f64() == 1.283 + // invalid conversions + assert sample_data['bool'].f64() == 0.0 + assert sample_data['str'].f64() == 0.0 + assert sample_data['null'].f64() == 0.0 + assert sample_data['arr'].f64() == 0.0 + assert sample_data['obj'].f64() == 0.0 +} + +fn test_int() { + // valid conversions + assert sample_data['int'].int() == 1 + assert sample_data['i64'].int() == 128 + assert sample_data['f32'].int() == 2 + assert sample_data['f64'].int() == 1 + assert json2.Any(true).int() == 1 + // invalid conversions + assert json2.Any('123').int() == 0 + assert sample_data['null'].int() == 0 + assert sample_data['arr'].int() == 0 + assert sample_data['obj'].int() == 0 +} + +fn test_i64() { + // valid conversions + assert sample_data['int'].i64() == 1 + assert sample_data['i64'].i64() == 128 + assert sample_data['f32'].i64() == 2 + assert sample_data['f64'].i64() == 1 + assert json2.Any(true).i64() == 1 + // invalid conversions + assert json2.Any('123').i64() == 0 + assert sample_data['null'].i64() == 0 + assert sample_data['arr'].i64() == 0 + assert sample_data['obj'].i64() == 0 +} + +fn test_as_map() { + assert sample_data['int'].as_map()['0'].int() == 1 + assert sample_data['i64'].as_map()['0'].i64() == 128.0 + assert sample_data['f32'].as_map()['0'].f32() == 2.0 + assert sample_data['f64'].as_map()['0'].f64() == 1.283 + assert sample_data['bool'].as_map()['0'].bool() == false + assert sample_data['str'].as_map()['0'].str() == 'test' + assert is_null(sample_data['null'].as_map()['0']) == true + assert sample_data['arr'].as_map()['0'].str() == 'lol' + assert sample_data['obj'].as_map()['foo'].int() == 10 +} + +fn test_arr() { + assert sample_data['int'].arr()[0].int() == 1 + assert sample_data['i64'].arr()[0].i64() == 128.0 + assert sample_data['f32'].arr()[0].f32() == 2.0 + assert sample_data['f64'].arr()[0].f64() == 1.283 + assert sample_data['bool'].arr()[0].bool() == false + assert sample_data['str'].arr()[0].str() == 'test' + assert is_null(sample_data['null'].arr()[0]) == true + assert sample_data['arr'].arr()[0].str() == 'lol' + assert sample_data['obj'].arr()[0].int() == 10 +} + +fn test_bool() { + // valid conversions + assert sample_data['bool'].bool() == false + assert json2.Any('true').bool() == true + // invalid conversions + assert sample_data['int'].bool() == false + assert sample_data['i64'].bool() == false + assert sample_data['f32'].bool() == false + assert sample_data['f64'].bool() == false + assert sample_data['null'].bool() == false + assert sample_data['arr'].bool() == false + assert sample_data['obj'].bool() == false +} + +fn test_str() { + assert sample_data['int'].str() == '1' + assert sample_data['i64'].str() == '128' + assert sample_data['f32'].str() == '2.0' + assert sample_data['f64'].str() == '1.283' + assert sample_data['bool'].str() == 'false' + assert sample_data['str'].str() == 'test' + assert sample_data['null'].str() == 'null' + assert sample_data['arr'].str() == '["lol"]' + assert sample_data.str() == + '{"int":1,"i64":128,"f32":2.0,"f64":1.283,"bool":false,"str":"test","null":null,"arr":["lol"],"obj":{"foo":10}}' +} diff --git a/vlib/x/json2/builder.v b/vlib/x/json2/builder.v index 626c5e41fa..573e85a622 100644 --- a/vlib/x/json2/builder.v +++ b/vlib/x/json2/builder.v @@ -9,56 +9,69 @@ pub fn (mut obj map[string]Any) insert_str(key string, val string) { 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 index 938ba9eef1..bce944e792 100644 --- a/vlib/x/json2/decoder.v +++ b/vlib/x/json2/decoder.v @@ -12,32 +12,34 @@ import v.pref // `Any` is a sum type that lists the possible types to be decoded and used. pub type Any = string | int | i64 | f32 | 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 {} +pub struct Null { +} enum ParseMode { - array - bool - invalid - null - number - object - string + array + bool + invalid + null + number + object + string } const ( formfeed_err = 'formfeed not allowed.' - eof_err = 'reached eof. data not closed properly.' + 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 - mode ParseMode = .invalid - n_level int + scanner &scanner.Scanner + p_tok token.Token + tok token.Token + n_tok token.Token + mode ParseMode = .invalid + n_level int convert_type bool = true } @@ -79,7 +81,7 @@ fn new_parser(srce string, convert_type bool) Parser { } } return Parser{ - scanner: scanner.new_scanner(src, .parse_comments, &pref.Preferences{}), + scanner: scanner.new_scanner(src, .parse_comments, &pref.Preferences{}) convert_type: convert_type } } @@ -88,9 +90,10 @@ fn check_valid_hex(str string) ? { if str.len != 4 { return error('hex string must be 4 characters.') } - for l in str { - if l.is_hex_digit() { continue } + if l.is_hex_digit() { + continue + } return error('provided string is not a hex digit.') } } @@ -111,18 +114,16 @@ fn (mut p Parser) decode() ?Any { 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] == `'` + return src[prev_tok_pos] == `\'` } fn (mut p Parser) detect_parse_mode() { @@ -131,21 +132,28 @@ fn (mut p Parser) detect_parse_mode() { p.mode = .invalid return } - p.tok = p.scanner.scan() p.n_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 } + .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 @@ -164,11 +172,10 @@ fn (mut p Parser) decode_value() ?Any { if p.n_level == 500 { return error('reached maximum nesting level of 500.') } - - if (p.tok.kind == .lsbr && p.n_tok.kind == .lcbr) || (p.p_tok.kind == p.tok.kind && p.tok.kind == .lsbr) { + if (p.tok.kind == .lsbr && p.n_tok.kind == .lcbr) || + (p.p_tok.kind == p.tok.kind && p.tok.kind == .lsbr) { p.n_level++ } - match p.tok.kind { .lsbr { return p.decode_array() @@ -181,59 +188,64 @@ fn (mut p Parser) decode_value() ?Any { } .key_true { p.next() - return if p.convert_type { Any(true) } else { Any('true') } + return if p.convert_type { + Any(true) + } else { + Any('true') + } } .key_false { p.next() - return if p.convert_type { Any(false) } else { Any('false') } + return if p.convert_type { + Any(false) + } else { + Any('false') + } } .name { if p.tok.lit != 'null' { return error('unknown identifier `$p.tok.lit`') } - p.next() - return if p.convert_type { Any(Null{}) } else { Any('null') } + return if p.convert_type { + Any(Null{}) + } else { + Any('null') + } } .string { if p.is_singlequote() { return error('strings must be in double-quotes.') } - return p.decode_string() } else { - if p.tok.kind == .minus && p.n_tok.kind == .number && p.n_tok.pos == p.tok.pos+1 { + 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()? + d_num := p.decode_number() ? return d_num } - - return error('unknown token \'$p.tok.lit\' when decoding value') + return error("unknown token '$p.tok.lit' when decoding value") } } - if p.is_formfeed() { return error(formfeed_err) } - return Any{} } fn (mut p Parser) decode_string() ?Any { mut strwr := strings.new_builder(200) for i := 0; i < p.tok.lit.len; i++ { - if ((i-1 >= 0 && p.tok.lit[i-1] != `/`) || i == 0) && int(p.tok.lit[i]) in [9, 10, 0] { + 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 { + 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] - match peek{ + if i + 1 < p.tok.lit.len && p.tok.lit[i] == 92 { + peek := p.tok.lit[i + 1] + match peek { `b` { i++ strwr.write_b(`\b`) @@ -260,9 +272,9 @@ fn (mut p Parser) decode_string() ?Any { continue } `u` { - if i+5 < p.tok.lit.len { - codepoint := p.tok.lit[i+2..i+6] - check_valid_hex(codepoint)? + if i + 5 < p.tok.lit.len { + codepoint := p.tok.lit[i + 2..i + 6] + check_valid_hex(codepoint) ? hex_val := strconv.parse_int(codepoint, 16, 0) strwr.write_b(byte(hex_val)) i += 5 @@ -288,16 +300,13 @@ fn (mut p Parser) decode_string() ?Any { } else { return error('invalid backslash escape.') } } - if int(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]) } p.next() @@ -314,23 +323,18 @@ fn (mut p Parser) decode_number() ?Any { 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] @@ -341,11 +345,9 @@ fn (mut p Parser) decode_number() ?Any { 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 { + if p.p_tok.kind == .minus && p.tok.pos == p.p_tok.pos + 1 { tl = '-$tl' } - p.next() if p.convert_type { return if is_fl { @@ -354,7 +356,6 @@ fn (mut p Parser) decode_number() ?Any { Any(tl.i64()) } } - return Any(tl) } @@ -365,62 +366,51 @@ fn (mut p Parser) decode_array() ?Any { if p.tok.kind == .eof { return error(eof_err) } - - item := p.decode_value()? + item := p.decode_value() ? 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 error("unknown token '$p.tok.lit' when decoding arrays.") } p.next() return Any(items) } fn (mut p Parser) decode_object() ?Any { - mut fields := map[string]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) + // 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\'') + return error("invalid token `$p.tok.lit`, expected \'string\'") } - cur_key = p.tok.lit p.next() p.next() - - fields[cur_key] = p.decode_value()? + fields[cur_key] = p.decode_value() ? if p.tok.kind == .comma && p.n_tok.kind !in [.rcbr, .comma] { p.next() continue } else if p.tok.kind == .rcbr { break } - - return error('unknown token \'$p.tok.lit\' when decoding object.') + return error("unknown token '$p.tok.lit' when decoding object.") } p.next() return Any(fields) diff --git a/vlib/x/json2/decoder_test.v b/vlib/x/json2/decoder_test.v new file mode 100644 index 0000000000..5a613edfff --- /dev/null +++ b/vlib/x/json2/decoder_test.v @@ -0,0 +1,61 @@ +import x.json2 + +fn test_raw_decode_string() { + str := json2.raw_decode('"Hello!"') or { + assert false + json2.Any{} + } + assert str.str() == 'Hello!' +} + +fn test_raw_decode_number() { + num := json2.raw_decode('123') or { + assert false + json2.Any{} + } + assert num.int() == 123 +} + +fn test_raw_decode_array() { + raw_arr := json2.raw_decode('["Foo", 1]') or { + assert false + json2.Any{} + } + arr := raw_arr.arr() + assert arr[0].str() == 'Foo' + assert arr[1].int() == 1 +} + +fn test_raw_decode_bool() { + bol := json2.raw_decode('false') or { + assert false + json2.Any{} + } + assert bol.bool() == false +} + +fn test_raw_decode_map() { + raw_mp := json2.raw_decode('{"name":"Bob","age":20}') or { + assert false + json2.Any{} + } + mp := raw_mp.as_map() + assert mp['name'].str() == 'Bob' + assert mp['age'].int() == 20 +} + +fn test_raw_decode_null() { + nul := json2.raw_decode('null') or { + assert false + json2.Any{} + } + assert nul is json2.Null +} + +fn test_raw_decode_invalid() { + json2.raw_decode('1z') or { + assert err == '[json] invalid JSON. (0:0)' + return + } + assert false +} diff --git a/vlib/x/json2/encoder.v b/vlib/x/json2/encoder.v index 6fe39b2f07..8ffc047c49 100644 --- a/vlib/x/json2/encoder.v +++ b/vlib/x/json2/encoder.v @@ -12,11 +12,13 @@ fn write_value(v Any, i int, len int, mut wr strings.Builder) { } else { wr.write(str) } - if i >= len-1 { return } + if i >= len - 1 { + return + } wr.write_b(`,`) } -// String representation of the `map[string]Any`. +// str returns the string representation of the `map[string]Any`. pub fn (flds map[string]Any) str() string { mut wr := strings.new_builder(200) wr.write_b(`{`) @@ -34,7 +36,7 @@ pub fn (flds map[string]Any) str() string { return res } -// String representation of the `[]Any`. +// str returns the string representation of the `[]Any`. pub fn (flds []Any) str() string { mut wr := strings.new_builder(200) wr.write_b(`[`) @@ -49,19 +51,49 @@ pub fn (flds []Any) str() string { return res } -// String representation of the `Any` type. +// str returns the string representation of the `Any` type. pub fn (f Any) str() string { match f { - string { return f } - int { return f.str() } - i64 { return f.str() } - f32 { 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' } + string { + return f + } + int { + return f.str() + } + i64 { + return f.str() + } + f32 { + str_f32 := f.str() + return if str_f32.ends_with('.') { + str_f32 + '0' + } else { + str_f32 + } + } + f64 { + str_f64 := f.str() + return if str_f64.ends_with('.') { + str_f64 + '0' + } else { + str_f64 + } + } + 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 { return f.str() diff --git a/vlib/x/json2/json2.v b/vlib/x/json2/json2.v index cc95f24695..abb58d6f8a 100644 --- a/vlib/x/json2/json2.v +++ b/vlib/x/json2/json2.v @@ -3,6 +3,10 @@ // that can be found in the LICENSE file. module json2 +pub const ( + null = Null{} +) + pub interface Serializable { from_json(f Any) to_json() string @@ -19,110 +23,73 @@ pub fn fast_raw_decode(src string) ?Any { mut p := new_parser(src, false) return p.decode() } -// 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) - } + +// decode is a generic function that decodes a JSON string into the target type. +pub fn decode(src string) ?T { + res := raw_decode(src) ? mut typ := T{} typ.from_json(res) return typ } -// TODO: decode must return an optional generics -// pub fn decode2(src string) ?T { -// res := raw_decode(src)? -// match typeof(T) { -// 'string' { -// return res.str() -// } -// 'int' { -// return res.int() -// } -// 'f64' { -// return res.f64() -// } -// else { -// mut typ := T{} -// typ.from_json(res) -// return typ -// } -// } -// } -// A generic function that encodes a type into a JSON string. +// encode is a generic function that encodes a type into a JSON string. pub fn encode(typ T) string { - // if typeof(typ) in ['string', 'int', 'f64'] { - // return Any(typ).str() - // } - 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. + +// as_map uses `Any` as a map. pub fn (f Any) as_map() map[string]Any { if f is map[string]Any { return f } else if f is []Any { - mut mp := map[string]Any + mut mp := map[string]Any{} for i, fi in f { mp['$i'] = fi } return mp } - return { '0': f } + return { + '0': f + } } -// Use `Any` as an integer. +// int uses `Any` as an integer. pub fn (f Any) int() int { match f { - int { return f } - i64 { return int(f) } - f64 { return f.str().int() } - f32 { return f.str().int() } - bool { return int(f) } + int { return f } + i64, f32, f64, bool { return int(f) } else { return 0 } } } -// Use `Any` as a 64-bit integer. +// i64 uses `Any` as a 64-bit integer. pub fn (f Any) i64() i64 { match f { - int { return f } - i64 { return int(f) } - f64 { return f.str().i64() } - f32 { return f.str().i64() } - bool { return int(f) } + i64 { return f } + int, f32, f64, bool { return i64(f) } else { return 0 } } } -// Use `Any` as a 32-bit float. +// f32 uses `Any` as a 32-bit float. pub fn (f Any) f32() f32 { match f { - int { return f } - i64 { return f.str().f32() } - f64 { return f.str().f32() } f32 { return f } + int, i64, f64 { return f32(f) } else { return 0.0 } } } -// Use `Any` as a float. +// f64 uses `Any` as a float. pub fn (f Any) f64() f64 { match f { - int { return f } - i64 { return f } f64 { return f } - f32 { return f.str().f64() } + int, i64, f32 { return f64(f) } else { return 0.0 } } } -// Use `Any` as an array. + +// arr uses `Any` as an array. pub fn (f Any) arr() []Any { if f is []Any { return f @@ -136,7 +103,7 @@ pub fn (f Any) arr() []Any { return [f] } -// Use `Any` as a bool +// bool uses `Any` as a bool pub fn (f Any) bool() bool { match f { bool { return f } diff --git a/vlib/x/json2/json2_test.v b/vlib/x/json2/json2_test.v index defd544cad..9bdc5d176f 100644 --- a/vlib/x/json2/json2_test.v +++ b/vlib/x/json2/json2_test.v @@ -7,6 +7,7 @@ enum JobTitle { } struct Employee { +pub mut: name string age int salary f32 @@ -14,12 +15,11 @@ struct Employee { } fn (e Employee) to_json() string { - mut mp := map[string]json2.Any + mut mp := map[string]json2.Any{} mp['name'] = e.name mp['age'] = e.age mp['salary'] = e.salary mp['title'] = int(e.title) - /* $for field in Employee.fields { d := e.$(field.name) @@ -31,31 +31,31 @@ fn (e Employee) to_json() string { } } */ - return mp.str() } +fn (mut e Employee) from_json(any json2.Any) { + mp := any.as_map() + e.name = mp['name'].str() + e.age = mp['age'].int() + e.salary = mp['salary'].f32() + e.title = JobTitle(mp['title'].int()) +} + 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 { + y := json2.decode(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 + assert y.name == 'Peter' + assert y.age == 28 + assert y.salary == 95000.5 + assert y.title == .worker } fn test_fast_raw_decode() { @@ -65,17 +65,16 @@ fn test_fast_raw_decode() { json2.Any{} } str := o.str() - assert str == '{"name":"Peter","age":"28","salary":"95000.5","title":"2"}' } fn test_character_unescape() { // Need to test `\r`, `\b`, `\f` ?? message := '{ - "newline":"new\\nline", - "tab":"\\ttab", - "backslash": "back\\\\slash", - "quotes": "\\"quotes\\"", + "newline":"new\\nline", + "tab":"\\ttab", + "backslash": "back\\\\slash", + "quotes": "\\"quotes\\"", "slash":"\/dev\/null" }' mut obj := json2.raw_decode(message) or { @@ -83,7 +82,7 @@ fn test_character_unescape() { json2.Any{} } lines := obj.as_map() - eprintln("$lines") + eprintln('$lines') assert lines['newline'].str() == 'new\nline' assert lines['tab'].str() == '\ttab' assert lines['backslash'].str() == 'back\\slash' @@ -91,13 +90,34 @@ fn test_character_unescape() { assert lines['slash'].str() == '/dev/null' } -/* struct User2 { +pub mut: age int nums []int } +fn (mut u User2) from_json(an json2.Any) { + mp := an.as_map() + mut js_field_name := '' + $for field in User.fields { + js_field_name = field.name + for attr in field.attrs { + if attr.starts_with('json:') { + js_field_name = attr.all_after('json:').trim_left(' ') + break + } + } + match field.name { + 'age' { u.age = mp[js_field_name].int() } + 'nums' { u.nums = mp[js_field_name].arr().map(it.int()) } + else {} + } + } +} + +// User struct needs to be `pub mut` for now in order to access and manipulate values struct User { +pub mut: age int nums []int last_name string [json: lastName] @@ -106,16 +126,53 @@ struct User { pets string [raw; json: 'pet_animals'] } +fn (mut u User) from_json(an json2.Any) { + mp := an.as_map() + mut js_field_name := '' + $for field in User.fields { + // FIXME: C error when initializing js_field_name inside comptime for + js_field_name = field.name + for attr in field.attrs { + if attr.starts_with('json:') { + js_field_name = attr.all_after('json:').trim_left(' ') + break + } + } + match field.name { + 'age' { u.age = mp[js_field_name].int() } + 'nums' { u.nums = mp[js_field_name].arr().map(it.int()) } + 'last_name' { u.last_name = mp[js_field_name].str() } + 'is_registered' { u.is_registered = mp[js_field_name].bool() } + 'typ' { u.typ = mp[js_field_name].int() } + 'pets' { u.pets = mp[js_field_name].str() } + else {} + } + } +} + +fn (u User) to_json() string { + // TODO: derive from field + mut mp := { + 'age': json2.Any(u.age) + } + mp['nums'] = u.nums.map(json2.Any(it)) + mp['lastName'] = u.last_name + mp['IsRegistered'] = u.is_registered + mp['type'] = u.typ + mp['pet_animals'] = u.pets + return mp.str() +} + 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) + u2 := json2.decode(s) or { + assert false + User2{} } - println(u2) - u := json.decode(User, s) or { - exit(1) + u := json2.decode(s) or { + assert false + User{} } - println(u) assert u.age == 10 assert u.last_name == 'Johnson' assert u.is_registered == true @@ -137,25 +194,37 @@ fn test_encode_user() { pets: 'foo' } expected := '{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"foo"}' - out := json.encode(usr) - println(out) + out := json2.encode(usr) assert out == expected } struct Color { +pub mut: space string point string [raw] } +fn (mut c Color) from_json(an json2.Any) { + mp := an.as_map() + $for field in Color.fields { + match field.name { + 'space' { c.space = mp[field.name].str() } + 'point' { c.point = mp[field.name].str() } + else {} + } + } +} + fn test_raw_json_field() { - color := json.decode(Color, '{"space": "YCbCr", "point": {"Y": 123}}') or { - println('text') - return + color := json2.decode('{"space": "YCbCr", "point": {"Y": 123}}') or { + assert false + Color{} } assert color.point == '{"Y":123}' assert color.space == 'YCbCr' } +/* struct City { name string } @@ -177,7 +246,6 @@ fn test_struct_in_struct() { println(country.cities) } */ - fn test_encode_map() { expected := '{"one":1,"two":2,"three":3,"four":4}' numbers := { @@ -187,9 +255,6 @@ fn test_encode_map() { 'four': json2.Any(4) } out := numbers.str() - // out := json.encode(numbers) - - println(out) assert out == expected } /*