mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-06 14:26:22 +00:00
884 lines
23 KiB
Go
884 lines
23 KiB
Go
package reflection
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
|
|
)
|
|
|
|
type PrimaryKeyNameProvider interface {
|
|
GetIDName() string
|
|
}
|
|
|
|
// GetPrimaryKeyName extracts the primary key column name from a model
|
|
// It first checks if the model implements PrimaryKeyNameProvider (GetIDName method)
|
|
// Falls back to reflection to find bun:",pk" tag, then gorm:"primaryKey" tag
|
|
func GetPrimaryKeyName(model any) string {
|
|
if reflect.TypeOf(model) == nil {
|
|
return ""
|
|
}
|
|
// If we are given a string model name, look up the model
|
|
if reflect.TypeOf(model).Kind() == reflect.String {
|
|
name := model.(string)
|
|
m, err := modelregistry.GetModelByName(name)
|
|
if err == nil {
|
|
model = m
|
|
}
|
|
}
|
|
|
|
// Check if model implements PrimaryKeyNameProvider
|
|
if provider, ok := model.(PrimaryKeyNameProvider); ok {
|
|
return provider.GetIDName()
|
|
}
|
|
|
|
// Try Bun tag first
|
|
if pkName := getPrimaryKeyFromReflection(model, "bun"); pkName != "" {
|
|
return pkName
|
|
}
|
|
|
|
// Fall back to GORM tag
|
|
if pkName := getPrimaryKeyFromReflection(model, "gorm"); pkName != "" {
|
|
return pkName
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// GetPrimaryKeyValue extracts the primary key value from a model instance
|
|
// Returns the value of the primary key field
|
|
func GetPrimaryKeyValue(model any) any {
|
|
if model == nil || reflect.TypeOf(model) == nil {
|
|
return nil
|
|
}
|
|
|
|
val := reflect.ValueOf(model)
|
|
if val.Kind() == reflect.Pointer {
|
|
val = val.Elem()
|
|
}
|
|
|
|
if val.Kind() != reflect.Struct {
|
|
return nil
|
|
}
|
|
|
|
// Try Bun tag first
|
|
if pkValue := findPrimaryKeyValue(val, "bun"); pkValue != nil {
|
|
return pkValue
|
|
}
|
|
|
|
// Fall back to GORM tag
|
|
if pkValue := findPrimaryKeyValue(val, "gorm"); pkValue != nil {
|
|
return pkValue
|
|
}
|
|
|
|
// Last resort: look for field named "ID" or "Id"
|
|
if pkValue := findFieldByName(val, "id"); pkValue != nil {
|
|
return pkValue
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findPrimaryKeyValue recursively searches for a primary key field in the struct
|
|
func findPrimaryKeyValue(val reflect.Value, ormType string) any {
|
|
typ := val.Type()
|
|
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
fieldValue := val.Field(i)
|
|
|
|
// Check if this is an embedded struct
|
|
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
|
// Recursively search in embedded struct
|
|
if pkValue := findPrimaryKeyValue(fieldValue, ormType); pkValue != nil {
|
|
return pkValue
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Check for primary key tag
|
|
switch ormType {
|
|
case "bun":
|
|
bunTag := field.Tag.Get("bun")
|
|
if strings.Contains(bunTag, "pk") && fieldValue.CanInterface() {
|
|
return fieldValue.Interface()
|
|
}
|
|
case "gorm":
|
|
gormTag := field.Tag.Get("gorm")
|
|
if strings.Contains(gormTag, "primaryKey") && fieldValue.CanInterface() {
|
|
return fieldValue.Interface()
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// findFieldByName recursively searches for a field by name in the struct
|
|
func findFieldByName(val reflect.Value, name string) any {
|
|
typ := val.Type()
|
|
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
fieldValue := val.Field(i)
|
|
|
|
// Check if this is an embedded struct
|
|
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
|
// Recursively search in embedded struct
|
|
if result := findFieldByName(fieldValue, name); result != nil {
|
|
return result
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Check if field name matches
|
|
if strings.EqualFold(field.Name, name) && fieldValue.CanInterface() {
|
|
return fieldValue.Interface()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetModelColumns extracts all column names from a model using reflection
|
|
// It checks bun tags first, then gorm tags, then json tags, and finally falls back to lowercase field names
|
|
// This function recursively processes embedded structs to include their fields
|
|
func GetModelColumns(model any) []string {
|
|
var columns []string
|
|
|
|
modelType := reflect.TypeOf(model)
|
|
|
|
// Unwrap pointers, slices, and arrays to get to the base struct type
|
|
for modelType != nil && (modelType.Kind() == reflect.Pointer || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
// Validate that we have a struct type
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
return columns
|
|
}
|
|
|
|
collectColumnsFromType(modelType, &columns)
|
|
|
|
return columns
|
|
}
|
|
|
|
// collectColumnsFromType recursively collects column names from a struct type and its embedded fields
|
|
func collectColumnsFromType(typ reflect.Type, columns *[]string) {
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
|
|
// Check if this is an embedded struct
|
|
if field.Anonymous {
|
|
// Unwrap pointer type if necessary
|
|
fieldType := field.Type
|
|
if fieldType.Kind() == reflect.Pointer {
|
|
fieldType = fieldType.Elem()
|
|
}
|
|
|
|
// Recursively process embedded struct
|
|
if fieldType.Kind() == reflect.Struct {
|
|
collectColumnsFromType(fieldType, columns)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Get column name using the same logic as primary key extraction
|
|
columnName := getColumnNameFromField(field)
|
|
|
|
if columnName != "" {
|
|
*columns = append(*columns, columnName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// getColumnNameFromField extracts the column name from a struct field
|
|
// Priority: bun tag -> gorm tag -> json tag -> lowercase field name
|
|
func getColumnNameFromField(field reflect.StructField) string {
|
|
// Try bun tag first
|
|
bunTag := field.Tag.Get("bun")
|
|
if bunTag != "" && bunTag != "-" {
|
|
if colName := ExtractColumnFromBunTag(bunTag); colName != "" {
|
|
return colName
|
|
}
|
|
}
|
|
|
|
// Try gorm tag
|
|
gormTag := field.Tag.Get("gorm")
|
|
if gormTag != "" && gormTag != "-" {
|
|
if colName := ExtractColumnFromGormTag(gormTag); colName != "" {
|
|
return colName
|
|
}
|
|
}
|
|
|
|
// Fall back to json tag
|
|
jsonTag := field.Tag.Get("json")
|
|
if jsonTag != "" && jsonTag != "-" {
|
|
// Extract just the field name before any options
|
|
parts := strings.Split(jsonTag, ",")
|
|
if len(parts) > 0 && parts[0] != "" {
|
|
return parts[0]
|
|
}
|
|
}
|
|
|
|
// Last resort: use field name in lowercase
|
|
return strings.ToLower(field.Name)
|
|
}
|
|
|
|
// getPrimaryKeyFromReflection uses reflection to find the primary key field
|
|
// This function recursively searches embedded structs
|
|
func getPrimaryKeyFromReflection(model any, ormType string) string {
|
|
val := reflect.ValueOf(model)
|
|
if val.Kind() == reflect.Pointer {
|
|
val = val.Elem()
|
|
}
|
|
|
|
if val.Kind() != reflect.Struct {
|
|
return ""
|
|
}
|
|
|
|
typ := val.Type()
|
|
return findPrimaryKeyNameFromType(typ, ormType)
|
|
}
|
|
|
|
// findPrimaryKeyNameFromType recursively searches for the primary key field name in a struct type
|
|
func findPrimaryKeyNameFromType(typ reflect.Type, ormType string) string {
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
|
|
// Check if this is an embedded struct
|
|
if field.Anonymous {
|
|
// Unwrap pointer type if necessary
|
|
fieldType := field.Type
|
|
if fieldType.Kind() == reflect.Pointer {
|
|
fieldType = fieldType.Elem()
|
|
}
|
|
|
|
// Recursively search in embedded struct
|
|
if fieldType.Kind() == reflect.Struct {
|
|
if pkName := findPrimaryKeyNameFromType(fieldType, ormType); pkName != "" {
|
|
return pkName
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch ormType {
|
|
case "gorm":
|
|
// Check for gorm tag with primaryKey
|
|
gormTag := field.Tag.Get("gorm")
|
|
if strings.Contains(gormTag, "primaryKey") {
|
|
// Try to extract column name from gorm tag
|
|
if colName := ExtractColumnFromGormTag(gormTag); colName != "" {
|
|
return colName
|
|
}
|
|
// Fall back to json tag
|
|
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
|
return strings.Split(jsonTag, ",")[0]
|
|
}
|
|
}
|
|
case "bun":
|
|
// Check for bun tag with pk flag
|
|
bunTag := field.Tag.Get("bun")
|
|
if strings.Contains(bunTag, "pk") {
|
|
// Extract column name from bun tag
|
|
if colName := ExtractColumnFromBunTag(bunTag); colName != "" {
|
|
return colName
|
|
}
|
|
// Fall back to json tag
|
|
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
|
|
return strings.Split(jsonTag, ",")[0]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// ExtractColumnFromGormTag extracts the column name from a gorm tag
|
|
// Example: "column:id;primaryKey" -> "id"
|
|
func ExtractColumnFromGormTag(tag string) string {
|
|
parts := strings.Split(tag, ";")
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if colName, found := strings.CutPrefix(part, "column:"); found {
|
|
return colName
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ExtractColumnFromBunTag extracts the column name from a bun tag
|
|
// Example: "id,pk" -> "id"
|
|
// Example: ",pk" -> "" (will fall back to json tag)
|
|
func ExtractColumnFromBunTag(tag string) string {
|
|
parts := strings.Split(tag, ",")
|
|
if strings.HasPrefix(strings.ToLower(tag), "table:") || strings.HasPrefix(strings.ToLower(tag), "rel:") || strings.HasPrefix(strings.ToLower(tag), "join:") {
|
|
return ""
|
|
}
|
|
if len(parts) > 0 && parts[0] != "" {
|
|
return parts[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetSQLModelColumns extracts column names that have valid SQL field mappings
|
|
// This function only returns columns that:
|
|
// 1. Have bun or gorm tags (not just json tags)
|
|
// 2. Are not relations (no rel:, join:, foreignKey, references, many2many tags)
|
|
// 3. Are not scan-only embedded fields
|
|
func GetSQLModelColumns(model any) []string {
|
|
var columns []string
|
|
|
|
modelType := reflect.TypeOf(model)
|
|
|
|
// Unwrap pointers, slices, and arrays to get to the base struct type
|
|
for modelType != nil && (modelType.Kind() == reflect.Pointer || modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array) {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
// Validate that we have a struct type
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
return columns
|
|
}
|
|
|
|
collectSQLColumnsFromType(modelType, &columns, false)
|
|
|
|
return columns
|
|
}
|
|
|
|
// collectSQLColumnsFromType recursively collects SQL column names from a struct type
|
|
// scanOnlyEmbedded indicates if we're inside a scan-only embedded struct
|
|
func collectSQLColumnsFromType(typ reflect.Type, columns *[]string, scanOnlyEmbedded bool) {
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
|
|
// Check if this is an embedded struct
|
|
if field.Anonymous {
|
|
// Unwrap pointer type if necessary
|
|
fieldType := field.Type
|
|
if fieldType.Kind() == reflect.Pointer {
|
|
fieldType = fieldType.Elem()
|
|
}
|
|
|
|
// Check if the embedded struct itself is scan-only
|
|
isScanOnly := scanOnlyEmbedded
|
|
bunTag := field.Tag.Get("bun")
|
|
if bunTag != "" && isBunFieldScanOnly(bunTag) {
|
|
isScanOnly = true
|
|
}
|
|
|
|
// Recursively process embedded struct
|
|
if fieldType.Kind() == reflect.Struct {
|
|
collectSQLColumnsFromType(fieldType, columns, isScanOnly)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Skip fields in scan-only embedded structs
|
|
if scanOnlyEmbedded {
|
|
continue
|
|
}
|
|
|
|
// Get bun and gorm tags
|
|
bunTag := field.Tag.Get("bun")
|
|
gormTag := field.Tag.Get("gorm")
|
|
|
|
// Skip if neither bun nor gorm tag exists
|
|
if bunTag == "" && gormTag == "" {
|
|
continue
|
|
}
|
|
|
|
// Skip if explicitly marked with "-"
|
|
if bunTag == "-" || gormTag == "-" {
|
|
continue
|
|
}
|
|
|
|
// Skip if field itself is scan-only (bun)
|
|
if bunTag != "" && isBunFieldScanOnly(bunTag) {
|
|
continue
|
|
}
|
|
|
|
// Skip if field itself is read-only (gorm)
|
|
if gormTag != "" && isGormFieldReadOnly(gormTag) {
|
|
continue
|
|
}
|
|
|
|
// Skip relation fields (bun)
|
|
if bunTag != "" {
|
|
// Skip if it's a bun relation (rel:, join:, or m2m:)
|
|
if strings.Contains(bunTag, "rel:") ||
|
|
strings.Contains(bunTag, "join:") ||
|
|
strings.Contains(bunTag, "m2m:") {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Skip relation fields (gorm)
|
|
if gormTag != "" {
|
|
// Skip if it has gorm relationship tags
|
|
if strings.Contains(gormTag, "foreignKey:") ||
|
|
strings.Contains(gormTag, "references:") ||
|
|
strings.Contains(gormTag, "many2many:") ||
|
|
strings.Contains(gormTag, "constraint:") {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Get column name
|
|
columnName := ""
|
|
if bunTag != "" {
|
|
columnName = ExtractColumnFromBunTag(bunTag)
|
|
}
|
|
if columnName == "" && gormTag != "" {
|
|
columnName = ExtractColumnFromGormTag(gormTag)
|
|
}
|
|
|
|
// Skip if we couldn't extract a column name
|
|
if columnName == "" {
|
|
continue
|
|
}
|
|
|
|
*columns = append(*columns, columnName)
|
|
}
|
|
}
|
|
|
|
// IsColumnWritable checks if a column can be written to in the database
|
|
// For bun: returns false if the field has "scanonly" tag
|
|
// For gorm: returns false if the field has "<-:false" or "->" (read-only) tag
|
|
// This function recursively searches embedded structs
|
|
func IsColumnWritable(model any, columnName string) bool {
|
|
modelType := reflect.TypeOf(model)
|
|
|
|
// Unwrap pointers to get to the base struct type
|
|
for modelType != nil && modelType.Kind() == reflect.Pointer {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
// Validate that we have a struct type
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
return false
|
|
}
|
|
|
|
found, writable := isColumnWritableInType(modelType, columnName)
|
|
if found {
|
|
return writable
|
|
}
|
|
|
|
// Column not found in model, allow it (might be a dynamic column)
|
|
return true
|
|
}
|
|
|
|
// isColumnWritableInType recursively searches for a column and checks if it's writable
|
|
// Returns (found, writable) where found indicates if the column was found
|
|
func isColumnWritableInType(typ reflect.Type, columnName string) (found bool, writable bool) {
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
|
|
// Check if this is an embedded struct
|
|
if field.Anonymous {
|
|
// Unwrap pointer type if necessary
|
|
fieldType := field.Type
|
|
if fieldType.Kind() == reflect.Pointer {
|
|
fieldType = fieldType.Elem()
|
|
}
|
|
|
|
// Recursively search in embedded struct
|
|
if fieldType.Kind() == reflect.Struct {
|
|
if found, writable := isColumnWritableInType(fieldType, columnName); found {
|
|
return true, writable
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Check if this field matches the column name
|
|
fieldColumnName := getColumnNameFromField(field)
|
|
if fieldColumnName != columnName {
|
|
continue
|
|
}
|
|
|
|
// Found the field, now check if it's writable
|
|
// Check bun tag for scanonly
|
|
bunTag := field.Tag.Get("bun")
|
|
if bunTag != "" {
|
|
if isBunFieldScanOnly(bunTag) {
|
|
return true, false
|
|
}
|
|
}
|
|
|
|
// Check gorm tag for write restrictions
|
|
gormTag := field.Tag.Get("gorm")
|
|
if gormTag != "" {
|
|
if isGormFieldReadOnly(gormTag) {
|
|
return true, false
|
|
}
|
|
}
|
|
|
|
// Column is writable
|
|
return true, true
|
|
}
|
|
|
|
// Column not found
|
|
return false, false
|
|
}
|
|
|
|
// isBunFieldScanOnly checks if a bun tag indicates the field is scan-only
|
|
// Example: "column_name,scanonly" -> true
|
|
func isBunFieldScanOnly(tag string) bool {
|
|
parts := strings.Split(tag, ",")
|
|
for _, part := range parts {
|
|
if strings.TrimSpace(part) == "scanonly" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isGormFieldReadOnly checks if a gorm tag indicates the field is read-only
|
|
// Examples:
|
|
// - "<-:false" -> true (no writes allowed)
|
|
// - "->" -> true (read-only, common pattern)
|
|
// - "column:name;->" -> true
|
|
// - "<-:create" -> false (writes allowed on create)
|
|
func isGormFieldReadOnly(tag string) bool {
|
|
parts := strings.Split(tag, ";")
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
|
|
// Check for read-only marker
|
|
if part == "->" {
|
|
return true
|
|
}
|
|
|
|
// Check for write restrictions
|
|
if value, found := strings.CutPrefix(part, "<-:"); found {
|
|
if value == "false" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ExtractSourceColumn extracts the base column name from PostgreSQL JSON operators
|
|
// Examples:
|
|
// - "columna->>'val'" returns "columna"
|
|
// - "columna->'key'" returns "columna"
|
|
// - "columna" returns "columna"
|
|
// - "table.columna->>'val'" returns "table.columna"
|
|
func ExtractSourceColumn(colName string) string {
|
|
// Check for PostgreSQL JSON operators: -> and ->>
|
|
if idx := strings.Index(colName, "->>"); idx != -1 {
|
|
return strings.TrimSpace(colName[:idx])
|
|
}
|
|
if idx := strings.Index(colName, "->"); idx != -1 {
|
|
return strings.TrimSpace(colName[:idx])
|
|
}
|
|
return colName
|
|
}
|
|
|
|
// ToSnakeCase converts a string from CamelCase to snake_case
|
|
func ToSnakeCase(s string) string {
|
|
var result strings.Builder
|
|
for i, r := range s {
|
|
if i > 0 && r >= 'A' && r <= 'Z' {
|
|
result.WriteRune('_')
|
|
}
|
|
result.WriteRune(r)
|
|
}
|
|
return strings.ToLower(result.String())
|
|
}
|
|
|
|
// GetColumnTypeFromModel uses reflection to determine the Go type of a column in a model
|
|
func GetColumnTypeFromModel(model interface{}, colName string) reflect.Kind {
|
|
if model == nil {
|
|
return reflect.Invalid
|
|
}
|
|
|
|
// Extract the source column name (remove JSON operators like ->> or ->)
|
|
sourceColName := ExtractSourceColumn(colName)
|
|
|
|
modelType := reflect.TypeOf(model)
|
|
// Dereference pointer if needed
|
|
if modelType.Kind() == reflect.Ptr {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
// Ensure it's a struct
|
|
if modelType.Kind() != reflect.Struct {
|
|
return reflect.Invalid
|
|
}
|
|
|
|
// Find the field by JSON tag or field name
|
|
for i := 0; i < modelType.NumField(); i++ {
|
|
field := modelType.Field(i)
|
|
|
|
// Check JSON tag
|
|
jsonTag := field.Tag.Get("json")
|
|
if jsonTag != "" {
|
|
// Parse JSON tag (format: "name,omitempty")
|
|
parts := strings.Split(jsonTag, ",")
|
|
if parts[0] == sourceColName {
|
|
return field.Type.Kind()
|
|
}
|
|
}
|
|
|
|
// Check field name (case-insensitive)
|
|
if strings.EqualFold(field.Name, sourceColName) {
|
|
return field.Type.Kind()
|
|
}
|
|
|
|
// Check snake_case conversion
|
|
snakeCaseName := ToSnakeCase(field.Name)
|
|
if snakeCaseName == sourceColName {
|
|
return field.Type.Kind()
|
|
}
|
|
}
|
|
|
|
return reflect.Invalid
|
|
}
|
|
|
|
// IsNumericType checks if a reflect.Kind is a numeric type
|
|
func IsNumericType(kind reflect.Kind) bool {
|
|
return kind == reflect.Int || kind == reflect.Int8 || kind == reflect.Int16 ||
|
|
kind == reflect.Int32 || kind == reflect.Int64 || kind == reflect.Uint ||
|
|
kind == reflect.Uint8 || kind == reflect.Uint16 || kind == reflect.Uint32 ||
|
|
kind == reflect.Uint64 || kind == reflect.Float32 || kind == reflect.Float64
|
|
}
|
|
|
|
// IsStringType checks if a reflect.Kind is a string type
|
|
func IsStringType(kind reflect.Kind) bool {
|
|
return kind == reflect.String
|
|
}
|
|
|
|
// IsNumericValue checks if a string value can be parsed as a number
|
|
func IsNumericValue(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
_, err := strconv.ParseFloat(value, 64)
|
|
return err == nil
|
|
}
|
|
|
|
// ConvertToNumericType converts a string value to the appropriate numeric type
|
|
func ConvertToNumericType(value string, kind reflect.Kind) (interface{}, error) {
|
|
value = strings.TrimSpace(value)
|
|
|
|
switch kind {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
// Parse as integer
|
|
bitSize := 64
|
|
switch kind {
|
|
case reflect.Int8:
|
|
bitSize = 8
|
|
case reflect.Int16:
|
|
bitSize = 16
|
|
case reflect.Int32:
|
|
bitSize = 32
|
|
}
|
|
|
|
intVal, err := strconv.ParseInt(value, 10, bitSize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid integer value: %w", err)
|
|
}
|
|
|
|
// Return the appropriate type
|
|
switch kind {
|
|
case reflect.Int:
|
|
return int(intVal), nil
|
|
case reflect.Int8:
|
|
return int8(intVal), nil
|
|
case reflect.Int16:
|
|
return int16(intVal), nil
|
|
case reflect.Int32:
|
|
return int32(intVal), nil
|
|
case reflect.Int64:
|
|
return intVal, nil
|
|
}
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
// Parse as unsigned integer
|
|
bitSize := 64
|
|
switch kind {
|
|
case reflect.Uint8:
|
|
bitSize = 8
|
|
case reflect.Uint16:
|
|
bitSize = 16
|
|
case reflect.Uint32:
|
|
bitSize = 32
|
|
}
|
|
|
|
uintVal, err := strconv.ParseUint(value, 10, bitSize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid unsigned integer value: %w", err)
|
|
}
|
|
|
|
// Return the appropriate type
|
|
switch kind {
|
|
case reflect.Uint:
|
|
return uint(uintVal), nil
|
|
case reflect.Uint8:
|
|
return uint8(uintVal), nil
|
|
case reflect.Uint16:
|
|
return uint16(uintVal), nil
|
|
case reflect.Uint32:
|
|
return uint32(uintVal), nil
|
|
case reflect.Uint64:
|
|
return uintVal, nil
|
|
}
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
// Parse as float
|
|
bitSize := 64
|
|
if kind == reflect.Float32 {
|
|
bitSize = 32
|
|
}
|
|
|
|
floatVal, err := strconv.ParseFloat(value, bitSize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid float value: %w", err)
|
|
}
|
|
|
|
if kind == reflect.Float32 {
|
|
return float32(floatVal), nil
|
|
}
|
|
return floatVal, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsupported numeric type: %v", kind)
|
|
}
|
|
|
|
// GetRelationModel gets the model type for a relation field
|
|
// It searches for the field by name in the following order (case-insensitive):
|
|
// 1. Actual field name
|
|
// 2. Bun tag name (if exists)
|
|
// 3. Gorm tag name (if exists)
|
|
// 4. JSON tag name (if exists)
|
|
//
|
|
// Supports recursive field paths using dot notation (e.g., "MAL.MAL.DEF")
|
|
// For nested fields, it traverses through each level of the struct hierarchy
|
|
func GetRelationModel(model interface{}, fieldName string) interface{} {
|
|
if model == nil || fieldName == "" {
|
|
return nil
|
|
}
|
|
|
|
// Split the field name by "." to handle nested/recursive relations
|
|
fieldParts := strings.Split(fieldName, ".")
|
|
|
|
// Start with the current model
|
|
currentModel := model
|
|
|
|
// Traverse through each level of the field path
|
|
for _, part := range fieldParts {
|
|
if part == "" {
|
|
continue
|
|
}
|
|
|
|
currentModel = getRelationModelSingleLevel(currentModel, part)
|
|
if currentModel == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return currentModel
|
|
}
|
|
|
|
// getRelationModelSingleLevel gets the model type for a single level field (non-recursive)
|
|
// This is a helper function used by GetRelationModel to handle one level at a time
|
|
func getRelationModelSingleLevel(model interface{}, fieldName string) interface{} {
|
|
if model == nil || fieldName == "" {
|
|
return nil
|
|
}
|
|
|
|
modelType := reflect.TypeOf(model)
|
|
if modelType == nil {
|
|
return nil
|
|
}
|
|
|
|
if modelType.Kind() == reflect.Ptr {
|
|
modelType = modelType.Elem()
|
|
}
|
|
|
|
if modelType == nil || modelType.Kind() != reflect.Struct {
|
|
return nil
|
|
}
|
|
|
|
// Find the field by checking in priority order (case-insensitive)
|
|
var field *reflect.StructField
|
|
normalizedFieldName := strings.ToLower(fieldName)
|
|
|
|
for i := 0; i < modelType.NumField(); i++ {
|
|
f := modelType.Field(i)
|
|
|
|
// 1. Check actual field name (case-insensitive)
|
|
if strings.EqualFold(f.Name, fieldName) {
|
|
field = &f
|
|
break
|
|
}
|
|
|
|
// 2. Check bun tag name
|
|
bunTag := f.Tag.Get("bun")
|
|
if bunTag != "" {
|
|
bunColName := ExtractColumnFromBunTag(bunTag)
|
|
if bunColName != "" && strings.EqualFold(bunColName, normalizedFieldName) {
|
|
field = &f
|
|
break
|
|
}
|
|
}
|
|
|
|
// 3. Check gorm tag name
|
|
gormTag := f.Tag.Get("gorm")
|
|
if gormTag != "" {
|
|
gormColName := ExtractColumnFromGormTag(gormTag)
|
|
if gormColName != "" && strings.EqualFold(gormColName, normalizedFieldName) {
|
|
field = &f
|
|
break
|
|
}
|
|
}
|
|
|
|
// 4. Check JSON tag name
|
|
jsonTag := f.Tag.Get("json")
|
|
if jsonTag != "" {
|
|
parts := strings.Split(jsonTag, ",")
|
|
if len(parts) > 0 && parts[0] != "" && parts[0] != "-" {
|
|
if strings.EqualFold(parts[0], normalizedFieldName) {
|
|
field = &f
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if field == nil {
|
|
return nil
|
|
}
|
|
|
|
// Get the target type
|
|
targetType := field.Type
|
|
if targetType == nil {
|
|
return nil
|
|
}
|
|
|
|
if targetType.Kind() == reflect.Slice {
|
|
targetType = targetType.Elem()
|
|
if targetType == nil {
|
|
return nil
|
|
}
|
|
}
|
|
if targetType.Kind() == reflect.Ptr {
|
|
targetType = targetType.Elem()
|
|
if targetType == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if targetType.Kind() != reflect.Struct {
|
|
return nil
|
|
}
|
|
|
|
// Create a zero value of the target type
|
|
return reflect.New(targetType).Elem().Interface()
|
|
}
|