json2: decode fn returns `?T`; add new tests (#6933)

pull/6213/head
Ned Palacios 2020-11-29 21:54:45 +08:00 committed by GitHub
parent 8f15af6adc
commit 6c634086b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 498 additions and 211 deletions

View File

@ -1,7 +1,7 @@
> `json2` was named just to avoid any unwanted potential conflicts with the existing codegen
> tailored for the main `json` module which is powered by CJSON.
> The name `json2` was chosen to avoid any unwanted potential conflicts with the
> existing codegen tailored for the main `json` module which is powered by CJSON.
An experimental version of the JSON parser written from scratch on V.
`x.json2` is an experimental JSON parser written from scratch on V.
## Usage
```v oksyntax
@ -29,7 +29,7 @@ fn main() {
mut arr := []json2.Any
arr << 'rock'
arr << 'papers'
arr << json2.null()
arr << json2.null
arr << 12
me['interests'] = arr
@ -40,7 +40,12 @@ fn main() {
// Stringify to JSON
println(me.str())
//{"name":"Bob","age":18,"interests":["rock","papers","scissors",null,12],"pets":{"Sam":"Maltese"}}
//{
// "name":"Bob",
// "age":18,
// "interests":["rock","papers","scissors",null,12],
// "pets":{"Sam":"Maltese"}
//}
// Encode a struct/type to JSON
encoded_json := json2.encode<Person>(person2)
@ -86,7 +91,7 @@ fn (p Person) to_json() string {
fn main() {
resp := os.read_file('./person.json')?
person := json2.decode<Person>(resp)
person := json2.decode<Person>(resp)?
println(person) // Person{name: 'Bob', age: 28, pets: ['Floof']}
person_json := json2.encode<Person>(person)
println(person_json) // {"name": "Bob", "age": 28, "pets": ["Floof"]}
@ -94,12 +99,36 @@ fn main() {
```
## Using struct tags
`x.json2` cannot use struct tags just like when you use the `json` module.
However, it emits an `Any` type when decoding so it can be flexible on the way you use it.
`x.json2` can access and use the struct field tags similar to the
`json` module by using the comp-time `$for` for structs.
```v ignore
fn (mut p Person) from_json(f json2.Any) {
mp := an.as_map()
mut js_field_name := ''
$for field in Person.fields {
js_field_name = field.name
for attr in field.attrs {
if attr.starts_with('json:') {
js_field_name = attr.all_after('json:').trim_left(' ')
break
}
}
match field.name {
'name' { p.name = mp[js_field_name].str() }
'age' { u.age = mp[js_field_name].int() }
'pets' { u.pets = mp[js_field_name].arr().map(it.str()) }
else {}
}
}
}
```
### Null Values
`x.json2` have a `null` value for differentiating an undefined value and a null value.
Use `is` for verifying the field you're using is a null.
`x.json2` has a separate `null` type for differentiating an undefined value and a null value.
To verify that the field you're accessing is a `null`, use `<typ> is json2.Null`.
```v ignore
fn (mut p Person) from_json(f json2.Any) {
@ -112,9 +141,8 @@ fn (mut p Person) from_json(f json2.Any) {
```
### Custom field names
In `json`, you can specify the field name you're mapping into the struct field by specifying
a `json:` tag. In `x.json2`, just simply cast the base field into a map (`as_map()`)
and get the value of the field you wish to put into the struct/type.
Aside from using struct tags, you can also just simply cast the base field into a map (`as_map()`)
and access the field you wish to put into the struct/type.
```v ignore
fn (mut p Person) from_json(f json2.Any) {
@ -142,6 +170,6 @@ The following list shows the possible outputs when casting a value to an incompa
1. Casting non-array values as array (`arr()`) will return an array with the value as the content.
2. Casting non-map values as map (`as_map()`) will return a map with the value as the content.
3. Casting non-string values to string (`str()`)
will return the stringified representation of the value.
4. Casting non-numeric values to int/float (`int()`/`f64()`) will return zero.
3. Casting non-string values to string (`str()`) will return the
JSON string representation of the value.
4. Casting non-numeric values to int/float (`int()`/`i64()`/`f32()`/`f64()`) will return zero.

View File

@ -0,0 +1,131 @@
import x.json2
const (
sample_data = {
'int': json2.Any(int(1))
'i64': json2.Any(i64(128))
'f32': json2.Any(f32(2.0))
'f64': json2.Any(f64(1.283))
'bool': json2.Any(false)
'str': json2.Any('test')
'null': json2.Any(json2.null)
'arr': json2.Any([json2.Any('lol')])
'obj': json2.Any({
'foo': json2.Any(10)
})
}
)
fn is_null(f json2.Any) bool {
match f {
json2.Null { return true }
else { return false }
}
}
fn test_f32() {
// valid conversions
assert sample_data['int'].f32() == 1.0
assert sample_data['i64'].f32() == 128.0
assert sample_data['f32'].f32() == 2.0
assert sample_data['f64'].f32() == 1.2829999923706055
// invalid conversions
assert sample_data['bool'].f32() == 0.0
assert sample_data['str'].f32() == 0.0
assert sample_data['null'].f32() == 0.0
assert sample_data['arr'].f32() == 0.0
assert sample_data['obj'].f32() == 0.0
}
fn test_f64() {
// valid conversions
assert sample_data['int'].f64() == 1.0
assert sample_data['i64'].f64() == 128.0
assert sample_data['f32'].f64() == 2.0
assert sample_data['f64'].f64() == 1.283
// invalid conversions
assert sample_data['bool'].f64() == 0.0
assert sample_data['str'].f64() == 0.0
assert sample_data['null'].f64() == 0.0
assert sample_data['arr'].f64() == 0.0
assert sample_data['obj'].f64() == 0.0
}
fn test_int() {
// valid conversions
assert sample_data['int'].int() == 1
assert sample_data['i64'].int() == 128
assert sample_data['f32'].int() == 2
assert sample_data['f64'].int() == 1
assert json2.Any(true).int() == 1
// invalid conversions
assert json2.Any('123').int() == 0
assert sample_data['null'].int() == 0
assert sample_data['arr'].int() == 0
assert sample_data['obj'].int() == 0
}
fn test_i64() {
// valid conversions
assert sample_data['int'].i64() == 1
assert sample_data['i64'].i64() == 128
assert sample_data['f32'].i64() == 2
assert sample_data['f64'].i64() == 1
assert json2.Any(true).i64() == 1
// invalid conversions
assert json2.Any('123').i64() == 0
assert sample_data['null'].i64() == 0
assert sample_data['arr'].i64() == 0
assert sample_data['obj'].i64() == 0
}
fn test_as_map() {
assert sample_data['int'].as_map()['0'].int() == 1
assert sample_data['i64'].as_map()['0'].i64() == 128.0
assert sample_data['f32'].as_map()['0'].f32() == 2.0
assert sample_data['f64'].as_map()['0'].f64() == 1.283
assert sample_data['bool'].as_map()['0'].bool() == false
assert sample_data['str'].as_map()['0'].str() == 'test'
assert is_null(sample_data['null'].as_map()['0']) == true
assert sample_data['arr'].as_map()['0'].str() == 'lol'
assert sample_data['obj'].as_map()['foo'].int() == 10
}
fn test_arr() {
assert sample_data['int'].arr()[0].int() == 1
assert sample_data['i64'].arr()[0].i64() == 128.0
assert sample_data['f32'].arr()[0].f32() == 2.0
assert sample_data['f64'].arr()[0].f64() == 1.283
assert sample_data['bool'].arr()[0].bool() == false
assert sample_data['str'].arr()[0].str() == 'test'
assert is_null(sample_data['null'].arr()[0]) == true
assert sample_data['arr'].arr()[0].str() == 'lol'
assert sample_data['obj'].arr()[0].int() == 10
}
fn test_bool() {
// valid conversions
assert sample_data['bool'].bool() == false
assert json2.Any('true').bool() == true
// invalid conversions
assert sample_data['int'].bool() == false
assert sample_data['i64'].bool() == false
assert sample_data['f32'].bool() == false
assert sample_data['f64'].bool() == false
assert sample_data['null'].bool() == false
assert sample_data['arr'].bool() == false
assert sample_data['obj'].bool() == false
}
fn test_str() {
assert sample_data['int'].str() == '1'
assert sample_data['i64'].str() == '128'
assert sample_data['f32'].str() == '2.0'
assert sample_data['f64'].str() == '1.283'
assert sample_data['bool'].str() == 'false'
assert sample_data['str'].str() == 'test'
assert sample_data['null'].str() == 'null'
assert sample_data['arr'].str() == '["lol"]'
assert sample_data.str() ==
'{"int":1,"i64":128,"f32":2.0,"f64":1.283,"bool":false,"str":"test","null":null,"arr":["lol"],"obj":{"foo":10}}'
}

View File

@ -9,56 +9,69 @@ pub fn (mut obj map[string]Any) insert_str(key string, val string) {
fi = val
obj[key] = fi
}
// Inserts an int into the map.
pub fn (mut obj map[string]Any) insert_int(key string, val int) {
obj[key] = Any(val)
}
// Inserts a float into the map.
pub fn (mut obj map[string]Any) insert_f(key string, val f64) {
obj[key] = Any(val)
}
// Inserts a null into the map.
pub fn (mut obj map[string]Any) insert_null(key string) {
obj[key] = Any(Null{})
}
// Inserts a bool into the map.
pub fn (mut obj map[string]Any) insert_bool(key string, val bool) {
obj[key] = Any(val)
}
// Inserts a map into the map.
pub fn (mut obj map[string]Any) insert_map(key string, val map[string]Any) {
obj[key] = Any(val)
}
// Inserts an array into the map.
pub fn (mut obj map[string]Any) insert_arr(key string, val []Any) {
obj[key] = Any(val)
}
// Inserts a string into the array.
pub fn (mut arr []Any) insert_str(val string) {
mut fi := Any{}
fi = val
arr << fi
}
// Inserts an int into the array.
pub fn (mut arr []Any) insert_int(val int) {
arr << Any(val)
}
// Inserts a float into the array.
pub fn (mut arr []Any) insert_f(val f64) {
arr << Any(val)
}
// Inserts a null into the array.
pub fn (mut arr []Any) insert_null() {
arr << Any(Null{})
}
// Inserts a bool into the array.
pub fn (mut arr []Any) insert_bool(val bool) {
arr << Any(val)
}
// Inserts a map into the array.
pub fn (mut arr []Any) insert_map(val map[string]Any) {
arr << Any(val)
}
// Inserts an array into the array.
pub fn (mut arr []Any) insert_arr(val []Any) {
arr << Any(val)

View File

@ -12,32 +12,34 @@ import v.pref
// `Any` is a sum type that lists the possible types to be decoded and used.
pub type Any = string | int | i64 | f32 | f64 | any_int | any_float | bool | Null | []Any | map[string]Any
// `Null` struct is a simple representation of the `null` value in JSON.
pub struct Null {}
pub struct Null {
}
enum ParseMode {
array
bool
invalid
null
number
object
string
array
bool
invalid
null
number
object
string
}
const (
formfeed_err = 'formfeed not allowed.'
eof_err = 'reached eof. data not closed properly.'
eof_err = 'reached eof. data not closed properly.'
)
struct Parser {
mut:
scanner &scanner.Scanner
p_tok token.Token
tok token.Token
n_tok token.Token
mode ParseMode = .invalid
n_level int
scanner &scanner.Scanner
p_tok token.Token
tok token.Token
n_tok token.Token
mode ParseMode = .invalid
n_level int
convert_type bool = true
}
@ -79,7 +81,7 @@ fn new_parser(srce string, convert_type bool) Parser {
}
}
return Parser{
scanner: scanner.new_scanner(src, .parse_comments, &pref.Preferences{}),
scanner: scanner.new_scanner(src, .parse_comments, &pref.Preferences{})
convert_type: convert_type
}
}
@ -88,9 +90,10 @@ fn check_valid_hex(str string) ? {
if str.len != 4 {
return error('hex string must be 4 characters.')
}
for l in str {
if l.is_hex_digit() { continue }
if l.is_hex_digit() {
continue
}
return error('provided string is not a hex digit.')
}
}
@ -111,18 +114,16 @@ fn (mut p Parser) decode() ?Any {
fn (p Parser) is_formfeed() bool {
prev_tok_pos := p.p_tok.pos + p.p_tok.len - 2
if prev_tok_pos < p.scanner.text.len && p.scanner.text[prev_tok_pos] == 0x0c {
return true
}
return false
}
fn (p Parser) is_singlequote() bool {
src := p.scanner.text
prev_tok_pos := p.p_tok.pos + p.p_tok.len
return src[prev_tok_pos] == `'`
return src[prev_tok_pos] == `\'`
}
fn (mut p Parser) detect_parse_mode() {
@ -131,21 +132,28 @@ fn (mut p Parser) detect_parse_mode() {
p.mode = .invalid
return
}
p.tok = p.scanner.scan()
p.n_tok = p.scanner.scan()
if src.len == 1 && p.tok.kind == .string && p.n_tok.kind == .eof {
p.mode = .invalid
return
}
match p.tok.kind {
.lcbr { p.mode = .object }
.lsbr { p.mode = .array }
.number { p.mode = .number }
.key_true, .key_false { p.mode = .bool }
.string { p.mode = .string }
.lcbr {
p.mode = .object
}
.lsbr {
p.mode = .array
}
.number {
p.mode = .number
}
.key_true, .key_false {
p.mode = .bool
}
.string {
p.mode = .string
}
.name {
if p.tok.lit == 'null' {
p.mode = .null
@ -164,11 +172,10 @@ fn (mut p Parser) decode_value() ?Any {
if p.n_level == 500 {
return error('reached maximum nesting level of 500.')
}
if (p.tok.kind == .lsbr && p.n_tok.kind == .lcbr) || (p.p_tok.kind == p.tok.kind && p.tok.kind == .lsbr) {
if (p.tok.kind == .lsbr && p.n_tok.kind == .lcbr) ||
(p.p_tok.kind == p.tok.kind && p.tok.kind == .lsbr) {
p.n_level++
}
match p.tok.kind {
.lsbr {
return p.decode_array()
@ -181,59 +188,64 @@ fn (mut p Parser) decode_value() ?Any {
}
.key_true {
p.next()
return if p.convert_type { Any(true) } else { Any('true') }
return if p.convert_type {
Any(true)
} else {
Any('true')
}
}
.key_false {
p.next()
return if p.convert_type { Any(false) } else { Any('false') }
return if p.convert_type {
Any(false)
} else {
Any('false')
}
}
.name {
if p.tok.lit != 'null' {
return error('unknown identifier `$p.tok.lit`')
}
p.next()
return if p.convert_type { Any(Null{}) } else { Any('null') }
return if p.convert_type {
Any(Null{})
} else {
Any('null')
}
}
.string {
if p.is_singlequote() {
return error('strings must be in double-quotes.')
}
return p.decode_string()
}
else {
if p.tok.kind == .minus && p.n_tok.kind == .number && p.n_tok.pos == p.tok.pos+1 {
if p.tok.kind == .minus && p.n_tok.kind == .number && p.n_tok.pos == p.tok.pos + 1 {
p.next()
d_num := p.decode_number()?
d_num := p.decode_number() ?
return d_num
}
return error('unknown token \'$p.tok.lit\' when decoding value')
return error("unknown token '$p.tok.lit' when decoding value")
}
}
if p.is_formfeed() {
return error(formfeed_err)
}
return Any{}
}
fn (mut p Parser) decode_string() ?Any {
mut strwr := strings.new_builder(200)
for i := 0; i < p.tok.lit.len; i++ {
if ((i-1 >= 0 && p.tok.lit[i-1] != `/`) || i == 0) && int(p.tok.lit[i]) in [9, 10, 0] {
if ((i - 1 >= 0 && p.tok.lit[i - 1] != `/`) || i == 0) && int(p.tok.lit[i]) in [9, 10, 0] {
return error('character must be escaped with a backslash.')
}
if i == p.tok.lit.len-1 && p.tok.lit[i] == 92 {
if i == p.tok.lit.len - 1 && p.tok.lit[i] == 92 {
return error('invalid backslash escape.')
}
if i+1 < p.tok.lit.len && p.tok.lit[i] == 92 {
peek := p.tok.lit[i+1]
match peek{
if i + 1 < p.tok.lit.len && p.tok.lit[i] == 92 {
peek := p.tok.lit[i + 1]
match peek {
`b` {
i++
strwr.write_b(`\b`)
@ -260,9 +272,9 @@ fn (mut p Parser) decode_string() ?Any {
continue
}
`u` {
if i+5 < p.tok.lit.len {
codepoint := p.tok.lit[i+2..i+6]
check_valid_hex(codepoint)?
if i + 5 < p.tok.lit.len {
codepoint := p.tok.lit[i + 2..i + 6]
check_valid_hex(codepoint) ?
hex_val := strconv.parse_int(codepoint, 16, 0)
strwr.write_b(byte(hex_val))
i += 5
@ -288,16 +300,13 @@ fn (mut p Parser) decode_string() ?Any {
}
else { return error('invalid backslash escape.') }
}
if int(peek) == 85 {
return error('unicode endpoints must be in lowercase `u`.')
}
if int(peek) in [9, 229] {
return error('unicode endpoint not allowed.')
}
}
strwr.write_b(p.tok.lit[i])
}
p.next()
@ -314,23 +323,18 @@ fn (mut p Parser) decode_number() ?Any {
mut tl := p.tok.lit
mut is_fl := false
sep_by_dot := tl.to_lower().split('.')
if tl.starts_with('0x') && tl.all_after('0x').len <= 2 {
return error('hex numbers should not be less than or equal to two digits.')
}
if src[p.p_tok.pos + p.p_tok.len] == `0` && src[p.p_tok.pos + p.p_tok.len + 1].is_digit() {
return error('leading zeroes in integers are not allowed.')
}
if tl.starts_with('.') {
return error('decimals must start with a digit followed by a dot.')
}
if tl.ends_with('+') || tl.ends_with('-') {
return error('exponents must have a digit before the sign.')
}
if sep_by_dot.len > 1 {
// analyze json number structure
// -[digit][dot][digit][E/e][-/+][digit]
@ -341,11 +345,9 @@ fn (mut p Parser) decode_number() ?Any {
return error('exponents must have a digit before the exponent notation.')
}
}
if p.p_tok.kind == .minus && p.tok.pos == p.p_tok.pos+1 {
if p.p_tok.kind == .minus && p.tok.pos == p.p_tok.pos + 1 {
tl = '-$tl'
}
p.next()
if p.convert_type {
return if is_fl {
@ -354,7 +356,6 @@ fn (mut p Parser) decode_number() ?Any {
Any(tl.i64())
}
}
return Any(tl)
}
@ -365,62 +366,51 @@ fn (mut p Parser) decode_array() ?Any {
if p.tok.kind == .eof {
return error(eof_err)
}
item := p.decode_value()?
item := p.decode_value() ?
items << item
if p.tok.kind == .comma && p.n_tok.kind !in [.rsbr, .comma] {
p.next()
continue
}
if p.tok.kind == .rsbr {
break
}
return error('unknown token \'$p.tok.lit\' when decoding arrays.')
return error("unknown token '$p.tok.lit' when decoding arrays.")
}
p.next()
return Any(items)
}
fn (mut p Parser) decode_object() ?Any {
mut fields := map[string]Any
mut fields := map[string]Any{}
mut cur_key := ''
p.next()
for p.tok.kind != .rcbr {
is_key := p.tok.kind == .string && p.n_tok.kind == .colon
// todo
// if p.is_formfeed() {
// return error(formfeed_err)
// return error(formfeed_err)
// }
if p.tok.kind == .eof {
return error(eof_err)
}
if p.is_singlequote() {
return error('object keys must be in single quotes.')
}
if !is_key {
return error('invalid token `$p.tok.lit`, expected \'string\'')
return error("invalid token `$p.tok.lit`, expected \'string\'")
}
cur_key = p.tok.lit
p.next()
p.next()
fields[cur_key] = p.decode_value()?
fields[cur_key] = p.decode_value() ?
if p.tok.kind == .comma && p.n_tok.kind !in [.rcbr, .comma] {
p.next()
continue
} else if p.tok.kind == .rcbr {
break
}
return error('unknown token \'$p.tok.lit\' when decoding object.')
return error("unknown token '$p.tok.lit' when decoding object.")
}
p.next()
return Any(fields)

View File

@ -0,0 +1,61 @@
import x.json2
fn test_raw_decode_string() {
str := json2.raw_decode('"Hello!"') or {
assert false
json2.Any{}
}
assert str.str() == 'Hello!'
}
fn test_raw_decode_number() {
num := json2.raw_decode('123') or {
assert false
json2.Any{}
}
assert num.int() == 123
}
fn test_raw_decode_array() {
raw_arr := json2.raw_decode('["Foo", 1]') or {
assert false
json2.Any{}
}
arr := raw_arr.arr()
assert arr[0].str() == 'Foo'
assert arr[1].int() == 1
}
fn test_raw_decode_bool() {
bol := json2.raw_decode('false') or {
assert false
json2.Any{}
}
assert bol.bool() == false
}
fn test_raw_decode_map() {
raw_mp := json2.raw_decode('{"name":"Bob","age":20}') or {
assert false
json2.Any{}
}
mp := raw_mp.as_map()
assert mp['name'].str() == 'Bob'
assert mp['age'].int() == 20
}
fn test_raw_decode_null() {
nul := json2.raw_decode('null') or {
assert false
json2.Any{}
}
assert nul is json2.Null
}
fn test_raw_decode_invalid() {
json2.raw_decode('1z') or {
assert err == '[json] invalid JSON. (0:0)'
return
}
assert false
}

View File

@ -12,11 +12,13 @@ fn write_value(v Any, i int, len int, mut wr strings.Builder) {
} else {
wr.write(str)
}
if i >= len-1 { return }
if i >= len - 1 {
return
}
wr.write_b(`,`)
}
// String representation of the `map[string]Any`.
// str returns the string representation of the `map[string]Any`.
pub fn (flds map[string]Any) str() string {
mut wr := strings.new_builder(200)
wr.write_b(`{`)
@ -34,7 +36,7 @@ pub fn (flds map[string]Any) str() string {
return res
}
// String representation of the `[]Any`.
// str returns the string representation of the `[]Any`.
pub fn (flds []Any) str() string {
mut wr := strings.new_builder(200)
wr.write_b(`[`)
@ -49,19 +51,49 @@ pub fn (flds []Any) str() string {
return res
}
// String representation of the `Any` type.
// str returns the string representation of the `Any` type.
pub fn (f Any) str() string {
match f {
string { return f }
int { return f.str() }
i64 { return f.str() }
f32 { return f.str() }
f64 { return f.str() }
any_int { return f.str() }
any_float { return f.str() }
bool { return f.str() }
map[string]Any { return f.str() }
Null { return 'null' }
string {
return f
}
int {
return f.str()
}
i64 {
return f.str()
}
f32 {
str_f32 := f.str()
return if str_f32.ends_with('.') {
str_f32 + '0'
} else {
str_f32
}
}
f64 {
str_f64 := f.str()
return if str_f64.ends_with('.') {
str_f64 + '0'
} else {
str_f64
}
}
any_int {
return f.str()
}
any_float {
return f.str()
}
bool {
return f.str()
}
map[string]Any {
return f.str()
}
Null {
return 'null'
}
else {
if f is []Any {
return f.str()

View File

@ -3,6 +3,10 @@
// that can be found in the LICENSE file.
module json2
pub const (
null = Null{}
)
pub interface Serializable {
from_json(f Any)
to_json() string
@ -19,110 +23,73 @@ pub fn fast_raw_decode(src string) ?Any {
mut p := new_parser(src, false)
return p.decode()
}
// A generic function that decodes a JSON string into the target type.
//
// TODO: decode must return an optional generics
pub fn decode<T>(src string) T {
res := raw_decode(src) or {
panic(err)
}
// decode is a generic function that decodes a JSON string into the target type.
pub fn decode<T>(src string) ?T {
res := raw_decode(src) ?
mut typ := T{}
typ.from_json(res)
return typ
}
// TODO: decode must return an optional generics
// pub fn decode2<T>(src string) ?T {
// res := raw_decode(src)?
// match typeof(T) {
// 'string' {
// return res.str()
// }
// 'int' {
// return res.int()
// }
// 'f64' {
// return res.f64()
// }
// else {
// mut typ := T{}
// typ.from_json(res)
// return typ
// }
// }
// }
// A generic function that encodes a type into a JSON string.
// encode is a generic function that encodes a type into a JSON string.
pub fn encode<T>(typ T) string {
// if typeof(typ) in ['string', 'int', 'f64'] {
// return Any(typ).str()
// }
return typ.to_json()
}
// A simple function that returns `Null` struct. For use on constructing an `Any` object.
pub fn null() Null {
return Null{}
}
// Use `Any` as a map.
// as_map uses `Any` as a map.
pub fn (f Any) as_map() map[string]Any {
if f is map[string]Any {
return f
} else if f is []Any {
mut mp := map[string]Any
mut mp := map[string]Any{}
for i, fi in f {
mp['$i'] = fi
}
return mp
}
return { '0': f }
return {
'0': f
}
}
// Use `Any` as an integer.
// int uses `Any` as an integer.
pub fn (f Any) int() int {
match f {
int { return f }
i64 { return int(f) }
f64 { return f.str().int() }
f32 { return f.str().int() }
bool { return int(f) }
int { return f }
i64, f32, f64, bool { return int(f) }
else { return 0 }
}
}
// Use `Any` as a 64-bit integer.
// i64 uses `Any` as a 64-bit integer.
pub fn (f Any) i64() i64 {
match f {
int { return f }
i64 { return int(f) }
f64 { return f.str().i64() }
f32 { return f.str().i64() }
bool { return int(f) }
i64 { return f }
int, f32, f64, bool { return i64(f) }
else { return 0 }
}
}
// Use `Any` as a 32-bit float.
// f32 uses `Any` as a 32-bit float.
pub fn (f Any) f32() f32 {
match f {
int { return f }
i64 { return f.str().f32() }
f64 { return f.str().f32() }
f32 { return f }
int, i64, f64 { return f32(f) }
else { return 0.0 }
}
}
// Use `Any` as a float.
// f64 uses `Any` as a float.
pub fn (f Any) f64() f64 {
match f {
int { return f }
i64 { return f }
f64 { return f }
f32 { return f.str().f64() }
int, i64, f32 { return f64(f) }
else { return 0.0 }
}
}
// Use `Any` as an array.
// arr uses `Any` as an array.
pub fn (f Any) arr() []Any {
if f is []Any {
return f
@ -136,7 +103,7 @@ pub fn (f Any) arr() []Any {
return [f]
}
// Use `Any` as a bool
// bool uses `Any` as a bool
pub fn (f Any) bool() bool {
match f {
bool { return f }

View File

@ -7,6 +7,7 @@ enum JobTitle {
}
struct Employee {
pub mut:
name string
age int
salary f32
@ -14,12 +15,11 @@ struct Employee {
}
fn (e Employee) to_json() string {
mut mp := map[string]json2.Any
mut mp := map[string]json2.Any{}
mp['name'] = e.name
mp['age'] = e.age
mp['salary'] = e.salary
mp['title'] = int(e.title)
/*
$for field in Employee.fields {
d := e.$(field.name)
@ -31,31 +31,31 @@ fn (e Employee) to_json() string {
}
}
*/
return mp.str()
}
fn (mut e Employee) from_json(any json2.Any) {
mp := any.as_map()
e.name = mp['name'].str()
e.age = mp['age'].int()
e.salary = mp['salary'].f32()
e.title = JobTitle(mp['title'].int())
}
fn test_simple() {
x := Employee{'Peter', 28, 95000.5, .worker}
s := json2.encode<Employee>(x)
eprintln('Employee x: $s')
assert s == '{"name":"Peter","age":28,"salary":95000.5,"title":2}'
/*
y := json.decode(Employee, s) or {
y := json2.decode<Employee>(s) or {
assert false
Employee{}
}*/
y := json2.raw_decode(s) or {
assert false
json2.Any{}
}
eprintln('Employee y: $y')
ym := y.as_map()
assert ym['name'].str() == 'Peter'
assert ym['age'].int() == 28
assert ym['salary'].f64() == 95000.5
// assert y['title'] == .worker
assert ym['title'].int() == 2
assert y.name == 'Peter'
assert y.age == 28
assert y.salary == 95000.5
assert y.title == .worker
}
fn test_fast_raw_decode() {
@ -65,17 +65,16 @@ fn test_fast_raw_decode() {
json2.Any{}
}
str := o.str()
assert str == '{"name":"Peter","age":"28","salary":"95000.5","title":"2"}'
}
fn test_character_unescape() {
// Need to test `\r`, `\b`, `\f` ??
message := '{
"newline":"new\\nline",
"tab":"\\ttab",
"backslash": "back\\\\slash",
"quotes": "\\"quotes\\"",
"newline":"new\\nline",
"tab":"\\ttab",
"backslash": "back\\\\slash",
"quotes": "\\"quotes\\"",
"slash":"\/dev\/null"
}'
mut obj := json2.raw_decode(message) or {
@ -83,7 +82,7 @@ fn test_character_unescape() {
json2.Any{}
}
lines := obj.as_map()
eprintln("$lines")
eprintln('$lines')
assert lines['newline'].str() == 'new\nline'
assert lines['tab'].str() == '\ttab'
assert lines['backslash'].str() == 'back\\slash'
@ -91,13 +90,34 @@ fn test_character_unescape() {
assert lines['slash'].str() == '/dev/null'
}
/*
struct User2 {
pub mut:
age int
nums []int
}
fn (mut u User2) from_json(an json2.Any) {
mp := an.as_map()
mut js_field_name := ''
$for field in User.fields {
js_field_name = field.name
for attr in field.attrs {
if attr.starts_with('json:') {
js_field_name = attr.all_after('json:').trim_left(' ')
break
}
}
match field.name {
'age' { u.age = mp[js_field_name].int() }
'nums' { u.nums = mp[js_field_name].arr().map(it.int()) }
else {}
}
}
}
// User struct needs to be `pub mut` for now in order to access and manipulate values
struct User {
pub mut:
age int
nums []int
last_name string [json: lastName]
@ -106,16 +126,53 @@ struct User {
pets string [raw; json: 'pet_animals']
}
fn (mut u User) from_json(an json2.Any) {
mp := an.as_map()
mut js_field_name := ''
$for field in User.fields {
// FIXME: C error when initializing js_field_name inside comptime for
js_field_name = field.name
for attr in field.attrs {
if attr.starts_with('json:') {
js_field_name = attr.all_after('json:').trim_left(' ')
break
}
}
match field.name {
'age' { u.age = mp[js_field_name].int() }
'nums' { u.nums = mp[js_field_name].arr().map(it.int()) }
'last_name' { u.last_name = mp[js_field_name].str() }
'is_registered' { u.is_registered = mp[js_field_name].bool() }
'typ' { u.typ = mp[js_field_name].int() }
'pets' { u.pets = mp[js_field_name].str() }
else {}
}
}
}
fn (u User) to_json() string {
// TODO: derive from field
mut mp := {
'age': json2.Any(u.age)
}
mp['nums'] = u.nums.map(json2.Any(it))
mp['lastName'] = u.last_name
mp['IsRegistered'] = u.is_registered
mp['type'] = u.typ
mp['pet_animals'] = u.pets
return mp.str()
}
fn test_parse_user() {
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 := json2.decode<User2>(s) or {
assert false
User2{}
}
println(u2)
u := json.decode(User, s) or {
exit(1)
u := json2.decode<User>(s) or {
assert false
User{}
}
println(u)
assert u.age == 10
assert u.last_name == 'Johnson'
assert u.is_registered == true
@ -137,25 +194,37 @@ fn test_encode_user() {
pets: 'foo'
}
expected := '{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"foo"}'
out := json.encode(usr)
println(out)
out := json2.encode<User>(usr)
assert out == expected
}
struct Color {
pub mut:
space string
point string [raw]
}
fn (mut c Color) from_json(an json2.Any) {
mp := an.as_map()
$for field in Color.fields {
match field.name {
'space' { c.space = mp[field.name].str() }
'point' { c.point = mp[field.name].str() }
else {}
}
}
}
fn test_raw_json_field() {
color := json.decode(Color, '{"space": "YCbCr", "point": {"Y": 123}}') or {
println('text')
return
color := json2.decode<Color>('{"space": "YCbCr", "point": {"Y": 123}}') or {
assert false
Color{}
}
assert color.point == '{"Y":123}'
assert color.space == 'YCbCr'
}
/*
struct City {
name string
}
@ -177,7 +246,6 @@ fn test_struct_in_struct() {
println(country.cities)
}
*/
fn test_encode_map() {
expected := '{"one":1,"two":2,"three":3,"four":4}'
numbers := {
@ -187,9 +255,6 @@ fn test_encode_map() {
'four': json2.Any(4)
}
out := numbers.str()
// out := json.encode(numbers)
println(out)
assert out == expected
}
/*