From 979066856b7bff543a7a6901e5ae94c372160f73 Mon Sep 17 00:00:00 2001 From: Hitalo Souza <63821277+enghitalo@users.noreply.github.com> Date: Fri, 24 Mar 2023 08:45:26 -0300 Subject: [PATCH] json2: encode map (#16928) --- vlib/x/json2/encode_struct_test.v | 21 ++ vlib/x/json2/encoder.v | 32 ++- vlib/x/json2/json2.v | 202 +++++++++--------- .../json_test.v | 57 ++++- .../json_todo_test.vv | 25 +++ 5 files changed, 232 insertions(+), 105 deletions(-) diff --git a/vlib/x/json2/encode_struct_test.v b/vlib/x/json2/encode_struct_test.v index 3aaabcdd02..bcc942a6f9 100644 --- a/vlib/x/json2/encode_struct_test.v +++ b/vlib/x/json2/encode_struct_test.v @@ -252,3 +252,24 @@ fn test_sumtypes() { } }) == '{"val":{"val":1}}' } + +fn test_maps() { + assert json.encode(StructType[map[string]map[string]int]{}) == '{"val":{}}' + assert json.encode(StructType[map[string]string]{ + val: { + '1': '1' + } + }) == '{"val":{"1":"1"}}' + assert json.encode(StructType[map[string]int]{ + val: { + '1': 1 + } + }) == '{"val":{"1":1}}' + // assert json.encode(StructType[map[string]map[string]int]{ + // val: { + // 'a': { + // '1': 1 + // } + // } + // }) == '{"val":{"a":{"1":1}}}' +} diff --git a/vlib/x/json2/encoder.v b/vlib/x/json2/encoder.v index 2f4a7f99b5..94356df97c 100644 --- a/vlib/x/json2/encoder.v +++ b/vlib/x/json2/encoder.v @@ -39,6 +39,10 @@ const quote_bytes = [u8(`"`)] const escaped_chars = [(r'\b').bytes(), (r'\f').bytes(), (r'\n').bytes(), (r'\r').bytes(), (r'\t').bytes()] +const curly_open = [u8(`{`)] + +const curly_close = [u8(`}`)] + // encode_value encodes a value to the specific writer. pub fn (e &Encoder) encode_value[T](val T, mut wr io.Writer) ! { e.encode_value_with_level[T](val, 1, mut wr)! @@ -83,7 +87,7 @@ fn (e &Encoder) encode_any(val Any, level int, mut wr io.Writer) ! { wr.write(json2.zero_in_bytes)! } map[string]Any { - wr.write([u8(`{`)])! + wr.write(json2.curly_open)! mut i := 0 for k, v in val { e.encode_newline(level, mut wr)! @@ -99,7 +103,7 @@ fn (e &Encoder) encode_any(val Any, level int, mut wr io.Writer) ! { i++ } e.encode_newline(level - 1, mut wr)! - wr.write([u8(`}`)])! + wr.write(json2.curly_close)! } []Any { wr.write([u8(`[`)])! @@ -128,6 +132,8 @@ fn (e &Encoder) encode_value_with_level[T](val T, level int, mut wr io.Writer) ! } $else $if T is map[string]Any { // weird quirk but val is destructured immediately to Any e.encode_any(val, level, mut wr)! + } $else $if T is $map { + // FIXME - `e.encode_struct` can not encode `map[string]map[string]int` type } $else $if T is []Any { e.encode_any(val, level, mut wr)! } $else $if T is Encodable { @@ -145,7 +151,7 @@ fn (e &Encoder) encode_value_with_level[T](val T, level int, mut wr io.Writer) ! } fn (e &Encoder) encode_struct[U](val U, level int, mut wr io.Writer) ! { - wr.write([u8(`{`)])! + wr.write(json2.curly_open)! mut i := 0 mut fields_len := 0 $for field in U.fields { @@ -254,6 +260,24 @@ fn (e &Encoder) encode_struct[U](val U, level int, mut wr io.Writer) ! { // e.encode_array(value, level + 1, mut wr)! // FIXME - error: could not infer generic type `U` in call to `encode_array` } $else $if field.typ is $struct { e.encode_struct(value, level + 1, mut wr)! + } $else $if field.is_map { + wr.write(json2.curly_open)! + mut idx := 0 + for k, v in value { + e.encode_newline(level, mut wr)! + e.encode_string(k.str(), mut wr)! + wr.write(json2.colon_bytes)! + if e.newline != 0 { + wr.write(json2.space_bytes)! + } + e.encode_value_with_level(v, level + 1, mut wr)! + if idx < value.len - 1 { + wr.write(json2.comma_bytes)! + } + idx++ + } + e.encode_newline(level, mut wr)! + wr.write(json2.curly_close)! } $else $if field.is_enum { // TODO - replace for `field.typ is $enum` wr.write(int(val.$(field.name)).str().bytes())! @@ -356,7 +380,7 @@ fn (e &Encoder) encode_struct[U](val U, level int, mut wr io.Writer) ! { } } e.encode_newline(level - 1, mut wr)! - wr.write([u8(`}`)])! + wr.write(json2.curly_close)! } fn (e &Encoder) encode_array[U](val []U, level int, mut wr io.Writer) ! { diff --git a/vlib/x/json2/json2.v b/vlib/x/json2/json2.v index 5c0f5a37f0..9ebdeead94 100644 --- a/vlib/x/json2/json2.v +++ b/vlib/x/json2/json2.v @@ -20,115 +20,119 @@ pub fn fast_raw_decode(src string) !Any { // decode is a generic function that decodes a JSON string into the target type. pub fn decode[T](src string) !T { - res := raw_decode(src)!.as_map() mut typ := T{} - $for field in T.fields { - mut json_name := field.name - for attr in field.attrs { - if attr.contains('json: ') { - json_name = attr.replace('json: ', '') - break + $if T is $struct { + res := raw_decode(src)!.as_map() + $for field in T.fields { + mut json_name := field.name + for attr in field.attrs { + if attr.contains('json: ') { + json_name = attr.replace('json: ', '') + break + } } - } - $if field.is_enum { - typ.$(field.name) = if key := res[field.name] { - key.int() - } else { - res[json_name]!.int() - } - } $else $if field.typ is u8 { - typ.$(field.name) = res[json_name]!.u64() - } $else $if field.typ is u16 { - typ.$(field.name) = res[json_name]!.u64() - } $else $if field.typ is u32 { - typ.$(field.name) = res[json_name]!.u64() - } $else $if field.typ is u64 { - typ.$(field.name) = res[json_name]!.u64() - } $else $if field.typ is int { - typ.$(field.name) = res[json_name]!.int() - } $else $if field.typ is i8 { - typ.$(field.name) = res[json_name]!.int() - } $else $if field.typ is i16 { - typ.$(field.name) = res[json_name]!.int() - } $else $if field.typ is i32 { - typ.$(field.name) = i32(res[field.name]!.int()) - } $else $if field.typ is i64 { - typ.$(field.name) = res[json_name]!.i64() - } $else $if field.typ is ?u8 { - if json_name in res { - typ.$(field.name) = ?u8(res[json_name]!.i64()) - } - } $else $if field.typ is ?i8 { - if json_name in res { - typ.$(field.name) = ?i8(res[json_name]!.i64()) - } - } $else $if field.typ is ?u16 { - if json_name in res { - typ.$(field.name) = ?u16(res[json_name]!.i64()) - } - } $else $if field.typ is ?i16 { - if json_name in res { - typ.$(field.name) = ?i16(res[json_name]!.i64()) - } - } $else $if field.typ is ?u32 { - if json_name in res { - typ.$(field.name) = ?u32(res[json_name]!.i64()) - } - } $else $if field.typ is ?i32 { - if json_name in res { - typ.$(field.name) = ?i32(res[json_name]!.i64()) - } - } $else $if field.typ is ?u64 { - if json_name in res { - typ.$(field.name) = ?u64(res[json_name]!.i64()) - } - } $else $if field.typ is ?i64 { - if json_name in res { - typ.$(field.name) = ?i64(res[json_name]!.i64()) - } - } $else $if field.typ is ?int { - if json_name in res { - typ.$(field.name) = ?int(res[json_name]!.i64()) - } - } $else $if field.typ is f32 { - typ.$(field.name) = res[json_name]!.f32() - } $else $if field.typ is ?f32 { - if json_name in res { + $if field.is_enum { + typ.$(field.name) = if key := res[field.name] { + key.int() + } else { + res[json_name]!.int() + } + } $else $if field.typ is u8 { + typ.$(field.name) = res[json_name]!.u64() + } $else $if field.typ is u16 { + typ.$(field.name) = res[json_name]!.u64() + } $else $if field.typ is u32 { + typ.$(field.name) = res[json_name]!.u64() + } $else $if field.typ is u64 { + typ.$(field.name) = res[json_name]!.u64() + } $else $if field.typ is int { + typ.$(field.name) = res[json_name]!.int() + } $else $if field.typ is i8 { + typ.$(field.name) = res[json_name]!.int() + } $else $if field.typ is i16 { + typ.$(field.name) = res[json_name]!.int() + } $else $if field.typ is i32 { + typ.$(field.name) = i32(res[field.name]!.int()) + } $else $if field.typ is i64 { + typ.$(field.name) = res[json_name]!.i64() + } $else $if field.typ is ?u8 { + if json_name in res { + typ.$(field.name) = ?u8(res[json_name]!.i64()) + } + } $else $if field.typ is ?i8 { + if json_name in res { + typ.$(field.name) = ?i8(res[json_name]!.i64()) + } + } $else $if field.typ is ?u16 { + if json_name in res { + typ.$(field.name) = ?u16(res[json_name]!.i64()) + } + } $else $if field.typ is ?i16 { + if json_name in res { + typ.$(field.name) = ?i16(res[json_name]!.i64()) + } + } $else $if field.typ is ?u32 { + if json_name in res { + typ.$(field.name) = ?u32(res[json_name]!.i64()) + } + } $else $if field.typ is ?i32 { + if json_name in res { + typ.$(field.name) = ?i32(res[json_name]!.i64()) + } + } $else $if field.typ is ?u64 { + if json_name in res { + typ.$(field.name) = ?u64(res[json_name]!.i64()) + } + } $else $if field.typ is ?i64 { + if json_name in res { + typ.$(field.name) = ?i64(res[json_name]!.i64()) + } + } $else $if field.typ is ?int { + if json_name in res { + typ.$(field.name) = ?int(res[json_name]!.i64()) + } + } $else $if field.typ is f32 { typ.$(field.name) = res[json_name]!.f32() - } - } $else $if field.typ is f64 { - typ.$(field.name) = res[json_name]!.f64() - } $else $if field.typ is ?f64 { - if json_name in res { + } $else $if field.typ is ?f32 { + if json_name in res { + typ.$(field.name) = res[json_name]!.f32() + } + } $else $if field.typ is f64 { typ.$(field.name) = res[json_name]!.f64() - } - } $else $if field.typ is bool { - typ.$(field.name) = res[json_name]!.bool() - } $else $if field.typ is ?bool { - if json_name in res { + } $else $if field.typ is ?f64 { + if json_name in res { + typ.$(field.name) = res[json_name]!.f64() + } + } $else $if field.typ is bool { typ.$(field.name) = res[json_name]!.bool() - } - } $else $if field.typ is string { - typ.$(field.name) = res[json_name]!.str() - } $else $if field.typ is ?string { - if json_name in res { + } $else $if field.typ is ?bool { + if json_name in res { + typ.$(field.name) = res[json_name]!.bool() + } + } $else $if field.typ is string { typ.$(field.name) = res[json_name]!.str() - } - } $else $if field.typ is time.Time { - typ.$(field.name) = res[field.name]!.to_time()! - } $else $if field.typ is ?time.Time { - if json_name in res { + } $else $if field.typ is ?string { + if json_name in res { + typ.$(field.name) = res[json_name]!.str() + } + } $else $if field.typ is time.Time { typ.$(field.name) = res[field.name]!.to_time()! + } $else $if field.typ is ?time.Time { + if json_name in res { + typ.$(field.name) = res[field.name]!.to_time()! + } + } $else $if field.is_array { + // typ.$(field.name) = res[field.name]!.arr() + } $else $if field.is_struct { + } $else $if field.is_alias { + } $else $if field.is_map { + } $else { + return error("The type of `${field.name}` can't be decoded. Please open an issue at https://github.com/vlang/v/issues/new/choose") } - } $else $if field.is_array { - // typ.$(field.name) = res[field.name]!.arr() - } $else $if field.is_struct { - } $else $if field.is_alias { - } $else $if field.is_map { - } $else { - return error("The type of `${field.name}` can't be decoded. Please open an issue at https://github.com/vlang/v/issues/new/choose") } + } $else $if T is $map { + return error('Decode map is not allowed for now') } return typ } diff --git a/vlib/x/json2/json_module_compatibility_test/json_test.v b/vlib/x/json2/json_module_compatibility_test/json_test.v index 48ea1f6b92..7a455afdf4 100644 --- a/vlib/x/json2/json_module_compatibility_test/json_test.v +++ b/vlib/x/json2/json_module_compatibility_test/json_test.v @@ -61,8 +61,8 @@ mut: reg_date time.Time } -// // User struct needs to be `pub mut` for now in order to access and manipulate values -struct User { +// User struct needs to be `pub mut` for now in order to access and manipulate values +pub struct User { pub mut: age int nums []int @@ -264,3 +264,56 @@ fn test_encode_alias_field() { }) assert s == '{"sub":{"a":1}}' } + +pub struct City { +mut: + name string +} + +pub struct Country { +mut: + cities []City + name string +} + +struct Data { +mut: + countries []Country + users map[string]User +} + +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"}}}' + 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' + } + } + } + out := json.encode(data) + assert out == data_expected +} diff --git a/vlib/x/json2/json_module_compatibility_test/json_todo_test.vv b/vlib/x/json2/json_module_compatibility_test/json_todo_test.vv index 75896efe7a..8fd258222a 100644 --- a/vlib/x/json2/json_module_compatibility_test/json_todo_test.vv +++ b/vlib/x/json2/json_module_compatibility_test/json_todo_test.vv @@ -112,6 +112,31 @@ pub mut: pets string [json: 'pet_animals'; raw] } +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)! + u := json.decode[User](s)! + 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"}' +} + +//! BUGFIX - .from_json(res) +fn test_encode_decode_time() { + user := User2{ + age: 25 + reg_date: time.new_time(year: 2020, month: 12, day: 22, hour: 7, minute: 23) + } + s := json.encode(user) + assert s.contains('"reg_date":1608621780') + user2 := json.decode[User2](s)! + assert user2.reg_date.str() == '2020-12-22 07:23:00' struct City { name string }