Skip to content

Commit 7c3e606

Browse files
authored
refact(validators) (#169)
* refact(validators) Readability improvements: * Introduced private constructors for all validators * All private constructors can use a *SchemaValidatorOptions (generalize options passing pattern) * All exported methods use private constructors (hide the implementation of options) * Readability: reduced the level of indentation with early exit conditions than nested conditions * Object validator: refactored Validate in smaller functions * Removed dead code/commented out code Minor efficiency improvements * In Applies method: reflect replaced by type switch whenever possible * Removed (mostly unusable) debugLog * In object validator: * pattern properties now use the regexp cache readily available * split path may be performed only once * In schema props: keep arrays of pointers []*SchemaValidator and avoid copying a new schema * In default & example validators: * map[string]bool -> map[string]struct{} * clear, don't reallocate the visited map * []validator slice -> [n]validator array * introduced a benchmark follow-up document, based on the existing Kubernetes API fixture Signed-off-by: Frederic BIDON <[email protected]> * fixed typo Signed-off-by: Frederic BIDON <[email protected]> --------- Signed-off-by: Frederic BIDON <[email protected]>
1 parent ff196e5 commit 7c3e606

24 files changed

+1342
-658
lines changed

BENCHMARK.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Benchmark
2+
3+
Validating the Kubernetes Swagger API
4+
5+
v0.22.6: 60,000,000 allocs
6+
```
7+
goos: linux
8+
goarch: amd64
9+
pkg: github.com/go-openapi/validate
10+
cpu: AMD Ryzen 7 5800X 8-Core Processor
11+
Benchmark_KubernetesSpec/validating_kubernetes_API-16 1 8549863982 ns/op 7067424936 B/op 59583275 allocs/op
12+
```
13+
14+
After refact PR: minor but noticable improvements: 25,000,000 allocs
15+
```
16+
go test -bench Spec
17+
goos: linux
18+
goarch: amd64
19+
pkg: github.com/go-openapi/validate
20+
cpu: AMD Ryzen 7 5800X 8-Core Processor
21+
Benchmark_KubernetesSpec/validating_kubernetes_API-16 1 4064535557 ns/op 3379715592 B/op 25320330 allocs/op
22+
```

benchmark_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package validate
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"github.com/go-openapi/loads"
8+
"github.com/go-openapi/strfmt"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func Benchmark_KubernetesSpec(b *testing.B) {
13+
fp := filepath.Join("fixtures", "go-swagger", "canary", "kubernetes", "swagger.json")
14+
doc, err := loads.Spec(fp)
15+
require.NoError(b, err)
16+
require.NotNil(b, doc)
17+
18+
b.Run("validating kubernetes API", func(b *testing.B) {
19+
b.ResetTimer()
20+
b.ReportAllocs()
21+
22+
for i := 0; i < b.N; i++ {
23+
validator := NewSpecValidator(doc.Schema(), strfmt.Default)
24+
res, _ := validator.Validate(doc)
25+
if res == nil || !res.IsValid() {
26+
b.FailNow()
27+
}
28+
}
29+
})
30+
}

default_validator.go

+51-34
Original file line numberDiff line numberDiff line change
@@ -25,48 +25,61 @@ import (
2525
// According to Swagger spec, default values MUST validate their schema.
2626
type defaultValidator struct {
2727
SpecValidator *SpecValidator
28-
visitedSchemas map[string]bool
28+
visitedSchemas map[string]struct{}
29+
schemaOptions *SchemaValidatorOptions
2930
}
3031

3132
// resetVisited resets the internal state of visited schemas
3233
func (d *defaultValidator) resetVisited() {
33-
d.visitedSchemas = map[string]bool{}
34+
if d.visitedSchemas == nil {
35+
d.visitedSchemas = make(map[string]struct{})
36+
37+
return
38+
}
39+
40+
// TODO(go1.21): clear(ex.visitedSchemas)
41+
for k := range d.visitedSchemas {
42+
delete(d.visitedSchemas, k)
43+
}
3444
}
3545

36-
func isVisited(path string, visitedSchemas map[string]bool) bool {
37-
found := visitedSchemas[path]
38-
if !found {
39-
// search for overlapping paths
40-
frags := strings.Split(path, ".")
41-
if len(frags) < 2 {
42-
// shortcut exit on smaller paths
43-
return found
46+
func isVisited(path string, visitedSchemas map[string]struct{}) bool {
47+
_, found := visitedSchemas[path]
48+
if found {
49+
return true
50+
}
51+
52+
// search for overlapping paths
53+
frags := strings.Split(path, ".")
54+
if len(frags) < 2 {
55+
// shortcut exit on smaller paths
56+
return found
57+
}
58+
last := len(frags) - 1
59+
var currentFragStr, parent string
60+
for i := range frags {
61+
if i == 0 {
62+
currentFragStr = frags[last]
63+
} else {
64+
currentFragStr = strings.Join([]string{frags[last-i], currentFragStr}, ".")
4465
}
45-
last := len(frags) - 1
46-
var currentFragStr, parent string
47-
for i := range frags {
48-
if i == 0 {
49-
currentFragStr = frags[last]
50-
} else {
51-
currentFragStr = strings.Join([]string{frags[last-i], currentFragStr}, ".")
52-
}
53-
if i < last {
54-
parent = strings.Join(frags[0:last-i], ".")
55-
} else {
56-
parent = ""
57-
}
58-
if strings.HasSuffix(parent, currentFragStr) {
59-
found = true
60-
break
61-
}
66+
if i < last {
67+
parent = strings.Join(frags[0:last-i], ".")
68+
} else {
69+
parent = ""
70+
}
71+
if strings.HasSuffix(parent, currentFragStr) {
72+
found = true
73+
break
6274
}
6375
}
76+
6477
return found
6578
}
6679

6780
// beingVisited asserts a schema is being visited
6881
func (d *defaultValidator) beingVisited(path string) {
69-
d.visitedSchemas[path] = true
82+
d.visitedSchemas[path] = struct{}{}
7083
}
7184

7285
// isVisited tells if a path has already been visited
@@ -75,8 +88,8 @@ func (d *defaultValidator) isVisited(path string) bool {
7588
}
7689

7790
// Validate validates the default values declared in the swagger spec
78-
func (d *defaultValidator) Validate() (errs *Result) {
79-
errs = new(Result)
91+
func (d *defaultValidator) Validate() *Result {
92+
errs := new(Result)
8093
if d == nil || d.SpecValidator == nil {
8194
return errs
8295
}
@@ -107,7 +120,7 @@ func (d *defaultValidator) validateDefaultValueValidAgainstSchema() *Result {
107120
// default values provided must validate against their inline definition (no explicit schema)
108121
if param.Default != nil && param.Schema == nil {
109122
// check param default value is valid
110-
red := NewParamValidator(&param, s.KnownFormats).Validate(param.Default) //#nosec
123+
red := newParamValidator(&param, s.KnownFormats, d.schemaOptions).Validate(param.Default) //#nosec
111124
if red.HasErrorsOrWarnings() {
112125
res.AddErrors(defaultValueDoesNotValidateMsg(param.Name, param.In))
113126
res.Merge(red)
@@ -176,7 +189,7 @@ func (d *defaultValidator) validateDefaultInResponse(resp *spec.Response, respon
176189
d.resetVisited()
177190

178191
if h.Default != nil {
179-
red := NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default) //#nosec
192+
red := newHeaderValidator(nm, &h, s.KnownFormats, d.schemaOptions).Validate(h.Default) //#nosec
180193
if red.HasErrorsOrWarnings() {
181194
res.AddErrors(defaultValueHeaderDoesNotValidateMsg(operationID, nm, responseName))
182195
res.Merge(red)
@@ -223,7 +236,9 @@ func (d *defaultValidator) validateDefaultValueSchemaAgainstSchema(path, in stri
223236
s := d.SpecValidator
224237

225238
if schema.Default != nil {
226-
res.Merge(NewSchemaValidator(schema, s.spec.Spec(), path+".default", s.KnownFormats, SwaggerSchema(true)).Validate(schema.Default))
239+
res.Merge(
240+
newSchemaValidator(schema, s.spec.Spec(), path+".default", s.KnownFormats, d.schemaOptions).Validate(schema.Default),
241+
)
227242
}
228243
if schema.Items != nil {
229244
if schema.Items.Schema != nil {
@@ -267,7 +282,9 @@ func (d *defaultValidator) validateDefaultValueItemsAgainstSchema(path, in strin
267282
s := d.SpecValidator
268283
if items != nil {
269284
if items.Default != nil {
270-
res.Merge(newItemsValidator(path, in, items, root, s.KnownFormats).Validate(0, items.Default))
285+
res.Merge(
286+
newItemsValidator(path, in, items, root, s.KnownFormats, d.schemaOptions).Validate(0, items.Default),
287+
)
271288
}
272289
if items.Items != nil {
273290
res.Merge(d.validateDefaultValueItemsAgainstSchema(path+"[0].default", in, root, items.Items))

doc_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func ExampleSpec_second() {
6565
fmt.Printf("This spec has some validation errors: %v\n", errs)
6666
}
6767
} else {
68-
fmt.Println("Could not load this spec")
68+
fmt.Println("Could not load this spec:", err)
6969
}
7070

7171
// Output:

example_validator.go

+28-11
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,27 @@ import (
2323
// ExampleValidator validates example values defined in a spec
2424
type exampleValidator struct {
2525
SpecValidator *SpecValidator
26-
visitedSchemas map[string]bool
26+
visitedSchemas map[string]struct{}
27+
schemaOptions *SchemaValidatorOptions
2728
}
2829

2930
// resetVisited resets the internal state of visited schemas
3031
func (ex *exampleValidator) resetVisited() {
31-
ex.visitedSchemas = map[string]bool{}
32+
if ex.visitedSchemas == nil {
33+
ex.visitedSchemas = make(map[string]struct{})
34+
35+
return
36+
}
37+
38+
// TODO(go1.21): clear(ex.visitedSchemas)
39+
for k := range ex.visitedSchemas {
40+
delete(ex.visitedSchemas, k)
41+
}
3242
}
3343

3444
// beingVisited asserts a schema is being visited
3545
func (ex *exampleValidator) beingVisited(path string) {
36-
ex.visitedSchemas[path] = true
46+
ex.visitedSchemas[path] = struct{}{}
3747
}
3848

3949
// isVisited tells if a path has already been visited
@@ -48,8 +58,8 @@ func (ex *exampleValidator) isVisited(path string) bool {
4858
// - schemas
4959
// - individual property
5060
// - responses
51-
func (ex *exampleValidator) Validate() (errs *Result) {
52-
errs = new(Result)
61+
func (ex *exampleValidator) Validate() *Result {
62+
errs := new(Result)
5363
if ex == nil || ex.SpecValidator == nil {
5464
return errs
5565
}
@@ -82,7 +92,7 @@ func (ex *exampleValidator) validateExampleValueValidAgainstSchema() *Result {
8292
// default values provided must validate against their inline definition (no explicit schema)
8393
if param.Example != nil && param.Schema == nil {
8494
// check param default value is valid
85-
red := NewParamValidator(&param, s.KnownFormats).Validate(param.Example) //#nosec
95+
red := newParamValidator(&param, s.KnownFormats, ex.schemaOptions).Validate(param.Example) //#nosec
8696
if red.HasErrorsOrWarnings() {
8797
res.AddWarnings(exampleValueDoesNotValidateMsg(param.Name, param.In))
8898
res.MergeAsWarnings(red)
@@ -151,7 +161,7 @@ func (ex *exampleValidator) validateExampleInResponse(resp *spec.Response, respo
151161
ex.resetVisited()
152162

153163
if h.Example != nil {
154-
red := NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Example) //#nosec
164+
red := newHeaderValidator(nm, &h, s.KnownFormats, ex.schemaOptions).Validate(h.Example) //#nosec
155165
if red.HasErrorsOrWarnings() {
156166
res.AddWarnings(exampleValueHeaderDoesNotValidateMsg(operationID, nm, responseName))
157167
res.MergeAsWarnings(red)
@@ -189,7 +199,9 @@ func (ex *exampleValidator) validateExampleInResponse(resp *spec.Response, respo
189199
if response.Examples != nil {
190200
if response.Schema != nil {
191201
if example, ok := response.Examples["application/json"]; ok {
192-
res.MergeAsWarnings(NewSchemaValidator(response.Schema, s.spec.Spec(), path+".examples", s.KnownFormats, SwaggerSchema(true)).Validate(example))
202+
res.MergeAsWarnings(
203+
newSchemaValidator(response.Schema, s.spec.Spec(), path+".examples", s.KnownFormats, s.schemaOptions).Validate(example),
204+
)
193205
} else {
194206
// TODO: validate other media types too
195207
res.AddWarnings(examplesMimeNotSupportedMsg(operationID, responseName))
@@ -211,7 +223,9 @@ func (ex *exampleValidator) validateExampleValueSchemaAgainstSchema(path, in str
211223
res := new(Result)
212224

213225
if schema.Example != nil {
214-
res.MergeAsWarnings(NewSchemaValidator(schema, s.spec.Spec(), path+".example", s.KnownFormats, SwaggerSchema(true)).Validate(schema.Example))
226+
res.MergeAsWarnings(
227+
newSchemaValidator(schema, s.spec.Spec(), path+".example", s.KnownFormats, ex.schemaOptions).Validate(schema.Example),
228+
)
215229
}
216230
if schema.Items != nil {
217231
if schema.Items.Schema != nil {
@@ -256,14 +270,17 @@ func (ex *exampleValidator) validateExampleValueItemsAgainstSchema(path, in stri
256270
s := ex.SpecValidator
257271
if items != nil {
258272
if items.Example != nil {
259-
res.MergeAsWarnings(newItemsValidator(path, in, items, root, s.KnownFormats).Validate(0, items.Example))
273+
res.MergeAsWarnings(
274+
newItemsValidator(path, in, items, root, s.KnownFormats, ex.schemaOptions).Validate(0, items.Example),
275+
)
260276
}
261277
if items.Items != nil {
262-
res.Merge(ex.validateExampleValueItemsAgainstSchema(path+"[0].example", in, root, items.Items))
278+
res.Merge(ex.validateExampleValueItemsAgainstSchema(path+"[0].example", in, root, items.Items)) // TODO(fred): intern string
263279
}
264280
if _, err := compileRegexp(items.Pattern); err != nil {
265281
res.AddErrors(invalidPatternInMsg(path, in, items.Pattern))
266282
}
267283
}
284+
268285
return res
269286
}

formats.go

+33-23
Original file line numberDiff line numberDiff line change
@@ -22,48 +22,58 @@ import (
2222
)
2323

2424
type formatValidator struct {
25-
Format string
2625
Path string
2726
In string
27+
Format string
2828
KnownFormats strfmt.Registry
29+
Options *SchemaValidatorOptions
30+
}
31+
32+
func newFormatValidator(path, in, format string, formats strfmt.Registry, opts *SchemaValidatorOptions) *formatValidator {
33+
if opts == nil {
34+
opts = new(SchemaValidatorOptions)
35+
}
36+
37+
f := new(formatValidator)
38+
39+
f.Path = path
40+
f.In = in
41+
f.Format = format
42+
f.KnownFormats = formats
43+
f.Options = opts
44+
45+
return f
2946
}
3047

3148
func (f *formatValidator) SetPath(path string) {
3249
f.Path = path
3350
}
3451

3552
func (f *formatValidator) Applies(source interface{}, kind reflect.Kind) bool {
36-
doit := func() bool {
37-
if source == nil {
38-
return false
39-
}
40-
switch source := source.(type) {
41-
case *spec.Items:
42-
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
43-
case *spec.Parameter:
44-
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
45-
case *spec.Schema:
46-
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
47-
case *spec.Header:
48-
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
49-
}
53+
if source == nil || f.KnownFormats == nil {
54+
return false
55+
}
56+
57+
switch source := source.(type) {
58+
case *spec.Items:
59+
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
60+
case *spec.Parameter:
61+
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
62+
case *spec.Schema:
63+
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
64+
case *spec.Header:
65+
return kind == reflect.String && f.KnownFormats.ContainsName(source.Format)
66+
default:
5067
return false
5168
}
52-
r := doit()
53-
debugLog("format validator for %q applies %t for %T (kind: %v)\n", f.Path, r, source, kind)
54-
return r
5569
}
5670

5771
func (f *formatValidator) Validate(val interface{}) *Result {
5872
result := new(Result)
59-
debugLog("validating \"%v\" against format: %s", val, f.Format)
6073

6174
if err := FormatOf(f.Path, f.In, f.Format, val.(string), f.KnownFormats); err != nil {
6275
result.AddErrors(err)
6376
}
6477

65-
if result.HasErrors() {
66-
return result
67-
}
68-
return nil
78+
return result
6979
}

0 commit comments

Comments
 (0)