Skip to content

Commit

Permalink
std/encoding/json: add dynamic decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
mertcandav committed Feb 8, 2025
1 parent 1219e1c commit 62ae718
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 29 deletions.
121 changes: 92 additions & 29 deletions std/encoding/json/decode.jule
Original file line number Diff line number Diff line change
Expand Up @@ -275,21 +275,25 @@ impl jsonDecoder {
}
}

fn objectMap[Map: map[K]V, K, V](self, mut &m: Map)! {
fn objectMap[Map](self, mut &m: Map)! {
const mapT = comptime::TypeOf(Map)
comptime::TypeAlias(K, mapT.Key())
comptime::TypeAlias(V, mapT.Value())
const keyT = comptime::TypeOf(K)
const match keyT.Kind() {
| comptime::Kind.Str
| comptime::Kind.Int
| comptime::Kind.I8
| comptime::Kind.I16
| comptime::Kind.I32
| comptime::Kind.I64
| comptime::Kind.Uint
| comptime::Kind.Uintptr
| comptime::Kind.U8
| comptime::Kind.U16
| comptime::Kind.U32
| comptime::Kind.U64:
const match {
| keyT == valueT | keyT == stringT
| keyT.Kind() == comptime::Kind.Str
| keyT.Kind() == comptime::Kind.Int
| keyT.Kind() == comptime::Kind.I8
| keyT.Kind() == comptime::Kind.I16
| keyT.Kind() == comptime::Kind.I32
| keyT.Kind() == comptime::Kind.I64
| keyT.Kind() == comptime::Kind.Uint
| keyT.Kind() == comptime::Kind.Uintptr
| keyT.Kind() == comptime::Kind.U8
| keyT.Kind() == comptime::Kind.U16
| keyT.Kind() == comptime::Kind.U32
| keyT.Kind() == comptime::Kind.U64:
break
|:
error(EncodeError.UnsupportedType)
Expand All @@ -300,7 +304,8 @@ impl jsonDecoder {
ret
}
self.pushParseState(parseState.Object) else { error(error) }
const quoted = keyT.Kind() == comptime::Kind.Str
// Consider string and dynamic JSON vlaue types as quoted. Directly assign string as key.
const quoted = keyT.Kind() == comptime::Kind.Str || keyT == valueT || keyT == stringT
for {
self.skipSpace()
if self.eof() || self.data[self.i] != '"' {
Expand Down Expand Up @@ -328,7 +333,12 @@ impl jsonDecoder {
if m == nil {
m = {} // create new map
}
m[K(s)] = value
const match {
| keyT == valueT:
m[String(s)] = value
|:
m[K(s)] = value
}
|:
// Quoted non-string type, parse unquoted value by type to assign.
key := unquoteBytes(lit)
Expand All @@ -345,7 +355,7 @@ impl jsonDecoder {
panic("json: unimplemented type, this panic call should be unreachable")
}
if m == nil {
m = {} // create new map
m = {} // Create new empty map.
}
m[keyV] = value
}
Expand Down Expand Up @@ -502,27 +512,44 @@ impl jsonDecoder {
b := self.data[self.i]
match b {
| '{': // Object.
const match tt.Kind() {
| comptime::Kind.Map:
const match {
| tt == valueT | tt == objectT:
mut m := Object{}
self.objectMap(m) else { error(error) }
t = m
ret
| tt.Kind() == comptime::Kind.Map:
self.objectMap(t) else { error(error) }
ret
| comptime::Kind.Struct:
| tt.Kind() == comptime::Kind.Struct:
self.objectStruct(t) else { error(error) }
ret
|:
error(DecodeError.InvalidValue)
}
| '[': // Array.
const match tt.Kind() {
| comptime::Kind.Array
| comptime::Kind.Slice:
const match {
| tt == valueT | tt == arrayT:
mut array := Array(nil)
self.array(array) else { error(error) }
t = array
ret
| tt.Kind() == comptime::Kind.Array
| tt.Kind() == comptime::Kind.Slice:
self.array(t) else { error(error) }
ret
|:
error(DecodeError.InvalidValue)
}
| '"': // String literal.
const match {
| tt == valueT | tt == stringT:
// Don't check validity for literal, following algorithms will check it.
lit := self.scanLit() else { error(error) }
mut s := ""
decodeString(s, lit) else { error(error) }
t = String(s)
ret
| tt.Kind() == comptime::Kind.Str:
// Don't check validity for literal, following algorithms will check it.
lit := self.scanLit() else { error(error) }
Expand Down Expand Up @@ -553,8 +580,11 @@ impl jsonDecoder {
}
| 't' | 'f': // Boolean literal.
self.scanValidLit() else { error(error) }
const match tt.Kind() {
| comptime::Kind.Bool:
const match {
| tt == valueT | tt == boolT:
t = Bool(b == 't')
ret
| tt.Kind() == comptime::Kind.Bool:
t = b == 't'
ret
|:
Expand All @@ -565,14 +595,29 @@ impl jsonDecoder {
// Don't check validity for literal, following algorithms will check it.
lit := self.scanLit() else { error(error) }
_ = lit // Avoid unused error.
const match tt.Kind() {
| comptime::Kind.Int | comptime::Kind.I8 | comptime::Kind.I16 | comptime::Kind.I32 | comptime::Kind.I64:
const match {
| tt == valueT | tt == numberT:
mut f := f64(0)
decodeFloat(f, lit) else { error(error) }
t = Number(f)
ret
| tt.Kind() == comptime::Kind.Int
| tt.Kind() == comptime::Kind.I8
| tt.Kind() == comptime::Kind.I16
| tt.Kind() == comptime::Kind.I32
| tt.Kind() == comptime::Kind.I64:
decodeInt(t, lit) else { error(error) }
ret
| comptime::Kind.Uint | comptime::Kind.Uintptr | comptime::Kind.U8 | comptime::Kind.U16 | comptime::Kind.U32 | comptime::Kind.U64:
| tt.Kind() == comptime::Kind.Uint
| tt.Kind() == comptime::Kind.Uintptr
| tt.Kind() == comptime::Kind.U8
| tt.Kind() == comptime::Kind.U16
| tt.Kind() == comptime::Kind.U32
| tt.Kind() == comptime::Kind.U64:
decodeUInt(t, lit) else { error(error) }
ret
| comptime::Kind.F32 | comptime::Kind.F64:
| tt.Kind() == comptime::Kind.F32
| tt.Kind() == comptime::Kind.F64:
decodeFloat(t, lit) else { error(error) }
ret
|:
Expand Down Expand Up @@ -669,6 +714,24 @@ impl jsonDecoder {
// If smart pointer is nil, will be allocated by the algorithm for decoding.
// Otherwise, will decode into dereferenced value.
//
// Dynamic decoding details:
// Dynamic JSON decoding uses dynamic JSON types:
// Value, Object, Array, Bool, Number, and String.
// No dynamic decoding can be achieved outside of these types;
// for example, the [any] type is not supported.
// If you want to obtain any JSON value, use [Value] instead.
//
// Dynamic decoding will always decode using dynamic types;
// nil -> for JSON null
// Object -> for JSON object
// Array -> for JSON array
// Bool -> for JSON boolean
// Number -> for JSON number
// String -> for JSON string
//
// If you use Value as destination type, it may store any JSON value,
// and the type will be determined dynamically based on the JSON value.
//
// Too many nested types are not specifically checked and may cause too many
// recursive function calls, resulting in a crash at runtime. As a result of the tests,
// it is recommended that a data type can carry a maximum of 10000 nested data.
Expand Down
138 changes: 138 additions & 0 deletions std/encoding/json/decode_test.jule
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD 3-Clause
// license that can be found in the LICENSE file.

use "std/conv"
use "std/encoding"
use "std/strings"
use "std/testing"

#test
Expand Down Expand Up @@ -346,4 +348,140 @@ fn testObjectCustomDecode(t: &testing::T) {
if f.baz != "custom json" {
t.Errorf("objectfoo.baz != \"custom json\", found \"{}\"", f.baz)
}
}

fn printArray(mut &s: strings::Builder, a: Value) {
if a == nil {
s.WriteStr("nil")!
ret
}
match type a {
| String:
s.WriteStr("\"")!
s.WriteStr(str(String(a)))!
s.WriteStr("\"")!
| Bool:
if Bool(a) {
s.WriteStr("true")!
} else {
s.WriteStr("false")!
}
| Number:
s.WriteStr(conv::FmtFloat(f64(Number(a)), 'f', -1, 64))!
| Array:
s.WriteByte('[')!
for _, e in Array(a) {
printArray(s, e)
s.WriteByte(',')!
}
s.WriteByte(']')!
|:
panic("unreachable")
}
}

#test
fn testDynDecode(t: &testing::T) {
mut v := new(Value)
data := []byte(`
{
"foo": "pi",
"bar": "number",
"baz": {
"foo2": 123.456,
"bar2": null
},
"fiz": [
"barbaz",
1234,
-1234,
123.456,
null,
false,
true
]
}
`)
Decode(data, v) else {
t.Errorf("unexpected error: {}", error)
ret
}
mut ok := false
m, ok := Object(*v)
if !ok {
t.Errorf("value is not object")
ret
}
*v, ok = m["foo"]
if !ok {
t.Errorf("key is not exist: foo")
} else {
s, (ok) := String(*v)
if ok {
if s != "pi" {
t.Errorf("key foo is not `pi`")
}
} else {
t.Errorf("key foo is not string")
}
}
*v, ok = m["bar"]
if !ok {
t.Errorf("key is not exist: bar")
} else {
s, (ok) := String(*v)
if ok {
if s != "number" {
t.Errorf("key bar is not `number`")
}
} else {
t.Errorf("key bar is not string")
}
}
*v, ok = m["baz"]
if !ok {
t.Errorf("key is not exist: baz")
} else {
sm, (ok) := Object(*v)
if ok {
*v, ok = sm["foo2"]
if ok {
number, (ok) := Number(*v)
if ok {
if number != 123.456 {
t.Errorf("key bar2 is not 123.456")
}
} else {
t.Errorf("key bar2 is not number")
}
}
*v, ok = sm["bar2"]
if ok {
if *v != nil {
t.Errorf("key bar2 is not nil")
}
} else {
t.Errorf("key is not exist: bar2")
}
} else {
t.Errorf("key baz is not object")
}
}
*v, ok = m["fiz"]
if !ok {
t.Errorf("key is not exist: fiz")
} else {
_, (ok) := Array(*v)
if ok {
mut s := strings::Builder{}
printArray(s, *v)
found := s.Str()
want := `["barbaz",1234,-1234,123.456,nil,false,true,]`
if found != want {
t.Errorf("key fiz is not expected array;\n\tfound: {}\n\t want: {}", found, want)
}
} else {
t.Errorf("key fiz is not array")
}
}
}
38 changes: 38 additions & 0 deletions std/encoding/json/dyn.jule
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2025 The Jule Programming Language.
// Use of this source code is governed by a BSD 3-Clause
// license that can be found in the LICENSE file.

use "std/comptime"

// Comptime type information for the dynamic JSON value types.
const valueT = comptime::TypeOf(Value)
const objectT = comptime::TypeOf(Object)
const arrayT = comptime::TypeOf(Array)
const boolT = comptime::TypeOf(Bool)
const numberT = comptime::TypeOf(Number)
const stringT = comptime::TypeOf(String)

// Dynamic JSON value type.
// Can store any JSON value.
enum Value: type {
Object,
Array,
Bool,
Number,
String,
}

// Dynamic JSON object type.
type Object: map[str]Value

// Dynamic JSON array type.
type Array: []Value

// Dynamic JSON boolean type.
type Bool: bool

// Dynamic JSON number type.
type Number: f64

// Dynamic JSON string type.
type String: str

0 comments on commit 62ae718

Please sign in to comment.