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