diff --git a/cfg/config.go b/cfg/config.go index a4e22a458..bf933858e 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -305,7 +305,17 @@ func tryApplyFunc(app funcApplier, field *simplejson.Json) (string, bool) { } func DecodeConfig(config any, configJson []byte) error { - err := SetDefaultValues(config) + object, err := simplejson.NewJson(configJson) + if err != nil { + return err + } + + err = preallocSlicesFromJSON(reflect.ValueOf(config), object) + if err != nil { + return err + } + + err = SetDefaultValues(config) if err != nil { return err } @@ -716,6 +726,14 @@ func SetDefaultValues(data any) error { defaultValue := tField.Tag.Get("default") if defaultValue != "" { + if vField.IsZero() { + if j, ok := vField.Addr().Interface().(json.Unmarshaler); ok { + if err := j.UnmarshalJSON([]byte(strconv.Quote(defaultValue))); err != nil { + return err + } + continue + } + } switch vFieldKind { case reflect.Bool: currentValue := vField.Bool() @@ -731,13 +749,29 @@ func SetDefaultValues(data any) error { if vField.String() == "" { vField.SetString(defaultValue) } - case reflect.Int: + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if vField.Int() == 0 { // like in vField.IsZero - val, err := strconv.Atoi(defaultValue) + val, err := strconv.ParseInt(defaultValue, 10, vField.Type().Bits()) + if err != nil { + return fmt.Errorf("default value for field %s should be %s, got=%s: %w", tField.Name, vFieldKind, defaultValue, err) + } + vField.SetInt(val) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if vField.Uint() == 0 { + val, err := strconv.ParseUint(defaultValue, 10, vField.Type().Bits()) if err != nil { - return fmt.Errorf("default value for field %s should be int, got=%s: %w", tField.Name, defaultValue, err) + return fmt.Errorf("default value for field %s should be %s, got=%s: %w", tField.Name, vFieldKind, defaultValue, err) } - vField.SetInt(int64(val)) + vField.SetUint(val) + } + case reflect.Float32, reflect.Float64: + if vField.Float() == 0 { + val, err := strconv.ParseFloat(defaultValue, vField.Type().Bits()) + if err != nil { + return fmt.Errorf("default value for field %s should be %s, got=%s: %w", tField.Name, vFieldKind, defaultValue, err) + } + vField.SetFloat(val) } case reflect.Slice: if vField.Len() == 0 { @@ -775,8 +809,66 @@ func CompileRegex(s string) (*regexp.Regexp, error) { return regexp.Compile(s[1 : len(s)-1]) } -func mergeYAMLs(a, b map[interface{}]interface{}) map[interface{}]interface{} { - merged := make(map[interface{}]interface{}) +// preallocSlicesFromJSON pre-sizes []struct config fields from the raw JSON. +func preallocSlicesFromJSON(v reflect.Value, object *simplejson.Json) error { + if object == nil { + return nil + } + + switch v.Kind() { + case reflect.Pointer: + if v.IsNil() { + return nil + } + return preallocSlicesFromJSON(v.Elem(), object) + case reflect.Struct: + t := v.Type() + for i := range t.NumField() { + tField := t.Field(i) + if !tField.IsExported() { + continue + } + + // before: `json:"something,omitempty"` + key, _, _ := strings.Cut(tField.Tag.Get("json"), ",") + // after: `json:"something"` + child, ok := object.CheckGet(key) + if !ok { + continue + } + + if err := preallocSlicesFromJSON(v.Field(i), child); err != nil { + return err + } + } + case reflect.Slice: + arr, err := object.Array() + if err != nil { + return nil + } + + elemType := v.Type().Elem() + // only prealloc for slices whose elements type may carry defaults. + if elemType.Kind() != reflect.Struct { + return nil + } + + newSlice := reflect.MakeSlice(v.Type(), len(arr), len(arr)) + for i := range len(arr) { + item := newSlice.Index(i) + if err := preallocSlicesFromJSON(item, object.GetIndex(i)); err != nil { + return err + } + } + + v.Set(newSlice) + } + + return nil +} + +func mergeYAMLs(a, b map[any]any) map[any]any { + merged := make(map[any]any) for k, v := range a { merged[k] = v } diff --git a/cfg/config_test.go b/cfg/config_test.go index 68e940da8..a24daa72d 100644 --- a/cfg/config_test.go +++ b/cfg/config_test.go @@ -32,22 +32,10 @@ func TestSimple(t *testing.T) { assert.Equal(t, "devnull", outputType, "output type is not overrided") } -type intDefault struct { - T int `default:"5"` -} - type strRequired struct { T string `required:"true"` } -type strDefault struct { - T string `default:"sync"` -} - -type boolDefault struct { - T bool `default:"true"` -} - type PersistenceMode byte const ( @@ -125,12 +113,14 @@ func TestParseRequiredErr(t *testing.T) { } func TestParseDefault(t *testing.T) { - s := &strDefault{} + s := &struct { + S string `default:"sync"` + }{} SetDefaultValues(s) err := Parse(s, nil) assert.NoError(t, err, "shouldn't be an error") - assert.Equal(t, "sync", s.T, "wrong value") + assert.Equal(t, "sync", s.S, "wrong value") } func TestParseDuration(t *testing.T) { @@ -675,50 +665,36 @@ func TestApplyConfigFuncs(t *testing.T) { } } -func TestParseDefaultInt(t *testing.T) { - testCases := []struct { - s *intDefault - expected int +func TestPipelineValidator(t *testing.T) { + cases := []struct { + name string + input []string + wantErr bool }{ - {s: &intDefault{}, expected: 5}, - {s: &intDefault{T: 17}, expected: 17}, - } - for i, tc := range testCases { - SetDefaultValues(tc.s) - err := Parse(tc.s, nil) - - assert.NoError(t, err, "shouldn't be an error tc: %d", i) - assert.Equal(t, tc.expected, tc.s.T, "wrong value tc: %d", i) - } -} - -func TestParseDefaultBool(t *testing.T) { - testCases := []struct { - s *boolDefault - expected bool - }{ - {s: &boolDefault{}, expected: true}, - } - for i, tc := range testCases { - SetDefaultValues(tc.s) - err := Parse(tc.s, nil) - - assert.NoError(t, err, "shouldn't be an error tc: %d", i) - assert.Equal(t, tc.expected, tc.s.T, "wrong value tc: %d", i) + { + name: "valid", + input: []string{"pipeline_name", "PipeLine_NAME", "pipelinename", "PIPELINENAME", "pipeline_k8s"}, + }, + { + name: "invalid", + input: []string{"Pipeline-name", "pipeline-name", "", "пайплайн_нейм"}, + wantErr: true, + }, } -} -func TestPipelineValidatorValid(t *testing.T) { - testName := []string{"pipeline_name", "PipeLine_NAME", "pipelinename", "PIPELINENAME", "pipeline_k8s"} - for _, tl := range testName { - assert.NoError(t, validatePipelineName(tl)) - } -} + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() -func TestPipelineValidatorInvalid(t *testing.T) { - testName := []string{"Pipeline-name", "pipeline-name", "", "пайплайн_нейм"} - for _, tl := range testName { - assert.Error(t, validatePipelineName(tl)) + for _, s := range tt.input { + err := validatePipelineName(s) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } + }) } } @@ -864,6 +840,8 @@ func TestMergeYAMLs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := mergeYAMLs(tt.a, tt.b) if !reflect.DeepEqual(result, tt.expected) { t.Errorf("expected %v, got %v", tt.expected, result) @@ -924,3 +902,198 @@ func TestBuildFieldSelector(t *testing.T) { }) } } + +type primitiveDefaults struct { + B bool `default:"true"` + S string `default:"strValue"` + I int `default:"1"` + I8 int8 `default:"2"` + I16 int16 `default:"3"` + I32 int32 `default:"4"` + I64 int64 `default:"5"` + U uint `default:"6"` + U8 uint8 `default:"7"` + U16 uint16 `default:"8"` + U32 uint32 `default:"9"` + U64 uint64 `default:"10"` + F32 float32 `default:"11.0"` + F64 float64 `default:"12.0"` +} + +type stringSliceDefaults struct { + SS []string `default:"str1 str2 str3"` +} + +type childrenDefaults struct { + ValueStr string `json:"value_str" default:"children"` + ValueInt64 int64 `json:"value_int64" default:"67"` +} + +type parentDefaults struct { + ValueBool bool `json:"value_bool" default:"true"` + Nested childrenDefaults `json:"nested"` + Children []childrenDefaults `json:"children"` +} + +func TestSetDefaultValues(t *testing.T) { + cases := []struct { + name string + input any + want any + wantErr bool + }{ + { + name: "primitive_types", + input: &primitiveDefaults{}, + want: &primitiveDefaults{ + B: true, S: "strValue", I: 1, + I8: 2, I16: 3, I32: 4, + I64: 5, U: 6, U8: 7, + U16: 8, U32: 9, U64: 10, + F32: 11.0, F64: 12.0, + }, + }, + { + name: "string_slice", + input: &stringSliceDefaults{}, + want: &stringSliceDefaults{SS: []string{"str1", "str2", "str3"}}, + }, + { + name: "recurse_into_struct_and_slice_elements", + input: &parentDefaults{ + Children: []childrenDefaults{ + {ValueStr: "str"}, + {}, + }, + }, + want: &parentDefaults{ + ValueBool: true, + Nested: childrenDefaults{ValueStr: "children", ValueInt64: 67}, + Children: []childrenDefaults{ + {ValueStr: "str", ValueInt64: 67}, + {ValueStr: "children", ValueInt64: 67}, + }, + }, + }, + { + name: "err_negative_default_for_uint", + input: &struct { + U uint `default:"-1"` + }{}, + wantErr: true, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := SetDefaultValues(tt.input) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tt.want, tt.input) + }) + } +} + +func TestPreallocSlicesFromJson(t *testing.T) { + cases := []struct { + name string + input string + wantLen int + }{ + { + name: "preallocate_struct_slice", + input: `{"children":[{},{"value_str":"size"}]}`, + wantLen: 2, + }, + { + name: "preallocate_empty_slice", + input: `{"children":[]}`, + wantLen: 0, + }, + { + name: "nil_object", + input: ``, + wantLen: -1, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var root *simplejson.Json + if tt.input != "" { + var err error + root, err = simplejson.NewJson([]byte(tt.input)) + require.NoError(t, err) + } + + got := &parentDefaults{} + require.NoError(t, preallocSlicesFromJSON(reflect.ValueOf(got), root)) + + if tt.wantLen == -1 { + require.Nil(t, got.Children) + return + } + + require.Len(t, got.Children, tt.wantLen) + }) + } +} + +func TestDecodeConfig(t *testing.T) { + cases := []struct { + name string + input string + want *parentDefaults + }{ + { + name: "empty_json_set_all_defaults", + input: `{}`, + want: &parentDefaults{ + ValueBool: true, + Nested: childrenDefaults{ + ValueStr: "children", ValueInt64: 67, + }, + }, + }, + { + name: "bool_false_overrides_default_true", + input: `{"value_bool":false}`, + want: &parentDefaults{ + ValueBool: false, + Nested: childrenDefaults{ + ValueStr: "children", ValueInt64: 67, + }, + }, + }, + { + name: "slice_element_defaults_applied_for_missing_fields", + input: `{"children":[{"value_str":"first"}, {"value_int64":52}]}`, + want: &parentDefaults{ + ValueBool: true, + Nested: childrenDefaults{ValueStr: "children", ValueInt64: 67}, + Children: []childrenDefaults{ + {ValueStr: "first", ValueInt64: 67}, + {ValueStr: "children", ValueInt64: 52}, + }, + }, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := &parentDefaults{} + require.NoError(t, DecodeConfig(got, []byte(tt.input))) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/cfg/matchrule/matchrule.go b/cfg/matchrule/matchrule.go index b49f3dc4e..c38c7ab70 100644 --- a/cfg/matchrule/matchrule.go +++ b/cfg/matchrule/matchrule.go @@ -28,14 +28,14 @@ func (m *Mode) UnmarshalJSON(i []byte) error { } const ( - ModePrefix Mode = iota - ModeContains + ModeContains Mode = iota + ModePrefix ModeSuffix ) var ( - modePrefixBytes = []byte(`"prefix"`) modeContainsBytes = []byte(`"contains"`) + modePrefixBytes = []byte(`"prefix"`) modeSuffixBytes = []byte(`"suffix"`) ) @@ -48,7 +48,7 @@ type Rule struct { // > @3@4@5@6 // > // > Matching mode. Allowed modes: `prefix`, `contains`, `suffix`. Defaults to `contains`. - Mode Mode `json:"mode"` // * + Mode Mode `json:"mode" default:"contains"` // * // > @3@4@5@6 // >