Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ The following table shows the validations and possible types, where "I" means "I
| max | I | - | - | W | W | W | W | W |
| min | I | - | - | W | W | W | W | W |
| in | I | W | W | W | W | W | - | W |
| nin | W | W | W | W | W | W | - | W |
| nin | I | W | W | W | W | W | - | W |
| required | I | W | W | W | W | W | W | W |
| email | I | - | - | - | - | - | - | - |

Expand Down
12 changes: 8 additions & 4 deletions tests/endtoend/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type StringType struct {
FieldNeq string `validate:"neq=cba"`
FieldNeqIC string `validate:"neq_ignore_case=YeS"`
FieldIn string `validate:"in=ab bc cd"`
FieldNotIn string `validate:"nin=xx yy zz"`
EmailReq string `validate:"required,email"`
EmailOpt string `validate:"email"`
}
Expand All @@ -28,8 +29,9 @@ func string_tests() {
FieldNeq: "cba",
FieldNeqIC: "yeS",
FieldIn: "abc",
EmailReq: "invalid.email.format", // Invalid required email
EmailOpt: "invalid", // Invalid optional email
FieldNotIn: "zz",
EmailReq: "invalid.email.format", // Invalid required email
EmailOpt: "invalid", // Invalid optional email
}
expectedMsgErrors = []string{
"FieldReq is required",
Expand All @@ -40,6 +42,7 @@ func string_tests() {
"FieldNeq must not be equal to 'cba'",
"FieldNeqIC must not be equal to 'yes'",
"FieldIn must be one of 'ab' 'bc' 'cd'",
"FieldNotIn must not be one of 'xx' 'yy' 'zz'",
"EmailReq must be a valid email",
"EmailOpt must be a valid email",
}
Expand All @@ -58,8 +61,9 @@ func string_tests() {
FieldNeq: "ops",
FieldNeqIC: "No",
FieldIn: "bc",
EmailReq: "user@example.com", // Valid required email
EmailOpt: "", // Empty optional email (valid)
FieldNotIn: "xy",
EmailReq: "user@example.com", // Valid required email
EmailOpt: "", // Empty optional email (valid)
}
expectedMsgErrors = nil
errs = StringTypeValidate(v)
Expand Down
4 changes: 4 additions & 0 deletions tests/endtoend/stringtype_validator.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 24 additions & 8 deletions validgen/get_test_elements_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,11 @@ func TestGetTestElementsWithStringFields(t *testing.T) {
fieldValidation: "in=a b c",
},
want: TestElements{
leftOperand: "obj.InField",
operator: "==",
rightOperands: []string{`"a"`, `"b"`, `"c"`},
errorMessage: "InField must be one of 'a' 'b' 'c'",
leftOperand: "obj.InField",
operator: "==",
rightOperands: []string{`"a"`, `"b"`, `"c"`},
concatOperator: "||",
errorMessage: "InField must be one of 'a' 'b' 'c'",
},
},
{
Expand All @@ -139,10 +140,25 @@ func TestGetTestElementsWithStringFields(t *testing.T) {
fieldValidation: "in=' a ' ' b ' ' c '",
},
want: TestElements{
leftOperand: "obj.InField",
operator: "==",
rightOperands: []string{`" a "`, `" b "`, `" c "`},
errorMessage: "InField must be one of ' a ' ' b ' ' c '",
leftOperand: "obj.InField",
operator: "==",
rightOperands: []string{`" a "`, `" b "`, `" c "`},
concatOperator: "||",
errorMessage: "InField must be one of ' a ' ' b ' ' c '",
},
},
{
name: "NotIn string with spaces",
args: args{
fieldName: "NotInField",
fieldValidation: "nin=a b c",
},
want: TestElements{
leftOperand: "obj.NotInField",
operator: "!=",
rightOperands: []string{`"a"`, `"b"`, `"c"`},
concatOperator: "&&",
errorMessage: "NotInField must not be one of 'a' 'b' 'c'",
},
},
{
Expand Down
2 changes: 1 addition & 1 deletion validgen/if_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func IfCode(fieldName, fieldValidation, fieldType string) (string, error) {
booleanCondition := ""
for _, roperand := range testElements.rightOperands {
if booleanCondition != "" {
booleanCondition += " || "
booleanCondition += " " + testElements.concatOperator + " "
Copy link

Copilot AI Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concatenation operator logic assumes testElements.concatOperator is always non-empty for multi-value validations, but the default case in test_elements.go sets it to empty string. This will cause malformed boolean expressions for unsupported multi-value operations.

Copilot uses AI. Check for mistakes.
}

booleanCondition += fmt.Sprintf("%s %s %s", testElements.leftOperand, testElements.operator, roperand)
Expand Down
13 changes: 13 additions & 0 deletions validgen/if_code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ func TestIfCode(t *testing.T) {
if !(obj.strField == "a" || obj.strField == "b" || obj.strField == "c") {
errs = append(errs, types.NewValidationError("strField must be one of 'a' 'b' 'c'"))
}
`,
},
{
name: "if code with string and not in",
args: args{
fieldName: "strField",
fieldType: "string",
fieldValidation: "nin=a b c",
},
want: `
if !(obj.strField != "a" && obj.strField != "b" && obj.strField != "c") {
errs = append(errs, types.NewValidationError("strField must not be one of 'a' 'b' 'c'"))
}
`,
},
}
Expand Down
1 change: 1 addition & 0 deletions validgen/parser_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func ParserValidation(fieldValidation string) (*Validation, error) {
"neq": ONE_VALUE,
"neq_ignore_case": ONE_VALUE,
"in": MANY_VALUES,
"nin": MANY_VALUES,
"email": ZERO_VALUE,
}

Expand Down
41 changes: 30 additions & 11 deletions validgen/test_elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import (
)

const (
EqIgnoreCaseTag = "eq_ignore_case"
NeqIgnoreCaseTag = "neq_ignore_case"
EqIgnoreCaseOp = "eq_ignore_case"
NeqIgnoreCaseOp = "neq_ignore_case"
InOp = "in"
NotInOp = "nin"
)

type TestElements struct {
leftOperand string
operator string
rightOperands []string
errorMessage string
leftOperand string
operator string
rightOperands []string
concatOperator string
errorMessage string
}

func GetTestElements(fieldName, fieldValidation, fieldType string) (TestElements, error) {
Expand All @@ -41,6 +44,7 @@ func GetTestElements(fieldName, fieldValidation, fieldType string) (TestElements
"neq,string": {"{{.Name}}", "!=", `"{{.Target}}"`, "{{.Name}} must not be equal to '{{.Target}}'"},
"neq_ignore_case,string": {"types.ToLower({{.Name}})", "!=", `"{{.Target}}"`, "{{.Name}} must not be equal to '{{.Target}}'"},
"in,string": {"{{.Name}}", "==", `"{{.Target}}"`, "{{.Name}} must be one of {{.Targets}}"},
"nin,string": {"{{.Name}}", "!=", `"{{.Target}}"`, "{{.Name}} must not be one of {{.Targets}}"},
"email,string": {"types.IsValidEmail({{.Name}})", "==", `true`, "{{.Name}} must be a valid email"},
}

Expand All @@ -54,7 +58,7 @@ func GetTestElements(fieldName, fieldValidation, fieldType string) (TestElements
return TestElements{}, types.NewValidationError("unsupported validation %s type %s", fieldValidation, fieldType)
}

if validation.Operation == EqIgnoreCaseTag || validation.Operation == NeqIgnoreCaseTag {
if validation.Operation == EqIgnoreCaseOp || validation.Operation == NeqIgnoreCaseOp {
for i := range validation.Values {
validation.Values[i] = strings.ToLower(validation.Values[i])
}
Expand All @@ -77,16 +81,31 @@ func GetTestElements(fieldName, fieldValidation, fieldType string) (TestElements
}
}

var concatOperator string
switch validation.Operation {
case InOp:
concatOperator = "||"
case NotInOp:
concatOperator = "&&"
default:
concatOperator = ""
}

if len(roperands) > 1 && concatOperator == "" {
return TestElements{}, types.NewValidationError("missed concat operator")
}

targetValues = strings.TrimSpace(targetValues)
errorMsg := condition.errorMessage
errorMsg = replaceNameAndTargetWithoutPrefix(errorMsg, fieldName, targetValue)
errorMsg = replaceTargetInErrors(errorMsg, targetValue, targetValues)

return TestElements{
leftOperand: replaceNameAndTargetWithPrefix(condition.loperand, fieldName, targetValue),
operator: condition.operator,
rightOperands: roperands,
errorMessage: errorMsg,
leftOperand: replaceNameAndTargetWithPrefix(condition.loperand, fieldName, targetValue),
operator: condition.operator,
rightOperands: roperands,
concatOperator: concatOperator,
errorMessage: errorMsg,
}, nil
}

Expand Down