From e32e9f70cecb8be22b755f364a8eeea88c6fb2b6 Mon Sep 17 00:00:00 2001 From: Hitalo Souza Date: Sat, 7 Dec 2024 04:22:46 -0400 Subject: [PATCH] x.json2.decode2: minor improvements and bugfixes (#23083) --- vlib/x/json2/decoder2/decode.v | 104 ++--- vlib/x/json2/decoder2/decode_sumtype.v | 7 +- vlib/x/json2/decoder2/decode_test.v | 28 +- .../decode_and_encode_struct_any_test.v | 29 ++ .../tests/json2_tests/decode_map_test.v | 58 +++ .../tests/json2_tests/decode_struct_test.v | 212 +++++++++ .../decoder2/tests/json2_tests/decoder_test.v | 74 +++ .../decoder2/tests/json2_tests/encoder_test.v | 226 +++++++++ .../decoder2/tests/json2_tests/json2_test.v | 143 ++++++ .../json_decode_todo_test.v | 84 ++++ .../json_test.v | 441 ++++++++++++++++++ vlib/x/json2/encoder.v | 4 +- 12 files changed, 1329 insertions(+), 81 deletions(-) create mode 100644 vlib/x/json2/decoder2/tests/json2_tests/decode_and_encode_struct_any_test.v create mode 100644 vlib/x/json2/decoder2/tests/json2_tests/decode_map_test.v create mode 100644 vlib/x/json2/decoder2/tests/json2_tests/decode_struct_test.v create mode 100644 vlib/x/json2/decoder2/tests/json2_tests/decoder_test.v create mode 100644 vlib/x/json2/decoder2/tests/json2_tests/encoder_test.v create mode 100644 vlib/x/json2/decoder2/tests/json2_tests/json2_test.v create mode 100644 vlib/x/json2/decoder2/tests/json2_tests/json_module_compatibility_test/json_decode_todo_test.v create mode 100644 vlib/x/json2/decoder2/tests/json2_tests/json_module_compatibility_test/json_test.v diff --git a/vlib/x/json2/decoder2/decode.v b/vlib/x/json2/decoder2/decode.v index 81379b65a0..ebd3608429 100644 --- a/vlib/x/json2/decoder2/decode.v +++ b/vlib/x/json2/decoder2/decode.v @@ -12,6 +12,8 @@ const false_in_string = 'false' const float_zero_in_string = '0.0' +const whitespace_chars = [` `, `\t`, `\n`]! + // Node represents a node in a linked list to store ValueInfo. struct Node[T] { mut: @@ -147,55 +149,6 @@ pub enum ValueKind { null } -// check_if_json_match checks if the JSON string matches the expected type T. -fn check_if_json_match[T](val string) ! { - // check if the JSON string is empty - if val == '' { - return error('empty string') - } - - // check if generic type matches the JSON type - value_kind := get_value_kind(val[0]) - - $if T is $option { - // TODO - } $else $if T is $sumtype { - // TODO - } $else $if T is $alias { - // TODO - } $else $if T is $string { - if value_kind != .string_ { - return error('Expected string, but got ${value_kind}') - } - } $else $if T is time.Time { - if value_kind != .string_ { - return error('Expected string, but got ${value_kind}') - } - } $else $if T is $map { - if value_kind != .object { - return error('Expected object, but got ${value_kind}') - } - } $else $if T is $array { - if value_kind != .array { - return error('Expected array, but got ${value_kind}') - } - } $else $if T is $struct { - if value_kind != .object { - return error('Expected object, but got ${value_kind}') - } - } $else $if T in [$enum, $int, $float] { - if value_kind != .number { - return error('Expected number, but got ${value_kind}') - } - } $else $if T is bool { - if value_kind != .boolean { - return error('Expected boolean, but got ${value_kind}') - } - } $else { - return error('cannot decode value with ${value_kind} type') - } -} - // error generates an error message with context from the JSON string. fn (mut checker Decoder) error(message string) ! { json := if checker.json.len < checker.checker_idx + 5 { @@ -234,6 +187,14 @@ fn (mut checker Decoder) check_json_format(val string) ! { return checker.error('empty string') } + // skip whitespace + for val[checker.checker_idx] in whitespace_chars { + if checker.checker_idx >= checker_end - 1 { + break + } + checker.checker_idx++ + } + // check if generic type matches the JSON type value_kind := get_value_kind(val[checker.checker_idx]) start_idx_position := checker.checker_idx @@ -264,6 +225,9 @@ fn (mut checker Decoder) check_json_format(val string) ! { checker.checker_idx += 3 } .object { + if checker_end - checker.checker_idx < 2 { + return checker.error('EOF error: expecting a complete object after `{`') + } checker.checker_idx++ for val[checker.checker_idx] != `}` { // check if the JSON string is an empty object @@ -272,7 +236,7 @@ fn (mut checker Decoder) check_json_format(val string) ! { } // skip whitespace - for val[checker.checker_idx] in [` `, `\t`, `\n`] { + for val[checker.checker_idx] in whitespace_chars { if checker.checker_idx >= checker_end - 1 { break } @@ -294,7 +258,7 @@ fn (mut checker Decoder) check_json_format(val string) ! { if checker.checker_idx >= checker_end - 1 { return checker.error('EOF error: key colon not found') } - if val[checker.checker_idx] !in [` `, `\t`, `\n`] { + if val[checker.checker_idx] !in whitespace_chars { return checker.error('invalid value after object key') } checker.checker_idx++ @@ -307,7 +271,7 @@ fn (mut checker Decoder) check_json_format(val string) ! { checker.checker_idx++ // skip whitespace - for val[checker.checker_idx] in [` `, `\t`, `\n`] { + for val[checker.checker_idx] in whitespace_chars { checker.checker_idx++ } @@ -315,7 +279,7 @@ fn (mut checker Decoder) check_json_format(val string) ! { `"`, `[`, `{`, `0`...`9`, `-`, `n`, `t`, `f` { checker.check_json_format(val)! // whitespace - for val[checker.checker_idx] in [` `, `\t`, `\n`] { + for val[checker.checker_idx] in whitespace_chars { checker.checker_idx++ } if val[checker.checker_idx] == `}` { @@ -327,11 +291,11 @@ fn (mut checker Decoder) check_json_format(val string) ! { if val[checker.checker_idx] == `,` { checker.checker_idx++ - for val[checker.checker_idx] in [` `, `\t`, `\n`] { + for val[checker.checker_idx] in whitespace_chars { checker.checker_idx++ } if val[checker.checker_idx] != `"` { - return checker.error('Expecting object key') + return checker.error('Expecting object key after `,`') } } else { if val[checker.checker_idx] == `}` { @@ -360,7 +324,7 @@ fn (mut checker Decoder) check_json_format(val string) ! { for val[checker.checker_idx] != `]` { // skip whitespace - for val[checker.checker_idx] in [` `, `\t`, `\n`] { + for val[checker.checker_idx] in whitespace_chars { if checker.checker_idx >= checker_end - 1 { break } @@ -378,7 +342,7 @@ fn (mut checker Decoder) check_json_format(val string) ! { checker.check_json_format(val)! // whitespace - for val[checker.checker_idx] in [` `, `\t`, `\n`] { + for val[checker.checker_idx] in whitespace_chars { checker.checker_idx++ } if val[checker.checker_idx] == `]` { @@ -390,7 +354,7 @@ fn (mut checker Decoder) check_json_format(val string) ! { if val[checker.checker_idx] == `,` { checker.checker_idx++ - for val[checker.checker_idx] in [` `, `\t`, `\n`] { + for val[checker.checker_idx] in whitespace_chars { checker.checker_idx++ } if val[checker.checker_idx] == `]` { @@ -548,7 +512,7 @@ fn (mut checker Decoder) check_json_format(val string) ! { for checker.checker_idx < checker_end - 1 && val[checker.checker_idx] !in [`,`, `:`, `}`, `]`] { // get trash characters after the value - if val[checker.checker_idx] !in [` `, `\t`, `\n`] { + if val[checker.checker_idx] !in whitespace_chars { checker.error('invalid value. Unexpected character after ${value_kind} end')! } else { // whitespace @@ -558,17 +522,23 @@ fn (mut checker Decoder) check_json_format(val string) ! { } // decode decodes a JSON string into a specified type. +@[manualfree] pub fn decode[T](val string) !T { + if val == '' { + return error('empty string') + } mut decoder := Decoder{ json: val } decoder.check_json_format(val)! - check_if_json_match[T](val)! mut result := T{} decoder.current_node = decoder.values_info.head decoder.decode_value(mut &result)! + unsafe { + decoder.values_info.free() + } return result } @@ -629,6 +599,8 @@ fn (mut decoder Decoder) decode_value[T](mut val T) ! { } val = string_buffer.bytestr() + } else { + return error('Expected string, but got ${string_info.value_kind}') } } $else $if T.unaliased_typ is $sumtype { decoder.decode_sumtype(mut val)! @@ -852,6 +824,8 @@ fn (mut decoder Decoder) decode_value[T](mut val T) ! { } current_field_info = current_field_info.next } + } else { + return error('Expected object, but got ${struct_info.value_kind}') } unsafe { struct_fields_info.free() @@ -860,6 +834,10 @@ fn (mut decoder Decoder) decode_value[T](mut val T) ! { } $else $if T.unaliased_typ is bool { value_info := decoder.current_node.value + if value_info.value_kind != .boolean { + return error('Expected boolean, but got ${value_info.value_kind}') + } + unsafe { val = vmemcmp(decoder.json.str + value_info.position, true_in_string.str, true_in_string.len) == 0 @@ -873,6 +851,8 @@ fn (mut decoder Decoder) decode_value[T](mut val T) ! { unsafe { string_buffer_to_generic_number(val, bytes) } + } else { + return error('Expected number, but got ${value_info.value_kind}') } } $else { return error('cannot decode value with ${typeof(val).name} type') @@ -904,6 +884,8 @@ fn (mut decoder Decoder) decode_array[T](mut val []T) ! { val << array_element } + } else { + return error('Expected array, but got ${array_info.value_kind}') } } @@ -946,6 +928,8 @@ fn (mut decoder Decoder) decode_map[K, V](mut val map[K]V) ! { } decoder.decode_value(mut val[key])! } + } else { + return error('Expected object, but got ${map_info.value_kind}') } } diff --git a/vlib/x/json2/decoder2/decode_sumtype.v b/vlib/x/json2/decoder2/decode_sumtype.v index 2dff869112..52e6535e6d 100644 --- a/vlib/x/json2/decoder2/decode_sumtype.v +++ b/vlib/x/json2/decoder2/decode_sumtype.v @@ -6,11 +6,8 @@ fn (mut decoder Decoder) get_decoded_sumtype_workaround[T](initialized_sumtype T $if initialized_sumtype is $sumtype { $for v in initialized_sumtype.variants { if initialized_sumtype is v { - $if v is $array { - mut val := initialized_sumtype.clone() - decoder.decode_value(mut val)! - return T(val) - } $else { + // workaround for auto generated function considering sumtype as array + unsafe { mut val := initialized_sumtype decoder.decode_value(mut val)! return T(val) diff --git a/vlib/x/json2/decoder2/decode_test.v b/vlib/x/json2/decoder2/decode_test.v index d7012b2cab..152e928179 100644 --- a/vlib/x/json2/decoder2/decode_test.v +++ b/vlib/x/json2/decoder2/decode_test.v @@ -4,42 +4,42 @@ fn test_check_if_json_match() { // /* Test wrong string values */ mut has_error := false - check_if_json_match[string]('{"key": "value"}') or { + decode[string]('{"key": "value"}') or { assert err.str() == 'Expected string, but got object' has_error = true } assert has_error, 'Expected error' has_error = false - check_if_json_match[map[string]string]('"value"') or { + decode[map[string]string]('"value"') or { assert err.str() == 'Expected object, but got string_' has_error = true } assert has_error, 'Expected error' has_error = false - check_if_json_match[[]int]('{"key": "value"}') or { + decode[[]int]('{"key": "value"}') or { assert err.str() == 'Expected array, but got object' has_error = true } assert has_error, 'Expected error' has_error = false - check_if_json_match[string]('[1, 2, 3]') or { + decode[string]('[1, 2, 3]') or { assert err.str() == 'Expected string, but got array' has_error = true } assert has_error, 'Expected error' has_error = false - check_if_json_match[int]('{"key": "value"}') or { + decode[int]('{"key": "value"}') or { assert err.str() == 'Expected number, but got object' has_error = true } assert has_error, 'Expected error' has_error = false - check_if_json_match[bool]('{"key": "value"}') or { + decode[bool]('{"key": "value"}') or { assert err.str() == 'Expected boolean, but got object' has_error = true } @@ -47,19 +47,19 @@ fn test_check_if_json_match() { has_error = false // /* Right string values */ - check_if_json_match[string]('"value"') or { assert false } + decode[string]('"value"') or { assert false } - check_if_json_match[map[string]string]('{"key": "value"}') or { assert false } + decode[map[string]string]('{"key": "value"}') or { assert false } - check_if_json_match[[]int]('[1, 2, 3]') or { assert false } + decode[[]int]('[1, 2, 3]') or { assert false } - check_if_json_match[string]('"string"') or { assert false } + decode[string]('"string"') or { assert false } - check_if_json_match[int]('123') or { assert false } + decode[int]('123') or { assert false } - check_if_json_match[bool]('true') or { assert false } + decode[bool]('true') or { assert false } - check_if_json_match[bool]('false') or { assert false } + decode[bool]('false') or { assert false } // TODO: test null } @@ -157,7 +157,7 @@ fn test_check_json_format() { }, { 'json': '{"key": 123, "key2": 456,}' - 'error': '\n{"key": 123, "key2": 456,}\n ^ Expecting object key' + 'error': '\n{"key": 123, "key2": 456,}\n ^ Expecting object key after `,`' }, { 'json': '[[1, 2, 3], [4, 5, 6],]' diff --git a/vlib/x/json2/decoder2/tests/json2_tests/decode_and_encode_struct_any_test.v b/vlib/x/json2/decoder2/tests/json2_tests/decode_and_encode_struct_any_test.v new file mode 100644 index 0000000000..96b0ddda66 --- /dev/null +++ b/vlib/x/json2/decoder2/tests/json2_tests/decode_and_encode_struct_any_test.v @@ -0,0 +1,29 @@ +import x.json2.decoder2 as json +import x.json2 + +struct AnyStruct[T] { + val T +} + +struct OptAnyStruct[T] { + val ?T +} + +// struct OptAnyArrStruct { +// val []?json2.Any +// } + +fn test_values() { + assert json.decode[AnyStruct[json2.Any]]('{"val":5}')!.val.int() == 5 + assert json.decode[OptAnyStruct[json2.Any]]('{}')!.val == none + assert json.decode[AnyStruct[[]json2.Any]]('{"val":[5,10]}')!.val.map(it.int()) == [ + 5, + 10, + ] + // assert json.decode[OptAnyArrStruct]('{"val":[5,null,10]}')!.val == [?json2.Any(5),json.Null{},10] // skipped because test still fails even though they're the same + + assert json2.encode[AnyStruct[json2.Any]](AnyStruct[json2.Any]{json2.Any(5)}) == '{"val":5}' + assert json2.encode[OptAnyStruct[json2.Any]](OptAnyStruct[json2.Any]{none}) == '{}' + assert json2.encode[AnyStruct[[]json2.Any]](AnyStruct[[]json2.Any]{[json2.Any(5), 10]}) == '{"val":[5,10]}' + // assert json2.encode[OptAnyArrStruct](OptAnyArrStruct{[?json2.Any(5),none,10]}) == '{"val":[5,null,10]}' // encode_array has not implemented optional arrays yet +} diff --git a/vlib/x/json2/decoder2/tests/json2_tests/decode_map_test.v b/vlib/x/json2/decoder2/tests/json2_tests/decode_map_test.v new file mode 100644 index 0000000000..17adc70958 --- /dev/null +++ b/vlib/x/json2/decoder2/tests/json2_tests/decode_map_test.v @@ -0,0 +1,58 @@ +import x.json2.decoder2 as json2 + +const data = ' +{ + "comments": { + "26788945": { + "id": "26788945", + "message": "some comment 1" + }, + "26788946": { + "id": "26788946", + "message": "some comment 2" + }, + "26788947": { + "id": "26788947", + "message": "some comment 3" + } + }, + "comments2": { + "26788945": true, + "26788946": false, + "26788947": true + }, + "comments3": { + "26788945": 1, + "26788946": 2, + "26788947": 3 + } +} +' + +pub struct Comment { + id string + message string +} + +struct Comments { +mut: + comments map[string]Comment + comments2 map[string]bool + comments3 map[string]int +} + +fn test_main() { + mut root := json2.decode[Comments](data)! + assert root.comments.len == 3 + assert root.comments['26788945']!.id == '26788945' + assert root.comments['26788946']!.id == '26788946' + assert root.comments['26788947']!.id == '26788947' + + assert root.comments2['26788945']! == true + assert root.comments2['26788946']! == false + assert root.comments2['26788947']! == true + + assert root.comments3['26788945']! == 1 + assert root.comments3['26788946']! == 2 + assert root.comments3['26788947']! == 3 +} diff --git a/vlib/x/json2/decoder2/tests/json2_tests/decode_struct_test.v b/vlib/x/json2/decoder2/tests/json2_tests/decode_struct_test.v new file mode 100644 index 0000000000..945f8f1018 --- /dev/null +++ b/vlib/x/json2/decoder2/tests/json2_tests/decode_struct_test.v @@ -0,0 +1,212 @@ +import x.json2.decoder2 as json +import x.json2 +import time + +const fixed_time = time.new( + year: 2022 + month: 3 + day: 11 + hour: 13 + minute: 54 + second: 25 +) + +type StringAlias = string +type BoolAlias = bool +type IntAlias = int + +type SumTypes = bool | int | string + +enum Enumerates { + a + b + c + d + e = 99 + f +} + +struct StructType[T] { +mut: + val T +} + +struct StructTypeSub { + test string +} + +struct StructTypeOption[T] { +mut: + val ?T +} + +struct StructTypePointer[T] { +mut: + val &T +} + +struct StructTypeSkippedFields[T] { +mut: + val T @[json: '-'] + val1 T + val2 T @[json: '-'] + val3 T +} + +struct StructTypeSkippedFields2[T] { +mut: + val T + val1 T @[json: '-'] + val2 T + val3 T @[json: '-'] +} + +struct StructTypeSkippedFields3[T] { +mut: + val T @[json: '-'] + val1 T @[json: '-'] + val2 T @[json: '-'] + val3 T @[json: '-'] +} + +struct StructTypeSkippedField4 { +mut: + val map[string]string @[json: '-'] +} + +struct StructTypeSkippedFields5[T] { +mut: + val T @[skip] + val1 T @[skip] + val2 T @[skip] + val3 T @[skip] +} + +struct StructTypeSkippedFields6[T] { +mut: + val T + val1 T @[skip] + val2 T + val3 T @[skip] +} + +fn test_types() { + assert json.decode[StructType[string]]('{"val": ""}')!.val == '' + assert json.decode[StructType[string]]('{"val": "0"}')!.val == '0' + assert json.decode[StructType[string]]('{"val": "1"}')!.val == '1' + assert json.decode[StructType[string]]('{"val": "2"}')!.val == '2' + // assert json.decode[StructType[string]]('{"val": 0}')!.val == '0' // This should be a error + // assert json.decode[StructType[string]]('{"val": 1}')!.val == '1' // This should be a error + // assert json.decode[StructType[string]]('{"val": 2}')!.val == '2' // This should be a error + assert json.decode[StructType[string]]('{"val": "true"}')!.val == 'true' + assert json.decode[StructType[string]]('{"val": "false"}')!.val == 'false' + // assert json.decode[StructType[string]]('{"val": true}')!.val == 'true' // This should be a error + // assert json.decode[StructType[string]]('{"val": false}')!.val == 'false' // This should be a error + + // assert json.decode[StructType[bool]]('{"val": ""}')!.val == false // This should be a error + // assert json.decode[StructType[bool]]('{"val": "0"}')!.val == false // This should be a error + // assert json.decode[StructType[bool]]('{"val": "1"}')!.val == true // This should be a error + // assert json.decode[StructType[bool]]('{"val": "2"}')!.val == true // This should be a error + // assert json.decode[StructType[bool]]('{"val": 0}')!.val == false // This should be a error + // assert json.decode[StructType[bool]]('{"val": 1}')!.val == true // This should be a error + // assert json.decode[StructType[bool]]('{"val": 2}')!.val == true // This should be a error + // assert json.decode[StructType[bool]]('{"val": "true"}')!.val == true // This should be a error + // assert json.decode[StructType[bool]]('{"val": "false"}')!.val == false // This should be a error + assert json.decode[StructType[bool]]('{"val": true}')!.val == true + assert json.decode[StructType[bool]]('{"val": false}')!.val == false + + // assert json.decode[StructType[int]]('{"val": ""}')!.val == 0 // This should be a error + // assert json.decode[StructType[int]]('{"val": "0"}')!.val == 0 // This should be a error + // assert json.decode[StructType[int]]('{"val": "1"}')!.val == 1 // This should be a error + // assert json.decode[StructType[int]]('{"val": "2"}')!.val == 2 // This should be a error + assert json.decode[StructType[int]]('{"val": 0}')!.val == 0 + assert json.decode[StructType[int]]('{"val": 1}')!.val == 1 + assert json.decode[StructType[int]]('{"val": 2}')!.val == 2 + // assert json.decode[StructType[int]]('{"val": "true"}')!.val == 0 // This should be a error + // assert json.decode[StructType[int]]('{"val": "false"}')!.val == 0 // This should be a error + // assert json.decode[StructType[int]]('{"val": true}')!.val == 1 // This should be a error + // assert json.decode[StructType[int]]('{"val": false}')!.val == 0 // This should be a error + + assert json.decode[StructType[time.Time]]('{"val": "2022-03-11T13:54:25.000Z"}')!.val == fixed_time + assert json.decode[StructType[time.Time]]('{"val": "2001-01-05"}')!.val.year == 2001 + assert json.decode[StructType[time.Time]]('{"val": "2001-01-05"}')!.val.month == 1 + assert json.decode[StructType[time.Time]]('{"val": "2001-01-05"}')!.val.day == 5 + assert json.decode[StructType[time.Time]]('{"val": "2001-01-05"}')!.val.hour == 0 + assert json.decode[StructType[time.Time]]('{"val": "2001-01-05"}')!.val.minute == 0 + assert json.decode[StructType[time.Time]]('{"val": "2001-01-05"}')!.val.second == 0 + + assert json.decode[StructType[StructTypeSub]]('{"val": {"test": "test"}}')!.val.test == 'test' + + assert json.decode[StructType[Enumerates]]('{"val": 0}')!.val == .a + assert json.decode[StructType[Enumerates]]('{"val": 1}')!.val == .b + assert json.decode[StructType[Enumerates]]('{"val": 99}')!.val == .e + assert json.decode[StructType[Enumerates]]('{}')!.val == .a + + if x := json.decode[StructTypeOption[Enumerates]]('{"val": 0}')!.val { + assert x == .a + } + if x := json.decode[StructTypeOption[Enumerates]]('{"val": 1}')!.val { + assert x == .b + } + if x := json.decode[StructTypeOption[Enumerates]]('{"val": 99}')!.val { + assert x == .e + } + if x := json.decode[StructTypeOption[Enumerates]]('{}')!.val { + assert false + } else { + assert true + } +} + +fn test_skipped_fields() { + if x := json.decode[StructTypeSkippedFields[int]]('{"val":10,"val1":10,"val2":10,"val3":10}') { + assert x.val == 0 + assert x.val1 == 10 + assert x.val2 == 0 + assert x.val3 == 10 + } else { + assert false + } + + if x := json.decode[StructTypeSkippedFields2[int]]('{"val":10,"val1":10,"val2":10,"val3":10}') { + assert x.val == 10 + assert x.val1 == 0 + assert x.val2 == 10 + assert x.val3 == 0 + } else { + assert false + } + + if x := json.decode[StructTypeSkippedFields3[int]]('{"val":10,"val1":10,"val2":10,"val3":10}') { + assert x.val == 0 + assert x.val1 == 0 + assert x.val2 == 0 + assert x.val3 == 0 + } else { + assert false + } + + if x := json.decode[StructTypeSkippedField4]('{"val":{"a":"b"}}') { + assert x.val.len == 0 + } else { + assert false + } + + if x := json.decode[StructTypeSkippedFields5[int]]('{"val":10,"val1":10,"val2":10,"val3":10}') { + assert x.val == 0 + assert x.val1 == 0 + assert x.val2 == 0 + assert x.val3 == 0 + } else { + assert false + } + + if x := json.decode[StructTypeSkippedFields6[int]]('{"val":10,"val1":10,"val2":10,"val3":10}') { + assert x.val == 10 + assert x.val1 == 0 + assert x.val2 == 10 + assert x.val3 == 0 + } else { + assert false + } +} diff --git a/vlib/x/json2/decoder2/tests/json2_tests/decoder_test.v b/vlib/x/json2/decoder2/tests/json2_tests/decoder_test.v new file mode 100644 index 0000000000..61466de1ab --- /dev/null +++ b/vlib/x/json2/decoder2/tests/json2_tests/decoder_test.v @@ -0,0 +1,74 @@ +import x.json2.decoder2 as json +import x.json2 + +fn test_raw_decode_string() { + str := json.decode[json2.Any]('"Hello!"')! + assert str.str() == 'Hello!' +} + +fn test_raw_decode_string_escape() { + jstr := json.decode[json2.Any]('"\u001b"')! + str := jstr.str() + assert str.len == 1 + assert str[0] == 27 +} + +fn test_raw_decode_number() { + num := json.decode[json2.Any]('123')! + assert num.int() == 123 +} + +fn test_raw_decode_array() { + raw_arr := json.decode[json2.Any]('["Foo", 1]')! + arr := raw_arr.arr() + assert arr[0] or { 0 }.str() == 'Foo' + assert arr[1] or { 0 }.int() == 1 +} + +fn test_raw_decode_bool() { + bol := json.decode[json2.Any]('false')! + assert bol.bool() == false +} + +fn test_raw_decode_map() { + raw_mp := json.decode[json2.Any]('{"name":"Bob","age":20}')! + mp := raw_mp.as_map() + assert mp['name'] or { 0 }.str() == 'Bob' + assert mp['age'] or { 0 }.int() == 20 +} + +fn test_raw_decode_string_with_dollarsign() { + str := json.decode[json2.Any](r'"Hello $world"')! + assert str.str() == r'Hello $world' +} + +fn test_raw_decode_map_with_whitespaces() { + raw_mp := json.decode[json2.Any](' \n\t{"name":"Bob","age":20}\n\t')! + mp := raw_mp.as_map() + assert mp['name'] or { 0 }.str() == 'Bob' + assert mp['age'] or { 0 }.int() == 20 +} + +fn test_nested_array_object() { + mut parser := json2.new_parser(r'[[[[[],[],[]]]],{"Test":{}},[[]]]', false) + decoded := parser.decode()! + assert parser.n_level == 0 +} + +fn test_raw_decode_map_invalid() { + json.decode[json2.Any]('{"name","Bob","age":20}') or { + assert err.msg() == '\n{"name",\n ^ invalid value after object key' + + return + } + assert false +} + +fn test_raw_decode_array_invalid() { + json.decode[json2.Any]('["Foo", 1,}') or { + assert err.msg() == '\n["Foo", 1,}\n ^ EOF error: array not closed' + + return + } + assert false +} diff --git a/vlib/x/json2/decoder2/tests/json2_tests/encoder_test.v b/vlib/x/json2/decoder2/tests/json2_tests/encoder_test.v new file mode 100644 index 0000000000..c468c1f67f --- /dev/null +++ b/vlib/x/json2/decoder2/tests/json2_tests/encoder_test.v @@ -0,0 +1,226 @@ +import x.json2.decoder2 as json +import x.json2 +import strings +import time + +struct StructType[T] { +mut: + val T +} + +fn test_json_string_characters() { + assert json2.encode([u8(`/`)].bytestr()).bytes() == r'"\/"'.bytes() + assert json2.encode([u8(`\\`)].bytestr()).bytes() == r'"\\"'.bytes() + assert json2.encode([u8(`"`)].bytestr()).bytes() == r'"\""'.bytes() + assert json2.encode([u8(`\n`)].bytestr()).bytes() == r'"\n"'.bytes() + assert json2.encode(r'\n\r') == r'"\\n\\r"' + assert json2.encode('\\n') == r'"\\n"' + assert json2.encode(r'\n\r\b') == r'"\\n\\r\\b"' + assert json2.encode(r'\"/').bytes() == r'"\\\"\/"'.bytes() + + assert json2.encode(r'\n\r\b\f\t\\\"\/') == r'"\\n\\r\\b\\f\\t\\\\\\\"\\\/"' + + assert json2.encode("fn main(){nprintln('Hello World! Helo \$a')\n}") == '"fn main(){nprintln(\'Hello World! Helo \$a\')\\n}"' + assert json2.encode(' And when "\'s are in the string, along with # "') == '" And when \\"\'s are in the string, along with # \\""' + assert json2.encode('a \\\nb') == r'"a \\\nb"' + assert json2.encode('Name\tJosé\nLocation\tSF.') == '"Name\\tJosé\\nLocation\\tSF."' +} + +fn test_json_escape_low_chars() { + esc := '\u001b' + assert esc.len == 1 + text := json2.Any(esc) + assert text.json_str() == r'"\u001b"' + + assert json2.encode('\u000f') == r'"\u000f"' + assert json2.encode('\u0020') == r'" "' + assert json2.encode('\u0000') == r'"\u0000"' +} + +fn test_json_string() { + text := json2.Any('te✔st') + + assert text.json_str() == r'"te\u2714st"' + assert json2.encode('te✔st') == r'"te\u2714st"' + + boolean := json2.Any(true) + assert boolean.json_str() == 'true' + integer := json2.Any(int(-5)) + assert integer.json_str() == '-5' + u64integer := json2.Any(u64(5000)) + assert u64integer.json_str() == '5000' + i64integer := json2.Any(i64(-17)) + assert i64integer.json_str() == '-17' +} + +fn test_json_string_emoji() { + text := json2.Any('🐈') + assert text.json_str() == r'"🐈"' + assert json2.Any('💀').json_str() == r'"💀"' + + assert json2.encode('🐈') == r'"🐈"' + assert json2.encode('💀') == r'"💀"' + assert json2.encode('🐈💀') == r'"🐈💀"' +} + +fn test_json_string_non_ascii() { + text := json2.Any('ひらがな') + assert text.json_str() == r'"\u3072\u3089\u304c\u306a"' + + assert json2.encode('ひらがな') == r'"\u3072\u3089\u304c\u306a"' +} + +fn test_utf8_strings_are_not_modified() { + original := '{"s":"Schilddrüsenerkrankungen"}' + deresult := json.decode[json2.Any](original)! + assert deresult.str() == original + + assert json2.encode('ü') == '"ü"' + assert json2.encode('Schilddrüsenerkrankungen') == '"Schilddrüsenerkrankungen"' +} + +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}"' + sb.go_back_to(0) + + emoji_text := json2.Any('🐈') + enc.encode_value(emoji_text, mut sb)! + assert sb.str() == '"${emoji_text}"' + + mut buf := []u8{cap: 14} + + enc.encode_value('ひらがな', mut buf)! + + assert buf.len == 14 + assert buf.bytestr() == '"ひらがな"' +} + +fn test_encoder_prettify() { + obj := { + 'hello': json2.Any('world') + 'arr': [json2.Any('im a string'), [json2.Any('3rd level')]] + 'obj': { + 'map': json2.Any('map inside a map') + } + } + enc := json2.Encoder{ + newline: `\n` + 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", + "arr": [ + "im a string", + [ + "3rd level" + ] + ], + "obj": { + "map": "map inside a map" + } +}' +} + +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() { + array_of_struct := [StructType[[]bool]{ + val: [false, true] + }, StructType[[]bool]{ + val: [true, false] + }] + + assert json2.encode([1, 2, 3]) == '[1,2,3]' + + assert json2.encode(array_of_struct) == '[{"val":[false,true]},{"val":[true,false]}]' +} + +fn test_encode_simple() { + assert json2.encode('hello!') == '"hello!"' + assert json2.encode(1) == '1' +} + +fn test_encode_value() { + json_enc := json2.Encoder{ + newline: `\n` + newline_spaces_count: 2 + escape_unicode: false + } + + mut manifest := map[string]json2.Any{} + + manifest['server_path'] = json2.Any('new_path') + manifest['last_updated'] = json2.Any('timestamp.format_ss()') + manifest['from_source'] = json2.Any('from_source') + + mut sb := strings.new_builder(64) + mut buffer := []u8{} + json_enc.encode_value(manifest, mut buffer)! + + assert buffer.len > 0 + assert buffer == [u8(123), 10, 32, 32, 34, 115, 101, 114, 118, 101, 114, 95, 112, 97, 116, + 104, 34, 58, 32, 34, 110, 101, 119, 95, 112, 97, 116, 104, 34, 44, 10, 32, 32, 34, 108, + 97, 115, 116, 95, 117, 112, 100, 97, 116, 101, 100, 34, 58, 32, 34, 116, 105, 109, 101, + 115, 116, 97, 109, 112, 46, 102, 111, 114, 109, 97, 116, 95, 115, 115, 40, 41, 34, 44, + 10, 32, 32, 34, 102, 114, 111, 109, 95, 115, 111, 117, 114, 99, 101, 34, 58, 32, 34, 102, + 114, 111, 109, 95, 115, 111, 117, 114, 99, 101, 34, 10, 125] + + sb.write(buffer)! + + unsafe { buffer.free() } + + assert sb.str() == r'{ + "server_path": "new_path", + "last_updated": "timestamp.format_ss()", + "from_source": "from_source" +}' +} + +fn test_encode_time() { + assert json2.encode({ + 'bro': json2.Any(time.Time{}) + }) == '{"bro":"0000-00-00T00:00:00.000Z"}' + + assert json2.encode({ + 'bro': time.Time{} + }) == '{"bro":"0000-00-00T00:00:00.000Z"}' + + assert json2.encode(time.Time{}) == '"0000-00-00T00:00:00.000Z"' +} diff --git a/vlib/x/json2/decoder2/tests/json2_tests/json2_test.v b/vlib/x/json2/decoder2/tests/json2_tests/json2_test.v new file mode 100644 index 0000000000..117415850d --- /dev/null +++ b/vlib/x/json2/decoder2/tests/json2_tests/json2_test.v @@ -0,0 +1,143 @@ +import x.json2.decoder2 as json +import x.json2 + +enum JobTitle { + manager + executive + worker +} + +struct Employee { +pub mut: + name string + age int + salary f32 + title JobTitle +} + +fn test_fast_raw_decode() { + s := '{"name":"Peter","age":28,"salary":95000.5,"title":2}' + o := json2.fast_raw_decode(s) or { + assert false + json2.Any('') + } + str := o.str() + assert str == '{"name":"Peter","age":"28","salary":"95000.5","title":"2"}' +} + +struct StructType[T] { +mut: + val T +} + +fn test_struct_with_bool_to_map() { + array_of_struct := [StructType[bool]{ + val: true + }, StructType[bool]{ + val: false + }] + + mut array_of_map := []json2.Any{} + + for variable in array_of_struct { + array_of_map << json2.map_from(variable) + } + + assert array_of_map.str() == '[{"val":true},{"val":false}]' +} + +fn test_struct_with_string_to_map() { + array_of_struct := [StructType[string]{ + val: 'true' + }, StructType[string]{ + val: 'false' + }] + + mut array_of_map := []json2.Any{} + + for variable in array_of_struct { + array_of_map << json2.map_from(variable) + } + + assert array_of_map.str() == '[{"val":"true"},{"val":"false"}]' +} + +fn test_struct_with_array_to_map() { + array_of_struct := [StructType[[]bool]{ + val: [false, true] + }, StructType[[]bool]{ + val: [true, false] + }] + + mut array_of_map := []json2.Any{} + + for variable in array_of_struct { + array_of_map << json2.map_from(variable) + } + + assert array_of_map.str() == '[{"val":[false,true]},{"val":[true,false]}]' +} + +fn test_struct_with_array_of_arrays_to_map() { + array_of_struct := [ + StructType[[][]bool]{ + val: [[true, false], [true, false]] + }, + StructType[[][]bool]{ + val: [[false, true], [false, true]] + }, + ] + mut array_of_map := []json2.Any{} + for variable in array_of_struct { + array_of_map << json2.map_from(variable) + } + assert array_of_map.str() == '[{"val":[[true,false],[true,false]]},{"val":[[false,true],[false,true]]}]' + + array_of_struct_int := [ + StructType[[][]int]{ + val: [[1, 0], [1, 0]] + }, + StructType[[][]int]{ + val: [[0, 1], [0, 1]] + }, + ] + mut array_of_map_int := []json2.Any{} + for variable in array_of_struct_int { + array_of_map_int << json2.map_from(variable) + } + assert array_of_map_int.str() == '[{"val":[[1,0],[1,0]]},{"val":[[0,1],[0,1]]}]' +} + +fn test_struct_with_number_to_map() { + assert json2.map_from(StructType[string]{'3'}).str() == '{"val":"3"}' + assert json2.map_from(StructType[bool]{true}).str() == '{"val":true}' + assert json2.map_from(StructType[i8]{3}).str() == '{"val":3}' + assert json2.map_from(StructType[i16]{3}).str() == '{"val":3}' + assert json2.map_from(StructType[int]{3}).str() == '{"val":3}' + assert json2.map_from(StructType[i64]{3}).str() == '{"val":3}' + assert json2.map_from(StructType[i8]{-3}).str() == '{"val":-3}' + assert json2.map_from(StructType[i16]{i16(-3)}).str() == '{"val":-3}' + assert json2.map_from(StructType[int]{-3}).str() == '{"val":-3}' + assert json2.map_from(StructType[i64]{-3}).str() == '{"val":-3}' + assert json2.map_from(StructType[f32]{3.0}).str() == '{"val":3}' + assert json2.map_from(StructType[f64]{3.0}).str() == '{"val":3}' + assert json2.map_from(StructType[u8]{3}).str() == '{"val":3}' + assert json2.map_from(StructType[u16]{3}).str() == '{"val":3}' + assert json2.map_from(StructType[u32]{3}).str() == '{"val":3}' + assert json2.map_from(StructType[u64]{3}).str() == '{"val":3}' +} + +fn test_struct_with_struct_to_map() { + assert json2.map_from(StructType[StructType[string]]{StructType[string]{'3'}}).str() == '{"val":{"val":"3"}}' + assert json2.map_from(StructType[StructType[int]]{StructType[int]{3}}).str() == '{"val":{"val":3}}' +} + +fn test_maps() { + assert json.decode[map[string]string]('{"test":"abc"}')! == { + 'test': 'abc' + } + + assert json.decode[map[string]StructType[bool]]('{"test":{"val":true}}')! == { + 'test': StructType[bool]{true} + } +} diff --git a/vlib/x/json2/decoder2/tests/json2_tests/json_module_compatibility_test/json_decode_todo_test.v b/vlib/x/json2/decoder2/tests/json2_tests/json_module_compatibility_test/json_decode_todo_test.v new file mode 100644 index 0000000000..3688858734 --- /dev/null +++ b/vlib/x/json2/decoder2/tests/json2_tests/json_module_compatibility_test/json_decode_todo_test.v @@ -0,0 +1,84 @@ +import x.json2.decoder2 as json + +struct Mount { + size u64 +} + +fn test_decode_u64() { + data := '{"size": 10737418240}' + m := json.decode[Mount](data)! + assert m.size == 10737418240 +} + +pub struct Comment { +pub mut: + id string + comment string +} + +pub struct Task { +mut: + description string + id int + total_comments int + file_name string @[skip] + comments []Comment @[skip] +} + +fn test_skip_fields_should_be_initialised_by_json_decode() { + data := '{"total_comments": 55, "id": 123}' + mut task := json.decode[Task](data)! + assert task.id == 123 + assert task.total_comments == 55 + assert task.comments == [] +} + +// + +struct DbConfig { + host string + dbname string + user string +} + +fn test_decode_error_message_should_have_enough_context_empty() { + json.decode[DbConfig]('') or { + assert err.msg() == 'empty string' + return + } + assert false +} + +fn test_decode_error_message_should_have_enough_context_just_brace() { + json.decode[DbConfig]('{') or { + assert err.msg() == ' +{ +^ EOF error: expecting a complete object after `{`' + return + } + assert false +} + +fn test_decode_error_message_should_have_enough_context_trailing_comma_at_end() { + txt := '{ + "host": "localhost", + "dbname": "alex", + "user": "alex", +}' + + json.decode[DbConfig](txt) or { + assert err.msg() == '\n\n}\n ^ Expecting object key after `,`' + + return + } + assert false +} + +fn test_decode_error_message_should_have_enough_context_in_the_middle() { + txt := '{"host": "localhost", "dbname": "alex" "user": "alex", "port": "1234"}' + json.decode[DbConfig](txt) or { + assert err.msg() == '\n{"host": "localhost", "dbname": "alex" "\n ^ invalid value. Unexpected character after string_ end' + return + } + assert false +} diff --git a/vlib/x/json2/decoder2/tests/json2_tests/json_module_compatibility_test/json_test.v b/vlib/x/json2/decoder2/tests/json2_tests/json_module_compatibility_test/json_test.v new file mode 100644 index 0000000000..7088bff313 --- /dev/null +++ b/vlib/x/json2/decoder2/tests/json2_tests/json_module_compatibility_test/json_test.v @@ -0,0 +1,441 @@ +import x.json2.decoder2 as json +import x.json2 +import time + +enum JobTitle { + manager + executive + worker +} + +struct Employee { +pub mut: + name string + age int + salary f32 + title JobTitle + sub_employee SubEmployee //! FIXME - decode +} + +pub struct SubEmployee { +pub mut: + name string + age int + salary f32 + title JobTitle +} + +fn test_simple() { + sub_employee := SubEmployee{ + name: 'João' + } + x := Employee{'Peter', 28, 95000.5, .worker, sub_employee} + s := json2.encode[Employee](x) + assert s == '{"name":"Peter","age":28,"salary":95000.5,"title":2,"sub_employee":{"name":"João","age":0,"salary":0,"title":0}}' + + y := json.decode[Employee](s) or { + println(err) + assert false + return + } + assert y.name == 'Peter' + assert y.age == 28 + assert y.salary == 95000.5 + assert y.title == .worker + // assert y.sub_employee.name == 'João' + assert y.sub_employee.age == 0 + assert y.sub_employee.salary == 0.0 + // assert y.sub_employee.title == .worker //! FIXME +} + +// const currency_id = 'cconst' + +// struct Price { +// net f64 +// currency_id string @[json: currencyId] = currency_id +// } + +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 +pub struct User { +pub mut: + age int + nums []int + last_name string @[json: lastName] + is_registered bool @[json: IsRegistered] + typ int @[json: 'type'] + 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"}}' + + 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"}' +} + +fn test_encode_decode_time() { + user := User2{ + age: 25 + reg_date: time.new(year: 2020, month: 12, day: 22, hour: 7, minute: 23) + } + s := json2.encode(user) + + assert s.contains('"reg_date":"2020-12-22T07:23:00.000Z"') +} + +fn (mut u User) foo() string { + return json2.encode(u) +} + +fn test_encode_user() { + mut 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 := json2.encode[User](usr) + // println(out) + assert out == expected + // Test json2.encode on mutable pointers + assert usr.foo() == expected +} + +struct Color { +pub mut: + space string + point string @[raw] +} + +fn test_raw_json_field() { + color := json.decode[Color]('{"space": "YCbCr", "point": {"Y": 123}}') or { + assert false + Color{} + } + assert color.point == '{"Y": 123}' + assert color.space == 'YCbCr' +} + +fn test_bad_raw_json_field() { + color := json.decode[Color]('{"space": "YCbCr"}') or { return } + assert color.point == '' + assert color.space == 'YCbCr' +} + +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) + } + // numbers := { + // 'one': 1 + // 'two': 2 + // 'three': 3 + // 'four': 4 + // } + assert json2.encode(numbers) == expected + assert numbers.str() == expected +} + +type ID = string +type GG = int + +struct Message { + id ID + ij GG +} + +fn test_encode_alias_struct() { + expected := '{"id":"118499178790780929","ij":999998888}' + msg := Message{'118499178790780929', 999998888} + out := json2.encode[Message](msg) + assert out == expected +} + +struct StByteArray { + ba []u8 +} + +fn test_byte_array() { + assert json2.encode(StByteArray{ ba: [u8(1), 2, 3, 4, 5] }) == '{"ba":[1,2,3,4,5]}' +} + +struct Bar { + x string +} + +fn bar[T](payload string) !Bar { // ?T doesn't work currently + result := json.decode[T](payload)! + return result +} + +fn test_generic() { + result := bar[Bar]('{"x":"test"}') or { Bar{} } + assert result.x == 'test' +} + +struct Foo[T] { +pub: + name string + data T +} + +fn test_generic_struct() { + foo_int := Foo[int]{'bar', 12} + foo_enc := json2.encode(foo_int) + assert foo_enc == '{"name":"bar","data":12}' + foo_dec := json.decode[Foo[int]](foo_enc)! + assert foo_dec.name == 'bar' + assert foo_dec.data == 12 +} + +type StringAlias = string + +// TODO: encode_pretty array, sum types, struct, alias of struct and others... +struct Foo2 { + ux8 u8 + ux16 u16 + ux32 u32 + ux64 u64 + sx8 i8 + sx16 i16 + sx32 int + sx64 i64 + a bool + b string + c StringAlias +} + +fn test_pretty() { + foo := Foo2{1, 2, 3, 4, -1, -2, -3, -4, true, 'abc', 'aliens'} + assert json2.encode_pretty(foo) == '{ + "ux8": 1, + "ux16": 2, + "ux32": 3, + "ux64": 4, + "sx8": -1, + "sx16": -2, + "sx32": -3, + "sx64": -4, + "a": true, + "b": "abc", + "c": "aliens" +}' +} + +struct Aa { + sub AliasType +} + +struct Bb { + a int +} + +type AliasType = Bb + +fn test_encode_alias_field() { + s := json2.encode(Aa{ + sub: Bb{ + a: 1 + } + }) + assert s == '{"sub":{"a":1}}' +} + +struct APrice {} + +pub struct Association { + association &Association = unsafe { nil } + price APrice +} + +fn test_encoding_struct_with_pointers() { + value := Association{ + association: &Association{ + price: APrice{} + } + price: APrice{} + } + // println(value) + assert json2.encode(value) == '{"association":{"price":{}},"price":{}}' +} + +pub struct City { +mut: + name string +} + +pub struct Country { +mut: + cities []City + name string +} + +struct Data { +mut: + 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 := json2.encode(data) + assert out == data_expected +} + +struct Human { + name string +} + +struct Item { + tag string +} + +enum Animal { + dog // Will be encoded as `0` + cat +} + +type Entity = Animal | Human | Item | string | time.Time + +struct SomeGame { + title string + player Entity + other []Entity +} + +fn test_encode_decode_sumtype() { + t := time.now() + game := SomeGame{ + title: 'Super Mega Game' + player: Human{'Monke'} + other: [ + Entity(Item{'Pen'}), + Item{'Cookie'}, + Animal.cat, + 'Stool', + t, + ] + } + + enc := json2.encode(game) + + assert enc == '{"title":"Super Mega Game","player":{"name":"Monke"},"other":[{"tag":"Pen"},{"tag":"Cookie"},1,"Stool","${t.format_rfc3339()}"]}' +} + +struct Foo3 { + name string + age int @[omitempty] +} + +// fn test_omit_empty() { +// foo := Foo3{'Bob', 0} +// assert json2.encode(foo) == '{"name":"Bob"}' +// assert json2.encode_pretty(foo) == '{ +// "name": "Bob" +// }' +// } + +struct Foo31 { + name string + age ?int +} + +fn test_option_instead_of_omit_empty() { + foo := Foo31{ + name: 'Bob' + } + assert json2.encode_pretty(foo) == '{ + "name": "Bob" +}' +} + +struct Asdasd { + data GamePacketData +} + +type GamePacketData = GPEquipItem | GPScale + +struct GPScale { + value f32 +} + +struct GPEquipItem { + name string +} + +fn create_game_packet(data &GamePacketData) string { + return json2.encode(data) +} + +// fn test_encode_sumtype_defined_ahead() { +// ret := create_game_packet(&GamePacketData(GPScale{})) +// assert ret == '{"value":0}' +// } diff --git a/vlib/x/json2/encoder.v b/vlib/x/json2/encoder.v index 778f7ae47a..22d65d5278 100644 --- a/vlib/x/json2/encoder.v +++ b/vlib/x/json2/encoder.v @@ -17,7 +17,7 @@ pub: } // byte array versions of the most common tokens/chars to avoid reallocations -const null_in_bytes = 'null' +const null_in_string = 'null' const true_in_string = 'true' @@ -180,7 +180,7 @@ fn (e &Encoder) encode_value_with_level[T](val T, level int, mut buf []u8) ! { str_value := val.json_str() unsafe { buf.push_many(str_value.str, str_value.len) } } $else $if T is Null { - unsafe { buf.push_many(null_in_bytes.str, null_in_bytes.len) } + unsafe { buf.push_many(null_in_string.str, null_in_string.len) } } $else $if T is $struct { e.encode_struct(val, level, mut buf)! } $else $if T is $enum {