commit bb09b7fd5e4006be24782681c8b01dc29874b164
Author: hhvn <dev@hhvn.uk>
Date: Sun, 18 Feb 2024 12:14:27 +0000
Init
Diffstat:
A | LICENSE | | | 13 | +++++++++++++ |
A | emarshal.go | | | 139 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | example_test.go | | | 68 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | go.mod | | | 5 | +++++ |
A | go.sum | | | 2 | ++ |
A | helpers.go | | | 53 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ |
6 files changed, 280 insertions(+), 0 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2024 hhvn <dev@hhvn.uk>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/emarshal.go b/emarshal.go
@@ -0,0 +1,139 @@
+// Package emarshal makes (un)marshalling embedded types easy.
+//
+// The embedded types should each specify a marshalling and unmarshalling
+// method. The are then accessed by the Marshal and Unmarshal functions in this
+// package.
+//
+// The intended use case (as seen in the example) is for these functions to be
+// run by the MarshalJSON/UnmarshalJSON (or equivalent for other encodings)
+// methods on the parent type.
+package emarshal
+
+import (
+ "fmt"
+ "errors"
+ "reflect"
+
+ "hhvn.uk/go-superstruct"
+)
+
+var (
+ ErrNotPointer = errors.New("value is not pointer")
+ ErrBadMarshal = errors.New("bad marshaller")
+ ErrBadUnmarshal = errors.New("bad unmarshaller")
+)
+
+type MarshalFunc func(any) ([]byte, error)
+
+// Marshal returns an encoded form of in.
+//
+// The encoded form is made by running all methods with the prefix "EMarshal"
+// of in, and then combining these values into a single struct and encoding it
+// with fn.
+func Marshal(in any, fn MarshalFunc) ([]byte, error) {
+ t := reflect.TypeOf(in)
+ tn := t.Name()
+
+ mm := getMethods(t, "EMarshal")
+
+ rs := make([]any, len(mm))
+
+ for i, m := range mm {
+ mn := m.Name
+ man, _, mrn, mrt := methodInfo(m)
+
+ switch {
+ case man != 0:
+ return nil, methodErr(tn, mn, ErrBadMarshal,
+ fmt.Sprintf("expected 0 arguments, not %d", man))
+ case mrn != 2:
+ return nil, methodErr(tn, mn, ErrBadMarshal,
+ fmt.Sprintf("expected 2 return values, not %d", mrn))
+ case mrt[0].Kind() != reflect.Interface:
+ return nil, methodErr(tn, mn, ErrBadMarshal,
+ "2nd return value is not an interface")
+ case !isErr(mrt[1]):
+ return nil, methodErr(tn, mn, ErrBadMarshal,
+ "2nd return value is not an error")
+ }
+
+ r := m.Func.Call([]reflect.Value{reflect.ValueOf(in)})
+
+ err, ok := r[1].Interface().(error)
+ if ok { // ok is only true when err is not nil
+ return nil, err
+ }
+
+ rs[i] = r[0].Interface()
+ }
+
+ s, err := superstruct.CreateAndCopy(rs...)
+ if err != nil {
+ return nil, err
+ }
+
+ return fn(s)
+}
+
+type UnmarshalFunc func([]byte, any) error
+type Unpack func(any) error
+
+// Unmarshal decodes a byte slice into the value pointed to by in.
+//
+// It does this by running all the methods with the prefix "EUnmarshal" of in.
+// The methods are passed an anonymous function that will unpack the data into
+// a chosen variable using fn.
+func Unmarshal(in any, b []byte, fn UnmarshalFunc) error {
+ t := reflect.TypeOf(in)
+
+ if t.Kind() != reflect.Interface && t.Kind() != reflect.Pointer {
+ return fmt.Errorf("unmarshal: 1st %w", ErrNotPointer)
+ }
+
+ tn := t.Elem().Name()
+
+ mm := getMethods(t, "EUnmarshal")
+
+ unpacker := func(a any) error {
+ return fn(b, a)
+ }
+
+ for _, m := range mm {
+ mn := m.Name
+ man, mat, mrn, mrt := methodInfo(m)
+
+ switch {
+ case man != 1:
+ return methodErr(tn, mn, ErrBadUnmarshal,
+ fmt.Sprintf("expected 1 argument, not %d", man))
+ case mat[0].Kind() != reflect.Func:
+ return methodErr(tn, mn, ErrBadUnmarshal,
+ "argument is not a function")
+ case mrn != 1:
+ return methodErr(tn, mn, ErrBadUnmarshal,
+ fmt.Sprintf("expected 1 return not %d", mrn))
+ case !isErr(mrt[0]):
+ return methodErr(tn, mn, ErrBadUnmarshal,
+ "return value is not an error")
+ }
+
+ fan, fat, frn, frt := funcInfo(mat[0])
+
+ if fan != 1 || frn != 1 || fat[0].Kind() != reflect.Interface || !isErr(frt[0]) {
+ return methodErr(tn, mn, ErrBadMarshal,
+ "first argument is not an Unpack function")
+ }
+
+ r := m.Func.Call([]reflect.Value{
+ reflect.ValueOf(in),
+ reflect.ValueOf(unpacker),
+ })
+
+ err, ok := r[0].Interface().(error)
+ if ok {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/example_test.go b/example_test.go
@@ -0,0 +1,68 @@
+package emarshal
+
+import (
+ "fmt"
+ "encoding/json"
+)
+
+type A struct {
+ A string
+}
+
+func (a A) EMarshalA() (any, error) {
+ return a, nil
+}
+
+func (a *A) EUnmarshalA(unpack Unpack) error {
+ a.A = "hello"
+ return nil
+}
+
+type B struct {
+ B string
+}
+
+func (b B) EMarshalB() (any, error) {
+ return b, nil
+}
+
+func (b *B) EUnmarshalB(unpack Unpack) error {
+ return unpack(b)
+}
+
+type C struct {
+ A
+ B
+}
+
+func (c C) MarshalJSON() ([]byte, error) {
+ return Marshal(c, json.Marshal)
+}
+
+func (c *C) UnmarshalJSON(b []byte) error {
+ return Unmarshal(c, b, json.Unmarshal)
+}
+
+func Example() {
+ from := C{
+ A: A{"hi"},
+ B: B{"bye"},
+ }
+
+ jsontxt, err := json.MarshalIndent(from, "", " ")
+ fmt.Printf("%s\n%+v\n", jsontxt, err)
+
+ var to C
+
+ err = json.Unmarshal(jsontxt, &to)
+ fmt.Printf("%+v\n%+v\n", to, err)
+
+ // Unordered output:
+ // {
+ // "A": "hi",
+ // "B": "bye"
+ // }
+ // <nil>
+ // {A:{A:hello} B:{B:bye}}
+ // <nil>
+}
diff --git a/go.mod b/go.mod
@@ -0,0 +1,5 @@
+module hhvn.uk/go-emarshal
+
+go 1.21.5
+
+require hhvn.uk/go-superstruct v0.0.0-20240211204153-ccf1f90ce50f // indirect
diff --git a/go.sum b/go.sum
@@ -0,0 +1,2 @@
+hhvn.uk/go-superstruct v0.0.0-20240211204153-ccf1f90ce50f h1:MYek8OhTjmXaLC0nMgSlpE0CIEipz7Q+hr4IfnP3lnQ=
+hhvn.uk/go-superstruct v0.0.0-20240211204153-ccf1f90ce50f/go.mod h1:p+0zw0yuOXyNEuoSpnDQ8p7fIffkTtYO8RqeK8eOC8s=
diff --git a/helpers.go b/helpers.go
@@ -0,0 +1,53 @@
+package emarshal
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+func getMethods(t reflect.Type, prefix string) []reflect.Method {
+ var r []reflect.Method
+
+ for i := 0; i < t.NumMethod(); i++ {
+ m := t.Method(i)
+ if !strings.HasPrefix(m.Name, prefix) {
+ continue
+ }
+ r = append(r, m)
+ }
+
+ return r
+}
+
+func funcInfo(t reflect.Type) (an int, at []reflect.Type, rn int, rt []reflect.Type) {
+ an = t.NumIn()
+ at = make([]reflect.Type, an)
+ for i := 0; i < an; i++ {
+ at[i] = t.In(i)
+ }
+
+ rn = t.NumOut()
+ rt = make([]reflect.Type, rn)
+ for i := 0; i < rn; i++ {
+ rt[i] = t.Out(i)
+ }
+
+ return
+}
+
+func methodInfo(m reflect.Method) (int, []reflect.Type, int, []reflect.Type) {
+ an, at, rn, rt := funcInfo(m.Func.Type())
+
+ return an - 1, at[1:], rn, rt
+}
+
+func methodErr(tn, mn string, err error, desc string) error {
+ return fmt.Errorf("%v.%v(): %w: %v", tn, mn, err, desc)
+}
+
+var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
+
+func isErr(t reflect.Type) bool {
+ return t.Implements(errorInterface)
+}