From 69e80ba96d3a576d5d3ae1c28af2cdddcde91791 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Tue, 9 Sep 2025 15:00:05 +0300 Subject: [PATCH 1/2] builtin: add a temporary ctovstring_impl/1 API to enable `ui` to compile cleanly for PR#25264, part 1 --- vlib/builtin/string.v | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/vlib/builtin/string.v b/vlib/builtin/string.v index 53f9c87f08..b3476c4fa8 100644 --- a/vlib/builtin/string.v +++ b/vlib/builtin/string.v @@ -3052,3 +3052,16 @@ pub fn (mut ri RunesIterator) next() ?rune { } return rune(impl_utf8_to_utf32(start, len)) } + +// ctovstring_impl is a temporary API, to enable clean CI runs for https://github.com/vlang/v/pull/25264 . +// It will be deleted after the migration to the new `builtin` naming scheme is finished. +@[export: 'builtin__ctovstring'] +pub fn ctovstring_impl(s &u8) string { + unsafe { + len := C.strlen(voidptr(s)) + return string{ + str: memdup(voidptr(s), isize(len)) + len: len + } + } +} From bae7684276f5e56a9293a38bb286df3644157109 Mon Sep 17 00:00:00 2001 From: Larsimusrex Date: Tue, 9 Sep 2025 17:50:22 +0200 Subject: [PATCH 2/2] json2: replace encoder with new implementation (#25224) --- cmd/tools/vls.v | 19 +- .../math/big/{json_decode.v => json_custom.v} | 5 + .../time/{json_decode.c.v => json_custom.c.v} | 5 + .../tests/testdata/json_encoding_test.out | 2 +- vlib/v/gen/js/sourcemap/basic_test.v | 4 +- vlib/x/json2/constants.v | 5 + vlib/x/json2/count.v | 98 --- vlib/x/json2/count_test.v | 99 --- vlib/x/json2/custom.v | 15 + .../decode_and_encode_struct_any_test.v | 1 + .../decoder2/tests/json2_tests/encoder_test.v | 215 ------- .../json_test.v | 8 +- vlib/x/json2/encode.v | 490 +++++++++++++++ vlib/x/json2/encoder.v | 581 +----------------- vlib/x/json2/tests/any_test.v | 4 +- vlib/x/json2/tests/encode_struct_test.v | 18 +- vlib/x/json2/tests/encode_test.v | 330 ++++++++++ vlib/x/json2/tests/encoder_test.v | 74 +-- .../json_test.v | 8 +- vlib/x/json2/types.v | 23 +- 20 files changed, 902 insertions(+), 1102 deletions(-) rename vlib/math/big/{json_decode.v => json_custom.v} (84%) rename vlib/time/{json_decode.c.v => json_custom.c.v} (86%) create mode 100644 vlib/x/json2/constants.v delete mode 100644 vlib/x/json2/count.v delete mode 100644 vlib/x/json2/count_test.v create mode 100644 vlib/x/json2/custom.v create mode 100644 vlib/x/json2/encode.v create mode 100644 vlib/x/json2/tests/encode_test.v diff --git a/cmd/tools/vls.v b/cmd/tools/vls.v index 319a2cf84e..ca211f1171 100644 --- a/cmd/tools/vls.v +++ b/cmd/tools/vls.v @@ -64,12 +64,6 @@ const vls_src_folder = os.join_path(vls_folder, 'src') const server_not_found_err = error_with_code('Language server is not installed nor found.', 101) -const json_enc = json2.Encoder{ - newline: `\n` - newline_spaces_count: 2 - escape_unicode: false -} - fn (upd VlsUpdater) check_or_create_vls_folder() ! { if !os.exists(vls_folder) { upd.log('Creating .vls folder...') @@ -106,22 +100,11 @@ fn (upd VlsUpdater) update_manifest(new_path string, from_source bool, timestamp } } - mut manifest_file := os.open_file(vls_manifest_path, 'w+')! - defer { - manifest_file.close() - } - manifest['server_path'] = json2.Any(new_path) manifest['last_updated'] = json2.Any(timestamp.format_ss()) manifest['from_source'] = json2.Any(from_source) - mut buffer := []u8{} - - json_enc.encode_value(manifest, mut buffer)! - - manifest_file.write(buffer)! - - unsafe { buffer.free() } + os.write_file(vls_manifest_path, json2.encode(manifest))! } fn (upd VlsUpdater) init_download_prebuilt() ! { diff --git a/vlib/math/big/json_decode.v b/vlib/math/big/json_custom.v similarity index 84% rename from vlib/math/big/json_decode.v rename to vlib/math/big/json_custom.v index cc2d89adad..4a96df3c07 100644 --- a/vlib/math/big/json_decode.v +++ b/vlib/math/big/json_custom.v @@ -28,3 +28,8 @@ pub fn (mut result Integer) from_json_number(raw_number string) ! { result = result * integer_from_int(-1) } } + +// to_json implements a custom encoder for json2 +pub fn (result Integer) to_json() string { + return result.str() +} diff --git a/vlib/time/json_decode.c.v b/vlib/time/json_custom.c.v similarity index 86% rename from vlib/time/json_decode.c.v rename to vlib/time/json_custom.c.v index c958334b8e..c9a98bcc8d 100644 --- a/vlib/time/json_decode.c.v +++ b/vlib/time/json_custom.c.v @@ -34,3 +34,8 @@ pub fn (mut t Time) from_json_string(raw_string string) ! { return error('Expected iso8601/rfc3339/unix time but got: ${raw_string}') } + +// to_json implements a custom encoder for json2 (rfc3339) +pub fn (t Time) to_json() string { + return '"' + t.format_rfc3339() + '"' +} diff --git a/vlib/toml/tests/testdata/json_encoding_test.out b/vlib/toml/tests/testdata/json_encoding_test.out index 63e8d36c7a..ff2bf2bb88 100644 --- a/vlib/toml/tests/testdata/json_encoding_test.out +++ b/vlib/toml/tests/testdata/json_encoding_test.out @@ -1 +1 @@ -{ "\u3072\u3089\u304c\u306a": "\u3072\u3089" } \ No newline at end of file +{ "ひらがな": "ひら" } \ No newline at end of file diff --git a/vlib/v/gen/js/sourcemap/basic_test.v b/vlib/v/gen/js/sourcemap/basic_test.v index d0ab9b636d..6497063597 100644 --- a/vlib/v/gen/js/sourcemap/basic_test.v +++ b/vlib/v/gen/js/sourcemap/basic_test.v @@ -132,7 +132,7 @@ fn test_simple() { json_data := sm.to_json() - expected := '{"version":3,"file":"hello.js","sourceRoot":"\\/","sources":["hello.v"],"sourcesContent":["fn main(){nprintln(\'Hello World! Helo \$a\')\\n}"],"names":["hello_name"],"mappings":"AAAA;AAAA,EAAA,OAAO,CAACA,GAAR,CAAY,aAAZ,CAAA,CAAA;AAAA"}' + expected := '{"version":3,"file":"hello.js","sourceRoot":"/","sources":["hello.v"],"sourcesContent":["fn main(){nprintln(\'Hello World! Helo \$a\')\\n}"],"names":["hello_name"],"mappings":"AAAA;AAAA,EAAA,OAAO,CAACA,GAAR,CAAY,aAAZ,CAAA,CAAA;AAAA"}' assert json_data.str() == expected } @@ -153,6 +153,6 @@ fn test_source_null() { }, 3, 1, '') json_data := sm.to_json() - expected := '{"version":3,"file":"hello.js","sourceRoot":"\\/","sources":["hello.v","hello_lib1.v","hello_lib2.v"],"sourcesContent":[null,null,null],"names":[],"mappings":"CA+\\/\\/\\/\\/\\/HA;CCAA;CCAA"}' + expected := '{"version":3,"file":"hello.js","sourceRoot":"/","sources":["hello.v","hello_lib1.v","hello_lib2.v"],"sourcesContent":[null,null,null],"names":[],"mappings":"CA+/////HA;CCAA;CCAA"}' assert json_data.str() == expected } diff --git a/vlib/x/json2/constants.v b/vlib/x/json2/constants.v new file mode 100644 index 0000000000..178427a982 --- /dev/null +++ b/vlib/x/json2/constants.v @@ -0,0 +1,5 @@ +module json2 + +const true_string = 'true' +const false_string = 'false' +const null_string = 'null' diff --git a/vlib/x/json2/count.v b/vlib/x/json2/count.v deleted file mode 100644 index 6615f9ce16..0000000000 --- a/vlib/x/json2/count.v +++ /dev/null @@ -1,98 +0,0 @@ -module json2 - -import time - -struct Count { -mut: - total int -} - -// get_total -fn (mut count Count) get_total() int { - return count.total -} - -// reset_total -fn (mut count Count) reset_total() { - count.total = 0 -} - -// count_chars count json sizen without new encode -fn (mut count Count) count_chars[T](val T) { - $if val is $option { - workaround := val - if workaround != none { - count.count_chars(val) - } - } $else $if T is string { - count.chars_in_string(val) - } $else $if T is $sumtype { - $for v in val.variants { - if val is v { - count.count_chars(val) - } - } - } $else $if T is $alias { - // TODO - } $else $if T is time.Time { - count.total += 26 // "YYYY-MM-DDTHH:mm:ss.123Z" - } $else $if T is $map { - count.total++ // { - for k, v in val { - count.count_chars(k) - count.total++ // : - count.count_chars(v) - } - count.total++ // } - } $else $if T is $array { - count.total += 2 // [] - if val.len > 0 { - for element in val { - count.count_chars(element) - } - count.total += val.len - 1 // , - } - } $else $if T is $struct { - count.chars_in_struct(val) - } $else $if T is $enum { - count.count_chars(int(val)) - } $else $if T is $int { - // TODO: benchmark - mut abs_val := val - if val < 0 { - count.total++ // - - abs_val = -val - } - for number_value := abs_val; number_value >= 1; number_value /= 10 { - count.total++ - } - if val == 0 { - count.total++ - } - } $else $if T is $float { - // TODO - } $else $if T is bool { - if val { - count.total += 4 // true - } else { - count.total += 5 // false - } - } $else { - } -} - -// chars_in_struct -fn (mut count Count) chars_in_struct[T](val T) { - count.total += 2 // {} - $for field in T.fields { - // TODO: handle attributes - count.total += field.name.len + 3 // "": - workaround := val.$(field.name) - count.count_chars(workaround) - } -} - -// chars_in_string -fn (mut count Count) chars_in_string(val string) { - count.total += val.len + 2 // "" -} diff --git a/vlib/x/json2/count_test.v b/vlib/x/json2/count_test.v deleted file mode 100644 index 1acb7e2894..0000000000 --- a/vlib/x/json2/count_test.v +++ /dev/null @@ -1,99 +0,0 @@ -module 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 TimeAlias = time.Time -type StructAlias = StructType[int] -type EnumAlias = Enumerates - -type SumTypes = StructType[string] | []SumTypes | []string | bool | int | string | time.Time - -enum Enumerates { - a - b - c - d - e = 99 - f -} - -struct StructType[T] { -mut: - val T -} - -struct StructTypeOption[T] { -mut: - val ?T -} - -struct StructTypePointer[T] { -mut: - val &T -} - -fn count_test[T](value T) { - mut count := Count{0} - - count.count_chars(value) - assert encode(value).len == count.get_total() -} - -fn test_empty() { - count_test(map[string]string{}) - - count_test([]string{}) - - count_test(StructType[bool]{}) - - count_test(map[string]string{}) -} - -fn test_types() { - count_test(StructType[string]{}) - - count_test(StructType[string]{ val: '' }) - - count_test(StructType[string]{ val: 'abcd' }) - - count_test(StructType[bool]{ val: false }) - - count_test(StructType[bool]{ val: true }) - - count_test(StructType[int]{ val: 26 }) - - count_test(StructType[int]{ val: 1 }) - - count_test(StructType[int]{ val: -125 }) - - count_test(StructType[u64]{ val: u64(-1) }) - - count_test(StructType[time.Time]{}) - - count_test(StructType[time.Time]{ val: fixed_time }) - - count_test(StructType[StructType[int]]{ - val: StructType[int]{ - val: 1 - } - }) - - count_test(StructType[Enumerates]{}) - count_test(StructType[Enumerates]{}) - count_test(StructType[Enumerates]{ val: Enumerates.f }) - count_test(StructType[[]int]{}) - count_test(StructType[[]int]{ val: [0] }) - count_test(StructType[[]int]{ val: [0, 1, 0, 2, 3, 2, 5, 1] }) -} diff --git a/vlib/x/json2/custom.v b/vlib/x/json2/custom.v new file mode 100644 index 0000000000..b4e8b456e7 --- /dev/null +++ b/vlib/x/json2/custom.v @@ -0,0 +1,15 @@ +module json2 + +// implements encoding json, this is not validated so implementations must be correct +pub interface JsonEncoder { + // to_json returns a string containing an objects json representation + to_json() string +} + +// Encodable is an interface, that allows custom implementations for encoding structs to their string based JSON representations. + +@[deprecated: 'use `to_json` to implement `JsonEncoder` instead'] +@[deprecated_after: '2025-10-30'] +pub interface Encodable { + json_str() string +} 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 index 96b0ddda66..ce1c96f43f 100644 --- 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 @@ -16,6 +16,7 @@ struct OptAnyStruct[T] { 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[OptAnyStruct[json2.Any]]('{"val":null}')!.val == none assert json.decode[AnyStruct[[]json2.Any]]('{"val":[5,10]}')!.val.map(it.int()) == [ 5, 10, diff --git a/vlib/x/json2/decoder2/tests/json2_tests/encoder_test.v b/vlib/x/json2/decoder2/tests/json2_tests/encoder_test.v index c468c1f67f..8659f05f04 100644 --- a/vlib/x/json2/decoder2/tests/json2_tests/encoder_test.v +++ b/vlib/x/json2/decoder2/tests/json2_tests/encoder_test.v @@ -1,74 +1,5 @@ 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"}' @@ -78,149 +9,3 @@ fn test_utf8_strings_are_not_modified() { 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/json_module_compatibility_test/json_test.v b/vlib/x/json2/decoder2/tests/json2_tests/json_module_compatibility_test/json_test.v index 7088bff313..ea705d11bc 100644 --- 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 @@ -30,7 +30,7 @@ fn test_simple() { name: 'João' } x := Employee{'Peter', 28, 95000.5, .worker, sub_employee} - s := json2.encode[Employee](x) + s := json2.encode[Employee](x, enum_as_int: true) 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 { @@ -230,7 +230,7 @@ struct Foo2 { fn test_pretty() { foo := Foo2{1, 2, 3, 4, -1, -2, -3, -4, true, 'abc', 'aliens'} - assert json2.encode_pretty(foo) == '{ + assert json2.encode(foo, prettify: true, indent_string: ' ') == '{ "ux8": 1, "ux16": 2, "ux32": 3, @@ -385,7 +385,7 @@ fn test_encode_decode_sumtype() { ] } - enc := json2.encode(game) + enc := json2.encode(game, enum_as_int: true) assert enc == '{"title":"Super Mega Game","player":{"name":"Monke"},"other":[{"tag":"Pen"},{"tag":"Cookie"},1,"Stool","${t.format_rfc3339()}"]}' } @@ -412,7 +412,7 @@ fn test_option_instead_of_omit_empty() { foo := Foo31{ name: 'Bob' } - assert json2.encode_pretty(foo) == '{ + assert json2.encode(foo, prettify: true, indent_string: ' ') == '{ "name": "Bob" }' } diff --git a/vlib/x/json2/encode.v b/vlib/x/json2/encode.v new file mode 100644 index 0000000000..1d53c95916 --- /dev/null +++ b/vlib/x/json2/encode.v @@ -0,0 +1,490 @@ +module json2 + +// EncoderOptions provides a list of options for encoding +@[params] +pub struct EncoderOptions { +pub: + prettify bool + indent_string string = ' ' + newline_string string = '\n' + + enum_as_int bool + + escape_unicode bool +} + +struct Encoder { + EncoderOptions +mut: + level int + prefix string + + output []u8 = []u8{cap: 2048} +} + +@[inline] +fn workaround_cast[T](val voidptr) T { + return *(&T(val)) +} + +// encode is a generic function that encodes a type into a JSON string. +pub fn encode[T](val T, config EncoderOptions) string { + mut encoder := Encoder{ + EncoderOptions: config + } + + encoder.encode_value(val) + + return encoder.output.bytestr() +} + +fn (mut encoder Encoder) encode_value[T](val T) { + $if T.unaliased_typ is string { + encoder.encode_string(workaround_cast[string](&val)) + } $else $if T.unaliased_typ is bool { + encoder.encode_boolean(workaround_cast[bool](&val)) + } $else $if T.unaliased_typ is u8 { + encoder.encode_number(workaround_cast[u8](&val)) + } $else $if T.unaliased_typ is u16 { + encoder.encode_number(workaround_cast[u16](&val)) + } $else $if T.unaliased_typ is u32 { + encoder.encode_number(workaround_cast[u32](&val)) + } $else $if T.unaliased_typ is u64 { + encoder.encode_number(workaround_cast[u64](&val)) + } $else $if T.unaliased_typ is i8 { + encoder.encode_number(workaround_cast[i8](&val)) + } $else $if T.unaliased_typ is i16 { + encoder.encode_number(workaround_cast[i16](&val)) + } $else $if T.unaliased_typ is int || T.unaliased_typ is i32 { + encoder.encode_number(workaround_cast[int](&val)) + } $else $if T.unaliased_typ is i64 { + encoder.encode_number(workaround_cast[i64](&val)) + } $else $if T.unaliased_typ is usize { + encoder.encode_number(workaround_cast[usize](&val)) + } $else $if T.unaliased_typ is isize { + encoder.encode_number(workaround_cast[isize](&val)) + } $else $if T.unaliased_typ is f32 { + encoder.encode_number(workaround_cast[f32](&val)) + } $else $if T.unaliased_typ is f64 { + encoder.encode_number(workaround_cast[f64](&val)) + } $else $if T.unaliased_typ is $array { + encoder.encode_array(val) + } $else $if T.unaliased_typ is $map { + encoder.encode_map(val) + } $else $if T.unaliased_typ is $enum { + encoder.encode_enum(val) + } $else $if T.unaliased_typ is $sumtype { + encoder.encode_sumtype(val) + } $else $if T is JsonEncoder { // uses T, because alias could be implementing JsonEncoder, while the base type does not + encoder.encode_custom(val) + } $else $if T is Encodable { // uses T, because alias could be implementing JsonEncoder, while the base type does not + encoder.encode_custom2(val) + } $else $if T.unaliased_typ is $struct { + unsafe { encoder.encode_struct(val) } + } +} + +fn (mut encoder Encoder) encode_string(val string) { + encoder.output << `"` + mut buffer_start := 0 + mut buffer_end := 0 + for buffer_end < val.len { + character := val[buffer_end] + match character { + `"`, `\\` { + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + buffer_end++ + buffer_start = buffer_end + + encoder.output << `\\` + encoder.output << character + } + `\b` { + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + buffer_end++ + buffer_start = buffer_end + + encoder.output << `\\` + encoder.output << `b` + } + `\n` { + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + buffer_end++ + buffer_start = buffer_end + + encoder.output << `\\` + encoder.output << `n` + } + `\f` { + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + buffer_end++ + buffer_start = buffer_end + + encoder.output << `\\` + encoder.output << `f` + } + `\t` { + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + buffer_end++ + buffer_start = buffer_end + + encoder.output << `\\` + encoder.output << `t` + } + `\r` { + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + buffer_end++ + buffer_start = buffer_end + + encoder.output << `\\` + encoder.output << `r` + } + else { + if character < 0x20 { // control characters + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + buffer_end++ + buffer_start = buffer_end + + encoder.output << `\\` + encoder.output << `u` + + hex_string := '${character:04x}' + + unsafe { encoder.output.push_many(hex_string.str, 4) } + + continue + } + if encoder.escape_unicode { + if character >= 0b1111_0000 { // four bytes + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + unicode_point_low := val[buffer_end..buffer_end + 4].bytes().byterune() or { + 0 + } - 0x10000 + + hex_string := '\\u${0xD800 + ((unicode_point_low >> 10) & 0x3FF):04X}\\u${ + 0xDC00 + (unicode_point_low & 0x3FF):04x}' + + buffer_end += 4 + buffer_start = buffer_end + + unsafe { encoder.output.push_many(hex_string.str, 12) } + + continue + } else if character >= 0b1110_0000 { // three bytes + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + hex_string := '\\u${val[buffer_end..buffer_end + 3].bytes().byterune() or { + 0 + }:04x}' + + buffer_end += 3 + buffer_start = buffer_end + + unsafe { encoder.output.push_many(hex_string.str, 6) } + + continue + } else if character >= 0b1100_0000 { // two bytes + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + hex_string := '\\u${val[buffer_end..buffer_end + 2].bytes().byterune() or { + 0 + }:04x}' + + buffer_end += 2 + buffer_start = buffer_end + + unsafe { encoder.output.push_many(hex_string.str, 6) } + + continue + } + } + + buffer_end++ + } + } + } + unsafe { encoder.output.push_many(val.str + buffer_start, buffer_end - buffer_start) } + + encoder.output << `"` +} + +fn (mut encoder Encoder) encode_boolean(val bool) { + if val { + unsafe { encoder.output.push_many(true_string.str, true_string.len) } + } else { + unsafe { encoder.output.push_many(false_string.str, false_string.len) } + } +} + +fn (mut encoder Encoder) encode_number[T](val T) { + integer_val := val.str() + $if T is $float { + if integer_val[integer_val.len - 1] == `0` { // ends in .0 + unsafe { + integer_val.len -= 2 + } + } + } + unsafe { encoder.output.push_many(integer_val.str, integer_val.len) } +} + +fn (mut encoder Encoder) encode_null() { + unsafe { encoder.output.push_many(null_string.str, null_string.len) } +} + +fn (mut encoder Encoder) encode_array[T](val []T) { + encoder.output << `[` + if encoder.prettify { + encoder.increment_level() + encoder.add_indent() + } + + for i, item in val { + encoder.encode_value(item) + if i < val.len - 1 { + encoder.output << `,` + if encoder.prettify { + encoder.add_indent() + } + } else { + if encoder.prettify { + encoder.decrement_level() + encoder.add_indent() + } + } + } + + encoder.output << `]` +} + +fn (mut encoder Encoder) encode_map[T](val map[string]T) { + encoder.output << `{` + if encoder.prettify { + encoder.increment_level() + encoder.add_indent() + } + + mut i := 0 + for key, value in val { + encoder.encode_string(key) + encoder.output << `:` + if encoder.prettify { + encoder.output << ` ` + } + encoder.encode_value(value) + if i < val.len - 1 { + encoder.output << `,` + if encoder.prettify { + encoder.add_indent() + } + } else { + if encoder.prettify { + encoder.decrement_level() + encoder.add_indent() + } + } + + i++ + } + + encoder.output << `}` +} + +fn (mut encoder Encoder) encode_enum[T](val T) { + if encoder.enum_as_int { + encoder.encode_number(workaround_cast[int](&val)) + } else { + mut enum_val := val.str() + $if val is $alias { + mut i := enum_val.len - 3 + for enum_val[i] != `(` { + i-- + } + + enum_val = enum_val[i + 1..enum_val.len - 1] + } + encoder.output << `"` + unsafe { encoder.output.push_many(enum_val.str, enum_val.len) } + encoder.output << `"` + } +} + +fn (mut encoder Encoder) encode_sumtype[T](val T) { + $for variant in T.variants { + if val is variant { + encoder.encode_value(val) + } + } +} + +struct EncoderFieldInfo { + key_name string + + is_skip bool + is_omitempty bool + is_required bool +} + +fn check_not_empty[T](val T) bool { + $if val is string { + if val == '' { + return false + } + } $else $if val is $int || val is $float { + if val == 0 { + return false + } + } + return true +} + +@[unsafe] +fn (mut encoder Encoder) encode_struct[T](val T) { + encoder.output << `{` + + static field_infos := &[]EncoderFieldInfo(nil) + + if field_infos == nil { + field_infos = &[]EncoderFieldInfo{} + + $for field in T.fields { + mut is_skip := false + mut key_name := '' + mut is_omitempty := false + mut is_required := false + + for attr in field.attrs { + match attr { + 'skip' { + is_skip = true + break + } + 'omitempty' { + is_omitempty = true + } + 'required' { + is_required = true + } + else {} + } + + if attr.starts_with('json:') { + if attr == 'json: -' { + is_skip = true + break + } + + key_name = attr[6..] + } + } + field_infos << EncoderFieldInfo{ + key_name: if key_name == '' { field.name } else { key_name } + is_skip: is_skip + is_omitempty: is_omitempty + is_required: is_required + } + } + } + + mut i := 0 + mut is_first := true + $for field in T.fields { + field_info := field_infos[i] + i++ + + mut write_field := true + + if field_info.is_skip { + write_field = false + } else { + value := val.$(field.name) + + if field_info.is_omitempty { + $if value is $option { + write_field = check_not_empty(value) + } $else { + write_field = check_not_empty(value) + } + } + + if !field_info.is_required { + $if value is $option { + if value == none { + write_field = false + } + } + } + } + + $if field.indirections != 0 { + if val.$(field.name) == unsafe { nil } { + write_field = false + } + } + + if write_field { + if is_first { + if encoder.prettify { + encoder.increment_level() + } + is_first = false + } else { + encoder.output << `,` + } + if encoder.prettify { + encoder.add_indent() + } + + encoder.encode_string(field_info.key_name) + + encoder.output << `:` + if encoder.prettify { + encoder.output << ` ` + } + + $if field is $option { + if val.$(field.name) == none { + unsafe { encoder.output.push_many(null_string.str, null_string.len) } + } else { + encoder.encode_value(val.$(field.name)) + } + } $else $if field.indirections == 1 { + encoder.encode_value(*val.$(field.name)) + } $else $if field.indirections == 2 { + encoder.encode_value(**val.$(field.name)) + } $else $if field.indirections == 3 { + encoder.encode_value(***val.$(field.name)) + } $else { + encoder.encode_value(val.$(field.name)) + } + } + } + if encoder.prettify && !is_first { + encoder.decrement_level() + encoder.add_indent() + } + + encoder.output << `}` +} + +fn (mut encoder Encoder) encode_custom[T](val T) { + integer_val := val.to_json() + unsafe { encoder.output.push_many(integer_val.str, integer_val.len) } +} + +fn (mut encoder Encoder) encode_custom2[T](val T) { + integer_val := val.json_str() + unsafe { encoder.output.push_many(integer_val.str, integer_val.len) } +} + +fn (mut encoder Encoder) increment_level() { + encoder.level++ + encoder.prefix = encoder.newline_string + encoder.indent_string.repeat(encoder.level) +} + +fn (mut encoder Encoder) decrement_level() { + encoder.level-- + encoder.prefix = encoder.newline_string + encoder.indent_string.repeat(encoder.level) +} + +fn (mut encoder Encoder) add_indent() { + unsafe { encoder.output.push_many(encoder.prefix.str, encoder.prefix.len) } +} diff --git a/vlib/x/json2/encoder.v b/vlib/x/json2/encoder.v index ff1b758827..2f7299df4a 100644 --- a/vlib/x/json2/encoder.v +++ b/vlib/x/json2/encoder.v @@ -3,427 +3,11 @@ // that can be found in the LICENSE file. module json2 -import time -import math -import strconv - -// Encoder encodes the an `Any` type into JSON representation. -// It provides parameters in order to change the end result. -pub struct Encoder { -pub: - newline u8 - newline_spaces_count int - escape_unicode bool = true -} - -// byte array versions of the most common tokens/chars to avoid reallocations -const null_in_string = 'null' - -const true_in_string = 'true' - -const false_in_string = 'false' - -const empty_array = [u8(`[`), `]`]! - -const comma_rune = `,` - -const colon_rune = `:` - -const quote_rune = `"` - -const back_slash = [u8(`\\`), `\\`]! - -const quote = [u8(`\\`), `"`]! - -const slash = [u8(`\\`), `/`]! - -const null_unicode = [u8(`\\`), `u`, `0`, `0`, `0`, `0`]! - -const ascii_control_characters = ['\\u0000', '\\t', '\\n', '\\r', '\\u0004', '\\u0005', '\\u0006', - '\\u0007', '\\b', '\\t', '\\n', '\\u000b', '\\f', '\\r', '\\u000e', '\\u000f', '\\u0010', - '\\u0011', '\\u0012', '\\u0013', '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', '\\u0019', - '\\u001a', '\\u001b', '\\u001c', '\\u001d', '\\u001e', '\\u001f']! - -const curly_open_rune = `{` - -const curly_close_rune = `}` - -const ascii_especial_characters = [u8(`\\`), `"`, `/`]! - -// encode is a generic function that encodes a type into a JSON string. -@[manualfree] -pub fn encode[T](val T) string { - $if T is $array { - return encode_array(val) - } $else { - mut count := Count{0} - count.count_chars(val) - - mut buf := []u8{cap: count.total} - - defer { - unsafe { buf.free() } - } - encoder := Encoder{} - - encoder.encode_value(val, mut buf) or { - println(err) - encoder.encode_value[string]('null', mut buf) or {} - } - - return buf.bytestr() - } -} - -// encode_array is a generic function that encodes a array into a JSON string. -@[manualfree] -fn encode_array[T](val []T) string { - if val.len == 0 { - return '[]' - } - - mut buf := []u8{} - - defer { - unsafe { buf.free() } - } - - encoder := Encoder{} - encoder.encode_array(val, 1, mut buf) or { - println(err) - encoder.encode_value[string]('null', mut buf) or {} - } - - return buf.bytestr() -} - // encode_pretty ... +@[deprecated: 'use `encode(..., prettify: true)` instead'] +@[deprecated_after: '2025-10-30'] pub fn encode_pretty[T](typed_data T) string { - encoded := encode(typed_data) - raw_decoded := decode[Any](encoded) or { 0 } - return raw_decoded.prettify_json_str() -} - -// encode_value encodes a value to the specific buffer. -pub fn (e &Encoder) encode_value[T](val T, mut buf []u8) ! { - e.encode_value_with_level[T](val, 1, mut buf)! -} - -fn (e &Encoder) encode_newline(level int, mut buf []u8) ! { - if e.newline != 0 { - buf << e.newline - for j := 0; j < level * e.newline_spaces_count; j++ { - buf << ` ` - } - } -} - -fn (e &Encoder) encode_map[T](value T, level int, mut buf []u8) ! { - buf << curly_open_rune - mut idx := 0 - for k, v in value { - e.encode_newline(level, mut buf)! - // e.encode_string(k.str(), mut buf)! - e.encode_string(k, mut buf)! - buf << colon_rune - if e.newline != 0 { - buf << ` ` - } - - // workaround to avoid `cannot convert 'struct x__json2__Any' to 'struct string'` - $if v is $sumtype { - $for variant_value in v.variants { - if v is variant_value { - e.encode_value_with_level(v, level + 1, mut buf)! - } - } - } $else { - e.encode_value_with_level(v, level + 1, mut buf)! - } - - if idx < value.len - 1 { - buf << comma_rune - } - idx++ - } - - e.encode_newline(level - 1, mut buf)! - buf << curly_close_rune -} - -fn (e &Encoder) encode_value_with_level[T](val T, level int, mut buf []u8) ! { - $if val is $option { - workaround := val - if workaround != none { - e.encode_value_with_level(val, level, mut buf)! - } - } $else $if T is string { - e.encode_string(val, mut buf)! - } $else $if T is $sumtype { - $for v in val.variants { - if val is v { - e.encode_value_with_level(val, level, mut buf)! - } - } - } $else $if T is $alias { - // TODO - } $else $if T is time.Time { - str_value := val.format_rfc3339() - buf << quote_rune - unsafe { buf.push_many(str_value.str, str_value.len) } - buf << quote_rune - } $else $if T is $map { - e.encode_map(val, level, mut buf)! - } $else $if T is $array { - e.encode_array(val, level, mut buf)! - } $else $if T is Encodable { - 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_string.str, null_in_string.len) } - } $else $if T is $struct { - e.encode_struct(val, level, mut buf)! - } $else $if T is $enum { - str_int := int(val).str() - unsafe { buf.push_many(str_int.str, str_int.len) } - } $else $if T is $int || T is bool { - str_int := val.str() - unsafe { buf.push_many(str_int.str, str_int.len) } - } $else $if T is $float { - str_float := encode_number(val) - unsafe { buf.push_many(str_float.str, str_float.len) } - } $else { - return error('cannot encode value with ${typeof(val).name} type') - } -} - -fn (e &Encoder) encode_struct[U](val U, level int, mut buf []u8) ! { - buf << curly_open_rune - mut i := 0 - mut fields_len := 0 - - $for field in U.fields { - mut @continue := false - for attr in field.attrs { - if attr.contains('skip') { - @continue = true - } - if attr.contains('json: ') { - if attr.replace('json: ', '') == '-' { - @continue = true - } - break - } - } - if !@continue { - $if field.is_option { - if val.$(field.name) != none { - fields_len++ - } - } $else { - fields_len++ - } - } - } - $for field in U.fields { - mut ignore_field := false - - value := val.$(field.name) - mut is_nil := false - $if value is $option { - if field.indirections > 0 { - is_nil = value == none - } - } $else $if field.indirections > 0 { - is_nil = value == unsafe { nil } - } - mut json_name := '' - - for attr in field.attrs { - if attr.contains('skip') { - ignore_field = true - } - if attr.contains('json: ') { - json_name = attr.replace('json: ', '') - if json_name == '-' { - ignore_field = true - } - break - } - } - - if !ignore_field { - $if value is $option { - workaround := val.$(field.name) - if workaround != none { // smartcast - e.encode_newline(level, mut buf)! - if json_name != '' { - e.encode_string(json_name, mut buf)! - } else { - e.encode_string(field.name, mut buf)! - } - buf << colon_rune - - if e.newline != 0 { - buf << ` ` - } - e.encode_value_with_level(value, level, mut buf)! - } else { - ignore_field = true - } - } $else { - is_none := val.$(field.name).str() == 'unknown sum type value' // assert json.encode(StructType[SumTypes]{}) == '{}' - if !is_none && !is_nil { - e.encode_newline(level, mut buf)! - if json_name != '' { - e.encode_string(json_name, mut buf)! - } else { - e.encode_string(field.name, mut buf)! - } - buf << colon_rune - - if e.newline != 0 { - buf << ` ` - } - } - - $if field.indirections != 0 { - if val.$(field.name) != unsafe { nil } { - $if field.indirections == 1 { - e.encode_value_with_level(*val.$(field.name), level + 1, mut - buf)! - } - $if field.indirections == 2 { - e.encode_value_with_level(**val.$(field.name), level + 1, mut - buf)! - } - $if field.indirections == 3 { - e.encode_value_with_level(***val.$(field.name), level + 1, mut - buf)! - } - } - } $else $if field.typ is string { - e.encode_string(val.$(field.name).str(), mut buf)! - } $else $if field.typ is time.Time { - str_value := val.$(field.name).format_rfc3339() - buf << quote_rune - unsafe { buf.push_many(str_value.str, str_value.len) } - buf << quote_rune - } $else $if field.typ is bool { - if value { - unsafe { buf.push_many(true_in_string.str, true_in_string.len) } - } else { - unsafe { buf.push_many(false_in_string.str, false_in_string.len) } - } - } $else $if field.typ is $int { - str_value := val.$(field.name).str() - unsafe { buf.push_many(str_value.str, str_value.len) } - } $else $if field.typ is $float { - str_value := encode_number(val.$(field.name)) - unsafe { buf.push_many(str_value.str, str_value.len) } - } $else $if field.is_array { - // TODO: replace for `field.typ is $array` - e.encode_array(value, level + 1, mut buf)! - } $else $if field.typ is $array { - // e.encode_array(value, level + 1, mut buf)! // 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 buf)! - } $else $if field.is_map { - e.encode_map(value, level + 1, mut buf)! - } $else $if field.is_enum { - // TODO: replace for `field.typ is $enum` - // str_value := int(val.$(field.name)).str() - // unsafe { buf.push_many(str_value.str, str_value.len) } - e.encode_value_with_level(val.$(field.name), level + 1, mut buf)! - } $else $if field.typ is $enum { - } $else $if field.typ is $sumtype { - field_value := val.$(field.name) - if field_value.str() != 'unknown sum type value' { - $for v in field_value.variants { - if field_value is v { - e.encode_value_with_level(field_value, level, mut buf)! - } - } - } - } $else $if field.typ is $alias { - $if field.unaliased_typ is string { - e.encode_string(val.$(field.name).str(), mut buf)! - } $else $if field.unaliased_typ is time.Time { - parsed_time := time.parse(val.$(field.name).str()) or { time.Time{} } - e.encode_string(parsed_time.format_rfc3339(), mut buf)! - } $else $if field.unaliased_typ is bool { - if val.$(field.name) { - unsafe { buf.push_many(true_in_string.str, true_in_string.len) } - } else { - unsafe { buf.push_many(false_in_string.str, false_in_string.len) } - } - } $else $if field.unaliased_typ is $int { - str_value := val.$(field.name).str() - unsafe { buf.push_many(str_value.str, str_value.len) } - } $else $if field.unaliased_typ is $float { - str_value := encode_number(val) - unsafe { buf.push_many(str_value.str, str_value.len) } - } $else $if field.unaliased_typ is $array { - // TODO - } $else $if field.unaliased_typ is $struct { - e.encode_struct(value, level + 1, mut buf)! - } $else $if field.unaliased_typ is $enum { - // TODO - } $else $if field.unaliased_typ is $sumtype { - // TODO - } $else { - return error('the alias ${typeof(val).name} cannot be encoded') - } - } $else { - return error('type ${typeof(val).name} cannot be array encoded') - } - } - } - - if i < fields_len - 1 && !ignore_field { - if !is_nil { - buf << comma_rune - } - } - if !ignore_field { - i++ - } - } - e.encode_newline(level - 1, mut buf)! - buf << curly_close_rune - // b.measure('encode_struct') -} - -fn (e &Encoder) encode_array[U](val []U, level int, mut buf []u8) ! { - if val.len == 0 { - unsafe { buf.push_many(&empty_array[0], empty_array.len) } - return - } - buf << `[` - for i in 0 .. val.len { - e.encode_newline(level, mut buf)! - - $if U is string || U is bool || U is $int || U is $float { - e.encode_value_with_level(val[i], level + 1, mut buf)! - } $else $if U is $array { - e.encode_array(val[i], level + 1, mut buf)! - } $else $if U is $struct { - e.encode_struct(val[i], level + 1, mut buf)! - } $else $if U is $sumtype { - e.encode_value_with_level(val[i], level + 1, mut buf)! - } $else $if U is $enum { - // TODO: test - e.encode_value_with_level(val[i], level + 1, mut buf)! - } $else { - return error('type ${typeof(val).name} cannot be array encoded') - } - if i < val.len - 1 { - buf << comma_rune - } - } - - e.encode_newline(level - 1, mut buf)! - buf << `]` + return encode(typed_data, prettify: true) } // str returns the JSON string representation of the `map[string]Any` type. @@ -452,161 +36,8 @@ pub fn (f Any) json_str() string { } // prettify_json_str returns the pretty-formatted JSON string representation of the `Any` type. -@[manualfree] +@[deprecated: 'use `encode(Any(...), prettify: true)` instead'] +@[deprecated_after: '2025-10-30'] pub fn (f Any) prettify_json_str() string { - mut buf := []u8{} - defer { - unsafe { buf.free() } - } - mut enc := Encoder{ - newline: `\n` - newline_spaces_count: 2 - } - enc.encode_value(f, mut buf) or {} - return buf.bytestr() -} - -// TODO: Need refactor. Is so slow. The longer the string, the lower the performance. -// encode_string returns the JSON spec-compliant version of the string. -@[direct_array_access] -fn (e &Encoder) encode_string(s string, mut buf []u8) ! { - if s == '' { - empty := [u8(quote_rune), quote_rune]! - unsafe { buf.push_many(&empty[0], 2) } - return - } - mut last_no_buffer_expansible_char_position_candidate := 0 - buf << quote_rune - - if !e.escape_unicode { - unsafe { - buf.push_many(s.str, s.len) - buf << quote_rune - } - return - } - - for idx := 0; idx < s.len; idx++ { - current_byte := s[idx] - - mut current_utf8_len := ((0xe5000000 >> ((current_byte >> 3) & 0x1e)) & 3) + 1 - - current_value_cause_buffer_expansion := - (current_utf8_len == 1 && ((current_byte < 32 || current_byte > 127) - || current_byte in ascii_especial_characters)) || current_utf8_len == 3 - - if !current_value_cause_buffer_expansion { - // while it is not the last one - if idx < s.len - 1 { - if s.len > (idx + current_utf8_len) { - if current_utf8_len == 2 || current_utf8_len == 4 { - // runes like: ã, ü, etc. - // Emojis ranges - // (0x1F300, 0x1F5FF), # Miscellaneous Symbols and Pictographs - // (0x1F600, 0x1F64F), # Emoticons - // (0x1F680, 0x1F6FF), # Transport and Map Symbols - idx += current_utf8_len - 1 - continue - } - } else { - unsafe { - buf.push_many(s.str + last_no_buffer_expansible_char_position_candidate, - s.len - last_no_buffer_expansible_char_position_candidate) - } - break - } - } else if idx == s.len - 1 { - unsafe { - buf.push_many(s.str + last_no_buffer_expansible_char_position_candidate, - s.len - last_no_buffer_expansible_char_position_candidate) - } - } - } else { - if idx > 0 { - length := idx - last_no_buffer_expansible_char_position_candidate - unsafe { - buf.push_many(s.str + last_no_buffer_expansible_char_position_candidate, - length) - } - last_no_buffer_expansible_char_position_candidate = idx + 1 - } - } - - if current_utf8_len == 1 { - if current_byte < 32 { - // ASCII Control Characters - unsafe { - buf.push_many(ascii_control_characters[current_byte].str, ascii_control_characters[current_byte].len) - } - last_no_buffer_expansible_char_position_candidate = idx + 1 - } else if current_byte >= 32 && current_byte < 128 { - // ASCII especial characters - if current_byte == `\\` { - unsafe { buf.push_many(&back_slash[0], back_slash.len) } - last_no_buffer_expansible_char_position_candidate = idx + 1 - continue - } else if current_byte == `"` { - unsafe { buf.push_many("e[0], quote.len) } - last_no_buffer_expansible_char_position_candidate = idx + 1 - continue - } else if current_byte == `/` { - unsafe { buf.push_many(&slash[0], slash.len) } - last_no_buffer_expansible_char_position_candidate = idx + 1 - continue - } - } - continue - } else if current_utf8_len == 3 { - // runes like: ✔, ひらがな ... - - // Handle multi-byte characters byte-by-byte - mut codepoint := u32(current_byte & ((1 << (7 - current_utf8_len)) - 1)) - for j in 1 .. current_utf8_len { - if idx + j >= s.len { - // Incomplete UTF-8 sequence, TODO handle error - idx++ - continue - } - - mut b := s[idx + j] - if (b & 0xC0) != 0x80 { - // Invalid continuation byte, TODO handle error - idx++ - continue - } - - codepoint = u32((codepoint << 6) | (b & 0x3F)) - } - // runes like: ✔, ひらがな ... - unsafe { buf.push_many(&null_unicode[0], null_unicode.len) } - buf[buf.len - 1] = hex_digit(codepoint & 0xF) - buf[buf.len - 2] = hex_digit((codepoint >> 4) & 0xF) - buf[buf.len - 3] = hex_digit((codepoint >> 8) & 0xF) - buf[buf.len - 4] = hex_digit((codepoint >> 12) & 0xF) - idx += current_utf8_len - 1 - last_no_buffer_expansible_char_position_candidate = idx + 1 - } - } - - buf << quote_rune -} - -fn hex_digit(n u32) u8 { - if n < 10 { - return `0` + n - } - return `a` + (n - 10) -} - -fn encode_number(value f64) string { - if math.is_nan(value) || math.is_inf(value, 0) { - return 'null' - } else if value == f64(int(value)) { - return int(value).str() - } else { - // TODO:cjson Try 15 decimal places of precision to avoid nonsignificant nonzero digits - // If not, print with 17 decimal places of precision - // strconv.f64_to_str_l try max 18 digits instead. - return strconv.f64_to_str_l(value) - } + return encode(f, prettify: true) } diff --git a/vlib/x/json2/tests/any_test.v b/vlib/x/json2/tests/any_test.v index a232f35e9d..9db959304f 100644 --- a/vlib/x/json2/tests/any_test.v +++ b/vlib/x/json2/tests/any_test.v @@ -337,11 +337,11 @@ fn test_str() { assert sample_data['i32'] or { 0 }.str() == '7' assert sample_data['int'] or { 0 }.str() == '8' assert sample_data['i64'] or { 0 }.str() == '9' - assert sample_data['f32'] or { 0 }.str() == '2.299999952316284' + assert sample_data['f32'] or { 0 }.str() == '2.3' assert sample_data['f64'] or { 0 }.str() == '1.283' assert sample_data['bool'] or { 0 }.str() == 'false' assert sample_data['str'] or { 0 }.str() == 'test' assert sample_data['null'] or { 0 }.str() == 'null' assert sample_data['arr'] or { 'not lol' }.str() == '["lol"]' - assert sample_data.str() == '{"u8":1,"u16":2,"u32":3,"u64":4,"i8":5,"i16":6,"i32":7,"int":8,"i64":9,"f32":2.299999952316284,"f64":1.283,"bool":false,"str":"test","null":null,"arr":["lol"],"obj":{"foo":10}}' + assert sample_data.str() == '{"u8":1,"u16":2,"u32":3,"u64":4,"i8":5,"i16":6,"i32":7,"int":8,"i64":9,"f32":2.3,"f64":1.283,"bool":false,"str":"test","null":null,"arr":["lol"],"obj":{"foo":10}}' } diff --git a/vlib/x/json2/tests/encode_struct_test.v b/vlib/x/json2/tests/encode_struct_test.v index 781e97781b..0d9a52bdc5 100644 --- a/vlib/x/json2/tests/encode_struct_test.v +++ b/vlib/x/json2/tests/encode_struct_test.v @@ -65,11 +65,19 @@ fn test_types() { } }) == '{"val":{"val":1}}' - assert json.encode(StructType[Enumerates]{}) == '{"val":0}' - assert json.encode(StructType[Enumerates]{ val: Enumerates.a }) == '{"val":0}' - assert json.encode(StructType[Enumerates]{ val: Enumerates.d }) == '{"val":3}' - assert json.encode(StructType[Enumerates]{ val: Enumerates.e }) == '{"val":99}' - assert json.encode(StructType[Enumerates]{ val: Enumerates.f }) == '{"val":100}' + assert json.encode(StructType[Enumerates]{}, enum_as_int: true) == '{"val":0}' + assert json.encode(StructType[Enumerates]{ val: Enumerates.a }, + enum_as_int: true + ) == '{"val":0}' + assert json.encode(StructType[Enumerates]{ val: Enumerates.d }, + enum_as_int: true + ) == '{"val":3}' + assert json.encode(StructType[Enumerates]{ val: Enumerates.e }, + enum_as_int: true + ) == '{"val":99}' + assert json.encode(StructType[Enumerates]{ val: Enumerates.f }, + enum_as_int: true + ) == '{"val":100}' } fn test_option_types() { diff --git a/vlib/x/json2/tests/encode_test.v b/vlib/x/json2/tests/encode_test.v new file mode 100644 index 0000000000..4e93fb6768 --- /dev/null +++ b/vlib/x/json2/tests/encode_test.v @@ -0,0 +1,330 @@ +import json2 as json +import time +import math.big + +type StrAlias = string +type BoolAlias = bool +type IntAlias = int +type FloatAlias = f64 + +enum TestEnum { + a + b + c = 10 +} + +type EnumAlias = TestEnum + +type Sum = int | string +type SumAlias = Sum + +struct Basic { + a int + b string + c bool +} + +type BasicAlias = Basic + +struct Opt { + a ?int +} + +type OptAlias = Opt + +struct OptRequiered { + a ?int @[required] +} + +type OptRequieredAlias = OptRequiered + +struct CustomString { + data string +} + +pub fn (cs CustomString) to_json() string { + return '"<<<' + cs.data + '>>>"' +} + +type CustomStringAlias = CustomString + +type NullAlias = json.Null + +type TimeAlias = time.Time + +type BigAlias = big.Integer + +struct NamedFields { + a int @[json: 'id'] + name string @[json: 'Name'] +} + +type NamedFieldsAlias = NamedFields + +struct SkipFields { + a int @[json: '-'] + name string @[skip] +} + +type SkipFieldsAlias = SkipFields + +struct SkipSomeFields { + a int @[json: '-'] + name string @[skip] + hi bool = true +} + +type SkipSomeFieldsAlias = SkipSomeFields + +struct PointerFields { + next &PointerFields = unsafe { nil } + data int +} + +type PointerFieldsAlias = PointerFields + +struct OmitFields { + a ?bool @[omitempty] + b string @[omitempty] + c int @[omitempty] + d f64 @[omitempty] + e ?string = '' @[omitempty] + f ?int = 0 @[omitempty] + g ?f64 = 0.0 @[omitempty] +} + +type OmitFieldsAlias = OmitFields + +fn test_primitives() { + assert json.encode('hello') == '"hello"' + assert json.encode(StrAlias('hello')) == '"hello"' + assert json.encode(true) == 'true' + assert json.encode(BoolAlias(false)) == 'false' + assert json.encode(-12345) == '-12345' + assert json.encode(IntAlias(-12345)) == '-12345' + assert json.encode(123.323) == '123.323' + assert json.encode(FloatAlias(123.323)) == '123.323' +} + +fn test_arrays() { + assert json.encode([1, 2, 3, 4]) == '[1,2,3,4]' +} + +fn test_maps() { + assert json.encode({ + 'hi': 0 + 'bye': 1 + }) == '{"hi":0,"bye":1}' +} + +fn test_enums() { + assert json.encode(TestEnum.c) == '"c"' + assert json.encode(EnumAlias(TestEnum.c)) == '"c"' + assert json.encode(TestEnum.c, enum_as_int: true) == '10' + assert json.encode(EnumAlias(TestEnum.c), enum_as_int: true) == '10' +} + +fn test_sumtypes() { + assert json.encode(Sum(10)) == '10' + assert json.encode(Sum('hi')) == '"hi"' + assert json.encode(SumAlias(10)) == '10' + assert json.encode(SumAlias('hi')) == '"hi"' +} + +fn test_basic_structs() { + assert json.encode(Basic{ + a: 10 + b: 'hi' + c: true + }) == '{"a":10,"b":"hi","c":true}' + + assert json.encode(BasicAlias{ + a: 10 + b: 'hi' + c: true + }) == '{"a":10,"b":"hi","c":true}' +} + +fn test_nested() { + assert json.encode([ + { + 'hi': Basic{ a: 1, b: 'a', c: false } + 'bye': Basic{ + a: 2 + b: 'b' + c: true + } + }, + { + 'hi2': Basic{ + a: 3 + b: 'c' + c: false + } + 'bye2': Basic{ + a: 4 + b: 'd' + c: true + } + }, + ]) == '[{"hi":{"a":1,"b":"a","c":false},"bye":{"a":2,"b":"b","c":true}},{"hi2":{"a":3,"b":"c","c":false},"bye2":{"a":4,"b":"d","c":true}}]' + assert json.encode([ + { + 'hi': Basic{ a: 1, b: 'a', c: false } + 'bye': Basic{ + a: 2 + b: 'b' + c: true + } + }, + { + 'hi2': Basic{ + a: 3 + b: 'c' + c: false + } + 'bye2': Basic{ + a: 4 + b: 'd' + c: true + } + }, + ], + prettify: true + ) == '[ + { + "hi": { + "a": 1, + "b": "a", + "c": false + }, + "bye": { + "a": 2, + "b": "b", + "c": true + } + }, + { + "hi2": { + "a": 3, + "b": "c", + "c": false + }, + "bye2": { + "a": 4, + "b": "d", + "c": true + } + } +]' +} + +fn test_string_escapes() { + assert json.encode('normal escapes: ", \\ special control escapes: \b, \n, \f, \t, \r, other control escapes: \0, \u001b') == r'"normal escapes: \", \\ special control escapes: \b, \n, \f, \t, \r, other control escapes: \u0000, \u001b"' + assert json.encode('ascii, é, 한, 😀, ascii') == r'"ascii, é, 한, 😀, ascii"' + assert json.encode('ascii, é, 한, 😀, ascii', escape_unicode: true) == r'"ascii, \u00e9, \ud55c, \uD83D\ude00, ascii"' +} + +fn test_options() { + assert json.encode(Opt{none}) == '{}' + assert json.encode(Opt{99}) == '{"a":99}' + assert json.encode(OptAlias{none}) == '{}' + assert json.encode(OptAlias{99}) == '{"a":99}' + + assert json.encode(OptRequiered{none}) == '{"a":null}' + assert json.encode(OptRequiered{99}) == '{"a":99}' + assert json.encode(OptRequieredAlias{none}) == '{"a":null}' + assert json.encode(OptRequieredAlias{99}) == '{"a":99}' +} + +fn test_custom_encoders() { + assert json.encode(CustomString{'hi'}) == '"<<>>"' + assert json.encode(CustomStringAlias{'hi'}) == '"<<>>"' + + assert json.encode(json.Null{}) == 'null' + assert json.encode(NullAlias{}) == 'null' + + assert json.encode(time.Time{}) == '"0000-00-00T00:00:00.000Z"' + assert json.encode(TimeAlias{}) == '"0000-00-00T00:00:00.000Z"' + + assert json.encode(big.integer_from_i64(1234567890)) == '1234567890' + assert json.encode(BigAlias(big.integer_from_i64(1234567890))) == '1234567890' +} + +fn test_named_fields() { + assert json.encode(NamedFields{ a: 1, name: 'john' }) == '{"id":1,"Name":"john"}' + assert json.encode(NamedFieldsAlias{ a: 1, name: 'john' }) == '{"id":1,"Name":"john"}' +} + +fn test_skip_fields() { + assert json.encode(SkipFields{ a: 1, name: 'john' }) == '{}' + assert json.encode(SkipFieldsAlias{ a: 1, name: 'john' }) == '{}' + assert json.encode(SkipFields{ a: 1, name: 'john' }, + prettify: true + ) == '{}' + + assert json.encode(SkipSomeFields{ a: 1, name: 'john' }) == '{"hi":true}' + assert json.encode(SkipSomeFieldsAlias{ a: 1, name: 'john' }) == '{"hi":true}' + assert json.encode(SkipSomeFields{ a: 1, name: 'john' }, + prettify: true + ) == '{ + "hi": true +}' +} + +fn test_omit_fields() { + assert json.encode(OmitFields{}) == '{}' + assert json.encode(OmitFieldsAlias{}) == '{}' +} + +fn test_pointer_fields() { + assert json.encode(PointerFields{ + next: &PointerFields{ + next: &PointerFields{ + next: &PointerFields{ + data: 4 + } + data: 3 + } + data: 2 + } + data: 1 + }) == '{"next":{"next":{"next":{"data":4},"data":3},"data":2},"data":1}' + assert json.encode(PointerFieldsAlias{ + next: &PointerFieldsAlias{ + next: &PointerFieldsAlias{ + next: &PointerFieldsAlias{ + data: 4 + } + data: 3 + } + data: 2 + } + data: 1 + }) == '{"next":{"next":{"next":{"data":4},"data":3},"data":2},"data":1}' + assert json.encode(PointerFields{ + next: &PointerFields{ + next: &PointerFields{ + next: &PointerFields{ + data: 4 + } + data: 3 + } + data: 2 + } + data: 1 + }, + prettify: true + ) == '{ + "next": { + "next": { + "next": { + "data": 4 + }, + "data": 3 + }, + "data": 2 + }, + "data": 1 +}' +} diff --git a/vlib/x/json2/tests/encoder_test.v b/vlib/x/json2/tests/encoder_test.v index ff30262583..37fc3f2f53 100644 --- a/vlib/x/json2/tests/encoder_test.v +++ b/vlib/x/json2/tests/encoder_test.v @@ -8,19 +8,19 @@ mut: } fn test_json_string_characters() { - assert json.encode([u8(`/`)].bytestr()).bytes() == r'"\/"'.bytes() + assert json.encode([u8(`/`)].bytestr()).bytes() == r'"/"'.bytes() assert json.encode([u8(`\\`)].bytestr()).bytes() == r'"\\"'.bytes() assert json.encode([u8(`"`)].bytestr()).bytes() == r'"\""'.bytes() assert json.encode([u8(`\n`)].bytestr()).bytes() == r'"\n"'.bytes() assert json.encode(r'\n\r') == r'"\\n\\r"' assert json.encode('\\n') == r'"\\n"' assert json.encode(r'\n\r\b') == r'"\\n\\r\\b"' - assert json.encode(r'\"/').bytes() == r'"\\\"\/"'.bytes() + assert json.encode(r'\"/').bytes() == r'"\\\"/"'.bytes() - assert json.encode(r'\n\r\b\f\t\\\"\/') == r'"\\n\\r\\b\\f\\t\\\\\\\"\\\/"' + assert json.encode(r'\n\r\b\f\t\\\"\/') == r'"\\n\\r\\b\\f\\t\\\\\\\"\\/"' text := json.raw_decode(r'"\n\r\b\f\t\\\"\/"') or { '' } - assert text.json_str() == '"\\n\\r\\b\\f\\t\\\\\\"\\/"' + assert text.json_str() == '"\\n\\r\\b\\f\\t\\\\\\"/"' assert json.encode("fn main(){nprintln('Hello World! Helo \$a')\n}") == '"fn main(){nprintln(\'Hello World! Helo \$a\')\\n}"' assert json.encode(' And when "\'s are in the string, along with # "') == '" And when \\"\'s are in the string, along with # \\""' @@ -42,8 +42,8 @@ fn test_json_escape_low_chars() { fn test_json_string() { text := json.Any('te✔st') - assert text.json_str() == r'"te\u2714st"' - assert json.encode('te✔st') == r'"te\u2714st"' + assert text.json_str() == r'"te✔st"' + assert json.encode('te✔st', escape_unicode: true) == r'"te\u2714st"' boolean := json.Any(true) assert boolean.json_str() == 'true' @@ -67,9 +67,9 @@ fn test_json_string_emoji() { fn test_json_string_non_ascii() { text := json.Any('ひらがな') - assert text.json_str() == r'"\u3072\u3089\u304c\u306a"' + assert text.json_str() == r'"ひらがな"' - assert json.encode('ひらがな') == r'"\u3072\u3089\u304c\u306a"' + assert json.encode('ひらがな', escape_unicode: true) == r'"\u3072\u3089\u304c\u306a"' } fn test_utf8_strings_are_not_modified() { @@ -83,30 +83,13 @@ fn test_utf8_strings_are_not_modified() { fn test_encoder_unescaped_utf32() ! { jap_text := json.Any('ひらがな') - enc := json.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) + assert json.encode(jap_text) == '"${jap_text}"' emoji_text := json.Any('🐈') - enc.encode_value(emoji_text, mut sb)! - assert sb.str() == '"${emoji_text}"' + assert json.encode(emoji_text) == '"${emoji_text}"' - mut buf := []u8{cap: 14} - - enc.encode_value('ひらがな', mut buf)! - - assert buf.len == 14 - assert buf.bytestr() == '"ひらがな"' + assert json.encode('ひらがな') == '"ひらがな"' } fn test_encoder_prettify() { @@ -117,16 +100,7 @@ fn test_encoder_prettify() { 'map': json.Any('map inside a map') } } - enc := json.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() == '{ + assert json.encode(obj, prettify: true, indent_string: ' ') == '{ "hello": "world", "arr": [ "im a string", @@ -180,35 +154,13 @@ fn test_encode_simple() { } fn test_encode_value() { - json_enc := json.Encoder{ - newline: `\n` - newline_spaces_count: 2 - escape_unicode: false - } - mut manifest := map[string]json.Any{} manifest['server_path'] = json.Any('new_path') manifest['last_updated'] = json.Any('timestamp.format_ss()') manifest['from_source'] = json.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'{ + assert json.encode(manifest, prettify: true, indent_string: ' ') == r'{ "server_path": "new_path", "last_updated": "timestamp.format_ss()", "from_source": "from_source" diff --git a/vlib/x/json2/tests/json_module_compatibility_test/json_test.v b/vlib/x/json2/tests/json_module_compatibility_test/json_test.v index c466479344..26840e9c4f 100644 --- a/vlib/x/json2/tests/json_module_compatibility_test/json_test.v +++ b/vlib/x/json2/tests/json_module_compatibility_test/json_test.v @@ -29,7 +29,7 @@ fn test_simple() { name: 'João' } x := Employee{'Peter', 28, 95000.5, .worker, sub_employee} - s := json.encode[Employee](x) + s := json.encode[Employee](x, enum_as_int: true) 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 { @@ -232,7 +232,7 @@ struct Foo2 { fn test_pretty() { foo := Foo2{1, 2, 3, 4, -1, -2, -3, -4, true, 'abc', 'aliens'} - assert json.encode_pretty(foo) == '{ + assert json.encode(foo, prettify: true, indent_string: ' ') == '{ "ux8": 1, "ux16": 2, "ux32": 3, @@ -387,7 +387,7 @@ fn test_encode_decode_sumtype() { ] } - enc := json.encode(game) + enc := json.encode(game, enum_as_int: true) assert enc == '{"title":"Super Mega Game","player":{"name":"Monke"},"other":[{"tag":"Pen"},{"tag":"Cookie"},1,"Stool","${t.format_rfc3339()}"]}' } @@ -414,7 +414,7 @@ fn test_option_instead_of_omit_empty() { foo := Foo31{ name: 'Bob' } - assert json.encode_pretty(foo) == '{ + assert json.encode(foo, prettify: true, indent_string: ' ') == '{ "name": "Bob" }' } diff --git a/vlib/x/json2/types.v b/vlib/x/json2/types.v index 4c13bcf1a8..b9a71fd5b6 100644 --- a/vlib/x/json2/types.v +++ b/vlib/x/json2/types.v @@ -23,11 +23,6 @@ pub type Any = []Any | u8 | Null -// Encodable is an interface, that allows custom implementations for encoding structs to their string based JSON representations. -pub interface Encodable { - json_str() string -} - // Null is a simple representation of the `null` value in JSON. pub struct Null { is_null bool = true @@ -39,6 +34,11 @@ pub const null = Null{} // from_json_null implements a custom decoder for json2 pub fn (mut n Null) from_json_null() {} +// to_json implements a custom encoder for json2 +pub fn (n Null) to_json() string { + return 'null' +} + // ValueKind enumerates the kinds of possible values of the Any sumtype. enum ValueKind { unknown @@ -49,16 +49,3 @@ enum ValueKind { boolean null } - -// str returns the string representation of the specific ValueKind. -fn (k ValueKind) str() string { - return match k { - .unknown { 'unknown' } - .array { 'array' } - .object { 'object' } - .string { 'string' } - .number { 'number' } - .boolean { 'boolean' } - .null { 'null' } - } -}