replace json2 encoder and rewrite tests

This commit is contained in:
Larsimusrex 2025-09-02 20:33:22 +02:00
parent d31aaecc42
commit 5d4b91b03e
19 changed files with 567 additions and 1102 deletions

View file

@ -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()
}

View file

@ -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() + '"'
}

5
vlib/x/json2/constants.v Normal file
View file

@ -0,0 +1,5 @@
module json2
const true_string = 'true'
const false_string = 'false'
const null_string = 'null'

View file

@ -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 // ""
}

View file

@ -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] })
}

15
vlib/x/json2/custom.v Normal file
View file

@ -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
}

View file

@ -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,
@ -23,7 +24,7 @@ fn test_values() {
// 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[OptAnyStruct[json2.Any]](OptAnyStruct[json2.Any]{none}) == '{"val":null}'
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
}

View file

@ -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('test')
assert text.json_str() == r'"te\u2714st"'
assert json2.encode('test') == 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"'
}

View file

@ -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,8 +412,9 @@ fn test_option_instead_of_omit_empty() {
foo := Foo31{
name: 'Bob'
}
assert json2.encode_pretty(foo) == '{
"name": "Bob"
assert json2.encode(foo, prettify: true, indent_string: ' ') == '{
"name": "Bob",
"age": null
}'
}

466
vlib/x/json2/encode.v Normal file
View file

@ -0,0 +1,466 @@
module json2
@[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))
}
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
}
@[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
for attr in field.attrs {
match attr {
'skip' {
is_skip = true
break
}
'omitempty' {
is_omitempty = 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
}
}
}
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 if field_info.is_omitempty {
value := val.$(field.name)
$if value is $option {
if value == none {
write_field = false
}
} $else $if value is string {
if value == '' {
write_field = false
}
} $else $if value is $int || value is $float {
if value == 0 {
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) }
}

View file

@ -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(&quote[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)
}

View file

@ -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}}'
}

View file

@ -22,7 +22,7 @@ fn test_values() {
// assert json.decode[OptAnyArrStruct]('{"val":[5,null,10]}')!.val == [?json.Any(5),json.Null{},10] skipped because test still fails even though they're the same
assert json.encode[AnyStruct[json.Any]](AnyStruct[json.Any]{json.Any(5)}) == '{"val":5}'
assert json.encode[OptAnyStruct[json.Any]](OptAnyStruct[json.Any]{none}) == '{}'
assert json.encode[OptAnyStruct[json.Any]](OptAnyStruct[json.Any]{none}) == '{"val":null}'
assert json.encode[AnyStruct[[]json.Any]](AnyStruct[[]json.Any]{[json.Any(5), 10]}) == '{"val":[5,10]}'
// assert json.encode[OptAnyArrStruct](OptAnyArrStruct{[?json.Any(5),none,10]}) == '{"val":[5,null,10]}' encode_array has not implemented optional arrays yet
}

View file

@ -20,5 +20,5 @@ fn test_main() {
fn test_none() {
disk := Disk{}
disk_str := json2.encode[Disk](disk)
assert disk_str == '{"dev":""}'
assert disk_str == '{"dev":"","size":null}'
}

View file

@ -9,5 +9,5 @@ pub mut:
fn test_main() {
res := json2.encode(JoseHeader{ alg: 'HS256' })
assert res == '{"alg":"HS256","typ":"JWT"}'
assert res == '{"cty":null,"alg":"HS256","typ":"JWT"}'
}

View file

@ -65,30 +65,38 @@ 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() {
assert json.encode(StructTypeOption[string]{ val: none }) == '{}'
assert json.encode(StructTypeOption[string]{}) == '{}'
assert json.encode(StructTypeOption[string]{ val: none }) == '{"val":null}'
assert json.encode(StructTypeOption[string]{}) == '{"val":null}'
assert json.encode(StructTypeOption[string]{ val: '' }) == '{"val":""}'
assert json.encode(StructTypeOption[string]{ val: 'a' }) == '{"val":"a"}'
assert json.encode(StructTypeOption[bool]{ val: none }) == '{}'
assert json.encode(StructTypeOption[bool]{}) == '{}'
assert json.encode(StructTypeOption[bool]{ val: none }) == '{"val":null}'
assert json.encode(StructTypeOption[bool]{}) == '{"val":null}'
assert json.encode(StructTypeOption[bool]{ val: false }) == '{"val":false}'
assert json.encode(StructTypeOption[bool]{ val: true }) == '{"val":true}'
assert json.encode(StructTypeOption[int]{ val: none }) == '{}'
assert json.encode(StructTypeOption[int]{}) == '{}'
assert json.encode(StructTypeOption[int]{ val: none }) == '{"val":null}'
assert json.encode(StructTypeOption[int]{}) == '{"val":null}'
assert json.encode(StructTypeOption[int]{ val: 0 }) == '{"val":0}'
assert json.encode(StructTypeOption[int]{ val: 1 }) == '{"val":1}'
assert json.encode(StructTypeOption[time.Time]{}) == '{}'
assert json.encode(StructTypeOption[time.Time]{}) == '{"val":null}'
assert json.encode(StructTypeOption[time.Time]{ val: time.Time{} }) == '{"val":"0000-00-00T00:00:00.000Z"}'
assert json.encode(StructTypeOption[time.Time]{ val: fixed_time }) == '{"val":"2022-03-11T13:54:25.000Z"}'
@ -98,7 +106,7 @@ fn test_option_types() {
}
}) == '{"val":{"val":1}}'
assert json.encode(StructTypeOption[Enumerates]{}) == '{}'
assert json.encode(StructTypeOption[Enumerates]{}) == '{"val":null}'
}
fn test_array() {
@ -157,42 +165,42 @@ fn test_array() {
}
fn test_option_array() {
assert json.encode(StructTypeOption[[]string]{}) == '{}'
assert json.encode(StructTypeOption[[]string]{}) == '{"val":null}'
assert json.encode(StructTypeOption[[]string]{ val: [] }) == '{"val":[]}'
assert json.encode(StructTypeOption[[]string]{ val: ['0'] }) == '{"val":["0"]}'
assert json.encode(StructTypeOption[[]string]{ val: ['1'] }) == '{"val":["1"]}'
assert json.encode(StructTypeOption[[]int]{}) == '{}'
assert json.encode(StructTypeOption[[]int]{}) == '{"val":null}'
assert json.encode(StructTypeOption[[]int]{ val: [] }) == '{"val":[]}'
assert json.encode(StructTypeOption[[]int]{ val: [0] }) == '{"val":[0]}'
assert json.encode(StructTypeOption[[]int]{ val: [1] }) == '{"val":[1]}'
assert json.encode(StructTypeOption[[]int]{ val: [0, 1, 0, 2, 3, 2, 5, 1] }) == '{"val":[0,1,0,2,3,2,5,1]}'
assert json.encode(StructTypeOption[[]u8]{}) == '{}'
assert json.encode(StructTypeOption[[]u8]{}) == '{"val":null}'
assert json.encode(StructTypeOption[[]u8]{ val: [] }) == '{"val":[]}'
assert json.encode(StructTypeOption[[]u8]{ val: [u8(0)] }) == '{"val":[0]}'
assert json.encode(StructTypeOption[[]u8]{ val: [u8(1)] }) == '{"val":[1]}'
assert json.encode(StructTypeOption[[]u8]{ val: [u8(0), 1, 0, 2, 3, 2, 5, 1] }) == '{"val":[0,1,0,2,3,2,5,1]}'
assert json.encode(StructTypeOption[[]i64]{}) == '{}'
assert json.encode(StructTypeOption[[]i64]{}) == '{"val":null}'
assert json.encode(StructTypeOption[[]i64]{ val: [] }) == '{"val":[]}'
assert json.encode(StructTypeOption[[]i64]{ val: [i64(0)] }) == '{"val":[0]}'
assert json.encode(StructTypeOption[[]i64]{ val: [i64(1)] }) == '{"val":[1]}'
assert json.encode(StructTypeOption[[]i64]{ val: [i64(0), 1, 0, 2, 3, 2, 5, 1] }) == '{"val":[0,1,0,2,3,2,5,1]}'
assert json.encode(StructTypeOption[[]u64]{}) == '{}'
assert json.encode(StructTypeOption[[]u64]{}) == '{"val":null}'
assert json.encode(StructTypeOption[[]u64]{ val: [] }) == '{"val":[]}'
assert json.encode(StructTypeOption[[]u64]{ val: [u64(0)] }) == '{"val":[0]}'
assert json.encode(StructTypeOption[[]u64]{ val: [u64(1)] }) == '{"val":[1]}'
assert json.encode(StructTypeOption[[]u64]{ val: [u64(0), 1, 0, 2, 3, 2, 5, 1] }) == '{"val":[0,1,0,2,3,2,5,1]}'
assert json.encode(StructTypeOption[[]f64]{}) == '{}'
assert json.encode(StructTypeOption[[]f64]{}) == '{"val":null}'
assert json.encode(StructTypeOption[[]f64]{ val: [] }) == '{"val":[]}'
assert json.encode(StructTypeOption[[]f64]{ val: [f64(0)] }) == '{"val":[0]}'
assert json.encode(StructTypeOption[[]f64]{ val: [f64(1)] }) == '{"val":[1]}'
assert json.encode(StructTypeOption[[]f64]{ val: [f64(0), 1, 0, 2, 3, 2, 5, 1] }) == '{"val":[0,1,0,2,3,2,5,1]}'
assert json.encode(StructTypeOption[[]bool]{}) == '{}'
assert json.encode(StructTypeOption[[]bool]{}) == '{"val":null}'
assert json.encode(StructTypeOption[[]bool]{ val: [] }) == '{"val":[]}'
assert json.encode(StructTypeOption[[]bool]{ val: [true] }) == '{"val":[true]}'
assert json.encode(StructTypeOption[[]bool]{ val: [false] }) == '{"val":[false]}'

View file

@ -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('test')
assert text.json_str() == r'"te\u2714st"'
assert json.encode('test') == r'"te\u2714st"'
assert text.json_str() == r'"test"'
assert json.encode('test', 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"

View file

@ -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,8 +414,9 @@ fn test_option_instead_of_omit_empty() {
foo := Foo31{
name: 'Bob'
}
assert json.encode_pretty(foo) == '{
"name": "Bob"
assert json.encode(foo, prettify: true, indent_string: ' ') == '{
"name": "Bob",
"age": null
}'
}

View file

@ -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' }
}
}