From 30966e589e30a0ef7694938f4b23feb54d7fee67 Mon Sep 17 00:00:00 2001 From: Sergey Lazarenko Date: Fri, 4 Sep 2026 16:52:14 +0300 Subject: [PATCH 1/9] fix setting defaults value in config --- cfg/config.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index a4e22a458..72403080b 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -731,14 +731,30 @@ 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 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)) } + 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 %s, got=%s: %w", tField.Name, vFieldKind, defaultValue, err) + } + vField.SetUint(uint64(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(float64(val)) + } case reflect.Slice: if vField.Len() == 0 { val := strings.Fields(defaultValue) From 51d17e6ac611aedb73b848d404f10c053567ec5c Mon Sep 17 00:00:00 2001 From: Sergey Lazarenko Date: Fri, 4 Sep 2026 21:54:38 +0300 Subject: [PATCH 2/9] remove unnecessary conversion --- cfg/config.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index 72403080b..f7b39b406 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -737,7 +737,7 @@ func SetDefaultValues(data any) error { if err != nil { return fmt.Errorf("default value for field %s should be %s, got=%s: %w", tField.Name, vFieldKind, defaultValue, err) } - vField.SetInt(int64(val)) + vField.SetInt(val) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if vField.Uint() == 0 { @@ -745,7 +745,7 @@ func SetDefaultValues(data any) error { if err != nil { return fmt.Errorf("default value for field %s should be %s, got=%s: %w", tField.Name, vFieldKind, defaultValue, err) } - vField.SetUint(uint64(val)) + vField.SetUint(val) } case reflect.Float32, reflect.Float64: if vField.Float() == 0 { @@ -753,7 +753,7 @@ func SetDefaultValues(data any) error { if err != nil { return fmt.Errorf("default value for field %s should be %s, got=%s: %w", tField.Name, vFieldKind, defaultValue, err) } - vField.SetFloat(float64(val)) + vField.SetFloat(val) } case reflect.Slice: if vField.Len() == 0 { @@ -791,8 +791,8 @@ 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{}) +func mergeYAMLs(a, b map[any]any) map[any]any { + merged := make(map[any]any) for k, v := range a { merged[k] = v } From aad898d393b7b5dc74a6a1b2445e2dbb52a5219a Mon Sep 17 00:00:00 2001 From: Sergey Lazarenko Date: Tue, 8 Sep 2026 23:19:34 +0300 Subject: [PATCH 3/9] fix default values not being applied to slice elements --- cfg/config.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index f7b39b406..45242c6e9 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 } @@ -739,7 +749,7 @@ func SetDefaultValues(data any) error { } vField.SetInt(val) } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: if vField.Uint() == 0 { val, err := strconv.ParseUint(defaultValue, 10, vField.Type().Bits()) if err != nil { @@ -791,6 +801,68 @@ func CompileRegex(s string) (*regexp.Regexp, error) { return regexp.Compile(s[1 : len(s)-1]) } +// preallocSlicesFromJSON pre-sizes []struct config fields from the raw JSON +// and applies tag defaults to each element. +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 := SetDefaultValues(item.Addr().Interface()); err != nil { + return err + } + 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 { From 918f89ddce9ed8803de4f4608c2a50dc4288736f Mon Sep 17 00:00:00 2001 From: Sergey Lazarenko Date: Wed, 9 Sep 2026 19:25:33 +0300 Subject: [PATCH 4/9] set default to uint8 and change the type of Cond --- cfg/config.go | 2 +- cfg/matchrule/matchrule.go | 27 +++++++++++---------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index 45242c6e9..587d36da6 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -749,7 +749,7 @@ func SetDefaultValues(data any) error { } vField.SetInt(val) } - case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + 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 { diff --git a/cfg/matchrule/matchrule.go b/cfg/matchrule/matchrule.go index b49f3dc4e..35f5d2e8c 100644 --- a/cfg/matchrule/matchrule.go +++ b/cfg/matchrule/matchrule.go @@ -160,32 +160,27 @@ func (r *Rule) match(raw []byte) bool { return false } -type Cond byte +type Cond string + +const ( + CondAnd Cond = "and" + CondOr Cond = "or" +) var _ json.Unmarshaler = (*Cond)(nil) -func (c *Cond) UnmarshalJSON(i []byte) error { - switch { - case bytes.Equal(i, condAndBytes): +func (c *Cond) UnmarshalJSON(data []byte) error { + switch string(data) { + case "and": *c = CondAnd - case bytes.Equal(i, condOrBytes): + case "or": *c = CondOr default: - return fmt.Errorf("unknown condition %s", string(i)) + return fmt.Errorf("unknown condition %s", data) } return nil } -const ( - CondAnd Cond = iota - CondOr -) - -var ( - condAndBytes = []byte(`"and"`) - condOrBytes = []byte(`"or"`) -) - type RuleSet struct { // > @3@4@5@6 // > From 52043ab0c30df37029b7ac733035935b0ffef395 Mon Sep 17 00:00:00 2001 From: Sergey Lazarenko Date: Wed, 9 Sep 2026 20:10:37 +0300 Subject: [PATCH 5/9] fix --- cfg/matchrule/matchrule.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cfg/matchrule/matchrule.go b/cfg/matchrule/matchrule.go index 35f5d2e8c..0924d906c 100644 --- a/cfg/matchrule/matchrule.go +++ b/cfg/matchrule/matchrule.go @@ -171,9 +171,9 @@ var _ json.Unmarshaler = (*Cond)(nil) func (c *Cond) UnmarshalJSON(data []byte) error { switch string(data) { - case "and": + case `"and"`: *c = CondAnd - case "or": + case `"or"`: *c = CondOr default: return fmt.Errorf("unknown condition %s", data) From 99ec57b747d9ea28f091ce8fe1984f4c7caf52f1 Mon Sep 17 00:00:00 2001 From: Sergey Lazarenko Date: Thu, 17 Sep 2026 11:42:21 +0300 Subject: [PATCH 6/9] add tests --- cfg/config_test.go | 285 ++++++++++++++++++++++++++------ cfg/matchrule/matchrule_test.go | 40 +++++ 2 files changed, 271 insertions(+), 54 deletions(-) diff --git a/cfg/config_test.go b/cfg/config_test.go index 68e940da8..6269f9c90 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,202 @@ 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 childDefaults struct { + ValueStr string `json:"value_str" default:"child"` + ValueInt64 int64 `json:"value_int64" default:"67"` +} + +type parentDefaults struct { + ValueBool bool `json:"value_bool" default:"true"` + Nested childDefaults `json:"nested"` + Childs []childDefaults `json:"childs"` +} + +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{ + Childs: []childDefaults{ + {ValueStr: "str"}, + {}, + }, + }, + want: &parentDefaults{ + ValueBool: true, + Nested: childDefaults{ValueStr: "child", ValueInt64: 67}, + Childs: []childDefaults{ + {ValueStr: "str", ValueInt64: 67}, + {ValueStr: "child", 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 + want *parentDefaults + }{ + { + name: "preallocate_struct_slice", + input: `{"childs":[{},{"value_str":"size"}]}`, + want: &parentDefaults{ + Childs: []childDefaults{ + {ValueStr: "child", ValueInt64: 67}, + {ValueStr: "child", ValueInt64: 67}, + }, + }, + }, + { + name: "skip_nil_slice", + input: `{"childs":null}`, + want: &parentDefaults{}, + }, + { + name: "skip_wrong_type_in_json", + input: `{"childs":"52"}`, + want: &parentDefaults{}, + }, + { + name: "nil_object", + input: ``, + want: &parentDefaults{}, + }, + } + + 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)) + require.Equal(t, tt.want, got) + }) + } +} + +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: childDefaults{ + ValueStr: "child", ValueInt64: 67, + }, + }, + }, + { + name: "bool_false_overrides_default_true", + input: `{"value_bool":false}`, + want: &parentDefaults{ + ValueBool: false, + Nested: childDefaults{ + ValueStr: "child", ValueInt64: 67, + }, + }, + }, + { + name: "slice_element_defaults_applied_for_missing_fields", + input: `{"childs":[{"value_str":"first"}, {"value_int64":52}]}`, + want: &parentDefaults{ + ValueBool: true, + Nested: childDefaults{ValueStr: "child", ValueInt64: 67}, + Childs: []childDefaults{ + {ValueStr: "first", ValueInt64: 67}, + {ValueStr: "child", 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_test.go b/cfg/matchrule/matchrule_test.go index 96892d1d1..83e552c20 100644 --- a/cfg/matchrule/matchrule_test.go +++ b/cfg/matchrule/matchrule_test.go @@ -165,3 +165,43 @@ func TestException_Match(t *testing.T) { require.True(t, e.Match([]byte("ok"))) } + +func TestCond_UnmarshalJSON(t *testing.T) { + cases := []struct { + name string + input string + want Cond + wantErr bool + }{ + { + name: "and", + input: `"and"`, + want: CondAnd, + }, + { + name: "or", + input: `"or"`, + want: CondOr, + }, + { + name: "unknown_value", + input: `"AND"`, + wantErr: true, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var c Cond + err := c.UnmarshalJSON([]byte(tt.input)) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, c) + } + }) + } +} From 2c33acdd14d7257c229283209799ad3a923d0283 Mon Sep 17 00:00:00 2001 From: Sergey Lazarenko Date: Thu, 24 Sep 2026 15:56:45 +0300 Subject: [PATCH 7/9] update --- cfg/config.go | 12 +++--- cfg/config_test.go | 76 ++++++++++++++++----------------- cfg/matchrule/matchrule.go | 35 ++++++++------- cfg/matchrule/matchrule_test.go | 40 ----------------- 4 files changed, 63 insertions(+), 100 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index 587d36da6..71055b880 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -726,6 +726,12 @@ func SetDefaultValues(data any) error { defaultValue := tField.Tag.Get("default") if defaultValue != "" { + 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() @@ -801,8 +807,7 @@ func CompileRegex(s string) (*regexp.Regexp, error) { return regexp.Compile(s[1 : len(s)-1]) } -// preallocSlicesFromJSON pre-sizes []struct config fields from the raw JSON -// and applies tag defaults to each element. +// preallocSlicesFromJSON pre-sizes []struct config fields from the raw JSON. func preallocSlicesFromJSON(v reflect.Value, object *simplejson.Json) error { if object == nil { return nil @@ -849,9 +854,6 @@ func preallocSlicesFromJSON(v reflect.Value, object *simplejson.Json) error { newSlice := reflect.MakeSlice(v.Type(), len(arr), len(arr)) for i := range len(arr) { item := newSlice.Index(i) - if err := SetDefaultValues(item.Addr().Interface()); err != nil { - return err - } if err := preallocSlicesFromJSON(item, object.GetIndex(i)); err != nil { return err } diff --git a/cfg/config_test.go b/cfg/config_test.go index 6269f9c90..a3ea8a192 100644 --- a/cfg/config_test.go +++ b/cfg/config_test.go @@ -924,15 +924,15 @@ type stringSliceDefaults struct { SS []string `default:"str1 str2 str3"` } -type childDefaults struct { - ValueStr string `json:"value_str" default:"child"` +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 childDefaults `json:"nested"` - Childs []childDefaults `json:"childs"` + ValueBool bool `json:"value_bool" default:"true"` + Nested childrenDefaults `json:"nested"` + Children []childrenDefaults `json:"children"` } func TestSetDefaultValues(t *testing.T) { @@ -961,17 +961,17 @@ func TestSetDefaultValues(t *testing.T) { { name: "recurse_into_struct_and_slice_elements", input: &parentDefaults{ - Childs: []childDefaults{ + Children: []childrenDefaults{ {ValueStr: "str"}, {}, }, }, want: &parentDefaults{ ValueBool: true, - Nested: childDefaults{ValueStr: "child", ValueInt64: 67}, - Childs: []childDefaults{ + Nested: childrenDefaults{ValueStr: "children", ValueInt64: 67}, + Children: []childrenDefaults{ {ValueStr: "str", ValueInt64: 67}, - {ValueStr: "child", ValueInt64: 67}, + {ValueStr: "children", ValueInt64: 67}, }, }, }, @@ -1002,34 +1002,24 @@ func TestSetDefaultValues(t *testing.T) { func TestPreallocSlicesFromJson(t *testing.T) { cases := []struct { - name string - input string - want *parentDefaults + name string + input string + wantLen int }{ { - name: "preallocate_struct_slice", - input: `{"childs":[{},{"value_str":"size"}]}`, - want: &parentDefaults{ - Childs: []childDefaults{ - {ValueStr: "child", ValueInt64: 67}, - {ValueStr: "child", ValueInt64: 67}, - }, - }, - }, - { - name: "skip_nil_slice", - input: `{"childs":null}`, - want: &parentDefaults{}, + name: "preallocate_struct_slice", + input: `{"children":[{},{"value_str":"size"}]}`, + wantLen: 2, }, { - name: "skip_wrong_type_in_json", - input: `{"childs":"52"}`, - want: &parentDefaults{}, + name: "preallocate_empty_slice", + input: `{"childern":[]}`, + wantLen: 0, }, { - name: "nil_object", - input: ``, - want: &parentDefaults{}, + name: "nil_object", + input: ``, + wantLen: -1, }, } @@ -1046,7 +1036,13 @@ func TestPreallocSlicesFromJson(t *testing.T) { got := &parentDefaults{} require.NoError(t, preallocSlicesFromJSON(reflect.ValueOf(got), root)) - require.Equal(t, tt.want, got) + + if tt.wantLen == -1 { + require.Nil(t, got.Children) + return + } + + require.Len(t, got.Children, tt.wantLen) }) } } @@ -1062,8 +1058,8 @@ func TestDecodeConfig(t *testing.T) { input: `{}`, want: &parentDefaults{ ValueBool: true, - Nested: childDefaults{ - ValueStr: "child", ValueInt64: 67, + Nested: childrenDefaults{ + ValueStr: "children", ValueInt64: 67, }, }, }, @@ -1072,20 +1068,20 @@ func TestDecodeConfig(t *testing.T) { input: `{"value_bool":false}`, want: &parentDefaults{ ValueBool: false, - Nested: childDefaults{ - ValueStr: "child", ValueInt64: 67, + Nested: childrenDefaults{ + ValueStr: "children", ValueInt64: 67, }, }, }, { name: "slice_element_defaults_applied_for_missing_fields", - input: `{"childs":[{"value_str":"first"}, {"value_int64":52}]}`, + input: `{"children":[{"value_str":"first"}, {"value_int64":52}]}`, want: &parentDefaults{ ValueBool: true, - Nested: childDefaults{ValueStr: "child", ValueInt64: 67}, - Childs: []childDefaults{ + Nested: childrenDefaults{ValueStr: "children", ValueInt64: 67}, + Children: []childrenDefaults{ {ValueStr: "first", ValueInt64: 67}, - {ValueStr: "child", ValueInt64: 52}, + {ValueStr: "children", ValueInt64: 52}, }, }, }, diff --git a/cfg/matchrule/matchrule.go b/cfg/matchrule/matchrule.go index 0924d906c..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 // > @@ -160,27 +160,32 @@ func (r *Rule) match(raw []byte) bool { return false } -type Cond string - -const ( - CondAnd Cond = "and" - CondOr Cond = "or" -) +type Cond byte var _ json.Unmarshaler = (*Cond)(nil) -func (c *Cond) UnmarshalJSON(data []byte) error { - switch string(data) { - case `"and"`: +func (c *Cond) UnmarshalJSON(i []byte) error { + switch { + case bytes.Equal(i, condAndBytes): *c = CondAnd - case `"or"`: + case bytes.Equal(i, condOrBytes): *c = CondOr default: - return fmt.Errorf("unknown condition %s", data) + return fmt.Errorf("unknown condition %s", string(i)) } return nil } +const ( + CondAnd Cond = iota + CondOr +) + +var ( + condAndBytes = []byte(`"and"`) + condOrBytes = []byte(`"or"`) +) + type RuleSet struct { // > @3@4@5@6 // > diff --git a/cfg/matchrule/matchrule_test.go b/cfg/matchrule/matchrule_test.go index 83e552c20..96892d1d1 100644 --- a/cfg/matchrule/matchrule_test.go +++ b/cfg/matchrule/matchrule_test.go @@ -165,43 +165,3 @@ func TestException_Match(t *testing.T) { require.True(t, e.Match([]byte("ok"))) } - -func TestCond_UnmarshalJSON(t *testing.T) { - cases := []struct { - name string - input string - want Cond - wantErr bool - }{ - { - name: "and", - input: `"and"`, - want: CondAnd, - }, - { - name: "or", - input: `"or"`, - want: CondOr, - }, - { - name: "unknown_value", - input: `"AND"`, - wantErr: true, - }, - } - - for _, tt := range cases { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - var c Cond - err := c.UnmarshalJSON([]byte(tt.input)) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - require.Equal(t, tt.want, c) - } - }) - } -} From 68508cfdce676fb2de2c74c46a9d097141a3612f Mon Sep 17 00:00:00 2001 From: Sergey Lazarenko Date: Thu, 24 Sep 2026 16:45:07 +0300 Subject: [PATCH 8/9] fix --- cfg/config.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index 71055b880..bf933858e 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -726,11 +726,13 @@ func SetDefaultValues(data any) error { defaultValue := tField.Tag.Get("default") if defaultValue != "" { - if j, ok := vField.Addr().Interface().(json.Unmarshaler); ok { - if err := j.UnmarshalJSON([]byte(strconv.Quote(defaultValue))); err != nil { - return err + 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 } - continue } switch vFieldKind { case reflect.Bool: From b3a883c6b3aa5843b4d8808cda3d67ee1d4afa73 Mon Sep 17 00:00:00 2001 From: Sergey Lazarenko Date: Fri, 25 Sep 2026 14:41:02 +0300 Subject: [PATCH 9/9] fix typo --- cfg/config_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfg/config_test.go b/cfg/config_test.go index a3ea8a192..a24daa72d 100644 --- a/cfg/config_test.go +++ b/cfg/config_test.go @@ -1013,7 +1013,7 @@ func TestPreallocSlicesFromJson(t *testing.T) { }, { name: "preallocate_empty_slice", - input: `{"childern":[]}`, + input: `{"children":[]}`, wantLen: 0, }, {