Compare commits

...

9 Commits

9 changed files with 579 additions and 90 deletions

20
errors.go Normal file

@ -0,0 +1,20 @@
package orm
import (
"errors"
)
var (
ErrNotASlice = errors.New("Current object or field is not a slice!")
ErrNotAStruct = errors.New("Current object or field is not a struct!")
ErrOutOfBounds = errors.New("Index(es) out of bounds!")
ErrAppendMultipleDocuments = errors.New("Cannot append to multiple documents!")
ErrNotSliceOrStruct = errors.New("Current object or field is not a slice nor a struct!")
)
const (
errFmtMalformedField = "Malformed field name passed: '%s'"
errFmtNotAModel = "Type '%s' is not a model"
errFmtNotHasID = "Type '%s' does not implement HasID"
errFmtModelNotRegistered = "Model not registered for type: '%s'"
)

197
gridfs.go Normal file

@ -0,0 +1,197 @@
package orm
import (
"bytes"
"context"
"errors"
"fmt"
"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/gridfs"
"go.mongodb.org/mongo-driver/mongo/options"
"html/template"
"io"
"reflect"
"strings"
)
type GridFSFile struct {
ID primitive.ObjectID `bson:"_id"`
Name string `bson:"filename"`
Length int `bson:"length"`
}
func parseFmt(format string, value any) string {
tmpl, err := template.New("filename").Parse(format)
panik(err)
w := new(strings.Builder)
err = tmpl.Execute(w, value)
panik(err)
return w.String()
}
func bucket(gfsRef GridFSReference) *gridfs.Bucket {
b, _ := gridfs.NewBucket(DB, options.GridFSBucket().SetName(gfsRef.BucketName))
return b
}
func gridFsLoad(val any, g GridFSReference, field string) any {
doc := reflect.ValueOf(val)
rdoc := reflect.ValueOf(val)
if doc.Kind() != reflect.Pointer {
doc = reflect.New(reflect.TypeOf(val))
doc.Elem().Set(reflect.ValueOf(val))
}
var next string
if len(strings.Split(field, ".")) > 1 {
next = strings.Join(strings.Split(field, ".")[1:], ".")
field = strings.Split(field, ".")[0]
} else {
next = field
}
_, rfield, ferr := getNested(field, rdoc)
if ferr != nil {
return nil
}
switch rfield.Kind() {
case reflect.Slice:
for i := 0; i < rfield.Len(); i++ {
cur := rfield.Index(i)
if cur.Kind() != reflect.Pointer {
tmp := reflect.New(cur.Type())
tmp.Elem().Set(cur)
cur = tmp
}
intermediate := gridFsLoad(cur.Interface(), g, next)
if intermediate == nil {
continue
}
ival := reflect.ValueOf(intermediate)
if ival.Kind() == reflect.Pointer {
ival = ival.Elem()
}
rfield.Index(i).Set(ival)
}
case reflect.Struct:
intermediate := gridFsLoad(rfield.Interface(), g, next)
if intermediate != nil {
rfield.Set(reflect.ValueOf(intermediate))
}
default:
b := bucket(g)
var found GridFSFile
cursor, err := b.Find(bson.M{"filename": parseFmt(g.FilenameFmt, val)})
if err != nil {
return nil
}
cursor.Next(context.TODO())
_ = cursor.Decode(&found)
bb := bytes.NewBuffer(nil)
_, err = b.DownloadToStream(found.ID, bb)
if err != nil {
return nil
}
if rfield.Type().AssignableTo(reflect.TypeFor[[]byte]()) {
rfield.Set(reflect.ValueOf(bb.Bytes()))
} else if rfield.Type().AssignableTo(reflect.TypeFor[string]()) {
rfield.Set(reflect.ValueOf(bb.String()))
}
}
if rdoc.Kind() != reflect.Pointer {
return doc.Elem().Interface()
}
return doc.Interface()
}
func gridFsSave(val any, imodel InternalModel) error {
var rerr error
v := reflect.ValueOf(val)
el := v
if v.Kind() == reflect.Pointer {
el = el.Elem()
}
switch el.Kind() {
case reflect.Struct:
for i := 0; i < el.NumField(); i++ {
ft := el.Type().Field(i)
fv := el.Field(i)
if !ft.IsExported() {
continue
}
_, err := structtag.Parse(string(ft.Tag))
panik(err)
var gfsRef *GridFSReference
for kk, vv := range imodel.GridFSReferences {
if strings.HasPrefix(kk, ft.Name) {
gfsRef = &vv
break
}
}
var inner = func(b *gridfs.Bucket, it reflect.Value) error {
filename := parseFmt(gfsRef.FilenameFmt, it.Interface())
contents := GridFSFile{}
curs, err2 := b.Find(bson.M{"filename": filename})
if !errors.Is(err2, mongo.ErrNoDocuments) {
_ = curs.Decode(&contents)
if !reflect.ValueOf(contents).IsZero() {
_ = b.Delete(contents.ID)
}
}
c := it.Field(gfsRef.Idx)
var rdr io.Reader
if c.Type().AssignableTo(reflect.TypeOf([]byte{})) {
rdr = bytes.NewReader(c.Interface().([]byte))
} else if c.Type().AssignableTo(reflect.TypeOf("")) {
rdr = strings.NewReader(c.Interface().(string))
} else {
return fmt.Errorf("gridfs loader type '%s' not supported", c.Type().String())
}
_, err = b.UploadFromStream(filename, rdr)
return err
}
if gfsRef != nil {
b := bucket(*gfsRef)
if fv.Kind() == reflect.Slice {
for j := 0; j < fv.Len(); j++ {
lerr := inner(b, fv.Index(j))
if lerr != nil {
return lerr
}
}
} else if fv.Kind() == reflect.Struct {
lerr := inner(b, fv)
if lerr != nil {
return lerr
}
} else {
lerr := inner(b, el)
if lerr != nil {
return lerr
}
}
}
err = gridFsSave(fv.Interface(), imodel)
if err != nil {
return err
}
}
case reflect.Slice:
for i := 0; i < el.Len(); i++ {
rerr = gridFsSave(el.Index(i).Interface(), imodel)
if rerr != nil {
return rerr
}
}
default:
break
}
return rerr
}

@ -66,6 +66,7 @@ type IModel interface {
getIdxs() []*mongo.IndexModel
getParsedIdxs() map[string][]InternalIndex
serializeToStore() any
getTypeName() string
setTypeName(str string)
getExists() bool
setExists(n bool)
@ -76,6 +77,10 @@ type IModel interface {
setSelf(arg interface{})
}
func (m *Model) getTypeName() string {
return m.typeName
}
func (m *Model) setTypeName(str string) {
m.typeName = str
}
@ -135,10 +140,10 @@ func (m *Model) Find(query interface{}, opts ...*options.FindOptions) (*Query, e
qqv := reflect.New(qqt)
qqv.Elem().Set(reflect.MakeSlice(qqt, 0, 0))
qq := &Query{
Model: m,
Collection: m.getColl(),
model: m,
collection: m.getColl(),
doc: qqv.Interface(),
Op: OP_FIND_ALL,
op: OP_FIND_ALL,
}
q, err := m.FindRaw(query, opts...)
@ -172,7 +177,7 @@ func (m *Model) FindPaged(query interface{}, page int64, perPage int64, opts ...
opts = append(opts, options.Find().SetSkip(skipAmt).SetLimit(perPage))
}
q, err := m.Find(query, opts...)
q.Op = OP_FIND_PAGED
q.op = OP_FIND_PAGED
return q, err
}
@ -195,11 +200,11 @@ func (m *Model) FindOne(query interface{}, options ...*options.FindOneOptions) (
v := reflect.New(reflect.TypeOf(qqn))
v.Elem().Set(reflect.ValueOf(qqn))
qq := &Query{
Collection: m.getColl(),
collection: m.getColl(),
rawDoc: raw,
doc: v.Elem().Interface(),
Op: OP_FIND_ONE,
Model: m,
op: OP_FIND_ONE,
model: m,
}
qq.rawDoc = raw
err = rip.Decode(qq.doc)
@ -259,7 +264,7 @@ func (m *Model) Append(field string, a ...interface{}) error {
fv = fv.Elem()
}
if fv.Kind() != reflect.Slice {
return fmt.Errorf("Current object is not a slice!")
return ErrNotASlice
}
for _, b := range a {
val := reflect.ValueOf(incrementTagged(b))
@ -291,7 +296,7 @@ func (m *Model) Pull(field string, a ...any) error {
fv = fv.Elem()
}
if fv.Kind() != reflect.Slice {
return fmt.Errorf("Current object is not a slice!")
return ErrNotASlice
}
outer:
for _, b := range a {
@ -332,7 +337,7 @@ func (m *Model) Swap(field string, i, j int) error {
return err
}
if i >= fv.Len() || j >= fv.Len() {
return fmt.Errorf("index(es) out of bounds")
return ErrOutOfBounds
}
oi := fv.Index(i).Interface()
oj := fv.Index(j).Interface()

@ -127,7 +127,7 @@ func doSave(c *mongo.Collection, isNew bool, arg interface{}) error {
var err error
m, ok := arg.(IModel)
if !ok {
return fmt.Errorf("type '%s' is not a model", nameOf(arg))
return fmt.Errorf(errFmtNotAModel, nameOf(arg))
}
m.setSelf(m)
now := time.Now()
@ -138,6 +138,7 @@ func doSave(c *mongo.Collection, isNew bool, arg interface{}) error {
vp.Elem().Set(selfo)
}
var asHasId = vp.Interface().(HasID)
var asModel = vp.Interface().(IModel)
if isNew {
m.setCreated(now)
}
@ -156,6 +157,8 @@ func doSave(c *mongo.Collection, isNew bool, arg interface{}) error {
(asHasId).SetId(pnid)
}
incrementAll(asHasId)
_, im, _ := ModelRegistry.HasByName(asModel.getTypeName())
_ = gridFsSave(asHasId, *im)
_, err = c.InsertOne(context.TODO(), m.serializeToStore())
if err == nil {
@ -170,7 +173,7 @@ func doDelete(m *Model, arg interface{}) error {
self, ok := arg.(HasID)
if !ok {
return fmt.Errorf("Object '%s' does not implement HasID", nameOf(arg))
return fmt.Errorf(errFmtNotHasID, nameOf(arg))
}
c := m.getColl()
_, err := c.DeleteOne(context.TODO(), bson.M{"_id": self.Id()})

@ -11,14 +11,14 @@ import (
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)
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)
storyDoc := Create(iti_multi()).(*story)
lauthor := Create(author).(*user)
storyDoc.Author = lauthor
assert.Equal(t, storyDoc.Id(), int64(0))
@ -37,8 +37,8 @@ func TestSave(t *testing.T) {
func TestPopulate(t *testing.T) {
initTest()
bandDoc := Create(iti_single.Chapters[0].Bands[0]).(*band)
storyDoc := Create(iti_single).(*story)
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
storyDoc := Create(iti_single()).(*story)
mauthor := Create(author).(*user)
saveDoc(t, mauthor)
saveDoc(t, bandDoc)
@ -79,7 +79,8 @@ func TestUpdate(t *testing.T) {
func TestModel_FindAll(t *testing.T) {
initTest()
createAndSave(t, &iti_multi)
im := iti_multi()
createAndSave(t, &im)
smodel := Create(story{}).(*story)
query, err := smodel.Find(bson.M{}, options.Find())
assert.Equal(t, nil, err)
@ -90,12 +91,13 @@ func TestModel_FindAll(t *testing.T) {
func TestModel_PopulateMulti(t *testing.T) {
initTest()
bandDoc := Create(iti_single.Chapters[0].Bands[0]).(*band)
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
saveDoc(t, bandDoc)
mauthor := Create(author).(*user)
saveDoc(t, mauthor)
iti_multi.Author = mauthor
createAndSave(t, &iti_multi)
im := iti_multi()
im.Author = mauthor
createAndSave(t, &im)
smodel := Create(story{}).(*story)
query, err := smodel.Find(bson.M{}, options.Find())
assert.Equal(t, nil, err)
@ -107,6 +109,52 @@ func TestModel_PopulateMulti(t *testing.T) {
}
}
func TestModel_PopulateChained_Multi(t *testing.T) {
initTest()
im := iti_multi()
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
saveDoc(t, bandDoc)
mauthor := Create(author).(*user)
saveDoc(t, mauthor)
im.Author = mauthor
createAndSave(t, &im)
smodel := Create(story{}).(*story)
query, err := smodel.Find(bson.M{}, options.Find())
assert.Equal(t, nil, err)
final := CreateSlice(story{})
query.Populate("Author").Populate("Chapters.Bands").Exec(&final)
assert.Greater(t, len(final), 0)
for _, s := range final {
assert.NotZero(t, s.Chapters[0].Bands[0].Name)
}
}
func TestPopulate_Chained(t *testing.T) {
initTest()
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
storyDoc := Create(iti_single()).(*story)
mauthor := Create(author).(*user)
saveDoc(t, mauthor)
saveDoc(t, bandDoc)
storyDoc.Author = mauthor
saveDoc(t, storyDoc)
assert.Greater(t, storyDoc.ID, int64(0))
smodel := Create(story{}).(*story)
q, err := smodel.FindByID(storyDoc.ID)
assert.Equal(t, nil, err)
assert.NotPanics(t, func() {
foundDoc := &story{}
q.Populate("Author").Populate("Chapters.Bands").Exec(foundDoc)
j, _ := q.JSON()
fmt.Printf("%s\n", j)
})
for _, c := range storyDoc.Chapters {
assert.NotZero(t, c.Bands[0].Name)
}
}
func TestModel_Append(t *testing.T) {
initTest()
bandDoc := Create(metallica).(*band)
@ -136,7 +184,7 @@ func TestModel_Delete(t *testing.T) {
func TestModel_Pull(t *testing.T) {
initTest()
storyDoc := Create(iti_multi).(*story)
storyDoc := Create(iti_multi()).(*story)
smodel := Create(story{}).(*story)
saveDoc(t, storyDoc)
err := storyDoc.Pull("Chapters", storyDoc.Chapters[4])
@ -152,15 +200,78 @@ func TestModel_Pull(t *testing.T) {
func TestModel_Swap(t *testing.T) {
initTest()
iti_single.Author = &author
storyDoc := Create(iti_single).(*story)
is := iti_single()
is.Author = &author
storyDoc := Create(iti_single()).(*story)
saveDoc(t, storyDoc)
storyDoc.Chapters[0].Bands = append(storyDoc.Chapters[0].Bands, metallica)
storyDoc.Chapters[0].Bands = append(storyDoc.Chapters[0].Bands, bodom)
assert.Equal(t, 2, len(storyDoc.Chapters[0].Bands))
err := storyDoc.Swap("Chapters[0].Bands", 0, 1)
assert.Nil(t, err)
c := storyDoc.Chapters[0].Bands
assert.Equal(t, metallica.ID, c[0].ID)
assert.Equal(t, dh.ID, c[1].ID)
assert.Equal(t, bodom.ID, c[0].ID)
assert.Equal(t, diamondHead.ID, c[1].ID)
saveDoc(t, storyDoc)
}
func TestModel_GridFSLoad(t *testing.T) {
initTest()
ModelRegistry.Model(somethingWithNestedChapters{})
model := Create(somethingWithNestedChapters{}).(*somethingWithNestedChapters)
thingDoc := Create(doSomethingWithNested()).(*somethingWithNestedChapters)
found := &somethingWithNestedChapters{}
saveDoc(t, thingDoc)
assert.NotZero(t, thingDoc.ID)
fq, err := model.FindByID(thingDoc.ID)
assert.Nil(t, err)
fq.LoadFile("NestedText", "Chapters.Text").Exec(found)
assert.NotZero(t, found.NestedText)
assert.NotZero(t, len(found.Chapters))
for _, c := range found.Chapters {
assert.NotZero(t, c.Text)
}
}
func TestModel_GridFSLoad_Chained(t *testing.T) {
initTest()
ModelRegistry.Model(somethingWithNestedChapters{})
model := Create(somethingWithNestedChapters{}).(*somethingWithNestedChapters)
thingDoc := Create(doSomethingWithNested()).(*somethingWithNestedChapters)
found := &somethingWithNestedChapters{}
saveDoc(t, thingDoc)
assert.NotZero(t, thingDoc.ID)
fq, err := model.FindByID(thingDoc.ID)
assert.Nil(t, err)
fq.LoadFile("NestedText").LoadFile("Chapters.Text").Exec(found)
assert.NotZero(t, found.NestedText)
assert.NotZero(t, len(found.Chapters))
for _, c := range found.Chapters {
assert.NotZero(t, c.Text)
}
}
func TestModel_GridFSLoad_Complex(t *testing.T) {
initTest()
model := Create(story{}).(*story)
bandDoc := Create(iti_single().Chapters[0].Bands[0]).(*band)
thingDoc := Create(iti_multi()).(*story)
mauthor := Create(author).(*user)
found := &story{}
saveDoc(t, bandDoc)
saveDoc(t, mauthor)
thingDoc.Author = mauthor
saveDoc(t, thingDoc)
assert.NotZero(t, thingDoc.ID)
fq, err := model.FindByID(thingDoc.ID)
assert.Nil(t, err)
fq.Populate("Author", "Chapters.Bands").LoadFile("Chapters.Text").Exec(found)
assert.NotZero(t, len(found.Chapters))
for _, c := range found.Chapters {
assert.NotZero(t, c.Text)
assert.NotZero(t, c.Bands[0].Name)
}
j, _ := fq.JSON()
fmt.Printf("%s\n", j)
}

@ -15,15 +15,12 @@ import (
)
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
collection *mongo.Collection
op string
model *Model
done bool
rawDoc any
doc any
}
const (
@ -187,9 +184,41 @@ func populate(r Reference, rcoll string, rawDoc interface{}, d string, src inter
return src
}
// LoadFile - loads the contents of one or more files
// stored in gridFS into the fields named by `fields`.
//
// gridFS fields can be either a `string` or `[]byte`, and are
// tagged with `gridfs:"BUCKET,FILE_FORMAT`
// where:
// - `BUCKET` is the name of the bucket where the files are stored
// - `FILE_FORMAT` is any valid go template string that resolves to
// the unique file name.
// all exported values and methods present in the surrounding
// struct can be used in this template.
func (q *Query) LoadFile(fields ...string) *Query {
_, cm, _ := ModelRegistry.HasByName(q.model.typeName)
if cm != nil {
for _, field := range fields {
var r GridFSReference
hasAnnotated := false
for k2, v := range cm.GridFSReferences {
if strings.HasPrefix(k2, field) {
r = v
hasAnnotated = true
break
}
}
if hasAnnotated {
q.doc = gridFsLoad(q.doc, r, field)
}
}
}
return q
}
// Populate populates document references via reflection
func (q *Query) Populate(fields ...string) *Query {
_, cm, _ := ModelRegistry.HasByName(q.Model.typeName)
_, cm, _ := ModelRegistry.HasByName(q.model.typeName)
if cm != nil {
rawDoc := q.rawDoc
@ -254,7 +283,7 @@ func (q *Query) Populate(fields ...string) *Query {
func (q *Query) reOrganize() {
var trvo reflect.Value
if arr, ok := q.rawDoc.(bson.A); ok {
typ := ModelRegistry[q.Model.typeName].Type
typ := ModelRegistry[q.model.typeName].Type
if typ.Kind() != reflect.Pointer {
typ = reflect.PointerTo(typ)
}
@ -468,7 +497,7 @@ func handleAnon(raw interface{}, rtype reflect.Type, rval reflect.Value) reflect
// JSON - marshals this Query's results into json format
func (q *Query) JSON() (string, error) {
res, err := json.MarshalIndent(q.doc, "\n", "\t")
res, err := json.MarshalIndent(q.doc, "", "\t")
if err != nil {
return "", err
}
@ -495,6 +524,6 @@ func (q *Query) Exec(result interface{}) {
}
}
reflect.ValueOf(result).Elem().Set(reflect.ValueOf(q.doc).Elem())
q.Model.self = q.doc
q.model.self = q.doc
q.done = true
}

@ -19,11 +19,12 @@ import (
// 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
Idx int
Type reflect.Type
Collection string
References map[string]Reference
Indexes map[string][]InternalIndex
GridFSReferences map[string]GridFSReference
}
// Reference stores a typed document reference
@ -43,6 +44,13 @@ type Reference struct {
Exists bool
}
type GridFSReference struct {
BucketName string
FilenameFmt string
LoadType reflect.Type
Idx int
}
type TModelRegistry map[string]*InternalModel
// ModelRegistry - the ModelRegistry stores a map containing
@ -82,6 +90,36 @@ func getRawTypeFromTag(tagOpt string, slice bool) reflect.Type {
return t
}
func makeGfsRef(tag *structtag.Tag, idx int) GridFSReference {
opts := tag.Options
var ffmt string
if len(opts) < 1 {
ffmt = "%s"
} else {
ffmt = opts[0]
}
var typ reflect.Type
if len(opts) < 2 {
typ = reflect.TypeOf("")
} else {
switch opts[1] {
case "bytes":
typ = reflect.TypeOf([]byte{})
case "string":
typ = reflect.TypeOf("")
default:
typ = reflect.TypeOf("")
}
}
return GridFSReference{
FilenameFmt: ffmt,
BucketName: tag.Name,
LoadType: typ,
Idx: idx,
}
}
func makeRef(idx int, modelName string, fieldName string, ht reflect.Type) Reference {
if modelName != "" {
if ModelRegistry.Index(modelName) != -1 {
@ -106,10 +144,11 @@ func makeRef(idx int, modelName string, fieldName string, ht reflect.Type) Refer
panic("model name was empty")
}
func parseTags(t reflect.Type, v reflect.Value) (map[string][]InternalIndex, map[string]Reference, string) {
func parseTags(t reflect.Type, v reflect.Value) (map[string][]InternalIndex, map[string]Reference, map[string]GridFSReference, string) {
coll := ""
refs := make(map[string]Reference, 0)
idcs := make(map[string][]InternalIndex, 0)
refs := make(map[string]Reference)
idcs := make(map[string][]InternalIndex)
gfsRefs := make(map[string]GridFSReference)
for i := 0; i < v.NumField(); i++ {
sft := t.Field(i)
@ -126,15 +165,17 @@ func parseTags(t reflect.Type, v reflect.Value) (map[string][]InternalIndex, map
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
ii2, rr2, gg2, _ := parseTags(ft, reflect.New(ft).Elem())
for k, vv := range ii2 {
idcs[sft.Name+"."+k] = vv
}
for k, v := range rr2 {
refs[sft.Name+"."+k] = v
for k, vv := range rr2 {
refs[sft.Name+"."+k] = vv
}
for k, vv := range gg2 {
gfsRefs[sft.Name+"."+k] = vv
}
}
}
continue
case reflect.Pointer:
@ -160,18 +201,26 @@ func parseTags(t reflect.Type, v reflect.Value) (map[string][]InternalIndex, map
sname := sft.Name + "@" + refTag.Name
refs[sname] = makeRef(i, refTag.Name, sft.Name, sft.Type)
}
if gtag, ok := tags.Get("gridfs"); ok == nil {
sname := sft.Name + "@" + gtag.Name
gfsRefs[sname] = makeGfsRef(gtag, i)
}
fallthrough
default:
idxTag, err := tags.Get("idx")
if err == nil {
idcs[sft.Name] = scanIndex(idxTag.Value())
}
if gtag, ok := tags.Get("gridfs"); ok == nil {
sname := sft.Name + "@" + gtag.Name
gfsRefs[sname] = makeGfsRef(gtag, i)
}
shouldContinue = false
}
}
}
return idcs, refs, coll
return idcs, refs, gfsRefs, coll
}
// Has returns the model typename and InternalModel instance corresponding
@ -191,7 +240,7 @@ func (r TModelRegistry) Has(i interface{}) (string, *InternalModel, bool) {
// HasByName functions almost identically to Has,
// except that it takes a string as its argument.
func (t TModelRegistry) HasByName(n string) (string, *InternalModel, bool) {
func (r TModelRegistry) HasByName(n string) (string, *InternalModel, bool) {
if t, ok := ModelRegistry[n]; ok {
return n, t, true
}
@ -206,7 +255,7 @@ func (r TModelRegistry) Index(n string) int {
return -1
}
func (t TModelRegistry) new_(n string) interface{} {
func (r TModelRegistry) new_(n string) interface{} {
if name, m, ok := ModelRegistry.HasByName(n); ok {
v := reflect.New(m.Type)
df := v.Elem().Field(m.Idx)
@ -262,16 +311,17 @@ func (r TModelRegistry) Model(mdl ...any) {
if idx < 0 {
panic("A model must embed the Model struct!")
}
inds, refs, coll := parseTags(t, v)
inds, refs, gfs, 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,
Idx: idx,
Type: t,
Collection: coll,
Indexes: inds,
References: refs,
GridFSReferences: gfs,
}
}
for k, v := range ModelRegistry {

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/stretchr/testify/assert"
"strings"
"testing"
"time"
@ -31,7 +32,7 @@ type chapter struct {
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:"-"`
Text string `json:"text" bson:"-" gridfs:"story_text,/stories/{{.ChapterID}}.txt"`
}
type band struct {
@ -59,6 +60,20 @@ type story struct {
Completed bool `bson:"completed" json:"completed" form:"completed"`
Downloads int `bson:"downloads" json:"downloads"`
}
type somethingWithNestedChapters struct {
ID int64 `bson:"_id" json:"_id"`
Model `bson:",inline" json:",inline" coll:"nested_stuff"`
Chapters []chapter `bson:"chapters" json:"chapters"`
NestedText string `json:"text" bson:"-" gridfs:"nested_text,/nested/{{.ID}}.txt"`
}
func (s *somethingWithNestedChapters) Id() any {
return s.ID
}
func (s *somethingWithNestedChapters) SetId(id any) {
s.ID = id.(int64)
}
func (s *story) Id() any {
return s.ID
@ -114,6 +129,7 @@ func genChaps(single bool) []chapter {
{"Sean Harris", "Colin Kimberley", "Brian Tatler"},
},
}
l := loremipsum.New()
for i := 0; i < ceil; i++ {
spf := fmt.Sprintf("%d.md", i+1)
@ -124,34 +140,47 @@ func genChaps(single bool) []chapter {
Words: 50,
Notes: "notenotenote !!!",
Genre: []string{"Slash"},
Bands: []band{dh},
Bands: []band{diamondHead},
Characters: []string{"Sean Harris", "Brian Tatler", "Duncan Scott", "Colin Kimberley"},
Relationships: relMap[i],
Adult: true,
Summary: loremipsum.New().Paragraph(),
Summary: l.Paragraph(),
Hidden: false,
LoggedInOnly: true,
FileName: spf,
Text: strings.Join(l.ParagraphList(10), "\n\n"),
})
}
return ret
}
var iti_single story = story{
Title: "title",
Completed: true,
Chapters: genChaps(true),
func doSomethingWithNested() somethingWithNestedChapters {
l := loremipsum.New()
swnc := somethingWithNestedChapters{
Chapters: genChaps(false),
NestedText: strings.Join(l.ParagraphList(15), "\n\n"),
}
return swnc
}
func iti_single() story {
return 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 iti_multi() story {
return story{
Title: "Brian Tatler Fucked and Abused Sean Harris",
Completed: false,
Chapters: genChaps(false),
}
}
func iti_blank() story {
t := iti_single
t := iti_single()
t.Chapters = make([]chapter, 0)
return t
}
@ -190,11 +219,30 @@ var metallica = band{
Locked: false,
}
var dh = band{
ID: 503,
Name: "Diamond Head",
Locked: false,
Characters: []string{"Brian Tatler", "Sean Harris", "Duncan Scott", "Colin Kimberley"},
var diamondHead = band{
ID: 503,
Name: "Diamond Head",
Locked: false,
Characters: []string{
"Brian Tatler",
"Sean Harris",
"Duncan Scott",
"Colin Kimberley",
},
}
var bodom = band{
ID: 74,
Name: "Children of Bodom",
Locked: false,
Characters: []string{
"Janne Wirman",
"Alexi Laiho",
"Jaska Raatikainen",
"Henkka T. Blacksmith",
"Roope Latvala",
"Daniel Freyberg",
"Alexander Kuoppala",
},
}
func saveDoc(t *testing.T, doc IModel) {

44
util.go

@ -54,19 +54,45 @@ func coerceInt(input reflect.Value, dst reflect.Value) interface{} {
var arrRegex, _ = regexp.Compile(`\[(?P<index>\d+)]$`)
func getNested(field string, value reflect.Value) (*reflect.StructField, *reflect.Value, error) {
func getNested(field string, aValue reflect.Value) (*reflect.Type, *reflect.Value, error) {
if strings.HasPrefix(field, ".") || strings.HasSuffix(field, ".") {
return nil, nil, fmt.Errorf("Malformed field name %s passed", field)
return nil, nil, fmt.Errorf(errFmtMalformedField, field)
}
value := aValue
if value.Kind() == reflect.Pointer {
value = value.Elem()
}
aft := value.Type()
dots := strings.Split(field, ".")
if value.Kind() != reflect.Struct && arrRegex.FindString(dots[0]) == "" {
return nil, nil, fmt.Errorf("This value is not a struct!")
if value.Kind() != reflect.Struct /*&& arrRegex.FindString(dots[0]) == ""*/ {
if value.Kind() == reflect.Slice {
st := reflect.MakeSlice(value.Type().Elem(), 0, 0)
for i := 0; i < value.Len(); i++ {
cur := value.Index(i)
if len(dots) > 1 {
_, cv, _ := getNested(strings.Join(dots[1:], "."), cur.FieldByName(dots[0]))
reflect.Append(st, *cv)
//return getNested(, "."), fv)
} else {
reflect.Append(st, cur)
}
}
typ := st.Type().Elem()
return &typ, &st, nil
}
if len(dots) > 1 {
return nil, nil, ErrNotSliceOrStruct
} else {
return &aft, &value, nil
}
/*ft := value.Type()
*/
}
ref := value
if ref.Kind() == reflect.Pointer {
ref = ref.Elem()
}
var fv reflect.Value = ref.FieldByName(arrRegex.ReplaceAllString(dots[0], ""))
var fv = ref.FieldByName(arrRegex.ReplaceAllString(dots[0], ""))
if arrRegex.FindString(dots[0]) != "" && fv.Kind() == reflect.Slice {
matches := arrRegex.FindStringSubmatch(dots[0])
ridx, _ := strconv.Atoi(matches[0])
@ -78,7 +104,7 @@ func getNested(field string, value reflect.Value) (*reflect.StructField, *reflec
if len(dots) > 1 {
return getNested(strings.Join(dots[1:], "."), fv)
} else {
return &ft, &fv, nil
return &ft.Type, &fv, nil
}
}
func makeSettable(rval reflect.Value, value interface{}) reflect.Value {
@ -127,17 +153,17 @@ func pull(s reflect.Value, idx int, typ reflect.Type) reflect.Value {
func checkStruct(ref reflect.Value) error {
if ref.Kind() == reflect.Slice {
return fmt.Errorf("Cannot append to multiple documents!")
return ErrAppendMultipleDocuments
}
if ref.Kind() != reflect.Struct {
return fmt.Errorf("Current object is not a struct!")
return ErrNotAStruct
}
return nil
}
func checkSlice(ref reflect.Value) error {
if ref.Kind() != reflect.Slice {
return fmt.Errorf("Current field is not a slice!")
return ErrNotASlice
}
return nil
}