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