association.go

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
package rel

import (
    "reflect"
    "sync"

    "github.com/serenize/snaker"
)

// AssociationType defines the type of association in database.
type AssociationType uint8

const (
    // BelongsTo association.
    BelongsTo = iota
    // HasOne association.
    HasOne
    // HasMany association.
    HasMany
)

type associationKey struct {
    rt    reflect.Type
    index int
}

type associationData struct {
    typ             AssociationType
    targetIndex     []int
    referenceColumn string
    referenceIndex  int
    foreignField    string
    foreignIndex    int
}

var associationCache sync.Map

// Association provides abstraction to work with association of document or collection.
type Association struct {
    data associationData
    rv   reflect.Value
}

// Type of association.
func (a Association) Type() AssociationType {
    return a.data.typ
}

// Document returns association target as document.
// If association is zero, second return value will be false.
func (a Association) Document() (*Document, bool) {
    var (
        rv = a.rv.FieldByIndex(a.data.targetIndex)
    )

    switch rv.Kind() {
    case reflect.Ptr:
        if rv.IsNil() {
            rv.Set(reflect.New(rv.Type().Elem()))
            return NewDocument(rv), false
        }

        var (
            doc = NewDocument(rv)
        )

        return doc, doc.Persisted()
    default:
        var (
            doc = NewDocument(rv.Addr())
        )

        return doc, doc.Persisted()
    }
}

// Collection returns association target as collection.
// If association is zero, second return value will be false.
func (a Association) Collection() (*Collection, bool) {
    var (
        rv     = a.rv.FieldByIndex(a.data.targetIndex)
        loaded = !rv.IsNil()
    )

    if rv.Kind() == reflect.Ptr {
        if !loaded {
            rv.Set(reflect.New(rv.Type().Elem()))
            rv.Elem().Set(reflect.MakeSlice(rv.Elem().Type(), 0, 0))
        }

        return NewCollection(rv), loaded
    }

    if !loaded {
        rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
    }

    return NewCollection(rv.Addr()), loaded
}

// IsZero returns true if association is not loaded.
func (a Association) IsZero() bool {
    var (
        rv = a.rv.FieldByIndex(a.data.targetIndex)
    )

    return isDeepZero(reflect.Indirect(rv), 1)
}

// ReferenceField of the association.
func (a Association) ReferenceField() string {
    return a.data.referenceColumn
}

// ReferenceValue of the association.
func (a Association) ReferenceValue() interface{} {
    return indirect(a.rv.Field(a.data.referenceIndex))
}

// ForeignField of the association.
func (a Association) ForeignField() string {
    return a.data.foreignField
}

// ForeignValue of the association.
// It'll panic if association type is has many.
func (a Association) ForeignValue() interface{} {
    if a.Type() == HasMany {
        panic("cannot infer foreign value for has many association")
    }

    var (
        rv = a.rv.FieldByIndex(a.data.targetIndex)
    )

    if rv.Kind() == reflect.Ptr {
        rv = rv.Elem()
    }

    return indirect(rv.Field(a.data.foreignIndex))
}

func newAssociation(rv reflect.Value, index int) Association {
    if rv.Kind() == reflect.Ptr {
        rv = rv.Elem()
    }

    return Association{
        data: extractAssociationData(rv.Type(), index),
        rv:   rv,
    }
}

func extractAssociationData(rt reflect.Type, index int) associationData {
    var (
        key = associationKey{
            rt:    rt,
            index: index,
        }
    )

    if val, cached := associationCache.Load(key); cached {
        return val.(associationData)
    }

    var (
        sf        = rt.Field(index)
        ft        = sf.Type
        ref       = sf.Tag.Get("ref")
        fk        = sf.Tag.Get("fk")
        fName     = fieldName(sf)
        assocData = associationData{
            targetIndex: sf.Index,
        }
    )

    for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice {
        ft = ft.Elem()
    }

    var (
        refDocData = extractDocumentData(rt, true)
        fkDocData  = extractDocumentData(ft, true)
    )

    // Try to guess ref and fk if not defined.
    if ref == "" || fk == "" {
        if _, isBelongsTo := refDocData.index[fName+"_id"]; isBelongsTo {
            ref = fName + "_id"
            fk = "id"
        } else {
            ref = "id"
            fk = snaker.CamelToSnake(rt.Name()) + "_id"
        }
    }

    if id, exist := refDocData.index[ref]; !exist {
        panic("rel: references (" + ref + ") field not found ")
    } else {
        assocData.referenceIndex = id
        assocData.referenceColumn = ref
    }

    if id, exist := fkDocData.index[fk]; !exist {
        panic("rel: foreign_key (" + fk + ") field not found")
    } else {
        assocData.foreignIndex = id
        assocData.foreignField = fk
    }

    // guess assoc type
    if sf.Type.Kind() == reflect.Slice ||
        (sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Slice) {
        assocData.typ = HasMany
    } else {
        if len(assocData.referenceColumn) > len(assocData.foreignField) {
            assocData.typ = BelongsTo
        } else {
            assocData.typ = HasOne
        }
    }

    associationCache.Store(key, assocData)

    return assocData
}