From f46aab2f5fac2a97fd9438bd9bc4c4706e600d99 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: Sun, 1 Sep 2024 16:17:48 -0400 Subject: [PATCH] =?UTF-8?q?hello=20world!=20=F0=9F=8C=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + .idea/.gitignore | 8 + go.mod | 26 +++ go.work | 3 + idcounter.go | 74 ++++++++ indexes.go | 148 +++++++++++++++ model.go | 371 ++++++++++++++++++++++++++++++++++++ model_test.go | 109 +++++++++++ query.go | 482 +++++++++++++++++++++++++++++++++++++++++++++++ registry.go | 351 ++++++++++++++++++++++++++++++++++ testing.go | 174 +++++++++++++++++ util.go | 70 +++++++ 12 files changed, 1819 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 go.mod create mode 100644 go.work create mode 100644 idcounter.go create mode 100644 indexes.go create mode 100644 model.go create mode 100644 model_test.go create mode 100644 query.go create mode 100644 registry.go create mode 100644 testing.go create mode 100644 util.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7301c01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.exe +*.sum +muck/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..1c2fda5 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..db26349 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module rockfic.com/orm + +go 1.23 + +require ( + github.com/fatih/structtag v1.2.0 + go.mongodb.org/mongo-driver v1.16.1 + golang.org/x/net v0.21.0 +) + +require ( + github.com/go-loremipsum/loremipsum v1.1.3 + github.com/golang/snappy v0.0.4 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/stretchr/testify v1.9.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/text v0.14.0 // indirect +) + +replace rockfic.com/orm => C:/rockfic/orm diff --git a/go.work b/go.work new file mode 100644 index 0000000..784f04a --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.23.0 + +use . diff --git a/idcounter.go b/idcounter.go new file mode 100644 index 0000000..d91c523 --- /dev/null +++ b/idcounter.go @@ -0,0 +1,74 @@ +package orm + +import ( + "context" + "reflect" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const COUNTER_COL = "@counters" + +type Counter struct { + Current any `bson:"current"` + Collection string `bson:"collection"` +} + +func getLastInColl(cname string, id interface{}) interface{} { + var opts *options.FindOneOptions = options.FindOne() + + switch id.(type) { + case int, int64, int32, uint, uint32, uint64, primitive.ObjectID: + opts.SetSort(bson.M{"_id": -1}) + case string: + opts.SetSort(bson.M{"createdAt": -1}) + default: + panic("unsupported id type provided") + } + + var cnt Counter + if !reflect.ValueOf(id).IsZero() { + return id + } + if err := DB.Collection(COUNTER_COL).FindOne(context.TODO(), bson.M{ + "collection": cname, + }, opts).Decode(&cnt); err != nil { + cnt = Counter{ + Current: id, + } + cnt.Collection = cname + switch nini := cnt.Current.(type) { + case int: + if nini == 0 { + cnt.Current = nini + 1 + } + case int32: + if nini == int32(0) { + cnt.Current = nini + 1 + } + case int64: + if nini == int64(0) { + cnt.Current = nini + 1 + } + case uint: + if nini == uint(0) { + cnt.Current = nini + 1 + } + case uint32: + if nini == uint32(0) { + cnt.Current = nini + 1 + } + case uint64: + if nini == uint64(0) { + cnt.Current = nini + 1 + } + case string: + cnt.Current = NextStringID() + case primitive.ObjectID: + cnt.Current = primitive.NewObjectID() + } + } + return cnt.Current +} diff --git a/indexes.go b/indexes.go new file mode 100644 index 0000000..344efcc --- /dev/null +++ b/indexes.go @@ -0,0 +1,148 @@ +package orm + +import ( + "go/scanner" + "go/token" + "strings" + "unicode" + + "go.mongodb.org/mongo-driver/mongo" +) + +var optionKeywords = [...]string{"unique", "sparse", "background", "dropdups"} + +// prolly won't need to use indexes, but just in case... +type InternalIndex struct { + Fields []string + Options []string + lastFieldIdx int + sticky bool +} + +func (in *InternalIndex) appendOption(o string) { + o = strings.ToLower(o) + o = strings.Trim(o, " ") + for i := range optionKeywords { + if optionKeywords[i] == o { + in.Options = append(in.Options, o) + } + } +} + +func (in *InternalIndex) appendField(f string) { + in.Fields = append(in.Fields, f) +} + +func (in *InternalIndex) appendDotField(f string) { + in.Fields[in.lastFieldIdx] = in.Fields[in.lastFieldIdx] + "." + f +} + +func (in *InternalIndex) updateLastField() { + if len(in.Fields) > 0 { + in.lastFieldIdx = len(in.Fields) - 1 + return + } + in.lastFieldIdx = 0 +} + +func (in *InternalIndex) setSticky(s bool) { + in.sticky = s +} + +func (in *InternalIndex) getSticky() bool { + return in.sticky +} + +func scanIndex(src string) []InternalIndex { + var s scanner.Scanner + var parsed []InternalIndex + + src = func(ss string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + return r + }, ss) + }(src) + fset := token.NewFileSet() + file := fset.AddFile("", fset.Base(), len(src)) + s.Init(file, []byte(src), nil, scanner.ScanComments) + + lb := false + + p := &InternalIndex{} + + for { + _, tok, lit := s.Scan() + + switch tok { + case token.LBRACE: + if lb { + goto panik + } + lb = true + case token.RBRACE: + if !lb || len(p.Fields) == 0 { + goto panik + } + lb = false + case token.IDENT: + if lb { + if p.getSticky() { + p.appendDotField(lit) + p.setSticky(false) + break + } + p.appendField(lit) + break + } + p.appendOption(lit) + case token.PERIOD: + if p.getSticky() { + goto panik + } + p.setSticky(true) + p.updateLastField() + case token.COMMA: + case token.COLON: + if lb { + goto panik + } + case token.SEMICOLON: + if lb { + goto panik + } + parsed = append(parsed, *p) + p = &InternalIndex{} + case token.EOF: + if lb { + goto panik + } + return parsed + default: + goto panik + } + } + +panik: + panic("parsing error in index expression!") +} + +func BuildIndex(i InternalIndex) *mongo.IndexModel { + idx := &mongo.IndexModel{ + Keys: i.Fields, + } + + for _, o := range i.Options { + switch o { + case "unique": + idx.Options.SetUnique(true) + case "background": + idx.Options.SetBackground(true) + case "sparse": + idx.Options.SetSparse(true) + } + } + return idx +} diff --git a/model.go b/model.go new file mode 100644 index 0000000..3b14e6f --- /dev/null +++ b/model.go @@ -0,0 +1,371 @@ +package orm + +import ( + "context" + "fmt" + "reflect" + "time" + + "github.com/fatih/structtag" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Model - "base" struct for all queryable models +type Model struct { + // Created time. updated/added automatically. + Created time.Time `bson:"createdAt" json:"createdAt"` + // Modified time. updated/added automatically. + Modified time.Time `bson:"updatedAt" json:"updatedAt"` + typeName string `bson:"-" json:"-"` + self any `bson:"-" json:"-"` + exists bool `bson:"-"` +} + +// HasID is a simple interface that you must implement. +// This allows for more flexibility if your ID isn't an +// ObjectID (e.g., int, uint, string...). +// +// and yes, those darn ugly ObjectIDs are supported :) +type HasID interface { + Id() any + SetId(id any) +} + +type IModel interface { + Find(query interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error) + FindAll(query interface{}, opts ...*options.FindOptions) (*Query, error) + FindByID(id interface{}) (*Query, error) + FindOne(query interface{}, options ...*options.FindOneOptions) (*Query, error) + FindPaged(query interface{}, page int64, perPage int64, options ...*options.FindOptions) (*Query, error) + getColl() *mongo.Collection + getIdxs() []*mongo.IndexModel + getParsedIdxs() map[string][]InternalIndex + Save() error + serializeToStore() primitive.M + setTypeName(str string) +} + +func (m *Model) setTypeName(str string) { + m.typeName = str +} + +func (m *Model) getColl() *mongo.Collection { + _, ri, ok := ModelRegistry.HasByName(m.typeName) + if !ok { + panic(fmt.Sprintf("the model '%s' has not been registered", m.typeName)) + } + return DB.Collection(ri.Collection) +} + +func (m *Model) getIdxs() []*mongo.IndexModel { + mi := []*mongo.IndexModel{} + if mpi := m.getParsedIdxs(); mpi != nil { + for _, v := range mpi { + for _, i := range v { + mi = append(mi, BuildIndex(i)) + } + } + return mi + } + return nil +} + +func (m *Model) getParsedIdxs() map[string][]InternalIndex { + _, ri, ok := ModelRegistry.HasByName(m.typeName) + if !ok { + panic(fmt.Sprintf("model '%s' not registered", m.typeName)) + } + return ri.Indexes +} + +func (m *Model) Find(query interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error) { + coll := m.getColl() + cursor, err := coll.Find(context.TODO(), query, opts...) + return cursor, err +} + +func (m *Model) FindAll(query interface{}, opts ...*options.FindOptions) (*Query, error) { + qqn := ModelRegistry.new_(m.typeName) + qqv := reflect.New(reflect.SliceOf(reflect.TypeOf(qqn).Elem())) + qqv.Elem().Set(reflect.Zero(qqv.Elem().Type())) + qq := &Query{ + Model: *m, + Collection: m.getColl(), + doc: qqv.Interface(), + Op: OP_FIND_ALL, + } + q, err := m.Find(query, opts...) + + if err == nil { + rawRes := bson.A{} + err = q.All(context.TODO(), &rawRes) + if err == nil { + m.exists = true + } + qq.rawDoc = rawRes + err = q.All(context.TODO(), &qq.doc) + if err != nil { + qq.reOrganize() + err = nil + } + } + + return qq, err +} + +func (m *Model) FindPaged(query interface{}, page int64, perPage int64, options ...*options.FindOptions) (*Query, error) { + skipAmt := perPage * (page - 1) + if skipAmt < 0 { + skipAmt = 0 + } + if len(options) > 0 { + options[0].SetSkip(skipAmt).SetLimit(perPage) + } + q, err := m.FindAll(query, options...) + q.Op = OP_FIND_PAGED + return q, err +} + +func (m *Model) FindByID(id interface{}) (*Query, error) { + return m.FindOne(bson.D{{"_id", id}}) +} + +// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 +// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 + +// ^ ^ +// ^ ^ + +func (m *Model) FindOne(query interface{}, options ...*options.FindOneOptions) (*Query, error) { + coll := m.getColl() + rip := coll.FindOne(context.TODO(), query, options...) + raw := bson.M{} + err := rip.Decode(&raw) + panik(err) + m.exists = true + qq := &Query{ + Collection: m.getColl(), + rawDoc: raw, + doc: ModelRegistry.new_(m.typeName), + Op: OP_FIND_ONE, + Model: *m, + } + qq.rawDoc = raw + err = rip.Decode(qq.doc) + if err != nil { + qq.reOrganize() + err = nil + } + m.self = qq.doc + return qq, err +} + +func (m *Model) Save() error { + var err error + c := m.getColl() + now := time.Now() + selfo := reflect.ValueOf(m.self) + vp := selfo + if vp.Kind() != reflect.Ptr { + vp = reflect.New(selfo.Type()) + vp.Elem().Set(selfo) + } + var asHasId = vp.Interface().(HasID) + (asHasId).Id() + isNew := reflect.ValueOf(asHasId.Id()).IsZero() || !m.exists + if isNew { + m.Created = now + } + m.Modified = now + idxs := m.getIdxs() + for _, i := range idxs { + _, err = c.Indexes().CreateOne(context.TODO(), *i) + if err != nil { + return err + } + } + if isNew { + nid := getLastInColl(c.Name(), asHasId.Id()) + switch pnid := nid.(type) { + case uint: + nid = pnid + 1 + case uint32: + nid = pnid + 1 + case uint64: + nid = pnid + 1 + case int: + nid = pnid + 1 + case int32: + nid = pnid + 1 + case int64: + nid = pnid + 1 + case string: + nid = NextStringID() + case primitive.ObjectID: + nid = primitive.NewObjectID() + default: + panic("unknown or unsupported id type") + } + if reflect.ValueOf(asHasId.Id()).IsZero() { + (asHasId).SetId(nid) + } + + m.self = asHasId + c.InsertOne(context.TODO(), m.serializeToStore()) + m.exists = true + } else { + _, err = c.ReplaceOne(context.TODO(), bson.D{{Key: "_id", Value: m.self.(HasID).Id()}}, m.serializeToStore()) + } + return err +} + +func (m *Model) serializeToStore() bson.M { + return serializeIDs((*m).self) +} + +func serializeIDs(input interface{}) bson.M { + ret := bson.M{} + mv := reflect.ValueOf(input) + mt := reflect.TypeOf(input) + + vp := mv + if vp.Kind() != reflect.Ptr { + vp = reflect.New(mv.Type()) + vp.Elem().Set(mv) + } + if mv.Kind() == reflect.Pointer { + mv = mv.Elem() + } + if mt.Kind() == reflect.Pointer { + mt = mt.Elem() + } + for i := 0; i < mv.NumField(); i++ { + fv := mv.Field(i) + ft := mt.Field(i) + var dr reflect.Value = fv + tag, err := structtag.Parse(string(mt.Field(i).Tag)) + panik(err) + bbson, err := tag.Get("bson") + if err != nil || bbson.Name == "-" { + continue + } + _, terr := tag.Get("ref") + switch dr.Type().Kind() { + case reflect.Slice: + rarr := make([]interface{}, 0) + intArr := iFaceSlice(fv.Interface()) + for _, idHaver := range intArr { + if terr == nil { + if reflect.ValueOf(idHaver).Type().Kind() != reflect.Pointer { + vp := reflect.New(reflect.ValueOf(idHaver).Type()) + vp.Elem().Set(reflect.ValueOf(idHaver)) + idHaver = vp.Interface() + } + ifc, ok := idHaver.(HasID) + if !ok { + panic(fmt.Sprintf("referenced model slice '%s' does not implement HasID", ft.Name)) + } + rarr = append(rarr, ifc.Id()) + } else if reflect.ValueOf(idHaver).Kind() == reflect.Struct { + rarr = append(rarr, serializeIDs(idHaver)) + } else { + if reflect.ValueOf(idHaver).Kind() == reflect.Slice { + rarr = append(rarr, serializeIDSlice(iFaceSlice(idHaver))) + } else { + rarr = append(rarr, idHaver) + } + } + ret[bbson.Name] = rarr + } + case reflect.Pointer: + dr = fv.Elem() + fallthrough + case reflect.Struct: + if bbson.Name != "" { + if terr == nil { + idHaver := fv.Interface() + ifc, ok := idHaver.(HasID) + if !ok { + panic(fmt.Sprintf("referenced model '%s' does not implement HasID", ft.Name)) + } + if !fv.IsNil() { + ret[bbson.Name] = ifc.Id() + } else { + ret[bbson.Name] = nil + } + + } else if reflect.TypeOf(fv.Interface()) != reflect.TypeOf(time.Now()) { + ret[bbson.Name] = serializeIDs(fv.Interface()) + } else { + ret[bbson.Name] = fv.Interface() + } + } else { + for k, v := range serializeIDs(fv.Interface()) { + ret[k] = v + } + } + + default: + ret[bbson.Name] = fv.Interface() + } + } + return ret +} + +func serializeIDSlice(input []interface{}) bson.A { + var a bson.A + for _, in := range input { + fv := reflect.ValueOf(in) + switch fv.Type().Kind() { + case reflect.Slice: + a = append(a, serializeIDSlice(iFaceSlice(fv.Interface()))) + case reflect.Struct: + a = append(a, serializeIDs(fv.Interface())) + default: + a = append(a, fv.Interface()) + } + } + return a +} + +// Create creates a new instance of a given model. +// returns a pointer to the newly created model. +func Create(d any) any { + var n string + var ri *InternalModel + var ok bool + + n, ri, ok = ModelRegistry.HasByName(nameOf(d)) + + if !ok { + ModelRegistry.Model(d) + n, ri, _ = ModelRegistry.Has(d) + } + t := ri.Type + v := valueOf(d) + i := ModelRegistry.Index(n) + + r := reflect.New(t) + + r.Elem().Set(v) + + df := r.Elem().Field(i) + dm := df.Interface().(Model) + if reflect.ValueOf(d).Kind() == reflect.Pointer { + r.Elem().Set(reflect.ValueOf(d).Elem()) + } else { + r.Elem().Set(reflect.ValueOf(d)) + } + dm.typeName = n + what := r.Interface() + dm.self = what + df.Set(reflect.ValueOf(dm)) + return what +} + +func (m *Model) PrintMe() { + fmt.Printf("My name is %s !\n", nameOf(m)) +} diff --git a/model_test.go b/model_test.go new file mode 100644 index 0000000..142b4f2 --- /dev/null +++ b/model_test.go @@ -0,0 +1,109 @@ +package orm + +import ( + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNew(t *testing.T) { + initTest() + doc := Create(&iti_single).(*story) + assert.Equal(t, iti_single.Title, doc.Title) + assert.Equal(t, iti_single.Chapters[0].Summary, doc.Chapters[0].Summary) +} + +func TestSave(t *testing.T) { + initTest() + storyDoc := Create(iti_multi).(*story) + lauthor := Create(author).(*user) + storyDoc.Author = lauthor + assert.Equal(t, storyDoc.Id(), int64(0)) + assert.Equal(t, lauthor.ID, storyDoc.Author.ID) + aerr := lauthor.Save() + assert.Equal(t, nil, aerr) + serr := storyDoc.Save() + assert.Equal(t, nil, serr) + assert.Less(t, int64(0), storyDoc.ID) + assert.Less(t, int64(0), lauthor.ID) +} + +func TestPopulate(t *testing.T) { + initTest() + + bandDoc := Create(iti_single.Chapters[0].Bands[0]).(*band) + storyDoc := Create(iti_single).(*story) + author := Create(author).(*user) + + err := bandDoc.Save() + assert.Equal(t, nil, err) + storyDoc.Author = author + err = author.Save() + assert.Equal(t, nil, err) + err = storyDoc.Save() + assert.Equal(t, nil, err) + assert.Greater(t, storyDoc.ID, int64(0)) + + smodel := Create(story{}).(*story) + q, err := smodel.FindByID(storyDoc.ID) + assert.Equal(t, nil, err) + //assert.Greater(t, smodel.ID, int64(0)) + assert.NotPanics(t, func() { + foundDoc := &story{} + q.Populate("Author", "Chapters.Bands").Exec(foundDoc) + j, _ := q.JSON() + fmt.Printf("%s\n", j) + }) +} + +func TestUpdate(t *testing.T) { + initTest() + nb := Create(band{ + ID: 1, + Name: "Metallica", + Characters: []string{ + "James Hetfield", + "Lars Ulrich", + "Kirk Hammett", + "Cliff Burton", + }, + Locked: false, + }).(*band) + err := nb.Save() + assert.Equal(t, nil, err) + nb.Locked = true + err = nb.Save() + assert.Equal(t, nil, err) + + foundM := Create(band{}).(*band) + q, err := foundM.FindByID(int64(1)) + found := &band{} + q.Exec(found) + assert.Equal(t, nil, err) + assert.Equal(t, int64(1), found.ID) + assert.Equal(t, nil, err) + assert.Equal(t, true, found.Locked) +} + +func TestModel_FindAll(t *testing.T) { + initTest() + smodel := Create(story{}).(*story) + query, err := smodel.FindAll(bson.M{}, options.Find()) + assert.Equal(t, nil, err) + final := make([]story, 0) + query.Exec(&final) + assert.Greater(t, len(final), 0) +} + +func TestModel_PopulateMulti(t *testing.T) { + initTest() + smodel := Create(story{}).(*story) + query, err := smodel.FindAll(bson.M{}, options.Find()) + assert.Equal(t, nil, err) + final := make([]story, 0) + query.Populate("Author", "Chapters.Bands").Exec(&final) + assert.Greater(t, len(final), 0) +} diff --git a/query.go b/query.go new file mode 100644 index 0000000..f4b1167 --- /dev/null +++ b/query.go @@ -0,0 +1,482 @@ +package orm + +import ( + "context" + "encoding/json" + "fmt" + "github.com/fatih/structtag" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "log" + "reflect" + "strings" + "time" +) + +type Query struct { + // the handle of the collection associated with this query + Collection *mongo.Collection + // the operation from which this query stems + Op string + // the model instance associated with this query + Model Model + done bool + rawDoc any + doc any +} + +const ( + OP_FIND_ONE = "findOne" + OP_FIND_PAGED = "findPaged" + OP_FIND_ALL = "findAll" + OP_FIND = "find" +) + +func populate(r Reference, rcoll string, rawDoc interface{}, d string, src interface{}) any { + rt := reflect.TypeOf(src) + rv := reflect.ValueOf(src) + srt := rt + if srt.Kind() == reflect.Pointer { + srt = rt.Elem() + } + if rv.Kind() != reflect.Pointer { + rv = reflect.New(rt) + rv.Elem().Set(reflect.ValueOf(src)) + } + + var fieldsMap [3]string + type bsonWhat struct { + What string + } + /*switch cl.Kind() { + case reflect.Struct: + + }*/ + var w bsonWhat + switch rawDoc.(type) { + case bson.A: + w.What = "A" + case bson.D: + w.What = "D" + case bson.M: + w.What = "M" + default: + w.What = "-" + } + var next string + if len(strings.Split(d, ".")) > 1 { + next = strings.Join(strings.Split(d, ".")[1:], ".") + d = strings.Split(d, ".")[0] + } else { + next = d + } + var toReturn interface{} + switch w.What { + case "A": + rvs := reflect.MakeSlice(rt, 0, 0) + var rahh reflect.Value + if rv.Kind() == reflect.Ptr { + rahh = rv.Elem() + } else { + rahh = rv + } + for i, el := range rawDoc.(bson.A) { + it := rahh.Index(i) + popped := populate(r, rcoll, el, next, it.Interface()) + poppedVal := reflect.ValueOf(popped) + if poppedVal.Kind() == reflect.Pointer { + rvs = reflect.Append(rvs, poppedVal.Elem()) + } else { + rvs = reflect.Append(rvs, poppedVal) + } + } + if rv.CanSet() { + rv.Set(rvs) + } else if rv.Kind() == reflect.Pointer { + rv.Elem().Set(rvs) + } else { + src = rvs.Interface() + toReturn = src + } + case "D": + loc := rawDoc.(bson.D) + nrd := bson.M{} + for _, el := range loc { + nrd[el.Key] = el.Value + } + rawDoc = nrd + fallthrough + case "M": + dd := rawDoc.(bson.M) + var sf reflect.Value + var rsf reflect.Value + if rv.Kind() == reflect.Pointer { + sf = rv.Elem().FieldByName(d) + } else { + sf = rv.FieldByName(d) + } + if rv.Kind() == reflect.Pointer { + rsf = rv.Elem().FieldByName(d) + } else { + rsf = rv.FieldByName(d) + } + + var ff reflect.StructField + var ok bool + if rt.Kind() == reflect.Pointer { + ff, ok = rt.Elem().FieldByName(d) + } else { + ff, ok = rt.FieldByName(d) + } + + if ok { + tag, err := structtag.Parse(string(ff.Tag)) + if err == nil { + val, err2 := tag.Get("bson") + if err2 == nil { + fttt := ff.Type + if fttt.Kind() == reflect.Pointer || fttt.Kind() == reflect.Slice { + fttt = fttt.Elem() + } + fieldsMap = [3]string{d, fttt.Name(), val.Name} + } + } + } + intermediate := populate(r, rcoll, dd[fieldsMap[2]], next, sf.Interface()) + if rsf.CanSet() { + ival := reflect.ValueOf(intermediate) + if ival.Kind() != reflect.Pointer && rsf.Kind() == reflect.Pointer { + rsf.Set(ival.Elem()) + } else if ival.Kind() == reflect.Pointer && rsf.Kind() != reflect.Pointer { + rsf.Set(ival.Elem()) + } else { + rsf.Set(ival) + } + } else { + src = intermediate + } + default: + tto := r.HydratedType + if tto.Kind() == reflect.Pointer || tto.Kind() == reflect.Slice { + tto = tto.Elem() + } + + rawt := ModelRegistry.new_(tto.Name()) + t := rawt.(IModel) + q := bson.M{"_id": rawDoc} + reso := DB.Collection(rcoll).FindOne(context.TODO(), q) + reso.Decode(t) + hatred := rv + if hatred.Kind() == reflect.Pointer { + hatred = hatred.Elem() + } + if hatred.CanSet() { + if reflect.ValueOf(t).Kind() == reflect.Pointer { + fmt.Println(reflect.ValueOf(t).Elem().Type().String()) + fmt.Println(rv.Type().String()) + hatred.Set(reflect.ValueOf(t).Elem()) + } else { + hatred.Set(reflect.ValueOf(t)) + } + } else { + src = t + toReturn = src + } + } + + if toReturn == nil { + return rv.Interface() + } + return src +} + +// Populate populates document references via reflection +func (q *Query) Populate(fields ...string) *Query { + _, cm, _ := ModelRegistry.HasByName(q.Model.typeName) + + if cm != nil { + rawDoc := q.rawDoc + for _, field := range fields { + // 0 = fieldname, 1 = typename, 2 = bson name + + var r Reference + + for k2, v := range cm.References { + if strings.HasPrefix(k2, field) { + r = v + break + } + } + + if r.Exists { + // get self + // get ptr + // find + // unmarshal... + htt := r.HydratedType + if htt.Kind() == reflect.Pointer || htt.Kind() == reflect.Slice { + htt = htt.Elem() + } + if strings.HasSuffix(field, ".") || strings.HasPrefix(field, ".") { + log.Printf("WARN: invalid field name passed to Populate(). skipping...\n") + continue + } + + tto := r.HydratedType + if tto.Kind() == reflect.Pointer || tto.Kind() == reflect.Slice { + tto = tto.Elem() + } + _, refColl, _ := ModelRegistry.HasByName(tto.Name()) + var tmp1 interface{} + if arr, ok := rawDoc.(bson.A); ok { + typ := cm.Type + slic := reflect.New(reflect.SliceOf(typ)) + for i, val2 := range arr { + ref := reflect.ValueOf(q.doc) + if ref.Kind() == reflect.Pointer { + ref = ref.Elem() + } + src := ref.Index(i).Interface() + inter := populate(r, refColl.Collection, val2, field, src) + if reflect.ValueOf(inter).Kind() == reflect.Pointer { + slic.Elem().Set(reflect.Append(slic.Elem(), reflect.ValueOf(inter).Elem())) + } else { + slic.Elem().Set(reflect.Append(slic, reflect.ValueOf(inter))) + } + } + tmp1 = slic.Interface() + } else { + tmp1 = populate(r, refColl.Collection, rawDoc, field, reflect.ValueOf(q.doc).Interface()) + } + q.doc = tmp1 + } + } + } + return q +} +func (q *Query) reOrganize() { + var trvo reflect.Value + if arr, ok := q.rawDoc.(bson.A); ok { + typ := ModelRegistry[q.Model.typeName].Type + slic := reflect.New(reflect.SliceOf(typ)) + for _, v2 := range arr { + inter := reflect.ValueOf(rerere(v2, typ)) + if inter.Kind() == reflect.Pointer { + inter = inter.Elem() + } + slic.Elem().Set(reflect.Append(slic.Elem(), inter)) + } + trvo = slic.Elem() + } else { + trvo = reflect.ValueOf(rerere(q.rawDoc, reflect.TypeOf(q.doc))) + for { + if trvo.Kind() == reflect.Pointer { + trvo = trvo.Elem() + } else { + break + } + } + } + + resV := reflect.ValueOf(q.doc) + for { + if resV.Kind() == reflect.Pointer { + resV = resV.Elem() + } else { + break + } + } + resV.Set(trvo) +} + +func rerere(input interface{}, resType reflect.Type) interface{} { + t := reflect.TypeOf(input) + v := reflect.ValueOf(input) + if input == nil { + return nil + } + if v.CanAddr() && v.IsNil() { + return nil + } + if v.Type().Kind() == reflect.Pointer { + v = v.Elem() + } + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + + if resType.Kind() == reflect.Pointer { + resType = resType.Elem() + } + resV := reflect.New(resType) + var rve reflect.Value = resV + if rve.Kind() == reflect.Pointer { + rve = resV.Elem() + } + + if d, isD := v.Interface().(bson.D); isD { + m := bson.M{} + for _, el := range d { + m[el.Key] = el.Value + } + input = m + v = reflect.ValueOf(input) + } + + switch resType.Kind() { + case reflect.Struct: + shouldBreak := false + mipmap, ok := v.Interface().(bson.M) + if ok { + for i := 0; i < resType.NumField(); i++ { + ft := resType.Field(i) + fv := rve.Field(i) + if ft.Anonymous { + fv.Set(handleAnon(input, ft.Type, fv)) + continue + } + tags, err := structtag.Parse(string(ft.Tag)) + panik(err) + btag, err := tags.Get("bson") + if err != nil { + continue + } + if btag.Name == "-" { + continue + } + intermediate := mipmap[btag.Name] + _, err = tags.Get("ref") + if err != nil { + tmp := rerere(intermediate, ft.Type) + fuck := reflect.ValueOf(tmp) + if !fuck.IsZero() { + if fuck.Type().Kind() == reflect.Pointer { + fuck = fuck.Elem() + } + } + fv.Set(fuck) + shouldBreak = true + } else { + tt := ft.Type + if tt.Kind() == reflect.Pointer { + tt = tt.Elem() + } + tmp := rerere(intermediate, ft.Type) + if tmp != nil { + if reflect.ValueOf(tmp).Kind() == reflect.Pointer && fv.Kind() != reflect.Pointer { + fv.Set(reflect.ValueOf(tmp).Elem()) + } else { + fv.Set(reflect.ValueOf(tmp)) + } + } else { + fv.Set(reflect.Zero(ft.Type)) + } + } + } + if shouldBreak { + // break + } + } else { + nunu := ModelRegistry.new_(resType.Name()) + ider, ok := nunu.(HasID) + if ok { + ider.SetId(v.Interface()) + if reflect.ValueOf(ider).Kind() == reflect.Pointer { + nunu = reflect.ValueOf(ider).Elem().Interface() + } + rve.Set(reflect.ValueOf(nunu)) + } + } + case reflect.Slice: + arr := v.Interface().(bson.A) + for _, it := range arr { + if it != nil { + tmp := reflect.ValueOf(rerere(it, rve.Type().Elem())) + if tmp.Kind() == reflect.Pointer { + tmp = tmp.Elem() + } + rve.Set(reflect.Append(rve, tmp)) + } + } + default: + if resType.AssignableTo(v.Type()) { + rve.Set(reflect.ValueOf(input)) + } else { + switch rve.Interface().(type) { + case int, int32, int64, uint, uint32, uint64: + rve.Set(reflect.ValueOf(coerceInt(v, rve))) + } + } + } + + return resV.Interface() +} + +func handleAnon(raw interface{}, rtype reflect.Type, rval reflect.Value) reflect.Value { + f := rval + g := rtype + if rtype.Kind() == reflect.Pointer { + g = rtype.Elem() + } + if !f.CanSet() { + f = reflect.New(g) + f.Elem().Set(rval) + } + if rtype.Kind() != reflect.Struct { + return rval + } + + for i := 0; i < rtype.NumField(); i++ { + typeField := rtype.Field(i) + valueField := f.Field(i) + tags, err := structtag.Parse(string(typeField.Tag)) + if !typeField.IsExported() { + continue + } + if err == nil { + var btag *structtag.Tag + btag, err = tags.Get("bson") + if err != nil { + continue + } + if btag.Name == "-" { + continue + } + amap, ok := raw.(bson.M) + if ok { + fval := amap[btag.Name] + if reflect.TypeOf(fval) == reflect.TypeFor[primitive.DateTime]() { + fval = time.UnixMilli(int64(fval.(primitive.DateTime))) + } + if valueField.Kind() == reflect.Pointer { + valueField.Elem().Set(reflect.ValueOf(fval)) + } else { + valueField.Set(reflect.ValueOf(fval)) + } + } + } + } + return f +} + +// JSON - marshals this Query's results into json format +func (q *Query) JSON() (string, error) { + res, err := json.MarshalIndent(q.doc, "\n", "\t") + if err != nil { + return "", err + } + return string(res[:]), nil +} + +// Exec - executes the query and puts its results into the +// provided argument. +// +// Will panic if called more than once on the same Query instance. +func (q *Query) Exec(result interface{}) { + if q.done { + panic("Exec() has already been called!") + } + reflect.ValueOf(result).Elem().Set(reflect.ValueOf(q.doc).Elem()) + q.done = true +} diff --git a/registry.go b/registry.go new file mode 100644 index 0000000..34eb399 --- /dev/null +++ b/registry.go @@ -0,0 +1,351 @@ +package orm + +import ( + context2 "context" + "fmt" + "log" + "reflect" + "strings" + "sync" + + "github.com/fatih/structtag" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "golang.org/x/net/context" +) + +// InternalModel, as the name suggests, is used +// internally by the model registry +type InternalModel struct { + Idx int + Type reflect.Type + Collection string + References map[string]Reference + Indexes map[string][]InternalIndex +} + +// Reference stores a typed document reference +type Reference struct { + // owning model name + Model string + // the name of the struct field + FieldName string + // index of field in owning struct + Idx int + // the type of the referenced object + HydratedType reflect.Type + + // field kind (struct, slice, ...) + Kind reflect.Kind + + Exists bool +} + +type TModelRegistry map[string]*InternalModel + +// ModelRegistry - the ModelRegistry stores a map containing +// pointers to InternalModel instances, keyed by an associated +// model name +var ModelRegistry = make(TModelRegistry, 0) + +// DB - The mongodb database handle +var DB *mongo.Database + +// DBClient - The mongodb client +var DBClient *mongo.Client + +// NextStringID - Override this function with your own +// string ID generator! +var NextStringID func() string + +var mutex sync.Mutex + +func getRawTypeFromTag(tagOpt string, slice bool) reflect.Type { + var t reflect.Type + switch strings.ToLower(tagOpt) { + case "int": + var v int64 = 0 + t = reflect.TypeOf(v) + case "uint": + var v uint = 0 + t = reflect.TypeOf(v) + case "string": + var v string = "0" + t = reflect.TypeOf(v) + + } + if slice { + return reflect.SliceOf(t) + } + return t +} + +func makeRef(idx int, modelName string, fieldName string, ht reflect.Type) Reference { + if modelName != "" { + if ModelRegistry.Index(modelName) != -1 { + return Reference{ + Idx: idx, + Model: modelName, + HydratedType: ht, + Kind: ht.Kind(), + Exists: true, + FieldName: fieldName, + } + } + return Reference{ + Idx: idx, + Model: modelName, + FieldName: fieldName, + HydratedType: ht, + Kind: ht.Kind(), + Exists: true, + } + } + panic("model name was empty") +} + +func parseTags(t reflect.Type, v reflect.Value) (map[string][]InternalIndex, map[string]Reference, string) { + coll := "" + refs := make(map[string]Reference, 0) + idcs := make(map[string][]InternalIndex, 0) + + for i := 0; i < v.NumField(); i++ { + sft := t.Field(i) + ft := sft.Type + tags, err := structtag.Parse(string(sft.Tag)) + panik(err) + shouldContinue := true + for { + if !shouldContinue { + break + } + switch ft.Kind() { + case reflect.Slice: + ft = ft.Elem() + if _, ok := tags.Get("ref"); ok != nil { + if ft.Kind() == reflect.Struct { + ii2, rr2, _ := parseTags(ft, reflect.New(ft).Elem()) + for k, v := range ii2 { + idcs[sft.Name+"."+k] = v + } + for k, v := range rr2 { + refs[sft.Name+"."+k] = v + } + } + + } + continue + case reflect.Pointer: + ft = ft.Elem() + fallthrough + case reflect.Struct: + if ft.ConvertibleTo(reflect.TypeOf(Model{})) { + collTag, err := tags.Get("coll") + panik(err) + coll = collTag.Name + idxTag, err := tags.Get("idx") + if err == nil { + idcs[sft.Type.Name()] = scanIndex(idxTag.Value()) + } + shouldContinue = false + break + } + if refTag, ok := tags.Get("ref"); ok == nil { + // ref:"ModelName,refType" + /* if len(refTag.Options) < 1 { + panic("no raw type provided for ref") + } */ + sname := sft.Name + "@" + refTag.Name + refs[sname] = makeRef(i, refTag.Name, sft.Name, sft.Type) + } + fallthrough + default: + idxTag, err := tags.Get("idx") + if err == nil { + idcs[sft.Name] = scanIndex(idxTag.Value()) + } + shouldContinue = false + } + + } + } + return idcs, refs, coll +} + +// Has returns the model typename and InternalModel instance corresponding +// to the argument passed, as well as a boolean indicating whether it +// was found. otherwise returns `"", nil, false` +func (r TModelRegistry) Has(i interface{}) (string, *InternalModel, bool) { + t := reflect.TypeOf(i) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + n := t.Name() + if rT, ok := ModelRegistry[n]; ok { + return n, rT, true + } + return "", nil, false +} + +// HasByName functions almost identically to Has, +// except that it takes a string as its argument. +func (t TModelRegistry) HasByName(n string) (string, *InternalModel, bool) { + if t, ok := ModelRegistry[n]; ok { + return n, t, true + } + return "", nil, false +} + +// Index returns the index at which the Model struct is embedded +func (r TModelRegistry) Index(n string) int { + if v, ok := ModelRegistry[n]; ok { + return v.Idx + } + return -1 +} + +func (t TModelRegistry) new_(n string) interface{} { + if n, m, ok := ModelRegistry.HasByName(n); ok { + v := reflect.New(m.Type) + df := v.Elem().Field(m.Idx) + d := df.Interface().(Model) + d.typeName = n + df.Set(reflect.ValueOf(d)) + return v.Interface() + } + return nil +} + +// Model registers models in the ModelRegistry, where +// they can be accessed via a model's struct name +func (r TModelRegistry) Model(mdl ...any) { + defer mutex.Unlock() + mutex.Lock() + + for _, m := range mdl { + t := reflect.TypeOf(m) + v := reflect.ValueOf(m) + vp := v + if vp.Kind() != reflect.Ptr { + vp = reflect.New(v.Type()) + vp.Elem().Set(v) + } + id, ok := vp.Interface().(HasID) + if !ok { + panic(fmt.Sprintf("you MUST implement the HasID interface!!! skipping...\n")) + } + switch (id).Id().(type) { + case int, int64, int32, string, primitive.ObjectID, uint, uint32, uint64: + break + default: + log.Printf("invalid ID type specified!!! skipping...\n") + } + + if t.Kind() == reflect.Ptr { + t = reflect.Indirect(reflect.ValueOf(m)).Type() + v = reflect.ValueOf(m).Elem() + } + n := t.Name() + if t.Kind() != reflect.Struct { + panic(fmt.Sprintf("Only structs can be passed to this function, silly! (passed type: %s)", n)) + } + idx := -1 + for i := 0; i < v.NumField(); i++ { + ft := t.Field(i) + if (ft.Type.ConvertibleTo(reflect.TypeOf(Model{}))) { + idx = i + break + } + } + if idx < 0 { + panic("A model must embed the Model struct!") + } + inds, refs, coll := parseTags(t, v) + if coll == "" { + panic(fmt.Sprintf("a model needs to be given a collection name! (passed type: %s)", n)) + } + ModelRegistry[n] = &InternalModel{ + Idx: idx, + Type: t, + Collection: coll, + Indexes: inds, + References: refs, + } + } + for k, v := range ModelRegistry { + for k2, v2 := range v.References { + if !v2.Exists { + if _, ok := ModelRegistry[v2.FieldName]; ok { + tmp := ModelRegistry[k].References[k2] + ModelRegistry[k].References[k2] = Reference{ + Model: k, + Idx: tmp.Idx, + FieldName: tmp.FieldName, + Kind: tmp.Kind, + HydratedType: tmp.HydratedType, + Exists: true, + } + } + } + } + } +} + +func innerWatch(coll *mongo.Collection) { + sspipeline := mongo.Pipeline{ + bson.D{{"$match", bson.D{{"$or", + bson.A{ + bson.D{{ + "operationType", "insert", + }}, + bson.D{{"operationType", "update"}}, + }, + }}, + }}, + } + + stream, err := coll.Watch(context.TODO(), sspipeline, options.ChangeStream().SetFullDocument(options.UpdateLookup).SetFullDocumentBeforeChange(options.WhenAvailable)) + panik(err) + defer func(stream *mongo.ChangeStream, ctx context2.Context) { + err := stream.Close(ctx) + panik(err) + }(stream, context.TODO()) + for stream.Next(context.TODO()) { + var data bson.M + if err := stream.Decode(&data); err != nil { + log.Fatal(err) + } + + var uid = data["documentKey"].(bson.M)["_id"] + + if data["operationType"] == "insert" { + counterColl := DB.Collection(COUNTER_COL) + counterColl.UpdateOne(context.TODO(), bson.M{"collection": coll.Name()}, bson.M{"$set": bson.M{ + "current": uid, + }}, options.Update().SetUpsert(true)) + } + + fmt.Printf("%v\n", data) + } +} + +func Connect(uri string, dbName string) { + cli, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri)) + if err != nil { + log.Fatal("failed to open database") + } + panik(err) + DB = cli.Database(dbName) + colls, err := DB.ListCollectionNames(context.TODO(), bson.M{"name": bson.M{"$ne": COUNTER_COL}}, options.ListCollections().SetNameOnly(true)) + + for _, c := range colls { + if c == COUNTER_COL { + continue + } + go innerWatch(DB.Collection(c)) + } + + DBClient = cli +} diff --git a/testing.go b/testing.go new file mode 100644 index 0000000..ef8f83e --- /dev/null +++ b/testing.go @@ -0,0 +1,174 @@ +package orm + +import ( + "context" + "fmt" + "time" + + "github.com/go-loremipsum/loremipsum" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type chapter struct { + ID primitive.ObjectID `bson:"_id" json:"_id"` + Title string `bson:"chapterTitle" json:"chapterTitle" form:"chapterTitle"` + ChapterID int `bson:"id" json:"chapterID"` + Index int `bson:"index" json:"index" form:"index"` + Words int `bson:"words" json:"words"` + Notes string `bson:"notes" json:"notes" form:"notes"` + Genre []string `bson:"genre" json:"genre" form:"genre"` + Bands []band `json:"bands" bson:"bands" ref:"band,bands"` + Characters []string `bson:"characters" json:"characters" form:"characters"` + Relationships [][]string `bson:"relationships" json:"relationships" form:"relationships"` + Adult bool `bson:"adult" json:"adult" form:"adult"` + Summary string `bson:"summary" json:"summary" form:"summary"` + Hidden bool `bson:"hidden" json:"hidden" form:"hidden"` + LoggedInOnly bool `bson:"loggedInOnly" json:"loggedInOnly" form:"loggedInOnly"` + Posted time.Time `bson:"datePosted,omitempty" json:"datePosted"` + FileName string `json:"fileName" bson:"-"` + Text string `json:"text" bson:"-"` +} + +type band struct { + ID int64 `bson:"_id" json:"_id"` + Model `bson:",inline" json:",inline" coll:"bands"` + Name string `bson:"name" json:"name" form:"name"` + Locked bool `bson:"locked" json:"locked" form:"locked"` + Characters []string `bson:"characters" json:"characters" form:"characters"` +} +type user struct { + ID int64 `bson:"_id" json:"_id"` + Model `bson:",inline" json:",inline" coll:"users"` + Username string `bson:"username" json:"username"` +} +type story struct { + ID int64 `bson:"_id" json:"_id"` + Model `bson:",inline" json:",inline" coll:"stories"` + Title string `bson:"title" json:"title" form:"title"` + Author *user `bson:"author" json:"author" ref:"user"` + CoAuthor *user `bson:"coAuthor" json:"coAuthor" ref:"user"` + Chapters []chapter `bson:"chapters" json:"chapters"` + Recs int `bson:"recs" json:"recs"` + Favs int `bson:"favs" json:"favs"` + Views int `bson:"views" json:"views"` + Completed bool `bson:"completed" json:"completed" form:"completed"` + Downloads int `bson:"downloads" json:"downloads"` +} + +func (s *story) Id() any { + return s.ID +} + +func (s *band) Id() any { + return s.ID +} +func (s *user) Id() any { + return s.ID +} + +func (s *story) SetId(id any) { + s.ID = id.(int64) + //var t IModel = s +} + +func (s *band) SetId(id any) { + s.ID = id.(int64) +} + +func (s *user) SetId(id any) { + s.ID = id.(int64) +} + +var author = user{ + Username: "tablet.exe", +} + +func genChaps(single bool) []chapter { + var ret []chapter + var ceil int + if single { + ceil = 1 + } else { + ceil = 5 + } + emptyRel := make([][]string, 0) + emptyRel = append(emptyRel, make([]string, 0)) + relMap := [][][]string{ + { + {"Sean Harris", "Brian Tatler"}, + }, + { + {"Sean Harris", "Brian Tatler"}, + {"Duncan Scott", "Colin Kimberley"}, + }, + { + {"Duncan Scott", "Colin Kimberley"}, + }, + emptyRel, + { + {"Sean Harris", "Colin Kimberley", "Brian Tatler"}, + }, + } + var b band = band{ + Name: "Diamond Head", + Locked: false, + Characters: []string{"Brian Tatler", "Sean Harris", "Duncan Scott", "Colin Kimberley"}, + } + b.ID = 503 + for i := 0; i < ceil; i++ { + spf := fmt.Sprintf("%d.md", i+1) + ret = append(ret, chapter{ + Title: fmt.Sprintf("-%d-", i+1), + Index: int(i + 1), + Words: 50, + Notes: "notenotenote !!!", + Genre: []string{"Slash"}, + Bands: []band{b}, + Characters: []string{"Sean Harris", "Brian Tatler", "Duncan Scott", "Colin Kimberley"}, + Relationships: relMap[i], + Adult: true, + Summary: loremipsum.New().Paragraph(), + Hidden: false, + LoggedInOnly: true, + FileName: spf, + }) + } + + return ret +} + +var iti_single story = story{ + Title: "title", + Completed: true, + Chapters: genChaps(true), +} + +var iti_multi story = story{ + Title: "Brian Tatler Fucked and Abused Sean Harris", + Completed: false, + Chapters: genChaps(false), +} + +func initTest() { + uri := "mongodb://127.0.0.1:27017" + db := "rockfic_ormTest" + ic, _ := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri)) + colls, _ := ic.Database(db).ListCollectionNames(context.TODO(), bson.M{}) + if len(colls) < 1 { + mdb := ic.Database(db) + mdb.CreateCollection(context.TODO(), "bands") + mdb.CreateCollection(context.TODO(), "stories") + mdb.CreateCollection(context.TODO(), "users") + } + defer ic.Disconnect(context.TODO()) + Connect(uri, db) + author.ID = 696969 + ModelRegistry.Model(band{}, user{}, story{}) +} +func after() { + err := DBClient.Disconnect(context.TODO()) + panik(err) +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..2b70249 --- /dev/null +++ b/util.go @@ -0,0 +1,70 @@ +package orm + +import "reflect" + +func panik(err error) { + if err != nil { + panic(err) + } +} + +func nameOf(i interface{}) string { + v := reflect.ValueOf(i) + var n string + switch v.Kind() { + case reflect.Slice, reflect.Map: + if v.Type().Elem().Kind() == reflect.Pointer { + n = v.Type().Elem().Elem().Name() + } + case reflect.Pointer: + n = nameOf(reflect.Indirect(v).Interface()) + default: + n = v.Type().Name() + } + return n +} + +func valueOf(i interface{}) reflect.Value { + v := reflect.ValueOf(i) + if v.Type().Kind() == reflect.Slice || v.Type().Kind() == reflect.Map { + in := v.Type().Elem() + switch in.Kind() { + case reflect.Pointer: + v = reflect.New(in.Elem()).Elem() + default: + v = reflect.New(in).Elem() + } + } else if v.Type().Kind() == reflect.Pointer { + v = valueOf(reflect.Indirect(v).Interface()) + } + return v +} + +func iFace(input interface{}) interface{} { + return reflect.ValueOf(input).Interface() +} + +func iFaceSlice(input interface{}) []interface{} { + ret := make([]interface{}, 0) + fv := reflect.ValueOf(input) + if fv.Type().Kind() != reflect.Slice { + return ret + } + for i := 0; i < fv.Len(); i++ { + ret = append(ret, fv.Index(i).Interface()) + } + return ret +} + +func coerceInt(input reflect.Value, dst reflect.Value) interface{} { + if input.Type().Kind() == reflect.Pointer { + input = input.Elem() + } + if dst.Type().Kind() == reflect.Pointer { + dst = dst.Elem() + } + if input.Type().ConvertibleTo(dst.Type()) { + return input.Convert(dst.Type()).Interface() + } + return nil +}