diff --git a/README.md b/README.md index c2226498c..3596289db 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,11 @@ Be sure to check [OpenAPI Initiative](https://github.com/OAI)'s [great tooling l * Generates `*openapi3.Schema` values for Go types. # Some recipes +## Validating an OpenAPI document +```shell +go run github.com/getkin/kin-openapi/cmd/validate@latest [--defaults] [--examples] [--ext] [--patterns] -- +``` + ## Loading OpenAPI document Use `openapi3.Loader`, which resolves all references: ```go @@ -196,6 +201,11 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.112.0 +* `(openapi3.ValidationOptions).ExamplesValidationDisabled` has been unexported. +* `(openapi3.ValidationOptions).SchemaFormatValidationEnabled` has been unexported. +* `(openapi3.ValidationOptions).SchemaPatternValidationDisabled` has been unexported. + ### v0.111.0 * Changed `func (*_) Validate(ctx context.Context) error` to `func (*_) Validate(ctx context.Context, opts ...ValidationOption) error`. * `openapi3.WithValidationOptions(ctx context.Context, opts *ValidationOptions) context.Context` prototype changed to `openapi3.WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context`. diff --git a/cmd/validate/main.go b/cmd/validate/main.go new file mode 100644 index 000000000..9759564fa --- /dev/null +++ b/cmd/validate/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "flag" + "log" + "os" + "strings" + + "github.com/invopop/yaml" + + "github.com/getkin/kin-openapi/openapi2" + "github.com/getkin/kin-openapi/openapi3" +) + +var ( + defaultDefaults = true + defaults = flag.Bool("defaults", defaultDefaults, "when false, disables schemas' default field validation") +) + +var ( + defaultExamples = true + examples = flag.Bool("examples", defaultExamples, "when false, disables all example schema validation") +) + +var ( + defaultExt = false + ext = flag.Bool("ext", defaultExt, "enables visiting other files") +) + +var ( + defaultPatterns = true + patterns = flag.Bool("patterns", defaultPatterns, "when false, allows schema patterns unsupported by the Go regexp engine") +) + +func main() { + flag.Parse() + filename := flag.Arg(0) + if len(flag.Args()) != 1 || filename == "" { + log.Fatalf("Usage: go run github.com/getkin/kin-openapi/cmd/validate@latest [--defaults] [--examples] [--ext] [--patterns] -- \nGot: %+v\n", os.Args) + } + + data, err := os.ReadFile(filename) + if err != nil { + log.Fatal(err) + } + + var vd struct { + OpenAPI string `json:"openapi" yaml:"openapi"` + Swagger string `json:"swagger" yaml:"swagger"` + } + if err := yaml.Unmarshal(data, &vd); err != nil { + log.Fatal(err) + } + + switch { + case vd.OpenAPI == "3" || strings.HasPrefix(vd.OpenAPI, "3."): + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = *ext + + doc, err := loader.LoadFromFile(filename) + if err != nil { + log.Fatal(err) + } + + var opts []openapi3.ValidationOption + if !*defaults { + opts = append(opts, openapi3.DisableSchemaDefaultsValidation()) + } + if !*examples { + opts = append(opts, openapi3.DisableExamplesValidation()) + } + if !*patterns { + opts = append(opts, openapi3.DisableSchemaPatternValidation()) + } + + if err = doc.Validate(loader.Context, opts...); err != nil { + log.Fatal(err) + } + + case vd.Swagger == "2" || strings.HasPrefix(vd.Swagger, "2."): + if *defaults != defaultDefaults { + log.Fatal("Flag --defaults is only for OpenAPIv3") + } + if *examples != defaultExamples { + log.Fatal("Flag --examples is only for OpenAPIv3") + } + if *ext != defaultExt { + log.Fatal("Flag --ext is only for OpenAPIv3") + } + if *patterns != defaultPatterns { + log.Fatal("Flag --patterns is only for OpenAPIv3") + } + + var doc openapi2.T + if err := yaml.Unmarshal(data, &doc); err != nil { + log.Fatal(err) + } + + default: + log.Fatal("Missing or incorrect 'openapi' or 'swagger' field") + } +} diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 8e3fef7e6..090be7657 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -90,7 +90,7 @@ func (mediaType *MediaType) Validate(ctx context.Context, opts ...ValidationOpti return errors.New("example and examples are mutually exclusive") } - if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled { + if vo := getValidationOptions(ctx); vo.examplesValidationDisabled { return nil } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index c55af474d..04e13b203 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -323,7 +323,7 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name) } - if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled { + if vo := getValidationOptions(ctx); vo.examplesValidationDisabled { return nil } if example := parameter.Example; example != nil { diff --git a/openapi3/request_body.go b/openapi3/request_body.go index baf7f81e8..f0d9e1ec2 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -112,7 +112,7 @@ func (requestBody *RequestBody) Validate(ctx context.Context, opts ...Validation return errors.New("content of the request body is required") } - if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled { + if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled { vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false } diff --git a/openapi3/response.go b/openapi3/response.go index eaf8e57f8..324f77ddc 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -119,7 +119,7 @@ func (response *Response) Validate(ctx context.Context, opts ...ValidationOption if response.Description == nil { return errors.New("a short description of the response is required") } - if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled { + if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled { vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true } diff --git a/openapi3/schema.go b/openapi3/schema.go index bbce46c7b..f2b4ea2c8 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -678,7 +678,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "float", "double": default: - if validationOpts.SchemaFormatValidationEnabled { + if validationOpts.schemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -688,7 +688,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "int32", "int64": default: - if validationOpts.SchemaFormatValidationEnabled { + if validationOpts.schemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -710,12 +710,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: // Try to check for custom defined formats - if _, ok := SchemaStringFormats[format]; !ok && validationOpts.SchemaFormatValidationEnabled { + if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { return unsupportedFormat(format) } } } - if schema.Pattern != "" && !validationOpts.SchemaPatternValidationDisabled { + if schema.Pattern != "" && !validationOpts.schemaPatternValidationDisabled { if err = schema.compilePattern(); err != nil { return err } @@ -771,13 +771,13 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } - if v := schema.Default; v != nil { + if v := schema.Default; v != nil && !validationOpts.schemaDefaultsValidationDisabled { if err := schema.VisitJSON(v); err != nil { return fmt.Errorf("invalid default: %w", err) } } - if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled { + if x := schema.Example; x != nil && !validationOpts.examplesValidationDisabled { if err := validateExampleValue(ctx, x, schema); err != nil { return fmt.Errorf("invalid example: %w", err) } diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index a74364dae..343b6836e 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -7,10 +7,11 @@ type ValidationOption func(options *ValidationOptions) // ValidationOptions provides configuration for validating OpenAPI documents. type ValidationOptions struct { - SchemaFormatValidationEnabled bool - SchemaPatternValidationDisabled bool - ExamplesValidationDisabled bool examplesValidationAsReq, examplesValidationAsRes bool + examplesValidationDisabled bool + schemaDefaultsValidationDisabled bool + schemaFormatValidationEnabled bool + schemaPatternValidationDisabled bool } type validationOptionsKey struct{} @@ -19,7 +20,7 @@ type validationOptionsKey struct{} // By default, schema format validation is disabled. func EnableSchemaFormatValidation() ValidationOption { return func(options *ValidationOptions) { - options.SchemaFormatValidationEnabled = true + options.schemaFormatValidationEnabled = true } } @@ -27,7 +28,7 @@ func EnableSchemaFormatValidation() ValidationOption { // By default, schema format validation is disabled. func DisableSchemaFormatValidation() ValidationOption { return func(options *ValidationOptions) { - options.SchemaFormatValidationEnabled = false + options.schemaFormatValidationEnabled = false } } @@ -35,14 +36,30 @@ func DisableSchemaFormatValidation() ValidationOption { // By default, schema pattern validation is enabled. func EnableSchemaPatternValidation() ValidationOption { return func(options *ValidationOptions) { - options.SchemaPatternValidationDisabled = false + options.schemaPatternValidationDisabled = false } } // DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. func DisableSchemaPatternValidation() ValidationOption { return func(options *ValidationOptions) { - options.SchemaPatternValidationDisabled = true + options.schemaPatternValidationDisabled = true + } +} + +// EnableSchemaDefaultsValidation does the opposite of DisableSchemaDefaultsValidation. +// By default, schema default values are validated against their schema. +func EnableSchemaDefaultsValidation() ValidationOption { + return func(options *ValidationOptions) { + options.schemaDefaultsValidationDisabled = false + } +} + +// DisableSchemaDefaultsValidation disables schemas' default field validation. +// By default, schema default values are validated against their schema. +func DisableSchemaDefaultsValidation() ValidationOption { + return func(options *ValidationOptions) { + options.schemaDefaultsValidationDisabled = true } } @@ -50,7 +67,7 @@ func DisableSchemaPatternValidation() ValidationOption { // By default, all schema examples are validated. func EnableExamplesValidation() ValidationOption { return func(options *ValidationOptions) { - options.ExamplesValidationDisabled = false + options.examplesValidationDisabled = false } } @@ -58,7 +75,7 @@ func EnableExamplesValidation() ValidationOption { // By default, all schema examples are validated. func DisableExamplesValidation() ValidationOption { return func(options *ValidationOptions) { - options.ExamplesValidationDisabled = true + options.examplesValidationDisabled = true } }