Skip to content

Commit 608bc3f

Browse files
committed
add default functionality
1 parent 9ce03b6 commit 608bc3f

8 files changed

Lines changed: 161 additions & 13 deletions

File tree

model/condition.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
NOT_FROM ConditionType = "nfr"
2222
REGX ConditionType = "rex"
2323
FUNC ConditionType = "fun"
24+
DEF ConditionType = "def"
2425
)
2526

2627
var ValidConditionTypes = map[ConditionType]int{
@@ -34,6 +35,7 @@ var ValidConditionTypes = map[ConditionType]int{
3435
FROM: 7,
3536
NOT_FROM: 8,
3637
REGX: 9,
38+
DEF: 10,
3739
}
3840

3941
// LookupConditionType checks our validConditionType map for the scanned condition type.

model/parserAst.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ type RootNode struct {
1010
RootValue *AstValue
1111
}
1212

13+
// ExtractDefault searches the AST for a 'DEF' condition and returns its value.
14+
func (r *RootNode) ExtractDefault() string {
15+
if r.RootValue == nil {
16+
return ""
17+
}
18+
return r.RootValue.ExtractDefault()
19+
}
20+
1321
// AstValue (=abstract syntax tree value) holds a Type ("Condition" or "Group") as well as a `ConditionType` and `ConditionValue`.
1422
// The ConditionType is a [model.ConditionType] and the ConditionValue is any string (numbers are also represented as string).
1523
type AstValue struct {
@@ -22,6 +30,21 @@ type AstValue struct {
2230
End int
2331
}
2432

33+
// ExtractDefault recursively searches the AST value for a 'DEF' condition and returns its value.
34+
func (r *AstValue) ExtractDefault() string {
35+
for _, v := range r.ConditionGroup {
36+
if v.Type == CONDITION && v.ConditionType == DEF {
37+
return v.ConditionValue
38+
}
39+
if v.Type == GROUP {
40+
if def := v.ExtractDefault(); def != "" {
41+
return def
42+
}
43+
}
44+
}
45+
return ""
46+
}
47+
2548
// AstValueType is the type for all available AST value types.
2649
type AstValueType string
2750

model/parserAst_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,51 @@ import (
77
"github.com/stretchr/testify/assert"
88
)
99

10+
func TestExtractDefault(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
astValue AstValue
14+
expected string
15+
}{
16+
{
17+
name: "No default",
18+
astValue: AstValue{
19+
ConditionGroup: ConditionGroup{
20+
&AstValue{Type: CONDITION, ConditionType: EQUAL, ConditionValue: "1"},
21+
},
22+
},
23+
expected: "",
24+
},
25+
{
26+
name: "With default",
27+
astValue: AstValue{
28+
ConditionGroup: ConditionGroup{
29+
&AstValue{Type: CONDITION, ConditionType: DEF, ConditionValue: "test"},
30+
},
31+
},
32+
expected: "test",
33+
},
34+
{
35+
name: "With default in group",
36+
astValue: AstValue{
37+
ConditionGroup: ConditionGroup{
38+
&AstValue{Type: GROUP, ConditionGroup: ConditionGroup{
39+
&AstValue{Type: CONDITION, ConditionType: DEF, ConditionValue: "nested"},
40+
}},
41+
},
42+
},
43+
expected: "nested",
44+
},
45+
}
46+
47+
for _, test := range tests {
48+
t.Run(test.name, func(t *testing.T) {
49+
result := test.astValue.ExtractDefault()
50+
assert.Equal(t, test.expected, result, "Expected default string to match")
51+
})
52+
}
53+
}
54+
1055
func TestAstGroupToString(t *testing.T) {
1156
tests := []struct {
1257
name string

model/validation.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ type Validation struct {
1212
Default string
1313
// Inner Struct validation
1414
InnerValidation []Validation
15+
// Parsed AST representation of the requirement to avoid redundant parsing
16+
RequirementAST *AstValue
1517
}
1618

1719
// ValidatorMap is a map of validation keys to Validation objects.

validationExtract_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,17 @@ func TestGetValidationsFromStruct(t *testing.T) {
162162
assert.Error(t, err, "Expected error when getting validations")
163163
} else {
164164
assert.NoError(t, err, "Expected no error when getting validations")
165+
166+
// Helper to clear ASTs for equality testing
167+
var clearASTs func(vs []model.Validation)
168+
clearASTs = func(vs []model.Validation) {
169+
for i := range vs {
170+
vs[i].RequirementAST = nil
171+
clearASTs(vs[i].InnerValidation)
172+
}
173+
}
174+
clearASTs(validations)
175+
165176
assert.Equal(t, test.expected, validations, "Expected validations to match")
166177
}
167178
})

validator.go

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ func (r *Validator) ValidateWithValidation(jsonInput map[string]any, validations
134134
var ok bool
135135
var jsonValue any
136136
if jsonValue, ok = jsonInput[validation.Key]; !ok {
137-
if strings.TrimSpace(validation.Requirement) == string(model.NONE) {
137+
if validation.Default != "" {
138+
jsonValue = validation.Default
139+
ok = true
140+
} else if strings.TrimSpace(validation.Requirement) == string(model.NONE) {
138141
continue
139142
} else if len(validation.Groups) == 0 {
140143
return map[string]any{}, fmt.Errorf("json %v key not in map", validation.Key)
@@ -189,13 +192,18 @@ func (r *Validator) ValidateWithValidation(jsonInput map[string]any, validations
189192
err = r.ValidateValueWithParser(jsonValue, &validation)
190193
}
191194

192-
if err != nil && len(validation.Groups) == 0 {
193-
return map[string]any{}, fmt.Errorf("field %v invalid: %v", validation.Key, err.Error())
194-
} else if err != nil {
195-
for _, group := range validation.Groups {
196-
groupErrors[group.Name] = append(groupErrors[group.Name], fmt.Errorf("field %v invalid: %v", validation.Key, err.Error()))
195+
if err != nil {
196+
if validation.Default != "" {
197+
jsonValue = validation.Default
198+
err = nil
199+
} else if len(validation.Groups) == 0 {
200+
return map[string]any{}, fmt.Errorf("field %v invalid: %v", validation.Key, err.Error())
201+
} else {
202+
for _, group := range validation.Groups {
203+
groupErrors[group.Name] = append(groupErrors[group.Name], fmt.Errorf("field %v invalid: %v", validation.Key, err.Error()))
204+
}
205+
continue
197206
}
198-
continue
199207
}
200208

201209
validateValues[validation.Key] = jsonValue
@@ -214,13 +222,20 @@ func (r *Validator) ValidateWithValidation(jsonInput map[string]any, validations
214222
//
215223
// It returns an error if the validation fails.
216224
func (r *Validator) ValidateValueWithParser(input any, validation *model.Validation) error {
217-
p := parser.NewParser()
218-
v, err := p.ParseValidation(validation.Requirement)
219-
if err != nil {
220-
return err
225+
var astValue *model.AstValue
226+
227+
if validation.RequirementAST != nil {
228+
astValue = validation.RequirementAST
229+
} else {
230+
p := parser.NewParser()
231+
v, err := p.ParseValidation(validation.Requirement)
232+
if err != nil {
233+
return err
234+
}
235+
astValue = v.RootValue
221236
}
222237

223-
err = r.RunValidatorsOnConditionGroup(input, v.RootValue)
238+
err := r.RunValidatorsOnConditionGroup(input, astValue)
224239
if err != nil {
225240
return err
226241
}
@@ -244,7 +259,7 @@ func (r *Validator) RunValidatorsOnConditionGroup(input any, astValue *model.Ast
244259
err = r.RunValidatorsOnConditionGroup(input, v)
245260
case model.CONDITION:
246261
switch v.ConditionType {
247-
case model.NONE:
262+
case model.NONE, model.DEF:
248263
continue
249264
case model.EQUAL:
250265
err = validators.ValidateEqual(input, v)

validatorExtract.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/siherrmann/validator/helper"
99
"github.com/siherrmann/validator/model"
10+
"github.com/siherrmann/validator/parser"
1011
)
1112

1213
// GetValidationsFromStruct extracts validation rules from a struct based on the provided tag type.
@@ -65,6 +66,15 @@ func GetValidationFromStructField(tagType string, fieldValue reflect.Value, fiel
6566
}
6667
validation.Requirement = tagSplit[tagIndex]
6768
tagIndex++
69+
70+
// Extract default value & cache AST
71+
p := parser.NewParser()
72+
root, err := p.ParseValidation(validation.Requirement)
73+
if err != nil {
74+
return nil, fmt.Errorf("error parsing validation: %v", err)
75+
}
76+
validation.Default = root.ExtractDefault()
77+
validation.RequirementAST = root.RootValue
6878
}
6979

7080
if len(tagSplit) > tagIndex {

validator_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,46 @@ func TestValidateAndUpdate(t *testing.T) {
194194
assert.Equal(t, "test", testStruct.Items[0].Name, "Name should not be updated (has ignore upd tag)")
195195
assert.Equal(t, 19, testStruct.Items[0].Age, "Age should be updated (has full upd tag)")
196196
})
197+
t.Run("Valid struct with missing field having def tag", func(t *testing.T) {
198+
testStruct := &struct {
199+
Fruit string `json:"fruit" vld:"equapple defunasigned"`
200+
}{}
201+
err := r.ValidateAndUpdate(map[string]any{}, testStruct, model.VLD)
202+
assert.NoError(t, err, "Expected no error but got one")
203+
assert.Equal(t, "unasigned", testStruct.Fruit, "Expected output to match default")
204+
})
205+
206+
t.Run("Valid struct with invalid field having def tag", func(t *testing.T) {
207+
testStruct := &struct {
208+
Fruit string `json:"fruit" vld:"equapple defunasigned"`
209+
}{}
210+
err := r.ValidateAndUpdate(map[string]any{"fruit": "banana"}, testStruct, model.VLD)
211+
assert.NoError(t, err, "Expected no error but got one")
212+
assert.Equal(t, "unasigned", testStruct.Fruit, "Expected output to match default")
213+
})
214+
215+
t.Run("Valid struct with valid field having def tag", func(t *testing.T) {
216+
testStruct := &struct {
217+
Fruit string `json:"fruit" vld:"equapple defunasigned"`
218+
}{}
219+
err := r.ValidateAndUpdate(map[string]any{"fruit": "apple"}, testStruct, model.VLD)
220+
assert.NoError(t, err, "Expected no error but got one")
221+
assert.Equal(t, "apple", testStruct.Fruit, "Expected output to match input")
222+
})
223+
224+
t.Run("Valid struct with def tag for int", func(t *testing.T) {
225+
testStruct := &struct {
226+
Count int `json:"count" vld:"min1 def10"`
227+
}{}
228+
err := r.ValidateAndUpdate(map[string]any{}, testStruct, model.VLD)
229+
assert.NoError(t, err, "Expected no error but got one")
230+
assert.Equal(t, 10, testStruct.Count, "Expected output to match default")
231+
232+
testStruct.Count = 0
233+
err = r.ValidateAndUpdate(map[string]any{"count": 0}, testStruct, model.VLD)
234+
assert.NoError(t, err, "Expected no error but got one")
235+
assert.Equal(t, 10, testStruct.Count, "Expected output to match default")
236+
})
197237
}
198238

199239
func TestValidateAndUpdateWithValidation(t *testing.T) {

0 commit comments

Comments
 (0)