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
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ jobs:
go-version-file: 'go.mod'

- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v7
with:
version: v1.59.1
version: v2.1.2
30 changes: 30 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: "2"
linters:
default: all
disable:
- depguard
- err113
- ireturn
- nlreturn
- mnd
- paralleltest
- tagliatelle
- testpackage
- varnamelen
- wsl
exclusions:
generated: lax
warn-unused: true
rules:
- path: _test\.go
linters:
- exhaustruct
- forcetypeassert
- funlen
- gochecknoglobals
- perfsprint
- path: example/
linters:
- exhaustruct
- gochecknoglobals
- revive
52 changes: 42 additions & 10 deletions container.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
// Package jsonpoly provides a way to unmarshal polymorphic JSON objects into
// a specific type based on a key. It uses reflection to determine the type of
// the object and to create a new instance of the unmarshalled object. The
// [Container] struct is a generic struct that can be used to unmarshal polymorphic
// JSON objects into a specific type based on a key. It is using the
// [Helper] interface to determine the type of the object and to create a new
// instance of the unmarshalled object. The [Helper] interface must be
// implemented by the user to provide the necessary methods to create and set
// the value of the object based on the key. The struct implementing this
// interface should be a pointer type and should contain public fields
// annotated with JSON tags that match the keys in the JSON object.
//
// See the example package for usage.
package jsonpoly

import (
Expand All @@ -7,9 +20,9 @@ import (
"reflect"
)

var (
ErrNotJSONObject = errors.New("not a JSON object")
)
// ErrNotJSONObject is returned when the helper or value structs are not marshaled
// into a JSON object (i.e. the first and last byte are not '{' and '}').
var ErrNotJSONObject = errors.New("not a JSON object")

// Container is a generic struct that can be used to unmarshal polymorphic JSON
// objects into a specific type based on a key. It is using the Helper interface
Expand All @@ -26,13 +39,17 @@ type Container[V any, H Helper[V]] struct {
// in the JSON object.
type Helper[V any] interface {
Get() V
Set(V)
Set(value V)
}

// UnmarshalJSON unmarshals the raw JSON bytes into the Container struct. After
// unmarshalling, the Value field will contain the unmarshalled object. The
// helper struct is used to determine the type of the object and to create a new
// instance of the unmarshalled object.
func (c *Container[V, H]) UnmarshalJSON(b []byte) error {
var helper H
if err := json.Unmarshal(b, &helper); err != nil {
return err
return err //nolint:wrapcheck // Don't wrap stdlib error.
}

v := helper.Get()
Expand All @@ -43,8 +60,8 @@ func (c *Container[V, H]) UnmarshalJSON(b []byte) error {
val := reflect.ValueOf(v)
if !val.IsValid() {
// Apparently this is an unknown type, marshal the helper to represent
// the type and include it in the error message. We can safely ignore
// the error, since the type was already unmarshalled successfully.
// the type and include it in the error message.
//nolint:errchkjson // We can safely ignore the error, since the type was already unmarshalled successfully.
b, _ := json.Marshal(helper)
return fmt.Errorf("unknown type %v", string(b))
}
Expand All @@ -58,15 +75,17 @@ func (c *Container[V, H]) UnmarshalJSON(b []byte) error {
// Set the newly allocated object to the value of 'v'.
ptrVal.Elem().Set(val)
// Now 'ptrVal' is a reflect.Value of type '*V' which can be used as a pointer.
//nolint:forcetypeassert // We know this is safe because we created it.
v = ptrVal.Interface().(V)
}

if err := json.Unmarshal(b, v); err != nil {
return err
return err //nolint:wrapcheck // Don't wrap stdlib error.
}

if ptrVal.IsValid() {
// If we used a pointer, we need to get the underlying value.
//nolint:forcetypeassert // We know this is safe because we created it.
c.Value = ptrVal.Elem().Interface().(V)
} else {
// If we used the value directly, we store it in the 'Value' field.
Expand All @@ -76,18 +95,22 @@ func (c *Container[V, H]) UnmarshalJSON(b []byte) error {
return nil
}

// MarshalJSON marshals the Container struct into JSON bytes. It uses the
// helper struct to determine the type of the object and to attach the type
// information to the JSON bytes.
func (c Container[V, H]) MarshalJSON() ([]byte, error) {
//nolint:forcetypeassert // We know this is safe because we created it.
helper := reflect.New(reflect.TypeFor[H]().Elem()).Interface().(H)
helper.Set(c.Value)

jsonHelper, err := json.Marshal(helper)
if err != nil {
return nil, err
return nil, err //nolint:wrapcheck // Don't wrap stdlib error.
}

jsonValue, err := json.Marshal(c.Value)
if err != nil {
return nil, err
return nil, err //nolint:wrapcheck // Don't wrap stdlib error.
}

return mergeJSONObjects(jsonHelper, jsonValue)
Expand All @@ -98,6 +121,15 @@ func mergeJSONObjects(o1, o2 []byte) ([]byte, error) {
return nil, ErrNotJSONObject
}

switch {
case len(o1) == 2:
// This is an empty object, so we just return the second object.
return o2, nil
case len(o2) == 2:
// This is an empty object, so we just return the first object.
return o1, nil
}

// We know this is only used internally, we can manipulate the slices.
// We append the second object to the first one, replacing the closing
// object bracket with a comma.
Expand Down
26 changes: 21 additions & 5 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ func (c Cat) Name() string {
return c.XName
}

type Pikachu struct{}

func (Pikachu) Type() string { return "pikachu" }
func (Pikachu) Name() string { return "Pikachu" }

type UnknownAnimal struct {
XType string `json:"-"`
XName string `json:"name"`
Expand All @@ -52,8 +57,9 @@ func (a UnknownAnimal) Name() string {

var (
KnownAnimals = map[string]Animal{
"dog": Dog{},
"cat": Cat{},
Dog{}.Type(): Dog{},
Cat{}.Type(): Cat{},
Pikachu{}.Type(): Pikachu{},
}
)

Expand Down Expand Up @@ -137,6 +143,11 @@ func TestContainer_value(t *testing.T) {
},
want: `{"type":"cat","name":"Whiskers","owner":"Alice","color":"White"}`,
},
{
name: "pikachu",
have: Pikachu{},
want: `{"type":"pikachu"}`,
},
{
name: "dolphin",
have: UnknownAnimal{
Expand All @@ -149,7 +160,6 @@ func TestContainer_value(t *testing.T) {

for _, tc := range testCases {
t.Run(fmt.Sprintf("%s_marshal", tc.name), func(t *testing.T) {
t.Skipf("skipping test")
c := Container[Animal, *AnimalContainerHelper]{
Value: tc.have,
}
Expand Down Expand Up @@ -186,8 +196,9 @@ type AnimalPtrContainerHelper struct {

func (h *AnimalPtrContainerHelper) Get() Animal {
knownAnimals := map[string]Animal{
"dog": &Dog{},
"cat": &Cat{},
Dog{}.Type(): &Dog{},
Cat{}.Type(): &Cat{},
Pikachu{}.Type(): &Pikachu{},
}

if a, ok := knownAnimals[h.Type]; ok {
Expand Down Expand Up @@ -223,6 +234,11 @@ func TestContainer_pointer(t *testing.T) {
},
want: `{"type":"cat","name":"Whiskers","owner":"Alice","color":"White"}`,
},
{
name: "pikachu",
have: &Pikachu{},
want: `{"type":"pikachu"}`,
},
{
name: "dolphin",
have: &UnknownAnimal{
Expand Down
Loading