vlib: add x/json2 (#6336)
parent
bc8aab4775
commit
368c2a6bf0
|
@ -0,0 +1,133 @@
|
|||
> `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.
|
||||
|
||||
An experimental version of the JSON parser written from scratch on V.
|
||||
|
||||
## Usage
|
||||
```v
|
||||
import x.json2
|
||||
import http
|
||||
|
||||
fn main() {
|
||||
// Decoding
|
||||
resp := http.get('https://example.com')?
|
||||
|
||||
// raw decode
|
||||
raw_person := json2.raw_decode(resp.text)?
|
||||
|
||||
// Casting `Any` type / Navigating
|
||||
person := raw_person.as_map()
|
||||
name := person['name'].str() // Bob
|
||||
age := person['age'].int() // 19
|
||||
pi := person['pi'].f64() // 3.14....
|
||||
|
||||
// Constructing an `Any` type
|
||||
mut me := map[string]json2.Any
|
||||
me['name'] = 'Bob'
|
||||
me['age'] = 18
|
||||
|
||||
mut arr := []json2.Any
|
||||
arr << 'rock'
|
||||
arr << 'papers'
|
||||
arr << json2.null()
|
||||
arr << 12
|
||||
|
||||
me['interests'] = arr
|
||||
|
||||
mut pets := map[string]json2.Any
|
||||
pets['Sam'] = 'Maltese Shitzu'
|
||||
me['pets'] = pets
|
||||
|
||||
// Stringify to JSON
|
||||
println(me.str())
|
||||
//{"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)
|
||||
}
|
||||
```
|
||||
## Using `decode<T>` and `encode<T>`
|
||||
> Codegen for this feature is still WIP. You need to manually define the methods before using the module to structs.
|
||||
|
||||
In order to use the `decode<T>` and `encode<T>` function, you need to explicitly define two methods: `from_json` and `to_json`. `from_json` accepts a `json2.Any` argument and inside of it you need to map the fields you're going to put into the type. As for `to_json` method, you just need to map the values into `json2.Any` and turn it into a string.
|
||||
|
||||
```v
|
||||
struct Person {
|
||||
mut:
|
||||
name string
|
||||
age int = 20
|
||||
pets []string
|
||||
}
|
||||
|
||||
fn (mut p Person) from_json(f json2.Any) {
|
||||
obj := f.as_map()
|
||||
for k, v in obj {
|
||||
match k {
|
||||
'name' { p.name = v.str() }
|
||||
'age' { p.age = v.int() }
|
||||
'pets' { p.pets = v.arr().map(it.str()) }
|
||||
else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (p Person) to_json() string {
|
||||
mut obj := map[string]json2.Any
|
||||
obj['name'] = p.name
|
||||
obj['age'] = p.age
|
||||
obj['pets'] = p.pets
|
||||
return obj.str()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
resp := os.read_file('./person.json')?
|
||||
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"]}
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
### 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.
|
||||
|
||||
```v
|
||||
fn (mut p Person) from_json(f json2.Any) {
|
||||
obj := f.as_map()
|
||||
if obj['age'] is json2.Null {
|
||||
// use a default value
|
||||
p.age = 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```v
|
||||
fn (mut p Person) from_json(f json2.Any) {
|
||||
obj := f.as_map()
|
||||
p.name = obj['nickname'].str()
|
||||
}
|
||||
```
|
||||
|
||||
```v
|
||||
fn (mut p Person) to_json() string {
|
||||
obj := f.as_map()
|
||||
obj['nickname'] = p.name
|
||||
return obj.str()
|
||||
}
|
||||
```
|
||||
|
||||
### Undefined Values
|
||||
Getting undefined values has the same behavior as regular V types. If you're casting a base field into `map[string]json2.Any` and fetch an undefined entry/value, it simply returns empty. As for the `[]json2.Any`, it returns an index error.
|
||||
|
||||
## Casting a value to an incompatible type
|
||||
`x.json2` provides methods for turning `Any` types into usable types. The following list shows the possible outputs when casting a value to an incompatible type.
|
||||
|
||||
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.
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module json2
|
||||
|
||||
// Inserts a string into the map.
|
||||
pub fn (mut obj map[string]Any) insert_str(key string, val string) {
|
||||
mut fi := Any{}
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,392 @@
|
|||
// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module json2
|
||||
|
||||
import strings
|
||||
import strconv
|
||||
import v.scanner
|
||||
import v.token
|
||||
import v.util
|
||||
import v.pref
|
||||
|
||||
// `Any` is a sum type that lists the possible types to be decoded and used.
|
||||
pub type Any = string | int | 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 {}
|
||||
|
||||
enum ParseMode {
|
||||
array
|
||||
bool
|
||||
invalid
|
||||
null
|
||||
number
|
||||
object
|
||||
string
|
||||
}
|
||||
|
||||
const (
|
||||
formfeed_err = 'formfeed not allowed.'
|
||||
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
|
||||
nn_tok token.Token
|
||||
mode ParseMode = .invalid
|
||||
n_level int
|
||||
}
|
||||
|
||||
fn (mut p Parser) next() {
|
||||
p.p_tok = p.tok
|
||||
p.tok = p.n_tok
|
||||
p.n_tok = p.nn_tok
|
||||
p.nn_tok = p.scanner.scan()
|
||||
}
|
||||
|
||||
fn (p Parser) emit_error(msg string) string {
|
||||
source := p.scanner.text
|
||||
cur := p.tok
|
||||
mut pp := util.imax(0, util.imin(source.len - 1, cur.pos))
|
||||
if source.len > 0 {
|
||||
for ; pp >= 0; pp-- {
|
||||
if source[pp] == `\r` || source[pp] == `\n` {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
column := util.imax(0, cur.pos - pp + cur.len - 1)
|
||||
line := cur.line_nr
|
||||
return '[jisoni] ' + msg + ' (At line $line, column $column)'
|
||||
}
|
||||
|
||||
fn new_parser(srce string) Parser {
|
||||
mut src := srce
|
||||
// from v/util/util.v
|
||||
if src.len >= 3 {
|
||||
c_text := src.str
|
||||
unsafe {
|
||||
if c_text[0] == 0xEF && c_text[1] == 0xBB && c_text[2] == 0xBF {
|
||||
// skip three BOM bytes
|
||||
offset_from_begin := 3
|
||||
src = tos(c_text[offset_from_begin], vstrlen(c_text) - offset_from_begin)
|
||||
}
|
||||
}
|
||||
}
|
||||
mut p := Parser{
|
||||
scanner: scanner.new_scanner(src, .parse_comments, &pref.Preferences{}),
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
fn check_valid_hex(str string) ?bool {
|
||||
if str.len != 4 {
|
||||
return error('Hex string must be 4 characters.')
|
||||
}
|
||||
|
||||
for l in str {
|
||||
if l.is_hex_digit() { continue }
|
||||
return error('Provided string is not a hex digit.')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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] == `'`
|
||||
}
|
||||
|
||||
fn (mut p Parser) detect_parse_mode() {
|
||||
src := p.scanner.text
|
||||
if src.len > 1 && src[0].is_digit() && !src[1].is_digit() {
|
||||
p.mode == .invalid
|
||||
return
|
||||
}
|
||||
|
||||
p.tok = p.scanner.scan()
|
||||
p.n_tok = p.scanner.scan()
|
||||
p.nn_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 }
|
||||
.name {
|
||||
if p.tok.lit == 'null' {
|
||||
p.mode = .null
|
||||
}
|
||||
}
|
||||
.minus {
|
||||
if p.n_tok.kind == .number {
|
||||
p.mode = .number
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut p Parser) decode_value() ?Any {
|
||||
mut fi := Any{}
|
||||
|
||||
if (p.tok.kind == .lsbr && p.n_tok.kind == .lcbr) || (p.p_tok.kind == p.tok.kind && p.tok.kind == .lsbr) {
|
||||
p.n_level++
|
||||
}
|
||||
|
||||
if p.n_level == 500 {
|
||||
return error('Reached maximum nesting level of 500.')
|
||||
}
|
||||
|
||||
match p.tok.kind {
|
||||
.lsbr {
|
||||
item := p.decode_array()?
|
||||
fi = item
|
||||
}
|
||||
.lcbr {
|
||||
item := p.decode_object()?
|
||||
fi = item
|
||||
}
|
||||
.number {
|
||||
item := p.decode_number()?
|
||||
fi = item
|
||||
}
|
||||
.key_true {
|
||||
fi = Any(true)
|
||||
}
|
||||
.key_false {
|
||||
fi = Any(false)
|
||||
}
|
||||
.name {
|
||||
if p.tok.lit != 'null' {
|
||||
return error('Unknown identifier `$p.tok.lit`')
|
||||
}
|
||||
|
||||
fi = Any(Null{})
|
||||
}
|
||||
.string {
|
||||
if p.is_singlequote() {
|
||||
return error('Strings must be in double-quotes.')
|
||||
}
|
||||
|
||||
item := p.decode_string() or {
|
||||
return error(err)
|
||||
}
|
||||
|
||||
fi = item
|
||||
}
|
||||
else {
|
||||
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() or {
|
||||
return error(err)
|
||||
}
|
||||
p.next()
|
||||
fi = d_num
|
||||
return fi
|
||||
}
|
||||
|
||||
return error('[decode_value] Unknown token `$p.tok.lit`')
|
||||
}
|
||||
}
|
||||
p.next()
|
||||
|
||||
if p.is_formfeed() {
|
||||
return error(formfeed_err)
|
||||
}
|
||||
|
||||
return fi
|
||||
}
|
||||
|
||||
fn (mut p Parser) decode_string() ?Any {
|
||||
mut strwr := strings.new_builder(200)
|
||||
mut fi := Any{}
|
||||
for i := 0; i < p.tok.lit.len; i++ {
|
||||
// s := p.tok.lit[i].str()
|
||||
// println('$i $s')
|
||||
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 {
|
||||
return error('Invalid backslash escape.')
|
||||
}
|
||||
|
||||
if i+1 < p.tok.lit.len && p.tok.lit[i] == 92 {
|
||||
peek := p.tok.lit[i+1]
|
||||
if peek in [`b`, `f`, `n`, `r`, `t`, `u`, `\\`, `"`, `/`] {
|
||||
if peek == `u` {
|
||||
if i+5 < p.tok.lit.len {
|
||||
codepoint := p.tok.lit[i+2..i+6]
|
||||
check_valid_hex(codepoint) or {
|
||||
return error(err)
|
||||
}
|
||||
hex_val := strconv.parse_int(codepoint, 16, 0)
|
||||
strwr.write_b(byte(hex_val))
|
||||
i += 5
|
||||
continue
|
||||
} else {
|
||||
return error('Incomplete unicode escape.')
|
||||
}
|
||||
}
|
||||
|
||||
i++
|
||||
strwr.write_b(p.tok.lit[i])
|
||||
continue
|
||||
} else {
|
||||
return error('Invalid backslash escape.')
|
||||
}
|
||||
|
||||
if 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])
|
||||
}
|
||||
fi = strwr.str()
|
||||
return fi
|
||||
}
|
||||
|
||||
fn (mut p Parser) decode_number() ?Any {
|
||||
src := p.scanner.text
|
||||
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]
|
||||
is_fl = true
|
||||
last := sep_by_dot.last()
|
||||
|
||||
if last.starts_with('e') {
|
||||
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 {
|
||||
tl = '-' + tl
|
||||
}
|
||||
|
||||
return if is_fl { Any(tl.f64()) } else { Any(tl.int()) }
|
||||
}
|
||||
|
||||
fn (mut p Parser) decode_array() ?Any {
|
||||
mut items := []Any{}
|
||||
p.next()
|
||||
for p.tok.kind != .rsbr {
|
||||
if p.tok.kind == .eof {
|
||||
return error(eof_err)
|
||||
}
|
||||
|
||||
item := p.decode_value() or {
|
||||
return error(err)
|
||||
}
|
||||
|
||||
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 Any(items)
|
||||
}
|
||||
|
||||
fn (mut p Parser) decode_object() ?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)
|
||||
// }
|
||||
|
||||
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`')
|
||||
}
|
||||
|
||||
cur_key = p.tok.lit
|
||||
p.next()
|
||||
p.next()
|
||||
|
||||
item := p.decode_value() or {
|
||||
return error(err)
|
||||
}
|
||||
|
||||
fields[cur_key] = item
|
||||
|
||||
if p.tok.kind == .comma && p.n_tok.kind !in [.rcbr, .comma] {
|
||||
p.next()
|
||||
continue
|
||||
}
|
||||
|
||||
if p.tok.kind == .rcbr {
|
||||
break
|
||||
}
|
||||
|
||||
return error('Unknown token `$p.tok.lit` when decoding object.')
|
||||
}
|
||||
return Any(fields)
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module json2
|
||||
|
||||
import strings
|
||||
// String representation of the `map[string]Any`.
|
||||
pub fn (flds map[string]Any) str() string {
|
||||
mut wr := strings.new_builder(200)
|
||||
wr.write('{')
|
||||
mut i := 0
|
||||
for k, v in flds {
|
||||
wr.write('"$k":')
|
||||
if v is string {
|
||||
wr.write('"' + *v + '"')
|
||||
} else {
|
||||
wr.write(v.str())
|
||||
}
|
||||
if i < flds.len-1 { wr.write(',') }
|
||||
i++
|
||||
}
|
||||
wr.write('}')
|
||||
return wr.str()
|
||||
}
|
||||
// String representation of the `[]Any`.
|
||||
pub fn (flds []Any) str() string {
|
||||
mut wr := strings.new_builder(200)
|
||||
wr.write('[')
|
||||
for i, v in flds {
|
||||
if v is string {
|
||||
wr.write('"' + *v + '"')
|
||||
} else {
|
||||
wr.write(v.str())
|
||||
}
|
||||
if i < flds.len-1 { wr.write(',') }
|
||||
}
|
||||
wr.write(']')
|
||||
return wr.str()
|
||||
}
|
||||
// String representation of the `Any` type.
|
||||
pub fn (f Any) str() string {
|
||||
match f {
|
||||
string { return *f }
|
||||
int { 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' }
|
||||
else {
|
||||
if f is []Any {
|
||||
arr := f
|
||||
return (*arr).str()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module json2
|
||||
|
||||
pub interface Serializable {
|
||||
from_json(f Any)
|
||||
to_json() string
|
||||
}
|
||||
|
||||
// Decodes a JSON string into an `Any` type. Returns an option.
|
||||
pub fn raw_decode(src string) ?Any {
|
||||
mut p := new_parser(src)
|
||||
p.detect_parse_mode()
|
||||
|
||||
if p.mode == .invalid {
|
||||
return error(p.emit_error('Invalid JSON.'))
|
||||
}
|
||||
|
||||
fi := p.decode_value() or {
|
||||
return error(p.emit_error(err))
|
||||
}
|
||||
|
||||
if p.tok.kind != .eof {
|
||||
return error(p.emit_error('Unknown token `$p.tok.kind`.'))
|
||||
}
|
||||
|
||||
return fi
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
mut typ := T{}
|
||||
typ.from_json(res)
|
||||
return typ
|
||||
}
|
||||
// A generic function that encodes a type into a JSON string.
|
||||
pub fn encode<T>(typ T) string {
|
||||
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.
|
||||
pub fn (f Any) as_map() map[string]Any {
|
||||
mut mp := map[string]Any
|
||||
|
||||
match f {
|
||||
map[string]Any {
|
||||
return *f
|
||||
}
|
||||
string {
|
||||
mp['0'] = f
|
||||
return mp
|
||||
}
|
||||
int {
|
||||
mp['0'] = f
|
||||
return mp
|
||||
}
|
||||
bool {
|
||||
mp['0'] = f
|
||||
return mp
|
||||
}
|
||||
f64 {
|
||||
mp['0'] = f
|
||||
return mp
|
||||
}
|
||||
Null {
|
||||
mp['0'] = f
|
||||
return mp
|
||||
}
|
||||
else {
|
||||
if typeof(f) == 'array_Any' {
|
||||
arr := f as []Any
|
||||
for i, fi in arr {
|
||||
mp[i.str()] = fi
|
||||
}
|
||||
|
||||
return mp
|
||||
}
|
||||
|
||||
return mp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use `Any` as an integer.
|
||||
pub fn (f Any) int() int {
|
||||
match f {
|
||||
int { return *f }
|
||||
f64 { return f.str().int() }
|
||||
else { return 0 }
|
||||
}
|
||||
}
|
||||
// Use `Any` as a float.
|
||||
pub fn (f Any) f64() f64 {
|
||||
match f {
|
||||
int { return *f }
|
||||
f64 { return *f }
|
||||
else { return 0.0 }
|
||||
}
|
||||
}
|
||||
// Use `Any` as an array.
|
||||
pub fn (f Any) arr() []Any {
|
||||
if f is []Any {
|
||||
return *f
|
||||
}
|
||||
|
||||
if f is map[string]Any {
|
||||
mut arr := []Any{}
|
||||
mp := *f
|
||||
for _, v in mp {
|
||||
arr << v
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
return [f]
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
import x.json2
|
||||
|
||||
enum JobTitle {
|
||||
manager
|
||||
executive
|
||||
worker
|
||||
}
|
||||
|
||||
struct Employee {
|
||||
name string
|
||||
age int
|
||||
salary f32
|
||||
title JobTitle
|
||||
}
|
||||
|
||||
fn (e Employee) to_json() string {
|
||||
mut mp := map[string]json2.Any
|
||||
mp['name'] = e.name
|
||||
mp['age'] = e.age
|
||||
mp['salary'] = f64(e.salary)
|
||||
mp['title'] = int(e.title)
|
||||
|
||||
/*
|
||||
$for field in Employee.fields {
|
||||
d := e.$(field.name)
|
||||
|
||||
$if field.Type is JobTitle {
|
||||
mp[field.name] = json.encode<int>(d)
|
||||
} $else {
|
||||
mp[field.name] = d
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return mp.str()
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
struct User2 {
|
||||
age int
|
||||
nums []int
|
||||
}
|
||||
|
||||
struct User {
|
||||
age int
|
||||
nums []int
|
||||
last_name string [json: lastName]
|
||||
is_registered bool [json: IsRegistered]
|
||||
typ int [json: 'type']
|
||||
pets string [raw; json: 'pet_animals']
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
println(u2)
|
||||
u := json.decode(User, s) or {
|
||||
exit(1)
|
||||
}
|
||||
println(u)
|
||||
assert u.age == 10
|
||||
assert u.last_name == 'Johnson'
|
||||
assert u.is_registered == true
|
||||
assert u.nums.len == 3
|
||||
assert u.nums[0] == 1
|
||||
assert u.nums[1] == 2
|
||||
assert u.nums[2] == 3
|
||||
assert u.typ == 1
|
||||
assert u.pets == '{"name":"Bob","animal":"Dog"}'
|
||||
}
|
||||
|
||||
fn test_encode_user() {
|
||||
usr := User{
|
||||
age: 10
|
||||
nums: [1, 2, 3]
|
||||
last_name: 'Johnson'
|
||||
is_registered: true
|
||||
typ: 0
|
||||
pets: 'foo'
|
||||
}
|
||||
expected := '{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"foo"}'
|
||||
out := json.encode(usr)
|
||||
println(out)
|
||||
assert out == expected
|
||||
}
|
||||
|
||||
struct Color {
|
||||
space string
|
||||
point string [raw]
|
||||
}
|
||||
|
||||
fn test_raw_json_field() {
|
||||
color := json.decode(Color, '{"space": "YCbCr", "point": {"Y": 123}}') or {
|
||||
println('text')
|
||||
return
|
||||
}
|
||||
assert color.point == '{"Y":123}'
|
||||
assert color.space == 'YCbCr'
|
||||
}
|
||||
|
||||
struct City {
|
||||
name string
|
||||
}
|
||||
|
||||
struct Country {
|
||||
cities []City
|
||||
name string
|
||||
}
|
||||
|
||||
fn test_struct_in_struct() {
|
||||
country := json.decode(Country, '{ "name": "UK", "cities": [{"name":"London"}, {"name":"Manchester"}]}') or {
|
||||
assert false
|
||||
exit(1)
|
||||
}
|
||||
assert country.name == 'UK'
|
||||
assert country.cities.len == 2
|
||||
assert country.cities[0].name == 'London'
|
||||
assert country.cities[1].name == 'Manchester'
|
||||
println(country.cities)
|
||||
}
|
||||
*/
|
||||
|
||||
fn test_encode_map() {
|
||||
expected := '{"one":1,"two":2,"three":3,"four":4}'
|
||||
numbers := {
|
||||
'one': json2.Any(1)
|
||||
'two': json2.Any(2)
|
||||
'three': json2.Any(3)
|
||||
'four': json2.Any(4)
|
||||
}
|
||||
out := numbers.str()
|
||||
// out := json.encode(numbers)
|
||||
|
||||
println(out)
|
||||
assert out == expected
|
||||
}
|
||||
/*
|
||||
fn test_parse_map() {
|
||||
expected := {
|
||||
'one': 1
|
||||
'two': 2
|
||||
'three': 3
|
||||
'four': 4
|
||||
}
|
||||
out := json.decode<map[string]int>('{"one":1,"two":2,"three":3,"four":4}') or {
|
||||
assert false
|
||||
r := {
|
||||
'': 0
|
||||
}
|
||||
r
|
||||
}
|
||||
println(out)
|
||||
assert out == expected
|
||||
}
|
||||
|
||||
struct Data {
|
||||
countries []Country
|
||||
users map[string]User
|
||||
extra map[string]map[string]int
|
||||
}
|
||||
|
||||
fn test_nested_type() {
|
||||
data_expected := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":[{"name":"Donlon"},{"name":"Termanches"}],"name":"KU"}],"users":{"Foo":{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"little foo"},"Boo":{"age":20,"nums":[5,3,1],"lastName":"Smith","IsRegistered":false,"type":4,"pet_animals":"little boo"}},"extra":{"2":{"n1":2,"n2":4,"n3":8,"n4":16},"3":{"n1":3,"n2":9,"n3":27,"n4":81}}}'
|
||||
|
||||
data := Data{
|
||||
countries: [
|
||||
Country{
|
||||
name: 'UK'
|
||||
cities: [City{'London'},
|
||||
City{'Manchester'},
|
||||
]
|
||||
},
|
||||
Country{
|
||||
name: 'KU'
|
||||
cities: [City{'Donlon'},
|
||||
City{'Termanches'},
|
||||
]
|
||||
},
|
||||
]
|
||||
users: {
|
||||
'Foo': User{
|
||||
age: 10
|
||||
nums: [1, 2, 3]
|
||||
last_name: 'Johnson'
|
||||
is_registered: true
|
||||
typ: 0
|
||||
pets: 'little foo'
|
||||
},
|
||||
'Boo': User{
|
||||
age: 20
|
||||
nums: [5, 3, 1]
|
||||
last_name: 'Smith'
|
||||
is_registered: false
|
||||
typ: 4
|
||||
pets: 'little boo'
|
||||
}
|
||||
},
|
||||
extra: {
|
||||
'2': {
|
||||
'n1': 2
|
||||
'n2': 4
|
||||
'n3': 8
|
||||
'n4': 16
|
||||
},
|
||||
'3': {
|
||||
'n1': 3
|
||||
'n2': 9
|
||||
'n3': 27
|
||||
'n4': 81
|
||||
},
|
||||
}
|
||||
}
|
||||
out := json.encode(data)
|
||||
println(out)
|
||||
assert out == data_expected
|
||||
|
||||
data2 := json.decode(Data, data_expected) or {
|
||||
assert false
|
||||
Data{}
|
||||
}
|
||||
assert data2.countries.len == data.countries.len
|
||||
for i in 0..1 {
|
||||
assert data2.countries[i].name == data.countries[i].name
|
||||
assert data2.countries[i].cities.len == data.countries[i].cities.len
|
||||
for j in 0..1 {
|
||||
assert data2.countries[i].cities[j].name == data.countries[i].cities[j].name
|
||||
}
|
||||
}
|
||||
|
||||
for key, user in data.users {
|
||||
assert data2.users[key].age == user.age
|
||||
assert data2.users[key].nums == user.nums
|
||||
assert data2.users[key].last_name == user.last_name
|
||||
assert data2.users[key].is_registered == user.is_registered
|
||||
assert data2.users[key].typ == user.typ
|
||||
// assert data2.users[key].pets == user.pets // TODO FIX
|
||||
}
|
||||
|
||||
for k, v in data.extra {
|
||||
for k2, v2 in v {
|
||||
assert data2.extra[k][k2] == v2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_errors() {
|
||||
invalid_array := fn () {
|
||||
data := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":{"name":"Donlon"},"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}}}'
|
||||
|
||||
json.decode(Data, data) or {
|
||||
println(err)
|
||||
assert err.starts_with('Json element is not an array:')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
invalid_object := fn() {
|
||||
data := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":[{"name":"Donlon"},{"name":"Termanches"}],"name":"KU"}],"users":[{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"little foo"},{"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}}}'
|
||||
|
||||
json.decode(Data, data) or {
|
||||
println(err)
|
||||
assert err.starts_with('Json element is not an object:')
|
||||
return
|
||||
}
|
||||
assert false
|
||||
}
|
||||
invalid_array()
|
||||
invalid_object()
|
||||
}
|
||||
*/
|
Loading…
Reference in New Issue