Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type User struct {
}

func (u *User) Schema() []fmt.Field {
return []fmt.Field{{Name: "Name", Type: fmt.FieldText, JSON: "name"}}
return []fmt.Field{{Name: "name", Type: fmt.FieldText}}
}
func (u *User) Pointers() []any { return []any{&u.Name} }

Expand All @@ -64,14 +64,21 @@ func main() {

### `Encode(data fmt.Fielder, output any) error`

Serializes a `Fielder` to JSON.
Serializes a `Fielder` to JSON. JSON keys are always taken from `field.Name`. If `field.OmitEmpty` is true, the field is skipped if its value is zero.

- **data**: Must implement `fmt.Fielder`.
- **output**: `*[]byte`, `*string`, or `io.Writer`.

### `Decode(input any, data fmt.Fielder) error`

Parses JSON into a `Fielder`.
Parses JSON into a `Fielder` and calls `Validate()` if the fielder implements `fmt.Validator`.

- **input**: `[]byte`, `string`, or `io.Reader`.
- **data**: Must implement `fmt.Fielder`.

### `DecodeRaw(input any, data fmt.Fielder) error`

Parses JSON into a `Fielder` without calling `Validate()`.

- **input**: `[]byte`, `string`, or `io.Reader`.
- **data**: Must implement `fmt.Fielder`.
Expand Down
16 changes: 15 additions & 1 deletion decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,23 @@ import (
"unsafe"
)

// Decode parses JSON into a Fielder.
// Decode parses JSON into a Fielder and calls Validate() if implemented.
// input: []byte | string | io.Reader.
func Decode(input any, data fmt.Fielder) error {
if err := DecodeRaw(input, data); err != nil {
return err
}

// Post-decode validation
if v, ok := data.(fmt.Validator); ok {
return v.Validate()
}
return nil
}

// DecodeRaw parses JSON into a Fielder without calling Validate().
// input: []byte | string | io.Reader.
func DecodeRaw(input any, data fmt.Fielder) error {
var raw []byte
switch in := input.(type) {
case []byte:
Expand Down
File renamed without changes.
38 changes: 3 additions & 35 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,7 @@ func encodeFielder(b *fmt.Conv, f fmt.Fielder) error {

first := true
for i, field := range schema {
key, omitempty := parseJSONTag(field)
if key == "-" {
continue
}

ptr := ptrs[i]

if omitempty && isZeroPtr(ptr, field.Type) {
if field.OmitEmpty && isZeroPtr(ptrs[i], field.Type) {
continue
}

Expand All @@ -63,11 +56,11 @@ func encodeFielder(b *fmt.Conv, f fmt.Fielder) error {
first = false

b.WriteByte('"')
fmt.JSONEscape(key, b)
fmt.JSONEscape(field.Name, b)
b.WriteByte('"')
b.WriteByte(':')

encodeFromPtr(b, ptr, field.Type)
encodeFromPtr(b, ptrs[i], field.Type)
}

b.WriteByte('}')
Expand Down Expand Up @@ -196,28 +189,3 @@ func isZeroPtr(ptr any, ft fmt.FieldType) bool {
return false
}

// parseJSONTag extracts key and omitempty from Field.JSON.
func parseJSONTag(f fmt.Field) (key string, omitempty bool) {
tag := f.JSON
if tag == "" {
return f.Name, false
}
if tag == "-" {
return "-", false
}
comma := -1
for i := 0; i < len(tag); i++ {
if tag[i] == ',' {
comma = i
break
}
}
if comma < 0 {
return tag, false
}
key = tag[:comma]
if key == "" {
key = f.Name
}
return key, tag[comma+1:] == "omitempty"
}
6 changes: 2 additions & 4 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,7 @@ func (p *parser) matchFieldIndex(schema []fmt.Field) (int, error) {
keyBytes := p.data[start:p.pos]
p.pos++ // consume closing '"'
for i, field := range schema {
k, _ := parseJSONTag(field)
if len(k) == len(keyBytes) && matchBytesStr(k, keyBytes) {
if len(field.Name) == len(keyBytes) && matchBytesStr(field.Name, keyBytes) {
return i, nil
}
}
Expand All @@ -363,8 +362,7 @@ func (p *parser) matchFieldIndex(schema []fmt.Field) (int, error) {
return -1, err
}
for i, field := range schema {
k, _ := parseJSONTag(field)
if k == key {
if field.Name == key {
return i, nil
}
}
Expand Down
10 changes: 5 additions & 5 deletions tests/bench_encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ type benchUser struct {
}

var benchSchema = []fmt.Field{
{Name: "Name", Type: fmt.FieldText, JSON: "name"},
{Name: "Email", Type: fmt.FieldText, JSON: "email"},
{Name: "Age", Type: fmt.FieldInt, JSON: "age"},
{Name: "Score", Type: fmt.FieldFloat, JSON: "score"},
{Name: "name", Type: fmt.FieldText},
{Name: "email", Type: fmt.FieldText},
{Name: "age", Type: fmt.FieldInt},
{Name: "score", Type: fmt.FieldFloat},
}

func (u *benchUser) Schema() []fmt.Field { return benchSchema }
func (u *benchUser) Pointers() []any { return []any{&u.Name, &u.Email, &u.Age, &u.Score} }

var benchInput = &benchUser{Name: "Alice", Email: "alice@example.com", Age: 30, Score: 9.5}
var benchInput = &benchUser{Name: "alice", Email: "alice@example.com", Age: 30, Score: 9.5}

func BenchmarkEncode_tinywasm(b *testing.B) {
var out string
Expand Down
18 changes: 9 additions & 9 deletions tests/decode_basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ func TestDecodeSimple(t *testing.T) {
var active bool
m := &mockFielder{
schema: []fmt.Field{
{Name: "Name", Type: fmt.FieldText, JSON: "name"},
{Name: "Age", Type: fmt.FieldInt, JSON: "age"},
{Name: "Active", Type: fmt.FieldBool, JSON: "active"},
{Name: "name", Type: fmt.FieldText},
{Name: "age", Type: fmt.FieldInt},
{Name: "active", Type: fmt.FieldBool},
},
pointers: []any{&name, &age, &active},
}
Expand All @@ -31,15 +31,15 @@ func TestDecodeNested(t *testing.T) {
var city string
inner := &mockFielder{
schema: []fmt.Field{
{Name: "City", Type: fmt.FieldText, JSON: "city"},
{Name: "city", Type: fmt.FieldText},
},
pointers: []any{&city},
}
var user string
outer := &mockFielder{
schema: []fmt.Field{
{Name: "User", Type: fmt.FieldText, JSON: "user"},
{Name: "Address", Type: fmt.FieldStruct, JSON: "address"},
{Name: "user", Type: fmt.FieldText},
{Name: "address", Type: fmt.FieldStruct},
},
pointers: []any{&user, inner},
}
Expand All @@ -56,7 +56,7 @@ func TestDecodeStringEscapes(t *testing.T) {
var msg string
m := &mockFielder{
schema: []fmt.Field{
{Name: "Msg", Type: fmt.FieldText, JSON: "msg"},
{Name: "msg", Type: fmt.FieldText},
},
pointers: []any{&msg},
}
Expand All @@ -74,7 +74,7 @@ func TestDecodeNull(t *testing.T) {
name := "Alice"
m := &mockFielder{
schema: []fmt.Field{
{Name: "Name", Type: fmt.FieldText, JSON: "name"},
{Name: "name", Type: fmt.FieldText},
},
pointers: []any{&name},
}
Expand All @@ -91,7 +91,7 @@ func TestDecodeBytes(t *testing.T) {
var data []byte
m := &mockFielder{
schema: []fmt.Field{
{Name: "Data", Type: fmt.FieldBlob, JSON: "data"},
{Name: "data", Type: fmt.FieldBlob},
},
pointers: []any{&data},
}
Expand Down
4 changes: 2 additions & 2 deletions tests/decode_input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func TestDecodeFromReader(t *testing.T) {
var name string
m := &mockFielder{
schema: []fmt.Field{
{Name: "Name", Type: fmt.FieldText, JSON: "name"},
{Name: "name", Type: fmt.FieldText},
},
pointers: []any{&name},
}
Expand Down Expand Up @@ -53,7 +53,7 @@ func TestDecodeFromBytes(t *testing.T) {
var name string
m := &mockFielder{
schema: []fmt.Field{
{Name: "Name", Type: fmt.FieldText, JSON: "name"},
{Name: "name", Type: fmt.FieldText},
},
pointers: []any{&name},
}
Expand Down
2 changes: 1 addition & 1 deletion tests/decode_nested_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func TestDecodeStructNotFielder(t *testing.T) {
var s string = "initial"
m := &mockFielder{
schema: []fmt.Field{
{Name: "Nested", Type: fmt.FieldStruct, JSON: "nested"},
{Name: "nested", Type: fmt.FieldStruct},
},
pointers: []any{&s}, // string doesn't implement Fielder
}
Expand Down
38 changes: 2 additions & 36 deletions tests/decode_tags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,11 @@ import (
"testing"
)

func TestDecodeJSONKey(t *testing.T) {
var firstName string
m := &mockFielder{
schema: []fmt.Field{
{Name: "FirstName", Type: fmt.FieldText, JSON: "first_name"},
},
pointers: []any{&firstName},
}
input := `{"first_name":"Alice"}`
if err := json.Decode(input, m); err != nil {
t.Fatal(err)
}
if firstName != "Alice" {
t.Errorf("got %s", firstName)
}
}

func TestDecodeJSONExclude(t *testing.T) {
secret := "initial"
m := &mockFielder{
schema: []fmt.Field{
{Name: "Secret", Type: fmt.FieldText, JSON: "-"},
},
pointers: []any{&secret},
}
input := `{"Secret":"new"}`
if err := json.Decode(input, m); err != nil {
t.Fatal(err)
}
if secret != "initial" {
t.Errorf("secret changed to %s", secret)
}
}

func TestDecodeMissingField(t *testing.T) {
age := int64(20)
m := &mockFielder{
schema: []fmt.Field{
{Name: "Age", Type: fmt.FieldInt, JSON: "age"},
{Name: "age", Type: fmt.FieldInt},
},
pointers: []any{&age},
}
Expand All @@ -61,7 +27,7 @@ func TestDecodeExtraField(t *testing.T) {
var name string
m := &mockFielder{
schema: []fmt.Field{
{Name: "Name", Type: fmt.FieldText, JSON: "name"},
{Name: "name", Type: fmt.FieldText},
},
pointers: []any{&name},
}
Expand Down
18 changes: 9 additions & 9 deletions tests/decode_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func TestDecodeFloatFromInt(t *testing.T) {
var price float64
m := &mockFielder{
schema: []fmt.Field{
{Name: "Price", Type: fmt.FieldFloat, JSON: "price"},
{Name: "price", Type: fmt.FieldFloat},
},
pointers: []any{&price},
}
Expand All @@ -27,7 +27,7 @@ func TestDecodeFloatFromInt(t *testing.T) {
func TestDecodeInt(t *testing.T) {
var v int
m := &mockFielder{
schema: []fmt.Field{{Name: "V", Type: fmt.FieldInt, JSON: "v"}},
schema: []fmt.Field{{Name: "v", Type: fmt.FieldInt}},
pointers: []any{&v},
}
if err := json.Decode(`{"v":42}`, m); err != nil {
Expand All @@ -42,7 +42,7 @@ func TestDecodeInt(t *testing.T) {
func TestDecodeInt32(t *testing.T) {
var v int32
m := &mockFielder{
schema: []fmt.Field{{Name: "V", Type: fmt.FieldInt, JSON: "v"}},
schema: []fmt.Field{{Name: "v", Type: fmt.FieldInt}},
pointers: []any{&v},
}
if err := json.Decode(`{"v":42}`, m); err != nil {
Expand All @@ -57,7 +57,7 @@ func TestDecodeInt32(t *testing.T) {
func TestDecodeFloat32(t *testing.T) {
var v float32
m := &mockFielder{
schema: []fmt.Field{{Name: "V", Type: fmt.FieldFloat, JSON: "v"}},
schema: []fmt.Field{{Name: "v", Type: fmt.FieldFloat}},
pointers: []any{&v},
}
if err := json.Decode(`{"v":1.5}`, m); err != nil {
Expand All @@ -72,7 +72,7 @@ func TestDecodeFloat32(t *testing.T) {
func TestDecodeInt32FromFloat(t *testing.T) {
var v int32
m := &mockFielder{
schema: []fmt.Field{{Name: "V", Type: fmt.FieldInt, JSON: "v"}},
schema: []fmt.Field{{Name: "v", Type: fmt.FieldInt}},
pointers: []any{&v},
}
if err := json.Decode(`{"v":42.0}`, m); err != nil {
Expand All @@ -87,7 +87,7 @@ func TestDecodeInt32FromFloat(t *testing.T) {
func TestDecodeIntFromFloat(t *testing.T) {
var v int
m := &mockFielder{
schema: []fmt.Field{{Name: "V", Type: fmt.FieldInt, JSON: "v"}},
schema: []fmt.Field{{Name: "v", Type: fmt.FieldInt}},
pointers: []any{&v},
}
if err := json.Decode(`{"v":42.0}`, m); err != nil {
Expand All @@ -102,7 +102,7 @@ func TestDecodeIntFromFloat(t *testing.T) {
func TestDecodeFloat32FromInt(t *testing.T) {
var v float32
m := &mockFielder{
schema: []fmt.Field{{Name: "V", Type: fmt.FieldFloat, JSON: "v"}},
schema: []fmt.Field{{Name: "v", Type: fmt.FieldFloat}},
pointers: []any{&v},
}
if err := json.Decode(`{"v":42}`, m); err != nil {
Expand All @@ -117,7 +117,7 @@ func TestDecodeFloat32FromInt(t *testing.T) {
func TestDecodeInt64Ptr(t *testing.T) {
var v int64
m := &mockFielder{
schema: []fmt.Field{{Name: "V", Type: fmt.FieldInt, JSON: "v"}},
schema: []fmt.Field{{Name: "v", Type: fmt.FieldInt}},
pointers: []any{&v},
}
if err := json.Decode(`{"v":42}`, m); err != nil {
Expand All @@ -132,7 +132,7 @@ func TestDecodeInt64Ptr(t *testing.T) {
func TestDecodeInt64FromFloat(t *testing.T) {
var v int64
m := &mockFielder{
schema: []fmt.Field{{Name: "V", Type: fmt.FieldInt, JSON: "v"}},
schema: []fmt.Field{{Name: "v", Type: fmt.FieldInt}},
pointers: []any{&v},
}
if err := json.Decode(`{"v":42.0}`, m); err != nil {
Expand Down
Loading