go-emarshal

[library] easy programmable marshalling of embedded types
git clone https://hhvn.uk/go-emarshal
git clone git://hhvn.uk/go-emarshal
Log | Files | Refs | LICENSE

commit bb09b7fd5e4006be24782681c8b01dc29874b164
Author: hhvn <dev@hhvn.uk>
Date:   Sun, 18 Feb 2024 12:14:27 +0000

Init

Diffstat:
ALICENSE | 13+++++++++++++
Aemarshal.go | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexample_test.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 5+++++
Ago.sum | 2++
Ahelpers.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) +}