Files
summercms/modules/cabana/model_fields.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)
}