Skip to content

Commit

Permalink
std/encoding/json: add the custom encoder and decoder method support
Browse files Browse the repository at this point in the history
  • Loading branch information
mertcandav committed Nov 26, 2024
1 parent ad849cb commit d211c96
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 0 deletions.
66 changes: 66 additions & 0 deletions std/encoding/json/decode.jule
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,51 @@ impl jsonDecoder {
self.popParseState()
}

// Tries to use custom decoding method if exist for supported types.
// Reports whether it found and decoded successfully.
// Forwards any exception, all exceptionals are forwarded.
// So, any exception means custom decode method found and it throwed exception.
fn tryCustomDecode[T](self, mut &t: T)!: bool {
const tt = comptime::TypeOf(T)
const match {
| tt.Strict() || tt.Kind() == comptime::Kind.Struct:
const for _, method in tt.Decl().Methods() {
const match {
| method.Name() == "DecodeJSON":
const params = method.Params()
const match {
| len(params) == 2 && params[0].Mutable():
// Checking params[0] above is safe, because methods
// always same the receiver parameter. However,
// check params[1] here to avoid index-overflow error.
const match {
| !params[1].Mutable():
const m = comptime::ValueOf(t).Method(method.Name())
const match type m.Type().Params()[1].Type() {
| []byte:
const match m.Type().Result().Kind() {
| comptime::Kind.Void:
lit := self.scanValidLit() else { error(error) }
m.Unwrap()(lit) else { error(error) }
ret true
}
}
}
}
}
}
}
ret false
}

fn value1[T](self, mut &t: T)! {
// Before using the default decoding strategy, look for the custom decoder
// method and use it, if any. If not exist any custom decoder method,
// fallback to default decoding.
ok := self.tryCustomDecode(t) else { error(error) }
if ok {
ret
}
const tt = comptime::TypeOf(T)
b := self.data[self.i]
match b {
Expand Down Expand Up @@ -519,6 +563,13 @@ impl jsonDecoder {
}

fn value[T](self, mut &t: T)! {
// Before using the default decoding strategy, look for the custom decoder
// method and use it, if any. If not exist any custom decoder method,
// fallback to default decoding.
ok := self.tryCustomDecode(t) else { error(error) }
if ok {
ret
}
const tt = comptime::TypeOf(T)
const match tt.Kind() {
| comptime::Kind.SmartPtr:
Expand Down Expand Up @@ -601,6 +652,21 @@ impl jsonDecoder {
// 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.
// However, tousands of nested-data is always risky even below 10000.
//
// Custom decoder methods are supported. Any type that supports it must define
// an appropriate decoder method:
//
// fn DecodeJSON(self, data: []byte)!
//
// For types with this method, this method is called instead of the default
// decoding strategy and custom decoding is performed. The data parameter is the
// corresponding data equivalent and is always a validated, error-free JSON data.
// It is a mutable copy taken from the data used for decoding, so any change may
// cause mutation in the main data. According to the defined behavior,
// decoder methods should not mutate the content of the data.
// Throwing any exception is considered valid. The thrown exception will be
// forwarded by the Decode. Successful decoding should not throw any exceptions
// and self should be changed as required.
fn Decode[T](data: []byte, mut &t: T)! {
decoder := jsonDecoder{
data: data,
Expand Down
88 changes: 88 additions & 0 deletions std/encoding/json/decode_test.jule
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,92 @@ fn testDecodeBigHead(t: &testing::T) {
Decode(bigBytes, out) else {
t.Errorf("Decode(bigBytes) failed")
}
}

#test
fn testCustomDecode(t: &testing::T) {
mut b := []byte("false")
mut a := new(abc, -1)
Decode(b, a) else {
t.Errorf("want 0, throwed exception")
*a = 0
}
if *a != 0 {
t.Errorf("want 0, found {}", *a)
}
*a = -1
b = []byte("true")
Decode(b, a) else {
t.Errorf("want 1, throwed exception")
*a = 1
}
if *a != 1 {
t.Errorf("want 1, found {}", *a)
}
}

#test
fn testCustomDecode1(t: &testing::T) {
mut b := []byte("false")
mut a := abc(-1)
Decode(b, a) else {
t.Errorf("want 0, throwed exception")
a = 0
}
if a != 0 {
t.Errorf("want 0, found {}", a)
}
a = -1
b = []byte("true")
Decode(b, a) else {
t.Errorf("want 1, throwed exception")
a = 1
}
if a != 1 {
t.Errorf("want 1, found {}", a)
}
}

#test
fn testCustomDecode2(t: &testing::T) {
mut b := []byte("false")
mut a := abc2(new(int, -1))
Decode(b, a) else {
t.Errorf("want 0, throwed exception")
*a = 0
}
if *a != 0 {
t.Errorf("want 0, found {}", *a)
}
*a = -1
b = []byte("true")
Decode(b, a) else {
t.Errorf("want 1, throwed exception")
*a = 1
}
if *a != 1 {
t.Errorf("want 1, found {}", *a)
}
}

#test
fn testCustomDecode3(t: &testing::T) {
mut b := []byte("false")
mut a := new(abc2, new(int, -1))
Decode(b, a) else {
t.Errorf("want 0, throwed exception")
**a = 0
}
if **a != 0 {
t.Errorf("want 0, found {}", **a)
}
**a = -1
b = []byte("true")
Decode(b, a) else {
t.Errorf("want 1, throwed exception")
**a = 1
}
if **a != 1 {
t.Errorf("want 1, found {}", **a)
}
}
35 changes: 35 additions & 0 deletions std/encoding/json/encode.jule
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,30 @@ impl jsonEncoder {
const match {
| tt.Binded():
error(EncodeError.UnsupportedType)
| tt.Strict() || tt.Kind() == comptime::Kind.Struct:
// Before using the default encoding strategy, look for the custom encoder
// method and use it, if any. If not exist any custom encoder method,
// fallback to default encoding.
const for _, method in tt.Decl().Methods() {
const match {
| method.Name() == "EncodeJSON":
const params = method.Params()
const match {
| len(params) == 1 && !params[0].Mutable():
const m = comptime::ValueOf(t).Method(method.Name())
const match type m.Type().Result() {
| []byte:
bytes := m.Unwrap()() else { error(error) }
if !Valid(bytes) {
error(EncodeError.EncodeJSON)
}
self.buf.write(bytes)
ret
}
}
}
}
// Fallback to the defult strategy.
}
const match tt.Kind() {
| comptime::Kind.Int | comptime::Kind.I8 | comptime::Kind.I16 | comptime::Kind.I32 | comptime::Kind.I64:
Expand Down Expand Up @@ -459,6 +483,17 @@ fn encoder(): jsonEncoder {
// 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.
// However, tousands of nested-data is always risky even below 10000.
//
// Custom encoder methods are supported. Any type that supports it must define
// an appropriate encoder method:
//
// fn EncodeJSON(self)!: []byte
//
// For types with this method, this method is called instead of the default
// encoding strategy and custom encoding is performed. The returned bytes must
// be a valid JSON value. Otherwise, EncodeError.EncodeJSON exception is thrown.
// Throwing any exception is considered valid. The thrown exception will be
// forwarded by the Encode. Successful encoding should not throw any exceptions.
fn Encode[T](t: T)!: []byte {
mut encoder := encoder()
encoder.encode[T, encodeFlagType.Plain](t) else { error(error) }
Expand Down
128 changes: 128 additions & 0 deletions std/encoding/json/encode_test.jule
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,132 @@ fn testEncodeBigHead(t: &testing::T) {
if !Valid(bigBytes) {
t.Errorf("Valid() returns false for Encode(bigHead)")
}
}

type abc: int

impl abc {
fn EncodeJSON(self)!: []byte {
match self {
| 1:
ret []byte("true")
| 0:
ret []byte("false")
|:
panic("unimplemented case")
}
}

fn DecodeJSON(mut self, data: []byte)! {
match {
| str(data) == "true":
self = 1
| str(data) == "false":
self = 0
}
}
}

#test
fn testCustomEncode(t: &testing::T) {
mut a := new(abc, 0)
mut r := Encode(a) else {
t.Errorf("want false, throwed exception")
use []byte("false")
}
if str(r) != "false" {
t.Errorf("want false, found {}", str(r))
}
*a = 1
r = Encode(a) else {
t.Errorf("want true, throwed exception")
use []byte("true")
}
if str(r) != "true" {
t.Errorf("want true, found {}", str(r))
}
}

#test
fn testCustomEncode1(t: &testing::T) {
mut a := abc(0)
mut r := Encode(a) else {
t.Errorf("want false, throwed exception")
use []byte("false")
}
if str(r) != "false" {
t.Errorf("want false, found {}", str(r))
}
a = 1
r = Encode(a) else {
t.Errorf("want true, throwed exception")
use []byte("true")
}
if str(r) != "true" {
t.Errorf("want true, found {}", str(r))
}
}

type abc2: &int

impl abc2 {
fn EncodeJSON(self)!: []byte {
match *self {
| 1:
ret []byte("true")
| 0:
ret []byte("false")
|:
panic("unimplemented case")
}
}

fn DecodeJSON(mut self, data: []byte)! {
match {
| str(data) == "true":
*self = 1
| str(data) == "false":
*self = 0
}
}
}

#test
fn testCustomEncode2(t: &testing::T) {
mut a := abc2(new(int, 0))
mut r := Encode(a) else {
t.Errorf("want false, throwed exception")
use []byte("false")
}
if str(r) != "false" {
t.Errorf("want false, found {}", str(r))
}
*a = 1
r = Encode(a) else {
t.Errorf("want true, throwed exception")
use []byte("true")
}
if str(r) != "true" {
t.Errorf("want true, found {}", str(r))
}
}

#test
fn testCustomEncode3(t: &testing::T) {
mut a := new(abc2, new(int, 0))
mut r := Encode(a) else {
t.Errorf("want false, throwed exception")
use []byte("false")
}
if str(r) != "false" {
t.Errorf("want false, found {}", str(r))
}
**a = 1
r = Encode(a) else {
t.Errorf("want true, throwed exception")
use []byte("true")
}
if str(r) != "true" {
t.Errorf("want true, found {}", str(r))
}
}
1 change: 1 addition & 0 deletions std/encoding/json/error.jule
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
enum EncodeError {
UnsupportedType,
UnsupportedFloatValue, // NaN or ±Inf
EncodeJSON, // EncodeJSON returned invalid JSON value
}

// JSON decoding error codes.
Expand Down

0 comments on commit d211c96

Please sign in to comment.