650 lines
13 KiB
V
650 lines
13 KiB
V
// Copyright (c) 2019-2024 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 json
|
|
|
|
import math
|
|
import strconv
|
|
import time
|
|
|
|
#flag -I @VEXEROOT/thirdparty/cJSON
|
|
#flag @VEXEROOT/thirdparty/cJSON/cJSON.o
|
|
#include "cJSON.h"
|
|
#define js_get(object, key) cJSON_GetObjectItemCaseSensitive((object), (key))
|
|
|
|
// As cJSON use `libm`, we need to link it.
|
|
$if windows {
|
|
$if tinyc {
|
|
#flag @VEXEROOT/thirdparty/tcc/lib/openlibm.o
|
|
}
|
|
} $else {
|
|
#flag -lm
|
|
}
|
|
|
|
pub struct C.cJSON {
|
|
next &C.cJSON
|
|
prev &C.cJSON
|
|
child &C.cJSON
|
|
type int
|
|
valuestring &char
|
|
valueint int
|
|
valuedouble f64
|
|
}
|
|
|
|
fn C.cJSON_IsTrue(&C.cJSON) bool
|
|
fn C.cJSON_IsFalse(&C.cJSON) bool
|
|
fn C.cJSON_IsNull(&C.cJSON) bool
|
|
fn C.cJSON_IsNumber(&C.cJSON) bool
|
|
fn C.cJSON_IsString(&C.cJSON) bool
|
|
fn C.cJSON_IsObject(&C.cJSON) bool
|
|
fn C.cJSON_IsArray(&C.cJSON) bool
|
|
|
|
fn C.cJSON_CreateNumber(f64) &C.cJSON
|
|
fn C.cJSON_CreateRaw(&char) &C.cJSON
|
|
|
|
fn C.cJSON_CreateBool(bool) &C.cJSON
|
|
|
|
fn C.cJSON_CreateString(&char) &C.cJSON
|
|
|
|
fn C.cJSON_CreateRaw(&char) &C.cJSON
|
|
|
|
fn C.cJSON_Parse(&char) &C.cJSON
|
|
|
|
fn C.cJSON_PrintUnformatted(&C.cJSON) &char
|
|
|
|
fn C.cJSON_Print(&C.cJSON) &char
|
|
|
|
fn C.cJSON_free(voidptr)
|
|
fn C.malloc(usize) voidptr
|
|
fn C.memcpy(voidptr, voidptr, usize) voidptr
|
|
|
|
// decode tries to decode the provided JSON string, into a V structure.
|
|
// If it can not do that, it returns an error describing the reason for
|
|
// the parsing failure.
|
|
pub fn decode(typ voidptr, s string) !voidptr {
|
|
// compiler implementation
|
|
return 0
|
|
}
|
|
|
|
// encode serialises the provided V value as a JSON string, optimised for shortness.
|
|
pub fn encode(x voidptr) string {
|
|
// compiler implementation
|
|
return ''
|
|
}
|
|
|
|
// encode_pretty serialises the provided V value as a JSON string, in a formatted way, optimised for viewing by humans.
|
|
pub fn encode_pretty(x voidptr) string {
|
|
// compiler implementation
|
|
return ''
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_int(root &C.cJSON) int {
|
|
if isnil(root) {
|
|
return 0
|
|
}
|
|
return root.valueint
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_i8(root &C.cJSON) i8 {
|
|
if isnil(root) {
|
|
return i8(0)
|
|
}
|
|
return i8(root.valueint)
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_i16(root &C.cJSON) i16 {
|
|
if isnil(root) {
|
|
return i16(0)
|
|
}
|
|
return i16(root.valueint)
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_i64(root &C.cJSON) i64 {
|
|
if isnil(root) {
|
|
return i64(0)
|
|
}
|
|
if value := decode_exact_i64(root) {
|
|
return value
|
|
}
|
|
return i64(root.valuedouble) // i64 is double in C
|
|
}
|
|
|
|
// TODO: remove when `byte` is removed
|
|
@[markused]
|
|
fn decode_byte(root &C.cJSON) u8 {
|
|
return decode_u8(root)
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_u8(root &C.cJSON) u8 {
|
|
if isnil(root) {
|
|
return u8(0)
|
|
}
|
|
return u8(root.valueint)
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_u16(root &C.cJSON) u16 {
|
|
if isnil(root) {
|
|
return u16(0)
|
|
}
|
|
return u16(root.valueint)
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_u32(root &C.cJSON) u32 {
|
|
if isnil(root) {
|
|
return u32(0)
|
|
}
|
|
return u32(root.valueint)
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_u64(root &C.cJSON) u64 {
|
|
if isnil(root) {
|
|
return u64(0)
|
|
}
|
|
if value := decode_exact_u64(root) {
|
|
return value
|
|
}
|
|
return u64(root.valuedouble)
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_f32(root &C.cJSON) f32 {
|
|
if isnil(root) {
|
|
return f32(0)
|
|
}
|
|
return f32(root.valuedouble)
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_f64(root &C.cJSON) f64 {
|
|
if isnil(root) {
|
|
return f64(0)
|
|
}
|
|
return root.valuedouble
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_rune(root &C.cJSON) rune {
|
|
if isnil(root) {
|
|
return rune(0)
|
|
}
|
|
if isnil(root.valuestring) || !C.cJSON_IsString(root) {
|
|
return rune(0)
|
|
}
|
|
|
|
// TODO: Parse as runes, bypassing string casting...?
|
|
return unsafe { tos_clone(&u8(root.valuestring)).runes().first() }
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_string(root &C.cJSON) string {
|
|
if isnil(root) {
|
|
return ''
|
|
}
|
|
if !isnil(root.valuestring) && C.cJSON_IsString(root) {
|
|
return unsafe { tos_clone(&u8(root.valuestring)) } // , _strlen(root.valuestring))
|
|
}
|
|
// Object/array values can be stringified JSON payloads (e.g. `{}` from JSON.stringify()).
|
|
if C.cJSON_IsObject(root) || C.cJSON_IsArray(root) {
|
|
return json_print(root)
|
|
}
|
|
return ''
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_bool(root &C.cJSON) bool {
|
|
if isnil(root) {
|
|
return false
|
|
}
|
|
return C.cJSON_IsTrue(root)
|
|
}
|
|
|
|
@[markused]
|
|
fn decode_time(root &C.cJSON) !time.Time {
|
|
if isnil(root) || C.cJSON_IsNull(root) {
|
|
return time.Time{}
|
|
}
|
|
mut decoded_time := time.Time{}
|
|
if C.cJSON_IsString(root) {
|
|
decoded_time.from_json_string(decode_string(root))!
|
|
return decoded_time
|
|
}
|
|
if C.cJSON_IsNumber(root) {
|
|
decoded_time.from_json_number(json_print(root))!
|
|
return decoded_time
|
|
}
|
|
return error('expected time.Time to decode from a JSON string or number, got: ${json_print(root)}')
|
|
}
|
|
|
|
// ///////////////////
|
|
|
|
@[markused]
|
|
fn encode_int(val int) &C.cJSON {
|
|
return C.cJSON_CreateNumber(val)
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_i8(val i8) &C.cJSON {
|
|
return C.cJSON_CreateNumber(val)
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_i16(val i16) &C.cJSON {
|
|
return C.cJSON_CreateNumber(val)
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_i64(val i64) &C.cJSON {
|
|
lit := val.str()
|
|
return C.cJSON_CreateRaw(&char(lit.str))
|
|
}
|
|
|
|
// TODO: remove when `byte` is removed
|
|
@[markused]
|
|
fn encode_byte(root u8) &C.cJSON {
|
|
return encode_u8(root)
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_u8(val u8) &C.cJSON {
|
|
return C.cJSON_CreateNumber(val)
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_u16(val u16) &C.cJSON {
|
|
return C.cJSON_CreateNumber(val)
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_u32(val u32) &C.cJSON {
|
|
return C.cJSON_CreateNumber(val)
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_u64(val u64) &C.cJSON {
|
|
lit := val.str()
|
|
return C.cJSON_CreateRaw(&char(lit.str))
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_f32(val f32) &C.cJSON {
|
|
return C.cJSON_CreateRaw(&char(json_float_to_raw_string(val).str))
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_f64(val f64) &C.cJSON {
|
|
return C.cJSON_CreateRaw(&char(json_float_to_raw_string(val).str))
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_bool(val bool) &C.cJSON {
|
|
return C.cJSON_CreateBool(val)
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_rune(val rune) &C.cJSON {
|
|
return C.cJSON_CreateRaw(&char(json_ascii_string(val.str()).str))
|
|
}
|
|
|
|
@[markused]
|
|
fn encode_string(val string) &C.cJSON {
|
|
return C.cJSON_CreateRaw(&char(json_ascii_string(val).str))
|
|
}
|
|
|
|
// json_ascii_string returns a quoted JSON string with non-ASCII runes escaped as `\uXXXX`.
|
|
fn json_ascii_string(val string) string {
|
|
mut output := []u8{cap: val.len + 2}
|
|
output << `"`
|
|
for character in val.runes() {
|
|
match character {
|
|
`"` {
|
|
output << `\\`
|
|
output << `"`
|
|
}
|
|
`\\` {
|
|
output << `\\`
|
|
output << `\\`
|
|
}
|
|
`\b` {
|
|
output << `\\`
|
|
output << `b`
|
|
}
|
|
`\f` {
|
|
output << `\\`
|
|
output << `f`
|
|
}
|
|
`\n` {
|
|
output << `\\`
|
|
output << `n`
|
|
}
|
|
`\r` {
|
|
output << `\\`
|
|
output << `r`
|
|
}
|
|
`\t` {
|
|
output << `\\`
|
|
output << `t`
|
|
}
|
|
else {
|
|
if character < 0x20 || character > 0x7f {
|
|
if character <= 0xffff {
|
|
output << `\\`
|
|
output << `u`
|
|
hex_string := '${u32(character):04x}'
|
|
unsafe { output.push_many(hex_string.str, 4) }
|
|
} else {
|
|
unicode_point_low := u32(character) - 0x10000
|
|
surrogate_pair := '\\u${0xD800 + ((unicode_point_low >> 10) & 0x3FF):04X}\\u${
|
|
0xDC00 + (unicode_point_low & 0x3FF):04x}'
|
|
unsafe { output.push_many(surrogate_pair.str, surrogate_pair.len) }
|
|
}
|
|
} else {
|
|
output << u8(character)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
output << `"`
|
|
return output.bytestr()
|
|
}
|
|
|
|
// json_float_to_raw_string uses V's float formatter so json.encode keeps exact float round-trips.
|
|
fn json_float_to_raw_string[T](val T) string {
|
|
if val == 0 {
|
|
return '0'
|
|
}
|
|
if math.is_nan(f64(val)) || math.is_inf(f64(val), 0) {
|
|
return 'null'
|
|
}
|
|
mut raw := val.str()
|
|
if raw.len > 2 && raw[raw.len - 2] == `.` && raw[raw.len - 1] == `0` {
|
|
raw = raw[..raw.len - 2]
|
|
}
|
|
return raw
|
|
}
|
|
|
|
fn decode_exact_i64(root &C.cJSON) ?i64 {
|
|
if isnil(root) || isnil(root.valuestring) {
|
|
return none
|
|
}
|
|
lit := unsafe { tos_clone(&u8(root.valuestring)) }
|
|
value := strconv.parse_int(lit, 10, 64) or { return none }
|
|
return value
|
|
}
|
|
|
|
fn decode_exact_u64(root &C.cJSON) ?u64 {
|
|
if isnil(root) || isnil(root.valuestring) {
|
|
return none
|
|
}
|
|
lit := unsafe { tos_clone(&u8(root.valuestring)) }
|
|
value := strconv.parse_uint(lit, 10, 64) or { return none }
|
|
return value
|
|
}
|
|
|
|
fn clone_cstring(s string) &char {
|
|
buf := C.malloc(usize(s.len + 1))
|
|
if buf == unsafe { nil } {
|
|
return unsafe { nil }
|
|
}
|
|
unsafe {
|
|
if s.len > 0 {
|
|
C.memcpy(buf, s.str, usize(s.len))
|
|
}
|
|
(&u8(buf))[s.len] = 0
|
|
}
|
|
return &char(buf)
|
|
}
|
|
|
|
@[inline]
|
|
fn is_json_whitespace(ch u8) bool {
|
|
return ch == ` ` || ch == `\n` || ch == `\r` || ch == `\t`
|
|
}
|
|
|
|
fn skip_json_whitespace(src string, idx int) int {
|
|
mut pos := idx
|
|
for pos < src.len && is_json_whitespace(src[pos]) {
|
|
pos++
|
|
}
|
|
return pos
|
|
}
|
|
|
|
fn skip_json_string(src string, idx int) int {
|
|
mut pos := idx
|
|
if pos >= src.len || src[pos] != `"` {
|
|
return -1
|
|
}
|
|
pos++
|
|
for pos < src.len {
|
|
match src[pos] {
|
|
`"` {
|
|
return pos + 1
|
|
}
|
|
`\\` {
|
|
pos++
|
|
if pos >= src.len {
|
|
return -1
|
|
}
|
|
if src[pos] == `u` {
|
|
pos += 5
|
|
} else {
|
|
pos++
|
|
}
|
|
}
|
|
else {
|
|
pos++
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
fn skip_json_literal(src string, lit string, idx int) int {
|
|
if idx + lit.len > src.len || src[idx..idx + lit.len] != lit {
|
|
return -1
|
|
}
|
|
return idx + lit.len
|
|
}
|
|
|
|
fn skip_json_number(src string, idx int) (string, int) {
|
|
mut pos := idx
|
|
start := pos
|
|
if pos < src.len && src[pos] == `-` {
|
|
pos++
|
|
}
|
|
if pos >= src.len {
|
|
return '', -1
|
|
}
|
|
if src[pos] == `0` {
|
|
pos++
|
|
} else {
|
|
if !src[pos].is_digit() {
|
|
return '', -1
|
|
}
|
|
for pos < src.len && src[pos].is_digit() {
|
|
pos++
|
|
}
|
|
}
|
|
if pos < src.len && src[pos] == `.` {
|
|
pos++
|
|
if pos >= src.len || !src[pos].is_digit() {
|
|
return '', -1
|
|
}
|
|
for pos < src.len && src[pos].is_digit() {
|
|
pos++
|
|
}
|
|
}
|
|
if pos < src.len && (src[pos] == `e` || src[pos] == `E`) {
|
|
pos++
|
|
if pos < src.len && (src[pos] == `+` || src[pos] == `-`) {
|
|
pos++
|
|
}
|
|
if pos >= src.len || !src[pos].is_digit() {
|
|
return '', -1
|
|
}
|
|
for pos < src.len && src[pos].is_digit() {
|
|
pos++
|
|
}
|
|
}
|
|
return src[start..pos], pos
|
|
}
|
|
|
|
fn annotate_json_number_literal(root &C.cJSON, lit string) {
|
|
if lit.len == 0 || !isnil(root.valuestring) {
|
|
return
|
|
}
|
|
unsafe {
|
|
root.valuestring = clone_cstring(lit)
|
|
}
|
|
}
|
|
|
|
fn annotate_json_value(root &C.cJSON, src string, idx int) int {
|
|
if isnil(root) {
|
|
return -1
|
|
}
|
|
mut pos := skip_json_whitespace(src, idx)
|
|
if C.cJSON_IsObject(root) {
|
|
if pos >= src.len || src[pos] != `{` {
|
|
return -1
|
|
}
|
|
pos++
|
|
pos = skip_json_whitespace(src, pos)
|
|
mut child := root.child
|
|
if isnil(child) {
|
|
if pos >= src.len || src[pos] != `}` {
|
|
return -1
|
|
}
|
|
return pos + 1
|
|
}
|
|
for !isnil(child) {
|
|
pos = skip_json_string(src, pos)
|
|
if pos == -1 {
|
|
return -1
|
|
}
|
|
pos = skip_json_whitespace(src, pos)
|
|
if pos >= src.len || src[pos] != `:` {
|
|
return -1
|
|
}
|
|
pos++
|
|
pos = annotate_json_value(child, src, pos)
|
|
if pos == -1 {
|
|
return -1
|
|
}
|
|
pos = skip_json_whitespace(src, pos)
|
|
if !isnil(child.next) {
|
|
if pos >= src.len || src[pos] != `,` {
|
|
return -1
|
|
}
|
|
pos++
|
|
}
|
|
child = child.next
|
|
}
|
|
if pos >= src.len || src[pos] != `}` {
|
|
return -1
|
|
}
|
|
return pos + 1
|
|
}
|
|
if C.cJSON_IsArray(root) {
|
|
if pos >= src.len || src[pos] != `[` {
|
|
return -1
|
|
}
|
|
pos++
|
|
pos = skip_json_whitespace(src, pos)
|
|
mut child := root.child
|
|
if isnil(child) {
|
|
if pos >= src.len || src[pos] != `]` {
|
|
return -1
|
|
}
|
|
return pos + 1
|
|
}
|
|
for !isnil(child) {
|
|
pos = annotate_json_value(child, src, pos)
|
|
if pos == -1 {
|
|
return -1
|
|
}
|
|
pos = skip_json_whitespace(src, pos)
|
|
if !isnil(child.next) {
|
|
if pos >= src.len || src[pos] != `,` {
|
|
return -1
|
|
}
|
|
pos++
|
|
}
|
|
child = child.next
|
|
}
|
|
if pos >= src.len || src[pos] != `]` {
|
|
return -1
|
|
}
|
|
return pos + 1
|
|
}
|
|
if C.cJSON_IsString(root) {
|
|
return skip_json_string(src, pos)
|
|
}
|
|
if C.cJSON_IsNumber(root) {
|
|
lit, next_pos := skip_json_number(src, pos)
|
|
if next_pos == -1 {
|
|
return -1
|
|
}
|
|
annotate_json_number_literal(root, lit)
|
|
return next_pos
|
|
}
|
|
if C.cJSON_IsTrue(root) {
|
|
return skip_json_literal(src, 'true', pos)
|
|
}
|
|
if C.cJSON_IsFalse(root) {
|
|
return skip_json_literal(src, 'false', pos)
|
|
}
|
|
if C.cJSON_IsNull(root) {
|
|
return skip_json_literal(src, 'null', pos)
|
|
}
|
|
return -1
|
|
}
|
|
|
|
fn annotate_json_number_literals(root &C.cJSON, src string) {
|
|
_ := annotate_json_value(root, src, 0)
|
|
}
|
|
|
|
// ///////////////////////
|
|
// user := decode_User(json_parse(js_string_var))
|
|
@[markused]
|
|
fn json_parse(s string) &C.cJSON {
|
|
root := C.cJSON_Parse(&char(s.str))
|
|
if !isnil(root) {
|
|
annotate_json_number_literals(root, s)
|
|
}
|
|
return root
|
|
}
|
|
|
|
// json_string := json_print(encode_User(user))
|
|
@[markused]
|
|
fn json_print(data &C.cJSON) string {
|
|
s := C.cJSON_PrintUnformatted(data)
|
|
if s == unsafe { nil } {
|
|
return ''
|
|
}
|
|
r := unsafe { tos_clone(&u8(s)) }
|
|
C.cJSON_free(s)
|
|
return r
|
|
}
|
|
|
|
@[markused]
|
|
fn json_print_pretty(data &C.cJSON) string {
|
|
s := C.cJSON_Print(data)
|
|
if s == unsafe { nil } {
|
|
return ''
|
|
}
|
|
r := unsafe { tos_clone(&u8(s)) }
|
|
C.cJSON_free(s)
|
|
return r
|
|
}
|
|
|
|
// / cjson wrappers
|
|
// fn json_array_for_each(val, root &C.cJSON) {
|
|
// #cJSON_ArrayForEach (val ,root)
|
|
// }
|