Skip to content

Commit 50de3d8

Browse files
authored
Add regex pattern validation (#87)
1 parent 4ef98fb commit 50de3d8

10 files changed

Lines changed: 711 additions & 108 deletions

File tree

docs/validation.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ When `true`, no `Validate()` methods are generated. Use this if you don't need v
1919

2020
### `simple`
2121

22-
When `true`, all struct types use simple `validate.Struct()` validation instead of custom validation logic. This produces cleaner code but doesn't support advanced features like union type validation.
22+
When `true`, all struct types use simple `validate.Struct()` validation instead of custom validation logic. This produces cleaner code but doesn't support advanced features like union type validation or regex `pattern` constraints.
2323

2424
### `response`
2525

@@ -42,6 +42,11 @@ The following OpenAPI constraints are translated to validation tags:
4242
| `minItems` | `min=N` | arrays |
4343
| `maxItems` | `max=N` | arrays |
4444
| `enum` | custom switch | string, integer enums |
45+
| `pattern` | `runtime.ValidatePattern()` | strings |
46+
47+
`pattern` is a regex, which `validate.Struct()` cannot express, so it is enforced
48+
wherever per-field/per-item validation is generated (full-mode structs, arrays, maps).
49+
It is not enforced in `simple` mode (see below).
4550

4651
## Generated Code Examples
4752

examples/circular/mutual/gen.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/client/example1/example1/headers.go

Lines changed: 22 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/codegen/schema.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ func (s GoSchema) NeedsValidation() bool {
145145
return true
146146
}
147147

148+
// If it carries a regex pattern, it needs validation
149+
if s.needsPatternValidation() {
150+
return true
151+
}
152+
148153
// If it has union elements, it needs validation
149154
if len(s.UnionElements) > 0 {
150155
return true
@@ -157,6 +162,10 @@ func (s GoSchema) NeedsValidation() bool {
157162
if len(prop.Constraints.ValidationTags) > 0 {
158163
return true
159164
}
165+
// Property carries a regex pattern
166+
if prop.needsPatternValidation() {
167+
return true
168+
}
160169
// Property needs custom validation (RefType, struct, union, etc.)
161170
if prop.needsCustomValidation() {
162171
return true
@@ -216,6 +225,68 @@ func (s GoSchema) NeedsValidation() bool {
216225
return true
217226
}
218227

228+
// ValidateDecl generates the body of the Validate() method for this schema.
229+
// It returns the Go code that should appear inside the Validate() method.
230+
// The alias parameter is the receiver variable name (e.g., "p" for "func (p Person) Validate()").
231+
// The validatorVar parameter is the name of the validator variable to use (e.g., "bodyTypesValidate").
232+
func (s GoSchema) ValidateDecl(alias string, validatorVar string) string {
233+
return s.ValidateDeclWithOptions(alias, validatorVar, false)
234+
}
235+
236+
// ValidateDeclWithOptions generates the body of the Validate() method for this schema with options.
237+
// The forceSimple parameter forces the use of simple validation (validate.Struct()) even for complex types.
238+
func (s GoSchema) ValidateDeclWithOptions(alias string, validatorVar string, forceSimple bool) string {
239+
// If forceSimple is true, always use simple validation for structs
240+
if forceSimple && isStructType(s) {
241+
return generateSimpleStructValidation(s, alias, validatorVar)
242+
}
243+
244+
// OPTIMIZATION: If this is a struct with no unions anywhere in its tree,
245+
// AND no properties need custom validation (like RefTypes),
246+
// we can use the simple validate.Struct() approach instead of custom validation.
247+
// This is much cleaner and more efficient.
248+
if canUseSimpleStructValidation(s) {
249+
return generateSimpleStructValidation(s, alias, validatorVar)
250+
}
251+
252+
// Handle array types
253+
if isArrayType(s) {
254+
return generateArrayValidation(s, alias, validatorVar)
255+
}
256+
257+
// If this schema has a RefType set, it means it's a reference to another type
258+
// In this case, we should delegate validation to the underlying type
259+
if isRefTypeDelegation(s) {
260+
return generateRefTypeDelegation(s, alias)
261+
}
262+
263+
// If this schema has properties but GoType is a reference to another type
264+
// (not a struct/map/slice), delegate to the underlying type
265+
if isTypeAliasDelegation(s) {
266+
return generateTypeAliasDelegation(s, alias)
267+
}
268+
269+
// Handle map types (from additionalProperties)
270+
if isMapType(s) && !hasCustomValidation(s) {
271+
return generateMapValidation(s, alias, validatorVar)
272+
}
273+
274+
// For other non-struct types (slices, primitives) without custom validation
275+
if !hasCustomValidation(s) {
276+
return generateNonStructValidation(s, alias, validatorVar)
277+
}
278+
279+
// Generate custom validation for struct properties
280+
return generateCustomPropertyValidation(s, alias, validatorVar)
281+
}
282+
283+
// needsPatternValidation reports whether this schema carries an enforceable regex pattern.
284+
func (s GoSchema) needsPatternValidation() bool {
285+
return s.Constraints.hasPattern() &&
286+
isPatternValidatable(s.TypeDecl()) &&
287+
patternCompiles(*s.Constraints.Pattern)
288+
}
289+
219290
// ContainsUnions returns true if this schema or any of its nested schemas contain union types.
220291
// This is used to determine if we can use simple validate.Struct() or need custom validation logic.
221292
func (s GoSchema) ContainsUnions() bool {

pkg/codegen/schema_constraints.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ func (c Constraints) Count() int {
105105
return count
106106
}
107107

108+
// hasPattern reports whether these constraints carry a non-empty regex pattern.
109+
func (c Constraints) hasPattern() bool {
110+
return c.Pattern != nil && *c.Pattern != ""
111+
}
112+
108113
func newConstraints(schema *base.Schema, opts ConstraintsContext) Constraints {
109114
if schema == nil {
110115
return Constraints{}

pkg/codegen/schema_property.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ func (p Property) needsCustomValidation() bool {
162162
return true
163163
}
164164

165+
// needsPatternValidation reports whether this property carries an enforceable regex pattern
166+
// (resolved from its schema, including $ref targets).
167+
func (p Property) needsPatternValidation() bool {
168+
return p.Constraints.hasPattern() &&
169+
isPatternValidatable(p.Schema.TypeDecl()) &&
170+
patternCompiles(*p.Constraints.Pattern)
171+
}
172+
165173
func createPropertyGoFieldName(jsonName string, extensions map[string]any) string {
166174
goFieldName := jsonName
167175
if extension, ok := extensions[extGoName]; ok {

0 commit comments

Comments
 (0)