hello world! 🌸
This commit is contained in:
commit
f46aab2f5f
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*.exe
|
||||
*.sum
|
||||
muck/
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -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
|
26
go.mod
Normal file
26
go.mod
Normal file
@ -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
|
3
go.work
Normal file
3
go.work
Normal file
@ -0,0 +1,3 @@
|
||||
go 1.23.0
|
||||
|
||||
use .
|
74
idcounter.go
Normal file
74
idcounter.go
Normal file
@ -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
|
||||
}
|
148
indexes.go
Normal file
148
indexes.go
Normal file
@ -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
|
||||
}
|
371
model.go
Normal file
371
model.go
Normal file
@ -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))
|
||||
}
|
109
model_test.go
Normal file
109
model_test.go
Normal file
@ -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)
|
||||
}
|
482
query.go
Normal file
482
query.go
Normal file
@ -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
|
||||
}
|
351
registry.go
Normal file
351
registry.go
Normal file
@ -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
|
||||
}
|
174
testing.go
Normal file
174
testing.go
Normal file
@ -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)
|
||||
}
|
70
util.go
Normal file
70
util.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user