diff --git a/validate_test.go b/validate_test.go index 8af51ff..98070fa 100644 --- a/validate_test.go +++ b/validate_test.go @@ -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) diff --git a/validating.go b/validating.go index e31d944..a764907 100644 --- a/validating.go +++ b/validating.go @@ -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) diff --git a/validation.go b/validation.go index b11ba3c..03da911 100644 --- a/validation.go +++ b/validation.go @@ -629,3 +629,126 @@ func (v *Validation) isNotNeedToCheck(field string) bool { _, 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()) + 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 +}