Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,151 @@ func TestStruct_json_tag_name_parsing(t *testing.T) {
assert.True(t, strings.HasPrefix(errStr, "Field "))
}

type TestPermission struct {
TestUserData `json:",inline" validate:"required_if:Type,give"`
Type string `json:"type" validate:"required|in:give,remove"`
Access string `json:"access" validate:"required_if:Type,remove"`
}

type TestUserData struct {
TestNameField `json:",inline"`
TestBranchField `json:",inline"`
}

type TestNameField struct {
Name string `json:"name" validate:"required|max_len:5000"`
}

type TestBranchField struct {
Branch string `json:"branch" validate:"required|min_len:32|max_len:32"`
}

func TestEmbeddedStructRequiredIf(t *testing.T) {
// Test case 1: Type is "give", should validate UserData fields
perm1 := TestPermission{
TestUserData: TestUserData{},
Type: "give",
}

v1 := Struct(perm1)
v1.StopOnError = false
assert.False(t, v1.Validate())
fmt.Println("perm1 errors (expected to fail):", v1.Errors.All())

// Should have errors for UserData and its nested fields
assert.True(t, v1.Errors.HasField("TestUserData"))

// Test case 2: Type is "remove", should NOT validate UserData fields
perm2 := TestPermission{
Type: "remove",
Access: "change_types",
}
v2 := Struct(perm2)
v2.StopOnError = false
if !v2.Validate() {
fmt.Println("perm2 errors (should be empty but currently fails):", v2.Errors.All())
// This was the bug - it should validate successfully but doesn't
t.Errorf("perm2 should validate successfully when Type=remove, but got errors: %v", v2.Errors.All())
} else {
fmt.Println("perm2: No errors (expected)")
}
// This should now pass with our fix
assert.True(t, v2.Validate())

// Test case 3: Type is "give" with valid UserData, should pass
perm3 := TestPermission{
TestUserData: TestUserData{
TestNameField: TestNameField{Name: "test"},
TestBranchField: TestBranchField{Branch: "12345678901234567890123456789012"},
},
Type: "give",
}
v3 := Struct(perm3)
v3.StopOnError = false
if !v3.Validate() {
fmt.Println("perm3 errors (unexpected):", v3.Errors.All())
} else {
fmt.Println("perm3: No errors (expected)")
}
assert.True(t, v3.Validate())
}

// Test edge cases for embedded struct conditional validation
func TestEmbeddedStructRequiredIfEdgeCases(t *testing.T) {
// Test case: required_unless
type TestStruct struct {
UserData2 TestUserData `validate:"required_unless:Mode,skip"`
Mode string `validate:"required"`
}

// Mode is "skip", so UserData2 should not be required
test1 := TestStruct{
Mode: "skip",
}
v1 := Struct(test1)
assert.True(t, v1.Validate(), "Should pass when Mode=skip")

// Mode is "process", so UserData2 should be required
test2 := TestStruct{
Mode: "process",
}
v2 := Struct(test2)
assert.False(t, v2.Validate(), "Should fail when Mode=process and UserData2 is empty")
}

// Test the exact structures from the original issue
type OriginalPermission struct {
UserData `json:",inline" validate:"required_if:Type,give"`
Type string `json:"type" validate:"required|in:give,remove"`
Access string `json:"access" validate:"required_if:Type,remove"`
}

type UserData struct {
NameField `json:",inline"`
BranchField `json:",inline"`
}

type NameField struct {
Name string `json:"name" validate:"required|max_len:5000"`
}

type BranchField struct {
Branch string `json:"branch" validate:"required|min_len:32|max_len:32"`
}

func TestOriginalIssueExample(t *testing.T) {
// This should fail. UserData is required if type is give
perm1 := OriginalPermission{
UserData: UserData{},
Type: "give",
}

val1 := Struct(perm1)
val1.StopOnError = false
assert.False(t, val1.Validate(), "perm1 should fail validation when Type=give and UserData is empty")

// This should not need UserData and should pass
perm2 := OriginalPermission{
Type: "remove",
Access: "change_types",
}
val2 := Struct(&perm2)
val2.StopOnError = false
assert.True(t, val2.Validate(), "perm2 should pass validation when Type=remove")

// This should pass with valid UserData
perm3 := OriginalPermission{
UserData: UserData{
NameField: NameField{Name: "test"},
BranchField: BranchField{Branch: "12345678901234567890123456789012"},
},
Type: "give",
}
val3 := Struct(perm3)
val3.StopOnError = false
assert.True(t, val3.Validate(), "perm3 should pass validation with valid data")
}

func TestValidation_RestoreRequestBody(t *testing.T) {
request, err := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`{"test": "data"}`))
assert.Nil(t, err)
Expand Down
5 changes: 5 additions & 0 deletions validating.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ func (r *Rule) Apply(v *Validation) (stop bool) {
continue
}

// check if field belongs to an embedded struct with conditional validation
if v.shouldSkipEmbeddedFieldValidation(field) {
continue
}

// uploaded file validate
if isFileValidator(name) {
status := r.fileValidate(field, name, v)
Expand Down
123 changes: 123 additions & 0 deletions validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,3 +629,126 @@
_, ok := v.sceneFields[field]
return !ok
}

// shouldSkipEmbeddedFieldValidation checks if a field belongs to an embedded struct
// with conditional validation (like required_if) and if those conditions are not met
func (v *Validation) shouldSkipEmbeddedFieldValidation(field string) bool {
// Only applies to struct data source
if _, ok := v.data.(*StructData); !ok {
return false
}

// Check if this field has a parent path (contains dots)
parts := strings.Split(field, ".")
if len(parts) < 2 {
return false
}

// Check each level of the path for conditional validation
for i := 1; i < len(parts); i++ {
parentPath := strings.Join(parts[:i], ".")

// Find rules for this parent path that have conditional validation
for _, rule := range v.rules {
for _, ruleField := range rule.fields {
if ruleField == parentPath {
// Check if this rule has conditional validation (required_if, required_unless, etc.)
if v.isConditionalValidator(rule.realName) {
// Check if the condition is met (different logic for each conditional validator)
conditionMet := v.evaluateConditionalValidatorCondition(rule)
if !conditionMet {
// Condition not met, skip validation of nested field
return true
}
// If condition is met, don't skip - let normal validation proceed
return false
}
}
}
}
}

return false
}

// evaluateConditionalValidatorCondition checks if the condition part of a conditional validator is met
func (v *Validation) evaluateConditionalValidatorCondition(rule *Rule) bool {
switch rule.realName {
case "requiredIf":
return v.evaluateRequiredIfCondition(rule)
case "requiredUnless":
return v.evaluateRequiredUnlessCondition(rule)
// Add other conditional validators as needed
default:
return true // Safe default - assume condition is met
}
}

// evaluateRequiredIfCondition checks if the required_if condition is met
func (v *Validation) evaluateRequiredIfCondition(rule *Rule) bool {
if len(rule.arguments) < 2 {
return false
}

// Convert arguments to strings (same logic as RequiredIf validator)
kvs := make([]string, len(rule.arguments))
for i, arg := range rule.arguments {
kvs[i] = fmt.Sprintf("%v", arg)
}

dstField, args := kvs[0], kvs[1:]
if dstVal, has := v.Get(dstField); has {
// Check if destination field value matches any of the specified values
if len(args) == 1 {
rftDv := reflect.ValueOf(dstVal)
wantVal, err := convTypeByBaseKind(args[0], stringKind, rftDv.Kind())

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.21

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.20

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.22

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.24

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.23

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.19

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.21

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.22

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.20

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.19

too many arguments in call to convTypeByBaseKind

Check failure on line 704 in validation.go

View workflow job for this annotation

GitHub Actions / Test on go 1.24

too many arguments in call to convTypeByBaseKind
if err == nil && dstVal == wantVal {
return true // Condition is met
}
} else if Enum(dstVal, args) {
return true // Condition is met
}
}

return false // Condition is not met
}

// evaluateRequiredUnlessCondition checks if the required_unless condition is met
func (v *Validation) evaluateRequiredUnlessCondition(rule *Rule) bool {
if len(rule.arguments) < 2 {
return false
}

// Convert arguments to strings
kvs := make([]string, len(rule.arguments))
for i, arg := range rule.arguments {
kvs[i] = fmt.Sprintf("%v", arg)
}

dstField, values := kvs[0], kvs[1:]
if dstVal, has, _ := v.tryGet(dstField); has {
// For required_unless, condition is met when field value is NOT in the specified values
return !Enum(dstVal, values)
}

return true // If field doesn't exist, condition is met
}

// isConditionalValidator checks if a validator is conditional (like required_if)
func (v *Validation) isConditionalValidator(validatorName string) bool {
conditionalValidators := []string{
"required_if", "requiredIf",
"required_unless", "requiredUnless",
"required_with", "requiredWith",
"required_with_all", "requiredWithAll",
"required_without", "requiredWithout",
"required_without_all", "requiredWithoutAll",
}

for _, cv := range conditionalValidators {
if validatorName == cv {
return true
}
}
return false
}
Loading