From 5dd8f6c061519af3c85e6f74fe7c566c8837ba95 Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Thu, 20 Feb 2025 12:33:01 -0500 Subject: [PATCH] :ambulance: :umbrella: Fix more tests --- pkg/cmds/helpers/test-helpers.go | 19 +- pkg/cmds/parameters/gather-arguments_test.go | 40 +- pkg/cmds/parameters/parameters.go | 9 +- .../parameters_from_defaults_test.go | 436 ++++++++++++++++++ pkg/cmds/parameters/parse.go | 5 +- pkg/cmds/parameters/parse_test.go | 86 +++- pkg/cmds/parameters/parsed-parameter.go | 3 +- pkg/helpers/cast/normalize.go | 82 ++++ 8 files changed, 630 insertions(+), 50 deletions(-) create mode 100644 pkg/cmds/parameters/parameters_from_defaults_test.go create mode 100644 pkg/helpers/cast/normalize.go diff --git a/pkg/cmds/helpers/test-helpers.go b/pkg/cmds/helpers/test-helpers.go index 85104846..c7aebf67 100644 --- a/pkg/cmds/helpers/test-helpers.go +++ b/pkg/cmds/helpers/test-helpers.go @@ -294,18 +294,13 @@ func NewTestParsedLayers(pls *layers.ParameterLayers, ls ...TestParsedLayer) *la // compareValues handles comparison of values with special cases for slice types func compareValues(t *testing.T, expected, actual interface{}, key string) { - // Check if we're dealing with a string slice and interface slice - if actualStrSlice, ok := actual.([]string); ok { - if expectedInterfaceSlice, ok := expected.([]interface{}); ok { - // Try to convert the expected interface slice to string slice - expectedStrSlice, err := cast.CastListToStringList(expectedInterfaceSlice) - require.NoError(t, err) - assert.Equal(t, expectedStrSlice, actualStrSlice, "mismatch for key %s", key) - return - } - } - // Default comparison for other types - assert.Equal(t, expected, actual, "mismatch for key %s", key) + normalizedExpected, err := cast.NormalizeValue(expected) + require.NoError(t, err, "failed to normalize expected value for key %s", key) + + normalizedActual, err := cast.NormalizeValue(actual) + require.NoError(t, err, "failed to normalize actual value for key %s", key) + + assert.Equal(t, normalizedExpected, normalizedActual, "mismatch for key %s", key) } func TestExpectedOutputs(t *testing.T, expectedLayers []TestExpectedLayer, parsedLayers *layers.ParsedLayers) { diff --git a/pkg/cmds/parameters/gather-arguments_test.go b/pkg/cmds/parameters/gather-arguments_test.go index b27b483b..ac0761c7 100644 --- a/pkg/cmds/parameters/gather-arguments_test.go +++ b/pkg/cmds/parameters/gather-arguments_test.go @@ -1,11 +1,12 @@ package parameters import ( + "testing" + "github.com/go-go-golems/glazed/pkg/helpers/cast" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) // Test no arguments are passed to the function @@ -378,12 +379,12 @@ func TestObjectListFromFileParsing(t *testing.T) { assert.NoError(t, err) v1, present := result.Get("arg1") assert.True(t, present) - assert.Equal(t, []interface{}{ - map[string]interface{}{ + assert.Equal(t, []map[string]interface{}{ + { "name": "objectList1", "type": "object", }, - map[string]interface{}{ + { "name": "objectList2", "type": "object", }, @@ -406,12 +407,31 @@ func TestObjectListFromFilesParsing(t *testing.T) { v1, present := result.Get("arg1") assert.True(t, present) assert.Equal(t, - []interface{}{map[string]interface{}{"name": "objectList1", "type": "object"}, - map[string]interface{}{"name": "objectList2", "type": "object"}, - map[string]interface{}{"name": "objectList3", "type": "object"}, - map[string]interface{}{"name": "objectList4", "type": "object"}, - map[string]interface{}{"name": "objectList5", "type": "object"}, - map[string]interface{}{"name": "objectList6", "type": "object"}, + []map[string]interface{}{ + { + "name": "objectList1", + "type": "object", + }, + { + "name": "objectList2", + "type": "object", + }, + { + "name": "objectList3", + "type": "object", + }, + { + "name": "objectList4", + "type": "object", + }, + { + "name": "objectList5", + "type": "object", + }, + { + "name": "objectList6", + "type": "object", + }, }, v1.Value) } diff --git a/pkg/cmds/parameters/parameters.go b/pkg/cmds/parameters/parameters.go index f7c958f2..eaabdae1 100644 --- a/pkg/cmds/parameters/parameters.go +++ b/pkg/cmds/parameters/parameters.go @@ -280,7 +280,10 @@ func (p *ParameterDefinition) SetValueFromInterface(value reflect.Value, v inter func (pds *ParameterDefinitions) ParsedParametersFromDefaults() (*ParsedParameters, error) { ret := NewParsedParameters() err := pds.ForEachE(func(definition *ParameterDefinition) error { - err := ret.UpdateValue(definition.Name, definition, definition.Default, + if definition.Default == nil { + return nil + } + err := ret.UpdateValue(definition.Name, definition, *definition.Default, WithParseStepSource("defaults"), WithParseStepValue(definition.Default), ) @@ -442,9 +445,9 @@ func (p *ParameterDefinition) CheckValueValidity(v interface{}) (interface{}, er case ParameterTypeObjectListFromFile: fallthrough case ParameterTypeObjectListFromFiles: - l, ok := v.([]interface{}) + l, ok := cast.CastList2[map[string]interface{}, interface{}](v) if !ok { - return nil, errors.Errorf("Value for parameter %s is not a list of objects: %v", p.Name, v) + return nil, errors.Errorf("Value for parameter %s (type %T) is not a list of objects: %v", p.Name, v, v) } return l, nil diff --git a/pkg/cmds/parameters/parameters_from_defaults_test.go b/pkg/cmds/parameters/parameters_from_defaults_test.go new file mode 100644 index 00000000..20da9d45 --- /dev/null +++ b/pkg/cmds/parameters/parameters_from_defaults_test.go @@ -0,0 +1,436 @@ +package parameters + +import ( + "testing" + "time" + + "github.com/go-go-golems/glazed/pkg/helpers/cast" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParsedParametersFromDefaults_BasicTypes(t *testing.T) { + tests := []struct { + name string + parameterDefinitions *ParameterDefinitions + expectedValues map[string]interface{} + expectedError string + }{ + { + name: "basic types with defaults", + parameterDefinitions: NewParameterDefinitions( + WithParameterDefinitionList([]*ParameterDefinition{ + { + Name: "string-param", + Type: ParameterTypeString, + Default: cast.InterfaceAddr("default-string"), + }, + { + Name: "int-param", + Type: ParameterTypeInteger, + Default: cast.InterfaceAddr(42), + }, + { + Name: "bool-param", + Type: ParameterTypeBool, + Default: cast.InterfaceAddr(true), + }, + { + Name: "choice-param", + Type: ParameterTypeChoice, + Default: cast.InterfaceAddr("choice1"), + Choices: []string{"choice1", "choice2"}, + }, + { + Name: "date-param", + Type: ParameterTypeDate, + Default: cast.InterfaceAddr("2024-01-01"), + }, + }), + ), + expectedValues: map[string]interface{}{ + "string-param": "default-string", + "int-param": 42, + "bool-param": true, + "choice-param": "choice1", + // date will be checked separately due to time.Time comparison + }, + }, + { + name: "mixed defaults and no defaults", + parameterDefinitions: NewParameterDefinitions( + WithParameterDefinitionList([]*ParameterDefinition{ + { + Name: "with-default", + Type: ParameterTypeString, + Default: cast.InterfaceAddr("has-default"), + }, + { + Name: "no-default", + Type: ParameterTypeString, + Default: nil, + }, + }), + ), + expectedValues: map[string]interface{}{ + "with-default": "has-default", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tt.parameterDefinitions.ParsedParametersFromDefaults() + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + require.NoError(t, err) + require.NotNil(t, result) + + // Check each expected value + for paramName, expectedValue := range tt.expectedValues { + param, ok := result.Get(paramName) + assert.True(t, ok, "parameter %s should exist", paramName) + assert.Equal(t, expectedValue, param.Value, "parameter %s should have correct value", paramName) + } + + // Special check for date parameter in the first test case + if tt.name == "basic types with defaults" { + dateParam, ok := result.Get("date-param") + assert.True(t, ok, "date parameter should exist") + dateTime, ok := dateParam.Value.(time.Time) + assert.True(t, ok, "date parameter should be time.Time") + expectedDate, _ := time.ParseInLocation("2006-01-02", "2024-01-01", time.Local) + assert.Equal(t, expectedDate, dateTime) + } + }) + } +} + +func TestParsedParametersFromDefaults_EdgeCases(t *testing.T) { + tests := []struct { + name string + parameterDefinitions *ParameterDefinitions + expectedValues map[string]interface{} + expectedError string + }{ + { + name: "empty parameter definitions", + parameterDefinitions: NewParameterDefinitions(), + expectedValues: map[string]interface{}{}, + }, + { + name: "all nil defaults", + parameterDefinitions: NewParameterDefinitions( + WithParameterDefinitionList([]*ParameterDefinition{ + { + Name: "param1", + Type: ParameterTypeString, + Default: nil, + }, + { + Name: "param2", + Type: ParameterTypeInteger, + Default: nil, + }, + }), + ), + expectedValues: map[string]interface{}{}, + }, + { + name: "zero values as defaults", + parameterDefinitions: NewParameterDefinitions( + WithParameterDefinitionList([]*ParameterDefinition{ + { + Name: "empty-string", + Type: ParameterTypeString, + Default: cast.InterfaceAddr(""), + }, + { + Name: "zero-int", + Type: ParameterTypeInteger, + Default: cast.InterfaceAddr(0), + }, + { + Name: "false-bool", + Type: ParameterTypeBool, + Default: cast.InterfaceAddr(false), + }, + }), + ), + expectedValues: map[string]interface{}{ + "empty-string": "", + "zero-int": 0, + "false-bool": false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tt.parameterDefinitions.ParsedParametersFromDefaults() + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + require.NoError(t, err) + require.NotNil(t, result) + + // Check that we have exactly the expected number of parameters + assert.Equal(t, len(tt.expectedValues), result.Len(), + "number of parameters should match expected") + + // Check each expected value + for paramName, expectedValue := range tt.expectedValues { + param, ok := result.Get(paramName) + assert.True(t, ok, "parameter %s should exist", paramName) + assert.Equal(t, expectedValue, param.Value, "parameter %s should have correct value", paramName) + } + }) + } +} + +func TestParsedParametersFromDefaults_ListTypes(t *testing.T) { + pd := NewParameterDefinitions( + WithParameterDefinitionList([]*ParameterDefinition{ + { + Name: "string-list", + Type: ParameterTypeStringList, + Default: cast.InterfaceAddr([]string{"one", "two", "three"}), + }, + { + Name: "integer-list", + Type: ParameterTypeIntegerList, + Default: cast.InterfaceAddr([]int{1, 2, 3}), + }, + { + Name: "choice-list", + Type: ParameterTypeChoiceList, + Default: cast.InterfaceAddr([]string{"choice1", "choice2"}), + Choices: []string{"choice1", "choice2", "choice3"}, + }, + { + Name: "float-list", + Type: ParameterTypeFloatList, + Default: cast.InterfaceAddr([]float64{1.1, 2.2, 3.3}), + }, + }), + ) + + result, err := pd.ParsedParametersFromDefaults() + require.NoError(t, err) + require.NotNil(t, result) + + // Check string list + param, ok := result.Get("string-list") + assert.True(t, ok, "string-list parameter should exist") + assert.Equal(t, []string{"one", "two", "three"}, param.Value) + + // Check integer list + param, ok = result.Get("integer-list") + assert.True(t, ok, "integer-list parameter should exist") + assert.Equal(t, []int{1, 2, 3}, param.Value) + + // Check choice list + param, ok = result.Get("choice-list") + assert.True(t, ok, "choice-list parameter should exist") + assert.Equal(t, []string{"choice1", "choice2"}, param.Value) + + // Check float list + param, ok = result.Get("float-list") + assert.True(t, ok, "float-list parameter should exist") + assert.Equal(t, []float64{1.1, 2.2, 3.3}, param.Value) +} + +func TestParsedParametersFromDefaults_MapTypes(t *testing.T) { + pd := NewParameterDefinitions( + WithParameterDefinitionList([]*ParameterDefinition{ + { + Name: "key-value", + Type: ParameterTypeKeyValue, + Default: cast.InterfaceAddr(map[string]string{"key1": "value1", "key2": "value2"}), + }, + { + Name: "object-from-file", + Type: ParameterTypeObjectFromFile, + Default: cast.InterfaceAddr(map[string]interface{}{"name": "test", "value": 42}), + }, + }), + ) + + result, err := pd.ParsedParametersFromDefaults() + require.NoError(t, err) + require.NotNil(t, result) + + // Check key-value map + param, ok := result.Get("key-value") + assert.True(t, ok, "key-value parameter should exist") + assert.Equal(t, map[string]string{"key1": "value1", "key2": "value2"}, param.Value) + + // Check object map + param, ok = result.Get("object-from-file") + assert.True(t, ok, "object-from-file parameter should exist") + assert.Equal(t, map[string]interface{}{"name": "test", "value": 42}, param.Value) +} + +func TestParsedParametersFromDefaults_FileLoadingTypes(t *testing.T) { + pd := NewParameterDefinitions( + WithParameterDefinitionList([]*ParameterDefinition{ + { + Name: "string-list-from-file", + Type: ParameterTypeStringListFromFile, + Default: cast.InterfaceAddr([]string{"file1", "file2"}), + }, + { + Name: "object-list-from-file", + Type: ParameterTypeObjectListFromFile, + Default: cast.InterfaceAddr([]map[string]interface{}{ + {"name": "obj1", "value": 1}, + {"name": "obj2", "value": 2}, + }), + }, + }), + ) + + result, err := pd.ParsedParametersFromDefaults() + require.NoError(t, err) + require.NotNil(t, result) + + // Check string list from file + param, ok := result.Get("string-list-from-file") + assert.True(t, ok, "string-list-from-file parameter should exist") + assert.Equal(t, []string{"file1", "file2"}, param.Value) + + // Check object list from file + param, ok = result.Get("object-list-from-file") + assert.True(t, ok, "object-list-from-file parameter should exist") + assert.Equal(t, []map[string]interface{}{ + {"name": "obj1", "value": 1}, + {"name": "obj2", "value": 2}, + }, param.Value) +} + +func TestParsedParametersFromDefaults_EmptyCollections(t *testing.T) { + pd := NewParameterDefinitions( + WithParameterDefinitionList([]*ParameterDefinition{ + { + Name: "empty-string-list", + Type: ParameterTypeStringList, + Default: cast.InterfaceAddr([]string{}), + }, + { + Name: "empty-key-value", + Type: ParameterTypeKeyValue, + Default: cast.InterfaceAddr(map[string]string{}), + }, + { + Name: "empty-object", + Type: ParameterTypeObjectFromFile, + Default: cast.InterfaceAddr(map[string]interface{}{}), + }, + }), + ) + + result, err := pd.ParsedParametersFromDefaults() + require.NoError(t, err) + require.NotNil(t, result) + + // Check empty string list + param, ok := result.Get("empty-string-list") + assert.True(t, ok, "empty-string-list parameter should exist") + assert.Equal(t, []string{}, param.Value) + + // Check empty key-value map + param, ok = result.Get("empty-key-value") + assert.True(t, ok, "empty-key-value parameter should exist") + assert.Equal(t, map[string]string{}, param.Value) + + // Check empty object + param, ok = result.Get("empty-object") + assert.True(t, ok, "empty-object parameter should exist") + assert.Equal(t, map[string]interface{}{}, param.Value) +} + +func TestParsedParametersFromDefaults_NilComplexTypes(t *testing.T) { + pd := NewParameterDefinitions( + WithParameterDefinitionList([]*ParameterDefinition{ + { + Name: "nil-string-list", + Type: ParameterTypeStringList, + Default: nil, + }, + { + Name: "nil-integer-list", + Type: ParameterTypeIntegerList, + Default: nil, + }, + { + Name: "nil-float-list", + Type: ParameterTypeFloatList, + Default: nil, + }, + { + Name: "nil-choice-list", + Type: ParameterTypeChoiceList, + Default: nil, + Choices: []string{"choice1", "choice2"}, + }, + { + Name: "nil-key-value", + Type: ParameterTypeKeyValue, + Default: nil, + }, + { + Name: "nil-object-from-file", + Type: ParameterTypeObjectFromFile, + Default: nil, + }, + { + Name: "nil-object-list-from-file", + Type: ParameterTypeObjectListFromFile, + Default: nil, + }, + { + Name: "nil-string-list-from-file", + Type: ParameterTypeStringListFromFile, + Default: nil, + }, + { + Name: "nil-string-from-file", + Type: ParameterTypeStringFromFile, + Default: nil, + }, + { + Name: "nil-file", + Type: ParameterTypeFile, + Default: nil, + }, + { + Name: "nil-file-list", + Type: ParameterTypeFileList, + Default: nil, + }, + }), + ) + + result, err := pd.ParsedParametersFromDefaults() + require.NoError(t, err) + require.NotNil(t, result) + + // All parameters should be excluded since they have nil defaults + assert.Equal(t, 0, result.Len(), "no parameters should be included") + + // Verify each parameter is not present + paramNames := []string{ + "nil-string-list", "nil-integer-list", "nil-float-list", + "nil-choice-list", "nil-key-value", "nil-object-from-file", + "nil-object-list-from-file", "nil-string-list-from-file", + "nil-string-from-file", "nil-file", "nil-file-list", + } + + for _, name := range paramNames { + _, ok := result.Get(name) + assert.False(t, ok, "parameter %s should not exist", name) + } +} diff --git a/pkg/cmds/parameters/parse.go b/pkg/cmds/parameters/parse.go index 863df62f..f96fb1f8 100644 --- a/pkg/cmds/parameters/parse.go +++ b/pkg/cmds/parameters/parse.go @@ -12,6 +12,7 @@ import ( "time" "github.com/araddon/dateparse" + "github.com/go-go-golems/glazed/pkg/helpers/cast" "github.com/pkg/errors" "github.com/tj/go-naturaldate" "gopkg.in/yaml.v3" @@ -213,13 +214,13 @@ func (p *ParameterDefinition) ParseParameter(v []string, options ...ParseStepOpt case ParameterTypeObjectListFromFiles: fallthrough case ParameterTypeObjectListFromFile: - ret_ := []interface{}{} + ret_ := []map[string]interface{}{} for _, fileName := range v { l, err := parseFromFileName(fileName, p, options...) if err != nil { return nil, err } - lObj, ok := l.Value.([]interface{}) + lObj, ok := cast.CastList2[map[string]interface{}, interface{}](l.Value) if !ok { return nil, errors.Errorf("Could not parse file %s as list of objects", fileName) } diff --git a/pkg/cmds/parameters/parse_test.go b/pkg/cmds/parameters/parse_test.go index e71ce63d..4ec9fac4 100644 --- a/pkg/cmds/parameters/parse_test.go +++ b/pkg/cmds/parameters/parse_test.go @@ -678,7 +678,7 @@ func parseObjectListFromString(parameter *ParameterDefinition, input string, fil if err != nil { return nil, err } - v, ok := cast.CastList[map[string]interface{}, interface{}](i.Value.([]interface{})) + v, ok := cast.CastList2[map[string]interface{}, interface{}](i.Value) if !ok { return nil, errors.New("failed to cast") } @@ -759,27 +759,39 @@ func TestParseStringListFromFileRealFile(t *testing.T) { func TestParseObjectListFromFileRealFile(t *testing.T) { parameter := NewParameterDefinition("test", ParameterTypeObjectListFromFile, - WithDefault([]interface{}{}), + WithDefault([]map[string]interface{}{}), ) v, err := parameter.ParseParameter([]string{"test-data/object.json"}) require.NoError(t, err) - assert.Equal(t, []interface{}{map[string]interface{}{"name": "object1", "type": "object"}}, v.Value) + assert.Equal(t, []map[string]interface{}{{"name": "object1", "type": "object"}}, v.Value) v, err = parameter.ParseParameter([]string{"test-data/objectList.json"}) require.NoError(t, err) assert.Equal(t, - []interface{}{ - map[string]interface{}{"name": "objectList1", "type": "object"}, - map[string]interface{}{"name": "objectList2", "type": "object"}, + []map[string]interface{}{ + { + "name": "objectList1", + "type": "object", + }, + { + "name": "objectList2", + "type": "object", + }, }, v.Value) v, err = parameter.ParseParameter([]string{"test-data/objectList3.csv"}) require.NoError(t, err) assert.Equal(t, - []interface{}{ - map[string]interface{}{"name": "objectList5", "type": "object"}, - map[string]interface{}{"name": "objectList6", "type": "object"}, + []map[string]interface{}{ + { + "name": "objectList5", + "type": "object", + }, + { + "name": "objectList6", + "type": "object", + }, }, v.Value) parameter = NewParameterDefinition("test", ParameterTypeObjectListFromFiles, @@ -788,14 +800,20 @@ func TestParseObjectListFromFileRealFile(t *testing.T) { v, err = parameter.ParseParameter([]string{"test-data/object.json"}) require.NoError(t, err) - assert.Equal(t, []interface{}{map[string]interface{}{"name": "object1", "type": "object"}}, v.Value) + assert.Equal(t, []map[string]interface{}{{"name": "object1", "type": "object"}}, v.Value) v, err = parameter.ParseParameter([]string{"test-data/object.json", "test-data/object2.json"}) require.NoError(t, err) assert.Equal(t, - []interface{}{ - map[string]interface{}{"name": "object1", "type": "object"}, - map[string]interface{}{"name": "object2", "type": "object"}, + []map[string]interface{}{ + { + "name": "object1", + "type": "object", + }, + { + "name": "object2", + "type": "object", + }, }, v.Value) @@ -807,15 +825,39 @@ func TestParseObjectListFromFileRealFile(t *testing.T) { "test-data/objectList3.csv"}) require.NoError(t, err) assert.Equal(t, - []interface{}{ - map[string]interface{}{"name": "objectList1", "type": "object"}, - map[string]interface{}{"name": "objectList2", "type": "object"}, - map[string]interface{}{"name": "objectList3", "type": "object"}, - map[string]interface{}{"name": "objectList4", "type": "object"}, - map[string]interface{}{"name": "object1", "type": "object"}, - map[string]interface{}{"name": "object2", "type": "object"}, - map[string]interface{}{"name": "objectList5", "type": "object"}, - map[string]interface{}{"name": "objectList6", "type": "object"}, + []map[string]interface{}{ + { + "name": "objectList1", + "type": "object", + }, + { + "name": "objectList2", + "type": "object", + }, + { + "name": "objectList3", + "type": "object", + }, + { + "name": "objectList4", + "type": "object", + }, + { + "name": "object1", + "type": "object", + }, + { + "name": "object2", + "type": "object", + }, + { + "name": "objectList5", + "type": "object", + }, + { + "name": "objectList6", + "type": "object", + }, }, v.Value) } diff --git a/pkg/cmds/parameters/parsed-parameter.go b/pkg/cmds/parameters/parsed-parameter.go index 933a6756..d3505049 100644 --- a/pkg/cmds/parameters/parsed-parameter.go +++ b/pkg/cmds/parameters/parsed-parameter.go @@ -223,7 +223,8 @@ func (p *ParsedParameters) Update( // XXX Add proper error return handling here func (p *ParsedParameters) UpdateValue( - key string, pd *ParameterDefinition, + key string, + pd *ParameterDefinition, v interface{}, options ...ParseStepOption, ) error { diff --git a/pkg/helpers/cast/normalize.go b/pkg/helpers/cast/normalize.go new file mode 100644 index 00000000..22495139 --- /dev/null +++ b/pkg/helpers/cast/normalize.go @@ -0,0 +1,82 @@ +package cast + +import ( + "fmt" + "reflect" +) + +// NormalizeValue converts various types into a normalized format for comparison +func NormalizeValue(v interface{}) (interface{}, error) { + if v == nil { + return nil, nil + } + + switch val := v.(type) { + case []interface{}: + return normalizeSlice(val) + case map[interface{}]interface{}: + return normalizeMap(val) + case map[string]interface{}: + return normalizeStringMap(val) + case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool: + return val, nil + default: + // Handle slices that aren't []interface{} + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Slice { + interfaceSlice := make([]interface{}, rv.Len()) + for i := 0; i < rv.Len(); i++ { + interfaceSlice[i] = rv.Index(i).Interface() + } + return normalizeSlice(interfaceSlice) + } + + // Try to convert to string if possible + str, err := ToString(v) + if err == nil { + return str, nil + } + // Return as-is if we can't normalize + return v, nil + } +} + +func normalizeSlice(slice []interface{}) ([]interface{}, error) { + result := make([]interface{}, len(slice)) + for i, v := range slice { + normalized, err := NormalizeValue(v) + if err != nil { + return nil, fmt.Errorf("error normalizing slice element %d: %w", i, err) + } + result[i] = normalized + } + return result, nil +} + +func normalizeMap(m map[interface{}]interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + for k, v := range m { + key, err := ToString(k) + if err != nil { + return nil, fmt.Errorf("error converting map key to string: %w", err) + } + normalized, err := NormalizeValue(v) + if err != nil { + return nil, fmt.Errorf("error normalizing map value for key %s: %w", key, err) + } + result[key] = normalized + } + return result, nil +} + +func normalizeStringMap(m map[string]interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + for k, v := range m { + normalized, err := NormalizeValue(v) + if err != nil { + return nil, fmt.Errorf("error normalizing map value for key %s: %w", k, err) + } + result[k] = normalized + } + return result, nil +}