From 8c85d1b56bcb9a1bdf830575f3e467ad8216f5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lovro=20Ma=C5=BEgon?= Date: Wed, 23 Apr 2025 13:24:44 +0200 Subject: [PATCH] add linter config, support empty types --- .github/workflows/lint.yml | 4 +-- .golangci.yml | 30 ++++++++++++++++++++++ container.go | 52 ++++++++++++++++++++++++++++++-------- container_test.go | 26 +++++++++++++++---- 4 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 .golangci.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6fa1ed9..544ebe8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..6a80ac9 --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/container.go b/container.go index 7397631..9ee8f47 100644 --- a/container.go +++ b/container.go @@ -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 ( @@ -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 @@ -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() @@ -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)) } @@ -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. @@ -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) @@ -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. diff --git a/container_test.go b/container_test.go index 7c873ba..dba33b9 100644 --- a/container_test.go +++ b/container_test.go @@ -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"` @@ -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{}, } ) @@ -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{ @@ -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, } @@ -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 { @@ -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{