From 6a0297713a72e6c9296688fd3d8fd7541f3ebab2 Mon Sep 17 00:00:00 2001 From: Hein Date: Wed, 7 Jan 2026 10:23:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(reflection):=20=E2=9C=A8=20enhance=20ToSna?= =?UTF-8?q?keCase=20and=20add=20convertSlice=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve ToSnakeCase to handle consecutive uppercase letters. * Introduce convertSlice for element-wise conversions between slices. * Update setFieldValue to support new slice conversion logic. --- pkg/reflection/model_utils.go | 111 ++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/pkg/reflection/model_utils.go b/pkg/reflection/model_utils.go index 06c0fe9..60a3208 100644 --- a/pkg/reflection/model_utils.go +++ b/pkg/reflection/model_utils.go @@ -584,11 +584,23 @@ func ExtractSourceColumn(colName string) string { } // ToSnakeCase converts a string from CamelCase to snake_case +// Handles consecutive uppercase letters (acronyms) correctly: +// "HTTPServer" -> "http_server", "UserID" -> "user_id", "MyHTTPServer" -> "my_http_server" func ToSnakeCase(s string) string { var result strings.Builder - for i, r := range s { + runes := []rune(s) + + for i, r := range runes { if i > 0 && r >= 'A' && r <= 'Z' { - result.WriteRune('_') + // Add underscore if: + // 1. Previous character is lowercase, OR + // 2. Next character is lowercase (transition from acronym to word) + prevIsLower := runes[i-1] >= 'a' && runes[i-1] <= 'z' + nextIsLower := i+1 < len(runes) && runes[i+1] >= 'a' && runes[i+1] <= 'z' + + if prevIsLower || nextIsLower { + result.WriteRune('_') + } } result.WriteRune(r) } @@ -961,7 +973,7 @@ func MapToStruct(dataMap map[string]interface{}, target interface{}) error { // 4. Field name variations columnNames = append(columnNames, field.Name) columnNames = append(columnNames, strings.ToLower(field.Name)) - columnNames = append(columnNames, ToSnakeCase(field.Name)) + //columnNames = append(columnNames, ToSnakeCase(field.Name)) // Map all column name variations to this field index for _, colName := range columnNames { @@ -1067,7 +1079,7 @@ func setFieldValue(field reflect.Value, value interface{}) error { case string: field.SetBytes([]byte(v)) return nil - case map[string]interface{}, []interface{}: + case map[string]interface{}, []interface{}, []*any, map[string]*any: // Marshal complex types to JSON for SqlJSONB fields jsonBytes, err := json.Marshal(v) if err != nil { @@ -1077,6 +1089,11 @@ func setFieldValue(field reflect.Value, value interface{}) error { return nil } } + + // Handle slice-to-slice conversions (e.g., []interface{} to []*SomeModel) + if valueReflect.Kind() == reflect.Slice { + return convertSlice(field, valueReflect) + } } // Handle struct types (like SqlTimeStamp, SqlDate, SqlTime which wrap SqlNull[time.Time]) @@ -1156,6 +1173,92 @@ func setFieldValue(field reflect.Value, value interface{}) error { return fmt.Errorf("cannot convert %v to %v", valueReflect.Type(), field.Type()) } +// convertSlice converts a source slice to a target slice type, handling element-wise conversions +// Supports converting []interface{} to slices of structs or pointers to structs +func convertSlice(targetSlice reflect.Value, sourceSlice reflect.Value) error { + if sourceSlice.Kind() != reflect.Slice || targetSlice.Kind() != reflect.Slice { + return fmt.Errorf("both source and target must be slices") + } + + // Get the element type of the target slice + targetElemType := targetSlice.Type().Elem() + sourceLen := sourceSlice.Len() + + // Create a new slice with the same length as the source + newSlice := reflect.MakeSlice(targetSlice.Type(), sourceLen, sourceLen) + + // Convert each element + for i := 0; i < sourceLen; i++ { + sourceElem := sourceSlice.Index(i) + targetElem := newSlice.Index(i) + + // Get the actual value from the source element + var sourceValue interface{} + if sourceElem.CanInterface() { + sourceValue = sourceElem.Interface() + } else { + continue + } + + // Handle nil elements + if sourceValue == nil { + // For pointer types, nil is valid + if targetElemType.Kind() == reflect.Ptr { + targetElem.Set(reflect.Zero(targetElemType)) + } + continue + } + + // If target element type is a pointer to struct, we need to create new instances + if targetElemType.Kind() == reflect.Ptr { + // Create a new instance of the pointed-to type + newElemPtr := reflect.New(targetElemType.Elem()) + + // Convert the source value to the struct + switch sv := sourceValue.(type) { + case map[string]interface{}: + // Source is a map, use MapToStruct to populate the new instance + if err := MapToStruct(sv, newElemPtr.Interface()); err != nil { + return fmt.Errorf("failed to convert element %d: %w", i, err) + } + default: + // Try direct conversion or setFieldValue + if err := setFieldValue(newElemPtr.Elem(), sourceValue); err != nil { + return fmt.Errorf("failed to convert element %d: %w", i, err) + } + } + + targetElem.Set(newElemPtr) + } else if targetElemType.Kind() == reflect.Struct { + // Target element is a struct (not a pointer) + switch sv := sourceValue.(type) { + case map[string]interface{}: + // Use MapToStruct to populate the element + elemPtr := targetElem.Addr() + if elemPtr.CanInterface() { + if err := MapToStruct(sv, elemPtr.Interface()); err != nil { + return fmt.Errorf("failed to convert element %d: %w", i, err) + } + } + default: + // Try direct conversion + if err := setFieldValue(targetElem, sourceValue); err != nil { + return fmt.Errorf("failed to convert element %d: %w", i, err) + } + } + } else { + // For other types, use setFieldValue + if err := setFieldValue(targetElem, sourceValue); err != nil { + return fmt.Errorf("failed to convert element %d: %w", i, err) + } + } + } + + // Set the converted slice to the target field + targetSlice.Set(newSlice) + return nil +} + // convertToInt64 attempts to convert various types to int64 func convertToInt64(value interface{}) (int64, bool) { switch v := value.(type) {