json: support for a [required] field attribute (#10955)
parent
8a097293a8
commit
684c10af1f
|
@ -3450,7 +3450,12 @@ struct Foo {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct User {
|
struct User {
|
||||||
name string
|
// Adding a [required] attribute will make decoding fail, if that
|
||||||
|
// field is not present in the input.
|
||||||
|
// If a field is not [required], but is missing, it will be assumed
|
||||||
|
// to have its default value, like 0 for numbers, or '' for strings,
|
||||||
|
// and decoding will not fail.
|
||||||
|
name string [required]
|
||||||
age int
|
age int
|
||||||
// Use the `skip` attribute to skip certain fields
|
// Use the `skip` attribute to skip certain fields
|
||||||
foo Foo [skip]
|
foo Foo [skip]
|
||||||
|
@ -3460,7 +3465,7 @@ struct User {
|
||||||
|
|
||||||
data := '{ "name": "Frodo", "lastName": "Baggins", "age": 25 }'
|
data := '{ "name": "Frodo", "lastName": "Baggins", "age": 25 }'
|
||||||
user := json.decode(User, data) or {
|
user := json.decode(User, data) or {
|
||||||
eprintln('Failed to decode json')
|
eprintln('Failed to decode json, error: $err')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
println(user.name)
|
println(user.name)
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
struct TestTwin {
|
||||||
|
id int
|
||||||
|
seed string
|
||||||
|
pubkey string
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestTwins {
|
||||||
|
mut:
|
||||||
|
twins []TestTwin [required]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_json_decode_fails_to_decode_unrecognised_array_of_dicts() {
|
||||||
|
data := '[{"twins":[{"id":123,"seed":"abcde","pubkey":"xyzasd"},{"id":456,"seed":"dfgdfgdfgd","pubkey":"skjldskljh45sdf"}]}]'
|
||||||
|
json.decode(TestTwins, data) or {
|
||||||
|
assert err.msg == "expected field 'twins' is missing"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_json_decode_works_with_a_dict_of_arrays() {
|
||||||
|
data := '{"twins":[{"id":123,"seed":"abcde","pubkey":"xyzasd"},{"id":456,"seed":"dfgdfgdfgd","pubkey":"skjldskljh45sdf"}]}'
|
||||||
|
res := json.decode(TestTwins, data) or {
|
||||||
|
assert false
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
assert res.twins[0].id == 123
|
||||||
|
assert res.twins[0].seed == 'abcde'
|
||||||
|
assert res.twins[0].pubkey == 'xyzasd'
|
||||||
|
assert res.twins[1].id == 456
|
||||||
|
assert res.twins[1].seed == 'dfgdfgdfgd'
|
||||||
|
assert res.twins[1].pubkey == 'skjldskljh45sdf'
|
||||||
|
}
|
|
@ -14,15 +14,12 @@ struct Employee {
|
||||||
title JobTitle
|
title JobTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_simple() {
|
fn test_simple() ? {
|
||||||
x := Employee{'Peter', 28, 95000.5, .worker}
|
x := Employee{'Peter', 28, 95000.5, .worker}
|
||||||
s := json.encode(x)
|
s := json.encode(x)
|
||||||
eprintln('Employee x: $s')
|
eprintln('Employee x: $s')
|
||||||
assert s == '{"name":"Peter","age":28,"salary":95000.5,"title":2}'
|
assert s == '{"name":"Peter","age":28,"salary":95000.5,"title":2}'
|
||||||
y := json.decode(Employee, s) or {
|
y := json.decode(Employee, s) ?
|
||||||
assert false
|
|
||||||
Employee{}
|
|
||||||
}
|
|
||||||
eprintln('Employee y: $y')
|
eprintln('Employee y: $y')
|
||||||
assert y.name == 'Peter'
|
assert y.name == 'Peter'
|
||||||
assert y.age == 28
|
assert y.age == 28
|
||||||
|
@ -59,11 +56,11 @@ struct User {
|
||||||
pets string [json: 'pet_animals'; raw]
|
pets string [json: 'pet_animals'; raw]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_parse_user() {
|
fn test_parse_user() ? {
|
||||||
s := '{"age": 10, "nums": [1,2,3], "type": 1, "lastName": "Johnson", "IsRegistered": true, "pet_animals": {"name": "Bob", "animal": "Dog"}}'
|
s := '{"age": 10, "nums": [1,2,3], "type": 1, "lastName": "Johnson", "IsRegistered": true, "pet_animals": {"name": "Bob", "animal": "Dog"}}'
|
||||||
u2 := json.decode(User2, s) or { exit(1) }
|
u2 := json.decode(User2, s) ?
|
||||||
println(u2)
|
println(u2)
|
||||||
u := json.decode(User, s) or { exit(1) }
|
u := json.decode(User, s) ?
|
||||||
println(u)
|
println(u)
|
||||||
assert u.age == 10
|
assert u.age == 10
|
||||||
assert u.last_name == 'Johnson'
|
assert u.last_name == 'Johnson'
|
||||||
|
@ -76,7 +73,7 @@ fn test_parse_user() {
|
||||||
assert u.pets == '{"name":"Bob","animal":"Dog"}'
|
assert u.pets == '{"name":"Bob","animal":"Dog"}'
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_encode_decode_time() {
|
fn test_encode_decode_time() ? {
|
||||||
user := User2{
|
user := User2{
|
||||||
age: 25
|
age: 25
|
||||||
reg_date: time.new_time(year: 2020, month: 12, day: 22, hour: 7, minute: 23)
|
reg_date: time.new_time(year: 2020, month: 12, day: 22, hour: 7, minute: 23)
|
||||||
|
@ -84,10 +81,7 @@ fn test_encode_decode_time() {
|
||||||
s := json.encode(user)
|
s := json.encode(user)
|
||||||
println(s)
|
println(s)
|
||||||
assert s.contains('"reg_date":1608621780')
|
assert s.contains('"reg_date":1608621780')
|
||||||
user2 := json.decode(User2, s) or {
|
user2 := json.decode(User2, s) ?
|
||||||
assert false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert user2.reg_date.str() == '2020-12-22 07:23:00'
|
assert user2.reg_date.str() == '2020-12-22 07:23:00'
|
||||||
println(user2)
|
println(user2)
|
||||||
println(user2.reg_date)
|
println(user2.reg_date)
|
||||||
|
@ -146,11 +140,8 @@ struct Country {
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_struct_in_struct() {
|
fn test_struct_in_struct() ? {
|
||||||
country := json.decode(Country, '{ "name": "UK", "cities": [{"name":"London"}, {"name":"Manchester"}]}') or {
|
country := json.decode(Country, '{ "name": "UK", "cities": [{"name":"London"}, {"name":"Manchester"}]}') ?
|
||||||
assert false
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
assert country.name == 'UK'
|
assert country.name == 'UK'
|
||||||
assert country.cities.len == 2
|
assert country.cities.len == 2
|
||||||
assert country.cities[0].name == 'London'
|
assert country.cities[0].name == 'London'
|
||||||
|
@ -171,20 +162,14 @@ fn test_encode_map() {
|
||||||
assert out == expected
|
assert out == expected
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_parse_map() {
|
fn test_parse_map() ? {
|
||||||
expected := map{
|
expected := map{
|
||||||
'one': 1
|
'one': 1
|
||||||
'two': 2
|
'two': 2
|
||||||
'three': 3
|
'three': 3
|
||||||
'four': 4
|
'four': 4
|
||||||
}
|
}
|
||||||
out := json.decode(map[string]int, '{"one":1,"two":2,"three":3,"four":4}') or {
|
out := json.decode(map[string]int, '{"one":1,"two":2,"three":3,"four":4}') ?
|
||||||
assert false
|
|
||||||
r := map{
|
|
||||||
'': 0
|
|
||||||
}
|
|
||||||
r
|
|
||||||
}
|
|
||||||
println(out)
|
println(out)
|
||||||
assert out == expected
|
assert out == expected
|
||||||
}
|
}
|
||||||
|
@ -195,7 +180,7 @@ struct Data {
|
||||||
extra map[string]map[string]int
|
extra map[string]map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_nested_type() {
|
fn test_nested_type() ? {
|
||||||
data_expected := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":[{"name":"Donlon"},{"name":"Termanches"}],"name":"KU"}],"users":{"Foo":{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"little foo"},"Boo":{"age":20,"nums":[5,3,1],"lastName":"Smith","IsRegistered":false,"type":4,"pet_animals":"little boo"}},"extra":{"2":{"n1":2,"n2":4,"n3":8,"n4":16},"3":{"n1":3,"n2":9,"n3":27,"n4":81}}}'
|
data_expected := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":[{"name":"Donlon"},{"name":"Termanches"}],"name":"KU"}],"users":{"Foo":{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"little foo"},"Boo":{"age":20,"nums":[5,3,1],"lastName":"Smith","IsRegistered":false,"type":4,"pet_animals":"little boo"}},"extra":{"2":{"n1":2,"n2":4,"n3":8,"n4":16},"3":{"n1":3,"n2":9,"n3":27,"n4":81}}}'
|
||||||
data := Data{
|
data := Data{
|
||||||
countries: [
|
countries: [
|
||||||
|
@ -244,10 +229,7 @@ fn test_nested_type() {
|
||||||
out := json.encode(data)
|
out := json.encode(data)
|
||||||
println(out)
|
println(out)
|
||||||
assert out == data_expected
|
assert out == data_expected
|
||||||
data2 := json.decode(Data, data_expected) or {
|
data2 := json.decode(Data, data_expected) ?
|
||||||
assert false
|
|
||||||
Data{}
|
|
||||||
}
|
|
||||||
assert data2.countries.len == data.countries.len
|
assert data2.countries.len == data.countries.len
|
||||||
for i in 0 .. 1 {
|
for i in 0 .. 1 {
|
||||||
assert data2.countries[i].name == data.countries[i].name
|
assert data2.countries[i].name == data.countries[i].name
|
||||||
|
@ -277,11 +259,11 @@ pub:
|
||||||
data T
|
data T
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_generic_struct() {
|
fn test_generic_struct() ? {
|
||||||
foo_int := Foo<int>{'bar', 12}
|
foo_int := Foo<int>{'bar', 12}
|
||||||
foo_enc := json.encode(foo_int)
|
foo_enc := json.encode(foo_int)
|
||||||
assert foo_enc == '{"name":"bar","data":12}'
|
assert foo_enc == '{"name":"bar","data":12}'
|
||||||
foo_dec := json.decode(Foo<int>, foo_enc) or { exit(1) }
|
foo_dec := json.decode(Foo<int>, foo_enc) ?
|
||||||
assert foo_dec.name == 'bar'
|
assert foo_dec.name == 'bar'
|
||||||
assert foo_dec.data == 12
|
assert foo_dec.data == 12
|
||||||
}
|
}
|
||||||
|
@ -315,11 +297,8 @@ struct Message {
|
||||||
id ID
|
id ID
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_decode_alias_struct() {
|
fn test_decode_alias_struct() ? {
|
||||||
msg := json.decode(Message, '{"id": "118499178790780929"}') or {
|
msg := json.decode(Message, '{"id": "118499178790780929"}') ?
|
||||||
assert false
|
|
||||||
Message{}
|
|
||||||
}
|
|
||||||
// hacky way of comparing aliased strings
|
// hacky way of comparing aliased strings
|
||||||
assert msg.id.str() == '118499178790780929'
|
assert msg.id.str() == '118499178790780929'
|
||||||
}
|
}
|
||||||
|
@ -336,29 +315,20 @@ struct List {
|
||||||
items []string
|
items []string
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_list() {
|
fn test_list() ? {
|
||||||
list := json.decode(List, '{"id": 1, "items": ["1", "2"]}') or {
|
list := json.decode(List, '{"id": 1, "items": ["1", "2"]}') ?
|
||||||
println('error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert list.id == 1
|
assert list.id == 1
|
||||||
assert list.items == ['1', '2']
|
assert list.items == ['1', '2']
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_list_no_id() {
|
fn test_list_no_id() ? {
|
||||||
list := json.decode(List, '{"items": ["1", "2"]}') or {
|
list := json.decode(List, '{"items": ["1", "2"]}') ?
|
||||||
println('error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert list.id == 0
|
assert list.id == 0
|
||||||
assert list.items == ['1', '2']
|
assert list.items == ['1', '2']
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_list_no_items() {
|
fn test_list_no_items() ? {
|
||||||
list := json.decode(List, '{"id": 1}') or {
|
list := json.decode(List, '{"id": 1}') ?
|
||||||
println('error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert list.id == 1
|
assert list.id == 1
|
||||||
assert list.items == []
|
assert list.items == []
|
||||||
}
|
}
|
||||||
|
@ -369,8 +339,8 @@ struct Info {
|
||||||
maps map[string]string
|
maps map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_decode_null_object() {
|
fn test_decode_null_object() ? {
|
||||||
info := json.decode(Info, '{"id": 22, "items": null, "maps": null}') or { panic(err) }
|
info := json.decode(Info, '{"id": 22, "items": null, "maps": null}') ?
|
||||||
assert info.id == 22
|
assert info.id == 22
|
||||||
assert '$info.items' == '[]'
|
assert '$info.items' == '[]'
|
||||||
assert '$info.maps' == '{}'
|
assert '$info.maps' == '{}'
|
||||||
|
|
|
@ -113,20 +113,34 @@ $enc_fn_dec {
|
||||||
fn (mut g Gen) gen_struct_enc_dec(type_info ast.TypeInfo, styp string, mut enc strings.Builder, mut dec strings.Builder) {
|
fn (mut g Gen) gen_struct_enc_dec(type_info ast.TypeInfo, styp string, mut enc strings.Builder, mut dec strings.Builder) {
|
||||||
info := type_info as ast.Struct
|
info := type_info as ast.Struct
|
||||||
for field in info.fields {
|
for field in info.fields {
|
||||||
if field.attrs.contains('skip') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mut name := field.name
|
mut name := field.name
|
||||||
|
mut is_raw := false
|
||||||
|
mut is_skip := false
|
||||||
|
mut is_required := false
|
||||||
for attr in field.attrs {
|
for attr in field.attrs {
|
||||||
if attr.name == 'json' {
|
match attr.name {
|
||||||
|
'json' {
|
||||||
name = attr.arg
|
name = attr.arg
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
'skip' {
|
||||||
|
is_skip = true
|
||||||
|
}
|
||||||
|
'raw' {
|
||||||
|
is_raw = true
|
||||||
|
}
|
||||||
|
'required' {
|
||||||
|
is_required = true
|
||||||
|
}
|
||||||
|
else {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_skip {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
field_type := g.typ(field.typ)
|
field_type := g.typ(field.typ)
|
||||||
field_sym := g.table.get_type_symbol(field.typ)
|
field_sym := g.table.get_type_symbol(field.typ)
|
||||||
// First generate decoding
|
// First generate decoding
|
||||||
if field.attrs.contains('raw') {
|
if is_raw {
|
||||||
dec.writeln('\tres.${c_name(field.name)} = tos5(cJSON_PrintUnformatted(' +
|
dec.writeln('\tres.${c_name(field.name)} = tos5(cJSON_PrintUnformatted(' +
|
||||||
'js_get(root, "$name")));')
|
'js_get(root, "$name")));')
|
||||||
} else {
|
} else {
|
||||||
|
@ -135,35 +149,36 @@ fn (mut g Gen) gen_struct_enc_dec(type_info ast.TypeInfo, styp string, mut enc s
|
||||||
g.gen_json_for_type(field.typ)
|
g.gen_json_for_type(field.typ)
|
||||||
dec_name := js_dec_name(field_type)
|
dec_name := js_dec_name(field_type)
|
||||||
if is_js_prim(field_type) {
|
if is_js_prim(field_type) {
|
||||||
dec.writeln('\tres.${c_name(field.name)} = $dec_name (js_get(root, "$name"));')
|
tmp := g.new_tmp_var()
|
||||||
|
gen_js_get(styp, tmp, name, mut dec, is_required)
|
||||||
|
dec.writeln('\tres.${c_name(field.name)} = $dec_name (jsonroot_$tmp);')
|
||||||
} else if field_sym.kind == .enum_ {
|
} else if field_sym.kind == .enum_ {
|
||||||
dec.writeln('\tres.${c_name(field.name)} = json__decode_u64(js_get(root, "$name"));')
|
tmp := g.new_tmp_var()
|
||||||
|
gen_js_get(styp, tmp, name, mut dec, is_required)
|
||||||
|
dec.writeln('\tres.${c_name(field.name)} = json__decode_u64(jsonroot_$tmp);')
|
||||||
} else if field_sym.name == 'time.Time' {
|
} else if field_sym.name == 'time.Time' {
|
||||||
// time struct requires special treatment
|
// time struct requires special treatment
|
||||||
// it has to be decoded from a unix timestamp number
|
// it has to be decoded from a unix timestamp number
|
||||||
dec.writeln('\tres.${c_name(field.name)} = time__unix(json__decode_u64(js_get(root, "$name")));')
|
tmp := g.new_tmp_var()
|
||||||
|
gen_js_get(styp, tmp, name, mut dec, is_required)
|
||||||
|
dec.writeln('\tres.${c_name(field.name)} = time__unix(json__decode_u64(jsonroot_$tmp));')
|
||||||
} else if field_sym.kind == .alias {
|
} else if field_sym.kind == .alias {
|
||||||
alias := field_sym.info as ast.Alias
|
alias := field_sym.info as ast.Alias
|
||||||
parent_type := g.typ(alias.parent_type)
|
parent_type := g.typ(alias.parent_type)
|
||||||
parent_dec_name := js_dec_name(parent_type)
|
parent_dec_name := js_dec_name(parent_type)
|
||||||
if is_js_prim(parent_type) {
|
if is_js_prim(parent_type) {
|
||||||
dec.writeln('\tres.${c_name(field.name)} = $parent_dec_name (js_get(root, "$name"));')
|
tmp := g.new_tmp_var()
|
||||||
|
gen_js_get(styp, tmp, name, mut dec, is_required)
|
||||||
|
dec.writeln('\tres.${c_name(field.name)} = $parent_dec_name (jsonroot_$tmp);')
|
||||||
} else {
|
} else {
|
||||||
g.gen_json_for_type(field.typ)
|
g.gen_json_for_type(field.typ)
|
||||||
tmp := g.new_tmp_var()
|
tmp := g.new_tmp_var()
|
||||||
dec.writeln('\tOption_$field_type $tmp = $dec_name (js_get(root,"$name"));')
|
gen_js_get_opt(dec_name, field_type, styp, tmp, name, mut dec, is_required)
|
||||||
dec.writeln('\tif(${tmp}.state != 0) {')
|
|
||||||
dec.writeln('\t\treturn (Option_$styp){ .state = ${tmp}.state, .err = ${tmp}.err, .data = {0} };')
|
|
||||||
dec.writeln('\t}')
|
|
||||||
dec.writeln('\tres.${c_name(field.name)} = *($field_type*) ${tmp}.data;')
|
dec.writeln('\tres.${c_name(field.name)} = *($field_type*) ${tmp}.data;')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// dec.writeln(' $dec_name (js_get(root, "$name"), & (res . $field.name));')
|
|
||||||
tmp := g.new_tmp_var()
|
tmp := g.new_tmp_var()
|
||||||
dec.writeln('\tOption_$field_type $tmp = $dec_name (js_get(root,"$name"));')
|
gen_js_get_opt(dec_name, field_type, styp, tmp, name, mut dec, is_required)
|
||||||
dec.writeln('\tif(${tmp}.state != 0) {')
|
|
||||||
dec.writeln('\t\treturn (Option_$styp){ .state = ${tmp}.state, .err = ${tmp}.err, .data = {0} };')
|
|
||||||
dec.writeln('\t}')
|
|
||||||
dec.writeln('\tres.${c_name(field.name)} = *($field_type*) ${tmp}.data;')
|
dec.writeln('\tres.${c_name(field.name)} = *($field_type*) ${tmp}.data;')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,6 +204,23 @@ fn (mut g Gen) gen_struct_enc_dec(type_info ast.TypeInfo, styp string, mut enc s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gen_js_get(styp string, tmp string, name string, mut dec strings.Builder, is_required bool) {
|
||||||
|
dec.writeln('\tcJSON *jsonroot_$tmp = js_get(root,"$name");')
|
||||||
|
if is_required {
|
||||||
|
dec.writeln('\tif(jsonroot_$tmp == 0) {')
|
||||||
|
dec.writeln('\t\treturn (Option_$styp){ .state = 2, .err = _v_error(_SLIT("expected field \'$name\' is missing")), .data = {0} };')
|
||||||
|
dec.writeln('\t}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_js_get_opt(dec_name string, field_type string, styp string, tmp string, name string, mut dec strings.Builder, is_required bool) {
|
||||||
|
gen_js_get(styp, tmp, name, mut dec, is_required)
|
||||||
|
dec.writeln('\tOption_$field_type $tmp = $dec_name (jsonroot_$tmp);')
|
||||||
|
dec.writeln('\tif(${tmp}.state != 0) {')
|
||||||
|
dec.writeln('\t\treturn (Option_$styp){ .state = ${tmp}.state, .err = ${tmp}.err, .data = {0} };')
|
||||||
|
dec.writeln('\t}')
|
||||||
|
}
|
||||||
|
|
||||||
fn js_enc_name(typ string) string {
|
fn js_enc_name(typ string) string {
|
||||||
suffix := if typ.ends_with('*') { typ.replace('*', '') } else { typ }
|
suffix := if typ.ends_with('*') { typ.replace('*', '') } else { typ }
|
||||||
name := 'json__encode_$suffix'
|
name := 'json__encode_$suffix'
|
||||||
|
|
Loading…
Reference in New Issue