99 lines
2.8 KiB
Go
99 lines
2.8 KiB
Go
package cabana
|
|
|
|
import (
|
|
"database/sql"
|
|
"database/sql/driver"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"gorm.io/gorm/schema"
|
|
)
|
|
|
|
// modelField is an exported struct field of a model together with its index
|
|
// path from the model type, so a field promoted from an embedded struct
|
|
// (gorm.Model, a shared timestamps struct) is reachable like a top-level one.
|
|
type modelField struct {
|
|
Field reflect.StructField
|
|
Path []int
|
|
}
|
|
|
|
var modelFieldCache sync.Map // reflect.Type -> []modelField
|
|
|
|
// modelFields lists the exported fields of the struct type behind t in
|
|
// declaration order, with anonymous (embedded) structs flattened in place.
|
|
// Embedded scalar-like structs (time.Time, sql.Null*, anything that is a
|
|
// Scanner or Valuer) stay single fields. It returns nil for a non-struct.
|
|
func modelFields(t reflect.Type) []modelField {
|
|
for t != nil && t.Kind() == reflect.Pointer {
|
|
t = t.Elem()
|
|
}
|
|
if t == nil || t.Kind() != reflect.Struct {
|
|
return nil
|
|
}
|
|
if cached, ok := modelFieldCache.Load(t); ok {
|
|
return cached.([]modelField)
|
|
}
|
|
fields := flattenModelFields(t, nil, 0)
|
|
modelFieldCache.Store(t, fields)
|
|
return fields
|
|
}
|
|
|
|
func flattenModelFields(t reflect.Type, prefix []int, depth int) []modelField {
|
|
var out []modelField
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := t.Field(i)
|
|
path := append(append([]int(nil), prefix...), i)
|
|
if f.Anonymous && depth < 5 {
|
|
if inner := embeddedStructType(f.Type); inner != nil {
|
|
out = append(out, flattenModelFields(inner, path, depth+1)...)
|
|
continue
|
|
}
|
|
}
|
|
if f.PkgPath != "" {
|
|
continue
|
|
}
|
|
out = append(out, modelField{Field: f, Path: path})
|
|
}
|
|
return out
|
|
}
|
|
|
|
var (
|
|
scannerType = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
|
|
valuerType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
|
|
timeType = reflect.TypeOf(time.Time{})
|
|
)
|
|
|
|
// embeddedStructType returns the struct type to flatten for an embedded field,
|
|
// or nil when the field is a single value.
|
|
func embeddedStructType(t reflect.Type) reflect.Type {
|
|
if t.Kind() == reflect.Pointer {
|
|
t = t.Elem()
|
|
}
|
|
if t.Kind() != reflect.Struct || t == timeType {
|
|
return nil
|
|
}
|
|
ptr := reflect.PointerTo(t)
|
|
if ptr.Implements(scannerType) || t.Implements(valuerType) || ptr.Implements(valuerType) {
|
|
return nil
|
|
}
|
|
return t
|
|
}
|
|
|
|
// hasPrimaryKeyTag reports whether a field's gorm tag declares it the primary
|
|
// key. GORM reads the key case-insensitively (gorm.Model writes "primarykey").
|
|
func hasPrimaryKeyTag(field reflect.StructField) bool {
|
|
for _, part := range strings.Split(field.Tag.Get("gorm"), ";") {
|
|
if strings.EqualFold(strings.TrimSpace(part), "primarykey") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// defaultColumnName is GORM's default column for an untagged field.
|
|
func defaultColumnName(field reflect.StructField) string {
|
|
return schema.NamingStrategy{}.ColumnName("", field.Name)
|
|
}
|