hello world! 🌸

This commit is contained in:
commit f46aab2f5f
Signed by: tablet
GPG Key ID: 924A5F6AF051E87C
12 changed files with 1819 additions and 0 deletions

3
.gitignore vendored Normal file

@ -0,0 +1,3 @@
*.exe
*.sum
muck/

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

@ -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

@ -0,0 +1,3 @@
go 1.23.0
use .

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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
}