From d98fd682061391bea123820627e3016c8d113478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=98=99=E2=97=A6=20The=20Tablet=20=E2=9D=80=20GamerGirla?= =?UTF-8?q?ndCo=20=E2=97=A6=E2=9D=A7?= Date: Thu, 17 Jul 2025 16:27:01 -0400 Subject: [PATCH] add json serialization+deserialization helpers --- go.mod | 3 + go.sum | 6 + json.go | 319 +++++++++++++++++++++++++++++++++++++++++++++++++++ json_test.go | 74 ++++++++++++ 4 files changed, 402 insertions(+) create mode 100644 json.go create mode 100644 json_test.go diff --git a/go.mod b/go.mod index 0fbb4f4..51e7efb 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,10 @@ require ( ) require ( + github.com/Jeffail/gabs v1.4.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/henvic/pgq v0.0.4 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/go.sum b/go.sum index 1f0a329..65e71c6 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,15 @@ +github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= +github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/go-loremipsum/loremipsum v1.1.4 h1:RJaJlJwX4y9A2+CMgKIyPcjuFHFKTmaNMhxbL+sI6Vg= github.com/go-loremipsum/loremipsum v1.1.4/go.mod h1:whNWskGoefTakPnCu2CO23v5Y7RwiG4LMOEtTDaBeOY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/henvic/pgq v0.0.4 h1:BgLnxofZJSWWs+9VOf19Gr9uBkSVbHWGiu8wix1nsIY= github.com/henvic/pgq v0.0.4/go.mod h1:k0FMvOgmQ45MQ3TgCLe8I3+sDKy9lPAiC2m9gg37pVA= github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs= diff --git a/json.go b/json.go new file mode 100644 index 0000000..1612344 --- /dev/null +++ b/json.go @@ -0,0 +1,319 @@ +package orm + +import ( + "encoding/json" + "fmt" + "github.com/fatih/structtag" + "reflect" + "time" +) + +func defaultEngine() *Engine { + return engines.Engines[defaultKey] +} + +func anyToModel(input any) *Model { + rv := reflect.TypeOf(input) + for rv.Kind() == reflect.Ptr || + rv.Kind() == reflect.Interface || + rv.Kind() == reflect.Slice || rv.Kind() == reflect.Pointer { + rv = rv.Elem() + } + maybeEngine := defaultEngine() + if maybeEngine == nil { + return nil + } + return maybeEngine.modelMap.Map[rv.Name()] +} + +func JSONSerialize(input any, pretty bool) ([]byte, error) { + vp := reflect.ValueOf(input) + vt := reflect.TypeOf(input) + if vt.Kind() != reflect.Pointer { + return nil, fmt.Errorf("Argument must be a pointer or pointer to a slice; got: %v", vt.Kind()) + } + ser, err := innerSerialize(vp) + if err != nil { + return nil, err + } + if pretty { + return json.MarshalIndent(ser, "", "\t") + } + return json.Marshal(ser) +} + +func JSONDeserialize(val any, ser []byte) error { + var fiv any + if err := json.Unmarshal(ser, &fiv); err != nil { + return err + } + vp := reflect.ValueOf(val) + if vp.Kind() != reflect.Pointer { + return fmt.Errorf("Argument must be a pointer or pointer to a slice; got: %v", vp.Kind()) + } + m := anyToModel(val) + if m == nil { + return fmt.Errorf("No model found for type '%s'", vp.Type().Name()) + } + maybeEngine := defaultEngine() + if maybeEngine == nil { + return fmt.Errorf("No engines have been created!?") + } + fv, err := innerDeserialize(fiv, m, maybeEngine) + if err != nil { + return err + } + vp.Elem().Set(fv) + return nil +} + +func innerSerialize(v reflect.Value) (ret any, err error) { + switch v.Kind() { + case reflect.Interface: + v = v.Elem() + fallthrough + case reflect.Pointer: + if v.IsNil() { + return ret, nil + } + for v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.IsZero() { + return ret, nil + } + fallthrough + case reflect.Struct: + m := anyToModel(v.Interface()) + + if m == nil { + if canConvertTo[time.Time](v.Type()) { + ret = v.Interface().(time.Time).Format(time.RFC3339) + } else { + var bytes []byte + bytes, err = json.Marshal(v.Interface()) + if err != nil { + return nil, err + } + ser := make(map[string]any) + err = json.Unmarshal(bytes, &ser) + if err != nil { + return nil, err + } + ret = ser + } + + } else { + depopulated, depopulatedId := isDepopulated(v, m.IDField) + if depopulated { + ret = depopulatedId + } else { + rmap := make(map[string]any) + for i := range v.NumField() { + fv := v.Field(i) + ft := v.Type().Field(i) + var tag *structtag.Tags + tag, err = structtag.Parse(string(ft.Tag)) + if err != nil { + return nil, err + } + var jsonTag *structtag.Tag + jsonTag, err = tag.Get("json") + if err != nil || jsonTag.Name == "-" { + continue + } + if jsonTag.Name == "" { + // we are dealing with an inlined/anonymous struct + var maybeMap any + maybeMap, err = innerSerialize(fv) + if amap, ok := maybeMap.(map[string]any); ok { + for k, vv := range amap { + rmap[k] = vv + } + } + } else { + rmap[jsonTag.Name], err = innerSerialize(fv) + if err != nil { + return nil, err + } + } + } + ret = rmap + } + } + case reflect.Slice, reflect.Array: + ret0 := make([]any, 0) + for i := range v.Len() { + var ser any + ser, err = innerSerialize(v.Index(i)) + if err != nil { + return nil, err + } + ret0 = append(ret0, ser) + } + ret = ret0 + default: + ret = v.Interface() + } + return ret, nil +} + +func innerDeserialize(input any, m *Model, e *Engine) (nv reflect.Value, err error) { + t := m.Type + irv := reflect.ValueOf(input) + if irv.Kind() == reflect.Slice || irv.Kind() == reflect.Array { + nv = reflect.MakeSlice(reflect.SliceOf(t), 0, 0) + for i := range irv.Len() { + var snv reflect.Value + cur := irv.Index(i) + snv, err = innerDeserialize(cur.Interface(), m, e) + if err != nil { + return snv, err + } + nv = reflect.Append(nv, snv) + } + } else { // it's a map or primitive value + nv = reflect.New(t).Elem() + if asMap, ok := input.(map[string]any); ok { + for _, f := range m.Fields { + if f.Index < 0 { + continue + } + ft := f.Original + fv := nv.Field(f.Index) + var tags *structtag.Tags + var btag *structtag.Tag + tags, err = structtag.Parse(string(ft.Tag)) + if err != nil { + return + } + btag, err = tags.Get("json") + if err != nil || btag.Name == "-" { + continue + } + interm := asMap[btag.Name] + var tmp any + if str, sok := interm.(string); sok { + if ttmp, terr := time.Parse(time.RFC3339, str); terr == nil { + tmp = ttmp + } else { + tmp = interm + } + } else { + tmp = interm + } + switch fv.Kind() { + case reflect.Int64, reflect.Int32, reflect.Int, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if tmp != nil { + fv.Set(reflect.ValueOf(tmp).Convert(ft.Type)) + } + case reflect.Array, reflect.Slice: + if interm != nil { + + slic := reflect.ValueOf(interm) + fv.Set(handleSliceMaybe(slic, fv.Type().Elem())) + } + default: + if ft.Anonymous { + var nfv reflect.Value + nfv, err = handleAnon(input, ft.Type) + if err != nil { + return + } + fv.Set(nfv) + } else { + fv.Set(reflect.ValueOf(tmp)) + } + } + } + for _, r := range m.Relationships { + if r.Type == BelongsTo || r.Type == ManyToOne || r.Idx < 0 { + continue + } + ft := r.OriginalField + fv := nv.Field(r.Idx) + var tags *structtag.Tags + var btag *structtag.Tag + tags, err = structtag.Parse(string(ft.Tag)) + if err != nil { + return + } + btag, err = tags.Get("json") + if err != nil || btag.Name == "-" { + continue + } + var rv reflect.Value + interm := asMap[btag.Name] + rv, err = innerDeserialize(interm, r.RelatedModel, e) + if err != nil { + return reflect.Value{}, err + } + fv.Set(rv) + } + } else { + iface := nv.Addr().Interface() + err = e.Model(iface).Where(fmt.Sprintf("%s = ?", m.IDField), input).Find(iface) + if err != nil { + return reflect.Value{}, err + } + } + } + return +} + +func handleAnon(raw any, rtype reflect.Type) (nv reflect.Value, err error) { + nv = reflect.New(rtype).Elem() + amap, ok := raw.(map[string]any) + if ok { + for i := range rtype.NumField() { + ft := rtype.Field(i) + fv := nv.Field(i) + var tags *structtag.Tags + var btag *structtag.Tag + tags, err = structtag.Parse(string(ft.Tag)) + if err != nil { + return + } + btag, terr := tags.Get("json") + if terr != nil || btag.Name == "-" || !ft.IsExported() { + continue + } + fval := amap[btag.Name] + if reflect.TypeOf(fval) == reflect.TypeFor[string]() && ft.Type == reflect.TypeFor[time.Time]() { + tt, _ := time.Parse(time.RFC3339, fval.(string)) + fv.Set(reflect.ValueOf(tt)) + } else if fval != nil { + fv.Set(reflect.ValueOf(fval)) + } + } + } + return +} +func handleSliceMaybe(iv reflect.Value, dstType reflect.Type) reflect.Value { + if iv.Kind() != reflect.Slice && iv.Kind() != reflect.Pointer { + return iv + } + dst := reflect.MakeSlice(reflect.SliceOf(dstType), 0, 0) + for i := range iv.Len() { + //dst.Set(reflect.Append(fv, handleSliceMaybe(iv.Index(i).Elem()))) + maybeIface := iv.Index(i) + if maybeIface.Kind() == reflect.Interface { + maybeIface = maybeIface.Elem() + } + maybeNdest := dstType + if maybeNdest.Kind() == reflect.Slice || maybeNdest.Kind() == reflect.Array { + maybeNdest = maybeNdest.Elem() + } + dst = reflect.Append(dst, handleSliceMaybe(maybeIface, maybeNdest)) + } + return dst +} +func isDepopulated(v reflect.Value, idField string) (bool, any) { + for v.Kind() == reflect.Ptr { + v = v.Elem() + } + syn := reflect.New(v.Type()).Elem() + syn.FieldByName(idField).Set(v.FieldByName(idField)) + finalId := v.FieldByName(idField).Interface() + return reflect.DeepEqual(v.Interface(), syn.Interface()), finalId +} diff --git a/json_test.go b/json_test.go new file mode 100644 index 0000000..b9c4676 --- /dev/null +++ b/json_test.go @@ -0,0 +1,74 @@ +package orm + +import ( + "encoding/json" + "fmt" + "github.com/Jeffail/gabs" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestJsonSerialize(t *testing.T) { + e := initTest(t) + defer e.Disconnect() + u := author(t) + err := e.Model(&user{}).Save(&u) + if err != nil { + fmt.Println(err.Error()) + } + assert.Nil(t, err) + insertBands(t, e) + ns := storyBase(e, t, u, "Chapters.Bands") + bytes, err := JSONSerialize(ns, true) + assert.Nil(t, err) + fmt.Println(string(bytes)) +} + +func TestJSONDeserialize(t *testing.T) { + e := initTest(t) + defer e.Disconnect() + u := author(t) + err := e.Model(&user{}).Save(&u) + if err != nil { + fmt.Println(err.Error()) + } + assert.Nil(t, err) + insertBands(t, e) + ns := storyBase(e, t, u, "Chapters.Bands") + bytes, err := JSONSerialize(ns, true) + assert.Nil(t, err) + fmt.Println(string(bytes)) + msi := make(map[string]any) + err = json.Unmarshal(bytes, &msi) + assert.Nil(t, err) + obj, err := gabs.Consume(msi) + assert.Nil(t, err) + children, err := obj.S("chapters").Children() + assert.Nil(t, err) + for _, child := range children { + bands := child.S("bands") + var bcontainer []*gabs.Container + bcontainer, err = bands.Children() + assert.Nil(t, err) + if err != nil { + break + } + for j := range bcontainer { + id := bcontainer[j].S("_id").Data() + //obj.S("chapters").Index(i).S("bands").Index + _, err = bands.SetIndex(id, j) + assert.Nil(t, err) + if err != nil { + break + } + } + } + nbytes := obj.Bytes() + var des story + err = JSONDeserialize(&des, nbytes) + assert.Nil(t, err) + for _, c := range des.Chapters { + assert.NotNil(t, c.Bands) + assert.GreaterOrEqual(t, len(c.Bands), 1) + } +}