From 74613bd6368df89ad3ac830ce5a4827f7f4205aa Mon Sep 17 00:00:00 2001 From: Hitalo Souza <63821277+enghitalo@users.noreply.github.com> Date: Fri, 18 Nov 2022 06:09:24 -0300 Subject: [PATCH] x.json2: generic-based encoder (finish PR#15137) (#16464) --- vlib/v/tests/valgrind/import_x_json2.v | 2 +- vlib/x/json2/decoder.v | 12 ++- vlib/x/json2/encoder.v | 133 +++++++++++++++++++++---- vlib/x/json2/encoder_test.v | 41 +++++++- vlib/x/json2/json2.v | 21 +++- vlib/x/json2/json2_test.v | 55 +++++----- 6 files changed, 210 insertions(+), 54 deletions(-) diff --git a/vlib/v/tests/valgrind/import_x_json2.v b/vlib/v/tests/valgrind/import_x_json2.v index 036ad8e75c..90b93d5afb 100644 --- a/vlib/v/tests/valgrind/import_x_json2.v +++ b/vlib/v/tests/valgrind/import_x_json2.v @@ -2,5 +2,5 @@ import x.json2 fn main() { x := '[[],[],[]]' - println(json2.raw_decode(x)?) + println(json2.raw_decode(x)!) } diff --git a/vlib/x/json2/decoder.v b/vlib/x/json2/decoder.v index 8570d58688..afa307c9f7 100644 --- a/vlib/x/json2/decoder.v +++ b/vlib/x/json2/decoder.v @@ -4,7 +4,17 @@ module json2 // `Any` is a sum type that lists the possible types to be decoded and used. -pub type Any = Null | []Any | bool | f32 | f64 | i64 | int | map[string]Any | string | u64 +pub type Any = Null + | []Any + | []int + | bool + | f32 + | f64 + | i64 + | int + | map[string]Any + | string + | u64 // `Null` struct is a simple representation of the `null` value in JSON. pub struct Null { diff --git a/vlib/x/json2/encoder.v b/vlib/x/json2/encoder.v index 1b902f4ebb..d9481a56cc 100644 --- a/vlib/x/json2/encoder.v +++ b/vlib/x/json2/encoder.v @@ -14,6 +14,8 @@ pub struct Encoder { escape_unicode bool = true } +pub const default_encoder = Encoder{} + // byte array versions of the most common tokens/chars // to avoid reallocations const null_in_bytes = 'null'.bytes() @@ -37,9 +39,9 @@ const quote_bytes = [u8(`"`)] const escaped_chars = [(r'\b').bytes(), (r'\f').bytes(), (r'\n').bytes(), (r'\r').bytes(), (r'\t').bytes()] -// encode_value encodes an `Any` value to the specific writer. -pub fn (e &Encoder) encode_value(f Any, mut wr io.Writer) ! { - e.encode_value_with_level(f, 1, mut wr)! +// encode_value encodes a value to the specific writer. +pub fn (e &Encoder) encode_value(val T, mut wr io.Writer) ! { + e.encode_value_with_level(val, 1, mut wr)! } fn (e &Encoder) encode_newline(level int, mut wr io.Writer) ! { @@ -51,24 +53,24 @@ fn (e &Encoder) encode_newline(level int, mut wr io.Writer) ! { } } -fn (e &Encoder) encode_value_with_level(f Any, level int, mut wr io.Writer) ! { - match f { +fn (e &Encoder) encode_any(val Any, level int, mut wr io.Writer) ! { + match val { string { - e.encode_string(f, mut wr)! + e.encode_string(val, mut wr)! } bool { - if f == true { + if val == true { wr.write(json2.true_in_bytes)! } else { wr.write(json2.false_in_bytes)! } } int, u64, i64 { - wr.write(f.str().bytes())! + wr.write(val.str().bytes())! } f32, f64 { $if !nofloat ? { - str_float := f.str().bytes() + str_float := val.str().bytes() wr.write(str_float)! if str_float[str_float.len - 1] == `.` { wr.write(json2.zero_in_bytes)! @@ -80,7 +82,7 @@ fn (e &Encoder) encode_value_with_level(f Any, level int, mut wr io.Writer) ! { map[string]Any { wr.write([u8(`{`)])! mut i := 0 - for k, v in f { + for k, v in val { e.encode_newline(level, mut wr)! e.encode_string(k, mut wr)! wr.write(json2.colon_bytes)! @@ -88,7 +90,7 @@ fn (e &Encoder) encode_value_with_level(f Any, level int, mut wr io.Writer) ! { wr.write(json2.space_bytes)! } e.encode_value_with_level(v, level + 1, mut wr)! - if i < f.len - 1 { + if i < val.len - 1 { wr.write(json2.comma_bytes)! } i++ @@ -98,13 +100,27 @@ fn (e &Encoder) encode_value_with_level(f Any, level int, mut wr io.Writer) ! { } []Any { wr.write([u8(`[`)])! - for i, v in f { + for i in 0 .. val.len { e.encode_newline(level, mut wr)! - e.encode_value_with_level(v, level + 1, mut wr)! - if i < f.len - 1 { + e.encode_value_with_level(val[i], level + 1, mut wr)! + if i < val.len - 1 { wr.write(json2.comma_bytes)! } } + + e.encode_newline(level - 1, mut wr)! + wr.write([u8(`]`)])! + } + []int { + wr.write([u8(`[`)])! + for i in 0 .. val.len { + e.encode_newline(level, mut wr)! + e.encode_value_with_level(val[i], level + 1, mut wr)! + if i < val.len - 1 { + wr.write(json2.comma_bytes)! + } + } + e.encode_newline(level - 1, mut wr)! wr.write([u8(`]`)])! } @@ -114,6 +130,85 @@ fn (e &Encoder) encode_value_with_level(f Any, level int, mut wr io.Writer) ! { } } +fn (e &Encoder) encode_value_with_level(val T, level int, mut wr io.Writer) ! { + $if T is string { + e.encode_string(val, mut wr)! + } $else $if T is Any { + e.encode_any(val, level, mut wr)! + } $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 []Any { + e.encode_any(val, level, mut wr)! + } $else $if T is Null || T is bool || T is f32 || T is f64 || T is i64 || T is int || T is u64 { + e.encode_any(val, level, mut wr)! + } $else $if T is Encodable { + wr.write(val.json_str().bytes())! + } $else $if T is []int { + // wr.write(val.str)! + e.encode_any(val, level, mut wr)! + } $else $if T is $Struct { + e.encode_struct(val, level, mut wr)! + } $else $if T is $Enum { + e.encode_any(Any(int(val)), level, mut wr)! + } $else { + return error('cannot encode value with ${typeof(val).name} type') + } +} + +fn (e &Encoder) encode_struct(val U, level int, mut wr io.Writer) ! { + wr.write([u8(`{`)])! + mut i := 0 + mut fields_len := 0 + $for _ in U.fields { + fields_len++ + } + $for field in U.fields { + mut json_name := '' + for attr in field.attrs { + if attr.contains('json: ') { + json_name = attr.replace('json: ', '') + break + } + } + e.encode_newline(level, mut wr)! + if json_name != '' { + e.encode_string(json_name, mut wr)! + } else { + e.encode_string(field.name, mut wr)! + } + wr.write(json2.colon_bytes)! + if e.newline != 0 { + wr.write(json2.space_bytes)! + } + field_value := val.$(field.name) + e.encode_value_with_level(field_value, level + 1, mut wr)! + if i < fields_len - 1 { + wr.write(json2.comma_bytes)! + } + i++ + } + e.encode_newline(level - 1, mut wr)! + wr.write([u8(`}`)])! +} + +fn (e &Encoder) encode_array(val U, level int, mut wr io.Writer) ! { + $if U is $Array { + wr.write([u8(`[`)])! + for i in 0 .. val.len { + e.encode_newline(level, mut wr)! + e.encode_value_with_level(&val[i], level + 1, mut wr)! + if i < val.len - 1 { + wr.write(json2.comma_bytes)! + } + } + e.encode_newline(level - 1, mut wr)! + wr.write([u8(`]`)])! + } $else { + return error('encoded array value is not an array') + } +} + // str returns the JSON string representation of the `map[string]Any` type. pub fn (f map[string]Any) str() string { return Any(f).json_str() @@ -137,13 +232,7 @@ pub fn (f Any) str() string { // json_str returns the JSON string representation of the `Any` type. [manualfree] pub fn (f Any) json_str() string { - mut sb := strings.new_builder(4096) - defer { - unsafe { sb.free() } - } - mut enc := Encoder{} - enc.encode_value(f, mut sb) or { return '' } - return sb.str() + return encode(f) } // prettify_json_str returns the pretty-formatted JSON string representation of the `Any` type. @@ -157,7 +246,7 @@ pub fn (f Any) prettify_json_str() string { newline: `\n` newline_spaces_count: 4 } - enc.encode_value(f, mut sb) or { return '' } + enc.encode_value(f, mut sb) or {} return sb.str() } diff --git a/vlib/x/json2/encoder_test.v b/vlib/x/json2/encoder_test.v index 4a8e6f7bfb..e2207746f5 100644 --- a/vlib/x/json2/encoder_test.v +++ b/vlib/x/json2/encoder_test.v @@ -38,19 +38,20 @@ fn test_json_string_non_ascii() { fn test_utf8_strings_are_not_modified() { original := '{"s":"Schilddrüsenerkrankungen"}' - // dump(original) deresult := json2.raw_decode(original)! - // dump(deresult) assert deresult.str() == original } -fn test_encoder_unescaped_utf32() { +fn test_encoder_unescaped_utf32() ! { jap_text := json2.Any('ひらがな') enc := json2.Encoder{ escape_unicode: false } mut sb := strings.new_builder(20) + defer { + unsafe { sb.free() } + } enc.encode_value(jap_text, mut sb)! assert sb.str() == '"${jap_text}"' @@ -74,6 +75,9 @@ fn test_encoder_prettify() { newline_spaces_count: 2 } mut sb := strings.new_builder(20) + defer { + unsafe { sb.free() } + } enc.encode_value(obj, mut sb)! assert sb.str() == '{ "hello": "world", @@ -88,3 +92,34 @@ fn test_encoder_prettify() { } }' } + +pub struct Test { + val string +} + +fn test_encode_struct() { + enc := json2.encode(Test{'hello!'}) + assert enc == '{"val":"hello!"}' +} + +pub struct Uri { + protocol string + path string +} + +pub fn (u Uri) json_str() string { + return '"${u.protocol}://${u.path}"' +} + +fn test_encode_encodable() { + assert json2.encode(Uri{'file', 'path/to/file'}) == '"file://path/to/file"' +} + +fn test_encode_array() { + assert json2.encode([1, 2, 3]) == '[1,2,3]' +} + +fn test_encode_simple() { + assert json2.encode('hello!') == '"hello!"' + assert json2.encode(1) == '1' +} diff --git a/vlib/x/json2/json2.v b/vlib/x/json2/json2.v index a87ba83d3d..e9112ad471 100644 --- a/vlib/x/json2/json2.v +++ b/vlib/x/json2/json2.v @@ -3,13 +3,18 @@ // that can be found in the LICENSE file. module json2 +import strings + pub const ( null = Null{} ) -pub interface Serializable { +pub interface Decodable { from_json(f Any) - to_json() string +} + +pub interface Encodable { + json_str() string } // Decodes a JSON string into an `Any` type. Returns an option. @@ -33,8 +38,16 @@ pub fn decode(src string) !T { } // encode is a generic function that encodes a type into a JSON string. -pub fn encode(typ T) string { - return typ.to_json() +pub fn encode(val T) string { + mut sb := strings.new_builder(64) + defer { + unsafe { sb.free() } + } + default_encoder.encode_value(val, mut sb) or { + dump(err) + default_encoder.encode_value(json2.null, mut sb) or {} + } + return sb.str() } // as_map uses `Any` as a map. diff --git a/vlib/x/json2/json2_test.v b/vlib/x/json2/json2_test.v index 35403cfbe9..1a56d81fe4 100644 --- a/vlib/x/json2/json2_test.v +++ b/vlib/x/json2/json2_test.v @@ -1,4 +1,5 @@ import x.json2 +import time enum JobTitle { manager @@ -45,14 +46,12 @@ fn (mut e Employee) from_json(any json2.Any) { 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 := json2.decode(s) or { println(err) assert false return } - eprintln('Employee y: ${y}') assert y.name == 'Peter' assert y.age == 28 assert y.salary == 95000.5 @@ -83,7 +82,6 @@ fn test_character_unescape() { return } lines := obj.as_map() - eprintln('${lines}') assert lines['newline'] or { 0 }.str() == 'new\nline' assert lines['tab'] or { 0 }.str() == '\ttab' assert lines['backslash'] or { 0 }.str() == 'back\\slash' @@ -91,12 +89,6 @@ fn test_character_unescape() { assert lines['slash'] or { 0 }.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 := '' @@ -116,6 +108,13 @@ fn (mut u User2) from_json(an json2.Any) { } } +struct User2 { +mut: + age int + nums []int + reg_date time.Time +} + // User struct needs to be `pub mut` for now in order to access and manipulate values struct User { pub mut: @@ -166,17 +165,8 @@ fn (u User) to_json() string { fn test_parse_user() { s := '{"age": 10, "nums": [1,2,3], "type": 1, "lastName": "Johnson", "IsRegistered": true, "pet_animals": {"name": "Bob", "animal": "Dog"}}' - u2 := json2.decode(s) or { - println(err) - assert false - return - } - println(u2) - u := json2.decode(s) or { - println(err) - assert false - return - } + u2 := json2.decode(s)! + u := json2.decode(s)! assert u.age == 10 assert u.last_name == 'Johnson' assert u.is_registered == true @@ -188,8 +178,26 @@ fn test_parse_user() { assert u.pets == '{"name":"Bob","animal":"Dog"}' } +// 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 := json2.encode(user) +// // println(s) //{"age":25,"nums":[],"reg_date":{"year":2020,"month":12,"day":22,"hour":7,"minute":23,"second":0,"microsecond":0,"unix":1608621780,"is_local":false}} +// assert s.contains('"reg_date":1608621780') +// user2 := json2.decode(s)! +// assert user2.reg_date.str() == '2020-12-22 07:23:00' +// // println(user2) +// // println(user2.reg_date) +// } + +fn (mut u User) foo() string { + return json2.encode(u) +} + fn test_encode_user() { - usr := User{ + mut usr := User{ age: 10 nums: [1, 2, 3] last_name: 'Johnson' @@ -200,6 +208,8 @@ fn test_encode_user() { expected := '{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"foo"}' out := json2.encode(usr) assert out == expected + // Test json.encode on mutable pointers + assert usr.foo() == expected } struct Color { @@ -237,9 +247,8 @@ struct Country { cities []City name string } - fn test_struct_in_struct() { - country := json.decode(Country, '{ "name": "UK", "cities": [{"name":"London"}, {"name":"Manchester"}]}') or { + country := json2.decode('{ "name": "UK", "cities": [{"name":"London"}, {"name":"Manchester"}]}') or { assert false exit(1) }