diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index c68529d34..000000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-*.yml text eol=lf
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index c828e2ff3..000000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-# These are supported funding model platforms
-
-github: [fenollp] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
-#patreon: # Replace with a single Patreon username
-#open_collective: # Replace with a single Open Collective username
-#ko_fi: # Replace with a single Ko-fi username
-#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
-#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
-#liberapay: # Replace with a single Liberapay username
-#issuehunt: # Replace with a single IssueHunt username
-#otechie: # Replace with a single Otechie username
-#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/docs/openapi2.txt b/.github/docs/openapi2.txt
deleted file mode 100644
index e2ac28b20..000000000
--- a/.github/docs/openapi2.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-type Header struct{ ... }
-type Operation struct{ ... }
-type Parameter struct{ ... }
-type Parameters []*Parameter
-type PathItem struct{ ... }
-type Response struct{ ... }
-type SecurityRequirements []map[string][]string
-type SecurityScheme struct{ ... }
-type T struct{ ... }
diff --git a/.github/docs/openapi2conv.txt b/.github/docs/openapi2conv.txt
deleted file mode 100644
index e7c1db1bc..000000000
--- a/.github/docs/openapi2conv.txt
+++ /dev/null
@@ -1,25 +0,0 @@
-func FromV3(doc3 *openapi3.T) (*openapi2.T, error)
-func FromV3Headers(defs openapi3.Headers, components *openapi3.Components) (map[string]*openapi2.Header, error)
-func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2.Operation, error)
-func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components) (*openapi2.Parameter, error)
-func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.PathItem, error)
-func FromV3Ref(ref string) string
-func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, ...) (*openapi2.Parameter, error)
-func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameters
-func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) (*openapi2.Response, error)
-func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error)
-func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter)
-func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter)
-func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements
-func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error)
-func ToV3(doc2 *openapi2.T) (*openapi3.T, error)
-func ToV3Headers(defs map[string]*openapi2.Header) openapi3.Headers
-func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, ...) (*openapi3.Operation, error)
-func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Parameter, ...) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, ...)
-func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, ...) (*openapi3.PathItem, error)
-func ToV3Ref(ref string) string
-func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.ResponseRef, error)
-func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef
-func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef
-func ToV3SecurityRequirements(requirements openapi2.SecurityRequirements) openapi3.SecurityRequirements
-func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.SecuritySchemeRef, error)
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
deleted file mode 100644
index 82a5e0bc8..000000000
--- a/.github/docs/openapi3.txt
+++ /dev/null
@@ -1,155 +0,0 @@
-const ParameterInPath = "path" ...
-const TypeArray = "array" ...
-const FormatOfStringForUUIDOfRFC4122 = `^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$` ...
-const SerializationSimple = "simple" ...
-var SchemaErrorDetailsDisabled = false ...
-var CircularReferenceCounter = 3
-var CircularReferenceError = "kin-openapi bug found: circular schema reference not handled"
-var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile))
-var ErrURINotSupported = errors.New("unsupported URI")
-var IdentifierRegExp = regexp.MustCompile(identifierPattern)
-var SchemaStringFormats = make(map[string]Format, 4)
-func BoolPtr(value bool) *bool
-func DefaultRefNameResolver(ref string) string
-func DefineIPv4Format()
-func DefineIPv6Format()
-func DefineStringFormat(name string, pattern string)
-func DefineStringFormatCallback(name string, callback FormatCallback)
-func Float64Ptr(value float64) *float64
-func Int64Ptr(value int64) *int64
-func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error)
-func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker)
-func Uint64Ptr(value uint64) *uint64
-func ValidateIdentifier(value string) error
-func WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context
-type AdditionalProperties struct{ ... }
-type Callback map[string]*PathItem
-type CallbackRef struct{ ... }
-type Callbacks map[string]*CallbackRef
-type Components struct{ ... }
- func NewComponents() Components
-type Contact struct{ ... }
-type Content map[string]*MediaType
- func NewContent() Content
- func NewContentWithFormDataSchema(schema *Schema) Content
- func NewContentWithFormDataSchemaRef(schema *SchemaRef) Content
- func NewContentWithJSONSchema(schema *Schema) Content
- func NewContentWithJSONSchemaRef(schema *SchemaRef) Content
- func NewContentWithSchema(schema *Schema, consumes []string) Content
- func NewContentWithSchemaRef(schema *SchemaRef, consumes []string) Content
-type Discriminator struct{ ... }
-type Encoding struct{ ... }
- func NewEncoding() *Encoding
-type Example struct{ ... }
- func NewExample(value interface{}) *Example
-type ExampleRef struct{ ... }
-type Examples map[string]*ExampleRef
-type ExternalDocs struct{ ... }
-type Format struct{ ... }
-type FormatCallback func(value string) error
-type Header struct{ ... }
-type HeaderRef struct{ ... }
-type Headers map[string]*HeaderRef
-type Info struct{ ... }
-type License struct{ ... }
-type Link struct{ ... }
-type LinkRef struct{ ... }
-type Links map[string]*LinkRef
-type Loader struct{ ... }
- func NewLoader() *Loader
-type MediaType struct{ ... }
- func NewMediaType() *MediaType
-type MultiError []error
-type OAuthFlow struct{ ... }
-type OAuthFlows struct{ ... }
-type Operation struct{ ... }
- func NewOperation() *Operation
-type Parameter struct{ ... }
- func NewCookieParameter(name string) *Parameter
- func NewHeaderParameter(name string) *Parameter
- func NewPathParameter(name string) *Parameter
- func NewQueryParameter(name string) *Parameter
-type ParameterRef struct{ ... }
-type Parameters []*ParameterRef
- func NewParameters() Parameters
-type ParametersMap map[string]*ParameterRef
-type PathItem struct{ ... }
-type Paths map[string]*PathItem
-type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
- func ReadFromHTTP(cl *http.Client) ReadFromURIFunc
- func ReadFromURIs(readers ...ReadFromURIFunc) ReadFromURIFunc
- func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc
-type Ref struct{ ... }
-type RefNameResolver func(string) string
-type RequestBodies map[string]*RequestBodyRef
-type RequestBody struct{ ... }
- func NewRequestBody() *RequestBody
-type RequestBodyRef struct{ ... }
-type Response struct{ ... }
- func NewResponse() *Response
-type ResponseRef struct{ ... }
-type Responses map[string]*ResponseRef
- func NewResponses() Responses
-type Schema struct{ ... }
- func NewAllOfSchema(schemas ...*Schema) *Schema
- func NewAnyOfSchema(schemas ...*Schema) *Schema
- func NewArraySchema() *Schema
- func NewBoolSchema() *Schema
- func NewBytesSchema() *Schema
- func NewDateTimeSchema() *Schema
- func NewFloat64Schema() *Schema
- func NewInt32Schema() *Schema
- func NewInt64Schema() *Schema
- func NewIntegerSchema() *Schema
- func NewObjectSchema() *Schema
- func NewOneOfSchema(schemas ...*Schema) *Schema
- func NewSchema() *Schema
- func NewStringSchema() *Schema
- func NewUUIDSchema() *Schema
-type SchemaError struct{ ... }
-type SchemaRef struct{ ... }
- func NewSchemaRef(ref string, value *Schema) *SchemaRef
-type SchemaRefs []*SchemaRef
-type SchemaValidationOption func(*schemaValidationSettings)
- func DefaultsSet(f func()) SchemaValidationOption
- func DisablePatternValidation() SchemaValidationOption
- func DisableReadOnlyValidation() SchemaValidationOption
- func DisableWriteOnlyValidation() SchemaValidationOption
- func EnableFormatValidation() SchemaValidationOption
- func FailFast() SchemaValidationOption
- func MultiErrors() SchemaValidationOption
- func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaValidationOption
- func VisitAsRequest() SchemaValidationOption
- func VisitAsResponse() SchemaValidationOption
-type Schemas map[string]*SchemaRef
-type SecurityRequirement map[string][]string
- func NewSecurityRequirement() SecurityRequirement
-type SecurityRequirements []SecurityRequirement
- func NewSecurityRequirements() *SecurityRequirements
-type SecurityScheme struct{ ... }
- func NewCSRFSecurityScheme() *SecurityScheme
- func NewJWTSecurityScheme() *SecurityScheme
- func NewOIDCSecurityScheme(oidcUrl string) *SecurityScheme
- func NewSecurityScheme() *SecurityScheme
-type SecuritySchemeRef struct{ ... }
-type SecuritySchemes map[string]*SecuritySchemeRef
-type SerializationMethod struct{ ... }
-type Server struct{ ... }
-type ServerVariable struct{ ... }
-type Servers []*Server
-type SliceUniqueItemsChecker func(items []interface{}) bool
-type T struct{ ... }
-type Tag struct{ ... }
-type Tags []*Tag
-type ValidationOption func(options *ValidationOptions)
- func AllowExtraSiblingFields(fields ...string) ValidationOption
- func DisableExamplesValidation() ValidationOption
- func DisableSchemaDefaultsValidation() ValidationOption
- func DisableSchemaFormatValidation() ValidationOption
- func DisableSchemaPatternValidation() ValidationOption
- func EnableExamplesValidation() ValidationOption
- func EnableSchemaDefaultsValidation() ValidationOption
- func EnableSchemaFormatValidation() ValidationOption
- func EnableSchemaPatternValidation() ValidationOption
-type ValidationOptions struct{ ... }
-type XML struct{ ... }
diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt
deleted file mode 100644
index ac738e75a..000000000
--- a/.github/docs/openapi3filter.txt
+++ /dev/null
@@ -1,54 +0,0 @@
-const ErrCodeOK = 0 ...
-var DefaultOptions = &Options{}
-var ErrAuthenticationServiceMissing = errors.New("missing AuthenticationFunc")
-var ErrInvalidEmptyValue = errors.New("empty value is not allowed")
-var ErrInvalidRequired = errors.New("value is required but missing")
-var JSONPrefixes = []string{ ... }
-func DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter)
-func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, ...) (interface{}, error)
-func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error
-func RegisterBodyDecoder(contentType string, decoder BodyDecoder)
-func RegisterBodyEncoder(contentType string, encoder BodyEncoder)
-func TrimJSONPrefix(data []byte) []byte
-func UnregisterBodyDecoder(contentType string)
-func UnregisterBodyEncoder(contentType string)
-func ValidateParameter(ctx context.Context, input *RequestValidationInput, ...) error
-func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err error)
-func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, ...) error
-func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
-func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, ...) error
-type AuthenticationFunc func(context.Context, *AuthenticationInput) error
-type AuthenticationInput struct{ ... }
-type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (interface{}, error)
- func RegisteredBodyDecoder(contentType string) BodyDecoder
-type BodyEncoder func(body interface{}) ([]byte, error)
- func RegisteredBodyEncoder(contentType string) BodyEncoder
-type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error)
-type CustomSchemaErrorFunc func(err *openapi3.SchemaError) string
-type EncodingFn func(partName string) *openapi3.Encoding
-type ErrCode int
-type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error)
-type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter)
-type Headerer interface{ ... }
-type LogFunc func(message string, err error)
-type Options struct{ ... }
-type ParseError struct{ ... }
-type ParseErrorKind int
- const KindOther ParseErrorKind = iota ...
-type RequestError struct{ ... }
-type RequestValidationInput struct{ ... }
-type ResponseError struct{ ... }
-type ResponseValidationInput struct{ ... }
-type SecurityRequirementsError struct{ ... }
-type StatusCoder interface{ ... }
-type ValidationError struct{ ... }
-type ValidationErrorEncoder struct{ ... }
-type ValidationErrorSource struct{ ... }
-type ValidationHandler struct{ ... }
-type Validator struct{ ... }
- func NewValidator(router routers.Router, options ...ValidatorOption) *Validator
-type ValidatorOption func(*Validator)
- func OnErr(f ErrFunc) ValidatorOption
- func OnLog(f LogFunc) ValidatorOption
- func Strict(strict bool) ValidatorOption
- func ValidationOptions(options Options) ValidatorOption
diff --git a/.github/docs/openapi3filter_fixtures.txt b/.github/docs/openapi3filter_fixtures.txt
deleted file mode 100644
index e69de29bb..000000000
diff --git a/.github/docs/openapi3gen.txt b/.github/docs/openapi3gen.txt
deleted file mode 100644
index 741a5043f..000000000
--- a/.github/docs/openapi3gen.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-var RefSchemaRef = openapi3.NewSchemaRef("Ref", ...)
-func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error)
-type CycleError struct{}
-type ExcludeSchemaSentinel struct{}
-type Generator struct{ ... }
- func NewGenerator(opts ...Option) *Generator
-type Option func(*generatorOpt)
- func SchemaCustomizer(sc SchemaCustomizerFn) Option
- func ThrowErrorOnCycle() Option
- func UseAllExportedFields() Option
-type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error
diff --git a/.github/docs/routers.txt b/.github/docs/routers.txt
deleted file mode 100644
index fdd5a9dda..000000000
--- a/.github/docs/routers.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-var ErrMethodNotAllowed error = &RouteError{ ... }
-var ErrPathNotFound error = &RouteError{ ... }
-type Route struct{ ... }
-type RouteError struct{ ... }
-type Router interface{ ... }
diff --git a/.github/docs/routers_gorillamux.txt b/.github/docs/routers_gorillamux.txt
deleted file mode 100644
index 82aad106d..000000000
--- a/.github/docs/routers_gorillamux.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-func NewRouter(doc *openapi3.T) (routers.Router, error)
-type Router struct{ ... }
diff --git a/.github/docs/routers_legacy.txt b/.github/docs/routers_legacy.txt
deleted file mode 100644
index 71017a6c1..000000000
--- a/.github/docs/routers_legacy.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-func NewRouter(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Router, error)
-type Router struct{ ... }
-type Routers []*Router
diff --git a/.github/docs/routers_legacy_pathpattern.txt b/.github/docs/routers_legacy_pathpattern.txt
deleted file mode 100644
index 7ad87f2b5..000000000
--- a/.github/docs/routers_legacy_pathpattern.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-const SuffixKindConstant = SuffixKind(iota) ...
-var DefaultOptions = &Options{ ... }
-func EqualSuffix(a, b Suffix) bool
-func PathFromHost(host string, specialDashes bool) string
-type Node struct{ ... }
-type Options struct{ ... }
-type Suffix struct{ ... }
-type SuffixKind int
-type SuffixList []Suffix
diff --git a/.github/sponsors/speakeasy.png b/.github/sponsors/speakeasy.png
deleted file mode 100644
index a7d842107..000000000
Binary files a/.github/sponsors/speakeasy.png and /dev/null differ
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index a09546a40..000000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: "CodeQL"
-
-on:
- push:
- branches: [ "master" ]
- pull_request:
- branches: [ "master" ]
- schedule:
- - cron: "4 8 * * 4"
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: [ go ]
-
- steps:
- - name: Checkout
- uses: actions/checkout@v3
-
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- queries: +security-and-quality
-
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
- with:
- category: "/language:${{ matrix.language }}"
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
deleted file mode 100644
index 9d9ff726f..000000000
--- a/.github/workflows/go.yml
+++ /dev/null
@@ -1,196 +0,0 @@
-name: go
-on:
- pull_request:
- push:
-
-jobs:
- build-and-test:
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GO111MODULE: 'on'
- CGO_ENABLED: '0'
- strategy:
- fail-fast: true
- matrix:
- go: ['1.16', '1.x']
- os:
- - ubuntu-latest
- - windows-latest
- - macos-latest
- runs-on: ${{ matrix.os }}
- defaults:
- run:
- shell: bash
- name: ${{ matrix.go }} on ${{ matrix.os }}
- steps:
-
- - uses: actions/setup-go@v2
- with:
- go-version: ${{ matrix.go }}
-
- - id: go-cache-paths
- run: |
- echo "::set-output name=go-build::$(go env GOCACHE)"
- echo "::set-output name=go-mod::$(go env GOMODCACHE)"
- - run: echo ${{ steps.go-cache-paths.outputs.go-build }}
- - run: echo ${{ steps.go-cache-paths.outputs.go-mod }}
-
- - name: Go Build Cache
- uses: actions/cache@v3
- with:
- path: ${{ steps.go-cache-paths.outputs.go-build }}
- key: ${{ runner.os }}-go-${{ matrix.go }}-build-${{ hashFiles('**/go.sum') }}
-
- - name: Go Mod Cache (go>=1.15)
- uses: actions/cache@v3
- with:
- path: ${{ steps.go-cache-paths.outputs.go-mod }}
- key: ${{ runner.os }}-go-${{ matrix.go }}-mod-${{ hashFiles('**/go.sum') }}
-
- - if: runner.os == 'Linux'
- run: sudo apt install silversearcher-ag
-
- - uses: actions/checkout@v2
-
- - name: Check codegen
- run: |
- ./refs.sh | tee openapi3/refs.go
- git --no-pager diff --exit-code
-
- - name: Check docsgen
- run: ./docs.sh
-
- - run: go mod download && go mod tidy && go mod verify
- - run: git --no-pager diff --exit-code
-
- - run: go vet ./...
- - run: git --no-pager diff --exit-code
-
- - run: go fmt ./...
- - run: git --no-pager diff --exit-code
-
- - run: go test ./...
- - if: runner.os == 'Linux'
- run: go test -count=10 ./...
- env:
- GOARCH: '386'
- - run: go test -count=10 ./...
- - run: go test -count=2 -covermode=atomic ./...
- - run: go test -v -run TestRaceyPatternSchema -race ./...
- env:
- CGO_ENABLED: '1'
- - run: go test -v -run TestIssue741 -race ./...
- env:
- CGO_ENABLED: '1'
- - run: git --no-pager diff --exit-code
-
- - if: runner.os == 'Linux'
- name: Errors must not be capitalized https://github.com/golang/go/wiki/CodeReviewComments#error-strings
- run: |
- ! git grep -E '(fmt|errors)[^(]+\(.[A-Z]'
-
- - if: runner.os == 'Linux'
- name: Did you mean %q
- run: |
- ! git grep -E "'[%].'"
-
- - if: runner.os == 'Linux'
- name: Also add yaml tags
- run: |
- ! git grep -InE 'json:"' | grep -v _test.go | grep -v yaml:
-
- - if: runner.os == 'Linux' && matrix.go != '1.16'
- name: nilness
- run: go run golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness@latest ./...
-
- - if: runner.os == 'Linux'
- name: Check for superfluous trailing whitespace
- run: |
- ! grep -IErn '\s$' --exclude-dir={.git,target,pgdata}
-
- - if: runner.os == 'Linux'
- name: Ensure use of unmarshal
- run: |
- [[ "$(git grep -F yaml. -- openapi3/ | grep -v _test.go | wc -l)" = 1 ]]
-
- - if: runner.os == 'Linux'
- name: Ensure non-pointer MarshalJSON
- run: |
- ! git grep -InE 'func.+[*].+[)].MarshalJSON[(][)]'
-
- - if: runner.os == 'Linux'
- name: Missing specification object link to definition
- run: |
- [[ 31 -eq $(git grep -InE '^// See https:.+OpenAPI-Specification.+3[.]0[.]3[.]md#.+bject$' openapi3/*.go | grep -v _test.go | grep -v doc.go | wc -l) ]]
-
- - if: runner.os == 'Linux'
- name: Missing validation of unknown fields in extensions
- run: |
- [[ $(git grep -InF 'return validateExtensions' -- openapi3 | wc -l) -eq $(git grep -InE '^\s+Extensions.+`' -- openapi3 | wc -l) ]]
-
- - if: runner.os == 'Linux'
- name: Style around Extensions embedding
- run: |
- ! ag -B2 -A2 'type.[A-Z].+struct..\n.+Extensions\n[^\n]' openapi3/*.go
-
- - if: runner.os == 'Linux'
- name: Ensure all exported fields are mentioned in Validate() impls
- run: |
- for ty in $TYPES; do
- # Ensure definition
- if ! ag 'type.[A-Z].+struct..\n.+Extensions' openapi3/*.go | grep -F "type $ty struct"; then
- echo "OAI type $ty is not defined" && exit 1
- fi
-
- # Ensure impl Validate()
- if ! git grep -InE 'func [(].+'"$ty"'[)] Validate[(]ctx context.Context, opts [.][.][.]ValidationOption[)].+error.+[{]'; then
- echo "OAI type $ty does not implement Validate()" && exit 1
- fi
-
- # TODO: $ty mention all its exported fields within Validate()
- done
- env:
- TYPES: >
- Components
- Contact
- Discriminator
- Encoding
- Example
- ExternalDocs
- Info
- License
- Link
- MediaType
- OAuthFlow
- OAuthFlows
- Operation
- Parameter
- PathItem
- RequestBody
- Response
- Schema
- SecurityScheme
- Server
- ServerVariable
- T
- Tag
- XML
-
- - if: runner.os == 'Linux'
- name: Ensure readableType() covers all possible values of resolved var
- run: |
- [[ "$(git grep -F 'var resolved ' -- openapi3/loader.go | awk '{print $4}' | sort | tr '\n' ' ')" = "$RESOLVEDS" ]]
- env:
- RESOLVEDS: 'Callback CallbackRef ExampleRef HeaderRef LinkRef ParameterRef PathItem RequestBodyRef ResponseRef SchemaRef SecuritySchemeRef '
-
- check-goimports:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-go@v3
- with:
- go-version: '>=1.17.0'
- - run: go install github.com/incu6us/goimports-reviser/v2@latest
- - run: which goimports-reviser
- - run: find . -type f -iname '*.go' ! -iname '*.pb.go' -exec goimports-reviser -file-path {} \;
- - run: git --no-pager diff --exit-code
diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml
deleted file mode 100644
index e1f8d1242..000000000
--- a/.github/workflows/shellcheck.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: ShellCheck
-
-on:
- push:
- pull_request:
-
-jobs:
- shellcheck:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
-
- - name: Run shellcheck
- uses: ludeeus/action-shellcheck@1.1.0
- with:
- check_together: 'yes'
- severity: error
diff --git a/.gitignore b/.gitignore
index 40b671156..a5bf17cb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,4 +18,4 @@
# IntelliJ / GoLand
.idea
-.vscode
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..5dd1e0251
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,12 @@
+language: go
+go:
+- 1.11.x
+env:
+ global:
+ - GO111MODULE: 'on'
+ - CGO_ENABLED: '0'
+after_success:
+- go mod tidy && git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]]
+notifications:
+ email:
+ on_success: never
diff --git a/README.md b/README.md
index b85af9864..fd3db0fbe 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,27 @@
-[![CI](https://github.com/getkin/kin-openapi/workflows/go/badge.svg)](https://github.com/getkin/kin-openapi/actions)
+[![Build Status](https://travis-ci.com/getkin/kin-openapi.svg?branch=master)](https://travis-ci.com/getkin/kin-openapi)
[![Go Report Card](https://goreportcard.com/badge/github.com/getkin/kin-openapi)](https://goreportcard.com/report/github.com/getkin/kin-openapi)
[![GoDoc](https://godoc.org/github.com/getkin/kin-openapi?status.svg)](https://godoc.org/github.com/getkin/kin-openapi)
[![Join Gitter Chat Channel -](https://badges.gitter.im/getkin/kin.svg)](https://gitter.im/getkin/kin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
# Introduction
-A [Go](https://golang.org) project for handling [OpenAPI](https://www.openapis.org/) files. We target:
-* [OpenAPI `v2.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md) (formerly known as Swagger)
-* [OpenAPI `v3.0`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md)
-* [OpenAPI `v3.1`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md) Soon! [Tracking issue here.](https://github.com/getkin/kin-openapi/issues/230)
+A [Go](https://golang.org) project for handling [OpenAPI](https://www.openapis.org/) files. We target the latest OpenAPI version (currently 3), but the project contains support for older OpenAPI versions too.
-Licensed under the [MIT License](./LICENSE).
+Licensed under the [MIT License](LICENSE).
-## Contributors, users and sponsors
-The project has received pull requests [from many people](https://github.com/getkin/kin-openapi/graphs/contributors). Thanks to everyone!
-
-Be sure to [give back to this project](https://github.com/sponsors/fenollp) like our sponsors:
-
-
-
-
+## Contributors and users
+The project has received pull requests from many people. Thanks to everyone!
Here's some projects that depend on _kin-openapi_:
- * [github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3"
+ * [github.com/getkin/kin](https://github.com/getkin/kin) - "A configurable backend"
* [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation"
- * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - "Generate Go client and server boilerplate from OpenAPI 3 specifications"
+ * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPI 3 spec
* [github.com/dunglas/vulcain](https://github.com/dunglas/vulcain) - "Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs"
- * [github.com/danielgtaylor/restish](https://github.com/danielgtaylor/restish) - "...a CLI for interacting with REST-ish HTTP APIs with some nice features built-in"
- * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Design-based APIs and microservices in Go"
- * [github.com/hashicorp/nomad-openapi](https://github.com/hashicorp/nomad-openapi) - "Nomad is an easy-to-use, flexible, and performant workload orchestrator that can deploy a mix of microservice, batch, containerized, and non-containerized applications. Nomad is easy to operate and scale and has native Consul and Vault integrations."
- * [gitlab.com/jamietanna/httptest-openapi](https://gitlab.com/jamietanna/httptest-openapi) ([*blog post*](https://www.jvt.me/posts/2022/05/22/go-openapi-contract-test/)) - "Go OpenAPI Contract Verification for use with `net/http`"
- * [github.com/SIMITGROUP/openapigenerator](https://github.com/SIMITGROUP/openapigenerator) - "Openapi v3 microservices generator"
* (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request)
-## Alternatives
-* [go-swagger](https://github.com/go-swagger/go-swagger) stated [*OpenAPIv3 won't be supported*](https://github.com/go-swagger/go-swagger/issues/1122#issuecomment-575968499)
-* [swaggo](https://github.com/swaggo/swag) has an [open issue on OpenAPIv3](https://github.com/swaggo/swag/issues/386)
-* [go-openapi](https://github.com/go-openapi)'s [spec3](https://github.com/go-openapi/spec3)
- * an iteration on [spec](https://github.com/go-openapi/spec) (for OpenAPIv2)
- * see [README](https://github.com/go-openapi/spec3/tree/3fab9faa9094e06ebd19ded7ea96d156c2283dca#oai-object-model---) for the missing parts
-
-Be sure to check [OpenAPI Initiative](https://github.com/OAI)'s [great tooling list](https://github.com/OAI/OpenAPI-Specification/blob/master/IMPLEMENTATIONS.md) as well as [OpenAPI.Tools](https://openapi.tools/).
+## Alternative projects
+ * [go-openapi](https://github.com/go-openapi)
+ * Supports OpenAPI version 2.
+ * See [this list](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/IMPLEMENTATIONS.md).
# Structure
* _openapi2_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi2))
@@ -50,30 +32,33 @@ Be sure to check [OpenAPI Initiative](https://github.com/OAI)'s [great tooling l
* Support for OpenAPI 3 files, including serialization, deserialization, and validation.
* _openapi3filter_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi3filter))
* Validates HTTP requests and responses
- * Provides a [gorilla/mux](https://github.com/gorilla/mux) router for OpenAPI operations
* _openapi3gen_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi3gen))
* Generates `*openapi3.Schema` values for Go types.
+ * _pathpattern_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/pathpattern))
+ * Matches strings with OpenAPI path patterns ("/path/{parameter}")
# 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:
+Use `SwaggerLoader`, which resolves all JSON references:
```go
-doc, err := openapi3.NewLoader().LoadFromFile("swagger.json")
+swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("swagger.json")
```
## Getting OpenAPI operation that matches request
```go
-loader := openapi3.NewLoader()
-doc, _ := loader.LoadFromData([]byte(`...`))
-_ := doc.Validate(loader.Context)
-router, _ := gorillamux.NewRouter(doc)
-route, pathParams, _ := router.FindRoute(httpRequest)
-// Do something with route.Operation
+func GetOperation(httpRequest *http.Request) (*openapi3.Operation, error) {
+ // Load Swagger file
+ router := openapi3filter.NewRouter().WithSwaggerFromFile("swagger.json")
+
+ // Find route
+ route, _, err := router.FindRoute("GET", req.URL)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get OpenAPI 3 operation
+ return route.Operation
+}
```
## Validating HTTP requests/responses
@@ -81,26 +66,22 @@ route, pathParams, _ := router.FindRoute(httpRequest)
package main
import (
+ "bytes"
"context"
- "fmt"
+ "encoding/json"
+ "log"
"net/http"
- "github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
)
func main() {
- ctx := context.Background()
- loader := &openapi3.Loader{Context: ctx, IsExternalRefsAllowed: true}
- doc, _ := loader.LoadFromFile(".../My-OpenAPIv3-API.yml")
- // Validate document
- _ := doc.Validate(ctx)
- router, _ := gorillamux.NewRouter(doc)
+ router := openapi3filter.NewRouter().WithSwaggerFromFile("swagger.json")
+ ctx := context.TODO()
httpReq, _ := http.NewRequest(http.MethodGet, "/items", nil)
// Find route
- route, pathParams, _ := router.FindRoute(httpReq)
+ route, pathParams, _ := router.FindRoute(httpReq.Method, httpReq.URL)
// Validate request
requestValidationInput := &openapi3filter.RequestValidationInput{
@@ -108,22 +89,35 @@ func main() {
PathParams: pathParams,
Route: route,
}
- _ := openapi3filter.ValidateRequest(ctx, requestValidationInput)
+ if err := openapi3filter.ValidateRequest(ctx, requestValidationInput); err != nil {
+ panic(err)
+ }
- // Handle that request
- // --> YOUR CODE GOES HERE <--
- responseHeaders := http.Header{"Content-Type": []string{"application/json"}}
- responseCode := 200
- responseBody := []byte(`{}`)
+ var (
+ respStatus = 200
+ respContentType = "application/json"
+ respBody = bytes.NewBufferString(`{}`)
+ )
- // Validate response
+ log.Println("Response:", respStatus)
responseValidationInput := &openapi3filter.ResponseValidationInput{
RequestValidationInput: requestValidationInput,
- Status: responseCode,
- Header: responseHeaders,
+ Status: respStatus,
+ Header: http.Header{
+ "Content-Type": []string{
+ respContentType,
+ },
+ },
+ }
+ if respBody != nil {
+ data, _ := json.Marshal(respBody)
+ responseValidationInput.SetBodyBytes(data)
+ }
+
+ // Validate response.
+ if err := openapi3filter.ValidateResponse(ctx, responseValidationInput); err != nil {
+ panic(err)
}
- responseValidationInput.SetBodyBytes(responseBody)
- _ := openapi3filter.ValidateResponse(ctx, responseValidationInput)
}
```
@@ -158,12 +152,12 @@ func main() {
}
}
-func xmlBodyDecoder(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn openapi3filter.EncodingFn) (decoded interface{}, err error) {
+func xmlBodyDecoder(body []byte) (interface{}, error) {
// Decode body to a primitive, []inteface{}, or map[string]interface{}.
}
```
-## Custom function to check uniqueness of array items
+## Custom function for check uniqueness of JSON array
By defaut, the library check unique items by below predefined function
@@ -172,6 +166,8 @@ func isSliceOfUniqueItems(xs []interface{}) bool {
s := len(xs)
m := make(map[string]struct{}, s)
for _, x := range xs {
+ // The input slice is coverted from a JSON string, there shall
+ // have no error when covert it back.
key, _ := json.Marshal(&x)
m[string(key)] = struct{}{}
}
@@ -181,7 +177,7 @@ func isSliceOfUniqueItems(xs []interface{}) bool {
In the predefined function using `json.Marshal` to generate a string can
be used as a map key which is to support check the uniqueness of an array
-when the array items are objects or arrays. You can register
+when the array items are JSON objects or JSON arraies. You can register
you own function according to your input data to get better performance:
```go
@@ -195,124 +191,6 @@ func main() {
}
func arrayUniqueItemsChecker(items []interface{}) bool {
- // Check the uniqueness of the input slice
+ // Check the uniqueness of the input slice(array in JSON)
}
```
-
-## Custom function to change schema error messages
-
-By default, the error message returned when validating a value includes the error reason, the schema, and the input value.
-
-For example, given the following schema:
-
-```json
-{
- "type": "string",
- "allOf": [
- { "pattern": "[A-Z]" },
- { "pattern": "[a-z]" },
- { "pattern": "[0-9]" },
- { "pattern": "[!@#$%^&*()_+=-?~]" }
- ]
-}
-```
-
-Passing the input value `"secret"` to this schema will produce the following error message:
-
-```
-string doesn't match the regular expression "[A-Z]"
-Schema:
- {
- "pattern": "[A-Z]"
- }
-
-Value:
- "secret"
-```
-
-Including the original value in the error message can be helpful for debugging, but it may not be appropriate for sensitive information such as secrets.
-
-To disable the extra details in the schema error message, you can set the `openapi3.SchemaErrorDetailsDisabled` option to `true`:
-
-```go
-func main() {
- // ...
-
- // Disable schema error detailed error messages
- openapi3.SchemaErrorDetailsDisabled = true
-
- // ... other validate codes
-}
-```
-
-This will shorten the error message to present only the reason:
-
-```
-string doesn't match the regular expression "[A-Z]"
-```
-
-For more fine-grained control over the error message, you can pass a custom `openapi3filter.Options` object to `openapi3filter.RequestValidationInput` that includes a `openapi3filter.CustomSchemaErrorFunc`.
-
-```go
-func validationOptions() *openapi3filter.Options {
- options := openapi3filter.DefaultOptions
- options.WithCustomSchemaErrorFunc(safeErrorMessage)
- return options
-}
-
-func safeErrorMessage(err *openapi3.SchemaError) string {
- return err.Reason
-}
-```
-
-This will change the schema validation errors to return only the `Reason` field, which is guaranteed to not include the original value.
-
-## Sub-v0 breaking API changes
-
-### v0.113.0
-* The string format `email` has been removed by default. To use it please call `openapi3.DefineStringFormat("email", openapi3.FormatOfStringForEmail)`.
-* Field `openapi3.T.Components` is now a pointer.
-* Fields `openapi3.Schema.AdditionalProperties` and `openapi3.Schema.AdditionalPropertiesAllowed` are replaced by `openapi3.Schema.AdditionalProperties.Schema` and `openapi3.Schema.AdditionalProperties.Has` respectively.
-* Type `openapi3.ExtensionProps` is now just `map[string]interface{}` and extensions are accessible through the `Extensions` field.
-
-### 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`.
-
-### v0.101.0
-* `openapi3.SchemaFormatValidationDisabled` has been removed in favour of an option `openapi3.EnableSchemaFormatValidation()` passed to `openapi3.T.Validate`. The default behaviour is also now to not validate formats, as the OpenAPI spec mentions the `format` is an open value.
-
-### v0.84.0
-* The prototype of `openapi3gen.NewSchemaRefForValue` changed:
- * It no longer returns a map but that is still accessible under the field `(*Generator).SchemaRefs`.
- * It now takes in an additional argument (basically `doc.Components.Schemas`) which gets written to so `$ref` cycles can be properly handled.
-
-### v0.61.0
-* Renamed `openapi2.Swagger` to `openapi2.T`.
-* Renamed `openapi2conv.FromV3Swagger` to `openapi2conv.FromV3`.
-* Renamed `openapi2conv.ToV3Swagger` to `openapi2conv.ToV3`.
-* Renamed `openapi3.LoadSwaggerFromData` to `openapi3.LoadFromData`.
-* Renamed `openapi3.LoadSwaggerFromDataWithPath` to `openapi3.LoadFromDataWithPath`.
-* Renamed `openapi3.LoadSwaggerFromFile` to `openapi3.LoadFromFile`.
-* Renamed `openapi3.LoadSwaggerFromURI` to `openapi3.LoadFromURI`.
-* Renamed `openapi3.NewSwaggerLoader` to `openapi3.NewLoader`.
-* Renamed `openapi3.Swagger` to `openapi3.T`.
-* Renamed `openapi3.SwaggerLoader` to `openapi3.Loader`.
-* Renamed `openapi3filter.ValidationHandler.SwaggerFile` to `openapi3filter.ValidationHandler.File`.
-* Renamed `routers.Route.Swagger` to `routers.Route.Spec`.
-
-### v0.51.0
-* Type `openapi3filter.Route` moved to `routers` (and `Route.Handler` was dropped. See https://github.com/getkin/kin-openapi/issues/329)
-* Type `openapi3filter.RouteError` moved to `routers` (so did `ErrPathNotFound` and `ErrMethodNotAllowed` which are now `RouteError`s)
-* Routers' `FindRoute(...)` method now takes only one argument: `*http.Request`
-* `getkin/kin-openapi/openapi3filter.Router` moved to `getkin/kin-openapi/routers/legacy`
-* `openapi3filter.NewRouter()` and its related `WithSwaggerFromFile(string)`, `WithSwagger(*openapi3.Swagger)`, `AddSwaggerFromFile(string)` and `AddSwagger(*openapi3.Swagger)` are all replaced with a single `.NewRouter(*openapi3.Swagger)`
- * NOTE: the `NewRouter(doc)` call now requires that the user ensures `doc` is valid (`doc.Validate() != nil`). This used to be asserted.
-
-### v0.47.0
-Field `(*openapi3.SwaggerLoader).LoadSwaggerFromURIFunc` of type `func(*openapi3.SwaggerLoader, *url.URL) (*openapi3.Swagger, error)` was removed after the addition of the field `(*openapi3.SwaggerLoader).ReadFromURIFunc` of type `func(*openapi3.SwaggerLoader, *url.URL) ([]byte, error)`.
diff --git a/cmd/validate/main.go b/cmd/validate/main.go
deleted file mode 100644
index d8c0fe6ad..000000000
--- a/cmd/validate/main.go
+++ /dev/null
@@ -1,102 +0,0 @@
-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.Fatalln("Loading error:", 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.Fatalln("Validation error:", 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.Fatalln("Loading error:", err)
- }
-
- default:
- log.Fatal("Missing or incorrect 'openapi' or 'swagger' field")
- }
-}
diff --git a/docs.sh b/docs.sh
deleted file mode 100755
index 5485feb2f..000000000
--- a/docs.sh
+++ /dev/null
@@ -1,25 +0,0 @@
-#!/bin/bash -eux
-set -o pipefail
-
-outdir=.github/docs
-mkdir -p "$outdir"
-for pkgpath in $(git ls-files | grep / | while read -r path; do dirname "$path"; done | sort -u | grep -vE '[.]git|testdata|cmd/'); do
- go doc -short "./$pkgpath" | tee "$outdir/${pkgpath////_}.txt"
-done
-
-git --no-pager diff -- .github/docs/
-
-count_missing_mentions() {
- local errors=0
- for thing in $(git --no-pager diff -- .github/docs/ \
- | grep -vE '[-]{3}' \
- | grep -Eo '^-[^ ]+ ([^ (]+)[ (]' \
- | sed 's%(% %' \
- | cut -d' ' -f2); do
- if ! grep -A999999 '## Sub-v0 breaking API changes' README.md | grep -F "$thing"; then
- ((errors++)) || true
- fi
- done
- return $errors
-}
-count_missing_mentions
diff --git a/go.mod b/go.mod
index 12a2f1af7..230138307 100644
--- a/go.mod
+++ b/go.mod
@@ -1,14 +1,9 @@
module github.com/getkin/kin-openapi
-go 1.16
+go 1.14
require (
- github.com/go-openapi/jsonpointer v0.19.5
- github.com/gorilla/mux v1.8.0
- github.com/invopop/yaml v0.1.0
- github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
- github.com/perimeterx/marshmallow v1.1.4
- github.com/stretchr/testify v1.8.1
- gopkg.in/yaml.v2 v2.4.0 // indirect
- gopkg.in/yaml.v3 v3.0.1
+ github.com/ghodss/yaml v1.0.0
+ github.com/stretchr/testify v1.5.1
+ gopkg.in/yaml.v2 v2.2.8 // indirect
)
diff --git a/go.sum b/go.sum
index 4d05787e4..22c1b575c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,52 +1,13 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
-github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
-github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
-github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
-github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
-github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
-github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
-github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
-github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
-github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
-github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=
-github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
-github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
-github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
-github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/jsoninfo/doc.go b/jsoninfo/doc.go
new file mode 100644
index 000000000..e59ec2c34
--- /dev/null
+++ b/jsoninfo/doc.go
@@ -0,0 +1,2 @@
+// Package jsoninfo provides information and functions for marshalling/unmarshalling JSON.
+package jsoninfo
diff --git a/openapi3gen/field_info.go b/jsoninfo/field_info.go
similarity index 66%
rename from openapi3gen/field_info.go
rename to jsoninfo/field_info.go
index 13f5ba048..d949a79d3 100644
--- a/openapi3gen/field_info.go
+++ b/jsoninfo/field_info.go
@@ -1,4 +1,4 @@
-package openapi3gen
+package jsoninfo
import (
"reflect"
@@ -7,8 +7,9 @@ import (
"unicode/utf8"
)
-// theFieldInfo contains information about JSON serialization of a field.
-type theFieldInfo struct {
+// FieldInfo contains information about JSON serialization of a field.
+type FieldInfo struct {
+ MultipleFields bool // Whether multiple Go fields share this JSON name
HasJSONTag bool
TypeIsMarshaller bool
TypeIsUnmarshaller bool
@@ -19,10 +20,7 @@ type theFieldInfo struct {
JSONName string
}
-func appendFields(fields []theFieldInfo, parentIndex []int, t reflect.Type) []theFieldInfo {
- if t.Kind() == reflect.Ptr {
- t = t.Elem()
- }
+func AppendFields(fields []FieldInfo, parentIndex []int, t reflect.Type) []FieldInfo {
// For each field
numField := t.NumField()
iteration:
@@ -34,14 +32,11 @@ iteration:
// See whether this is an embedded field
if f.Anonymous {
- jsonTag := f.Tag.Get("json")
- if jsonTag == "-" {
+ if f.Tag.Get("json") == "-" {
continue
}
- if jsonTag == "" {
- fields = appendFields(fields, index, f.Type)
- continue iteration
- }
+ fields = AppendFields(fields, index, f.Type)
+ continue iteration
}
// Ignore certain types
@@ -57,7 +52,7 @@ iteration:
}
// Declare a field
- field := theFieldInfo{
+ field := FieldInfo{
Index: index,
Type: f.Type,
JSONName: f.Name,
@@ -66,17 +61,24 @@ iteration:
// Read "json" tag
jsonTag := f.Tag.Get("json")
+ // Read our custom "multijson" tag that
+ // allows multiple fields with the same name.
+ if v := f.Tag.Get("multijson"); len(v) > 0 {
+ field.MultipleFields = true
+ jsonTag = v
+ }
+
// Handle "-"
if jsonTag == "-" {
continue
}
// Parse the tag
- if jsonTag != "" {
+ if len(jsonTag) > 0 {
field.HasJSONTag = true
for i, part := range strings.Split(jsonTag, ",") {
if i == 0 {
- if part != "" {
+ if len(part) > 0 {
field.JSONName = part
}
} else {
@@ -90,8 +92,12 @@ iteration:
}
}
- _, field.TypeIsMarshaller = field.Type.MethodByName("MarshalJSON")
- _, field.TypeIsUnmarshaller = field.Type.MethodByName("UnmarshalJSON")
+ if _, ok := field.Type.MethodByName("MarshalJSON"); ok {
+ field.TypeIsMarshaller = true
+ }
+ if _, ok := field.Type.MethodByName("UnmarshalJSON"); ok {
+ field.TypeIsUnmarshaller = true
+ }
// Field is done
fields = append(fields, field)
@@ -100,7 +106,7 @@ iteration:
return fields
}
-type sortableFieldInfos []theFieldInfo
+type sortableFieldInfos []FieldInfo
func (list sortableFieldInfos) Len() int {
return len(list)
diff --git a/jsoninfo/marshal.go b/jsoninfo/marshal.go
new file mode 100644
index 000000000..93de99a56
--- /dev/null
+++ b/jsoninfo/marshal.go
@@ -0,0 +1,162 @@
+package jsoninfo
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+)
+
+// MarshalStrictStruct function:
+// * Marshals struct fields, ignoring MarshalJSON() and fields without 'json' tag.
+// * Correctly handles StrictStruct semantics.
+func MarshalStrictStruct(value StrictStruct) ([]byte, error) {
+ encoder := NewObjectEncoder()
+ if err := value.EncodeWith(encoder, value); err != nil {
+ return nil, err
+ }
+ return encoder.Bytes()
+}
+
+type ObjectEncoder struct {
+ result map[string]json.RawMessage
+}
+
+func NewObjectEncoder() *ObjectEncoder {
+ return &ObjectEncoder{
+ result: make(map[string]json.RawMessage, 8),
+ }
+}
+
+// Bytes returns the result of encoding.
+func (encoder *ObjectEncoder) Bytes() ([]byte, error) {
+ return json.Marshal(encoder.result)
+}
+
+// EncodeExtension adds a key/value to the current JSON object.
+func (encoder *ObjectEncoder) EncodeExtension(key string, value interface{}) error {
+ data, err := json.Marshal(value)
+ if err != nil {
+ return err
+ }
+ encoder.result[key] = data
+ return nil
+}
+
+// EncodeExtensionMap adds all properties to the result.
+func (encoder *ObjectEncoder) EncodeExtensionMap(value map[string]json.RawMessage) error {
+ if value != nil {
+ result := encoder.result
+ for k, v := range value {
+ result[k] = v
+ }
+ }
+ return nil
+}
+
+func (encoder *ObjectEncoder) EncodeStructFieldsAndExtensions(value interface{}) error {
+ reflection := reflect.ValueOf(value)
+
+ // Follow "encoding/json" semantics
+ if reflection.Kind() != reflect.Ptr {
+ // Panic because this is a clear programming error
+ panic(fmt.Errorf("Value %s is not a pointer", reflection.Type().String()))
+ }
+ if reflection.IsNil() {
+ // Panic because this is a clear programming error
+ panic(fmt.Errorf("Value %s is nil", reflection.Type().String()))
+ }
+
+ // Take the element
+ reflection = reflection.Elem()
+
+ // Obtain typeInfo
+ typeInfo := GetTypeInfo(reflection.Type())
+
+ // Declare result
+ result := encoder.result
+
+ // Supported fields
+iteration:
+ for _, field := range typeInfo.Fields {
+ // Fields without JSON tag are ignored
+ if !field.HasJSONTag {
+ continue
+ }
+
+ // Marshal
+ fieldValue := reflection.FieldByIndex(field.Index)
+ if v, ok := fieldValue.Interface().(json.Marshaler); ok {
+ if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
+ if field.JSONOmitEmpty {
+ continue iteration
+ }
+ result[field.JSONName] = []byte("null")
+ continue
+ }
+ fieldData, err := v.MarshalJSON()
+ if err != nil {
+ return err
+ }
+ result[field.JSONName] = fieldData
+ continue
+ }
+ switch fieldValue.Kind() {
+ case reflect.Ptr, reflect.Interface:
+ if fieldValue.IsNil() {
+ if field.JSONOmitEmpty {
+ continue iteration
+ }
+ result[field.JSONName] = []byte("null")
+ continue
+ }
+ case reflect.Struct:
+ case reflect.Map:
+ if field.JSONOmitEmpty && (fieldValue.IsNil() || fieldValue.Len() == 0) {
+ continue iteration
+ }
+ case reflect.Slice:
+ if field.JSONOmitEmpty && fieldValue.Len() == 0 {
+ continue iteration
+ }
+ case reflect.Bool:
+ x := fieldValue.Bool()
+ if field.JSONOmitEmpty && !x {
+ continue iteration
+ }
+ s := "false"
+ if x {
+ s = "true"
+ }
+ result[field.JSONName] = []byte(s)
+ continue iteration
+ case reflect.Int64, reflect.Int, reflect.Int32:
+ if field.JSONOmitEmpty && fieldValue.Int() == 0 {
+ continue iteration
+ }
+ case reflect.Uint64, reflect.Uint, reflect.Uint32:
+ if field.JSONOmitEmpty && fieldValue.Uint() == 0 {
+ continue iteration
+ }
+ case reflect.Float64:
+ if field.JSONOmitEmpty && fieldValue.Float() == 0.0 {
+ continue iteration
+ }
+ case reflect.String:
+ if field.JSONOmitEmpty && len(fieldValue.String()) == 0 {
+ continue iteration
+ }
+ default:
+ panic(fmt.Errorf("Field '%s' has unsupported type %s", field.JSONName, field.Type.String()))
+ }
+
+ // No special treament is needed
+ // Use plain old "encoding/json".Marshal
+ fieldData, err := json.Marshal(fieldValue.Addr().Interface())
+ if err != nil {
+ return err
+ }
+ result[field.JSONName] = fieldData
+ }
+
+ return nil
+}
diff --git a/jsoninfo/marshal_ref.go b/jsoninfo/marshal_ref.go
new file mode 100644
index 000000000..9738bf08f
--- /dev/null
+++ b/jsoninfo/marshal_ref.go
@@ -0,0 +1,30 @@
+package jsoninfo
+
+import (
+ "encoding/json"
+)
+
+func MarshalRef(value string, otherwise interface{}) ([]byte, error) {
+ if len(value) > 0 {
+ return json.Marshal(&refProps{
+ Ref: value,
+ })
+ }
+ return json.Marshal(otherwise)
+}
+
+func UnmarshalRef(data []byte, destRef *string, destOtherwise interface{}) error {
+ refProps := &refProps{}
+ if err := json.Unmarshal(data, refProps); err == nil {
+ ref := refProps.Ref
+ if len(ref) > 0 {
+ *destRef = ref
+ return nil
+ }
+ }
+ return json.Unmarshal(data, destOtherwise)
+}
+
+type refProps struct {
+ Ref string `json:"$ref,omitempty"`
+}
diff --git a/jsoninfo/marshal_test.go b/jsoninfo/marshal_test.go
new file mode 100644
index 000000000..05a6ac31b
--- /dev/null
+++ b/jsoninfo/marshal_test.go
@@ -0,0 +1,189 @@
+package jsoninfo_test
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/getkin/kin-openapi/jsoninfo"
+ "github.com/getkin/kin-openapi/openapi3"
+)
+
+type Simple struct {
+ openapi3.ExtensionProps
+ Bool bool `json:"bool"`
+ Int int `json:"int"`
+ Int64 int64 `json:"int64"`
+ Float64 float64 `json:"float64"`
+ Time time.Time `json:"time"`
+ String string `json:"string"`
+ Bytes []byte `json:"bytes"`
+}
+
+type SimpleOmitEmpty struct {
+ openapi3.ExtensionProps
+ Bool bool `json:"bool,omitempty"`
+ Int int `json:"int,omitempty"`
+ Int64 int64 `json:"int64,omitempty"`
+ Float64 float64 `json:"float64,omitempty"`
+ Time time.Time `json:"time,omitempty"`
+ String string `json:"string,omitempty"`
+ Bytes []byte `json:"bytes,omitempty"`
+}
+
+type SimplePtrOmitEmpty struct {
+ openapi3.ExtensionProps
+ Bool *bool `json:"bool,omitempty"`
+ Int *int `json:"int,omitempty"`
+ Int64 *int64 `json:"int64,omitempty"`
+ Float64 *float64 `json:"float64,omitempty"`
+ Time *time.Time `json:"time,omitempty"`
+ String *string `json:"string,omitempty"`
+ Bytes *[]byte `json:"bytes,omitempty"`
+}
+
+type OriginalNameType struct {
+ openapi3.ExtensionProps
+ Field string `json:",omitempty"`
+}
+
+type RootType struct {
+ openapi3.ExtensionProps
+ EmbeddedType0
+ EmbeddedType1
+}
+
+type EmbeddedType0 struct {
+ openapi3.ExtensionProps
+ Field0 string `json:"embedded0,omitempty"`
+}
+
+type EmbeddedType1 struct {
+ openapi3.ExtensionProps
+ Field1 string `json:"embedded1,omitempty"`
+}
+
+// Example describes expected outcome of:
+// 1.Marshal JSON
+// 2.Unmarshal value
+// 3.Marshal value
+type Example struct {
+ NoMarshal bool
+ NoUnmarshal bool
+ Value jsoninfo.StrictStruct
+ JSON interface{}
+}
+
+var Examples = []Example{
+ // Primitives
+ {
+ Value: &SimpleOmitEmpty{},
+ JSON: Object{
+ "time": time.Unix(0, 0),
+ },
+ },
+ {
+ Value: &SimpleOmitEmpty{},
+ JSON: Object{
+ "bool": true,
+ "int": 42,
+ "int64": 42,
+ "float64": 3.14,
+ "string": "abc",
+ "bytes": []byte{1, 2, 3},
+ "time": time.Unix(1, 0),
+ },
+ },
+
+ // Pointers
+ {
+ Value: &SimplePtrOmitEmpty{},
+ JSON: Object{},
+ },
+ {
+ Value: &SimplePtrOmitEmpty{},
+ JSON: Object{
+ "bool": true,
+ "int": 42,
+ "int64": 42,
+ "float64": 3.14,
+ "string": "abc",
+ "bytes": []byte{1, 2, 3},
+ "time": time.Unix(1, 0),
+ },
+ },
+
+ // JSON tag "fieldName"
+ {
+ Value: &Simple{},
+ JSON: Object{
+ "bool": false,
+ "int": 0,
+ "int64": 0,
+ "float64": 0,
+ "string": "",
+ "bytes": []byte{},
+ "time": time.Unix(0, 0),
+ },
+ },
+
+ // JSON tag ",omitempty"
+ {
+ Value: &OriginalNameType{},
+ JSON: Object{
+ "Field": "abc",
+ },
+ },
+
+ // Embedding
+ {
+ Value: &RootType{},
+ JSON: Object{},
+ },
+ {
+ Value: &RootType{},
+ JSON: Object{
+ "embedded0": "0",
+ "embedded1": "1",
+ "x-other": "abc",
+ },
+ },
+}
+
+type Object map[string]interface{}
+
+func TestExtensions(t *testing.T) {
+ for _, example := range Examples {
+ // Define JSON that will be unmarshalled
+ expectedData, err := json.Marshal(example.JSON)
+ if err != nil {
+ panic(err)
+ }
+ expected := string(expectedData)
+
+ // Define value that will marshalled
+ x := example.Value
+
+ // Unmarshal
+ if !example.NoUnmarshal {
+ t.Logf("Unmarshalling %T", x)
+ if err := jsoninfo.UnmarshalStrictStruct(expectedData, x); err != nil {
+ t.Fatalf("Error unmarshalling %T: %v", x, err)
+ }
+ t.Logf("Marshalling %T", x)
+ }
+
+ // Marshal
+ if !example.NoMarshal {
+ data, err := jsoninfo.MarshalStrictStruct(x)
+ if err != nil {
+ t.Fatalf("Error marshalling: %v", err)
+ }
+ actually := string(data)
+
+ if actually != expected {
+ t.Fatalf("Error!\nExpected: %s\nActually: %s", expected, actually)
+ }
+ }
+ }
+}
diff --git a/jsoninfo/strict_struct.go b/jsoninfo/strict_struct.go
new file mode 100644
index 000000000..6b4d83977
--- /dev/null
+++ b/jsoninfo/strict_struct.go
@@ -0,0 +1,6 @@
+package jsoninfo
+
+type StrictStruct interface {
+ EncodeWith(encoder *ObjectEncoder, value interface{}) error
+ DecodeWith(decoder *ObjectDecoder, value interface{}) error
+}
diff --git a/jsoninfo/type_info.go b/jsoninfo/type_info.go
new file mode 100644
index 000000000..3dbb8d5d6
--- /dev/null
+++ b/jsoninfo/type_info.go
@@ -0,0 +1,68 @@
+package jsoninfo
+
+import (
+ "reflect"
+ "sort"
+ "sync"
+)
+
+var (
+ typeInfos = map[reflect.Type]*TypeInfo{}
+ typeInfosMutex sync.RWMutex
+)
+
+// TypeInfo contains information about JSON serialization of a type
+type TypeInfo struct {
+ Type reflect.Type
+ Fields []FieldInfo
+}
+
+func GetTypeInfoForValue(value interface{}) *TypeInfo {
+ return GetTypeInfo(reflect.TypeOf(value))
+}
+
+// GetTypeInfo returns TypeInfo for the given type.
+func GetTypeInfo(t reflect.Type) *TypeInfo {
+ for t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ }
+ typeInfosMutex.RLock()
+ typeInfo, exists := typeInfos[t]
+ typeInfosMutex.RUnlock()
+ if exists {
+ return typeInfo
+ }
+ if t.Kind() != reflect.Struct {
+ typeInfo = &TypeInfo{
+ Type: t,
+ }
+ } else {
+ // Allocate
+ typeInfo = &TypeInfo{
+ Type: t,
+ Fields: make([]FieldInfo, 0, 16),
+ }
+
+ // Add fields
+ typeInfo.Fields = AppendFields(nil, nil, t)
+
+ // Sort fields
+ sort.Sort(sortableFieldInfos(typeInfo.Fields))
+ }
+
+ // Publish
+ typeInfosMutex.Lock()
+ typeInfos[t] = typeInfo
+ typeInfosMutex.Unlock()
+ return typeInfo
+}
+
+// FieldNames returns all field names
+func (typeInfo *TypeInfo) FieldNames() []string {
+ fields := typeInfo.Fields
+ names := make([]string, 0, len(fields))
+ for _, field := range fields {
+ names = append(names, field.JSONName)
+ }
+ return names
+}
diff --git a/jsoninfo/unmarshal.go b/jsoninfo/unmarshal.go
new file mode 100644
index 000000000..329718758
--- /dev/null
+++ b/jsoninfo/unmarshal.go
@@ -0,0 +1,121 @@
+package jsoninfo
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+)
+
+// UnmarshalStrictStruct function:
+// * Unmarshals struct fields, ignoring UnmarshalJSON(...) and fields without 'json' tag.
+// * Correctly handles StrictStruct
+func UnmarshalStrictStruct(data []byte, value StrictStruct) error {
+ decoder, err := NewObjectDecoder(data)
+ if err != nil {
+ return err
+ }
+ return value.DecodeWith(decoder, value)
+}
+
+type ObjectDecoder struct {
+ Data []byte
+ remainingFields map[string]json.RawMessage
+}
+
+func NewObjectDecoder(data []byte) (*ObjectDecoder, error) {
+ var remainingFields map[string]json.RawMessage
+ if err := json.Unmarshal(data, &remainingFields); err != nil {
+ return nil, fmt.Errorf("Failed to unmarshal extension properties: %v\nInput: %s", err, data)
+ }
+ return &ObjectDecoder{
+ Data: data,
+ remainingFields: remainingFields,
+ }, nil
+}
+
+// DecodeExtensionMap returns all properties that were not decoded previously.
+func (decoder *ObjectDecoder) DecodeExtensionMap() map[string]json.RawMessage {
+ return decoder.remainingFields
+}
+
+func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) error {
+ reflection := reflect.ValueOf(value)
+ if reflection.Kind() != reflect.Ptr {
+ panic(fmt.Errorf("Value %T is not a pointer", value))
+ }
+ if reflection.IsNil() {
+ panic(fmt.Errorf("Value %T is nil", value))
+ }
+ reflection = reflection.Elem()
+ for (reflection.Kind() == reflect.Interface || reflection.Kind() == reflect.Ptr) && !reflection.IsNil() {
+ reflection = reflection.Elem()
+ }
+ reflectionType := reflection.Type()
+ if reflectionType.Kind() != reflect.Struct {
+ panic(fmt.Errorf("Value %T is not a struct", value))
+ }
+ typeInfo := GetTypeInfo(reflectionType)
+
+ // Supported fields
+ fields := typeInfo.Fields
+ remainingFields := decoder.remainingFields
+ for fieldIndex, field := range fields {
+ // Fields without JSON tag are ignored
+ if !field.HasJSONTag {
+ continue
+ }
+
+ // Get data
+ fieldData, exists := remainingFields[field.JSONName]
+ if !exists {
+ continue
+ }
+
+ // Unmarshal
+ if field.TypeIsUnmarshaller {
+ fieldType := field.Type
+ isPtr := false
+ if fieldType.Kind() == reflect.Ptr {
+ fieldType = fieldType.Elem()
+ isPtr = true
+ }
+ fieldValue := reflect.New(fieldType)
+ if err := fieldValue.Interface().(json.Unmarshaler).UnmarshalJSON(fieldData); err != nil {
+ if field.MultipleFields {
+ i := fieldIndex + 1
+ if i < len(fields) && fields[i].JSONName == field.JSONName {
+ continue
+ }
+ }
+ return fmt.Errorf("Error while unmarshalling property '%s' (%s): %v",
+ field.JSONName, fieldValue.Type().String(), err)
+ }
+ if !isPtr {
+ fieldValue = fieldValue.Elem()
+ }
+ reflection.FieldByIndex(field.Index).Set(fieldValue)
+
+ // Remove the field from remaining fields
+ delete(remainingFields, field.JSONName)
+ } else {
+ fieldPtr := reflection.FieldByIndex(field.Index)
+ if fieldPtr.Kind() != reflect.Ptr || fieldPtr.IsNil() {
+ fieldPtr = fieldPtr.Addr()
+ }
+ if err := json.Unmarshal(fieldData, fieldPtr.Interface()); err != nil {
+ if field.MultipleFields {
+ i := fieldIndex + 1
+ if i < len(fields) && fields[i].JSONName == field.JSONName {
+ continue
+ }
+ }
+ return fmt.Errorf("Error while unmarshalling property '%s' (%s): %v",
+ field.JSONName, fieldPtr.Type().String(), err)
+ }
+
+ // Remove the field from remaining fields
+ delete(remainingFields, field.JSONName)
+ }
+ }
+ return nil
+}
diff --git a/jsoninfo/unmarshal_test.go b/jsoninfo/unmarshal_test.go
new file mode 100644
index 000000000..ce448a5fb
--- /dev/null
+++ b/jsoninfo/unmarshal_test.go
@@ -0,0 +1,157 @@
+package jsoninfo_test
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/getkin/kin-openapi/jsoninfo"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewObjectDecoder(t *testing.T) {
+ data := []byte(`
+ {
+ "field1": 1,
+ "field2": 2
+ }
+`)
+ t.Run("test new object decoder", func(t *testing.T) {
+ decoder, err := jsoninfo.NewObjectDecoder(data)
+ assert.Nil(t, err)
+ assert.NotNil(t, decoder)
+ assert.Equal(t, data, decoder.Data)
+ assert.Equal(t, 2, len(decoder.DecodeExtensionMap()))
+ })
+}
+
+type mockStrictStruct struct {
+ EncodeWithFn func(encoder *jsoninfo.ObjectEncoder, value interface{}) error
+ DecodeWithFn func(decoder *jsoninfo.ObjectDecoder, value interface{}) error
+}
+
+func (m *mockStrictStruct) EncodeWith(encoder *jsoninfo.ObjectEncoder, value interface{}) error {
+ return m.EncodeWithFn(encoder, value)
+}
+
+func (m *mockStrictStruct) DecodeWith(decoder *jsoninfo.ObjectDecoder, value interface{}) error {
+ return m.DecodeWithFn(decoder, value)
+}
+
+func TestUnmarshalStrictStruct(t *testing.T) {
+ data := []byte(`
+ {
+ "field1": 1,
+ "field2": 2
+ }
+ `)
+
+ t.Run("test unmarshal with StrictStruct without err", func(t *testing.T) {
+ decodeWithFnCalled := 0
+ mockStruct := &mockStrictStruct{
+ EncodeWithFn: func(encoder *jsoninfo.ObjectEncoder, value interface{}) error {
+ return nil
+ },
+ DecodeWithFn: func(decoder *jsoninfo.ObjectDecoder, value interface{}) error {
+ decodeWithFnCalled++
+ return nil
+ },
+ }
+ err := jsoninfo.UnmarshalStrictStruct(data, mockStruct)
+ assert.Nil(t, err)
+ assert.Equal(t, 1, decodeWithFnCalled)
+ })
+
+ t.Run("test unmarshal with StrictStruct with err", func(t *testing.T) {
+ decodeWithFnCalled := 0
+ mockStruct := &mockStrictStruct{
+ EncodeWithFn: func(encoder *jsoninfo.ObjectEncoder, value interface{}) error {
+ return nil
+ },
+ DecodeWithFn: func(decoder *jsoninfo.ObjectDecoder, value interface{}) error {
+ decodeWithFnCalled++
+ return errors.New("unable to decode the value")
+ },
+ }
+ err := jsoninfo.UnmarshalStrictStruct(data, mockStruct)
+ assert.NotNil(t, err)
+ assert.Equal(t, 1, decodeWithFnCalled)
+ })
+}
+
+func TestDecodeStructFieldsAndExtensions(t *testing.T) {
+ data := []byte(`
+ {
+ "field1": "field1",
+ "field2": "field2"
+ }
+`)
+ decoder, err := jsoninfo.NewObjectDecoder(data)
+ assert.Nil(t, err)
+ assert.NotNil(t, decoder)
+
+ t.Run("value is not pointer", func(t *testing.T) {
+ var value interface{}
+ assert.Panics(t, func() {
+ _ = decoder.DecodeStructFieldsAndExtensions(value)
+ }, "value is not a pointer")
+ })
+
+ t.Run("value is nil", func(t *testing.T) {
+ var value *string = nil
+ assert.Panics(t, func() {
+ _ = decoder.DecodeStructFieldsAndExtensions(value)
+ }, "value is nil")
+ })
+
+ t.Run("value is not struct", func(t *testing.T) {
+ var value = "simple string"
+ assert.Panics(t, func() {
+ _ = decoder.DecodeStructFieldsAndExtensions(&value)
+ }, "value is not struct")
+ })
+
+ t.Run("successfully decoded with all fields", func(t *testing.T) {
+ d, err := jsoninfo.NewObjectDecoder(data)
+ assert.Nil(t, err)
+ assert.NotNil(t, d)
+
+ var value = struct {
+ Field1 string `json:"field1"`
+ Field2 string `json:"field2"`
+ }{}
+ err = d.DecodeStructFieldsAndExtensions(&value)
+ assert.Nil(t, err)
+ assert.Equal(t, "field1", value.Field1)
+ assert.Equal(t, "field2", value.Field2)
+ assert.Equal(t, 0, len(d.DecodeExtensionMap()))
+ })
+
+ t.Run("successfully decoded with renaming field", func(t *testing.T) {
+ d, err := jsoninfo.NewObjectDecoder(data)
+ assert.Nil(t, err)
+ assert.NotNil(t, d)
+
+ var value = struct {
+ Field1 string `json:"field1"`
+ }{}
+ err = d.DecodeStructFieldsAndExtensions(&value)
+ assert.Nil(t, err)
+ assert.Equal(t, "field1", value.Field1)
+ assert.Equal(t, 1, len(d.DecodeExtensionMap()))
+ })
+
+ t.Run("un-successfully decoded due to data mismatch", func(t *testing.T) {
+ d, err := jsoninfo.NewObjectDecoder(data)
+ assert.Nil(t, err)
+ assert.NotNil(t, d)
+
+ var value = struct {
+ Field1 int `json:"field1"`
+ }{}
+ err = d.DecodeStructFieldsAndExtensions(&value)
+ assert.NotNil(t, err)
+ assert.EqualError(t, err, "Error while unmarshalling property 'field1' (*int): json: cannot unmarshal string into Go value of type int")
+ assert.Equal(t, 0, value.Field1)
+ assert.Equal(t, 2, len(d.DecodeExtensionMap()))
+ })
+}
diff --git a/jsoninfo/unsupported_properties_error.go b/jsoninfo/unsupported_properties_error.go
new file mode 100644
index 000000000..258efef28
--- /dev/null
+++ b/jsoninfo/unsupported_properties_error.go
@@ -0,0 +1,45 @@
+package jsoninfo
+
+import (
+ "encoding/json"
+ "fmt"
+ "sort"
+ "strings"
+)
+
+// UnsupportedPropertiesError is a helper for extensions that want to refuse
+// unsupported JSON object properties.
+//
+// It produces a helpful error message.
+type UnsupportedPropertiesError struct {
+ Value interface{}
+ UnsupportedProperties map[string]json.RawMessage
+}
+
+func NewUnsupportedPropertiesError(v interface{}, m map[string]json.RawMessage) error {
+ return &UnsupportedPropertiesError{
+ Value: v,
+ UnsupportedProperties: m,
+ }
+}
+
+func (err *UnsupportedPropertiesError) Error() string {
+ m := err.UnsupportedProperties
+ typeInfo := GetTypeInfoForValue(err.Value)
+ if m == nil || typeInfo == nil {
+ return "Invalid UnsupportedPropertiesError"
+ }
+ keys := make([]string, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ supported := typeInfo.FieldNames()
+ if len(supported) == 0 {
+ return fmt.Sprintf("Type '%T' doesn't take any properties. Unsupported properties: '%s'\n",
+ err.Value, strings.Join(keys, "', '"))
+ }
+ return fmt.Sprintf("Unsupported properties: '%s'\nSupported properties are: '%s'",
+ strings.Join(keys, "', '"),
+ strings.Join(supported, "', '"))
+}
diff --git a/openapi2/doc.go b/openapi2/doc.go
deleted file mode 100644
index b4762d597..000000000
--- a/openapi2/doc.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// Package openapi2 parses and writes OpenAPIv2 specification documents.
-//
-// Does not cover all elements of OpenAPIv2.
-// When OpenAPI version 3 is backwards-compatible with version 2, version 3 elements have been used.
-//
-// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md
-package openapi2
diff --git a/openapi2/header.go b/openapi2/header.go
deleted file mode 100644
index a51f99dee..000000000
--- a/openapi2/header.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package openapi2
-
-type Header struct {
- Parameter
-}
-
-// MarshalJSON returns the JSON encoding of Header.
-func (header Header) MarshalJSON() ([]byte, error) {
- return header.Parameter.MarshalJSON()
-}
-
-// UnmarshalJSON sets Header to a copy of data.
-func (header *Header) UnmarshalJSON(data []byte) error {
- return header.Parameter.UnmarshalJSON(data)
-}
diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go
index 88835db95..3da171ddd 100644
--- a/openapi2/openapi2.go
+++ b/openapi2/openapi2.go
@@ -1,117 +1,192 @@
+// Package openapi2 parses and writes OpenAPI 2 specifications.
+//
+// Does not cover all elements of OpenAPI 2.
+// When OpenAPI version 3 is backwards-compatible with version 2, version 3 elements have been used.
+//
+// The specification:
+// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md
package openapi2
import (
- "encoding/json"
+ "fmt"
+ "net/http"
"github.com/getkin/kin-openapi/openapi3"
)
-// T is the root of an OpenAPI v2 document
-type T struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Swagger string `json:"swagger" yaml:"swagger"` // required
- Info openapi3.Info `json:"info" yaml:"info"` // required
- ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
- Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"`
- Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"`
- Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"`
- Host string `json:"host,omitempty" yaml:"host,omitempty"`
- BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"`
- Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"`
- Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"`
- Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"`
- Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"`
- SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"`
- Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"`
- Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"`
+type Swagger struct {
+ Info openapi3.Info `json:"info"`
+ ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty"`
+ Schemes []string `json:"schemes,omitempty"`
+ Host string `json:"host,omitempty"`
+ BasePath string `json:"basePath,omitempty"`
+ Paths map[string]*PathItem `json:"paths,omitempty"`
+ Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty,noref"`
+ Parameters map[string]*Parameter `json:"parameters,omitempty,noref"`
+ Responses map[string]*Response `json:"responses,omitempty,noref"`
+ SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty"`
+ Security SecurityRequirements `json:"security,omitempty"`
+ Tags openapi3.Tags `json:"tags,omitempty"`
}
-// MarshalJSON returns the JSON encoding of T.
-func (doc T) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 15+len(doc.Extensions))
- for k, v := range doc.Extensions {
- m[k] = v
- }
- m["swagger"] = doc.Swagger
- m["info"] = doc.Info
- if x := doc.ExternalDocs; x != nil {
- m["externalDocs"] = x
- }
- if x := doc.Schemes; len(x) != 0 {
- m["schemes"] = x
- }
- if x := doc.Consumes; len(x) != 0 {
- m["consumes"] = x
+func (swagger *Swagger) AddOperation(path string, method string, operation *Operation) {
+ paths := swagger.Paths
+ if paths == nil {
+ paths = make(map[string]*PathItem, 8)
+ swagger.Paths = paths
}
- if x := doc.Produces; len(x) != 0 {
- m["produces"] = x
- }
- if x := doc.Host; x != "" {
- m["host"] = x
- }
- if x := doc.BasePath; x != "" {
- m["basePath"] = x
+ pathItem := paths[path]
+ if pathItem == nil {
+ pathItem = &PathItem{}
+ paths[path] = pathItem
}
- if x := doc.Paths; len(x) != 0 {
- m["paths"] = x
+ pathItem.SetOperation(method, operation)
+}
+
+type PathItem struct {
+ Ref string `json:"$ref,omitempty"`
+ Delete *Operation `json:"delete,omitempty"`
+ Get *Operation `json:"get,omitempty"`
+ Head *Operation `json:"head,omitempty"`
+ Options *Operation `json:"options,omitempty"`
+ Patch *Operation `json:"patch,omitempty"`
+ Post *Operation `json:"post,omitempty"`
+ Put *Operation `json:"put,omitempty"`
+ Parameters Parameters `json:"parameters,omitempty"`
+}
+
+func (pathItem *PathItem) Operations() map[string]*Operation {
+ operations := make(map[string]*Operation, 8)
+ if v := pathItem.Delete; v != nil {
+ operations[http.MethodDelete] = v
}
- if x := doc.Definitions; len(x) != 0 {
- m["definitions"] = x
+ if v := pathItem.Get; v != nil {
+ operations[http.MethodGet] = v
}
- if x := doc.Parameters; len(x) != 0 {
- m["parameters"] = x
+ if v := pathItem.Head; v != nil {
+ operations[http.MethodHead] = v
}
- if x := doc.Responses; len(x) != 0 {
- m["responses"] = x
+ if v := pathItem.Options; v != nil {
+ operations[http.MethodOptions] = v
}
- if x := doc.SecurityDefinitions; len(x) != 0 {
- m["securityDefinitions"] = x
+ if v := pathItem.Patch; v != nil {
+ operations[http.MethodPatch] = v
}
- if x := doc.Security; len(x) != 0 {
- m["security"] = x
+ if v := pathItem.Post; v != nil {
+ operations[http.MethodPost] = v
}
- if x := doc.Tags; len(x) != 0 {
- m["tags"] = x
+ if v := pathItem.Put; v != nil {
+ operations[http.MethodPut] = v
}
- return json.Marshal(m)
+ return operations
}
-// UnmarshalJSON sets T to a copy of data.
-func (doc *T) UnmarshalJSON(data []byte) error {
- type TBis T
- var x TBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
+func (pathItem *PathItem) GetOperation(method string) *Operation {
+ switch method {
+ case http.MethodDelete:
+ return pathItem.Delete
+ case http.MethodGet:
+ return pathItem.Get
+ case http.MethodHead:
+ return pathItem.Head
+ case http.MethodOptions:
+ return pathItem.Options
+ case http.MethodPatch:
+ return pathItem.Patch
+ case http.MethodPost:
+ return pathItem.Post
+ case http.MethodPut:
+ return pathItem.Put
+ default:
+ panic(fmt.Errorf("Unsupported HTTP method '%s'", method))
}
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "swagger")
- delete(x.Extensions, "info")
- delete(x.Extensions, "externalDocs")
- delete(x.Extensions, "schemes")
- delete(x.Extensions, "consumes")
- delete(x.Extensions, "produces")
- delete(x.Extensions, "host")
- delete(x.Extensions, "basePath")
- delete(x.Extensions, "paths")
- delete(x.Extensions, "definitions")
- delete(x.Extensions, "parameters")
- delete(x.Extensions, "responses")
- delete(x.Extensions, "securityDefinitions")
- delete(x.Extensions, "security")
- delete(x.Extensions, "tags")
- *doc = T(x)
- return nil
}
-func (doc *T) AddOperation(path string, method string, operation *Operation) {
- if doc.Paths == nil {
- doc.Paths = make(map[string]*PathItem)
+func (pathItem *PathItem) SetOperation(method string, operation *Operation) {
+ switch method {
+ case http.MethodDelete:
+ pathItem.Delete = operation
+ case http.MethodGet:
+ pathItem.Get = operation
+ case http.MethodHead:
+ pathItem.Head = operation
+ case http.MethodOptions:
+ pathItem.Options = operation
+ case http.MethodPatch:
+ pathItem.Patch = operation
+ case http.MethodPost:
+ pathItem.Post = operation
+ case http.MethodPut:
+ pathItem.Put = operation
+ default:
+ panic(fmt.Errorf("Unsupported HTTP method '%s'", method))
}
- pathItem := doc.Paths[path]
- if pathItem == nil {
- pathItem = &PathItem{}
- doc.Paths[path] = pathItem
- }
- pathItem.SetOperation(method, operation)
+}
+
+type Operation struct {
+ Summary string `json:"summary,omitempty"`
+ Description string `json:"description,omitempty"`
+ ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ OperationID string `json:"operationId,omitempty"`
+ Parameters Parameters `json:"parameters,omitempty"`
+ Responses map[string]*Response `json:"responses"`
+ Consumes []string `json:"consumes,omitempty"`
+ Produces []string `json:"produces,omitempty"`
+ Security *SecurityRequirements `json:"security,omitempty"`
+}
+
+type Parameters []*Parameter
+
+type Parameter struct {
+ Ref string `json:"$ref,omitempty"`
+ In string `json:"in,omitempty"`
+ Name string `json:"name,omitempty"`
+ Description string `json:"description,omitempty"`
+ Required bool `json:"required,omitempty"`
+ UniqueItems bool `json:"uniqueItems,omitempty"`
+ ExclusiveMin bool `json:"exclusiveMinimum,omitempty"`
+ ExclusiveMax bool `json:"exclusiveMaximum,omitempty"`
+ Schema *openapi3.SchemaRef `json:"schema,omitempty"`
+ Type string `json:"type,omitempty"`
+ Format string `json:"format,omitempty"`
+ Enum []interface{} `json:"enum,omitempty"`
+ Minimum *float64 `json:"minimum,omitempty"`
+ Maximum *float64 `json:"maximum,omitempty"`
+ MinLength uint64 `json:"minLength,omitempty"`
+ MaxLength *uint64 `json:"maxLength,omitempty"`
+ Pattern string `json:"pattern,omitempty"`
+ Items *openapi3.SchemaRef `json:"items,omitempty"`
+ MinItems uint64 `json:"minItems,omitempty"`
+ MaxItems *uint64 `json:"maxItems,omitempty"`
+ Default interface{} `json:"default,omitempty"`
+}
+
+type Response struct {
+ Ref string `json:"$ref,omitempty"`
+ Description string `json:"description,omitempty"`
+ Schema *openapi3.SchemaRef `json:"schema,omitempty"`
+ Headers map[string]*Header `json:"headers,omitempty"`
+ Examples map[string]interface{} `json:"examples,omitempty"`
+}
+
+type Header struct {
+ Ref string `json:"$ref,omitempty"`
+ Description string `json:"description,omitempty"`
+ Type string `json:"type,omitempty"`
+}
+
+type SecurityRequirements []map[string][]string
+
+type SecurityScheme struct {
+ Ref string `json:"$ref,omitempty"`
+ Description string `json:"description,omitempty"`
+ Type string `json:"type,omitempty"`
+ In string `json:"in,omitempty"`
+ Name string `json:"name,omitempty"`
+ Flow string `json:"flow,omitempty"`
+ AuthorizationURL string `json:"authorizationUrl,omitempty"`
+ TokenURL string `json:"tokenUrl,omitempty"`
+ Scopes map[string]string `json:"scopes,omitempty"`
+ Tags openapi3.Tags `json:"tags,omitempty"`
}
diff --git a/openapi2/openapi2_test.go b/openapi2/openapi2_test.go
index 65e92d601..78194850a 100644
--- a/openapi2/openapi2_test.go
+++ b/openapi2/openapi2_test.go
@@ -1,53 +1,25 @@
-package openapi2_test
+package openapi2
import (
"encoding/json"
- "fmt"
"io/ioutil"
- "reflect"
+ "testing"
- "github.com/invopop/yaml"
-
- "github.com/getkin/kin-openapi/openapi2"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
-func Example() {
- input, err := ioutil.ReadFile("testdata/swagger.json")
- if err != nil {
- panic(err)
- }
+func TestReadingSwagger(t *testing.T) {
+ var swagger Swagger
- var doc openapi2.T
- if err = json.Unmarshal(input, &doc); err != nil {
- panic(err)
- }
- if doc.ExternalDocs.Description != "Find out more about Swagger" {
- panic(`doc.ExternalDocs was parsed incorrectly!`)
- }
+ input, err := ioutil.ReadFile("testdata/swagger.json")
+ require.NoError(t, err)
- outputJSON, err := json.Marshal(doc)
- if err != nil {
- panic(err)
- }
- var docAgainFromJSON openapi2.T
- if err = json.Unmarshal(outputJSON, &docAgainFromJSON); err != nil {
- panic(err)
- }
- if !reflect.DeepEqual(doc, docAgainFromJSON) {
- fmt.Println("objects doc & docAgainFromJSON should be the same")
- }
+ err = json.Unmarshal(input, &swagger)
+ require.NoError(t, err)
- outputYAML, err := yaml.Marshal(doc)
- if err != nil {
- panic(err)
- }
- var docAgainFromYAML openapi2.T
- if err = yaml.Unmarshal(outputYAML, &docAgainFromYAML); err != nil {
- panic(err)
- }
- if !reflect.DeepEqual(doc, docAgainFromYAML) {
- fmt.Println("objects doc & docAgainFromYAML should be the same")
- }
+ output, err := json.Marshal(swagger)
+ require.NoError(t, err)
- // Output:
+ assert.JSONEq(t, string(input), string(output))
}
diff --git a/openapi2/operation.go b/openapi2/operation.go
deleted file mode 100644
index b29f67de3..000000000
--- a/openapi2/operation.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package openapi2
-
-import (
- "encoding/json"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-type Operation struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
- Description string `json:"description,omitempty" yaml:"description,omitempty"`
- Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
- ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
- Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
- OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"`
- Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"`
- Responses map[string]*Response `json:"responses" yaml:"responses"`
- Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"`
- Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"`
- Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"`
- Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"`
-}
-
-// MarshalJSON returns the JSON encoding of Operation.
-func (operation Operation) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 12+len(operation.Extensions))
- for k, v := range operation.Extensions {
- m[k] = v
- }
- if x := operation.Summary; x != "" {
- m["summary"] = x
- }
- if x := operation.Description; x != "" {
- m["description"] = x
- }
- if x := operation.Deprecated; x {
- m["deprecated"] = x
- }
- if x := operation.ExternalDocs; x != nil {
- m["externalDocs"] = x
- }
- if x := operation.Tags; len(x) != 0 {
- m["tags"] = x
- }
- if x := operation.OperationID; x != "" {
- m["operationId"] = x
- }
- if x := operation.Parameters; len(x) != 0 {
- m["parameters"] = x
- }
- m["responses"] = operation.Responses
- if x := operation.Consumes; len(x) != 0 {
- m["consumes"] = x
- }
- if x := operation.Produces; len(x) != 0 {
- m["produces"] = x
- }
- if x := operation.Schemes; len(x) != 0 {
- m["schemes"] = x
- }
- if x := operation.Security; x != nil {
- m["security"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets Operation to a copy of data.
-func (operation *Operation) UnmarshalJSON(data []byte) error {
- type OperationBis Operation
- var x OperationBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "summary")
- delete(x.Extensions, "description")
- delete(x.Extensions, "deprecated")
- delete(x.Extensions, "externalDocs")
- delete(x.Extensions, "tags")
- delete(x.Extensions, "operationId")
- delete(x.Extensions, "parameters")
- delete(x.Extensions, "responses")
- delete(x.Extensions, "consumes")
- delete(x.Extensions, "produces")
- delete(x.Extensions, "schemes")
- delete(x.Extensions, "security")
- *operation = Operation(x)
- return nil
-}
diff --git a/openapi2/parameter.go b/openapi2/parameter.go
deleted file mode 100644
index d2c71c64f..000000000
--- a/openapi2/parameter.go
+++ /dev/null
@@ -1,176 +0,0 @@
-package openapi2
-
-import (
- "encoding/json"
- "sort"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-type Parameters []*Parameter
-
-var _ sort.Interface = Parameters{}
-
-func (ps Parameters) Len() int { return len(ps) }
-func (ps Parameters) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] }
-func (ps Parameters) Less(i, j int) bool {
- if ps[i].Name != ps[j].Name {
- return ps[i].Name < ps[j].Name
- }
- if ps[i].In != ps[j].In {
- return ps[i].In < ps[j].In
- }
- return ps[i].Ref < ps[j].Ref
-}
-
-type Parameter struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
-
- In string `json:"in,omitempty" yaml:"in,omitempty"`
- Name string `json:"name,omitempty" yaml:"name,omitempty"`
- Description string `json:"description,omitempty" yaml:"description,omitempty"`
- CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"`
- Type string `json:"type,omitempty" yaml:"type,omitempty"`
- Format string `json:"format,omitempty" yaml:"format,omitempty"`
- Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"`
- AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
- Required bool `json:"required,omitempty" yaml:"required,omitempty"`
- UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
- ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"`
- ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"`
- Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
- Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"`
- Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"`
- MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"`
- Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
- Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"`
- MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"`
- MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"`
- MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"`
- MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"`
- Default interface{} `json:"default,omitempty" yaml:"default,omitempty"`
-}
-
-// MarshalJSON returns the JSON encoding of Parameter.
-func (parameter Parameter) MarshalJSON() ([]byte, error) {
- if ref := parameter.Ref; ref != "" {
- return json.Marshal(openapi3.Ref{Ref: ref})
- }
-
- m := make(map[string]interface{}, 24+len(parameter.Extensions))
- for k, v := range parameter.Extensions {
- m[k] = v
- }
-
- if x := parameter.In; x != "" {
- m["in"] = x
- }
- if x := parameter.Name; x != "" {
- m["name"] = x
- }
- if x := parameter.Description; x != "" {
- m["description"] = x
- }
- if x := parameter.CollectionFormat; x != "" {
- m["collectionFormat"] = x
- }
- if x := parameter.Type; x != "" {
- m["type"] = x
- }
- if x := parameter.Format; x != "" {
- m["format"] = x
- }
- if x := parameter.Pattern; x != "" {
- m["pattern"] = x
- }
- if x := parameter.AllowEmptyValue; x {
- m["allowEmptyValue"] = x
- }
- if x := parameter.Required; x {
- m["required"] = x
- }
- if x := parameter.UniqueItems; x {
- m["uniqueItems"] = x
- }
- if x := parameter.ExclusiveMin; x {
- m["exclusiveMinimum"] = x
- }
- if x := parameter.ExclusiveMax; x {
- m["exclusiveMaximum"] = x
- }
- if x := parameter.Schema; x != nil {
- m["schema"] = x
- }
- if x := parameter.Items; x != nil {
- m["items"] = x
- }
- if x := parameter.Enum; x != nil {
- m["enum"] = x
- }
- if x := parameter.MultipleOf; x != nil {
- m["multipleOf"] = x
- }
- if x := parameter.Minimum; x != nil {
- m["minimum"] = x
- }
- if x := parameter.Maximum; x != nil {
- m["maximum"] = x
- }
- if x := parameter.MaxLength; x != nil {
- m["maxLength"] = x
- }
- if x := parameter.MaxItems; x != nil {
- m["maxItems"] = x
- }
- if x := parameter.MinLength; x != 0 {
- m["minLength"] = x
- }
- if x := parameter.MinItems; x != 0 {
- m["minItems"] = x
- }
- if x := parameter.Default; x != nil {
- m["default"] = x
- }
-
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets Parameter to a copy of data.
-func (parameter *Parameter) UnmarshalJSON(data []byte) error {
- type ParameterBis Parameter
- var x ParameterBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "$ref")
-
- delete(x.Extensions, "in")
- delete(x.Extensions, "name")
- delete(x.Extensions, "description")
- delete(x.Extensions, "collectionFormat")
- delete(x.Extensions, "type")
- delete(x.Extensions, "format")
- delete(x.Extensions, "pattern")
- delete(x.Extensions, "allowEmptyValue")
- delete(x.Extensions, "required")
- delete(x.Extensions, "uniqueItems")
- delete(x.Extensions, "exclusiveMinimum")
- delete(x.Extensions, "exclusiveMaximum")
- delete(x.Extensions, "schema")
- delete(x.Extensions, "items")
- delete(x.Extensions, "enum")
- delete(x.Extensions, "multipleOf")
- delete(x.Extensions, "minimum")
- delete(x.Extensions, "maximum")
- delete(x.Extensions, "maxLength")
- delete(x.Extensions, "maxItems")
- delete(x.Extensions, "minLength")
- delete(x.Extensions, "minItems")
- delete(x.Extensions, "default")
-
- *parameter = Parameter(x)
- return nil
-}
diff --git a/openapi2/path_item.go b/openapi2/path_item.go
deleted file mode 100644
index 95c060e7b..000000000
--- a/openapi2/path_item.go
+++ /dev/null
@@ -1,150 +0,0 @@
-package openapi2
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-type PathItem struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
-
- Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"`
- Get *Operation `json:"get,omitempty" yaml:"get,omitempty"`
- Head *Operation `json:"head,omitempty" yaml:"head,omitempty"`
- Options *Operation `json:"options,omitempty" yaml:"options,omitempty"`
- Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"`
- Post *Operation `json:"post,omitempty" yaml:"post,omitempty"`
- Put *Operation `json:"put,omitempty" yaml:"put,omitempty"`
- Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"`
-}
-
-// MarshalJSON returns the JSON encoding of PathItem.
-func (pathItem PathItem) MarshalJSON() ([]byte, error) {
- if ref := pathItem.Ref; ref != "" {
- return json.Marshal(openapi3.Ref{Ref: ref})
- }
-
- m := make(map[string]interface{}, 8+len(pathItem.Extensions))
- for k, v := range pathItem.Extensions {
- m[k] = v
- }
- if x := pathItem.Delete; x != nil {
- m["delete"] = x
- }
- if x := pathItem.Get; x != nil {
- m["get"] = x
- }
- if x := pathItem.Head; x != nil {
- m["head"] = x
- }
- if x := pathItem.Options; x != nil {
- m["options"] = x
- }
- if x := pathItem.Patch; x != nil {
- m["patch"] = x
- }
- if x := pathItem.Post; x != nil {
- m["post"] = x
- }
- if x := pathItem.Put; x != nil {
- m["put"] = x
- }
- if x := pathItem.Parameters; len(x) != 0 {
- m["parameters"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets PathItem to a copy of data.
-func (pathItem *PathItem) UnmarshalJSON(data []byte) error {
- type PathItemBis PathItem
- var x PathItemBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "$ref")
- delete(x.Extensions, "delete")
- delete(x.Extensions, "get")
- delete(x.Extensions, "head")
- delete(x.Extensions, "options")
- delete(x.Extensions, "patch")
- delete(x.Extensions, "post")
- delete(x.Extensions, "put")
- delete(x.Extensions, "parameters")
- *pathItem = PathItem(x)
- return nil
-}
-
-func (pathItem *PathItem) Operations() map[string]*Operation {
- operations := make(map[string]*Operation)
- if v := pathItem.Delete; v != nil {
- operations[http.MethodDelete] = v
- }
- if v := pathItem.Get; v != nil {
- operations[http.MethodGet] = v
- }
- if v := pathItem.Head; v != nil {
- operations[http.MethodHead] = v
- }
- if v := pathItem.Options; v != nil {
- operations[http.MethodOptions] = v
- }
- if v := pathItem.Patch; v != nil {
- operations[http.MethodPatch] = v
- }
- if v := pathItem.Post; v != nil {
- operations[http.MethodPost] = v
- }
- if v := pathItem.Put; v != nil {
- operations[http.MethodPut] = v
- }
- return operations
-}
-
-func (pathItem *PathItem) GetOperation(method string) *Operation {
- switch method {
- case http.MethodDelete:
- return pathItem.Delete
- case http.MethodGet:
- return pathItem.Get
- case http.MethodHead:
- return pathItem.Head
- case http.MethodOptions:
- return pathItem.Options
- case http.MethodPatch:
- return pathItem.Patch
- case http.MethodPost:
- return pathItem.Post
- case http.MethodPut:
- return pathItem.Put
- default:
- panic(fmt.Errorf("unsupported HTTP method %q", method))
- }
-}
-
-func (pathItem *PathItem) SetOperation(method string, operation *Operation) {
- switch method {
- case http.MethodDelete:
- pathItem.Delete = operation
- case http.MethodGet:
- pathItem.Get = operation
- case http.MethodHead:
- pathItem.Head = operation
- case http.MethodOptions:
- pathItem.Options = operation
- case http.MethodPatch:
- pathItem.Patch = operation
- case http.MethodPost:
- pathItem.Post = operation
- case http.MethodPut:
- pathItem.Put = operation
- default:
- panic(fmt.Errorf("unsupported HTTP method %q", method))
- }
-}
diff --git a/openapi2/response.go b/openapi2/response.go
deleted file mode 100644
index bd18f882d..000000000
--- a/openapi2/response.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package openapi2
-
-import (
- "encoding/json"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-type Response struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
-
- Description string `json:"description,omitempty" yaml:"description,omitempty"`
- Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
- Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"`
- Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"`
-}
-
-// MarshalJSON returns the JSON encoding of Response.
-func (response Response) MarshalJSON() ([]byte, error) {
- if ref := response.Ref; ref != "" {
- return json.Marshal(openapi3.Ref{Ref: ref})
- }
-
- m := make(map[string]interface{}, 4+len(response.Extensions))
- for k, v := range response.Extensions {
- m[k] = v
- }
- if x := response.Description; x != "" {
- m["description"] = x
- }
- if x := response.Schema; x != nil {
- m["schema"] = x
- }
- if x := response.Headers; len(x) != 0 {
- m["headers"] = x
- }
- if x := response.Examples; len(x) != 0 {
- m["examples"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets Response to a copy of data.
-func (response *Response) UnmarshalJSON(data []byte) error {
- type ResponseBis Response
- var x ResponseBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "$ref")
- delete(x.Extensions, "description")
- delete(x.Extensions, "schema")
- delete(x.Extensions, "headers")
- delete(x.Extensions, "examples")
- *response = Response(x)
- return nil
-}
diff --git a/openapi2/security_scheme.go b/openapi2/security_scheme.go
deleted file mode 100644
index 5a8c278bd..000000000
--- a/openapi2/security_scheme.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package openapi2
-
-import (
- "encoding/json"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-type SecurityRequirements []map[string][]string
-
-type SecurityScheme struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
-
- Description string `json:"description,omitempty" yaml:"description,omitempty"`
- Type string `json:"type,omitempty" yaml:"type,omitempty"`
- In string `json:"in,omitempty" yaml:"in,omitempty"`
- Name string `json:"name,omitempty" yaml:"name,omitempty"`
- Flow string `json:"flow,omitempty" yaml:"flow,omitempty"`
- AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
- TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
- Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"`
- Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"`
-}
-
-// MarshalJSON returns the JSON encoding of SecurityScheme.
-func (securityScheme SecurityScheme) MarshalJSON() ([]byte, error) {
- if ref := securityScheme.Ref; ref != "" {
- return json.Marshal(openapi3.Ref{Ref: ref})
- }
-
- m := make(map[string]interface{}, 10+len(securityScheme.Extensions))
- for k, v := range securityScheme.Extensions {
- m[k] = v
- }
- if x := securityScheme.Description; x != "" {
- m["description"] = x
- }
- if x := securityScheme.Type; x != "" {
- m["type"] = x
- }
- if x := securityScheme.In; x != "" {
- m["in"] = x
- }
- if x := securityScheme.Name; x != "" {
- m["name"] = x
- }
- if x := securityScheme.Flow; x != "" {
- m["flow"] = x
- }
- if x := securityScheme.AuthorizationURL; x != "" {
- m["authorizationUrl"] = x
- }
- if x := securityScheme.TokenURL; x != "" {
- m["tokenUrl"] = x
- }
- if x := securityScheme.Scopes; len(x) != 0 {
- m["scopes"] = x
- }
- if x := securityScheme.Tags; len(x) != 0 {
- m["tags"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets SecurityScheme to a copy of data.
-func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error {
- type SecuritySchemeBis SecurityScheme
- var x SecuritySchemeBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "$ref")
- delete(x.Extensions, "description")
- delete(x.Extensions, "type")
- delete(x.Extensions, "in")
- delete(x.Extensions, "name")
- delete(x.Extensions, "flow")
- delete(x.Extensions, "authorizationUrl")
- delete(x.Extensions, "tokenUrl")
- delete(x.Extensions, "scopes")
- delete(x.Extensions, "tags")
- *securityScheme = SecurityScheme(x)
- return nil
-}
diff --git a/openapi2/testdata/swagger.json b/openapi2/testdata/swagger.json
index 57f75d9a7..91484ff26 100644
--- a/openapi2/testdata/swagger.json
+++ b/openapi2/testdata/swagger.json
@@ -1 +1 @@
-{"swagger":"2.0","info":{"title":"Swagger Petstore","description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.","termsOfService":"http://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"1.0.3"},"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"},"schemes":["https","http"],"host":"petstore.swagger.io","basePath":"/v2","paths":{"/pet":{"post":{"summary":"Add a new pet to the store","tags":["pet"],"operationId":"addPet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"put":{"summary":"Update an existing pet","tags":["pet"],"operationId":"updatePet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"405":{"description":"Validation exception"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByStatus":{"get":{"summary":"Finds Pets by status","description":"Multiple status values can be provided with comma separated strings","tags":["pet"],"operationId":"findPetsByStatus","parameters":[{"in":"query","name":"status","description":"Status values that need to be considered for filter","required":true,"type":"array","items":{"default":"available","enum":["available","pending","sold"],"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid status value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByTags":{"get":{"summary":"Finds Pets by tags","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","tags":["pet"],"operationId":"findPetsByTags","parameters":[{"in":"query","name":"tags","description":"Tags to filter by","required":true,"type":"array","items":{"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid tag value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}":{"delete":{"summary":"Deletes a pet","tags":["pet"],"operationId":"deletePet","parameters":[{"in":"header","name":"api_key","type":"string"},{"in":"path","name":"petId","description":"Pet id to delete","required":true,"type":"integer","format":"int64"}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"get":{"summary":"Find pet by ID","description":"Returns a single pet","tags":["pet"],"operationId":"getPetById","parameters":[{"in":"path","name":"petId","description":"ID of pet to return","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Pet"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"api_key":[]}]},"post":{"summary":"Updates a pet in the store with form data","tags":["pet"],"operationId":"updatePetWithForm","parameters":[{"in":"path","name":"petId","description":"ID of pet that needs to be updated","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"name","description":"Updated name of the pet","type":"string"},{"in":"formData","name":"status","description":"Updated status of the pet","type":"string"}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/x-www-form-urlencoded"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}/uploadImage":{"post":{"summary":"uploads an image","tags":["pet"],"operationId":"uploadFile","parameters":[{"in":"path","name":"petId","description":"ID of pet to update","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"additionalMetadata","description":"Additional data to pass to server","type":"string"},{"in":"formData","name":"file","description":"file to upload","type":"file"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ApiResponse"}}},"consumes":["multipart/form-data"],"produces":["application/json"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/store/inventory":{"get":{"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities","tags":["store"],"operationId":"getInventory","responses":{"200":{"description":"successful operation","schema":{"additionalProperties":{"format":"int32","type":"integer"},"type":"object"}}},"produces":["application/json"],"security":[{"api_key":[]}]}},"/store/order":{"post":{"summary":"Place an order for a pet","tags":["store"],"operationId":"placeOrder","parameters":[{"in":"body","name":"body","description":"order placed for purchasing the pet","required":true,"schema":{"$ref":"#/definitions/Order"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid Order"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/store/order/{orderId}":{"delete":{"summary":"Delete purchase order by ID","description":"For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors","tags":["store"],"operationId":"deleteOrder","parameters":[{"in":"path","name":"orderId","description":"ID of the order that needs to be deleted","required":true,"type":"integer","format":"int64","minimum":1}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Find purchase order by ID","description":"For valid response try integer IDs with value \u003e= 1 and \u003c= 10. Other values will generated exceptions","tags":["store"],"operationId":"getOrderById","parameters":[{"in":"path","name":"orderId","description":"ID of pet that needs to be fetched","required":true,"type":"integer","format":"int64","minimum":1,"maximum":10}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]}},"/user":{"post":{"summary":"Create user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"createUser","parameters":[{"in":"body","name":"body","description":"Created user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithArray":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithArrayInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithList":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithListInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/login":{"get":{"summary":"Logs user into the system","tags":["user"],"operationId":"loginUser","parameters":[{"in":"query","name":"username","description":"The user name for login","required":true,"type":"string"},{"in":"query","name":"password","description":"The password for login in clear text","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"string"},"headers":{"X-Expires-After":{"description":"date in UTC when token expires","type":"string"},"X-Rate-Limit":{"description":"calls per hour allowed by the user","type":"integer"}}},"400":{"description":"Invalid username/password supplied"}},"produces":["application/json","application/xml"]}},"/user/logout":{"get":{"summary":"Logs out current logged in user session","tags":["user"],"operationId":"logoutUser","responses":{"default":{"description":"successful operation"}},"produces":["application/json","application/xml"]}},"/user/{username}":{"delete":{"summary":"Delete user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"deleteUser","parameters":[{"in":"path","name":"username","description":"The name that needs to be deleted","required":true,"type":"string"}],"responses":{"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Get user by user name","tags":["user"],"operationId":"getUserByName","parameters":[{"in":"path","name":"username","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"put":{"summary":"Updated user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"updateUser","parameters":[{"in":"path","name":"username","description":"name that need to be updated","required":true,"type":"string"},{"in":"body","name":"body","description":"Updated user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"400":{"description":"Invalid user supplied"},"404":{"description":"User not found"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}}},"definitions":{"ApiResponse":{"properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"},"type":{"type":"string"}},"type":"object"},"Category":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Category"}},"Order":{"properties":{"complete":{"type":"boolean"},"id":{"format":"int64","type":"integer"},"petId":{"format":"int64","type":"integer"},"quantity":{"format":"int32","type":"integer"},"shipDate":{"format":"date-time","type":"string"},"status":{"description":"Order Status","enum":["placed","approved","delivered"],"type":"string"}},"type":"object","xml":{"name":"Order"}},"Pet":{"properties":{"category":{"$ref":"#/definitions/Category"},"id":{"format":"int64","type":"integer"},"name":{"example":"doggie","type":"string"},"photoUrls":{"items":{"type":"string","xml":{"name":"photoUrl"}},"type":"array","xml":{"wrapped":true}},"status":{"description":"pet status in the store","enum":["available","pending","sold"],"type":"string"},"tags":{"items":{"$ref":"#/definitions/Tag"},"type":"array","xml":{"wrapped":true}}},"required":["name","photoUrls"],"type":"object","xml":{"name":"Pet"}},"Tag":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Tag"}},"User":{"properties":{"email":{"type":"string"},"firstName":{"type":"string"},"id":{"format":"int64","type":"integer"},"lastName":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"description":"User Status","format":"int32","type":"integer"},"username":{"type":"string"}},"type":"object","xml":{"name":"User"}}},"securityDefinitions":{"api_key":{"type":"apiKey","in":"header","name":"api_key"},"petstore_auth":{"type":"oauth2","flow":"implicit","authorizationUrl":"https://petstore.swagger.io/oauth/authorize","scopes":{"read:pets":"read your pets","write:pets":"modify pets in your account"}}},"tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"store","description":"Access to Petstore orders"},{"name":"user","description":"Operations about user","externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}]}
\ No newline at end of file
+{"info":{"title":"Swagger Petstore","description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.","termsOfService":"http://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"1.0.3"},"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"},"schemes":["https","http"],"host":"petstore.swagger.io","basePath":"/v2","paths":{"/pet":{"post":{"summary":"Add a new pet to the store","tags":["pet"],"operationId":"addPet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"put":{"summary":"Update an existing pet","tags":["pet"],"operationId":"updatePet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"405":{"description":"Validation exception"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByStatus":{"get":{"summary":"Finds Pets by status","description":"Multiple status values can be provided with comma separated strings","tags":["pet"],"operationId":"findPetsByStatus","parameters":[{"in":"query","name":"status","description":"Status values that need to be considered for filter","required":true,"type":"array","items":{"default":"available","enum":["available","pending","sold"],"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid status value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByTags":{"get":{"summary":"Finds Pets by tags","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","tags":["pet"],"operationId":"findPetsByTags","parameters":[{"in":"query","name":"tags","description":"Tags to filter by","required":true,"type":"array","items":{"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid tag value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}":{"delete":{"summary":"Deletes a pet","tags":["pet"],"operationId":"deletePet","parameters":[{"in":"header","name":"api_key","type":"string"},{"in":"path","name":"petId","description":"Pet id to delete","required":true,"type":"integer","format":"int64"}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"get":{"summary":"Find pet by ID","description":"Returns a single pet","tags":["pet"],"operationId":"getPetById","parameters":[{"in":"path","name":"petId","description":"ID of pet to return","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Pet"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"api_key":[]}]},"post":{"summary":"Updates a pet in the store with form data","tags":["pet"],"operationId":"updatePetWithForm","parameters":[{"in":"path","name":"petId","description":"ID of pet that needs to be updated","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"name","description":"Updated name of the pet","type":"string"},{"in":"formData","name":"status","description":"Updated status of the pet","type":"string"}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/x-www-form-urlencoded"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}/uploadImage":{"post":{"summary":"uploads an image","tags":["pet"],"operationId":"uploadFile","parameters":[{"in":"path","name":"petId","description":"ID of pet to update","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"additionalMetadata","description":"Additional data to pass to server","type":"string"},{"in":"formData","name":"file","description":"file to upload","type":"file"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ApiResponse"}}},"consumes":["multipart/form-data"],"produces":["application/json"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/store/inventory":{"get":{"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities","tags":["store"],"operationId":"getInventory","responses":{"200":{"description":"successful operation","schema":{"additionalProperties":{"format":"int32","type":"integer"},"type":"object"}}},"produces":["application/json"],"security":[{"api_key":[]}]}},"/store/order":{"post":{"summary":"Place an order for a pet","tags":["store"],"operationId":"placeOrder","parameters":[{"in":"body","name":"body","description":"order placed for purchasing the pet","required":true,"schema":{"$ref":"#/definitions/Order"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid Order"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/store/order/{orderId}":{"delete":{"summary":"Delete purchase order by ID","description":"For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors","tags":["store"],"operationId":"deleteOrder","parameters":[{"in":"path","name":"orderId","description":"ID of the order that needs to be deleted","required":true,"type":"integer","format":"int64","minimum":1}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Find purchase order by ID","description":"For valid response try integer IDs with value \u003e= 1 and \u003c= 10. Other values will generated exceptions","tags":["store"],"operationId":"getOrderById","parameters":[{"in":"path","name":"orderId","description":"ID of pet that needs to be fetched","required":true,"type":"integer","format":"int64","minimum":1,"maximum":10}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]}},"/user":{"post":{"summary":"Create user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"createUser","parameters":[{"in":"body","name":"body","description":"Created user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithArray":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithArrayInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithList":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithListInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/login":{"get":{"summary":"Logs user into the system","tags":["user"],"operationId":"loginUser","parameters":[{"in":"query","name":"username","description":"The user name for login","required":true,"type":"string"},{"in":"query","name":"password","description":"The password for login in clear text","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"string"},"headers":{"X-Expires-After":{"description":"date in UTC when token expires","type":"string"},"X-Rate-Limit":{"description":"calls per hour allowed by the user","type":"integer"}}},"400":{"description":"Invalid username/password supplied"}},"produces":["application/json","application/xml"]}},"/user/logout":{"get":{"summary":"Logs out current logged in user session","tags":["user"],"operationId":"logoutUser","responses":{"default":{"description":"successful operation"}},"produces":["application/json","application/xml"]}},"/user/{username}":{"delete":{"summary":"Delete user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"deleteUser","parameters":[{"in":"path","name":"username","description":"The name that needs to be deleted","required":true,"type":"string"}],"responses":{"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Get user by user name","tags":["user"],"operationId":"getUserByName","parameters":[{"in":"path","name":"username","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"put":{"summary":"Updated user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"updateUser","parameters":[{"in":"path","name":"username","description":"name that need to be updated","required":true,"type":"string"},{"in":"body","name":"body","description":"Updated user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"400":{"description":"Invalid user supplied"},"404":{"description":"User not found"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}}},"definitions":{"ApiResponse":{"properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"},"type":{"type":"string"}},"type":"object"},"Category":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Category"}},"Order":{"properties":{"complete":{"type":"boolean"},"id":{"format":"int64","type":"integer"},"petId":{"format":"int64","type":"integer"},"quantity":{"format":"int32","type":"integer"},"shipDate":{"format":"date-time","type":"string"},"status":{"description":"Order Status","enum":["placed","approved","delivered"],"type":"string"}},"type":"object","xml":{"name":"Order"}},"Pet":{"properties":{"category":{"$ref":"#/definitions/Category"},"id":{"format":"int64","type":"integer"},"name":{"example":"doggie","type":"string"},"photoUrls":{"items":{"type":"string","xml":{"name":"photoUrl"}},"type":"array","xml":{"wrapped":true}},"status":{"description":"pet status in the store","enum":["available","pending","sold"],"type":"string"},"tags":{"items":{"$ref":"#/definitions/Tag"},"type":"array","xml":{"wrapped":true}}},"required":["name","photoUrls"],"type":"object","xml":{"name":"Pet"}},"Tag":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Tag"}},"User":{"properties":{"email":{"type":"string"},"firstName":{"type":"string"},"id":{"format":"int64","type":"integer"},"lastName":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"description":"User Status","format":"int32","type":"integer"},"username":{"type":"string"}},"type":"object","xml":{"name":"User"}}},"securityDefinitions":{"api_key":{"type":"apiKey","in":"header","name":"api_key"},"petstore_auth":{"type":"oauth2","flow":"implicit","authorizationUrl":"https://petstore.swagger.io/oauth/authorize","scopes":{"read:pets":"read your pets","write:pets":"modify pets in your account"}}},"tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"store","description":"Access to Petstore orders"},{"name":"user","description":"Operations about user","externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}]}
\ No newline at end of file
diff --git a/openapi2conv/doc.go b/openapi2conv/doc.go
deleted file mode 100644
index 7b87ec224..000000000
--- a/openapi2conv/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package openapi2conv converts an OpenAPI v2 specification document to v3.
-package openapi2conv
diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go
deleted file mode 100644
index 93914d9f9..000000000
--- a/openapi2conv/issue187_test.go
+++ /dev/null
@@ -1,194 +0,0 @@
-package openapi2conv
-
-import (
- "context"
- "encoding/json"
- "testing"
-
- "github.com/invopop/yaml"
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi2"
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func v2v3JSON(spec2 []byte) (doc3 *openapi3.T, err error) {
- var doc2 openapi2.T
- if err = json.Unmarshal(spec2, &doc2); err != nil {
- return
- }
- doc3, err = ToV3(&doc2)
- return
-}
-
-func v2v3YAML(spec2 []byte) (doc3 *openapi3.T, err error) {
- var doc2 openapi2.T
- if err = yaml.Unmarshal(spec2, &doc2); err != nil {
- return
- }
- doc3, err = ToV3(&doc2)
- return
-}
-
-func TestIssue187(t *testing.T) {
- spec := `
-{
- "swagger": "2.0",
- "info": {
- "description": "Test Golang Application",
- "version": "1.0",
- "title": "Test",
- "contact": {
- "name": "Test",
- "email": "test@test.com"
- }
- },
-
- "paths": {
- "/me": {
- "get": {
- "description": "",
- "operationId": "someTest",
- "summary": "Some test",
- "tags": ["probe"],
- "produces": ["application/json"],
- "responses": {
- "200": {
- "description": "successful operation",
- "schema": {"$ref": "#/definitions/model.ProductSearchAttributeRequest"}
- }
- }
- }
- }
- },
-
- "host": "",
- "basePath": "/test",
- "definitions": {
- "model.ProductSearchAttributeRequest": {
- "type": "object",
- "properties": {
- "filterField": {
- "type": "string"
- },
- "filterKey": {
- "type": "string"
- },
- "type": {
- "type": "string"
- },
- "values": {
- "$ref": "#/definitions/model.ProductSearchAttributeValueRequest"
- }
- },
- "title": "model.ProductSearchAttributeRequest"
- },
- "model.ProductSearchAttributeValueRequest": {
- "type": "object",
- "properties": {
- "imageUrl": {
- "type": "string"
- },
- "text": {
- "type": "string"
- }
- },
- "title": "model.ProductSearchAttributeValueRequest"
- }
- }
-}
-`
- doc3, err := v2v3JSON([]byte(spec))
- require.NoError(t, err)
-
- spec3, err := json.Marshal(doc3)
- require.NoError(t, err)
- const expected = `{"components":{"schemas":{"model.ProductSearchAttributeRequest":{"properties":{"filterField":{"type":"string"},"filterKey":{"type":"string"},"type":{"type":"string"},"values":{"$ref":"#/components/schemas/model.ProductSearchAttributeValueRequest"}},"title":"model.ProductSearchAttributeRequest","type":"object"},"model.ProductSearchAttributeValueRequest":{"properties":{"imageUrl":{"type":"string"},"text":{"type":"string"}},"title":"model.ProductSearchAttributeValueRequest","type":"object"}}},"info":{"contact":{"email":"test@test.com","name":"Test"},"description":"Test Golang Application","title":"Test","version":"1.0"},"openapi":"3.0.3","paths":{"/me":{"get":{"operationId":"someTest","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/model.ProductSearchAttributeRequest"}}},"description":"successful operation"}},"summary":"Some test","tags":["probe"]}}}}`
- require.JSONEq(t, string(spec3), expected)
-
- err = doc3.Validate(context.Background())
- require.NoError(t, err)
-}
-
-func TestIssue237(t *testing.T) {
- spec := `
-swagger: '2.0'
-info:
- version: 1.0.0
- title: title
-paths:
- /test:
- get:
- parameters:
- - in: body
- schema:
- $ref: '#/definitions/TestRef'
- responses:
- '200':
- description: description
-definitions:
- TestRef:
- type: object
- allOf:
- - $ref: '#/definitions/TestRef2'
- TestRef2:
- type: object
-`
- doc3, err := v2v3YAML([]byte(spec))
- require.NoError(t, err)
-
- spec3, err := yaml.Marshal(doc3)
- require.NoError(t, err)
- const expected = `components:
- schemas:
- TestRef:
- allOf:
- - $ref: '#/components/schemas/TestRef2'
- type: object
- TestRef2:
- type: object
-info:
- title: title
- version: 1.0.0
-openapi: 3.0.3
-paths:
- /test:
- get:
- requestBody:
- content:
- '*/*':
- schema:
- $ref: '#/components/schemas/TestRef'
- responses:
- "200":
- description: description
-`
- require.YAMLEq(t, string(spec3), expected)
-
- err = doc3.Validate(context.Background())
- require.NoError(t, err)
-}
-
-func TestPR449(t *testing.T) {
- spec := `
-swagger: '2.0'
-info:
- version: 1.0.0
- title: title
-
-securityDefinitions:
- OAuth2Application:
- type: "oauth2"
- flow: "application"
- tokenUrl: "example.com/oauth2/token"
-`
- doc3, err := v2v3YAML([]byte(spec))
- require.NoError(t, err)
- require.NotNil(t, doc3.Components.SecuritySchemes["OAuth2Application"].Value.Flows.ClientCredentials)
- _, err = yaml.Marshal(doc3)
- require.NoError(t, err)
-
- doc2, err := FromV3(doc3)
- require.NoError(t, err)
- require.Equal(t, doc2.SecurityDefinitions["OAuth2Application"].Flow, "application")
-}
diff --git a/openapi2conv/issue440_test.go b/openapi2conv/issue440_test.go
deleted file mode 100644
index 2478384ff..000000000
--- a/openapi2conv/issue440_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package openapi2conv
-
-import (
- "context"
- "encoding/json"
- "os"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi2"
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestIssue440(t *testing.T) {
- doc2file, err := os.Open("testdata/swagger.json")
- require.NoError(t, err)
- defer doc2file.Close()
- var doc2 openapi2.T
- err = json.NewDecoder(doc2file).Decode(&doc2)
- require.NoError(t, err)
-
- doc3, err := ToV3(&doc2)
- require.NoError(t, err)
- err = doc3.Validate(context.Background())
- require.NoError(t, err)
- require.Equal(t, openapi3.Servers{
- {URL: "https://petstore.swagger.io/v2"},
- {URL: "http://petstore.swagger.io/v2"},
- }, doc3.Servers)
-
- doc2.Host = "your-bot-domain.de"
- doc2.Schemes = nil
- doc2.BasePath = ""
- doc3, err = ToV3(&doc2)
- require.NoError(t, err)
- err = doc3.Validate(context.Background())
- require.NoError(t, err)
- require.Equal(t, openapi3.Servers{
- {URL: "https://your-bot-domain.de/"},
- }, doc3.Servers)
-
- doc2.Host = "https://your-bot-domain.de"
- doc2.Schemes = nil
- doc2.BasePath = ""
- doc3, err = ToV3(&doc2)
- require.Error(t, err)
- require.Contains(t, err.Error(), `invalid host`)
-}
diff --git a/openapi2conv/issue558_test.go b/openapi2conv/issue558_test.go
deleted file mode 100644
index 78661bf78..000000000
--- a/openapi2conv/issue558_test.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package openapi2conv
-
-import (
- "testing"
-
- "github.com/invopop/yaml"
- "github.com/stretchr/testify/require"
-)
-
-func TestPR558(t *testing.T) {
- spec := `
-swagger: '2.0'
-info:
- version: 1.0.0
- title: title
-paths:
- /test:
- get:
- deprecated: true
- parameters:
- - in: body
- schema:
- type: object
- responses:
- '200':
- description: description
-`
- doc3, err := v2v3YAML([]byte(spec))
- require.NoError(t, err)
- require.NotEmpty(t, doc3.Paths["/test"].Get.Deprecated)
- _, err = yaml.Marshal(doc3)
- require.NoError(t, err)
-
- doc2, err := FromV3(doc3)
- require.NoError(t, err)
- require.NotEmpty(t, doc2.Paths["/test"].Get.Deprecated)
-}
diff --git a/openapi2conv/issue573_test.go b/openapi2conv/issue573_test.go
deleted file mode 100644
index cefac409e..000000000
--- a/openapi2conv/issue573_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package openapi2conv
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue573(t *testing.T) {
- spec := []byte(`paths:
- /ping:
- get:
- produces:
- - application/toml
- - application/xml
- responses:
- 200:
- schema:
- type: object
- properties:
- username:
- type: string
- description: The user name.
- post:
- responses:
- 200:
- schema:
- type: object
- properties:
- username:
- type: string
- description: The user name.`)
-
- v3, err := v2v3YAML(spec)
- require.NoError(t, err)
-
- // Make sure the response content appears for each mime-type originally
- // appeared in "produces".
- pingGetContent := v3.Paths["/ping"].Get.Responses["200"].Value.Content
- require.Len(t, pingGetContent, 2)
- require.Contains(t, pingGetContent, "application/toml")
- require.Contains(t, pingGetContent, "application/xml")
-
- // Is "produces" is not explicitly specified, default to "application/json".
- pingPostContent := v3.Paths["/ping"].Post.Responses["200"].Value.Content
- require.Len(t, pingPostContent, 1)
- require.Contains(t, pingPostContent, "application/json")
-}
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index c80e67201..6e0b10e7e 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -1,451 +1,228 @@
+// Package openapi2conv converts an OpenAPI v2 specification to v3.
package openapi2conv
import (
"errors"
"fmt"
"net/url"
- "sort"
"strings"
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi3"
)
-// ToV3 converts an OpenAPIv2 spec to an OpenAPIv3 spec
-func ToV3(doc2 *openapi2.T) (*openapi3.T, error) {
- doc3 := &openapi3.T{
- OpenAPI: "3.0.3",
- Info: &doc2.Info,
- Components: &openapi3.Components{},
- Tags: doc2.Tags,
- Extensions: stripNonExtensions(doc2.Extensions),
- ExternalDocs: doc2.ExternalDocs,
- }
-
- if host := doc2.Host; host != "" {
- if strings.Contains(host, "/") {
- err := fmt.Errorf("invalid host %q. This MUST be the host only and does not include the scheme nor sub-paths.", host)
- return nil, err
- }
- schemes := doc2.Schemes
+// ToV3Swagger converts an OpenAPIv2 spec to an OpenAPIv3 spec
+func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
+ result := &openapi3.Swagger{
+ OpenAPI: "3.0.2",
+ Info: &swagger.Info,
+ Components: openapi3.Components{},
+ Tags: swagger.Tags,
+ }
+ host := swagger.Host
+ if len(host) > 0 {
+ schemes := swagger.Schemes
if len(schemes) == 0 {
- schemes = []string{"https"}
- }
- basePath := doc2.BasePath
- if basePath == "" {
- basePath = "/"
+ schemes = []string{
+ "https://",
+ }
}
+ basePath := swagger.BasePath
for _, scheme := range schemes {
u := url.URL{
Scheme: scheme,
Host: host,
Path: basePath,
}
- doc3.AddServer(&openapi3.Server{URL: u.String()})
+ result.AddServer(&openapi3.Server{
+ URL: u.String(),
+ })
}
}
-
- doc3.Components.Schemas = make(map[string]*openapi3.SchemaRef)
- if parameters := doc2.Parameters; len(parameters) != 0 {
- doc3.Components.Parameters = make(map[string]*openapi3.ParameterRef)
- doc3.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef)
- for k, parameter := range parameters {
- v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(doc3.Components, parameter, doc2.Consumes)
- switch {
- case err != nil:
+ if paths := swagger.Paths; paths != nil {
+ resultPaths := make(map[string]*openapi3.PathItem, len(paths))
+ for path, pathItem := range paths {
+ r, err := ToV3PathItem(swagger, pathItem)
+ if err != nil {
return nil, err
- case v3RequestBody != nil:
- doc3.Components.RequestBodies[k] = v3RequestBody
- case v3SchemaMap != nil:
- for _, v3Schema := range v3SchemaMap {
- doc3.Components.Schemas[k] = v3Schema
- }
- default:
- doc3.Components.Parameters[k] = v3Parameter
}
+ resultPaths[path] = r
}
+ result.Paths = resultPaths
}
-
- if paths := doc2.Paths; len(paths) != 0 {
- doc3Paths := make(map[string]*openapi3.PathItem, len(paths))
- for path, pathItem := range paths {
- r, err := ToV3PathItem(doc2, doc3.Components, pathItem, doc2.Consumes)
+ if parameters := swagger.Parameters; parameters != nil {
+ result.Components.Parameters = make(map[string]*openapi3.ParameterRef)
+ result.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef)
+ for k, parameter := range parameters {
+ resultParameter, resultRequestBody, err := ToV3Parameter(parameter)
if err != nil {
return nil, err
}
- doc3Paths[path] = r
+ if resultParameter != nil {
+ result.Components.Parameters[k] = resultParameter
+ }
+ if resultRequestBody != nil {
+ result.Components.RequestBodies[k] = resultRequestBody
+ }
}
- doc3.Paths = doc3Paths
}
-
- if responses := doc2.Responses; len(responses) != 0 {
- doc3.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses))
+ if responses := swagger.Responses; responses != nil {
+ result.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses))
for k, response := range responses {
- r, err := ToV3Response(response, doc2.Produces)
+ r, err := ToV3Response(response)
if err != nil {
return nil, err
}
- doc3.Components.Responses[k] = r
+ result.Components.Responses[k] = r
}
}
-
- for key, schema := range ToV3Schemas(doc2.Definitions) {
- doc3.Components.Schemas[key] = schema
- }
-
- if m := doc2.SecurityDefinitions; len(m) != 0 {
- doc3SecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef)
+ result.Components.Schemas = ToV3Schemas(swagger.Definitions)
+ if m := swagger.SecurityDefinitions; m != nil {
+ resultSecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef)
for k, v := range m {
r, err := ToV3SecurityScheme(v)
if err != nil {
return nil, err
}
- doc3SecuritySchemes[k] = r
+ resultSecuritySchemes[k] = r
}
- doc3.Components.SecuritySchemes = doc3SecuritySchemes
+ result.Components.SecuritySchemes = resultSecuritySchemes
}
-
- doc3.Security = ToV3SecurityRequirements(doc2.Security)
- {
- sl := openapi3.NewLoader()
- if err := sl.ResolveRefsIn(doc3, nil); err != nil {
- return nil, err
- }
- }
- return doc3, nil
+ result.Security = ToV3SecurityRequirements(swagger.Security)
+ return result, nil
}
-func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) {
- doc3 := &openapi3.PathItem{
- Extensions: stripNonExtensions(pathItem.Extensions),
- }
+func ToV3PathItem(swagger *openapi2.Swagger, pathItem *openapi2.PathItem) (*openapi3.PathItem, error) {
+ result := &openapi3.PathItem{}
for method, operation := range pathItem.Operations() {
- doc3Operation, err := ToV3Operation(doc2, components, pathItem, operation, consumes)
+ resultOperation, err := ToV3Operation(swagger, pathItem, operation)
if err != nil {
return nil, err
}
- doc3.SetOperation(method, doc3Operation)
+ result.SetOperation(method, resultOperation)
}
for _, parameter := range pathItem.Parameters {
- v3Parameter, v3RequestBody, v3Schema, err := ToV3Parameter(components, parameter, consumes)
- switch {
- case err != nil:
+ v3Parameter, v3RequestBody, err := ToV3Parameter(parameter)
+ if err != nil {
return nil, err
- case v3RequestBody != nil:
- return nil, errors.New("pathItem must not have a body parameter")
- case v3Schema != nil:
- return nil, errors.New("pathItem must not have a schema parameter")
- default:
- doc3.Parameters = append(doc3.Parameters, v3Parameter)
}
+ if v3RequestBody != nil {
+ return nil, errors.New("PathItem shouldn't have a body parameter")
+ }
+ result.Parameters = append(result.Parameters, v3Parameter)
}
- return doc3, nil
+ return result, nil
}
-func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, operation *openapi2.Operation, consumes []string) (*openapi3.Operation, error) {
+func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, operation *openapi2.Operation) (*openapi3.Operation, error) {
if operation == nil {
return nil, nil
}
- doc3 := &openapi3.Operation{
+ result := &openapi3.Operation{
OperationID: operation.OperationID,
Summary: operation.Summary,
Description: operation.Description,
- Deprecated: operation.Deprecated,
Tags: operation.Tags,
- Extensions: stripNonExtensions(operation.Extensions),
}
if v := operation.Security; v != nil {
- doc3Security := ToV3SecurityRequirements(*v)
- doc3.Security = &doc3Security
- }
-
- if len(operation.Consumes) > 0 {
- consumes = operation.Consumes
+ resultSecurity := ToV3SecurityRequirements(*v)
+ result.Security = &resultSecurity
}
-
- var reqBodies []*openapi3.RequestBodyRef
- formDataSchemas := make(map[string]*openapi3.SchemaRef)
for _, parameter := range operation.Parameters {
- v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(components, parameter, consumes)
- switch {
- case err != nil:
+ v3Parameter, v3RequestBody, err := ToV3Parameter(parameter)
+ if err != nil {
return nil, err
- case v3RequestBody != nil:
- reqBodies = append(reqBodies, v3RequestBody)
- case v3SchemaMap != nil:
- for key, v3Schema := range v3SchemaMap {
- formDataSchemas[key] = v3Schema
- }
- default:
- doc3.Parameters = append(doc3.Parameters, v3Parameter)
+ }
+ if v3RequestBody != nil {
+ result.RequestBody = v3RequestBody
+ } else if v3Parameter != nil {
+ result.Parameters = append(result.Parameters, v3Parameter)
}
}
- var err error
- if doc3.RequestBody, err = onlyOneReqBodyParam(reqBodies, formDataSchemas, components, consumes); err != nil {
- return nil, err
- }
-
if responses := operation.Responses; responses != nil {
- doc3Responses := make(openapi3.Responses, len(responses))
+ resultResponses := make(openapi3.Responses, len(responses))
for k, response := range responses {
- doc3, err := ToV3Response(response, operation.Produces)
+ result, err := ToV3Response(response)
if err != nil {
return nil, err
}
- doc3Responses[k] = doc3
+ resultResponses[k] = result
}
- doc3.Responses = doc3Responses
+ result.Responses = resultResponses
}
- return doc3, nil
-}
-
-func getParameterNameFromOldRef(ref string) string {
- cleanPath := strings.TrimPrefix(ref, "#/parameters/")
- pathSections := strings.SplitN(cleanPath, "/", 1)
-
- return pathSections[0]
+ return result, nil
}
-func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Parameter, consumes []string) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, map[string]*openapi3.SchemaRef, error) {
- if ref := parameter.Ref; ref != "" {
- if strings.HasPrefix(ref, "#/parameters/") {
- name := getParameterNameFromOldRef(ref)
- if _, ok := components.RequestBodies[name]; ok {
- v3Ref := strings.Replace(ref, "#/parameters/", "#/components/requestBodies/", 1)
- return nil, &openapi3.RequestBodyRef{Ref: v3Ref}, nil, nil
- } else if schema, ok := components.Schemas[name]; ok {
- schemaRefMap := make(map[string]*openapi3.SchemaRef)
- if val, ok := schema.Value.Extensions["x-formData-name"]; ok {
- name = val.(string)
- }
- v3Ref := strings.Replace(ref, "#/parameters/", "#/components/schemas/", 1)
- schemaRefMap[name] = &openapi3.SchemaRef{Ref: v3Ref}
- return nil, nil, schemaRefMap, nil
- }
- }
- return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil, nil
+func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, error) {
+ if parameter == nil {
+ return nil, nil, nil
}
-
- switch parameter.In {
- case "body":
+ if ref := parameter.Ref; len(ref) > 0 {
+ return &openapi3.ParameterRef{
+ Ref: ToV3Ref(ref),
+ }, nil, nil
+ }
+ in := parameter.In
+ if in == "body" {
result := &openapi3.RequestBody{
Description: parameter.Description,
Required: parameter.Required,
- Extensions: stripNonExtensions(parameter.Extensions),
}
- if parameter.Name != "" {
- if result.Extensions == nil {
- result.Extensions = make(map[string]interface{}, 1)
- }
- result.Extensions["x-originalParamName"] = parameter.Name
- }
-
if schemaRef := parameter.Schema; schemaRef != nil {
- result.WithSchemaRef(ToV3SchemaRef(schemaRef), consumes)
- }
- return nil, &openapi3.RequestBodyRef{Value: result}, nil, nil
-
- case "formData":
- format, typ := parameter.Format, parameter.Type
- if typ == "file" {
- format, typ = "binary", "string"
+ // Assume it's JSON
+ result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef))
}
- if parameter.Extensions == nil {
- parameter.Extensions = make(map[string]interface{}, 1)
- }
- parameter.Extensions["x-formData-name"] = parameter.Name
- var required []string
- if parameter.Required {
- required = []string{parameter.Name}
- }
- schemaRef := &openapi3.SchemaRef{Value: &openapi3.Schema{
- Description: parameter.Description,
- Type: typ,
- Extensions: stripNonExtensions(parameter.Extensions),
- Format: format,
- Enum: parameter.Enum,
- Min: parameter.Minimum,
- Max: parameter.Maximum,
- ExclusiveMin: parameter.ExclusiveMin,
- ExclusiveMax: parameter.ExclusiveMax,
- MinLength: parameter.MinLength,
- MaxLength: parameter.MaxLength,
- Default: parameter.Default,
- Items: parameter.Items,
- MinItems: parameter.MinItems,
- MaxItems: parameter.MaxItems,
- Pattern: parameter.Pattern,
- AllowEmptyValue: parameter.AllowEmptyValue,
- UniqueItems: parameter.UniqueItems,
- MultipleOf: parameter.MultipleOf,
- Required: required,
- }}
- schemaRefMap := make(map[string]*openapi3.SchemaRef, 1)
- schemaRefMap[parameter.Name] = schemaRef
- return nil, nil, schemaRefMap, nil
-
- default:
- required := parameter.Required
- if parameter.In == openapi3.ParameterInPath {
- required = true
- }
-
- var schemaRefRef string
- if schemaRef := parameter.Schema; schemaRef != nil && schemaRef.Ref != "" {
- schemaRefRef = schemaRef.Ref
- }
- result := &openapi3.Parameter{
- In: parameter.In,
- Name: parameter.Name,
- Description: parameter.Description,
- Required: required,
- Extensions: stripNonExtensions(parameter.Extensions),
- Schema: ToV3SchemaRef(&openapi3.SchemaRef{Value: &openapi3.Schema{
- Type: parameter.Type,
- Format: parameter.Format,
- Enum: parameter.Enum,
- Min: parameter.Minimum,
- Max: parameter.Maximum,
- ExclusiveMin: parameter.ExclusiveMin,
- ExclusiveMax: parameter.ExclusiveMax,
- MinLength: parameter.MinLength,
- MaxLength: parameter.MaxLength,
- Default: parameter.Default,
- Items: parameter.Items,
- MinItems: parameter.MinItems,
- MaxItems: parameter.MaxItems,
- Pattern: parameter.Pattern,
- AllowEmptyValue: parameter.AllowEmptyValue,
- UniqueItems: parameter.UniqueItems,
- MultipleOf: parameter.MultipleOf,
- },
- Ref: schemaRefRef,
- }),
- }
- return &openapi3.ParameterRef{Value: result}, nil, nil, nil
- }
-}
-
-func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool, consumes []string) *openapi3.RequestBodyRef {
- if len(bodies) != len(reqs) {
- panic(`request bodies and them being required must match`)
- }
- requireds := make([]string, 0, len(reqs))
- for propName, req := range reqs {
- if _, ok := bodies[propName]; !ok {
- panic(`request bodies and them being required must match`)
- }
- if req {
- requireds = append(requireds, propName)
- }
- }
- schema := &openapi3.Schema{
- Type: "object",
- Properties: ToV3Schemas(bodies),
- Required: requireds,
- }
- return &openapi3.RequestBodyRef{
- Value: openapi3.NewRequestBody().WithSchema(schema, consumes),
- }
-}
-
-func getParameterNameFromNewRef(ref string) string {
- cleanPath := strings.TrimPrefix(ref, "#/components/schemas/")
- pathSections := strings.SplitN(cleanPath, "/", 1)
-
- return pathSections[0]
-}
-
-func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[string]*openapi3.SchemaRef, components *openapi3.Components, consumes []string) (*openapi3.RequestBodyRef, error) {
- if len(bodies) > 1 {
- return nil, errors.New("multiple body parameters cannot exist for the same operation")
- }
-
- if len(bodies) != 0 && len(formDataSchemas) != 0 {
- return nil, errors.New("body and form parameters cannot exist together for the same operation")
+ return nil, &openapi3.RequestBodyRef{
+ Value: result,
+ }, nil
}
-
- for _, requestBodyRef := range bodies {
- return requestBodyRef, nil
+ result := &openapi3.Parameter{
+ In: in,
+ Name: parameter.Name,
+ Description: parameter.Description,
+ Required: parameter.Required,
}
- if len(formDataSchemas) > 0 {
- formDataParams := make(map[string]*openapi3.SchemaRef, len(formDataSchemas))
- formDataReqs := make(map[string]bool, len(formDataSchemas))
- for formDataName, formDataSchema := range formDataSchemas {
- if formDataSchema.Ref != "" {
- name := getParameterNameFromNewRef(formDataSchema.Ref)
- if schema := components.Schemas[name]; schema != nil && schema.Value != nil {
- if tempName, ok := schema.Value.Extensions["x-formData-name"]; ok {
- name = tempName.(string)
- }
- formDataParams[name] = formDataSchema
- formDataReqs[name] = false
- for _, req := range schema.Value.Required {
- if name == req {
- formDataReqs[name] = true
- }
- }
- }
- } else if formDataSchema.Value != nil {
- formDataParams[formDataName] = formDataSchema
- formDataReqs[formDataName] = false
- for _, req := range formDataSchema.Value.Required {
- if formDataName == req {
- formDataReqs[formDataName] = true
- }
- }
- }
+ if parameter.Type != "" {
+ schema := &openapi3.SchemaRef{
+ Value: &openapi3.Schema{
+ Type: parameter.Type,
+ Format: parameter.Format,
+ Enum: parameter.Enum,
+ Min: parameter.Minimum,
+ Max: parameter.Maximum,
+ ExclusiveMin: parameter.ExclusiveMin,
+ ExclusiveMax: parameter.ExclusiveMax,
+ MinLength: parameter.MinLength,
+ MaxLength: parameter.MaxLength,
+ Default: parameter.Default,
+ Items: parameter.Items,
+ MinItems: parameter.MinItems,
+ MaxItems: parameter.MaxItems,
+ },
}
-
- return formDataBody(formDataParams, formDataReqs, consumes), nil
+ result.Schema = ToV3SchemaRef(schema)
}
-
- return nil, nil
+ return &openapi3.ParameterRef{
+ Value: result,
+ }, nil, nil
}
-func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.ResponseRef, error) {
- if ref := response.Ref; ref != "" {
- return &openapi3.ResponseRef{Ref: ToV3Ref(ref)}, nil
+func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) {
+ if ref := response.Ref; len(ref) > 0 {
+ return &openapi3.ResponseRef{
+ Ref: ToV3Ref(ref),
+ }, nil
}
result := &openapi3.Response{
Description: &response.Description,
- Extensions: stripNonExtensions(response.Extensions),
}
-
- // Default to "application/json" if "produces" is not specified.
- if len(produces) == 0 {
- produces = []string{"application/json"}
- }
-
if schemaRef := response.Schema; schemaRef != nil {
- schema := ToV3SchemaRef(schemaRef)
- result.Content = make(openapi3.Content, len(produces))
- for _, mime := range produces {
- result.Content[mime] = openapi3.NewMediaType().WithSchemaRef(schema)
- }
- }
- if headers := response.Headers; len(headers) > 0 {
- result.Headers = ToV3Headers(headers)
- }
- return &openapi3.ResponseRef{Value: result}, nil
-}
-
-func ToV3Headers(defs map[string]*openapi2.Header) openapi3.Headers {
- headers := make(openapi3.Headers, len(defs))
- for name, header := range defs {
- header.In = ""
- header.Name = ""
- if ref := header.Ref; ref != "" {
- headers[name] = &openapi3.HeaderRef{Ref: ToV3Ref(ref)}
- } else {
- parameter, _, _, _ := ToV3Parameter(nil, &header.Parameter, nil)
- headers[name] = &openapi3.HeaderRef{Value: &openapi3.Header{
- Parameter: *parameter.Value,
- }}
- }
+ result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef))
}
- return headers
+ return &openapi3.ResponseRef{
+ Value: result,
+ }, nil
}
func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef {
@@ -457,8 +234,10 @@ func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.Schem
}
func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef {
- if ref := schema.Ref; ref != "" {
- return &openapi3.SchemaRef{Ref: ToV3Ref(ref)}
+ if ref := schema.Ref; len(ref) > 0 {
+ return &openapi3.SchemaRef{
+ Ref: ToV3Ref(ref),
+ }
}
if schema.Value == nil {
return schema
@@ -469,17 +248,9 @@ func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef {
for k, v := range schema.Value.Properties {
schema.Value.Properties[k] = ToV3SchemaRef(v)
}
- if v := schema.Value.AdditionalProperties.Schema; v != nil {
- schema.Value.AdditionalProperties.Schema = ToV3SchemaRef(v)
+ if schema.Value.AdditionalProperties != nil {
+ schema.Value.AdditionalProperties = ToV3SchemaRef(schema.Value.AdditionalProperties)
}
- for i, v := range schema.Value.AllOf {
- schema.Value.AllOf[i] = ToV3SchemaRef(v)
- }
- if val, ok := schema.Value.Extensions["x-nullable"]; ok {
- schema.Value.Nullable, _ = val.(bool)
- delete(schema.Value.Extensions, "x-nullable")
- }
-
return schema
}
@@ -502,8 +273,6 @@ func FromV3Ref(ref string) string {
for new, old := range ref2To3 {
if strings.HasPrefix(ref, old) {
ref = strings.Replace(ref, old, new, 1)
- } else if strings.HasPrefix(ref, "#/components/requestBodies/") {
- ref = strings.Replace(ref, "#/components/requestBodies/", "#/parameters/", 1)
}
}
return ref
@@ -526,7 +295,6 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu
}
result := &openapi3.SecurityScheme{
Description: securityScheme.Description,
- Extensions: stripNonExtensions(securityScheme.Extensions),
}
switch securityScheme.Type {
case "basic":
@@ -552,14 +320,12 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu
switch securityScheme.Flow {
case "implicit":
flows.Implicit = flow
- case "accessCode":
+ case "accesscode":
flows.AuthorizationCode = flow
case "password":
flows.Password = flow
- case "application":
- flows.ClientCredentials = flow
default:
- return nil, fmt.Errorf("unsupported flow %q", securityScheme.Flow)
+ return nil, fmt.Errorf("Unsupported flow '%s'", securityScheme.Flow)
}
}
return &openapi3.SecuritySchemeRef{
@@ -568,27 +334,20 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu
}, nil
}
-// FromV3 converts an OpenAPIv3 spec to an OpenAPIv2 spec
-func FromV3(doc3 *openapi3.T) (*openapi2.T, error) {
- doc2Responses, err := FromV3Responses(doc3.Components.Responses, doc3.Components)
+func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
+ resultResponses, err := FromV3Responses(swagger.Components.Responses)
if err != nil {
return nil, err
}
- schemas, parameters := FromV3Schemas(doc3.Components.Schemas, doc3.Components)
- doc2 := &openapi2.T{
- Swagger: "2.0",
- Info: *doc3.Info,
- Definitions: schemas,
- Parameters: parameters,
- Responses: doc2Responses,
- Tags: doc3.Tags,
- Extensions: stripNonExtensions(doc3.Extensions),
- ExternalDocs: doc3.ExternalDocs,
+ result := &openapi2.Swagger{
+ Info: *swagger.Info,
+ Definitions: FromV3Schemas(swagger.Components.Schemas),
+ Responses: resultResponses,
+ Tags: swagger.Tags,
}
-
isHTTPS := false
isHTTP := false
- servers := doc3.Servers
+ servers := swagger.Servers
for i, server := range servers {
parsedURL, err := url.Parse(server.URL)
if err == nil {
@@ -600,254 +359,114 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) {
}
// The first server is assumed to provide the base path
if i == 0 {
- doc2.Host = parsedURL.Host
- doc2.BasePath = parsedURL.Path
+ result.Host = parsedURL.Host
+ result.BasePath = parsedURL.Path
}
}
}
if isHTTPS {
- doc2.Schemes = append(doc2.Schemes, "https")
+ result.Schemes = append(result.Schemes, "https")
}
if isHTTP {
- doc2.Schemes = append(doc2.Schemes, "http")
+ result.Schemes = append(result.Schemes, "http")
}
- for path, pathItem := range doc3.Paths {
+ for path, pathItem := range swagger.Paths {
if pathItem == nil {
continue
}
- doc2.AddOperation(path, "GET", nil)
- addPathExtensions(doc2, path, stripNonExtensions(pathItem.Extensions))
+ result.AddOperation(path, "GET", nil)
for method, operation := range pathItem.Operations() {
if operation == nil {
continue
}
- doc2Operation, err := FromV3Operation(doc3, operation)
+ resultOperation, err := FromV3Operation(swagger, operation)
if err != nil {
return nil, err
}
- doc2.AddOperation(path, method, doc2Operation)
+ result.AddOperation(path, method, resultOperation)
}
params := openapi2.Parameters{}
for _, param := range pathItem.Parameters {
- p, err := FromV3Parameter(param, doc3.Components)
+ p, err := FromV3Parameter(param)
if err != nil {
return nil, err
}
params = append(params, p)
}
- sort.Sort(params)
- doc2.Paths[path].Parameters = params
+ result.Paths[path].Parameters = params
}
-
- for name, param := range doc3.Components.Parameters {
- if doc2.Parameters[name], err = FromV3Parameter(param, doc3.Components); err != nil {
+ result.Parameters = map[string]*openapi2.Parameter{}
+ for name, param := range swagger.Components.Parameters {
+ if result.Parameters[name], err = FromV3Parameter(param); err != nil {
return nil, err
}
}
-
- for name, requestBodyRef := range doc3.Components.RequestBodies {
- bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, doc3.Components)
- if err != nil {
- return nil, err
- }
- if len(formDataParameters) != 0 {
- for _, param := range formDataParameters {
- doc2.Parameters[param.Name] = param
- }
- } else if len(bodyOrRefParameters) != 0 {
- for _, param := range bodyOrRefParameters {
- doc2.Parameters[name] = param
- }
- }
-
- if len(consumes) != 0 {
- doc2.Consumes = consumesToArray(consumes)
- }
- }
-
- if m := doc3.Components.SecuritySchemes; m != nil {
- doc2SecuritySchemes := make(map[string]*openapi2.SecurityScheme)
+ if m := swagger.Components.SecuritySchemes; m != nil {
+ resultSecuritySchemes := make(map[string]*openapi2.SecurityScheme)
for id, securityScheme := range m {
- v, err := FromV3SecurityScheme(doc3, securityScheme)
+ v, err := FromV3SecurityScheme(swagger, securityScheme)
if err != nil {
return nil, err
}
- doc2SecuritySchemes[id] = v
- }
- doc2.SecurityDefinitions = doc2SecuritySchemes
- }
- doc2.Security = FromV3SecurityRequirements(doc3.Security)
-
- return doc2, nil
-}
-
-func consumesToArray(consumes map[string]struct{}) []string {
- consumesArr := make([]string, 0, len(consumes))
- for key := range consumes {
- consumesArr = append(consumesArr, key)
- }
- sort.Strings(consumesArr)
- return consumesArr
-}
-
-func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, components *openapi3.Components) (
- bodyOrRefParameters openapi2.Parameters,
- formParameters openapi2.Parameters,
- consumes map[string]struct{},
- err error,
-) {
- if ref := requestBodyRef.Ref; ref != "" {
- bodyOrRefParameters = append(bodyOrRefParameters, &openapi2.Parameter{Ref: FromV3Ref(ref)})
- return
- }
-
- //Only select one formData or request body for an individual requestBody as OpenAPI 2 does not support multiples
- if requestBodyRef.Value != nil {
- for contentType, mediaType := range requestBodyRef.Value.Content {
- if consumes == nil {
- consumes = make(map[string]struct{})
- }
- consumes[contentType] = struct{}{}
- if contentType == "application/x-www-form-urlencoded" || contentType == "multipart/form-data" {
- formParameters = FromV3RequestBodyFormData(mediaType)
- continue
- }
-
- paramName := name
- if originalName, ok := requestBodyRef.Value.Extensions["x-originalParamName"]; ok {
- paramName = originalName.(string)
- }
-
- var r *openapi2.Parameter
- if r, err = FromV3RequestBody(paramName, requestBodyRef, mediaType, components); err != nil {
- return
- }
-
- bodyOrRefParameters = append(bodyOrRefParameters, r)
+ resultSecuritySchemes[id] = v
}
+ result.SecurityDefinitions = resultSecuritySchemes
}
- return
+ result.Security = FromV3SecurityRequirements(swagger.Security)
+ return result, nil
}
-func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) {
- v2Defs := make(map[string]*openapi3.SchemaRef)
- v2Params := make(map[string]*openapi2.Parameter)
+func FromV3Schemas(schemas map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef {
+ v2Defs := make(map[string]*openapi3.SchemaRef, len(schemas))
for name, schema := range schemas {
- schemaConv, parameterConv := FromV3SchemaRef(schema, components)
- if schemaConv != nil {
- v2Defs[name] = schemaConv
- } else if parameterConv != nil {
- if parameterConv.Name == "" {
- parameterConv.Name = name
- }
- v2Params[name] = parameterConv
- }
+ v2Defs[name] = FromV3SchemaRef(schema)
}
- return v2Defs, v2Params
+ return v2Defs
}
-func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) {
- if ref := schema.Ref; ref != "" {
- name := getParameterNameFromNewRef(ref)
- if val, ok := components.Schemas[name]; ok {
- if val.Value.Format == "binary" {
- v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1)
- return nil, &openapi2.Parameter{Ref: v2Ref}
- }
+func FromV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef {
+ if ref := schema.Ref; len(ref) > 0 {
+ return &openapi3.SchemaRef{
+ Ref: FromV3Ref(ref),
}
-
- return &openapi3.SchemaRef{Ref: FromV3Ref(ref)}, nil
}
if schema.Value == nil {
- return schema, nil
- }
-
- if schema.Value != nil {
- if schema.Value.Type == "string" && schema.Value.Format == "binary" {
- paramType := "file"
- required := false
-
- value, _ := schema.Value.Extensions["x-formData-name"]
- originalName, _ := value.(string)
- for _, prop := range schema.Value.Required {
- if originalName == prop {
- required = true
- break
- }
- }
- return nil, &openapi2.Parameter{
- In: "formData",
- Name: originalName,
- Description: schema.Value.Description,
- Type: paramType,
- Enum: schema.Value.Enum,
- Minimum: schema.Value.Min,
- Maximum: schema.Value.Max,
- ExclusiveMin: schema.Value.ExclusiveMin,
- ExclusiveMax: schema.Value.ExclusiveMax,
- MinLength: schema.Value.MinLength,
- MaxLength: schema.Value.MaxLength,
- Default: schema.Value.Default,
- Items: schema.Value.Items,
- MinItems: schema.Value.MinItems,
- MaxItems: schema.Value.MaxItems,
- AllowEmptyValue: schema.Value.AllowEmptyValue,
- UniqueItems: schema.Value.UniqueItems,
- MultipleOf: schema.Value.MultipleOf,
- Extensions: stripNonExtensions(schema.Value.Extensions),
- Required: required,
- }
- }
- }
- if v := schema.Value.Items; v != nil {
- schema.Value.Items, _ = FromV3SchemaRef(v, components)
- }
- keys := make([]string, 0, len(schema.Value.Properties))
- for k := range schema.Value.Properties {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- for _, key := range keys {
- schema.Value.Properties[key], _ = FromV3SchemaRef(schema.Value.Properties[key], components)
+ return schema
}
- if v := schema.Value.AdditionalProperties.Schema; v != nil {
- schema.Value.AdditionalProperties.Schema, _ = FromV3SchemaRef(v, components)
+ if schema.Value.Items != nil {
+ schema.Value.Items = FromV3SchemaRef((schema.Value.Items))
}
- for i, v := range schema.Value.AllOf {
- schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components)
+ for k, v := range schema.Value.Properties {
+ schema.Value.Properties[k] = FromV3SchemaRef(v)
}
- if schema.Value.Nullable {
- schema.Value.Nullable = false
- schema.Value.Extensions["x-nullable"] = true
+ if schema.Value.AdditionalProperties != nil {
+ schema.Value.AdditionalProperties = FromV3SchemaRef(schema.Value.AdditionalProperties)
}
-
- return schema, nil
+ return schema
}
func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements {
if requirements == nil {
return nil
}
- result := make([]map[string][]string, 0, len(requirements))
- for _, item := range requirements {
- result = append(result, item)
+ result := make([]map[string][]string, len(requirements))
+ for i, item := range requirements {
+ result[i] = item
}
return result
}
-func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) {
- result := &openapi2.PathItem{
- Extensions: stripNonExtensions(pathItem.Extensions),
- }
+func FromV3PathItem(swagger *openapi3.Swagger, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) {
+ result := &openapi2.PathItem{}
for method, operation := range pathItem.Operations() {
- r, err := FromV3Operation(doc3, operation)
+ r, err := FromV3Operation(swagger, operation)
if err != nil {
return nil, err
}
result.SetOperation(method, r)
}
for _, parameter := range pathItem.Parameters {
- p, err := FromV3Parameter(parameter, doc3.Components)
+ p, err := FromV3Parameter(parameter)
if err != nil {
return nil, err
}
@@ -870,57 +489,7 @@ nameSearch:
return ""
}
-func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameters {
- parameters := openapi2.Parameters{}
- for propName, schemaRef := range mediaType.Schema.Value.Properties {
- if ref := schemaRef.Ref; ref != "" {
- v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1)
- parameters = append(parameters, &openapi2.Parameter{Ref: v2Ref})
- continue
- }
- val := schemaRef.Value
- typ := val.Type
- if val.Format == "binary" {
- typ = "file"
- }
- required := false
- for _, name := range val.Required {
- if name == propName {
- required = true
- break
- }
- }
- parameter := &openapi2.Parameter{
- Name: propName,
- Description: val.Description,
- Type: typ,
- In: "formData",
- Extensions: stripNonExtensions(val.Extensions),
- Enum: val.Enum,
- ExclusiveMin: val.ExclusiveMin,
- ExclusiveMax: val.ExclusiveMax,
- MinLength: val.MinLength,
- MaxLength: val.MaxLength,
- Default: val.Default,
- Items: val.Items,
- MinItems: val.MinItems,
- MaxItems: val.MaxItems,
- Maximum: val.Max,
- Minimum: val.Min,
- Pattern: val.Pattern,
- // CollectionFormat: val.CollectionFormat,
- // Format: val.Format,
- AllowEmptyValue: val.AllowEmptyValue,
- Required: required,
- UniqueItems: val.UniqueItems,
- MultipleOf: val.MultipleOf,
- }
- parameters = append(parameters, parameter)
- }
- return parameters
-}
-
-func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2.Operation, error) {
+func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) (*openapi2.Operation, error) {
if operation == nil {
return nil, nil
}
@@ -928,50 +497,28 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2
OperationID: operation.OperationID,
Summary: operation.Summary,
Description: operation.Description,
- Deprecated: operation.Deprecated,
Tags: operation.Tags,
- Extensions: stripNonExtensions(operation.Extensions),
}
if v := operation.Security; v != nil {
resultSecurity := FromV3SecurityRequirements(*v)
result.Security = &resultSecurity
}
for _, parameter := range operation.Parameters {
- r, err := FromV3Parameter(parameter, doc3.Components)
+ r, err := FromV3Parameter(parameter)
if err != nil {
return nil, err
}
result.Parameters = append(result.Parameters, r)
}
if v := operation.RequestBody; v != nil {
- // Find parameter name that we can use for the body
- name := findNameForRequestBody(operation)
- if name == "" {
- return nil, errors.New("could not find a name for request body")
- }
-
- bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, doc3.Components)
+ r, err := FromV3RequestBody(swagger, operation, v)
if err != nil {
return nil, err
}
- if len(formDataParameters) != 0 {
- result.Parameters = append(result.Parameters, formDataParameters...)
- } else if len(bodyOrRefParameters) != 0 {
- for _, param := range bodyOrRefParameters {
- result.Parameters = append(result.Parameters, param)
- break // add a single request body
- }
-
- }
-
- if len(consumes) != 0 {
- result.Consumes = consumesToArray(consumes)
- }
+ result.Parameters = append(result.Parameters, r)
}
- sort.Sort(result.Parameters)
-
if responses := operation.Responses; responses != nil {
- resultResponses, err := FromV3Responses(responses, doc3.Components)
+ resultResponses, err := FromV3Responses(responses)
if err != nil {
return nil, err
}
@@ -980,26 +527,41 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2
return result, nil
}
-func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, mediaType *openapi3.MediaType, components *openapi3.Components) (*openapi2.Parameter, error) {
+func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation, requestBodyRef *openapi3.RequestBodyRef) (*openapi2.Parameter, error) {
+ if ref := requestBodyRef.Ref; len(ref) > 0 {
+ return &openapi2.Parameter{
+ Ref: FromV3Ref(ref),
+ }, nil
+ }
requestBody := requestBodyRef.Value
+ // Find parameter name that we can use for the body
+ name := findNameForRequestBody(operation)
+
+ // If found an available name
+ if name == "" {
+ return nil, errors.New("Could not find a name for request body")
+ }
result := &openapi2.Parameter{
In: "body",
Name: name,
Description: requestBody.Description,
Required: requestBody.Required,
- Extensions: stripNonExtensions(requestBody.Extensions),
}
+ // Add JSON schema
+ mediaType := requestBody.GetMediaType("application/json")
if mediaType != nil {
- result.Schema, _ = FromV3SchemaRef(mediaType.Schema, components)
+ result.Schema = mediaType.Schema
}
return result, nil
}
-func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components) (*openapi2.Parameter, error) {
- if ref := ref.Ref; ref != "" {
- return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil
+func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) {
+ if v := ref.Ref; len(v) > 0 {
+ return &openapi2.Parameter{
+ Ref: FromV3Ref(v),
+ }, nil
}
parameter := ref.Value
if parameter == nil {
@@ -1010,14 +572,9 @@ func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components
In: parameter.In,
Name: parameter.Name,
Required: parameter.Required,
- Extensions: stripNonExtensions(parameter.Extensions),
}
if schemaRef := parameter.Schema; schemaRef != nil {
- schemaRef, _ = FromV3SchemaRef(schemaRef, components)
- if ref := schemaRef.Ref; ref != "" {
- result.Schema = &openapi3.SchemaRef{Ref: FromV3Ref(ref)}
- return result, nil
- }
+ schemaRef = FromV3SchemaRef(schemaRef)
schema := schemaRef.Value
result.Type = schema.Type
result.Format = schema.Format
@@ -1033,18 +590,14 @@ func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components
result.Items = schema.Items
result.MinItems = schema.MinItems
result.MaxItems = schema.MaxItems
- result.AllowEmptyValue = schema.AllowEmptyValue
- // result.CollectionFormat = schema.CollectionFormat
- result.UniqueItems = schema.UniqueItems
- result.MultipleOf = schema.MultipleOf
}
return result, nil
}
-func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error) {
+func FromV3Responses(responses map[string]*openapi3.ResponseRef) (map[string]*openapi2.Response, error) {
v2Responses := make(map[string]*openapi2.Response, len(responses))
for k, response := range responses {
- r, err := FromV3Response(response, components)
+ r, err := FromV3Response(response)
if err != nil {
return nil, err
}
@@ -1053,9 +606,11 @@ func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *ope
return v2Responses, nil
}
-func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) (*openapi2.Response, error) {
- if ref := ref.Ref; ref != "" {
- return &openapi2.Response{Ref: FromV3Ref(ref)}, nil
+func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) {
+ if v := ref.Ref; len(v) > 0 {
+ return &openapi2.Response{
+ Ref: FromV3Ref(v),
+ }, nil
}
response := ref.Value
@@ -1068,38 +623,16 @@ func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components)
}
result := &openapi2.Response{
Description: description,
- Extensions: stripNonExtensions(response.Extensions),
}
if content := response.Content; content != nil {
if ct := content["application/json"]; ct != nil {
- result.Schema, _ = FromV3SchemaRef(ct.Schema, components)
- }
- }
- if headers := response.Headers; len(headers) > 0 {
- var err error
- if result.Headers, err = FromV3Headers(headers, components); err != nil {
- return nil, err
+ result.Schema = FromV3SchemaRef(ct.Schema)
}
}
return result, nil
}
-func FromV3Headers(defs openapi3.Headers, components *openapi3.Components) (map[string]*openapi2.Header, error) {
- headers := make(map[string]*openapi2.Header, len(defs))
- for name, header := range defs {
- ref := openapi3.ParameterRef{Ref: header.Ref, Value: &header.Value.Parameter}
- parameter, err := FromV3Parameter(&ref, components)
- if err != nil {
- return nil, err
- }
- parameter.In = ""
- parameter.Name = ""
- headers[name] = &openapi2.Header{Parameter: *parameter}
- }
- return headers, nil
-}
-
-func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) {
+func FromV3SecurityScheme(swagger *openapi3.Swagger, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) {
securityScheme := ref.Value
if securityScheme == nil {
return nil, nil
@@ -1107,7 +640,6 @@ func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*o
result := &openapi2.SecurityScheme{
Ref: FromV3Ref(ref.Ref),
Description: securityScheme.Description,
- Extensions: stripNonExtensions(securityScheme.Extensions),
}
switch securityScheme.Type {
case "http":
@@ -1129,39 +661,21 @@ func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*o
if flows != nil {
var flow *openapi3.OAuthFlow
// TODO: Is this the right priority? What if multiple defined?
- switch {
- case flows.Implicit != nil:
+ if flow = flows.Implicit; flow != nil {
result.Flow = "implicit"
- flow = flows.Implicit
- result.AuthorizationURL = flow.AuthorizationURL
-
- case flows.AuthorizationCode != nil:
- result.Flow = "accessCode"
- flow = flows.AuthorizationCode
- result.AuthorizationURL = flow.AuthorizationURL
- result.TokenURL = flow.TokenURL
-
- case flows.Password != nil:
+ } else if flow = flows.AuthorizationCode; flow != nil {
+ result.Flow = "accesscode"
+ } else if flow = flows.Password; flow != nil {
result.Flow = "password"
- flow = flows.Password
- result.TokenURL = flow.TokenURL
-
- case flows.ClientCredentials != nil:
- result.Flow = "application"
- flow = flows.ClientCredentials
- result.TokenURL = flow.TokenURL
-
- default:
+ } else {
return nil, nil
}
-
- result.Scopes = make(map[string]string, len(flow.Scopes))
for scope, desc := range flow.Scopes {
result.Scopes[scope] = desc
}
}
default:
- return nil, fmt.Errorf("unsupported security scheme type %q", securityScheme.Type)
+ return nil, fmt.Errorf("Unsupported security scheme type '%s'", securityScheme.Type)
}
return result, nil
}
@@ -1170,25 +684,3 @@ var attemptedBodyParameterNames = []string{
"body",
"requestBody",
}
-
-// stripNonExtensions removes invalid extensions: those not prefixed by "x-" and returns them
-func stripNonExtensions(extensions map[string]interface{}) map[string]interface{} {
- for extName := range extensions {
- if !strings.HasPrefix(extName, "x-") {
- delete(extensions, extName)
- }
- }
- return extensions
-}
-
-func addPathExtensions(doc2 *openapi2.T, path string, extensions map[string]interface{}) {
- if doc2.Paths == nil {
- doc2.Paths = make(map[string]*openapi2.PathItem)
- }
- pathItem := doc2.Paths[path]
- if pathItem == nil {
- pathItem = &openapi2.PathItem{}
- doc2.Paths[path] = pathItem
- }
- pathItem.Extensions = extensions
-}
diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go
index 317772152..2eeefecb3 100644
--- a/openapi2conv/openapi2_conv_test.go
+++ b/openapi2conv/openapi2_conv_test.go
@@ -1,861 +1,421 @@
package openapi2conv
import (
- "context"
"encoding/json"
"testing"
- "github.com/stretchr/testify/require"
-
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi3"
+ "github.com/stretchr/testify/require"
)
func TestConvOpenAPIV3ToV2(t *testing.T) {
- var doc3 openapi3.T
- err := json.Unmarshal([]byte(exampleV3), &doc3)
+ var swagger3 openapi3.Swagger
+ err := json.Unmarshal([]byte(exampleV3), &swagger3)
require.NoError(t, err)
- {
- // Refs need resolving before we can Validate
- sl := openapi3.NewLoader()
- err = sl.ResolveRefsIn(&doc3, nil)
- require.NoError(t, err)
- err = doc3.Validate(context.Background())
- require.NoError(t, err)
- }
- doc2, err := FromV3(&doc3)
+ actualV2, err := FromV3Swagger(&swagger3)
require.NoError(t, err)
- data, err := json.Marshal(doc2)
+ data, err := json.Marshal(actualV2)
require.NoError(t, err)
require.JSONEq(t, exampleV2, string(data))
}
-func TestConvOpenAPIV3ToV2WithReqBody(t *testing.T) {
- var doc3 openapi3.T
- err := json.Unmarshal([]byte(exampleRequestBodyV3), &doc3)
- require.NoError(t, err)
- {
- // Refs need resolving before we can Validate
- sl := openapi3.NewLoader()
- err = sl.ResolveRefsIn(&doc3, nil)
- require.NoError(t, err)
- err = doc3.Validate(context.Background())
- require.NoError(t, err)
- }
-
- doc2, err := FromV3(&doc3)
- require.NoError(t, err)
- data, err := json.Marshal(doc2)
- require.NoError(t, err)
- require.JSONEq(t, exampleRequestBodyV2, string(data))
-}
-
func TestConvOpenAPIV2ToV3(t *testing.T) {
- var doc2 openapi2.T
- err := json.Unmarshal([]byte(exampleV2), &doc2)
+ var swagger2 openapi2.Swagger
+ err := json.Unmarshal([]byte(exampleV2), &swagger2)
require.NoError(t, err)
- doc3, err := ToV3(&doc2)
- require.NoError(t, err)
- err = doc3.Validate(context.Background())
+ actualV3, err := ToV3Swagger(&swagger2)
require.NoError(t, err)
- data, err := json.Marshal(doc3)
+ data, err := json.Marshal(actualV3)
require.NoError(t, err)
require.JSONEq(t, exampleV3, string(data))
}
const exampleV2 = `
{
- "basePath": "/v2",
- "consumes": [
- "application/json",
- "application/xml"
- ],
- "definitions": {
- "Error": {
- "description": "Error response.",
- "properties": {
- "message": {
- "type": "string"
- }
- },
- "required": [
- "message"
- ],
- "type": "object"
- },
- "Item": {
- "additionalProperties": true,
- "properties": {
- "foo": {
- "type": "string",
- "x-nullable": true
- },
- "quux": {
- "$ref": "#/definitions/ItemExtension"
- }
- },
- "type": "object"
- },
- "ItemExtension": {
- "description": "It could be anything.",
- "type": "boolean"
- },
- "foo": {
- "description": "foo description",
- "enum": [
- "bar",
- "baz"
- ],
- "type": "string"
- }
- },
- "externalDocs": {
- "description": "Example Documentation",
- "url": "https://example/doc/"
- },
- "host": "test.example.com",
- "info": {
- "title": "MyAPI",
- "version": "0.1",
- "x-info": "info extension"
- },
- "parameters": {
- "banana": {
- "in": "path",
- "name": "banana",
- "required": true,
- "type": "string"
- },
- "post_form_ref": {
- "description": "param description",
- "in": "formData",
- "name": "fileUpload2",
- "required": true,
- "type": "file",
- "x-formData-name": "fileUpload2",
- "x-mimetype": "text/plain"
- },
- "put_body": {
- "in": "body",
- "name": "banana",
- "required": true,
- "schema": {
- "type": "string"
- },
- "x-originalParamName": "banana"
- }
- },
- "paths": {
- "/another/{banana}/{id}": {
- "parameters": [
- {
- "$ref": "#/parameters/banana"
- },
- {
- "in": "path",
- "name": "id",
- "required": true,
- "type": "integer"
- }
- ]
- },
- "/example": {
- "delete": {
- "description": "example delete",
- "operationId": "example-delete",
- "parameters": [
- {
- "description": "Only return results that intersect the provided bounding box.",
- "in": "query",
- "items": {
- "type": "number"
- },
- "maxItems": 4,
- "minItems": 4,
- "name": "bbox",
- "type": "array"
- },
- {
- "in": "query",
- "name": "x",
- "type": "string",
- "x-parameter": "parameter extension 1"
- },
- {
- "default": 250,
- "description": "The y parameter",
- "in": "query",
- "maximum": 10000,
- "minimum": 1,
- "name": "y",
- "type": "integer"
- }
- ],
- "responses": {
- "200": {
- "description": "ok",
- "schema": {
- "items": {
- "$ref": "#/definitions/Item"
- },
- "type": "array"
- },
- "headers": {
- "ETag": {
- "description": "The ETag (or entity tag) HTTP response header is an identifier for a specific version of a resource.",
- "type": "string",
- "maxLength": 64
- }
- }
- },
- "404": {
- "description": "404 response"
- },
- "default": {
- "description": "default response",
- "x-response": "response extension 1"
- }
- },
- "security": [
- {
- "get_security_0": [
- "scope0",
- "scope1"
- ],
- "get_security_1": []
- }
- ],
- "summary": "example get",
- "tags": [
- "Example"
- ]
- },
- "get": {
- "description": "example get",
- "responses": {
- "403": {
- "$ref": "#/responses/ForbiddenError"
- },
- "404": {
- "description": "404 response"
- },
- "default": {
- "description": "default response"
- }
- },
- "x-operation": "operation extension 1"
- },
- "head": {
- "description": "example head",
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "options": {
- "description": "example options",
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "patch": {
- "consumes": [
- "application/json",
- "application/xml"
- ],
- "description": "example patch",
- "parameters": [
- {
- "in": "body",
- "name": "patch_body",
- "schema": {
- "allOf": [
- {
- "$ref": "#/definitions/Item"
- }
- ]
- },
- "x-originalParamName": "patch_body",
- "x-requestBody": "requestbody extension 1"
- }
- ],
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "post": {
- "consumes": [
- "multipart/form-data"
- ],
- "description": "example post",
- "parameters": [
- {
- "$ref": "#/parameters/post_form_ref"
- },
- {
- "description": "param description",
- "in": "formData",
- "name": "fileUpload",
- "type": "file",
- "x-formData-name": "fileUpload",
- "x-mimetype": "text/plain"
- },
- {
- "description": "File Id",
- "in": "query",
- "name": "id",
- "type": "integer"
- },
- {
- "description": "Description of file contents",
- "in": "formData",
- "name": "note",
- "type": "integer",
- "x-formData-name": "note"
- }
- ],
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "put": {
- "description": "example put",
- "parameters": [
- {
- "$ref": "#/parameters/put_body"
- }
- ],
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "x-path": "path extension 1",
- "x-path2": "path extension 2"
- },
- "/foo": {
- "get": {
- "operationId": "getFoo",
- "consumes": [
- "application/json",
- "application/xml"
- ],
- "parameters": [
- {
- "x-originalParamName": "foo",
- "in": "body",
- "name": "foo",
- "schema": {
- "$ref": "#/definitions/foo"
- }
- }
- ],
- "responses": {
- "default": {
- "description": "OK",
- "schema": {
- "$ref": "#/definitions/foo"
- }
- }
- },
- "summary": "get foo"
- }
- }
- },
- "responses": {
- "ForbiddenError": {
- "description": "Insufficient permission to perform the requested action.",
- "schema": {
- "$ref": "#/definitions/Error"
- }
- }
- },
- "schemes": [
- "https"
- ],
- "security": [
- {
- "default_security_0": [
- "scope0",
- "scope1"
- ],
- "default_security_1": []
- }
- ],
- "swagger": "2.0",
- "tags": [
- {
- "description": "An example tag.",
- "name": "Example"
- }
- ],
- "x-root": "root extension 1",
- "x-root2": "root extension 2"
+ "info": {"title":"MyAPI","version":"0.1"},
+ "schemes": ["https"],
+ "host": "test.example.com",
+ "basePath": "/v2",
+ "tags": [
+ {
+ "name": "Example",
+ "description": "An example tag."
+ }
+ ],
+ "paths": {
+ "/another/{banana}/{id}": {
+ "parameters": [
+ {
+ "$ref": "#/parameters/banana"
+ },
+ {
+ "in": "path",
+ "name": "id",
+ "type": "integer",
+ "required": true
+ }
+ ]
+ },
+ "/example": {
+ "delete": {
+ "description": "example delete",
+ "responses": {
+ "default": {
+ "description": "default response"
+ },
+ "403": {
+ "$ref": "#/responses/ForbiddenError"
+ },
+ "404": {
+ "description": "404 response"
+ }
+ }
+ },
+ "get": {
+ "operationId": "example-get",
+ "summary": "example get",
+ "description": "example get",
+ "tags": [
+ "Example"
+ ],
+ "parameters": [
+ {
+ "in": "query",
+ "name": "x"
+ },
+ {
+ "in": "query",
+ "name": "y",
+ "description": "The y parameter",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10000,
+ "default": 250
+ },
+ {
+ "in": "query",
+ "name": "bbox",
+ "description": "Only return results that intersect the provided bounding box.",
+ "maxItems": 4,
+ "minItems": 4,
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "in": "body",
+ "name": "body",
+ "schema": {}
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "ok",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Item"
+ }
+ }
+ },
+ "default": {
+ "description": "default response"
+ },
+ "404": {
+ "description": "404 response"
+ }
+ },
+ "security": [
+ {
+ "get_security_0": [
+ "scope0",
+ "scope1"
+ ],
+ "get_security_1": []
+ }
+ ]
+ },
+ "head": {
+ "description": "example head",
+ "responses": {}
+ },
+ "patch": {
+ "description": "example patch",
+ "responses": {}
+ },
+ "post": {
+ "description": "example post",
+ "responses": {}
+ },
+ "put": {
+ "description": "example put",
+ "responses": {}
+ },
+ "options": {
+ "description": "example options",
+ "responses": {}
+ }
+ }
+ },
+ "responses": {
+ "ForbiddenError": {
+ "description": "Insufficient permission to perform the requested action.",
+ "schema": {
+ "$ref": "#/definitions/Error"
+ }
+ }
+ },
+ "definitions": {
+ "Item": {
+ "type": "object",
+ "properties": {
+ "foo": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": {
+ "$ref": "#/definitions/ItemExtension"
+ }
+ },
+ "ItemExtension": {
+ "description": "It could be anything."
+ },
+ "Error": {
+ "description": "Error response.",
+ "type": "object",
+ "required": [
+ "message"
+ ],
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "parameters": {
+ "banana": {
+ "in": "path",
+ "type": "string"
+ }
+ },
+ "security": [
+ {
+ "default_security_0": [
+ "scope0",
+ "scope1"
+ ],
+ "default_security_1": []
+ }
+ ]
}
`
const exampleV3 = `
{
- "components": {
- "parameters": {
- "banana": {
- "in": "path",
- "name": "banana",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- },
- "requestBodies": {
- "put_body": {
- "content": {
- "application/json": {
- "schema": {
- "type": "string"
- }
- },
- "application/xml": {
- "schema": {
- "type": "string"
- }
- }
- },
- "required": true,
- "x-originalParamName": "banana"
- }
- },
- "responses": {
- "ForbiddenError": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Error"
- }
- }
- },
- "description": "Insufficient permission to perform the requested action."
- }
- },
- "schemas": {
- "Error": {
- "description": "Error response.",
- "properties": {
- "message": {
- "type": "string"
- }
- },
- "required": [
- "message"
- ],
- "type": "object"
- },
- "Item": {
- "additionalProperties": true,
- "properties": {
- "foo": {
- "type": "string",
- "nullable": true
- },
- "quux": {
- "$ref": "#/components/schemas/ItemExtension"
- }
- },
- "type": "object"
- },
- "ItemExtension": {
- "description": "It could be anything.",
- "type": "boolean"
- },
- "post_form_ref": {
- "description": "param description",
- "format": "binary",
- "required": [
- "fileUpload2"
- ],
- "type": "string",
- "x-formData-name": "fileUpload2",
- "x-mimetype": "text/plain"
- },
- "foo": {
- "description": "foo description",
- "enum": [
- "bar",
- "baz"
- ],
- "type": "string"
- }
- }
- },
- "externalDocs": {
- "description": "Example Documentation",
- "url": "https://example/doc/"
- },
- "info": {
- "title": "MyAPI",
- "version": "0.1",
- "x-info": "info extension"
- },
- "openapi": "3.0.3",
- "paths": {
- "/another/{banana}/{id}": {
- "parameters": [
- {
- "$ref": "#/components/parameters/banana"
- },
- {
- "in": "path",
- "name": "id",
- "required": true,
- "schema": {
- "type": "integer"
- }
- }
- ]
- },
- "/example": {
- "delete": {
- "description": "example delete",
- "operationId": "example-delete",
- "parameters": [
- {
- "description": "Only return results that intersect the provided bounding box.",
- "in": "query",
- "name": "bbox",
- "schema": {
- "items": {
- "type": "number"
- },
- "maxItems": 4,
- "minItems": 4,
- "type": "array"
- }
- },
- {
- "in": "query",
- "name": "x",
- "schema": {
- "type": "string"
- },
- "x-parameter": "parameter extension 1"
- },
- {
- "description": "The y parameter",
- "in": "query",
- "name": "y",
- "schema": {
- "default": 250,
- "maximum": 10000,
- "minimum": 1,
- "type": "integer"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "items": {
- "$ref": "#/components/schemas/Item"
- },
- "type": "array"
- }
- }
- },
- "description": "ok",
- "headers": {
- "ETag": {
- "description": "The ETag (or entity tag) HTTP response header is an identifier for a specific version of a resource.",
- "schema": {
- "type": "string",
- "maxLength": 64
- }
- }
- }
- },
- "404": {
- "description": "404 response"
- },
- "default": {
- "description": "default response",
- "x-response": "response extension 1"
- }
- },
- "security": [
- {
- "get_security_0": [
- "scope0",
- "scope1"
- ],
- "get_security_1": []
- }
- ],
- "summary": "example get",
- "tags": [
- "Example"
- ]
- },
- "get": {
- "description": "example get",
- "responses": {
- "403": {
- "$ref": "#/components/responses/ForbiddenError"
- },
- "404": {
- "description": "404 response"
- },
- "default": {
- "description": "default response"
- }
- },
- "x-operation": "operation extension 1"
- },
- "head": {
- "description": "example head",
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "options": {
- "description": "example options",
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "patch": {
- "description": "example patch",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "allOf": [
- {
- "$ref": "#/components/schemas/Item"
- }
- ]
- }
- },
- "application/xml": {
- "schema": {
- "allOf": [
- {
- "$ref": "#/components/schemas/Item"
- }
- ]
- }
- }
- },
- "x-originalParamName": "patch_body",
- "x-requestBody": "requestbody extension 1"
- },
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "post": {
- "description": "example post",
- "parameters": [
- {
- "description": "File Id",
- "in": "query",
- "name": "id",
- "schema": {
- "type": "integer"
- }
- }
- ],
- "requestBody": {
- "content": {
- "multipart/form-data": {
- "schema": {
- "properties": {
- "fileUpload": {
- "description": "param description",
- "format": "binary",
- "type": "string",
- "x-formData-name": "fileUpload",
- "x-mimetype": "text/plain"
- },
- "fileUpload2": {
- "$ref": "#/components/schemas/post_form_ref"
- },
- "note": {
- "description": "Description of file contents",
- "type": "integer",
- "x-formData-name": "note"
- }
- },
- "required": [
- "fileUpload2"
- ],
- "type": "object"
- }
- }
- }
- },
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "put": {
- "description": "example put",
- "requestBody": {
- "$ref": "#/components/requestBodies/put_body"
- },
- "responses": {
- "default": {
- "description": "default response"
- }
- }
- },
- "x-path": "path extension 1",
- "x-path2": "path extension 2"
- },
- "/foo": {
- "get": {
- "operationId": "getFoo",
- "requestBody": {
- "x-originalParamName": "foo",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/foo"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/foo"
- }
- }
- }
- },
- "responses": {
- "default": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/foo"
- }
- }
- },
- "description": "OK"
- }
- },
- "summary": "get foo"
- }
- }
- },
- "security": [
- {
- "default_security_0": [
- "scope0",
- "scope1"
- ],
- "default_security_1": []
- }
- ],
- "servers": [
- {
- "url": "https://test.example.com/v2"
- }
- ],
- "tags": [
- {
- "description": "An example tag.",
- "name": "Example"
- }
- ],
- "x-root": "root extension 1",
- "x-root2": "root extension 2"
-}
-`
-
-const exampleRequestBodyV3 = `{
- "info": {
- "description": "Test Spec",
- "title": "Test Spec",
- "version": "0.0.0"
- },
- "components": {
- "requestBodies": {
- "FooBody": {
- "content": {
- "application/json": {
- "schema": {
- "properties": { "message": { "type": "string" } },
- "type": "object"
- }
- }
- },
- "description": "test spec request body.",
- "required": true
- }
- }
- },
- "paths": {
- "/foo-path": {
- "post": {
- "requestBody": { "$ref": "#/components/requestBodies/FooBody" },
- "responses": { "202": { "description": "Test spec post." } },
- "summary": "Test spec path"
- }
- }
- },
- "servers": [{ "url": "http://localhost/" }],
- "openapi": "3.0.3"
-}
-`
-
-const exampleRequestBodyV2 = `{
- "basePath": "/",
- "consumes": ["application/json"],
- "host": "localhost",
- "info": {
- "description": "Test Spec",
- "title": "Test Spec",
- "version": "0.0.0"
- },
- "parameters": {
- "FooBody": {
- "description": "test spec request body.",
- "in": "body",
- "name": "FooBody",
- "required": true,
- "schema": {
- "properties": { "message": { "type": "string" } },
- "type": "object"
- }
- }
- },
- "paths": {
- "/foo-path": {
- "post": {
- "parameters": [{ "$ref": "#/parameters/FooBody" }],
- "responses": { "202": { "description": "Test spec post." } },
- "summary": "Test spec path"
- }
- }
- },
- "schemes": ["http"],
- "swagger": "2.0"
+ "openapi": "3.0.2",
+ "info": {"title":"MyAPI","version":"0.1"},
+ "components": {
+ "responses": {
+ "ForbiddenError": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ },
+ "description": "Insufficient permission to perform the requested action."
+ }
+ },
+ "parameters": {
+ "banana": {
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ },
+ "schemas": {
+ "Item": {
+ "type": "object",
+ "properties": {
+ "foo": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/ItemExtension"
+ }
+ },
+ "ItemExtension": {
+ "description": "It could be anything."
+ },
+ "Error": {
+ "description": "Error response.",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "Example",
+ "description": "An example tag."
+ }
+ ],
+ "servers": [
+ {
+ "url": "https://test.example.com/v2"
+ }
+ ],
+ "paths": {
+ "/another/{banana}/{id}": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/banana"
+ },
+ {
+ "in": "path",
+ "name": "id",
+ "schema": {
+ "type": "integer"
+ },
+ "required": true
+ }
+ ]
+ },
+ "/example": {
+ "delete": {
+ "description": "example delete",
+ "responses": {
+ "default": {
+ "description": "default response"
+ },
+ "403": {
+ "$ref": "#/components/responses/ForbiddenError"
+ },
+ "404": {
+ "description": "404 response"
+ }
+ }
+ },
+ "get": {
+ "operationId": "example-get",
+ "summary": "example get",
+ "description": "example get",
+ "tags": [
+ "Example"
+ ],
+ "parameters": [
+ {
+ "in": "query",
+ "name": "x"
+ },
+ {
+ "description": "The y parameter",
+ "in": "query",
+ "name": "y",
+ "schema": {
+ "default": 250,
+ "maximum": 10000,
+ "minimum": 1,
+ "type": "integer"
+ }
+ },
+ {
+ "description": "Only return results that intersect the provided bounding box.",
+ "in": "query",
+ "name": "bbox",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 4,
+ "maxItems": 4
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "ok",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/Item"
+ },
+ "type": "array"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "default response"
+ },
+ "404": {
+ "description": "404 response"
+ }
+ },
+ "security": [
+ {
+ "get_security_0": [
+ "scope0",
+ "scope1"
+ ],
+ "get_security_1": []
+ }
+ ]
+ },
+ "head": {
+ "description": "example head",
+ "responses": {}
+ },
+ "options": {
+ "description": "example options",
+ "responses": {}
+ },
+ "patch": {
+ "description": "example patch",
+ "responses": {}
+ },
+ "post": {
+ "description": "example post",
+ "responses": {}
+ },
+ "put": {
+ "description": "example put",
+ "responses": {}
+ }
+ }
+ },
+ "security": [
+ {
+ "default_security_0": [
+ "scope0",
+ "scope1"
+ ],
+ "default_security_1": []
+ }
+ ]
}
`
diff --git a/openapi2conv/testdata/swagger.json b/openapi2conv/testdata/swagger.json
deleted file mode 120000
index c211aa245..000000000
--- a/openapi2conv/testdata/swagger.json
+++ /dev/null
@@ -1 +0,0 @@
-../../openapi2/testdata/swagger.json
\ No newline at end of file
diff --git a/openapi3/callback.go b/openapi3/callback.go
index 62cea72d8..60196ba16 100644
--- a/openapi3/callback.go
+++ b/openapi3/callback.go
@@ -1,46 +1,13 @@
package openapi3
-import (
- "context"
- "fmt"
- "sort"
+import "context"
- "github.com/go-openapi/jsonpointer"
-)
-
-type Callbacks map[string]*CallbackRef
-
-var _ jsonpointer.JSONPointable = (*Callbacks)(nil)
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (c Callbacks) JSONLookup(token string) (interface{}, error) {
- ref, ok := c[token]
- if ref == nil || !ok {
- return nil, fmt.Errorf("object has no field %q", token)
- }
-
- if ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
-// Callback is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callback-object
+// Callback is specified by OpenAPI/Swagger standard version 3.0.
type Callback map[string]*PathItem
-// Validate returns an error if Callback does not comply with the OpenAPI spec.
-func (callback Callback) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- keys := make([]string, 0, len(callback))
- for key := range callback {
- keys = append(keys, key)
- }
- sort.Strings(keys)
- for _, key := range keys {
- v := callback[key]
- if err := v.Validate(ctx); err != nil {
+func (value Callback) Validate(c context.Context) error {
+ for _, v := range value {
+ if err := v.Validate(c); err != nil {
return err
}
}
diff --git a/openapi3/components.go b/openapi3/components.go
index 0981e8bfe..78b66aa31 100644
--- a/openapi3/components.go
+++ b/openapi3/components.go
@@ -2,241 +2,103 @@ package openapi3
import (
"context"
- "encoding/json"
"fmt"
"regexp"
- "sort"
+
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-// Components is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#components-object
+// Components is specified by OpenAPI/Swagger standard version 3.0.
type Components struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"`
- Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"`
- Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"`
- RequestBodies RequestBodies `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"`
- Responses Responses `json:"responses,omitempty" yaml:"responses,omitempty"`
- SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"`
- Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"`
- Links Links `json:"links,omitempty" yaml:"links,omitempty"`
- Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"`
+ ExtensionProps
+ Schemas map[string]*SchemaRef `json:"schemas,omitempty" yaml:"schemas,omitempty"`
+ Parameters map[string]*ParameterRef `json:"parameters,omitempty" yaml:"parameters,omitempty"`
+ Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"`
+ RequestBodies map[string]*RequestBodyRef `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"`
+ Responses map[string]*ResponseRef `json:"responses,omitempty" yaml:"responses,omitempty"`
+ SecuritySchemes map[string]*SecuritySchemeRef `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"`
+ Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"`
+ Links map[string]*LinkRef `json:"links,omitempty" yaml:"links,omitempty"`
+ Callbacks map[string]*CallbackRef `json:"callbacks,omitempty" yaml:"callbacks,omitempty"`
}
func NewComponents() Components {
return Components{}
}
-// MarshalJSON returns the JSON encoding of Components.
-func (components Components) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 9+len(components.Extensions))
- for k, v := range components.Extensions {
- m[k] = v
- }
- if x := components.Schemas; len(x) != 0 {
- m["schemas"] = x
- }
- if x := components.Parameters; len(x) != 0 {
- m["parameters"] = x
- }
- if x := components.Headers; len(x) != 0 {
- m["headers"] = x
- }
- if x := components.RequestBodies; len(x) != 0 {
- m["requestBodies"] = x
- }
- if x := components.Responses; len(x) != 0 {
- m["responses"] = x
- }
- if x := components.SecuritySchemes; len(x) != 0 {
- m["securitySchemes"] = x
- }
- if x := components.Examples; len(x) != 0 {
- m["examples"] = x
- }
- if x := components.Links; len(x) != 0 {
- m["links"] = x
- }
- if x := components.Callbacks; len(x) != 0 {
- m["callbacks"] = x
- }
- return json.Marshal(m)
+func (components *Components) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(components)
}
-// UnmarshalJSON sets Components to a copy of data.
func (components *Components) UnmarshalJSON(data []byte) error {
- type ComponentsBis Components
- var x ComponentsBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "schemas")
- delete(x.Extensions, "parameters")
- delete(x.Extensions, "headers")
- delete(x.Extensions, "requestBodies")
- delete(x.Extensions, "responses")
- delete(x.Extensions, "securitySchemes")
- delete(x.Extensions, "examples")
- delete(x.Extensions, "links")
- delete(x.Extensions, "callbacks")
- *components = Components(x)
- return nil
+ return jsoninfo.UnmarshalStrictStruct(data, components)
}
-// Validate returns an error if Components does not comply with the OpenAPI spec.
-func (components *Components) Validate(ctx context.Context, opts ...ValidationOption) (err error) {
- ctx = WithValidationOptions(ctx, opts...)
-
- schemas := make([]string, 0, len(components.Schemas))
- for name := range components.Schemas {
- schemas = append(schemas, name)
- }
- sort.Strings(schemas)
- for _, k := range schemas {
- v := components.Schemas[k]
+func (components *Components) Validate(c context.Context) (err error) {
+ for k, v := range components.Schemas {
if err = ValidateIdentifier(k); err != nil {
- return fmt.Errorf("schema %q: %w", k, err)
+ return
}
- if err = v.Validate(ctx); err != nil {
- return fmt.Errorf("schema %q: %w", k, err)
+ if err = v.Validate(c); err != nil {
+ return
}
}
- parameters := make([]string, 0, len(components.Parameters))
- for name := range components.Parameters {
- parameters = append(parameters, name)
- }
- sort.Strings(parameters)
- for _, k := range parameters {
- v := components.Parameters[k]
+ for k, v := range components.Parameters {
if err = ValidateIdentifier(k); err != nil {
- return fmt.Errorf("parameter %q: %w", k, err)
+ return
}
- if err = v.Validate(ctx); err != nil {
- return fmt.Errorf("parameter %q: %w", k, err)
+ if err = v.Validate(c); err != nil {
+ return
}
}
- requestBodies := make([]string, 0, len(components.RequestBodies))
- for name := range components.RequestBodies {
- requestBodies = append(requestBodies, name)
- }
- sort.Strings(requestBodies)
- for _, k := range requestBodies {
- v := components.RequestBodies[k]
+ for k, v := range components.RequestBodies {
if err = ValidateIdentifier(k); err != nil {
- return fmt.Errorf("request body %q: %w", k, err)
+ return
}
- if err = v.Validate(ctx); err != nil {
- return fmt.Errorf("request body %q: %w", k, err)
+ if err = v.Validate(c); err != nil {
+ return
}
}
- responses := make([]string, 0, len(components.Responses))
- for name := range components.Responses {
- responses = append(responses, name)
- }
- sort.Strings(responses)
- for _, k := range responses {
- v := components.Responses[k]
+ for k, v := range components.Responses {
if err = ValidateIdentifier(k); err != nil {
- return fmt.Errorf("response %q: %w", k, err)
+ return
}
- if err = v.Validate(ctx); err != nil {
- return fmt.Errorf("response %q: %w", k, err)
+ if err = v.Validate(c); err != nil {
+ return
}
}
- headers := make([]string, 0, len(components.Headers))
- for name := range components.Headers {
- headers = append(headers, name)
- }
- sort.Strings(headers)
- for _, k := range headers {
- v := components.Headers[k]
+ for k, v := range components.Headers {
if err = ValidateIdentifier(k); err != nil {
- return fmt.Errorf("header %q: %w", k, err)
+ return
}
- if err = v.Validate(ctx); err != nil {
- return fmt.Errorf("header %q: %w", k, err)
+ if err = v.Validate(c); err != nil {
+ return
}
}
- securitySchemes := make([]string, 0, len(components.SecuritySchemes))
- for name := range components.SecuritySchemes {
- securitySchemes = append(securitySchemes, name)
- }
- sort.Strings(securitySchemes)
- for _, k := range securitySchemes {
- v := components.SecuritySchemes[k]
- if err = ValidateIdentifier(k); err != nil {
- return fmt.Errorf("security scheme %q: %w", k, err)
- }
- if err = v.Validate(ctx); err != nil {
- return fmt.Errorf("security scheme %q: %w", k, err)
- }
- }
-
- examples := make([]string, 0, len(components.Examples))
- for name := range components.Examples {
- examples = append(examples, name)
- }
- sort.Strings(examples)
- for _, k := range examples {
- v := components.Examples[k]
- if err = ValidateIdentifier(k); err != nil {
- return fmt.Errorf("example %q: %w", k, err)
- }
- if err = v.Validate(ctx); err != nil {
- return fmt.Errorf("example %q: %w", k, err)
- }
- }
-
- links := make([]string, 0, len(components.Links))
- for name := range components.Links {
- links = append(links, name)
- }
- sort.Strings(links)
- for _, k := range links {
- v := components.Links[k]
- if err = ValidateIdentifier(k); err != nil {
- return fmt.Errorf("link %q: %w", k, err)
- }
- if err = v.Validate(ctx); err != nil {
- return fmt.Errorf("link %q: %w", k, err)
- }
- }
-
- callbacks := make([]string, 0, len(components.Callbacks))
- for name := range components.Callbacks {
- callbacks = append(callbacks, name)
- }
- sort.Strings(callbacks)
- for _, k := range callbacks {
- v := components.Callbacks[k]
+ for k, v := range components.SecuritySchemes {
if err = ValidateIdentifier(k); err != nil {
- return fmt.Errorf("callback %q: %w", k, err)
+ return
}
- if err = v.Validate(ctx); err != nil {
- return fmt.Errorf("callback %q: %w", k, err)
+ if err = v.Validate(c); err != nil {
+ return
}
}
- return validateExtensions(ctx, components.Extensions)
+ return
}
-const identifierPattern = `^[a-zA-Z0-9._-]+$`
+const identifierPattern = `^[a-zA-Z0-9.\-_]+$`
-// IdentifierRegExp verifies whether Component object key matches 'identifierPattern' pattern, according to OapiAPI v3.x.0.
-// Hovever, to be able supporting legacy OpenAPI v2.x, there is a need to customize above pattern in orde not to fail
-// converted v2-v3 validation
-var IdentifierRegExp = regexp.MustCompile(identifierPattern)
+var identifierRegExp = regexp.MustCompile(identifierPattern)
func ValidateIdentifier(value string) error {
- if IdentifierRegExp.MatchString(value) {
+ if identifierRegExp.MatchString(value) {
return nil
}
- return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (regexp: %q)", value, identifierPattern)
+ return fmt.Errorf("Identifier '%s' is not supported by OpenAPI version 3 standard (regexp: '%s')", value, identifierPattern)
}
diff --git a/openapi3/content.go b/openapi3/content.go
index 81b070eec..8d187fd91 100644
--- a/openapi3/content.go
+++ b/openapi3/content.go
@@ -2,7 +2,6 @@ package openapi3
import (
"context"
- "sort"
"strings"
)
@@ -10,33 +9,7 @@ import (
type Content map[string]*MediaType
func NewContent() Content {
- return make(map[string]*MediaType)
-}
-
-func NewContentWithSchema(schema *Schema, consumes []string) Content {
- if len(consumes) == 0 {
- return Content{
- "*/*": NewMediaType().WithSchema(schema),
- }
- }
- content := make(map[string]*MediaType, len(consumes))
- for _, mediaType := range consumes {
- content[mediaType] = NewMediaType().WithSchema(schema)
- }
- return content
-}
-
-func NewContentWithSchemaRef(schema *SchemaRef, consumes []string) Content {
- if len(consumes) == 0 {
- return Content{
- "*/*": NewMediaType().WithSchemaRef(schema),
- }
- }
- content := make(map[string]*MediaType, len(consumes))
- for _, mediaType := range consumes {
- content[mediaType] = NewMediaType().WithSchemaRef(schema)
- }
- return content
+ return make(map[string]*MediaType, 4)
}
func NewContentWithJSONSchema(schema *Schema) Content {
@@ -50,18 +23,6 @@ func NewContentWithJSONSchemaRef(schema *SchemaRef) Content {
}
}
-func NewContentWithFormDataSchema(schema *Schema) Content {
- return Content{
- "multipart/form-data": NewMediaType().WithSchema(schema),
- }
-}
-
-func NewContentWithFormDataSchemaRef(schema *SchemaRef) Content {
- return Content{
- "multipart/form-data": NewMediaType().WithSchemaRef(schema),
- }
-}
-
func (content Content) Get(mime string) *MediaType {
// If the mime is empty then short-circuit to the wildcard.
// We do this here so that we catch only the specific case of
@@ -105,18 +66,10 @@ func (content Content) Get(mime string) *MediaType {
return content["*/*"]
}
-// Validate returns an error if Content does not comply with the OpenAPI spec.
-func (content Content) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- keys := make([]string, 0, len(content))
- for key := range content {
- keys = append(keys, key)
- }
- sort.Strings(keys)
- for _, k := range keys {
- v := content[k]
- if err := v.Validate(ctx); err != nil {
+func (content Content) Validate(c context.Context) error {
+ for _, v := range content {
+ // Validate MediaType
+ if err := v.Validate(c); err != nil {
return err
}
}
diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go
index 8b6b813f2..de518d578 100644
--- a/openapi3/discriminator.go
+++ b/openapi3/discriminator.go
@@ -2,48 +2,25 @@ package openapi3
import (
"context"
- "encoding/json"
+
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-// Discriminator is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object
+// Discriminator is specified by OpenAPI/Swagger standard version 3.0.
type Discriminator struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- PropertyName string `json:"propertyName" yaml:"propertyName"` // required
+ ExtensionProps
+ PropertyName string `json:"propertyName" yaml:"propertyName"`
Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
}
-// MarshalJSON returns the JSON encoding of Discriminator.
-func (discriminator Discriminator) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 2+len(discriminator.Extensions))
- for k, v := range discriminator.Extensions {
- m[k] = v
- }
- m["propertyName"] = discriminator.PropertyName
- if x := discriminator.Mapping; len(x) != 0 {
- m["mapping"] = x
- }
- return json.Marshal(m)
+func (value *Discriminator) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(value)
}
-// UnmarshalJSON sets Discriminator to a copy of data.
-func (discriminator *Discriminator) UnmarshalJSON(data []byte) error {
- type DiscriminatorBis Discriminator
- var x DiscriminatorBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "propertyName")
- delete(x.Extensions, "mapping")
- *discriminator = Discriminator(x)
- return nil
+func (value *Discriminator) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalStrictStruct(data, value)
}
-// Validate returns an error if Discriminator does not comply with the OpenAPI spec.
-func (discriminator *Discriminator) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- return validateExtensions(ctx, discriminator.Extensions)
+func (value *Discriminator) Validate(c context.Context) error {
+ return nil
}
diff --git a/openapi3/discriminator_test.go b/openapi3/discriminator_test.go
index 7c16992cf..c12227141 100644
--- a/openapi3/discriminator_test.go
+++ b/openapi3/discriminator_test.go
@@ -1,24 +1,15 @@
-package openapi3
+package openapi3_test
import (
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
-func TestParsingDiscriminator(t *testing.T) {
- const spec = `
+var jsonSpecWithDiscriminator = []byte(`
{
"openapi": "3.0.0",
- "info": {
- "version": "1.0.0",
- "title": "title",
- "description": "desc",
- "contact": {
- "email": "email"
- }
- },
- "paths": {},
"components": {
"schemas": {
"MyResponseType": {
@@ -43,14 +34,10 @@ func TestParsingDiscriminator(t *testing.T) {
}
}
}
-`
-
- loader := NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
+`)
- err = doc.Validate(loader.Context)
+func TestParsingDiscriminator(t *testing.T) {
+ loader, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(jsonSpecWithDiscriminator)
require.NoError(t, err)
-
- require.Equal(t, 2, len(doc.Components.Schemas["MyResponseType"].Value.Discriminator.Mapping))
+ require.Equal(t, 2, len(loader.Components.Schemas["MyResponseType"].Value.OneOf))
}
diff --git a/openapi3/doc.go b/openapi3/doc.go
index 41c9965c6..9f9554962 100644
--- a/openapi3/doc.go
+++ b/openapi3/doc.go
@@ -1,4 +1,5 @@
-// Package openapi3 parses and writes OpenAPI 3 specification documents.
+// Package openapi3 parses and writes OpenAPI 3 specifications.
//
-// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md
+// The OpenAPI 3.0 specification can be found at:
+// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.md
package openapi3
diff --git a/openapi3/encoding.go b/openapi3/encoding.go
index dc2e54438..a60bddf82 100644
--- a/openapi3/encoding.go
+++ b/openapi3/encoding.go
@@ -2,21 +2,20 @@ package openapi3
import (
"context"
- "encoding/json"
"fmt"
- "sort"
+
+ "github.com/getkin/kin-openapi/jsoninfo"
)
// Encoding is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encoding-object
type Encoding struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
+ ExtensionProps
- ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"`
- Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"`
- Style string `json:"style,omitempty" yaml:"style,omitempty"`
- Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"`
- AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"`
+ ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"`
+ Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"`
+ Style string `json:"style,omitempty" yaml:"style,omitempty"`
+ Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"`
+ AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"`
}
func NewEncoding() *Encoding {
@@ -39,45 +38,12 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding {
return encoding
}
-// MarshalJSON returns the JSON encoding of Encoding.
-func (encoding Encoding) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 5+len(encoding.Extensions))
- for k, v := range encoding.Extensions {
- m[k] = v
- }
- if x := encoding.ContentType; x != "" {
- m["contentType"] = x
- }
- if x := encoding.Headers; len(x) != 0 {
- m["headers"] = x
- }
- if x := encoding.Style; x != "" {
- m["style"] = x
- }
- if x := encoding.Explode; x != nil {
- m["explode"] = x
- }
- if x := encoding.AllowReserved; x {
- m["allowReserved"] = x
- }
- return json.Marshal(m)
+func (encoding *Encoding) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(encoding)
}
-// UnmarshalJSON sets Encoding to a copy of data.
func (encoding *Encoding) UnmarshalJSON(data []byte) error {
- type EncodingBis Encoding
- var x EncodingBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "contentType")
- delete(x.Extensions, "headers")
- delete(x.Extensions, "style")
- delete(x.Extensions, "explode")
- delete(x.Extensions, "allowReserved")
- *encoding = Encoding(x)
- return nil
+ return jsoninfo.UnmarshalStrictStruct(data, encoding)
}
// SerializationMethod returns a serialization method of request body.
@@ -95,25 +61,15 @@ func (encoding *Encoding) SerializationMethod() *SerializationMethod {
return sm
}
-// Validate returns an error if Encoding does not comply with the OpenAPI spec.
-func (encoding *Encoding) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (encoding *Encoding) Validate(c context.Context) error {
if encoding == nil {
return nil
}
-
- headers := make([]string, 0, len(encoding.Headers))
- for k := range encoding.Headers {
- headers = append(headers, k)
- }
- sort.Strings(headers)
- for _, k := range headers {
- v := encoding.Headers[k]
+ for k, v := range encoding.Headers {
if err := ValidateIdentifier(k); err != nil {
return nil
}
- if err := v.Validate(ctx); err != nil {
+ if err := v.Validate(c); err != nil {
return nil
}
}
@@ -128,9 +84,10 @@ func (encoding *Encoding) Validate(ctx context.Context, opts ...ValidationOption
sm.Style == SerializationPipeDelimited && sm.Explode,
sm.Style == SerializationPipeDelimited && !sm.Explode,
sm.Style == SerializationDeepObject && sm.Explode:
+ // it is a valid
default:
- return fmt.Errorf("serialization method with style=%q and explode=%v is not supported by media type", sm.Style, sm.Explode)
+ return fmt.Errorf("Serialization method with style=%q and explode=%v is not supported by media type", sm.Style, sm.Explode)
}
- return validateExtensions(ctx, encoding.Extensions)
+ return nil
}
diff --git a/openapi3/encoding_test.go b/openapi3/encoding_test.go
index 5c354540d..67e7b6b65 100644
--- a/openapi3/encoding_test.go
+++ b/openapi3/encoding_test.go
@@ -1,4 +1,4 @@
-package openapi3
+package openapi3_test
import (
"context"
@@ -6,6 +6,7 @@ import (
"reflect"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
@@ -16,13 +17,13 @@ func TestEncodingJSON(t *testing.T) {
require.NotEmpty(t, data)
t.Log("Unmarshal *openapi3.Encoding from JSON")
- docA := &Encoding{}
+ docA := &openapi3.Encoding{}
err = json.Unmarshal(encodingJSON, &docA)
require.NoError(t, err)
require.NotEmpty(t, data)
t.Log("Validate *openapi3.Encoding")
- err = docA.Validate(context.Background())
+ err = docA.Validate(context.TODO())
require.NoError(t, err)
t.Log("Ensure representations match")
@@ -44,13 +45,13 @@ var encodingJSON = []byte(`
}
`)
-func encoding() *Encoding {
+func encoding() *openapi3.Encoding {
explode := true
- return &Encoding{
+ return &openapi3.Encoding{
ContentType: "application/json",
- Headers: map[string]*HeaderRef{
+ Headers: map[string]*openapi3.HeaderRef{
"someHeader": {
- Value: &Header{},
+ Value: &openapi3.Header{},
},
},
Style: "form",
@@ -63,32 +64,32 @@ func TestEncodingSerializationMethod(t *testing.T) {
boolPtr := func(b bool) *bool { return &b }
testCases := []struct {
name string
- enc *Encoding
- want *SerializationMethod
+ enc *openapi3.Encoding
+ want *openapi3.SerializationMethod
}{
{
name: "default",
- want: &SerializationMethod{Style: SerializationForm, Explode: true},
+ want: &openapi3.SerializationMethod{Style: openapi3.SerializationForm, Explode: true},
},
{
name: "encoding with style",
- enc: &Encoding{Style: SerializationSpaceDelimited},
- want: &SerializationMethod{Style: SerializationSpaceDelimited, Explode: true},
+ enc: &openapi3.Encoding{Style: openapi3.SerializationSpaceDelimited},
+ want: &openapi3.SerializationMethod{Style: openapi3.SerializationSpaceDelimited, Explode: true},
},
{
name: "encoding with explode",
- enc: &Encoding{Explode: boolPtr(true)},
- want: &SerializationMethod{Style: SerializationForm, Explode: true},
+ enc: &openapi3.Encoding{Explode: boolPtr(true)},
+ want: &openapi3.SerializationMethod{Style: openapi3.SerializationForm, Explode: true},
},
{
name: "encoding with no explode",
- enc: &Encoding{Explode: boolPtr(false)},
- want: &SerializationMethod{Style: SerializationForm, Explode: false},
+ enc: &openapi3.Encoding{Explode: boolPtr(false)},
+ want: &openapi3.SerializationMethod{Style: openapi3.SerializationForm, Explode: false},
},
{
name: "encoding with style and explode ",
- enc: &Encoding{Style: SerializationSpaceDelimited, Explode: boolPtr(false)},
- want: &SerializationMethod{Style: SerializationSpaceDelimited, Explode: false},
+ enc: &openapi3.Encoding{Style: openapi3.SerializationSpaceDelimited, Explode: boolPtr(false)},
+ want: &openapi3.SerializationMethod{Style: openapi3.SerializationSpaceDelimited, Explode: false},
},
}
for _, tc := range testCases {
diff --git a/openapi3/errors.go b/openapi3/errors.go
deleted file mode 100644
index 74baab9a5..000000000
--- a/openapi3/errors.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package openapi3
-
-import (
- "bytes"
- "errors"
-)
-
-// MultiError is a collection of errors, intended for when
-// multiple issues need to be reported upstream
-type MultiError []error
-
-func (me MultiError) Error() string {
- return spliceErr(" | ", me)
-}
-
-func spliceErr(sep string, errs []error) string {
- buff := &bytes.Buffer{}
- for i, e := range errs {
- buff.WriteString(e.Error())
- if i != len(errs)-1 {
- buff.WriteString(sep)
- }
- }
- return buff.String()
-}
-
-// Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()`
-// It will also return true if any of the contained errors match target
-func (me MultiError) Is(target error) bool {
- if _, ok := target.(MultiError); ok {
- return true
- }
- for _, e := range me {
- if errors.Is(e, target) {
- return true
- }
- }
- return false
-}
-
-// As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type
-func (me MultiError) As(target interface{}) bool {
- for _, e := range me {
- if errors.As(e, target) {
- return true
- }
- }
- return false
-}
-
-type multiErrorForOneOf MultiError
-
-func (meo multiErrorForOneOf) Error() string {
- return spliceErr(" Or ", meo)
-}
-
-func (meo multiErrorForOneOf) Unwrap() error {
- return MultiError(meo)
-}
diff --git a/openapi3/example.go b/openapi3/example.go
deleted file mode 100644
index 04338beee..000000000
--- a/openapi3/example.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package openapi3
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
-
- "github.com/go-openapi/jsonpointer"
-)
-
-type Examples map[string]*ExampleRef
-
-var _ jsonpointer.JSONPointable = (*Examples)(nil)
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (e Examples) JSONLookup(token string) (interface{}, error) {
- ref, ok := e[token]
- if ref == nil || !ok {
- return nil, fmt.Errorf("object has no field %q", token)
- }
-
- if ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
-// Example is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object
-type Example struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
- Description string `json:"description,omitempty" yaml:"description,omitempty"`
- Value interface{} `json:"value,omitempty" yaml:"value,omitempty"`
- ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"`
-}
-
-func NewExample(value interface{}) *Example {
- return &Example{Value: value}
-}
-
-// MarshalJSON returns the JSON encoding of Example.
-func (example Example) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 4+len(example.Extensions))
- for k, v := range example.Extensions {
- m[k] = v
- }
- if x := example.Summary; x != "" {
- m["summary"] = x
- }
- if x := example.Description; x != "" {
- m["description"] = x
- }
- if x := example.Value; x != nil {
- m["value"] = x
- }
- if x := example.ExternalValue; x != "" {
- m["externalValue"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets Example to a copy of data.
-func (example *Example) UnmarshalJSON(data []byte) error {
- type ExampleBis Example
- var x ExampleBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "summary")
- delete(x.Extensions, "description")
- delete(x.Extensions, "value")
- delete(x.Extensions, "externalValue")
- *example = Example(x)
- return nil
-}
-
-// Validate returns an error if Example does not comply with the OpenAPI spec.
-func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if example.Value != nil && example.ExternalValue != "" {
- return errors.New("value and externalValue are mutually exclusive")
- }
- if example.Value == nil && example.ExternalValue == "" {
- return errors.New("no value or externalValue field")
- }
-
- return validateExtensions(ctx, example.Extensions)
-}
diff --git a/openapi3/example_test.go b/openapi3/example_test.go
index 4e9296ac0..a5dfb3008 100644
--- a/openapi3/example_test.go
+++ b/openapi3/example_test.go
@@ -1,9 +1,10 @@
-package openapi3
+package openapi3_test
import (
"encoding/json"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
@@ -14,7 +15,7 @@ func TestExampleJSON(t *testing.T) {
require.NotEmpty(t, data)
t.Log("Unmarshal *openapi3.Example from JSON")
- docA := &Example{}
+ docA := &openapi3.Example{}
err = json.Unmarshal(exampleJSON, &docA)
require.NoError(t, err)
require.NotEmpty(t, data)
@@ -39,7 +40,7 @@ var exampleJSON = []byte(`
}
`)
-func example() *Example {
+func example() *openapi3.Example {
value := map[string]string{
"name": "Fluffy",
"petType": "Cat",
@@ -47,7 +48,7 @@ func example() *Example {
"gender": "male",
"breed": "Persian",
}
- return &Example{
+ return &openapi3.Example{
Summary: "An example of a cat",
Value: value,
}
diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go
deleted file mode 100644
index fb7a1da16..000000000
--- a/openapi3/example_validation.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package openapi3
-
-import "context"
-
-func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error {
- opts := make([]SchemaValidationOption, 0, 2)
-
- if vo := getValidationOptions(ctx); vo.examplesValidationAsReq {
- opts = append(opts, VisitAsRequest())
- } else if vo.examplesValidationAsRes {
- opts = append(opts, VisitAsResponse())
- }
- opts = append(opts, MultiErrors())
-
- return schema.VisitJSON(input, opts...)
-}
diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go
deleted file mode 100644
index de8954828..000000000
--- a/openapi3/example_validation_test.go
+++ /dev/null
@@ -1,527 +0,0 @@
-package openapi3
-
-import (
- "bytes"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestExamplesSchemaValidation(t *testing.T) {
- type testCase struct {
- name string
- requestSchemaExample string
- responseSchemaExample string
- mediaTypeRequestExample string
- mediaTypeResponseExample string
- readWriteOnlyMediaTypeRequestExample string
- readWriteOnlyMediaTypeResponseExample string
- parametersExample string
- componentExamples string
- errContains string
- }
-
- testCases := []testCase{
- {
- name: "invalid_parameter_examples",
- parametersExample: `
- examples:
- param1example:
- value: abcd
- `,
- errContains: `invalid paths: invalid path /user: invalid operation POST: param1example`,
- },
- {
- name: "valid_parameter_examples",
- parametersExample: `
- examples:
- param1example:
- value: 1
- `,
- },
- {
- name: "invalid_parameter_example",
- parametersExample: `
- example: abcd
- `,
- errContains: `invalid path /user: invalid operation POST: invalid example`,
- },
- {
- name: "valid_parameter_example",
- parametersExample: `
- example: 1
- `,
- },
- {
- name: "invalid_component_examples",
- mediaTypeRequestExample: `
- examples:
- BadUser:
- $ref: '#/components/examples/BadUser'
- `,
- componentExamples: `
- examples:
- BadUser:
- value:
- username: "]bad["
- email: bad
- password: short
- `,
- errContains: `invalid paths: invalid path /user: invalid operation POST: example BadUser`,
- },
- {
- name: "valid_component_examples",
- mediaTypeRequestExample: `
- examples:
- BadUser:
- $ref: '#/components/examples/BadUser'
- `,
- componentExamples: `
- examples:
- BadUser:
- value:
- username: good
- email: good@mail.com
- password: password
- `,
- },
- {
- name: "invalid_mediatype_examples",
- mediaTypeRequestExample: `
- example:
- username: "]bad["
- email: bad
- password: short
- `,
- errContains: `invalid path /user: invalid operation POST: invalid example`,
- },
- {
- name: "valid_mediatype_examples",
- mediaTypeRequestExample: `
- example:
- username: good
- email: good@mail.com
- password: password
- `,
- },
- {
- name: "invalid_schema_request_example",
- requestSchemaExample: `
- example:
- username: good
- email: good@email.com
- # missing password
- `,
- errContains: `schema "CreateUserRequest": invalid example`,
- },
- {
- name: "valid_schema_request_example",
- requestSchemaExample: `
- example:
- username: good
- email: good@email.com
- password: password
- `,
- },
- {
- name: "invalid_schema_response_example",
- responseSchemaExample: `
- example:
- user_id: 1
- # missing access_token
- `,
- errContains: `schema "CreateUserResponse": invalid example`,
- },
- {
- name: "valid_schema_response_example",
- responseSchemaExample: `
- example:
- user_id: 1
- access_token: "abcd"
- `,
- },
- {
- name: "valid_readonly_writeonly_examples",
- readWriteOnlyMediaTypeRequestExample: `
- examples:
- ReadWriteOnlyRequest:
- $ref: '#/components/examples/ReadWriteOnlyRequestData'
-`,
- readWriteOnlyMediaTypeResponseExample: `
- examples:
- ReadWriteOnlyResponse:
- $ref: '#/components/examples/ReadWriteOnlyResponseData'
-`,
- componentExamples: `
- examples:
- ReadWriteOnlyRequestData:
- value:
- username: user
- password: password
- ReadWriteOnlyResponseData:
- value:
- user_id: 4321
- `,
- },
- {
- name: "invalid_readonly_request_examples",
- readWriteOnlyMediaTypeRequestExample: `
- examples:
- ReadWriteOnlyRequest:
- $ref: '#/components/examples/ReadWriteOnlyRequestData'
-`,
- componentExamples: `
- examples:
- ReadWriteOnlyRequestData:
- value:
- username: user
- password: password
- user_id: 4321
-`,
- errContains: `ReadWriteOnlyRequest: readOnly property "user_id" in request`,
- },
- {
- name: "invalid_writeonly_response_examples",
- readWriteOnlyMediaTypeResponseExample: `
- examples:
- ReadWriteOnlyResponse:
- $ref: '#/components/examples/ReadWriteOnlyResponseData'
-`,
- componentExamples: `
- examples:
- ReadWriteOnlyResponseData:
- value:
- password: password
- user_id: 4321
-`,
-
- errContains: `ReadWriteOnlyResponse: writeOnly property "password" in response`,
- },
- }
-
- testOptions := []struct {
- name string
- disableExamplesValidation bool
- }{
- {
- name: "examples_validation_disabled",
- disableExamplesValidation: true,
- },
- {
- name: "examples_validation_enabled",
- disableExamplesValidation: false,
- },
- }
-
- t.Parallel()
-
- for _, testOption := range testOptions {
- testOption := testOption
- t.Run(testOption.name, func(t *testing.T) {
- t.Parallel()
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- spec := bytes.Buffer{}
- spec.WriteString(`
-openapi: 3.0.3
-info:
- title: An API
- version: 1.2.3.4
-paths:
- /user:
- post:
- description: User creation.
- operationId: createUser
- parameters:
- - name: param1
- in: 'query'
- schema:
- format: int64
- type: integer`)
- spec.WriteString(tc.parametersExample)
- spec.WriteString(`
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/CreateUserRequest"
-`)
- spec.WriteString(tc.mediaTypeRequestExample)
- spec.WriteString(`
- description: Created user object
- responses:
- '204':
- description: "success"
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/CreateUserResponse"`)
- spec.WriteString(tc.mediaTypeResponseExample)
- spec.WriteString(`
- /readWriteOnly:
- post:
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/ReadWriteOnlyData"
-`)
- spec.WriteString(tc.readWriteOnlyMediaTypeRequestExample)
- spec.WriteString(`
- responses:
- '201':
- description: a response
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/ReadWriteOnlyData"`)
- spec.WriteString(tc.readWriteOnlyMediaTypeResponseExample)
- spec.WriteString(`
-components:
- schemas:
- CreateUserRequest:`)
- spec.WriteString(tc.requestSchemaExample)
- spec.WriteString(`
- required:
- - username
- - email
- - password
- properties:
- username:
- type: string
- pattern: "^[ a-zA-Z0-9_-]+$"
- minLength: 3
- email:
- type: string
- pattern: "^[A-Za-z0-9+_.-]+@(.+)$"
- password:
- type: string
- minLength: 7
- type: object
- CreateUserResponse:`)
- spec.WriteString(tc.responseSchemaExample)
- spec.WriteString(`
- required:
- - access_token
- - user_id
- properties:
- access_token:
- type: string
- user_id:
- format: int64
- type: integer
- type: object
- ReadWriteOnlyData:
- required:
- # only required in request
- - username
- - password
- # only required in response
- - user_id
- properties:
- username:
- type: string
- default: default
- writeOnly: true # only sent in a request
- password:
- type: string
- default: default
- writeOnly: true # only sent in a request
- user_id:
- format: int64
- default: 1
- type: integer
- readOnly: true # only returned in a response
- type: object
-`)
- spec.WriteString(tc.componentExamples)
-
- loader := NewLoader()
- doc, err := loader.LoadFromData(spec.Bytes())
- require.NoError(t, err)
-
- if testOption.disableExamplesValidation {
- err = doc.Validate(loader.Context, DisableExamplesValidation())
- } else {
- err = doc.Validate(loader.Context, EnableExamplesValidation())
- }
-
- if tc.errContains != "" && !testOption.disableExamplesValidation {
- require.Error(t, err)
- require.Contains(t, err.Error(), tc.errContains)
- } else {
- require.NoError(t, err)
- }
- })
- }
- })
- }
-}
-
-func TestExampleObjectValidation(t *testing.T) {
- type testCase struct {
- name string
- mediaTypeRequestExample string
- componentExamples string
- errContains string
- }
-
- testCases := []testCase{
- {
- name: "example_examples_mutually_exclusive",
- mediaTypeRequestExample: `
- examples:
- BadUser:
- $ref: '#/components/examples/BadUser'
- example:
- username: good
- email: real@email.com
- password: validpassword
-`,
- errContains: `invalid path /user: invalid operation POST: example and examples are mutually exclusive`,
- componentExamples: `
- examples:
- BadUser:
- value:
- username: "]bad["
- email: bad
- password: short
-`,
- },
- {
- name: "example_without_value",
- componentExamples: `
- examples:
- BadUser:
- description: empty user example
-`,
- errContains: `invalid components: example "BadUser": no value or externalValue field`,
- },
- {
- name: "value_externalValue_mutual_exclusion",
- componentExamples: `
- examples:
- BadUser:
- value:
- username: good
- email: real@email.com
- password: validpassword
- externalValue: 'http://example.com/examples/example'
-`,
- errContains: `invalid components: example "BadUser": value and externalValue are mutually exclusive`,
- },
- }
-
- testOptions := []struct {
- name string
- disableExamplesValidation bool
- }{
- {
- name: "examples_validation_disabled",
- disableExamplesValidation: true,
- },
- {
- name: "examples_validation_enabled",
- disableExamplesValidation: false,
- },
- }
-
- t.Parallel()
-
- for _, testOption := range testOptions {
- testOption := testOption
- t.Run(testOption.name, func(t *testing.T) {
- t.Parallel()
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- spec := bytes.Buffer{}
- spec.WriteString(`
-openapi: 3.0.3
-info:
- title: An API
- version: 1.2.3.4
-paths:
- /user:
- post:
- description: User creation.
- operationId: createUser
- parameters:
- - name: param1
- in: 'query'
- schema:
- format: int64
- type: integer
- requestBody:
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/CreateUserRequest"
-`)
- spec.WriteString(tc.mediaTypeRequestExample)
- spec.WriteString(`
- description: Created user object
- required: true
- responses:
- '204':
- description: "success"
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/CreateUserResponse"
-components:
- schemas:
- CreateUserRequest:
- required:
- - username
- - email
- - password
- properties:
- username:
- type: string
- pattern: "^[ a-zA-Z0-9_-]+$"
- minLength: 3
- email:
- type: string
- pattern: "^[A-Za-z0-9+_.-]+@(.+)$"
- password:
- type: string
- minLength: 7
- type: object
- CreateUserResponse:
- description: represents the response to a User creation
- required:
- - access_token
- - user_id
- properties:
- access_token:
- type: string
- user_id:
- format: int64
- type: integer
- type: object
-`)
- spec.WriteString(tc.componentExamples)
-
- loader := NewLoader()
- doc, err := loader.LoadFromData(spec.Bytes())
- require.NoError(t, err)
-
- if testOption.disableExamplesValidation {
- err = doc.Validate(loader.Context, DisableExamplesValidation())
- } else {
- err = doc.Validate(loader.Context)
- }
-
- if tc.errContains != "" {
- require.Error(t, err)
- require.Contains(t, err.Error(), tc.errContains)
- } else {
- require.NoError(t, err)
- }
- })
- }
- })
- }
-}
diff --git a/openapi3/examples.go b/openapi3/examples.go
new file mode 100644
index 000000000..d89263ebc
--- /dev/null
+++ b/openapi3/examples.go
@@ -0,0 +1,29 @@
+package openapi3
+
+import (
+ "github.com/getkin/kin-openapi/jsoninfo"
+)
+
+// Example is specified by OpenAPI/Swagger 3.0 standard.
+type Example struct {
+ ExtensionProps
+
+ Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
+ Description string `json:"description,omitempty" yaml:"description,omitempty"`
+ Value interface{} `json:"value,omitempty" yaml:"value,omitempty"`
+ ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"`
+}
+
+func NewExample(value interface{}) *Example {
+ return &Example{
+ Value: value,
+ }
+}
+
+func (example *Example) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(example)
+}
+
+func (example *Example) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalStrictStruct(data, example)
+}
diff --git a/openapi3/extension.go b/openapi3/extension.go
index c29959091..f6b7ef9bb 100644
--- a/openapi3/extension.go
+++ b/openapi3/extension.go
@@ -1,24 +1,38 @@
package openapi3
import (
- "context"
- "fmt"
- "sort"
- "strings"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-func validateExtensions(ctx context.Context, extensions map[string]interface{}) error { // FIXME: newtype + Validate(...)
- var unknowns []string
- for k := range extensions {
- if !strings.HasPrefix(k, "x-") {
- unknowns = append(unknowns, k)
+// ExtensionProps provides support for OpenAPI extensions.
+// It reads/writes all properties that begin with "x-".
+type ExtensionProps struct {
+ Extensions map[string]interface{} `json:"-" yaml:"-"`
+}
+
+// Assert that the type implements the interface
+var _ jsoninfo.StrictStruct = &ExtensionProps{}
+
+// EncodeWith will be invoked by package "jsoninfo"
+func (props *ExtensionProps) EncodeWith(encoder *jsoninfo.ObjectEncoder, value interface{}) error {
+ for k, v := range props.Extensions {
+ if err := encoder.EncodeExtension(k, v); err != nil {
+ return err
}
}
+ return encoder.EncodeStructFieldsAndExtensions(value)
+}
- if len(unknowns) != 0 {
- sort.Strings(unknowns)
- return fmt.Errorf("extra sibling fields: %+v", unknowns)
+// DecodeWith will be invoked by package "jsoninfo"
+func (props *ExtensionProps) DecodeWith(decoder *jsoninfo.ObjectDecoder, value interface{}) error {
+ if err := decoder.DecodeStructFieldsAndExtensions(value); err != nil {
+ return err
}
-
+ source := decoder.DecodeExtensionMap()
+ result := make(map[string]interface{}, len(source))
+ for k, v := range source {
+ result[k] = v
+ }
+ props.Extensions = result
return nil
}
diff --git a/openapi3/extension_test.go b/openapi3/extension_test.go
new file mode 100644
index 000000000..775d8b6bc
--- /dev/null
+++ b/openapi3/extension_test.go
@@ -0,0 +1,100 @@
+package openapi3_test
+
+import (
+ "github.com/getkin/kin-openapi/jsoninfo"
+ "github.com/getkin/kin-openapi/openapi3"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestExtensionProps_EncodeWith(t *testing.T) {
+ t.Run("successfully encoded", func(t *testing.T) {
+ encoder := jsoninfo.NewObjectEncoder()
+ var extensionProps = openapi3.ExtensionProps{
+ Extensions: map[string]interface{}{
+ "field1": "value1",
+ },
+ }
+
+ var value = struct {
+ Field1 string `json:"field1"`
+ Field2 string `json:"field2"`
+ }{}
+
+ err := extensionProps.EncodeWith(encoder, &value)
+ assert.Nil(t, err)
+ })
+}
+
+func TestExtensionProps_DecodeWith(t *testing.T) {
+ data := []byte(`
+ {
+ "field1": "value1",
+ "field2": "value2"
+ }
+`)
+ t.Run("successfully decode all the fields", func(t *testing.T) {
+ decoder, err := jsoninfo.NewObjectDecoder(data)
+ assert.Nil(t, err)
+ var extensionProps = &openapi3.ExtensionProps{
+ Extensions: map[string]interface{}{
+ "field1": "value1",
+ "field2": "value1",
+ },
+ }
+
+ var value = struct {
+ Field1 string `json:"field1"`
+ Field2 string `json:"field2"`
+ }{}
+
+ err = extensionProps.DecodeWith(decoder, &value)
+ assert.Nil(t, err)
+ assert.Equal(t, 0, len(extensionProps.Extensions))
+ assert.Equal(t, "value1", value.Field1)
+ assert.Equal(t, "value2", value.Field2)
+ })
+
+ t.Run("successfully decode some of the fields", func(t *testing.T) {
+ decoder, err := jsoninfo.NewObjectDecoder(data)
+ assert.Nil(t, err)
+ var extensionProps = &openapi3.ExtensionProps{
+ Extensions: map[string]interface{}{
+ "field1": "value1",
+ "field2": "value2",
+ },
+ }
+
+ var value = &struct {
+ Field1 string `json:"field1"`
+ }{}
+
+ err = extensionProps.DecodeWith(decoder, value)
+ assert.Nil(t, err)
+ assert.Equal(t, 1, len(extensionProps.Extensions))
+ assert.Equal(t, "value1", value.Field1)
+ })
+
+ t.Run("successfully decode none of the fields", func(t *testing.T) {
+ decoder, err := jsoninfo.NewObjectDecoder(data)
+ assert.Nil(t, err)
+
+ var extensionProps = &openapi3.ExtensionProps{
+ Extensions: map[string]interface{}{
+ "field1": "value1",
+ "field2": "value2",
+ },
+ }
+
+ var value = struct {
+ Field3 string `json:"field3"`
+ Field4 string `json:"field4"`
+ }{}
+
+ err = extensionProps.DecodeWith(decoder, &value)
+ assert.Nil(t, err)
+ assert.Equal(t, 2, len(extensionProps.Extensions))
+ assert.Empty(t, value.Field3)
+ assert.Empty(t, value.Field4)
+ })
+}
diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go
index 276a36cce..5a1476bde 100644
--- a/openapi3/external_docs.go
+++ b/openapi3/external_docs.go
@@ -1,61 +1,21 @@
package openapi3
import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "net/url"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-// ExternalDocs is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object
+// ExternalDocs is specified by OpenAPI/Swagger standard version 3.0.
type ExternalDocs struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
+ ExtensionProps
- Description string `json:"description,omitempty" yaml:"description,omitempty"`
- URL string `json:"url,omitempty" yaml:"url,omitempty"`
+ Description string `json:"description,omitempty"`
+ URL string `json:"url,omitempty"`
}
-// MarshalJSON returns the JSON encoding of ExternalDocs.
-func (e ExternalDocs) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 2+len(e.Extensions))
- for k, v := range e.Extensions {
- m[k] = v
- }
- if x := e.Description; x != "" {
- m["description"] = x
- }
- if x := e.URL; x != "" {
- m["url"] = x
- }
- return json.Marshal(m)
+func (e *ExternalDocs) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(e)
}
-// UnmarshalJSON sets ExternalDocs to a copy of data.
func (e *ExternalDocs) UnmarshalJSON(data []byte) error {
- type ExternalDocsBis ExternalDocs
- var x ExternalDocsBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "description")
- delete(x.Extensions, "url")
- *e = ExternalDocs(x)
- return nil
-}
-
-// Validate returns an error if ExternalDocs does not comply with the OpenAPI spec.
-func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if e.URL == "" {
- return errors.New("url is required")
- }
- if _, err := url.Parse(e.URL); err != nil {
- return fmt.Errorf("url is incorrect: %w", err)
- }
-
- return validateExtensions(ctx, e.Extensions)
+ return jsoninfo.UnmarshalStrictStruct(data, e)
}
diff --git a/openapi3/external_docs_test.go b/openapi3/external_docs_test.go
deleted file mode 100644
index f2fb64f2e..000000000
--- a/openapi3/external_docs_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package openapi3
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestExternalDocs_Validate(t *testing.T) {
- tests := []struct {
- name string
- extDocs *ExternalDocs
- expectedErr string
- }{
- {
- name: "url is missing",
- extDocs: &ExternalDocs{},
- expectedErr: "url is required",
- },
- {
- name: "url is incorrect",
- extDocs: &ExternalDocs{URL: "ht tps://example.com"},
- expectedErr: `url is incorrect: parse "ht tps://example.com": first path segment in URL cannot contain colon`,
- },
- {
- name: "ok",
- extDocs: &ExternalDocs{URL: "https://example.com"},
- },
- }
- for i := range tests {
- tt := tests[i]
- t.Run(tt.name, func(t *testing.T) {
- err := tt.extDocs.Validate(context.Background())
- if tt.expectedErr != "" {
- require.EqualError(t, err, tt.expectedErr)
- } else {
- require.NoError(t, err)
- }
- })
- }
-}
diff --git a/openapi3/header.go b/openapi3/header.go
index 8bce69f2e..310ef9f92 100644
--- a/openapi3/header.go
+++ b/openapi3/header.go
@@ -2,101 +2,31 @@ package openapi3
import (
"context"
- "errors"
- "fmt"
- "github.com/go-openapi/jsonpointer"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-type Headers map[string]*HeaderRef
-
-var _ jsonpointer.JSONPointable = (*Headers)(nil)
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (h Headers) JSONLookup(token string) (interface{}, error) {
- ref, ok := h[token]
- if ref == nil || !ok {
- return nil, fmt.Errorf("object has no field %q", token)
- }
-
- if ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
-// Header is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#header-object
type Header struct {
- Parameter
+ ExtensionProps
+
+ // Optional description. Should use CommonMark syntax.
+ Description string `json:"description,omitempty" yaml:"description,omitempty"`
+ Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
+ Required bool `json:"required,omitempty" yaml:"required,omitempty"`
+ Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
+ Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
+ Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"`
+ Content Content `json:"content,omitempty" yaml:"content,omitempty"`
}
-var _ jsonpointer.JSONPointable = (*Header)(nil)
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (header Header) JSONLookup(token string) (interface{}, error) {
- return header.Parameter.JSONLookup(token)
-}
-
-// MarshalJSON returns the JSON encoding of Header.
-func (header Header) MarshalJSON() ([]byte, error) {
- return header.Parameter.MarshalJSON()
+func (value *Header) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalStrictStruct(data, value)
}
-// UnmarshalJSON sets Header to a copy of data.
-func (header *Header) UnmarshalJSON(data []byte) error {
- return header.Parameter.UnmarshalJSON(data)
-}
-
-// SerializationMethod returns a header's serialization method.
-func (header *Header) SerializationMethod() (*SerializationMethod, error) {
- style := header.Style
- if style == "" {
- style = SerializationSimple
- }
- explode := false
- if header.Explode != nil {
- explode = *header.Explode
- }
- return &SerializationMethod{Style: style, Explode: explode}, nil
-}
-
-// Validate returns an error if Header does not comply with the OpenAPI spec.
-func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if header.Name != "" {
- return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map")
- }
- if header.In != "" {
- return errors.New("header 'in' MUST NOT be specified, it is implicitly in header")
- }
-
- // Validate a parameter's serialization method.
- sm, err := header.SerializationMethod()
- if err != nil {
- return err
- }
- if smSupported := false ||
- sm.Style == SerializationSimple && !sm.Explode ||
- sm.Style == SerializationSimple && sm.Explode; !smSupported {
- e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a header parameter", sm.Style, sm.Explode)
- return fmt.Errorf("header schema is invalid: %w", e)
- }
-
- if (header.Schema == nil) == (header.Content == nil) {
- e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", header)
- return fmt.Errorf("header schema is invalid: %w", e)
- }
- if schema := header.Schema; schema != nil {
- if err := schema.Validate(ctx); err != nil {
- return fmt.Errorf("header schema is invalid: %w", err)
- }
- }
-
- if content := header.Content; content != nil {
- if err := content.Validate(ctx); err != nil {
- return fmt.Errorf("header content is invalid: %w", err)
+func (value *Header) Validate(c context.Context) error {
+ if v := value.Schema; v != nil {
+ if err := v.Validate(c); err != nil {
+ return err
}
}
return nil
diff --git a/openapi3/info.go b/openapi3/info.go
index 381047fca..59e03cc13 100644
--- a/openapi3/info.go
+++ b/openapi3/info.go
@@ -2,15 +2,15 @@ package openapi3
import (
"context"
- "encoding/json"
"errors"
+ "fmt"
+
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-// Info is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#info-object
+// Info is specified by OpenAPI/Swagger standard version 3.0.
type Info struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
+ ExtensionProps
Title string `json:"title" yaml:"title"` // Required
Description string `json:"description,omitempty" yaml:"description,omitempty"`
TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"`
@@ -19,167 +19,76 @@ type Info struct {
Version string `json:"version" yaml:"version"` // Required
}
-// MarshalJSON returns the JSON encoding of Info.
-func (info Info) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 6+len(info.Extensions))
- for k, v := range info.Extensions {
- m[k] = v
- }
- m["title"] = info.Title
- if x := info.Description; x != "" {
- m["description"] = x
- }
- if x := info.TermsOfService; x != "" {
- m["termsOfService"] = x
- }
- if x := info.Contact; x != nil {
- m["contact"] = x
- }
- if x := info.License; x != nil {
- m["license"] = x
- }
- m["version"] = info.Version
- return json.Marshal(m)
+func (value *Info) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(value)
}
-// UnmarshalJSON sets Info to a copy of data.
-func (info *Info) UnmarshalJSON(data []byte) error {
- type InfoBis Info
- var x InfoBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "title")
- delete(x.Extensions, "description")
- delete(x.Extensions, "termsOfService")
- delete(x.Extensions, "contact")
- delete(x.Extensions, "license")
- delete(x.Extensions, "version")
- *info = Info(x)
- return nil
+func (value *Info) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalStrictStruct(data, value)
}
-// Validate returns an error if Info does not comply with the OpenAPI spec.
-func (info *Info) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if contact := info.Contact; contact != nil {
- if err := contact.Validate(ctx); err != nil {
- return err
+func (value *Info) Validate(c context.Context) error {
+ if contact := value.Contact; contact != nil {
+ if err := contact.Validate(c); err != nil {
+ return fmt.Errorf("Error when validating Contact: %s", err.Error())
}
}
- if license := info.License; license != nil {
- if err := license.Validate(ctx); err != nil {
- return err
+ if license := value.License; license != nil {
+ if err := license.Validate(c); err != nil {
+ return fmt.Errorf("Error when validating License: %s", err.Error())
}
}
- if info.Version == "" {
- return errors.New("value of version must be a non-empty string")
+ if value.Version == "" {
+ return errors.New("Variable 'version' must be a non-empty JSON string")
}
- if info.Title == "" {
- return errors.New("value of title must be a non-empty string")
+ if value.Title == "" {
+ return errors.New("Variable 'title' must be a non-empty JSON string")
}
- return validateExtensions(ctx, info.Extensions)
+ return nil
}
-// Contact is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contact-object
+// Contact is specified by OpenAPI/Swagger standard version 3.0.
type Contact struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
+ ExtensionProps
Name string `json:"name,omitempty" yaml:"name,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
Email string `json:"email,omitempty" yaml:"email,omitempty"`
}
-// MarshalJSON returns the JSON encoding of Contact.
-func (contact Contact) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 3+len(contact.Extensions))
- for k, v := range contact.Extensions {
- m[k] = v
- }
- if x := contact.Name; x != "" {
- m["name"] = x
- }
- if x := contact.URL; x != "" {
- m["url"] = x
- }
- if x := contact.Email; x != "" {
- m["email"] = x
- }
- return json.Marshal(m)
+func (value *Contact) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(value)
}
-// UnmarshalJSON sets Contact to a copy of data.
-func (contact *Contact) UnmarshalJSON(data []byte) error {
- type ContactBis Contact
- var x ContactBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "name")
- delete(x.Extensions, "url")
- delete(x.Extensions, "email")
- *contact = Contact(x)
- return nil
+func (value *Contact) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalStrictStruct(data, value)
}
-// Validate returns an error if Contact does not comply with the OpenAPI spec.
-func (contact *Contact) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- return validateExtensions(ctx, contact.Extensions)
+func (value *Contact) Validate(c context.Context) error {
+ return nil
}
-// License is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#license-object
+// License is specified by OpenAPI/Swagger standard version 3.0.
type License struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
+ ExtensionProps
Name string `json:"name" yaml:"name"` // Required
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}
-// MarshalJSON returns the JSON encoding of License.
-func (license License) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 2+len(license.Extensions))
- for k, v := range license.Extensions {
- m[k] = v
- }
- m["name"] = license.Name
- if x := license.URL; x != "" {
- m["url"] = x
- }
- return json.Marshal(m)
+func (value *License) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(value)
}
-// UnmarshalJSON sets License to a copy of data.
-func (license *License) UnmarshalJSON(data []byte) error {
- type LicenseBis License
- var x LicenseBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "name")
- delete(x.Extensions, "url")
- *license = License(x)
- return nil
+func (value *License) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalStrictStruct(data, value)
}
-// Validate returns an error if License does not comply with the OpenAPI spec.
-func (license *License) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if license.Name == "" {
- return errors.New("value of license name must be a non-empty string")
+func (value *License) Validate(c context.Context) error {
+ if value.Name == "" {
+ return errors.New("Variable 'name' must be a non-empty JSON string")
}
-
- return validateExtensions(ctx, license.Extensions)
+ return nil
}
diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go
deleted file mode 100644
index b8506535e..000000000
--- a/openapi3/internalize_refs.go
+++ /dev/null
@@ -1,434 +0,0 @@
-package openapi3
-
-import (
- "context"
- "path/filepath"
- "strings"
-)
-
-type RefNameResolver func(string) string
-
-// DefaultRefResolver is a default implementation of refNameResolver for the
-// InternalizeRefs function.
-//
-// If a reference points to an element inside a document, it returns the last
-// element in the reference using filepath.Base. Otherwise if the reference points
-// to a file, it returns the file name trimmed of all extensions.
-func DefaultRefNameResolver(ref string) string {
- if ref == "" {
- return ""
- }
- split := strings.SplitN(ref, "#", 2)
- if len(split) == 2 {
- return filepath.Base(split[1])
- }
- ref = split[0]
- for ext := filepath.Ext(ref); len(ext) > 0; ext = filepath.Ext(ref) {
- ref = strings.TrimSuffix(ref, ext)
- }
- return filepath.Base(ref)
-}
-
-func schemaNames(s Schemas) []string {
- out := make([]string, 0, len(s))
- for i := range s {
- out = append(out, i)
- }
- return out
-}
-
-func parametersMapNames(s ParametersMap) []string {
- out := make([]string, 0, len(s))
- for i := range s {
- out = append(out, i)
- }
- return out
-}
-
-func isExternalRef(ref string, parentIsExternal bool) bool {
- return ref != "" && (!strings.HasPrefix(ref, "#/components/") || parentIsExternal)
-}
-
-func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
- if s == nil || !isExternalRef(s.Ref, parentIsExternal) {
- return false
- }
-
- name := refNameResolver(s.Ref)
- if doc.Components != nil {
- if _, ok := doc.Components.Schemas[name]; ok {
- s.Ref = "#/components/schemas/" + name
- return true
- }
- }
-
- if doc.Components == nil {
- doc.Components = &Components{}
- }
- if doc.Components.Schemas == nil {
- doc.Components.Schemas = make(Schemas)
- }
- doc.Components.Schemas[name] = s.Value.NewRef()
- s.Ref = "#/components/schemas/" + name
- return true
-}
-
-func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
- if p == nil || !isExternalRef(p.Ref, parentIsExternal) {
- return false
- }
- name := refNameResolver(p.Ref)
- if doc.Components != nil {
- if _, ok := doc.Components.Parameters[name]; ok {
- p.Ref = "#/components/parameters/" + name
- return true
- }
- }
-
- if doc.Components == nil {
- doc.Components = &Components{}
- }
- if doc.Components.Parameters == nil {
- doc.Components.Parameters = make(ParametersMap)
- }
- doc.Components.Parameters[name] = &ParameterRef{Value: p.Value}
- p.Ref = "#/components/parameters/" + name
- return true
-}
-
-func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
- if h == nil || !isExternalRef(h.Ref, parentIsExternal) {
- return false
- }
- name := refNameResolver(h.Ref)
- if doc.Components != nil {
- if _, ok := doc.Components.Headers[name]; ok {
- h.Ref = "#/components/headers/" + name
- return true
- }
- }
-
- if doc.Components == nil {
- doc.Components = &Components{}
- }
- if doc.Components.Headers == nil {
- doc.Components.Headers = make(Headers)
- }
- doc.Components.Headers[name] = &HeaderRef{Value: h.Value}
- h.Ref = "#/components/headers/" + name
- return true
-}
-
-func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
- if r == nil || !isExternalRef(r.Ref, parentIsExternal) {
- return false
- }
- name := refNameResolver(r.Ref)
- if doc.Components != nil {
- if _, ok := doc.Components.RequestBodies[name]; ok {
- r.Ref = "#/components/requestBodies/" + name
- return true
- }
- }
-
- if doc.Components == nil {
- doc.Components = &Components{}
- }
- if doc.Components.RequestBodies == nil {
- doc.Components.RequestBodies = make(RequestBodies)
- }
- doc.Components.RequestBodies[name] = &RequestBodyRef{Value: r.Value}
- r.Ref = "#/components/requestBodies/" + name
- return true
-}
-
-func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
- if r == nil || !isExternalRef(r.Ref, parentIsExternal) {
- return false
- }
- name := refNameResolver(r.Ref)
- if doc.Components != nil {
- if _, ok := doc.Components.Responses[name]; ok {
- r.Ref = "#/components/responses/" + name
- return true
- }
- }
-
- if doc.Components == nil {
- doc.Components = &Components{}
- }
- if doc.Components.Responses == nil {
- doc.Components.Responses = make(Responses)
- }
- doc.Components.Responses[name] = &ResponseRef{Value: r.Value}
- r.Ref = "#/components/responses/" + name
- return true
-}
-
-func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver RefNameResolver, parentIsExternal bool) {
- if ss == nil || !isExternalRef(ss.Ref, parentIsExternal) {
- return
- }
- name := refNameResolver(ss.Ref)
- if doc.Components != nil {
- if _, ok := doc.Components.SecuritySchemes[name]; ok {
- ss.Ref = "#/components/securitySchemes/" + name
- return
- }
- }
-
- if doc.Components == nil {
- doc.Components = &Components{}
- }
- if doc.Components.SecuritySchemes == nil {
- doc.Components.SecuritySchemes = make(SecuritySchemes)
- }
- doc.Components.SecuritySchemes[name] = &SecuritySchemeRef{Value: ss.Value}
- ss.Ref = "#/components/securitySchemes/" + name
-
-}
-
-func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver, parentIsExternal bool) {
- if e == nil || !isExternalRef(e.Ref, parentIsExternal) {
- return
- }
- name := refNameResolver(e.Ref)
- if doc.Components != nil {
- if _, ok := doc.Components.Examples[name]; ok {
- e.Ref = "#/components/examples/" + name
- return
- }
- }
-
- if doc.Components == nil {
- doc.Components = &Components{}
- }
- if doc.Components.Examples == nil {
- doc.Components.Examples = make(Examples)
- }
- doc.Components.Examples[name] = &ExampleRef{Value: e.Value}
- e.Ref = "#/components/examples/" + name
-
-}
-
-func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver, parentIsExternal bool) {
- if l == nil || !isExternalRef(l.Ref, parentIsExternal) {
- return
- }
- name := refNameResolver(l.Ref)
- if doc.Components != nil {
- if _, ok := doc.Components.Links[name]; ok {
- l.Ref = "#/components/links/" + name
- return
- }
- }
-
- if doc.Components == nil {
- doc.Components = &Components{}
- }
- if doc.Components.Links == nil {
- doc.Components.Links = make(Links)
- }
- doc.Components.Links[name] = &LinkRef{Value: l.Value}
- l.Ref = "#/components/links/" + name
-
-}
-
-func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
- if c == nil || !isExternalRef(c.Ref, parentIsExternal) {
- return false
- }
- name := refNameResolver(c.Ref)
-
- if doc.Components == nil {
- doc.Components = &Components{}
- }
- if doc.Components.Callbacks == nil {
- doc.Components.Callbacks = make(Callbacks)
- }
- c.Ref = "#/components/callbacks/" + name
- doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value}
- return true
-}
-
-func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsExternal bool) {
- if s == nil || doc.isVisitedSchema(s) {
- return
- }
-
- for _, list := range []SchemaRefs{s.AllOf, s.AnyOf, s.OneOf} {
- for _, s2 := range list {
- isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal)
- if s2 != nil {
- doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal)
- }
- }
- }
- for _, s2 := range s.Properties {
- isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal)
- if s2 != nil {
- doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal)
- }
- }
- for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties.Schema, s.Items} {
- isExternal := doc.addSchemaToSpec(ref, refNameResolver, parentIsExternal)
- if ref != nil {
- doc.derefSchema(ref.Value, refNameResolver, isExternal || parentIsExternal)
- }
- }
-}
-
-func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver, parentIsExternal bool) {
- for _, h := range hs {
- isExternal := doc.addHeaderToSpec(h, refNameResolver, parentIsExternal)
- if doc.isVisitedHeader(h.Value) {
- continue
- }
- doc.derefParameter(h.Value.Parameter, refNameResolver, parentIsExternal || isExternal)
- }
-}
-
-func (doc *T) derefExamples(es Examples, refNameResolver RefNameResolver, parentIsExternal bool) {
- for _, e := range es {
- doc.addExampleToSpec(e, refNameResolver, parentIsExternal)
- }
-}
-
-func (doc *T) derefContent(c Content, refNameResolver RefNameResolver, parentIsExternal bool) {
- for _, mediatype := range c {
- isExternal := doc.addSchemaToSpec(mediatype.Schema, refNameResolver, parentIsExternal)
- if mediatype.Schema != nil {
- doc.derefSchema(mediatype.Schema.Value, refNameResolver, isExternal || parentIsExternal)
- }
- doc.derefExamples(mediatype.Examples, refNameResolver, parentIsExternal)
- for _, e := range mediatype.Encoding {
- doc.derefHeaders(e.Headers, refNameResolver, parentIsExternal)
- }
- }
-}
-
-func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver, parentIsExternal bool) {
- for _, l := range ls {
- doc.addLinkToSpec(l, refNameResolver, parentIsExternal)
- }
-}
-
-func (doc *T) derefResponses(es Responses, refNameResolver RefNameResolver, parentIsExternal bool) {
- for _, e := range es {
- isExternal := doc.addResponseToSpec(e, refNameResolver, parentIsExternal)
- if e.Value != nil {
- doc.derefHeaders(e.Value.Headers, refNameResolver, isExternal || parentIsExternal)
- doc.derefContent(e.Value.Content, refNameResolver, isExternal || parentIsExternal)
- doc.derefLinks(e.Value.Links, refNameResolver, isExternal || parentIsExternal)
- }
- }
-}
-
-func (doc *T) derefParameter(p Parameter, refNameResolver RefNameResolver, parentIsExternal bool) {
- isExternal := doc.addSchemaToSpec(p.Schema, refNameResolver, parentIsExternal)
- doc.derefContent(p.Content, refNameResolver, parentIsExternal)
- if p.Schema != nil {
- doc.derefSchema(p.Schema.Value, refNameResolver, isExternal || parentIsExternal)
- }
-}
-
-func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver, parentIsExternal bool) {
- doc.derefContent(r.Content, refNameResolver, parentIsExternal)
-}
-
-func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver, parentIsExternal bool) {
- for _, ops := range paths {
- if isExternalRef(ops.Ref, parentIsExternal) {
- parentIsExternal = true
- }
- // inline full operations
- ops.Ref = ""
-
- for _, param := range ops.Parameters {
- doc.addParameterToSpec(param, refNameResolver, parentIsExternal)
- }
-
- for _, op := range ops.Operations() {
- isExternal := doc.addRequestBodyToSpec(op.RequestBody, refNameResolver, parentIsExternal)
- if op.RequestBody != nil && op.RequestBody.Value != nil {
- doc.derefRequestBody(*op.RequestBody.Value, refNameResolver, parentIsExternal || isExternal)
- }
- for _, cb := range op.Callbacks {
- isExternal := doc.addCallbackToSpec(cb, refNameResolver, parentIsExternal)
- if cb.Value != nil {
- doc.derefPaths(*cb.Value, refNameResolver, parentIsExternal || isExternal)
- }
- }
- doc.derefResponses(op.Responses, refNameResolver, parentIsExternal)
- for _, param := range op.Parameters {
- isExternal := doc.addParameterToSpec(param, refNameResolver, parentIsExternal)
- if param.Value != nil {
- doc.derefParameter(*param.Value, refNameResolver, parentIsExternal || isExternal)
- }
- }
- }
- }
-}
-
-// InternalizeRefs removes all references to external files from the spec and moves them
-// to the components section.
-//
-// refNameResolver takes in references to returns a name to store the reference under locally.
-// It MUST return a unique name for each reference type.
-// A default implementation is provided that will suffice for most use cases. See the function
-// documention for more details.
-//
-// Example:
-//
-// doc.InternalizeRefs(context.Background(), nil)
-func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) {
- doc.resetVisited()
-
- if refNameResolver == nil {
- refNameResolver = DefaultRefNameResolver
- }
-
- if components := doc.Components; components != nil {
- names := schemaNames(components.Schemas)
- for _, name := range names {
- schema := components.Schemas[name]
- isExternal := doc.addSchemaToSpec(schema, refNameResolver, false)
- if schema != nil {
- schema.Ref = "" // always dereference the top level
- doc.derefSchema(schema.Value, refNameResolver, isExternal)
- }
- }
- names = parametersMapNames(components.Parameters)
- for _, name := range names {
- p := components.Parameters[name]
- isExternal := doc.addParameterToSpec(p, refNameResolver, false)
- if p != nil && p.Value != nil {
- p.Ref = "" // always dereference the top level
- doc.derefParameter(*p.Value, refNameResolver, isExternal)
- }
- }
- doc.derefHeaders(components.Headers, refNameResolver, false)
- for _, req := range components.RequestBodies {
- isExternal := doc.addRequestBodyToSpec(req, refNameResolver, false)
- if req != nil && req.Value != nil {
- req.Ref = "" // always dereference the top level
- doc.derefRequestBody(*req.Value, refNameResolver, isExternal)
- }
- }
- doc.derefResponses(components.Responses, refNameResolver, false)
- for _, ss := range components.SecuritySchemes {
- doc.addSecuritySchemeToSpec(ss, refNameResolver, false)
- }
- doc.derefExamples(components.Examples, refNameResolver, false)
- doc.derefLinks(components.Links, refNameResolver, false)
- for _, cb := range components.Callbacks {
- isExternal := doc.addCallbackToSpec(cb, refNameResolver, false)
- if cb != nil && cb.Value != nil {
- cb.Ref = "" // always dereference the top level
- doc.derefPaths(*cb.Value, refNameResolver, isExternal)
- }
- }
- }
-
- doc.derefPaths(doc.Paths, refNameResolver, false)
-}
diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go
deleted file mode 100644
index fe6b29d90..000000000
--- a/openapi3/internalize_refs_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package openapi3
-
-import (
- "context"
- "os"
- "regexp"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestInternalizeRefs(t *testing.T) {
- ctx := context.Background()
-
- regexpRef := regexp.MustCompile(`"\$ref":`)
- regexpRefInternal := regexp.MustCompile(`"\$ref":"#`)
-
- tests := []struct {
- filename string
- }{
- {"testdata/testref.openapi.yml"},
- {"testdata/recursiveRef/openapi.yml"},
- {"testdata/spec.yaml"},
- {"testdata/callbacks.yml"},
- }
-
- for _, test := range tests {
- t.Run(test.filename, func(t *testing.T) {
- // Load in the reference spec from the testdata
- sl := NewLoader()
- sl.IsExternalRefsAllowed = true
- doc, err := sl.LoadFromFile(test.filename)
- require.NoError(t, err, "loading test file")
- err = doc.Validate(ctx)
- require.NoError(t, err, "validating spec")
-
- // Internalize the references
- doc.InternalizeRefs(ctx, nil)
-
- // Validate the internalized spec
- err = doc.Validate(ctx)
- require.NoError(t, err, "validating internalized spec")
-
- actual, err := doc.MarshalJSON()
- require.NoError(t, err, "marshalling internalized spec")
-
- // run a static check over the file, making sure each occurence of a
- // reference is followed by a #
- numRefs := len(regexpRef.FindAll(actual, -1))
- numInternalRefs := len(regexpRefInternal.FindAll(actual, -1))
- require.Equal(t, numRefs, numInternalRefs, "checking all references are internal")
-
- // load from actual, but with the path set to the current directory
- doc2, err := sl.LoadFromData(actual)
- require.NoError(t, err, "reloading spec")
- err = doc2.Validate(ctx)
- require.NoError(t, err, "validating reloaded spec")
-
- // compare with expected
- expected, err := os.ReadFile(test.filename + ".internalized.yml")
- require.NoError(t, err)
- require.JSONEq(t, string(expected), string(actual))
- })
- }
-}
diff --git a/openapi3/issue136_test.go b/openapi3/issue136_test.go
deleted file mode 100644
index 3aa7edd8f..000000000
--- a/openapi3/issue136_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue136(t *testing.T) {
- specf := func(dflt string) string {
- return `
-openapi: 3.0.2
-info:
- title: "Hello World REST APIs"
- version: "1.0"
-paths: {}
-components:
- schemas:
- SomeSchema:
- type: string
- default: ` + dflt + `
-`
- }
-
- for _, testcase := range []struct {
- dflt, err string
- }{
- {
- dflt: `"foo"`,
- err: "",
- },
- {
- dflt: `1`,
- err: "invalid components: invalid schema default: value must be a string",
- },
- } {
- t.Run(testcase.dflt, func(t *testing.T) {
- spec := specf(testcase.dflt)
-
- sl := NewLoader()
-
- doc, err := sl.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(sl.Context)
- if testcase.err == "" {
- require.NoError(t, err)
- } else {
- require.Error(t, err, testcase.err)
- }
- })
- }
-}
diff --git a/openapi3/issue241_test.go b/openapi3/issue241_test.go
deleted file mode 100644
index 14caa9b26..000000000
--- a/openapi3/issue241_test.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package openapi3_test
-
-import (
- "bytes"
- "io/ioutil"
- "testing"
-
- "github.com/stretchr/testify/require"
- "gopkg.in/yaml.v3"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestIssue241(t *testing.T) {
- data, err := ioutil.ReadFile("testdata/issue241.yml")
- require.NoError(t, err)
-
- loader := openapi3.NewLoader()
- loader.IsExternalRefsAllowed = true
- spec, err := loader.LoadFromData(data)
- require.NoError(t, err)
-
- var buf bytes.Buffer
- enc := yaml.NewEncoder(&buf)
- enc.SetIndent(2)
- err = enc.Encode(spec)
- require.NoError(t, err)
- require.Equal(t, string(data), buf.String())
-}
diff --git a/openapi3/issue301_test.go b/openapi3/issue301_test.go
deleted file mode 100644
index a0225fdb8..000000000
--- a/openapi3/issue301_test.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue301(t *testing.T) {
- sl := NewLoader()
- sl.IsExternalRefsAllowed = true
-
- doc, err := sl.LoadFromFile("testdata/callbacks.yml")
- require.NoError(t, err)
-
- err = doc.Validate(sl.Context)
- require.NoError(t, err)
-
- transCallbacks := doc.Paths["/trans"].Post.Callbacks["transactionCallback"].Value
- require.Equal(t, "object", (*transCallbacks)["http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}"].Post.RequestBody.
- Value.Content["application/json"].Schema.
- Value.Type)
-
- otherCallbacks := doc.Paths["/other"].Post.Callbacks["myEvent"].Value
- require.Equal(t, "boolean", (*otherCallbacks)["{$request.query.queryUrl}"].Post.RequestBody.
- Value.Content["application/json"].Schema.
- Value.Type)
-}
diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go
deleted file mode 100644
index ba9bed76b..000000000
--- a/openapi3/issue341_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package openapi3
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue341(t *testing.T) {
- sl := NewLoader()
- sl.IsExternalRefsAllowed = true
- doc, err := sl.LoadFromFile("testdata/main.yaml")
- require.NoError(t, err)
-
- err = doc.Validate(sl.Context)
- require.NoError(t, err)
-
- err = sl.ResolveRefsIn(doc, nil)
- require.NoError(t, err)
-
- bs, err := doc.MarshalJSON()
- require.NoError(t, err)
- require.JSONEq(t, `{"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"$ref":"testpath.yaml#/paths/~1testpath"}}}`, string(bs))
-
- require.Equal(t, "string", doc.Paths["/testpath"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type)
-
- doc.InternalizeRefs(context.Background(), nil)
- bs, err = doc.MarshalJSON()
- require.NoError(t, err)
- require.JSONEq(t, `{
- "components": {
- "responses": {
- "testpath_200_response": {
- "content": {
- "application/json": {
- "schema": {
- "type": "string"
- }
- }
- },
- "description": "a custom response"
- }
- }
- },
- "info": {
- "title": "test file",
- "version": "n/a"
- },
- "openapi": "3.0.0",
- "paths": {
- "/testpath": {
- "get": {
- "responses": {
- "200": {
- "$ref": "#/components/responses/testpath_200_response"
- }
- }
- }
- }
- }
- }`, string(bs))
-}
diff --git a/openapi3/issue344_test.go b/openapi3/issue344_test.go
deleted file mode 100644
index 44ba2b7f5..000000000
--- a/openapi3/issue344_test.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue344(t *testing.T) {
- sl := NewLoader()
- sl.IsExternalRefsAllowed = true
-
- doc, err := sl.LoadFromFile("testdata/spec.yaml")
- require.NoError(t, err)
-
- err = doc.Validate(sl.Context)
- require.NoError(t, err)
-
- require.Equal(t, "string", doc.Components.Schemas["Test"].Value.Properties["test"].Value.Properties["name"].Value.Type)
-}
diff --git a/openapi3/issue376_test.go b/openapi3/issue376_test.go
deleted file mode 100644
index 825f1d1ac..000000000
--- a/openapi3/issue376_test.go
+++ /dev/null
@@ -1,165 +0,0 @@
-package openapi3
-
-import (
- "context"
- "fmt"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue376(t *testing.T) {
- spec := []byte(`
-openapi: 3.0.0
-components:
- schemas:
- schema1:
- type: object
- additionalProperties:
- type: string
- schema2:
- type: object
- properties:
- prop:
- $ref: '#/components/schemas/schema1/additionalProperties'
-paths: {}
-info:
- title: An API
- version: 1.2.3.4
-`)
-
- loader := NewLoader()
-
- doc, err := loader.LoadFromData(spec)
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- require.Equal(t, "An API", doc.Info.Title)
- require.Equal(t, 2, len(doc.Components.Schemas))
- require.Equal(t, 0, len(doc.Paths))
-
- require.Equal(t, "string", doc.Components.Schemas["schema2"].Value.Properties["prop"].Value.Type)
-}
-
-func TestExclusiveValuesOfValuesAdditionalProperties(t *testing.T) {
- schema := &Schema{
- AdditionalProperties: AdditionalProperties{
- Has: BoolPtr(false),
- Schema: NewSchemaRef("", &Schema{}),
- },
- }
- err := schema.Validate(context.Background())
- require.ErrorContains(t, err, ` to both `)
-
- schema = &Schema{
- AdditionalProperties: AdditionalProperties{
- Has: BoolPtr(false),
- },
- }
- err = schema.Validate(context.Background())
- require.NoError(t, err)
-
- schema = &Schema{
- AdditionalProperties: AdditionalProperties{
- Schema: NewSchemaRef("", &Schema{}),
- },
- }
- err = schema.Validate(context.Background())
- require.NoError(t, err)
-}
-
-func TestMultijsonTagSerialization(t *testing.T) {
- specYAML := []byte(`
-openapi: 3.0.0
-components:
- schemas:
- unset:
- type: number
- empty-object:
- additionalProperties: {}
- object:
- additionalProperties: {type: string}
- boolean:
- additionalProperties: false
-paths: {}
-info:
- title: An API
- version: 1.2.3.4
-`)
-
- specJSON := []byte(`{
- "openapi": "3.0.0",
- "components": {
- "schemas": {
- "unset": {
- "type": "number"
- },
- "empty-object": {
- "additionalProperties": {
- }
- },
- "object": {
- "additionalProperties": {
- "type": "string"
- }
- },
- "boolean": {
- "additionalProperties": false
- }
- }
- },
- "paths": {
- },
- "info": {
- "title": "An API",
- "version": "1.2.3.4"
- }
-}`)
-
- for i, spec := range [][]byte{specJSON, specYAML} {
- t.Run(fmt.Sprintf("spec%02d", i), func(t *testing.T) {
- loader := NewLoader()
-
- doc, err := loader.LoadFromData(spec)
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- for propName, propSchema := range doc.Components.Schemas {
- t.Run(propName, func(t *testing.T) {
- ap := propSchema.Value.AdditionalProperties.Schema
- apa := propSchema.Value.AdditionalProperties.Has
-
- apStr := ""
- if ap != nil {
- apStr = fmt.Sprintf("{Ref:%s Value.Type:%v}", (*ap).Ref, (*ap).Value.Type)
- }
- apaStr := ""
- if apa != nil {
- apaStr = fmt.Sprintf("%v", *apa)
- }
-
- encoded, err := propSchema.MarshalJSON()
- require.NoError(t, err)
- require.Equal(t, map[string]string{
- "unset": `{"type":"number"}`,
- "empty-object": `{"additionalProperties":{}}`,
- "object": `{"additionalProperties":{"type":"string"}}`,
- "boolean": `{"additionalProperties":false}`,
- }[propName], string(encoded))
-
- if propName == "unset" {
- require.True(t, ap == nil && apa == nil)
- return
- }
-
- require.Truef(t, (ap != nil && apa == nil) || (ap == nil && apa != nil),
- "%s: isnil(%s) xor isnil(%s)", propName, apaStr, apStr)
- })
- }
- })
- }
-}
diff --git a/openapi3/issue382_test.go b/openapi3/issue382_test.go
deleted file mode 100644
index c29b7e981..000000000
--- a/openapi3/issue382_test.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestOverridingGlobalParametersValidation(t *testing.T) {
- loader := NewLoader()
- doc, err := loader.LoadFromFile("testdata/Test_param_override.yml")
- require.NoError(t, err)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-}
diff --git a/openapi3/issue513_test.go b/openapi3/issue513_test.go
deleted file mode 100644
index 332b9226e..000000000
--- a/openapi3/issue513_test.go
+++ /dev/null
@@ -1,173 +0,0 @@
-package openapi3
-
-import (
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue513OKWithExtension(t *testing.T) {
- spec := `
-openapi: "3.0.3"
-info:
- title: 'My app'
- version: 1.0.0
- description: 'An API'
-
-paths:
- /v1/operation:
- delete:
- summary: Delete something
- responses:
- 200:
- description: Success
- default:
- description: '* **400** - Bad Request'
- x-my-extension: {val: ue}
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
-components:
- schemas:
- Error:
- type: object
- description: An error response body.
- properties:
- message:
- description: A detailed message describing the error.
- type: string
-`[1:]
- sl := NewLoader()
- doc, err := sl.LoadFromData([]byte(spec))
- require.NoError(t, err)
- err = doc.Validate(sl.Context)
- require.NoError(t, err)
- data, err := json.Marshal(doc)
- require.NoError(t, err)
- require.Contains(t, string(data), `x-my-extension`)
-}
-
-func TestIssue513KOHasExtraFieldSchema(t *testing.T) {
- spec := `
-openapi: "3.0.3"
-info:
- title: 'My app'
- version: 1.0.0
- description: 'An API'
-
-paths:
- /v1/operation:
- delete:
- summary: Delete something
- responses:
- 200:
- description: Success
- default:
- description: '* **400** - Bad Request'
- x-my-extension: {val: ue}
- # Notice here schema is invalid. It should instead be:
- # content:
- # application/json:
- # schema:
- # $ref: '#/components/schemas/Error'
- schema:
- $ref: '#/components/schemas/Error'
-components:
- schemas:
- Error:
- type: object
- description: An error response body.
- properties:
- message:
- description: A detailed message describing the error.
- type: string
-`[1:]
- sl := NewLoader()
- doc, err := sl.LoadFromData([]byte(spec))
- require.NoError(t, err)
- require.Contains(t, doc.Paths["/v1/operation"].Delete.Responses["default"].Value.Extensions, `x-my-extension`)
- err = doc.Validate(sl.Context)
- require.ErrorContains(t, err, `extra sibling fields: [schema]`)
-}
-
-func TestIssue513KOMixesRefAlongWithOtherFields(t *testing.T) {
- spec := `
-openapi: "3.0.3"
-info:
- title: 'My app'
- version: 1.0.0
- description: 'An API'
-
-paths:
- /v1/operation:
- delete:
- summary: Delete something
- responses:
- 200:
- description: A sibling field that the spec says is ignored
- $ref: '#/components/responses/SomeResponseBody'
-components:
- responses:
- SomeResponseBody:
- description: Success
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
- schemas:
- Error:
- type: object
- description: An error response body.
- properties:
- message:
- description: A detailed message describing the error.
- type: string
-`[1:]
- sl := NewLoader()
- doc, err := sl.LoadFromData([]byte(spec))
- require.NoError(t, err)
- err = doc.Validate(sl.Context)
- require.ErrorContains(t, err, `extra sibling fields: [description]`)
-}
-
-func TestIssue513KOMixesRefAlongWithOtherFieldsAllowed(t *testing.T) {
- spec := `
-openapi: "3.0.3"
-info:
- title: 'My app'
- version: 1.0.0
- description: 'An API'
-
-paths:
- /v1/operation:
- delete:
- summary: Delete something
- responses:
- 200:
- description: A sibling field that the spec says is ignored
- $ref: '#/components/responses/SomeResponseBody'
-components:
- responses:
- SomeResponseBody:
- description: Success
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
- schemas:
- Error:
- type: object
- description: An error response body.
- properties:
- message:
- description: A detailed message describing the error.
- type: string
-`[1:]
- sl := NewLoader()
- doc, err := sl.LoadFromData([]byte(spec))
- require.NoError(t, err)
- err = doc.Validate(sl.Context, AllowExtraSiblingFields("description"))
- require.NoError(t, err)
-}
diff --git a/openapi3/issue542_test.go b/openapi3/issue542_test.go
deleted file mode 100644
index 05f5db64d..000000000
--- a/openapi3/issue542_test.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue542(t *testing.T) {
- spec := []byte(`
-openapi: '3.0.0'
-info:
- version: '1.0.0'
- title: Swagger Petstore
- license:
- name: MIT
-servers:
-- url: http://petstore.swagger.io/v1
-paths: {}
-components:
- schemas:
- Cat:
- anyOf:
- - $ref: '#/components/schemas/Kitten'
- - type: object
- Kitten:
- type: string
-`[1:])
-
- sl := NewLoader()
-
- doc, err := sl.LoadFromData(spec)
- require.NoError(t, err)
-
- doc.Validate(sl.Context)
- require.NoError(t, err)
-}
diff --git a/openapi3/issue570_test.go b/openapi3/issue570_test.go
deleted file mode 100644
index f3c527e3b..000000000
--- a/openapi3/issue570_test.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue570(t *testing.T) {
- loader := NewLoader()
- _, err := loader.LoadFromFile("testdata/issue570.json")
- require.Error(t, err)
- assert.Contains(t, err.Error(), CircularReferenceError)
-}
diff --git a/openapi3/issue601_test.go b/openapi3/issue601_test.go
deleted file mode 100644
index ef841c25f..000000000
--- a/openapi3/issue601_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue601(t *testing.T) {
- // Document is invalid: first validation error returned is because
- // schema:
- // example: {key: value}
- // is not how schema examples are defined (but how components' examples are defined. Components are maps.)
- // Correct code should be:
- // schema: {example: value}
- sl := NewLoader()
- doc, err := sl.LoadFromFile("testdata/lxkns.yaml")
- require.NoError(t, err)
-
- err = doc.Validate(sl.Context)
- require.Contains(t, err.Error(), `invalid components: schema "DiscoveryResult": invalid example: Error at "/type": property "type" is missing`)
- require.Contains(t, err.Error(), `| Error at "/nsid": property "nsid" is missing`)
-
- err = doc.Validate(sl.Context, DisableExamplesValidation())
- require.NoError(t, err)
-
- // Now let's remove all the invalid parts
- for _, schema := range doc.Components.Schemas {
- schema.Value.Example = nil
- }
-
- err = doc.Validate(sl.Context)
- require.NoError(t, err)
-}
diff --git a/openapi3/issue615_test.go b/openapi3/issue615_test.go
deleted file mode 100644
index 496a972bb..000000000
--- a/openapi3/issue615_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package openapi3_test
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestIssue615(t *testing.T) {
- {
- var old int
- old, openapi3.CircularReferenceCounter = openapi3.CircularReferenceCounter, 1
- defer func() { openapi3.CircularReferenceCounter = old }()
-
- loader := openapi3.NewLoader()
- loader.IsExternalRefsAllowed = true
- _, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml")
- require.ErrorContains(t, err, openapi3.CircularReferenceError)
- }
-
- var old int
- old, openapi3.CircularReferenceCounter = openapi3.CircularReferenceCounter, 4
- defer func() { openapi3.CircularReferenceCounter = old }()
-
- loader := openapi3.NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml")
- require.NoError(t, err)
-
- doc.Validate(loader.Context)
- require.NoError(t, err)
-}
diff --git a/openapi3/issue618_test.go b/openapi3/issue618_test.go
deleted file mode 100644
index 2085ca0ee..000000000
--- a/openapi3/issue618_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue618(t *testing.T) {
- spec := `
-openapi: 3.0.0
-info:
- title: foo
- version: 0.0.0
-paths:
- /foo:
- get:
- responses:
- '200':
- description: Some description value text
- content:
- application/json:
- schema:
- $ref: ./testdata/schema618.yml#/components/schemas/JournalEntry
-`[1:]
-
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- ctx := loader.Context
-
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- doc.InternalizeRefs(ctx, nil)
-
- require.Contains(t, doc.Components.Schemas, "JournalEntry")
- require.Contains(t, doc.Components.Schemas, "Record")
- require.Contains(t, doc.Components.Schemas, "Account")
-}
diff --git a/openapi3/issue638_test.go b/openapi3/issue638_test.go
deleted file mode 100644
index 1db8a6f51..000000000
--- a/openapi3/issue638_test.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue638(t *testing.T) {
- for i := 0; i < 50; i++ {
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- // This path affects the occurrence of the issue #638.
- // ../openapi3/testdata/issue638/test1.yaml : reproduce
- // ./testdata/issue638/test1.yaml : not reproduce
- // testdata/issue638/test1.yaml : reproduce
- doc, err := loader.LoadFromFile("testdata/issue638/test1.yaml")
- require.NoError(t, err)
- require.Equal(t, "int", doc.Components.Schemas["test1d"].Value.Type)
- }
-}
diff --git a/openapi3/issue652_test.go b/openapi3/issue652_test.go
deleted file mode 100644
index f36e92005..000000000
--- a/openapi3/issue652_test.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package openapi3_test
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestIssue652(t *testing.T) {
- loader := openapi3.NewLoader()
- loader.IsExternalRefsAllowed = true
-
- // Test checks that no slice bounds out of range error occurs while loading
- // from file that contains reference to file in the parent directory.
- require.NotPanics(t, func() {
- const schemaName = "ReferenceToParentDirectory"
-
- spec, err := loader.LoadFromFile("testdata/issue652/nested/schema.yml")
- require.NoError(t, err)
- require.Contains(t, spec.Components.Schemas, schemaName)
-
- schema := spec.Components.Schemas[schemaName]
- assert.Equal(t, schema.Ref, "../definitions.yml#/components/schemas/TestSchema")
- assert.Equal(t, schema.Value.Type, "string")
- })
-}
diff --git a/openapi3/issue657_test.go b/openapi3/issue657_test.go
deleted file mode 100644
index 195ccd19c..000000000
--- a/openapi3/issue657_test.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package openapi3_test
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestOneOf_Warning_Errors(t *testing.T) {
- t.Parallel()
-
- loader := openapi3.NewLoader()
- spec := `
-components:
- schemas:
- Something:
- type: object
- properties:
- field:
- title: Some field
- oneOf:
- - title: First rule
- type: string
- minLength: 10
- maxLength: 10
- - title: Second rule
- type: string
- minLength: 15
- maxLength: 15
-`[1:]
-
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- tests := [...]struct {
- name string
- value string
- checkErr require.ErrorAssertionFunc
- }{
- {
- name: "valid value",
- value: "ABCDE01234",
- checkErr: require.NoError,
- },
- {
- name: "valid value",
- value: "ABCDE0123456789",
- checkErr: require.NoError,
- },
- {
- name: "no valid value",
- value: "ABCDE",
- checkErr: func(t require.TestingT, err error, i ...interface{}) {
- require.Equal(t, "doesn't match schema due to: minimum string length is 10\nSchema:\n {\n \"maxLength\": 10,\n \"minLength\": 10,\n \"title\": \"First rule\",\n \"type\": \"string\"\n }\n\nValue:\n \"ABCDE\"\n Or minimum string length is 15\nSchema:\n {\n \"maxLength\": 15,\n \"minLength\": 15,\n \"title\": \"Second rule\",\n \"type\": \"string\"\n }\n\nValue:\n \"ABCDE\"\n", err.Error())
-
- wErr := &openapi3.MultiError{}
- require.ErrorAs(t, err, wErr)
-
- require.Len(t, *wErr, 2)
-
- require.Equal(t, "minimum string length is 10", (*wErr)[0].(*openapi3.SchemaError).Reason)
- require.Equal(t, "minimum string length is 15", (*wErr)[1].(*openapi3.SchemaError).Reason)
- },
- },
- }
-
- for _, test := range tests {
- test := test
- t.Run(test.name, func(t *testing.T) {
- t.Parallel()
-
- err = doc.Components.Schemas["Something"].Value.Properties["field"].Value.VisitJSON(test.value)
-
- test.checkErr(t, err)
- })
- }
-}
diff --git a/openapi3/issue689_test.go b/openapi3/issue689_test.go
deleted file mode 100644
index cafbadfac..000000000
--- a/openapi3/issue689_test.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package openapi3_test
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestIssue689(t *testing.T) {
- t.Parallel()
-
- tests := [...]struct {
- name string
- schema *openapi3.Schema
- value map[string]interface{}
- opts []openapi3.SchemaValidationOption
- checkErr require.ErrorAssertionFunc
- }{
- // read-only
- {
- name: "read-only property succeeds when read-only validation is disabled",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", ReadOnly: true}}),
- value: map[string]interface{}{"foo": true},
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsRequest(),
- openapi3.DisableReadOnlyValidation()},
- checkErr: require.NoError,
- },
- {
- name: "non read-only property succeeds when read-only validation is disabled",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", ReadOnly: false}}),
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsRequest()},
- value: map[string]interface{}{"foo": true},
- checkErr: require.NoError,
- },
- {
- name: "read-only property fails when read-only validation is enabled",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", ReadOnly: true}}),
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsRequest()},
- value: map[string]interface{}{"foo": true},
- checkErr: require.Error,
- },
- {
- name: "non read-only property succeeds when read-only validation is enabled",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", ReadOnly: false}}),
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsRequest()},
- value: map[string]interface{}{"foo": true},
- checkErr: require.NoError,
- },
- // write-only
- {
- name: "write-only property succeeds when write-only validation is disabled",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", WriteOnly: true}}),
- value: map[string]interface{}{"foo": true},
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsResponse(),
- openapi3.DisableWriteOnlyValidation()},
- checkErr: require.NoError,
- },
- {
- name: "non write-only property succeeds when write-only validation is disabled",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", WriteOnly: false}}),
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsResponse()},
- value: map[string]interface{}{"foo": true},
- checkErr: require.NoError,
- },
- {
- name: "write-only property fails when write-only validation is enabled",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", WriteOnly: true}}),
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsResponse()},
- value: map[string]interface{}{"foo": true},
- checkErr: require.Error,
- },
- {
- name: "non write-only property succeeds when write-only validation is enabled",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", WriteOnly: false}}),
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsResponse()},
- value: map[string]interface{}{"foo": true},
- checkErr: require.NoError,
- },
- }
-
- for _, test := range tests {
- test := test
- t.Run(test.name, func(t *testing.T) {
- t.Parallel()
- err := test.schema.VisitJSON(test.value, test.opts...)
- test.checkErr(t, err)
- })
- }
-}
diff --git a/openapi3/issue697_test.go b/openapi3/issue697_test.go
deleted file mode 100644
index c7317584a..000000000
--- a/openapi3/issue697_test.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue697(t *testing.T) {
- loader := NewLoader()
- doc, err := loader.LoadFromFile("testdata/issue697.yml")
- require.NoError(t, err)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-}
diff --git a/openapi3/issue735_test.go b/openapi3/issue735_test.go
deleted file mode 100644
index f7e420c5d..000000000
--- a/openapi3/issue735_test.go
+++ /dev/null
@@ -1,278 +0,0 @@
-package openapi3
-
-import (
- "fmt"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-type testCase struct {
- name string
- schema *Schema
- value interface{}
- extraNotContains []interface{}
- options []SchemaValidationOption
-}
-
-func TestIssue735(t *testing.T) {
- DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122)
- DefineStringFormat("email", FormatOfStringForEmail)
- DefineIPv4Format()
- DefineIPv6Format()
-
- testCases := []testCase{
- {
- name: "type string",
- schema: NewStringSchema(),
- value: 42,
- },
- {
- name: "type boolean",
- schema: NewBoolSchema(),
- value: 42,
- },
- {
- name: "type integer",
- schema: NewIntegerSchema(),
- value: "foo",
- },
- {
- name: "type number",
- schema: NewFloat64Schema(),
- value: "foo",
- },
- {
- name: "type array",
- schema: NewArraySchema(),
- value: 42,
- },
- {
- name: "type object",
- schema: NewObjectSchema(),
- value: 42,
- },
- {
- name: "min",
- schema: NewSchema().WithMin(100),
- value: 42,
- },
- {
- name: "max",
- schema: NewSchema().WithMax(0),
- value: 42,
- },
- {
- name: "exclusive min",
- schema: NewSchema().WithMin(100).WithExclusiveMin(true),
- value: 42,
- },
- {
- name: "exclusive max",
- schema: NewSchema().WithMax(0).WithExclusiveMax(true),
- value: 42,
- },
- {
- name: "multiple of",
- schema: &Schema{MultipleOf: Float64Ptr(5.0)},
- value: 42,
- },
- {
- name: "enum",
- schema: NewSchema().WithEnum(3, 5),
- value: 42,
- },
- {
- name: "min length",
- schema: NewSchema().WithMinLength(100),
- value: "foo",
- },
- {
- name: "max length",
- schema: NewSchema().WithMaxLength(0),
- value: "foo",
- },
- {
- name: "pattern",
- schema: NewSchema().WithPattern("[0-9]"),
- value: "foo",
- },
- {
- name: "items",
- schema: NewSchema().WithItems(NewStringSchema()),
- value: []interface{}{42},
- extraNotContains: []interface{}{42},
- },
- {
- name: "min items",
- schema: NewSchema().WithMinItems(100),
- value: []interface{}{42},
- extraNotContains: []interface{}{42},
- },
- {
- name: "max items",
- schema: NewSchema().WithMaxItems(0),
- value: []interface{}{42},
- extraNotContains: []interface{}{42},
- },
- {
- name: "unique items",
- schema: NewSchema().WithUniqueItems(true),
- value: []interface{}{42, 42},
- extraNotContains: []interface{}{42},
- },
- {
- name: "min properties",
- schema: NewSchema().WithMinProperties(100),
- value: map[string]interface{}{"foo": 42},
- extraNotContains: []interface{}{42},
- },
- {
- name: "max properties",
- schema: NewSchema().WithMaxProperties(0),
- value: map[string]interface{}{"foo": 42},
- extraNotContains: []interface{}{42},
- },
- {
- name: "additional properties other schema type",
- schema: NewSchema().WithAdditionalProperties(NewStringSchema()),
- value: map[string]interface{}{"foo": 42},
- extraNotContains: []interface{}{42},
- },
- {
- name: "additional properties false",
- schema: &Schema{AdditionalProperties: AdditionalProperties{
- Has: BoolPtr(false),
- }},
- value: map[string]interface{}{"foo": 42},
- extraNotContains: []interface{}{42},
- },
- {
- name: "invalid properties schema",
- schema: NewSchema().WithProperties(map[string]*Schema{
- "foo": NewStringSchema(),
- }),
- value: map[string]interface{}{"foo": 42},
- extraNotContains: []interface{}{42},
- },
- // TODO: uncomment when https://github.com/getkin/kin-openapi/issues/502 is fixed
- //{
- // name: "read only properties",
- // schema: NewSchema().WithProperties(map[string]*Schema{
- // "foo": {ReadOnly: true},
- // }).WithoutAdditionalProperties(),
- // value: map[string]interface{}{"foo": 42},
- // extraNotContains: []interface{}{42},
- // options: []SchemaValidationOption{VisitAsRequest()},
- //},
- //{
- // name: "write only properties",
- // schema: NewSchema().WithProperties(map[string]*Schema{
- // "foo": {WriteOnly: true},
- // }).WithoutAdditionalProperties(),
- // value: map[string]interface{}{"foo": 42},
- // extraNotContains: []interface{}{42},
- // options: []SchemaValidationOption{VisitAsResponse()},
- //},
- {
- name: "required properties",
- schema: &Schema{
- Properties: Schemas{
- "bar": NewStringSchema().NewRef(),
- },
- Required: []string{"bar"},
- },
- value: map[string]interface{}{"foo": 42},
- extraNotContains: []interface{}{42},
- },
- {
- name: "one of (matches more then one)",
- schema: NewOneOfSchema(
- &Schema{MultipleOf: Float64Ptr(6)},
- &Schema{MultipleOf: Float64Ptr(7)},
- ),
- value: 42,
- },
- {
- name: "one of (no matches)",
- schema: NewOneOfSchema(
- &Schema{MultipleOf: Float64Ptr(5)},
- &Schema{MultipleOf: Float64Ptr(10)},
- ),
- value: 42,
- },
- {
- name: "any of",
- schema: NewAnyOfSchema(
- &Schema{MultipleOf: Float64Ptr(5)},
- &Schema{MultipleOf: Float64Ptr(10)},
- ),
- value: 42,
- },
- {
- name: "all of (match some)",
- schema: NewAllOfSchema(
- &Schema{MultipleOf: Float64Ptr(6)},
- &Schema{MultipleOf: Float64Ptr(5)},
- ),
- value: 42,
- },
- {
- name: "all of (no match)",
- schema: NewAllOfSchema(
- &Schema{MultipleOf: Float64Ptr(10)},
- &Schema{MultipleOf: Float64Ptr(5)},
- ),
- value: 42,
- },
- {
- name: "uuid format",
- schema: NewUUIDSchema(),
- value: "foo",
- },
- {
- name: "date time format",
- schema: NewDateTimeSchema(),
- value: "foo",
- },
- {
- name: "date format",
- schema: NewSchema().WithFormat("date"),
- value: "foo",
- },
- {
- name: "ipv4 format",
- schema: NewSchema().WithFormat("ipv4"),
- value: "foo",
- },
- {
- name: "ipv6 format",
- schema: NewSchema().WithFormat("ipv6"),
- value: "foo",
- },
- {
- name: "email format",
- schema: NewSchema().WithFormat("email"),
- value: "foo",
- },
- {
- name: "byte format",
- schema: NewBytesSchema(),
- value: "foo!",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- err := tc.schema.VisitJSON(tc.value, tc.options...)
- var schemaError = &SchemaError{}
- require.Error(t, err)
- require.ErrorAs(t, err, &schemaError)
- require.NotZero(t, schemaError.Reason)
- require.NotContains(t, schemaError.Reason, fmt.Sprint(tc.value))
- for _, extra := range tc.extraNotContains {
- require.NotContains(t, schemaError.Reason, fmt.Sprint(extra))
- }
- })
- }
-}
diff --git a/openapi3/issue741_test.go b/openapi3/issue741_test.go
deleted file mode 100644
index aad522023..000000000
--- a/openapi3/issue741_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package openapi3
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "sync"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue741(t *testing.T) {
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- body := `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Foo":{"type":"string"}}}}`
- _, err := w.Write([]byte(body))
- if err != nil {
- panic(err)
- }
- }))
- defer ts.Close()
-
- rootSpec := []byte(fmt.Sprintf(
- `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Bar1":{"$ref":"%s#/components/schemas/Foo"}}}}`,
- ts.URL,
- ))
-
- wg := &sync.WaitGroup{}
- n := 10
- for i := 0; i < n; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromData(rootSpec)
- require.NoError(t, err)
- require.NotNil(t, doc)
- }()
- }
- wg.Wait()
-}
diff --git a/openapi3/issue746_test.go b/openapi3/issue746_test.go
deleted file mode 100644
index 390a34848..000000000
--- a/openapi3/issue746_test.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package openapi3
-
-import (
- "encoding/json"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue746(t *testing.T) {
- schema := &Schema{}
- err := schema.UnmarshalJSON([]byte(`{"additionalProperties": false}`))
- require.NoError(t, err)
-
- var value interface{}
- err = json.Unmarshal([]byte(`{"foo": "bar"}`), &value)
- require.NoError(t, err)
-
- err = schema.VisitJSON(value)
- require.Error(t, err)
-
- schemaErr := &SchemaError{}
- require.ErrorAs(t, err, &schemaErr)
- require.Equal(t, "properties", schemaErr.SchemaField)
- require.Equal(t, `property "foo" is unsupported`, schemaErr.Reason)
-}
diff --git a/openapi3/issue753_test.go b/openapi3/issue753_test.go
deleted file mode 100644
index 4390641a4..000000000
--- a/openapi3/issue753_test.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue753(t *testing.T) {
- loader := NewLoader()
-
- doc, err := loader.LoadFromFile("testdata/issue753.yml")
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- require.NotNil(t, (*doc.Paths["/test1"].Post.Callbacks["callback1"].Value)["{$request.body#/callback}"].Post.RequestBody.Value.Content["application/json"].Schema.Value)
- require.NotNil(t, (*doc.Paths["/test2"].Post.Callbacks["callback2"].Value)["{$request.body#/callback}"].Post.RequestBody.Value.Content["application/json"].Schema.Value)
-}
diff --git a/openapi3/issue759_test.go b/openapi3/issue759_test.go
deleted file mode 100644
index 255d8b7b6..000000000
--- a/openapi3/issue759_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue759(t *testing.T) {
- spec := []byte(`
-openapi: 3.0.0
-info:
- title: title
- description: description
- version: 0.0.0
-paths:
- /slash:
- get:
- responses:
- "200":
- # Ref should point to a response, not a schema
- $ref: "#/components/schemas/UserStruct"
-components:
- schemas:
- UserStruct:
- type: object
-`[1:])
-
- loader := NewLoader()
-
- doc, err := loader.LoadFromData(spec)
- require.Nil(t, doc)
- require.EqualError(t, err, `bad data in "#/components/schemas/UserStruct" (expecting ref to response object)`)
-}
diff --git a/openapi3/issue767_test.go b/openapi3/issue767_test.go
deleted file mode 100644
index d498877c9..000000000
--- a/openapi3/issue767_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package openapi3_test
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestIssue767(t *testing.T) {
- t.Parallel()
-
- tests := [...]struct {
- name string
- schema *openapi3.Schema
- value map[string]interface{}
- opts []openapi3.SchemaValidationOption
- checkErr require.ErrorAssertionFunc
- }{
- {
- name: "default values disabled should fail with minProps 1",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", Default: true}}).WithMinProperties(1),
- value: map[string]interface{}{},
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsRequest(),
- },
- checkErr: require.Error,
- },
- {
- name: "default values enabled should pass with minProps 1",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", Default: true}}).WithMinProperties(1),
- value: map[string]interface{}{},
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsRequest(),
- openapi3.DefaultsSet(func() {}),
- },
- checkErr: require.NoError,
- },
- {
- name: "default values enabled should pass with minProps 2",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", Default: true},
- "bar": {Type: "boolean"},
- }).WithMinProperties(2),
- value: map[string]interface{}{"bar": false},
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsRequest(),
- openapi3.DefaultsSet(func() {}),
- },
- checkErr: require.NoError,
- },
- {
- name: "default values enabled should fail with maxProps 1",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", Default: true},
- "bar": {Type: "boolean"},
- }).WithMaxProperties(1),
- value: map[string]interface{}{"bar": false},
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsRequest(),
- openapi3.DefaultsSet(func() {}),
- },
- checkErr: require.Error,
- },
- {
- name: "default values disabled should pass with maxProps 1",
- schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
- "foo": {Type: "boolean", Default: true},
- "bar": {Type: "boolean"},
- }).WithMaxProperties(1),
- value: map[string]interface{}{"bar": false},
- opts: []openapi3.SchemaValidationOption{
- openapi3.VisitAsRequest(),
- },
- checkErr: require.NoError,
- },
- }
-
- for _, test := range tests {
- test := test
- t.Run(test.name, func(t *testing.T) {
- t.Parallel()
- err := test.schema.VisitJSON(test.value, test.opts...)
- test.checkErr(t, err)
- })
- }
-}
diff --git a/openapi3/link.go b/openapi3/link.go
index 08dfa8d67..2c1ec013f 100644
--- a/openapi3/link.go
+++ b/openapi3/link.go
@@ -2,100 +2,37 @@ package openapi3
import (
"context"
- "encoding/json"
"errors"
"fmt"
- "github.com/go-openapi/jsonpointer"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-type Links map[string]*LinkRef
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (links Links) JSONLookup(token string) (interface{}, error) {
- ref, ok := links[token]
- if ok == false {
- return nil, fmt.Errorf("object has no field %q", token)
- }
-
- if ref != nil && ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
-var _ jsonpointer.JSONPointable = (*Links)(nil)
-
-// Link is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#link-object
+// Link is specified by OpenAPI/Swagger standard version 3.0.
type Link struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"`
+ ExtensionProps
OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"`
+ OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"`
Server *Server `json:"server,omitempty" yaml:"server,omitempty"`
RequestBody interface{} `json:"requestBody,omitempty" yaml:"requestBody,omitempty"`
}
-// MarshalJSON returns the JSON encoding of Link.
-func (link Link) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 6+len(link.Extensions))
- for k, v := range link.Extensions {
- m[k] = v
- }
-
- if x := link.OperationRef; x != "" {
- m["operationRef"] = x
- }
- if x := link.OperationID; x != "" {
- m["operationId"] = x
- }
- if x := link.Description; x != "" {
- m["description"] = x
- }
- if x := link.Parameters; len(x) != 0 {
- m["parameters"] = x
- }
- if x := link.Server; x != nil {
- m["server"] = x
- }
- if x := link.RequestBody; x != nil {
- m["requestBody"] = x
- }
-
- return json.Marshal(m)
+func (value *Link) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(value)
}
-// UnmarshalJSON sets Link to a copy of data.
-func (link *Link) UnmarshalJSON(data []byte) error {
- type LinkBis Link
- var x LinkBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "operationRef")
- delete(x.Extensions, "operationId")
- delete(x.Extensions, "description")
- delete(x.Extensions, "parameters")
- delete(x.Extensions, "server")
- delete(x.Extensions, "requestBody")
- *link = Link(x)
- return nil
+func (value *Link) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalStrictStruct(data, value)
}
-// Validate returns an error if Link does not comply with the OpenAPI spec.
-func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if link.OperationID == "" && link.OperationRef == "" {
+func (value *Link) Validate(c context.Context) error {
+ if value.OperationID == "" && value.OperationRef == "" {
return errors.New("missing operationId or operationRef on link")
}
- if link.OperationID != "" && link.OperationRef != "" {
- return fmt.Errorf("operationId %q and operationRef %q are mutually exclusive", link.OperationID, link.OperationRef)
+ if value.OperationID != "" && value.OperationRef != "" {
+ return fmt.Errorf("operationId '%s' and operationRef '%s' are mutually exclusive", value.OperationID, value.OperationRef)
}
-
- return validateExtensions(ctx, link.Extensions)
+ return nil
}
diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go
deleted file mode 100644
index 9bcaaf77f..000000000
--- a/openapi3/load_cicular_ref_with_external_file_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-//go:build go1.16
-// +build go1.16
-
-package openapi3_test
-
-import (
- "embed"
- "encoding/json"
- "net/url"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-//go:embed testdata/circularRef/*
-var circularResSpecs embed.FS
-
-func TestLoadCircularRefFromFile(t *testing.T) {
- loader := openapi3.NewLoader()
- loader.IsExternalRefsAllowed = true
- loader.ReadFromURIFunc = func(loader *openapi3.Loader, uri *url.URL) ([]byte, error) {
- return circularResSpecs.ReadFile(uri.Path)
- }
-
- got, err := loader.LoadFromFile("testdata/circularRef/base.yml")
- require.NoError(t, err)
-
- foo := &openapi3.SchemaRef{
- Value: &openapi3.Schema{
- Properties: map[string]*openapi3.SchemaRef{
- "foo2": {
- Ref: "other.yml#/components/schemas/Foo2", // reference to an external file
- Value: &openapi3.Schema{
- Properties: map[string]*openapi3.SchemaRef{
- "id": {
- Value: &openapi3.Schema{Type: "string"}},
- },
- },
- },
- },
- },
- }
- bar := &openapi3.SchemaRef{Value: &openapi3.Schema{Properties: make(map[string]*openapi3.SchemaRef)}}
- // circular reference
- bar.Value.Properties["foo"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Foo", Value: foo.Value}
- foo.Value.Properties["bar"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Bar", Value: bar.Value}
-
- want := &openapi3.T{
- OpenAPI: "3.0.3",
- Info: &openapi3.Info{
- Title: "Recursive cyclic refs example",
- Version: "1.0",
- },
- Components: &openapi3.Components{
- Schemas: openapi3.Schemas{
- "Foo": foo,
- "Bar": bar,
- },
- },
- }
-
- jsoner := func(doc *openapi3.T) string {
- data, err := json.Marshal(doc)
- require.NoError(t, err)
- return string(data)
- }
- require.JSONEq(t, jsoner(want), jsoner(got))
-}
diff --git a/openapi3/load_with_go_embed_test.go b/openapi3/load_with_go_embed_test.go
deleted file mode 100644
index e0fb915ba..000000000
--- a/openapi3/load_with_go_embed_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-//go:build go1.16
-// +build go1.16
-
-package openapi3_test
-
-import (
- "embed"
- "fmt"
- "net/url"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-//go:embed testdata/recursiveRef/*
-var fs embed.FS
-
-func Example() {
- loader := openapi3.NewLoader()
- loader.IsExternalRefsAllowed = true
- loader.ReadFromURIFunc = func(loader *openapi3.Loader, uri *url.URL) ([]byte, error) {
- return fs.ReadFile(uri.Path)
- }
-
- doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml")
- if err != nil {
- panic(err)
- }
-
- if err = doc.Validate(loader.Context); err != nil {
- panic(err)
- }
-
- fmt.Println(doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Type)
- // Output: string
-}
diff --git a/openapi3/loader.go b/openapi3/loader.go
deleted file mode 100644
index 4a14f67f0..000000000
--- a/openapi3/loader.go
+++ /dev/null
@@ -1,1039 +0,0 @@
-package openapi3
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "net/url"
- "path"
- "path/filepath"
- "reflect"
- "sort"
- "strconv"
- "strings"
-
- "github.com/invopop/yaml"
-)
-
-var CircularReferenceError = "kin-openapi bug found: circular schema reference not handled"
-var CircularReferenceCounter = 3
-
-func foundUnresolvedRef(ref string) error {
- return fmt.Errorf("found unresolved ref: %q", ref)
-}
-
-func failedToResolveRefFragmentPart(value, what string) error {
- return fmt.Errorf("failed to resolve %q in fragment in URI: %q", what, value)
-}
-
-// Loader helps deserialize an OpenAPIv3 document
-type Loader struct {
- // IsExternalRefsAllowed enables visiting other files
- IsExternalRefsAllowed bool
-
- // ReadFromURIFunc allows overriding the any file/URL reading func
- ReadFromURIFunc ReadFromURIFunc
-
- Context context.Context
-
- rootDir string
- rootLocation string
-
- visitedPathItemRefs map[string]struct{}
-
- visitedDocuments map[string]*T
-
- visitedCallback map[*Callback]struct{}
- visitedExample map[*Example]struct{}
- visitedHeader map[*Header]struct{}
- visitedLink map[*Link]struct{}
- visitedParameter map[*Parameter]struct{}
- visitedRequestBody map[*RequestBody]struct{}
- visitedResponse map[*Response]struct{}
- visitedSchema map[*Schema]struct{}
- visitedSecurityScheme map[*SecurityScheme]struct{}
-}
-
-// NewLoader returns an empty Loader
-func NewLoader() *Loader {
- return &Loader{
- Context: context.Background(),
- }
-}
-
-func (loader *Loader) resetVisitedPathItemRefs() {
- loader.visitedPathItemRefs = make(map[string]struct{})
-}
-
-// LoadFromURI loads a spec from a remote URL
-func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) {
- loader.resetVisitedPathItemRefs()
- return loader.loadFromURIInternal(location)
-}
-
-// LoadFromFile loads a spec from a local file path
-func (loader *Loader) LoadFromFile(location string) (*T, error) {
- loader.rootDir = path.Dir(location)
- return loader.LoadFromURI(&url.URL{Path: filepath.ToSlash(location)})
-}
-
-func (loader *Loader) loadFromURIInternal(location *url.URL) (*T, error) {
- data, err := loader.readURL(location)
- if err != nil {
- return nil, err
- }
- return loader.loadFromDataWithPathInternal(data, location)
-}
-
-func (loader *Loader) allowsExternalRefs(ref string) (err error) {
- if !loader.IsExternalRefsAllowed {
- err = fmt.Errorf("encountered disallowed external reference: %q", ref)
- }
- return
-}
-
-// loadSingleElementFromURI reads the data from ref and unmarshals to the passed element.
-func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, element interface{}) (*url.URL, error) {
- if err := loader.allowsExternalRefs(ref); err != nil {
- return nil, err
- }
-
- parsedURL, err := url.Parse(ref)
- if err != nil {
- return nil, err
- }
- if fragment := parsedURL.Fragment; fragment != "" {
- return nil, fmt.Errorf("unexpected ref fragment %q", fragment)
- }
-
- resolvedPath, err := resolvePath(rootPath, parsedURL)
- if err != nil {
- return nil, fmt.Errorf("could not resolve path: %w", err)
- }
-
- data, err := loader.readURL(resolvedPath)
- if err != nil {
- return nil, err
- }
- if err := unmarshal(data, element); err != nil {
- return nil, err
- }
-
- return resolvedPath, nil
-}
-
-func (loader *Loader) readURL(location *url.URL) ([]byte, error) {
- if f := loader.ReadFromURIFunc; f != nil {
- return f(loader, location)
- }
- return DefaultReadFromURI(loader, location)
-}
-
-// LoadFromData loads a spec from a byte array
-func (loader *Loader) LoadFromData(data []byte) (*T, error) {
- loader.resetVisitedPathItemRefs()
- doc := &T{}
- if err := unmarshal(data, doc); err != nil {
- return nil, err
- }
- if err := loader.ResolveRefsIn(doc, nil); err != nil {
- return nil, err
- }
- return doc, nil
-}
-
-// LoadFromDataWithPath takes the OpenAPI document data in bytes and a path where the resolver can find referred
-// elements and returns a *T with all resolved data or an error if unable to load data or resolve refs.
-func (loader *Loader) LoadFromDataWithPath(data []byte, location *url.URL) (*T, error) {
- loader.resetVisitedPathItemRefs()
- return loader.loadFromDataWithPathInternal(data, location)
-}
-
-func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.URL) (*T, error) {
- if loader.visitedDocuments == nil {
- loader.visitedDocuments = make(map[string]*T)
- loader.rootLocation = location.Path
- }
- uri := location.String()
- if doc, ok := loader.visitedDocuments[uri]; ok {
- return doc, nil
- }
-
- doc := &T{}
- loader.visitedDocuments[uri] = doc
-
- if err := unmarshal(data, doc); err != nil {
- return nil, err
- }
- if err := loader.ResolveRefsIn(doc, location); err != nil {
- return nil, err
- }
-
- return doc, nil
-}
-
-func unmarshal(data []byte, v interface{}) error {
- // See https://github.com/getkin/kin-openapi/issues/680
- if err := json.Unmarshal(data, v); err != nil {
- // UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys
- return yaml.Unmarshal(data, v)
- }
- return nil
-}
-
-// ResolveRefsIn expands references if for instance spec was just unmarshalled
-func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) {
- if loader.Context == nil {
- loader.Context = context.Background()
- }
-
- if loader.visitedPathItemRefs == nil {
- loader.resetVisitedPathItemRefs()
- }
-
- if components := doc.Components; components != nil {
- for _, component := range components.Headers {
- if err = loader.resolveHeaderRef(doc, component, location); err != nil {
- return
- }
- }
- for _, component := range components.Parameters {
- if err = loader.resolveParameterRef(doc, component, location); err != nil {
- return
- }
- }
- for _, component := range components.RequestBodies {
- if err = loader.resolveRequestBodyRef(doc, component, location); err != nil {
- return
- }
- }
- for _, component := range components.Responses {
- if err = loader.resolveResponseRef(doc, component, location); err != nil {
- return
- }
- }
- for _, component := range components.Schemas {
- if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil {
- return
- }
- }
- for _, component := range components.SecuritySchemes {
- if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil {
- return
- }
- }
-
- examples := make([]string, 0, len(components.Examples))
- for name := range components.Examples {
- examples = append(examples, name)
- }
- sort.Strings(examples)
- for _, name := range examples {
- component := components.Examples[name]
- if err = loader.resolveExampleRef(doc, component, location); err != nil {
- return
- }
- }
-
- for _, component := range components.Callbacks {
- if err = loader.resolveCallbackRef(doc, component, location); err != nil {
- return
- }
- }
- }
-
- // Visit all operations
- for _, pathItem := range doc.Paths {
- if pathItem == nil {
- continue
- }
- if err = loader.resolvePathItemRef(doc, pathItem, location); err != nil {
- return
- }
- }
-
- return
-}
-
-func join(basePath *url.URL, relativePath *url.URL) (*url.URL, error) {
- if basePath == nil {
- return relativePath, nil
- }
- newPath, err := url.Parse(basePath.String())
- if err != nil {
- return nil, fmt.Errorf("cannot copy path: %q", basePath.String())
- }
- newPath.Path = path.Join(path.Dir(newPath.Path), relativePath.Path)
- return newPath, nil
-}
-
-func resolvePath(basePath *url.URL, componentPath *url.URL) (*url.URL, error) {
- if componentPath.Scheme == "" && componentPath.Host == "" {
- // support absolute paths
- if componentPath.Path[0] == '/' {
- return componentPath, nil
- }
- return join(basePath, componentPath)
- }
- return componentPath, nil
-}
-
-func isSingleRefElement(ref string) bool {
- return !strings.Contains(ref, "#")
-}
-
-func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolved interface{}) (
- componentDoc *T,
- componentPath *url.URL,
- err error,
-) {
- if componentDoc, ref, componentPath, err = loader.resolveRef(doc, ref, path); err != nil {
- return nil, nil, err
- }
-
- parsedURL, err := url.Parse(ref)
- if err != nil {
- return nil, nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL)
- }
- fragment := parsedURL.Fragment
- if !strings.HasPrefix(fragment, "/") {
- return nil, nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref)
- }
-
- drill := func(cursor interface{}) (interface{}, error) {
- for _, pathPart := range strings.Split(fragment[1:], "/") {
- pathPart = unescapeRefString(pathPart)
-
- if cursor, err = drillIntoField(cursor, pathPart); err != nil {
- e := failedToResolveRefFragmentPart(ref, pathPart)
- return nil, fmt.Errorf("%s: %w", e, err)
- }
- if cursor == nil {
- return nil, failedToResolveRefFragmentPart(ref, pathPart)
- }
- }
- return cursor, nil
- }
- var cursor interface{}
- if cursor, err = drill(componentDoc); err != nil {
- if path == nil {
- return nil, nil, err
- }
- var err2 error
- data, err2 := loader.readURL(path)
- if err2 != nil {
- return nil, nil, err
- }
- if err2 = unmarshal(data, &cursor); err2 != nil {
- return nil, nil, err
- }
- if cursor, err2 = drill(cursor); err2 != nil || cursor == nil {
- return nil, nil, err
- }
- err = nil
- }
-
- switch {
- case reflect.TypeOf(cursor) == reflect.TypeOf(resolved):
- reflect.ValueOf(resolved).Elem().Set(reflect.ValueOf(cursor).Elem())
- return componentDoc, componentPath, nil
-
- case reflect.TypeOf(cursor) == reflect.TypeOf(map[string]interface{}{}):
- codec := func(got, expect interface{}) error {
- enc, err := json.Marshal(got)
- if err != nil {
- return err
- }
- if err = json.Unmarshal(enc, expect); err != nil {
- return err
- }
- return nil
- }
- if err := codec(cursor, resolved); err != nil {
- return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved))
- }
- return componentDoc, componentPath, nil
-
- default:
- return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved))
- }
-}
-
-func readableType(x interface{}) string {
- switch x.(type) {
- case *Callback:
- return "callback object"
- case *CallbackRef:
- return "ref to callback object"
- case *ExampleRef:
- return "ref to example object"
- case *HeaderRef:
- return "ref to header object"
- case *LinkRef:
- return "ref to link object"
- case *ParameterRef:
- return "ref to parameter object"
- case *PathItem:
- return "pathItem object"
- case *RequestBodyRef:
- return "ref to requestBody object"
- case *ResponseRef:
- return "ref to response object"
- case *SchemaRef:
- return "ref to schema object"
- case *SecuritySchemeRef:
- return "ref to securityScheme object"
- default:
- panic(fmt.Sprintf("unreachable %T", x))
- }
-}
-
-func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) {
- // Special case due to multijson
- if s, ok := cursor.(*SchemaRef); ok && fieldName == "additionalProperties" {
- if ap := s.Value.AdditionalProperties.Has; ap != nil {
- return *ap, nil
- }
- return s.Value.AdditionalProperties.Schema, nil
- }
-
- switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() {
- case reflect.Map:
- elementValue := val.MapIndex(reflect.ValueOf(fieldName))
- if !elementValue.IsValid() {
- return nil, fmt.Errorf("map key %q not found", fieldName)
- }
- return elementValue.Interface(), nil
-
- case reflect.Slice:
- i, err := strconv.ParseUint(fieldName, 10, 32)
- if err != nil {
- return nil, err
- }
- index := int(i)
- if 0 > index || index >= val.Len() {
- return nil, errors.New("slice index out of bounds")
- }
- return val.Index(index).Interface(), nil
-
- case reflect.Struct:
- hasFields := false
- for i := 0; i < val.NumField(); i++ {
- hasFields = true
- if fieldName == strings.Split(val.Type().Field(i).Tag.Get("yaml"), ",")[0] {
- return val.Field(i).Interface(), nil
- }
- }
- // if cursor is a "ref wrapper" struct (e.g. RequestBodyRef),
- if _, ok := val.Type().FieldByName("Value"); ok {
- // try digging into its Value field
- return drillIntoField(val.FieldByName("Value").Interface(), fieldName)
- }
- if hasFields {
- if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "Extensions" {
- extensions := val.Field(0).Interface().(map[string]interface{})
- if enc, ok := extensions[fieldName]; ok {
- return enc, nil
- }
- }
- }
- return nil, fmt.Errorf("struct field %q not found", fieldName)
-
- default:
- return nil, errors.New("not a map, slice nor struct")
- }
-}
-
-func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) {
- if ref != "" && ref[0] == '#' {
- return doc, ref, path, nil
- }
-
- if err := loader.allowsExternalRefs(ref); err != nil {
- return nil, "", nil, err
- }
-
- parsedURL, err := url.Parse(ref)
- if err != nil {
- return nil, "", nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL)
- }
- fragment := parsedURL.Fragment
- parsedURL.Fragment = ""
-
- var resolvedPath *url.URL
- if resolvedPath, err = resolvePath(path, parsedURL); err != nil {
- return nil, "", nil, fmt.Errorf("error resolving path: %w", err)
- }
-
- if doc, err = loader.loadFromURIInternal(resolvedPath); err != nil {
- return nil, "", nil, fmt.Errorf("error resolving reference %q: %w", ref, err)
- }
-
- return doc, "#" + fragment, resolvedPath, nil
-}
-
-func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPath *url.URL) (err error) {
- if component != nil && component.Value != nil {
- if loader.visitedHeader == nil {
- loader.visitedHeader = make(map[*Header]struct{})
- }
- if _, ok := loader.visitedHeader[component.Value]; ok {
- return nil
- }
- loader.visitedHeader[component.Value] = struct{}{}
- }
-
- if component == nil {
- return errors.New("invalid header: value MUST be an object")
- }
- if ref := component.Ref; ref != "" {
- if isSingleRefElement(ref) {
- var header Header
- if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &header); err != nil {
- return err
- }
- component.Value = &header
- } else {
- var resolved HeaderRef
- doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
- if err != nil {
- return err
- }
- if err := loader.resolveHeaderRef(doc, &resolved, componentPath); err != nil {
- return err
- }
- component.Value = resolved.Value
- }
- }
- value := component.Value
- if value == nil {
- return nil
- }
-
- if schema := value.Schema; schema != nil {
- if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
- return err
- }
- }
- return nil
-}
-
-func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, documentPath *url.URL) (err error) {
- if component != nil && component.Value != nil {
- if loader.visitedParameter == nil {
- loader.visitedParameter = make(map[*Parameter]struct{})
- }
- if _, ok := loader.visitedParameter[component.Value]; ok {
- return nil
- }
- loader.visitedParameter[component.Value] = struct{}{}
- }
-
- if component == nil {
- return errors.New("invalid parameter: value MUST be an object")
- }
- ref := component.Ref
- if ref != "" {
- if isSingleRefElement(ref) {
- var param Parameter
- if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil {
- return err
- }
- component.Value = ¶m
- } else {
- var resolved ParameterRef
- doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
- if err != nil {
- return err
- }
- if err := loader.resolveParameterRef(doc, &resolved, componentPath); err != nil {
- return err
- }
- component.Value = resolved.Value
- }
- }
- value := component.Value
- if value == nil {
- return nil
- }
-
- if value.Content != nil && value.Schema != nil {
- return errors.New("cannot contain both schema and content in a parameter")
- }
- for _, contentType := range value.Content {
- if schema := contentType.Schema; schema != nil {
- if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
- return err
- }
- }
- }
- if schema := value.Schema; schema != nil {
- if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
- return err
- }
- }
- return nil
-}
-
-func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, documentPath *url.URL) (err error) {
- if component != nil && component.Value != nil {
- if loader.visitedRequestBody == nil {
- loader.visitedRequestBody = make(map[*RequestBody]struct{})
- }
- if _, ok := loader.visitedRequestBody[component.Value]; ok {
- return nil
- }
- loader.visitedRequestBody[component.Value] = struct{}{}
- }
-
- if component == nil {
- return errors.New("invalid requestBody: value MUST be an object")
- }
- if ref := component.Ref; ref != "" {
- if isSingleRefElement(ref) {
- var requestBody RequestBody
- if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil {
- return err
- }
- component.Value = &requestBody
- } else {
- var resolved RequestBodyRef
- doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
- if err != nil {
- return err
- }
- if err = loader.resolveRequestBodyRef(doc, &resolved, componentPath); err != nil {
- return err
- }
- component.Value = resolved.Value
- }
- }
- value := component.Value
- if value == nil {
- return nil
- }
-
- for _, contentType := range value.Content {
- examples := make([]string, 0, len(contentType.Examples))
- for name := range contentType.Examples {
- examples = append(examples, name)
- }
- sort.Strings(examples)
- for _, name := range examples {
- example := contentType.Examples[name]
- if err := loader.resolveExampleRef(doc, example, documentPath); err != nil {
- return err
- }
- contentType.Examples[name] = example
- }
- if schema := contentType.Schema; schema != nil {
- if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documentPath *url.URL) (err error) {
- if component != nil && component.Value != nil {
- if loader.visitedResponse == nil {
- loader.visitedResponse = make(map[*Response]struct{})
- }
- if _, ok := loader.visitedResponse[component.Value]; ok {
- return nil
- }
- loader.visitedResponse[component.Value] = struct{}{}
- }
-
- if component == nil {
- return errors.New("invalid response: value MUST be an object")
- }
- ref := component.Ref
- if ref != "" {
- if isSingleRefElement(ref) {
- var resp Response
- if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil {
- return err
- }
- component.Value = &resp
- } else {
- var resolved ResponseRef
- doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
- if err != nil {
- return err
- }
- if err := loader.resolveResponseRef(doc, &resolved, componentPath); err != nil {
- return err
- }
- component.Value = resolved.Value
- }
- }
- value := component.Value
- if value == nil {
- return nil
- }
-
- for _, header := range value.Headers {
- if err := loader.resolveHeaderRef(doc, header, documentPath); err != nil {
- return err
- }
- }
- for _, contentType := range value.Content {
- if contentType == nil {
- continue
- }
- examples := make([]string, 0, len(contentType.Examples))
- for name := range contentType.Examples {
- examples = append(examples, name)
- }
- sort.Strings(examples)
- for _, name := range examples {
- example := contentType.Examples[name]
- if err := loader.resolveExampleRef(doc, example, documentPath); err != nil {
- return err
- }
- contentType.Examples[name] = example
- }
- if schema := contentType.Schema; schema != nil {
- if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
- return err
- }
- contentType.Schema = schema
- }
- }
- for _, link := range value.Links {
- if err := loader.resolveLinkRef(doc, link, documentPath); err != nil {
- return err
- }
- }
- return nil
-}
-
-func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPath *url.URL, visited []string) (err error) {
- if component == nil {
- return errors.New("invalid schema: value MUST be an object")
- }
-
- if component.Value != nil {
- if loader.visitedSchema == nil {
- loader.visitedSchema = make(map[*Schema]struct{})
- }
- if _, ok := loader.visitedSchema[component.Value]; ok {
- return nil
- }
- loader.visitedSchema[component.Value] = struct{}{}
- }
-
- ref := component.Ref
- if ref != "" {
- if isSingleRefElement(ref) {
- var schema Schema
- if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil {
- return err
- }
- component.Value = &schema
- } else {
- if visitedLimit(visited, ref) {
- visited = append(visited, ref)
- return fmt.Errorf("%s - %s", CircularReferenceError, strings.Join(visited, " -> "))
- }
- visited = append(visited, ref)
-
- var resolved SchemaRef
- doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
- if err != nil {
- return err
- }
- if err := loader.resolveSchemaRef(doc, &resolved, componentPath, visited); err != nil {
- return err
- }
- component.Value = resolved.Value
- }
- if loader.visitedSchema == nil {
- loader.visitedSchema = make(map[*Schema]struct{})
- }
- loader.visitedSchema[component.Value] = struct{}{}
- }
- value := component.Value
- if value == nil {
- return nil
- }
-
- // ResolveRefs referred schemas
- if v := value.Items; v != nil {
- if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
- return err
- }
- }
- for _, v := range value.Properties {
- if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
- return err
- }
- }
- if v := value.AdditionalProperties.Schema; v != nil {
- if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
- return err
- }
- }
- if v := value.Not; v != nil {
- if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
- return err
- }
- }
- for _, v := range value.AllOf {
- if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
- return err
- }
- }
- for _, v := range value.AnyOf {
- if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
- return err
- }
- }
- for _, v := range value.OneOf {
- if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
- return err
- }
- }
- return nil
-}
-
-func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) {
- if component != nil && component.Value != nil {
- if loader.visitedSecurityScheme == nil {
- loader.visitedSecurityScheme = make(map[*SecurityScheme]struct{})
- }
- if _, ok := loader.visitedSecurityScheme[component.Value]; ok {
- return nil
- }
- loader.visitedSecurityScheme[component.Value] = struct{}{}
- }
-
- if component == nil {
- return errors.New("invalid securityScheme: value MUST be an object")
- }
- if ref := component.Ref; ref != "" {
- if isSingleRefElement(ref) {
- var scheme SecurityScheme
- if _, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil {
- return err
- }
- component.Value = &scheme
- } else {
- var resolved SecuritySchemeRef
- doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
- if err != nil {
- return err
- }
- if err := loader.resolveSecuritySchemeRef(doc, &resolved, componentPath); err != nil {
- return err
- }
- component.Value = resolved.Value
- }
- }
- return nil
-}
-
-func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentPath *url.URL) (err error) {
- if component != nil && component.Value != nil {
- if loader.visitedExample == nil {
- loader.visitedExample = make(map[*Example]struct{})
- }
- if _, ok := loader.visitedExample[component.Value]; ok {
- return nil
- }
- loader.visitedExample[component.Value] = struct{}{}
- }
-
- if component == nil {
- return errors.New("invalid example: value MUST be an object")
- }
- if ref := component.Ref; ref != "" {
- if isSingleRefElement(ref) {
- var example Example
- if _, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil {
- return err
- }
- component.Value = &example
- } else {
- var resolved ExampleRef
- doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
- if err != nil {
- return err
- }
- if err := loader.resolveExampleRef(doc, &resolved, componentPath); err != nil {
- return err
- }
- component.Value = resolved.Value
- }
- }
- return nil
-}
-
-func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) {
- if component != nil && component.Value != nil {
- if loader.visitedCallback == nil {
- loader.visitedCallback = make(map[*Callback]struct{})
- }
- if _, ok := loader.visitedCallback[component.Value]; ok {
- return nil
- }
- loader.visitedCallback[component.Value] = struct{}{}
- }
-
- if component == nil {
- return errors.New("invalid callback: value MUST be an object")
- }
- if ref := component.Ref; ref != "" {
- if isSingleRefElement(ref) {
- var resolved Callback
- if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil {
- return err
- }
- component.Value = &resolved
- } else {
- var resolved CallbackRef
- doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
- if err != nil {
- return err
- }
- if err = loader.resolveCallbackRef(doc, &resolved, componentPath); err != nil {
- return err
- }
- component.Value = resolved.Value
- }
- }
- value := component.Value
- if value == nil {
- return nil
- }
-
- for _, pathItem := range *value {
- if err = loader.resolvePathItemRef(doc, pathItem, documentPath); err != nil {
- return err
- }
- }
- return nil
-}
-
-func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *url.URL) (err error) {
- if component != nil && component.Value != nil {
- if loader.visitedLink == nil {
- loader.visitedLink = make(map[*Link]struct{})
- }
- if _, ok := loader.visitedLink[component.Value]; ok {
- return nil
- }
- loader.visitedLink[component.Value] = struct{}{}
- }
-
- if component == nil {
- return errors.New("invalid link: value MUST be an object")
- }
- if ref := component.Ref; ref != "" {
- if isSingleRefElement(ref) {
- var link Link
- if _, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil {
- return err
- }
- component.Value = &link
- } else {
- var resolved LinkRef
- doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
- if err != nil {
- return err
- }
- if err := loader.resolveLinkRef(doc, &resolved, componentPath); err != nil {
- return err
- }
- component.Value = resolved.Value
- }
- }
- return nil
-}
-
-func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPath *url.URL) (err error) {
- if pathItem == nil {
- return errors.New("invalid path item: value MUST be an object")
- }
- ref := pathItem.Ref
- if ref != "" {
- if pathItem.Summary != "" ||
- pathItem.Description != "" ||
- pathItem.Connect != nil ||
- pathItem.Delete != nil ||
- pathItem.Get != nil ||
- pathItem.Head != nil ||
- pathItem.Options != nil ||
- pathItem.Patch != nil ||
- pathItem.Post != nil ||
- pathItem.Put != nil ||
- pathItem.Trace != nil ||
- len(pathItem.Servers) != 0 ||
- len(pathItem.Parameters) != 0 {
- return nil
- }
- if isSingleRefElement(ref) {
- var p PathItem
- if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil {
- return err
- }
- *pathItem = p
- } else {
- var resolved PathItem
- if doc, documentPath, err = loader.resolveComponent(doc, ref, documentPath, &resolved); err != nil {
- return err
- }
- *pathItem = resolved
- }
- pathItem.Ref = ref
- }
-
- for _, parameter := range pathItem.Parameters {
- if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil {
- return
- }
- }
- for _, operation := range pathItem.Operations() {
- for _, parameter := range operation.Parameters {
- if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil {
- return
- }
- }
- if requestBody := operation.RequestBody; requestBody != nil {
- if err = loader.resolveRequestBodyRef(doc, requestBody, documentPath); err != nil {
- return
- }
- }
- for _, response := range operation.Responses {
- if err = loader.resolveResponseRef(doc, response, documentPath); err != nil {
- return
- }
- }
- for _, callback := range operation.Callbacks {
- if err = loader.resolveCallbackRef(doc, callback, documentPath); err != nil {
- return
- }
- }
- }
- return
-}
-
-func unescapeRefString(ref string) string {
- return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1)
-}
-
-func visitedLimit(visited []string, ref string) bool {
- visitedCount := 0
- for _, v := range visited {
- if v == ref {
- visitedCount++
- if visitedCount >= CircularReferenceCounter {
- return true
- }
- }
- }
- return false
-}
diff --git a/openapi3/loader_http_error_test.go b/openapi3/loader_http_error_test.go
deleted file mode 100644
index 5f7f137c8..000000000
--- a/openapi3/loader_http_error_test.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package openapi3
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "net/url"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestLoadReferenceFromRemoteURLFailsWithHttpError(t *testing.T) {
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusBadRequest)
- fmt.Fprint(w, "")
- }))
- defer ts.Close()
-
- spec := []byte(`
-{
- "openapi": "3.0.0",
- "info": {
- "title": "",
- "version": "1"
- },
- "paths": {
- "/test": {
- "post": {
- "responses": {
- "default": {
- "description": "test",
- "headers": {
- "X-TEST-HEADER": {
- "$ref": "` + ts.URL + `/components.openapi.json#/components/headers/CustomTestHeader"
- }
- }
- }
- }
- }
- }
- }
-}`)
-
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
-
- require.Nil(t, doc)
- require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL, ts.URL))
-
- doc, err = loader.LoadFromData(spec)
- require.Nil(t, doc)
- require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL, ts.URL))
-}
-
-func TestLoadFromRemoteURLFailsWithHttpError(t *testing.T) {
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusBadRequest)
- fmt.Fprint(w, "")
- }))
- defer ts.Close()
-
- spec := []byte(`
-{
- "openapi": "3.0.0",
- "info": {
- "title": "",
- "version": "1"
- },
- "paths": {
- "/test": {
- "post": {
- "responses": {
- "default": {
- "description": "test",
- "headers": {
- "X-TEST-HEADER": {
- "$ref": "` + ts.URL + `/components.openapi.json"
- }
- }
- }
- }
- }
- }
- }
-}`)
-
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
-
- require.Nil(t, doc)
- require.EqualError(t, err, fmt.Sprintf("error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL))
-
- doc, err = loader.LoadFromData(spec)
- require.Nil(t, doc)
- require.EqualError(t, err, fmt.Sprintf("error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL))
-}
diff --git a/openapi3/loader_issue220_test.go b/openapi3/loader_issue220_test.go
deleted file mode 100644
index 57a44d5d0..000000000
--- a/openapi3/loader_issue220_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package openapi3
-
-import (
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue220(t *testing.T) {
- for _, specPath := range []string{
- "testdata/my-openapi.json",
- filepath.FromSlash("testdata/my-openapi.json"),
- } {
- t.Logf("specPath: %q", specPath)
-
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile(specPath)
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- require.Equal(t, "integer", doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["bar"].Value.Type)
- }
-}
diff --git a/openapi3/loader_issue235_test.go b/openapi3/loader_issue235_test.go
deleted file mode 100644
index 4cb54eff1..000000000
--- a/openapi3/loader_issue235_test.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue235OK(t *testing.T) {
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/issue235.spec0.yml")
- require.NoError(t, err)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-}
-
-func TestIssue235CircularDep(t *testing.T) {
- t.Skip("TODO: return an error on circular dependencies between external files of a spec")
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/issue235.spec0-typo.yml")
- require.Nil(t, doc)
- require.Error(t, err)
-}
diff --git a/openapi3/loader_outside_refs_test.go b/openapi3/loader_outside_refs_test.go
deleted file mode 100644
index 5cec93452..000000000
--- a/openapi3/loader_outside_refs_test.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestLoadOutsideRefs(t *testing.T) {
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/303bis/service.yaml")
- require.NoError(t, err)
- require.NotNil(t, doc)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- require.Equal(t, "string", doc.Paths["/service"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Items.Value.AllOf[0].Value.Properties["created_at"].Value.Type)
-}
diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go
deleted file mode 100644
index 8fee2f4c2..000000000
--- a/openapi3/loader_read_from_uri_func_test.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package openapi3
-
-import (
- "fmt"
- "io/ioutil"
- "net/url"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestLoaderReadFromURIFunc(t *testing.T) {
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- loader.ReadFromURIFunc = func(loader *Loader, url *url.URL) ([]byte, error) {
- return ioutil.ReadFile(filepath.Join("testdata", url.Path))
- }
- doc, err := loader.LoadFromFile("recursiveRef/openapi.yml")
- require.NoError(t, err)
- require.NotNil(t, doc)
- require.NoError(t, doc.Validate(loader.Context))
- require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example)
-}
-
-type multipleSourceLoaderExample struct {
- Sources map[string][]byte
-}
-
-func (l *multipleSourceLoaderExample) LoadFromURI(
- loader *Loader,
- location *url.URL,
-) ([]byte, error) {
- source := l.resolveSourceFromURI(location)
- if source == nil {
- return nil, fmt.Errorf("unsupported URI: %q", location.String())
- }
- return source, nil
-}
-
-func (l *multipleSourceLoaderExample) resolveSourceFromURI(location fmt.Stringer) []byte {
- return l.Sources[location.String()]
-}
-
-func TestResolveSchemaExternalRef(t *testing.T) {
- rootLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "spec.json"}
- externalLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "external.json"}
- rootSpec := []byte(fmt.Sprintf(
- `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Root":{"allOf":[{"$ref":"%s#/components/schemas/External"}]}}}}`,
- externalLocation.String(),
- ))
- externalSpec := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"External Spec"},"paths":{},"components":{"schemas":{"External":{"type":"string"}}}}`)
- multipleSourceLoader := &multipleSourceLoaderExample{
- Sources: map[string][]byte{
- rootLocation.String(): rootSpec,
- externalLocation.String(): externalSpec,
- },
- }
- loader := &Loader{
- IsExternalRefsAllowed: true,
- ReadFromURIFunc: multipleSourceLoader.LoadFromURI,
- }
-
- doc, err := loader.LoadFromURI(rootLocation)
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- refRootVisited := doc.Components.Schemas["Root"].Value.AllOf[0]
- require.Equal(t, fmt.Sprintf("%s#/components/schemas/External", externalLocation.String()), refRootVisited.Ref)
- require.NotNil(t, refRootVisited.Value)
-}
diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go
deleted file mode 100644
index 924cb6be8..000000000
--- a/openapi3/loader_recursive_ref_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestLoaderSupportsRecursiveReference(t *testing.T) {
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml")
- require.NoError(t, err)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
- require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example)
- require.Equal(t, "ErrorDetails", doc.Paths["/foo"].Get.Responses.Get(400).Value.Content.Get("application/json").Schema.Value.Title)
- require.Equal(t, "ErrorDetails", doc.Paths["/double-ref-foo"].Get.Responses.Get(400).Value.Content.Get("application/json").Schema.Value.Title)
-}
-
-func TestIssue447(t *testing.T) {
- loader := NewLoader()
- doc, err := loader.LoadFromData([]byte(`
-openapi: 3.0.1
-info:
- title: Recursive refs example
- version: "1.0"
-paths: {}
-components:
- schemas:
- Complex:
- type: object
- properties:
- parent:
- $ref: '#/components/schemas/Complex'
-`))
- require.NoError(t, err)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
- require.Equal(t, "object", doc.Components.
- // Complex
- Schemas["Complex"].
- // parent
- Value.Properties["parent"].
- // parent
- Value.Properties["parent"].
- // parent
- Value.Properties["parent"].
- // type
- Value.Type)
-}
diff --git a/openapi3/loader_uri_reader.go b/openapi3/loader_uri_reader.go
deleted file mode 100644
index 92ac043f9..000000000
--- a/openapi3/loader_uri_reader.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package openapi3
-
-import (
- "errors"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "path/filepath"
- "sync"
-)
-
-// ReadFromURIFunc defines a function which reads the contents of a resource
-// located at a URI.
-type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
-
-var uriMu = &sync.RWMutex{}
-
-// ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a
-// given URI.
-var ErrURINotSupported = errors.New("unsupported URI")
-
-// ReadFromURIs returns a ReadFromURIFunc which tries to read a URI using the
-// given reader functions, in the same order. If a reader function does not
-// support the URI and returns ErrURINotSupported, the next function is checked
-// until a match is found, or the URI is not supported by any.
-func ReadFromURIs(readers ...ReadFromURIFunc) ReadFromURIFunc {
- return func(loader *Loader, url *url.URL) ([]byte, error) {
- for i := range readers {
- buf, err := readers[i](loader, url)
- if err == ErrURINotSupported {
- continue
- } else if err != nil {
- return nil, err
- }
- return buf, nil
- }
- return nil, ErrURINotSupported
- }
-}
-
-// DefaultReadFromURI returns a caching ReadFromURIFunc which can read remote
-// HTTP URIs and local file URIs.
-var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile))
-
-// ReadFromHTTP returns a ReadFromURIFunc which uses the given http.Client to
-// read the contents from a remote HTTP URI. This client may be customized to
-// implement timeouts, RFC 7234 caching, etc.
-func ReadFromHTTP(cl *http.Client) ReadFromURIFunc {
- return func(loader *Loader, location *url.URL) ([]byte, error) {
- if location.Scheme == "" || location.Host == "" {
- return nil, ErrURINotSupported
- }
- req, err := http.NewRequest("GET", location.String(), nil)
- if err != nil {
- return nil, err
- }
- resp, err := cl.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if resp.StatusCode > 399 {
- return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode)
- }
- return ioutil.ReadAll(resp.Body)
- }
-}
-
-// ReadFromFile is a ReadFromURIFunc which reads local file URIs.
-func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) {
- if location.Host != "" {
- return nil, ErrURINotSupported
- }
- if location.Scheme != "" && location.Scheme != "file" {
- return nil, ErrURINotSupported
- }
- return ioutil.ReadFile(location.Path)
-}
-
-// URIMapCache returns a ReadFromURIFunc that caches the contents read from URI
-// locations in a simple map. This cache implementation is suitable for
-// short-lived processes such as command-line tools which process OpenAPI
-// documents.
-func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc {
- cache := map[string][]byte{}
- return func(loader *Loader, location *url.URL) (buf []byte, err error) {
- if location.Scheme == "" || location.Scheme == "file" {
- if !filepath.IsAbs(location.Path) {
- // Do not cache relative file paths; this can cause trouble if
- // the current working directory changes when processing
- // multiple top-level documents.
- return reader(loader, location)
- }
- }
- uri := location.String()
- var ok bool
- uriMu.RLock()
- if buf, ok = cache[uri]; ok {
- uriMu.RUnlock()
- return
- }
- uriMu.RUnlock()
- if buf, err = reader(loader, location); err != nil {
- return
- }
- uriMu.Lock()
- defer uriMu.Unlock()
- cache[uri] = buf
- return
- }
-}
diff --git a/openapi3/media_type.go b/openapi3/media_type.go
index 2a9b4721c..3b8be81c7 100644
--- a/openapi3/media_type.go
+++ b/openapi3/media_type.go
@@ -2,27 +2,20 @@ package openapi3
import (
"context"
- "encoding/json"
- "errors"
- "fmt"
- "sort"
- "github.com/go-openapi/jsonpointer"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
// MediaType is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object
type MediaType struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
+ ExtensionProps
- Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
- Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
- Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"`
- Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"`
+ Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
+ Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
+ Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"`
+ Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"`
}
-var _ jsonpointer.JSONPointable = (*MediaType)(nil)
-
func NewMediaType() *MediaType {
return &MediaType{}
}
@@ -31,7 +24,9 @@ func (mediaType *MediaType) WithSchema(schema *Schema) *MediaType {
if schema == nil {
mediaType.Schema = nil
} else {
- mediaType.Schema = &SchemaRef{Value: schema}
+ mediaType.Schema = &SchemaRef{
+ Value: schema,
+ }
}
return mediaType
}
@@ -63,105 +58,22 @@ func (mediaType *MediaType) WithEncoding(name string, enc *Encoding) *MediaType
return mediaType
}
-// MarshalJSON returns the JSON encoding of MediaType.
-func (mediaType MediaType) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 4+len(mediaType.Extensions))
- for k, v := range mediaType.Extensions {
- m[k] = v
- }
- if x := mediaType.Schema; x != nil {
- m["schema"] = x
- }
- if x := mediaType.Example; x != nil {
- m["example"] = x
- }
- if x := mediaType.Examples; len(x) != 0 {
- m["examples"] = x
- }
- if x := mediaType.Encoding; len(x) != 0 {
- m["encoding"] = x
- }
- return json.Marshal(m)
+func (mediaType *MediaType) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(mediaType)
}
-// UnmarshalJSON sets MediaType to a copy of data.
func (mediaType *MediaType) UnmarshalJSON(data []byte) error {
- type MediaTypeBis MediaType
- var x MediaTypeBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "schema")
- delete(x.Extensions, "example")
- delete(x.Extensions, "examples")
- delete(x.Extensions, "encoding")
- *mediaType = MediaType(x)
- return nil
+ return jsoninfo.UnmarshalStrictStruct(data, mediaType)
}
-// Validate returns an error if MediaType does not comply with the OpenAPI spec.
-func (mediaType *MediaType) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (mediaType *MediaType) Validate(c context.Context) error {
if mediaType == nil {
return nil
}
if schema := mediaType.Schema; schema != nil {
- if err := schema.Validate(ctx); err != nil {
+ if err := schema.Validate(c); err != nil {
return err
}
-
- if mediaType.Example != nil && mediaType.Examples != nil {
- return errors.New("example and examples are mutually exclusive")
- }
-
- if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled {
- if example := mediaType.Example; example != nil {
- if err := validateExampleValue(ctx, example, schema.Value); err != nil {
- return fmt.Errorf("invalid example: %w", err)
- }
- }
-
- if examples := mediaType.Examples; examples != nil {
- names := make([]string, 0, len(examples))
- for name := range examples {
- names = append(names, name)
- }
- sort.Strings(names)
- for _, k := range names {
- v := examples[k]
- if err := v.Validate(ctx); err != nil {
- return fmt.Errorf("example %s: %w", k, err)
- }
- if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
- return fmt.Errorf("example %s: %w", k, err)
- }
- }
- }
- }
- }
-
- return validateExtensions(ctx, mediaType.Extensions)
-}
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (mediaType MediaType) JSONLookup(token string) (interface{}, error) {
- switch token {
- case "schema":
- if mediaType.Schema != nil {
- if mediaType.Schema.Ref != "" {
- return &Ref{Ref: mediaType.Schema.Ref}, nil
- }
- return mediaType.Schema.Value, nil
- }
- case "example":
- return mediaType.Example, nil
- case "examples":
- return mediaType.Examples, nil
- case "encoding":
- return mediaType.Encoding, nil
}
- v, _, err := jsonpointer.GetForToken(mediaType.Extensions, token)
- return v, err
+ return nil
}
diff --git a/openapi3/media_type_test.go b/openapi3/media_type_test.go
index 099c4b667..9d5092802 100644
--- a/openapi3/media_type_test.go
+++ b/openapi3/media_type_test.go
@@ -1,10 +1,11 @@
-package openapi3
+package openapi3_test
import (
"context"
"encoding/json"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
@@ -15,13 +16,13 @@ func TestMediaTypeJSON(t *testing.T) {
require.NotEmpty(t, data)
t.Log("Unmarshal *openapi3.MediaType from JSON")
- docA := &MediaType{}
+ docA := &openapi3.MediaType{}
err = json.Unmarshal(mediaTypeJSON, &docA)
require.NoError(t, err)
require.NotEmpty(t, data)
t.Log("Validate *openapi3.MediaType")
- err = docA.Validate(context.Background())
+ err = docA.Validate(context.TODO())
require.NoError(t, err)
t.Log("Ensure representations match")
@@ -51,22 +52,22 @@ var mediaTypeJSON = []byte(`
}
`)
-func mediaType() *MediaType {
+func mediaType() *openapi3.MediaType {
example := map[string]string{"name": "Some example"}
- return &MediaType{
- Schema: &SchemaRef{
- Value: &Schema{
+ return &openapi3.MediaType{
+ Schema: &openapi3.SchemaRef{
+ Value: &openapi3.Schema{
Description: "Some schema",
},
},
- Encoding: map[string]*Encoding{
+ Encoding: map[string]*openapi3.Encoding{
"someEncoding": {
ContentType: "application/xml; charset=utf-8",
},
},
- Examples: map[string]*ExampleRef{
+ Examples: map[string]*openapi3.ExampleRef{
"someExample": {
- Value: NewExample(example),
+ Value: openapi3.NewExample(example),
},
},
}
diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go
deleted file mode 100644
index 8b8f71bb7..000000000
--- a/openapi3/openapi3.go
+++ /dev/null
@@ -1,156 +0,0 @@
-package openapi3
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
-)
-
-// T is the root of an OpenAPI v3 document
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#openapi-object
-type T struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- OpenAPI string `json:"openapi" yaml:"openapi"` // Required
- Components *Components `json:"components,omitempty" yaml:"components,omitempty"`
- Info *Info `json:"info" yaml:"info"` // Required
- Paths Paths `json:"paths" yaml:"paths"` // Required
- Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"`
- Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"`
- Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"`
- ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
-
- visited visitedComponent
-}
-
-// MarshalJSON returns the JSON encoding of T.
-func (doc T) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 4+len(doc.Extensions))
- for k, v := range doc.Extensions {
- m[k] = v
- }
- m["openapi"] = doc.OpenAPI
- if x := doc.Components; x != nil {
- m["components"] = x
- }
- m["info"] = doc.Info
- m["paths"] = doc.Paths
- if x := doc.Security; len(x) != 0 {
- m["security"] = x
- }
- if x := doc.Servers; len(x) != 0 {
- m["servers"] = x
- }
- if x := doc.Tags; len(x) != 0 {
- m["tags"] = x
- }
- if x := doc.ExternalDocs; x != nil {
- m["externalDocs"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets T to a copy of data.
-func (doc *T) UnmarshalJSON(data []byte) error {
- type TBis T
- var x TBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "openapi")
- delete(x.Extensions, "components")
- delete(x.Extensions, "info")
- delete(x.Extensions, "paths")
- delete(x.Extensions, "security")
- delete(x.Extensions, "servers")
- delete(x.Extensions, "tags")
- delete(x.Extensions, "externalDocs")
- *doc = T(x)
- return nil
-}
-
-func (doc *T) AddOperation(path string, method string, operation *Operation) {
- if doc.Paths == nil {
- doc.Paths = make(Paths)
- }
- pathItem := doc.Paths[path]
- if pathItem == nil {
- pathItem = &PathItem{}
- doc.Paths[path] = pathItem
- }
- pathItem.SetOperation(method, operation)
-}
-
-func (doc *T) AddServer(server *Server) {
- doc.Servers = append(doc.Servers, server)
-}
-
-// Validate returns an error if T does not comply with the OpenAPI spec.
-// Validations Options can be provided to modify the validation behavior.
-func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if doc.OpenAPI == "" {
- return errors.New("value of openapi must be a non-empty string")
- }
-
- var wrap func(error) error
- // NOTE: only mention info/components/paths/... key in this func's errors.
-
- wrap = func(e error) error { return fmt.Errorf("invalid components: %w", e) }
- if v := doc.Components; v != nil {
- if err := v.Validate(ctx); err != nil {
- return wrap(err)
- }
- }
-
- wrap = func(e error) error { return fmt.Errorf("invalid info: %w", e) }
- if v := doc.Info; v != nil {
- if err := v.Validate(ctx); err != nil {
- return wrap(err)
- }
- } else {
- return wrap(errors.New("must be an object"))
- }
-
- wrap = func(e error) error { return fmt.Errorf("invalid paths: %w", e) }
- if v := doc.Paths; v != nil {
- if err := v.Validate(ctx); err != nil {
- return wrap(err)
- }
- } else {
- return wrap(errors.New("must be an object"))
- }
-
- wrap = func(e error) error { return fmt.Errorf("invalid security: %w", e) }
- if v := doc.Security; v != nil {
- if err := v.Validate(ctx); err != nil {
- return wrap(err)
- }
- }
-
- wrap = func(e error) error { return fmt.Errorf("invalid servers: %w", e) }
- if v := doc.Servers; v != nil {
- if err := v.Validate(ctx); err != nil {
- return wrap(err)
- }
- }
-
- wrap = func(e error) error { return fmt.Errorf("invalid tags: %w", e) }
- if v := doc.Tags; v != nil {
- if err := v.Validate(ctx); err != nil {
- return wrap(err)
- }
- }
-
- wrap = func(e error) error { return fmt.Errorf("invalid external docs: %w", e) }
- if v := doc.ExternalDocs; v != nil {
- if err := v.Validate(ctx); err != nil {
- return wrap(err)
- }
- }
-
- return validateExtensions(ctx, doc.Extensions)
-}
diff --git a/openapi3/operation.go b/openapi3/operation.go
index 645c0805f..0841863be 100644
--- a/openapi3/operation.go
+++ b/openapi3/operation.go
@@ -2,18 +2,15 @@ package openapi3
import (
"context"
- "encoding/json"
"errors"
- "fmt"
"strconv"
- "github.com/go-openapi/jsonpointer"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
// Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object
type Operation struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
+ ExtensionProps
// Optional tags for documentation.
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
@@ -37,7 +34,7 @@ type Operation struct {
Responses Responses `json:"responses" yaml:"responses"` // Required
// Optional callbacks
- Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"`
+ Callbacks map[string]*CallbackRef `json:"callbacks,omitempty" yaml:"callbacks,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
@@ -50,115 +47,16 @@ type Operation struct {
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
}
-var _ jsonpointer.JSONPointable = (*Operation)(nil)
-
func NewOperation() *Operation {
return &Operation{}
}
-// MarshalJSON returns the JSON encoding of Operation.
-func (operation Operation) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 12+len(operation.Extensions))
- for k, v := range operation.Extensions {
- m[k] = v
- }
- if x := operation.Tags; len(x) != 0 {
- m["tags"] = x
- }
- if x := operation.Summary; x != "" {
- m["summary"] = x
- }
- if x := operation.Description; x != "" {
- m["description"] = x
- }
- if x := operation.OperationID; x != "" {
- m["operationId"] = x
- }
- if x := operation.Parameters; len(x) != 0 {
- m["parameters"] = x
- }
- if x := operation.RequestBody; x != nil {
- m["requestBody"] = x
- }
- m["responses"] = operation.Responses
- if x := operation.Callbacks; len(x) != 0 {
- m["callbacks"] = x
- }
- if x := operation.Deprecated; x {
- m["deprecated"] = x
- }
- if x := operation.Security; x != nil {
- m["security"] = x
- }
- if x := operation.Servers; x != nil {
- m["servers"] = x
- }
- if x := operation.ExternalDocs; x != nil {
- m["externalDocs"] = x
- }
- return json.Marshal(m)
+func (operation *Operation) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(operation)
}
-// UnmarshalJSON sets Operation to a copy of data.
func (operation *Operation) UnmarshalJSON(data []byte) error {
- type OperationBis Operation
- var x OperationBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "tags")
- delete(x.Extensions, "summary")
- delete(x.Extensions, "description")
- delete(x.Extensions, "operationId")
- delete(x.Extensions, "parameters")
- delete(x.Extensions, "requestBody")
- delete(x.Extensions, "responses")
- delete(x.Extensions, "callbacks")
- delete(x.Extensions, "deprecated")
- delete(x.Extensions, "security")
- delete(x.Extensions, "servers")
- delete(x.Extensions, "externalDocs")
- *operation = Operation(x)
- return nil
-}
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (operation Operation) JSONLookup(token string) (interface{}, error) {
- switch token {
- case "requestBody":
- if operation.RequestBody != nil {
- if operation.RequestBody.Ref != "" {
- return &Ref{Ref: operation.RequestBody.Ref}, nil
- }
- return operation.RequestBody.Value, nil
- }
- case "tags":
- return operation.Tags, nil
- case "summary":
- return operation.Summary, nil
- case "description":
- return operation.Description, nil
- case "operationID":
- return operation.OperationID, nil
- case "parameters":
- return operation.Parameters, nil
- case "responses":
- return operation.Responses, nil
- case "callbacks":
- return operation.Callbacks, nil
- case "deprecated":
- return operation.Deprecated, nil
- case "security":
- return operation.Security, nil
- case "servers":
- return operation.Servers, nil
- case "externalDocs":
- return operation.ExternalDocs, nil
- }
-
- v, _, err := jsonpointer.GetForToken(operation.Extensions, token)
- return v, err
+ return jsoninfo.UnmarshalStrictStruct(data, operation)
}
func (operation *Operation) AddParameter(p *Parameter) {
@@ -173,44 +71,34 @@ func (operation *Operation) AddResponse(status int, response *Response) {
responses = NewResponses()
operation.Responses = responses
}
- code := "default"
- if status != 0 {
- code = strconv.FormatInt(int64(status), 10)
- }
- responses[code] = &ResponseRef{
- Value: response,
+ if status == 0 {
+ responses["default"] = &ResponseRef{
+ Value: response,
+ }
+ } else {
+ responses[strconv.FormatInt(int64(status), 10)] = &ResponseRef{
+ Value: response,
+ }
}
}
-// Validate returns an error if Operation does not comply with the OpenAPI spec.
-func (operation *Operation) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (operation *Operation) Validate(c context.Context) error {
if v := operation.Parameters; v != nil {
- if err := v.Validate(ctx); err != nil {
+ if err := v.Validate(c); err != nil {
return err
}
}
-
if v := operation.RequestBody; v != nil {
- if err := v.Validate(ctx); err != nil {
+ if err := v.Validate(c); err != nil {
return err
}
}
-
if v := operation.Responses; v != nil {
- if err := v.Validate(ctx); err != nil {
+ if err := v.Validate(c); err != nil {
return err
}
} else {
- return errors.New("value of responses must be an object")
- }
-
- if v := operation.ExternalDocs; v != nil {
- if err := v.Validate(ctx); err != nil {
- return fmt.Errorf("invalid external docs: %w", err)
- }
+ return errors.New("Variable 'Responses' must be a JSON object")
}
-
- return validateExtensions(ctx, operation.Extensions)
+ return nil
}
diff --git a/openapi3/operation_test.go b/openapi3/operation_test.go
index 50684a3ae..22f325ec5 100644
--- a/openapi3/operation_test.go
+++ b/openapi3/operation_test.go
@@ -53,7 +53,7 @@ func TestOperationValidation(t *testing.T) {
{
"when no Responses object is provided",
operationWithoutResponses(),
- errors.New("value of responses must be an object"),
+ errors.New("Variable 'Responses' must be a JSON object"),
},
{
"when a Responses object is provided",
diff --git a/openapi3/parameter.go b/openapi3/parameter.go
index ec1893e9a..30ec868b6 100644
--- a/openapi3/parameter.go
+++ b/openapi3/parameter.go
@@ -2,56 +2,15 @@ package openapi3
import (
"context"
- "encoding/json"
"errors"
"fmt"
- "sort"
- "strconv"
- "github.com/go-openapi/jsonpointer"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-type ParametersMap map[string]*ParameterRef
-
-var _ jsonpointer.JSONPointable = (*ParametersMap)(nil)
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (p ParametersMap) JSONLookup(token string) (interface{}, error) {
- ref, ok := p[token]
- if ref == nil || ok == false {
- return nil, fmt.Errorf("object has no field %q", token)
- }
-
- if ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
// Parameters is specified by OpenAPI/Swagger 3.0 standard.
type Parameters []*ParameterRef
-var _ jsonpointer.JSONPointable = (*Parameters)(nil)
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (p Parameters) JSONLookup(token string) (interface{}, error) {
- index, err := strconv.Atoi(token)
- if err != nil {
- return nil, err
- }
-
- if index < 0 || index >= len(p) {
- return nil, fmt.Errorf("index %d out of bounds of array of length %d", index, len(p))
- }
-
- ref := p[index]
-
- if ref != nil && ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
func NewParameters() Parameters {
return make(Parameters, 0, 4)
}
@@ -67,49 +26,46 @@ func (parameters Parameters) GetByInAndName(in string, name string) *Parameter {
return nil
}
-// Validate returns an error if Parameters does not comply with the OpenAPI spec.
-func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- dupes := make(map[string]struct{})
- for _, parameterRef := range parameters {
- if v := parameterRef.Value; v != nil {
- key := v.In + ":" + v.Name
- if _, ok := dupes[key]; ok {
- return fmt.Errorf("more than one %q parameter has name %q", v.In, v.Name)
- }
- dupes[key] = struct{}{}
- }
-
- if err := parameterRef.Validate(ctx); err != nil {
+func (parameters Parameters) Validate(c context.Context) error {
+ m := make(map[string]struct{})
+ for _, item := range parameters {
+ if err := item.Validate(c); err != nil {
return err
}
+ if v := item.Value; v != nil {
+ in := v.In
+ name := v.Name
+ key := in + ":" + name
+ if _, exists := m[key]; exists {
+ return fmt.Errorf("More than one '%s' parameter has name '%s'", in, name)
+ }
+ m[key] = struct{}{}
+ if err := item.Validate(c); err != nil {
+ return err
+ }
+ }
}
return nil
}
// Parameter is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object
type Parameter struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Name string `json:"name,omitempty" yaml:"name,omitempty"`
- In string `json:"in,omitempty" yaml:"in,omitempty"`
- Description string `json:"description,omitempty" yaml:"description,omitempty"`
- Style string `json:"style,omitempty" yaml:"style,omitempty"`
- Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"`
- AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
- AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"`
- Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
- Required bool `json:"required,omitempty" yaml:"required,omitempty"`
- Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
- Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
- Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"`
- Content Content `json:"content,omitempty" yaml:"content,omitempty"`
+ ExtensionProps
+ Name string `json:"name,omitempty" yaml:"name,omitempty"`
+ In string `json:"in,omitempty" yaml:"in,omitempty"`
+ Description string `json:"description,omitempty" yaml:"description,omitempty"`
+ Style string `json:"style,omitempty" yaml:"style,omitempty"`
+ Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"`
+ AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
+ AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"`
+ Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
+ Required bool `json:"required,omitempty" yaml:"required,omitempty"`
+ Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
+ Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
+ Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"`
+ Content Content `json:"content,omitempty" yaml:"content,omitempty"`
}
-var _ jsonpointer.JSONPointable = (*Parameter)(nil)
-
const (
ParameterInPath = "path"
ParameterInQuery = "query"
@@ -167,121 +123,12 @@ func (parameter *Parameter) WithSchema(value *Schema) *Parameter {
return parameter
}
-// MarshalJSON returns the JSON encoding of Parameter.
-func (parameter Parameter) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 13+len(parameter.Extensions))
- for k, v := range parameter.Extensions {
- m[k] = v
- }
-
- if x := parameter.Name; x != "" {
- m["name"] = x
- }
- if x := parameter.In; x != "" {
- m["in"] = x
- }
- if x := parameter.Description; x != "" {
- m["description"] = x
- }
- if x := parameter.Style; x != "" {
- m["style"] = x
- }
- if x := parameter.Explode; x != nil {
- m["explode"] = x
- }
- if x := parameter.AllowEmptyValue; x {
- m["allowEmptyValue"] = x
- }
- if x := parameter.AllowReserved; x {
- m["allowReserved"] = x
- }
- if x := parameter.Deprecated; x {
- m["deprecated"] = x
- }
- if x := parameter.Required; x {
- m["required"] = x
- }
- if x := parameter.Schema; x != nil {
- m["schema"] = x
- }
- if x := parameter.Example; x != nil {
- m["example"] = x
- }
- if x := parameter.Examples; len(x) != 0 {
- m["examples"] = x
- }
- if x := parameter.Content; len(x) != 0 {
- m["content"] = x
- }
-
- return json.Marshal(m)
+func (parameter *Parameter) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(parameter)
}
-// UnmarshalJSON sets Parameter to a copy of data.
func (parameter *Parameter) UnmarshalJSON(data []byte) error {
- type ParameterBis Parameter
- var x ParameterBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
-
- delete(x.Extensions, "name")
- delete(x.Extensions, "in")
- delete(x.Extensions, "description")
- delete(x.Extensions, "style")
- delete(x.Extensions, "explode")
- delete(x.Extensions, "allowEmptyValue")
- delete(x.Extensions, "allowReserved")
- delete(x.Extensions, "deprecated")
- delete(x.Extensions, "required")
- delete(x.Extensions, "schema")
- delete(x.Extensions, "example")
- delete(x.Extensions, "examples")
- delete(x.Extensions, "content")
-
- *parameter = Parameter(x)
- return nil
-}
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (parameter Parameter) JSONLookup(token string) (interface{}, error) {
- switch token {
- case "schema":
- if parameter.Schema != nil {
- if parameter.Schema.Ref != "" {
- return &Ref{Ref: parameter.Schema.Ref}, nil
- }
- return parameter.Schema.Value, nil
- }
- case "name":
- return parameter.Name, nil
- case "in":
- return parameter.In, nil
- case "description":
- return parameter.Description, nil
- case "style":
- return parameter.Style, nil
- case "explode":
- return parameter.Explode, nil
- case "allowEmptyValue":
- return parameter.AllowEmptyValue, nil
- case "allowReserved":
- return parameter.AllowReserved, nil
- case "deprecated":
- return parameter.Deprecated, nil
- case "required":
- return parameter.Required, nil
- case "example":
- return parameter.Example, nil
- case "examples":
- return parameter.Examples, nil
- case "content":
- return parameter.Content, nil
- }
-
- v, _, err := jsonpointer.GetForToken(parameter.Extensions, token)
- return v, err
+ return jsoninfo.UnmarshalStrictStruct(data, parameter)
}
// SerializationMethod returns a parameter's serialization method.
@@ -314,12 +161,9 @@ func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error)
}
}
-// Validate returns an error if Parameter does not comply with the OpenAPI spec.
-func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (parameter *Parameter) Validate(c context.Context) error {
if parameter.Name == "" {
- return errors.New("parameter name can't be blank")
+ return errors.New("Parameter name can't be blank")
}
in := parameter.In
switch in {
@@ -329,11 +173,7 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti
ParameterInHeader,
ParameterInCookie:
default:
- return fmt.Errorf("parameter can't have 'in' value %q", parameter.In)
- }
-
- if in == ParameterInPath && !parameter.Required {
- return fmt.Errorf("path parameter %q must be required", parameter.Name)
+ return fmt.Errorf("Parameter can't have 'in' value '%s'", parameter.In)
}
// Validate a parameter's serialization method.
@@ -366,53 +206,27 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti
smSupported = true
}
if !smSupported {
- e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in)
- return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, e)
+ e := fmt.Errorf("Serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in)
+ return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name, e)
}
- if (parameter.Schema == nil) == (parameter.Content == nil) {
- e := errors.New("parameter must contain exactly one of content and schema")
- return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, e)
+ if parameter.Schema != nil && parameter.Content != nil {
+ return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name,
+ errors.New("Cannot contain both schema and content in a parameter"))
}
-
- if content := parameter.Content; content != nil {
- if err := content.Validate(ctx); err != nil {
- return fmt.Errorf("parameter %q content is invalid: %w", parameter.Name, err)
- }
+ if parameter.Schema == nil && parameter.Content == nil {
+ return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name,
+ errors.New("A parameter MUST contain either a schema property, or a content property"))
}
-
if schema := parameter.Schema; schema != nil {
- if err := schema.Validate(ctx); err != nil {
- return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, err)
- }
- if parameter.Example != nil && parameter.Examples != nil {
- return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name)
+ if err := schema.Validate(c); err != nil {
+ return fmt.Errorf("Parameter '%v' schema is invalid: %v", parameter.Name, err)
}
-
- if vo := getValidationOptions(ctx); vo.examplesValidationDisabled {
- return nil
- }
- if example := parameter.Example; example != nil {
- if err := validateExampleValue(ctx, example, schema.Value); err != nil {
- return fmt.Errorf("invalid example: %w", err)
- }
- } else if examples := parameter.Examples; examples != nil {
- names := make([]string, 0, len(examples))
- for name := range examples {
- names = append(names, name)
- }
- sort.Strings(names)
- for _, k := range names {
- v := examples[k]
- if err := v.Validate(ctx); err != nil {
- return fmt.Errorf("%s: %w", k, err)
- }
- if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
- return fmt.Errorf("%s: %w", k, err)
- }
- }
+ }
+ if content := parameter.Content; content != nil {
+ if err := content.Validate(c); err != nil {
+ return fmt.Errorf("Parameter content is invalid: %v", err)
}
}
-
- return validateExtensions(ctx, parameter.Extensions)
+ return nil
}
diff --git a/openapi3/parameter_issue223_test.go b/openapi3/parameter_issue223_test.go
deleted file mode 100644
index 9b86954f4..000000000
--- a/openapi3/parameter_issue223_test.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package openapi3
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestPathParametersMatchPath(t *testing.T) {
- spec := `
-openapi: "3.0.0"
-info:
- version: 1.0.0
- title: Swagger Petstore
- license:
- name: MIT
-servers:
- - url: http://petstore.swagger.io/v1
-paths:
- /pets:
- get:
- summary: List all pets
- operationId: listPets
- tags:
- - pets
-
- responses:
- '200':
- description: A paged array of pets
- headers:
- x-next:
- description: A link to the next page of responses
- schema:
- type: string
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Pets"
- default:
- description: unexpected error
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Error"
- post:
- summary: Create a pet
- operationId: createPets
- tags:
- - pets
- responses:
- '201':
- description: Null response
- default:
- description: unexpected error
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Error"
- /pets/{petId}:
- get:
- summary: Info for a specific pet
- operationId: showPetById
- tags:
- - pets
- # <------------------ no parameters
- responses:
- '200':
- description: Expected response to a valid request
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Pet"
- default:
- description: unexpected error
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Error"
-components:
- schemas:
- Pet:
- type: object
- required:
- - id
- - name
- properties:
- id:
- type: integer
- format: int64
- name:
- type: string
- tag:
- type: string
- Pets:
- type: array
- items:
- $ref: "#/components/schemas/Pet"
- Error:
- type: object
- required:
- - code
- - message
- properties:
- code:
- type: integer
- format: int32
- message:
- type: string
-`
-
- doc, err := NewLoader().LoadFromData([]byte(spec))
- require.NoError(t, err)
- err = doc.Validate(context.Background())
- require.EqualError(t, err, `invalid paths: operation GET /pets/{petId} must define exactly all path parameters (missing: [petId])`)
-}
diff --git a/openapi3/path_item.go b/openapi3/path_item.go
index fab75d93c..2ed578aa0 100644
--- a/openapi3/path_item.go
+++ b/openapi3/path_item.go
@@ -2,17 +2,14 @@ package openapi3
import (
"context"
- "encoding/json"
"fmt"
"net/http"
- "sort"
+
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-// PathItem is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#path-item-object
type PathItem struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
+ ExtensionProps
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
@@ -29,86 +26,16 @@ type PathItem struct {
Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"`
}
-// MarshalJSON returns the JSON encoding of PathItem.
-func (pathItem PathItem) MarshalJSON() ([]byte, error) {
- if ref := pathItem.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
-
- m := make(map[string]interface{}, 13+len(pathItem.Extensions))
- for k, v := range pathItem.Extensions {
- m[k] = v
- }
- if x := pathItem.Summary; x != "" {
- m["summary"] = x
- }
- if x := pathItem.Description; x != "" {
- m["description"] = x
- }
- if x := pathItem.Connect; x != nil {
- m["connect"] = x
- }
- if x := pathItem.Delete; x != nil {
- m["delete"] = x
- }
- if x := pathItem.Get; x != nil {
- m["get"] = x
- }
- if x := pathItem.Head; x != nil {
- m["head"] = x
- }
- if x := pathItem.Options; x != nil {
- m["options"] = x
- }
- if x := pathItem.Patch; x != nil {
- m["patch"] = x
- }
- if x := pathItem.Post; x != nil {
- m["post"] = x
- }
- if x := pathItem.Put; x != nil {
- m["put"] = x
- }
- if x := pathItem.Trace; x != nil {
- m["trace"] = x
- }
- if x := pathItem.Servers; len(x) != 0 {
- m["servers"] = x
- }
- if x := pathItem.Parameters; len(x) != 0 {
- m["parameters"] = x
- }
- return json.Marshal(m)
+func (pathItem *PathItem) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(pathItem)
}
-// UnmarshalJSON sets PathItem to a copy of data.
func (pathItem *PathItem) UnmarshalJSON(data []byte) error {
- type PathItemBis PathItem
- var x PathItemBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "$ref")
- delete(x.Extensions, "summary")
- delete(x.Extensions, "description")
- delete(x.Extensions, "connect")
- delete(x.Extensions, "delete")
- delete(x.Extensions, "get")
- delete(x.Extensions, "head")
- delete(x.Extensions, "options")
- delete(x.Extensions, "patch")
- delete(x.Extensions, "post")
- delete(x.Extensions, "put")
- delete(x.Extensions, "trace")
- delete(x.Extensions, "servers")
- delete(x.Extensions, "parameters")
- *pathItem = PathItem(x)
- return nil
+ return jsoninfo.UnmarshalStrictStruct(data, pathItem)
}
func (pathItem *PathItem) Operations() map[string]*Operation {
- operations := make(map[string]*Operation)
+ operations := make(map[string]*Operation, 4)
if v := pathItem.Connect; v != nil {
operations[http.MethodConnect] = v
}
@@ -160,7 +87,7 @@ func (pathItem *PathItem) GetOperation(method string) *Operation {
case http.MethodTrace:
return pathItem.Trace
default:
- panic(fmt.Errorf("unsupported HTTP method %q", method))
+ panic(fmt.Errorf("Unsupported HTTP method '%s'", method))
}
}
@@ -185,27 +112,15 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) {
case http.MethodTrace:
pathItem.Trace = operation
default:
- panic(fmt.Errorf("unsupported HTTP method %q", method))
+ panic(fmt.Errorf("Unsupported HTTP method '%s'", method))
}
}
-// Validate returns an error if PathItem does not comply with the OpenAPI spec.
-func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- operations := pathItem.Operations()
-
- methods := make([]string, 0, len(operations))
- for method := range operations {
- methods = append(methods, method)
- }
- sort.Strings(methods)
- for _, method := range methods {
- operation := operations[method]
- if err := operation.Validate(ctx); err != nil {
- return fmt.Errorf("invalid operation %s: %v", method, err)
+func (pathItem *PathItem) Validate(c context.Context) error {
+ for _, operation := range pathItem.Operations() {
+ if err := operation.Validate(c); err != nil {
+ return err
}
}
-
- return validateExtensions(ctx, pathItem.Extensions)
+ return nil
}
diff --git a/openapi3/paths.go b/openapi3/paths.go
index 0986b0557..d738e9dac 100644
--- a/openapi3/paths.go
+++ b/openapi3/paths.go
@@ -3,153 +3,40 @@ package openapi3
import (
"context"
"fmt"
- "sort"
"strings"
)
-// Paths is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object
+// Paths is specified by OpenAPI/Swagger standard version 3.0.
type Paths map[string]*PathItem
-// Validate returns an error if Paths does not comply with the OpenAPI spec.
-func (paths Paths) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- normalizedPaths := make(map[string]string, len(paths))
-
- keys := make([]string, 0, len(paths))
- for key := range paths {
- keys = append(keys, key)
- }
- sort.Strings(keys)
- for _, path := range keys {
- pathItem := paths[path]
- if path == "" || path[0] != '/' {
- return fmt.Errorf("path %q does not start with a forward slash (/)", path)
- }
-
- if pathItem == nil {
- pathItem = &PathItem{}
- paths[path] = pathItem
+func (paths Paths) Validate(c context.Context) error {
+ normalizedPaths := make(map[string]string)
+ for path, pathItem := range paths {
+ normalizedPath := normalizePathKey(path)
+ if oldPath, exists := normalizedPaths[normalizedPath]; exists {
+ return fmt.Errorf("Conflicting paths '%v' and '%v'", path, oldPath)
}
-
- normalizedPath, _, varsInPath := normalizeTemplatedPath(path)
- if oldPath, ok := normalizedPaths[normalizedPath]; ok {
- return fmt.Errorf("conflicting paths %q and %q", path, oldPath)
+ if path == "" || path[0] != '/' {
+ return fmt.Errorf("Path '%v' does not start with '/'", path)
}
normalizedPaths[path] = path
-
- var commonParams []string
- for _, parameterRef := range pathItem.Parameters {
- if parameterRef != nil {
- if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
- commonParams = append(commonParams, parameter.Name)
- }
- }
- }
- operations := pathItem.Operations()
- methods := make([]string, 0, len(operations))
- for method := range operations {
- methods = append(methods, method)
+ if err := pathItem.Validate(c); err != nil {
+ return err
}
- sort.Strings(methods)
- for _, method := range methods {
- operation := operations[method]
- var setParams []string
- for _, parameterRef := range operation.Parameters {
- if parameterRef != nil {
- if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
- setParams = append(setParams, parameter.Name)
- }
- }
- }
- if expected := len(setParams) + len(commonParams); expected != len(varsInPath) {
- expected -= len(varsInPath)
- if expected < 0 {
- expected *= -1
- }
- missing := make(map[string]struct{}, expected)
- definedParams := append(setParams, commonParams...)
- for _, name := range definedParams {
- if _, ok := varsInPath[name]; !ok {
- missing[name] = struct{}{}
- }
- }
- for name := range varsInPath {
- got := false
- for _, othername := range definedParams {
- if othername == name {
- got = true
- break
- }
- }
- if !got {
- missing[name] = struct{}{}
- }
- }
- if len(missing) != 0 {
- missings := make([]string, 0, len(missing))
- for name := range missing {
- missings = append(missings, name)
- }
- return fmt.Errorf("operation %s %s must define exactly all path parameters (missing: %v)", method, path, missings)
- }
- }
- }
-
- if err := pathItem.Validate(ctx); err != nil {
- return fmt.Errorf("invalid path %s: %v", path, err)
- }
- }
-
- if err := paths.validateUniqueOperationIDs(); err != nil {
- return err
}
-
return nil
}
-// InMatchingOrder returns paths in the order they are matched against URLs.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object
-// When matching URLs, concrete (non-templated) paths would be matched
-// before their templated counterparts.
-func (paths Paths) InMatchingOrder() []string {
- // NOTE: sorting by number of variables ASC then by descending lexicographical
- // order seems to be a good heuristic.
- if paths == nil {
- return nil
- }
-
- vars := make(map[int][]string)
- max := 0
- for path := range paths {
- count := strings.Count(path, "}")
- vars[count] = append(vars[count], path)
- if count > max {
- max = count
- }
- }
-
- ordered := make([]string, 0, len(paths))
- for c := 0; c <= max; c++ {
- if ps, ok := vars[c]; ok {
- sort.Sort(sort.Reverse(sort.StringSlice(ps)))
- ordered = append(ordered, ps...)
- }
- }
- return ordered
-}
-
// Find returns a path that matches the key.
//
// The method ignores differences in template variable names (except possible "*" suffix).
//
// For example:
//
-// paths := openapi3.Paths {
-// "/person/{personName}": &openapi3.PathItem{},
-// }
-// pathItem := path.Find("/person/{name}")
+// paths := openapi3.Paths {
+// "/person/{personName}": &openapi3.PathItem{},
+// }
+// pathItem := path.Find("/person/{name}")
//
// would return the correct path item.
func (paths Paths) Find(key string) *PathItem {
@@ -159,85 +46,51 @@ func (paths Paths) Find(key string) *PathItem {
return pathItem
}
- normalizedPath, expected, _ := normalizeTemplatedPath(key)
+ // Use normalized keys
+ normalizedSearchedPath := normalizePathKey(key)
for path, pathItem := range paths {
- pathNormalized, got, _ := normalizeTemplatedPath(path)
- if got == expected && pathNormalized == normalizedPath {
+ normalizedPath := normalizePathKey(path)
+ if normalizedPath == normalizedSearchedPath {
return pathItem
}
}
return nil
}
-func (paths Paths) validateUniqueOperationIDs() error {
- operationIDs := make(map[string]string)
- for urlPath, pathItem := range paths {
- if pathItem == nil {
- continue
- }
- for httpMethod, operation := range pathItem.Operations() {
- if operation == nil || operation.OperationID == "" {
- continue
- }
- endpoint := httpMethod + " " + urlPath
- if endpointDup, ok := operationIDs[operation.OperationID]; ok {
- if endpoint > endpointDup { // For make error message a bit more deterministic. May be useful for tests.
- endpoint, endpointDup = endpointDup, endpoint
- }
- return fmt.Errorf("operations %q and %q have the same operation id %q",
- endpoint, endpointDup, operation.OperationID)
- }
- operationIDs[operation.OperationID] = endpoint
- }
- }
- return nil
-}
-
-func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) {
- if strings.IndexByte(path, '{') < 0 {
- return path, 0, nil
+func normalizePathKey(key string) string {
+ // If the argument has no path variables, return the argument
+ if strings.IndexByte(key, '{') < 0 {
+ return key
}
- var buffTpl strings.Builder
- buffTpl.Grow(len(path))
+ // Allocate buffer
+ buf := make([]byte, 0, len(key))
- var (
- cc rune
- count uint
- isVariable bool
- vars = make(map[string]struct{})
- buffVar strings.Builder
- )
- for i, c := range path {
+ // Visit each byte
+ isVariable := false
+ for i := 0; i < len(key); i++ {
+ c := key[i]
if isVariable {
if c == '}' {
- // End path variable
- isVariable = false
-
- vars[buffVar.String()] = struct{}{}
- buffVar = strings.Builder{}
-
+ // End path variables
// First append possible '*' before this character
// The character '}' will be appended
- if i > 0 && cc == '*' {
- buffTpl.WriteRune(cc)
+ if i > 0 && key[i-1] == '*' {
+ buf = append(buf, '*')
}
+ isVariable = false
} else {
- buffVar.WriteRune(c)
+ // Skip this character
continue
}
-
} else if c == '{' {
// Begin path variable
- isVariable = true
-
// The character '{' will be appended
- count++
+ isVariable = true
}
// Append the character
- buffTpl.WriteRune(c)
- cc = c
+ buf = append(buf, c)
}
- return buffTpl.String(), count, vars
+ return string(buf)
}
diff --git a/openapi3/paths_test.go b/openapi3/paths_test.go
deleted file mode 100644
index 4ff9fba00..000000000
--- a/openapi3/paths_test.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package openapi3
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestPathsValidate(t *testing.T) {
- tests := []struct {
- name string
- spec string
- wantErr string
- }{
- {
- name: "ok, empty paths",
- spec: `
-openapi: "3.0.0"
-info:
- version: 1.0.0
- title: Swagger Petstore
- license:
- name: MIT
-paths:
- /pets:
-`,
- },
- {
- name: "operation ids are not unique, same path",
- spec: `
-openapi: "3.0.0"
-info:
- version: 1.0.0
- title: Swagger Petstore
- license:
- name: MIT
-paths:
- /pets:
- post:
- operationId: createPet
- responses:
- 201:
- description: "entity created"
- delete:
- operationId: createPet
- responses:
- 204:
- description: "entity deleted"
-`,
- wantErr: `operations "DELETE /pets" and "POST /pets" have the same operation id "createPet"`,
- },
- {
- name: "operation ids are not unique, different paths",
- spec: `
-openapi: "3.0.0"
-info:
- version: 1.0.0
- title: Swagger Petstore
- license:
- name: MIT
-paths:
- /pets:
- post:
- operationId: createPet
- responses:
- 201:
- description: "entity created"
- /users:
- post:
- operationId: createPet
- responses:
- 201:
- description: "entity created"
-`,
- wantErr: `operations "POST /pets" and "POST /users" have the same operation id "createPet"`,
- },
- }
-
- for i := range tests {
- tt := tests[i]
- t.Run(tt.name, func(t *testing.T) {
- doc, err := NewLoader().LoadFromData([]byte(tt.spec))
- require.NoError(t, err)
-
- err = doc.Paths.Validate(context.Background())
- if tt.wantErr == "" {
- require.NoError(t, err)
- return
- }
- require.Equal(t, tt.wantErr, err.Error())
- })
- }
-}
diff --git a/openapi3/race_test.go b/openapi3/race_test.go
deleted file mode 100644
index c617cfe49..000000000
--- a/openapi3/race_test.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package openapi3_test
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestRaceyPatternSchema(t *testing.T) {
- schema := openapi3.Schema{
- Pattern: "^test|for|race|condition$",
- Type: "string",
- }
-
- err := schema.Validate(context.Background())
- require.NoError(t, err)
-
- visit := func() {
- err := schema.VisitJSONString("test")
- require.NoError(t, err)
- }
-
- go visit()
- visit()
-}
diff --git a/openapi3/ref.go b/openapi3/ref.go
deleted file mode 100644
index a937de4a5..000000000
--- a/openapi3/ref.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package openapi3
-
-// Ref is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object
-type Ref struct {
- Ref string `json:"$ref" yaml:"$ref"`
-}
diff --git a/openapi3/refs.go b/openapi3/refs.go
index 15f5179da..9790b4705 100644
--- a/openapi3/refs.go
+++ b/openapi3/refs.go
@@ -2,694 +2,198 @@ package openapi3
import (
"context"
- "encoding/json"
- "fmt"
- "sort"
- "github.com/go-openapi/jsonpointer"
- "github.com/perimeterx/marshmallow"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-// CallbackRef represents either a Callback or a $ref to a Callback.
-// When serializing and both fields are set, Ref is preferred over Value.
type CallbackRef struct {
Ref string
Value *Callback
- extra []string
}
-var _ jsonpointer.JSONPointable = (*CallbackRef)(nil)
-
-// MarshalYAML returns the YAML encoding of CallbackRef.
-func (x CallbackRef) MarshalYAML() (interface{}, error) {
- if ref := x.Ref; ref != "" {
- return &Ref{Ref: ref}, nil
- }
- return x.Value, nil
+func (value *CallbackRef) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalRef(value.Ref, value.Value)
}
-// MarshalJSON returns the JSON encoding of CallbackRef.
-func (x CallbackRef) MarshalJSON() ([]byte, error) {
- if ref := x.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
- return json.Marshal(x.Value)
-}
-
-// UnmarshalJSON sets CallbackRef to a copy of data.
-func (x *CallbackRef) UnmarshalJSON(data []byte) error {
- var refOnly Ref
- if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
- x.Ref = refOnly.Ref
- if len(extra) != 0 {
- x.extra = make([]string, 0, len(extra))
- for key := range extra {
- x.extra = append(x.extra, key)
- }
- sort.Strings(x.extra)
- }
- return nil
- }
- return json.Unmarshal(data, &x.Value)
-}
-
-// Validate returns an error if CallbackRef does not comply with the OpenAPI spec.
-func (x *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- if extra := x.extra; len(extra) != 0 {
- extras := make([]string, 0, len(extra))
- allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
- if allowed == nil {
- allowed = make(map[string]struct{}, 0)
- }
- for _, ex := range extra {
- if _, ok := allowed[ex]; !ok {
- extras = append(extras, ex)
- }
- }
- if len(extras) != 0 {
- return fmt.Errorf("extra sibling fields: %+v", extras)
- }
- }
- if v := x.Value; v != nil {
- return v.Validate(ctx)
- }
- return foundUnresolvedRef(x.Ref)
+func (value *CallbackRef) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (x *CallbackRef) JSONLookup(token string) (interface{}, error) {
- if token == "$ref" {
- return x.Ref, nil
+func (value *CallbackRef) Validate(c context.Context) error {
+ v := value.Value
+ if v == nil {
+ return foundUnresolvedRef(value.Ref)
}
- ptr, _, err := jsonpointer.GetForToken(x.Value, token)
- return ptr, err
+ return v.Validate(c)
}
-// ExampleRef represents either a Example or a $ref to a Example.
-// When serializing and both fields are set, Ref is preferred over Value.
type ExampleRef struct {
Ref string
Value *Example
- extra []string
}
-var _ jsonpointer.JSONPointable = (*ExampleRef)(nil)
-
-// MarshalYAML returns the YAML encoding of ExampleRef.
-func (x ExampleRef) MarshalYAML() (interface{}, error) {
- if ref := x.Ref; ref != "" {
- return &Ref{Ref: ref}, nil
- }
- return x.Value, nil
+func (value *ExampleRef) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalRef(value.Ref, value.Value)
}
-// MarshalJSON returns the JSON encoding of ExampleRef.
-func (x ExampleRef) MarshalJSON() ([]byte, error) {
- if ref := x.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
- return x.Value.MarshalJSON()
-}
-
-// UnmarshalJSON sets ExampleRef to a copy of data.
-func (x *ExampleRef) UnmarshalJSON(data []byte) error {
- var refOnly Ref
- if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
- x.Ref = refOnly.Ref
- if len(extra) != 0 {
- x.extra = make([]string, 0, len(extra))
- for key := range extra {
- x.extra = append(x.extra, key)
- }
- sort.Strings(x.extra)
- }
- return nil
- }
- return json.Unmarshal(data, &x.Value)
-}
-
-// Validate returns an error if ExampleRef does not comply with the OpenAPI spec.
-func (x *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- if extra := x.extra; len(extra) != 0 {
- extras := make([]string, 0, len(extra))
- allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
- if allowed == nil {
- allowed = make(map[string]struct{}, 0)
- }
- for _, ex := range extra {
- if _, ok := allowed[ex]; !ok {
- extras = append(extras, ex)
- }
- }
- if len(extras) != 0 {
- return fmt.Errorf("extra sibling fields: %+v", extras)
- }
- }
- if v := x.Value; v != nil {
- return v.Validate(ctx)
- }
- return foundUnresolvedRef(x.Ref)
+func (value *ExampleRef) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (x *ExampleRef) JSONLookup(token string) (interface{}, error) {
- if token == "$ref" {
- return x.Ref, nil
- }
- ptr, _, err := jsonpointer.GetForToken(x.Value, token)
- return ptr, err
+func (value *ExampleRef) Validate(c context.Context) error {
+ return nil
}
-// HeaderRef represents either a Header or a $ref to a Header.
-// When serializing and both fields are set, Ref is preferred over Value.
type HeaderRef struct {
Ref string
Value *Header
- extra []string
}
-var _ jsonpointer.JSONPointable = (*HeaderRef)(nil)
-
-// MarshalYAML returns the YAML encoding of HeaderRef.
-func (x HeaderRef) MarshalYAML() (interface{}, error) {
- if ref := x.Ref; ref != "" {
- return &Ref{Ref: ref}, nil
- }
- return x.Value, nil
+func (value *HeaderRef) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalRef(value.Ref, value.Value)
}
-// MarshalJSON returns the JSON encoding of HeaderRef.
-func (x HeaderRef) MarshalJSON() ([]byte, error) {
- if ref := x.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
- return x.Value.MarshalJSON()
-}
-
-// UnmarshalJSON sets HeaderRef to a copy of data.
-func (x *HeaderRef) UnmarshalJSON(data []byte) error {
- var refOnly Ref
- if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
- x.Ref = refOnly.Ref
- if len(extra) != 0 {
- x.extra = make([]string, 0, len(extra))
- for key := range extra {
- x.extra = append(x.extra, key)
- }
- sort.Strings(x.extra)
- }
- return nil
- }
- return json.Unmarshal(data, &x.Value)
-}
-
-// Validate returns an error if HeaderRef does not comply with the OpenAPI spec.
-func (x *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- if extra := x.extra; len(extra) != 0 {
- extras := make([]string, 0, len(extra))
- allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
- if allowed == nil {
- allowed = make(map[string]struct{}, 0)
- }
- for _, ex := range extra {
- if _, ok := allowed[ex]; !ok {
- extras = append(extras, ex)
- }
- }
- if len(extras) != 0 {
- return fmt.Errorf("extra sibling fields: %+v", extras)
- }
- }
- if v := x.Value; v != nil {
- return v.Validate(ctx)
- }
- return foundUnresolvedRef(x.Ref)
+func (value *HeaderRef) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (x *HeaderRef) JSONLookup(token string) (interface{}, error) {
- if token == "$ref" {
- return x.Ref, nil
+func (value *HeaderRef) Validate(c context.Context) error {
+ v := value.Value
+ if v == nil {
+ return foundUnresolvedRef(value.Ref)
}
- ptr, _, err := jsonpointer.GetForToken(x.Value, token)
- return ptr, err
+ return v.Validate(c)
}
-// LinkRef represents either a Link or a $ref to a Link.
-// When serializing and both fields are set, Ref is preferred over Value.
type LinkRef struct {
Ref string
Value *Link
- extra []string
}
-var _ jsonpointer.JSONPointable = (*LinkRef)(nil)
-
-// MarshalYAML returns the YAML encoding of LinkRef.
-func (x LinkRef) MarshalYAML() (interface{}, error) {
- if ref := x.Ref; ref != "" {
- return &Ref{Ref: ref}, nil
- }
- return x.Value, nil
+func (value *LinkRef) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalRef(value.Ref, value.Value)
}
-// MarshalJSON returns the JSON encoding of LinkRef.
-func (x LinkRef) MarshalJSON() ([]byte, error) {
- if ref := x.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
- return x.Value.MarshalJSON()
-}
-
-// UnmarshalJSON sets LinkRef to a copy of data.
-func (x *LinkRef) UnmarshalJSON(data []byte) error {
- var refOnly Ref
- if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
- x.Ref = refOnly.Ref
- if len(extra) != 0 {
- x.extra = make([]string, 0, len(extra))
- for key := range extra {
- x.extra = append(x.extra, key)
- }
- sort.Strings(x.extra)
- }
- return nil
- }
- return json.Unmarshal(data, &x.Value)
-}
-
-// Validate returns an error if LinkRef does not comply with the OpenAPI spec.
-func (x *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- if extra := x.extra; len(extra) != 0 {
- extras := make([]string, 0, len(extra))
- allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
- if allowed == nil {
- allowed = make(map[string]struct{}, 0)
- }
- for _, ex := range extra {
- if _, ok := allowed[ex]; !ok {
- extras = append(extras, ex)
- }
- }
- if len(extras) != 0 {
- return fmt.Errorf("extra sibling fields: %+v", extras)
- }
- }
- if v := x.Value; v != nil {
- return v.Validate(ctx)
- }
- return foundUnresolvedRef(x.Ref)
+func (value *LinkRef) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (x *LinkRef) JSONLookup(token string) (interface{}, error) {
- if token == "$ref" {
- return x.Ref, nil
+func (value *LinkRef) Validate(c context.Context) error {
+ v := value.Value
+ if v == nil {
+ return foundUnresolvedRef(value.Ref)
}
- ptr, _, err := jsonpointer.GetForToken(x.Value, token)
- return ptr, err
+ return v.Validate(c)
}
-// ParameterRef represents either a Parameter or a $ref to a Parameter.
-// When serializing and both fields are set, Ref is preferred over Value.
type ParameterRef struct {
Ref string
Value *Parameter
- extra []string
}
-var _ jsonpointer.JSONPointable = (*ParameterRef)(nil)
-
-// MarshalYAML returns the YAML encoding of ParameterRef.
-func (x ParameterRef) MarshalYAML() (interface{}, error) {
- if ref := x.Ref; ref != "" {
- return &Ref{Ref: ref}, nil
- }
- return x.Value, nil
+func (value *ParameterRef) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalRef(value.Ref, value.Value)
}
-// MarshalJSON returns the JSON encoding of ParameterRef.
-func (x ParameterRef) MarshalJSON() ([]byte, error) {
- if ref := x.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
- return x.Value.MarshalJSON()
-}
-
-// UnmarshalJSON sets ParameterRef to a copy of data.
-func (x *ParameterRef) UnmarshalJSON(data []byte) error {
- var refOnly Ref
- if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
- x.Ref = refOnly.Ref
- if len(extra) != 0 {
- x.extra = make([]string, 0, len(extra))
- for key := range extra {
- x.extra = append(x.extra, key)
- }
- sort.Strings(x.extra)
- }
- return nil
- }
- return json.Unmarshal(data, &x.Value)
-}
-
-// Validate returns an error if ParameterRef does not comply with the OpenAPI spec.
-func (x *ParameterRef) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- if extra := x.extra; len(extra) != 0 {
- extras := make([]string, 0, len(extra))
- allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
- if allowed == nil {
- allowed = make(map[string]struct{}, 0)
- }
- for _, ex := range extra {
- if _, ok := allowed[ex]; !ok {
- extras = append(extras, ex)
- }
- }
- if len(extras) != 0 {
- return fmt.Errorf("extra sibling fields: %+v", extras)
- }
- }
- if v := x.Value; v != nil {
- return v.Validate(ctx)
- }
- return foundUnresolvedRef(x.Ref)
+func (value *ParameterRef) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (x *ParameterRef) JSONLookup(token string) (interface{}, error) {
- if token == "$ref" {
- return x.Ref, nil
+func (value *ParameterRef) Validate(c context.Context) error {
+ v := value.Value
+ if v == nil {
+ return foundUnresolvedRef(value.Ref)
}
- ptr, _, err := jsonpointer.GetForToken(x.Value, token)
- return ptr, err
+ return v.Validate(c)
}
-// RequestBodyRef represents either a RequestBody or a $ref to a RequestBody.
-// When serializing and both fields are set, Ref is preferred over Value.
-type RequestBodyRef struct {
+type ResponseRef struct {
Ref string
- Value *RequestBody
- extra []string
+ Value *Response
}
-var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil)
-
-// MarshalYAML returns the YAML encoding of RequestBodyRef.
-func (x RequestBodyRef) MarshalYAML() (interface{}, error) {
- if ref := x.Ref; ref != "" {
- return &Ref{Ref: ref}, nil
- }
- return x.Value, nil
+func (value *ResponseRef) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalRef(value.Ref, value.Value)
}
-// MarshalJSON returns the JSON encoding of RequestBodyRef.
-func (x RequestBodyRef) MarshalJSON() ([]byte, error) {
- if ref := x.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
- return x.Value.MarshalJSON()
-}
-
-// UnmarshalJSON sets RequestBodyRef to a copy of data.
-func (x *RequestBodyRef) UnmarshalJSON(data []byte) error {
- var refOnly Ref
- if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
- x.Ref = refOnly.Ref
- if len(extra) != 0 {
- x.extra = make([]string, 0, len(extra))
- for key := range extra {
- x.extra = append(x.extra, key)
- }
- sort.Strings(x.extra)
- }
- return nil
- }
- return json.Unmarshal(data, &x.Value)
-}
-
-// Validate returns an error if RequestBodyRef does not comply with the OpenAPI spec.
-func (x *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- if extra := x.extra; len(extra) != 0 {
- extras := make([]string, 0, len(extra))
- allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
- if allowed == nil {
- allowed = make(map[string]struct{}, 0)
- }
- for _, ex := range extra {
- if _, ok := allowed[ex]; !ok {
- extras = append(extras, ex)
- }
- }
- if len(extras) != 0 {
- return fmt.Errorf("extra sibling fields: %+v", extras)
- }
- }
- if v := x.Value; v != nil {
- return v.Validate(ctx)
- }
- return foundUnresolvedRef(x.Ref)
+func (value *ResponseRef) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (x *RequestBodyRef) JSONLookup(token string) (interface{}, error) {
- if token == "$ref" {
- return x.Ref, nil
+func (value *ResponseRef) Validate(c context.Context) error {
+ v := value.Value
+ if v == nil {
+ return foundUnresolvedRef(value.Ref)
}
- ptr, _, err := jsonpointer.GetForToken(x.Value, token)
- return ptr, err
+ return v.Validate(c)
}
-// ResponseRef represents either a Response or a $ref to a Response.
-// When serializing and both fields are set, Ref is preferred over Value.
-type ResponseRef struct {
+type RequestBodyRef struct {
Ref string
- Value *Response
- extra []string
+ Value *RequestBody
}
-var _ jsonpointer.JSONPointable = (*ResponseRef)(nil)
-
-// MarshalYAML returns the YAML encoding of ResponseRef.
-func (x ResponseRef) MarshalYAML() (interface{}, error) {
- if ref := x.Ref; ref != "" {
- return &Ref{Ref: ref}, nil
- }
- return x.Value, nil
+func (value *RequestBodyRef) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalRef(value.Ref, value.Value)
}
-// MarshalJSON returns the JSON encoding of ResponseRef.
-func (x ResponseRef) MarshalJSON() ([]byte, error) {
- if ref := x.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
- return x.Value.MarshalJSON()
-}
-
-// UnmarshalJSON sets ResponseRef to a copy of data.
-func (x *ResponseRef) UnmarshalJSON(data []byte) error {
- var refOnly Ref
- if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
- x.Ref = refOnly.Ref
- if len(extra) != 0 {
- x.extra = make([]string, 0, len(extra))
- for key := range extra {
- x.extra = append(x.extra, key)
- }
- sort.Strings(x.extra)
- }
- return nil
- }
- return json.Unmarshal(data, &x.Value)
-}
-
-// Validate returns an error if ResponseRef does not comply with the OpenAPI spec.
-func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- if extra := x.extra; len(extra) != 0 {
- extras := make([]string, 0, len(extra))
- allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
- if allowed == nil {
- allowed = make(map[string]struct{}, 0)
- }
- for _, ex := range extra {
- if _, ok := allowed[ex]; !ok {
- extras = append(extras, ex)
- }
- }
- if len(extras) != 0 {
- return fmt.Errorf("extra sibling fields: %+v", extras)
- }
- }
- if v := x.Value; v != nil {
- return v.Validate(ctx)
- }
- return foundUnresolvedRef(x.Ref)
+func (value *RequestBodyRef) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (x *ResponseRef) JSONLookup(token string) (interface{}, error) {
- if token == "$ref" {
- return x.Ref, nil
+func (value *RequestBodyRef) Validate(c context.Context) error {
+ v := value.Value
+ if v == nil {
+ return foundUnresolvedRef(value.Ref)
}
- ptr, _, err := jsonpointer.GetForToken(x.Value, token)
- return ptr, err
+ return v.Validate(c)
}
-// SchemaRef represents either a Schema or a $ref to a Schema.
-// When serializing and both fields are set, Ref is preferred over Value.
type SchemaRef struct {
Ref string
Value *Schema
- extra []string
}
-var _ jsonpointer.JSONPointable = (*SchemaRef)(nil)
-
-// MarshalYAML returns the YAML encoding of SchemaRef.
-func (x SchemaRef) MarshalYAML() (interface{}, error) {
- if ref := x.Ref; ref != "" {
- return &Ref{Ref: ref}, nil
+func NewSchemaRef(ref string, value *Schema) *SchemaRef {
+ return &SchemaRef{
+ Ref: ref,
+ Value: value,
}
- return x.Value, nil
}
-// MarshalJSON returns the JSON encoding of SchemaRef.
-func (x SchemaRef) MarshalJSON() ([]byte, error) {
- if ref := x.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
- return x.Value.MarshalJSON()
-}
-
-// UnmarshalJSON sets SchemaRef to a copy of data.
-func (x *SchemaRef) UnmarshalJSON(data []byte) error {
- var refOnly Ref
- if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
- x.Ref = refOnly.Ref
- if len(extra) != 0 {
- x.extra = make([]string, 0, len(extra))
- for key := range extra {
- x.extra = append(x.extra, key)
- }
- sort.Strings(x.extra)
- }
- return nil
- }
- return json.Unmarshal(data, &x.Value)
-}
-
-// Validate returns an error if SchemaRef does not comply with the OpenAPI spec.
-func (x *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- if extra := x.extra; len(extra) != 0 {
- extras := make([]string, 0, len(extra))
- allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
- if allowed == nil {
- allowed = make(map[string]struct{}, 0)
- }
- for _, ex := range extra {
- if _, ok := allowed[ex]; !ok {
- extras = append(extras, ex)
- }
- }
- if len(extras) != 0 {
- return fmt.Errorf("extra sibling fields: %+v", extras)
- }
- }
- if v := x.Value; v != nil {
- return v.Validate(ctx)
- }
- return foundUnresolvedRef(x.Ref)
+func (value *SchemaRef) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalRef(value.Ref, value.Value)
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (x *SchemaRef) JSONLookup(token string) (interface{}, error) {
- if token == "$ref" {
- return x.Ref, nil
+func (value *SchemaRef) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
+}
+
+func (value *SchemaRef) Validate(c context.Context) error {
+ v := value.Value
+ if v == nil {
+ return foundUnresolvedRef(value.Ref)
}
- ptr, _, err := jsonpointer.GetForToken(x.Value, token)
- return ptr, err
+ return v.Validate(c)
}
-// SecuritySchemeRef represents either a SecurityScheme or a $ref to a SecurityScheme.
-// When serializing and both fields are set, Ref is preferred over Value.
type SecuritySchemeRef struct {
Ref string
Value *SecurityScheme
- extra []string
}
-var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil)
-
-// MarshalYAML returns the YAML encoding of SecuritySchemeRef.
-func (x SecuritySchemeRef) MarshalYAML() (interface{}, error) {
- if ref := x.Ref; ref != "" {
- return &Ref{Ref: ref}, nil
- }
- return x.Value, nil
+func (value *SecuritySchemeRef) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalRef(value.Ref, value.Value)
}
-// MarshalJSON returns the JSON encoding of SecuritySchemeRef.
-func (x SecuritySchemeRef) MarshalJSON() ([]byte, error) {
- if ref := x.Ref; ref != "" {
- return json.Marshal(Ref{Ref: ref})
- }
- return x.Value.MarshalJSON()
-}
-
-// UnmarshalJSON sets SecuritySchemeRef to a copy of data.
-func (x *SecuritySchemeRef) UnmarshalJSON(data []byte) error {
- var refOnly Ref
- if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
- x.Ref = refOnly.Ref
- if len(extra) != 0 {
- x.extra = make([]string, 0, len(extra))
- for key := range extra {
- x.extra = append(x.extra, key)
- }
- sort.Strings(x.extra)
- }
- return nil
- }
- return json.Unmarshal(data, &x.Value)
-}
-
-// Validate returns an error if SecuritySchemeRef does not comply with the OpenAPI spec.
-func (x *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- if extra := x.extra; len(extra) != 0 {
- extras := make([]string, 0, len(extra))
- allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
- if allowed == nil {
- allowed = make(map[string]struct{}, 0)
- }
- for _, ex := range extra {
- if _, ok := allowed[ex]; !ok {
- extras = append(extras, ex)
- }
- }
- if len(extras) != 0 {
- return fmt.Errorf("extra sibling fields: %+v", extras)
- }
- }
- if v := x.Value; v != nil {
- return v.Validate(ctx)
- }
- return foundUnresolvedRef(x.Ref)
+func (value *SecuritySchemeRef) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (x *SecuritySchemeRef) JSONLookup(token string) (interface{}, error) {
- if token == "$ref" {
- return x.Ref, nil
+func (value *SecuritySchemeRef) Validate(c context.Context) error {
+ v := value.Value
+ if v == nil {
+ return foundUnresolvedRef(value.Ref)
}
- ptr, _, err := jsonpointer.GetForToken(x.Value, token)
- return ptr, err
+ return v.Validate(c)
}
diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go
deleted file mode 100644
index e714da455..000000000
--- a/openapi3/refs_test.go
+++ /dev/null
@@ -1,275 +0,0 @@
-package openapi3
-
-import (
- "reflect"
- "testing"
-
- "github.com/go-openapi/jsonpointer"
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue222(t *testing.T) {
- spec := `
-openapi: 3.0.0
-info:
- version: 1.0.0
- title: Swagger Petstore
- license:
- name: MIT
-servers:
- - url: 'http://petstore.swagger.io/v1'
-paths:
- /pets:
- get:
- summary: List all pets
- operationId: listPets
- tags:
- - pets
- parameters:
- - name: limit
- in: query
- description: How many items to return at one time (max 100)
- required: false
- schema:
- type: integer
- format: int32
- responses:
- '200': # <--------------- PANIC HERE
-
- post:
- summary: Create a pet
- operationId: createPets
- tags:
- - pets
- responses:
- '201':
- description: Null response
- default:
- description: unexpected error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
- '/pets/{petId}':
- get:
- summary: Info for a specific pet
- operationId: showPetById
- tags:
- - pets
- parameters:
- - name: petId
- in: path
- required: true
- description: The id of the pet to retrieve
- schema:
- type: string
- responses:
- '200':
- description: Expected response to a valid request
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Pet'
- default:
- description: unexpected error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error'
-components:
- schemas:
- Pet:
- type: object
- required:
- - id
- - name
- properties:
- id:
- type: integer
- format: int64
- name:
- type: string
- tag:
- type: string
- Pets:
- type: array
- items:
- $ref: '#/components/schemas/Pet'
- Error:
- type: object
- required:
- - code
- - message
- properties:
- code:
- type: integer
- format: int32
- message:
- type: string
-`
-
- _, err := NewLoader().LoadFromData([]byte(spec))
- require.EqualError(t, err, `invalid response: value MUST be an object`)
-}
-
-func TestIssue247(t *testing.T) {
- spec := `openapi: 3.0.2
-info:
- title: Swagger Petstore - OpenAPI 3.0
- license:
- name: Apache 2.0
- url: http://www.apache.org/licenses/LICENSE-2.0.html
- version: 1.0.5
-servers:
-- url: /api/v3
-tags:
-- name: pet
- description: Everything about your Pets
- externalDocs:
- description: Find out more
- url: http://swagger.io
-- name: store
- description: Operations about user
-- name: user
- description: Access to Petstore orders
- externalDocs:
- description: Find out more about our store
- url: http://swagger.io
-paths:
- /pet:
- put:
- tags:
- - pet
- summary: Update an existing pet
- description: Update an existing pet by Id
- operationId: updatePet
- requestBody:
- description: Update an existent pet in the store
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Pet'
- application/xml:
- schema:
- $ref: '#/components/schemas/Pet'
- application/x-www-form-urlencoded:
- schema:
- $ref: '#/components/schemas/Pet'
- required: true
- responses:
- "200":
- description: Successful operation
- content:
- application/xml:
- schema:
- $ref: '#/components/schemas/Pet'
- application/json:
- schema:
- $ref: '#/components/schemas/Pet'
- "400":
- description: Invalid ID supplied
- "404":
- description: Pet not found
- "405":
- description: Validation exception
- security:
- - petstore_auth:
- - write:pets
- - read:pets
-components:
- schemas:
- Pet:
- type: object
- required:
- - id
- - name
- properties:
- id:
- type: integer
- format: int64
- name:
- type: string
- tag:
- type: string
- Pets:
- type: array
- items:
- $ref: '#/components/schemas/Pet'
- Error:
- type: object
- required:
- - code
- - message
- properties:
- code:
- type: integer
- format: int32
- message:
- type: string
- OneOfTest:
- type: object
- oneOf:
- - type: string
- - type: integer
- format: int32
- `
- root, err := NewLoader().LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- ptr, err := jsonpointer.New("/paths/~1pet/put/responses/200/content")
- require.NoError(t, err)
- v, kind, err := ptr.Get(root)
- require.NoError(t, err)
- require.Equal(t, reflect.TypeOf(Content{}).Kind(), kind)
- require.IsType(t, Content{}, v)
-
- ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content/application~1json/schema")
- require.NoError(t, err)
- v, kind, err = ptr.Get(root)
- require.NoError(t, err)
- require.Equal(t, reflect.Ptr, kind)
- require.IsType(t, &Ref{}, v)
- require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref)
-
- ptr, err = jsonpointer.New("/components/schemas/Pets/items")
- require.NoError(t, err)
- v, kind, err = ptr.Get(root)
- require.NoError(t, err)
- require.Equal(t, reflect.Ptr, kind)
- require.IsType(t, &Ref{}, v)
- require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref)
-
- ptr, err = jsonpointer.New("/components/schemas/Error/properties/code")
- require.NoError(t, err)
- v, kind, err = ptr.Get(root)
- require.NoError(t, err)
- require.Equal(t, reflect.Ptr, kind)
- require.IsType(t, &Schema{}, v)
- require.Equal(t, "integer", v.(*Schema).Type)
-
- ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/0")
- require.NoError(t, err)
- v, kind, err = ptr.Get(root)
- require.NoError(t, err)
- require.Equal(t, reflect.Ptr, kind)
- require.IsType(t, &Schema{}, v)
- require.Equal(t, "string", v.(*Schema).Type)
-
- ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/1")
- require.NoError(t, err)
- v, kind, err = ptr.Get(root)
- require.NoError(t, err)
- require.Equal(t, reflect.Ptr, kind)
- require.IsType(t, &Schema{}, v)
- require.Equal(t, "integer", v.(*Schema).Type)
-
- ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/5")
- require.NoError(t, err)
- _, _, err = ptr.Get(root)
- require.Error(t, err)
-
- ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/-1")
- require.NoError(t, err)
- _, _, err = ptr.Get(root)
- require.Error(t, err)
-}
diff --git a/openapi3/request_body.go b/openapi3/request_body.go
index de8919f41..6e8c8fc72 100644
--- a/openapi3/request_body.go
+++ b/openapi3/request_body.go
@@ -2,38 +2,16 @@ package openapi3
import (
"context"
- "encoding/json"
- "errors"
- "fmt"
- "github.com/go-openapi/jsonpointer"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-type RequestBodies map[string]*RequestBodyRef
-
-var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil)
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (r RequestBodies) JSONLookup(token string) (interface{}, error) {
- ref, ok := r[token]
- if ok == false {
- return nil, fmt.Errorf("object has no field %q", token)
- }
-
- if ref != nil && ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
// RequestBody is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#request-body-object
type RequestBody struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
+ ExtensionProps
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Required bool `json:"required,omitempty" yaml:"required,omitempty"`
- Content Content `json:"content" yaml:"content"`
+ Content Content `json:"content,omitempty" yaml:"content,omitempty"`
}
func NewRequestBody() *RequestBody {
@@ -55,16 +33,6 @@ func (requestBody *RequestBody) WithContent(content Content) *RequestBody {
return requestBody
}
-func (requestBody *RequestBody) WithSchemaRef(value *SchemaRef, consumes []string) *RequestBody {
- requestBody.Content = NewContentWithSchemaRef(value, consumes)
- return requestBody
-}
-
-func (requestBody *RequestBody) WithSchema(value *Schema, consumes []string) *RequestBody {
- requestBody.Content = NewContentWithSchema(value, consumes)
- return requestBody
-}
-
func (requestBody *RequestBody) WithJSONSchemaRef(value *SchemaRef) *RequestBody {
requestBody.Content = NewContentWithJSONSchemaRef(value)
return requestBody
@@ -75,16 +43,6 @@ func (requestBody *RequestBody) WithJSONSchema(value *Schema) *RequestBody {
return requestBody
}
-func (requestBody *RequestBody) WithFormDataSchemaRef(value *SchemaRef) *RequestBody {
- requestBody.Content = NewContentWithFormDataSchemaRef(value)
- return requestBody
-}
-
-func (requestBody *RequestBody) WithFormDataSchema(value *Schema) *RequestBody {
- requestBody.Content = NewContentWithFormDataSchema(value)
- return requestBody
-}
-
func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType {
m := requestBody.Content
if m == nil {
@@ -93,54 +51,19 @@ func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType {
return m[mediaType]
}
-// MarshalJSON returns the JSON encoding of RequestBody.
-func (requestBody RequestBody) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 3+len(requestBody.Extensions))
- for k, v := range requestBody.Extensions {
- m[k] = v
- }
- if x := requestBody.Description; x != "" {
- m["description"] = requestBody.Description
- }
- if x := requestBody.Required; x {
- m["required"] = x
- }
- if x := requestBody.Content; true {
- m["content"] = x
- }
- return json.Marshal(m)
+func (requestBody *RequestBody) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(requestBody)
}
-// UnmarshalJSON sets RequestBody to a copy of data.
func (requestBody *RequestBody) UnmarshalJSON(data []byte) error {
- type RequestBodyBis RequestBody
- var x RequestBodyBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "description")
- delete(x.Extensions, "required")
- delete(x.Extensions, "content")
- *requestBody = RequestBody(x)
- return nil
+ return jsoninfo.UnmarshalStrictStruct(data, requestBody)
}
-// Validate returns an error if RequestBody does not comply with the OpenAPI spec.
-func (requestBody *RequestBody) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if requestBody.Content == nil {
- return errors.New("content of the request body is required")
- }
-
- if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled {
- vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false
+func (requestBody *RequestBody) Validate(c context.Context) error {
+ if v := requestBody.Content; v != nil {
+ if err := v.Validate(c); err != nil {
+ return err
+ }
}
-
- if err := requestBody.Content.Validate(ctx); err != nil {
- return err
- }
-
- return validateExtensions(ctx, requestBody.Extensions)
+ return nil
}
diff --git a/openapi3/response.go b/openapi3/response.go
index b85c9145c..db83db71f 100644
--- a/openapi3/response.go
+++ b/openapi3/response.go
@@ -2,25 +2,17 @@ package openapi3
import (
"context"
- "encoding/json"
"errors"
- "fmt"
- "sort"
"strconv"
- "github.com/go-openapi/jsonpointer"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
// Responses is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responses-object
type Responses map[string]*ResponseRef
-var _ jsonpointer.JSONPointable = (*Responses)(nil)
-
func NewResponses() Responses {
- r := make(Responses)
- r["default"] = &ResponseRef{Value: NewResponse().WithDescription("")}
- return r
+ return make(Responses, 8)
}
func (responses Responses) Default() *ResponseRef {
@@ -31,50 +23,22 @@ func (responses Responses) Get(status int) *ResponseRef {
return responses[strconv.FormatInt(int64(status), 10)]
}
-// Validate returns an error if Responses does not comply with the OpenAPI spec.
-func (responses Responses) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if len(responses) == 0 {
- return errors.New("the responses object MUST contain at least one response code")
- }
-
- keys := make([]string, 0, len(responses))
- for key := range responses {
- keys = append(keys, key)
- }
- sort.Strings(keys)
- for _, key := range keys {
- v := responses[key]
- if err := v.Validate(ctx); err != nil {
+func (responses Responses) Validate(c context.Context) error {
+ for _, v := range responses {
+ if err := v.Validate(c); err != nil {
return err
}
}
return nil
}
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (responses Responses) JSONLookup(token string) (interface{}, error) {
- ref, ok := responses[token]
- if ok == false {
- return nil, fmt.Errorf("invalid token reference: %q", token)
- }
-
- if ref != nil && ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
// Response is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#response-object
type Response struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Description *string `json:"description,omitempty" yaml:"description,omitempty"`
- Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"`
- Content Content `json:"content,omitempty" yaml:"content,omitempty"`
- Links Links `json:"links,omitempty" yaml:"links,omitempty"`
+ ExtensionProps
+ Description *string `json:"description,omitempty" yaml:"description,omitempty"`
+ Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"`
+ Content Content `json:"content,omitempty" yaml:"content,omitempty"`
+ Links map[string]*LinkRef `json:"links,omitempty" yaml:"links,omitempty"`
}
func NewResponse() *Response {
@@ -101,83 +65,23 @@ func (response *Response) WithJSONSchemaRef(schema *SchemaRef) *Response {
return response
}
-// MarshalJSON returns the JSON encoding of Response.
-func (response Response) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 4+len(response.Extensions))
- for k, v := range response.Extensions {
- m[k] = v
- }
- if x := response.Description; x != nil {
- m["description"] = x
- }
- if x := response.Headers; len(x) != 0 {
- m["headers"] = x
- }
- if x := response.Content; len(x) != 0 {
- m["content"] = x
- }
- if x := response.Links; len(x) != 0 {
- m["links"] = x
- }
- return json.Marshal(m)
+func (response *Response) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(response)
}
-// UnmarshalJSON sets Response to a copy of data.
func (response *Response) UnmarshalJSON(data []byte) error {
- type ResponseBis Response
- var x ResponseBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "description")
- delete(x.Extensions, "headers")
- delete(x.Extensions, "content")
- delete(x.Extensions, "links")
- *response = Response(x)
- return nil
+ return jsoninfo.UnmarshalStrictStruct(data, response)
}
-// Validate returns an error if Response does not comply with the OpenAPI spec.
-func (response *Response) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (response *Response) Validate(c context.Context) error {
if response.Description == nil {
- return errors.New("a short description of the response is required")
- }
- if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled {
- vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true
+ return errors.New("A short description of the response is required")
}
if content := response.Content; content != nil {
- if err := content.Validate(ctx); err != nil {
- return err
- }
- }
-
- headers := make([]string, 0, len(response.Headers))
- for name := range response.Headers {
- headers = append(headers, name)
- }
- sort.Strings(headers)
- for _, name := range headers {
- header := response.Headers[name]
- if err := header.Validate(ctx); err != nil {
- return err
- }
- }
-
- links := make([]string, 0, len(response.Links))
- for name := range response.Links {
- links = append(links, name)
- }
- sort.Strings(links)
- for _, name := range links {
- link := response.Links[name]
- if err := link.Validate(ctx); err != nil {
+ if err := content.Validate(c); err != nil {
return err
}
}
-
- return validateExtensions(ctx, response.Extensions)
+ return nil
}
diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go
deleted file mode 100644
index 5de0525e4..000000000
--- a/openapi3/response_issue224_test.go
+++ /dev/null
@@ -1,461 +0,0 @@
-package openapi3
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestEmptyResponsesAreInvalid(t *testing.T) {
- spec := `{
- "openapi": "3.0.0",
- "servers": [
- {
- "url": "http://petstore.swagger.io/v2"
- }
- ],
- "info": {
- "description": ":dog: :cat: :rabbit: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key to test the authorization filters.",
- "version": "1.0.0",
- "title": "Swagger Petstore",
- "termsOfService": "http://swagger.io/terms/",
- "contact": {
- "email": "apiteam@swagger.io"
- },
- "license": {
- "name": "Apache 2.0",
- "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
- }
- },
- "tags": [
- {
- "name": "pet",
- "description": "Everything about your Pets",
- "externalDocs": {
- "description": "Find out more",
- "url": "http://swagger.io"
- }
- },
- {
- "name": "store",
- "description": "Access to Petstore orders"
- },
- {
- "name": "user",
- "description": "Operations about user",
- "externalDocs": {
- "description": "Find out more about our store",
- "url": "http://swagger.io"
- }
- }
- ],
- "paths": {
- "/pet": {
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Add a new pet to the store",
- "description": "",
- "operationId": "addPet",
- "responses": {
-
- },
- "security": [
- {
- "petstore_auth": [
- "write:pets",
- "read:pets"
- ]
- }
- ],
- "requestBody": {
- "$ref": "#/components/requestBodies/Pet"
- },
- "parameters": []
- },
- "put": {
- "tags": [
- "pet"
- ],
- "summary": "Update an existing pet",
- "description": "",
- "operationId": "updatePet",
- "responses": {
-
- },
- "security": [
- {
- "petstore_auth": [
- "write:pets",
- "read:pets"
- ]
- }
- ],
- "requestBody": {
- "$ref": "#/components/requestBodies/Pet"
- },
- "parameters": []
- }
- },
- "/pet/{petId}": {
- "get": {
- "tags": [
- "pet"
- ],
- "summary": "Find pet by ID",
- "description": "Returns a single pet",
- "operationId": "getPetById",
- "parameters": [
- {
- "name": "petId",
- "in": "path",
- "description": "ID of pet to return",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- }
- }
- ],
- "responses": {
-
- },
- "security": [
- {
- "api_key": []
- }
- ]
- },
- "post": {
- "tags": [
- "pet"
- ],
- "summary": "Updates a pet in the store with form data",
- "description": "",
- "operationId": "updatePetWithForm",
- "parameters": [
- {
- "name": "petId",
- "in": "path",
- "description": "ID of pet that needs to be updated",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- }
- }
- ],
- "responses": {
-
- },
- "security": [
- {
- "petstore_auth": [
- "write:pets",
- "read:pets"
- ]
- }
- ],
- "requestBody": {
- "content": {
- "application/x-www-form-urlencoded": {
- "schema": {
- "type": "object",
- "properties": {
- "name": {
- "description": "Updated name of the pet",
- "type": "string"
- },
- "status": {
- "description": "Updated status of the pet",
- "type": "string"
- }
- }
- }
- }
- }
- }
- },
- "delete": {
- "tags": [
- "pet"
- ],
- "summary": "Deletes a pet",
- "description": "",
- "operationId": "deletePet",
- "parameters": [
- {
- "name": "api_key",
- "in": "header",
- "required": false,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "petId",
- "in": "path",
- "description": "Pet id to delete",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int64"
- }
- }
- ],
- "responses": {
-
- },
- "security": [
- {
- "petstore_auth": [
- "write:pets",
- "read:pets"
- ]
- }
- ]
- }
- }
- },
- "externalDocs": {
- "description": "See AsyncAPI example",
- "url": "https://mermade.github.io/shins/asyncapi.html"
- },
- "components": {
- "schemas": {
- "Order": {
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int64"
- },
- "petId": {
- "type": "integer",
- "format": "int64"
- },
- "quantity": {
- "type": "integer",
- "format": "int32"
- },
- "shipDate": {
- "type": "string",
- "format": "date-time"
- },
- "status": {
- "type": "string",
- "description": "Order Status",
- "enum": [
- "placed",
- "approved",
- "delivered"
- ]
- },
- "complete": {
- "type": "boolean",
- "default": false
- }
- },
- "xml": {
- "name": "Order"
- }
- },
- "Category": {
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int64"
- },
- "name": {
- "type": "string"
- }
- },
- "xml": {
- "name": "Category"
- }
- },
- "User": {
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int64"
- },
- "username": {
- "type": "string"
- },
- "firstName": {
- "type": "string"
- },
- "lastName": {
- "type": "string"
- },
- "email": {
- "type": "string"
- },
- "password": {
- "type": "string"
- },
- "phone": {
- "type": "string"
- },
- "userStatus": {
- "type": "integer",
- "format": "int32",
- "description": "User Status"
- }
- },
- "xml": {
- "name": "User"
- }
- },
- "Tag": {
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int64"
- },
- "name": {
- "type": "string"
- }
- },
- "xml": {
- "name": "Tag"
- }
- },
- "Pet": {
- "type": "object",
- "required": [
- "name",
- "photoUrls"
- ],
- "properties": {
- "id": {
- "type": "integer",
- "format": "int64"
- },
- "category": {
- "$ref": "#/components/schemas/Category"
- },
- "name": {
- "type": "string",
- "example": "doggie"
- },
- "photoUrls": {
- "type": "array",
- "xml": {
- "name": "photoUrl",
- "wrapped": true
- },
- "items": {
- "type": "string"
- }
- },
- "tags": {
- "type": "array",
- "xml": {
- "name": "tag",
- "wrapped": true
- },
- "items": {
- "$ref": "#/components/schemas/Tag"
- }
- },
- "status": {
- "type": "string",
- "description": "pet status in the store",
- "enum": [
- "available",
- "pending",
- "sold"
- ]
- }
- },
- "xml": {
- "name": "Pet"
- }
- },
- "ApiResponse": {
- "type": "object",
- "properties": {
- "code": {
- "type": "integer",
- "format": "int32"
- },
- "type": {
- "type": "string"
- },
- "message": {
- "type": "string"
- }
- }
- }
- },
- "requestBodies": {
- "Pet": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- },
- "application/xml": {
- "schema": {
- "$ref": "#/components/schemas/Pet"
- }
- }
- },
- "description": "Pet object that needs to be added to the store",
- "required": true
- },
- "UserArray": {
- "content": {
- "application/json": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/User"
- }
- }
- }
- },
- "description": "List of user object",
- "required": true
- }
- },
- "securitySchemes": {
- "petstore_auth": {
- "type": "oauth2",
- "flows": {
- "implicit": {
- "authorizationUrl": "http://petstore.swagger.io/oauth/dialog",
- "scopes": {
- "write:pets": "modify pets in your account",
- "read:pets": "read your pets"
- }
- }
- }
- },
- "api_key": {
- "type": "apiKey",
- "name": "api_key",
- "in": "header"
- }
- },
- "links": {},
- "callbacks": {}
- },
- "security": []
-}
-`
-
- doc, err := NewLoader().LoadFromData([]byte(spec))
- require.NoError(t, err)
- require.Equal(t, doc.ExternalDocs.Description, "See AsyncAPI example")
- err = doc.Validate(context.Background())
- require.EqualError(t, err, `invalid paths: invalid path /pet: invalid operation POST: the responses object MUST contain at least one response code`)
-}
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 4bfbca0bd..427c376a4 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -8,45 +8,24 @@ import (
"fmt"
"math"
"math/big"
- "reflect"
"regexp"
- "sort"
"strconv"
- "strings"
"unicode/utf16"
- "github.com/go-openapi/jsonpointer"
- "github.com/mohae/deepcopy"
-)
-
-const (
- TypeArray = "array"
- TypeBoolean = "boolean"
- TypeInteger = "integer"
- TypeNumber = "number"
- TypeObject = "object"
- TypeString = "string"
-
- // constants for integer formats
- formatMinInt32 = float64(math.MinInt32)
- formatMaxInt32 = float64(math.MaxInt32)
- formatMinInt64 = float64(math.MinInt64)
- formatMaxInt64 = float64(math.MaxInt64)
+ "github.com/getkin/kin-openapi/jsoninfo"
)
var (
// SchemaErrorDetailsDisabled disables printing of details about schema errors.
SchemaErrorDetailsDisabled = false
- errSchema = errors.New("input does not match the schema")
+ //SchemaFormatValidationDisabled disables validation of schema type formats.
+ SchemaFormatValidationDisabled = false
- // ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema
- ErrOneOfConflict = errors.New("input matches more than one oneOf schemas")
+ errSchema = errors.New("Input does not match the schema")
- // ErrSchemaInputNaN may be returned when validating a number
- ErrSchemaInputNaN = errors.New("floating point NaN is not allowed")
- // ErrSchemaInputInf may be returned when validating a number
- ErrSchemaInputInf = errors.New("floating point Inf is not allowed")
+ ErrSchemaInputNaN = errors.New("NaN is not allowed")
+ ErrSchemaInputInf = errors.New("Inf is not allowed")
)
// Float64Ptr is a helper for defining OpenAPI schemas.
@@ -69,62 +48,13 @@ func Uint64Ptr(value uint64) *uint64 {
return &value
}
-// NewSchemaRef simply builds a SchemaRef
-func NewSchemaRef(ref string, value *Schema) *SchemaRef {
- return &SchemaRef{
- Ref: ref,
- Value: value,
- }
-}
-
-type Schemas map[string]*SchemaRef
-
-var _ jsonpointer.JSONPointable = (*Schemas)(nil)
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (s Schemas) JSONLookup(token string) (interface{}, error) {
- ref, ok := s[token]
- if ref == nil || ok == false {
- return nil, fmt.Errorf("object has no field %q", token)
- }
-
- if ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
-type SchemaRefs []*SchemaRef
-
-var _ jsonpointer.JSONPointable = (*SchemaRefs)(nil)
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (s SchemaRefs) JSONLookup(token string) (interface{}, error) {
- i, err := strconv.ParseUint(token, 10, 64)
- if err != nil {
- return nil, err
- }
-
- if i >= uint64(len(s)) {
- return nil, fmt.Errorf("index out of range: %d", i)
- }
-
- ref := s[i]
-
- if ref == nil || ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
// Schema is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object
type Schema struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
+ ExtensionProps
- OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
- AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"`
- AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"`
+ OneOf []*SchemaRef `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
+ AnyOf []*SchemaRef `json:"anyOf,omitempty" yaml:"anyOf,omitempty"`
+ AllOf []*SchemaRef `json:"allOf,omitempty" yaml:"allOf,omitempty"`
Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
Title string `json:"title,omitempty" yaml:"title,omitempty"`
@@ -135,18 +65,18 @@ type Schema struct {
Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
+ // Object-related, here for struct compactness
+ AdditionalPropertiesAllowed *bool `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"`
// Array-related, here for struct compactness
UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
// Number-related, here for struct compactness
ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"`
ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"`
// Properties
- Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
- ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"`
- WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"`
- AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
- Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
- XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"`
+ Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
+ ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"`
+ WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"`
+ XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"`
// Number
Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
@@ -157,7 +87,7 @@ type Schema struct {
MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"`
MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"`
Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"`
- compiledPattern *regexp.Regexp
+ compiledPattern *compiledPattern
// Array
MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"`
@@ -165,362 +95,24 @@ type Schema struct {
Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"`
// Object
- Required []string `json:"required,omitempty" yaml:"required,omitempty"`
- Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"`
- MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
- MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
- AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
- Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
-}
-
-type AdditionalProperties struct {
- Has *bool
- Schema *SchemaRef
-}
-
-// MarshalJSON returns the JSON encoding of AdditionalProperties.
-func (addProps AdditionalProperties) MarshalJSON() ([]byte, error) {
- if x := addProps.Has; x != nil {
- if *x {
- return []byte("true"), nil
- }
- return []byte("false"), nil
- }
- if x := addProps.Schema; x != nil {
- return json.Marshal(x)
- }
- return nil, nil
-}
-
-// UnmarshalJSON sets AdditionalProperties to a copy of data.
-func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error {
- var x interface{}
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- switch y := x.(type) {
- case nil:
- case bool:
- addProps.Has = &y
- case map[string]interface{}:
- if len(y) == 0 {
- addProps.Schema = &SchemaRef{Value: &Schema{}}
- } else {
- buf := new(bytes.Buffer)
- json.NewEncoder(buf).Encode(y)
- if err := json.NewDecoder(buf).Decode(&addProps.Schema); err != nil {
- return err
- }
- }
- default:
- return errors.New("cannot unmarshal additionalProperties: value must be either a schema object or a boolean")
- }
- return nil
+ Required []string `json:"required,omitempty" yaml:"required,omitempty"`
+ Properties map[string]*SchemaRef `json:"properties,omitempty" yaml:"properties,omitempty"`
+ MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
+ MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
+ AdditionalProperties *SchemaRef `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"`
+ Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
}
-var _ jsonpointer.JSONPointable = (*Schema)(nil)
-
func NewSchema() *Schema {
return &Schema{}
}
-// MarshalJSON returns the JSON encoding of Schema.
-func (schema Schema) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 36+len(schema.Extensions))
- for k, v := range schema.Extensions {
- m[k] = v
- }
-
- if x := schema.OneOf; len(x) != 0 {
- m["oneOf"] = x
- }
- if x := schema.AnyOf; len(x) != 0 {
- m["anyOf"] = x
- }
- if x := schema.AllOf; len(x) != 0 {
- m["allOf"] = x
- }
- if x := schema.Not; x != nil {
- m["not"] = x
- }
- if x := schema.Type; len(x) != 0 {
- m["type"] = x
- }
- if x := schema.Title; len(x) != 0 {
- m["title"] = x
- }
- if x := schema.Format; len(x) != 0 {
- m["format"] = x
- }
- if x := schema.Description; len(x) != 0 {
- m["description"] = x
- }
- if x := schema.Enum; len(x) != 0 {
- m["enum"] = x
- }
- if x := schema.Default; x != nil {
- m["default"] = x
- }
- if x := schema.Example; x != nil {
- m["example"] = x
- }
- if x := schema.ExternalDocs; x != nil {
- m["externalDocs"] = x
- }
-
- // Array-related
- if x := schema.UniqueItems; x {
- m["uniqueItems"] = x
- }
- // Number-related
- if x := schema.ExclusiveMin; x {
- m["exclusiveMinimum"] = x
- }
- if x := schema.ExclusiveMax; x {
- m["exclusiveMaximum"] = x
- }
- // Properties
- if x := schema.Nullable; x {
- m["nullable"] = x
- }
- if x := schema.ReadOnly; x {
- m["readOnly"] = x
- }
- if x := schema.WriteOnly; x {
- m["writeOnly"] = x
- }
- if x := schema.AllowEmptyValue; x {
- m["allowEmptyValue"] = x
- }
- if x := schema.Deprecated; x {
- m["deprecated"] = x
- }
- if x := schema.XML; x != nil {
- m["xml"] = x
- }
-
- // Number
- if x := schema.Min; x != nil {
- m["minimum"] = x
- }
- if x := schema.Max; x != nil {
- m["maximum"] = x
- }
- if x := schema.MultipleOf; x != nil {
- m["multipleOf"] = x
- }
-
- // String
- if x := schema.MinLength; x != 0 {
- m["minLength"] = x
- }
- if x := schema.MaxLength; x != nil {
- m["maxLength"] = x
- }
- if x := schema.Pattern; x != "" {
- m["pattern"] = x
- }
-
- // Array
- if x := schema.MinItems; x != 0 {
- m["minItems"] = x
- }
- if x := schema.MaxItems; x != nil {
- m["maxItems"] = x
- }
- if x := schema.Items; x != nil {
- m["items"] = x
- }
-
- // Object
- if x := schema.Required; len(x) != 0 {
- m["required"] = x
- }
- if x := schema.Properties; len(x) != 0 {
- m["properties"] = x
- }
- if x := schema.MinProps; x != 0 {
- m["minProperties"] = x
- }
- if x := schema.MaxProps; x != nil {
- m["maxProperties"] = x
- }
- if x := schema.AdditionalProperties; x.Has != nil || x.Schema != nil {
- m["additionalProperties"] = &x
- }
- if x := schema.Discriminator; x != nil {
- m["discriminator"] = x
- }
-
- return json.Marshal(m)
+func (schema *Schema) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(schema)
}
-// UnmarshalJSON sets Schema to a copy of data.
func (schema *Schema) UnmarshalJSON(data []byte) error {
- type SchemaBis Schema
- var x SchemaBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
-
- delete(x.Extensions, "oneOf")
- delete(x.Extensions, "anyOf")
- delete(x.Extensions, "allOf")
- delete(x.Extensions, "not")
- delete(x.Extensions, "type")
- delete(x.Extensions, "title")
- delete(x.Extensions, "format")
- delete(x.Extensions, "description")
- delete(x.Extensions, "enum")
- delete(x.Extensions, "default")
- delete(x.Extensions, "example")
- delete(x.Extensions, "externalDocs")
-
- // Array-related
- delete(x.Extensions, "uniqueItems")
- // Number-related
- delete(x.Extensions, "exclusiveMinimum")
- delete(x.Extensions, "exclusiveMaximum")
- // Properties
- delete(x.Extensions, "nullable")
- delete(x.Extensions, "readOnly")
- delete(x.Extensions, "writeOnly")
- delete(x.Extensions, "allowEmptyValue")
- delete(x.Extensions, "deprecated")
- delete(x.Extensions, "xml")
-
- // Number
- delete(x.Extensions, "minimum")
- delete(x.Extensions, "maximum")
- delete(x.Extensions, "multipleOf")
-
- // String
- delete(x.Extensions, "minLength")
- delete(x.Extensions, "maxLength")
- delete(x.Extensions, "pattern")
-
- // Array
- delete(x.Extensions, "minItems")
- delete(x.Extensions, "maxItems")
- delete(x.Extensions, "items")
-
- // Object
- delete(x.Extensions, "required")
- delete(x.Extensions, "properties")
- delete(x.Extensions, "minProperties")
- delete(x.Extensions, "maxProperties")
- delete(x.Extensions, "additionalProperties")
- delete(x.Extensions, "discriminator")
-
- *schema = Schema(x)
-
- if schema.Format == "date" {
- // This is a fix for: https://github.com/getkin/kin-openapi/issues/697
- if eg, ok := schema.Example.(string); ok {
- schema.Example = strings.TrimSuffix(eg, "T00:00:00Z")
- }
- }
- return nil
-}
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (schema Schema) JSONLookup(token string) (interface{}, error) {
- switch token {
- case "additionalProperties":
- if addProps := schema.AdditionalProperties.Has; addProps != nil {
- return *addProps, nil
- }
- if addProps := schema.AdditionalProperties.Schema; addProps != nil {
- if addProps.Ref != "" {
- return &Ref{Ref: addProps.Ref}, nil
- }
- return addProps.Value, nil
- }
- case "not":
- if schema.Not != nil {
- if schema.Not.Ref != "" {
- return &Ref{Ref: schema.Not.Ref}, nil
- }
- return schema.Not.Value, nil
- }
- case "items":
- if schema.Items != nil {
- if schema.Items.Ref != "" {
- return &Ref{Ref: schema.Items.Ref}, nil
- }
- return schema.Items.Value, nil
- }
- case "oneOf":
- return schema.OneOf, nil
- case "anyOf":
- return schema.AnyOf, nil
- case "allOf":
- return schema.AllOf, nil
- case "type":
- return schema.Type, nil
- case "title":
- return schema.Title, nil
- case "format":
- return schema.Format, nil
- case "description":
- return schema.Description, nil
- case "enum":
- return schema.Enum, nil
- case "default":
- return schema.Default, nil
- case "example":
- return schema.Example, nil
- case "externalDocs":
- return schema.ExternalDocs, nil
- case "uniqueItems":
- return schema.UniqueItems, nil
- case "exclusiveMin":
- return schema.ExclusiveMin, nil
- case "exclusiveMax":
- return schema.ExclusiveMax, nil
- case "nullable":
- return schema.Nullable, nil
- case "readOnly":
- return schema.ReadOnly, nil
- case "writeOnly":
- return schema.WriteOnly, nil
- case "allowEmptyValue":
- return schema.AllowEmptyValue, nil
- case "xml":
- return schema.XML, nil
- case "deprecated":
- return schema.Deprecated, nil
- case "min":
- return schema.Min, nil
- case "max":
- return schema.Max, nil
- case "multipleOf":
- return schema.MultipleOf, nil
- case "minLength":
- return schema.MinLength, nil
- case "maxLength":
- return schema.MaxLength, nil
- case "pattern":
- return schema.Pattern, nil
- case "minItems":
- return schema.MinItems, nil
- case "maxItems":
- return schema.MaxItems, nil
- case "required":
- return schema.Required, nil
- case "properties":
- return schema.Properties, nil
- case "minProps":
- return schema.MinProps, nil
- case "maxProps":
- return schema.MaxProps, nil
- case "discriminator":
- return schema.Discriminator, nil
- }
-
- v, _, err := jsonpointer.GetForToken(schema.Extensions, token)
- return v, err
+ return jsoninfo.UnmarshalStrictStruct(data, schema)
}
func (schema *Schema) NewRef() *SchemaRef {
@@ -561,76 +153,81 @@ func NewAllOfSchema(schemas ...*Schema) *Schema {
func NewBoolSchema() *Schema {
return &Schema{
- Type: TypeBoolean,
+ Type: "boolean",
}
}
func NewFloat64Schema() *Schema {
return &Schema{
- Type: TypeNumber,
+ Type: "number",
}
}
func NewIntegerSchema() *Schema {
return &Schema{
- Type: TypeInteger,
+ Type: "integer",
}
}
func NewInt32Schema() *Schema {
return &Schema{
- Type: TypeInteger,
+ Type: "integer",
Format: "int32",
}
}
func NewInt64Schema() *Schema {
return &Schema{
- Type: TypeInteger,
+ Type: "integer",
Format: "int64",
}
}
func NewStringSchema() *Schema {
return &Schema{
- Type: TypeString,
+ Type: "string",
}
}
func NewDateTimeSchema() *Schema {
return &Schema{
- Type: TypeString,
+ Type: "string",
Format: "date-time",
}
}
func NewUUIDSchema() *Schema {
return &Schema{
- Type: TypeString,
+ Type: "string",
Format: "uuid",
}
}
func NewBytesSchema() *Schema {
return &Schema{
- Type: TypeString,
+ Type: "string",
Format: "byte",
}
}
func NewArraySchema() *Schema {
return &Schema{
- Type: TypeArray,
+ Type: "array",
}
}
func NewObjectSchema() *Schema {
return &Schema{
- Type: TypeObject,
- Properties: make(Schemas),
+ Type: "object",
+ Properties: make(map[string]*SchemaRef),
}
}
+type compiledPattern struct {
+ Regexp *regexp.Regexp
+ ErrReason string
+}
+
func (schema *Schema) WithNullable() *Schema {
schema.Nullable = true
return schema
@@ -645,7 +242,6 @@ func (schema *Schema) WithMax(value float64) *Schema {
schema.Max = &value
return schema
}
-
func (schema *Schema) WithExclusiveMin(value bool) *Schema {
schema.ExclusiveMin = value
return schema
@@ -712,7 +308,6 @@ func (schema *Schema) WithMaxLengthDecodedBase64(i int64) *Schema {
func (schema *Schema) WithPattern(pattern string) *Schema {
schema.Pattern = pattern
- schema.compiledPattern = nil
return schema
}
@@ -749,7 +344,7 @@ func (schema *Schema) WithProperty(name string, propertySchema *Schema) *Schema
func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema {
properties := schema.Properties
if properties == nil {
- properties = make(Schemas)
+ properties = make(map[string]*SchemaRef)
schema.Properties = properties
}
properties[name] = ref
@@ -757,7 +352,7 @@ func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema {
}
func (schema *Schema) WithProperties(properties map[string]*Schema) *Schema {
- result := make(Schemas, len(properties))
+ result := make(map[string]*SchemaRef, len(properties))
for k, v := range properties {
result[k] = &SchemaRef{
Value: v,
@@ -780,28 +375,27 @@ func (schema *Schema) WithMaxProperties(i int64) *Schema {
}
func (schema *Schema) WithAnyAdditionalProperties() *Schema {
- schema.AdditionalProperties = AdditionalProperties{Has: BoolPtr(true)}
- return schema
-}
-
-func (schema *Schema) WithoutAdditionalProperties() *Schema {
- schema.AdditionalProperties = AdditionalProperties{Has: BoolPtr(false)}
+ schema.AdditionalProperties = nil
+ t := true
+ schema.AdditionalPropertiesAllowed = &t
return schema
}
func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema {
- schema.AdditionalProperties = AdditionalProperties{}
- if v != nil {
- schema.AdditionalProperties.Schema = &SchemaRef{Value: v}
+ if v == nil {
+ schema.AdditionalProperties = nil
+ } else {
+ schema.AdditionalProperties = &SchemaRef{
+ Value: v,
+ }
}
return schema
}
-// IsEmpty tells whether schema is equivalent to the empty schema `{}`.
func (schema *Schema) IsEmpty() bool {
if schema.Type != "" || schema.Format != "" || len(schema.Enum) != 0 ||
schema.UniqueItems || schema.ExclusiveMin || schema.ExclusiveMax ||
- schema.Nullable || schema.ReadOnly || schema.WriteOnly || schema.AllowEmptyValue ||
+ !schema.Nullable ||
schema.Min != nil || schema.Max != nil || schema.MultipleOf != nil ||
schema.MinLength != 0 || schema.MaxLength != nil || schema.Pattern != "" ||
schema.MinItems != 0 || schema.MaxItems != nil ||
@@ -812,10 +406,10 @@ func (schema *Schema) IsEmpty() bool {
if n := schema.Not; n != nil && !n.Value.IsEmpty() {
return false
}
- if ap := schema.AdditionalProperties.Schema; ap != nil && !ap.Value.IsEmpty() {
+ if ap := schema.AdditionalProperties; ap != nil && !ap.Value.IsEmpty() {
return false
}
- if apa := schema.AdditionalProperties.Has; apa != nil && !*apa {
+ if apa := schema.AdditionalPropertiesAllowed; apa != nil && !*apa {
return false
}
if items := schema.Items; items != nil && !items.Value.IsEmpty() {
@@ -844,33 +438,25 @@ func (schema *Schema) IsEmpty() bool {
return true
}
-// Validate returns an error if Schema does not comply with the OpenAPI spec.
-func (schema *Schema) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
- return schema.validate(ctx, []*Schema{})
+func (schema *Schema) Validate(c context.Context) error {
+ return schema.validate(c, []*Schema{})
}
-func (schema *Schema) validate(ctx context.Context, stack []*Schema) error {
- validationOpts := getValidationOptions(ctx)
-
+func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) {
for _, existing := range stack {
if existing == schema {
- return nil
+ return
}
}
stack = append(stack, schema)
- if schema.ReadOnly && schema.WriteOnly {
- return errors.New("a property MUST NOT be marked as both readOnly and writeOnly being true")
- }
-
for _, item := range schema.OneOf {
v := item.Value
if v == nil {
return foundUnresolvedRef(item.Ref)
}
- if err := v.validate(ctx, stack); err != nil {
- return err
+ if err = v.validate(c, stack); err == nil {
+ return
}
}
@@ -879,8 +465,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) error {
if v == nil {
return foundUnresolvedRef(item.Ref)
}
- if err := v.validate(ctx, stack); err != nil {
- return err
+ if err = v.validate(c, stack); err != nil {
+ return
}
}
@@ -889,8 +475,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) error {
if v == nil {
return foundUnresolvedRef(item.Ref)
}
- if err := v.validate(ctx, stack); err != nil {
- return err
+ if err = v.validate(c, stack); err != nil {
+ return
}
}
@@ -899,69 +485,60 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) error {
if v == nil {
return foundUnresolvedRef(ref.Ref)
}
- if err := v.validate(ctx, stack); err != nil {
- return err
+ if err = v.validate(c, stack); err != nil {
+ return
}
}
schemaType := schema.Type
switch schemaType {
case "":
- case TypeBoolean:
- case TypeNumber:
+ case "boolean":
+ case "number":
if format := schema.Format; len(format) > 0 {
switch format {
case "float", "double":
default:
- if validationOpts.schemaFormatValidationEnabled {
+ if !SchemaFormatValidationDisabled {
return unsupportedFormat(format)
}
}
}
- case TypeInteger:
+ case "integer":
if format := schema.Format; len(format) > 0 {
switch format {
case "int32", "int64":
default:
- if validationOpts.schemaFormatValidationEnabled {
+ if !SchemaFormatValidationDisabled {
return unsupportedFormat(format)
}
}
}
- case TypeString:
+ case "string":
if format := schema.Format; len(format) > 0 {
switch format {
- // Supported by OpenAPIv3.0.3:
- // https://spec.openapis.org/oas/v3.0.3
+ // Supported by OpenAPIv3.0.1:
case "byte", "binary", "date", "date-time", "password":
- // In JSON Draft-07 (not validated yet though):
- // https://json-schema.org/draft-07/json-schema-release-notes.html#formats
- case "iri", "iri-reference", "uri-template", "idn-email", "idn-hostname":
- case "json-pointer", "relative-json-pointer", "regex", "time":
- // In JSON Draft 2019-09 (not validated yet though):
- // https://json-schema.org/draft/2019-09/release-notes.html#format-vocabulary
- case "duration", "uuid":
- // Defined in some other specification
- case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference":
+ // In JSON Draft-07 (not validated yet though):
+ case "regex":
+ case "time", "email", "idn-email":
+ case "hostname", "idn-hostname", "ipv4", "ipv6":
+ case "uri", "uri-reference", "iri", "iri-reference", "uri-template":
+ case "json-pointer", "relative-json-pointer":
default:
// Try to check for custom defined formats
- if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled {
+ if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled {
return unsupportedFormat(format)
}
}
}
- if schema.Pattern != "" && !validationOpts.schemaPatternValidationDisabled {
- if err := schema.compilePattern(); err != nil {
- return err
- }
- }
- case TypeArray:
+ case "array":
if schema.Items == nil {
- return errors.New("when schema type is 'array', schema 'items' must be non-null")
+ return errors.New("When schema type is 'array', schema 'items' must be non-null")
}
- case TypeObject:
+ case "object":
default:
- return fmt.Errorf("unsupported 'type' value %q", schemaType)
+ return fmt.Errorf("Unsupported 'type' value '%s'", schemaType)
}
if ref := schema.Items; ref != nil {
@@ -969,100 +546,66 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) error {
if v == nil {
return foundUnresolvedRef(ref.Ref)
}
- if err := v.validate(ctx, stack); err != nil {
- return err
+ if err = v.validate(c, stack); err != nil {
+ return
}
}
- properties := make([]string, 0, len(schema.Properties))
- for name := range schema.Properties {
- properties = append(properties, name)
- }
- sort.Strings(properties)
- for _, name := range properties {
- ref := schema.Properties[name]
+ for _, ref := range schema.Properties {
v := ref.Value
if v == nil {
return foundUnresolvedRef(ref.Ref)
}
- if err := v.validate(ctx, stack); err != nil {
- return err
+ if err = v.validate(c, stack); err != nil {
+ return
}
}
- if schema.AdditionalProperties.Has != nil && schema.AdditionalProperties.Schema != nil {
- return errors.New("additionalProperties are set to both boolean and schema")
- }
- if ref := schema.AdditionalProperties.Schema; ref != nil {
+ if ref := schema.AdditionalProperties; ref != nil {
v := ref.Value
if v == nil {
return foundUnresolvedRef(ref.Ref)
}
- if err := v.validate(ctx, stack); err != nil {
- return err
- }
- }
-
- if v := schema.ExternalDocs; v != nil {
- if err := v.Validate(ctx); err != nil {
- return fmt.Errorf("invalid external docs: %w", err)
- }
- }
-
- 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 err := validateExampleValue(ctx, x, schema); err != nil {
- return fmt.Errorf("invalid example: %w", err)
+ if err = v.validate(c, stack); err != nil {
+ return
}
}
- return validateExtensions(ctx, schema.Extensions)
+ return
}
func (schema *Schema) IsMatching(value interface{}) bool {
- settings := newSchemaValidationSettings(FailFast())
- return schema.visitJSON(settings, value) == nil
+ return schema.visitJSON(value, true) == nil
}
func (schema *Schema) IsMatchingJSONBoolean(value bool) bool {
- settings := newSchemaValidationSettings(FailFast())
- return schema.visitJSON(settings, value) == nil
+ return schema.visitJSON(value, true) == nil
}
func (schema *Schema) IsMatchingJSONNumber(value float64) bool {
- settings := newSchemaValidationSettings(FailFast())
- return schema.visitJSON(settings, value) == nil
+ return schema.visitJSON(value, true) == nil
}
func (schema *Schema) IsMatchingJSONString(value string) bool {
- settings := newSchemaValidationSettings(FailFast())
- return schema.visitJSON(settings, value) == nil
+ return schema.visitJSON(value, true) == nil
}
func (schema *Schema) IsMatchingJSONArray(value []interface{}) bool {
- settings := newSchemaValidationSettings(FailFast())
- return schema.visitJSON(settings, value) == nil
+ return schema.visitJSON(value, true) == nil
}
func (schema *Schema) IsMatchingJSONObject(value map[string]interface{}) bool {
- settings := newSchemaValidationSettings(FailFast())
- return schema.visitJSON(settings, value) == nil
+ return schema.visitJSON(value, true) == nil
}
-func (schema *Schema) VisitJSON(value interface{}, opts ...SchemaValidationOption) error {
- settings := newSchemaValidationSettings(opts...)
- return schema.visitJSON(settings, value)
+func (schema *Schema) VisitJSON(value interface{}) error {
+ return schema.visitJSON(value, false)
}
-func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interface{}) (err error) {
+func (schema *Schema) visitJSON(value interface{}, fast bool) (err error) {
switch value := value.(type) {
case nil:
- return schema.visitJSONNull(settings)
+ return schema.visitJSONNull(fast)
case float64:
if math.IsNaN(value) {
return ErrSchemaInputNaN
@@ -1075,99 +618,48 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
if schema.IsEmpty() {
return
}
- if err = schema.visitSetOperations(settings, value); err != nil {
+ if err = schema.visitSetOperations(value, fast); err != nil {
return
}
switch value := value.(type) {
+ case nil:
+ return schema.visitJSONNull(fast)
case bool:
- return schema.visitJSONBoolean(settings, value)
- case json.Number:
- valueFloat64, err := value.Float64()
- if err != nil {
- return &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "type",
- Reason: "cannot convert json.Number to float64",
- customizeMessageError: settings.customizeMessageError,
- Origin: err,
- }
- }
- return schema.visitJSONNumber(settings, valueFloat64)
- case int:
- return schema.visitJSONNumber(settings, float64(value))
- case int32:
- return schema.visitJSONNumber(settings, float64(value))
- case int64:
- return schema.visitJSONNumber(settings, float64(value))
+ return schema.visitJSONBoolean(value, fast)
case float64:
- return schema.visitJSONNumber(settings, value)
+ return schema.visitJSONNumber(value, fast)
case string:
- return schema.visitJSONString(settings, value)
+ return schema.visitJSONString(value, fast)
case []interface{}:
- return schema.visitJSONArray(settings, value)
+ return schema.visitJSONArray(value, fast)
case map[string]interface{}:
- return schema.visitJSONObject(settings, value)
- case map[interface{}]interface{}: // for YAML cf. issue #444
- values := make(map[string]interface{}, len(value))
- for key, v := range value {
- if k, ok := key.(string); ok {
- values[k] = v
- }
- }
- if len(value) == len(values) {
- return schema.visitJSONObject(settings, values)
- }
- }
-
- // Catch slice of non-empty interface type
- if reflect.TypeOf(value).Kind() == reflect.Slice {
- valueR := reflect.ValueOf(value)
- newValue := make([]interface{}, 0, valueR.Len())
- for i := 0; i < valueR.Len(); i++ {
- newValue = append(newValue, valueR.Index(i).Interface())
+ return schema.visitJSONObject(value, fast)
+ default:
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "type",
+ Reason: fmt.Sprintf("Not a JSON value: %T", value),
}
- return schema.visitJSONArray(settings, newValue)
- }
-
- return &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "type",
- Reason: fmt.Sprintf("unhandled value of type %T", value),
- customizeMessageError: settings.customizeMessageError,
}
}
-func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) {
+func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err error) {
if enum := schema.Enum; len(enum) != 0 {
for _, v := range enum {
- switch c := value.(type) {
- case json.Number:
- var f float64
- if f, err = strconv.ParseFloat(c.String(), 64); err != nil {
- return err
- }
- if v == f {
- return
- }
- default:
- if reflect.DeepEqual(v, value) {
- return
- }
+ if value == v {
+ return
}
}
- if settings.failfast {
+ if fast {
return errSchema
}
- allowedValues, _ := json.Marshal(enum)
return &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "enum",
- Reason: fmt.Sprintf("value is not one of the allowed values %s", string(allowedValues)),
- customizeMessageError: settings.customizeMessageError,
+ Value: value,
+ Schema: schema,
+ SchemaField: "enum",
+ Reason: "JSON value is not one of the allowed values",
}
}
@@ -1176,146 +668,63 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
if v == nil {
return foundUnresolvedRef(ref.Ref)
}
- if err := v.visitJSON(settings, value); err == nil {
- if settings.failfast {
+ if err := v.visitJSON(value, true); err == nil {
+ if fast {
return errSchema
}
return &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "not",
- customizeMessageError: settings.customizeMessageError,
+ Value: value,
+ Schema: schema,
+ SchemaField: "not",
}
}
}
if v := schema.OneOf; len(v) > 0 {
- var discriminatorRef string
- if schema.Discriminator != nil {
- pn := schema.Discriminator.PropertyName
- if valuemap, okcheck := value.(map[string]interface{}); okcheck {
- discriminatorVal, okcheck := valuemap[pn]
- if !okcheck {
- return &SchemaError{
- Schema: schema,
- SchemaField: "discriminator",
- Reason: fmt.Sprintf("input does not contain the discriminator property %q", pn),
- }
- }
-
- discriminatorValString, okcheck := discriminatorVal.(string)
- if !okcheck {
- return &SchemaError{
- Value: discriminatorVal,
- Schema: schema,
- SchemaField: "discriminator",
- Reason: fmt.Sprintf("value of discriminator property %q is not a string", pn),
- }
- }
-
- if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck {
- return &SchemaError{
- Value: discriminatorVal,
- Schema: schema,
- SchemaField: "discriminator",
- Reason: fmt.Sprintf("discriminator property %q has invalid value", pn),
- }
- }
- }
- }
-
- var (
- ok = 0
- validationErrors = multiErrorForOneOf{}
- matchedOneOfIndices = make([]int, 0)
- tempValue = value
- )
- for idx, item := range v {
+ ok := 0
+ for _, item := range v {
v := item.Value
if v == nil {
return foundUnresolvedRef(item.Ref)
}
-
- if discriminatorRef != "" && discriminatorRef != item.Ref {
- continue
- }
-
- // make a deep copy to protect origin value from being injected default value that defined in mismatched oneOf schema
- if settings.asreq || settings.asrep {
- tempValue = deepcopy.Copy(value)
- }
-
- if err := v.visitJSON(settings, tempValue); err != nil {
- validationErrors = append(validationErrors, err)
- continue
+ if err := v.visitJSON(value, true); err == nil {
+ ok++
}
-
- matchedOneOfIndices = append(matchedOneOfIndices, idx)
- ok++
}
-
if ok != 1 {
- if settings.failfast {
+ if fast {
return errSchema
}
- e := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "oneOf",
- customizeMessageError: settings.customizeMessageError,
- }
- if ok > 1 {
- e.Origin = ErrOneOfConflict
- e.Reason = fmt.Sprintf(`value matches more than one schema from "oneOf" (matches schemas at indices %v)`, matchedOneOfIndices)
- } else {
- e.Origin = fmt.Errorf("doesn't match schema due to: %w", validationErrors)
- e.Reason = `value doesn't match any schema from "oneOf"`
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "oneOf",
}
-
- return e
- }
-
- // run again to inject default value that defined in matched oneOf schema
- if settings.asreq || settings.asrep {
- _ = v[matchedOneOfIndices[0]].Value.visitJSON(settings, value)
}
}
if v := schema.AnyOf; len(v) > 0 {
- var (
- ok = false
- matchedAnyOfIdx = 0
- tempValue = value
- )
- for idx, item := range v {
+ ok := false
+ for _, item := range v {
v := item.Value
if v == nil {
return foundUnresolvedRef(item.Ref)
}
- // make a deep copy to protect origin value from being injected default value that defined in mismatched anyOf schema
- if settings.asreq || settings.asrep {
- tempValue = deepcopy.Copy(value)
- }
- if err := v.visitJSON(settings, tempValue); err == nil {
+ if err := v.visitJSON(value, true); err == nil {
ok = true
- matchedAnyOfIdx = idx
break
}
}
if !ok {
- if settings.failfast {
+ if fast {
return errSchema
}
return &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "anyOf",
- Reason: `doesn't match any schema from "anyOf"`,
- customizeMessageError: settings.customizeMessageError,
+ Value: value,
+ Schema: schema,
+ SchemaField: "anyOf",
}
}
-
- _ = v[matchedAnyOfIdx].Value.visitJSON(settings, value)
}
for _, item := range schema.AllOf {
@@ -1323,188 +732,119 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
if v == nil {
return foundUnresolvedRef(item.Ref)
}
- if err := v.visitJSON(settings, value); err != nil {
- if settings.failfast {
+ if err := v.visitJSON(value, false); err != nil {
+ if fast {
return errSchema
}
return &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "allOf",
- Reason: `doesn't match all schemas from "allOf"`,
- Origin: err,
- customizeMessageError: settings.customizeMessageError,
+ Value: value,
+ Schema: schema,
+ SchemaField: "allOf",
+ Origin: err,
}
}
}
return
}
-// The value is not considered in visitJSONNull because according to the spec
-// "null is not supported as a type" unless `nullable` is also set to true
-// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#data-types
-// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object
-func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) {
+func (schema *Schema) visitJSONNull(fast bool) (err error) {
if schema.Nullable {
return
}
- if settings.failfast {
+ if fast {
return errSchema
}
return &SchemaError{
- Value: nil,
- Schema: schema,
- SchemaField: "nullable",
- Reason: "Value is not nullable",
- customizeMessageError: settings.customizeMessageError,
+ Value: nil,
+ Schema: schema,
+ SchemaField: "nullable",
+ Reason: "Value is not nullable",
}
}
func (schema *Schema) VisitJSONBoolean(value bool) error {
- settings := newSchemaValidationSettings()
- return schema.visitJSONBoolean(settings, value)
+ return schema.visitJSONBoolean(value, false)
}
-func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) {
- if schemaType := schema.Type; schemaType != "" && schemaType != TypeBoolean {
- return schema.expectedType(settings, value)
+func (schema *Schema) visitJSONBoolean(value bool, fast bool) (err error) {
+ if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" {
+ return schema.expectedType("boolean", fast)
}
return
}
func (schema *Schema) VisitJSONNumber(value float64) error {
- settings := newSchemaValidationSettings()
- return schema.visitJSONNumber(settings, value)
+ return schema.visitJSONNumber(value, false)
}
-func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error {
- var me MultiError
+func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) {
schemaType := schema.Type
- if schemaType == TypeInteger {
+ if schemaType == "integer" {
if bigFloat := big.NewFloat(value); !bigFloat.IsInt() {
- if settings.failfast {
- return errSchema
- }
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "type",
- Reason: fmt.Sprintf("value must be an integer"),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
- }
- me = append(me, err)
- }
- } else if schemaType != "" && schemaType != TypeNumber {
- return schema.expectedType(settings, value)
- }
-
- // formats
- if schemaType == TypeInteger && schema.Format != "" {
- formatMin := float64(0)
- formatMax := float64(0)
- switch schema.Format {
- case "int32":
- formatMin = formatMinInt32
- formatMax = formatMaxInt32
- case "int64":
- formatMin = formatMinInt64
- formatMax = formatMaxInt64
- default:
- if settings.formatValidationEnabled {
- return unsupportedFormat(schema.Format)
- }
- }
- if formatMin != 0 && formatMax != 0 && !(formatMin <= value && value <= formatMax) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "format",
- Reason: fmt.Sprintf("number must be an %s", schema.Format),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "type",
+ Reason: "Value must be an integer",
}
- me = append(me, err)
}
+ } else if schemaType != "" && schemaType != "number" {
+ return schema.expectedType("number, integer", fast)
}
// "exclusiveMinimum"
if v := schema.ExclusiveMin; v && !(*schema.Min < value) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "exclusiveMinimum",
- Reason: fmt.Sprintf("number must be more than %g", *schema.Min),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "exclusiveMinimum",
+ Reason: fmt.Sprintf("Number must be more than %g", *schema.Min),
}
- me = append(me, err)
}
// "exclusiveMaximum"
if v := schema.ExclusiveMax; v && !(*schema.Max > value) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "exclusiveMaximum",
- Reason: fmt.Sprintf("number must be less than %g", *schema.Max),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "exclusiveMaximum",
+ Reason: fmt.Sprintf("Number must be less than %g", *schema.Max),
}
- me = append(me, err)
}
// "minimum"
if v := schema.Min; v != nil && !(*v <= value) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "minimum",
- Reason: fmt.Sprintf("number must be at least %g", *v),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "minimum",
+ Reason: fmt.Sprintf("Number must be at least %g", *v),
}
- me = append(me, err)
}
// "maximum"
if v := schema.Max; v != nil && !(*v >= value) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "maximum",
- Reason: fmt.Sprintf("number must be at most %g", *v),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "maximum",
+ Reason: fmt.Sprintf("Number must be most %g", *v),
}
- me = append(me, err)
}
// "multipleOf"
@@ -1512,42 +852,28 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
// "A numeric instance is valid only if division by this keyword's
// value results in an integer."
if bigFloat := big.NewFloat(value / *v); !bigFloat.IsInt() {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "multipleOf",
- Reason: fmt.Sprintf("number must be a multiple of %g", *v),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "multipleOf",
}
- me = append(me, err)
}
}
-
- if len(me) > 0 {
- return me
- }
-
- return nil
+ return
}
func (schema *Schema) VisitJSONString(value string) error {
- settings := newSchemaValidationSettings()
- return schema.visitJSONString(settings, value)
+ return schema.visitJSONString(value, false)
}
-func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error {
- if schemaType := schema.Type; schemaType != "" && schemaType != TypeString {
- return schema.expectedType(settings, value)
+func (schema *Schema) visitJSONString(value string, fast bool) (err error) {
+ if schemaType := schema.Type; schemaType != "" && schemaType != "string" {
+ return schema.expectedType("string", fast)
}
- var me MultiError
-
// "minLength" and "maxLength"
minLength := schema.MinLength
maxLength := schema.MaxLength
@@ -1562,180 +888,121 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
}
}
if minLength != 0 && length < int64(minLength) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "minLength",
- Reason: fmt.Sprintf("minimum string length is %d", minLength),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "minLength",
+ Reason: fmt.Sprintf("Minimum string length is %d", minLength),
}
- me = append(me, err)
}
if maxLength != nil && length > int64(*maxLength) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "maxLength",
- Reason: fmt.Sprintf("maximum string length is %d", *maxLength),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "maxLength",
+ Reason: fmt.Sprintf("Maximum string length is %d", *maxLength),
}
- me = append(me, err)
}
}
- // "pattern"
- if schema.Pattern != "" && schema.compiledPattern == nil && !settings.patternValidationDisabled {
- var err error
- if err = schema.compilePattern(); err != nil {
- if !settings.multiError {
- return err
+ // "format" and "pattern"
+ cp := schema.compiledPattern
+ if cp == nil {
+ pattern := schema.Pattern
+ if v := schema.Pattern; len(v) > 0 {
+ // Pattern
+ re, err := regexp.Compile(v)
+ if err != nil {
+ return fmt.Errorf("Error while compiling regular expression '%s': %v", pattern, err)
}
- me = append(me, err)
- }
- }
- if cp := schema.compiledPattern; cp != nil && !cp.MatchString(value) {
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "pattern",
- Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
- }
- me = append(me, err)
- }
-
- // "format"
- var formatStrErr string
- var formatErr error
- if format := schema.Format; format != "" {
- if f, ok := SchemaStringFormats[format]; ok {
- switch {
- case f.regexp != nil && f.callback == nil:
- if cp := f.regexp; !cp.MatchString(value) {
- formatStrErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String())
- }
- case f.regexp == nil && f.callback != nil:
- if err := f.callback(value); err != nil {
- var schemaErr = &SchemaError{}
- if errors.As(err, &schemaErr) {
- formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%s)`, format, schemaErr.Reason)
- } else {
- formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, err)
- }
- formatErr = err
+ cp = &compiledPattern{
+ Regexp: re,
+ ErrReason: "JSON string doesn't match the regular expression '" + v + "'",
+ }
+ schema.compiledPattern = cp
+ } else if v := schema.Format; len(v) > 0 {
+ // No pattern, but does have a format
+ re := SchemaStringFormats[v]
+ if re != nil {
+ cp = &compiledPattern{
+ Regexp: re,
+ ErrReason: "JSON string doesn't match the format '" + v + " (regular expression `" + re.String() + "`)'",
}
- default:
- formatStrErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format)
+ schema.compiledPattern = cp
}
}
}
- if formatStrErr != "" || formatErr != nil {
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "format",
- Reason: formatStrErr,
- Origin: formatErr,
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ if cp != nil {
+ if !cp.Regexp.MatchString(value) {
+ field := "format"
+ if schema.Pattern != "" {
+ field = "pattern"
+ }
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: field,
+ Reason: cp.ErrReason,
+ }
}
- me = append(me, err)
-
}
-
- if len(me) > 0 {
- return me
- }
-
- return nil
+ return
}
func (schema *Schema) VisitJSONArray(value []interface{}) error {
- settings := newSchemaValidationSettings()
- return schema.visitJSONArray(settings, value)
+ return schema.visitJSONArray(value, false)
}
-func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error {
- if schemaType := schema.Type; schemaType != "" && schemaType != TypeArray {
- return schema.expectedType(settings, value)
+func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error) {
+ if schemaType := schema.Type; schemaType != "" && schemaType != "array" {
+ return schema.expectedType("array", fast)
}
- var me MultiError
-
lenValue := int64(len(value))
// "minItems"
if v := schema.MinItems; v != 0 && lenValue < int64(v) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "minItems",
- Reason: fmt.Sprintf("minimum number of items is %d", v),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "minItems",
+ Reason: fmt.Sprintf("Minimum number of items is %d", v),
}
- me = append(me, err)
}
// "maxItems"
if v := schema.MaxItems; v != nil && lenValue > int64(*v) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "maxItems",
- Reason: fmt.Sprintf("maximum number of items is %d", *v),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "maxItems",
+ Reason: fmt.Sprintf("Maximum number of items is %d", *v),
}
- me = append(me, err)
}
// "uniqueItems"
- if sliceUniqueItemsChecker == nil {
- sliceUniqueItemsChecker = isSliceOfUniqueItems
- }
if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "uniqueItems",
- Reason: "duplicate items found",
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "uniqueItems",
+ Reason: fmt.Sprintf("Duplicate items found"),
}
- me = append(me, err)
}
// "items"
@@ -1745,65 +1012,21 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [
return foundUnresolvedRef(itemSchemaRef.Ref)
}
for i, item := range value {
- if err := itemSchema.visitJSON(settings, item); err != nil {
- err = markSchemaErrorIndex(err, i)
- if !settings.multiError {
- return err
- }
- if itemMe, ok := err.(MultiError); ok {
- me = append(me, itemMe...)
- } else {
- me = append(me, err)
- }
+ if err := itemSchema.VisitJSON(item); err != nil {
+ return markSchemaErrorIndex(err, i)
}
}
}
-
- if len(me) > 0 {
- return me
- }
-
- return nil
+ return
}
func (schema *Schema) VisitJSONObject(value map[string]interface{}) error {
- settings := newSchemaValidationSettings()
- return schema.visitJSONObject(settings, value)
+ return schema.visitJSONObject(value, false)
}
-func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error {
- if schemaType := schema.Type; schemaType != "" && schemaType != TypeObject {
- return schema.expectedType(settings, value)
- }
-
- var me MultiError
-
- if settings.asreq || settings.asrep {
- properties := make([]string, 0, len(schema.Properties))
- for propName := range schema.Properties {
- properties = append(properties, propName)
- }
- sort.Strings(properties)
- for _, propName := range properties {
- propSchema := schema.Properties[propName]
- reqRO := settings.asreq && propSchema.Value.ReadOnly && !settings.readOnlyValidationDisabled
- repWO := settings.asrep && propSchema.Value.WriteOnly && !settings.writeOnlyValidationDisabled
-
- if f := settings.defaultsSet; f != nil && value[propName] == nil {
- if dflt := propSchema.Value.Default; dflt != nil && !reqRO && !repWO {
- value[propName] = dflt
- settings.onceSettingDefaults.Do(f)
- }
- }
-
- if value[propName] != nil {
- if reqRO {
- me = append(me, fmt.Errorf("readOnly property %q in request", propName))
- } else if repWO {
- me = append(me, fmt.Errorf("writeOnly property %q in response", propName))
- }
- }
- }
+func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) (err error) {
+ if schemaType := schema.Type; schemaType != "" && schemaType != "object" {
+ return schema.expectedType("object", fast)
}
// "properties"
@@ -1812,52 +1035,36 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
// "minProperties"
if v := schema.MinProps; v != 0 && lenValue < int64(v) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "minProperties",
- Reason: fmt.Sprintf("there must be at least %d properties", v),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "minProperties",
+ Reason: fmt.Sprintf("There must be at least %d properties", v),
}
- me = append(me, err)
}
// "maxProperties"
if v := schema.MaxProps; v != nil && lenValue > int64(*v) {
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "maxProperties",
- Reason: fmt.Sprintf("there must be at most %d properties", *v),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "maxProperties",
+ Reason: fmt.Sprintf("There must be at most %d properties", *v),
}
- me = append(me, err)
}
// "additionalProperties"
var additionalProperties *Schema
- if ref := schema.AdditionalProperties.Schema; ref != nil {
+ if ref := schema.AdditionalProperties; ref != nil {
additionalProperties = ref.Value
}
- keys := make([]string, 0, len(value))
- for k := range value {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- for _, k := range keys {
- v := value[k]
+ for k, v := range value {
if properties != nil {
propertyRef := properties[k]
if propertyRef != nil {
@@ -1865,165 +1072,88 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
if p == nil {
return foundUnresolvedRef(propertyRef.Ref)
}
- if err := p.visitJSON(settings, v); err != nil {
- if settings.failfast {
+ if err := p.VisitJSON(v); err != nil {
+ if fast {
return errSchema
}
- err = markSchemaErrorKey(err, k)
- if !settings.multiError {
- return err
- }
- if v, ok := err.(MultiError); ok {
- me = append(me, v...)
- continue
- }
- me = append(me, err)
+ return markSchemaErrorKey(err, k)
}
continue
}
}
- if allowed := schema.AdditionalProperties.Has; allowed == nil || *allowed {
+ allowed := schema.AdditionalPropertiesAllowed
+ if additionalProperties != nil || allowed == nil || (allowed != nil && *allowed) {
if additionalProperties != nil {
- if err := additionalProperties.visitJSON(settings, v); err != nil {
- if settings.failfast {
+ if err := additionalProperties.VisitJSON(v); err != nil {
+ if fast {
return errSchema
}
- err = markSchemaErrorKey(err, k)
- if !settings.multiError {
- return err
- }
- if v, ok := err.(MultiError); ok {
- me = append(me, v...)
- continue
- }
- me = append(me, err)
+ return markSchemaErrorKey(err, k)
}
}
continue
}
- if settings.failfast {
+ if fast {
return errSchema
}
- err := &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "properties",
- Reason: fmt.Sprintf("property %q is unsupported", k),
- customizeMessageError: settings.customizeMessageError,
- }
- if !settings.multiError {
- return err
+ return &SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "properties",
+ Reason: fmt.Sprintf("Property '%s' is unsupported", k),
}
- me = append(me, err)
}
-
- // "required"
for _, k := range schema.Required {
if _, ok := value[k]; !ok {
- if s := schema.Properties[k]; s != nil && s.Value.ReadOnly && settings.asreq {
- continue
- }
- if s := schema.Properties[k]; s != nil && s.Value.WriteOnly && settings.asrep {
- continue
- }
- if settings.failfast {
+ if fast {
return errSchema
}
- err := markSchemaErrorKey(&SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "required",
- Reason: fmt.Sprintf("property %q is missing", k),
- customizeMessageError: settings.customizeMessageError,
+ return markSchemaErrorKey(&SchemaError{
+ Value: value,
+ Schema: schema,
+ SchemaField: "required",
+ Reason: fmt.Sprintf("Property '%s' is missing", k),
}, k)
- if !settings.multiError {
- return err
- }
- me = append(me, err)
}
}
-
- if len(me) > 0 {
- return me
- }
-
- return nil
+ return
}
-func (schema *Schema) expectedType(settings *schemaValidationSettings, value interface{}) error {
- if settings.failfast {
+func (schema *Schema) expectedType(typ string, fast bool) error {
+ if fast {
return errSchema
}
-
- a := "a"
- switch schema.Type {
- case TypeArray, TypeObject, TypeInteger:
- a = "an"
- }
return &SchemaError{
- Value: value,
- Schema: schema,
- SchemaField: "type",
- Reason: fmt.Sprintf("value must be %s %s", a, schema.Type),
- customizeMessageError: settings.customizeMessageError,
+ Value: typ,
+ Schema: schema,
+ SchemaField: "type",
+ Reason: "Field must be set to " + schema.Type + " or not be present",
}
}
-func (schema *Schema) compilePattern() (err error) {
- if schema.compiledPattern, err = regexp.Compile(schema.Pattern); err != nil {
- return &SchemaError{
- Schema: schema,
- SchemaField: "pattern",
- Origin: err,
- Reason: fmt.Sprintf("cannot compile pattern %q: %v", schema.Pattern, err),
- }
- }
- return nil
-}
-
-// SchemaError is an error that occurs during schema validation.
type SchemaError struct {
- // Value is the value that failed validation.
- Value interface{}
- // reversePath is the path to the value that failed validation.
+ Value interface{}
reversePath []string
- // Schema is the schema that failed validation.
- Schema *Schema
- // SchemaField is the field of the schema that failed validation.
+ Schema *Schema
SchemaField string
- // Reason is a human-readable message describing the error.
- // The message should never include the original value to prevent leakage of potentially sensitive inputs in error messages.
- Reason string
- // Origin is the original error that caused this error.
- Origin error
- // customizeMessageError is a function that can be used to customize the error message.
- customizeMessageError func(err *SchemaError) string
+ Reason string
+ Origin error
}
-var _ interface{ Unwrap() error } = SchemaError{}
-
func markSchemaErrorKey(err error, key string) error {
- var me multiErrorForOneOf
-
- if errors.As(err, &me) {
- err = me.Unwrap()
- }
-
if v, ok := err.(*SchemaError); ok {
v.reversePath = append(v.reversePath, key)
return v
}
- if v, ok := err.(MultiError); ok {
- for _, e := range v {
- _ = markSchemaErrorKey(e, key)
- }
- return v
- }
return err
}
func markSchemaErrorIndex(err error, index int) error {
- return markSchemaErrorKey(err, strconv.FormatInt(int64(index), 10))
+ if v, ok := err.(*SchemaError); ok {
+ v.reversePath = append(v.reversePath, strconv.FormatInt(int64(index), 10))
+ return v
+ }
+ return err
}
func (err *SchemaError) JSONPointer() []string {
@@ -2036,14 +1166,11 @@ func (err *SchemaError) JSONPointer() []string {
}
func (err *SchemaError) Error() string {
- if err.customizeMessageError != nil {
- if msg := err.customizeMessageError(err); msg != "" {
- return msg
- }
+ if err.Origin != nil {
+ return err.Origin.Error()
}
buf := bytes.NewBuffer(make([]byte, 0, 256))
-
if len(err.reversePath) > 0 {
buf.WriteString(`Error at "`)
reversePath := err.reversePath
@@ -2051,15 +1178,8 @@ func (err *SchemaError) Error() string {
buf.WriteByte('/')
buf.WriteString(reversePath[i])
}
- buf.WriteString(`": `)
+ buf.WriteString(`":`)
}
-
- if err.Origin != nil {
- buf.WriteString(err.Origin.Error())
-
- return buf.String()
- }
-
reason := err.Reason
if reason == "" {
buf.WriteString(`Doesn't match schema "`)
@@ -2068,7 +1188,6 @@ func (err *SchemaError) Error() string {
} else {
buf.WriteString(reason)
}
-
if !SchemaErrorDetailsDisabled {
buf.WriteString("\nSchema:\n ")
encoder := json.NewEncoder(buf)
@@ -2081,14 +1200,9 @@ func (err *SchemaError) Error() string {
panic(err)
}
}
-
return buf.String()
}
-func (err SchemaError) Unwrap() error {
- return err.Origin
-}
-
func isSliceOfUniqueItems(xs []interface{}) bool {
s := len(xs)
m := make(map[string]struct{}, s)
@@ -2117,5 +1231,5 @@ func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) {
}
func unsupportedFormat(format string) error {
- return fmt.Errorf("unsupported 'format' value %q", format)
+ return fmt.Errorf("Unsupported 'format' value '%s'", format)
}
diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go
index ea38400c2..746e40882 100644
--- a/openapi3/schema_formats.go
+++ b/openapi3/schema_formats.go
@@ -2,87 +2,30 @@ package openapi3
import (
"fmt"
- "net"
"regexp"
- "strings"
)
const (
// FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122
- FormatOfStringForUUIDOfRFC4122 = `^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$`
-
- // FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses.
- // Use DefineStringFormat(...) if you need something stricter.
- FormatOfStringForEmail = `^[^@]+@[^@<>",\s]+$`
+ FormatOfStringForUUIDOfRFC4122 = `^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`
)
-// FormatCallback performs custom checks on exotic formats
-type FormatCallback func(value string) error
-
-// Format represents a format validator registered by either DefineStringFormat or DefineStringFormatCallback
-type Format struct {
- regexp *regexp.Regexp
- callback FormatCallback
-}
+var SchemaStringFormats = make(map[string]*regexp.Regexp, 8)
-// SchemaStringFormats allows for validating string formats
-var SchemaStringFormats = make(map[string]Format, 4)
-
-// DefineStringFormat defines a new regexp pattern for a given format
func DefineStringFormat(name string, pattern string) {
re, err := regexp.Compile(pattern)
if err != nil {
- err := fmt.Errorf("format %q has invalid pattern %q: %w", name, pattern, err)
+ err := fmt.Errorf("Format '%v' has invalid pattern '%v': %v", name, pattern, err)
panic(err)
}
- SchemaStringFormats[name] = Format{regexp: re}
-}
-
-// DefineStringFormatCallback adds a validation function for a specific schema format entry
-func DefineStringFormatCallback(name string, callback FormatCallback) {
- SchemaStringFormats[name] = Format{callback: callback}
-}
-
-func validateIP(ip string) error {
- parsed := net.ParseIP(ip)
- if parsed == nil {
- return &SchemaError{
- Value: ip,
- Reason: "Not an IP address",
- }
- }
- return nil
-}
-
-func validateIPv4(ip string) error {
- if err := validateIP(ip); err != nil {
- return err
- }
-
- if !(strings.Count(ip, ":") < 2) {
- return &SchemaError{
- Value: ip,
- Reason: "Not an IPv4 address (it's IPv6)",
- }
- }
- return nil
-}
-
-func validateIPv6(ip string) error {
- if err := validateIP(ip); err != nil {
- return err
- }
-
- if !(strings.Count(ip, ":") >= 2) {
- return &SchemaError{
- Value: ip,
- Reason: "Not an IPv6 address (it's IPv4)",
- }
- }
- return nil
+ SchemaStringFormats[name] = re
}
func init() {
+ // This pattern catches only some suspiciously wrong-looking email addresses.
+ // Use DefineStringFormat(...) if you need something stricter.
+ DefineStringFormat("email", `^[^@]+@[^@<>",\s]+$`)
+
// Base64
// The pattern supports base64 and b./ase64url. Padding ('=') is supported.
DefineStringFormat("byte", `(^$|^[a-zA-Z0-9+/\-_]*=*$)`)
@@ -91,16 +34,5 @@ func init() {
DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`)
// date-time
- DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`)
-
-}
-
-// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec
-func DefineIPv4Format() {
- DefineStringFormatCallback("ipv4", validateIPv4)
-}
-
-// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec
-func DefineIPv6Format() {
- DefineStringFormatCallback("ipv6", validateIPv6)
+ DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`)
}
diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go
deleted file mode 100644
index 70092d6de..000000000
--- a/openapi3/schema_formats_test.go
+++ /dev/null
@@ -1,156 +0,0 @@
-package openapi3
-
-import (
- "context"
- "errors"
- "fmt"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue430(t *testing.T) {
- schema := NewOneOfSchema(
- NewStringSchema().WithFormat("ipv4"),
- NewStringSchema().WithFormat("ipv6"),
- )
-
- delete(SchemaStringFormats, "ipv4")
- delete(SchemaStringFormats, "ipv6")
-
- err := schema.Validate(context.Background())
- require.NoError(t, err)
-
- data := map[string]bool{
- "127.0.1.1": true,
-
- // https://stackoverflow.com/a/48519490/1418165
-
- // v4
- "192.168.0.1": true,
- // "192.168.0.1:80" doesn't parse per net.ParseIP()
-
- // v6
- "::FFFF:C0A8:1": false,
- "::FFFF:C0A8:0001": false,
- "0000:0000:0000:0000:0000:FFFF:C0A8:1": false,
- // "::FFFF:C0A8:1%1" doesn't parse per net.ParseIP()
- "::FFFF:192.168.0.1": false,
- // "[::FFFF:C0A8:1]:80" doesn't parse per net.ParseIP()
- // "[::FFFF:C0A8:1%1]:80" doesn't parse per net.ParseIP()
- }
-
- for datum := range data {
- err = schema.VisitJSON(datum)
- require.Error(t, err, ErrOneOfConflict.Error())
- }
-
- DefineIPv4Format()
- DefineIPv6Format()
-
- for datum, isV4 := range data {
- err = schema.VisitJSON(datum)
- require.NoError(t, err)
- if isV4 {
- require.Nil(t, validateIPv4(datum), "%q should be IPv4", datum)
- require.NotNil(t, validateIPv6(datum), "%q should not be IPv6", datum)
- } else {
- require.NotNil(t, validateIPv4(datum), "%q should not be IPv4", datum)
- require.Nil(t, validateIPv6(datum), "%q should be IPv6", datum)
- }
- }
-}
-
-func TestFormatCallback_WrapError(t *testing.T) {
- var errSomething = errors.New("something error")
-
- DefineStringFormatCallback("foobar", func(value string) error {
- return errSomething
- })
-
- s := &Schema{Format: "foobar"}
- err := s.VisitJSONString("blablabla")
-
- assert.ErrorIs(t, err, errSomething)
-
- delete(SchemaStringFormats, "foobar")
-}
-
-func TestReversePathInMessageSchemaError(t *testing.T) {
- DefineIPv4Format()
-
- SchemaErrorDetailsDisabled = true
-
- const spc = `
-components:
- schemas:
- Something:
- type: object
- properties:
- ip:
- type: string
- format: ipv4
-`
- l := NewLoader()
-
- doc, err := l.LoadFromData([]byte(spc))
- require.NoError(t, err)
-
- err = doc.Components.Schemas["Something"].Value.VisitJSON(map[string]interface{}{
- `ip`: `123.0.0.11111`,
- })
-
- require.EqualError(t, err, `Error at "/ip": Not an IP address`)
-
- delete(SchemaStringFormats, "ipv4")
- SchemaErrorDetailsDisabled = false
-}
-
-func TestUuidFormat(t *testing.T) {
-
- type testCase struct {
- name string
- value string
- wantErr bool
- }
-
- DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122)
- testCases := []testCase{
- {
- name: "invalid",
- value: "foo",
- wantErr: true,
- },
- {
- name: "uuid v1",
- value: "77e66540-ca29-11ed-afa1-0242ac120002",
- wantErr: false,
- },
- {
- name: "uuid v4",
- value: "00f4d301-b9f4-4366-8907-2b5a03430aa1",
- wantErr: false,
- },
- {
- name: "uuid nil",
- value: "00000000-0000-0000-0000-000000000000",
- wantErr: false,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- err := NewUUIDSchema().VisitJSON(tc.value)
- var schemaError = &SchemaError{}
- if tc.wantErr {
- require.Error(t, err)
- require.ErrorAs(t, err, &schemaError)
-
- require.NotZero(t, schemaError.Reason)
- require.NotContains(t, schemaError.Reason, fmt.Sprint(tc.value))
- } else {
- require.Nil(t, err)
- }
- })
- }
-}
diff --git a/openapi3/schema_issue289_test.go b/openapi3/schema_issue289_test.go
deleted file mode 100644
index 56d1d4562..000000000
--- a/openapi3/schema_issue289_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue289(t *testing.T) {
- spec := []byte(`components:
- schemas:
- Server:
- properties:
- address:
- oneOf:
- - $ref: "#/components/schemas/ip-address"
- - $ref: "#/components/schemas/domain-name"
- name:
- type: string
- type: object
- domain-name:
- maxLength: 10
- minLength: 5
- pattern: "((([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.)*([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.?)|\\."
- type: string
- ip-address:
- pattern: "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$"
- type: string
-openapi: "3.0.1"
-`)
-
- s, err := NewLoader().LoadFromData(spec)
- require.NoError(t, err)
- err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{
- "name": "kin-openapi",
- "address": "127.0.0.1",
- })
- require.ErrorIs(t, err, ErrOneOfConflict)
-}
diff --git a/openapi3/schema_issue492_test.go b/openapi3/schema_issue492_test.go
deleted file mode 100644
index 535c82a66..000000000
--- a/openapi3/schema_issue492_test.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestIssue492(t *testing.T) {
- spec := []byte(`components:
- schemas:
- Server:
- properties:
- time:
- $ref: "#/components/schemas/timestamp"
- name:
- type: string
- type: object
- timestamp:
- type: string
- format: date-time
-openapi: "3.0.1"
-`)
-
- s, err := NewLoader().LoadFromData(spec)
- require.NoError(t, err)
-
- // verify that the expected format works
- err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{
- "name": "kin-openapi",
- "time": "2001-02-03T04:05:06.789Z",
- })
- require.NoError(t, err)
-
- // verify that the issue is fixed
- err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{
- "name": "kin-openapi",
- "time": "2001-02-03T04:05:06:789Z",
- })
- require.EqualError(t, err, "Error at \"/time\": string doesn't match the format \"date-time\" (regular expression \"^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|(\\+|-)[0-9]{2}:[0-9]{2})?$\")\nSchema:\n {\n \"format\": \"date-time\",\n \"type\": \"string\"\n }\n\nValue:\n \"2001-02-03T04:05:06:789Z\"\n")
-}
diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go
deleted file mode 100644
index 8d5451950..000000000
--- a/openapi3/schema_oneOf_test.go
+++ /dev/null
@@ -1,184 +0,0 @@
-package openapi3
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-var oneofSpec = []byte(`components:
- schemas:
- Cat:
- type: object
- properties:
- name:
- type: string
- scratches:
- type: boolean
- $type:
- type: string
- enum:
- - cat
- required:
- - name
- - scratches
- - $type
- Dog:
- type: object
- properties:
- name:
- type: string
- barks:
- type: boolean
- $type:
- type: string
- enum:
- - dog
- required:
- - name
- - barks
- - $type
- Animal:
- type: object
- oneOf:
- - $ref: "#/components/schemas/Cat"
- - $ref: "#/components/schemas/Dog"
- discriminator:
- propertyName: $type
- mapping:
- cat: "#/components/schemas/Cat"
- dog: "#/components/schemas/Dog"
-`)
-
-var oneofNoDiscriminatorSpec = []byte(`components:
- schemas:
- Cat:
- type: object
- properties:
- name:
- type: string
- scratches:
- type: boolean
- required:
- - name
- - scratches
- Dog:
- type: object
- properties:
- name:
- type: string
- barks:
- type: boolean
- required:
- - name
- - barks
- Animal:
- type: object
- oneOf:
- - $ref: "#/components/schemas/Cat"
- - $ref: "#/components/schemas/Dog"
-`)
-
-func TestVisitJSON_OneOf_MissingDiscriptorProperty(t *testing.T) {
- s, err := NewLoader().LoadFromData(oneofSpec)
- require.NoError(t, err)
- err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
- "name": "snoopy",
- })
- require.ErrorContains(t, err, "input does not contain the discriminator property \"$type\"\n")
-}
-
-func TestVisitJSON_OneOf_MissingDiscriptorValue(t *testing.T) {
- s, err := NewLoader().LoadFromData(oneofSpec)
- require.NoError(t, err)
- err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
- "name": "snoopy",
- "$type": "snake",
- })
- require.ErrorContains(t, err, "discriminator property \"$type\" has invalid value")
-}
-
-func TestVisitJSON_OneOf_MissingField(t *testing.T) {
- s, err := NewLoader().LoadFromData(oneofSpec)
- require.NoError(t, err)
- err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
- "name": "snoopy",
- "$type": "dog",
- })
- require.EqualError(t, err, "doesn't match schema due to: Error at \"/barks\": property \"barks\" is missing\nSchema:\n {\n \"properties\": {\n \"$type\": {\n \"enum\": [\n \"dog\"\n ],\n \"type\": \"string\"\n },\n \"barks\": {\n \"type\": \"boolean\"\n },\n \"name\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"barks\",\n \"$type\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"$type\": \"dog\",\n \"name\": \"snoopy\"\n }\n")
-}
-
-func TestVisitJSON_OneOf_NoDiscriptor_MissingField(t *testing.T) {
- s, err := NewLoader().LoadFromData(oneofNoDiscriminatorSpec)
- require.NoError(t, err)
- err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
- "name": "snoopy",
- })
- require.EqualError(t, err, "doesn't match schema due to: Error at \"/scratches\": property \"scratches\" is missing\nSchema:\n {\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"scratches\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"name\",\n \"scratches\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"name\": \"snoopy\"\n }\n Or Error at \"/barks\": property \"barks\" is missing\nSchema:\n {\n \"properties\": {\n \"barks\": {\n \"type\": \"boolean\"\n },\n \"name\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"barks\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"name\": \"snoopy\"\n }\n")
-}
-
-func TestVisitJSON_OneOf_BadDescriminatorType(t *testing.T) {
- s, err := NewLoader().LoadFromData(oneofSpec)
- require.NoError(t, err)
- err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
- "name": "snoopy",
- "scratches": true,
- "$type": 1,
- })
- require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string")
-
- err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
- "name": "snoopy",
- "barks": true,
- "$type": nil,
- })
- require.ErrorContains(t, err, "value of discriminator property \"$type\" is not a string")
-}
-
-func TestVisitJSON_OneOf_Path(t *testing.T) {
- t.Parallel()
-
- loader := NewLoader()
- spc := `
-components:
- schemas:
- Something:
- type: object
- properties:
- first:
- type: object
- properties:
- second:
- type: object
- properties:
- third:
- oneOf:
- - title: First rule
- type: string
- minLength: 5
- maxLength: 5
- - title: Second rule
- type: string
- minLength: 10
- maxLength: 10
-`[1:]
-
- doc, err := loader.LoadFromData([]byte(spc))
- require.NoError(t, err)
-
- err = doc.Components.Schemas["Something"].Value.VisitJSON(map[string]interface{}{
- "first": map[string]interface{}{
- "second": map[string]interface{}{
- "third": "123456789",
- },
- },
- })
-
- assert.Contains(t, err.Error(), `Error at "/first/second/third"`)
-
- var sErr *SchemaError
-
- assert.ErrorAs(t, err, &sErr)
- assert.Equal(t, []string{"first", "second", "third"}, sErr.JSONPointer())
-}
diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go
index 26669799a..e82aba26b 100644
--- a/openapi3/schema_test.go
+++ b/openapi3/schema_test.go
@@ -1,28 +1,27 @@
-package openapi3
+package openapi3_test
import (
"context"
"encoding/base64"
"encoding/json"
"math"
- "reflect"
"strings"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
- "gopkg.in/yaml.v3"
)
type schemaExample struct {
Title string
- Schema *Schema
+ Schema *openapi3.Schema
Serialization interface{}
AllValid []interface{}
AllInvalid []interface{}
}
func TestSchemas(t *testing.T) {
- DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122)
+ openapi3.DefineStringFormat("uuid", openapi3.FormatOfStringForUUIDOfRFC4122)
for _, example := range schemaExamples {
t.Run(example.Title, testSchema(t, example))
}
@@ -37,61 +36,43 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) {
jsonSchema, err := json.Marshal(schema)
require.NoError(t, err)
require.JSONEq(t, string(jsonSerialized), string(jsonSchema))
- var dataUnserialized Schema
+ var dataUnserialized openapi3.Schema
err = json.Unmarshal(jsonSerialized, &dataUnserialized)
require.NoError(t, err)
- var dataSchema Schema
+ var dataSchema openapi3.Schema
err = json.Unmarshal(jsonSchema, &dataSchema)
require.NoError(t, err)
require.Equal(t, dataUnserialized, dataSchema)
}
- for validateFuncIndex, validateFunc := range validateSchemaFuncs {
- for index, value := range example.AllValid {
- err := validateFunc(t, schema, value)
- require.NoErrorf(t, err, "ValidateFunc #%d, AllValid #%d: %#v", validateFuncIndex, index, value)
- }
- for index, value := range example.AllInvalid {
- err := validateFunc(t, schema, value)
- require.Errorf(t, err, "ValidateFunc #%d, AllInvalid #%d: %#v", validateFuncIndex, index, value)
- }
+ for _, value := range example.AllValid {
+ err := validateSchema(t, schema, value)
+ require.NoError(t, err)
+ }
+ for _, value := range example.AllInvalid {
+ err := validateSchema(t, schema, value)
+ require.Error(t, err)
}
// NaN and Inf aren't valid JSON but are handled
- for index, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} {
+ for _, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} {
err := schema.VisitJSON(value)
- require.Errorf(t, err, "NaNAndInf #%d: %#v", index, value)
+ require.Error(t, err)
}
}
}
-func validateSchemaJSON(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error {
+func validateSchema(t *testing.T, schema *openapi3.Schema, value interface{}) error {
data, err := json.Marshal(value)
require.NoError(t, err)
var val interface{}
err = json.Unmarshal(data, &val)
require.NoError(t, err)
- return schema.VisitJSON(val, opts...)
-}
-
-func validateSchemaYAML(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error {
- data, err := yaml.Marshal(value)
- require.NoError(t, err)
- var val interface{}
- err = yaml.Unmarshal(data, &val)
- require.NoError(t, err)
- return schema.VisitJSON(val, opts...)
-}
-
-type validateSchemaFunc func(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error
-
-var validateSchemaFuncs = []validateSchemaFunc{
- validateSchemaJSON,
- validateSchemaYAML,
+ return schema.VisitJSON(val)
}
var schemaExamples = []schemaExample{
{
Title: "EMPTY SCHEMA",
- Schema: &Schema{},
+ Schema: &openapi3.Schema{},
Serialization: map[string]interface{}{
// This OA3 schema is exactly this draft-04 schema:
// {"not": {"type": "null"}}
@@ -111,7 +92,7 @@ var schemaExamples = []schemaExample{
{
Title: "JUST NULLABLE",
- Schema: NewSchema().WithNullable(),
+ Schema: openapi3.NewSchema().WithNullable(),
Serialization: map[string]interface{}{
// This OA3 schema is exactly both this draft-04 schema: {} and:
// {anyOf: [type:string, type:number, type:integer, type:boolean
@@ -133,7 +114,7 @@ var schemaExamples = []schemaExample{
{
Title: "NULLABLE BOOLEAN",
- Schema: NewBoolSchema().WithNullable(),
+ Schema: openapi3.NewBoolSchema().WithNullable(),
Serialization: map[string]interface{}{
"nullable": true,
"type": "boolean",
@@ -155,9 +136,9 @@ var schemaExamples = []schemaExample{
{
Title: "NULLABLE ANYOF",
- Schema: NewAnyOfSchema(
- NewIntegerSchema(),
- NewFloat64Schema(),
+ Schema: openapi3.NewAnyOfSchema(
+ openapi3.NewIntegerSchema(),
+ openapi3.NewFloat64Schema(),
).WithNullable(),
Serialization: map[string]interface{}{
"nullable": true,
@@ -181,7 +162,7 @@ var schemaExamples = []schemaExample{
{
Title: "BOOLEAN",
- Schema: NewBoolSchema(),
+ Schema: openapi3.NewBoolSchema(),
Serialization: map[string]interface{}{
"type": "boolean",
},
@@ -200,7 +181,7 @@ var schemaExamples = []schemaExample{
{
Title: "NUMBER",
- Schema: NewFloat64Schema().
+ Schema: openapi3.NewFloat64Schema().
WithMin(2.5).
WithMax(3.5),
Serialization: map[string]interface{}{
@@ -227,7 +208,7 @@ var schemaExamples = []schemaExample{
{
Title: "INTEGER",
- Schema: NewInt64Schema().
+ Schema: openapi3.NewInt64Schema().
WithMin(2).
WithMax(5),
Serialization: map[string]interface{}{
@@ -252,59 +233,10 @@ var schemaExamples = []schemaExample{
map[string]interface{}{},
},
},
- {
- Title: "INTEGER OPTIONAL INT64 FORMAT",
- Schema: NewInt64Schema(),
- Serialization: map[string]interface{}{
- "type": "integer",
- "format": "int64",
- },
- AllValid: []interface{}{
- 1,
- 256,
- 65536,
- int64(math.MaxInt32) + 10,
- int64(math.MinInt32) - 10,
- },
- AllInvalid: []interface{}{
- nil,
- false,
- 3.5,
- true,
- "",
- []interface{}{},
- map[string]interface{}{},
- },
- },
- {
- Title: "INTEGER OPTIONAL INT32 FORMAT",
- Schema: NewInt32Schema(),
- Serialization: map[string]interface{}{
- "type": "integer",
- "format": "int32",
- },
- AllValid: []interface{}{
- 1,
- 256,
- 65536,
- int64(math.MaxInt32),
- int64(math.MaxInt32),
- },
- AllInvalid: []interface{}{
- nil,
- false,
- 3.5,
- int64(math.MaxInt32) + 10,
- int64(math.MinInt32) - 10,
- true,
- "",
- []interface{}{},
- map[string]interface{}{},
- },
- },
+
{
Title: "STRING",
- Schema: NewStringSchema().
+ Schema: openapi3.NewStringSchema().
WithMinLength(2).
WithMaxLength(3).
WithPattern("^[abc]+$"),
@@ -333,7 +265,7 @@ var schemaExamples = []schemaExample{
{
Title: "STRING: optional format 'uuid'",
- Schema: NewUUIDSchema(),
+ Schema: openapi3.NewUUIDSchema(),
Serialization: map[string]interface{}{
"type": "string",
"format": "uuid",
@@ -342,12 +274,6 @@ var schemaExamples = []schemaExample{
"dd7d8481-81a3-407f-95f0-a2f1cb382a4b",
"dcba3901-2fba-48c1-9db2-00422055804e",
"ace8e3be-c254-4c10-8859-1401d9a9d52a",
- "DD7D8481-81A3-407F-95F0-A2F1CB382A4B",
- "DCBA3901-2FBA-48C1-9DB2-00422055804E",
- "ACE8E3BE-C254-4C10-8859-1401D9A9D52A",
- "dd7D8481-81A3-407f-95F0-A2F1CB382A4B",
- "DCBA3901-2FBA-48C1-9db2-00422055804e",
- "ACE8E3BE-c254-4C10-8859-1401D9A9D52A",
},
AllInvalid: []interface{}{
nil,
@@ -355,19 +281,12 @@ var schemaExamples = []schemaExample{
"4cf3i040-ea14-4daa-b0b5-ea9329473519",
"aaf85740-7e27-4b4f-b4554-a03a43b1f5e3",
"56f5bff4-z4b6-48e6-a10d-b6cf66a83b04",
- "G39840B1-D0EF-446D-E555-48FCCA50A90A",
- "4CF3I040-EA14-4DAA-B0B5-EA9329473519",
- "AAF85740-7E27-4B4F-B4554-A03A43B1F5E3",
- "56F5BFF4-Z4B6-48E6-A10D-B6CF66A83B04",
- "4CF3I040-EA14-4Daa-B0B5-EA9329473519",
- "AAf85740-7E27-4B4F-B4554-A03A43b1F5E3",
- "56F5Bff4-Z4B6-48E6-a10D-B6CF66A83B04",
},
},
{
Title: "STRING: format 'date-time'",
- Schema: NewDateTimeSchema(),
+ Schema: openapi3.NewDateTimeSchema(),
Serialization: map[string]interface{}{
"type": "string",
"format": "date-time",
@@ -392,7 +311,7 @@ var schemaExamples = []schemaExample{
{
Title: "STRING: format 'date-time'",
- Schema: NewBytesSchema(),
+ Schema: openapi3.NewBytesSchema(),
Serialization: map[string]interface{}{
"type": "string",
"format": "byte",
@@ -417,19 +336,19 @@ var schemaExamples = []schemaExample{
AllInvalid: []interface{}{
nil,
" ",
- "\n\n", // a \n is ok for JSON but not for YAML decoder/encoder
+ "\n",
"%",
},
},
{
Title: "ARRAY",
- Schema: &Schema{
+ Schema: &openapi3.Schema{
Type: "array",
MinItems: 2,
- MaxItems: Uint64Ptr(3),
+ MaxItems: openapi3.Uint64Ptr(3),
UniqueItems: true,
- Items: NewFloat64Schema().NewRef(),
+ Items: openapi3.NewFloat64Schema().NewRef(),
},
Serialization: map[string]interface{}{
"type": "array",
@@ -464,13 +383,13 @@ var schemaExamples = []schemaExample{
},
{
Title: "ARRAY : items format 'object'",
- Schema: &Schema{
+ Schema: &openapi3.Schema{
Type: "array",
UniqueItems: true,
- Items: (&Schema{
+ Items: (&openapi3.Schema{
Type: "object",
- Properties: Schemas{
- "key1": NewFloat64Schema().NewRef(),
+ Properties: map[string]*openapi3.SchemaRef{
+ "key1": openapi3.NewFloat64Schema().NewRef(),
},
}).NewRef(),
},
@@ -521,16 +440,16 @@ var schemaExamples = []schemaExample{
{
Title: "ARRAY : items format 'object' and object with a property of array type ",
- Schema: &Schema{
+ Schema: &openapi3.Schema{
Type: "array",
UniqueItems: true,
- Items: (&Schema{
+ Items: (&openapi3.Schema{
Type: "object",
- Properties: Schemas{
- "key1": (&Schema{
+ Properties: map[string]*openapi3.SchemaRef{
+ "key1": (&openapi3.Schema{
Type: "array",
UniqueItems: true,
- Items: NewFloat64Schema().NewRef(),
+ Items: openapi3.NewFloat64Schema().NewRef(),
}).NewRef(),
},
}).NewRef(),
@@ -607,13 +526,13 @@ var schemaExamples = []schemaExample{
{
Title: "ARRAY : items format 'array'",
- Schema: &Schema{
+ Schema: &openapi3.Schema{
Type: "array",
UniqueItems: true,
- Items: (&Schema{
+ Items: (&openapi3.Schema{
Type: "array",
UniqueItems: true,
- Items: NewFloat64Schema().NewRef(),
+ Items: openapi3.NewFloat64Schema().NewRef(),
}).NewRef(),
},
Serialization: map[string]interface{}{
@@ -651,16 +570,16 @@ var schemaExamples = []schemaExample{
{
Title: "ARRAY : items format 'array' and array with object type items",
- Schema: &Schema{
+ Schema: &openapi3.Schema{
Type: "array",
UniqueItems: true,
- Items: (&Schema{
+ Items: (&openapi3.Schema{
Type: "array",
UniqueItems: true,
- Items: (&Schema{
+ Items: (&openapi3.Schema{
Type: "object",
- Properties: Schemas{
- "key1": NewFloat64Schema().NewRef(),
+ Properties: map[string]*openapi3.SchemaRef{
+ "key1": openapi3.NewFloat64Schema().NewRef(),
},
}).NewRef(),
}).NewRef(),
@@ -755,11 +674,11 @@ var schemaExamples = []schemaExample{
{
Title: "OBJECT",
- Schema: &Schema{
+ Schema: &openapi3.Schema{
Type: "object",
- MaxProps: Uint64Ptr(2),
- Properties: Schemas{
- "numberProperty": NewFloat64Schema().NewRef(),
+ MaxProps: openapi3.Uint64Ptr(2),
+ Properties: map[string]*openapi3.SchemaRef{
+ "numberProperty": openapi3.NewFloat64Schema().NewRef(),
},
},
Serialization: map[string]interface{}{
@@ -799,13 +718,13 @@ var schemaExamples = []schemaExample{
},
},
{
- Schema: &Schema{
+ Schema: &openapi3.Schema{
Type: "object",
- AdditionalProperties: AdditionalProperties{Schema: &SchemaRef{
- Value: &Schema{
+ AdditionalProperties: &openapi3.SchemaRef{
+ Value: &openapi3.Schema{
Type: "number",
},
- }},
+ },
},
Serialization: map[string]interface{}{
"type": "object",
@@ -827,9 +746,9 @@ var schemaExamples = []schemaExample{
},
},
{
- Schema: &Schema{
- Type: "object",
- AdditionalProperties: AdditionalProperties{Has: BoolPtr(true)},
+ Schema: &openapi3.Schema{
+ Type: "object",
+ AdditionalPropertiesAllowed: openapi3.BoolPtr(true),
},
Serialization: map[string]interface{}{
"type": "object",
@@ -846,9 +765,9 @@ var schemaExamples = []schemaExample{
{
Title: "NOT",
- Schema: &Schema{
- Not: &SchemaRef{
- Value: &Schema{
+ Schema: &openapi3.Schema{
+ Not: &openapi3.SchemaRef{
+ Value: &openapi3.Schema{
Enum: []interface{}{
nil,
true,
@@ -883,15 +802,15 @@ var schemaExamples = []schemaExample{
{
Title: "ANY OF",
- Schema: &Schema{
- AnyOf: []*SchemaRef{
+ Schema: &openapi3.Schema{
+ AnyOf: []*openapi3.SchemaRef{
{
- Value: NewFloat64Schema().
+ Value: openapi3.NewFloat64Schema().
WithMin(1).
WithMax(2),
},
{
- Value: NewFloat64Schema().
+ Value: openapi3.NewFloat64Schema().
WithMin(2).
WithMax(3),
},
@@ -924,15 +843,15 @@ var schemaExamples = []schemaExample{
{
Title: "ALL OF",
- Schema: &Schema{
- AllOf: []*SchemaRef{
+ Schema: &openapi3.Schema{
+ AllOf: []*openapi3.SchemaRef{
{
- Value: NewFloat64Schema().
+ Value: openapi3.NewFloat64Schema().
WithMin(1).
WithMax(2),
},
{
- Value: NewFloat64Schema().
+ Value: openapi3.NewFloat64Schema().
WithMin(2).
WithMax(3),
},
@@ -965,15 +884,15 @@ var schemaExamples = []schemaExample{
{
Title: "ONE OF",
- Schema: &Schema{
- OneOf: []*SchemaRef{
+ Schema: &openapi3.Schema{
+ OneOf: []*openapi3.SchemaRef{
{
- Value: NewFloat64Schema().
+ Value: openapi3.NewFloat64Schema().
WithMin(1).
WithMax(2),
},
{
- Value: NewFloat64Schema().
+ Value: openapi3.NewFloat64Schema().
WithMin(2).
WithMax(3),
},
@@ -1007,7 +926,7 @@ var schemaExamples = []schemaExample{
type schemaTypeExample struct {
Title string
- Schema *Schema
+ Schema *openapi3.Schema
AllValid []string
AllInvalid []string
}
@@ -1023,13 +942,12 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) {
baseSchema := example.Schema
for _, typ := range example.AllValid {
schema := baseSchema.WithFormat(typ)
- err := schema.Validate(context.Background())
+ err := schema.Validate(context.TODO())
require.NoError(t, err)
}
for _, typ := range example.AllInvalid {
schema := baseSchema.WithFormat(typ)
- ctx := WithValidationOptions(context.Background(), EnableSchemaFormatValidation())
- err := schema.Validate(ctx)
+ err := schema.Validate(context.TODO())
require.Error(t, err)
}
}
@@ -1038,7 +956,7 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) {
var typeExamples = []schemaTypeExample{
{
Title: "STRING",
- Schema: NewStringSchema(),
+ Schema: openapi3.NewStringSchema(),
AllValid: []string{
"",
"byte",
@@ -1056,7 +974,7 @@ var typeExamples = []schemaTypeExample{
{
Title: "NUMBER",
- Schema: NewFloat64Schema(),
+ Schema: openapi3.NewFloat64Schema(),
AllValid: []string{
"",
"float",
@@ -1069,7 +987,7 @@ var typeExamples = []schemaTypeExample{
{
Title: "INTEGER",
- Schema: NewIntegerSchema(),
+ Schema: openapi3.NewIntegerSchema(),
AllValid: []string{
"",
"int32",
@@ -1096,29 +1014,29 @@ func testSchemaError(t *testing.T, example schemaErrorExample) func(*testing.T)
type schemaErrorExample struct {
Title string
- Error *SchemaError
+ Error *openapi3.SchemaError
Want string
}
var schemaErrorExamples = []schemaErrorExample{
{
Title: "SIMPLE",
- Error: &SchemaError{
+ Error: &openapi3.SchemaError{
Value: 1,
- Schema: &Schema{},
+ Schema: &openapi3.Schema{},
Reason: "SIMPLE",
},
Want: "SIMPLE",
},
{
Title: "NEST",
- Error: &SchemaError{
+ Error: &openapi3.SchemaError{
Value: 1,
- Schema: &Schema{},
+ Schema: &openapi3.Schema{},
Reason: "PARENT",
- Origin: &SchemaError{
+ Origin: &openapi3.SchemaError{
Value: 1,
- Schema: &Schema{},
+ Schema: &openapi3.Schema{},
Reason: "NEST",
},
},
@@ -1126,243 +1044,30 @@ var schemaErrorExamples = []schemaErrorExample{
},
}
-type schemaMultiErrorExample struct {
- Title string
- Schema *Schema
- Values []interface{}
- ExpectedErrors []MultiError
-}
-
-func TestSchemasMultiError(t *testing.T) {
- for _, example := range schemaMultiErrorExamples {
- t.Run(example.Title, testSchemaMultiError(t, example))
- }
-}
-
-func testSchemaMultiError(t *testing.T, example schemaMultiErrorExample) func(*testing.T) {
- return func(t *testing.T) {
- schema := example.Schema
- for validateFuncIndex, validateFunc := range validateSchemaFuncs {
- for i, value := range example.Values {
- err := validateFunc(t, schema, value, MultiErrors())
- require.Errorf(t, err, "ValidateFunc #%d, value #%d: %#", validateFuncIndex, i, value)
- require.IsType(t, MultiError{}, err)
-
- merr, _ := err.(MultiError)
- expected := example.ExpectedErrors[i]
- require.True(t, len(merr) > 0)
- require.Len(t, merr, len(expected))
- for _, e := range merr {
- require.IsType(t, &SchemaError{}, e)
- var found bool
- scherr, _ := e.(*SchemaError)
- for _, expectedErr := range expected {
- expectedScherr, _ := expectedErr.(*SchemaError)
- if reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) &&
- expectedScherr.SchemaField == scherr.SchemaField {
- found = true
- break
- }
- }
- require.Truef(t, found, "ValidateFunc #%d, value #%d: missing %s error on %s", validateFunc, i, scherr.SchemaField, strings.Join(scherr.JSONPointer(), "."))
- }
- }
+func TestRegisterArrayUniqueItemsChecker(t *testing.T) {
+ var (
+ checker = func(items []interface{}) bool {
+ return false
}
- }
-}
-
-var schemaMultiErrorExamples = []schemaMultiErrorExample{
- {
- Title: "STRING",
- Schema: NewStringSchema().
- WithMinLength(2).
- WithMaxLength(3).
- WithPattern("^[abc]+$"),
- Values: []interface{}{
- "f",
- "foobar",
- },
- ExpectedErrors: []MultiError{
- {&SchemaError{SchemaField: "minLength"}, &SchemaError{SchemaField: "pattern"}},
- {&SchemaError{SchemaField: "maxLength"}, &SchemaError{SchemaField: "pattern"}},
- },
- },
- {
- Title: "NUMBER",
- Schema: NewIntegerSchema().
- WithMin(1).
- WithMax(10),
- Values: []interface{}{
- 0.5,
- 10.1,
- },
- ExpectedErrors: []MultiError{
- {&SchemaError{SchemaField: "type"}, &SchemaError{SchemaField: "minimum"}},
- {&SchemaError{SchemaField: "type"}, &SchemaError{SchemaField: "maximum"}},
- },
- },
- {
- Title: "ARRAY: simple",
- Schema: NewArraySchema().
- WithMinItems(2).
- WithMaxItems(2).
- WithItems(NewStringSchema().
- WithPattern("^[abc]+$")),
- Values: []interface{}{
- []interface{}{"foo"},
- []interface{}{"foo", "bar", "fizz"},
- },
- ExpectedErrors: []MultiError{
- {
- &SchemaError{SchemaField: "minItems"},
- &SchemaError{SchemaField: "pattern", reversePath: []string{"0"}},
- },
- {
- &SchemaError{SchemaField: "maxItems"},
- &SchemaError{SchemaField: "pattern", reversePath: []string{"0"}},
- &SchemaError{SchemaField: "pattern", reversePath: []string{"1"}},
- &SchemaError{SchemaField: "pattern", reversePath: []string{"2"}},
- },
- },
- },
- {
- Title: "ARRAY: object",
- Schema: NewArraySchema().
- WithItems(NewObjectSchema().
- WithProperties(map[string]*Schema{
- "key1": NewStringSchema(),
- "key2": NewIntegerSchema(),
- }),
- ),
- Values: []interface{}{
- []interface{}{
- map[string]interface{}{
- "key1": 100, // not a string
- "key2": "not an integer",
- },
- },
- },
- ExpectedErrors: []MultiError{
- {
- &SchemaError{SchemaField: "type", reversePath: []string{"key1", "0"}},
- &SchemaError{SchemaField: "type", reversePath: []string{"key2", "0"}},
- },
- },
- },
- {
- Title: "OBJECT",
- Schema: NewObjectSchema().
- WithProperties(map[string]*Schema{
- "key1": NewStringSchema(),
- "key2": NewIntegerSchema(),
- "key3": NewArraySchema().
- WithItems(NewStringSchema().
- WithPattern("^[abc]+$")),
- }),
- Values: []interface{}{
- map[string]interface{}{
- "key1": 100, // not a string
- "key2": "not an integer",
- "key3": []interface{}{"abc", "def"},
- },
- },
- ExpectedErrors: []MultiError{
- {
- &SchemaError{SchemaField: "type", reversePath: []string{"key1"}},
- &SchemaError{SchemaField: "type", reversePath: []string{"key2"}},
- &SchemaError{SchemaField: "pattern", reversePath: []string{"1", "key3"}},
- },
- },
- },
-}
-
-func TestIssue283(t *testing.T) {
- const api = `
-openapi: "3.0.1"
-components:
- schemas:
- Test:
- properties:
- name:
- type: string
- ownerName:
- not:
- type: boolean
- type: object
-`
- data := map[string]interface{}{
- "name": "kin-openapi",
- "ownerName": true,
- }
- s, err := NewLoader().LoadFromData([]byte(api))
- require.NoError(t, err)
- require.NotNil(t, s)
- err = s.Components.Schemas["Test"].Value.VisitJSON(data)
- require.NotNil(t, err)
- require.NotEqual(t, errSchema, err)
- require.Contains(t, err.Error(), `Error at "/ownerName": Doesn't match schema "not"`)
-}
-
-func TestValidationFailsOnInvalidPattern(t *testing.T) {
- schema := Schema{
- Pattern: "[",
- Type: "string",
- }
-
- err := schema.Validate(context.Background())
- require.Error(t, err)
-}
-
-func TestIssue646(t *testing.T) {
- data := []byte(`
-enum:
-- 42
-- []
-- [a]
-- {}
-- {b: c}
-`[1:])
-
- var schema Schema
- err := yaml.Unmarshal(data, &schema)
- require.NoError(t, err)
-
- err = schema.Validate(context.Background())
- require.NoError(t, err)
-
- err = schema.VisitJSON(42)
- require.NoError(t, err)
-
- err = schema.VisitJSON(1337)
- require.Error(t, err)
-
- err = schema.VisitJSON([]interface{}{})
- require.NoError(t, err)
-
- err = schema.VisitJSON([]interface{}{"a"})
- require.NoError(t, err)
-
- err = schema.VisitJSON([]interface{}{"b"})
- require.Error(t, err)
+ scheme = openapi3.Schema{
+ Type: "array",
+ UniqueItems: true,
+ Items: openapi3.NewStringSchema().NewRef(),
+ }
+ val = []interface{}{"1", "2", "3"}
+ err error
+ )
- err = schema.VisitJSON(map[string]interface{}{})
+ // Fist checked by predefined function
+ err = scheme.VisitJSON(val)
require.NoError(t, err)
- err = schema.VisitJSON(map[string]interface{}{"b": "c"})
- require.NoError(t, err)
+ // Register a function will always return false when check if a
+ // slice has unique items, then use a slice indeed has unique
+ // items to verify that check unique items will failed.
+ openapi3.RegisterArrayUniqueItemsChecker(checker)
- err = schema.VisitJSON(map[string]interface{}{"d": "e"})
+ err = scheme.VisitJSON(val)
require.Error(t, err)
-}
-
-func TestIssue751(t *testing.T) {
- schema := &Schema{
- Type: "array",
- UniqueItems: true,
- Items: NewStringSchema().NewRef(),
- }
- validData := []string{"foo", "bar"}
- invalidData := []string{"foo", "foo"}
- require.NoError(t, schema.VisitJSON(validData))
- require.ErrorContains(t, schema.VisitJSON(invalidData), "duplicate items found")
+ require.True(t, strings.HasPrefix(err.Error(), "Duplicate items found"))
}
diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go
deleted file mode 100644
index 17aad2fa7..000000000
--- a/openapi3/schema_validation_settings.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package openapi3
-
-import (
- "sync"
-)
-
-// SchemaValidationOption describes options a user has when validating request / response bodies.
-type SchemaValidationOption func(*schemaValidationSettings)
-
-type schemaValidationSettings struct {
- failfast bool
- multiError bool
- asreq, asrep bool // exclusive (XOR) fields
- formatValidationEnabled bool
- patternValidationDisabled bool
- readOnlyValidationDisabled bool
- writeOnlyValidationDisabled bool
-
- onceSettingDefaults sync.Once
- defaultsSet func()
-
- customizeMessageError func(err *SchemaError) string
-}
-
-// FailFast returns schema validation errors quicker.
-func FailFast() SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.failfast = true }
-}
-
-func MultiErrors() SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.multiError = true }
-}
-
-func VisitAsRequest() SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false }
-}
-
-func VisitAsResponse() SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true }
-}
-
-// EnableFormatValidation setting makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification.
-func EnableFormatValidation() SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.formatValidationEnabled = true }
-}
-
-// DisablePatternValidation setting makes Validate not return an error when validating patterns that are not supported by the Go regexp engine.
-func DisablePatternValidation() SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.patternValidationDisabled = true }
-}
-
-// DisableReadOnlyValidation setting makes Validate not return an error when validating properties marked as read-only
-func DisableReadOnlyValidation() SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.readOnlyValidationDisabled = true }
-}
-
-// DisableWriteOnlyValidation setting makes Validate not return an error when validating properties marked as write-only
-func DisableWriteOnlyValidation() SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.writeOnlyValidationDisabled = true }
-}
-
-// DefaultsSet executes the given callback (once) IFF schema validation set default values.
-func DefaultsSet(f func()) SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.defaultsSet = f }
-}
-
-// SetSchemaErrorMessageCustomizer allows to override the schema error message.
-// If the passed function returns an empty string, it returns to the previous Error() implementation.
-func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaValidationOption {
- return func(s *schemaValidationSettings) { s.customizeMessageError = f }
-}
-
-func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings {
- settings := &schemaValidationSettings{}
- for _, opt := range opts {
- opt(settings)
- }
- return settings
-}
diff --git a/openapi3/schema_validation_settings_test.go b/openapi3/schema_validation_settings_test.go
deleted file mode 100644
index db52d3bdf..000000000
--- a/openapi3/schema_validation_settings_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package openapi3_test
-
-import (
- "fmt"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func ExampleSetSchemaErrorMessageCustomizer() {
- loader := openapi3.NewLoader()
- spc := `
-components:
- schemas:
- Something:
- type: object
- properties:
- field:
- title: Some field
- type: string
-`[1:]
-
- doc, err := loader.LoadFromData([]byte(spc))
- if err != nil {
- panic(err)
- }
-
- opt := openapi3.SetSchemaErrorMessageCustomizer(func(err *openapi3.SchemaError) string {
- return fmt.Sprintf(`field "%s" should be string`, err.Schema.Title)
- })
-
- err = doc.Components.Schemas["Something"].Value.Properties["field"].Value.VisitJSON(123, opt)
-
- fmt.Println(err.Error())
-
- // Output: field "Some field" should be string
-}
diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go
index 87891c954..1d2c745f7 100644
--- a/openapi3/security_requirements.go
+++ b/openapi3/security_requirements.go
@@ -15,20 +15,15 @@ func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) *
return srs
}
-// Validate returns an error if SecurityRequirements does not comply with the OpenAPI spec.
-func (srs SecurityRequirements) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- for _, security := range srs {
- if err := security.Validate(ctx); err != nil {
+func (srs SecurityRequirements) Validate(c context.Context) error {
+ for _, item := range srs {
+ if err := item.Validate(c); err != nil {
return err
}
}
return nil
}
-// SecurityRequirement is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object
type SecurityRequirement map[string][]string
func NewSecurityRequirement() SecurityRequirement {
@@ -43,9 +38,6 @@ func (security SecurityRequirement) Authenticate(provider string, scopes ...stri
return security
}
-// Validate returns an error if SecurityRequirement does not comply with the OpenAPI spec.
-func (security *SecurityRequirement) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (security SecurityRequirement) Validate(c context.Context) error {
return nil
}
diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go
index 76cc21f37..0e991fb67 100644
--- a/openapi3/security_scheme.go
+++ b/openapi3/security_scheme.go
@@ -2,44 +2,22 @@ package openapi3
import (
"context"
- "encoding/json"
"errors"
"fmt"
- "net/url"
- "github.com/go-openapi/jsonpointer"
+ "github.com/getkin/kin-openapi/jsoninfo"
)
-type SecuritySchemes map[string]*SecuritySchemeRef
-
-// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable
-func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) {
- ref, ok := s[token]
- if ref == nil || ok == false {
- return nil, fmt.Errorf("object has no field %q", token)
- }
-
- if ref.Ref != "" {
- return &Ref{Ref: ref.Ref}, nil
- }
- return ref.Value, nil
-}
-
-var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil)
-
-// SecurityScheme is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object
type SecurityScheme struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
+ ExtensionProps
- Type string `json:"type,omitempty" yaml:"type,omitempty"`
- Description string `json:"description,omitempty" yaml:"description,omitempty"`
- Name string `json:"name,omitempty" yaml:"name,omitempty"`
- In string `json:"in,omitempty" yaml:"in,omitempty"`
- Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"`
- BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"`
- Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"`
- OpenIdConnectUrl string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"`
+ Type string `json:"type,omitempty" yaml:"type,omitempty"`
+ Description string `json:"description,omitempty" yaml:"description,omitempty"`
+ Name string `json:"name,omitempty" yaml:"name,omitempty"`
+ In string `json:"in,omitempty" yaml:"in,omitempty"`
+ Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"`
+ BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"`
+ Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"`
}
func NewSecurityScheme() *SecurityScheme {
@@ -54,13 +32,6 @@ func NewCSRFSecurityScheme() *SecurityScheme {
}
}
-func NewOIDCSecurityScheme(oidcUrl string) *SecurityScheme {
- return &SecurityScheme{
- Type: "openIdConnect",
- OpenIdConnectUrl: oidcUrl,
- }
-}
-
func NewJWTSecurityScheme() *SecurityScheme {
return &SecurityScheme{
Type: "http",
@@ -69,57 +40,12 @@ func NewJWTSecurityScheme() *SecurityScheme {
}
}
-// MarshalJSON returns the JSON encoding of SecurityScheme.
-func (ss SecurityScheme) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 8+len(ss.Extensions))
- for k, v := range ss.Extensions {
- m[k] = v
- }
- if x := ss.Type; x != "" {
- m["type"] = x
- }
- if x := ss.Description; x != "" {
- m["description"] = x
- }
- if x := ss.Name; x != "" {
- m["name"] = x
- }
- if x := ss.In; x != "" {
- m["in"] = x
- }
- if x := ss.Scheme; x != "" {
- m["scheme"] = x
- }
- if x := ss.BearerFormat; x != "" {
- m["bearerFormat"] = x
- }
- if x := ss.Flows; x != nil {
- m["flows"] = x
- }
- if x := ss.OpenIdConnectUrl; x != "" {
- m["openIdConnectUrl"] = x
- }
- return json.Marshal(m)
+func (ss *SecurityScheme) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(ss)
}
-// UnmarshalJSON sets SecurityScheme to a copy of data.
func (ss *SecurityScheme) UnmarshalJSON(data []byte) error {
- type SecuritySchemeBis SecurityScheme
- var x SecuritySchemeBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "type")
- delete(x.Extensions, "description")
- delete(x.Extensions, "name")
- delete(x.Extensions, "in")
- delete(x.Extensions, "scheme")
- delete(x.Extensions, "bearerFormat")
- delete(x.Extensions, "flows")
- delete(x.Extensions, "openIdConnectUrl")
- *ss = SecurityScheme(x)
- return nil
+ return jsoninfo.UnmarshalStrictStruct(data, ss)
}
func (ss *SecurityScheme) WithType(value string) *SecurityScheme {
@@ -152,10 +78,7 @@ func (ss *SecurityScheme) WithBearerFormat(value string) *SecurityScheme {
return ss
}
-// Validate returns an error if SecurityScheme does not comply with the OpenAPI spec.
-func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (ss *SecurityScheme) Validate(c context.Context) error {
hasIn := false
hasBearerFormat := false
hasFlow := false
@@ -167,18 +90,16 @@ func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption
switch scheme {
case "bearer":
hasBearerFormat = true
- case "basic", "negotiate", "digest":
+ case "basic":
default:
- return fmt.Errorf("security scheme of type 'http' has invalid 'scheme' value %q", scheme)
+ return fmt.Errorf("Security scheme of type 'http' has invalid 'scheme' value '%s'", scheme)
}
case "oauth2":
hasFlow = true
case "openIdConnect":
- if ss.OpenIdConnectUrl == "" {
- return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", ss.Name)
- }
+ return fmt.Errorf("Support for security schemes with type '%v' has not been implemented", ss.Type)
default:
- return fmt.Errorf("security scheme 'type' can't be %q", ss.Type)
+ return fmt.Errorf("Security scheme 'type' can't be '%v'", ss.Type)
}
// Validate "in" and "name"
@@ -186,44 +107,40 @@ func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption
switch ss.In {
case "query", "header", "cookie":
default:
- return fmt.Errorf("security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not %q", ss.In)
+ return fmt.Errorf("Security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not '%s'", ss.In)
}
if ss.Name == "" {
- return errors.New("security scheme of type 'apiKey' should have 'name'")
+ return errors.New("Security scheme of type 'apiKey' should have 'name'")
}
} else if len(ss.In) > 0 {
- return fmt.Errorf("security scheme of type %q can't have 'in'", ss.Type)
+ return fmt.Errorf("Security scheme of type '%s' can't have 'in'", ss.Type)
} else if len(ss.Name) > 0 {
- return fmt.Errorf("security scheme of type %q can't have 'name'", ss.Type)
+ return errors.New("Security scheme of type 'apiKey' can't have 'name'")
}
// Validate "format"
// "bearerFormat" is an arbitrary string so we only check if the scheme supports it
if !hasBearerFormat && len(ss.BearerFormat) > 0 {
- return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", ss.Type)
+ return fmt.Errorf("Security scheme of type '%v' can't have 'bearerFormat'", ss.Type)
}
// Validate "flow"
if hasFlow {
flow := ss.Flows
if flow == nil {
- return fmt.Errorf("security scheme of type %q should have 'flows'", ss.Type)
+ return fmt.Errorf("Security scheme of type '%v' should have 'flows'", ss.Type)
}
- if err := flow.Validate(ctx); err != nil {
- return fmt.Errorf("security scheme 'flow' is invalid: %w", err)
+ if err := flow.Validate(c); err != nil {
+ return fmt.Errorf("Security scheme 'flow' is invalid: %v", err)
}
} else if ss.Flows != nil {
- return fmt.Errorf("security scheme of type %q can't have 'flows'", ss.Type)
+ return fmt.Errorf("Security scheme of type '%s' can't have 'flows'", ss.Type)
}
-
- return validateExtensions(ctx, ss.Extensions)
+ return nil
}
-// OAuthFlows is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object
type OAuthFlows struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
+ ExtensionProps
Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"`
Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"`
ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"`
@@ -239,174 +156,59 @@ const (
oAuthFlowAuthorizationCode
)
-// MarshalJSON returns the JSON encoding of OAuthFlows.
-func (flows OAuthFlows) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 4+len(flows.Extensions))
- for k, v := range flows.Extensions {
- m[k] = v
- }
- if x := flows.Implicit; x != nil {
- m["implicit"] = x
- }
- if x := flows.Password; x != nil {
- m["password"] = x
- }
- if x := flows.ClientCredentials; x != nil {
- m["clientCredentials"] = x
- }
- if x := flows.AuthorizationCode; x != nil {
- m["authorizationCode"] = x
- }
- return json.Marshal(m)
+func (flows *OAuthFlows) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(flows)
}
-// UnmarshalJSON sets OAuthFlows to a copy of data.
func (flows *OAuthFlows) UnmarshalJSON(data []byte) error {
- type OAuthFlowsBis OAuthFlows
- var x OAuthFlowsBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "implicit")
- delete(x.Extensions, "password")
- delete(x.Extensions, "clientCredentials")
- delete(x.Extensions, "authorizationCode")
- *flows = OAuthFlows(x)
- return nil
+ return jsoninfo.UnmarshalStrictStruct(data, flows)
}
-// Validate returns an error if OAuthFlows does not comply with the OpenAPI spec.
-func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (flows *OAuthFlows) Validate(c context.Context) error {
if v := flows.Implicit; v != nil {
- if err := v.validate(ctx, oAuthFlowTypeImplicit, opts...); err != nil {
- return fmt.Errorf("the OAuth flow 'implicit' is invalid: %w", err)
- }
+ return v.Validate(c, oAuthFlowTypeImplicit)
}
-
if v := flows.Password; v != nil {
- if err := v.validate(ctx, oAuthFlowTypePassword, opts...); err != nil {
- return fmt.Errorf("the OAuth flow 'password' is invalid: %w", err)
- }
+ return v.Validate(c, oAuthFlowTypePassword)
}
-
if v := flows.ClientCredentials; v != nil {
- if err := v.validate(ctx, oAuthFlowTypeClientCredentials, opts...); err != nil {
- return fmt.Errorf("the OAuth flow 'clientCredentials' is invalid: %w", err)
- }
+ return v.Validate(c, oAuthFlowTypeClientCredentials)
}
-
if v := flows.AuthorizationCode; v != nil {
- if err := v.validate(ctx, oAuthFlowAuthorizationCode, opts...); err != nil {
- return fmt.Errorf("the OAuth flow 'authorizationCode' is invalid: %w", err)
- }
+ return v.Validate(c, oAuthFlowAuthorizationCode)
}
-
- return validateExtensions(ctx, flows.Extensions)
+ return errors.New("No OAuth flow is defined")
}
-// OAuthFlow is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object
type OAuthFlow struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
+ ExtensionProps
AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`
- Scopes map[string]string `json:"scopes" yaml:"scopes"` // required
+ Scopes map[string]string `json:"scopes" yaml:"scopes"`
}
-// MarshalJSON returns the JSON encoding of OAuthFlow.
-func (flow OAuthFlow) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 4+len(flow.Extensions))
- for k, v := range flow.Extensions {
- m[k] = v
- }
- if x := flow.AuthorizationURL; x != "" {
- m["authorizationUrl"] = x
- }
- if x := flow.TokenURL; x != "" {
- m["tokenUrl"] = x
- }
- if x := flow.RefreshURL; x != "" {
- m["refreshUrl"] = x
- }
- m["scopes"] = flow.Scopes
- return json.Marshal(m)
+func (flow *OAuthFlow) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(flow)
}
-// UnmarshalJSON sets OAuthFlow to a copy of data.
func (flow *OAuthFlow) UnmarshalJSON(data []byte) error {
- type OAuthFlowBis OAuthFlow
- var x OAuthFlowBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "authorizationUrl")
- delete(x.Extensions, "tokenUrl")
- delete(x.Extensions, "refreshUrl")
- delete(x.Extensions, "scopes")
- *flow = OAuthFlow(x)
- return nil
-}
-
-// Validate returns an error if OAuthFlows does not comply with the OpenAPI spec.
-func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if v := flow.RefreshURL; v != "" {
- if _, err := url.Parse(v); err != nil {
- return fmt.Errorf("field 'refreshUrl' is invalid: %w", err)
- }
- }
-
- if flow.Scopes == nil {
- return errors.New("field 'scopes' is missing")
- }
-
- return validateExtensions(ctx, flow.Extensions)
+ return jsoninfo.UnmarshalStrictStruct(data, flow)
}
-func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- typeIn := func(types ...oAuthFlowType) bool {
- for _, ty := range types {
- if ty == typ {
- return true
- }
+func (flow *OAuthFlow) Validate(c context.Context, typ oAuthFlowType) error {
+ if typ == oAuthFlowAuthorizationCode || typ == oAuthFlowTypeImplicit {
+ if v := flow.AuthorizationURL; v == "" {
+ return errors.New("An OAuth flow is missing 'authorizationUrl in authorizationCode or implicit '")
}
- return false
}
-
- if in := typeIn(oAuthFlowTypeImplicit, oAuthFlowAuthorizationCode); true {
- switch {
- case flow.AuthorizationURL == "" && in:
- return errors.New("field 'authorizationUrl' is empty or missing")
- case flow.AuthorizationURL != "" && !in:
- return errors.New("field 'authorizationUrl' should not be set")
- case flow.AuthorizationURL != "":
- if _, err := url.Parse(flow.AuthorizationURL); err != nil {
- return fmt.Errorf("field 'authorizationUrl' is invalid: %w", err)
- }
+ if typ != oAuthFlowTypeImplicit {
+ if v := flow.TokenURL; v == "" {
+ return errors.New("An OAuth flow is missing 'tokenUrl in not implicit'")
}
}
-
- if in := typeIn(oAuthFlowTypePassword, oAuthFlowTypeClientCredentials, oAuthFlowAuthorizationCode); true {
- switch {
- case flow.TokenURL == "" && in:
- return errors.New("field 'tokenUrl' is empty or missing")
- case flow.TokenURL != "" && !in:
- return errors.New("field 'tokenUrl' should not be set")
- case flow.TokenURL != "":
- if _, err := url.Parse(flow.TokenURL); err != nil {
- return fmt.Errorf("field 'tokenUrl' is invalid: %w", err)
- }
- }
+ if v := flow.Scopes; v == nil {
+ return errors.New("An OAuth flow is missing 'scopes'")
}
-
- return flow.Validate(ctx, opts...)
+ return nil
}
diff --git a/openapi3/security_scheme_test.go b/openapi3/security_scheme_test.go
index 790414ca2..7f013be4c 100644
--- a/openapi3/security_scheme_test.go
+++ b/openapi3/security_scheme_test.go
@@ -1,9 +1,10 @@
-package openapi3
+package openapi3_test
import (
"context"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
@@ -15,84 +16,75 @@ type securitySchemeExample struct {
func TestSecuritySchemaExample(t *testing.T) {
for _, example := range securitySchemeExamples {
- t.Run(example.title, func(t *testing.T) {
- ss := &SecurityScheme{}
- err := ss.UnmarshalJSON(example.raw)
- require.NoError(t, err)
+ t.Run(example.title, testSecuritySchemaExample(t, example))
+ }
+}
- err = ss.Validate(context.Background())
- if example.valid {
- require.NoError(t, err)
- } else {
- require.Error(t, err)
- }
- })
+func testSecuritySchemaExample(t *testing.T, e securitySchemeExample) func(*testing.T) {
+ return func(t *testing.T) {
+ var err error
+ ss := &openapi3.SecurityScheme{}
+ err = ss.UnmarshalJSON(e.raw)
+ require.NoError(t, err)
+ err = ss.Validate(context.TODO())
+ if e.valid {
+ require.NoError(t, err)
+ } else {
+ require.Error(t, err)
+ }
}
}
-// from https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-23
+// from https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#fixed-fields-23
var securitySchemeExamples = []securitySchemeExample{
{
title: "Basic Authentication Sample",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "http",
"scheme": "basic"
-}`),
- valid: true,
- },
-
- {
- title: "Negotiate Authentication Sample",
- raw: []byte(`{
- "type": "http",
- "scheme": "negotiate"
-}`),
+}
+`),
valid: true,
},
-
- {
- title: "Unknown http Authentication Sample",
- raw: []byte(`{
- "type": "http",
- "scheme": "notvalid"
-}`),
- valid: false,
- },
-
{
title: "API Key Sample",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "apiKey",
"name": "api_key",
"in": "header"
-}`),
+}
+`),
valid: true,
},
-
{
title: "apiKey with bearerFormat",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "apiKey",
- "in": "header",
- "name": "X-API-KEY",
+ "in": "header",
+ "name": "X-API-KEY",
"bearerFormat": "Arbitrary text"
-}`),
+}
+`),
valid: false,
},
-
{
title: "Bearer Sample with arbitrary format",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "http",
"scheme": "bearer",
"bearerFormat": "Arbitrary text"
-}`),
+}
+`),
valid: true,
},
-
{
title: "Implicit OAuth2 Sample",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "oauth2",
"flows": {
"implicit": {
@@ -103,13 +95,14 @@ var securitySchemeExamples = []securitySchemeExample{
}
}
}
-}`),
+}
+`),
valid: true,
},
-
{
title: "OAuth Flow Object Sample",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "oauth2",
"flows": {
"implicit": {
@@ -128,13 +121,14 @@ var securitySchemeExamples = []securitySchemeExample{
}
}
}
-}`),
+}
+`),
valid: true,
},
-
{
title: "OAuth Flow Object clientCredentials/password",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "oauth2",
"flows": {
"clientCredentials": {
@@ -150,71 +144,59 @@ var securitySchemeExamples = []securitySchemeExample{
}
}
}
-}`),
+}
+`),
valid: true,
},
-
{
title: "Invalid Basic",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "https",
"scheme": "basic"
-}`),
+}
+`),
valid: false,
},
-
{
title: "Apikey Cookie",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "apiKey",
"in": "cookie",
"name": "somecookie"
-}`),
+}
+`),
valid: true,
},
{
title: "OAuth Flow Object with no scopes",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "https://example.com/api/oauth/token"
}
}
-}`),
+}
+`),
valid: false,
},
-
{
title: "OAuth Flow Object with empty scopes",
- raw: []byte(`{
+ raw: []byte(`
+{
"type": "oauth2",
"flows": {
"password": {
- "tokenUrl": "https://example.com/api/oauth/token",
- "scopes": {}
+ "tokenUrl": "https://example.com/api/oauth/token",
+ "scopes": {}
}
}
-}`),
- valid: true,
- },
-
- {
- title: "OIDC Type With URL",
- raw: []byte(`{
- "type": "openIdConnect",
- "openIdConnectUrl": "https://example.com/.well-known/openid-configuration"
-}`),
+}
+`),
valid: true,
},
-
- {
- title: "OIDC Type Without URL",
- raw: []byte(`{
- "type": "openIdConnect",
- "openIdConnectUrl": ""
-}`),
- valid: false,
- },
}
diff --git a/openapi3/server.go b/openapi3/server.go
index 9fc99f90c..4392b09af 100644
--- a/openapi3/server.go
+++ b/openapi3/server.go
@@ -2,38 +2,24 @@ package openapi3
import (
"context"
- "encoding/json"
"errors"
- "fmt"
"math"
"net/url"
- "sort"
"strings"
)
-// Servers is specified by OpenAPI/Swagger standard version 3.
+// Servers is specified by OpenAPI/Swagger standard version 3.0.
type Servers []*Server
-// Validate returns an error if Servers does not comply with the OpenAPI spec.
-func (servers Servers) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (servers Servers) Validate(c context.Context) error {
for _, v := range servers {
- if err := v.Validate(ctx); err != nil {
+ if err := v.Validate(c); err != nil {
return err
}
}
return nil
}
-// BasePath returns the base path of the first server in the list, or /.
-func (servers Servers) BasePath() (string, error) {
- for _, server := range servers {
- return server.BasePath()
- }
- return "/", nil
-}
-
func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) {
rawURL := parsedURL.String()
if i := strings.IndexByte(rawURL, '?'); i >= 0 {
@@ -48,71 +34,13 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string)
return nil, nil, ""
}
-// Server is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-object
+// Server is specified by OpenAPI/Swagger standard version 3.0.
type Server struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- URL string `json:"url" yaml:"url"` // Required
+ URL string `json:"url" yaml:"url"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"`
}
-// BasePath returns the base path extracted from the default values of variables, if any.
-// Assumes a valid struct (per Validate()).
-func (server *Server) BasePath() (string, error) {
- if server == nil {
- return "/", nil
- }
-
- uri := server.URL
- for name, svar := range server.Variables {
- uri = strings.ReplaceAll(uri, "{"+name+"}", svar.Default)
- }
-
- u, err := url.ParseRequestURI(uri)
- if err != nil {
- return "", err
- }
-
- if bp := u.Path; bp != "" {
- return bp, nil
- }
-
- return "/", nil
-}
-
-// MarshalJSON returns the JSON encoding of Server.
-func (server Server) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 3+len(server.Extensions))
- for k, v := range server.Extensions {
- m[k] = v
- }
- m["url"] = server.URL
- if x := server.Description; x != "" {
- m["description"] = x
- }
- if x := server.Variables; len(x) != 0 {
- m["variables"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets Server to a copy of data.
-func (server *Server) UnmarshalJSON(data []byte) error {
- type ServerBis Server
- var x ServerBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "url")
- delete(x.Extensions, "description")
- delete(x.Extensions, "variables")
- *server = Server(x)
- return nil
-}
-
func (server Server) ParameterNames() ([]string, error) {
pattern := server.URL
var params []string
@@ -124,7 +52,7 @@ func (server Server) ParameterNames() ([]string, error) {
pattern = pattern[i+1:]
i = strings.IndexByte(pattern, '}')
if i < 0 {
- return nil, errors.New("missing '}'")
+ return nil, errors.New("Missing '}'")
}
params = append(params, strings.TrimSpace(pattern[:i]))
pattern = pattern[i+1:]
@@ -184,95 +112,37 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) {
return params, input, true
}
-// Validate returns an error if Server does not comply with the OpenAPI spec.
-func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (err error) {
- ctx = WithValidationOptions(ctx, opts...)
-
+func (server *Server) Validate(c context.Context) (err error) {
if server.URL == "" {
- return errors.New("value of url must be a non-empty string")
- }
-
- opening, closing := strings.Count(server.URL, "{"), strings.Count(server.URL, "}")
- if opening != closing {
- return errors.New("server URL has mismatched { and }")
- }
-
- if opening != len(server.Variables) {
- return errors.New("server has undeclared variables")
- }
-
- variables := make([]string, 0, len(server.Variables))
- for name := range server.Variables {
- variables = append(variables, name)
+ return errors.New("Variable 'URL' must be a non-empty JSON string")
}
- sort.Strings(variables)
- for _, name := range variables {
- v := server.Variables[name]
- if !strings.Contains(server.URL, "{"+name+"}") {
- return errors.New("server has undeclared variables")
- }
- if err = v.Validate(ctx); err != nil {
+ for _, v := range server.Variables {
+ if err = v.Validate(c); err != nil {
return
}
}
-
- return validateExtensions(ctx, server.Extensions)
+ return
}
-// ServerVariable is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object
+// ServerVariable is specified by OpenAPI/Swagger standard version 3.0.
type ServerVariable struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"`
- Default string `json:"default,omitempty" yaml:"default,omitempty"`
- Description string `json:"description,omitempty" yaml:"description,omitempty"`
-}
-
-// MarshalJSON returns the JSON encoding of ServerVariable.
-func (serverVariable ServerVariable) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 4+len(serverVariable.Extensions))
- for k, v := range serverVariable.Extensions {
- m[k] = v
- }
- if x := serverVariable.Enum; len(x) != 0 {
- m["enum"] = x
- }
- if x := serverVariable.Default; x != "" {
- m["default"] = x
- }
- if x := serverVariable.Description; x != "" {
- m["description"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets ServerVariable to a copy of data.
-func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error {
- type ServerVariableBis ServerVariable
- var x ServerVariableBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "enum")
- delete(x.Extensions, "default")
- delete(x.Extensions, "description")
- *serverVariable = ServerVariable(x)
- return nil
+ Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"`
+ Default interface{} `json:"default,omitempty" yaml:"default,omitempty"`
+ Description string `json:"description,omitempty" yaml:"description,omitempty"`
}
-// Validate returns an error if ServerVariable does not comply with the OpenAPI spec.
-func (serverVariable *ServerVariable) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if serverVariable.Default == "" {
- data, err := serverVariable.MarshalJSON()
- if err != nil {
- return err
+func (serverVariable *ServerVariable) Validate(c context.Context) error {
+ switch serverVariable.Default.(type) {
+ case float64, string:
+ default:
+ return errors.New("Variable 'default' must be either JSON number or JSON string")
+ }
+ for _, item := range serverVariable.Enum {
+ switch item.(type) {
+ case float64, string:
+ default:
+ return errors.New("Every variable 'enum' item must be number of string")
}
- return fmt.Errorf("field default is required in %s", data)
}
-
- return validateExtensions(ctx, serverVariable.Extensions)
+ return nil
}
diff --git a/openapi3/server_test.go b/openapi3/server_test.go
index c59b86e56..b877a3546 100644
--- a/openapi3/server_test.go
+++ b/openapi3/server_test.go
@@ -1,15 +1,16 @@
-package openapi3
+package openapi3_test
import (
"context"
"errors"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
func TestServerParamNames(t *testing.T) {
- server := &Server{
+ server := &openapi3.Server{
URL: "http://{x}.{y}.example.com",
}
values, err := server.ParameterNames()
@@ -18,7 +19,7 @@ func TestServerParamNames(t *testing.T) {
}
func TestServerParamValuesWithPath(t *testing.T) {
- server := &Server{
+ server := &openapi3.Server{
URL: "http://{arg0}.{arg1}.example.com/a/{arg3}-version/{arg4}c{arg5}",
}
for input, expected := range map[string]*serverMatch{
@@ -40,7 +41,7 @@ func TestServerParamValuesWithPath(t *testing.T) {
}
func TestServerParamValuesNoPath(t *testing.T) {
- server := &Server{
+ server := &openapi3.Server{
URL: "https://{arg0}.{arg1}.example.com/",
}
for input, expected := range map[string]*serverMatch{
@@ -50,26 +51,26 @@ func TestServerParamValuesNoPath(t *testing.T) {
}
}
-func validServer() *Server {
- return &Server{
+func validServer() *openapi3.Server {
+ return &openapi3.Server{
URL: "http://my.cool.website",
}
}
-func invalidServer() *Server {
- return &Server{}
+func invalidServer() *openapi3.Server {
+ return &openapi3.Server{}
}
func TestServerValidation(t *testing.T) {
tests := []struct {
name string
- input *Server
+ input *openapi3.Server
expectedError error
}{
{
"when no URL is provided",
invalidServer(),
- errors.New("value of url must be a non-empty string"),
+ errors.New("Variable 'URL' must be a non-empty JSON string"),
},
{
"when a URL is provided",
@@ -88,7 +89,7 @@ func TestServerValidation(t *testing.T) {
}
}
-func testServerParamValues(t *testing.T, server *Server, input string, expected *serverMatch) func(*testing.T) {
+func testServerParamValues(t *testing.T, server *openapi3.Server, input string, expected *serverMatch) func(*testing.T) {
return func(t *testing.T) {
args, remaining, ok := server.MatchRawURL(input)
if expected == nil {
@@ -116,88 +117,3 @@ func newServerMatch(remaining string, args ...string) *serverMatch {
Args: args,
}
}
-
-func TestServersBasePath(t *testing.T) {
- for _, testcase := range []struct {
- title string
- servers Servers
- expected string
- }{
- {
- title: "empty servers",
- servers: nil,
- expected: "/",
- },
- {
- title: "URL set, missing trailing slash",
- servers: Servers{&Server{URL: "https://example.com"}},
- expected: "/",
- },
- {
- title: "URL set, with trailing slash",
- servers: Servers{&Server{URL: "https://example.com/"}},
- expected: "/",
- },
- {
- title: "URL set",
- servers: Servers{&Server{URL: "https://example.com/b/l/a"}},
- expected: "/b/l/a",
- },
- {
- title: "URL set with variables",
- servers: Servers{&Server{
- URL: "{scheme}://example.com/b/l/a",
- Variables: map[string]*ServerVariable{
- "scheme": {
- Enum: []string{"http", "https"},
- Default: "https",
- },
- },
- }},
- expected: "/b/l/a",
- },
- {
- title: "URL set with variables in path",
- servers: Servers{&Server{
- URL: "http://example.com/b/{var1}/a",
- Variables: map[string]*ServerVariable{
- "var1": {
- Default: "lllll",
- },
- },
- }},
- expected: "/b/lllll/a",
- },
- {
- title: "URLs set with variables in path",
- servers: Servers{
- &Server{
- URL: "http://example.com/b/{var2}/a",
- Variables: map[string]*ServerVariable{
- "var2": {
- Default: "LLLLL",
- },
- },
- },
- &Server{
- URL: "https://example.com/b/{var1}/a",
- Variables: map[string]*ServerVariable{
- "var1": {
- Default: "lllll",
- },
- },
- },
- },
- expected: "/b/LLLLL/a",
- },
- } {
- t.Run(testcase.title, func(t *testing.T) {
- err := testcase.servers.Validate(context.Background())
- require.NoError(t, err)
-
- got, err := testcase.servers.BasePath()
- require.NoError(t, err)
- require.Exactly(t, testcase.expected, got)
- })
- }
-}
diff --git a/openapi3/swagger.go b/openapi3/swagger.go
new file mode 100644
index 000000000..6ca8f1e02
--- /dev/null
+++ b/openapi3/swagger.go
@@ -0,0 +1,81 @@
+package openapi3
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/getkin/kin-openapi/jsoninfo"
+)
+
+type Swagger struct {
+ ExtensionProps
+ OpenAPI string `json:"openapi" yaml:"openapi"` // Required
+ Info *Info `json:"info" yaml:"info"` // Required
+ Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"`
+ Paths Paths `json:"paths" yaml:"paths"` // Required
+ Components Components `json:"components,omitempty" yaml:"components,omitempty"`
+ Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"`
+ Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"`
+ ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
+}
+
+func (swagger *Swagger) MarshalJSON() ([]byte, error) {
+ return jsoninfo.MarshalStrictStruct(swagger)
+}
+
+func (swagger *Swagger) UnmarshalJSON(data []byte) error {
+ return jsoninfo.UnmarshalStrictStruct(data, swagger)
+}
+
+func (swagger *Swagger) AddOperation(path string, method string, operation *Operation) {
+ paths := swagger.Paths
+ if paths == nil {
+ paths = make(Paths)
+ swagger.Paths = paths
+ }
+ pathItem := paths[path]
+ if pathItem == nil {
+ pathItem = &PathItem{}
+ paths[path] = pathItem
+ }
+ pathItem.SetOperation(method, operation)
+}
+
+func (swagger *Swagger) AddServer(server *Server) {
+ swagger.Servers = append(swagger.Servers, server)
+}
+
+func (swagger *Swagger) Validate(c context.Context) error {
+ if swagger.OpenAPI == "" {
+ return errors.New("Variable 'openapi' must be a non-empty JSON string")
+ }
+ if err := swagger.Components.Validate(c); err != nil {
+ return fmt.Errorf("Error when validating Components: %s", err.Error())
+ }
+ if v := swagger.Security; v != nil {
+ if err := v.Validate(c); err != nil {
+ return fmt.Errorf("Error when validating Security: %s", err.Error())
+ }
+ }
+ if v := swagger.Servers; v != nil {
+ if err := v.Validate(c); err != nil {
+ return fmt.Errorf("Error when validating Servers: %s", err.Error())
+ }
+ }
+ if v := swagger.Paths; v != nil {
+ if err := v.Validate(c); err != nil {
+ return fmt.Errorf("Error when validating Paths: %s", err.Error())
+ }
+ } else {
+ return errors.New("Variable 'paths' must be a JSON object")
+ }
+ if v := swagger.Info; v != nil {
+ if err := v.Validate(c); err != nil {
+ return fmt.Errorf("Error when validating Info: %s", err.Error())
+ }
+ } else {
+ return errors.New("Variable 'info' must be a JSON object")
+ }
+ return nil
+}
diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go
new file mode 100644
index 000000000..3925e0e6c
--- /dev/null
+++ b/openapi3/swagger_loader.go
@@ -0,0 +1,875 @@
+package openapi3
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "path"
+ "reflect"
+ "strconv"
+ "strings"
+
+ "github.com/ghodss/yaml"
+)
+
+func foundUnresolvedRef(ref string) error {
+ return fmt.Errorf("Found unresolved ref: '%s'", ref)
+}
+
+func failedToResolveRefFragment(value string) error {
+ return fmt.Errorf("Failed to resolve fragment in URI: '%s'", value)
+}
+
+func failedToResolveRefFragmentPart(value string, what string) error {
+ return fmt.Errorf("Failed to resolve '%s' in fragment in URI: '%s'", what, value)
+}
+
+type SwaggerLoader struct {
+ IsExternalRefsAllowed bool
+ Context context.Context
+ LoadSwaggerFromURIFunc func(loader *SwaggerLoader, url *url.URL) (*Swagger, error)
+ visited map[interface{}]struct{}
+ visitedFiles map[string]struct{}
+}
+
+func NewSwaggerLoader() *SwaggerLoader {
+ return &SwaggerLoader{}
+}
+
+func (swaggerLoader *SwaggerLoader) reset() {
+ swaggerLoader.visitedFiles = make(map[string]struct{})
+}
+
+func (swaggerLoader *SwaggerLoader) LoadSwaggerFromURI(location *url.URL) (*Swagger, error) {
+ swaggerLoader.reset()
+ return swaggerLoader.loadSwaggerFromURIInternal(location)
+}
+
+func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL) (*Swagger, error) {
+ f := swaggerLoader.LoadSwaggerFromURIFunc
+ if f != nil {
+ return f(swaggerLoader, location)
+ }
+ data, err := readURL(location)
+ if err != nil {
+ return nil, err
+ }
+ return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, location)
+}
+
+// loadSingleElementFromURI read the data from ref and unmarshal to JSON to the
+// passed element.
+func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) error {
+ if !swaggerLoader.IsExternalRefsAllowed {
+ return fmt.Errorf("encountered non-allowed external reference: '%s'", ref)
+ }
+
+ parsedURL, err := url.Parse(ref)
+ if err != nil {
+ return err
+ }
+
+ if parsedURL.Fragment != "" {
+ return errors.New("references to files which contain more than one element definition are not supported")
+ }
+
+ resolvedPath, err := resolvePath(rootPath, parsedURL)
+ if err != nil {
+ return fmt.Errorf("could not resolve path: %v", err)
+ }
+
+ data, err := readURL(resolvedPath)
+ if err != nil {
+ return err
+ }
+ if err := yaml.Unmarshal(data, element); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func readURL(location *url.URL) ([]byte, error) {
+ if location.Scheme != "" && location.Host != "" {
+ resp, err := http.Get(location.String())
+ if err != nil {
+ return nil, err
+ }
+ data, err := ioutil.ReadAll(resp.Body)
+ defer resp.Body.Close()
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+ }
+ if location.Scheme != "" || location.Host != "" || location.RawQuery != "" {
+ return nil, fmt.Errorf("Unsupported URI: '%s'", location.String())
+ }
+ data, err := ioutil.ReadFile(location.Path)
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+}
+
+func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, error) {
+ swaggerLoader.reset()
+ return swaggerLoader.loadSwaggerFromFileInternal(path)
+}
+
+func (swaggerLoader *SwaggerLoader) loadSwaggerFromFileInternal(path string) (*Swagger, error) {
+ f := swaggerLoader.LoadSwaggerFromURIFunc
+ if f != nil {
+ return f(swaggerLoader, &url.URL{
+ Path: path,
+ })
+ }
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, &url.URL{
+ Path: path,
+ })
+}
+
+func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger, error) {
+ swaggerLoader.reset()
+ return swaggerLoader.loadSwaggerFromDataInternal(data)
+}
+
+func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataInternal(data []byte) (*Swagger, error) {
+ swagger := &Swagger{}
+ if err := yaml.Unmarshal(data, swagger); err != nil {
+ return nil, err
+ }
+ return swagger, swaggerLoader.ResolveRefsIn(swagger, nil)
+}
+
+// LoadSwaggerFromDataWithPath takes the OpenApi spec data in bytes and a path where the resolver can find referred
+// elements and returns a *Swagger with all resolved data or an error if unable to load data or resolve refs.
+func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, path *url.URL) (*Swagger, error) {
+ swaggerLoader.reset()
+ return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, path)
+}
+
+func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []byte, path *url.URL) (*Swagger, error) {
+ swagger := &Swagger{}
+ if err := yaml.Unmarshal(data, swagger); err != nil {
+ return nil, err
+ }
+ return swagger, swaggerLoader.ResolveRefsIn(swagger, path)
+}
+
+func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.URL) (err error) {
+ swaggerLoader.visited = make(map[interface{}]struct{})
+ if swaggerLoader.visitedFiles == nil {
+ swaggerLoader.visitedFiles = make(map[string]struct{})
+ }
+
+ // Visit all components
+ components := swagger.Components
+ for _, component := range components.Headers {
+ if err = swaggerLoader.resolveHeaderRef(swagger, component, path); err != nil {
+ return
+ }
+ }
+ for _, component := range components.Parameters {
+ if err = swaggerLoader.resolveParameterRef(swagger, component, path); err != nil {
+ return
+ }
+ }
+ for _, component := range components.RequestBodies {
+ if err = swaggerLoader.resolveRequestBodyRef(swagger, component, path); err != nil {
+ return
+ }
+ }
+ for _, component := range components.Responses {
+ if err = swaggerLoader.resolveResponseRef(swagger, component, path); err != nil {
+ return
+ }
+ }
+ for _, component := range components.Schemas {
+ if err = swaggerLoader.resolveSchemaRef(swagger, component, path); err != nil {
+ return
+ }
+ }
+ for _, component := range components.SecuritySchemes {
+ if err = swaggerLoader.resolveSecuritySchemeRef(swagger, component, path); err != nil {
+ return
+ }
+ }
+ for _, component := range components.Examples {
+ if err = swaggerLoader.resolveExampleRef(swagger, component, path); err != nil {
+ return
+ }
+ }
+
+ // Visit all operations
+ for entrypoint, pathItem := range swagger.Paths {
+ if pathItem == nil {
+ continue
+ }
+ if err = swaggerLoader.resolvePathItemRef(swagger, entrypoint, pathItem, path); err != nil {
+ return
+ }
+ }
+
+ return
+}
+
+func copyURL(basePath *url.URL) (*url.URL, error) {
+ return url.Parse(basePath.String())
+}
+
+func join(basePath *url.URL, relativePath *url.URL) (*url.URL, error) {
+ if basePath == nil {
+ return relativePath, nil
+ }
+ newPath, err := copyURL(basePath)
+ if err != nil {
+ return nil, fmt.Errorf("Can't copy path: '%s'", basePath.String())
+ }
+ newPath.Path = path.Join(path.Dir(newPath.Path), relativePath.Path)
+ return newPath, nil
+}
+
+func resolvePath(basePath *url.URL, componentPath *url.URL) (*url.URL, error) {
+ if componentPath.Scheme == "" && componentPath.Host == "" {
+ return join(basePath, componentPath)
+ }
+ return componentPath, nil
+}
+
+func isSingleRefElement(ref string) bool {
+ return !strings.Contains(ref, "#")
+}
+
+func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref string, path *url.URL) (
+ cursor interface{},
+ componentPath *url.URL,
+ err error,
+) {
+ if swagger, ref, componentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, path); err != nil {
+ return nil, nil, err
+ }
+
+ parsedURL, err := url.Parse(ref)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Can't parse reference: '%s': %v", ref, parsedURL)
+ }
+ fragment := parsedURL.Fragment
+ if !strings.HasPrefix(fragment, "/") {
+ err := fmt.Errorf("expected fragment prefix '#/' in URI '%s'", ref)
+ return nil, nil, err
+ }
+
+ cursor = swagger
+ for _, pathPart := range strings.Split(fragment[1:], "/") {
+
+ pathPart = strings.Replace(pathPart, "~1", "/", -1)
+ pathPart = strings.Replace(pathPart, "~0", "~", -1)
+
+ if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil {
+ return nil, nil, fmt.Errorf("Failed to resolve '%s' in fragment in URI: '%s': %v", ref, pathPart, err.Error())
+ }
+ if cursor == nil {
+ return nil, nil, failedToResolveRefFragmentPart(ref, pathPart)
+ }
+ }
+
+ return cursor, componentPath, nil
+}
+
+func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, error) {
+ switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() {
+ case reflect.Map:
+ elementValue := val.MapIndex(reflect.ValueOf(fieldName))
+ if !elementValue.IsValid() {
+ return nil, fmt.Errorf("Map key not found: %v", fieldName)
+ }
+ return elementValue.Interface(), nil
+
+ case reflect.Slice:
+ i, err := strconv.ParseUint(fieldName, 10, 32)
+ if err != nil {
+ return nil, err
+ }
+ index := int(i)
+ if index >= val.Len() {
+ return nil, errors.New("slice index out of bounds")
+ }
+ return val.Index(index).Interface(), nil
+
+ case reflect.Struct:
+ for i := 0; i < val.NumField(); i++ {
+ field := val.Type().Field(i)
+ tagValue := field.Tag.Get("yaml")
+ yamlKey := strings.Split(tagValue, ",")[0]
+ if yamlKey == fieldName {
+ return val.Field(i).Interface(), nil
+ }
+ }
+ // if cursor if a "ref wrapper" struct (e.g. RequestBodyRef), try digging into its Value field
+ _, ok := val.Type().FieldByName("Value")
+ if ok {
+ return drillIntoSwaggerField(val.FieldByName("Value").Interface(), fieldName) // recurse into .Value
+ }
+ // give up
+ return nil, fmt.Errorf("Struct field not found: %v", fieldName)
+
+ default:
+ return nil, errors.New("not a map, slice nor struct")
+ }
+}
+
+func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref string, path *url.URL) (*Swagger, string, *url.URL, error) {
+ componentPath := path
+ if !strings.HasPrefix(ref, "#") {
+ if !swaggerLoader.IsExternalRefsAllowed {
+ return nil, "", nil, fmt.Errorf("Encountered non-allowed external reference: '%s'", ref)
+ }
+ parsedURL, err := url.Parse(ref)
+ if err != nil {
+ return nil, "", nil, fmt.Errorf("Can't parse reference: '%s': %v", ref, parsedURL)
+ }
+ fragment := parsedURL.Fragment
+ parsedURL.Fragment = ""
+
+ resolvedPath, err := resolvePath(path, parsedURL)
+ if err != nil {
+ return nil, "", nil, fmt.Errorf("Error while resolving path: %v", err)
+ }
+
+ if swagger, err = swaggerLoader.loadSwaggerFromURIInternal(resolvedPath); err != nil {
+ return nil, "", nil, fmt.Errorf("Error while resolving reference '%s': %v", ref, err)
+ }
+ ref = fmt.Sprintf("#%s", fragment)
+ componentPath = resolvedPath
+ }
+ return swagger, ref, componentPath, nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, path *url.URL) error {
+ // Prevent infinite recursion
+ visited := swaggerLoader.visited
+ if _, isVisited := visited[component]; isVisited {
+ return nil
+ }
+ visited[component] = struct{}{}
+
+ // Resolve ref
+ const prefix = "#/components/headers/"
+ if ref := component.Ref; len(ref) > 0 {
+ if isSingleRefElement(ref) {
+ var header Header
+ if err := swaggerLoader.loadSingleElementFromURI(ref, path, &header); err != nil {
+ return err
+ }
+
+ component.Value = &header
+ } else {
+ untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
+ if err != nil {
+ return err
+ }
+ resolved, ok := untypedResolved.(*HeaderRef)
+ if !ok {
+ return failedToResolveRefFragment(ref)
+ }
+ if err := swaggerLoader.resolveHeaderRef(swagger, resolved, componentPath); err != nil {
+ return err
+ }
+ component.Value = resolved.Value
+ }
+ }
+ value := component.Value
+ if value == nil {
+ return nil
+ }
+ if schema := value.Schema; schema != nil {
+ if err := swaggerLoader.resolveSchemaRef(swagger, schema, path); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) error {
+ // Prevent infinite recursion
+ visited := swaggerLoader.visited
+ if _, isVisited := visited[component]; isVisited {
+ return nil
+ }
+ visited[component] = struct{}{}
+
+ // Resolve ref
+ const prefix = "#/components/parameters/"
+ ref := component.Ref
+ if len(ref) > 0 {
+ if isSingleRefElement(ref) {
+ var param Parameter
+ if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil {
+ return err
+ }
+ component.Value = ¶m
+ } else {
+ untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath)
+ if err != nil {
+ return err
+ }
+ resolved, ok := untypedResolved.(*ParameterRef)
+ if !ok {
+ return failedToResolveRefFragment(ref)
+ }
+ if err := swaggerLoader.resolveParameterRef(swagger, resolved, componentPath); err != nil {
+ return err
+ }
+ component.Value = resolved.Value
+ }
+ }
+ value := component.Value
+ if value == nil {
+ return nil
+ }
+
+ refDocumentPath, err := referencedDocumentPath(documentPath, ref)
+ if err != nil {
+ return err
+ }
+
+ if value.Content != nil && value.Schema != nil {
+ return errors.New("Cannot contain both schema and content in a parameter")
+ }
+ for _, contentType := range value.Content {
+ if schema := contentType.Schema; schema != nil {
+ if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ }
+ if schema := value.Schema; schema != nil {
+ if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, path *url.URL) error {
+ // Prevent infinite recursion
+ visited := swaggerLoader.visited
+ if _, isVisited := visited[component]; isVisited {
+ return nil
+ }
+ visited[component] = struct{}{}
+
+ // Resolve ref
+ const prefix = "#/components/requestBodies/"
+ if ref := component.Ref; len(ref) > 0 {
+ if isSingleRefElement(ref) {
+ var requestBody RequestBody
+ if err := swaggerLoader.loadSingleElementFromURI(ref, path, &requestBody); err != nil {
+ return err
+ }
+
+ component.Value = &requestBody
+ } else {
+ untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
+ if err != nil {
+ return err
+ }
+ resolved, ok := untypedResolved.(*RequestBodyRef)
+ if !ok {
+ return failedToResolveRefFragment(ref)
+ }
+ if err = swaggerLoader.resolveRequestBodyRef(swagger, resolved, componentPath); err != nil {
+ return err
+ }
+ component.Value = resolved.Value
+ }
+ }
+ value := component.Value
+ if value == nil {
+ return nil
+ }
+ for _, contentType := range value.Content {
+ for name, example := range contentType.Examples {
+ if err := swaggerLoader.resolveExampleRef(swagger, example, path); err != nil {
+ return err
+ }
+ contentType.Examples[name] = example
+ }
+ if schema := contentType.Schema; schema != nil {
+ if err := swaggerLoader.resolveSchemaRef(swagger, schema, path); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) error {
+ // Prevent infinite recursion
+ visited := swaggerLoader.visited
+ if _, isVisited := visited[component]; isVisited {
+ return nil
+ }
+ visited[component] = struct{}{}
+
+ // Resolve ref
+ ref := component.Ref
+ const prefix = "#/components/responses/"
+ if len(ref) > 0 {
+
+ if isSingleRefElement(ref) {
+ var resp Response
+ if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil {
+ return err
+ }
+
+ component.Value = &resp
+ } else {
+ untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath)
+ if err != nil {
+ return err
+ }
+ resolved, ok := untypedResolved.(*ResponseRef)
+ if !ok {
+ return failedToResolveRefFragment(ref)
+ }
+ if err := swaggerLoader.resolveResponseRef(swagger, resolved, componentPath); err != nil {
+ return err
+ }
+ component.Value = resolved.Value
+ }
+ }
+ refDocumentPath, err := referencedDocumentPath(documentPath, ref)
+ if err != nil {
+ return err
+ }
+
+ value := component.Value
+ if value == nil {
+ return nil
+ }
+ for _, header := range value.Headers {
+ if err := swaggerLoader.resolveHeaderRef(swagger, header, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ for _, contentType := range value.Content {
+ if contentType == nil {
+ continue
+ }
+ for name, example := range contentType.Examples {
+ if err := swaggerLoader.resolveExampleRef(swagger, example, refDocumentPath); err != nil {
+ return err
+ }
+ contentType.Examples[name] = example
+ }
+ if schema := contentType.Schema; schema != nil {
+ if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil {
+ return err
+ }
+ contentType.Schema = schema
+ }
+ }
+ for _, link := range value.Links {
+ if err := swaggerLoader.resolveLinkRef(swagger, link, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) error {
+ // Prevent infinite recursion
+ visited := swaggerLoader.visited
+ if _, isVisited := visited[component]; isVisited {
+ return nil
+ }
+ visited[component] = struct{}{}
+
+ // Resolve ref
+ const prefix = "#/components/schemas/"
+ ref := component.Ref
+ if len(ref) > 0 {
+ if isSingleRefElement(ref) {
+ var schema Schema
+ if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil {
+ return err
+ }
+ component.Value = &schema
+ } else {
+ untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, documentPath)
+ if err != nil {
+ return err
+ }
+
+ resolved, ok := untypedResolved.(*SchemaRef)
+ if !ok {
+ return failedToResolveRefFragment(ref)
+ }
+ if err := swaggerLoader.resolveSchemaRef(swagger, resolved, componentPath); err != nil {
+ return err
+ }
+ component.Value = resolved.Value
+ }
+ }
+
+ refDocumentPath, err := referencedDocumentPath(documentPath, ref)
+ if err != nil {
+ return err
+ }
+
+ value := component.Value
+ if value == nil {
+ return nil
+ }
+
+ // ResolveRefs referred schemas
+ if v := value.Items; v != nil {
+ if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ for _, v := range value.Properties {
+ if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ if v := value.AdditionalProperties; v != nil {
+ if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ if v := value.Not; v != nil {
+ if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ for _, v := range value.AllOf {
+ if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ for _, v := range value.AnyOf {
+ if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
+ return err
+ }
+ }
+ for _, v := range value.OneOf {
+ if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, path *url.URL) error {
+ // Prevent infinite recursion
+ visited := swaggerLoader.visited
+ if _, isVisited := visited[component]; isVisited {
+ return nil
+ }
+ visited[component] = struct{}{}
+
+ // Resolve ref
+ const prefix = "#/components/securitySchemes/"
+ if ref := component.Ref; len(ref) > 0 {
+ if isSingleRefElement(ref) {
+ var scheme SecurityScheme
+ if err := swaggerLoader.loadSingleElementFromURI(ref, path, &scheme); err != nil {
+ return err
+ }
+
+ component.Value = &scheme
+ } else {
+ untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
+ if err != nil {
+ return err
+ }
+ resolved, ok := untypedResolved.(*SecuritySchemeRef)
+ if !ok {
+ return failedToResolveRefFragment(ref)
+ }
+ if err := swaggerLoader.resolveSecuritySchemeRef(swagger, resolved, componentPath); err != nil {
+ return err
+ }
+ component.Value = resolved.Value
+ }
+ }
+ return nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, path *url.URL) error {
+ // Prevent infinite recursion
+ visited := swaggerLoader.visited
+ if _, isVisited := visited[component]; isVisited {
+ return nil
+ }
+ visited[component] = struct{}{}
+
+ const prefix = "#/components/examples/"
+ if ref := component.Ref; len(ref) > 0 {
+ if isSingleRefElement(ref) {
+ var example Example
+ if err := swaggerLoader.loadSingleElementFromURI(ref, path, &example); err != nil {
+ return err
+ }
+
+ component.Value = &example
+ } else {
+ untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
+ if err != nil {
+ return err
+ }
+ resolved, ok := untypedResolved.(*ExampleRef)
+ if !ok {
+ return failedToResolveRefFragment(ref)
+ }
+ if err := swaggerLoader.resolveExampleRef(swagger, resolved, componentPath); err != nil {
+ return err
+ }
+ component.Value = resolved.Value
+ }
+ }
+ return nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, path *url.URL) error {
+ // Prevent infinite recursion
+ visited := swaggerLoader.visited
+ if _, isVisited := visited[component]; isVisited {
+ return nil
+ }
+ visited[component] = struct{}{}
+
+ const prefix = "#/components/links/"
+ if ref := component.Ref; len(ref) > 0 {
+ if isSingleRefElement(ref) {
+ var link Link
+ if err := swaggerLoader.loadSingleElementFromURI(ref, path, &link); err != nil {
+ return err
+ }
+
+ component.Value = &link
+ } else {
+ untypedResolved, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, path)
+ if err != nil {
+ return err
+ }
+ resolved, ok := untypedResolved.(*LinkRef)
+ if !ok {
+ return failedToResolveRefFragment(ref)
+ }
+ if err := swaggerLoader.resolveLinkRef(swagger, resolved, componentPath); err != nil {
+ return err
+ }
+ component.Value = resolved.Value
+ }
+ }
+ return nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypoint string, pathItem *PathItem, documentPath *url.URL) (err error) {
+ // Prevent infinite recursion
+ visited := swaggerLoader.visitedFiles
+ key := "_"
+ if documentPath != nil {
+ key = documentPath.EscapedPath()
+ }
+ key += entrypoint
+ if _, isVisited := visited[key]; isVisited {
+ return nil
+ }
+ visited[key] = struct{}{}
+
+ ref := pathItem.Ref
+ if ref != "" {
+ if isSingleRefElement(ref) {
+ var p PathItem
+ if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil {
+ return err
+ }
+ *pathItem = p
+ } else {
+ if swagger, ref, documentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, documentPath); err != nil {
+ return
+ }
+
+ prefix := "#/paths/"
+ if !strings.HasPrefix(ref, prefix) {
+ err = fmt.Errorf("expected prefix '%s' in URI '%s'", prefix, ref)
+ return
+ }
+ id := unescapeRefString(ref[len(prefix):])
+
+ definitions := swagger.Paths
+ if definitions == nil {
+ return failedToResolveRefFragmentPart(ref, "paths")
+ }
+ resolved := definitions[id]
+ if resolved == nil {
+ return failedToResolveRefFragmentPart(ref, id)
+ }
+
+ *pathItem = *resolved
+ }
+ }
+
+ refDocumentPath, err := referencedDocumentPath(documentPath, ref)
+ if err != nil {
+ return err
+ }
+
+ for _, parameter := range pathItem.Parameters {
+ if err = swaggerLoader.resolveParameterRef(swagger, parameter, refDocumentPath); err != nil {
+ return
+ }
+ }
+ for _, operation := range pathItem.Operations() {
+ for _, parameter := range operation.Parameters {
+ if err = swaggerLoader.resolveParameterRef(swagger, parameter, refDocumentPath); err != nil {
+ return
+ }
+ }
+ if requestBody := operation.RequestBody; requestBody != nil {
+ if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, refDocumentPath); err != nil {
+ return
+ }
+ }
+ for _, response := range operation.Responses {
+ if err = swaggerLoader.resolveResponseRef(swagger, response, refDocumentPath); err != nil {
+ return
+ }
+ }
+ }
+
+ return nil
+}
+
+func unescapeRefString(ref string) string {
+ return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1)
+}
+
+func referencedDocumentPath(documentPath *url.URL, ref string) (*url.URL, error) {
+ newDocumentPath := documentPath
+ if documentPath != nil {
+ refDirectory, err := url.Parse(path.Dir(ref))
+ if err != nil {
+ return nil, err
+ }
+ joinedDirectory := path.Join(path.Dir(documentPath.String()), refDirectory.String())
+ if newDocumentPath, err = url.Parse(joinedDirectory + "/"); err != nil {
+ return nil, err
+ }
+ }
+ return newDocumentPath, nil
+}
diff --git a/openapi3/loader_empty_response_description_test.go b/openapi3/swagger_loader_empty_response_description_test.go
similarity index 79%
rename from openapi3/loader_empty_response_description_test.go
rename to openapi3/swagger_loader_empty_response_description_test.go
index 3c4b6bffd..5199ac169 100644
--- a/openapi3/loader_empty_response_description_test.go
+++ b/openapi3/swagger_loader_empty_response_description_test.go
@@ -1,10 +1,11 @@
-package openapi3
+package openapi3_test
import (
"encoding/json"
"strings"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
@@ -33,8 +34,8 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) {
{
spec := []byte(spec)
- loader := NewLoader()
- doc, err := loader.LoadFromData(spec)
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData(spec)
require.NoError(t, err)
got := doc.Paths["/path1"].Get.Responses["200"].Value.Description
expected := ""
@@ -46,8 +47,8 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) {
{
spec := []byte(strings.Replace(spec, `"description": ""`, `"description": "My response"`, 1))
- loader := NewLoader()
- doc, err := loader.LoadFromData(spec)
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData(spec)
require.NoError(t, err)
got := doc.Paths["/path1"].Get.Responses["200"].Value.Description
expected := "My response"
@@ -57,9 +58,9 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) {
require.NoError(t, err)
}
- noDescriptionIsInvalid := func(data []byte) *T {
- loader := NewLoader()
- doc, err := loader.LoadFromData(data)
+ noDescriptionIsInvalid := func(data []byte) *openapi3.Swagger {
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData(data)
require.NoError(t, err)
got := doc.Paths["/path1"].Get.Responses["200"].Value.Description
require.Nil(t, got)
@@ -69,7 +70,7 @@ func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) {
return doc
}
- var docWithNoResponseDescription *T
+ var docWithNoResponseDescription *openapi3.Swagger
{
spec := []byte(strings.Replace(spec, `"description": ""`, ``, 1))
docWithNoResponseDescription = noDescriptionIsInvalid(spec)
diff --git a/openapi3/loader_issue212_test.go b/openapi3/swagger_loader_issue212_test.go
similarity index 94%
rename from openapi3/loader_issue212_test.go
rename to openapi3/swagger_loader_issue212_test.go
index 252d0d224..1999db4d3 100644
--- a/openapi3/loader_issue212_test.go
+++ b/openapi3/swagger_loader_issue212_test.go
@@ -72,8 +72,8 @@ components:
pattern: ^\/images\/[0-9a-f]{64}$
`
- loader := NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
+ loader := NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData([]byte(spec))
require.NoError(t, err)
err = doc.Validate(loader.Context)
require.NoError(t, err)
@@ -81,7 +81,7 @@ components:
expected, err := json.Marshal(&Schema{
Type: "object",
Required: []string{"id", "uri"},
- Properties: Schemas{
+ Properties: map[string]*SchemaRef{
"id": {Value: &Schema{Type: "string"}},
"uri": {Value: &Schema{Type: "string"}},
},
diff --git a/openapi3/loader_paths_test.go b/openapi3/swagger_loader_paths_test.go
similarity index 64%
rename from openapi3/loader_paths_test.go
rename to openapi3/swagger_loader_paths_test.go
index f7edc7374..e805d3ae3 100644
--- a/openapi3/loader_paths_test.go
+++ b/openapi3/swagger_loader_paths_test.go
@@ -1,9 +1,10 @@
-package openapi3
+package openapi3_test
import (
"strings"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
@@ -13,6 +14,7 @@ openapi: "3.0"
info:
version: "1.0"
title: sample
+basePath: /adc/v1
paths:
PATH:
get:
@@ -22,11 +24,11 @@ paths:
`
for path, expectedErr := range map[string]string{
- "foo/bar": "invalid paths: path \"foo/bar\" does not start with a forward slash (/)",
+ "foo/bar": "Error when validating Paths: Path 'foo/bar' does not start with '/'",
"/foo/bar": "",
} {
- loader := NewLoader()
- doc, err := loader.LoadFromData([]byte(strings.Replace(spec, "PATH", path, 1)))
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData([]byte(strings.Replace(spec, "PATH", path, 1)))
require.NoError(t, err)
err = doc.Validate(loader.Context)
if expectedErr != "" {
diff --git a/openapi3/loader_relative_refs_test.go b/openapi3/swagger_loader_relative_refs_test.go
similarity index 68%
rename from openapi3/loader_relative_refs_test.go
rename to openapi3/swagger_loader_relative_refs_test.go
index 50d2c7d24..5ad7585ae 100644
--- a/openapi3/loader_relative_refs_test.go
+++ b/openapi3/swagger_loader_relative_refs_test.go
@@ -1,130 +1,132 @@
-package openapi3
+package openapi3_test
import (
"fmt"
+
"net/url"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
type refTestDataEntry struct {
name string
contentTemplate string
- testFunc func(t *testing.T, doc *T)
+ testFunc func(t *testing.T, swagger *openapi3.Swagger)
}
type refTestDataEntryWithErrorMessage struct {
name string
contentTemplate string
errorMessage *string
- testFunc func(t *testing.T, doc *T)
+ testFunc func(t *testing.T, swagger *openapi3.Swagger)
}
var refTestDataEntries = []refTestDataEntry{
{
name: "SchemaRef",
contentTemplate: externalSchemaRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.Schemas["TestSchema"].Value.Type)
- require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.Schemas["TestSchema"].Value.Type)
+ require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type)
},
},
{
name: "ResponseRef",
contentTemplate: externalResponseRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
desc := "description"
- require.Equal(t, &desc, doc.Components.Responses["TestResponse"].Value.Description)
+ require.Equal(t, &desc, swagger.Components.Responses["TestResponse"].Value.Description)
},
},
{
name: "ParameterRef",
contentTemplate: externalParameterRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.Parameters["TestParameter"].Value.Name)
- require.Equal(t, "id", doc.Components.Parameters["TestParameter"].Value.Name)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.Parameters["TestParameter"].Value.Name)
+ require.Equal(t, "id", swagger.Components.Parameters["TestParameter"].Value.Name)
},
},
{
name: "ExampleRef",
contentTemplate: externalExampleRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.Examples["TestExample"].Value.Description)
- require.Equal(t, "description", doc.Components.Examples["TestExample"].Value.Description)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.Examples["TestExample"].Value.Description)
+ require.Equal(t, "description", swagger.Components.Examples["TestExample"].Value.Description)
},
},
{
name: "RequestBodyRef",
contentTemplate: externalRequestBodyRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.RequestBodies["TestRequestBody"].Value.Content)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.RequestBodies["TestRequestBody"].Value.Content)
},
},
{
name: "SecuritySchemeRef",
contentTemplate: externalSecuritySchemeRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Description)
- require.Equal(t, "description", doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Description)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Description)
+ require.Equal(t, "description", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Description)
},
},
{
name: "ExternalHeaderRef",
contentTemplate: externalHeaderRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.Headers["TestHeader"].Value.Description)
- require.Equal(t, "description", doc.Components.Headers["TestHeader"].Value.Description)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.Headers["TestHeader"].Value.Description)
+ require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description)
},
},
{
name: "PathParameterRef",
contentTemplate: externalPathParameterRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test/{id}"].Parameters[0].Value.Name)
- require.Equal(t, "id", doc.Paths["/test/{id}"].Parameters[0].Value.Name)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test/{id}"].Parameters[0].Value.Name)
+ require.Equal(t, "id", swagger.Paths["/test/{id}"].Parameters[0].Value.Name)
},
},
{
name: "PathOperationParameterRef",
contentTemplate: externalPathOperationParameterRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test/{id}"].Get.Parameters[0].Value)
- require.Equal(t, "id", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Name)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test/{id}"].Get.Parameters[0].Value)
+ require.Equal(t, "id", swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Name)
},
},
{
name: "PathOperationRequestBodyRef",
contentTemplate: externalPathOperationRequestBodyRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value)
- require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value)
+ require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value.Content)
},
},
{
name: "PathOperationResponseRef",
contentTemplate: externalPathOperationResponseRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value)
desc := "description"
- require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description)
+ require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description)
},
},
{
name: "PathOperationParameterSchemaRef",
contentTemplate: externalPathOperationParameterSchemaRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value)
- require.Equal(t, "string", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value.Type)
- require.Equal(t, "id", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Name)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value)
+ require.Equal(t, "string", swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value.Type)
+ require.Equal(t, "id", swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Name)
},
},
{
name: "PathOperationParameterRefWithContentInQuery",
contentTemplate: externalPathOperationParameterWithContentInQueryTemplate,
- testFunc: func(t *testing.T, doc *T) {
- schemaRef := doc.Paths["/test/{id}"].Get.Parameters[0].Value.Content["application/json"].Schema
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ schemaRef := swagger.Paths["/test/{id}"].Get.Parameters[0].Value.Content["application/json"].Schema
require.NotNil(t, schemaRef.Value)
require.Equal(t, "string", schemaRef.Value.Type)
},
@@ -133,53 +135,53 @@ var refTestDataEntries = []refTestDataEntry{
{
name: "PathOperationRequestBodyExampleRef",
contentTemplate: externalPathOperationRequestBodyExampleRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value)
- require.Equal(t, "description", doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value.Description)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value)
+ require.Equal(t, "description", swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value.Description)
},
},
{
name: "PathOperationReqestBodyContentSchemaRef",
contentTemplate: externalPathOperationReqestBodyContentSchemaRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value)
- require.Equal(t, "string", doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Type)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value)
+ require.Equal(t, "string", swagger.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Type)
},
},
{
name: "PathOperationResponseExampleRef",
contentTemplate: externalPathOperationResponseExampleRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value)
desc := "testdescription"
- require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description)
- require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Examples["application/json"].Value.Description)
+ require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description)
+ require.Equal(t, "description", swagger.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Examples["application/json"].Value.Description)
},
},
{
name: "PathOperationResponseSchemaRef",
contentTemplate: externalPathOperationResponseSchemaRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value)
desc := "testdescription"
- require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description)
- require.Equal(t, "string", doc.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Schema.Value.Type)
+ require.Equal(t, &desc, swagger.Paths["/test"].Post.Responses["default"].Value.Description)
+ require.Equal(t, "string", swagger.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Schema.Value.Type)
},
},
{
name: "ComponentHeaderSchemaRef",
contentTemplate: externalComponentHeaderSchemaRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.Headers["TestHeader"].Value)
- require.Equal(t, "string", doc.Components.Headers["TestHeader"].Value.Schema.Value.Type)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.Headers["TestHeader"].Value)
+ require.Equal(t, "string", swagger.Components.Headers["TestHeader"].Value.Schema.Value.Type)
},
},
{
name: "RequestResponseHeaderRef",
contentTemplate: externalRequestResponseHeaderRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
- require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
+ require.Equal(t, "description", swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
},
},
}
@@ -188,48 +190,48 @@ var refTestDataEntriesResponseError = []refTestDataEntryWithErrorMessage{
{
name: "CannotContainBothSchemaAndContentInAParameter",
contentTemplate: externalCannotContainBothSchemaAndContentInAParameter,
- errorMessage: &(&struct{ x string }{"cannot contain both schema and content in a parameter"}).x,
- testFunc: func(t *testing.T, doc *T) {
+ errorMessage: &(&struct{ x string }{"Cannot contain both schema and content in a parameter"}).x,
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
},
},
}
func TestLoadFromDataWithExternalRef(t *testing.T) {
for _, td := range refTestDataEntries {
- t.Logf("testcase %q", td.name)
+ t.Logf("testcase '%s'", td.name)
spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json"))
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
+ swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
require.NoError(t, err)
- td.testFunc(t, doc)
+ td.testFunc(t, swagger)
}
}
func TestLoadFromDataWithExternalRefResponseError(t *testing.T) {
for _, td := range refTestDataEntriesResponseError {
- t.Logf("testcase %q", td.name)
+ t.Logf("testcase '%s'", td.name)
spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json"))
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
+ swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
require.EqualError(t, err, *td.errorMessage)
- td.testFunc(t, doc)
+ td.testFunc(t, swagger)
}
}
func TestLoadFromDataWithExternalNestedRef(t *testing.T) {
for _, td := range refTestDataEntries {
- t.Logf("testcase %q", td.name)
+ t.Logf("testcase '%s'", td.name)
spec := []byte(fmt.Sprintf(td.contentTemplate, "nesteddir/nestedcomponents.openapi.json"))
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
+ swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
require.NoError(t, err)
- td.testFunc(t, doc)
+ td.testFunc(t, swagger)
}
}
@@ -723,116 +725,116 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{
{
name: "SchemaRef",
contentTemplate: relativeSchemaDocsRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.Schemas["TestSchema"].Value.Type)
- require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.Schemas["TestSchema"].Value.Type)
+ require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type)
},
},
{
name: "ResponseRef",
contentTemplate: relativeResponseDocsRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
desc := "description"
- require.Equal(t, &desc, doc.Components.Responses["TestResponse"].Value.Description)
+ require.Equal(t, &desc, swagger.Components.Responses["TestResponse"].Value.Description)
},
},
{
name: "ParameterRef",
contentTemplate: relativeParameterDocsRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.Parameters["TestParameter"].Value.Name)
- require.Equal(t, "param", doc.Components.Parameters["TestParameter"].Value.Name)
- require.Equal(t, true, doc.Components.Parameters["TestParameter"].Value.Required)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.Parameters["TestParameter"].Value.Name)
+ require.Equal(t, "param", swagger.Components.Parameters["TestParameter"].Value.Name)
+ require.Equal(t, true, swagger.Components.Parameters["TestParameter"].Value.Required)
},
},
{
name: "ExampleRef",
contentTemplate: relativeExampleDocsRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, "param", doc.Components.Examples["TestExample"].Value.Summary)
- require.NotNil(t, "param", doc.Components.Examples["TestExample"].Value.Value)
- require.Equal(t, "An example", doc.Components.Examples["TestExample"].Value.Summary)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, "param", swagger.Components.Examples["TestExample"].Value.Summary)
+ require.NotNil(t, "param", swagger.Components.Examples["TestExample"].Value.Value)
+ require.Equal(t, "An example", swagger.Components.Examples["TestExample"].Value.Summary)
},
},
{
name: "RequestRef",
contentTemplate: relativeRequestDocsRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, "param", doc.Components.RequestBodies["TestRequestBody"].Value.Description)
- require.Equal(t, "example request", doc.Components.RequestBodies["TestRequestBody"].Value.Description)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, "param", swagger.Components.RequestBodies["TestRequestBody"].Value.Description)
+ require.Equal(t, "example request", swagger.Components.RequestBodies["TestRequestBody"].Value.Description)
},
},
{
name: "HeaderRef",
contentTemplate: relativeHeaderDocsRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, "param", doc.Components.Headers["TestHeader"].Value.Description)
- require.Equal(t, "description", doc.Components.Headers["TestHeader"].Value.Description)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, "param", swagger.Components.Headers["TestHeader"].Value.Description)
+ require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description)
},
},
{
name: "HeaderRef",
contentTemplate: relativeHeaderDocsRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, "param", doc.Components.Headers["TestHeader"].Value.Description)
- require.Equal(t, "description", doc.Components.Headers["TestHeader"].Value.Description)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, "param", swagger.Components.Headers["TestHeader"].Value.Description)
+ require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description)
},
},
{
name: "SecuritySchemeRef",
contentTemplate: relativeSecuritySchemeDocsRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Type)
- require.NotNil(t, doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme)
- require.Equal(t, "http", doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Type)
- require.Equal(t, "basic", doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme)
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Type)
+ require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme)
+ require.Equal(t, "http", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Type)
+ require.Equal(t, "basic", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme)
},
},
{
name: "PathRef",
contentTemplate: relativePathDocsRefTemplate,
- testFunc: func(t *testing.T, doc *T) {
- require.NotNil(t, doc.Paths["/pets"])
- require.NotNil(t, doc.Paths["/pets"].Get.Responses["200"])
- require.NotNil(t, doc.Paths["/pets"].Get.Responses["200"].Value.Content["application/json"])
+ testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+ require.NotNil(t, swagger.Paths["/pets"])
+ require.NotNil(t, swagger.Paths["/pets"].Get.Responses["200"])
+ require.NotNil(t, swagger.Paths["/pets"].Get.Responses["200"].Value.Content["application/json"])
},
},
}
func TestLoadSpecWithRelativeDocumentRefs(t *testing.T) {
for _, td := range relativeDocRefsTestDataEntries {
- t.Logf("testcase %q", td.name)
+ t.Logf("testcase '%s'", td.name)
spec := []byte(td.contentTemplate)
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/"})
+ swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/"})
require.NoError(t, err)
- td.testFunc(t, doc)
+ td.testFunc(t, swagger)
}
}
const relativeSchemaDocsRefTemplate = `
openapi: 3.0.0
-info:
+info:
title: ""
version: "1.0"
paths: {}
-components:
- schemas:
- TestSchema:
+components:
+ schemas:
+ TestSchema:
$ref: relativeDocs/CustomTestSchema.yml
`
const relativeResponseDocsRefTemplate = `
openapi: 3.0.0
-info:
+info:
title: ""
version: "1.0"
paths: {}
-components:
- responses:
- TestResponse:
+components:
+ responses:
+ TestResponse:
$ref: relativeDocs/CustomTestResponse.yml
`
@@ -844,7 +846,7 @@ info:
paths: {}
components:
parameters:
- TestParameter:
+ TestParameter:
$ref: relativeDocs/CustomTestParameter.yml
`
@@ -906,23 +908,21 @@ paths:
`
func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) {
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/relativeDocsUseDocumentPath/openapi/openapi.yml")
+ swagger, err := loader.LoadSwaggerFromFile("testdata/relativeDocsUseDocumentPath/openapi/openapi.yml")
require.NoError(t, err)
// path in nested directory
// check parameter
- nestedDirPath := doc.Paths["/pets/{id}"]
+ nestedDirPath := swagger.Paths["/pets/{id}"]
require.Equal(t, "param", nestedDirPath.Patch.Parameters[0].Value.Name)
require.Equal(t, "path", nestedDirPath.Patch.Parameters[0].Value.In)
require.Equal(t, true, nestedDirPath.Patch.Parameters[0].Value.Required)
// check header
require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description)
- require.Equal(t, "header1", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Another"].Value.Description)
- require.Equal(t, "header2", nestedDirPath.Patch.Responses["200"].Value.Headers["X-And-Another"].Value.Description)
// check request body
require.Equal(t, "example request", nestedDirPath.Patch.RequestBody.Value.Description)
@@ -934,15 +934,13 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) {
// path in more nested directory
// check parameter
- moreNestedDirPath := doc.Paths["/pets/{id}/{city}"]
+ moreNestedDirPath := swagger.Paths["/pets/{id}/{city}"]
require.Equal(t, "param", moreNestedDirPath.Patch.Parameters[0].Value.Name)
require.Equal(t, "path", moreNestedDirPath.Patch.Parameters[0].Value.In)
require.Equal(t, true, moreNestedDirPath.Patch.Parameters[0].Value.Required)
// check header
require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description)
- require.Equal(t, "header1", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Another"].Value.Description)
- require.Equal(t, "header2", nestedDirPath.Patch.Responses["200"].Value.Headers["X-And-Another"].Value.Description)
// check request body
require.Equal(t, "example request", moreNestedDirPath.Patch.RequestBody.Value.Description)
diff --git a/openapi3/loader_test.go b/openapi3/swagger_loader_test.go
similarity index 51%
rename from openapi3/loader_test.go
rename to openapi3/swagger_loader_test.go
index 9756f55fc..920911a17 100644
--- a/openapi3/loader_test.go
+++ b/openapi3/swagger_loader_test.go
@@ -1,4 +1,4 @@
-package openapi3
+package openapi3_test
import (
"fmt"
@@ -6,9 +6,9 @@ import (
"net/http"
"net/http/httptest"
"net/url"
- "strings"
"testing"
+ "github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)
@@ -54,77 +54,52 @@ paths:
$ref: '#/components/schemas/ErrorModel'
`)
- loader := NewLoader()
-
- doc, err := loader.LoadFromData(spec)
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData(spec)
require.NoError(t, err)
require.Equal(t, "An API", doc.Info.Title)
require.Equal(t, 2, len(doc.Components.Schemas))
require.Equal(t, 1, len(doc.Paths))
- require.Equal(t, "unexpected error", *doc.Paths["/items"].Put.Responses.Default().Value.Description)
-
+ def := doc.Paths["/items"].Put.Responses.Default().Value
+ desc := "unexpected error"
+ require.Equal(t, &desc, def.Description)
err = doc.Validate(loader.Context)
require.NoError(t, err)
}
-func TestIssue731(t *testing.T) {
- spec := []byte(`
-openapi: 3.0.0
-info:
- title: An API
- version: v1
-paths:
- /items:
- put:
- description: ''
- requestBody:
- required: true
- # Note mis-indented content block
- content:
- application/json:
- schema:
- type: object
- responses:
- default:
- description: unexpected error
- content:
- application/json:
- schema:
- type: object
-`[1:])
-
- loader := NewLoader()
-
- doc, err := loader.LoadFromData(spec)
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.ErrorContains(t, err, `content of the request body is required`)
-}
-
-func ExampleLoader() {
- const source = `{"info":{"description":"An API"}}`
- doc, err := NewLoader().LoadFromData([]byte(source))
+func ExampleSwaggerLoader() {
+ source := `{"info":{"description":"An API"}}`
+ swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData([]byte(source))
if err != nil {
panic(err)
}
- fmt.Print(doc.Info.Description)
- // Output: An API
+ fmt.Print(swagger.Info.Description)
+ // Output:
+ // An API
}
func TestResolveSchemaRef(t *testing.T) {
- source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`)
- loader := NewLoader()
- doc, err := loader.LoadFromData(source)
+ source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1",description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`)
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData(source)
require.NoError(t, err)
err = doc.Validate(loader.Context)
- require.NoError(t, err)
+ require.NoError(t, err)
refAVisited := doc.Components.Schemas["A"].Value.AllOf[0]
require.Equal(t, "#/components/schemas/B", refAVisited.Ref)
require.NotNil(t, refAVisited.Value)
}
+func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) {
+ source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{"/foo":{"post":{"requestBody":{"content":{"application/json":{"schema":null}}}}}}}`)
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData(source)
+ require.NoError(t, err)
+ err = doc.Validate(loader.Context)
+ require.EqualError(t, err, "Error when validating Paths: Found unresolved ref: ''")
+}
+
func TestResolveResponseExampleRef(t *testing.T) {
source := []byte(`
openapi: 3.0.1
@@ -147,8 +122,8 @@ paths:
examples:
test:
$ref: '#/components/examples/test'`)
- loader := NewLoader()
- doc, err := loader.LoadFromData(source)
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData(source)
require.NoError(t, err)
err = doc.Validate(loader.Context)
@@ -159,12 +134,76 @@ paths:
require.Equal(t, example.Value.Value.(map[string]interface{})["error"].(bool), false)
}
+type sourceExample struct {
+ Location *url.URL
+ Spec []byte
+}
+
+type multipleSourceSwaggerLoaderExample struct {
+ Sources []*sourceExample
+}
+
+func (l *multipleSourceSwaggerLoaderExample) LoadSwaggerFromURI(
+ loader *openapi3.SwaggerLoader,
+ location *url.URL,
+) (*openapi3.Swagger, error) {
+ source := l.resolveSourceFromURI(location)
+ if source == nil {
+ return nil, fmt.Errorf("Unsupported URI: '%s'", location.String())
+ }
+ return loader.LoadSwaggerFromData(source.Spec)
+}
+
+func (l *multipleSourceSwaggerLoaderExample) resolveSourceFromURI(location fmt.Stringer) *sourceExample {
+ locationString := location.String()
+ for _, v := range l.Sources {
+ if v.Location.String() == locationString {
+ return v
+ }
+ }
+ return nil
+}
+
+func TestResolveSchemaExternalRef(t *testing.T) {
+ rootLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "spec.json"}
+ externalLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "external.json"}
+ rootSpec := []byte(fmt.Sprintf(
+ `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Root":{"allOf":[{"$ref":"%s#/components/schemas/External"}]}}}}`,
+ externalLocation.String(),
+ ))
+ externalSpec := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"External Spec"},"paths":{},"components":{"schemas":{"External":{"type":"string"}}}}`)
+ multipleSourceLoader := &multipleSourceSwaggerLoaderExample{
+ Sources: []*sourceExample{
+ {
+ Location: rootLocation,
+ Spec: rootSpec,
+ },
+ {
+ Location: externalLocation,
+ Spec: externalSpec,
+ },
+ },
+ }
+ loader := &openapi3.SwaggerLoader{
+ IsExternalRefsAllowed: true,
+ LoadSwaggerFromURIFunc: multipleSourceLoader.LoadSwaggerFromURI,
+ }
+ doc, err := loader.LoadSwaggerFromURI(rootLocation)
+ require.NoError(t, err)
+ err = doc.Validate(loader.Context)
+
+ require.NoError(t, err)
+ refRootVisited := doc.Components.Schemas["Root"].Value.AllOf[0]
+ require.Equal(t, fmt.Sprintf("%s#/components/schemas/External", externalLocation.String()), refRootVisited.Ref)
+ require.NotNil(t, refRootVisited.Value)
+}
+
func TestLoadErrorOnRefMisuse(t *testing.T) {
spec := []byte(`
openapi: '3.0.0'
servers: [{url: /}]
info:
- title: Some API
+ title: ''
version: '1'
components:
schemas:
@@ -174,7 +213,6 @@ paths:
put:
description: ''
requestBody:
- # Uses a schema ref instead of a requestBody ref.
$ref: '#/components/schemas/Thing'
responses:
'201':
@@ -185,8 +223,8 @@ paths:
$ref: '#/components/schemas/Thing'
`)
- loader := NewLoader()
- _, err := loader.LoadFromData(spec)
+ loader := openapi3.NewSwaggerLoader()
+ _, err := loader.LoadSwaggerFromData(spec)
require.Error(t, err)
}
@@ -213,11 +251,11 @@ paths:
description: Test call.
`)
- loader := NewLoader()
- doc, err := loader.LoadFromData(spec)
+ loader := openapi3.NewSwaggerLoader()
+ swagger, err := loader.LoadSwaggerFromData(spec)
require.NoError(t, err)
- require.NotNil(t, doc.Paths["/"].Parameters[0].Value)
+ require.NotNil(t, swagger.Paths["/"].Parameters[0].Value)
}
func TestLoadRequestExampleRef(t *testing.T) {
@@ -245,101 +283,57 @@ paths:
description: Test call.
`)
- loader := NewLoader()
- doc, err := loader.LoadFromData(spec)
+ loader := openapi3.NewSwaggerLoader()
+ swagger, err := loader.LoadSwaggerFromData(spec)
require.NoError(t, err)
- require.NotNil(t, doc.Paths["/"].Post.RequestBody.Value.Content.Get("application/json").Examples["test"])
+ require.NotNil(t, swagger.Paths["/"].Post.RequestBody.Value.Content.Get("application/json").Examples["test"])
}
-func createTestServer(t *testing.T, handler http.Handler) *httptest.Server {
+func createTestServer(handler http.Handler) *httptest.Server {
ts := httptest.NewUnstartedServer(handler)
- l, err := net.Listen("tcp", addr)
- require.NoError(t, err)
+ l, _ := net.Listen("tcp", addr)
ts.Listener.Close()
ts.Listener = l
return ts
}
func TestLoadFromRemoteURL(t *testing.T) {
+
fs := http.FileServer(http.Dir("testdata"))
- ts := createTestServer(t, fs)
+ ts := createTestServer(fs)
ts.Start()
defer ts.Close()
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
url, err := url.Parse("http://" + addr + "/test.openapi.json")
require.NoError(t, err)
- doc, err := loader.LoadFromURI(url)
+ swagger, err := loader.LoadSwaggerFromURI(url)
require.NoError(t, err)
- require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type)
-}
-
-func TestLoadWithReferenceInReference(t *testing.T) {
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/refInRef/openapi.json")
- require.NoError(t, err)
- require.NotNil(t, doc)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
- require.Equal(t, "string", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type)
-}
-
-func TestLoadWithRecursiveReferenceInLocalReferenceInParentSubdir(t *testing.T) {
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/refInLocalRefInParentsSubdir/spec/openapi.json")
- require.NoError(t, err)
- require.NotNil(t, doc)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
- require.Equal(t, "object", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type)
-}
-
-func TestLoadWithRecursiveReferenceInRefrerenceInLocalReference(t *testing.T) {
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/refInLocalRef/openapi.json")
- require.NoError(t, err)
- require.NotNil(t, doc)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
- require.Equal(t, "integer", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Type)
- require.Equal(t, "int64", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Format)
-}
-
-func TestLoadWithReferenceInReferenceInProperty(t *testing.T) {
- loader := NewLoader()
- loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/refInRefInProperty/openapi.yaml")
- require.NoError(t, err)
- require.NotNil(t, doc)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
- require.Equal(t, "Problem details", doc.Paths["/api/test/ref/in/ref/in/property"].Post.Responses["401"].Value.Content["application/json"].Schema.Value.Properties["error"].Value.Title)
+ require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type)
}
func TestLoadFileWithExternalSchemaRef(t *testing.T) {
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/testref.openapi.json")
+ swagger, err := loader.LoadSwaggerFromFile("testdata/testref.openapi.json")
require.NoError(t, err)
- require.NotNil(t, doc.Components.Schemas["AnotherTestSchema"].Value.Type)
+
+ require.NotNil(t, swagger.Components.Schemas["AnotherTestSchema"].Value.Type)
}
func TestLoadFileWithExternalSchemaRefSingleComponent(t *testing.T) {
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/testrefsinglecomponent.openapi.json")
+ swagger, err := loader.LoadSwaggerFromFile("testdata/testrefsinglecomponent.openapi.json")
require.NoError(t, err)
- require.NotNil(t, doc.Components.Responses["SomeResponse"])
+ require.NotNil(t, swagger.Components.Responses["SomeResponse"])
desc := "this is a single response definition"
- require.Equal(t, &desc, doc.Components.Responses["SomeResponse"].Value.Description)
+ require.Equal(t, &desc, swagger.Components.Responses["SomeResponse"].Value.Description)
}
func TestLoadRequestResponseHeaderRef(t *testing.T) {
@@ -375,12 +369,12 @@ func TestLoadRequestResponseHeaderRef(t *testing.T) {
}
}`)
- loader := NewLoader()
- doc, err := loader.LoadFromData(spec)
+ loader := openapi3.NewSwaggerLoader()
+ swagger, err := loader.LoadSwaggerFromData(spec)
require.NoError(t, err)
- require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
- require.Equal(t, "testheader", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
+ require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
+ require.Equal(t, "testheader", swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
}
func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) {
@@ -410,45 +404,45 @@ func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) {
}`)
fs := http.FileServer(http.Dir("testdata"))
- ts := createTestServer(t, fs)
+ ts := createTestServer(fs)
ts.Start()
defer ts.Close()
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
+ swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"})
require.NoError(t, err)
- require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
- require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
+ require.NotNil(t, swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
+ require.Equal(t, "description", swagger.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description)
}
func TestLoadYamlFile(t *testing.T) {
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/test.openapi.yml")
+ swagger, err := loader.LoadSwaggerFromFile("testdata/test.openapi.yml")
require.NoError(t, err)
- require.Equal(t, "OAI Specification in YAML", doc.Info.Title)
+ require.Equal(t, "OAI Specification in YAML", swagger.Info.Title)
}
func TestLoadYamlFileWithExternalSchemaRef(t *testing.T) {
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/testref.openapi.yml")
+ swagger, err := loader.LoadSwaggerFromFile("testdata/testref.openapi.yml")
require.NoError(t, err)
- require.NotNil(t, doc.Components.Schemas["AnotherTestSchema"].Value.Type)
+ require.NotNil(t, swagger.Components.Schemas["AnotherTestSchema"].Value.Type)
}
func TestLoadYamlFileWithExternalPathRef(t *testing.T) {
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
loader.IsExternalRefsAllowed = true
- doc, err := loader.LoadFromFile("testdata/pathref.openapi.yml")
+ swagger, err := loader.LoadSwaggerFromFile("testdata/pathref.openapi.yml")
require.NoError(t, err)
- require.NotNil(t, doc.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type)
- require.Equal(t, "string", doc.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type)
+ require.NotNil(t, swagger.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type)
+ require.Equal(t, "string", swagger.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type)
}
func TestResolveResponseLinkRef(t *testing.T) {
@@ -471,7 +465,6 @@ paths:
parameters:
- name: id,
in: path
- required: true
schema:
type: string
responses:
@@ -483,8 +476,8 @@ paths:
father:
$ref: '#/components/links/Father'
`)
- loader := NewLoader()
- doc, err := loader.LoadFromData(source)
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData(source)
require.NoError(t, err)
err = doc.Validate(loader.Context)
@@ -497,20 +490,6 @@ paths:
require.Equal(t, "link to to the father", link.Description)
}
-func TestLinksFromOAISpec(t *testing.T) {
- loader := NewLoader()
- doc, err := loader.LoadFromFile("testdata/link-example.yaml")
- require.NoError(t, err)
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
- response := doc.Paths[`/2.0/repositories/{username}/{slug}`].Get.Responses.Get(200).Value
- link := response.Links[`repositoryPullRequests`].Value
- require.Equal(t, map[string]interface{}{
- "username": "$response.body#/owner/username",
- "slug": "$response.body#/slug",
- }, link.Parameters)
-}
-
func TestResolveNonComponentsRef(t *testing.T) {
spec := []byte(`
openapi: 3.0.0
@@ -566,44 +545,9 @@ paths:
$ref: '#/components/schemas/ErrorModel'
`)
- loader := NewLoader()
- doc, err := loader.LoadFromData(spec)
+ loader := openapi3.NewSwaggerLoader()
+ doc, err := loader.LoadSwaggerFromData(spec)
require.NoError(t, err)
err = doc.Validate(loader.Context)
require.NoError(t, err)
}
-
-func TestServersVariables(t *testing.T) {
- const spec = `
-openapi: 3.0.1
-info:
- title: My API
- version: 1.0.0
-paths: {}
-servers:
-- @@@
-`
- for value, expected := range map[string]string{
- `{url: /}`: "",
- `{url: "http://{x}.{y}.example.com"}`: "invalid servers: server has undeclared variables",
- `{url: "http://{x}.y}.example.com"}`: "invalid servers: server URL has mismatched { and }",
- `{url: "http://{x.example.com"}`: "invalid servers: server URL has mismatched { and }",
- `{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`: "",
- `{url: "http://{x}.example.com", variables: {x: {default: "www", enum: ["www"]}}}`: "",
- `{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`: `invalid servers: field default is required in {"enum":["www"]}`,
- `{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`: "invalid servers: server has undeclared variables",
- `{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`: "invalid servers: server has undeclared variables",
- } {
- t.Run(value, func(t *testing.T) {
- loader := NewLoader()
- doc, err := loader.LoadFromData([]byte(strings.Replace(spec, "@@@", value, 1)))
- require.NoError(t, err)
- err = doc.Validate(loader.Context)
- if expected == "" {
- require.NoError(t, err)
- } else {
- require.EqualError(t, err, expected)
- }
- })
- }
-}
diff --git a/openapi3/openapi3_test.go b/openapi3/swagger_test.go
similarity index 61%
rename from openapi3/openapi3_test.go
rename to openapi3/swagger_test.go
index e01af82ba..d4c433983 100644
--- a/openapi3/openapi3_test.go
+++ b/openapi3/swagger_test.go
@@ -1,38 +1,39 @@
-package openapi3
+package openapi3_test
import (
"context"
"encoding/json"
- "strings"
+ "errors"
"testing"
- "github.com/invopop/yaml"
+ "github.com/getkin/kin-openapi/openapi3"
+ "github.com/ghodss/yaml"
"github.com/stretchr/testify/require"
)
func TestRefsJSON(t *testing.T) {
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
- t.Log("Marshal *T to JSON")
+ t.Log("Marshal *openapi3.Swagger to JSON")
data, err := json.Marshal(spec())
require.NoError(t, err)
require.NotEmpty(t, data)
- t.Log("Unmarshal *T from JSON")
- docA := &T{}
+ t.Log("Unmarshal *openapi3.Swagger from JSON")
+ docA := &openapi3.Swagger{}
err = json.Unmarshal(specJSON, &docA)
require.NoError(t, err)
require.NotEmpty(t, data)
- t.Log("Resolve refs in unmarshalled *T")
+ t.Log("Resolve refs in unmarshalled *openapi3.Swagger")
err = loader.ResolveRefsIn(docA, nil)
require.NoError(t, err)
- t.Log("Resolve refs in marshalled *T")
- docB, err := loader.LoadFromData(data)
+ t.Log("Resolve refs in marshalled *openapi3.Swagger")
+ docB, err := loader.LoadSwaggerFromData(data)
require.NoError(t, err)
require.NotEmpty(t, docB)
- t.Log("Validate *T")
+ t.Log("Validate *openapi3.Swagger")
err = docA.Validate(loader.Context)
require.NoError(t, err)
err = docB.Validate(loader.Context)
@@ -49,28 +50,28 @@ func TestRefsJSON(t *testing.T) {
}
func TestRefsYAML(t *testing.T) {
- loader := NewLoader()
+ loader := openapi3.NewSwaggerLoader()
- t.Log("Marshal *T to YAML")
+ t.Log("Marshal *openapi3.Swagger to YAML")
data, err := yaml.Marshal(spec())
require.NoError(t, err)
require.NotEmpty(t, data)
- t.Log("Unmarshal *T from YAML")
- docA := &T{}
+ t.Log("Unmarshal *openapi3.Swagger from YAML")
+ docA := &openapi3.Swagger{}
err = yaml.Unmarshal(specYAML, &docA)
require.NoError(t, err)
require.NotEmpty(t, data)
- t.Log("Resolve refs in unmarshalled *T")
+ t.Log("Resolve refs in unmarshalled *openapi3.Swagger")
err = loader.ResolveRefsIn(docA, nil)
require.NoError(t, err)
- t.Log("Resolve refs in marshalled *T")
- docB, err := loader.LoadFromData(data)
+ t.Log("Resolve refs in marshalled *openapi3.Swagger")
+ docB, err := loader.LoadSwaggerFromData(data)
require.NoError(t, err)
require.NotEmpty(t, docB)
- t.Log("Validate *T")
+ t.Log("Validate *openapi3.Swagger")
err = docA.Validate(loader.Context)
require.NoError(t, err)
err = docB.Validate(loader.Context)
@@ -123,7 +124,6 @@ components:
requestBodies:
someRequestBody:
description: Some request body
- content: {}
responses:
someResponse:
description: Some response
@@ -131,8 +131,7 @@ components:
someSchema:
description: Some schema
headers:
- otherHeader:
- schema: {type: string}
+ otherHeader: {}
someHeader:
"$ref": "#/components/headers/otherHeader"
examples:
@@ -195,8 +194,7 @@ var specJSON = []byte(`
},
"requestBodies": {
"someRequestBody": {
- "description": "Some request body",
- "content": {}
+ "description": "Some request body"
}
},
"responses": {
@@ -210,11 +208,7 @@ var specJSON = []byte(`
}
},
"headers": {
- "otherHeader": {
- "schema": {
- "type": "string"
- }
- },
+ "otherHeader": {},
"someHeader": {
"$ref": "#/components/headers/otherHeader"
}
@@ -244,54 +238,53 @@ var specJSON = []byte(`
}
`)
-func spec() *T {
- parameter := &Parameter{
+func spec() *openapi3.Swagger {
+ parameter := &openapi3.Parameter{
Description: "Some parameter",
Name: "example",
In: "query",
- Schema: &SchemaRef{
+ Schema: &openapi3.SchemaRef{
Ref: "#/components/schemas/someSchema",
},
}
- requestBody := &RequestBody{
+ requestBody := &openapi3.RequestBody{
Description: "Some request body",
- Content: NewContent(),
}
responseDescription := "Some response"
- response := &Response{
+ response := &openapi3.Response{
Description: &responseDescription,
}
- schema := &Schema{
+ schema := &openapi3.Schema{
Description: "Some schema",
}
example := map[string]string{"name": "Some example"}
- return &T{
+ return &openapi3.Swagger{
OpenAPI: "3.0",
- Info: &Info{
+ Info: &openapi3.Info{
Title: "MyAPI",
Version: "0.1",
},
- Paths: Paths{
- "/hello": &PathItem{
- Post: &Operation{
- Parameters: Parameters{
+ Paths: openapi3.Paths{
+ "/hello": &openapi3.PathItem{
+ Post: &openapi3.Operation{
+ Parameters: openapi3.Parameters{
{
Ref: "#/components/parameters/someParameter",
Value: parameter,
},
},
- RequestBody: &RequestBodyRef{
+ RequestBody: &openapi3.RequestBodyRef{
Ref: "#/components/requestBodies/someRequestBody",
Value: requestBody,
},
- Responses: Responses{
- "200": &ResponseRef{
+ Responses: openapi3.Responses{
+ "200": &openapi3.ResponseRef{
Ref: "#/components/responses/someResponse",
Value: response,
},
},
},
- Parameters: Parameters{
+ Parameters: openapi3.Parameters{
{
Ref: "#/components/parameters/someParameter",
Value: parameter,
@@ -299,49 +292,49 @@ func spec() *T {
},
},
},
- Components: &Components{
- Parameters: ParametersMap{
+ Components: openapi3.Components{
+ Parameters: map[string]*openapi3.ParameterRef{
"someParameter": {
Value: parameter,
},
},
- RequestBodies: RequestBodies{
+ RequestBodies: map[string]*openapi3.RequestBodyRef{
"someRequestBody": {
Value: requestBody,
},
},
- Responses: Responses{
+ Responses: map[string]*openapi3.ResponseRef{
"someResponse": {
Value: response,
},
},
- Schemas: Schemas{
+ Schemas: map[string]*openapi3.SchemaRef{
"someSchema": {
Value: schema,
},
},
- Headers: Headers{
+ Headers: map[string]*openapi3.HeaderRef{
"someHeader": {
Ref: "#/components/headers/otherHeader",
},
"otherHeader": {
- Value: &Header{Parameter{Schema: &SchemaRef{Value: NewStringSchema()}}},
+ Value: &openapi3.Header{},
},
},
- Examples: Examples{
+ Examples: map[string]*openapi3.ExampleRef{
"someExample": {
Ref: "#/components/examples/otherExample",
},
"otherExample": {
- Value: NewExample(example),
+ Value: openapi3.NewExample(example),
},
},
- SecuritySchemes: SecuritySchemes{
+ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{
"someSecurityScheme": {
Ref: "#/components/securitySchemes/otherSecurityScheme",
},
"otherSecurityScheme": {
- Value: &SecurityScheme{
+ Value: &openapi3.SecurityScheme{
Description: "Some security scheme",
Type: "apiKey",
In: "query",
@@ -353,16 +346,117 @@ func spec() *T {
}
}
+// TestValidation tests validation of properties in the root of the OpenAPI
+// file.
func TestValidation(t *testing.T) {
- version := `
+ tests := []struct {
+ name string
+ input []byte
+ expectedError error
+ }{
+ {
+ "when no OpenAPI property is supplied",
+ []byte(`
+info:
+ title: "Hello World REST APIs"
+ version: "1.0"
+paths:
+ "/api/v2/greetings.json":
+ get:
+ operationId: listGreetings
+ responses:
+ 200:
+ description: "List different greetings"
+ "/api/v2/greetings/{id}.json":
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ example: "greeting"
+ get:
+ operationId: showGreeting
+ responses:
+ 200:
+ description: "Get a single greeting object"
+`),
+ errors.New("Variable 'openapi' must be a non-empty JSON string"),
+ },
+ {
+ "when an empty OpenAPI property is supplied",
+ []byte(`
+openapi: ''
+info:
+ title: "Hello World REST APIs"
+ version: "1.0"
+paths:
+ "/api/v2/greetings.json":
+ get:
+ operationId: listGreetings
+ responses:
+ 200:
+ description: "List different greetings"
+ "/api/v2/greetings/{id}.json":
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ example: "greeting"
+ get:
+ operationId: showGreeting
+ responses:
+ 200:
+ description: "Get a single greeting object"
+`),
+ errors.New("Variable 'openapi' must be a non-empty JSON string"),
+ },
+ {
+ "when the Info property is not supplied",
+ []byte(`
+openapi: '1.0'
+paths:
+ "/api/v2/greetings.json":
+ get:
+ operationId: listGreetings
+ responses:
+ 200:
+ description: "List different greetings"
+ "/api/v2/greetings/{id}.json":
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ example: "greeting"
+ get:
+ operationId: showGreeting
+ responses:
+ 200:
+ description: "Get a single greeting object"
+`),
+ errors.New("Variable 'info' must be a JSON object"),
+ },
+ {
+ "when the Paths property is not supplied",
+ []byte(`
+openapi: '1.0'
+info:
+ title: "Hello World REST APIs"
+ version: "1.0"
+`),
+ errors.New("Variable 'paths' must be a JSON object"),
+ },
+ {
+ "when a valid spec is supplied",
+ []byte(`
openapi: 3.0.2
-`
- info := `
info:
title: "Hello World REST APIs"
version: "1.0"
-`
- paths := `
paths:
"/api/v2/greetings.json":
get:
@@ -383,18 +477,6 @@ paths:
responses:
200:
description: "Get a single greeting object"
-`
- externalDocs := `
-externalDocs:
- url: https://root-ext-docs.com
-`
- tags := `
-tags:
- - name: "pet"
- externalDocs:
- url: https://tags-ext-docs.com
-`
- spec := version + info + paths + externalDocs + tags + `
components:
schemas:
GreetingObject:
@@ -408,63 +490,21 @@ components:
properties:
description:
type: string
-`
-
- tests := []struct {
- name string
- spec string
- expectedErr string
- }{
- {
- name: "no errors",
- spec: spec,
- },
- {
- name: "version is missing",
- spec: strings.Replace(spec, version, "", 1),
- expectedErr: "value of openapi must be a non-empty string",
- },
- {
- name: "version is empty string",
- spec: strings.Replace(spec, version, "openapi: ''", 1),
- expectedErr: "value of openapi must be a non-empty string",
- },
- {
- name: "info section is missing",
- spec: strings.Replace(spec, info, ``, 1),
- expectedErr: "invalid info: must be an object",
- },
- {
- name: "paths section is missing",
- spec: strings.Replace(spec, paths, ``, 1),
- expectedErr: "invalid paths: must be an object",
- },
- {
- name: "externalDocs section is invalid",
- spec: strings.Replace(spec, externalDocs,
- strings.ReplaceAll(externalDocs, "url: https://root-ext-docs.com", "url: ''"), 1),
- expectedErr: "invalid external docs: url is required",
- },
- {
- name: "tags section is invalid",
- spec: strings.Replace(spec, tags,
- strings.ReplaceAll(tags, "url: https://tags-ext-docs.com", "url: ''"), 1),
- expectedErr: "invalid tags: invalid external docs: url is required",
+`),
+ nil,
},
}
- for i := range tests {
- tt := tests[i]
- t.Run(tt.name, func(t *testing.T) {
- doc := &T{}
- err := yaml.Unmarshal([]byte(tt.spec), &doc)
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ doc := &openapi3.Swagger{}
+ err := yaml.Unmarshal(test.input, &doc)
require.NoError(t, err)
- err = doc.Validate(context.Background())
- if tt.expectedErr != "" {
- require.EqualError(t, err, tt.expectedErr)
- } else {
- require.NoError(t, err)
- }
+ c := context.Background()
+ validationErr := doc.Validate(c)
+
+ require.Equal(t, test.expectedError, validationErr, "expected errors (or lack of) to match")
})
}
}
diff --git a/openapi3/tag.go b/openapi3/tag.go
index 93009a13c..d5de72d59 100644
--- a/openapi3/tag.go
+++ b/openapi3/tag.go
@@ -1,11 +1,5 @@
package openapi3
-import (
- "context"
- "encoding/json"
- "fmt"
-)
-
// Tags is specified by OpenAPI/Swagger 3.0 standard.
type Tags []*Tag
@@ -18,70 +12,9 @@ func (tags Tags) Get(name string) *Tag {
return nil
}
-// Validate returns an error if Tags does not comply with the OpenAPI spec.
-func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- for _, v := range tags {
- if err := v.Validate(ctx); err != nil {
- return err
- }
- }
- return nil
-}
-
// Tag is specified by OpenAPI/Swagger 3.0 standard.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tag-object
type Tag struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
}
-
-// MarshalJSON returns the JSON encoding of Tag.
-func (t Tag) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 3+len(t.Extensions))
- for k, v := range t.Extensions {
- m[k] = v
- }
- if x := t.Name; x != "" {
- m["name"] = x
- }
- if x := t.Description; x != "" {
- m["description"] = x
- }
- if x := t.ExternalDocs; x != nil {
- m["externalDocs"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets Tag to a copy of data.
-func (t *Tag) UnmarshalJSON(data []byte) error {
- type TagBis Tag
- var x TagBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "name")
- delete(x.Extensions, "description")
- delete(x.Extensions, "externalDocs")
- *t = Tag(x)
- return nil
-}
-
-// Validate returns an error if Tag does not comply with the OpenAPI spec.
-func (t *Tag) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- if v := t.ExternalDocs; v != nil {
- if err := v.Validate(ctx); err != nil {
- return fmt.Errorf("invalid external docs: %w", err)
- }
- }
-
- return validateExtensions(ctx, t.Extensions)
-}
diff --git a/openapi3/testdata/303bis/common/properties.yaml b/openapi3/testdata/303bis/common/properties.yaml
deleted file mode 100644
index e5b6cdb46..000000000
--- a/openapi3/testdata/303bis/common/properties.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-timestamp:
- type: string
- description: Date and time in ISO 8601 format.
- example: "2020-04-09T18:14:30Z"
- readOnly: true
- nullable: true
-
-timestamps:
- type: object
- properties:
- created_at:
- $ref: "#/timestamp"
- deleted_at:
- $ref: "#/timestamp"
- updated_at:
- $ref: "#/timestamp"
diff --git a/openapi3/testdata/303bis/service.yaml b/openapi3/testdata/303bis/service.yaml
deleted file mode 100644
index 39dd06639..000000000
--- a/openapi3/testdata/303bis/service.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-openapi: 3.0.0
-info:
- title: 'some service spec'
- version: 1.2.3
-
-paths:
- /service:
- get:
- tags:
- - services/service
- summary: List services
- description: List services.
- operationId: list-services
- responses:
- "200":
- description: OK
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: "#/components/schemas/model_service"
-
-components:
- schemas:
- model_service:
- allOf:
- - $ref: "common/properties.yaml#/timestamps"
diff --git a/openapi3/testdata/Test_param_override.yml b/openapi3/testdata/Test_param_override.yml
deleted file mode 100644
index d6982414a..000000000
--- a/openapi3/testdata/Test_param_override.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-openapi: 3.0.0
-info:
- title: customer
- version: '1.0'
-servers:
- - url: 'httpbin.kwaf-demo.test'
-paths:
- '/customers/{customer_id}':
- parameters:
- - schema:
- type: integer
- name: customer_id
- in: path
- required: true
- get:
- parameters:
- - schema:
- type: integer
- maximum: 100
- name: customer_id
- in: path
- required: true
- summary: customer
- tags: []
- responses:
- '200':
- description: OK
- content:
- application/json:
- schema:
- type: object
- properties:
- customer_id:
- type: integer
- customer_name:
- type: string
- operationId: get-customers-customer_id
- description: Retrieve a specific customer by ID
-components:
- schemas: {}
diff --git a/openapi3/testdata/callback-transactioned.yml b/openapi3/testdata/callback-transactioned.yml
deleted file mode 100644
index 2d58b394b..000000000
--- a/openapi3/testdata/callback-transactioned.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-post:
- requestBody:
- description: Callback payload
- content:
- 'application/json':
- schema:
- $ref: 'callbacks.yml#/components/schemas/SomePayload'
- responses:
- '200':
- description: callback successfully processed
diff --git a/openapi3/testdata/callbacks.yml b/openapi3/testdata/callbacks.yml
deleted file mode 100644
index 4ad3f7d73..000000000
--- a/openapi3/testdata/callbacks.yml
+++ /dev/null
@@ -1,71 +0,0 @@
-openapi: 3.1.0
-info:
- title: Callback refd
- version: 1.2.3
-paths:
- /trans:
- post:
- description: ''
- requestBody:
- description: ''
- content:
- 'application/json':
- schema:
- properties:
- id: {type: string}
- email: {format: email}
- responses:
- '201':
- description: subscription successfully created
- content:
- application/json:
- schema:
- type: object
- callbacks:
- transactionCallback:
- 'http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}':
- $ref: callback-transactioned.yml
-
- /other:
- post:
- description: ''
- parameters:
- - name: queryUrl
- in: query
- required: true
- description: |
- bla
- bla
- bla
- schema:
- type: string
- format: uri
- example: https://example.com
- responses:
- '201':
- description: ''
- content:
- application/json:
- schema:
- type: object
- callbacks:
- myEvent:
- $ref: '#/components/callbacks/MyCallbackEvent'
-
-components:
- schemas:
- SomePayload: {type: object}
- SomeOtherPayload: {type: boolean}
- callbacks:
- MyCallbackEvent:
- '{$request.query.queryUrl}':
- post:
- requestBody:
- description: Callback payload
- content:
- 'application/json':
- schema:
- $ref: '#/components/schemas/SomeOtherPayload'
- responses:
- '200':
- description: callback successfully processed
diff --git a/openapi3/testdata/callbacks.yml.internalized.yml b/openapi3/testdata/callbacks.yml.internalized.yml
deleted file mode 100644
index 866cb5ca4..000000000
--- a/openapi3/testdata/callbacks.yml.internalized.yml
+++ /dev/null
@@ -1,131 +0,0 @@
-{
- "components": {
- "callbacks": {
- "MyCallbackEvent": {
- "{$request.query.queryUrl}": {
- "post": {
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/SomeOtherPayload"
- }
- }
- },
- "description": "Callback payload"
- },
- "responses": {
- "200": {
- "description": "callback successfully processed"
- }
- }
- }
- }
- }
- },
- "schemas": {
- "SomeOtherPayload": {
- "type": "boolean"
- },
- "SomePayload": {
- "type": "object"
- }
- }
- },
- "info": {
- "title": "Callback refd",
- "version": "1.2.3"
- },
- "openapi": "3.1.0",
- "paths": {
- "/other": {
- "post": {
- "callbacks": {
- "myEvent": {
- "$ref": "#/components/callbacks/MyCallbackEvent"
- }
- },
- "parameters": [
- {
- "description": "bla\nbla\nbla\n",
- "in": "query",
- "name": "queryUrl",
- "required": true,
- "schema": {
- "example": "https://example.com",
- "format": "uri",
- "type": "string"
- }
- }
- ],
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object"
- }
- }
- },
- "description": ""
- }
- }
- }
- },
- "/trans": {
- "post": {
- "callbacks": {
- "transactionCallback": {
- "http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}": {
- "post": {
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/SomePayload"
- }
- }
- },
- "description": "Callback payload"
- },
- "responses": {
- "200": {
- "description": "callback successfully processed"
- }
- }
- }
- }
- }
- },
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "properties": {
- "email": {
- "format": "email"
- },
- "id": {
- "type": "string"
- }
- }
- }
- }
- }
- },
- "responses": {
- "201": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object"
- }
- }
- },
- "description": "subscription successfully created"
- }
- }
- }
- }
- }
-}
diff --git a/openapi3/testdata/circularRef/base.yml b/openapi3/testdata/circularRef/base.yml
deleted file mode 100644
index ff8240eb0..000000000
--- a/openapi3/testdata/circularRef/base.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-openapi: "3.0.3"
-info:
- title: Recursive cyclic refs example
- version: "1.0"
-components:
- schemas:
- Foo:
- properties:
- foo2:
- $ref: "other.yml#/components/schemas/Foo2"
- bar:
- $ref: "#/components/schemas/Bar"
- Bar:
- properties:
- foo:
- $ref: "#/components/schemas/Foo"
diff --git a/openapi3/testdata/circularRef/other.yml b/openapi3/testdata/circularRef/other.yml
deleted file mode 100644
index 29b72d98c..000000000
--- a/openapi3/testdata/circularRef/other.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-openapi: "3.0.3"
-info:
- title: Recursive cyclic refs example
- version: "1.0"
-components:
- schemas:
- Foo2:
- properties:
- id:
- type: string
diff --git a/openapi3/testdata/ext.json b/openapi3/testdata/ext.json
deleted file mode 100644
index df227e62e..000000000
--- a/openapi3/testdata/ext.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "$schema": "http://json-schema.org/draft-04/schema#",
- "definitions": {
- "a": {
- "type": "string"
- },
- "b": {
- "type": "object",
- "description": "I use a local reference.",
- "properties": {
- "name": {
- "$ref": "#/definitions/a"
- }
- }
- }
- }
-}
diff --git a/openapi3/testdata/issue235.spec0-typo.yml b/openapi3/testdata/issue235.spec0-typo.yml
deleted file mode 100644
index 543600620..000000000
--- a/openapi3/testdata/issue235.spec0-typo.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-openapi: 3.0.0
-info:
- title: 'OAI Specification in YAML'
- version: 0.0.1
-paths:
- /test:
- get:
- responses:
- "200":
- $ref: '#/components/responses/GetTestOK'
-components:
- responses:
- GetTestOK:
- description: OK
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ObjectA'
- schemas:
- ObjectA:
- type: object
- properties:
- object_b:
- $ref: 'issue235.spec0-typo.yml#/components/schemas/ObjectD'
diff --git a/openapi3/testdata/issue235.spec0.yml b/openapi3/testdata/issue235.spec0.yml
deleted file mode 100644
index d9236aaec..000000000
--- a/openapi3/testdata/issue235.spec0.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-openapi: 3.0.0
-info:
- title: 'OAI Specification in YAML'
- version: 0.0.1
-paths:
- /test:
- get:
- responses:
- "200":
- $ref: '#/components/responses/GetTestOK'
-components:
- responses:
- GetTestOK:
- description: OK
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ObjectA'
- schemas:
- ObjectA:
- type: object
- properties:
- object_b:
- $ref: 'issue235.spec1.yml#/components/schemas/ObjectD'
diff --git a/openapi3/testdata/issue235.spec1.yml b/openapi3/testdata/issue235.spec1.yml
deleted file mode 100644
index a1bc67906..000000000
--- a/openapi3/testdata/issue235.spec1.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-components:
- schemas:
- ObjectD:
- type: object
- properties:
- result:
- $ref: '#/components/schemas/ObjectE'
-
- ObjectE:
- properties:
- name:
- $ref: issue235.spec2.yml#/components/schemas/ObjectX
diff --git a/openapi3/testdata/issue235.spec2.yml b/openapi3/testdata/issue235.spec2.yml
deleted file mode 100644
index b0bcb0fa2..000000000
--- a/openapi3/testdata/issue235.spec2.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-components:
- schemas:
- ObjectX:
- type: object
- properties:
- name:
- type: string
diff --git a/openapi3/testdata/issue241.yml b/openapi3/testdata/issue241.yml
deleted file mode 100644
index 07609c1d8..000000000
--- a/openapi3/testdata/issue241.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-openapi: 3.0.3
-components:
- schemas:
- FooBar:
- type: object
- properties:
- type_url:
- type: string
- value:
- type: string
- format: byte
-info:
- title: sample
- version: version not set
-paths: {}
diff --git a/openapi3/testdata/issue409.yml b/openapi3/testdata/issue409.yml
deleted file mode 100644
index 88394904e..000000000
--- a/openapi3/testdata/issue409.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-openapi: 3.0.3
-info:
- description: Contains Patterns that can't be compiled by the go regexp engine
- title: Issue 409
- version: 0.0.1
-paths:
- /v1/apis/{apiID}:
- get:
- description: Get a list of all Apis and there versions for a given workspace
- operationId: getApisV1
- parameters:
- - description: The ID of the API
- in: path
- name: apiID
- required: true
- schema:
- type: string
- pattern: ^[a-zA-Z0-9]{0,4096}$
- responses:
- "200":
- description: OK
diff --git a/openapi3/testdata/issue570.json b/openapi3/testdata/issue570.json
deleted file mode 100644
index ed3a7509b..000000000
--- a/openapi3/testdata/issue570.json
+++ /dev/null
@@ -1,155 +0,0 @@
-{
- "swagger": "2.0",
- "info": {
- "version": "internal",
- "title": "Rubrik INTERNAL REST API",
- "description": "Copyright © 2017-2021 Rubrik Inc.\n\n# Introduction\n\nThis is the INTERNAL REST API for Rubrik. We don't guarantee support or backward compatibility. Use at your own risk.\n\n# Changelog\n\n Revisions are listed with the most recent revision first.\n ### Changes to Internal API in Rubrik version 6.0\n ## Breaking changes:\n * Renamed field `node` to `nodeId` for object `NetworkInterface` used by\n `GET /cluster/{id}/network_interface`.\n * Removed `compliance24HourStatus` in `DataSourceTableRequest` for\n `POST /report/data_source/table`.\n Use `complianceStatus`, `awaitingFirstFull`, and `snapshotRange`\n as replacements.\n * Changed the sort_by attribute of `GET /vcd/vapp` to use\n `VcdVappObjectSortAttribute`.\n This attribute no longer uses the `VappCount` or `ConnectionStatus`\n parameters from the previously used `VcdHierarchyObjectSortAttribute`.\n\n ## Feature additions/improvements:\n * Added the `GET /sla_domain/{id}/protected_objects` endpoint to return\n objects explicitly protected by the SLA Domain with direct assignments.\n * Added new field `nodeName` for object `NetworkInterface` used by\n `GET /cluster/{id}/network_interface`.\n * Added the `POST /cluster/{id}/remove_nodes` endpoint to trigger a bulk\n node removal job.\n * Added new optional field `numChannels` to `ExportOracleDbConfig` object\n specifying the number of channels used during Oracle clone or same-host\n recovery.\n * Added new optional fields `forceFull` to the object\n `HypervVirtualMachineSummary` used by `GET /hyperv/vm`. This field is also\n used in `HypervVirtualMachineDetail` used by `GET /hyperv/vm/{id}` and\n `PATCH /hyperv/vm/{id}`.\n * Added the `GET /cluster/{id}/bootstrap_config` endpoint to enable Rubrik CDM\n to retrieve Rubrik cluster configuration information for the cluster nodes.\n * Added new optional field clusterUuid to the ClusterConfig object used\n by `POST /cluster/{id}/bootstrap` and `POST /cluster/{id}/setupnetwork`.\n * Added new optional fields `dataGuardGroupId` and `dataGuardGroupName` to\n the object `OracleHierarchyObjectSummary` used by\n `GET /oracle/hierarchy/{id}`, `GET /oracle/hierarchy/{id}/children`, and\n `GET /oracle/hierarchy/{id}/descendants`.\n * Added new optional fields `dataGuardGroupId` and `dataGuardGroupName` to\n the object `OracleDbSummary` used by `GET /oracle/db`.\n * Added new optional fields `dataGuardGroupId` and `dataGuardGroupName` to\n the object `OracleDbDetail` used by `GET /oracle/db/{id}` and\n `PATCH /oracle/db/{id}`.\n * Added a new optional field `immutabilityLockSummary` to the object\n `ArchivalLocationSummary` returned by GET `/archive/location` and\n GET `/organization/{id}/archive/location`\n * Added new optional fields `dbUniqueName` and `databaseRole` to the object\n `OracleHierarchyObjectSummary` used by `GET /oracle/hierarchy/{id}`,\n `GET /oracle/hierarchy/{id}/children`, and\n `GET /oracle/hierarchy/{id}/descendants`.\n * Added new required fields `dbUniqueName` and `databaseRole` to the object\n `OracleDbSummary` used by `GET /oracle/db`.\n * Added a new required field `databaseRole` to the object `OracleDbDetail`\n used by `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}`.\n * Added a new optional field `subnet` to `ManagedVolumeUpdate`, used by \n `PATCH /managed_volume/{id}` for updating the subnet to which the node IPs\n will belong during an SLA MV backup.\n * Added new optional field `numChannels` to `RecoverOracleDbConfig`\n and `MountOracleDbConfig` objects specifying the number of channels used\n during Oracle recovery.\n * Added a new optional field `immutabilityLockSummary` to the object\n `ObjectStoreLocationSummary` and `ObjectStoreUpdateDefinition` used by\n `GET/POST /archive/object_store` and `GET/POST /archive/object_store/{id}`\n * Added a new optional field `errorMessage` to `SupportTunnelInfo` object \n used by `GET /node/{id}/support_tunnel` and\n `PATCH /node/{id}/support_tunnel`.\n * Added new optional field `cloudStorageLocation` to the `ClusterConfig`\n object used by `POST /cluster/{id}/bootstrap`.\n * Added new enum `Disabled` to `DataLocationOwnershipStatus`\n used by `ArchivalLocationSummary`\n * Added a new optional field `installTarball` to the `ClusterConfig`\n object used by `POST /cluster/{id}/bootstrap`.\n * Added a new optional field `clusterInstall` to the `ClusterConfigStatus`\n object used by `GET /cluster/{id}/bootstrap`.\n * Added the `GET /cluster/{id}/install` endpoint to return the current\n status of Rubrik CDM install on a cluster.\n * Added the `POST /cluster/{id}/install` endpoint to allow Rubrik CDM \n install on cluster nodes which are not bootstrapped.\n * Added the `GET /cluster/{id}/packages` endpoint to return the list of\n Rubrik CDM packages available for installation.\n * Updated `request_id` parameter in the `GET /cluster/{id}/bootstrap` \n endpoint, as not required.\n * Updated `request_id` parameter in the `GET /cluster/{id}/install` \n endpoint, as not required.\n * Updated `BootstrappableNodeInfo` returned by `GET /cluster/{id}/discover`\n endpoint to include the `version` field, to indicate the\n Rubrik CDM software version.\n * Added a new optional field `isSetupNetworkOnly` to the `ClusterConfig`\n object used by `POST /cluster/{id}/setupnetwork`.\n * Added the `POST /cluster/{id}/setupnetwork` endpoint to enable Rubrik CDM\n to perform network setup on nodes that are not bootstrapped.\n * Added the `GET /cluster/{id}/setupnetwork` endpoint to return the current\n status of setup network command on node or nodes.\n * Added a new optional field `hostname` to the `NodeStatus` object used by\n `GET /cluster/{id}/node`, `GET /node`, `GET /node/stats`, `GET /node/{id}`,\n and `GET /node/{id}/stats`.\n * Added new optional fields `usedFastVhdx` and `fileSizeInBytes` to the\n `HypervVirtualMachineSnapshotSummary` returned by the API\n `GET /hyperv/vm/{id}/snapshot`.\n * Added the `GET /archive/location/request/{id}` endpoint to query the status\n of asynchronous archival location requests.\n\n ## Deprecation:\n * Deprecated the following Oracle endpoints\n * `GET /oracle/db`\n * `GET /oracle/db/{id}`\n * `PATCH /oracle/db/{id}`\n * Deprecated the following vcd hierarchy endpoints. \n * `GET /vcd/hierarchy/{id}`\n * `GET /vcd/hierarchy/{id}/children`\n * `GET /vcd/hierarchy/{id}/descendants`\n * Deprecated the following vcd cluster endpoints.\n * `GET /vcd/cluster`\n * `POST /vcd/cluster`\n * `GET /vcd/cluster/{id}/vimserver`\n * `POST /vcd/cluster/{id}/refresh`\n * `GET /vcd/cluster/{id}`\n * `PATCH /vcd/cluster/{id}`\n * `DELETE /vcd/cluster/{id}`\n * `GET /vcd/cluster/request/{id}`\n * Deprecated the following vcd vapp endpoints.\n * `GET /vcd/vapp`\n * `GET /vcd/vapp/{id}`\n * `PATCH /vcd/vapp/{id}`\n * `GET /vcd/vapp/{id}/snapshot`\n * `POST /vcd/vapp/{id}/snapshot`\n * `DELETE /vcd/vapp/{id}/snapshot`\n * `GET/vcd/vapp/snapshot/{id}`\n * `DELETE /vcd/vapp/snapshot/{id}`\n * `GET /vcd/vapp`\n * `GET /vcd/vapp/{id}/missed_snapshot`\n * `GET /vcd/vapp/snapshot/{snapshot_id}/export/options`\n * `POST /vcd/vapp/snapshot/{snapshot_id}/export`\n * `POST /vcd/vapp/snapshot/{snapshot_id}/instant_recover`\n * `GET /vcd/vapp/snapshot/{snapshot_id}/instant_recover/options`\n * `GET /vcd/vapp/request/{id}`\n * `GET /vcd/vapp/{id}/search`\n * `POST /vcd/vapp/snapshot/{id}/download`\n\n ### Changes to Internal API in Rubrik version 5.3.2\n ## Deprecation:\n * Deprecated `compliance24HourStatus` in `DataSourceTableRequest` for\n `POST /report/data_source/table`.\n Use `complianceStatus`, `awaitingFirstFull`, and `snapshotRange`\n as replacements.\n\n ### Changes to Internal API in Rubrik version 5.3.1\n ## Breaking changes:\n * Added new required field `isPwdEncryptionSupported` to\n the API response `PlatformInfo` for password-based encryption at rest\n in the API `GET /cluster/{id}/platforminfo`.\n\n ## Feature additions/improvements:\n * Added new field `hostsInfo` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/children`.\n * Added new field `hostsInfo` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/descendants`.\n * Added new field `hostsInfo` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}`.\n * Added `shouldKeepConvertedDisksOnFailure` as an optional field in\n CreateCloudInstanceRequest definition used in the on-demand API\n conversion API `/cloud_on/aws/instance` and `/cloud_on/azure/instance`.\n This will enable converted disks to be kept on failure for CloudOn\n conversion.\n * Added the `hostsInfo` field to the OracleDbDetail that the\n `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}` endpoints return.\n * Added new optional field `isOnNetAppSnapMirrorDestVolume` to\n HostShareParameters to support backup of NetApp SnapMirror\n destination volume.\n * Added new optional fields `encryptionPassword` and\n `newEncryptionPassword` to the KeyRotationOptions to support\n key rotation for password-based encryption at rest in\n internal API `POST /cluster/{id}/security/key_rotation`.\n * Added `Index` to `ReportableTaskType`.\n * Added new optional field `totpStatus` in `UserDetail` for\n showing the TOTP status of the user with the endpoint\n `GET /internal/user/{id}`\n * Added new optional field `isTotpEnforced` in `UserDefinition` for\n configuring the TOTP enforcement for the user with the endpoint\n `POST /internal/user`\n * Added new optional field `isTotpEnforced` in `UserUpdateInfo` for\n configuring the TOTP enforcement for the user with the endpoint\n `PATCH /internal/user/{id}`\n * Added a new field `HypervVirtualDiskInfo` to HypervVirtualMachineDetail \n used by `GET /hyperv/vm/{id}`.\n * Added a new field `virtualDiskIdsExcludedFromSnapshot` to \n HypervVirtualMachineUpdate used by `PATCH /hyperv/vm/{id}`.\n\n ### Changes to Internal API in Rubrik version 5.3.0\n ## Deprecation:\n * Deprecated `GET /authorization/role/admin`,\n `GET /authorization/role/compliance_officer`,\n `GET /authorization/role/end_user`,\n `GET /authorization/role/infra_admin`,\n `GET /authorization/role/managed_volume_admin`,\n `GET /authorization/role/managed_volume_user`,\n `GET /authorization/role/org_admin`,\n `GET /authorization/role/organization`,\n `GET /authorization/role/read_only_admin` endpoints. Use the new\n v1 endpoints for role management.\n * Deprecated `SnapshotCloudStorageTier` enum value Cold. It will be left,\n but will be mapped internally to the new value, AzureArchive, which is\n recommended as a replacement.\n * Deprecated the `GET /snapshot/{id}/storage/stats` endpoint. Use the v1\n version when possible.\n * Deprecated `POST /hierarchy/bulk_sla_conflicts`. It is migrated to\n v1 and using that is recommended.\n * Deprecated `GET /mssql/availability_group`,\n `GET /mssql/availability_group/{id}`,\n `PATCH /mssql/availability_group/{id}`, `PATCH /mssql/db/bulk`,\n `POST /mssql/db/bulk/snapshot`, `GET /mssql/db/bulk/snapshot/{id}`,\n `GET /mssql/db/count`, `DELETE /mssql/db/{id}/recoverable_range/download`,\n `GET /mssql/db/{id}/compatible_instance`, `GET /mssql/instance/count`,\n `GET /mssql/db/{id}/restore_estimate`, `GET /mssql/db/{id}/restore_files`,\n `GET /mssql/db/{id}/snappable_id`, `GET /mssql/db/defaults`,\n `PATCH /mssql/db/defaults` and `GET /mssql/db/recoverable_range/download/{id}`\n endpoints. Use the v1 version when possible.\n ## Breaking changes:\n * Added new Boolean field `isLinkLocalIpv4Mode` to `AddNodesConfig` and\n `ReplaceNodeConfig`.\n * Changed the type for ReplicationSnapshotLag, which is used by /report/{id} GET\n and PATCH endpoints from integer to string.\n * Added new required field `objectStore` to DataSourceDownloadConfig used by\n `POST /report/data_source/download`.\n * Removed the `storageClass` field from the DataSourceDownloadConfig object used\n by the `POST /report/data_source/download` endpoint. The value was not used.\n * Removed endpoint `GET /mfa/rsa/server` and moved it to v1.\n * Removed endpoint `POST /mfa/rsa/server` and moved it to v1.\n * Removed endpoint `GET /mfa/rsa/server/{id}` and moved it to v1.\n * Removed endpoint `PATCH /mfa/rsa/server/{id}` and moved it to v1.\n * Removed endpoint `DELETE /mfa/rsa/server/{id}` and moved it to v1.\n * Removed endpoint `PUT /cluster/{id}/security/web_signed_cert`\n and moved it to v1.\n * Removed endpoint `DELETE /cluster/{id}/security/web_signed_cert`\n and moved it to v1\n * Removed endpoint `PUT /cluster/{id}/security/kmip/client` and added it\n to v1.\n * Removed endpoint `GET /cluster/{id}/security/kmip/client` and added it\n to v1.\n * Removed endpoint `GET /cluster/{id}/security/kmip/server` and added it\n to v1.\n * Removed endpoint `PUT /cluster/{id}/security/kmip/server` and added it\n to v1.\n * Removed endpoint `DELETE /cluster/{id}/security/kmip/server` and added\n it to v1.\n * Removed endpoint `POST /replication/global_pause`. To toggle replication\n pause between enabled and disabled, use\n `POST /v1/replication/location_pause/disable` and\n `POST /v1/replication/location_pause/enable` instead.\n * Removed `GET /replication/global_pause`. To retrieve replication pause\n status, use `GET /internal/replication/source` and\n `GET /internal/replication/source/{id}` instead.\n * Removed `GET /node_management/{id}/fetch_package` since it was never used.\n * Removed `GET /node_management/{id}/upgrade` since it was never used.\n * Removed `POST /node_management/{id}/fetch_package` since it was never used.\n * Removed `POST /node_management/{id}/upgrade` since it was never used.\n\n ## Feature additions/improvements:\n * Added new optional field `pubKey` to the GlobalManagerConnectionUpdate\n object and the GlobalManagerConnectionInfo object used by\n `GET /cluster/{id}/global_manager` and `PUT /cluster/{id}/global_manager`.\n * Added a new optional field `storageClass` to the `ArchivalLocationSummary`\n type.\n * Added optional field `StartMethod` to the following components: \n ChartSummary, TableSummary, ReportTableRequest, FilterSummary and\n RequestFilters.\n * Added new enum field `StackedReplicationComplianceCountByStatus` to the\n measure property in ChartSummary.\n * Added new enum fields `ReplicationInComplianceCount`,\n `ReplicationNonComplianceCount` to the following properties:\n measure property in ChartSummary, column property in TableSummary,\n and sortBy property in ReportTableRequest.\n * Added the endpoint `GET /vmware/config/datastore_freespace_threshold` to\n query the VMware datastore freespace threshold config.\n * Added the endpoint `PATCH /vmware/config/set_datastore_freespace_threshold`\n to update the VMware datastore freespace threshold config.\n * Added two new optional query parameters `offset` and `limit` to\n `GET /organization`.\n * Added two new optional query parameters `offset` and `limit` to\n `GET /user/{id}/organization`.\n * Modified `SnapshotCloudStorageTier`, enum adding values AzureArchive, Glacier,\n and GlacierDeepArchive.\n * Added the `lastValidationResult` field to the OracleDbDetail that the\n `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}` endpoints return.\n * Added `isValid` field to the OracleDbSnapshotSummary of\n OracleRecoverableRange that the `GET /oracle/db/\n {id}/recoverable_range` endpoint returns.\n * Added the `isRemoteGlobalBlackoutActive` field to the\n ReplicationSourceSummary object that the\n `GET /organization/{id}/replication/source` endpoint returns.\n * Added the `isRemoteGlobalBlackoutActive` field to the\n ReplicationSourceSummary object that the\n `GET /replication/source/{id}` endpoint returns.\n * Added the `isRemoteGlobalBlackoutActive` field to the\n ReplicationSourceSummary object that the\n `GET /replication/source` endpoint returns.\n * Added the `isReplicationTargetPauseEnabled` field to the\n ReplicationSourceSummary object that the\n `GET /organization/{id}/replication/source` endpoint returns.\n * Added the `isReplicationTargetPauseEnabled` field to the\n ReplicationSourceSummary object that the\n `GET /replication/source/{id}` endpoint returns.\n * Added the `isReplicationTargetPauseEnabled` field to the\n ReplicationSourceSummary object that the\n `GET /replication/source` endpoint returns.\n * Added new optional field `cloudRehydrationSpeed` to the\n ObjectStoreLocationSummary, ObjectStoreUpdateDefinition,\n PolarisAwsArchivalLocationSpec, and PolarisAzureArchivalLocationSpec\n objects to specify the rehydration speed to use when performing cloud\n rehydration on objects tiered cold storage.\n * Added new optional field earliestTimestamp to the `POST\n /polaris/export_info` endpoint to enable incremental MDS synchronization.\n * Added new values `RetentionSlaDomainName` , `ObjectType`, `SnapshotCount`,\n `AutoSnapshotCount` and `ManualSnapshotCount` to\n `UnmanagedObjectSortAttribute` field of the `GET /unmanaged_object` endpont.\n * Added new optional field `endpoint` to the ObjectStorageDetail\n object used by several Polaris APIs.\n * Added new optional field `accessKey` to the ObjectStorageConfig\n object used by several Polaris APIs.\n * Added new optional field `endpoint` to DataSourceDownloadConfig used by\n `POST /report/data_source/download`.\n * Added new field `slaClientConfig` to the `ManagedVolumeUpdate`\n object used by the `PATCH /managed_volume/{id}` endpoint to enable\n edits to the configuration of SLA Managed Volumes.\n * Added new field `shouldSkipPrechecks` to DecommissionNodesConfig used by\n `POST /cluster/{id}/decommission_nodes`.\n * Added new query parameter `managed_volume_type` to allow filtering\n managed volumes based on their type using the `GET /managed_volume`\n endpoint.\n * Added new query parameter `managed_volume_type` to allow filtering\n managed volume exports based on their source managed volume type\n using the `GET /managed_volume/snapshot/export` endpoint.\n * Added the new fields `mvType` and `slaClientConfig` to the\n `ManagedVolumeConfig` object. These fields are used with the\n `POST /managed_volume` endpoint to manage SLA Managed Volumes.\n * Added the new fields `mvType` and `slaManagedVolumeDetails` to the\n `ManagedVolumeSummary` object returned by the `GET /managed_volume`,\n `POST /managed_volume`, `GET /managed_volume/{id}` and\n `POST /managed_volume/{id}` endpoints.\n * Added new field `mvType` to the `ManagedVolumeSnapshotExportSummary`\n object returned by the `GET /managed_volume/snapshot/export` and\n `GET /managed_volume/snapshot/export/{id}` endpoints.\n * Added optional field `hostMountPoint` in the `ManagedVolumeChannelConfig`.\n `ManagedVolumeChannelConfig` is returned as part of\n `ManagedVolumeSnapshotExportSummary`, which is returned\n by the `GET /managed_volume/snapshot/export` and\n `GET /managed_volume/snapshot/export/{id}` endpoints.\n * Added `POST /managed_volume/{id}/snapshot` method to take an on\n demand snapshot for SLA Managed Volumes.\n * Added new field `isPrimary` to OracleDbSummary returned by\n `GET /oracle/db`.\n * Added new field `isPrimary` to OracleDbDetail returned by\n `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}`.\n * Added new field `isOracleHost` to HostDetail\n returned by `GET /host/{id}`.\n * Added optional isShareAutoDiscoveryAndAdditionEnabled in the\n NasBaseConfig and NasConfig.\n NasBaseConfig is returned as part of HostSummary, which is returned by the\n `Get /host/envoy` and `Get /host` endpoints. NasConfig is used by\n HostRegister and HostUpdate. The HostRegister field is used by the\n `Post /host/bulk` endpoint and the HostUpdate is field used by the\n `PATCH /host/bulk` endpoint.\n * Added new endpoint `POST /managed_volume/{id}/resize` to resize managed\n volume to a larger size.\n * Added ReplicationComplianceStatus as an optional field to the TableSummary\n which is used by /report/{id} GET and PATCH endpoints and to RequestFilters\n which is used by /report/data_source/table.\n * Added `PATCH /cluster/{id}/trial_edge` endpoint to extend the trial period.\n * Added new optional fields `extensionsLeft` and `daysLeft` to\n EdgeTrialStatus returned by `GET /cluster/{id}/trial_edge` and\n `PATCH /cluster/{id}/trial_edge`.\n * Added new endpoint `POST /managed_volume/snapshot/{id}/restore` to export a\n managed volume snapshot and mount it on a host.\n * Added new endpoints `PATCH /config/{component}/reset` to allow configs to\n be reset to DEFAULT state.\n * Added a new field `logRetentionTimeInHours` to the `MssqlDbDefaults`\n object returned by the `GET /mssql/db/defaults` and\n `PATCH /mssql/db/defaults` endpoints.\n * Added new optional field `logRetentionTimeInHours` to `MssqlDbDefaultsUpdate`\n object which is used by `PATCH /mssql/db/defaults`.\n * Added new optional field `unreadable` to `BrowseResponse` and\n `SnapshotSearchResponse`, which are used by `GET /browse` and\n `GET /search/snapshot_search` respectively.\n * Added MissedReplicationSnapshots as an optional field to the TableSummary\n which is used by /report/{id} GET and PATCH endpoints.\n * Added new optional field `pitRecoveryInfo` to `ChildSnappableFailoverInfo`\n object which is used by `PUT /polaris/failover/target/{id}/start`\n * Added ReplicationDataLag as an optional field to the TableSummary\n which is used by /report/{id} GET and PATCH endpoints.\n * Added UnreplicatedSnapshots as an optional field to the TableSummary\n which is used by /report/{id} GET and PATCH endpoints.\n * Added the field `networkAdapterType` to `VappVmNetworkConnection`.\n `VappVmNetworkConnection` is returned by the\n `GET /vcd/vapp/snapshot/{snapshot_id}/instant_recover/options` and\n `GET /vcd/vapp/snapshot/{snapshot_id}/export/options` endpoints and is\n used by the `POST /vcd/vapp/snapshot/{snapshot_id}/export` and\n `POST /vcd/vapp/snapshot/{snapshot_id}/instant_recover` endpoints.\n Also added `VcdVmSnapshotDetail`, which is returned by the\n `GET /vcd/vapp/snapshot/{id}` endpoint.\n * Added new endpoint `GET /report/template` to return details\n of a report template.\n * Added new endpoint `POST /report/{id}/send_email` to send an email of the report.\n ## Breaking changes:\n * Made field `restoreScriptSmbPath` optional in `VolumeGroupMountSummary`.\n Endpoints `/volume_group/snapshot/mount` and\n `/volume_group/snapshot/mount/{id}` are affected by this change.\n * Moved endpoints `GET /volume_group`, `GET /volume_group/{id}`,\n `PATCH /volume_group/{id}`, `GET /volume_group/{id}/snapshot`,\n `POST /volume_group/{id}/snapshot`, `GET /volume_group/snapshot/{id}`,\n `GET /volume_group/snapshot/mount`, and\n `GET /volume_group/snapshot/mount/{id}` from internal to v1.\n * Moved endpoint `GET /host/{id}/volume` from internal to v1.\n\n ### Changes to Internal API in Rubrik version 5.2.2\n ## Feature Additions/improvements:\n * Added new field `exposeAllLogs` to ExportOracleTablespaceConfig\n used by `POST /oracle/db/{id}/export/tablespace`.\n\n ### Changes to Internal API in Rubrik version 5.2.1\n ## Feature Additions/improvements:\n * Added new field `shouldBlockOnNegativeFailureTolerance` to\n DecommissionNodesConfig used by `POST /cluster/{id}/decommission_nodes`.\n\n ### Changes to Internal API in Rubrik version 5.2.0\n ## Deprecation:\n * Deprecating `GET /replication/global_pause`. Use\n `GET /internal/replication/source` and\n `GET /internal/replication/source/{id}` to retrieve replication\n pause status in CDM v5.3.\n * Deprecating `POST /replication/global_pause`. Use\n `POST /v1/replication/location_pause/disable` and\n `POST /v1/replication/location_pause/enable` to toggle replication\n pause in CDM v5.3.\n * Deprecating `slaId` field returned by `GET /vcd/vapp/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /vcd/vapp/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /oracle/db/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /oracle/db/\n {id}/recoverable_range`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /oracle/db/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /hyperv/vm/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /hyperv/vm/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /volume_group/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /volume_group/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n * Deprecating `slaId` field returned by `GET /storage/array_volume_group\n/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /vcd/vapp/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /host_fileset/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /host_fileset/share/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /app_blueprint/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /app_blueprint/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /managed_volume/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `POST /managed_volume/{id\n}/end_snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /managed_volume/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /aws/ec2_instance/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /aws/ec2_instance/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /nutanix/vm/{id}/snapshot`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /nutanix/vm/snapshot/{id}`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Deprecating `slaId` field returned by `GET /fileset/bulk`.\n See **snapshotRetentionInfo** to track retention for\n snapshots.\n* Added a new field `pendingSlaDomain` to `VirtualMachineDetail`\n object referred by `VappVmDetail` returned by\n `GET /vcd/vapp/{id}` and `PATCH /vcd/vapp/{id}`\n * Deprecated `POST /internal/vmware/vcenter/{id}/refresh_vm` endpoint. Use\n `POST /v1/vmware/vcenter/{id}/refresh_vm` instead to refresh a\n virtual machine by MOID.\n\n ## Breaking changes:\n* Rename the field configuredSlaDomainId in the OracleUpdate object to\n configuredSlaDomainIdDeprecated and modify the behavior so\n configuredSlaDomainIdDeprecated is only used to determine log backup\n frequency and not to set retention time.\n* Removed `GET /event/count_by_status` endpoint and it will be\n replaced by `GET /job_monitoring/summary_by_job_state`.\n* Removed `GET /event/count_by_job_type` endpoint and it will be\n replaced by `GET /job_monitoring/summary_by_job_type`.\n* Removed `GET /event_series` endpoint and it will be replaced by\n `GET /job_monitoring`.\n* Refactor `PUT /cluster/{id}/security/web_signed_cert` to accept\n certificate_id instead of X.509 certificate text. Also removed\n the `POST /cluster/{id}/security/web_csr` endpoint.\n * Refactor `GET /rsa-server`, `POST /rsa-server`, `GET /rsa-server/{id}`,\n and `PATCH /rsa-server/{id}` to take in a certificate ID instead of\n a certificate.\n * Changed definition of CloudInstanceUpdate by updating the enums ON/OFF\n to POWERSTATUS_ON/POWERSTATUS_OFF\n * Removed `GET /event_series/{status}/csv_link` endpoint to download CSV\n with job monitoring information. It has been replaced by the\n `GET /job_monitoring//csv_download_link` v1 endpoint.\n * Removed GET `/report/summary/physical_storage_time_series`. Use\n GET `/stats/total_physical_storage/time_series` instead.\n * Removed GET `/report/summary/average_local_growth_per_day`. Use\n GET `/stats/average_storage_growth_per_day` instead.\n * Removed POST `/job/instances/`. Use GET `/job/{job_id}/instances` instead.\n * Removed the POST `/cluster/{id}/reset` endpoint.\n * Removed GET `/user`. Use the internal POST `/principal_search`\n or the v1 GET `/principal` instead for querying any principals,\n including users.\n\n ## Feature additions/improvements:\n * Added the `GET /replication/global_pause` endpoint to return the current\n status of global replication pause. Added the `POST /replication/global_pause`.\n endpoint to toggle the replication target global pause jobs status. When\n global replication pause is enabled, all replication jobs on the local\n cluster are paused. When disabling global replication pause, optional\n parameter `shouldOnlyReplicateNewSnapshots` can be set to `true` to only\n replicate snapshots taken after disabling the pause. These endpoints must\n be used at the target cluster.\n * Added new field `parentSnapshotId` to AppBlueprintSnapshotSummary returned\n by `GET /app_blueprint/{id}/snapshot`.\n * Added new field `parentSnapshotId` to AppBlueprintSnapshotDetail returned\n by `GET /app_blueprint/snapshot/{id}`.\n * Added new field `parentSnapshotId` to AwsEc2InstanceSummary returned by\n `GET /aws/ec2_instance`.\n * Added new field `parentSnapshotId` to AwsEc2InstanceDetail returned by\n `GET /aws/ec2_instance/{id}`.\n * Added new field `parentSnapshotId` to AwsEc2InstanceDetail returned by\n `PATCH /aws/ec2_instance/{id}`.\n * Added new field `parentSnapshotId` to HypervVirtualMachineSnapshotSummary\n returned by `GET /hyperv/vm/{id}/snapshot`.\n * Added new field `parentSnapshotId` to HypervVirtualMachineSnapshotDetail\n returned by `GET /hyperv/vm/snapshot/{id}`.\n * Added new field `parentSnapshotId` to ManagedVolumeSnapshotSummary\n returned by `GET /managed_volume/{id}/snapshot`.\n * Added new field `parentSnapshotId` to ManagedVolumeSnapshotSummary\n returned by `POST /managed_volume/{id}/end_snapshot`.\n * Added new field `parentSnapshotId` to ManagedVolumeSnapshotDetail returned\n by `GET /managed_volume/snapshot/{id}`.\n * Added new field `parentSnapshotId` to NutanixVmSnapshotSummary returned by\n `GET /nutanix/vm/{id}/snapshot`.\n * Added new field `parentSnapshotId` to NutanixVmSnapshotDetail returned by\n `GET /nutanix/vm/snapshot/{id}`.\n * Added new field `parentSnapshotId` to OracleDbSnapshotSummary returned by\n `GET /oracle/db/{id}/snapshot`.\n * Added new field `parentSnapshotId` to OracleDbSnapshotDetail returned by\n `GET /oracle/db/snapshot/{id}`.\n * Added new field `parentSnapshotId` to StorageArrayVolumeGroupSnapshotSummary\n returned by `GET /storage/array_volume_group/{id}/snapshot`.\n * Added new field `parentSnapshotId` to StorageArrayVolumeGroupSnapshotDetail\n returned by `GET /storage/array_volume_group/snapshot/{id}`.\n * Added new field `parentSnapshotId` to VcdVappSnapshotSummary returned by\n `GET /vcd/vapp/{id}/snapshot`.\n * Added new field `parentSnapshotId` to VcdVappSnapshotDetail returned by\n `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `parentSnapshotId` to VolumeGroupSnapshotSummary returned by\n `GET /volume_group/{id}/snapshot`.\n * Added new field `parentSnapshotId` to VolumeGroupSnapshotDetail returned by\n `GET /volume_group/snapshot/{id}`.\n * Added new field `retentionSlaDomanId` to MssqlAvailabilityGroupSummary\n returned by `GET /mssql/availability_group`.\n * Added new field `retentionSlaDomanId` to MssqlAvailabilityGroupDetail\n returned by `GET /mssql/availability_group/{id}`.\n * Added new field `retentionSlaDomanId` to MssqlAvailabilityGroupDetail\n returned by `PATCH /mssql/availability_group/{id}`.\n * Added new field `retentionSlaDomainId` to UnmanagedObjectSummary\n returned by `GET /unmanaged_object`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `GET /managed_volume`.\n * Added new field `retentionSlaDomainId` to AppBlueprintDetail\n returned by `GET /app_blueprint/{id}`.\n * Added new field `retentionSlaDomainId` to AppBlueprintDetail\n returned by `PATCH /polaris/app_blueprint/{id}`.\n * Added new field `retentionSlaDomainId` to AppBlueprintDetail\n returned by `POST /polaris/app_blueprint`.\n * Added new field `retentionSlaDomainId` to AppBlueprintExportSnapshotJobConfig\n returned by `POST /polaris/app_blueprint/snapshot/{id}/export`.\n * Added new field `retentionSlaDomainId` to AppBlueprintInstantRecoveryJobConfig\n returned by `POST /polaris/app_blueprint/snapshot/{id}/instant_recover`.\n * Added new field `retentionSlaDomainId` to AppBlueprintMountSnapshotJobConfig\n returned by `POST /polaris/app_blueprint/snapshot/{id}/mount`.\n * Added new field `retentionSlaDomainId` to AppBlueprintSummary\n returned by `GET /app_blueprint`.\n * Added new field `retentionSlaDomainId` to AwsEc2InstanceDetail\n returned by `GET /aws/ec2_instance/{id}`.\n * Added new field `retentionSlaDomainId` to AwsEc2InstanceDetail\n returned by `PATCH /aws/ec2_instance/{id}`.\n * Added new field `retentionSlaDomainId` to AwsEc2InstanceSummary\n returned by `GET /aws/ec2_instance`.\n * Added new field `retentionSlaDomainId` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to HypervHierarchyObjectSummary\n returned by `GET /organization/{id}/hyperv`.\n * Added new field `retentionSlaDomainId` to HypervVirtualMachineDetail\n returned by `GET /hyperv/vm/{id}`.\n * Added new field `retentionSlaDomainId` to HypervVirtualMachineDetail\n returned by `PATCH /hyperv/vm/{id}`.\n * Added new field `retentionSlaDomainId` to HypervVirtualMachineSummary\n returned by `GET /hyperv/vm`.\n * Added new field `retentionSlaDomainId` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}/sla_conflicts`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `GET /managed_volume/{id}`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `GET /organization/{id}/managed_volume`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `PATCH /managed_volume/{id}`.\n * Added new field `retentionSlaDomainId` to ManagedVolumeSummary\n returned by `POST /managed_volume`.\n * Added new field `retentionSlaDomainId` to MountDetail\n returned by `GET /vmware/vm/snapshot/mount/{id}`.\n * Added new field `retentionSlaDomainId` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to NutanixHierarchyObjectSummary\n returned by `GET /organization/{id}/nutanix`.\n * Added new field `retentionSlaDomainId` to OracleDbDetail\n returned by `GET /oracle/db/{id}`.\n * Added new field `retentionSlaDomainId` to OracleDbDetail\n returned by `PATCH /oracle/db/{id}`.\n * Added new field `retentionSlaDomainId` to OracleDbSummary\n returned by `GET /oracle/db`.\n * Added new field `retentionSlaDomainId` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to OracleHierarchyObjectSummary\n returned by `GET /organization/{id}/oracle`.\n * Added new field `retentionSlaDomainId` to SlaConflictsSummary\n returned by `POST /hierarchy/bulk_sla_conflicts`.\n * Added new field `retentionSlaDomainId` to SnappableRecoverySpecDetails\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new field `retentionSlaDomainId` to SnappableRecoverySpec\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new field `retentionSlaDomainId` to Snappable\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new field `retentionSlaDomainId` to Snappable\n returned by `POST /stats/snappable_storage`.\n * Added new field `retentionSlaDomainId` to StorageArrayHierarchyObjectSummary\n returned by `GET /organization/{id}/storage/array`.\n * Added new field `retentionSlaDomainId` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to StorageArrayVolumeGroupDetail\n returned by `GET /storage/array_volume_group/{id}`.\n * Added new field `retentionSlaDomainId` to StorageArrayVolumeGroupDetail\n returned by `PATCH /storage/array_volume_group/{id}`.\n * Added new field `retentionSlaDomainId` to StorageArrayVolumeGroupDetail\n returned by `POST /storage/array_volume_group`.\n * Added new field `retentionSlaDomainId` to StorageArrayVolumeGroupSummary\n returned by `GET /storage/array_volume_group`.\n * Added new field `retentionSlaDomainId` to TriggerFailoverOnTargetDefinition\n returned by `PUT /polaris/failover/target/{id}/resume`.\n * Added new field `retentionSlaDomainId` to TriggerFailoverOnTargetDefinition\n returned by `PUT /polaris/failover/target/{id}/start`.\n * Added new field `retentionSlaDomainId` to UpsertSnappableRecoverySpecResponse\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new field `retentionSlaDomainId` to VcdHierarchyObjectSummary\n returned by `GET /organization/{id}/vcd`.\n * Added new field `retentionSlaDomainId` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/children`.\n * Added new field `retentionSlaDomainId` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/descendants`.\n * Added new field `retentionSlaDomainId` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}`.\n * Added new field `retentionSlaDomainId` to VcdVappDetail\n returned by `GET /vcd/vapp/{id}`.\n * Added new field `retentionSlaDomainId` to VcdVappDetail\n returned by `PATCH /vcd/vapp/{id}`.\n * Added new field `retentionSlaDomainId` to VcdVappSnapshotDetail\n returned by `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `retentionSlaDomainId` to VolumeGroupDetail\n returned by `GET /volume_group/{id}`.\n * Added new field `retentionSlaDomainId` to VolumeGroupDetail\n returned by `PATCH /volume_group/{id}`.\n * Added new field `retentionSlaDomainId` to VolumeGroupSummary\n returned by `GET /volume_group`.\n * Added new field `retentionSlaDomainId` to AwsHierarchyObjectSummary\n returned by `GET /organization/{id}/aws`.\n * Added new field `retentionSlaDomainId` to VmwareVmMountSummary\n returned by `GET /vmware/vm/snapshot/mount`.\n * Added new field `retentionSlaDomainId` to VcdVappSummary\n returned by `GET /vcd/vapp`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `GET /replication/target`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `POST /replication/target`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `GET /replication/target/{id}`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `GET /replication/target/{id}`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `PATCH /replication/target/{id}`.\n * Added `isReplicationTargetPauseEnabled` to ReplicationTargetSummary\n returned by `GET /organization/{id}/replication/target`.\n * Added new field `hasSnapshotsWithPolicy` to UnmanagedObjectSummary returned\n by GET `/unmanaged_object`\n * Added new field `slaLastUpdateTime` to AppBlueprintDetail\n returned by POST `/polaris/app_blueprint`.\n * Added new field `slaLastUpdateTime` to AppBlueprintDetail\n returned by `GET /app_blueprint/{id}`.\n * Added new field `slaLastUpdateTime` to AppBlueprintDetail\n returned by `PATCH /polaris/app_blueprint/{id}`.\n * Added new field `slaLastUpdateTime` to AppBlueprintExportSnapshotJobConfig\n returned by POST `/polaris/app_blueprint/snapshot/{id}/export`.\n * Added new field `slaLastUpdateTime` to AppBlueprintInstantRecoveryJobConfig\n returned by POST `/polaris/app_blueprint/snapshot/{id}/instant_recover`.\n * Added new field `slaLastUpdateTime` to AppBlueprintMountSnapshotJobConfig\n returned by POST `/polaris/app_blueprint/snapshot/{id}/mount`.\n * Added new field `slaLastUpdateTime` to AppBlueprintSummary\n returned by `GET /app_blueprint`.\n * Added new field `slaLastUpdateTime` to AwsAccountDetail\n returned by `PATCH /aws/account/dca/{id}`.\n * Added new field `slaLastUpdateTime` to AwsAccountDetail\n returned by `GET /aws/account/{id}`.\n * Added new field `slaLastUpdateTime` to AwsAccountDetail\n returned by `PATCH /aws/account/{id}`.\n * Added new field `slaLastUpdateTime` to AwsEc2InstanceDetail\n returned by `GET /aws/ec2_instance/{id}`.\n * Added new field `slaLastUpdateTime` to AwsEc2InstanceDetail\n returned by `PATCH /aws/ec2_instance/{id}`.\n * Added new field `slaLastUpdateTime` to FilesetDetail\n returned by POST `/fileset/bulk`.\n * Added new field `slaLastUpdateTime` to AwsEc2InstanceSummary\n returned by `GET /aws/ec2_instance`.\n * Added new field `slaLastUpdateTime` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to AwsHierarchyObjectSummary\n returned by `GET /organization/{id}/aws`.\n * Added new field `slaLastUpdateTime` to DataCenterDetail\n returned by `GET /vmware/data_center/{id}`.\n * Added new field `slaLastUpdateTime` to DataCenterSummary\n returned by `GET /vmware/data_center`.\n * Added new field `slaLastUpdateTime` to DataStoreDetail\n returned by `GET /vmware/datastore/{id}`.\n * Added new field `slaLastUpdateTime` to FolderDetail\n returned by `GET /folder/host/{datacenter_id}`.\n * Added new field `slaLastUpdateTime` to FolderDetail\n returned by `GET /folder/vm/{datacenter_id}`.\n * Added new field `slaLastUpdateTime` to FolderDetail\n returned by `GET /folder/{id}`.\n * Added new field `slaLastUpdateTime` to HostFilesetDetail\n returned by `GET /host_fileset/{id}`.\n * Added new field `slaLastUpdateTime` to HostFilesetShareDetail\n returned by `GET /host_fileset/share/{id}`.\n * Added new field `slaLastUpdateTime` to HostFilesetShareSummary\n returned by `GET /host_fileset/share`.\n * Added new field `slaLastUpdateTime` to HostFilesetSummary\n returned by `GET /host_fileset`.\n * Added new field `slaLastUpdateTime` to HypervClusterDetail\n returned by `GET /hyperv/cluster/{id}`.\n * Added new field `slaLastUpdateTime` to HypervClusterDetail\n returned by `PATCH /hyperv/cluster/{id}`.\n * Added new field `slaLastUpdateTime` to HypervClusterSummary\n returned by `GET /hyperv/cluster`.\n * Added new field `slaLastUpdateTime` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to HypervHierarchyObjectSummary\n returned by `GET /organization/{id}/hyperv`.\n * Added new field `slaLastUpdateTime` to HypervHostDetail\n returned by `GET /hyperv/host/{id}`.\n * Added new field `slaLastUpdateTime` to HypervHostDetail\n returned by `PATCH /hyperv/host/{id}`.\n * Added new field `slaLastUpdateTime` to HypervHostSummary\n returned by `GET /hyperv/host`.\n * Added new field `slaLastUpdateTime` to HypervScvmmDetail\n returned by `GET /hyperv/scvmm/{id}`.\n * Added new field `slaLastUpdateTime` to HypervScvmmDetail\n returned by `PATCH /hyperv/scvmm/{id}`.\n * Added new field `slaLastUpdateTime` to HypervScvmmSummary\n returned by `GET /hyperv/scvmm`.\n * Added new field `slaLastUpdateTime` to HypervVirtualMachineDetail\n returned by `GET /hyperv/vm/{id}`.\n * Added new field `slaLastUpdateTime` to HypervVirtualMachineDetail\n returned by `PATCH /hyperv/vm/{id}`.\n * Added new field `slaLastUpdateTime` to HypervVirtualMachineSummary\n returned by `GET /hyperv/vm`.\n * Added new field `slaLastUpdateTime` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}/sla_conflicts`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by `GET /managed_volume`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by POST `/managed_volume`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by `GET /managed_volume/{id}`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by `PATCH /managed_volume/{id}`.\n * Added new field `slaLastUpdateTime` to ManagedVolumeSummary\n returned by `GET /organization/{id}/managed_volume`.\n * Added new field `slaLastUpdateTime` to MountDetail\n returned by `GET /vmware/vm/snapshot/mount/{id}`.\n * Added new field `slaLastUpdateTime` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to NutanixHierarchyObjectSummary\n returned by `GET /organization/{id}/nutanix`.\n * Added new field `slaLastUpdateTime` to OracleDbDetail\n returned by `GET /oracle/db/{id}`.\n * Added new field `slaLastUpdateTime` to OracleDbDetail\n returned by `PATCH /oracle/db/{id}`.\n * Added new field `slaLastUpdateTime` to OracleDbSummary\n returned by `GET /oracle/db`.\n * Added new field `slaLastUpdateTime` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to OracleHierarchyObjectSummary\n returned by `GET /organization/{id}/oracle`.\n * Added new field `slaLastUpdateTime` to OracleHostDetail\n returned by `GET /oracle/host/{id}`.\n * Added new field `slaLastUpdateTime` to OracleHostDetail\n returned by `PATCH /oracle/host/{id}`.\n * Added new field `slaLastUpdateTime` to OracleHostSummary\n returned by `GET /oracle/host`.\n * Added new field `slaLastUpdateTime` to OracleRacDetail\n returned by `GET /oracle/rac/{id}`.\n * Added new field `slaLastUpdateTime` to OracleRacDetail\n returned by `PATCH /oracle/rac/{id}`.\n * Added new field `slaLastUpdateTime` to OracleRacSummary\n returned by `GET /oracle/rac`.\n * Added new field `slaLastUpdateTime` to Snappable\n returned by POST `/polaris/failover/recovery_spec/upsert`.\n * Added new field `slaLastUpdateTime` to SnappableRecoverySpec\n returned by POST `/polaris/failover/recovery_spec/upsert`.\n * Added new field `slaLastUpdateTime` to SnappableRecoverySpecDetails\n returned by POST `/polaris/failover/recovery_spec/upsert`.\n * Added new field `slaLastUpdateTime` to StorageArrayHierarchyObjectSummary\n returned by `GET /organization/{id}/storage/array`.\n * Added new field `slaLastUpdateTime` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to StorageArrayHierarchyObjectSummary\n returned by `GET /storage/array/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to StorageArrayVolumeGroupDetail\n returned by POST `/storage/array_volume_group`.\n * Added new field `slaLastUpdateTime` to StorageArrayVolumeGroupDetail\n returned by `GET /storage/array_volume_group/{id}`.\n * Added new field `slaLastUpdateTime` to StorageArrayVolumeGroupDetail\n returned by `PATCH /storage/array_volume_group/{id}`.\n * Added new field `slaLastUpdateTime` to StorageArrayVolumeGroupSummary\n returned by `GET /storage/array_volume_group`.\n * Added new field `slaLastUpdateTime` to VcdClusterDetail\n returned by `GET /vcd/cluster/{id}`.\n * Added new field `slaLastUpdateTime` to VcdClusterDetail\n returned by `PATCH /vcd/cluster/{id}`.\n * Added new field `slaLastUpdateTime` to VcdClusterSummary\n returned by `GET /vcd/cluster`.\n * Added new field `slaLastUpdateTime` to VcdHierarchyObjectSummary\n returned by `GET /organization/{id}/vcd`.\n * Added new field `slaLastUpdateTime` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}`.\n * Added new field `slaLastUpdateTime` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/children`.\n * Added new field `slaLastUpdateTime` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/descendants`.\n * Added new field `slaLastUpdateTime` to VcdVappDetail\n returned by `GET /vcd/vapp/{id}`.\n * Added new field `slaLastUpdateTime` to VcdVappDetail\n returned by `PATCH /vcd/vapp/{id}`.\n * Added new field `slaLastUpdateTime` to VcdVappSnapshotDetail\n returned by `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `slaLastUpdateTime` to VcdVappSummary\n returned by `GET /vcd/vapp`.\n * Added new field `slaLastUpdateTime` to VmwareVmMountSummary\n returned by `GET /vmware/vm/snapshot/mount`.\n * Added new field `slaLastUpdateTime` to VolumeGroupDetail\n returned by `GET /volume_group/{id}`.\n * Added new field `slaLastUpdateTime` to VolumeGroupDetail\n returned by `PATCH /volume_group/{id}`.\n * Added new field `slaLastUpdateTime` to VolumeGroupSummary\n returned by `GET /volume_group`.\n * Added new field `slaLastUpdateTime` to VsphereCategory\n returned by `GET /vmware/vcenter/{id}/tag_category`.\n * Added new field `slaLastUpdateTime` to VsphereCategory\n returned by `GET /vmware/vcenter/tag_category/{tag_category_id}`.\n * Added new field `slaLastUpdateTime` to VsphereTag\n returned by `GET /vmware/vcenter/{id}/tag`.\n * Added new field `slaLastUpdateTime` to VsphereTag\n returned by `GET /vmware/vcenter/tag/{tag_id}`.\n * Added new Field `configuredSlaDomainType` to AppBlueprintDetail returned by\n `POST /polaris/app_blueprint`.\n * Added new Field `configuredSlaDomainType` to AppBlueprintDetail returned by\n `GET /app_blueprint/{id}`.\n * Added new Field `configuredSlaDomainType` to AppBlueprintDetail returned by\n `PATCH /polaris/app_blueprint/{id}`.\n * Added new Field `configuredSlaDomainType` to\n AppBlueprintExportSnapshotJobConfig returned by\n `POST /polaris/app_blueprint/snapshot/{id}/export`.\n * Added new Field `configuredSlaDomainType` to\n AppBlueprintInstantRecoveryJobConfig returned by\n `POST /polaris/app_blueprint/snapshot/{id}/instant_recover`.\n * Added new Field `configuredSlaDomainType` to\n AppBlueprintMountSnapshotJobConfig returned by\n `POST /polaris/app_blueprint/snapshot/{id}/mount`.\n * Added new Field `configuredSlaDomainType` to AppBlueprintSummary returned by\n `GET /app_blueprint`.\n * Added new Field `configuredSlaDomainType` to AwsAccountDetail returned by\n `PATCH /aws/account/dca/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsAccountDetail returned by\n `GET /aws/account/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsAccountDetail returned by\n `PATCH /aws/account/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsEc2InstanceDetail returned\n by `GET /aws/ec2_instance/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsEc2InstanceDetail returned\n by `PATCH /aws/ec2_instance/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsEc2InstanceSummary returned\n by `GET /aws/ec2_instance`.\n * Added new Field `configuredSlaDomainType` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to AwsHierarchyObjectSummary\n returned by `GET /aws/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to AwsHierarchyObjectSummary\n returned by `GET /organization/{id}/aws`.\n * Added new Field `configuredSlaDomainType` to DataCenterDetail returned by\n `GET /vmware/data_center/{id}`.\n * Added new Field `configuredSlaDomainType` to DataCenterSummary returned by\n `GET /vmware/data_center`.\n * Added new Field `configuredSlaDomainType` to DataStoreDetail returned by\n `GET /vmware/datastore/{id}`.\n * Added new Field `configuredSlaDomainType` to FilesetDetail returned by\n `POST /fileset/bulk`.\n * Added new Field `configuredSlaDomainType` to FolderDetail returned by\n `GET /folder/host/{datacenter_id}`.\n * Added new Field `configuredSlaDomainType` to FolderDetail returned by\n `GET /folder/vm/{datacenter_id}`.\n * Added new Field `configuredSlaDomainType` to FolderDetail returned by\n `GET /folder/{id}`.\n * Added new Field `configuredSlaDomainType` to HostFilesetDetail returned by\n `GET /host_fileset/{id}`.\n * Added new Field `configuredSlaDomainType` to HostFilesetShareDetail returned\n by `GET /host_fileset/share/{id}`.\n * Added new Field `configuredSlaDomainType` to HostFilesetShareSummary\n returned by `GET /host_fileset/share`.\n * Added new Field `configuredSlaDomainType` to HostFilesetSummary returned by\n `GET /host_fileset`.\n * Added new Field `configuredSlaDomainType` to HypervClusterDetail returned by\n `GET /hyperv/cluster/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervClusterDetail returned by\n `PATCH /hyperv/cluster/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervClusterSummary returned\n by `GET /hyperv/cluster`.\n * Added new Field `configuredSlaDomainType` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to HypervHierarchyObjectSummary\n returned by `GET /hyperv/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to HypervHierarchyObjectSummary\n returned by `GET /organization/{id}/hyperv`.\n * Added new Field `configuredSlaDomainType` to HypervHostDetail returned by\n `GET /hyperv/host/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervHostDetail returned by\n `PATCH /hyperv/host/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervHostSummary returned by\n `GET /hyperv/host`.\n * Added new Field `configuredSlaDomainType` to HypervScvmmDetail returned by\n `GET /hyperv/scvmm/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervScvmmDetail returned by\n `PATCH /hyperv/scvmm/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervScvmmSummary returned by\n `GET /hyperv/scvmm`.\n * Added new Field `configuredSlaDomainType` to HypervVirtualMachineDetail\n returned by `GET /hyperv/vm/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervVirtualMachineDetail\n returned by `PATCH /hyperv/vm/{id}`.\n * Added new Field `configuredSlaDomainType` to HypervVirtualMachineSummary\n returned by `GET /hyperv/vm`.\n * Added new Field `configuredSlaDomainType` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to ManagedHierarchyObjectSummary\n returned by `GET /hierarchy/{id}/sla_conflicts`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `GET /managed_volume`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `POST /managed_volume`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `GET /managed_volume/{id}`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `PATCH /managed_volume/{id}`.\n * Added new Field `configuredSlaDomainType` to ManagedVolumeSummary returned\n by `GET /organization/{id}/managed_volume`.\n * Added new Field `configuredSlaDomainType` to MountDetail returned by\n `GET /vmware/vm/snapshot/mount/{id}`.\n * Added new Field `configuredSlaDomainType` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to NutanixHierarchyObjectSummary\n returned by `GET /nutanix/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to NutanixHierarchyObjectSummary\n returned by `GET /organization/{id}/nutanix`.\n * Added new Field `configuredSlaDomainType` to OracleDbDetail returned by\n `GET /oracle/db/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleDbDetail returned by\n `PATCH /oracle/db/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleDbSummary returned by\n `GET /oracle/db`.\n * Added new Field `configuredSlaDomainType` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to OracleHierarchyObjectSummary\n returned by `GET /oracle/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to OracleHierarchyObjectSummary\n returned by `GET /organization/{id}/oracle`.\n * Added new Field `configuredSlaDomainType` to OracleHostDetail returned by\n `GET /oracle/host/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleHostDetail returned by\n `PATCH /oracle/host/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleHostSummary returned by\n `GET /oracle/host`.\n * Added new Field `configuredSlaDomainType` to OracleRacDetail returned by\n `GET /oracle/rac/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleRacDetail returned by\n `PATCH /oracle/rac/{id}`.\n * Added new Field `configuredSlaDomainType` to OracleRacSummary returned by\n `GET /oracle/rac`.\n * Added new Field `configuredSlaDomainType` to SlaConflictsSummary returned by\n `POST /hierarchy/bulk_sla_conflicts`.\n * Added new Field `configuredSlaDomainType` to Snappable returned by\n `POST /polaris/failover/recovery_spec/upsert`.\n * Added new Field `configuredSlaDomainType` to SnappableRecoverySpec returned\n by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new Field `configuredSlaDomainType` to SnappableRecoverySpecDetails\n returned by `POST /polaris/failover/recovery_spec/upsert`.\n * Added new Field `configuredSlaDomainType` to\n StorageArrayHierarchyObjectSummary returned by\n `GET /organization/{id}/storage/array`.\n * Added new Field `configuredSlaDomainType` to\n StorageArrayHierarchyObjectSummary returned by\n `GET /storage/array/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to\n StorageArrayHierarchyObjectSummary returned by\n `GET /storage/array/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to\n StorageArrayHierarchyObjectSummary returned by\n `GET /storage/array/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to StorageArrayVolumeGroupDetail\n returned by `POST /storage/array_volume_group`.\n * Added new Field `configuredSlaDomainType` to StorageArrayVolumeGroupDetail\n returned by `GET /storage/array_volume_group/{id}`.\n * Added new Field `configuredSlaDomainType` to StorageArrayVolumeGroupDetail\n returned by `PATCH /storage/array_volume_group/{id}`.\n * Added new Field `configuredSlaDomainType` to StorageArrayVolumeGroupSummary\n returned by `GET /storage/array_volume_group`.\n * Added new Field `configuredSlaDomainType` to\n TriggerFailoverOnTargetDefinition returned by\n `PUT /polaris/failover/target/{id}/start`.\n * Added new Field `configuredSlaDomainType` to\n TriggerFailoverOnTargetDefinition returned by\n `PUT /polaris/failover/target/{id}/resume`.\n * Added new Field `configuredSlaDomainType` to\n UnmanagedObjectSummary returned by `GET /unmanaged_object`.\n * Added new Field `configuredSlaDomainType` to\n UpsertSnappableRecoverySpecResponse returned by\n `POST /polaris/failover/recovery_spec/upsert`.\n * Added new Field `configuredSlaDomainType` to VcdClusterDetail returned by\n `GET /vcd/cluster/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdClusterDetail returned by\n `PATCH /vcd/cluster/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdClusterSummary returned by\n `GET /vcd/cluster`.\n * Added new Field `configuredSlaDomainType` to VcdHierarchyObjectSummary\n returned by `GET /organization/{id}/vcd`.\n * Added new Field `configuredSlaDomainType` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/children`.\n * Added new Field `configuredSlaDomainType` to VcdHierarchyObjectSummary\n returned by `GET /vcd/hierarchy/{id}/descendants`.\n * Added new Field `configuredSlaDomainType` to VcdVappDetail returned by\n `GET /vcd/vapp/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdVappDetail returned by\n `PATCH /vcd/vapp/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdVappSnapshotDetail returned\n by `GET /vcd/vapp/snapshot/{id}`.\n * Added new Field `configuredSlaDomainType` to VcdVappSummary returned by\n `GET /vcd/vapp`.\n * Added new Field `configuredSlaDomainType` to VmwareVmMountSummary returned\n by `GET /vmware/vm/snapshot/mount`.\n * Added new Field `configuredSlaDomainType` to VolumeGroupDetail returned by\n `GET /volume_group/{id}`.\n * Added new Field `configuredSlaDomainType` to VolumeGroupDetail returned by\n `PATCH /volume_group/{id}`.\n * Added new Field `configuredSlaDomainType` to VolumeGroupSummary returned by\n `GET /volume_group`.\n * Added new Field `configuredSlaDomainType` to VsphereCategory returned by\n `GET /vmware/vcenter/{id}/tag_category`.\n * Added new Field `configuredSlaDomainType` to VsphereCategory returned by\n `GET /vmware/vcenter/tag_category/{tag_category_id}`.\n * Added new Field `configuredSlaDomainType` to VsphereTag returned by\n `GET /vmware/vcenter/{id}/tag`.\n * Added new Field `configuredSlaDomainType` to VsphereTag returned by\n `GET /vmware/vcenter/tag/{tag_id}`.\n * Added a new optional query parameter `name` to\n `GET /user/{id}/organization`.\n * Added new field `hostLogRetentionHours` to OracleDbSummary returned by\n `GET /oracle/db`.\n * Added new field `isCustomRetentionApplied` to AppBlueprintSnapshotSummary\n returned by `GET /app_blueprint/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to AppBlueprintSnapshotDetail\n returned by `GET /app_blueprint/snapshot/{id}` .\n * Added new field `isCustomRetentionApplied` to AwsEc2InstanceSummary returned\n by `GET /aws/ec2_instance`.\n * Added new field `isCustomRetentionApplied` to AwsEc2InstanceDetail returned\n by `GET /aws/ec2_instance/{id}`.\n * Added new field `isCustomRetentionApplied` to AwsEc2InstanceDetail returned\n by `PATCH /aws/ec2_instance/{id}`.\n * Added new field `isCustomRetentionApplied` to\n HypervVirtualMachineSnapshotSummary returned by\n `GET /hyperv/vm/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to\n HypervVirtualMachineSnapshotDetail returned by\n `GET /hyperv/vm/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to ManagedVolumeSnapshotSummary\n returned by `GET /managed_volume/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to ManagedVolumeSnapshotSummary\n returned by `POST /managed_volume/{id}/end_snapshot`.\n * Added new field `isCustomRetentionApplied` to ManagedVolumeSnapshotDetail\n returned by `GET /managed_volume/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to NutanixVmSnapshotSummary\n returned by `GET /nutanix/vm/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to NutanixVmSnapshotDetail\n returned by `GET /nutanix/vm/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to OracleDbSnapshotSummary\n returned by `GET /oracle/db/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to OracleDbSnapshotDetail returned\n by `GET /oracle/db/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to\n StorageArrayVolumeGroupSnapshotSummary returned by\n `GET /storage/array_volume_group/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to\n StorageArrayVolumeGroupSnapshotDetail returned by\n `GET /storage/array_volume_group/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to VcdVappSnapshotSummary returned\n by `GET /vcd/vapp/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to VcdVappSnapshotDetail returned\n by `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `isCustomRetentionApplied` to VolumeGroupSnapshotSummary\n returned by `GET /volume_group/{id}/snapshot`.\n * Added new field `isCustomRetentionApplied` to VolumeGroupSnapshotDetail\n returned by `GET /volume_group/snapshot/{id}`.\n * Added optional field `isQueuedSnapshot` to the response of\n GET `/managed_volume/{id}/snapshot`, GET `/managed_volume/snapshot/{id}`.\n and POST `/managed_volume/{id}/end_snapshot`.\n The field specifies if ManagedVolume snapshots are in queue to be stored\n as patch file.\n * Added new field `securityLevel` to `SnmpTrapReceiverConfig` object as\n optional input parameter for SNMPv3, which is used in\n `PATCH /cluster/{id}/snmp_configuration` and\n `GET /cluster/{id}/snmp_configuration`.\n * Added new field `advancedRecoveryConfigBase64` to `ExportOracleDbConfig`.\n and `MountOracleDbConfig` objects as optional input parameter\n during Oracle recovery.\n * Added new optional field `isRemote` to UnmanagedObjectSummary object, which\n is returned from a `GET /unmanaged_object` call.\n * Added new field `hostLogRetentionHours` to OracleRacDetail returned by\n `GET /oracle/rac/{id}` and `PATCH /oracle/rac/{id}`.\n * Added new field `hostLogRetentionHours` to OracleHostDetail returned by\n `GET /oracle/host/{id}` and `PATCH /oracle/host/{id}`.\n * Added new field `hostLogRetentionHours` to OracleDbDetail returned by\n `GET /oracle/db/{id}` and `PATCH /oracle/db/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AppBlueprintSnapshotSummary returned\n by `GET /app_blueprint/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AppBlueprintSnapshotDetail returned by\n `GET /app_blueprint/snapshot/{id}` .\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AwsEc2InstanceSummary returned by\n `GET /aws/ec2_instance`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AwsEc2InstanceDetail returned by\n `GET /aws/ec2_instance/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of AwsEc2InstanceDetail returned by\n `PATCH /aws/ec2_instance/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of HypervVirtualMachineSnapshotSummary\n returned by `GET /hyperv/vm/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of HypervVirtualMachineSnapshotDetail\n returned by `GET /hyperv/vm/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of ManagedVolumeSnapshotSummary returned\n by `GET /managed_volume/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of ManagedVolumeSnapshotSummary returned by\n `POST /managed_volume/{id}/end_snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of ManagedVolumeSnapshotDetail returned\n by `GET /managed_volume/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of NutanixVmSnapshotSummary returned by\n `GET /nutanix/vm/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of NutanixVmSnapshotDetail returned by\n `GET /nutanix/vm/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of OracleDbSnapshotSummary returned by\n `GET /oracle/db/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of OracleDbSnapshotDetail returned by\n `GET /oracle/db/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of StorageArrayVolumeGroupSnapshotSummary\n returned by `GET /storage/array_volume_group/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of StorageArrayVolumeGroupSnapshotDetail\n returned by `GET /storage/array_volume_group/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of VcdVappSnapshotSummary returned by\n `GET /vcd/vapp/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of VcdVappSnapshotDetail returned by\n `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of VolumeGroupSnapshotSummary returned by\n `GET /volume_group/{id}/snapshot`.\n * Added new field `snapshotFrequency` to `snapshotLocationRetentionInfo` field\n of `SnapshotRetentionInfo` field of VolumeGroupSnapshotDetail returned by\n `GET /volume_group/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to AppBlueprintSnapshotSummary\n returned by `GET /app_blueprint/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to AppBlueprintSnapshotDetail\n returned by `GET /app_blueprint/snapshot/{id}` .\n * Added new field `SnapshotRetentionInfo` to AwsEc2InstanceSummary returned\n by `GET /aws/ec2_instance`.\n * Added new field `SnapshotRetentionInfo` to AwsEc2InstanceDetail returned\n by `GET /aws/ec2_instance/{id}`.\n * Added new field `SnapshotRetentionInfo` to AwsEc2InstanceDetail returned\n by `PATCH /aws/ec2_instance/{id}`.\n * Added new field `SnapshotRetentionInfo` to\n HypervVirtualMachineSnapshotSummary returned by\n `GET /hyperv/vm/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to\n HypervVirtualMachineSnapshotDetail returned by\n `GET /hyperv/vm/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to ManagedVolumeSnapshotSummary\n returned by `GET /managed_volume/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to ManagedVolumeSnapshotSummary\n returned by `POST /managed_volume/{id}/end_snapshot`.\n * Added new field `SnapshotRetentionInfo` to ManagedVolumeSnapshotDetail\n returned by `GET /managed_volume/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to NutanixVmSnapshotSummary\n returned by `GET /nutanix/vm/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to NutanixVmSnapshotDetail\n returned by `GET /nutanix/vm/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to OracleDbSnapshotSummary\n returned by `GET /oracle/db/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to OracleDbSnapshotDetail returned\n by `GET /oracle/db/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to\n StorageArrayVolumeGroupSnapshotSummary returned by\n `GET /storage/array_volume_group/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to\n StorageArrayVolumeGroupSnapshotDetail returned by\n `GET /storage/array_volume_group/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to VcdVappSnapshotSummary returned\n by `GET /vcd/vapp/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to VcdVappSnapshotDetail returned\n by `GET /vcd/vapp/snapshot/{id}`.\n * Added new field `SnapshotRetentionInfo` to VolumeGroupSnapshotSummary\n returned by `GET /volume_group/{id}/snapshot`.\n * Added new field `SnapshotRetentionInfo` to VolumeGroupSnapshotDetail\n returned by `GET /volume_group/snapshot/{id}`.\n * Added optional field `networkInterface` to `NetworkThrottleUpdate`. The\n field allows users to specify non standard network interfaces. This applies\n to the `PATCH /network_throttle/{id}` endpoint.\n * Added mandatory field `networkInterface` to `NetworkThrottleSummary`.\n This applies to the endpoints `GET /network_throttle` and\n `GET /network_throttle/{id}`.\n * Added endpoint `POST /cluster/{id}/manual_discover`, which allows\n the customer to manually input data that would be learned using\n mDNS discovery. Returns same output as discover.\n * `PATCH /cluster/{id}/snmp_configuration` will now use\n `SnmpConfigurationPatch` as a parameter.\n * Added optional field `user` to `SnmpTrapReceiverConfig`. The field\n specifies which user to use for SNMPv3 traps.\n * Added optional field `users` to `SnmpConfiguration`. The field contains\n usernames of users configured for SNMPv3.\n * Added two new models `SnmpUserConfig` to store user credentials and\n `SnmpConfigurationPatch`.\n * Added new endpoint `POST /role/authorization_query` to get authorizations\n granted to roles.\n * Added new endpoint `GET /role/{id}/authorization` to get authorizations\n granted to a role.\n * Added new endpoint `POST /role/{id}/authorization` to grant authorizations\n to a role.\n * Added new endpoint `POST /role/{id}/authorization/bulk_revoke` to revoke\n authorizations from a role.\n * Added optional field `recoveryInfo` to UnmanagedObjectSummary.\n * Added optional field `isRetentionLocked` to SlaInfo.\n The parameter indicates that the SLA Domain associated with the job is a\n Retention Lock SLA Domain.\n * Added optional field `legalHoldDownloadConfig` to\n `FilesetDownloadFilesJobConfig`,`HypervDownloadFileJobConfig`,\n `DownloadFilesJobConfig`,`ManagedVolumeDownloadFileJobConfig`,\n `NutanixDownloadFilesJobConfig`,`StorageArrayDownloadFilesJobConfig`,\n `VolumeGroupDownloadFilesJobConfig`.This is an optional argument\n containing a Boolean parameter to depict if the download is being\n triggered for Legal Hold use case. This change applies to\n /fileset/snapshot/{id}/download_files,\n /hyperv/vm/snapshot/{id}/download_file,\n /vmware/vm/snapshot/{id}/download_files,\n /managed_volume/snapshot/{id}/download_file,\n /nutanix/vm/snapshot/{id}/download_files,\n /storage/array_volume_group/snapshot/{id}/download_files and\n /volume_group/snapshot/{id}/download_files endpoints.\n * Added optional field isPlacedOnLegalHold to BaseSnapshotSummary.\n The Boolean parameter specifies whether the snapshot is placed under a\n Legal Hold.\n * Added new endpoint `GET /ods_configuration`.\n Returns the current configuration of on-demand snapshot handling.\n * Added new endpoint `PUT /ods_configuration`.\n Update the configuration of on-demand snapshot handling.\n * Added two new models `OdsConfigurationSummary`, `OdsPolicyOnPause` and a new\n enum `SchedulingType`.\n * Added `odsPolicyOnPause` field in `OdsConfigurationSummary` to include the\n policy followed by the on-demand snapshots, during an effective pause.\n * Added new enum field `schedulingType` in `OdsPolicyOnPause` to support\n deferring the on-demand snapshots during an effective pause.\n * Added optional query parameter `show_snapshots_legal_hold_status` to\n `GET /archive/location` endpoint, indicating if `isLegalHoldSnapshotPresent`.\n field should be populated in response.\n * Added storage array volume group asynchronous request status endpoint\n `GET /storage/array_volume_group/request/{id}`. Request statuses for\n storage array volume groups which previously used\n `/storage/array/request/{id}` must now use this new endpoint.\n * Added forceFull parameter to the properties of patch volume group object\n to permit forcing a full snapshot for a specified volume group.\n * Added `isDcaAccountInstance` field to `AwsEc2InstanceSummary` to indicate\n whether the EC2 instance belongs to a DCA account. This impacts the endpoints\n `GET /aws/ec2_instance` and `GET /aws/ec2_instance/{id}`.\n * Added `encryptionKeyId` as an optional field in CreateCloudInstanceRequest\n definition used in the on-demand API conversion API `/cloud_on/aws/instance`.\n to support KMS encryption for CloudOn conversion in AWS.\n * Added new endpoint `GET /job/{id}/child_job_instance`.\n Returns the child job instances (if any) spawned by the given parent job\n instance. This endpoint requires a support token to access.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include the `isConsolidationEnabled` field, to indicate\n if consolidation is enabled for the given archival location.\n * Changed `encryptionPassword` parameter to optional in\n `NfsLocationCreationDefinition` to support creating NFS archival location\n without encryption via `POST /archive/nfs`.\n * Added an optional parameter `disabledEncryption` to\n `NfsLocationCreationDefinition` with a default value of false, to enable or\n disable encryption via `POST /archive/nfs`.\n * Added a new model `ValidationResponse` and REST API endpoints\n `/cloud_on/validate/instantiate_on_cloud` and\n `/cloud_on/validate/cloud_image_conversion` for validation of cloud\n conversion.\n * Added `sortBy` and `sortOrder` parameters to `GET /hyperv/vm/snapshot/mount`.\n to allow sorting of Hyper-V mounts.\n Added the enum `HypervVirtualMachineMountListSortAttribute`, defining which\n properties of Hyper-V mounts are sortable.\n * Added an optional field `shouldApplyToExistingSnapshots` in\n `SlaDomainAssignmentInfo` to apply the new SLA configuration to existing\n snapshots of protected objects.\n * Added a new optional field `isOracleHost` to `HostRegister` in\n `POST /host/bulk` and `HostUpdate` in `PATCH /host/bulk` to indicate if we\n should discover Oracle information during registration and host refresh.\n * Added a new model `NutanixVirtualDiskSummary` that is returned by\n `GET /nutanix/vm/{id}` to include the disks information for a Nutanix\n virtual machine.\n * Added mandatory field `pendingSnapshot` to `SystemStorageStats`, which is\n returned by `GET /stats/system_storage`.\n * Added optional isIsilonChangelistEnabled in the NasBaseConfig and NasConfig.\n NasBaseConfig is returned as part of HostSummary, which is returned by the\n `Get /host/envoy` and `Get /host` endpoints. NasConfig is used by\n HostRegister and HostUpdate. The HostRegister is used by the\n `Post /host/bulk` endpoint and the HostUpdate is used by the\n `PATCH /host/bulk` endpoint.\n * Added a new model `HostShareParameters`. This model has two fields,\n isNetAppSnapDiffEnabled and isIsilonChangelistEnabled. The\n isNetAppSnapDiffEnabled is a Boolean value that specifies whether the\n SnapDiff feature of NetApp NAS is used to back up the NAS share. The\n isIsilonChangelistEnabled is a Boolean value that specifies whether\n the Changelist feature of Isilon NAS is used to back up the NAS share.\n * Added optional field `HostShareParameters` in `HostFilesetShareSummary`,\n `HostFilesetShareDetail` and `HostShareDetail`. The HostShareDetail impacts\n the endpoints `Get /host/share` and `Post /host/share`. The\n `HostFilesetShareDetail` impacts the endpoint `Get /host_fileset/share/{id}`.\n . The HostFilesetShareSummary impacts the endpoint\n `Get /host_fileset/share`.\n * Added `isInVmc` in `GET /vcd/vapp/{id}`, and `PATCH /vcd/vapp/{id}`.\n to return whether the virtual machine is in a VMC setup.\n * Added new endpoint `GET /vmware/hierarchy/{id}/export`. Returns the\n VmwareHierarchyInfo object with the given ID.\n * Added optional field `platformDetails` to `PlatformInfo`, which is returned\n by `GET /cluster/{id}/platforminfo`.\n * Added optional field `cpuCount` to `PlatformInfo`, which is returned by\n `GET /cluster/{id}/platforminfo`.\n * Added optional field `ramSize` to `PlatformInfo`, which is returned by\n `GET /cluster/{id}/platforminfo`.\n * Added new value `RangeInTime` to `RecoveryPointType` enum, which is used in\n the `ReportTableRequest` object for the POST `/report/{id}/table` and POST\n `/report/data_source/table` endpoints.\n * Added the optional field `shouldForceFull` to `MssqlDbUpdate` object,\n which is referred by `MssqlDbUpdateId`, which is referred as the\n body parameter of `PATCH /mssql/db/bulk`.\n\n ### Changes to Internal API in Rubrik version 5.1.1\n ## Breaking changes:\n * Changed response code of a successful\n `POST /managed_volume/{id}/begin_snapshot` API from 201 to 200.\n\n ### Changes to Internal API in Rubrik version 5.1.0\n ## Breaking changes:\n * Changed response type of percentInCompliance and percentOutOfCompliance\n in ComplianceSummary to double.\n * Renamed new enum field `MissedSnapshots` to `MissedLocalSnapshots`.\n and `LastSnapshot` to `LatestLocalSnapshot`, in the\n following properties:\n measure property in ChartSummary, column property in TableSummary,\n and sortBy property in ReportTableRequest.\n * Renamed effectiveThroughput to throughput in EventSeriesMonitoredJobSummary.\n * Renamed realThroughput to throughput in EventSeriesSummary.\n * Updated response of GET /event_series/{id} to remove effectiveThroughput.\n * Renamed paths `/storage/array/volume/group` to `/storage/array_volume_group`.\n * Renamed the field cassandraSetup in ReplaceNodeStatus to metadataSetup\n * Renamed the field cassandraSetup in RecommisionNodeStatus to metadataSetup\n * Renamed the field cassandraSetup in AddNodesStatus to metadataSetup\n * Renamed the field cassandraSetup in ClusterConfigStatus to metadataSetup\n * Renamed the field removeCassandra in RemoveNodeStatus to removeMetadatastore\n for the GET /cluster/{id}/remove_node endpoint.\n * Moved the `GET /blackout_window` endpoint from internal to V1.\n * Moved the `PATCH /blackout_window` endpoint from internal to V1.\n * Removed endpoint POST /report/global_object endpoint.\n /report/data_source/table can be used to get the same information.\n * Made accessKey optional in ObjectStoreLocationDetail as accessKey is not\n defined in Cross Account Role Based locations. Also made accessKey required\n again in ObjectStoreLocationDefinition.\n * Removed `progressPercentage` from `EventSeriesMonitoredJobSummary` object.\n * Removed endpoint `POST cluster-id-security-password-strength` since it is\n no longer used at bootstrap.\n * Moved the GET `/mssql/hierarchy/{id}/descendants` and\n GET `/mssql/hierarchy/{id}/children` endpoints from internal to v1.\n\n ## Feature Additions/improvements:\n * GET POST /cluster/{id}/node now accepts an optional encryption\n password in the encryptionPassword field.\n * GET /node_management/replace_node now accepts an optional encryption\n password in the encryptionPassword field.\n * Added optional field `shouldSkipScheduleRecoverArchivedMetadataJob` to\n the body parameter of `POST /archive/object_store/reader/connect`, to\n determine whether to schedule the archival recovery job.\n When the value is 'false,' the recovery job is scheduled normally.\n When the value is 'true,' the recovery job is not scheduled.\n The default behavior is to schedule the recovery job.\n * Added mandatory field `cdp` to SystemStorageStats.\n * Added optional field `agentStatus` to NutanixHierarchyObjectSummary.\n The field indicates whether a Rubrik backup agent is registered to the\n Nutanix object.\n * Added optional field `shouldUseAgent` to `RestoreFilesJobConfig`.\n in `POST /vmware/vm/snapshot/{id}/restore_files` to specify\n whether to use Rubrik Backup Service to restore files. Default value is true.\n * GET /managed_object/bulk/summary and GET\n /managed_object/{managed_id}/summary no longer include archived objects\n with no unexpired snapshots in their results.\n * Added new required Boolean field `isDbLocalToTheCluster` to\n `OracleDbSummary` and `OracleDbDetail`.\n * Added optional field `awsAccountId` to ObjectStoreLocationSummary.\n * Added optional field `shouldRecoverSnappableMetadataOnly` to all the\n reader location connect definitions.\n * Added new enum value `ArchivalComplianceStatus` to the following properties:\n attribute property in ChartSummary and column property in TableSummary\n * Added new enum fields `ArchivalInComplianceCount`,\n `ArchivalNonComplianceCount` and `MissedArchivalSnapshots` to the\n following properties:\n measure property in ChartSummary, column property in TableSummary,\n and sortBy property in ReportTableRequest.\n * GET /managed_object/bulk/summary and GET\n /managed_object/{managed_id}/summary will always include the correct relic\n status for hosts and their descendants.\n * Added field `isLocked` to PrincipalSummary.\n * Added optional query parameter `snappableStatus` to /vmware/data_center and\n /vmware/host. This parameter enables a user to fetch the set of protectable\n objects from the list of objects visible to that user.\n * Added optional field `archivalComplianceStatus` to RequestFilters\n * Added optional field `archivalComplianceStatus` to FilterSummary\n * Added optional field `alias` to HostSummary, HostRegister, and HostUpdate\n schemas. This field will allow the user to specify an alias for each host\n which can be used for search.\n * Added optional field `subnet` to ManagedVolumeExportConfig\n * Added optional field `status` to oracle/hierarchy/{id}/children\n * Added optional field `status` to oracle/hierarchy/{id}/descendants\n * Added optional field `status` to hyperv/hierarchy/{id}/children\n * Added optional field `status` to hyperv/hierarchy/{id}/descendants\n * Added optional field `numNoSla` to ProtectedObjectsCount\n * Added optional field `numDoNotProtect` to ProtectedObjectsCount\n * Added optional field `limit`, `offset`, `sort_by`, `sort_order` to\n /node/stats\n * Added optional field encryptionAtRestPassword to configure password-based\n encryption for an edge instance.\n * Added new endpoint GET /report/data_source/{data_source_name}/csv.\n * Added new endpoint POST /report-24_hour_complianace_summary.\n * Added new endpoint POST /report/data-source/{data_source_name} to get\n columns directly from report data source.\n * Added optional field compliance24HourStatus to RequestFilters object.\n * Added the `port` optional field to QstarLocationDefinition. The `port` field\n enables a user to specify the server port when adding a new location or\n editing an existing location.\n * Added optional field archivalTieringSpec to ArchivalSpec and ArchivalSpecV2\n to support archival tiering. This enables the user to configure either\n Instant Tiering or Smart Tiering (with a corresponding minimum accessible\n duration) on an SLA domain with archival configured to an Azure archival\n location.\n * Updated endpoints /vcd/vapp, /oracle/db and /aws/ec2_instance\n to have a new optional query paramter, indicating if backup task information\n should be included.\n * Added optional field logConfig to SlaDomainSummaryV2, SlaDomainDefinitionV2\n and SlaDomainPatchDefintionV2 to support CDP (ctrlc). The parameters\n distinguish SLAs with CDP enabled from SLAs with CDP disabled, and enable\n users to specify log retention time. The field also provides an optional\n frequency parameter whhich can be used by Oracle and SQL Server log backups.\n * Added optional field logRetentionLimit to ReplicationSpec to support\n CDP replication. The field gives the retention limit for logs at the\n specified location.\n * Moved the `GET /vmware/compute_cluster` endpoint from internal to V1.\n * Moved the `GET /vmware/compute_cluster/{id}` endpoint from internal to V1.\n * Changed the existing `PATCH mssql/db/bulk` endpoint to return an\n unprotectable reason as a string in the `unprotectableReason` field instead\n of a JSON struct.\n * Added optional field `kmsMasterKeyId` and changed the existing field\n `pemFileContent` to optional field in `DcaLocationDefinition`.\n * Added new optional field `enableHardlinkSupport` to FilesetSummary and\n FilesetCreate in `POST /fileset`, \"GET /fileset\" and \"PATCH /fileset/{id}\"\n endpoints to enable recognition and deduplication of hardlinks in\n fileset backup.\n * Added optional query parameter to `GET /archive/location` endpoint,\n indicating if `isRetentionLockedSnapshotProtectedPresent` field should\n be populated in response.\n * Added continuous data protection state for each VMware virtual machine\n * Added new endpoint `PUT /polaris/archive/proxy_setting`.\n * Added new endpoint `GET /polaris/archive/proxy_setting/{id}`.\n * Added new endpoint `DELETE /polaris/archive/proxy_setting/{id}`.\n * Added new endpoint `PUT /polaris/archive/aws_compute_setting`.\n * Added new endpoint `GET /polaris/archive/aws_compute_setting/{id}`.\n * Added new endpoint `DELETE /polaris/archive/aws_compute_setting/{id}`.\n * Added new endpoint `PUT /polaris/archive/azure_compute_setting`.\n * Added new endpoint `GET /polaris/archive/azure_compute_setting/{id}`.\n * Added new endpoint `DELETE /polaris/archive/azure_compute_setting/{id}`.\n * Added new endpoint `PUT /polaris/archive/aws_iam_location`.\n * Added new endpoint `GET /polaris/archive/aws_iam_location/{id}`.\n * Added new endpoint `PUT /polaris/archive/azure_oauth_location`.\n * Added new endpoint `GET /polaris/archive/azure_oauth_location/{id}`.\n * Added new endpoint `PUT /polaris/archive/aws_iam_customer_account`.\n * Added new endpoint `GET /polaris/archive/aws_iam_customer/{id}`.\n * Added new endpoint `DELETE /polaris/archive/aws_iam_customer/{id}`.\n * Added new endpoint `PUT /polaris/archive/azure_oauth_customer`.\n * Added new endpoint `GET /polaris/archive/azure_oauth_customer/{id}`.\n * Added new endpoint `DELETE /polaris/archive/azure_oauth_customer/{id}`.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include `currentState` field, to indicate whether the archival\n location is connected or temporarily disconnected.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include `isComputeEnabled` field, to indicate whether the\n archival location has cloud compute enabled.\n * Added optional field `cloudStorageTier` to `BaseSnapshotSummary`, to indicate\n the current storage tier of the archived copy of a snapshot.\n * Added endpoint `PUT /polaris/archive/aws_iam_location/reader_connect`.\n to connect as a reader to an IAM based AWS archival location.\n * Added endpoint `PUT /polaris/archive/azure_oauth_location/reader_connect`.\n to connect as a reader to an OAuth based Azure archival location.\n * Added endpoint `POST polaris/archive/location/{id}/reader/promote`.\n to promote the current cluster to be the owner of a specified IAM based AWS\n archival location that is currently connected as a reader location.\n * Added endpoint `POST polaris/archive/location/{id}/reader/refresh`.\n to sync the current reader cluster with the contents on the IAM based AWS\n archival location.\n * Added effectiveSlaDomainName and effectiveSlaDomainSourceId fields\n to `GET /vmware/vcenter/{id}/tag_category` response object.\n * Added effectiveSlaDomainName and effectiveSlaDomainSourceId fields\n to `GET /vmware/vcenter/{id}/tag` response object.\n * Added continuous data protection status for reporting.\n * Added optional field `localCdpStatus` to the following components:\n ChartSummary, TableSummary, ReportTableRequest, RequestFilters and\n FilterSummary.\n * Added `ReportSnapshotIndexState` and `ReportObjectIndexType` to\n `/internal_report_models/internal/definitions/enums/internal_report.yml`.\n * Added optional field `latestSnapshotIndexState` and `objectIndexType` to\n the following components:\n TableSummary, ReportTableRequest, RequestFilters and FilterSummary.\n * Added 24 hour continuous data protection healthy percentage for reporting.\n * Added optional field `PercentLocal24HourCdpHealthy` to the following\n components: TableSummary, ReportTableRequest.\n * Added optional field `replicas` to MssqlHierarchyObjectSummary.\n * Added optional field `hosts` to MssqlHierarchyObjectSummary.\n * Added continuous data protection local log storage size and local throughput\n consumption for reporting.\n * Added optional fields `localCdpThroughput` and `localCdpLogStorage` to the\n following components: ChartSummary, TableSummary and ReportTableRequest.\n * Added optional field requestExclusionFilters to ReportTableRequest.\n * Added an optional field to ManagedVolumeSummary to retrieve the associated\n subnet.\n * Added optional field isEffectiveSlaDomainRetentionLocked to Snappable.\n The parameter depicts if the effective SLA domain for the snappable is\n a Retention Lock SLA Domain.\n * Updated the set of possible continuous data protection statuses for each\n VmwareVirtualMachine.\n * Added the optional field isEffectiveSlaDomainRetentionLocked to\n FilesetSummary. The field is a Boolean that specifies whether the effective\n SLA Domain of a fileset is retention locked.\n * Added optional field iConfiguredSlaDomainRetentionLocked to SlaAssignable.\n The parameter depicts if the configured SLA domain for the object is a\n Retention Lock SLA Domain.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include the `isTieringSupported` field, to indicate\n whether a given archival location supports tiering.\n * Added continuous data protection replication status for reporting.\n * Added CdpReplicationStatus as an optional field to the TableSummary and\n ReportTableRequest components.\n * Added optional CdpReplicationStatus field to RequestFilters and\n FiltersSummary.\n * Added optional field isEffectiveSlaDomainRetentionLocked to\n SearchItemSummary. The Boolean parameter specifies whether the effective\n SLA Domain for the search item is a Retention Lock SLA Domain.\n * Updated `OracleMountSummary` returned by GET /oracle/db/mount\n endpoint to include the isInstantRecovered field, to indicate\n whether the mount was created during an Instant Recovery or Live Mount.\n * Added optional field isEffectiveSlaDomainRetentionLocked to\n ManagedObjectSummary. The Boolean parameter specifies whether the effective\n SLA Domain for the search item is a Retention Lock SLA Domain.\n * Added optional field `isRetentionSlaDomainRetentionLocked` to\n UnmanagedSnapshotSummary. The parameter indicates that the retention SLA\n Domain associated with the snapshot is a Retention Lock SLA Domain.\n * Added optional field `isSlaRetentionLocked` to EventSeriesSummary.\n The parameter indicates that the SLA Domain associated with the event\n series is a Retention Lock SLA Domain.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include the `isConsolidationEnabled` field, to indicate\n if consolidation is enabled for the given archival location.\n * Added the `hasUnavailableDisks` field to `NodeStatus` to indicate whether a\n node has unavailable (failed or missing) disks. This change affects the\n endpoints `GET /cluster/{id}/node`, `GET /node`, `GET /node/{id}`, `GET\n /node/stats`, and `GET /node/{id}/stats`.\n * Added optional NAS vendor type to the HostShareDetail\n This change affectes the endpoints `Get /host/share`, `Post /host/share` and\n `Get /host/share/{id}`.\n * Added optional isSnapdiffEnabled in the NasBaseConfig and NasConfig\n NasBaseConfig is returned as part of HostSummary, which is returned by the\n `Get /host/envoy` and `Get /host` endpoints. NasConfig is used by\n HostRegister and HostUpdate. The HostRegister field is used by the\n `Post /host/bulk` endpoint and the HostUpdate is field used by the\n `PATCH /host/bulk` endpoint.\n * Added optional snapdiffUsed in the FilesetSnapshotSummary\n The FilesetSnapshotSummary is used by FilesetDetail and\n FilesetSnapshotDetail. This change affects the endpoints `Post\n /fileset/bulk`, `Get /host_fileset/share/{id}` and\n `Get /fileset/snapshot/{id}`.\n\n ### Changes to Internal API in Rubrik version 5.0.4\n ## Feature Additions/improvements:\n * Added objectState to FilterSummary which is part of body parameter of\n PATCH/report/{id}\n * Added objectState to RequestFilters which is part of body parameter of\n POST /report/data_source/table\n\n ### Changes to Internal API in Rubrik version 5.0.3\n ## Breaking changes:\n * Removed fields 'virtualMedia' and 'ssh' from IpmiAccess and\n IpmiAccessUpdate.\n\n ## Feature Additions/improvements:\n * Added a new optional field 'oracleQueryUser' to HostRegister, HostUpdate\n and HostDetail objects, for setting the Oracle username for account with\n query privileges on the host. This applies to the following endpoints:\n `POST /host/bulk`, `PATCH /host/{id}`, and `GET /host/{id}`.\n * Added a field `affectedNodeIds` to the `SystemStatus` object. This object is\n returned by `GET /cluster/{id}/system_status`.\n * Made `nodeId` a required field of the `DiskStatus` object. This object, or\n an object containing this object, is returned by the following endpoints:\n `GET /cluster/{id}/disk`, `PATCH /cluster/{id}/disk/{disk_id}`, and\n `GET /node/{id}`.\n\n ### Changes to Internal API in Rubrik version 5.0.2\n ## Feature Additions/improvements:\n * Added an optional fields `subnet` to `ManagedVolumeSummary` to retrieve the associated\n subnet.\n * Added `tablespaces` field in `OracleDbSnapshotSummary` to include the list\n of tablespaces in the Oracle database snapshot.\n * Added new endpoint `POST /hierarchy/bulk_sla_conflicts`.\n * Added optional field `limit`, `offset`, `sort_by`, `sort_order` to\n `GET /node/stats`.\n * Added optional field `numNoSla` to `ProtectedObjectsCount`.\n * Added optional field `numDoNotProtect` to `ProtectedObjectsCount`.\n * Introduced optional field `logicalSize` to `VirtualMachineDetail`. This\n field gives the sum of logical sizes of all the disks in the virtual\n machine.\n * Added optional fields `nodeIds`, `slaId`, `numberOfRetries`, and\n `isFirstFullSnapshot` to the response of `GET /event_series/{id}`.\n * Added `SapHanaLog` tag in `applicationTag` field of `ManagedVolumeConfig`.\n for SAP HANA log managed volumes.\n * Added required field `dbSnapshotSummaries` in `OracleRecoverableRange` to include\n the list of database snapshots in each Oracle recoverable range.\n * Added field `isOnline` to MssqlDbSummary and changed `hasPermissions` to\n required field.\n * Added `DbTransactionLog` tag, in applicationTag field of\n `ManagedVolumeConfig`, for generic log managed volumes. ApplicationTag has\n to be specified in the request field of POST /managed_volume.\n\n ### Changes to Internal API in Rubrik version 5.0.1\n ## Breaking changes:\n * Removed `GET/POST /smb/enable_security` endpoints.\n * Changed the `objectId` type in `EventSeriesMonitoredJobSummary` and\n `EventSeriesSummary` to a user-visible ID instead of a simple ID.\n * Updated endpoint `POST /smb/domain` to accept a list of domain controllers.\n * Removed endpoint `POST /report/global_object`.\n * Added optional field `kmsMasterKeyId` and changed the existing field\n `pemFileContent` to optional field in `DcaLocationDefinition`.\n * Removed `progressPercentage` from `EventSeriesMonitoredJobSummary` object.\n\n ## Feature Additions/improvements:\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include `currentState` field, to indicate whether the archival\n location is connected or temporarily disconnected.\n * Added optional field `subnet` to ManagedVolumeExportConfig.\n * Added the`PUT /smb/config` endpoint to manage SMB configuration.\n * Added the following two endpoints.\n - `GET /stats/per_vm_storage`.\n - `GET /stats/per_vm_storage/{vm_id}`.\n * Added optional field `isStickySmbService` to the response of\n `GET /smb/domain` and `POST /smb/domain`.\n * Added new endpoint `GET /report/data_source/{data_source_name}/csv`.\n * Added new endpoint `POST /report_24_hour_complianace_summary`.\n * Added new endpoint `POST /report/data-source/{data_source_name}` to get\n columns directly from report data source.\n * Added new report API endpoints:\n - `GET /report/summary/physical_storage_time_series`.\n - `GET /report/summary/average_local_growth_per_day`.\n * Added `GET /node/stats` which returns stats for all nodes.\n * Added `GET /cluster/{id}/security/password/zxcvbn` to return\n the enabled or disabled status of ZXCVBN validation for new passwords.\n * Added `POST /cluster/{id}/security/password/zxcvbn` to toggle\n ZXCVBN validation for new passwords.\n\n ### Changes to Internal API in Rubrik version 5.0.0\n ## Breaking changes:\n * Removed `/user_notification` endpoints.\n * Added `rawName` field in `ArchivalLocationSummary`, which contains the\n raw name of the archival location.\n * Removed `shareType` from config field in PATCH /managedvolume request.\n * Changed `/cluster/me/ntp_server` endpoint to accept symmetric keys\n and the corresponding hashing algorithm.\n * Removed `/job/type/prune_job_instances` endpoint.\n * Removed `/kmip/configuration` endpoint.\n * Removed `/session/api_token` endpoint.\n * Added `subnet` field in `ManagedVolumeConfig`, which specifies an outgoing\n VLAN interface for a Rubrik node. This is a required value when creating a\n managed volume on a Rubrik node that has multiple VLAN interfaces.\n * Removed the `VolumeGroupVolumeSummary`, and replaced it with\n `HostVolumeSummary`.\n * Removed `volumeIdsIncludedInSnapshots` from `VolumeGroupDetail`.\n * Added new optional fields `mssqlCbtEnabled`, `mssqlCbtEffectiveStatus`,\n `mssqlCbtDriverInstalled`, `hostVfdEnabled` and `hostVfdDriverState` to\n GET /host/{id} response.\n * Responses for `/cluster/{id}/dns_nameserver` and\n `/cluster/{id}/dns_search_domain` changed to be array of strings.\n * Added new required field `language` in `UserPreferencesInfo` for\n GET /user/{id}/preferences and PATCH /user/{id}/preferences\n * Added new field `missedSnapshotTimeUnits` in `MissedSnapshot`.\n * Removed `localStorage` and `archiveStorage` from `UnmanagedSnapshotSummary`.\n * Moved the `Turn on or off a given AWS cloud instance` endpoint from PATCH of\n `/cloud_on/aws/instance` to PATCH of `/cloud_on/aws/instance/{id}/cloud_vm`.\n Also removed the `id` field from the definition of `CloudInstanceUpdate`.\n * Moved the `Turn on or off a given Azure cloud instance` endpoint from PATCH\n of `/cloud_on/azure/instance` to PATCH of\n `/cloud_on/azure/instance/{id}/cloud_vm`. Also removed the `id` field from\n the definition of `CloudInstanceUpdate`.\n * Moved the `Delete a given AWS cloud instance` endpoint from DELETE of\n `/cloud_on/aws/instance/{id}` to DELETE of\n `/cloud_on/aws/instance/{id}/cloud_vm`.\n * Moved the `Delete a given Azure cloud instance` endpoint from DELETE of\n `/cloud_on/azure/instance/{id}` to DELETE of\n `/cloud_on/azure/instance/{id}/cloud_vm`.\n * Modified the existing endpoint DELETE `/cloud_on/aws/instance/{id}` to\n remove entry of a given AWS cloud instance instead of terminating the\n instance.\n * Modified the existing endpoint DELETE `/cloud_on/azure/instance/{id}` to\n remove entry of a given Azure cloud instance instead of terminating the\n instance.\n * Removed `/job/type/job-schedule_gc_job_start_time_now` endpoint. Use\n endpoint POST `/job/type/garbageCollection` to schedule a GC job to\n run now.\n * Removed `config` parameter from `/job/type/garbageCollection`.\n * Added optional parameter `jobInstanceId` to `EventSummary`.\n * Added `jobInstanceId` as a new optional query parameter for\n GET /event_series/{id}/status endpoint.\n * Modified the endpoint GET /event_series/status to a POST and changed the\n input parameter to a request body of type `EventSeriesDetail`.\n * Modified the endpoint PATCH /replication/target/{id} to take a request body\n of type ReplicationTargetUpdate instead of ReplicationTargetDefinition.\n * Added Discovery EventType.\n * Added `name` and deprecated `hostname` in `HostSummary` and `HostDetail`.\n response.\n * Added `isDeleted` and deprecated `isArchived` in MssqlDbReplica response.\n * Removed `GET /stats/cloud_storage` endpoint.\n * Removed DELETE /oracle/db/{id} endpoint to delete an Oracle database.\n * By default, a volume group is not associated with any volumes at creation\n time. This default is a change from the 4.2 implementation, where newly\n created volume groups contain all of the host volumes. On 5.0 clusters,\n use the `GET /host/{id}/volume` endpoint to query all host volumes.\n\n ## Feature Additions/improvements:\n * Added new endpoint POST/report/data-source/{data_source_name} to get columns\n directly from report data source.\n * Added optional field compliance24HourStatus to RequestFilters object.\n * Added GET /event/event_count-by-status to get job counts based on job status.\n * Added GET /event/event_count-by-job-type to get job counts based on job type.\n * Added GET /event_series endpoint to get all event series information in the\n past 24 hours.\n * Added `oracleDatabase` to ManagedObjectDescendantCounts.\n * Introduced `POST /session/realm/{name}` endpoint to generate session\n tokens in the LDAP display name of {name}.\n * Added optional `storageClass` field to `ObjectStoreReaderConnectDefinition`.\n to store `storageClass` field for the newly connected reader location.\n * Added optional `encryptionType` field to `ObjectStoreLocationSummary` to\n return encryption type used for an object store archival location.\n * Added a new endpoint POST /oracle/db/download/{snapshot_id} to download\n a particular snapshot (and corresponding logs) for Oracle.\n * Added optional `ownerId` and `reference` fields to\n `/managed_volume/{id}/begin_snapshot`.\n * Added new endpoints regarding references to Managed Volumes, which track\n the processes writing to the Managed Volume.\n - GET `/managed_volume/{id}/snapshot/{snapshot_id}/reference/{reference_id}`.\n PUT `/managed_volume/{id}/snapshot/{snapshot_id}/reference/{reference_id}`.\n PATCH\n `/managed_volume/{id}/snapshot/{snapshot_id}/reference/{reference_id}`.\n DELETE\n `/managed_volume/{id}/snapshot/{snapshot_id}/reference/{reference_id}`.\n are the endpoints for viewing, adding, editing and deleting a Managed\n Volume snapshot reference respectively.\n * Added optional `apiToken` and `apiEndpoint` fields to NasConfig to support\n Pure FlashBlade devices.\n * Added optional `smbValidIps`, `smbDomainName` and `smbValidUsers` fields\n to `VolumeGroupMountSnapshotJobConfig` to support secure SMB.\n * Added optional `smbDomainName`, `smbValidIps`, `smbValidUsers` fields to\n ManagedVolumeExportConfig to support secure SMB.\n * Added a new optional field `oracleSysDbaUser` to /host/{id} POST endpoint\n during register host for setting the Oracle username for account with sysdba\n privileges on this host.\n * Added a new endpoint DELETE /smb/domain/{domain_name} to delete the\n SMB Domain.\n * Added a new endpoint POST /smb/domain/{domain_name}/join to configure\n SMB Domain.\n * Added a new optional filed `oracleSysDbaUser` to /host/{id} endpoint for\n changing the Oracle username for account with sysdba privileges on this\n host.\n * Added a new endpoint POST /smb/enable_security to enable Live Mount\n security\n * Made the `numChannels` field in ManagedVolumeConfig optional.\n * Added `applicationTag` field to ManagedVolumeConfig to specify workload\n type for a managed volume.\n * Added Maintenance EventType\n * Added POST `/report/global_object` endpoint to directly query table data\n from GlobalObject based on ReportTableRequest\n * Added new API endpoint GET `/diagnostic/snappable/{id}` returns\n diagnostic information of all backup tasks of a data source.\n * Added new API endpoint GET `/diagnostic/snappable/{id}/latest` returns\n diagnostic information of the most recent backup task of a data source.\n * Added `shareType` field to ManagedVolumeSummary and ManagedVolumeDetail.\n * Added oracle instant recovery API to trigger instant recovery of a\n database.\n * Added RAC, Oracle host and Oracle database fields to the the oracle\n hierarchy API\n * Added a new endpoint GET /smb/domain to get a list of discovered\n SMB domains in the environment.\n * Added a new endpoint GET /notification_setting to get all Notification\n Settings.\n * Added a new endpoint POST /notification_setting to create a new\n Notification Setting.\n * Added a new endpoint GET /notification_setting/{id} to get a Notification\n Setting specified by the input id.\n * Added a new endpoint PATCH /notification_setting/{id} to update the values\n for a specified Notification Setting.\n * Added a new endpoint DELTE /notification_setting/{id} to delete a\n specified Notification Setting.\n * Introduced `POST /oracle/db/snapshot/{id}/export/tablespace` endpoint to\n trigger the export of a single tablespace in an Oracle database.\n * Added a new optional field `shouldRestoreFilesOnly` to POST\n /oracle/db/snapshot/{id}/export endpoint, used when exporting an Oracle\n database, to specify whether the user requires a full recovery of the\n database or a restoration of the database files.\n * Added /oracle/hierarchy/{id}/children endpoint to get children of\n object in Oracle hierarchy\n * Added /oracle/hierarchy/{id}/descendants endpoint to get descendants of\n object in Oracle hierarchy\n * Added a new endpoint POST /fileset/{id}/unprotect, which can be used to\n unprotect a fileset and specify a retention policy to apply to existing\n snapshots.\n * Added a new optional field `existingSnapshotRetention` to POST\n /sla_domain/{id}/assign, used when unprotecting an object, to specify whether\n to retain existing snapshots according to the current SLA domain, keep\n existing snapshots forever, or expire all snapshots immediately. If not\n specified, this field will default to the existing behavior of keeping\n snapshots forever.\n * Introduced `GET /kmip/client` endpoint to get the stored KMIP client\n configuration.\n * Introduced `PUT /kmip/client` endpoint to set the KMIP client configuration.\n * Introduced `GET /kmip/server` endpoint to get stored KMIP server\n information.\n * Introduced `PUT /kmip/server` endpoint to add a a KMIP server.\n * Introduced `DELETE /kmip/server` endpoint to remove a a KMIP server.\n * Introduced `POST /session` endpoint to generate session tokens.\n * Added a new optional field `mfaServerId` to /user endpoint for\n associating a configured MFA server.\n * Added REST support for Oracle RAC, Oracle Host.\n Updated the detail and summary for Oracle Database.\n * Added support to run on-demand backup jobs, export snapshots, live\n mount for Oracle Database.\n * Introduced `POST /mfa/rsa/server` endpoint to\n create a new RSA server configuration for MFA integration.\n * Introduced `GET /mfa/rsa/server` endpoint to\n get a list of RSA server configured for MFA integration.\n * Introduced `PATCH /mfa/rsa/server/{id}` endpoint to\n modify RSA server configuration.\n * Introduced `GET /mfa/rsa/server/{id}` endpoint to\n get RSA server configuration.\n * Introduced `POST /mfa/initialize` to initialize an attempt\n to perform Multifactor authentication for a user.\n * Introduced `POST /mfa/session` to perform Multifactor\n authentication for a user.\n * Introduced `POST /session/api_token` to create an API Token.\n * Added a new optional field `isArrayEnabled` to `FilesetTemplateCreate`.\n for creation of storage array-enabled fileset templates. We also include\n this new field in `FilesetTemplateDetail`.\n * Added a new optional field `arraySpec` to `FilesetCreate` for\n creation of storage array-enabled filesets. We also include\n this new field in `FilesetSummary` and `FilesetDetail`.\n * Introduced `GET /cluster/{id}/is_azure_cloud_only` to query if the cluster\n supports only Azure public cloud.\n * Introduced `POST /unmanaged_object/assign_retention_sla` to set Retention\n SLA of unmanaged objects.\n * Introduced `POST /unmanaged_object/snapshot/assign_sla` to set Retention\n SLA of unmanaged snapshots.\n * Introduced `POST /mssql/db/bulk/snapshot/{id}` to take an on-demand snapshot\n of multiple SQL Server databases. The result of this asynchronous request\n can be obtained from `GET /mssql/db/bulk/snapshot/{id}`.\n * Added a new field unprotectable_reasons to GET /mssql/db/{id} and\n GET /mssql/instance/{id}. This field keeps track of the reasons that a\n SQL Server database or instance cannot be protected by Rubrik.\n * Introduced a new `GET /cluster/me/login_banner` and\n `PUT /cluster/me/login_banner` endpoints to get and set the banner\n that displays after each successful login.\n * Introduced a new `GET /cluster/me/security_classification` and\n `PUT /cluster/me/security_classification` endpoints to get and set\n the security classification banner for the cluster. The cluster UI\n displays the banner in the specified color.\n * Introduced `GET /cluster/{id}/security/rksupport_cred` to provide\n the status of the rksupport credentials.\n * Introduced `POST /cluster/{id}/security/rksupport_cred` to update\n the cluster-wide credentials for the specified cluster.\n * Introduced `POST /vmware/vm/snapshot/{id}/mount_disks` to attach VMDKs\n from a mount snapshot to an existing virtual machine\n * Introduced new `GET /host/{id}/volume` endpoint to query the HostVolume\n from the host.\n * Added the `HostVolumeSummary`, which is the response of the endpoint\n `GET /host/{id}/volume` and a part of `VolumeGroupDetail`.\n * Introduced a new `GET /volume_group/host_layout/{snapshot_id}` and\n `GET /volume_group/{host_id}/host_layout` to get the Windows host layout\n of all disks and volumes.\n * Added `WindowsHostLayout` which is the response of\n `GET /volume_group/host_layout/{snapshot_id}` and\n `GET /volume_group/{host_id}/host_layout`.\n * Added support for Blueprint.\n * Added new fields `retentionSlaDomainId` and `retentionSlaDomainName` to\n UnmanagedObjectSummary object, which is returned from a\n `GET /unmanaged_object` call.\n * Removed `unmanagedSnapshotCount` and added new fields `autoSnapshotCount`.\n and `manualSnapshotCount` to UnmanagedObjectSummary object, which is\n returned from a `GET /unmanaged_object` call.\n * Added new fields `retentionSlaDomainId` and `retentionSlaDomainName` to\n UnmanagedSnapshotSummary object, which is returned from a\n `GET /unmanaged_object/{id}/snapshot` call.\n * Added a new field `hasAttachingDisk` to `GET /vmware/vm/snapshot/mount` and\n `GET /vmware/vm/snapshot/mount/{id}` that indicates to the user whether\n this is an attaching disk mount job.\n * Added a new field `attachingDiskCount` to `GET /vmware/vm/snapshot/mount`.\n and `GET /vmware/vm/snapshot/mount/{id}` that indicate to the user how many\n disks are attached.\n * Added field `RetentionSlaDomainName` to sort_by of a\n `GET * /unmanaged_object/{id}/snapshot` call.\n * Added field `excludedDiskIds` to NutanixVmDetail which is returned from a\n `GET /nutanix/vm/{id}` to exclude certain disks from backup. Also added\n field to NutanixVmPatch via `PATCH /nutanix/vm/{id}` to allow the field\n to be updated.\n * Introduced the `PATCH /aws/ec2_instance/indexing_state` endpoint for\n enabling/disabling indexing per EC2 instance.\n * Added new optional fields `organizationId` and `organizationName` to\n `/host/{id}` and `/host` endpoints to get the organization a host is\n assigned to due to Envoy.\n * Introduced a new `GET /host/envoy` endpoint. Acts similar to queryHost but\n also includes Envoy organization info if Envoy is enabled.\n * Added a new endpoint `GET /vmware/vcenter/{id}/tag_category` to get a list of\n Tag Categories associated with a vCenter.\n * Added a new endpoint `Get /vmware/vcenter/tag_category/{tag_category_id}` to\n get a specific Tag Category associated with a vCenter.\n * Added a new endpoint `GET /vmware/vcenter/{id}/tag` to get a list of Tags\n associated with a vCenter. The optional category_id parameter allow the\n response to be filtered by Tag Category.\n * Added a new endpoint `GET /vmware/vcenter/tag/{tag_id}` to get a\n specific Tag associated with a vCenter.\n * Introduced `GET /cluster/{id}/global_manager_connectivity` to\n retrieve a set of URLs that are pingable from the CDM cluster.\n * Added optional field `instanceName` in `ManagedObjectProperties`.\n * Added new endpoint GET `/cloud_on/aws/app_image/{id}` to retrieve a specified\n AWS AppBlueprint image.\n * Added new endpoint DELETE `/cloud_on/aws/app_image/{id}` to delete the\n given AWS AppBlueprint image.\n * Added new endpoint GET `/cloud_on/azure/app_image/{id}` to retrieve a\n specified Azure AppBlueprint image.\n * Added new endpoint DELETE `/cloud_on/azure/app_image/{id}` to delete the\n given Azure AppBlueprint image.\n * Added organization endpoint for Oracle.\n * Added new endpoint GET `/cloud_on/aws/app_image` to retrieve all\n AWS AppBlueprint images.\n * Added new endpoints `GET /stats/cloud_storage/physical`, `GET\n /stats/cloud_storage/ingested` and `GET /stats/cloud_storage/logical` which\n return respective stats aggregated across all archival locations\n * Added a new endpoint `POST /vmware/standalone_host/datastore` to get a list\n of datastore names for a given ESXi host.\n * Added a new optional field `apiEndpoint` to `NasBaseConfig`.\n\n ### Changes to Internal API in Rubrik version 4.2\n ## Breaking changes:\n * Introduced a new `GET /cluster/{id}/ipv6` endpoint for getting all IPv6\n addresses configured on a specific or all network interfaces.\n * Introduced a new `PATCH /cluster/{id}/ipv6` endpoint for configuring IPv6\n addresses on a specific network interface for each nodes in cluster.\n * Introduced a new `GET /cluster/{id}/trial_edge` for getting whether the\n cluster is a trial edge.\n * Moved the /auth_domain/ endpoint from internal APIs to the v1 APIs.\n * Deprecated `POST /archive/nfs/reconnect` endpoint. Use\n `POST /archive/nfs/reader/connect` instead to connect as a reader to an\n existing NFS archival location.\n * Deprecated `POST /archive/object_store/reconnect` endpoint. Use\n `POST /archive/object_store/reader/connect` instead to connect as a reader to\n an existing object store location.\n * Deprecated `POST /archive/qstar/reconnect` endpoint. Use\n `POST /archive/qstar/reader/connect` instead to connect as a reader to an\n existing QStar archival location.\n * Deprecated `POST /archive/dca/reconnect` endpoint. Use\n `POST /archive/dca/reader/connect` instead to connect as a reader to an\n existing DCA archival location.\n * Removed `POST /hyperv/vm/snapshot/{id}/restore_file` endpoint. Use\n `POST /hyperv/vm/snapshot/{id}/restore_files` instead to support\n multi-files restore for Hyper-V vm.\n * Removed `POST /nutanix/vm/snapshot/{id}/restore_file` endpoint. Use\n `POST /nutainx/vm/snapshot/{id}/restore_files` instead to support\n multi-files restore for Nutanix vm.\n * Removed `search_timezone_offset` parameter from\n `GET /unmanaged_object/{id}/snapshot` endpoint. The endpoint will now\n use configured timezone on the cluster.\n * Renamed the field `id` in `UserDefinition` to `username` for `POST /user`.\n endpoint.\n * Removed the `/mssql/db/sla/{id}/availability_group_conflicts` endpoint.\n * Removed the `/mssql/db/sla/{id}/assign` endpoint.\n * Added support for Envoy VMs for Organization.\n * Modified the `DELETE /storage/array/{id}` endpoint so that it now triggers\n an asynchronous deletion job, responds with an async request object, and\n archives the storage array's hierarchy.\n * Added `numStorageArrayVolumeGroupsArchived` to `DataLocationUsage` which\n is the response of the `GET /stats/data_location/usage` endpoint.\n * Modified `POST /storage/array` endpoint so that it now triggers an\n asynchronous refresh job, and responds with an async request object.\n * Modified the `GET /storage/array/{id}` and `DELETE /storage/array/{id}`.\n endpoints so that the `id` field now corresponds to the managed ID\n instead of the simple ID. The `managed ID` is the ID assigned to the\n storage array object by the Rubrik REST API server.\n * Moved /throttle endpoint to /backup_throttle.\n * Introduced a new `EmailSubscriptionUpdate` object for the request of the\n `PATCH /report/email_subscription/{subscription_id}` endpoint.\n * Introduced a new `ReportSubscriptionOwner` object for the response of\n `GET /report/email_subscription/{subscription_id}` and\n `GET /report/{id}/email_subscription` endpoints.\n * Added the envoyStatus field to the response of the GET /organization\n endpoint.\n * Added new `attachments` field to the `POST /report/{id}/email_subscription`.\n and `PATCH /report/email_subscription/{subscription_id}` endpoints.\n * Removed fields `length` and `isLog` in response of\n `/mssql/db/{id}/restore_files`.\n * Moved the `/cluster/decommissionNode` endpoint to\n `/cluster/decommissionNodes`. The `DecommissionNodeConfig` object is renamed\n as `DecommissionNodesConfig` and now takes in a list of strings which\n correspond to the IDs of the nodes that are to be decommissioned.\n * Moved the `POST /vmware/vm/{id}/register_agent` endpoint from internal\n APIs to the v1 APIs.\n * Added a required field for environment in AzureComputeSummary to support\n Azure Gov Cloud.\n * Remove `POST internal/vmware/vm/snapshot/{id}/mount` endpoint. Use public\n API of `POST v1/vmware/vm/snapshot/{id}/mount`.\n * The input field OperatingSystemType value `Linux` is replaced by `UnixLike`.\n in FilesetTemplateCreateDefinition, used by POST /fileset-template, and\n in FilesetTemplatePatchDefinition, used by PATCH /fileset_template/{id}.\n * The input field operating_system_type value `Linux` is replaced by `UnixLike`.\n in GET /host-fileset and GET /host-count.\n * Added `snmpAgentPort` field to SnmpConfig object.\n\n ## Feature Additions/improvements:\n * Introduced the `GET /node_management/default_gateway` and `POST\n /node_management/default_gateway` endpoint to get and set default gateway.\n * Introduced the `GET cloud_on/aws/instance_type_list` and `GET\n cloud_on/azure/instance_type_list` endpoint to fetch list of instance types\n for aws and azures.\n * Introduced the `GET /aws/account/{id}/subnet` endpoint to fetch an\n information summary for each of the subnets available in an AWS account.\n * Introduced the `GET /aws/account/{id}/security_group` endpoint to fetch an\n information summary for each of the security groups belonging to a particular\n virtual network in an AWS account.\n * Moved definitions `Subnet` and `SecurityGroup` of `definitions/cloud_on.yml`.\n to `definitions/cloud_common.yml` so that both the CloudOn and CloudNative\n features can use them.\n * Introduced the `GET /host/{id}/diagnose` endpoint to support target host\n diagnosis features. Network connectivity (machine/agent ping) implemented\n in the current version.\n * Added vCD endpoints to support vCloud Director. The following endpoints\n have been added to the vcdCluster object:\n - `POST /vcd/cluster` to add a new vCD cluster object.\n * Added support for CRUD operations on vCloud Director cluster objects.\n - POST /vcd/cluster, PATCH /vcd/cluster/{id}, DELETE /vcd/cluster/{id},\n POST /vcd/cluster/{id}/refresh are the endpoints for adding, editing,\n deleting and refreshing a vCD cluster object respectively.\n * Introduced endpoint `GET /search/snapshot_search` to search files in a\n given snapshot. The search supports prefix search only.\n * Introduced the new `POST /storage/array/{id}/refresh` endpoint to\n create a new refresh job to update the Storage Array metadata.\n * Introduced the new `GET /storage/array/request/{id}` endpoint to\n get status of a storage array-related asynchronous request.\n * Introduced the new `POST /storage/array/volume/group` endpoint\n to add a new storage array volume group.\n * Introduced the new `GET /storage/array/volume/group/{id}` endpoint\n to get details of a storage array volume group.\n * Introduced the new `DELETE /storage/array/volume/group/{id}` endpoint\n to remove a storage array volume group.\n * Introduced the new `GET /storage/array/hierarchy/{id}` endpoint\n to get a summary of an object in the storage array hierarchy.\n * Introduced the new `GET /storage/array/hierarchy/{id}/children` endpoint\n to get the children of an object in the storage array hierarchy.\n * Introduced the new `GET /storage/array/hierarchy/{id}/descendants` endpoint\n to get the descendants of an object in the storage array hierarchy.\n * Introduced the new `GET /storage/array/volume` endpoint to get\n summary information of all storage array volumes.\n * Introduced the new `GET /storage/array/volume/{id}` endpoint to get\n details of a storage array volume.\n * Introduced the new `POST /storage/array/volume/group/{id}/snapshot`.\n endpoint to create a new on-demand backup job for a storage array\n volume group.\n * Introduced the new `PATCH /storage/array/volume/group/{id}` endpoint to\n update the properties of a storage array volume group object.\n * Introduced the new `GET /storage/array/volume/group` endpoint to\n get all storage array volume groups subject to specified filters.\n * Introduced endpoint `POST /archive/location/{id}/owner/pause` to pause\n archiving to a given archival location that is owned by the current cluster.\n * Introduced endpoint `POST /archive/location/{id}/owner/resume` to resume\n archiving to a given archival location that is owned by the current cluster.\n * Introduced endpoint `POST /archive/location/{id}/reader/promote` to promote\n the current cluster to be the owner of a specified archival location that is\n currently connected as a reader location.\n * Introduced endpoint `POST /archive/location/{id}/reader/refresh` to sync the\n current reader cluster with the contents on the archival location. This pulls\n in any changes made by the owner cluster to the archival location since the\n last time the current cluster was synced.\n * Introduced endpoint `POST /archive/dca/reader/connect` to connect as a reader\n to a DCA archival location.\n * Introduced endpoint `POST /archive/nfs/reader/connect` to connect as a reader\n to an NFS archival location.\n * Introduced endpoint `POST /archive/object_store/reader/connect` to connect as\n a reader to an object store location.\n * Introduced endpoint `POST /archive/dca/qstar/connect` to connect as a reader\n to a QStar archival location.\n * Updated `ArchivalLocationSummary` returned by `GET /archive/location`.\n endpoint to include `ownershipStatus` field, to indicate whether the current\n cluster is connected to the archival location as an owner (active or paused),\n as a reader, or if the archival location is deleted.\n * Added the `ca_certs` field to `StorageArrayDefinition` to allow admins\n to specify certificates used for validation when making network\n requests to the storage array API service. This effects endpoints\n `POST /storage/array`, `GET /storage/array/{id}`, and\n `PUT /storage/array/{id}`.\n * Introduced the `POST /vmware/vm/snapshot/{id}/download_files` endpoint to\n download multiple files/folders from a given vm snapshot. The URL to\n download the zip file including the files will be presented to the users.\n * Introduced the `POST /fileset/snapshot/{id}/download_files` endpoint to\n download multiple files/folders from a given fileset snapshot. The URL to\n download the zip file including the specific files/folders will be presented\n to the users.\n * Introduced the `POST /nutanix/vm/snapshot/{id}/download_files` endpoint to\n download multiple files/folders from a given nutanix snapshot. The URL to\n download the zip file including the specific files/folders will be presented\n to the users.\n * Removed the `POST /nutanix/vm/snapshot/{id}/download_file` endpoint as\n downloading a single file/folder from the nutanix backup is just a special\n case of downloading multiple files/folders.\n * Introduced the `POST /hyperv/snapshot/{id}/download_files` endpoint to\n download multiple files/folders from a given Hyper-V snapshot. The URL to\n download the zip file including the specific files/folders will be presented\n to the users.\n * Introduced the POST /managed_volume/snapshot/{id}/download_files endpoint\n to download multiple files and/or folders from a given managed volume\n snapshot. This endpoint returns the URL to download the ZIP file that\n contains the specified files and/or folders.\n * Introduced the new `GET /storage/array/volume/group/{id}/search` endpoint to\n search storage array volume group for a file.\n * Introduced the new `GET /storage/array/volume/group/snapshot/{id}`.\n endpoint to retrieve details of a storage array volume group snapshot.\n * Introduced the new `DELETE /storage/array/volume/group/snapshot/{id}`.\n endpoint to remove a storage array volume group snapshot.\n * Introduced the new `DELETE /storage/array/volume/group/{id}` endpoint\n to delete all snapshots of a storage array volume group.\n * Introduced the new `POST /storage/array/volume/group/{id}/download`.\n endpoint to download a storage array volume group snapshot from archival.\n * Introduced new `GET/storage/array/volume/group/snapshot/{id}/restore_files`.\n endpoint to restore files from snapshot of a storage array volume group.\n * Added storage volume endpoints for AWS cloud native workload protection.\n Endpoints added:\n - GET /aws/ec2_instance/{id}/storage_volume/ to retrieve\n all storage volumes details attached to an ec2 instance object.\n - GET /aws/ec2_instance/{ec2_instance_id}/storage_volume/{id} to retrieve\n details of a storage volume attached to an ec2 instance object.\n - POST /aws/ec2_intance/snapshot/{id}/export to export the snapshot of\n an ec2 instance object to a new ec2 instance object.\n * Introduced the new `POST /storage/array/volume/group/{id}/download_file`.\n endpoint to download a file from an archived storage array volume group\n snapshot.\n * Introduced the new `GET /storage/array/volume/group/{id}/missed_snapshot`.\n endpoint to get details about all missed snapshots of a storage array volume\n group.\n * Introduced the `GET /network_throttle` endpoint for retrieving the list of\n network throttles.\n * Introduced the `PATCH /network_throttle/{id}` endpoint for updating\n network throttles.\n * Introduced the new `GET /storage/array/host/{id}` endpoint to get details\n about all storage array volumes connected to a host.\n * Introduced the `GET /organization/{id}/storage/array` endpoint for getting\n information for authorized storage array resources in an organization.\n * Introduced the `GET /organization/{id}/storage/array/volume_group/metric`.\n endpoint for getting storage array volume groups metrics in an\n organization.\n * Introduced the new POST /vmware/vm/snapshot/mount/{id}/rollback endpoint to\n rollback the datastore used by a virtual machine, after an Instant Recovery\n that used the preserve MOID setting. This endpoint `rolls back` the\n recovered virtual machine's datastore from the Rubrik cluster to the\n original datastore.\n * Added `owner` and `status` fields to the `EmailSubscriptionSummary`.\n object used in responses for many `/report/{id}/email_subscription`.\n and `/report/email_subscription/{subscription_id}` endpoints.\n * Added `availableSpace` and `readerLocationSummary` fields to the\n `NfsLocationDetail` object used in responses for `/archive/nfs` and\n `/archive/nfs/{id}` endpoints.\n * Added `availableSpace` and `readerLocationSummary` fields to the\n `QstarLocationSummary` object used in responses for the `/archive/qstar`.\n endpoint.\n * Added `availableSpace` and `readerLocationSummary` fields to the\n `QstarLocationDetail` object used in responses for the `/archive/qstar/{id}`.\n endpoint.\n * Added `readerLocationSummary` field to the `ObjectStoreLocationDetail`.\n object used in responses for the `/archive/object_store` and\n `/archive/object_store/{id}` endpoints.\n * Added `readerLocationSummary` field to the `DcaLocationDetail` object\n used in responses for the `/archive/dca` and `/archive/dca/{id}` endpoints.\n * Added a new field `guestOsType` to `HypervVirtualMachineDetail`.\n object used in response of `GET /hyperv/vm/{id}`.\n * Added a new field `guestOsType` to `VirtualMachineDetail`.\n object referred by `VappVmDetail`.\n * Added new field `fileType` in response of `/mssql/db/{id}/restore_files`.\n * Added an optional field `agentStatus` to `VirtualMachineSummary` object used\n in response of `GET /vmware/vm` endpoint. This allows user to check the\n Rubrik Backup Service connection status of the corresponding VMware VM.\n * Introduced the new `POST /fileset/snapshot/{id}/export_files` endpoint to\n export multiple files or directories to destination host.\n * Introduced the new `GET /vmware/config/esx_subnets` endpoint to get the\n the preferred subnets to reach ESX hosts.\n * Introduced the new `PATCH /vmware/config/reset_esx_subnets` endpoint to\n reset the preferred subnets to reach ESX hosts.\n * Changed the `PATCH /vmware/config/reset_esx_subnets` endpoint to\n `PATCH /vmware/config/set_esx_subnets`.\n * Removed the `needsInspection` field from the NodeStatus object returned in\n the `/cluster/{id}/node` and `/node` endpoints.\n * Introduced the new `PATCH /auth_domain/{id}` endpoint to update the Active\n Directory configuration parameters.\n * Introduced the new `GET /cluster/{id}/auto_removed_node` endpoint to\n query for unacknowledged automatic node removals by the Rubrik cluster.\n * Introduced the new\n `DELETE /cluster/{id}/auto_removed_node/{node_id}/acknowledge` endpoint to\n acknowledge an automatic node removal.\n * Introduced the new `GET /cluster/{id}/system_status` endpoint to retrieve\n information about the status of the Rubrik cluster.\n * Changed the `POST /cloud_on/azure/subscription` endpoint to to take\n the parameter `AzureSubscriptionRequest` instead of\n `AzureSubscriptionCredential` in body.\n * Changed the `POST /cloud_on/azure/storage_account` endpoint to to take\n the parameter `AzureStorageAccountRequest` instead of\n `AzureStorageAccountCredential` in body.\n * Changed the `POST /cloud_on/azure/resource_group` endpoint to take\n the parameter `AzureResourceGroupRequest` instead of\n `AzureResourceGroupCredential` in body.\n * Added a `reportTemplate` field to the response of both the\n `GET /report/{id}/table` and `GET /report/{id}/chart` endpoints.\n\n ### Changes to Internal API in Rubrik version 4.1\n ## Changes to support instance from image\n * POST /aws/instance and /azure/instance was supported only from a Rubrik\n snapshot. Now it is changed to support instantiation from Rubrik snapshot as\n well as pre-existing image. Rest end point is same, we just changed the\n CreateCloudInstanceRequest object type.\n * Add a new field `ignoreErrors` to POST /vmware/vm/snapshot/{id}/restore_files\n that will let job restore ignore file errors during restore job.\n ## Breaking changes:\n * None is removed as a Nutanix snapshot consistency mandate so it is no\n longer valid in GET /nutanix/vm, GET /nutanix/vm/{id}, and\n PATCH /nutanix/vm/{id}.\n * computeSecurityGroupId is replaced by the object defaultComputeNetworkConfig\n in ObjectStoreLocationSummary ,ObjectStoreUpdateDefinition and\n ObjectStoreReconnectDefinition which are used by\n GET /archive/object_store/{id}, PATCH /archive/object_store/{id} and\n POST /archive/object_store/reconnect respectively.\n * The PUT /throttle endpoint was changed to provide configuration for\n Hyper-V adaptive throttling. Three parameters were added:\n hypervHostIoLatencyThreshold, hypervHostCpuUtilizationThreshold, and\n hypervVmCpuUtilizationThreshold. To differentiate between the multiple\n hypervisors, the existing configuration parameters for VMware were renamed\n VmwareVmIoLatencyThreshold, VmwareDatastoreIoLatencyThreshold and\n VmwareCpuUtilizationThreshold. These changes also required modifications\n and additions to the GET /throttle endpoint.\n * For `POST /cluster/{id}/node` endpoint, it gets now `AddNodesConfig` in body\n instead of `Map_NodeConfig` directly.\n * For `POST /node_management/replace_node` endpoint, added the `ipmiPassword`.\n field to the `ReplaceNodeConfig` object.\n * For `POST /stats/system_storage` endpoint, added the miscellaneous, liveMount\n and snapshot field to `SystemStorageStats` object.\n * For `POST /principal_search`, removed `managedId` field from the\n `PrincipalSummary` object and changed the `id` field of the\n `PrincipalSummary` object to correspond to the managed id instead of the\n simple id.\n * For `GET /cluster/{id}/timezone` and `PATCH /cluster/{id}/timezone`, the\n functionality has merged into `GET /cluster/{id}` and `PATCH /cluster/{id}`.\n in v1.\n * Removed the `GET /cluster/{id}/decommissionNodeStatus` endpoint.\n Decommission status is now available through queries of the `jobId` that is\n returned by a decommission request. Queries can be performed at the\n `GET /job/{id}` endpoint.\n * For `GET /api/internal/managed_volume/?name=`, the name match is now\n exact instead of infix\n * Updated the list of available attribute and measure values for the `chart0`.\n and `chart1` parameters for the `PATCH /report/{id}` endpoint.\n * Updated the list of available column values for the `table` parameter for the\n `PATCH /report/{id}` endpoint.\n * Updated the `FolderHierarchy` response object to include\n `effectiveSlaDomainId`, `effectiveSlaDomainName`,\n `effectiveSlaSourceObjectId`, and `effectiveSlaSourceObjectName`.\n\n ## Feature Additions/improvements:\n * Added the field `pendingSnapshotCount` to ManagedVolumeSummary and\n ManagedVolumeDetail objects used in responses for endpoints\n `GET /managed_volume`, `POST /managed_volume`, `GET /managed_volume/{id}`,\n `PATCH /managed_volume/{id}`, `GET /organization/{id}/managed_volume`.\n * Introduced the `GET /managed_volume/snapshot/export/{id}` endpoint\n to retrieve details of a specific managed volume snapshot export.\n * Added the `name` filter for GET requests on the /replication/target endpoint.\n This filter allows users to filter results based on the name of a\n replication target.\n * Added the `name` filter for GET requests on the /archive/location endpoint.\n This filter allows users to filter results based on the name of an\n archival location.\n * Added new fields `replicas` and `availabilityGroupId` on GET /mssql\n and GET /mssql/{id}. If a database is an availability database,\n it will have some number of replicas, which are copies of the database\n running on different instances. Otherwise, there will only be one\n replica, which represents the single copy of the database. The field\n `availabilityGroupId` will be set only for availability databases\n and points to the availability group of the database. Also deprecated\n several fields on these endpoints, as they should now be accessed via\n the `replicas` field.\n * Added `Cluster` notification type.\n * Added optional `organizationId` parameter to to the grant/revoke and get\n authorization endpoints. This parameter can be used to\n grant/revoke/get authorizations with respect to a specific Organization.\n * Added endpoint to get/set whether the Rubrik Backup Service is automatically\n deployed to a guest OS.\n * Added cloudInstantiationSpec field to Hyper-V VM endpoint for configuring\n automatic cloud conversion\n * Introduced a new end point /cluster/{id}/platforminfo to GET information\n about the platform the current software is running on\n * Introduced the `GET /organization` and `GET /organization/{id}` endpoints\n for retrieving the list of organizations and a single organization.\n * Introduced the `POST /organization` endpoint for creating organizations,\n the `PATCH /organization/{id}` endpoint for updating organizations and the\n `DELETE /organization/{id}` endpoint for deleting organizations.\n * Introduced the `GET /organization/{id}/stats/storage_growth_timeseries`.\n endpoint and the `GET /organization/{id}/stats/total_storage_usage` for\n getting Physical Storage Growth over Time and Total Physical Storage Usage\n on a per Organization basis.\n * Introduced a number of endpoints of the format\n `GET /organization/{id}/` for retrieving all the resources of\n the corresponding type in a given organization.\n * Introduced a number of endpoints of the format\n `GET /organization/{id}//metric` for retrieving the protection\n counts of the resources of the corresponding type in a given organization.\n * Added the `reportTemplate` filter for GET requests on the /report endpoint.\n This allows queried reports to be filtered and sorted by report template.\n * Introduced the `POST /cluster/{id}/security/password/strength` endpoint\n for assessing the strength of passwords during bootstrap through rkcli.\n * Added a new `ipv6` field in the response of the `GET /cluster/{id}/discover`.\n endpoint.\n * Added relatedIds field for EventSummary object to give more context about\n the event.\n * Added operatingSystemType field for NutanixSummary object. This field\n represents the type of operating system on the Nutanix virtual machine.\n\n ### Changes to Internal API in Rubrik version 4.0\n ## Breaking changes:\n * For `GET /unmanaged_object` endpoint, replaced the `Fileset` of object_type\n filter with more specific object types: `WindowsFileset`, `LinuxFileset` and\n `ShareFileset`. Also added filter value for additional unmanaged objects\n we now support.\n * For /mssql/db/{id}/compatible_instance added recoveryType as mandatory\n query parameter\n\n ## Feature Additions/improvements:\n * Added QStar end points to support it as an archival location. The location\n is always encrypted and an encryption password must be set while adding the\n location. End points added:\n - `DELETE /archive/qstar` to clean up the data in the bucket in the QStar\n archival location.\n - `GET /archive/qstar` to retrieve a summary of all QStar archival locations.\n - `POST /archive/qstar` to add a QStar archival location.\n - `POST /archive/qstar/reconnect` to reconnect to a specific QStar archival\n location.\n - `POST /archive/qstar/remove_bucket` to remove buckets matching a prefix\n from QStar archival location.\n - `GET /archive/qstar/{id}` to retrieve a summary information from a specific\n QStar archival location.\n - `PATCH /archive/qstar/{id}` to update a specific QStar archival location.\n * Added the `name` filter for GET requests on the /archive/location endpoint.\n This filter allows users to filter results based on the name of an\n archival location.\n * Introduced an optional parameter `encryptionPassword` for the\n `/data_location/nfs` `POST` endpoint. This password is used for\n deriving the master key for encrypting the NFS archival location.\n * Introduced /managed\\_volume, /managed\\_volume/snapshot/export/{id},\n and other child endpoints for creating, deleting, and updating\n Managed Volumes and its exports and snapshots.\n * Added support for Hyper-V.\n * Add new /hierarchy endpoint to support universal hierarchy view.\n * Added support for Nutanix.\n * Moved and merged vCenter refresh status and delete status from independent\n internal endpoints to a single status field in v1 vCenter detail.\n * Added endpoint to get/set whether the Rubrik Backup Service is automatically\n deployed to a guest OS.\n * Introduced an optional parameter `minTolerableNodeFailures` for the\n `/cluster/decommissionNode` `POST` endpoint. This parameter specifies the\n minimum fault tolerance to node failures that must exist when a node is\n decommissioned.\n * Added `nodeId` to `AsyncRequestStatus` to improve debugging job failures.\n\n ### Changes to Internal API in Rubrik version 3.2.0\n ## Breaking changes:\n * Introduced endpoint /host/share/id/search to search for\n files on the network share.\n * Introduced endpoints /host/share and /host/share/id to\n support native network shares under /host endpoint.\n * For /unmanaged_object endpoints, change sort_attr to sort_by\n sort_attr used to accept a comma separated list of column names to sort.\n Now sort_by only accepts a single column name.\n * For /unmanaged_object endpoints, removed the need for object type when\n deleting unmanaged objects and its snapshots.\n\n ## Feature Additions/improvements:\n * Added internal local_ end points. These are used for\n handling operations on per-node auto-scaling config values.\n Please see src/spec/local-config/comments for details.\n * For the response of /mssql/db/{id}/restore_files, added two more fields\n for each file object. They are the original file name and file length\n of the file to be restore.\n * Introduced a new end point /cluster/{id}/is_registered to GET registration\n status. With this change, we can query if the cluster is registered in the\n Rubrik customer database.\n * Introduced a new end point /cluster/{id}/registration_details to POST\n registration details. Customers are expected to get the registration details\n from the support portal. On successful submission of registration details\n with a valid registration id, the cluster will mark itself as registered.\n * For the /mssql/instance/{id} end point, added fields configuredSlaDomainId,\n configuredSlaDomainName, logBackupFrequencyInSeconds, logRetentionHours,\n and copyOnly.\n * Introduced optional parameter keepMacAddresses to\n POST /vmware/vm/snapshot/{id}/mount, /vmware/vm/snapshot/{id}/export, and\n /vmware/vm/snapshot/{id}/instant_recovery endpints.\n This allows new VMs to have the same MAC address as their source VMs.\n\n ## Bug fixes:\n * Made path parameter required in GET /browse. Previously, an error was\n thrown when path was not passed in. This solves that bug.\n",
- "x-logo": {
- "url": "https://www.rubrik.com/wp-content/uploads/2016/11/Rubrik-Snowflake-small.png"
- }
- },
- "paths": {
- "/polaris/replication/source/replicate_app/{snappable_id}": {
- "post": {
- "parameters": [
- {
- "name": "snappable_id",
- "in": "path",
- "description": "Snappable ID of which we are replicating snapshots.",
- "required": true,
- "type": "string"
- },
- {
- "in": "body",
- "name": "definition",
- "description": "Polaris source pull replicate definition.",
- "required": true,
- "schema": {
- "$ref": "#/definitions/PolarisPullReplicateDefinition"
- }
- }
- ],
- "responses": {
- "200": {
- "description": "OK"
- }
- }
- }
- }
- },
- "definitions": {
- "PolarisPullReplicateDefinition": {
- "type": "object",
- "required": [
- "accessKey",
- "isOnDemand",
- "polarisId",
- "secretKey",
- "snapshotInfo"
- ],
- "properties": {
- "polarisId": {
- "type": "string",
- "description": "Managed ID of the Polaris source cluster."
- },
- "snapshotInfo": {
- "description": "Info of the snapshot which this cluster is replicating from Polaris.",
- "$ref": "#/definitions/ReplicationSnapshotInfo"
- },
- "accessKey": {
- "type": "string",
- "description": "The access key used for accessing customer's volumes to pull replicate snapshots."
- },
- "secretKey": {
- "type": "string",
- "description": "The secret key used for accessing customer's volumes to pull replicate snapshots.",
- "x-secret": true
- },
- "isOnDemand": {
- "type": "boolean",
- "description": "Indicates if snapshot is on-demand."
- }
- }
- },
- "ReplicationSnapshotInfo": {
- "type": "object",
- "required": ["snappableId", "snapshotId"],
- "properties": {
- "snappableId": {
- "type": "string",
- "description": "The ID of the snappable stored on this cluster."
- },
- "snapshotId": {
- "type": "string",
- "description": "The ID of the snapshot that is being replicated."
- },
- "snapshotDate": {
- "type": "integer",
- "format": "int64",
- "description": "The date when the snapshot was taken in number of milliseconds since the UNIX epoch. This is a required field when the replication source is Polaris."
- },
- "snapshotDiskInfos": {
- "type": "array",
- "description": "An array of the details of the snapshot disks that need to be replicated. This is a required field when the replication source is Polaris.",
- "items": {
- "$ref": "#/definitions/ReplicationSnapshotDiskInfo"
- }
- },
- "appMetadata": {
- "type": "string",
- "description": "Serialized metadata specific to the snappable which is being replicated. This is a required field when the replication source is Polaris."
- },
- "childSnapshotInfos": {
- "type": "array",
- "description": "An array of child snapshots information.",
- "items": {
- "$ref": "#/definitions/ReplicationSnapshotInfo"
- }
- }
- }
- },
- "ReplicationSnapshotDiskInfo": {
- "type": "object",
- "required": [
- "diskFailoverInfo",
- "diskId",
- "isOsDisk",
- "logicalSizeInBytes",
- "snapshotDiskId"
- ],
- "properties": {
- "diskId": {
- "type": "string",
- "description": "The ID of the disk/volume that is being replicated."
- },
- "snapshotDiskId": {
- "type": "string",
- "description": "The ID of the snapshot of the disk/volume taken on the source that needs to be replicated."
- },
- "logicalSizeInBytes": {
- "type": "integer",
- "format": "int64",
- "description": "Size of the disk/volume that is being replicated."
- },
- "isOsDisk": {
- "type": "boolean",
- "description": "Flag to specify if the disk is OS disk."
- },
- "diskFailoverInfo": {
- "description": "Details specific to the target snappable required to failover the EBS volumes.",
- "$ref": "#/definitions/InstanceFailoverInfo"
- }
- }
- },
- "InstanceFailoverInfo": {
- "type": "object",
- "required": ["originalDiskIdentifier"],
- "properties": {
- "originalDiskIdentifier": {
- "type": "string",
- "description": "The identifier used to map the original disks before failover to the disks being replicated. For vmware to AWS, this would be the deviceKey of the vmware virtual disk this EBS volume corresponds to."
- }
- }
- }
- }
-}
diff --git a/openapi3/testdata/issue638/test1.yaml b/openapi3/testdata/issue638/test1.yaml
deleted file mode 100644
index f2ab5555c..000000000
--- a/openapi3/testdata/issue638/test1.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-openapi: "3.0.0"
-info:
- version: 1.0.0
- title: reference test part 1
- description: reference test part 1
-components:
- schemas:
- test1a:
- $ref: "test2.yaml#/components/schemas/test2a"
- test1b:
- $ref: "#/components/schemas/test1c"
- test1c:
- type: int
- test1d:
- $ref: "test2.yaml#/components/schemas/test2b"
diff --git a/openapi3/testdata/issue638/test2.yaml b/openapi3/testdata/issue638/test2.yaml
deleted file mode 100644
index d3ca4648b..000000000
--- a/openapi3/testdata/issue638/test2.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-openapi: "3.0.0"
-info:
- version: 1.0.0
- title: reference test part 2
- description: reference test part 2
-components:
- schemas:
- test2a:
- type: number
- test2b:
- $ref: "test1.yaml#/components/schemas/test1b"
- test1c:
- type: string
diff --git a/openapi3/testdata/issue652/definitions.yml b/openapi3/testdata/issue652/definitions.yml
deleted file mode 100644
index 98ef69254..000000000
--- a/openapi3/testdata/issue652/definitions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-components:
- schemas:
- TestSchema:
- type: string
diff --git a/openapi3/testdata/issue652/nested/schema.yml b/openapi3/testdata/issue652/nested/schema.yml
deleted file mode 100644
index ef321a101..000000000
--- a/openapi3/testdata/issue652/nested/schema.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-components:
- schemas:
- ReferenceToParentDirectory:
- $ref: "../definitions.yml#/components/schemas/TestSchema"
diff --git a/openapi3/testdata/issue697.yml b/openapi3/testdata/issue697.yml
deleted file mode 100644
index 71a4b2ae2..000000000
--- a/openapi3/testdata/issue697.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-openapi: 3.0.1
-components:
- schemas:
- API:
- properties:
- dateExample:
- type: string
- format: date
- example: 2019-09-12
-info:
- title: sample
- version: version not set
-paths: {}
-
diff --git a/openapi3/testdata/issue753.yml b/openapi3/testdata/issue753.yml
deleted file mode 100644
index 2123a6dbd..000000000
--- a/openapi3/testdata/issue753.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-openapi: '3'
-info:
- version: 0.0.1
- title: 'test'
-paths:
- /test1:
- post:
- requestBody:
- content:
- application/json:
- schema:
- type: object
- responses:
- '200':
- description: 'test'
- callbacks:
- callback1:
- '{$request.body#/callback}':
- post:
- requestBody:
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/test'
- responses:
- '200':
- description: 'test'
- /test2:
- post:
- requestBody:
- content:
- application/json:
- schema:
- type: object
- responses:
- '200':
- description: 'test'
- callbacks:
- callback2:
- '{$request.body#/callback}':
- post:
- requestBody:
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/test'
- responses:
- '200':
- description: 'test'
-components:
- schemas:
- test:
- type: string
diff --git a/openapi3/testdata/link-example.yaml b/openapi3/testdata/link-example.yaml
deleted file mode 100644
index 735e7dbb7..000000000
--- a/openapi3/testdata/link-example.yaml
+++ /dev/null
@@ -1,203 +0,0 @@
-openapi: 3.0.0
-info:
- title: Link Example
- version: 1.0.0
-paths:
- /2.0/users/{username}:
- get:
- operationId: getUserByName
- parameters:
- - name: username
- in: path
- required: true
- schema:
- type: string
- responses:
- '200':
- description: The User
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/user'
- links:
- userRepositories:
- $ref: '#/components/links/UserRepositories'
- /2.0/repositories/{username}:
- get:
- operationId: getRepositoriesByOwner
- parameters:
- - name: username
- in: path
- required: true
- schema:
- type: string
- responses:
- '200':
- description: repositories owned by the supplied user
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/repository'
- links:
- userRepository:
- $ref: '#/components/links/UserRepository'
- /2.0/repositories/{username}/{slug}:
- get:
- operationId: getRepository
- parameters:
- - name: username
- in: path
- required: true
- schema:
- type: string
- - name: slug
- in: path
- required: true
- schema:
- type: string
- responses:
- '200':
- description: The repository
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/repository'
- links:
- repositoryPullRequests:
- $ref: '#/components/links/RepositoryPullRequests'
- /2.0/repositories/{username}/{slug}/pullrequests:
- get:
- operationId: getPullRequestsByRepository
- parameters:
- - name: username
- in: path
- required: true
- schema:
- type: string
- - name: slug
- in: path
- required: true
- schema:
- type: string
- - name: state
- in: query
- schema:
- type: string
- enum:
- - open
- - merged
- - declined
- responses:
- '200':
- description: an array of pull request objects
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/pullrequest'
- /2.0/repositories/{username}/{slug}/pullrequests/{pid}:
- get:
- operationId: getPullRequestsById
- parameters:
- - name: username
- in: path
- required: true
- schema:
- type: string
- - name: slug
- in: path
- required: true
- schema:
- type: string
- - name: pid
- in: path
- required: true
- schema:
- type: string
- responses:
- '200':
- description: a pull request object
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/pullrequest'
- links:
- pullRequestMerge:
- $ref: '#/components/links/PullRequestMerge'
- /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge:
- post:
- operationId: mergePullRequest
- parameters:
- - name: username
- in: path
- required: true
- schema:
- type: string
- - name: slug
- in: path
- required: true
- schema:
- type: string
- - name: pid
- in: path
- required: true
- schema:
- type: string
- responses:
- '204':
- description: the PR was successfully merged
-components:
- links:
- UserRepositories:
- # returns array of '#/components/schemas/repository'
- operationId: getRepositoriesByOwner
- parameters:
- username: $response.body#/username
- UserRepository:
- # returns '#/components/schemas/repository'
- operationId: getRepository
- parameters:
- username: $response.body#/owner/username
- slug: $response.body#/slug
- RepositoryPullRequests:
- # returns '#/components/schemas/pullrequest'
- operationId: getPullRequestsByRepository
- parameters:
- username: $response.body#/owner/username
- slug: $response.body#/slug
- PullRequestMerge:
- # executes /2.0/repositories/{username}/{slug}/pullrequests/{pid}/merge
- operationId: mergePullRequest
- parameters:
- username: $response.body#/author/username
- slug: $response.body#/repository/slug
- pid: $response.body#/id
- schemas:
- user:
- type: object
- properties:
- username:
- type: string
- uuid:
- type: string
- repository:
- type: object
- properties:
- slug:
- type: string
- owner:
- $ref: '#/components/schemas/user'
- pullrequest:
- type: object
- properties:
- id:
- type: integer
- title:
- type: string
- repository:
- $ref: '#/components/schemas/repository'
- author:
- $ref: '#/components/schemas/user'
diff --git a/openapi3/testdata/lxkns.yaml b/openapi3/testdata/lxkns.yaml
deleted file mode 100644
index 6e1bee5d6..000000000
--- a/openapi3/testdata/lxkns.yaml
+++ /dev/null
@@ -1,988 +0,0 @@
-# https://raw.githubusercontent.com/thediveo/lxkns/71e8fb5e40c612ecc89d972d211221137e92d5f0/api/openapi-spec/lxkns.yaml
-openapi: 3.0.2
-security:
- - {}
-info:
- title: lxkns
- version: 0.22.0
- description: |-
- Discover Linux-kernel namespaces, almost everywhere in a Linux host. Also look
- for mount points and their hierarchy, as well as for containers.
- contact:
- url: 'https://github.com/thediveo/lxkns'
- license:
- name: Apache 2.0
- url: 'https://www.apache.org/licenses/LICENSE-2.0'
-servers:
- -
- url: /api
- description: lxkns as-a-service
-paths:
- /processes:
- summary: Process discovery
- get:
- responses:
- '200':
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ProcessTable'
- description: |-
- Returns information about all processes and their position within the process
- tree.
- summary: Linux processes
- description: |-
- Map of all processes in the process tree, with the keys being the PIDs in
- decimal string format.
- /pidmap:
- summary: Discover the translation of PIDs between PID namespaces
- get:
- responses:
- '200':
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/PIDMap'
- description: |-
- The namespaced PIDs of processes. For each process, the PIDs in their PID
- namespaces along the PID namespace hierarchy are returned.
- summary: PID translation data
- description: |
- Discovers the PIDs that processes have in different PID namespaces,
- according to the hierarchy of PID namespaces.
-
- > **IMPORTANT:** The order of processes is undefined. However, the order of
- > the namespaced PIDs of a particular process is well-defined.
- /namespaces:
- summary: Namespace discovery (includes process discovery for technical reasons)
- get:
- responses:
- '200':
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/DiscoveryResult'
- description: The discovered namespaces and processes.
- summary: Linux kernel namespaces
- description: |-
- Information about the Linux-kernel namespaces and how they relate to processes
- and vice versa.
-components:
- schemas:
- PIDMap:
- title: Root Type for PIDMap
- description: |-
- A "map" of the PIDs of processes in PID namespaces for translating a specific
- PID from one PID namespace into another PID namespace.
-
- > **IMPORTANT:** The order of *processes* is undefined. However, the order of
- > the namespaced PIDs of a particular process is well-defined: from the PID in
- > the process' own PID namespace up the hierarchy to the PID in the initial
- > PID namespace.
-
- The PID map is represented in a "condensed" format, which is designed to
- minimize transfer volume. Consuming applications thus might want to transfer
- this external representation into a performance-optimized internal
- representation, optimized for translating PIDs.
- type: array
- items:
- $ref: '#/components/schemas/NamespacedPIDs'
- example:
- -
- -
- pid: 12345
- nsid: 4026531905
- -
- pid: 1
- nsid: 4026538371
- -
- -
- pid: 666
- nsid: 4026538371
- NamespacedPID:
- title: Root Type for NamespacedPID
- description: |-
- A process identifier (PID) valid only in the accompanying PID namespace,
- referenced by the ID (inode number) of the PID namespace. Outside that PID
- namespace the PID is invalid and might be confused with some other process that
- happens to have the same PID in the other PID namespace. For instance, PID 1
- can be found not only in the initial PID namespace, but usually also in all
- other PID namespaces, but referencing completely different processes each time.
- required:
- - pid
- - nsid
- type: object
- properties:
- pid:
- description: a process identifier
- type: integer
- nsid:
- format: int64
- description: |-
- a PID namespace identified and referenced by its inode number (without any
- device number).
- type: integer
- example:
- pid: 1
- nsid: 4026531905
- NamespacedPIDs:
- description: |-
- The list of namespaced PIDs of a process, ordered according to the PID
- namespace hierarchy the process is in. The order is from the "bottom-most" PID
- namespace a particular process is joined to up to the initial PID namespace.
- Thus, the PID in the initial PID namespace always comes last.
- type: array
- items:
- $ref: '#/components/schemas/NamespacedPID'
- example:
- -
- pid: 12345
- nsid: 4026531905
- -
- pid: 1
- nsid: 4026532382
- Process:
- description: |-
- Information about a specific process, such as its PID, name, and command line
- arguments, the references (IDs) of the namespaces the process is joined to.
- required:
- - pid
- - ppid
- - name
- - cmdline
- - starttime
- - namespaces
- - cpucgroup
- # - fridgecgroup
- # - fridgefrozen
- type: object
- properties:
- pid:
- format: int32
- description: The process identifier (PID) of this process.
- type: integer
- ppid:
- format: int32
- description: |-
- The PID of the parent process, or 0 if there is no parent process. On Linux, the
- only processes without a parent are the initial process PID 1 and the PID 2
- kthreadd kernel threads "process".
- type: integer
- name:
- description: |-
- A synthesized name of the process:
- - a name set by the process itself,
- - a name derived from the command line of the process.
- type: string
- cmdline:
- description: |-
- The command line arguments of the process, including the process binary file
- name. Taken from /proc/$PID/cmdline, see also
- [https://man7.org/linux/man-pages/man5/proc.5.html](proc(5)).
- type: array
- items:
- type: string
- starttime:
- format: int64
- description: |-
- The time this process started after system boot and expressed in clock ticks.
- It is taken from /proc/$PID/stat, see also
- [https://man7.org/linux/man-pages/man5/proc.5.html](proc(5)).
- type: integer
- cpucgroup:
- description: |-
- The (CPU) cgroup (control group) path name in the hierarchy this process is in. The
- path name does not specify the root mount path of the complete hierarchy, but
- only the (pseudo) absolute path starting from the root of the particular (v1) or
- unified (v2) cgroup hierarchy.
- type: string
- namespaces:
- $ref: '#/components/schemas/NamespacesSet'
- description: |-
- References the namespaces this process is joined to, in form of the namespace
- IDs (inode numbers).
- fridgecgroup:
- description: The freezer cgroup path name in the hierarchy this process is in.
- type: string
- fridgefrozen:
- description: The effective freezer state of this process.
- type: boolean
- example:
- namespaces:
- mnt: 4026531840
- cgroup: 4026531835
- uts: 4026531838
- ipc: 4026531839
- user: 4026531837
- pid: 4026531836
- net: 4026531905
- pid: 1
- ppid: 0
- name: systemd
- cmdline:
- - /sbin/init
- - fixrtc
- - splash
- starttime: 0
- cpucgroup: /init.scope
- ProcessTable:
- description: |-
- Information about all processes in the process tree, with each process item
- being keyed by its PID in string form. Besides information about the process
- itself and its position in the process tree, the processes also reference the
- namespaces they are currently joined to.
- type: object
- additionalProperties:
- $ref: '#/components/schemas/Process'
- example:
- '1':
- namespaces:
- mnt: 4026531840
- cgroup: 4026531835
- uts: 4026531838
- ipc: 4026531839
- user: 4026531837
- pid: 4026531836
- net: 4026531905
- pid: 1
- ppid: 0
- name: systemd
- cmdline:
- - /sbin/init
- - fixrtc
- - splash
- starttime: 0
- cpucgroup: /init.scope
- '137024':
- namespaces:
- mnt: 4026532517
- cgroup: 4026531835
- uts: 4026531838
- ipc: 4026531839
- user: 4026532518
- pid: 4026531836
- net: 4026531905
- pid: 137024
- ppid: 1
- name: upowerd
- cmdline:
- - /usr/lib/upower/upowerd
- starttime: 3132568
- cpucgroup: /system.slice/upower.service
- DiscoveryResult:
- description: |-
- The discovered namespaces and processes with their mutual relationships, and
- optionally PID translation data.
- required:
- - namespaces
- - processes
- - containers
- - container-engines
- - container-groups
- type: object
- properties:
- processes:
- $ref: '#/components/schemas/ProcessTable'
- description: 'Information about all processes, including the process hierarchy.'
- namespaces:
- $ref: '#/components/schemas/NamespacesDict'
- description: Map of namespaces.
- pidmap:
- $ref: '#/components/schemas/PIDMap'
- description: Data for translating PIDs between different PID namespaces.
- options:
- $ref: '#/components/schemas/DiscoveryOptions'
- description: The options specified for discovery.
- mounts:
- $ref: '#/components/schemas/NamespacedMountPaths'
- description: Map of mount namespace'd mount paths with mount points.
- containers:
- $ref: '#/components/schemas/ContainerMap'
- description: Discovered containers.
- container-engines:
- $ref: '#/components/schemas/ContainerEngineMap'
- description: Container engines managing the discovered containers.
- container-groups:
- $ref: '#/components/schemas/ContainerGroupMap'
- description: Groups of containers.
- example:
- discovery-options:
- skipped-procs: false
- skipped-tasks: false
- skipped-fds: false
- skipped-bindmounts: false
- skipped-hierarchy: false
- skipped-ownership: false
- skipped-freezer: false
- scanned-namespace-types:
- - time
- - mnt
- - cgroup
- - uts
- - ipc
- - user
- - pid
- - net
- namespaces:
- '4026531835':
- nsid: 4026531835
- type: cgroup
- owner: 4026531837
- reference: /proc/2/ns/cgroup
- leaders:
- - 2
- - 1
- '4026531836':
- nsid: 4026531836
- type: pid
- owner: 4026531837
- reference: /proc/2/ns/pid
- leaders:
- - 2
- - 1
- children:
- - 4026532338
- '4026531837':
- nsid: 4026531837
- type: user
- reference: /proc/1/ns/user
- leaders:
- - 1
- - 2
- children:
- - 4026532518
- user-id: 0
- '4026531838':
- nsid: 4026531838
- type: uts
- owner: 4026531837
- reference: /proc/2/ns/uts
- leaders:
- - 2
- - 1
- '4026531839':
- nsid: 4026531839
- type: ipc
- owner: 4026531837
- reference: /proc/2/ns/ipc
- leaders:
- - 2
- - 1
- '4026532268':
- nsid: 4026532268
- type: mnt
- owner: 4026531837
- reference: /proc/1761/ns/mnt
- leaders:
- - 1761
- '4026532324':
- nsid: 4026532324
- type: uts
- owner: 4026531837
- reference: /proc/1781/ns/uts
- leaders:
- - 1781
- '4026532337':
- nsid: 4026532337
- type: ipc
- owner: 4026531837
- reference: /proc/33536/ns/ipc
- leaders:
- - 33536
- '4026532340':
- nsid: 4026532340
- type: net
- owner: 4026531837
- reference: /proc/33536/ns/net
- leaders:
- - 33536
- '4026532398':
- nsid: 4026532398
- type: pid
- owner: 4026531837
- reference: /proc/34110/ns/pid
- leaders:
- - 34110
- parent: 4026532338
- '4026532400':
- nsid: 4026532400
- type: net
- owner: 4026531837
- reference: /proc/34110/ns/net
- leaders:
- - 34110
- '4026532517':
- nsid: 4026532517
- type: mnt
- owner: 4026531837
- reference: /proc/137024/ns/mnt
- leaders:
- - 137024
- '4026532518':
- nsid: 4026532518
- type: user
- reference: /proc/137024/ns/user
- leaders:
- - 137024
- parent: 4026531837
- user-id: 0
- processes:
- '1':
- namespaces:
- mnt: 4026531840
- cgroup: 4026531835
- uts: 4026531838
- ipc: 4026531839
- user: 4026531837
- pid: 4026531836
- net: 4026531905
- pid: 1
- ppid: 0
- name: systemd
- cmdline:
- - /sbin/init
- - fixrtc
- - splash
- starttime: 0
- cpucgroup: /init.scope
- '17':
- namespaces:
- mnt: 4026531840
- cgroup: 4026531835
- uts: 4026531838
- ipc: 4026531839
- user: 4026531837
- pid: 4026531836
- net: 4026531905
- pid: 17
- ppid: 2
- name: migration/1
- cmdline:
- - ''
- starttime: 0
- cpucgroup: ''
- '1692':
- namespaces:
- mnt: 4026532246
- cgroup: 4026531835
- uts: 4026532247
- ipc: 4026531839
- user: 4026531837
- pid: 4026531836
- net: 4026531905
- pid: 1692
- ppid: 1
- name: systemd-timesyn
- cmdline:
- - /lib/systemd/systemd-timesyncd
- starttime: 2032
- cpucgroup: /system.slice/systemd-timesyncd.service
- Namespace:
- description: |-
- Information about a single Linux-kernel namespace. Depending on the extent of
- the discovery, not all namespace types might have been discovered, or data might
- be missing about the PID and user namespace hierarchies as well as which user
- namespace owns other namespaces.
-
- For more details, please see also:
- https://man7.org/linux/man-pages/man7/namespaces.7.html.
- required:
- - type
- - nsid
- type: object
- properties:
- nsid:
- format: int64
- description: |-
- Identifier of this namespace: an inode number.
-
- - lxkns only uses the inode number in the API, following current Linux kernel
- and CLI tool practise, which generally identify individual namespaces only by
- inode numbers (and leaving out the device number).
- - Namespace identifiers are not UUIDs, but instead reused by the kernel after a
- namespace has been destroyed.
- type: integer
- type:
- $ref: '#/components/schemas/NamespaceType'
- description: Type of this namespace.
- owner:
- format: int64
- description: The ID of the owning user namespace.
- type: integer
- reference:
- description: |-
- File system reference to the namespace, if available. The hierarchical PID and
- user namespaces can also exist without any file system references, as long as
- there are still child namespaces present for such a PID or user namespace.
- type: array
- items:
- type: string
- leaders:
- description: |-
- List of PIDs of "leader" processes joined to this namespace.
-
- Instead of listing all processes joined to this namespace, lxkns only lists the
- "most senior" processes: these processes are the highest processes in the
- process tree still joined to a namespace. Child processes also joined to this
- namespace can then be found using the child process relations from the process
- table information.
- type: array
- items:
- format: int32
- type: integer
- ealdorman:
- format: int32
- description: PID of the most senior leader process joined to this namespace.
- type: integer
- parent:
- format: int64
- description: 'Only for PID and user namespaces: the ID of the parent namespace.'
- type: integer
- user-id:
- description: |-
- Only for user namespaces: the UID of the Linux user who created this user
- namespace.
- type: integer
- user-name:
- description: |-
- Only for user namespaces: the name of the Linux user who created this user
- namespace.
- type: string
- children:
- description: 'For user and PID namespaces: the list of child namespace IDs.'
- type: array
- items:
- format: int64
- type: integer
- possessions:
- description: 'Only user namespaces: list of namespace IDs of owned (non-user) namespaces.'
- type: array
- items:
- format: int64
- type: integer
- example:
- '4026532338':
- nsid: 4026532338
- type: pid
- owner: 4026531837
- reference: /proc/33536/ns/pid
- leaders:
- - 33536
- parent: 4026531836
- children:
- - 4026532398
- NamespaceType:
- description: |-
- Type of Linux-kernel namespace. For more information about namespaces, please
- see also: https://man7.org/linux/man-pages/man7/namespaces.7.html.
- enum:
- - cgroup
- - ipc
- - net
- - mnt
- - pid
- - user
- - uts
- - time
- type: string
- example: 'net'
- NamespacesDict:
- description: |
- "Dictionary" or "map" of Linux-kernel namespaces, keyed by their namespace IDs in stringified
- form. Contrary to what the term "namespace" might suggest, namespaces do not
- have names but are identified by their (transient) inode numbers.
-
- > **Note:** following current best practice of the Linux kernel and CLI tools,
- > namespace references are only in the form of the inode number, without the
- > device number.
-
- For further details, please see also:
- https://man7.org/linux/man-pages/man7/namespaces.7.html.
- type: object
- additionalProperties:
- $ref: '#/components/schemas/Namespace'
- example:
- '4026532267':
- nsid: 4026532267
- type: mnt
- owner: 4026531837
- reference: /proc/1714/ns/mnt
- leaders:
- - 1714
- '4026532268':
- nsid: 4026532268
- type: mnt
- owner: 4026531837
- reference: /proc/1761/ns/mnt
- leaders:
- - 1761
- DiscoveryOptions:
- title: Root Type for DiscoveryOptions
- description: ''
- required:
- - scanned-namespace-types
- type: object
- properties:
- from-procs:
- type: boolean
- from-tasks:
- type: boolean
- from-fds:
- type: boolean
- from-bindmounts:
- type: boolean
- with-hierarchy:
- type: boolean
- with-ownership:
- type: boolean
- with-freezer:
- description: |-
- true if the discovery of the (effective) freezer states of processes has been
- skipped, so that all processes always appear to be "thawed" (running).
- type: boolean
- scanned-namespace-types:
- description: |-
- List of namespace types included in the discovery. This information might help
- consuming tools to understand which types of namespaces were scanned and which
- were not scanned for at all.
- type: array
- items:
- $ref: '#/components/schemas/NamespaceType'
- with-mounts:
- description: true if mount namespace'd mount paths with mount points were discovered.
- type: boolean
- labels:
- description: |-
- Dictionary of key=value pairs passed to decorators to optionally control the
- decoration of discovered containers.
- example:
- skipped-procs: false
- skipped-tasks: false
- skipped-fds: false
- skipped-bindmounts: false
- skipped-hierarchy: false
- skipped-ownership: false
- skipped-freezer: false
- scanned-namespace-types:
- - time
- - mnt
- - cgroup
- - uts
- - ipc
- - user
- - pid
- - net
- NamespacesSet:
- description: |-
- The set of 7 namespaces (8 namespaces since Linux 5.6+) every process is always
- joined to. The namespaces are referenced by their IDs (inode numbers):
- - cgroup namespace
- - IPC namespace
- - network namespace
- - mount namespace
- - PID namespace
- - user namespace
- - UTS namespace
- - time namespace (Linux kernel 5.6+)
-
- > **Note:** Since lxkns doesn't officially support Linux kernels before 4.9
- > all namespaces except the "time" namespace can safely be assumed to be
- > always present.
-
- For more details about namespaces, please see also:
- https://man7.org/linux/man-pages/man7/namespaces.7.html.
- type: object
- properties:
- cgroup:
- format: int64
- description: |-
- References a cgroup namespace by ID (inode number). Please see also:
- https://www.man7.org/linux/man-pages/man7/cgroup_namespaces.7.html.
- type: integer
- ipc:
- format: int64
- description: |-
- References an IPC namespace by ID (inode number). Please see also:
- https://www.man7.org/linux/man-pages/man7/ipc_namespaces.7.html.
- type: integer
- net:
- format: int64
- description: |-
- References a network namespace by ID (inode number). Please see also:
- https://www.man7.org/linux/man-pages/man7/network_namespaces.7.html.
- type: integer
- mnt:
- format: int64
- description: |-
- References a mount namespace by ID (inode number). Please see also:
- https://www.man7.org/linux/man-pages/man7/mount_namespaces.7.html.
- type: integer
- pid:
- format: int64
- description: |-
- References a PID namespace by ID (inode number). Please see also:
- https://www.man7.org/linux/man-pages/man7/pid_namespaces.7.html.
- type: integer
- user:
- format: int64
- description: |-
- References a user namespace by ID (inode number). Please see also:
- https://www.man7.org/linux/man-pages/man7/user_namespaces.7.html.
- type: integer
- uts:
- format: int64
- description: |-
- References a UTS (*nix timesharing system) namespace by ID (inode number).
- Please see also: https://www.man7.org/linux/man-pages/man7/uts_namespaces.7.html.
- type: integer
- time:
- format: int64
- description: |-
- References a (monotonous) time namespace by ID (inode number). Time namespaces
- are only supported on Linux kernels 5.6 or later. Please see also:
- https://www.man7.org/linux/man-pages/man7/time_namespaces.7.html.
- type: integer
- example:
- mnt: 4026531840
- cgroup: 4026531835
- uts: 4026531838
- ipc: 4026531839
- user: 4026531837
- pid: 4026531836
- net: 4026531905
- MountPoint:
- description: |-
- Information about a mount point as discovered from the proc filesystem. See also
- [proc(5)](https://man7.org/linux/man-pages/man5/procfs.5.html), and details about
- `/proc/[PID]/mountinfo` in particular.
- required:
- - mountid
- - parentid
- - major
- - minor
- - root
- - mountpoint
- - mountoptions
- - tags
- - source
- - fstype
- - superoptions
- - hidden
- type: object
- properties:
- parentid:
- description: |-
- ID of the parent mount. Please note that the parent mount might be outside a
- mount namespace.
- type: integer
- mountid:
- description: 'unique ID for the mount, might be reused after umount(2).'
- type: integer
- major:
- description: major ID for the st_dev for files on this filesystem.
- type: integer
- minor:
- description: minor ID for the st_dev for filed on this filesystem.
- type: integer
- root:
- description: pathname of the directory in the filesystem which forms the root of this mount.
- type: string
- mountpoint:
- description: pathname of the mount point relative to root directory of the process.
- type: string
- mountoptions:
- description: mount options specific to this mount.
- type: array
- items:
- type: string
- tags:
- $ref: '#/components/schemas/MountTags'
- description: |-
- optional tags with even more optional values. Tags cannot be a single hyphen
- "-".
- fstype:
- description: 'filesystem type in the form "type[.subtype]".'
- type: string
- source:
- description: filesystem-specific information or "none".
- type: string
- superoptions:
- description: per-superblock options.
- type: string
- hidden:
- description: |-
- true if this mount point is hidden by an "overmount" either at the same mount
- path or higher up the path hierarchy.
- type: boolean
- MountTags:
- description: |-
- dictionary of mount point tags with optional values. Tag names cannot be a single
- hyphen "-".
- type: object
- additionalProperties:
- type: string
- MountPath:
- description: |-
- path of one or more mount points in the Virtual File System (VFS). In case of
- multiple mount points at the same path, only at most one of them can be visible
- and all others (or all in case of an overmount higher up the path) will be hidden.
- required:
- - mounts
- - pathid
- - parentid
- type: object
- properties:
- mounts:
- description: one or more mount points at this path in the Virtual File System (VFS).
- type: array
- items:
- $ref: '#/components/schemas/MountPoint'
- pathid:
- description: 'unique mount path identifier, per mount namespace.'
- type: integer
- parentid:
- description: 'identifier of parent mount path, if any, otherwise 0.'
- type: integer
- MountPathsDict:
- description: |-
- "Dictionary" or "map" of mount paths with their corresponding mount points, keyed
- by the mount paths.
-
- Please note that additionally the mount path entries are organized in a "sparse"
- hierarchy with the help of mount path identifiers (these are user-space generated
- by lxkns).
- type: object
- additionalProperties:
- $ref: '#/components/schemas/MountPath'
- NamespacedMountPaths:
- description: 'the mount paths of each discovered mount namespace, separated by mount namespace.'
- type: object
- additionalProperties:
- $ref: '#/components/schemas/MountPathsDict'
- Container:
- description: 'Alive container with process(es), either running or paused.'
- required:
- - id
- - name
- - type
- - flavor
- - pid
- - paused
- - labels
- - groups
- - engine
- type: object
- properties:
- id:
- description: Container identifier
- type: string
- name:
- description: 'Container name as opposed to its id, might be the same for some container engines.'
- type: string
- type:
- description: 'Type of container identifier, such as "docker.com", et cetera.'
- type: string
- flavor:
- description: 'Flavor of container, might be the same as the type or different.'
- type: string
- pid:
- description: Process ID of initial container process.
- type: integer
- paused:
- description: Indicates whether the container is running or paused.
- type: boolean
- labels:
- $ref: '#/components/schemas/Labels'
- description: Label name=value pairs attached to this container.
- groups:
- description: |-
- List of group reference identifiers this container is a member of. For instance,
- (Docker) composer projects, Kubernetes pods, ...
- type: array
- items:
- type: integer
- engine:
- description: Reference identifier of the container engine managing this container.
- type: integer
- Labels:
- description: 'Dictionary (map) of KEY=VALUE pairs, with KEY and VALUE both strings.'
- type: object
- additionalProperties:
- type: string
- ContainerEngine:
- description: Information about a container engine managing a set of discovered containers.
- required:
- - id
- - type
- - version
- - api
- - pid
- - containers
- type: object
- properties:
- id:
- description: 'Container engine instance identifier, such as UUID, unique string, et cetera.'
- type: string
- type:
- description: 'Engine type identifier, such as "containerd.io", et cetera.'
- type: string
- version:
- description: 'Engine version information.'
- type: string
- api:
- description: Engine API path.
- type: string
- pid:
- description: 'Engine''s PID (in initial PID namespace) when known, otherwise zero.'
- type: integer
- containers:
- description: List of reference IDs (=PIDs) of containers managed by this engine.
- type: array
- items:
- type: integer
- ContainerGroup:
- description: A group of containers somehow related.
- required:
- - name
- - type
- - flavor
- - containers
- - labels
- type: object
- properties:
- name:
- description: |-
- Name of group, such as a (Docker) composer project name, Kubernetes pod
- namespace/name, et cetera.
- type: string
- type:
- description: Group type identifier.
- type: string
- flavor:
- description: 'Group flavor identifier, might be identical with group type identifier.'
- type: string
- containers:
- description: List of reference IDs (=PIDs) of containers belonging to this group.
- type: array
- items:
- type: integer
- labels:
- $ref: '#/components/schemas/Labels'
- description: Additional KEY=VALUE information.
- ContainerMap:
- description: |-
- Maps container PIDs to containers. Container PIDs are the PIDs of initial
- container processes only, but not any child processes.
- type: object
- additionalProperties:
- $ref: '#/components/schemas/Container'
- ContainerEngineMap:
- description: Maps reference IDs to container engines.
- type: object
- additionalProperties:
- $ref: '#/components/schemas/ContainerEngine'
- ContainerGroupMap:
- description: Maps reference IDs to container groups.
- type: object
- additionalProperties:
- $ref: '#/components/schemas/ContainerGroup'
diff --git a/openapi3/testdata/main.yaml b/openapi3/testdata/main.yaml
deleted file mode 100644
index e973cbecd..000000000
--- a/openapi3/testdata/main.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-openapi: "3.0.0"
-info:
- title: "test file"
- version: "n/a"
-paths:
- /testpath:
- $ref: "testpath.yaml#/paths/~1testpath"
diff --git a/openapi3/testdata/my-openapi.json b/openapi3/testdata/my-openapi.json
deleted file mode 100644
index b75d9ff3e..000000000
--- a/openapi3/testdata/my-openapi.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "openapi": "3.0.2",
- "info": {
- "title": "My API",
- "version": "0.1.0"
- },
- "paths": {
- "/foo": {
- "get": {
- "responses": {
- "200": {
- "$ref": "my-other-openapi.json#/components/responses/DefaultResponse"
- }
- }
- }
- }
- }
-}
diff --git a/openapi3/testdata/my-other-openapi.json b/openapi3/testdata/my-other-openapi.json
deleted file mode 100644
index 0c92486b3..000000000
--- a/openapi3/testdata/my-other-openapi.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "openapi": "3.0.2",
- "info": {
- "title": "My other API",
- "version": "0.1.0"
- },
- "components": {
- "schemas": {
- "DefaultObject": {
- "type": "object",
- "properties": {
- "foo": {
- "type": "string"
- },
- "bar": {
- "type": "integer"
- }
- }
- }
- },
- "responses": {
- "DefaultResponse": {
- "description": "",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/DefaultObject"
- }
- }
- }
- }
- }
- }
-}
diff --git a/openapi3/testdata/recursiveRef/components/Bar.yml b/openapi3/testdata/recursiveRef/components/Bar.yml
deleted file mode 100644
index cc59fc27b..000000000
--- a/openapi3/testdata/recursiveRef/components/Bar.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-type: string
-example: bar
diff --git a/openapi3/testdata/recursiveRef/components/Cat.yml b/openapi3/testdata/recursiveRef/components/Cat.yml
deleted file mode 100644
index c476aa1a5..000000000
--- a/openapi3/testdata/recursiveRef/components/Cat.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-type: object
-properties:
- cat:
- $ref: ../openapi.yml#/components/schemas/Cat
diff --git a/openapi3/testdata/recursiveRef/components/Foo.yml b/openapi3/testdata/recursiveRef/components/Foo.yml
deleted file mode 100644
index 53a233666..000000000
--- a/openapi3/testdata/recursiveRef/components/Foo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-type: object
-properties:
- bar:
- $ref: ../openapi.yml#/components/schemas/Bar
diff --git a/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml b/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml
deleted file mode 100644
index aeac81f48..000000000
--- a/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-type: object
-properties:
- foo:
- $ref: ../../openapi.yml#/components/schemas/Foo
diff --git a/openapi3/testdata/recursiveRef/components/models/error.yaml b/openapi3/testdata/recursiveRef/components/models/error.yaml
deleted file mode 100644
index b4d404793..000000000
--- a/openapi3/testdata/recursiveRef/components/models/error.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-type: object
-title: ErrorDetails
diff --git a/openapi3/testdata/recursiveRef/issue615.yml b/openapi3/testdata/recursiveRef/issue615.yml
deleted file mode 100644
index d1370e32e..000000000
--- a/openapi3/testdata/recursiveRef/issue615.yml
+++ /dev/null
@@ -1,60 +0,0 @@
-openapi: "3.0.3"
-info:
- title: Deep recursive cyclic refs example
- version: "1.0"
-paths:
- /foo:
- $ref: ./paths/foo.yml
-components:
- schemas:
- FilterColumnIncludes:
- type: object
- properties:
- $includes:
- $ref: '#/components/schemas/FilterPredicate'
- additionalProperties: false
- maxProperties: 1
- minProperties: 1
- FilterPredicate:
- oneOf:
- - $ref: '#/components/schemas/FilterValue'
- - type: array
- items:
- $ref: '#/components/schemas/FilterPredicate'
- minLength: 1
- - $ref: '#/components/schemas/FilterPredicateOp'
- - $ref: '#/components/schemas/FilterPredicateRangeOp'
- FilterPredicateOp:
- type: object
- properties:
- $any:
- oneOf:
- - type: array
- items:
- $ref: '#/components/schemas/FilterPredicate'
- $none:
- oneOf:
- - $ref: '#/components/schemas/FilterPredicate'
- - type: array
- items:
- $ref: '#/components/schemas/FilterPredicate'
- additionalProperties: false
- maxProperties: 1
- minProperties: 1
- FilterPredicateRangeOp:
- type: object
- properties:
- $lt:
- $ref: '#/components/schemas/FilterRangeValue'
- additionalProperties: false
- maxProperties: 2
- minProperties: 2
- FilterRangeValue:
- oneOf:
- - type: number
- - type: string
- FilterValue:
- oneOf:
- - type: number
- - type: string
- - type: boolean
\ No newline at end of file
diff --git a/openapi3/testdata/recursiveRef/openapi.yml b/openapi3/testdata/recursiveRef/openapi.yml
deleted file mode 100644
index 9f884c710..000000000
--- a/openapi3/testdata/recursiveRef/openapi.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-openapi: "3.0.3"
-info:
- title: Recursive refs example
- version: "1.0"
-paths:
- /foo:
- $ref: ./paths/foo.yml
- /double-ref-foo:
- get:
- summary: Double ref response
- description: Reference response with double reference.
- responses:
- "400":
- $ref: "#/components/responses/400"
-components:
- schemas:
- Foo:
- $ref: ./components/Foo.yml
- Foo2:
- $ref: ./components/Foo/Foo2.yml
- Bar:
- $ref: ./components/Bar.yml
- Cat:
- $ref: ./components/Cat.yml
- Error:
- $ref: ./components/models/error.yaml
- responses:
- "400":
- description: 400 Bad Request
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Error"
diff --git a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml
deleted file mode 100644
index 0d508527a..000000000
--- a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml
+++ /dev/null
@@ -1,110 +0,0 @@
-{
- "components": {
- "parameters": {
- "number": {
- "in": "query",
- "name": "someNumber",
- "schema": {
- "type": "string"
- }
- }
- },
- "responses": {
- "400": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Error"
- }
- }
- },
- "description": "400 Bad Request"
- }
- },
- "schemas": {
- "Bar": {
- "example": "bar",
- "type": "string"
- },
- "Error":{
- "title":"ErrorDetails",
- "type":"object"
- },
- "Foo": {
- "properties": {
- "bar": {
- "$ref": "#/components/schemas/Bar"
- }
- },
- "type": "object"
- },
- "Foo2": {
- "properties": {
- "foo": {
- "$ref": "#/components/schemas/Foo"
- }
- },
- "type": "object"
- },
- "error":{
- "title":"ErrorDetails",
- "type":"object"
- },
- "Cat": {
- "properties": {
- "cat": {
- "$ref": "#/components/schemas/Cat"
- }
- },
- "type": "object"
- }
- }
- },
- "info": {
- "title": "Recursive refs example",
- "version": "1.0"
- },
- "openapi": "3.0.3",
- "paths": {
- "/double-ref-foo": {
- "get": {
- "description": "Reference response with double reference.",
- "responses": {
- "400": {
- "$ref": "#/components/responses/400"
- }
- },
- "summary": "Double ref response"
- }
- },
- "/foo": {
- "get": {
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "properties": {
- "foo2": {
- "$ref": "#/components/schemas/Foo2"
- }
- },
- "type": "object"
- }
- }
- },
- "description": "OK"
- },
- "400": {
- "$ref": "#/components/responses/400"
- }
- }
- },
- "parameters": [
- {
- "$ref": "#/components/parameters/number"
- }
- ]
- }
- }
-}
diff --git a/openapi3/testdata/recursiveRef/parameters/number.yml b/openapi3/testdata/recursiveRef/parameters/number.yml
deleted file mode 100644
index 29f0f2640..000000000
--- a/openapi3/testdata/recursiveRef/parameters/number.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-name: someNumber
-in: query
-schema:
- type: string
diff --git a/openapi3/testdata/recursiveRef/paths/foo.yml b/openapi3/testdata/recursiveRef/paths/foo.yml
deleted file mode 100644
index 4c845b532..000000000
--- a/openapi3/testdata/recursiveRef/paths/foo.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-parameters:
- - $ref: ../parameters/number.yml
-get:
- responses:
- "200":
- description: OK
- content:
- application/json:
- schema:
- type: object
- properties:
- foo2:
- $ref: ../openapi.yml#/components/schemas/Foo2
- "400":
- $ref: "../openapi.yml#/components/responses/400"
diff --git a/openapi3/testdata/refInLocalRef/messages/data.json b/openapi3/testdata/refInLocalRef/messages/data.json
deleted file mode 100644
index cfdc18efb..000000000
--- a/openapi3/testdata/refInLocalRef/messages/data.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int32"
- },
- "ref_prop_part": {
- "$ref": "./dataPart.json"
- }
- }
-}
diff --git a/openapi3/testdata/refInLocalRef/messages/dataPart.json b/openapi3/testdata/refInLocalRef/messages/dataPart.json
deleted file mode 100644
index 9ecb5850a..000000000
--- a/openapi3/testdata/refInLocalRef/messages/dataPart.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "type": "object",
- "properties": {
- "idPart": {
- "type": "integer",
- "format": "int64"
- }
- }
-}
diff --git a/openapi3/testdata/refInLocalRef/messages/request.json b/openapi3/testdata/refInLocalRef/messages/request.json
deleted file mode 100644
index 7225ff190..000000000
--- a/openapi3/testdata/refInLocalRef/messages/request.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "type": "object",
- "required": [
- "definition_reference"
- ],
- "properties": {
- "definition_reference": {
- "$ref": "./data.json"
- }
- }
-}
diff --git a/openapi3/testdata/refInLocalRef/messages/response.json b/openapi3/testdata/refInLocalRef/messages/response.json
deleted file mode 100644
index b636f528b..000000000
--- a/openapi3/testdata/refInLocalRef/messages/response.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int32"
- }
- }
-}
diff --git a/openapi3/testdata/refInLocalRef/openapi.json b/openapi3/testdata/refInLocalRef/openapi.json
deleted file mode 100644
index f0c9915c7..000000000
--- a/openapi3/testdata/refInLocalRef/openapi.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "openapi": "3.0.3",
- "info": {
- "title": "Reference in reference example",
- "version": "1.0.0"
- },
- "paths": {
- "/api/test/ref/in/ref": {
- "post": {
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties" : {
- "data": {
- "$ref": "#/components/schemas/Request"
- }
- }
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "messages/response.json"
- }
- }
- }
- }
- }
- }
- }
- },
- "components": {
- "schemas": {
- "Request": {
- "$ref": "messages/request.json"
- }
- }
- }
-}
diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/data.json b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/data.json
deleted file mode 100644
index cfdc18efb..000000000
--- a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/data.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int32"
- },
- "ref_prop_part": {
- "$ref": "./dataPart.json"
- }
- }
-}
diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/dataPart.json b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/dataPart.json
deleted file mode 100644
index 9ecb5850a..000000000
--- a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/dataPart.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "type": "object",
- "properties": {
- "idPart": {
- "type": "integer",
- "format": "int64"
- }
- }
-}
diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/request.json b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/request.json
deleted file mode 100644
index 7225ff190..000000000
--- a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/request.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "type": "object",
- "required": [
- "definition_reference"
- ],
- "properties": {
- "definition_reference": {
- "$ref": "./data.json"
- }
- }
-}
diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/response.json b/openapi3/testdata/refInLocalRefInParentsSubdir/messages/response.json
deleted file mode 100644
index b636f528b..000000000
--- a/openapi3/testdata/refInLocalRefInParentsSubdir/messages/response.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int32"
- }
- }
-}
diff --git a/openapi3/testdata/refInLocalRefInParentsSubdir/spec/openapi.json b/openapi3/testdata/refInLocalRefInParentsSubdir/spec/openapi.json
deleted file mode 100644
index 0bf9bd36e..000000000
--- a/openapi3/testdata/refInLocalRefInParentsSubdir/spec/openapi.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "openapi": "3.0.3",
- "info": {
- "title": "Reference in reference example",
- "version": "1.0.0"
- },
- "paths": {
- "/api/test/ref/in/ref": {
- "post": {
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "../messages/request.json"
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "ref_prop": {
- "$ref": "#/components/schemas/Data"
- }
- }
- }
- }
- }
- }
- }
- }
- }
- },
- "components": {
- "schemas": {
- "Data": {
- "$ref": "../messages/data.json"
- }
- }
- }
-}
diff --git a/openapi3/testdata/refInRef/messages/definitions.json b/openapi3/testdata/refInRef/messages/definitions.json
deleted file mode 100644
index 78b942836..000000000
--- a/openapi3/testdata/refInRef/messages/definitions.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "definitions": {
- "External": {
- "type": "string"
- }
- }
-}
diff --git a/openapi3/testdata/refInRef/messages/request.json b/openapi3/testdata/refInRef/messages/request.json
deleted file mode 100644
index 10ff329bc..000000000
--- a/openapi3/testdata/refInRef/messages/request.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "type": "object",
- "required": [
- "definition_reference"
- ],
- "properties": {
- "definition_reference": {
- "$ref": "definitions.json#/definitions/External"
- }
- }
-}
diff --git a/openapi3/testdata/refInRef/messages/response.json b/openapi3/testdata/refInRef/messages/response.json
deleted file mode 100644
index b636f528b..000000000
--- a/openapi3/testdata/refInRef/messages/response.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int32"
- }
- }
-}
diff --git a/openapi3/testdata/refInRef/openapi.json b/openapi3/testdata/refInRef/openapi.json
deleted file mode 100644
index 0e9a5b1be..000000000
--- a/openapi3/testdata/refInRef/openapi.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "openapi": "3.0.3",
- "info": {
- "title": "Reference in reference example",
- "version": "1.0.0"
- },
- "paths": {
- "/api/test/ref/in/ref": {
- "post": {
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "messages/request.json"
- }
- }
- }
- },
- "responses": {
- "200": {
- "description": "Successful response",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "messages/response.json"
- }
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json b/openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json
deleted file mode 100644
index 47547ab08..000000000
--- a/openapi3/testdata/refInRefInProperty/common-data-objects/problem-details-0.0.1.schema.json
+++ /dev/null
@@ -1,93 +0,0 @@
-{
- "title": "Problem details",
- "description": "Common data object for describing an error details",
- "type": "object",
- "additionalProperties": false,
- "required": [
- "type",
- "title",
- "status"
- ],
- "properties": {
- "type": {
- "type": "string",
- "description": "Unique error code",
- "minLength": 1,
- "example": "unauthorized",
- "x-docs-examples": [
- "validation-error",
- "unauthorized",
- "forbidden",
- "internal-server-error",
- "wrong-basket",
- "not-found"
- ]
- },
- "title": {
- "type": "string",
- "description": "Human readable error message",
- "minLength": 1,
- "example": "Your request parameters didn't validate",
- "x-docs-examples": [
- "Your request parameters didn't validate",
- "Requested resource is not available",
- "Internal server error"
- ]
- },
- "status": {
- "type": "integer",
- "description": "HTTP status code",
- "maximum": 599,
- "minimum": 100,
- "example": 200,
- "x-docs-examples": [
- "200",
- "201",
- "400",
- "503"
- ]
- },
- "detail": {
- "type": "string",
- "description": "Human readable error description. Only for human",
- "example": "Basket must have more then 1 item",
- "x-docs-examples": [
- "Basket must have more then 1 item"
- ]
- },
- "invalid-params": {
- "type": "array",
- "description": "Param list with errors",
- "items": {
- "type": "object",
- "additionalProperties": false,
- "required": [
- "name",
- "reason"
- ],
- "properties": {
- "name": {
- "type": "string",
- "description": "field name",
- "minLength": 1,
- "example": "age",
- "x-docs-examples": [
- "age",
- "color"
- ]
- },
- "reason": {
- "type": "string",
- "description": "Field validation error text",
- "minLength": 1,
- "example": "must be a positive integer",
- "x-docs-examples": [
- "must be a positive integer",
- "must be 'green', 'red' or 'blue'"
- ]
- }
- }
- }
- }
- }
-}
diff --git a/openapi3/testdata/refInRefInProperty/components/errors.yaml b/openapi3/testdata/refInRefInProperty/components/errors.yaml
deleted file mode 100644
index 1dc8fa7e3..000000000
--- a/openapi3/testdata/refInRefInProperty/components/errors.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-components:
- schemas:
- Error:
- type: object
- description: Error info in problem-details-0.0.1 format
- properties:
- error:
- $ref: "../common-data-objects/problem-details-0.0.1.schema.json"
-
- responses:
- 400:
- description: ""
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Error"
- examples:
- json:
- $ref: "#/components/examples/BadRequest"
- 401:
- description: ""
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Error"
- examples:
- json:
- $ref: "#/components/examples/Unauthorized"
- 500:
- description: ""
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/Error"
- examples:
- json:
- $ref: "#/components/examples/InternalServerError"
-
- examples:
- BadRequest:
- summary: Wrong format
- value:
- error:
- type: validation-error
- title: Your request parameters didn't validate.
- status: 400
- invalid-params:
- - name: age
- reason: must be a positive integer
- - name: color
- reason: must be 'green', 'red' or 'blue'
- Unauthorized:
- summary: Not authenticated
- value:
- error:
- type: unauthorized
- title: The request has not been applied because it lacks valid authentication credentials
- for the target resource.
- status: 401
- InternalServerError:
- summary: Not handled internal server error
- value:
- error:
- type: internal-server-error
- title: Internal server error.
- status: 500
diff --git a/openapi3/testdata/refInRefInProperty/openapi.yaml b/openapi3/testdata/refInRefInProperty/openapi.yaml
deleted file mode 100644
index d44b21687..000000000
--- a/openapi3/testdata/refInRefInProperty/openapi.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-openapi: 3.0.3
-
-info:
- title: "Reference in reference in property example"
- version: "1.0.0"
-paths:
- /api/test/ref/in/ref/in/property:
- post:
- requestBody:
- content:
- multipart/form-data:
- schema:
- type: object
- properties:
- file:
- type: array
- items:
- type: string
- format: binary
- required: true
- responses:
- 200:
- description: "Files are saved successfully"
- 400:
- $ref: "./components/errors.yaml#/components/responses/400"
- 401:
- $ref: "./components/errors.yaml#/components/responses/401"
- 500:
- $ref: "./components/errors.yaml#/components/responses/500"
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml
index e5cf2b15b..9d12ac352 100644
--- a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml
@@ -1 +1 @@
-description: header
+description: header
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml
deleted file mode 100644
index 4a5d8f994..000000000
--- a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml
+++ /dev/null
@@ -1 +0,0 @@
-description: header1
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml
deleted file mode 100644
index 532e79203..000000000
--- a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-header:
- description: header1
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml
deleted file mode 100644
index 71536c564..000000000
--- a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml
+++ /dev/null
@@ -1 +0,0 @@
-description: header2
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml
deleted file mode 100644
index c14ae4710..000000000
--- a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-header:
- description: header2
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml
index 6e608808c..45046c421 100644
--- a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml
@@ -10,10 +10,6 @@ patch:
headers:
X-Rate-Limit-Reset:
$ref: "../../../CustomTestHeader.yml"
- X-Another:
- $ref: ../../../CustomTestHeader1.yml
- X-And-Another:
- $ref: ../../../CustomTestHeader2.yml
content:
application/json:
schema:
diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml
index 35c5ccf51..647852fc5 100644
--- a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml
+++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml
@@ -10,10 +10,6 @@ patch:
headers:
X-Rate-Limit-Reset:
$ref: "../../../../CustomTestHeader.yml"
- X-Another:
- $ref: '../../../../CustomTestHeader1bis.yml#/header'
- X-And-Another:
- $ref: '../../../../CustomTestHeader2bis.yml#/header'
content:
application/json:
schema:
diff --git a/openapi3/testdata/schema618.yml b/openapi3/testdata/schema618.yml
deleted file mode 100644
index 1ab400075..000000000
--- a/openapi3/testdata/schema618.yml
+++ /dev/null
@@ -1,62 +0,0 @@
-components:
- schemas:
- Account:
- required:
- - name
- - nature
- type: object
- properties:
- id:
- type: string
- name:
- type: string
- description:
- type: string
- type:
- type: string
- enum:
- - assets
- - liabilities
- nature:
- type: string
- enum:
- - asset
- - liability
- Record:
- required:
- - account
- - concept
- type: object
- properties:
- account:
- $ref: "#/components/schemas/Account"
- concept:
- type: string
- partial:
- type: number
- credit:
- type: number
- debit:
- type: number
- JournalEntry:
- required:
- - type
- - creationDate
- - records
- type: object
- properties:
- id:
- type: string
- type:
- type: string
- enum:
- - daily
- - ingress
- - egress
- creationDate:
- type: string
- format: date
- records:
- type: array
- items:
- $ref: "#/components/schemas/Record"
diff --git a/openapi3/testdata/spec.yaml b/openapi3/testdata/spec.yaml
deleted file mode 100644
index 781312f8e..000000000
--- a/openapi3/testdata/spec.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-openapi: 3.0.1
-info:
- version: 1.0.0
- title: Some Swagger
- license:
- name: MIT
-paths: {}
-components:
- schemas:
- Test:
- type: object
- properties:
- test:
- $ref: 'ext.json#/definitions/b'
diff --git a/openapi3/testdata/spec.yaml.internalized.yml b/openapi3/testdata/spec.yaml.internalized.yml
deleted file mode 100644
index feca4a00c..000000000
--- a/openapi3/testdata/spec.yaml.internalized.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "components": {
- "schemas": {
- "Test": {
- "properties": {
- "test": {
- "$ref": "#/components/schemas/b"
- }
- },
- "type": "object"
- },
- "a": {
- "type": "string"
- },
- "b": {
- "description": "I use a local reference.",
- "properties": {
- "name": {
- "$ref": "#/components/schemas/a"
- }
- },
- "type": "object"
- }
- }
- },
- "info": {
- "license": {
- "name": "MIT"
- },
- "title": "Some Swagger",
- "version": "1.0.0"
- },
- "openapi": "3.0.1",
- "paths": {
- }
-}
diff --git a/openapi3/testdata/testpath.yaml b/openapi3/testdata/testpath.yaml
deleted file mode 100644
index de85bb418..000000000
--- a/openapi3/testdata/testpath.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-paths:
- /testpath:
- get:
- responses:
- "200":
- $ref: "#/components/responses/testpath_200_response"
-
-components:
- responses:
- testpath_200_response:
- description: a custom response
- content:
- application/json:
- schema:
- type: string
diff --git a/openapi3/testdata/testref.openapi.json b/openapi3/testdata/testref.openapi.json
index f25ffbd3a..5beb61e34 100644
--- a/openapi3/testdata/testref.openapi.json
+++ b/openapi3/testdata/testref.openapi.json
@@ -1,8 +1,7 @@
{
"openapi": "3.0.0",
"info": {
- "title": "OAI Specification w/ refs in JSON",
- "x-my-extension": {"k": 42},
+ "title": "",
"version": "1"
},
"paths": {},
diff --git a/openapi3/testdata/testref.openapi.yml b/openapi3/testdata/testref.openapi.yml
index eace2456a..fe755936c 100644
--- a/openapi3/testdata/testref.openapi.yml
+++ b/openapi3/testdata/testref.openapi.yml
@@ -2,10 +2,9 @@
openapi: 3.0.0
info:
title: 'OAI Specification w/ refs in YAML'
- # x-my-extension: {k: 42},
version: '1'
paths: {}
components:
schemas:
AnotherTestSchema:
- $ref: 'components.openapi.yml#/components/schemas/CustomTestSchema'
+ "$ref": components.openapi.yml#/components/schemas/CustomTestSchema
diff --git a/openapi3/testdata/testref.openapi.yml.internalized.yml b/openapi3/testdata/testref.openapi.yml.internalized.yml
deleted file mode 100644
index e35a50041..000000000
--- a/openapi3/testdata/testref.openapi.yml.internalized.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "components": {
- "schemas": {
- "AnotherTestSchema": {
- "type": "string"
- },
- "CustomTestSchema": {
- "type": "string"
- }
- }
- },
- "info": {
- "title": "OAI Specification w/ refs in YAML",
- "version": "1"
- },
- "openapi": "3.0.0",
- "paths": {
- }
-}
diff --git a/openapi3/unique_items_checker_test.go b/openapi3/unique_items_checker_test.go
deleted file mode 100644
index 270b797e1..000000000
--- a/openapi3/unique_items_checker_test.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package openapi3_test
-
-import (
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestRegisterArrayUniqueItemsChecker(t *testing.T) {
- var (
- schema = openapi3.Schema{
- Type: "array",
- UniqueItems: true,
- Items: openapi3.NewStringSchema().NewRef(),
- }
- val = []interface{}{"1", "2", "3"}
- )
-
- // Fist checked by predefined function
- err := schema.VisitJSON(val)
- require.NoError(t, err)
-
- // Register a function will always return false when check if a
- // slice has unique items, then use a slice indeed has unique
- // items to verify that check unique items will failed.
- openapi3.RegisterArrayUniqueItemsChecker(func(items []interface{}) bool {
- return false
- })
- defer openapi3.RegisterArrayUniqueItemsChecker(nil) // Reset for other tests
-
- err = schema.VisitJSON(val)
- require.Error(t, err)
- require.True(t, strings.HasPrefix(err.Error(), "duplicate items found"))
-}
diff --git a/openapi3/validation_issue409_test.go b/openapi3/validation_issue409_test.go
deleted file mode 100644
index 561594fca..000000000
--- a/openapi3/validation_issue409_test.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package openapi3_test
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func TestIssue409PatternIgnored(t *testing.T) {
- l := openapi3.NewLoader()
- s, err := l.LoadFromFile("testdata/issue409.yml")
- require.NoError(t, err)
-
- err = s.Validate(l.Context, openapi3.DisableSchemaPatternValidation())
- assert.NoError(t, err)
-}
-
-func TestIssue409PatternNotIgnored(t *testing.T) {
- l := openapi3.NewLoader()
- s, err := l.LoadFromFile("testdata/issue409.yml")
- require.NoError(t, err)
-
- err = s.Validate(l.Context)
- assert.Error(t, err)
-}
-
-func TestIssue409HygienicUseOfCtx(t *testing.T) {
- l := openapi3.NewLoader()
- doc, err := l.LoadFromFile("testdata/issue409.yml")
- require.NoError(t, err)
-
- err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation())
- assert.NoError(t, err)
- err = doc.Validate(l.Context)
- assert.Error(t, err)
-
- // and the other way
-
- l = openapi3.NewLoader()
- doc, err = l.LoadFromFile("testdata/issue409.yml")
- require.NoError(t, err)
-
- err = doc.Validate(l.Context)
- assert.Error(t, err)
- err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation())
- assert.NoError(t, err)
-}
diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go
deleted file mode 100644
index 0ca12e5ab..000000000
--- a/openapi3/validation_options.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package openapi3
-
-import "context"
-
-// ValidationOption allows the modification of how the OpenAPI document is validated.
-type ValidationOption func(options *ValidationOptions)
-
-// ValidationOptions provides configuration for validating OpenAPI documents.
-type ValidationOptions struct {
- examplesValidationAsReq, examplesValidationAsRes bool
- examplesValidationDisabled bool
- schemaDefaultsValidationDisabled bool
- schemaFormatValidationEnabled bool
- schemaPatternValidationDisabled bool
- extraSiblingFieldsAllowed map[string]struct{}
-}
-
-type validationOptionsKey struct{}
-
-// AllowExtraSiblingFields called as AllowExtraSiblingFields("description") makes Validate not return an error when said field appears next to a $ref.
-func AllowExtraSiblingFields(fields ...string) ValidationOption {
- return func(options *ValidationOptions) {
- for _, field := range fields {
- if options.extraSiblingFieldsAllowed == nil {
- options.extraSiblingFieldsAllowed = make(map[string]struct{}, len(fields))
- }
- options.extraSiblingFieldsAllowed[field] = struct{}{}
- }
- }
-}
-
-// EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification.
-// By default, schema format validation is disabled.
-func EnableSchemaFormatValidation() ValidationOption {
- return func(options *ValidationOptions) {
- options.schemaFormatValidationEnabled = true
- }
-}
-
-// DisableSchemaFormatValidation does the opposite of EnableSchemaFormatValidation.
-// By default, schema format validation is disabled.
-func DisableSchemaFormatValidation() ValidationOption {
- return func(options *ValidationOptions) {
- options.schemaFormatValidationEnabled = false
- }
-}
-
-// EnableSchemaPatternValidation does the opposite of DisableSchemaPatternValidation.
-// By default, schema pattern validation is enabled.
-func EnableSchemaPatternValidation() ValidationOption {
- return func(options *ValidationOptions) {
- 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
- }
-}
-
-// 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
- }
-}
-
-// EnableExamplesValidation does the opposite of DisableExamplesValidation.
-// By default, all schema examples are validated.
-func EnableExamplesValidation() ValidationOption {
- return func(options *ValidationOptions) {
- options.examplesValidationDisabled = false
- }
-}
-
-// DisableExamplesValidation disables all example schema validation.
-// By default, all schema examples are validated.
-func DisableExamplesValidation() ValidationOption {
- return func(options *ValidationOptions) {
- options.examplesValidationDisabled = true
- }
-}
-
-// WithValidationOptions allows adding validation options to a context object that can be used when validationg any OpenAPI type.
-func WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context {
- if len(opts) == 0 {
- return ctx
- }
- options := &ValidationOptions{}
- for _, opt := range opts {
- opt(options)
- }
- return context.WithValue(ctx, validationOptionsKey{}, options)
-}
-
-func getValidationOptions(ctx context.Context) *ValidationOptions {
- if options, ok := ctx.Value(validationOptionsKey{}).(*ValidationOptions); ok {
- return options
- }
- return &ValidationOptions{}
-}
diff --git a/openapi3/visited.go b/openapi3/visited.go
deleted file mode 100644
index 67f970e36..000000000
--- a/openapi3/visited.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package openapi3
-
-func newVisited() visitedComponent {
- return visitedComponent{
- header: make(map[*Header]struct{}),
- schema: make(map[*Schema]struct{}),
- }
-}
-
-type visitedComponent struct {
- header map[*Header]struct{}
- schema map[*Schema]struct{}
-}
-
-// resetVisited clears visitedComponent map
-// should be called before recursion over doc *T
-func (doc *T) resetVisited() {
- doc.visited = newVisited()
-}
-
-// isVisitedHeader returns `true` if the *Header pointer was already visited
-// otherwise it returns `false`
-func (doc *T) isVisitedHeader(h *Header) bool {
- if _, ok := doc.visited.header[h]; ok {
- return true
- }
-
- doc.visited.header[h] = struct{}{}
- return false
-}
-
-// isVisitedHeader returns `true` if the *Schema pointer was already visited
-// otherwise it returns `false`
-func (doc *T) isVisitedSchema(s *Schema) bool {
- if _, ok := doc.visited.schema[s]; ok {
- return true
- }
-
- doc.visited.schema[s] = struct{}{}
- return false
-}
diff --git a/openapi3/xml.go b/openapi3/xml.go
deleted file mode 100644
index 34ed3be32..000000000
--- a/openapi3/xml.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package openapi3
-
-import (
- "context"
- "encoding/json"
-)
-
-// XML is specified by OpenAPI/Swagger standard version 3.
-// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object
-type XML struct {
- Extensions map[string]interface{} `json:"-" yaml:"-"`
-
- Name string `json:"name,omitempty" yaml:"name,omitempty"`
- Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
- Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
- Attribute bool `json:"attribute,omitempty" yaml:"attribute,omitempty"`
- Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"`
-}
-
-// MarshalJSON returns the JSON encoding of XML.
-func (xml XML) MarshalJSON() ([]byte, error) {
- m := make(map[string]interface{}, 5+len(xml.Extensions))
- for k, v := range xml.Extensions {
- m[k] = v
- }
- if x := xml.Name; x != "" {
- m["name"] = x
- }
- if x := xml.Namespace; x != "" {
- m["namespace"] = x
- }
- if x := xml.Prefix; x != "" {
- m["prefix"] = x
- }
- if x := xml.Attribute; x {
- m["attribute"] = x
- }
- if x := xml.Wrapped; x {
- m["wrapped"] = x
- }
- return json.Marshal(m)
-}
-
-// UnmarshalJSON sets XML to a copy of data.
-func (xml *XML) UnmarshalJSON(data []byte) error {
- type XMLBis XML
- var x XMLBis
- if err := json.Unmarshal(data, &x); err != nil {
- return err
- }
- _ = json.Unmarshal(data, &x.Extensions)
- delete(x.Extensions, "name")
- delete(x.Extensions, "namespace")
- delete(x.Extensions, "prefix")
- delete(x.Extensions, "attribute")
- delete(x.Extensions, "wrapped")
- *xml = XML(x)
- return nil
-}
-
-// Validate returns an error if XML does not comply with the OpenAPI spec.
-func (xml *XML) Validate(ctx context.Context, opts ...ValidationOption) error {
- ctx = WithValidationOptions(ctx, opts...)
-
- return validateExtensions(ctx, xml.Extensions)
-}
diff --git a/openapi3filter/authentication_input.go b/openapi3filter/authentication_input.go
index a53484b99..bae7c43d3 100644
--- a/openapi3filter/authentication_input.go
+++ b/openapi3filter/authentication_input.go
@@ -2,6 +2,7 @@ package openapi3filter
import (
"fmt"
+ "strings"
"github.com/getkin/kin-openapi/openapi3"
)
@@ -15,15 +16,19 @@ type AuthenticationInput struct {
func (input *AuthenticationInput) NewError(err error) error {
if err == nil {
- if len(input.Scopes) == 0 {
- err = fmt.Errorf("security requirement %q failed", input.SecuritySchemeName)
+ scopes := input.Scopes
+ if len(scopes) == 0 {
+ err = fmt.Errorf("Security requirement '%s' failed",
+ input.SecuritySchemeName)
} else {
- err = fmt.Errorf("security requirement %q (scopes: %+v) failed", input.SecuritySchemeName, input.Scopes)
+ err = fmt.Errorf("Security requirement '%s' (scopes: '%s') failed",
+ input.SecuritySchemeName,
+ strings.Join(input.Scopes, "', '"))
}
}
return &RequestError{
Input: input.RequestValidationInput,
- Reason: "authorization failed",
+ Reason: "Authorization failed",
Err: err,
}
}
diff --git a/openapi3filter/csv_file_upload_test.go b/openapi3filter/csv_file_upload_test.go
deleted file mode 100644
index 89efb96d9..000000000
--- a/openapi3filter/csv_file_upload_test.go
+++ /dev/null
@@ -1,127 +0,0 @@
-package openapi3filter_test
-
-import (
- "bytes"
- "context"
- "io"
- "mime/multipart"
- "net/http"
- "net/textproto"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestValidateCsvFileUpload(t *testing.T) {
- const spec = `
-openapi: 3.0.0
-info:
- title: 'Validator'
- version: 0.0.1
-paths:
- /test:
- post:
- requestBody:
- required: true
- content:
- multipart/form-data:
- schema:
- type: object
- required:
- - file
- properties:
- file:
- type: string
- format: string
- responses:
- '200':
- description: Created
-`
-
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
-
- tests := []struct {
- csvData string
- wantErr bool
- }{
- {
- `foo,bar`,
- false,
- },
- {
- `"foo","bar"`,
- false,
- },
- {
- `foo,bar
-baz,qux`,
- false,
- },
- {
- `foo,bar
-baz,qux,quux`,
- true,
- },
- {
- `"""`,
- true,
- },
- }
- for _, tt := range tests {
- body := &bytes.Buffer{}
- writer := multipart.NewWriter(body)
-
- { // Add file data
- h := make(textproto.MIMEHeader)
- h.Set("Content-Disposition", `form-data; name="file"; filename="hello.csv"`)
- h.Set("Content-Type", "text/csv")
-
- fw, err := writer.CreatePart(h)
- require.NoError(t, err)
- _, err = io.Copy(fw, strings.NewReader(tt.csvData))
-
- require.NoError(t, err)
- }
-
- writer.Close()
-
- req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes()))
- require.NoError(t, err)
-
- req.Header.Set("Content-Type", writer.FormDataContentType())
-
- route, pathParams, err := router.FindRoute(req)
- require.NoError(t, err)
-
- if err = openapi3filter.ValidateRequestBody(
- context.Background(),
- &openapi3filter.RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- },
- route.Operation.RequestBody.Value,
- ); err != nil {
- if !tt.wantErr {
- t.Errorf("got %v", err)
- }
- continue
- }
- if tt.wantErr {
- t.Errorf("want err")
- }
- }
-}
diff --git a/openapi3filter/errors.go b/openapi3filter/errors.go
index b5454a75c..9b46ebda6 100644
--- a/openapi3filter/errors.go
+++ b/openapi3filter/errors.go
@@ -1,58 +1,69 @@
package openapi3filter
import (
- "bytes"
+ "errors"
"fmt"
+ "net/http"
"github.com/getkin/kin-openapi/openapi3"
)
-var _ error = &RequestError{}
+var (
+ errRouteMissingSwagger = errors.New("Route is missing OpenAPI specification")
+ errRouteMissingOperation = errors.New("Route is missing OpenAPI operation")
+ ErrAuthenticationServiceMissing = errors.New("Request validator doesn't have an authentication service defined")
+)
+
+type RouteError struct {
+ Route Route
+ Reason string
+}
+
+func (err *RouteError) Error() string {
+ return err.Reason
+}
-// RequestError is returned by ValidateRequest when request does not match OpenAPI spec
type RequestError struct {
Input *RequestValidationInput
Parameter *openapi3.Parameter
RequestBody *openapi3.RequestBody
+ Status int
Reason string
Err error
}
-var _ interface{ Unwrap() error } = RequestError{}
+func (err *RequestError) HTTPStatus() int {
+ status := err.Status
+ if status == 0 {
+ status = http.StatusBadRequest
+ }
+ return status
+}
func (err *RequestError) Error() string {
reason := err.Reason
if e := err.Err; e != nil {
- if len(reason) == 0 || reason == e.Error() {
+ if len(reason) == 0 {
reason = e.Error()
} else {
reason += ": " + e.Error()
}
}
if v := err.Parameter; v != nil {
- return fmt.Sprintf("parameter %q in %s has an error: %s", v.Name, v.In, reason)
+ return fmt.Sprintf("Parameter '%s' in %s has an error: %s", v.Name, v.In, reason)
} else if v := err.RequestBody; v != nil {
- return fmt.Sprintf("request body has an error: %s", reason)
+ return fmt.Sprintf("Request body has an error: %s", reason)
} else {
return reason
}
}
-func (err RequestError) Unwrap() error {
- return err.Err
-}
-
-var _ error = &ResponseError{}
-
-// ResponseError is returned by ValidateResponse when response does not match OpenAPI spec
type ResponseError struct {
Input *ResponseValidationInput
Reason string
Err error
}
-var _ interface{ Unwrap() error } = ResponseError{}
-
func (err *ResponseError) Error() string {
reason := err.Reason
if e := err.Err; e != nil {
@@ -65,28 +76,11 @@ func (err *ResponseError) Error() string {
return reason
}
-func (err ResponseError) Unwrap() error {
- return err.Err
-}
-
-var _ error = &SecurityRequirementsError{}
-
-// SecurityRequirementsError is returned by ValidateSecurityRequirements
-// when no requirement is met.
type SecurityRequirementsError struct {
SecurityRequirements openapi3.SecurityRequirements
Errors []error
}
func (err *SecurityRequirementsError) Error() string {
- buff := &bytes.Buffer{}
- buff.WriteString("security requirements failed: ")
- for i, e := range err.Errors {
- buff.WriteString(e.Error())
- if i != len(err.Errors)-1 {
- buff.WriteString(" | ")
- }
- }
-
- return buff.String()
+ return "Security requirements failed"
}
diff --git a/openapi3filter/testdata/fixtures/petstore.json b/openapi3filter/fixtures/petstore.json
similarity index 89%
rename from openapi3filter/testdata/fixtures/petstore.json
rename to openapi3filter/fixtures/petstore.json
index 88c3c6a85..6c229a672 100644
--- a/openapi3filter/testdata/fixtures/petstore.json
+++ b/openapi3filter/fixtures/petstore.json
@@ -67,21 +67,7 @@
],
"requestBody": {
"$ref": "#/components/requestBodies/PetWithRequired"
- },
- "parameters": [
- {
- "schema": {
- "type": "string",
- "enum": [
- "demo",
- "prod"
- ]
- },
- "in": "header",
- "name": "x-environment",
- "description": "Where to send the data for processing"
- }
- ]
+ }
},
"patch": {
"tags": [
@@ -121,11 +107,7 @@
],
"summary": "Add a new pet to the store",
"description": "",
-<<<<<<< HEAD:openapi3filter/fixtures/petstore.json
"operationId": "addPet",
-=======
- "operationId": "addPet2",
->>>>>>> upstream/master:openapi3filter/testdata/fixtures/petstore.json
"responses": {
"405": {
"description": "Invalid input"
@@ -196,114 +178,6 @@
{"$ref": "#/components/schemas/Pet"},
{"$ref": "#/components/schemas/PetRequiredProperties"}
]
-<<<<<<< HEAD:openapi3filter/fixtures/petstore.json
-=======
- }
- }
- }
- }
- },
- "400": {
- "description": "Invalid status value"
- }
- },
- "security": [
- {
- "petstore_auth": [
- "write:pets",
- "read:pets"
- ]
- }
- ]
- }
- },
- "/pets/": {
- "get": {
- "tags": [
- "pet"
- ],
- "summary": "Find pets by the specified filters",
- "description": "Returns a list of pets that comply with the specified filters",
- "operationId": "findPets",
- "parameters": [
- {
- "name": "status",
- "in": "query",
- "description": "Status values that need to be considered for filter",
- "required": false,
- "explode": true,
- "allowEmptyValue": true,
- "schema": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "available",
- "pending",
- "sold"
- ],
- "default": "available"
- }
- }
- },
- {
- "name": "tags",
- "in": "query",
- "description": "Tags to filter by",
- "required": false,
- "explode": true,
- "schema": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- {
- "name": "kind",
- "in": "query",
- "description": "Kinds to filter by",
- "required": false,
- "explode": false,
- "style": "pipeDelimited",
- "schema": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "dog",
- "cat",
- "turtle",
- "bird,with,commas"
- ]
- }
- }
- }
- ],
- "responses": {
- "200": {
- "description": "successful operation",
- "content": {
- "application/xml": {
- "schema": {
- "type": "array",
- "items": {
- "allOf": [
- {"$ref": "#/components/schemas/Pet"},
- {"$ref": "#/components/schemas/PetRequiredProperties"}
- ]
- }
- }
- },
- "application/json": {
- "schema": {
- "type": "array",
- "items": {
- "allOf": [
- {"$ref": "#/components/schemas/Pet"},
- {"$ref": "#/components/schemas/PetRequiredProperties"}
- ]
->>>>>>> upstream/master:openapi3filter/testdata/fixtures/petstore.json
}
}
}
@@ -1262,11 +1136,7 @@
},
"name": {
"type": "string",
-<<<<<<< HEAD:openapi3filter/fixtures/petstore.json
"example": "doggie",
-=======
- "example": "doggie"
->>>>>>> upstream/master:openapi3filter/testdata/fixtures/petstore.json
},
"photoUrls": {
"type": "array",
diff --git a/openapi3filter/internal.go b/openapi3filter/internal.go
index 5c6a8a6c6..facaf1de5 100644
--- a/openapi3filter/internal.go
+++ b/openapi3filter/internal.go
@@ -1,7 +1,6 @@
package openapi3filter
import (
- "reflect"
"strings"
)
@@ -12,14 +11,3 @@ func parseMediaType(contentType string) string {
}
return contentType[:i]
}
-
-func isNilValue(value interface{}) bool {
- if value == nil {
- return true
- }
- switch reflect.TypeOf(value).Kind() {
- case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
- return reflect.ValueOf(value).IsNil()
- }
- return false
-}
diff --git a/openapi3filter/issue201_test.go b/openapi3filter/issue201_test.go
deleted file mode 100644
index ec0b2a1f1..000000000
--- a/openapi3filter/issue201_test.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package openapi3filter
-
-import (
- "context"
- "io"
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestIssue201(t *testing.T) {
- loader := openapi3.NewLoader()
- ctx := loader.Context
- spec := `
-openapi: '3.0.3'
-info:
- version: 1.0.0
- title: Sample API
-paths:
- /_:
- get:
- description: ''
- responses:
- default:
- description: ''
- content:
- application/json:
- schema:
- type: object
- headers:
- X-Blip:
- description: ''
- required: true
- schema:
- type: string
- pattern: '^blip$'
- x-blop:
- description: ''
- schema:
- type: string
- pattern: '^blop$'
- X-Blap:
- description: ''
- required: true
- schema:
- type: string
- pattern: '^blap$'
- X-Blup:
- description: ''
- required: true
- schema:
- type: string
- pattern: '^blup$'
-`[1:]
-
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(ctx)
- require.NoError(t, err)
-
- for name, testcase := range map[string]struct {
- headers map[string]string
- err string
- }{
-
- "no error": {
- headers: map[string]string{
- "X-Blip": "blip",
- "x-blop": "blop",
- "X-Blap": "blap",
- "X-Blup": "blup",
- },
- },
-
- "missing non-required header": {
- headers: map[string]string{
- "X-Blip": "blip",
- // "x-blop": "blop",
- "X-Blap": "blap",
- "X-Blup": "blup",
- },
- },
-
- "missing required header": {
- err: `response header "X-Blip" missing`,
- headers: map[string]string{
- // "X-Blip": "blip",
- "x-blop": "blop",
- "X-Blap": "blap",
- "X-Blup": "blup",
- },
- },
-
- "invalid required header": {
- err: `response header "X-Blup" doesn't match schema: string doesn't match the regular expression "^blup$"`,
- headers: map[string]string{
- "X-Blip": "blip",
- "x-blop": "blop",
- "X-Blap": "blap",
- "X-Blup": "bluuuuuup",
- },
- },
- } {
- t.Run(name, func(t *testing.T) {
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
-
- r, err := http.NewRequest(http.MethodGet, `/_`, nil)
- require.NoError(t, err)
-
- r.Header.Add(headerCT, "application/json")
- for k, v := range testcase.headers {
- r.Header.Add(k, v)
- }
-
- route, pathParams, err := router.FindRoute(r)
- require.NoError(t, err)
-
- err = ValidateResponse(context.Background(), &ResponseValidationInput{
- RequestValidationInput: &RequestValidationInput{
- Request: r,
- PathParams: pathParams,
- Route: route,
- },
- Status: 200,
- Header: r.Header,
- Body: io.NopCloser(strings.NewReader(`{}`)),
- })
- if e := testcase.err; e != "" {
- require.ErrorContains(t, err, e)
- } else {
- require.NoError(t, err)
- }
- })
- }
-}
diff --git a/openapi3filter/issue436_test.go b/openapi3filter/issue436_test.go
deleted file mode 100644
index fa106c5a1..000000000
--- a/openapi3filter/issue436_test.go
+++ /dev/null
@@ -1,135 +0,0 @@
-package openapi3filter_test
-
-import (
- "bytes"
- "context"
- "io"
- "mime/multipart"
- "net/http"
- "net/textproto"
- "strings"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func Example_validateMultipartFormData() {
- const spec = `
-openapi: 3.0.0
-info:
- title: 'Validator'
- version: 0.0.1
-paths:
- /test:
- post:
- requestBody:
- required: true
- content:
- multipart/form-data:
- schema:
- type: object
- required:
- - file
- properties:
- file:
- type: string
- format: binary
- categories:
- type: array
- items:
- $ref: "#/components/schemas/Category"
- responses:
- '200':
- description: Created
-
-components:
- schemas:
- Category:
- type: object
- properties:
- name:
- type: string
- required:
- - name
-`
-
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- if err != nil {
- panic(err)
- }
- if err = doc.Validate(loader.Context); err != nil {
- panic(err)
- }
-
- router, err := gorillamux.NewRouter(doc)
- if err != nil {
- panic(err)
- }
-
- body := &bytes.Buffer{}
- writer := multipart.NewWriter(body)
-
- { // Add a single "categories" item as part data
- h := make(textproto.MIMEHeader)
- h.Set("Content-Disposition", `form-data; name="categories"`)
- h.Set("Content-Type", "application/json")
- fw, err := writer.CreatePart(h)
- if err != nil {
- panic(err)
- }
- if _, err = io.Copy(fw, strings.NewReader(`{"name": "foo"}`)); err != nil {
- panic(err)
- }
- }
-
- { // Add a single "categories" item as part data, again
- h := make(textproto.MIMEHeader)
- h.Set("Content-Disposition", `form-data; name="categories"`)
- h.Set("Content-Type", "application/json")
- fw, err := writer.CreatePart(h)
- if err != nil {
- panic(err)
- }
- if _, err = io.Copy(fw, strings.NewReader(`{"name": "bar"}`)); err != nil {
- panic(err)
- }
- }
-
- { // Add file data
- fw, err := writer.CreateFormFile("file", "hello.txt")
- if err != nil {
- panic(err)
- }
- if _, err = io.Copy(fw, strings.NewReader("hello")); err != nil {
- panic(err)
- }
- }
-
- writer.Close()
-
- req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes()))
- if err != nil {
- panic(err)
- }
- req.Header.Set("Content-Type", writer.FormDataContentType())
-
- route, pathParams, err := router.FindRoute(req)
- if err != nil {
- panic(err)
- }
-
- if err = openapi3filter.ValidateRequestBody(
- context.Background(),
- &openapi3filter.RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- },
- route.Operation.RequestBody.Value,
- ); err != nil {
- panic(err)
- }
- // Output:
-}
diff --git a/openapi3filter/issue624_test.go b/openapi3filter/issue624_test.go
deleted file mode 100644
index 1fdbdea34..000000000
--- a/openapi3filter/issue624_test.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package openapi3filter
-
-import (
- "net/http"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestIssue624(t *testing.T) {
- loader := openapi3.NewLoader()
- ctx := loader.Context
- spec := `
-openapi: 3.0.0
-info:
- version: 1.0.0
- title: Sample API
-paths:
- /items:
- get:
- description: Returns a list of stuff
- parameters:
- - description: "test non object"
- explode: true
- style: form
- in: query
- name: test
- required: false
- content:
- application/json:
- schema:
- anyOf:
- - type: string
- - type: integer
- responses:
- '200':
- description: Successful response
-`[1:]
-
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(ctx)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
-
- for _, testcase := range []string{`test1`, `test[1`} {
- t.Run(testcase, func(t *testing.T) {
- httpReq, err := http.NewRequest(http.MethodGet, `/items?test=`+testcase, nil)
- require.NoError(t, err)
-
- route, pathParams, err := router.FindRoute(httpReq)
- require.NoError(t, err)
-
- requestValidationInput := &RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- }
- err = ValidateRequest(ctx, requestValidationInput)
- require.NoError(t, err)
- })
- }
-}
diff --git a/openapi3filter/issue625_test.go b/openapi3filter/issue625_test.go
deleted file mode 100644
index 5642a7e00..000000000
--- a/openapi3filter/issue625_test.go
+++ /dev/null
@@ -1,123 +0,0 @@
-package openapi3filter_test
-
-import (
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestIssue625(t *testing.T) {
-
- anyOfArraySpec := `
-openapi: 3.0.0
-info:
- version: 1.0.0
- title: Sample API
-paths:
- /items:
- get:
- description: Returns a list of stuff
- parameters:
- - description: test object
- explode: false
- in: query
- name: test
- required: false
- schema:
- type: array
- items:
- anyOf:
- - type: integer
- - type: boolean
- responses:
- '200':
- description: Successful response
-`[1:]
-
- oneOfArraySpec := strings.ReplaceAll(anyOfArraySpec, "anyOf", "oneOf")
-
- allOfArraySpec := strings.ReplaceAll(strings.ReplaceAll(anyOfArraySpec, "anyOf", "allOf"),
- "type: boolean", "type: number")
-
- tests := []struct {
- name string
- spec string
- req string
- errStr string
- }{
- {
- name: "success anyof object array",
- spec: anyOfArraySpec,
- req: "/items?test=3,7",
- },
- {
- name: "failed anyof object array",
- spec: anyOfArraySpec,
- req: "/items?test=s1,s2",
- errStr: `parameter "test" in query has an error: path 0: value s1: an invalid boolean: invalid syntax`,
- },
-
- {
- name: "success allof object array",
- spec: allOfArraySpec,
- req: `/items?test=1,3`,
- },
- {
- name: "failed allof object array",
- spec: allOfArraySpec,
- req: `/items?test=1.2,3.1`,
- errStr: `parameter "test" in query has an error: path 0: value 1.2: an invalid integer: invalid syntax`,
- },
- {
- name: "success oneof object array",
- spec: oneOfArraySpec,
- req: `/items?test=true,3`,
- },
- {
- name: "faled oneof object array",
- spec: oneOfArraySpec,
- req: `/items?test="val1","val2"`,
- errStr: `parameter "test" in query has an error: item 0: decoding oneOf failed: 0 schemas matched`,
- },
- }
-
- for _, testcase := range tests {
- t.Run(testcase.name, func(t *testing.T) {
- loader := openapi3.NewLoader()
- ctx := loader.Context
-
- doc, err := loader.LoadFromData([]byte(testcase.spec))
- require.NoError(t, err)
-
- err = doc.Validate(ctx)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
- httpReq, err := http.NewRequest(http.MethodGet, testcase.req, nil)
- require.NoError(t, err)
-
- route, pathParams, err := router.FindRoute(httpReq)
- require.NoError(t, err)
-
- requestValidationInput := &openapi3filter.RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- }
- err = openapi3filter.ValidateRequest(ctx, requestValidationInput)
- if testcase.errStr == "" {
- require.NoError(t, err)
- } else {
- require.Contains(t, err.Error(), testcase.errStr)
- }
- },
- )
- }
-}
diff --git a/openapi3filter/issue639_test.go b/openapi3filter/issue639_test.go
deleted file mode 100644
index 2caf1bd14..000000000
--- a/openapi3filter/issue639_test.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package openapi3filter
-
-import (
- "encoding/json"
- "io/ioutil"
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestIssue639(t *testing.T) {
- loader := openapi3.NewLoader()
- ctx := loader.Context
- spec := `
- openapi: 3.0.0
- info:
- version: 1.0.0
- title: Sample API
- paths:
- /items:
- put:
- requestBody:
- content:
- application/json:
- schema:
- properties:
- testWithdefault:
- default: false
- type: boolean
- testNoDefault:
- type: boolean
- type: object
- responses:
- '200':
- description: Successful respons
-`[1:]
-
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(ctx)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
-
- tests := []struct {
- name string
- options *Options
- expectedDefaultVal interface{}
- }{
- {
- name: "no defaults are added to requests",
- options: &Options{
- SkipSettingDefaults: true,
- },
- expectedDefaultVal: nil,
- },
-
- {
- name: "defaults are added to requests",
- expectedDefaultVal: false,
- },
- }
-
- for _, testcase := range tests {
- t.Run(testcase.name, func(t *testing.T) {
- body := "{\"testNoDefault\": true}"
- httpReq, err := http.NewRequest(http.MethodPut, "/items", strings.NewReader(body))
- require.NoError(t, err)
- httpReq.Header.Set("Content-Type", "application/json")
- require.NoError(t, err)
-
- route, pathParams, err := router.FindRoute(httpReq)
- require.NoError(t, err)
-
- requestValidationInput := &RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- Options: testcase.options,
- }
- err = ValidateRequest(ctx, requestValidationInput)
- require.NoError(t, err)
- bodyAfterValidation, err := ioutil.ReadAll(httpReq.Body)
- require.NoError(t, err)
-
- raw := map[string]interface{}{}
- err = json.Unmarshal(bodyAfterValidation, &raw)
- require.NoError(t, err)
- require.Equal(t, testcase.expectedDefaultVal,
- raw["testWithdefault"], "default value must not be included")
- })
- }
-}
diff --git a/openapi3filter/issue641_test.go b/openapi3filter/issue641_test.go
deleted file mode 100644
index 1c5277d0d..000000000
--- a/openapi3filter/issue641_test.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package openapi3filter_test
-
-import (
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestIssue641(t *testing.T) {
-
- anyOfSpec := `
-openapi: 3.0.0
-info:
- version: 1.0.0
- title: Sample API
-paths:
- /items:
- get:
- description: Returns a list of stuff
- parameters:
- - description: test object
- explode: false
- in: query
- name: test
- required: false
- schema:
- anyOf:
- - pattern: "^[0-9]{1,4}$"
- - pattern: "^[0-9]{1,4}$"
- type: string
- responses:
- '200':
- description: Successful response
-`[1:]
-
- allOfSpec := strings.ReplaceAll(anyOfSpec, "anyOf", "allOf")
-
- tests := []struct {
- name string
- spec string
- req string
- errStr string
- }{
-
- {
- name: "success anyof pattern",
- spec: anyOfSpec,
- req: "/items?test=51",
- },
- {
- name: "failed anyof pattern",
- spec: anyOfSpec,
- req: "/items?test=999999",
- errStr: `parameter "test" in query has an error: doesn't match any schema from "anyOf"`,
- },
-
- {
- name: "success allof pattern",
- spec: allOfSpec,
- req: `/items?test=51`,
- },
- {
- name: "failed allof pattern",
- spec: allOfSpec,
- req: `/items?test=999999`,
- errStr: `parameter "test" in query has an error: string doesn't match the regular expression "^[0-9]{1,4}$"`,
- },
- }
-
- for _, testcase := range tests {
- t.Run(testcase.name, func(t *testing.T) {
- loader := openapi3.NewLoader()
- ctx := loader.Context
-
- doc, err := loader.LoadFromData([]byte(testcase.spec))
- require.NoError(t, err)
-
- err = doc.Validate(ctx)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
- httpReq, err := http.NewRequest(http.MethodGet, testcase.req, nil)
- require.NoError(t, err)
-
- route, pathParams, err := router.FindRoute(httpReq)
- require.NoError(t, err)
-
- requestValidationInput := &openapi3filter.RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- }
- err = openapi3filter.ValidateRequest(ctx, requestValidationInput)
- if testcase.errStr == "" {
- require.NoError(t, err)
- } else {
- require.Contains(t, err.Error(), testcase.errStr)
- }
- },
- )
- }
-}
diff --git a/openapi3filter/issue689_test.go b/openapi3filter/issue689_test.go
deleted file mode 100644
index 592d53f74..000000000
--- a/openapi3filter/issue689_test.go
+++ /dev/null
@@ -1,168 +0,0 @@
-package openapi3filter
-
-import (
- "io"
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestIssue689(t *testing.T) {
- loader := openapi3.NewLoader()
- ctx := loader.Context
- spec := `
- openapi: 3.0.0
- info:
- version: 1.0.0
- title: Sample API
- paths:
- /items:
- put:
- requestBody:
- content:
- application/json:
- schema:
- properties:
- testWithReadOnly:
- readOnly: true
- type: boolean
- testNoReadOnly:
- type: boolean
- type: object
- responses:
- '200':
- description: OK
- get:
- responses:
- '200':
- description: OK
- content:
- application/json:
- schema:
- properties:
- testWithWriteOnly:
- writeOnly: true
- type: boolean
- testNoWriteOnly:
- type: boolean
-`[1:]
-
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(ctx)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
-
- tests := []struct {
- name string
- options *Options
- body string
- method string
- checkErr require.ErrorAssertionFunc
- }{
- // read-only
- {
- name: "non read-only property is added to request when validation enabled",
- body: `{"testNoReadOnly": true}`,
- method: http.MethodPut,
- checkErr: require.NoError,
- },
- {
- name: "non read-only property is added to request when validation disabled",
- body: `{"testNoReadOnly": true}`,
- method: http.MethodPut,
- options: &Options{
- ExcludeReadOnlyValidations: true,
- },
- checkErr: require.NoError,
- },
- {
- name: "read-only property is added to requests when validation enabled",
- body: `{"testWithReadOnly": true}`,
- method: http.MethodPut,
- checkErr: require.Error,
- },
- {
- name: "read-only property is added to requests when validation disabled",
- body: `{"testWithReadOnly": true}`,
- method: http.MethodPut,
- options: &Options{
- ExcludeReadOnlyValidations: true,
- },
- checkErr: require.NoError,
- },
- // write-only
- {
- name: "non write-only property is added to request when validation enabled",
- body: `{"testNoWriteOnly": true}`,
- method: http.MethodGet,
- checkErr: require.NoError,
- },
- {
- name: "non write-only property is added to request when validation disabled",
- body: `{"testNoWriteOnly": true}`,
- method: http.MethodGet,
- options: &Options{
- ExcludeWriteOnlyValidations: true,
- },
- checkErr: require.NoError,
- },
- {
- name: "write-only property is added to requests when validation enabled",
- body: `{"testWithWriteOnly": true}`,
- method: http.MethodGet,
- checkErr: require.Error,
- },
- {
- name: "write-only property is added to requests when validation disabled",
- body: `{"testWithWriteOnly": true}`,
- method: http.MethodGet,
- options: &Options{
- ExcludeWriteOnlyValidations: true,
- },
- checkErr: require.NoError,
- },
- }
-
- for _, test := range tests {
- t.Run(test.name, func(t *testing.T) {
- httpReq, err := http.NewRequest(test.method, "/items", strings.NewReader(test.body))
- require.NoError(t, err)
- httpReq.Header.Set("Content-Type", "application/json")
- require.NoError(t, err)
-
- route, pathParams, err := router.FindRoute(httpReq)
- require.NoError(t, err)
-
- requestValidationInput := &RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- Options: test.options,
- }
-
- if test.method == http.MethodGet {
- responseValidationInput := &ResponseValidationInput{
- RequestValidationInput: requestValidationInput,
- Status: 200,
- Header: httpReq.Header,
- Body: io.NopCloser(strings.NewReader(test.body)),
- Options: test.options,
- }
- err = ValidateResponse(ctx, responseValidationInput)
-
- } else {
- err = ValidateRequest(ctx, requestValidationInput)
- }
- test.checkErr(t, err)
- })
- }
-}
diff --git a/openapi3filter/issue707_test.go b/openapi3filter/issue707_test.go
deleted file mode 100644
index a7cbc39ed..000000000
--- a/openapi3filter/issue707_test.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package openapi3filter
-
-import (
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestIssue707(t *testing.T) {
- loader := openapi3.NewLoader()
- ctx := loader.Context
- spec := `
-openapi: 3.0.0
-info:
- version: 1.0.0
- title: Sample API
-paths:
- /items:
- get:
- description: Returns a list of stuff
- parameters:
- - description: parameter with a default value
- explode: true
- in: query
- name: param-with-default
- schema:
- default: 124
- type: integer
- required: false
- responses:
- '200':
- description: Successful response
-`[1:]
-
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(ctx)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
-
- tests := []struct {
- name string
- options *Options
- expectedQuery string
- }{
- {
- name: "no defaults are added to requests parameters",
- options: &Options{
- SkipSettingDefaults: true,
- },
- expectedQuery: "",
- },
-
- {
- name: "defaults are added to requests",
- expectedQuery: "param-with-default=124",
- },
- }
-
- for _, testcase := range tests {
- t.Run(testcase.name, func(t *testing.T) {
- httpReq, err := http.NewRequest(http.MethodGet, "/items", strings.NewReader(""))
- require.NoError(t, err)
-
- route, pathParams, err := router.FindRoute(httpReq)
- require.NoError(t, err)
-
- requestValidationInput := &RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- Options: testcase.options,
- }
- err = ValidateRequest(ctx, requestValidationInput)
- require.NoError(t, err)
-
- require.NoError(t, err)
- require.Equal(t, testcase.expectedQuery,
- httpReq.URL.RawQuery, "default value must not be included")
- })
- }
-}
diff --git a/openapi3filter/issue722_test.go b/openapi3filter/issue722_test.go
deleted file mode 100644
index 2ffa9d143..000000000
--- a/openapi3filter/issue722_test.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package openapi3filter_test
-
-import (
- "bytes"
- "context"
- "io"
- "mime/multipart"
- "net/http"
- "net/textproto"
- "strings"
- "testing"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestValidateMultipartFormDataContainingAllOf(t *testing.T) {
- const spec = `
-openapi: 3.0.0
-info:
- title: 'Validator'
- version: 0.0.1
-paths:
- /test:
- post:
- requestBody:
- required: true
- content:
- multipart/form-data:
- schema:
- type: object
- required:
- - file
- allOf:
- - $ref: '#/components/schemas/Category'
- - properties:
- file:
- type: string
- format: binary
- description:
- type: string
- responses:
- '200':
- description: Created
-
-components:
- schemas:
- Category:
- type: object
- properties:
- name:
- type: string
- required:
- - name
-`
-
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- if err != nil {
- t.Fatal(err)
- }
- if err = doc.Validate(loader.Context); err != nil {
- t.Fatal(err)
- }
-
- router, err := gorillamux.NewRouter(doc)
- if err != nil {
- t.Fatal(err)
- }
-
- body := &bytes.Buffer{}
- writer := multipart.NewWriter(body)
-
- { // Add file data
- fw, err := writer.CreateFormFile("file", "hello.txt")
- if err != nil {
- t.Fatal(err)
- }
- if _, err = io.Copy(fw, strings.NewReader("hello")); err != nil {
- t.Fatal(err)
- }
- }
-
- { // Add a single "name" item as part data
- h := make(textproto.MIMEHeader)
- h.Set("Content-Disposition", `form-data; name="name"`)
- fw, err := writer.CreatePart(h)
- if err != nil {
- t.Fatal(err)
- }
- if _, err = io.Copy(fw, strings.NewReader(`foo`)); err != nil {
- t.Fatal(err)
- }
- }
-
- { // Add a single "discription" item as part data
- h := make(textproto.MIMEHeader)
- h.Set("Content-Disposition", `form-data; name="description"`)
- fw, err := writer.CreatePart(h)
- if err != nil {
- t.Fatal(err)
- }
- if _, err = io.Copy(fw, strings.NewReader(`description note`)); err != nil {
- t.Fatal(err)
- }
- }
-
- writer.Close()
-
- req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes()))
- if err != nil {
- t.Fatal(err)
- }
- req.Header.Set("Content-Type", writer.FormDataContentType())
-
- route, pathParams, err := router.FindRoute(req)
- if err != nil {
- t.Fatal(err)
- }
-
- if err = openapi3filter.ValidateRequestBody(
- context.Background(),
- &openapi3filter.RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- },
- route.Operation.RequestBody.Value,
- ); err != nil {
- t.Error(err)
- }
-}
diff --git a/openapi3filter/issue733_test.go b/openapi3filter/issue733_test.go
deleted file mode 100644
index 0d2214b58..000000000
--- a/openapi3filter/issue733_test.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package openapi3filter_test
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "math"
- "math/big"
- "net/http"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestIntMax(t *testing.T) {
- spec := `
-openapi: 3.0.0
-info:
- version: 1.0.0
- title: test large integer value
-paths:
- /test:
- post:
- requestBody:
- content:
- application/json:
- schema:
- type: object
- properties:
- testInteger:
- type: integer
- format: int64
- testDefault:
- type: boolean
- default: false
- responses:
- '200':
- description: Successful response
-`[1:]
-
- loader := openapi3.NewLoader()
-
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
-
- testOne := func(value *big.Int, pass bool) {
- valueString := value.String()
-
- req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader([]byte(`{"testInteger":`+valueString+`}`)))
- require.NoError(t, err)
- req.Header.Set("Content-Type", "application/json")
-
- route, pathParams, err := router.FindRoute(req)
- require.NoError(t, err)
-
- err = openapi3filter.ValidateRequest(
- context.Background(),
- &openapi3filter.RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- })
- if pass {
- require.NoError(t, err)
-
- dec := json.NewDecoder(req.Body)
- dec.UseNumber()
- var jsonAfter map[string]interface{}
- err = dec.Decode(&jsonAfter)
- require.NoError(t, err)
-
- valueAfter := jsonAfter["testInteger"]
- require.IsType(t, json.Number(""), valueAfter)
- assert.Equal(t, valueString, string(valueAfter.(json.Number)))
- } else {
- if assert.Error(t, err) {
- var serr *openapi3.SchemaError
- if assert.ErrorAs(t, err, &serr) {
- assert.Equal(t, "number must be an int64", serr.Reason)
- }
- }
- }
- }
-
- bigMaxInt64 := big.NewInt(math.MaxInt64)
- bigMaxInt64Plus1 := new(big.Int).Add(bigMaxInt64, big.NewInt(1))
- bigMinInt64 := big.NewInt(math.MinInt64)
- bigMinInt64Minus1 := new(big.Int).Sub(bigMinInt64, big.NewInt(1))
-
- testOne(bigMaxInt64, true)
- // XXX not yet fixed
- // testOne(bigMaxInt64Plus1, false)
- testOne(bigMaxInt64Plus1, true)
- testOne(bigMinInt64, true)
- // XXX not yet fixed
- // testOne(bigMinInt64Minus1, false)
- testOne(bigMinInt64Minus1, true)
-}
diff --git a/openapi3filter/middleware.go b/openapi3filter/middleware.go
deleted file mode 100644
index 3bcb9db43..000000000
--- a/openapi3filter/middleware.go
+++ /dev/null
@@ -1,283 +0,0 @@
-package openapi3filter
-
-import (
- "bytes"
- "io"
- "io/ioutil"
- "log"
- "net/http"
-
- "github.com/getkin/kin-openapi/routers"
-)
-
-// Validator provides HTTP request and response validation middleware.
-type Validator struct {
- router routers.Router
- errFunc ErrFunc
- logFunc LogFunc
- strict bool
- options Options
-}
-
-// ErrFunc handles errors that may occur during validation.
-type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error)
-
-// LogFunc handles log messages that may occur during validation.
-type LogFunc func(message string, err error)
-
-// ErrCode is used for classification of different types of errors that may
-// occur during validation. These may be used to write an appropriate response
-// in ErrFunc.
-type ErrCode int
-
-const (
- // ErrCodeOK indicates no error. It is also the default value.
- ErrCodeOK = 0
- // ErrCodeCannotFindRoute happens when the validator fails to resolve the
- // request to a defined OpenAPI route.
- ErrCodeCannotFindRoute = iota
- // ErrCodeRequestInvalid happens when the inbound request does not conform
- // to the OpenAPI 3 specification.
- ErrCodeRequestInvalid = iota
- // ErrCodeResponseInvalid happens when the wrapped handler response does
- // not conform to the OpenAPI 3 specification.
- ErrCodeResponseInvalid = iota
-)
-
-func (e ErrCode) responseText() string {
- switch e {
- case ErrCodeOK:
- return "OK"
- case ErrCodeCannotFindRoute:
- return "not found"
- case ErrCodeRequestInvalid:
- return "bad request"
- default:
- return "server error"
- }
-}
-
-// NewValidator returns a new response validation middlware, using the given
-// routes from an OpenAPI 3 specification.
-func NewValidator(router routers.Router, options ...ValidatorOption) *Validator {
- v := &Validator{
- router: router,
- errFunc: func(w http.ResponseWriter, status int, code ErrCode, _ error) {
- http.Error(w, code.responseText(), status)
- },
- logFunc: func(message string, err error) {
- log.Printf("%s: %v", message, err)
- },
- }
- for i := range options {
- options[i](v)
- }
- return v
-}
-
-// ValidatorOption defines an option that may be specified when creating a
-// Validator.
-type ValidatorOption func(*Validator)
-
-// OnErr provides a callback that handles writing an HTTP response on a
-// validation error. This allows customization of error responses without
-// prescribing a particular form. This callback is only called on response
-// validator errors in Strict mode.
-func OnErr(f ErrFunc) ValidatorOption {
- return func(v *Validator) {
- v.errFunc = f
- }
-}
-
-// OnLog provides a callback that handles logging in the Validator. This allows
-// the validator to integrate with a services' existing logging system without
-// prescribing a particular one.
-func OnLog(f LogFunc) ValidatorOption {
- return func(v *Validator) {
- v.logFunc = f
- }
-}
-
-// Strict, if set, causes an internal server error to be sent if the wrapped
-// handler response fails response validation. If not set, the response is sent
-// and the error is only logged.
-func Strict(strict bool) ValidatorOption {
- return func(v *Validator) {
- v.strict = strict
- }
-}
-
-// ValidationOptions sets request/response validation options on the validator.
-func ValidationOptions(options Options) ValidatorOption {
- return func(v *Validator) {
- v.options = options
- }
-}
-
-// Middleware returns an http.Handler which wraps the given handler with
-// request and response validation.
-func (v *Validator) Middleware(h http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- route, pathParams, err := v.router.FindRoute(r)
- if err != nil {
- v.logFunc("validation error: failed to find route for "+r.URL.String(), err)
- v.errFunc(w, http.StatusNotFound, ErrCodeCannotFindRoute, err)
- return
- }
- requestValidationInput := &RequestValidationInput{
- Request: r,
- PathParams: pathParams,
- Route: route,
- Options: &v.options,
- }
- if err = ValidateRequest(r.Context(), requestValidationInput); err != nil {
- v.logFunc("invalid request", err)
- v.errFunc(w, http.StatusBadRequest, ErrCodeRequestInvalid, err)
- return
- }
-
- var wr responseWrapper
- if v.strict {
- wr = &strictResponseWrapper{w: w}
- } else {
- wr = newWarnResponseWrapper(w)
- }
-
- h.ServeHTTP(wr, r)
-
- if err = ValidateResponse(r.Context(), &ResponseValidationInput{
- RequestValidationInput: requestValidationInput,
- Status: wr.statusCode(),
- Header: wr.Header(),
- Body: ioutil.NopCloser(bytes.NewBuffer(wr.bodyContents())),
- Options: &v.options,
- }); err != nil {
- v.logFunc("invalid response", err)
- if v.strict {
- v.errFunc(w, http.StatusInternalServerError, ErrCodeResponseInvalid, err)
- }
- return
- }
-
- if err = wr.flushBodyContents(); err != nil {
- v.logFunc("failed to write response", err)
- }
- })
-}
-
-type responseWrapper interface {
- http.ResponseWriter
-
- // flushBodyContents writes the buffered response to the client, if it has
- // not yet been written.
- flushBodyContents() error
-
- // statusCode returns the response status code, 0 if not set yet.
- statusCode() int
-
- // bodyContents returns the buffered
- bodyContents() []byte
-}
-
-type warnResponseWrapper struct {
- w http.ResponseWriter
- headerWritten bool
- status int
- body bytes.Buffer
- tee io.Writer
-}
-
-func newWarnResponseWrapper(w http.ResponseWriter) *warnResponseWrapper {
- wr := &warnResponseWrapper{
- w: w,
- }
- wr.tee = io.MultiWriter(w, &wr.body)
- return wr
-}
-
-// Write implements http.ResponseWriter.
-func (wr *warnResponseWrapper) Write(b []byte) (int, error) {
- if !wr.headerWritten {
- wr.WriteHeader(http.StatusOK)
- }
- return wr.tee.Write(b)
-}
-
-// WriteHeader implements http.ResponseWriter.
-func (wr *warnResponseWrapper) WriteHeader(status int) {
- if !wr.headerWritten {
- // If the header hasn't been written, record the status for response
- // validation.
- wr.status = status
- wr.headerWritten = true
- }
- wr.w.WriteHeader(wr.status)
-}
-
-// Header implements http.ResponseWriter.
-func (wr *warnResponseWrapper) Header() http.Header {
- return wr.w.Header()
-}
-
-// Flush implements the optional http.Flusher interface.
-func (wr *warnResponseWrapper) Flush() {
- // If the wrapped http.ResponseWriter implements optional http.Flusher,
- // pass through.
- if fl, ok := wr.w.(http.Flusher); ok {
- fl.Flush()
- }
-}
-
-func (wr *warnResponseWrapper) flushBodyContents() error {
- return nil
-}
-
-func (wr *warnResponseWrapper) statusCode() int {
- return wr.status
-}
-
-func (wr *warnResponseWrapper) bodyContents() []byte {
- return wr.body.Bytes()
-}
-
-type strictResponseWrapper struct {
- w http.ResponseWriter
- headerWritten bool
- status int
- body bytes.Buffer
-}
-
-// Write implements http.ResponseWriter.
-func (wr *strictResponseWrapper) Write(b []byte) (int, error) {
- if !wr.headerWritten {
- wr.WriteHeader(http.StatusOK)
- }
- return wr.body.Write(b)
-}
-
-// WriteHeader implements http.ResponseWriter.
-func (wr *strictResponseWrapper) WriteHeader(status int) {
- if !wr.headerWritten {
- wr.status = status
- wr.headerWritten = true
- }
-}
-
-// Header implements http.ResponseWriter.
-func (wr *strictResponseWrapper) Header() http.Header {
- return wr.w.Header()
-}
-
-func (wr *strictResponseWrapper) flushBodyContents() error {
- wr.w.WriteHeader(wr.status)
- _, err := wr.w.Write(wr.body.Bytes())
- return err
-}
-
-func (wr *strictResponseWrapper) statusCode() int {
- return wr.status
-}
-
-func (wr *strictResponseWrapper) bodyContents() []byte {
- return wr.body.Bytes()
-}
diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go
deleted file mode 100644
index ff6059c9d..000000000
--- a/openapi3filter/middleware_test.go
+++ /dev/null
@@ -1,533 +0,0 @@
-package openapi3filter_test
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "path"
- "regexp"
- "strconv"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-const validatorSpec = `
-openapi: 3.0.0
-info:
- title: 'Validator'
- version: '0.0.0'
-paths:
- /test:
- post:
- operationId: newTest
- description: create a new test
- parameters:
- - in: query
- name: version
- schema:
- type: string
- required: true
- requestBody:
- required: true
- content:
- application/json:
- schema: { $ref: '#/components/schemas/TestContents' }
- responses:
- '201':
- description: 'created test'
- content:
- application/json:
- schema: { $ref: '#/components/schemas/TestResource' }
- '400': { $ref: '#/components/responses/ErrorResponse' }
- '500': { $ref: '#/components/responses/ErrorResponse' }
- /test/{id}:
- get:
- operationId: getTest
- description: get a test
- parameters:
- - in: path
- name: id
- schema:
- type: string
- required: true
- - in: query
- name: version
- schema:
- type: string
- required: true
- responses:
- '200':
- description: 'respond with test resource'
- content:
- application/json:
- schema: { $ref: '#/components/schemas/TestResource' }
- '400': { $ref: '#/components/responses/ErrorResponse' }
- '404': { $ref: '#/components/responses/ErrorResponse' }
- '500': { $ref: '#/components/responses/ErrorResponse' }
-components:
- schemas:
- TestContents:
- type: object
- properties:
- name:
- type: string
- expected:
- type: number
- actual:
- type: number
- required: [name, expected, actual]
- additionalProperties: false
- TestResource:
- type: object
- properties:
- id:
- type: string
- contents:
- { $ref: '#/components/schemas/TestContents' }
- required: [id, contents]
- additionalProperties: false
- Error:
- type: object
- properties:
- code:
- type: string
- message:
- type: string
- required: [code, message]
- additionalProperties: false
- responses:
- ErrorResponse:
- description: 'an error occurred'
- content:
- application/json:
- schema: { $ref: '#/components/schemas/Error' }
-`
-
-type validatorTestHandler struct {
- contentType string
- getBody, postBody string
- errBody string
- errStatusCode int
-}
-
-const validatorOkResponse = `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}}`
-
-func (h validatorTestHandler) withDefaults() validatorTestHandler {
- if h.contentType == "" {
- h.contentType = "application/json"
- }
- if h.getBody == "" {
- h.getBody = validatorOkResponse
- }
- if h.postBody == "" {
- h.postBody = validatorOkResponse
- }
- if h.errBody == "" {
- h.errBody = `{"code":"bad","message":"bad things"}`
- }
- return h
-}
-
-var testUrlRE = regexp.MustCompile(`^/test(/\d+)?$`)
-
-func (h *validatorTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", h.contentType)
- if h.errStatusCode != 0 {
- w.WriteHeader(h.errStatusCode)
- w.Write([]byte(h.errBody))
- return
- }
- if !testUrlRE.MatchString(r.URL.Path) {
- w.WriteHeader(http.StatusNotFound)
- w.Write([]byte(h.errBody))
- return
- }
- switch r.Method {
- case "GET":
- w.WriteHeader(http.StatusOK)
- w.Write([]byte(h.getBody))
- case "POST":
- w.WriteHeader(http.StatusCreated)
- w.Write([]byte(h.postBody))
- default:
- http.Error(w, h.errBody, http.StatusMethodNotAllowed)
- }
-}
-
-func TestValidator(t *testing.T) {
- doc, err := openapi3.NewLoader().LoadFromData([]byte(validatorSpec))
- require.NoError(t, err, "failed to load test fixture spec")
-
- ctx := context.Background()
- err = doc.Validate(ctx)
- require.NoError(t, err, "invalid test fixture spec")
-
- type testRequest struct {
- method, path, body, contentType string
- }
- type testResponse struct {
- statusCode int
- body string
- }
- tests := []struct {
- name string
- handler validatorTestHandler
- options []openapi3filter.ValidatorOption
- request testRequest
- response testResponse
- strict bool
- }{{
- name: "valid GET",
- handler: validatorTestHandler{}.withDefaults(),
- request: testRequest{
- method: "GET",
- path: "/test/42?version=1",
- },
- response: testResponse{
- 200, validatorOkResponse,
- },
- strict: true,
- }, {
- name: "valid POST",
- handler: validatorTestHandler{}.withDefaults(),
- request: testRequest{
- method: "POST",
- path: "/test?version=1",
- body: `{"name": "foo", "expected": 9, "actual": 10}`,
- contentType: "application/json",
- },
- response: testResponse{
- 201, validatorOkResponse,
- },
- strict: true,
- }, {
- name: "not found; no GET operation for /test",
- handler: validatorTestHandler{}.withDefaults(),
- request: testRequest{
- method: "GET",
- path: "/test?version=1",
- },
- response: testResponse{
- 404, "not found\n",
- },
- strict: true,
- }, {
- name: "not found; no POST operation for /test/42",
- handler: validatorTestHandler{}.withDefaults(),
- request: testRequest{
- method: "POST",
- path: "/test/42?version=1",
- },
- response: testResponse{
- 404, "not found\n",
- },
- strict: true,
- }, {
- name: "invalid request; missing version",
- handler: validatorTestHandler{}.withDefaults(),
- request: testRequest{
- method: "GET",
- path: "/test/42",
- },
- response: testResponse{
- 400, "bad request\n",
- },
- strict: true,
- }, {
- name: "invalid POST request; wrong property type",
- handler: validatorTestHandler{}.withDefaults(),
- request: testRequest{
- method: "POST",
- path: "/test?version=1",
- body: `{"name": "foo", "expected": "nine", "actual": "ten"}`,
- contentType: "application/json",
- },
- response: testResponse{
- 400, "bad request\n",
- },
- strict: true,
- }, {
- name: "invalid POST request; missing property",
- handler: validatorTestHandler{}.withDefaults(),
- request: testRequest{
- method: "POST",
- path: "/test?version=1",
- body: `{"name": "foo", "expected": 9}`,
- contentType: "application/json",
- },
- response: testResponse{
- 400, "bad request\n",
- },
- strict: true,
- }, {
- name: "invalid POST request; extra property",
- handler: validatorTestHandler{}.withDefaults(),
- request: testRequest{
- method: "POST",
- path: "/test?version=1",
- body: `{"name": "foo", "expected": 9, "actual": 10, "ideal": 8}`,
- contentType: "application/json",
- },
- response: testResponse{
- 400, "bad request\n",
- },
- strict: true,
- }, {
- name: "valid response; 404 error",
- handler: validatorTestHandler{
- contentType: "application/json",
- errBody: `{"code": "404", "message": "not found"}`,
- errStatusCode: 404,
- }.withDefaults(),
- request: testRequest{
- method: "GET",
- path: "/test/42?version=1",
- },
- response: testResponse{
- 404, `{"code": "404", "message": "not found"}`,
- },
- strict: true,
- }, {
- name: "invalid response; invalid error",
- handler: validatorTestHandler{
- errBody: `"not found"`,
- errStatusCode: 404,
- }.withDefaults(),
- request: testRequest{
- method: "GET",
- path: "/test/42?version=1",
- },
- response: testResponse{
- 500, "server error\n",
- },
- strict: true,
- }, {
- name: "invalid POST response; not strict",
- handler: validatorTestHandler{
- postBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`,
- }.withDefaults(),
- request: testRequest{
- method: "POST",
- path: "/test?version=1",
- body: `{"name": "foo", "expected": 9, "actual": 10}`,
- contentType: "application/json",
- },
- response: testResponse{
- statusCode: 201,
- body: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`,
- },
- strict: false,
- }, {
- name: "POST response status code not in spec (return 200, spec only has 201)",
- handler: validatorTestHandler{
- postBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`,
- errStatusCode: 200,
- errBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`,
- }.withDefaults(),
- options: []openapi3filter.ValidatorOption{openapi3filter.ValidationOptions(openapi3filter.Options{
- IncludeResponseStatus: true,
- })},
- request: testRequest{
- method: "POST",
- path: "/test?version=1",
- body: `{"name": "foo", "expected": 9, "actual": 10}`,
- contentType: "application/json",
- },
- response: testResponse{
- statusCode: 200,
- body: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`,
- },
- strict: false,
- }}
- for i, test := range tests {
- t.Logf("test#%d: %s", i, test.name)
- t.Run(test.name, func(t *testing.T) {
- // Set up a test HTTP server
- var h http.Handler
- s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- h.ServeHTTP(w, r)
- }))
- defer s.Close()
-
- // Update the OpenAPI servers section with the test server URL This is
- // needed by the router which matches request routes for OpenAPI
- // validation.
- doc.Servers = []*openapi3.Server{{URL: s.URL}}
- err = doc.Validate(ctx)
- require.NoError(t, err, "failed to validate with test server")
-
- // Create the router and validator
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err, "failed to create router")
-
- // Now wrap the test handler with the validator middlware
- v := openapi3filter.NewValidator(router, append(test.options, openapi3filter.Strict(test.strict))...)
- h = v.Middleware(&test.handler)
-
- // Test: make a client request
- var requestBody io.Reader
- if test.request.body != "" {
- requestBody = bytes.NewBufferString(test.request.body)
- }
- req, err := http.NewRequest(test.request.method, s.URL+test.request.path, requestBody)
- require.NoError(t, err, "failed to create request")
-
- if test.request.contentType != "" {
- req.Header.Set("Content-Type", test.request.contentType)
- }
- resp, err := s.Client().Do(req)
- require.NoError(t, err, "request failed")
- defer resp.Body.Close()
- require.Equalf(t, test.response.statusCode, resp.StatusCode,
- "response code expect %d got %d", test.response.statusCode, resp.StatusCode)
-
- body, err := ioutil.ReadAll(resp.Body)
- require.NoError(t, err, "failed to read response body")
- require.Equalf(t, test.response.body, string(body),
- "response body expect %q got %q", test.response.body, string(body))
- })
- }
-}
-
-func ExampleValidator() {
- // OpenAPI specification for a simple service that squares integers, with
- // some limitations.
- doc, err := openapi3.NewLoader().LoadFromData([]byte(`
-openapi: 3.0.0
-info:
- title: 'Validator - square example'
- version: '0.0.0'
-paths:
- /square/{x}:
- get:
- description: square an integer
- parameters:
- - name: x
- in: path
- schema:
- type: integer
- required: true
- responses:
- '200':
- description: squared integer response
- content:
- "application/json":
- schema:
- type: object
- properties:
- result:
- type: integer
- minimum: 0
- maximum: 1000000
- required: [result]
- additionalProperties: false`[1:]))
- if err != nil {
- panic(err)
- }
-
- // Square service handler sanity checks inputs, but just crashes on invalid
- // requests.
- squareHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- xParam := path.Base(r.URL.Path)
- x, err := strconv.ParseInt(xParam, 10, 64)
- if err != nil {
- panic(err)
- }
- w.Header().Set("Content-Type", "application/json")
- result := map[string]interface{}{"result": x * x}
- if x == 42 {
- // An easter egg. Unfortunately, the spec does not allow additional properties...
- result["comment"] = "the answer to the ulitimate question of life, the universe, and everything"
- }
- if err = json.NewEncoder(w).Encode(&result); err != nil {
- panic(err)
- }
- })
-
- // Start an http server.
- var mainHandler http.Handler
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // Why are we wrapping the main server handler with a closure here?
- // Validation matches request Host: to server URLs in the spec. With an
- // httptest.Server, the URL is dynamic and we have to create it first!
- // In a real configured service, this is less likely to be needed.
- mainHandler.ServeHTTP(w, r)
- }))
- defer srv.Close()
-
- // Patch the OpenAPI spec to match the httptest.Server.URL. Only needed
- // because the server URL is dynamic here.
- doc.Servers = []*openapi3.Server{{URL: srv.URL}}
- if err := doc.Validate(context.Background()); err != nil { // Assert our OpenAPI is valid!
- panic(err)
- }
- // This router is used by the validator to match requests with the OpenAPI
- // spec. It does not place restrictions on how the wrapped handler routes
- // requests; use of gorilla/mux is just a validator implementation detail.
- router, err := gorillamux.NewRouter(doc)
- if err != nil {
- panic(err)
- }
- // Strict validation will respond HTTP 500 if the service tries to emit a
- // response that does not conform to the OpenAPI spec. Very useful for
- // testing a service against its spec in development and CI. In production,
- // availability may be more important than strictness.
- v := openapi3filter.NewValidator(router, openapi3filter.Strict(true),
- openapi3filter.OnErr(func(w http.ResponseWriter, status int, code openapi3filter.ErrCode, err error) {
- // Customize validation error responses to use JSON
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(status)
- json.NewEncoder(w).Encode(map[string]interface{}{
- "status": status,
- "message": http.StatusText(status),
- })
- }))
- // Now we can finally set the main server handler.
- mainHandler = v.Middleware(squareHandler)
-
- printResp := func(resp *http.Response, err error) {
- if err != nil {
- panic(err)
- }
- defer resp.Body.Close()
- contents, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- panic(err)
- }
- fmt.Println(resp.StatusCode, strings.TrimSpace(string(contents)))
- }
- // Valid requests to our sum service
- printResp(srv.Client().Get(srv.URL + "/square/2"))
- printResp(srv.Client().Get(srv.URL + "/square/789"))
- // 404 Not found requests - method or path not found
- printResp(srv.Client().Post(srv.URL+"/square/2", "application/json", bytes.NewBufferString(`{"result": 5}`)))
- printResp(srv.Client().Get(srv.URL + "/sum/2"))
- printResp(srv.Client().Get(srv.URL + "/square/circle/4")) // Handler would process this; validation rejects it
- printResp(srv.Client().Get(srv.URL + "/square"))
- // 400 Bad requests - note they never reach the wrapped square handler (which would panic)
- printResp(srv.Client().Get(srv.URL + "/square/five"))
- // 500 Invalid responses
- printResp(srv.Client().Get(srv.URL + "/square/42")) // Our "easter egg" added a property which is not allowed
- printResp(srv.Client().Get(srv.URL + "/square/65536")) // Answer overflows the maximum allowed value (1000000)
- // Output:
- // 200 {"result":4}
- // 200 {"result":622521}
- // 404 {"message":"Not Found","status":404}
- // 404 {"message":"Not Found","status":404}
- // 404 {"message":"Not Found","status":404}
- // 404 {"message":"Not Found","status":404}
- // 400 {"message":"Bad Request","status":400}
- // 500 {"message":"Internal Server Error","status":500}
- // 500 {"message":"Internal Server Error","status":500}
-}
diff --git a/openapi3filter/options.go b/openapi3filter/options.go
index 4ea9e9907..510b77756 100644
--- a/openapi3filter/options.go
+++ b/openapi3filter/options.go
@@ -1,47 +1,14 @@
package openapi3filter
-import "github.com/getkin/kin-openapi/openapi3"
+import (
+ "context"
+)
-// DefaultOptions do not set an AuthenticationFunc.
-// A spec with security schemes defined will not pass validation
-// unless an AuthenticationFunc is defined.
var DefaultOptions = &Options{}
-// Options used by ValidateRequest and ValidateResponse
type Options struct {
- // Set ExcludeRequestBody so ValidateRequest skips request body validation
- ExcludeRequestBody bool
-
- // Set ExcludeResponseBody so ValidateResponse skips response body validation
- ExcludeResponseBody bool
-
- // Set ExcludeReadOnlyValidations so ValidateRequest skips read-only validations
- ExcludeReadOnlyValidations bool
-
- // Set ExcludeWriteOnlyValidations so ValidateResponse skips write-only validations
- ExcludeWriteOnlyValidations bool
-
- // Set IncludeResponseStatus so ValidateResponse fails on response
- // status not defined in OpenAPI spec
+ ExcludeRequestBody bool
+ ExcludeResponseBody bool
IncludeResponseStatus bool
-
- MultiError bool
-
- // See NoopAuthenticationFunc
- AuthenticationFunc AuthenticationFunc
-
- // Indicates whether default values are set in the
- // request. If true, then they are not set
- SkipSettingDefaults bool
-
- customSchemaErrorFunc CustomSchemaErrorFunc
-}
-
-// CustomSchemaErrorFunc allows for custom the schema error message.
-type CustomSchemaErrorFunc func(err *openapi3.SchemaError) string
-
-// WithCustomSchemaErrorFunc sets a function to override the schema error message.
-// If the passed function returns an empty string, it returns to the previous Error() implementation.
-func (o *Options) WithCustomSchemaErrorFunc(f CustomSchemaErrorFunc) {
- o.customSchemaErrorFunc = f
+ AuthenticationFunc func(c context.Context, input *AuthenticationInput) error
}
diff --git a/openapi3filter/options_test.go b/openapi3filter/options_test.go
deleted file mode 100644
index fd19329ff..000000000
--- a/openapi3filter/options_test.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package openapi3filter_test
-
-import (
- "context"
- "fmt"
- "net/http"
- "strings"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func ExampleOptions_WithCustomSchemaErrorFunc() {
- const spec = `
-openapi: 3.0.0
-info:
- title: 'Validator'
- version: 0.0.1
-paths:
- /some:
- post:
- requestBody:
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- field:
- title: Some field
- type: integer
- responses:
- '200':
- description: Created
-`
-
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- if err != nil {
- panic(err)
- }
-
- if err = doc.Validate(loader.Context); err != nil {
- panic(err)
- }
-
- router, err := gorillamux.NewRouter(doc)
- if err != nil {
- panic(err)
- }
-
- opts := &openapi3filter.Options{}
-
- opts.WithCustomSchemaErrorFunc(func(err *openapi3.SchemaError) string {
- return fmt.Sprintf(`field "%s" must be an integer`, err.Schema.Title)
- })
-
- req, err := http.NewRequest(http.MethodPost, "/some", strings.NewReader(`{"field":"not integer"}`))
- if err != nil {
- panic(err)
- }
-
- req.Header.Add("Content-Type", "application/json")
-
- route, pathParams, err := router.FindRoute(req)
- if err != nil {
- panic(err)
- }
-
- validationInput := &openapi3filter.RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- Options: opts,
- }
- err = openapi3filter.ValidateRequest(context.Background(), validationInput)
-
- fmt.Println(err.Error())
-
- // Output: request body has an error: doesn't match schema: field "Some field" must be an integer
-}
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index 384b5122e..3b1de11f1 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -1,9 +1,6 @@
package openapi3filter
import (
- "archive/zip"
- "bytes"
- "encoding/csv"
"encoding/json"
"errors"
"fmt"
@@ -17,8 +14,6 @@ import (
"strconv"
"strings"
- "gopkg.in/yaml.v3"
-
"github.com/getkin/kin-openapi/openapi3"
)
@@ -46,8 +41,6 @@ type ParseError struct {
path []interface{}
}
-var _ interface{ Unwrap() error } = ParseError{}
-
func (e *ParseError) Error() string {
var msg []string
if p := e.Path(); len(p) > 0 {
@@ -87,10 +80,6 @@ func (e *ParseError) RootCause() error {
return e.Cause
}
-func (e ParseError) Unwrap() error {
- return e.Cause
-}
-
// Path returns a path to the root cause.
func (e *ParseError) Path() []interface{} {
var path []interface{}
@@ -113,12 +102,10 @@ func invalidSerializationMethodErr(sm *openapi3.SerializationMethod) error {
// Decodes a parameter defined via the content property as an object. It uses
// the user specified decoder, or our build-in decoder for application/json
func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationInput) (
- value interface{},
- schema *openapi3.Schema,
- found bool,
- err error,
-) {
+ value interface{}, schema *openapi3.Schema, err error) {
+
var paramValues []string
+ var found bool
switch param.In {
case openapi3.ParameterInPath:
var paramValue string
@@ -128,9 +115,9 @@ func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationI
case openapi3.ParameterInQuery:
paramValues, found = input.GetQueryParams()[param.Name]
case openapi3.ParameterInHeader:
- var headerValues []string
- if headerValues, found = input.Request.Header[http.CanonicalHeaderKey(param.Name)]; found {
- paramValues = headerValues
+ if paramValue := input.Request.Header.Get(http.CanonicalHeaderKey(param.Name)); paramValue != "" {
+ paramValues = []string{paramValue}
+ found = true
}
case openapi3.ParameterInCookie:
var cookie *http.Cookie
@@ -143,13 +130,13 @@ func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationI
found = true
}
default:
- err = fmt.Errorf("unsupported parameter.in: %q", param.In)
+ err = fmt.Errorf("unsupported parameter's 'in': %s", param.In)
return
}
if !found {
if param.Required {
- err = fmt.Errorf("parameter %q is required, but missing", param.Name)
+ err = fmt.Errorf("parameter '%s' is required, but missing", param.Name)
}
return
}
@@ -164,54 +151,43 @@ func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationI
}
func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) (
- outValue interface{},
- outSchema *openapi3.Schema,
- err error,
-) {
+ outValue interface{}, outSchema *openapi3.Schema, err error) {
// Only query parameters can have multiple values.
if len(values) > 1 && param.In != openapi3.ParameterInQuery {
- err = fmt.Errorf("%s parameter %q cannot have multiple values", param.In, param.Name)
+ err = fmt.Errorf("%s parameter '%s' can't have multiple values", param.In, param.Name)
return
}
content := param.Content
if content == nil {
- err = fmt.Errorf("parameter %q expected to have content", param.Name)
+ err = fmt.Errorf("parameter '%s' expected to have content", param.Name)
return
}
+
// We only know how to decode a parameter if it has one content, application/json
if len(content) != 1 {
- err = fmt.Errorf("multiple content types for parameter %q", param.Name)
+ err = fmt.Errorf("multiple content types for parameter '%s'", param.Name)
return
}
mt := content.Get("application/json")
if mt == nil {
- err = fmt.Errorf("parameter %q has no content schema", param.Name)
+ err = fmt.Errorf("parameter '%s' has no json content schema", param.Name)
return
}
outSchema = mt.Schema.Value
- unmarshal := func(encoded string, paramSchema *openapi3.SchemaRef) (decoded interface{}, err error) {
- if err = json.Unmarshal([]byte(encoded), &decoded); err != nil {
- if paramSchema != nil && paramSchema.Value.Type != "object" {
- decoded, err = encoded, nil
- }
- }
- return
- }
-
if len(values) == 1 {
- if outValue, err = unmarshal(values[0], mt.Schema); err != nil {
- err = fmt.Errorf("error unmarshaling parameter %q", param.Name)
+ if err = json.Unmarshal([]byte(values[0]), &outValue); err != nil {
+ err = fmt.Errorf("error unmarshaling parameter '%s' as json", param.Name)
return
}
} else {
outArray := make([]interface{}, 0, len(values))
for _, v := range values {
var item interface{}
- if item, err = unmarshal(v, outSchema.Items); err != nil {
- err = fmt.Errorf("error unmarshaling parameter %q", param.Name)
+ if err = json.Unmarshal([]byte(v), &item); err != nil {
+ err = fmt.Errorf("error unmarshaling parameter '%s' as json", param.Name)
return
}
outArray = append(outArray, item)
@@ -222,30 +198,30 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string)
}
type valueDecoder interface {
- DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error)
- DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error)
- DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error)
+ DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error)
+ DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error)
+ DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error)
}
// decodeStyledParameter returns a value of an operation's parameter from HTTP request for
-// parameters defined using the style format, and whether the parameter is supplied in the input.
+// parameters defined using the style format.
// The function returns ParseError when HTTP request contains an invalid value of a parameter.
-func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationInput) (interface{}, bool, error) {
+func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationInput) (interface{}, error) {
sm, err := param.SerializationMethod()
if err != nil {
- return nil, false, err
+ return nil, err
}
var dec valueDecoder
switch param.In {
case openapi3.ParameterInPath:
if len(input.PathParams) == 0 {
- return nil, false, nil
+ return nil, nil
}
dec = &pathParamDecoder{pathParams: input.PathParams}
case openapi3.ParameterInQuery:
if len(input.GetQueryParams()) == 0 {
- return nil, false, nil
+ return nil, nil
}
dec = &urlValuesDecoder{values: input.GetQueryParams()}
case openapi3.ParameterInHeader:
@@ -253,79 +229,76 @@ func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationIn
case openapi3.ParameterInCookie:
dec = &cookieParamDecoder{req: input.Request}
default:
- return nil, false, fmt.Errorf("unsupported parameter's 'in': %s", param.In)
+ return nil, fmt.Errorf("unsupported parameter's 'in': %s", param.In)
}
return decodeValue(dec, param.Name, sm, param.Schema, param.Required)
}
-func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (interface{}, bool, error) {
- var found bool
+func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (interface{}, error) {
+ var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error)
if len(schema.Value.AllOf) > 0 {
var value interface{}
var err error
for _, sr := range schema.Value.AllOf {
- var f bool
- value, f, err = decodeValue(dec, param, sm, sr, required)
- found = found || f
+ value, err = decodeValue(dec, param, sm, sr, required)
if value == nil || err != nil {
break
}
}
- return value, found, err
+ return value, err
}
if len(schema.Value.AnyOf) > 0 {
for _, sr := range schema.Value.AnyOf {
- value, f, _ := decodeValue(dec, param, sm, sr, required)
- found = found || f
+ value, _ := decodeValue(dec, param, sm, sr, required)
if value != nil {
- return value, found, nil
+ return value, nil
}
}
- if required {
- return nil, found, fmt.Errorf("decoding anyOf for parameter %q failed", param)
+ if required == true {
+ return nil, fmt.Errorf("decoding anyOf for parameter %q failed", param)
+ } else {
+ return nil, nil
}
- return nil, found, nil
+
}
if len(schema.Value.OneOf) > 0 {
isMatched := 0
var value interface{}
for _, sr := range schema.Value.OneOf {
- v, f, _ := decodeValue(dec, param, sm, sr, required)
- found = found || f
+ v, _ := decodeValue(dec, param, sm, sr, required)
if v != nil {
value = v
isMatched++
}
}
if isMatched == 1 {
- return value, found, nil
+ return value, nil
} else if isMatched > 1 {
- return nil, found, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched)
+ return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched)
}
- if required {
- return nil, found, fmt.Errorf("decoding oneOf failed: %q is required", param)
+ if required == true {
+ return nil, fmt.Errorf("decoding oneOf failed: %q is required", param)
+ } else {
+ return nil, nil
}
- return nil, found, nil
}
-
if schema.Value.Not != nil {
// TODO(decode not): handle decoding "not" JSON Schema
- return nil, found, errors.New("not implemented: decoding 'not'")
+ return nil, errors.New("not implemented: decoding 'not'")
}
if schema.Value.Type != "" {
- var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error)
switch schema.Value.Type {
case "array":
- decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) {
+ decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) {
return dec.DecodeArray(param, sm, schema)
}
case "object":
- decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) {
+ decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) {
return dec.DecodeObject(param, sm, schema)
}
default:
@@ -333,23 +306,7 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho
}
return decodeFn(param, sm, schema)
}
- switch vDecoder := dec.(type) {
- case *pathParamDecoder:
- _, found = vDecoder.pathParams[param]
- case *urlValuesDecoder:
- if schema.Value.Pattern != "" {
- return dec.DecodePrimitive(param, sm, schema)
- }
- _, found = vDecoder.values[param]
- case *headerParamDecoder:
- _, found = vDecoder.header[http.CanonicalHeaderKey(param)]
- case *cookieParamDecoder:
- _, err := vDecoder.req.Cookie(param)
- found = err != http.ErrNoCookie
- default:
- return nil, found, errors.New("unsupported decoder")
- }
- return nil, found, nil
+ return nil, nil
}
// pathParamDecoder decodes values of path parameters.
@@ -357,7 +314,7 @@ type pathParamDecoder struct {
pathParams map[string]string
}
-func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) {
+func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) {
var prefix string
switch sm.Style {
case "simple":
@@ -367,27 +324,26 @@ func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializat
case "matrix":
prefix = ";" + param + "="
default:
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
if d.pathParams == nil {
// HTTP request does not contains a value of the target path parameter.
- return nil, false, nil
+ return nil, nil
}
- raw, ok := d.pathParams[param]
+ raw, ok := d.pathParams[paramKey(param, sm)]
if !ok || raw == "" {
// HTTP request does not contains a value of the target path parameter.
- return nil, false, nil
+ return nil, nil
}
src, err := cutPrefix(raw, prefix)
if err != nil {
- return nil, ok, err
+ return nil, err
}
- val, err := parsePrimitive(src, schema)
- return val, ok, err
+ return parsePrimitive(src, schema)
}
-func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) {
+func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) {
var prefix, delim string
switch {
case sm.Style == "simple":
@@ -405,74 +361,84 @@ func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationM
prefix = ";" + param + "="
delim = ";" + param + "="
default:
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
if d.pathParams == nil {
// HTTP request does not contains a value of the target path parameter.
- return nil, false, nil
+ return nil, nil
}
- raw, ok := d.pathParams[param]
+ raw, ok := d.pathParams[paramKey(param, sm)]
if !ok || raw == "" {
// HTTP request does not contains a value of the target path parameter.
- return nil, false, nil
+ return nil, nil
}
src, err := cutPrefix(raw, prefix)
if err != nil {
- return nil, ok, err
+ return nil, err
}
- val, err := parseArray(strings.Split(src, delim), schema)
- return val, ok, err
+ return parseArray(strings.Split(src, delim), schema)
}
-func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) {
+func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) {
var prefix, propsDelim, valueDelim string
switch {
- case sm.Style == "simple" && !sm.Explode:
+ case sm.Style == "simple" && sm.Explode == false:
propsDelim = ","
valueDelim = ","
- case sm.Style == "simple" && sm.Explode:
+ case sm.Style == "simple" && sm.Explode == true:
propsDelim = ","
valueDelim = "="
- case sm.Style == "label" && !sm.Explode:
+ case sm.Style == "label" && sm.Explode == false:
prefix = "."
propsDelim = ","
valueDelim = ","
- case sm.Style == "label" && sm.Explode:
+ case sm.Style == "label" && sm.Explode == true:
prefix = "."
propsDelim = "."
valueDelim = "="
- case sm.Style == "matrix" && !sm.Explode:
+ case sm.Style == "matrix" && sm.Explode == false:
prefix = ";" + param + "="
propsDelim = ","
valueDelim = ","
- case sm.Style == "matrix" && sm.Explode:
+ case sm.Style == "matrix" && sm.Explode == true:
prefix = ";"
propsDelim = ";"
valueDelim = "="
default:
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
if d.pathParams == nil {
// HTTP request does not contains a value of the target path parameter.
- return nil, false, nil
+ return nil, nil
}
- raw, ok := d.pathParams[param]
+ raw, ok := d.pathParams[paramKey(param, sm)]
if !ok || raw == "" {
// HTTP request does not contains a value of the target path parameter.
- return nil, false, nil
+ return nil, nil
}
src, err := cutPrefix(raw, prefix)
if err != nil {
- return nil, ok, err
+ return nil, err
}
props, err := propsFromString(src, propsDelim, valueDelim)
if err != nil {
- return nil, ok, err
+ return nil, err
+ }
+ return makeObject(props, schema)
+}
+
+// paramKey returns a key to get a raw value of a path parameter.
+func paramKey(param string, sm *openapi3.SerializationMethod) string {
+ switch sm.Style {
+ case "label":
+ return "." + param
+ case "matrix":
+ return ";" + param
+ default:
+ return param
}
- val, err := makeObject(props, schema)
- return val, ok, err
}
// cutPrefix validates that a raw value of a path parameter has the specified prefix,
@@ -496,33 +462,28 @@ type urlValuesDecoder struct {
values url.Values
}
-func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) {
+func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) {
if sm.Style != "form" {
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
- values, ok := d.values[param]
+ values := d.values[param]
if len(values) == 0 {
// HTTP request does not contain a value of the target query parameter.
- return nil, ok, nil
- }
-
- if schema.Value.Type == "" && schema.Value.Pattern != "" {
- return values[0], ok, nil
+ return nil, nil
}
- val, err := parsePrimitive(values[0], schema)
- return val, ok, err
+ return parsePrimitive(values[0], schema)
}
-func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) {
+func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) {
if sm.Style == "deepObject" {
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
- values, ok := d.values[param]
+ values := d.values[param]
if len(values) == 0 {
// HTTP request does not contain a value of the target query parameter.
- return nil, ok, nil
+ return nil, nil
}
if !sm.Explode {
var delim string
@@ -536,92 +497,10 @@ func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationM
}
values = strings.Split(values[0], delim)
}
- val, err := d.parseArray(values, sm, schema)
- return val, ok, err
-}
-
-// parseArray returns an array that contains items from a raw array.
-// Every item is parsed as a primitive value.
-// The function returns an error when an error happened while parse array's items.
-func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMethod, schemaRef *openapi3.SchemaRef) ([]interface{}, error) {
- var value []interface{}
-
- for i, v := range raw {
- item, err := d.parseValue(v, schemaRef.Value.Items)
- if err != nil {
- if v, ok := err.(*ParseError); ok {
- return nil, &ParseError{path: []interface{}{i}, Cause: v}
- }
- return nil, fmt.Errorf("item %d: %w", i, err)
- }
-
- // If the items are nil, then the array is nil. There shouldn't be case where some values are actual primitive
- // values and some are nil values.
- if item == nil {
- return nil, nil
- }
- value = append(value, item)
- }
- return value, nil
-}
-
-func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (interface{}, error) {
- if len(schema.Value.AllOf) > 0 {
- var value interface{}
- var err error
- for _, sr := range schema.Value.AllOf {
- value, err = d.parseValue(v, sr)
- if value == nil || err != nil {
- break
- }
- }
- return value, err
- }
-
- if len(schema.Value.AnyOf) > 0 {
- var value interface{}
- var err error
- for _, sr := range schema.Value.AnyOf {
- if value, err = d.parseValue(v, sr); err == nil {
- return value, nil
- }
- }
-
- return nil, err
- }
-
- if len(schema.Value.OneOf) > 0 {
- isMatched := 0
- var value interface{}
- var err error
- for _, sr := range schema.Value.OneOf {
- result, err := d.parseValue(v, sr)
- if err == nil {
- value = result
- isMatched++
- }
- }
- if isMatched == 1 {
- return value, nil
- } else if isMatched > 1 {
- return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched)
- } else if isMatched == 0 {
- return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched)
- }
-
- return nil, err
- }
-
- if schema.Value.Not != nil {
- // TODO(decode not): handle decoding "not" JSON Schema
- return nil, errors.New("not implemented: decoding 'not'")
- }
-
- return parsePrimitive(v, schema)
-
+ return parseArray(values, schema)
}
-func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) {
+func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) {
var propsFn func(url.Values) (map[string]string, error)
switch sm.Style {
case "form":
@@ -662,27 +541,17 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization
return props, nil
}
default:
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
props, err := propsFn(d.values)
if err != nil {
- return nil, false, err
+ return nil, err
}
if props == nil {
- return nil, false, nil
- }
-
- // check the props
- found := false
- for propName := range schema.Value.Properties {
- if _, ok := props[propName]; ok {
- found = true
- break
- }
+ return nil, nil
}
- val, err := makeObject(props, schema)
- return val, found, err
+ return makeObject(props, schema)
}
// headerParamDecoder decodes values of header parameters.
@@ -690,56 +559,47 @@ type headerParamDecoder struct {
header http.Header
}
-func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) {
+func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) {
if sm.Style != "simple" {
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
- raw, ok := d.header[http.CanonicalHeaderKey(param)]
- if !ok || len(raw) == 0 {
- // HTTP request does not contains a corresponding header or has the empty value
- return nil, ok, nil
- }
-
- val, err := parsePrimitive(raw[0], schema)
- return val, ok, err
+ raw := d.header.Get(http.CanonicalHeaderKey(param))
+ return parsePrimitive(raw, schema)
}
-func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) {
+func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) {
if sm.Style != "simple" {
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
- raw, ok := d.header[http.CanonicalHeaderKey(param)]
- if !ok || len(raw) == 0 {
+ raw := d.header.Get(http.CanonicalHeaderKey(param))
+ if raw == "" {
// HTTP request does not contains a corresponding header
- return nil, ok, nil
+ return nil, nil
}
-
- val, err := parseArray(strings.Split(raw[0], ","), schema)
- return val, ok, err
+ return parseArray(strings.Split(raw, ","), schema)
}
-func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) {
+func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) {
if sm.Style != "simple" {
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
valueDelim := ","
if sm.Explode {
valueDelim = "="
}
- raw, ok := d.header[http.CanonicalHeaderKey(param)]
- if !ok || len(raw) == 0 {
+ raw := d.header.Get(http.CanonicalHeaderKey(param))
+ if raw == "" {
// HTTP request does not contain a corresponding header.
- return nil, ok, nil
+ return nil, nil
}
- props, err := propsFromString(raw[0], ",", valueDelim)
+ props, err := propsFromString(raw, ",", valueDelim)
if err != nil {
- return nil, ok, err
+ return nil, err
}
- val, err := makeObject(props, schema)
- return val, ok, err
+ return makeObject(props, schema)
}
// cookieParamDecoder decodes values of cookie parameters.
@@ -747,63 +607,56 @@ type cookieParamDecoder struct {
req *http.Request
}
-func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) {
+func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) {
if sm.Style != "form" {
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
cookie, err := d.req.Cookie(param)
- found := err != http.ErrNoCookie
- if !found {
+ if err == http.ErrNoCookie {
// HTTP request does not contain a corresponding cookie.
- return nil, found, nil
+ return nil, nil
}
if err != nil {
- return nil, found, fmt.Errorf("decoding param %q: %w", param, err)
+ return nil, fmt.Errorf("decoding param %q: %s", param, err)
}
-
- val, err := parsePrimitive(cookie.Value, schema)
- return val, found, err
+ return parsePrimitive(cookie.Value, schema)
}
-func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) {
+func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) {
if sm.Style != "form" || sm.Explode {
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
cookie, err := d.req.Cookie(param)
- found := err != http.ErrNoCookie
- if !found {
+ if err == http.ErrNoCookie {
// HTTP request does not contain a corresponding cookie.
- return nil, found, nil
+ return nil, nil
}
if err != nil {
- return nil, found, fmt.Errorf("decoding param %q: %w", param, err)
+ return nil, fmt.Errorf("decoding param %q: %s", param, err)
}
- val, err := parseArray(strings.Split(cookie.Value, ","), schema)
- return val, found, err
+ return parseArray(strings.Split(cookie.Value, ","), schema)
}
-func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) {
+func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) {
if sm.Style != "form" || sm.Explode {
- return nil, false, invalidSerializationMethodErr(sm)
+ return nil, invalidSerializationMethodErr(sm)
}
cookie, err := d.req.Cookie(param)
- found := err != http.ErrNoCookie
- if !found {
+ if err == http.ErrNoCookie {
// HTTP request does not contain a corresponding cookie.
- return nil, found, nil
+ return nil, nil
}
if err != nil {
- return nil, found, fmt.Errorf("decoding param %q: %w", param, err)
+ return nil, fmt.Errorf("decoding param %q: %s", param, err)
}
props, err := propsFromString(cookie.Value, ",", ",")
if err != nil {
- return nil, found, err
+ return nil, err
}
- val, err := makeObject(props, schema)
- return val, found, err
+ return makeObject(props, schema)
}
// propsFromString returns a properties map that is created by splitting a source string by propDelim and valueDelim.
@@ -858,7 +711,7 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string
if v, ok := err.(*ParseError); ok {
return nil, &ParseError{path: []interface{}{propName}, Cause: v}
}
- return nil, fmt.Errorf("property %q: %w", propName, err)
+ return nil, fmt.Errorf("property %q: %s", propName, err)
}
obj[propName] = value
}
@@ -876,13 +729,7 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err
if v, ok := err.(*ParseError); ok {
return nil, &ParseError{path: []interface{}{i}, Cause: v}
}
- return nil, fmt.Errorf("item %d: %w", i, err)
- }
-
- // If the items are nil, then the array is nil. There shouldn't be case where some values are actual primitive
- // values and some are nil values.
- if item == nil {
- return nil, nil
+ return nil, fmt.Errorf("item %d: %s", i, err)
}
value = append(value, item)
}
@@ -890,36 +737,29 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err
}
// parsePrimitive returns a value that is created by parsing a source string to a primitive type
-// that is specified by a schema. The function returns nil when the source string is empty.
-// The function panics when a schema has a non-primitive type.
+// that is specified by a JSON schema. The function returns nil when the source string is empty.
+// The function panics when a JSON schema has a non primitive type.
func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) {
if raw == "" {
return nil, nil
}
switch schema.Value.Type {
case "integer":
- if schema.Value.Format == "int32" {
- v, err := strconv.ParseInt(raw, 0, 32)
- if err != nil {
- return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err}
- }
- return int32(v), nil
- }
- v, err := strconv.ParseInt(raw, 0, 64)
+ v, err := strconv.ParseFloat(raw, 64)
if err != nil {
- return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err}
+ return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid integer", Cause: err}
}
return v, nil
case "number":
v, err := strconv.ParseFloat(raw, 64)
if err != nil {
- return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err}
+ return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid number", Cause: err}
}
return v, nil
case "boolean":
v, err := strconv.ParseBool(raw)
if err != nil {
- return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err}
+ return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid number", Cause: err}
}
return v, nil
case "string":
@@ -940,19 +780,10 @@ type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (
// By default, there is content type "application/json" is supported only.
var bodyDecoders = make(map[string]BodyDecoder)
-// RegisteredBodyDecoder returns the registered body decoder for the given content type.
-//
-// If no decoder was registered for the given content type, nil is returned.
-// This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines.
-func RegisteredBodyDecoder(contentType string) BodyDecoder {
- return bodyDecoders[contentType]
-}
-
// RegisterBodyDecoder registers a request body's decoder for a content type.
//
// If a decoder for the specified content type already exists, the function replaces
// it with the specified decoder.
-// This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines.
func RegisterBodyDecoder(contentType string, decoder BodyDecoder) {
if contentType == "" {
panic("contentType is empty")
@@ -966,7 +797,6 @@ func RegisterBodyDecoder(contentType string, decoder BodyDecoder) {
// UnregisterBodyDecoder dissociates a body decoder from a content type.
//
// Decoding this content type will result in an error.
-// This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines.
func UnregisterBodyDecoder(contentType string) {
if contentType == "" {
panic("contentType is empty")
@@ -974,50 +804,31 @@ func UnregisterBodyDecoder(contentType string) {
delete(bodyDecoders, contentType)
}
-var headerCT = http.CanonicalHeaderKey("Content-Type")
-
-const prefixUnsupportedCT = "unsupported content type"
-
// decodeBody returns a decoded body.
// The function returns ParseError when a body is invalid.
-func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (
- string,
- interface{},
- error,
-) {
- contentType := header.Get(headerCT)
- if contentType == "" {
- if _, ok := body.(*multipart.Part); ok {
- contentType = "text/plain"
- }
- }
+func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
+ contentType := header.Get(http.CanonicalHeaderKey("Content-Type"))
mediaType := parseMediaType(contentType)
decoder, ok := bodyDecoders[mediaType]
if !ok {
- return "", nil, &ParseError{
+ return nil, &ParseError{
Kind: KindUnsupportedFormat,
- Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, mediaType),
+ Reason: fmt.Sprintf("unsupported content type %q", mediaType),
}
}
value, err := decoder(body, header, schema, encFn)
if err != nil {
- return "", nil, err
+ return nil, err
}
- return mediaType, value, nil
+ return value, nil
}
func init() {
+ RegisterBodyDecoder("text/plain", plainBodyDecoder)
RegisterBodyDecoder("application/json", jsonBodyDecoder)
- RegisterBodyDecoder("application/json-patch+json", jsonBodyDecoder)
- RegisterBodyDecoder("application/octet-stream", FileBodyDecoder)
- RegisterBodyDecoder("application/problem+json", jsonBodyDecoder)
RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder)
- RegisterBodyDecoder("application/x-yaml", yamlBodyDecoder)
- RegisterBodyDecoder("application/yaml", yamlBodyDecoder)
- RegisterBodyDecoder("application/zip", zipFileBodyDecoder)
RegisterBodyDecoder("multipart/form-data", multipartBodyDecoder)
- RegisterBodyDecoder("text/csv", csvBodyDecoder)
- RegisterBodyDecoder("text/plain", plainBodyDecoder)
+ RegisterBodyDecoder("application/octet-stream", FileBodyDecoder)
}
func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
@@ -1030,37 +841,27 @@ func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schem
func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
var value interface{}
- dec := json.NewDecoder(body)
- dec.UseNumber()
- if err := dec.Decode(&value); err != nil {
- return nil, &ParseError{Kind: KindInvalidFormat, Cause: err}
- }
- return value, nil
-}
-
-func yamlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
- var value interface{}
- if err := yaml.NewDecoder(body).Decode(&value); err != nil {
+ if err := json.NewDecoder(body).Decode(&value); err != nil {
return nil, &ParseError{Kind: KindInvalidFormat, Cause: err}
}
return value, nil
}
func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
- // Validate schema of request body.
+ // Validate JSON schema of request body.
// By the OpenAPI 3 specification request body's schema must have type "object".
// Properties of the schema describes individual parts of request body.
if schema.Value.Type != "object" {
- return nil, errors.New("unsupported schema of request body")
+ return nil, errors.New("unsupported JSON schema of request body")
}
for propName, propSchema := range schema.Value.Properties {
switch propSchema.Value.Type {
case "object":
- return nil, fmt.Errorf("unsupported schema of request body's property %q", propName)
+ return nil, fmt.Errorf("unsupported JSON schema of request body's property %q", propName)
case "array":
items := propSchema.Value.Items.Value
if items.Type != "string" && items.Type != "integer" && items.Type != "number" && items.Type != "boolean" {
- return nil, fmt.Errorf("unsupported schema of request body's property %q", propName)
+ return nil, fmt.Errorf("unsupported JSON schema of request body's property %q", propName)
}
}
}
@@ -1088,7 +889,7 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.
}
sm := enc.SerializationMethod()
- if value, _, err = decodeValue(dec, name, sm, prop, false); err != nil {
+ if value, err = decodeValue(dec, name, sm, prop, false); err != nil {
return nil, err
}
obj[name] = value
@@ -1099,12 +900,12 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.
func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
if schema.Value.Type != "object" {
- return nil, errors.New("unsupported schema of request body")
+ return nil, errors.New("unsupported JSON schema of request body")
}
// Parse form.
values := make(map[string][]interface{})
- contentType := header.Get(headerCT)
+ contentType := header.Get(http.CanonicalHeaderKey("Content-Type"))
_, params, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, err
@@ -1127,78 +928,54 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S
enc = encFn(name)
}
subEncFn := func(string) *openapi3.Encoding { return enc }
-
+ // If the property's schema has type "array" it is means that the form contains a few parts with the same name.
+ // Every such part has a type that is defined by an items schema in the property's schema.
var valueSchema *openapi3.SchemaRef
- if len(schema.Value.AllOf) > 0 {
- var exists bool
- for _, sr := range schema.Value.AllOf {
- if valueSchema, exists = sr.Value.Properties[name]; exists {
- break
+ var exists bool
+ valueSchema, exists = schema.Value.Properties[name]
+ if !exists {
+ anyProperties := schema.Value.AdditionalPropertiesAllowed
+ if anyProperties != nil {
+ switch *anyProperties {
+ case true:
+ //additionalProperties: true
+ continue
+ default:
+ //additionalProperties: false
+ return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
}
}
- if !exists {
+ if schema.Value.AdditionalProperties == nil {
return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
}
- } else {
- // If the property's schema has type "array" it is means that the form contains a few parts with the same name.
- // Every such part has a type that is defined by an items schema in the property's schema.
- var exists bool
- if valueSchema, exists = schema.Value.Properties[name]; !exists {
- if anyProperties := schema.Value.AdditionalProperties.Has; anyProperties != nil {
- switch *anyProperties {
- case true:
- //additionalProperties: true
- continue
- default:
- //additionalProperties: false
- return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
- }
- }
- if schema.Value.AdditionalProperties.Schema == nil {
- return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
- }
- if valueSchema, exists = schema.Value.AdditionalProperties.Schema.Value.Properties[name]; !exists {
- return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
- }
- }
- if valueSchema.Value.Type == "array" {
- valueSchema = valueSchema.Value.Items
+ valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name]
+ if !exists {
+ return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)}
}
}
+ if valueSchema.Value.Type == "array" {
+ valueSchema = valueSchema.Value.Items
+ }
var value interface{}
- if _, value, err = decodeBody(part, http.Header(part.Header), valueSchema, subEncFn); err != nil {
+ if value, err = decodeBody(part, http.Header(part.Header), valueSchema, subEncFn); err != nil {
if v, ok := err.(*ParseError); ok {
return nil, &ParseError{path: []interface{}{name}, Cause: v}
}
- return nil, fmt.Errorf("part %s: %w", name, err)
+ return nil, fmt.Errorf("part %s: %s", name, err)
}
values[name] = append(values[name], value)
}
allTheProperties := make(map[string]*openapi3.SchemaRef)
- if len(schema.Value.AllOf) > 0 {
- for _, sr := range schema.Value.AllOf {
- for k, v := range sr.Value.Properties {
- allTheProperties[k] = v
- }
- if addProps := sr.Value.AdditionalProperties.Schema; addProps != nil {
- for k, v := range addProps.Value.Properties {
- allTheProperties[k] = v
- }
- }
- }
- } else {
- for k, v := range schema.Value.Properties {
+ for k, v := range schema.Value.Properties {
+ allTheProperties[k] = v
+ }
+ if schema.Value.AdditionalProperties != nil {
+ for k, v := range schema.Value.AdditionalProperties.Value.Properties {
allTheProperties[k] = v
}
- if addProps := schema.Value.AdditionalProperties.Schema; addProps != nil {
- for k, v := range addProps.Value.Properties {
- allTheProperties[k] = v
- }
- }
}
-
// Make an object value from form values.
obj := make(map[string]interface{})
for name, prop := range allTheProperties {
@@ -1224,74 +1001,3 @@ func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema
}
return string(data), nil
}
-
-// zipFileBodyDecoder is a body decoder that decodes a zip file body to a string.
-func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
- buff := bytes.NewBuffer([]byte{})
- size, err := io.Copy(buff, body)
- if err != nil {
- return nil, err
- }
-
- zr, err := zip.NewReader(bytes.NewReader(buff.Bytes()), size)
- if err != nil {
- return nil, err
- }
-
- const bufferSize = 256
- content := make([]byte, 0, bufferSize*len(zr.File))
- buffer := make([]byte /*0,*/, bufferSize)
-
- for _, f := range zr.File {
- err := func() error {
- rc, err := f.Open()
- if err != nil {
- return err
- }
- defer func() {
- _ = rc.Close()
- }()
-
- for {
- n, err := rc.Read(buffer)
- if 0 < n {
- content = append(content, buffer...)
- }
- if err == io.EOF {
- break
- }
- if err != nil {
- return err
- }
- }
-
- return nil
- }()
-
- if err != nil {
- return nil, err
- }
- }
-
- return string(content), nil
-}
-
-// csvBodyDecoder is a body decoder that decodes a csv body to a string.
-func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
- r := csv.NewReader(body)
-
- var content string
- for {
- record, err := r.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, err
- }
-
- content += strings.Join(record, ",") + "\n"
- }
-
- return content, nil
-}
diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go
index 449ba0e3a..4a5cefc5e 100644
--- a/openapi3filter/req_resp_decoder_test.go
+++ b/openapi3filter/req_resp_decoder_test.go
@@ -2,7 +2,6 @@ package openapi3filter
import (
"bytes"
- "context"
"encoding/json"
"fmt"
"io"
@@ -15,10 +14,8 @@ import (
"strings"
"testing"
- "github.com/stretchr/testify/require"
-
"github.com/getkin/kin-openapi/openapi3"
- legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
+ "github.com/stretchr/testify/require"
)
func TestDecodeParameter(t *testing.T) {
@@ -32,7 +29,7 @@ func TestDecodeParameter(t *testing.T) {
objectOf = func(args ...interface{}) *openapi3.SchemaRef {
s := &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "object", Properties: make(map[string]*openapi3.SchemaRef)}}
if len(args)%2 != 0 {
- panic("invalid arguments. must be an even number of arguments")
+ panic("invalid arguments. must be an odd number of arguments")
}
for i := 0; i < len(args)/2; i++ {
propName := args[i*2].(string)
@@ -76,7 +73,6 @@ func TestDecodeParameter(t *testing.T) {
header string
cookie string
want interface{}
- found bool
err error
}
@@ -92,27 +88,23 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: stringSchema},
path: "/foo",
want: "foo",
- found: true,
},
{
name: "simple explode",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: stringSchema},
path: "/foo",
want: "foo",
- found: true,
},
{
name: "label",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringSchema},
path: "/.foo",
want: "foo",
- found: true,
},
{
name: "label invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringSchema},
path: "/foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -120,13 +112,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringSchema},
path: "/.foo",
want: "foo",
- found: true,
},
{
name: "label explode invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringSchema},
path: "/foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -134,13 +124,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringSchema},
path: "/;param=foo",
want: "foo",
- found: true,
},
{
name: "matrix invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringSchema},
path: "/foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -148,13 +136,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringSchema},
path: "/;param=foo",
want: "foo",
- found: true,
},
{
name: "matrix explode invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringSchema},
path: "/foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -162,27 +148,23 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Schema: stringSchema},
path: "/foo",
want: "foo",
- found: true,
},
{
name: "string",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: stringSchema},
path: "/foo",
want: "foo",
- found: true,
},
{
name: "integer",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: integerSchema},
path: "/1",
- want: int64(1),
- found: true,
+ want: float64(1),
},
{
name: "integer invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: integerSchema},
path: "/foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -190,13 +172,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Schema: numberSchema},
path: "/1.1",
want: 1.1,
- found: true,
},
{
name: "number invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: numberSchema},
path: "/foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -204,13 +184,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Schema: booleanSchema},
path: "/true",
want: true,
- found: true,
},
{
name: "boolean invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: booleanSchema},
path: "/foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
},
@@ -223,27 +201,23 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: arraySchema},
path: "/foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "simple explode",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: arraySchema},
path: "/foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "label",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema},
path: "/.foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "label invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema},
path: "/foo,bar",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"},
},
{
@@ -251,13 +225,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema},
path: "/.foo.bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "label explode invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema},
path: "/foo.bar",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo.bar"},
},
{
@@ -265,13 +237,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema},
path: "/;param=foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "matrix invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema},
path: "/foo,bar",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"},
},
{
@@ -279,13 +249,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema},
path: "/;param=foo;param=bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "matrix explode invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema},
path: "/foo,bar",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"},
},
{
@@ -293,27 +261,23 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Schema: arraySchema},
path: "/foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "invalid integer items",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(integerSchema)},
path: "/1,foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
{
name: "invalid number items",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(numberSchema)},
path: "/1.1,foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
{
name: "invalid boolean items",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(booleanSchema)},
path: "/true,foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
},
@@ -326,27 +290,23 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: objectSchema},
path: "/id,foo,name,bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "simple explode",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: objectSchema},
path: "/id=foo,name=bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "label",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema},
path: "/.id,foo,name,bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "label invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema},
path: "/id,foo,name,bar",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "id,foo,name,bar"},
},
{
@@ -354,13 +314,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema},
path: "/.id=foo.name=bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "label explode invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema},
path: "/id=foo.name=bar",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "id=foo.name=bar"},
},
{
@@ -368,13 +326,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema},
path: "/;param=id,foo,name,bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "matrix invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema},
path: "/id,foo,name,bar",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "id,foo,name,bar"},
},
{
@@ -382,13 +338,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema},
path: "/;id=foo;name=bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "matrix explode invalid",
param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema},
path: "/id=foo;name=bar",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "id=foo;name=bar"},
},
{
@@ -396,27 +350,23 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectSchema},
path: "/id,foo,name,bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "invalid integer prop",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", integerSchema)},
path: "/foo,bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
{
name: "invalid number prop",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", numberSchema)},
path: "/foo,bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
{
name: "invalid boolean prop",
param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", booleanSchema)},
path: "/foo,bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
},
@@ -429,41 +379,35 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: stringSchema},
query: "param=foo",
want: "foo",
- found: true,
},
{
name: "form explode",
param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: stringSchema},
query: "param=foo",
want: "foo",
- found: true,
},
{
name: "default",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: stringSchema},
query: "param=foo",
want: "foo",
- found: true,
},
{
name: "string",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: stringSchema},
query: "param=foo",
want: "foo",
- found: true,
},
{
name: "integer",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: integerSchema},
query: "param=1",
- want: int64(1),
- found: true,
+ want: float64(1),
},
{
name: "integer invalid",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: integerSchema},
query: "param=foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -471,13 +415,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "query", Schema: numberSchema},
query: "param=1.1",
want: 1.1,
- found: true,
},
{
name: "number invalid",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: numberSchema},
query: "param=foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -485,13 +427,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "query", Schema: booleanSchema},
query: "param=true",
want: true,
- found: true,
},
{
name: "boolean invalid",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: booleanSchema},
query: "param=foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
},
@@ -504,13 +444,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "query", Schema: allofSchema},
query: "param=1",
want: float64(1),
- found: true,
},
{
name: "allofSchema string",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: allofSchema},
query: "param=abdf",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "abdf"},
},
},
@@ -522,15 +460,13 @@ func TestDecodeParameter(t *testing.T) {
name: "anyofSchema integer",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: anyofSchema},
query: "param=1",
- want: int64(1),
- found: true,
+ want: float64(1),
},
{
name: "anyofSchema string",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: anyofSchema},
query: "param=abdf",
want: "abdf",
- found: true,
},
},
},
@@ -542,21 +478,18 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema},
query: "param=true",
want: true,
- found: true,
},
{
name: "oneofSchema int",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema},
query: "param=1122",
- want: int64(1122),
- found: true,
+ want: float64(1122),
},
{
name: "oneofSchema string",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema},
query: "param=abcd",
want: nil,
- found: true,
},
},
},
@@ -568,69 +501,59 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: arraySchema},
query: "param=foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "form explode",
param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: arraySchema},
query: "param=foo¶m=bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "spaceDelimited",
param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: noExplode, Schema: arraySchema},
query: "param=foo bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "spaceDelimited explode",
param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: explode, Schema: arraySchema},
query: "param=foo¶m=bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "pipeDelimited",
param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: noExplode, Schema: arraySchema},
query: "param=foo|bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "pipeDelimited explode",
param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: explode, Schema: arraySchema},
query: "param=foo¶m=bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "default",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: arraySchema},
query: "param=foo¶m=bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "invalid integer items",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(integerSchema)},
query: "param=1¶m=foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
{
name: "invalid number items",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(numberSchema)},
query: "param=1.1¶m=foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
{
name: "invalid boolean items",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(booleanSchema)},
query: "param=true¶m=foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
},
@@ -643,48 +566,41 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: objectSchema},
query: "param=id,foo,name,bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "form explode",
param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: objectSchema},
query: "id=foo&name=bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "deepObject explode",
param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectSchema},
query: "param[id]=foo¶m[name]=bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "default",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectSchema},
query: "id=foo&name=bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "invalid integer prop",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", integerSchema)},
query: "foo=bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
{
name: "invalid number prop",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", numberSchema)},
query: "foo=bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
{
name: "invalid boolean prop",
param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", booleanSchema)},
query: "foo=bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
},
@@ -697,41 +613,35 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: stringSchema},
header: "X-Param:foo",
want: "foo",
- found: true,
},
{
name: "simple explode",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: stringSchema},
header: "X-Param:foo",
want: "foo",
- found: true,
},
{
name: "default",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: stringSchema},
header: "X-Param:foo",
want: "foo",
- found: true,
},
{
name: "string",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: stringSchema},
header: "X-Param:foo",
want: "foo",
- found: true,
},
{
name: "integer",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema},
header: "X-Param:1",
- want: int64(1),
- found: true,
+ want: float64(1),
},
{
name: "integer invalid",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema},
header: "X-Param:foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -739,13 +649,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: numberSchema},
header: "X-Param:1.1",
want: 1.1,
- found: true,
},
{
name: "number invalid",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: numberSchema},
header: "X-Param:foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -753,13 +661,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: booleanSchema},
header: "X-Param:true",
want: true,
- found: true,
},
{
name: "boolean invalid",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: booleanSchema},
header: "X-Param:foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
},
@@ -772,41 +678,35 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: arraySchema},
header: "X-Param:foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "simple explode",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: arraySchema},
header: "X-Param:foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "default",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arraySchema},
header: "X-Param:foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "invalid integer items",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(integerSchema)},
header: "X-Param:1,foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
{
name: "invalid number items",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(numberSchema)},
header: "X-Param:1.1,foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
{
name: "invalid boolean items",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(booleanSchema)},
header: "X-Param:true,foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
},
@@ -819,48 +719,35 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: objectSchema},
header: "X-Param:id,foo,name,bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "simple explode",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: objectSchema},
header: "X-Param:id=foo,name=bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "default",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectSchema},
header: "X-Param:id,foo,name,bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
- },
- {
- name: "valid integer prop",
- param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema},
- header: "X-Param:88",
- found: true,
- want: int64(88),
},
{
name: "invalid integer prop",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", integerSchema)},
header: "X-Param:foo,bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
{
name: "invalid number prop",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", numberSchema)},
header: "X-Param:foo,bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
{
name: "invalid boolean prop",
param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", booleanSchema)},
header: "X-Param:foo,bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
},
@@ -873,41 +760,35 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: stringSchema},
cookie: "X-Param:foo",
want: "foo",
- found: true,
},
{
name: "form explode",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: explode, Schema: stringSchema},
cookie: "X-Param:foo",
want: "foo",
- found: true,
},
{
name: "default",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: stringSchema},
cookie: "X-Param:foo",
want: "foo",
- found: true,
},
{
name: "string",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: stringSchema},
cookie: "X-Param:foo",
want: "foo",
- found: true,
},
{
name: "integer",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: integerSchema},
cookie: "X-Param:1",
- want: int64(1),
- found: true,
+ want: float64(1),
},
{
name: "integer invalid",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: integerSchema},
cookie: "X-Param:foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -915,13 +796,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: numberSchema},
cookie: "X-Param:1.1",
want: 1.1,
- found: true,
},
{
name: "number invalid",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: numberSchema},
cookie: "X-Param:foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
{
@@ -929,13 +808,11 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: booleanSchema},
cookie: "X-Param:true",
want: true,
- found: true,
},
{
name: "boolean invalid",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: booleanSchema},
cookie: "X-Param:foo",
- found: true,
err: &ParseError{Kind: KindInvalidFormat, Value: "foo"},
},
},
@@ -948,27 +825,23 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arraySchema},
cookie: "X-Param:foo,bar",
want: []interface{}{"foo", "bar"},
- found: true,
},
{
name: "invalid integer items",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(integerSchema)},
cookie: "X-Param:1,foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
{
name: "invalid number items",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(numberSchema)},
cookie: "X-Param:1.1,foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
{
name: "invalid boolean items",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(booleanSchema)},
cookie: "X-Param:true,foo",
- found: true,
err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}},
},
},
@@ -981,27 +854,23 @@ func TestDecodeParameter(t *testing.T) {
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectSchema},
cookie: "X-Param:id,foo,name,bar",
want: map[string]interface{}{"id": "foo", "name": "bar"},
- found: true,
},
{
name: "invalid integer prop",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", integerSchema)},
cookie: "X-Param:foo,bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
{
name: "invalid number prop",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", numberSchema)},
cookie: "X-Param:foo,bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
{
name: "invalid boolean prop",
param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", booleanSchema)},
cookie: "X-Param:foo,bar",
- found: true,
err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}},
},
},
@@ -1034,35 +903,37 @@ func TestDecodeParameter(t *testing.T) {
req.AddCookie(&http.Cookie{Name: v[0], Value: v[1]})
}
- path := "/test"
+ var path string
if tc.path != "" {
- path += "/{" + tc.param.Name + "}"
- tc.param.Required = true
+ switch tc.param.Style {
+ case "label":
+ path = "." + tc.param.Name
+ case "matrix":
+ path = ";" + tc.param.Name
+ default:
+ path = tc.param.Name
+ }
+ if tc.param.Explode != nil && *tc.param.Explode {
+ path += "*"
+ }
+ path = "/{" + path + "}"
}
info := &openapi3.Info{
Title: "MyAPI",
Version: "0.1",
}
- spec := &openapi3.T{OpenAPI: "3.0.0", Info: info}
- op := &openapi3.Operation{
- OperationID: "test",
- Parameters: []*openapi3.ParameterRef{{Value: tc.param}},
- Responses: openapi3.NewResponses(),
- }
- spec.AddOperation(path, http.MethodGet, op)
- err = spec.Validate(context.Background())
- require.NoError(t, err)
- router, err := legacyrouter.NewRouter(spec)
- require.NoError(t, err)
+ spec := &openapi3.Swagger{OpenAPI: "3.0.0", Info: info}
+ op := &openapi3.Operation{OperationID: "test", Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, Responses: make(openapi3.Responses)}
+ spec.AddOperation("/test"+path, http.MethodGet, op)
+ router := NewRouter()
+ require.NoError(t, router.AddSwagger(spec), "failed to create a router")
- route, pathParams, err := router.FindRoute(req)
- require.NoError(t, err)
+ route, pathParams, err := router.FindRoute(req.Method, req.URL)
+ require.NoError(t, err, "failed to find a route")
input := &RequestValidationInput{Request: req, PathParams: pathParams, Route: route}
- got, found, err := decodeStyledParameter(tc.param, input)
-
- require.Truef(t, found == tc.found, "got found: %t, want found: %t", found, tc.found)
+ got, err := decodeStyledParameter(tc.param, input)
if tc.err != nil {
require.Error(t, err)
@@ -1106,7 +977,6 @@ func TestDecodeBody(t *testing.T) {
{name: "c", contentType: "text/plain", data: strings.NewReader("c2")},
{name: "d", contentType: "application/json", data: bytes.NewReader(d)},
{name: "f", contentType: "application/octet-stream", data: strings.NewReader("foo"), filename: "f1"},
- {name: "g", data: strings.NewReader("g1")},
})
require.NoError(t, err)
@@ -1120,14 +990,10 @@ func TestDecodeBody(t *testing.T) {
{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
})
- require.NoError(t, err)
-
multipartAdditionalProps, multipartMimeAdditionalProps, err := newTestMultipartForm([]*testFormPart{
{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
})
- require.NoError(t, err)
-
multipartAdditionalPropsErr, multipartMimeAdditionalPropsErr, err := newTestMultipartForm([]*testFormPart{
{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
@@ -1145,7 +1011,7 @@ func TestDecodeBody(t *testing.T) {
wantErr error
}{
{
- name: prefixUnsupportedCT,
+ name: "unsupported content type",
mime: "application/xml",
wantErr: &ParseError{Kind: KindUnsupportedFormat},
},
@@ -1167,18 +1033,6 @@ func TestDecodeBody(t *testing.T) {
body: strings.NewReader("\"foo\""),
want: "foo",
},
- {
- name: "x-yaml",
- mime: "application/x-yaml",
- body: strings.NewReader("foo"),
- want: "foo",
- },
- {
- name: "yaml",
- mime: "application/yaml",
- body: strings.NewReader("foo"),
- want: "foo",
- },
{
name: "urlencoded form",
mime: "application/x-www-form-urlencoded",
@@ -1187,7 +1041,7 @@ func TestDecodeBody(t *testing.T) {
WithProperty("a", openapi3.NewStringSchema()).
WithProperty("b", openapi3.NewIntegerSchema()).
WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())),
- want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}},
+ want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}},
},
{
name: "urlencoded space delimited",
@@ -1200,7 +1054,7 @@ func TestDecodeBody(t *testing.T) {
encoding: map[string]*openapi3.Encoding{
"c": {Style: openapi3.SerializationSpaceDelimited, Explode: boolPtr(false)},
},
- want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}},
+ want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}},
},
{
name: "urlencoded pipe delimited",
@@ -1213,7 +1067,7 @@ func TestDecodeBody(t *testing.T) {
encoding: map[string]*openapi3.Encoding{
"c": {Style: openapi3.SerializationPipeDelimited, Explode: boolPtr(false)},
},
- want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}},
+ want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}},
},
{
name: "multipart",
@@ -1224,9 +1078,8 @@ func TestDecodeBody(t *testing.T) {
WithProperty("b", openapi3.NewIntegerSchema()).
WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())).
WithProperty("d", openapi3.NewObjectSchema().WithProperty("d1", openapi3.NewStringSchema())).
- WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")).
- WithProperty("g", openapi3.NewStringSchema()),
- want: map[string]interface{}{"a": "a1", "b": json.Number("10"), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"},
+ WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")),
+ want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo"},
},
{
name: "multipartExtraPart",
@@ -1277,7 +1130,7 @@ func TestDecodeBody(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
h := make(http.Header)
- h.Set(headerCT, tc.mime)
+ h.Set(http.CanonicalHeaderKey("Content-Type"), tc.mime)
var schemaRef *openapi3.SchemaRef
if tc.schema != nil {
schemaRef = tc.schema.NewRef()
@@ -1288,7 +1141,7 @@ func TestDecodeBody(t *testing.T) {
}
return tc.encoding[name]
}
- _, got, err := decodeBody(tc.body, h, schemaRef, encFn)
+ got, err := decodeBody(tc.body, h, schemaRef, encFn)
if tc.wantErr != nil {
require.Error(t, err)
@@ -1323,7 +1176,7 @@ func newTestMultipartForm(parts []*testFormPart) (io.Reader, string, error) {
}
h := make(textproto.MIMEHeader)
- h.Set(headerCT, p.contentType)
+ h.Set("Content-Type", p.contentType)
h.Set("Content-Disposition", disp)
pw, err := w.CreatePart(h)
if err != nil {
@@ -1337,42 +1190,39 @@ func newTestMultipartForm(parts []*testFormPart) (io.Reader, string, error) {
}
func TestRegisterAndUnregisterBodyDecoder(t *testing.T) {
- var decoder BodyDecoder
- decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (decoded interface{}, err error) {
- var data []byte
- if data, err = ioutil.ReadAll(body); err != nil {
- return
+ var (
+ contentType = "text/csv"
+ decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
+ data, err := ioutil.ReadAll(body)
+ if err != nil {
+ return nil, err
+ }
+ var vv []interface{}
+ for _, v := range strings.Split(string(data), ",") {
+ vv = append(vv, v)
+ }
+ return vv, nil
}
- return strings.Split(string(data), ","), nil
- }
- contentType := "application/csv"
+ schema = openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema()).NewRef()
+ encFn = func(string) *openapi3.Encoding { return nil }
+ body = strings.NewReader("foo,bar")
+ want = []interface{}{"foo", "bar"}
+ wantErr = &ParseError{Kind: KindUnsupportedFormat}
+ )
h := make(http.Header)
- h.Set(headerCT, contentType)
-
- originalDecoder := RegisteredBodyDecoder(contentType)
- require.Nil(t, originalDecoder)
+ h.Set(http.CanonicalHeaderKey("Content-Type"), contentType)
RegisterBodyDecoder(contentType, decoder)
- require.Equal(t, fmt.Sprintf("%v", decoder), fmt.Sprintf("%v", RegisteredBodyDecoder(contentType)))
-
- body := strings.NewReader("foo,bar")
- schema := openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema()).NewRef()
- encFn := func(string) *openapi3.Encoding { return nil }
- _, got, err := decodeBody(body, h, schema, encFn)
+ got, err := decodeBody(body, h, schema, encFn)
require.NoError(t, err)
- require.Equal(t, []string{"foo", "bar"}, got)
+ require.Truef(t, reflect.DeepEqual(got, want), "got %v, want %v", got, want)
UnregisterBodyDecoder(contentType)
+ _, err = decodeBody(body, h, schema, encFn)
- originalDecoder = RegisteredBodyDecoder(contentType)
- require.Nil(t, originalDecoder)
-
- _, _, err = decodeBody(body, h, schema, encFn)
- require.Equal(t, &ParseError{
- Kind: KindUnsupportedFormat,
- Reason: prefixUnsupportedCT + ` "application/csv"`,
- }, err)
+ require.Error(t, err)
+ require.Truef(t, matchParseError(err, wantErr), "got error:\n%v\nwant error:\n%v", err, wantErr)
}
func matchParseError(got, want error) bool {
diff --git a/openapi3filter/req_resp_encoder.go b/openapi3filter/req_resp_encoder.go
deleted file mode 100644
index 36b7db6fd..000000000
--- a/openapi3filter/req_resp_encoder.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package openapi3filter
-
-import (
- "encoding/json"
- "fmt"
-)
-
-func encodeBody(body interface{}, mediaType string) ([]byte, error) {
- encoder, ok := bodyEncoders[mediaType]
- if !ok {
- return nil, &ParseError{
- Kind: KindUnsupportedFormat,
- Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, mediaType),
- }
- }
- return encoder(body)
-}
-
-type BodyEncoder func(body interface{}) ([]byte, error)
-
-var bodyEncoders = map[string]BodyEncoder{
- "application/json": json.Marshal,
-}
-
-func RegisterBodyEncoder(contentType string, encoder BodyEncoder) {
- if contentType == "" {
- panic("contentType is empty")
- }
- if encoder == nil {
- panic("encoder is not defined")
- }
- bodyEncoders[contentType] = encoder
-}
-
-// This call is not thread-safe: body encoders should not be created/destroyed by multiple goroutines.
-func UnregisterBodyEncoder(contentType string) {
- if contentType == "" {
- panic("contentType is empty")
- }
- delete(bodyEncoders, contentType)
-}
-
-// RegisteredBodyEncoder returns the registered body encoder for the given content type.
-//
-// If no encoder was registered for the given content type, nil is returned.
-// This call is not thread-safe: body encoders should not be created/destroyed by multiple goroutines.
-func RegisteredBodyEncoder(contentType string) BodyEncoder {
- return bodyEncoders[contentType]
-}
diff --git a/openapi3filter/req_resp_encoder_test.go b/openapi3filter/req_resp_encoder_test.go
deleted file mode 100644
index 11fe2afa9..000000000
--- a/openapi3filter/req_resp_encoder_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package openapi3filter
-
-import (
- "fmt"
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestRegisterAndUnregisterBodyEncoder(t *testing.T) {
- var encoder BodyEncoder
- encoder = func(body interface{}) (data []byte, err error) {
- return []byte(strings.Join(body.([]string), ",")), nil
- }
- contentType := "text/csv"
- h := make(http.Header)
- h.Set(headerCT, contentType)
-
- originalEncoder := RegisteredBodyEncoder(contentType)
- require.Nil(t, originalEncoder)
-
- RegisterBodyEncoder(contentType, encoder)
- require.Equal(t, fmt.Sprintf("%v", encoder), fmt.Sprintf("%v", RegisteredBodyEncoder(contentType)))
-
- body := []string{"foo", "bar"}
- got, err := encodeBody(body, contentType)
-
- require.NoError(t, err)
- require.Equal(t, []byte("foo,bar"), got)
-
- UnregisterBodyEncoder(contentType)
-
- originalEncoder = RegisteredBodyEncoder(contentType)
- require.Nil(t, originalEncoder)
-
- _, err = encodeBody(body, contentType)
- require.Equal(t, &ParseError{
- Kind: KindUnsupportedFormat,
- Reason: prefixUnsupportedCT + ` "text/csv"`,
- }, err)
-}
diff --git a/openapi3filter/router.go b/openapi3filter/router.go
new file mode 100644
index 000000000..3904750eb
--- /dev/null
+++ b/openapi3filter/router.go
@@ -0,0 +1,217 @@
+package openapi3filter
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/getkin/kin-openapi/openapi3"
+ "github.com/getkin/kin-openapi/pathpattern"
+)
+
+type Route struct {
+ Swagger *openapi3.Swagger
+ Server *openapi3.Server
+ Path string
+ PathItem *openapi3.PathItem
+ Method string
+ Operation *openapi3.Operation
+
+ // For developers who want use the router for handling too
+ Handler http.Handler
+}
+
+// Routers maps a HTTP request to a Router.
+type Routers []*Router
+
+func (routers Routers) FindRoute(method string, url *url.URL) (*Router, *Route, map[string]string, error) {
+ for _, router := range routers {
+ // Skip routers that have DO NOT have servers
+ if len(router.swagger.Servers) == 0 {
+ continue
+ }
+ route, pathParams, err := router.FindRoute(method, url)
+ if err == nil {
+ return router, route, pathParams, nil
+ }
+ }
+ for _, router := range routers {
+ // Skip routers that DO have servers
+ if len(router.swagger.Servers) > 0 {
+ continue
+ }
+ route, pathParams, err := router.FindRoute(method, url)
+ if err == nil {
+ return router, route, pathParams, nil
+ }
+ }
+ return nil, nil, nil, &RouteError{
+ Reason: "None of the routers matches",
+ }
+}
+
+// Router maps a HTTP request to an OpenAPI operation.
+type Router struct {
+ swagger *openapi3.Swagger
+ pathNode *pathpattern.Node
+}
+
+// NewRouter creates a new router.
+//
+// If the given Swagger has servers, router will use them.
+// All operations of the Swagger will be added to the router.
+func NewRouter() *Router {
+ return &Router{}
+}
+
+// WithSwaggerFromFile loads the Swagger file and adds it using WithSwagger.
+// Panics on any error.
+func (router *Router) WithSwaggerFromFile(path string) *Router {
+ if err := router.AddSwaggerFromFile(path); err != nil {
+ panic(err)
+ }
+ return router
+}
+
+// WithSwagger adds all operations in the OpenAPI specification.
+// Panics on any error.
+func (router *Router) WithSwagger(swagger *openapi3.Swagger) *Router {
+ if err := router.AddSwagger(swagger); err != nil {
+ panic(err)
+ }
+ return router
+}
+
+// AddSwaggerFromFile loads the Swagger file and adds it using AddSwagger.
+func (router *Router) AddSwaggerFromFile(path string) error {
+ swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile(path)
+ if err != nil {
+ return err
+ }
+ return router.AddSwagger(swagger)
+}
+
+// AddSwagger adds all operations in the OpenAPI specification.
+func (router *Router) AddSwagger(swagger *openapi3.Swagger) error {
+ if err := swagger.Validate(context.TODO()); err != nil {
+ return fmt.Errorf("Validating Swagger failed: %v", err)
+ }
+ router.swagger = swagger
+ root := router.node()
+ for path, pathItem := range swagger.Paths {
+ for method, operation := range pathItem.Operations() {
+ method = strings.ToUpper(method)
+ if err := root.Add(method+" "+path, &Route{
+ Swagger: swagger,
+ Path: path,
+ PathItem: pathItem,
+ Method: method,
+ Operation: operation,
+ }, nil); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// AddRoute adds a route in the router.
+func (router *Router) AddRoute(route *Route) error {
+ method := route.Method
+ if method == "" {
+ return errors.New("Route is missing method")
+ }
+ method = strings.ToUpper(method)
+ path := route.Path
+ if path == "" {
+ return errors.New("Route is missing path")
+ }
+ return router.node().Add(method+" "+path, router, nil)
+}
+
+func (router *Router) node() *pathpattern.Node {
+ root := router.pathNode
+ if root == nil {
+ root = &pathpattern.Node{}
+ router.pathNode = root
+ }
+ return root
+}
+
+func (router *Router) FindRoute(method string, url *url.URL) (*Route, map[string]string, error) {
+ swagger := router.swagger
+
+ // Get server
+ servers := swagger.Servers
+ var server *openapi3.Server
+ var remainingPath string
+ var pathParams map[string]string
+ if len(servers) == 0 {
+ remainingPath = url.Path
+ } else {
+ var paramValues []string
+ server, paramValues, remainingPath = servers.MatchURL(url)
+ if server == nil {
+ return nil, nil, &RouteError{
+ Route: Route{
+ Swagger: swagger,
+ },
+ Reason: "Does not match any server",
+ }
+ }
+ pathParams = make(map[string]string, 8)
+ paramNames, _ := server.ParameterNames()
+ for i, value := range paramValues {
+ name := paramNames[i]
+ pathParams[name] = value
+ }
+ }
+
+ // Get PathItem
+ root := router.node()
+ var route *Route
+ node, paramValues := root.Match(method + " " + remainingPath)
+ if node != nil {
+ route, _ = node.Value.(*Route)
+ }
+ if route == nil {
+ pathItem := swagger.Paths[remainingPath]
+ if pathItem == nil {
+ return nil, nil, &RouteError{
+ Route: Route{
+ Swagger: swagger,
+ Server: server,
+ },
+ Reason: "Path was not found",
+ }
+ }
+
+ // Get operation
+ if pathItem.GetOperation(method) == nil {
+ return nil, nil, &RouteError{
+ Route: Route{
+ Swagger: swagger,
+ Server: server,
+ },
+ Reason: "Path doesn't support the HTTP method",
+ }
+ }
+
+ }
+
+ if pathParams == nil {
+ pathParams = make(map[string]string, len(paramValues))
+ }
+ paramKeys := node.VariableNames
+ for i, value := range paramValues {
+ key := paramKeys[i]
+ if strings.HasSuffix(key, "*") {
+ key = key[:len(key)-1]
+ }
+ pathParams[key] = value
+ }
+ return route, pathParams, nil
+}
diff --git a/openapi3filter/router_test.go b/openapi3filter/router_test.go
new file mode 100644
index 000000000..4eba2c368
--- /dev/null
+++ b/openapi3filter/router_test.go
@@ -0,0 +1,172 @@
+package openapi3filter_test
+
+import (
+ "net/http"
+ "sort"
+ "testing"
+
+ "github.com/getkin/kin-openapi/openapi3"
+ "github.com/getkin/kin-openapi/openapi3filter"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRouter(t *testing.T) {
+ var (
+ pathNotFound = "Path was not found"
+ methodNotAllowed = "Path doesn't support the HTTP method"
+ doesNotMatchAnyServer = "Does not match any server"
+ )
+
+ // Build swagger
+ helloCONNECT := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ helloDELETE := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ helloGET := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ helloHEAD := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ helloOPTIONS := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ helloPATCH := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ helloPOST := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ helloPUT := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ helloTRACE := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ paramsGET := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ partialGET := &openapi3.Operation{Responses: make(openapi3.Responses)}
+ swagger := &openapi3.Swagger{
+ OpenAPI: "3.0.0",
+ Info: &openapi3.Info{
+ Title: "MyAPI",
+ Version: "0.1",
+ },
+ Paths: openapi3.Paths{
+ "/hello": &openapi3.PathItem{
+ Connect: helloCONNECT,
+ Delete: helloDELETE,
+ Get: helloGET,
+ Head: helloHEAD,
+ Options: helloOPTIONS,
+ Patch: helloPATCH,
+ Post: helloPOST,
+ Put: helloPUT,
+ Trace: helloTRACE,
+ },
+ "/onlyGET": &openapi3.PathItem{
+ Get: helloGET,
+ },
+ "/params/{x}/{y}/{z*}": &openapi3.PathItem{
+ Get: paramsGET,
+ },
+ "/partial": &openapi3.PathItem{
+ Get: partialGET,
+ },
+ },
+ }
+
+ // Build router
+ router := openapi3filter.NewRouter().WithSwagger(swagger)
+
+ // Declare a helper function
+ expect := func(method string, uri string, operation *openapi3.Operation, params map[string]string) {
+ req, err := http.NewRequest(method, uri, nil)
+ require.NoError(t, err)
+ route, pathParams, err := router.FindRoute(req.Method, req.URL)
+ if err != nil {
+ if operation == nil {
+ if err.Error() == doesNotMatchAnyServer {
+ return
+ }
+
+ pathItem := swagger.Paths[uri]
+ if pathItem == nil {
+ if err.Error() != pathNotFound {
+ t.Fatalf("'%s %s': should have returned '%s', but it returned an error: %v",
+ method, uri, pathNotFound, err)
+ }
+ return
+ }
+ if pathItem.GetOperation(method) == nil {
+ if err.Error() != methodNotAllowed {
+ t.Fatalf("'%s %s': should have returned '%s', but it returned an error: %v",
+ method, uri, methodNotAllowed, err)
+ }
+ }
+ } else {
+ t.Fatalf("'%s %s': should have returned an operation, but it returned an error: %v",
+ method, uri, err)
+ }
+ }
+ if operation == nil && err == nil {
+ t.Fatalf("'%s %s': should have returned an error, but didn't",
+ method, uri)
+ }
+ if route == nil {
+ return
+ }
+ if route.Operation != operation {
+ t.Fatalf("'%s %s': Returned wrong operation (%v)",
+ method, uri, route.Operation)
+ }
+ if params == nil {
+ if len(pathParams) != 0 {
+ t.Fatalf("'%s %s': should return no path arguments, but found some",
+ method, uri)
+ }
+ } else {
+ names := make([]string, 0, len(params))
+ for name := range params {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+ for _, name := range names {
+ expected := params[name]
+ actual, exists := pathParams[name]
+ if !exists {
+ t.Fatalf("'%s %s': path parameter '%s' should be '%s', but it's not defined.",
+ method, uri, name, expected)
+ }
+ if actual != expected {
+ t.Fatalf("'%s %s': path parameter '%s' should be '%s', but it's '%s'",
+ method, uri, name, expected, actual)
+ }
+ }
+ }
+ }
+ expect(http.MethodGet, "/not_existing", nil, nil)
+ expect(http.MethodDelete, "/hello", helloDELETE, nil)
+ expect(http.MethodGet, "/hello", helloGET, nil)
+ expect(http.MethodHead, "/hello", helloHEAD, nil)
+ expect(http.MethodPatch, "/hello", helloPATCH, nil)
+ expect(http.MethodPost, "/hello", helloPOST, nil)
+ expect(http.MethodPut, "/hello", helloPUT, nil)
+ expect(http.MethodGet, "/params/a/b/c/d", paramsGET, map[string]string{
+ "x": "a",
+ "y": "b",
+ "z": "c/d",
+ })
+ expect(http.MethodPost, "/partial", nil, nil)
+ swagger.Servers = append(swagger.Servers, &openapi3.Server{
+ URL: "https://www.example.com/api/v1/",
+ }, &openapi3.Server{
+ URL: "https://{d0}.{d1}.com/api/v1/",
+ })
+ expect(http.MethodGet, "/hello", nil, nil)
+ expect(http.MethodGet, "/api/v1/hello", nil, nil)
+ expect(http.MethodGet, "www.example.com/api/v1/hello", nil, nil)
+ expect(http.MethodGet, "https:///api/v1/hello", nil, nil)
+ expect(http.MethodGet, "https://www.example.com/hello", nil, nil)
+ expect(http.MethodGet, "https://www.example.com/api/v1/hello", helloGET, map[string]string{})
+ expect(http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{
+ "d0": "domain0",
+ "d1": "domain1",
+ })
+
+ {
+ uri := "https://www.example.com/api/v1/onlyGET"
+ expect(http.MethodGet, uri, helloGET, nil)
+ req, err := http.NewRequest(http.MethodDelete, uri, nil)
+ require.NoError(t, err)
+ require.NotNil(t, req)
+ route, pathParams, err := router.FindRoute(req.Method, req.URL)
+ require.Error(t, err)
+ require.Equal(t, err.(*openapi3filter.RouteError).Reason, "Path doesn't support the HTTP method")
+ require.Nil(t, route)
+ require.Nil(t, pathParams)
+ }
+}
diff --git a/openapi3filter/testdata/petstore.yaml b/openapi3filter/testdata/petstore.yaml
deleted file mode 100644
index 026c37e27..000000000
--- a/openapi3filter/testdata/petstore.yaml
+++ /dev/null
@@ -1,106 +0,0 @@
-openapi: "3.0.0"
-info:
- description: "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters."
- version: "1.0.0"
- title: "Swagger Petstore"
- termsOfService: "http://swagger.io/terms/"
- contact:
- email: "apiteam@swagger.io"
- license:
- name: "Apache 2.0"
- url: "http://www.apache.org/licenses/LICENSE-2.0.html"
-tags:
-- name: "pet"
- description: "Everything about your Pets"
- externalDocs:
- description: "Find out more"
- url: "http://swagger.io"
-- name: "store"
- description: "Access to Petstore orders"
-- name: "user"
- description: "Operations about user"
- externalDocs:
- description: "Find out more about our store"
- url: "http://swagger.io"
-paths:
- /pet:
- post:
- tags:
- - "pet"
- summary: "Add a new pet to the store"
- description: ""
- operationId: "addPet"
- parameters:
- - name: num
- in: query
- schema:
- type: integer
- minimum: 1
- requestBody:
- required: true
- content:
- 'application/json':
- schema:
- $ref: '#/components/schemas/Pet'
- responses:
- "405":
- description: "Invalid input"
-components:
- schemas:
- Category:
- type: "object"
- properties:
- id:
- type: "integer"
- format: "int64"
- name:
- type: "string"
- xml:
- name: "Category"
- Tag:
- type: "object"
- properties:
- id:
- type: "integer"
- format: "int64"
- name:
- type: "string"
- xml:
- name: "Tag"
- Pet:
- type: "object"
- required:
- - "name"
- - "photoUrls"
- properties:
- id:
- type: "integer"
- format: "int64"
- category:
- $ref: "#/components/schemas/Category"
- name:
- type: "string"
- example: "doggie"
- photoUrls:
- type: "array"
- xml:
- name: "photoUrl"
- wrapped: true
- items:
- type: "string"
- tags:
- type: "array"
- xml:
- name: "tag"
- wrapped: true
- items:
- $ref: "#/components/schemas/Tag"
- status:
- type: "string"
- description: "pet status in the store"
- enum:
- - "available"
- - "pending"
- - "sold"
- xml:
- name: "Pet"
diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go
deleted file mode 100644
index befff1054..000000000
--- a/openapi3filter/unpack_errors_test.go
+++ /dev/null
@@ -1,166 +0,0 @@
-package openapi3filter_test
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "sort"
- "strings"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func Example() {
- doc, err := openapi3.NewLoader().LoadFromFile("./testdata/petstore.yaml")
- if err != nil {
- panic(err)
- }
-
- router, err := gorillamux.NewRouter(doc)
- if err != nil {
- panic(err)
- }
-
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- route, pathParams, err := router.FindRoute(r)
- if err != nil {
- fmt.Println(err.Error())
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- err = openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{
- Request: r,
- PathParams: pathParams,
- Route: route,
- Options: &openapi3filter.Options{
- MultiError: true,
- },
- })
- switch err := err.(type) {
- case nil:
- case openapi3.MultiError:
- issues := convertError(err)
- names := make([]string, 0, len(issues))
- for k := range issues {
- names = append(names, k)
- }
- sort.Strings(names)
- for _, k := range names {
- msgs := issues[k]
- fmt.Println("===== Start New Error =====")
- fmt.Println(k + ":")
- for _, msg := range msgs {
- fmt.Printf("\t%s\n", msg)
- }
- }
- w.WriteHeader(http.StatusBadRequest)
- default:
- fmt.Println(err.Error())
- w.WriteHeader(http.StatusBadRequest)
- }
- }))
- defer ts.Close()
-
- // (note invalid type for name and invalid status)
- body := strings.NewReader(`{"name": 100, "photoUrls": [], "status": "invalidStatus"}`)
- req, err := http.NewRequest("POST", ts.URL+"/pet?num=0", body)
- if err != nil {
- panic(err)
- }
- req.Header.Set("Content-Type", "application/json")
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- panic(err)
- }
- defer resp.Body.Close()
- fmt.Printf("response: %d %s\n", resp.StatusCode, resp.Body)
-
- // Output:
- // ===== Start New Error =====
- // @body.name:
- // Error at "/name": value must be a string
- // Schema:
- // {
- // "example": "doggie",
- // "type": "string"
- // }
- //
- // Value:
- // 100
- //
- // ===== Start New Error =====
- // @body.status:
- // Error at "/status": value is not one of the allowed values ["available","pending","sold"]
- // Schema:
- // {
- // "description": "pet status in the store",
- // "enum": [
- // "available",
- // "pending",
- // "sold"
- // ],
- // "type": "string"
- // }
- //
- // Value:
- // "invalidStatus"
- //
- // ===== Start New Error =====
- // query.num:
- // parameter "num" in query has an error: number must be at least 1
- // Schema:
- // {
- // "minimum": 1,
- // "type": "integer"
- // }
- //
- // Value:
- // 0
- //
- // response: 400 {}
-}
-
-func convertError(me openapi3.MultiError) map[string][]string {
- issues := make(map[string][]string)
- for _, err := range me {
- const prefixBody = "@body"
- switch err := err.(type) {
- case *openapi3.SchemaError:
- // Can inspect schema validation errors here, e.g. err.Value
- field := prefixBody
- if path := err.JSONPointer(); len(path) > 0 {
- field = fmt.Sprintf("%s.%s", field, strings.Join(path, "."))
- }
- issues[field] = append(issues[field], err.Error())
- case *openapi3filter.RequestError: // possible there were multiple issues that failed validation
-
- // check if invalid HTTP parameter
- if err.Parameter != nil {
- prefix := err.Parameter.In
- name := fmt.Sprintf("%s.%s", prefix, err.Parameter.Name)
- issues[name] = append(issues[name], err.Error())
- continue
- }
-
- if err, ok := err.Err.(openapi3.MultiError); ok {
- for k, v := range convertError(err) {
- issues[k] = append(issues[k], v...)
- }
- continue
- }
-
- // check if requestBody
- if err.RequestBody != nil {
- issues[prefixBody] = append(issues[prefixBody], err.Error())
- continue
- }
- default:
- const unknown = "@unknown"
- issues[unknown] = append(issues[unknown], err.Error())
- }
- }
- return issues
-}
diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go
deleted file mode 100644
index bad6c961a..000000000
--- a/openapi3filter/validate_readonly_test.go
+++ /dev/null
@@ -1,230 +0,0 @@
-package openapi3filter
-
-import (
- "bytes"
- "io"
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
-)
-
-func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) {
- type testCase struct {
- name string
- requestSchema string
- responseSchema string
- requestBody string
- responseBody string
- responseErrContains string
- requestErrContains string
- }
-
- testCases := []testCase{
- {
- name: "valid_readonly_in_response_and_valid_writeonly_in_request",
- requestSchema: `
- "schema":{
- "type": "object",
- "required": ["_id"],
- "properties": {
- "_id": {
- "type": "string",
- "writeOnly": true
- }
- }
- }`,
- responseSchema: `
- "schema":{
- "type": "object",
- "required": ["access_token"],
- "properties": {
- "access_token": {
- "type": "string",
- "readOnly": true
- }
- }
- }`,
- requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`,
- responseBody: `{"access_token": "abcd"}`,
- },
- {
- name: "valid_readonly_in_response_and_invalid_readonly_in_request",
- requestSchema: `
- "schema":{
- "type": "object",
- "required": ["_id"],
- "properties": {
- "_id": {
- "type": "string",
- "readOnly": true
- }
- }
- }`,
- responseSchema: `
- "schema":{
- "type": "object",
- "required": ["access_token"],
- "properties": {
- "access_token": {
- "type": "string",
- "readOnly": true
- }
- }
- }`,
- requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`,
- responseBody: `{"access_token": "abcd"}`,
- requestErrContains: `readOnly property "_id" in request`,
- },
- {
- name: "invalid_writeonly_in_response_and_valid_writeonly_in_request",
- requestSchema: `
- "schema":{
- "type": "object",
- "required": ["_id"],
- "properties": {
- "_id": {
- "type": "string",
- "writeOnly": true
- }
- }
- }`,
- responseSchema: `
- "schema":{
- "type": "object",
- "required": ["access_token"],
- "properties": {
- "access_token": {
- "type": "string",
- "writeOnly": true
- }
- }
- }`,
- requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`,
- responseBody: `{"access_token": "abcd"}`,
- responseErrContains: `writeOnly property "access_token" in response`,
- },
- {
- name: "invalid_writeonly_in_response_and_invalid_readonly_in_request",
- requestSchema: `
- "schema":{
- "type": "object",
- "required": ["_id"],
- "properties": {
- "_id": {
- "type": "string",
- "readOnly": true
- }
- }
- }`,
- responseSchema: `
- "schema":{
- "type": "object",
- "required": ["access_token"],
- "properties": {
- "access_token": {
- "type": "string",
- "writeOnly": true
- }
- }
- }`,
- requestBody: `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`,
- responseBody: `{"access_token": "abcd"}`,
- responseErrContains: `writeOnly property "access_token" in response`,
- requestErrContains: `readOnly property "_id" in request`,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- spec := bytes.Buffer{}
- spec.WriteString(`{
- "openapi": "3.0.3",
- "info": {
- "version": "1.0.0",
- "title": "title"
- },
- "paths": {
- "/accounts": {
- "post": {
- "description": "Create a new account",
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {`)
- spec.WriteString(tc.requestSchema)
- spec.WriteString(`}
- }
- },
- "responses": {
- "201": {
- "description": "Successfully created a new account",
- "content": {
- "application/json": {`)
- spec.WriteString(tc.responseSchema)
- spec.WriteString(`}
- }
- },
- "400": {
- "description": "The server could not understand the request due to invalid syntax",
- }
- }
- }
- }
- }
- }`)
-
- sl := openapi3.NewLoader()
- doc, err := sl.LoadFromData(spec.Bytes())
- require.NoError(t, err)
- err = doc.Validate(sl.Context)
- require.NoError(t, err)
- router, err := legacyrouter.NewRouter(doc)
- require.NoError(t, err)
-
- httpReq, err := http.NewRequest(http.MethodPost, "/accounts", strings.NewReader(tc.requestBody))
- require.NoError(t, err)
- httpReq.Header.Add(headerCT, "application/json")
-
- route, pathParams, err := router.FindRoute(httpReq)
- require.NoError(t, err)
-
- reqValidationInput := &RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- }
-
- if tc.requestSchema != "" {
- err = ValidateRequest(sl.Context, reqValidationInput)
-
- if tc.requestErrContains != "" {
- require.Error(t, err)
- require.Contains(t, err.Error(), tc.requestErrContains)
- } else {
- require.NoError(t, err)
- }
- }
-
- if tc.responseSchema != "" {
- err = ValidateResponse(sl.Context, &ResponseValidationInput{
- RequestValidationInput: reqValidationInput,
- Status: 201,
- Header: httpReq.Header,
- Body: io.NopCloser(strings.NewReader(tc.responseBody)),
- })
-
- if tc.responseErrContains != "" {
- require.Error(t, err)
- require.Contains(t, err.Error(), tc.responseErrContains)
- } else {
- require.NoError(t, err)
- }
- }
- })
- }
-}
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index a8106a7c8..f22e71efb 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -5,7 +5,6 @@ import (
"context"
"errors"
"fmt"
- "io"
"io/ioutil"
"net/http"
"sort"
@@ -13,15 +12,8 @@ import (
"github.com/getkin/kin-openapi/openapi3"
)
-// ErrAuthenticationServiceMissing is returned when no authentication service
-// is defined for the request validator
-var ErrAuthenticationServiceMissing = errors.New("missing AuthenticationFunc")
-
-// ErrInvalidRequired is returned when a required value of a parameter or request body is not defined.
-var ErrInvalidRequired = errors.New("value is required but missing")
-
-// ErrInvalidEmptyValue is returned when a value of a parameter or request body is empty while it's not allowed.
-var ErrInvalidEmptyValue = errors.New("empty value is not allowed")
+// ErrInvalidRequired is an error that happens when a required value of a parameter or request's body is not defined.
+var ErrInvalidRequired = errors.New("must have a value")
// ValidateRequest is used to validate the given input according to previous
// loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a
@@ -29,34 +21,22 @@ var ErrInvalidEmptyValue = errors.New("empty value is not allowed")
//
// Note: One can tune the behavior of uniqueItems: true verification
// by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker
-func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err error) {
- var me openapi3.MultiError
-
+func ValidateRequest(c context.Context, input *RequestValidationInput) error {
options := input.Options
if options == nil {
options = DefaultOptions
}
route := input.Route
+ if route == nil {
+ return errors.New("invalid route")
+ }
operation := route.Operation
+ if operation == nil {
+ return errRouteMissingOperation
+ }
operationParameters := operation.Parameters
pathItemParameters := route.PathItem.Parameters
- // Security
- security := operation.Security
- // If there aren't any security requirements for the operation
- if security == nil {
- // Use the global security requirements.
- security = &route.Spec.Security
- }
- if security != nil {
- if err = ValidateSecurityRequirements(ctx, input, *security); err != nil && !options.MultiError {
- return
- }
- if err != nil {
- me = append(me, err)
- }
- }
-
// For each parameter of the PathItem
for _, parameterRef := range pathItemParameters {
parameter := parameterRef.Value
@@ -65,107 +45,75 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er
continue
}
}
-
- if err = ValidateParameter(ctx, input, parameter); err != nil && !options.MultiError {
- return
- }
- if err != nil {
- me = append(me, err)
+ if err := ValidateParameter(c, input, parameter); err != nil {
+ return err
}
}
// For each parameter of the Operation
for _, parameter := range operationParameters {
- if err = ValidateParameter(ctx, input, parameter.Value); err != nil && !options.MultiError {
- return
- }
- if err != nil {
- me = append(me, err)
+ if err := ValidateParameter(c, input, parameter.Value); err != nil {
+ return err
}
}
// RequestBody
requestBody := operation.RequestBody
if requestBody != nil && !options.ExcludeRequestBody {
- if err = ValidateRequestBody(ctx, input, requestBody.Value); err != nil && !options.MultiError {
- return
- }
- if err != nil {
- me = append(me, err)
+ if err := ValidateRequestBody(c, input, requestBody.Value); err != nil {
+ return err
}
}
- if len(me) > 0 {
- return me
+ // Security
+ security := operation.Security
+ // If there aren't any security requirements for the operation
+ if security == nil {
+ if route.Swagger == nil {
+ return errRouteMissingSwagger
+ }
+ // Use the global security requirements.
+ security = &route.Swagger.Security
+ }
+ if security != nil {
+ if err := ValidateSecurityRequirements(c, input, *security); err != nil {
+ return err
+ }
}
- return
+ return nil
}
// ValidateParameter validates a parameter's value by JSON schema.
// The function returns RequestError with a ParseError cause when unable to parse a value.
// The function returns RequestError with ErrInvalidRequired cause when a value of a required parameter is not defined.
-// The function returns RequestError with ErrInvalidEmptyValue cause when a value of a required parameter is not defined.
// The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema.
-func ValidateParameter(ctx context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error {
+func ValidateParameter(c context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error {
if parameter.Schema == nil && parameter.Content == nil {
// We have no schema for the parameter. Assume that everything passes
- // a schema-less check, but this could also be an error. The OpenAPI
+ // a schema-less check, but this could also be an error. The Swagger
// validation allows this to happen.
return nil
}
- options := input.Options
- if options == nil {
- options = DefaultOptions
- }
-
var value interface{}
var err error
- var found bool
var schema *openapi3.Schema
// Validation will ensure that we either have content or schema.
if parameter.Content != nil {
- if value, schema, found, err = decodeContentParameter(parameter, input); err != nil {
+ if value, schema, err = decodeContentParameter(parameter, input); err != nil {
return &RequestError{Input: input, Parameter: parameter, Err: err}
}
} else {
- if value, found, err = decodeStyledParameter(parameter, input); err != nil {
+ if value, err = decodeStyledParameter(parameter, input); err != nil {
return &RequestError{Input: input, Parameter: parameter, Err: err}
}
schema = parameter.Schema.Value
}
-
- // Set default value if needed
- if !options.SkipSettingDefaults && value == nil && schema != nil && schema.Default != nil {
- value = schema.Default
- req := input.Request
- switch parameter.In {
- case openapi3.ParameterInPath:
- // Path parameters are required.
- // Next check `parameter.Required && !found` will catch this.
- case openapi3.ParameterInQuery:
- q := req.URL.Query()
- q.Add(parameter.Name, fmt.Sprintf("%v", value))
- req.URL.RawQuery = q.Encode()
- case openapi3.ParameterInHeader:
- req.Header.Add(parameter.Name, fmt.Sprintf("%v", value))
- case openapi3.ParameterInCookie:
- req.AddCookie(&http.Cookie{
- Name: parameter.Name,
- Value: fmt.Sprintf("%v", value),
- })
- }
- }
-
- // Validate a parameter's value and presence.
- if parameter.Required && !found {
- return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired}
- }
-
- if isNilValue(value) {
- if !parameter.AllowEmptyValue && found {
- return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidEmptyValue.Error(), Err: ErrInvalidEmptyValue}
+ // Validate a parameter's value.
+ if value == nil {
+ if parameter.Required {
+ return &RequestError{Input: input, Parameter: parameter, Reason: "must have a value", Err: ErrInvalidRequired}
}
return nil
}
@@ -173,38 +121,22 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param
// A parameter's schema is not defined so skip validation of a parameter's value.
return nil
}
-
- var opts []openapi3.SchemaValidationOption
- if options.MultiError {
- opts = make([]openapi3.SchemaValidationOption, 0, 1)
- opts = append(opts, openapi3.MultiErrors())
- }
- if options.customSchemaErrorFunc != nil {
- opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
- }
- if err = schema.VisitJSON(value, opts...); err != nil {
+ if err = schema.VisitJSON(value); err != nil {
return &RequestError{Input: input, Parameter: parameter, Err: err}
}
return nil
}
-const prefixInvalidCT = "header Content-Type has unexpected value"
-
// ValidateRequestBody validates data of a request's body.
//
// The function returns RequestError with ErrInvalidRequired cause when a value is required but not defined.
// The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema.
-func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, requestBody *openapi3.RequestBody) error {
+func ValidateRequestBody(c context.Context, input *RequestValidationInput, requestBody *openapi3.RequestBody) error {
var (
req = input.Request
data []byte
)
- options := input.Options
- if options == nil {
- options = DefaultOptions
- }
-
if req.Body != http.NoBody && req.Body != nil {
defer req.Body.Close()
var err error
@@ -217,19 +149,7 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
}
}
// Put the data back into the input
- req.Body = nil
- if req.GetBody != nil {
- if req.Body, err = req.GetBody(); err != nil {
- req.Body = nil
- }
- }
- if req.Body == nil {
- req.ContentLength = int64(len(data))
- req.GetBody = func() (io.ReadCloser, error) {
- return io.NopCloser(bytes.NewReader(data)), nil
- }
- req.Body, _ = req.GetBody() // no error return
- }
+ req.Body = ioutil.NopCloser(bytes.NewReader(data))
}
if len(data) == 0 {
@@ -245,13 +165,13 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
return nil
}
- inputMIME := req.Header.Get(headerCT)
+ inputMIME := req.Header.Get("Content-Type")
contentType := requestBody.Content.Get(inputMIME)
if contentType == nil {
return &RequestError{
Input: input,
RequestBody: requestBody,
- Reason: fmt.Sprintf("%s %q", prefixInvalidCT, inputMIME),
+ Reason: fmt.Sprintf("header 'Content-Type' has unexpected value: %q", inputMIME),
}
}
@@ -261,7 +181,7 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
}
encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] }
- mediaType, value, err := decodeBody(bytes.NewReader(data), req.Header, contentType.Schema, encFn)
+ value, err := decodeBody(bytes.NewReader(data), req.Header, contentType.Schema, encFn)
if err != nil {
return &RequestError{
Input: input,
@@ -271,68 +191,28 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
}
}
- defaultsSet := false
- opts := make([]openapi3.SchemaValidationOption, 0, 4) // 4 potential opts here
- opts = append(opts, openapi3.VisitAsRequest())
- if !options.SkipSettingDefaults {
- opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true }))
- }
- if options.MultiError {
- opts = append(opts, openapi3.MultiErrors())
- }
- if options.customSchemaErrorFunc != nil {
- opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
- }
- if options.ExcludeReadOnlyValidations {
- opts = append(opts, openapi3.DisableReadOnlyValidation())
- }
-
// Validate JSON with the schema
- if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil {
- schemaId := getSchemaIdentifier(contentType.Schema)
- schemaId = prependSpaceIfNeeded(schemaId)
+ if err := contentType.Schema.Value.VisitJSON(value); err != nil {
return &RequestError{
Input: input,
RequestBody: requestBody,
- Reason: fmt.Sprintf("doesn't match schema%s", schemaId),
+ Reason: "doesn't match the schema",
Err: err,
}
}
-
- if defaultsSet {
- var err error
- if data, err = encodeBody(value, mediaType); err != nil {
- return &RequestError{
- Input: input,
- RequestBody: requestBody,
- Reason: "rewriting failed",
- Err: err,
- }
- }
- // Put the data back into the input
- if req.Body != nil {
- req.Body.Close()
- }
- req.ContentLength = int64(len(data))
- req.GetBody = func() (io.ReadCloser, error) {
- return io.NopCloser(bytes.NewReader(data)), nil
- }
- req.Body, _ = req.GetBody() // no error return
- }
-
return nil
}
// ValidateSecurityRequirements goes through multiple OpenAPI 3 security
// requirements in order and returns nil on the first valid requirement.
// If no requirement is met, errors are returned in order.
-func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error {
+func ValidateSecurityRequirements(c context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error {
if len(srs) == 0 {
return nil
}
var errs []error
for _, sr := range srs {
- if err := validateSecurityRequirement(ctx, input, sr); err != nil {
+ if err := validateSecurityRequirement(c, input, sr); err != nil {
if len(errs) == 0 {
errs = make([]error, 0, len(srs))
}
@@ -348,7 +228,14 @@ func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationI
}
// validateSecurityRequirement validates a single OpenAPI 3 security requirement
-func validateSecurityRequirement(ctx context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error {
+func validateSecurityRequirement(c context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error {
+ swagger := input.Route.Swagger
+ if swagger == nil {
+ return errRouteMissingSwagger
+ }
+ securitySchemes := swagger.Components.SecuritySchemes
+
+ // Ensure deterministic order
names := make([]string, 0, len(securityRequirement))
for name := range securityRequirement {
names = append(names, name)
@@ -365,11 +252,6 @@ func validateSecurityRequirement(ctx context.Context, input *RequestValidationIn
return ErrAuthenticationServiceMissing
}
- var securitySchemes openapi3.SecuritySchemes
- if components := input.Route.Spec.Components; components != nil {
- securitySchemes = components.SecuritySchemes
- }
-
// For each scheme for the requirement
for _, name := range names {
var securityScheme *openapi3.SecurityScheme
@@ -381,11 +263,11 @@ func validateSecurityRequirement(ctx context.Context, input *RequestValidationIn
if securityScheme == nil {
return &RequestError{
Input: input,
- Err: fmt.Errorf("security scheme %q is not declared", name),
+ Err: fmt.Errorf("Security scheme '%s' is not declared", name),
}
}
scopes := securityRequirement[name]
- if err := f(ctx, &AuthenticationInput{
+ if err := f(c, &AuthenticationInput{
RequestValidationInput: input,
SecuritySchemeName: name,
SecurityScheme: securityScheme,
diff --git a/openapi3filter/validate_request_input.go b/openapi3filter/validate_request_input.go
index 91dd102b6..44bc8579a 100644
--- a/openapi3filter/validate_request_input.go
+++ b/openapi3filter/validate_request_input.go
@@ -5,10 +5,9 @@ import (
"net/url"
"github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers"
)
-// A ContentParameterDecoder takes a parameter definition from the OpenAPI spec,
+// A ContentParameterDecoder takes a parameter definition from the swagger spec,
// and the value which we received for it. It is expected to return the
// value unmarshaled into an interface which can be traversed for
// validation, it should also return the schema to be used for validating the
@@ -23,7 +22,7 @@ type RequestValidationInput struct {
Request *http.Request
PathParams map[string]string
QueryParams url.Values
- Route *routers.Route
+ Route *Route
Options *Options
ParamDecoder ContentParameterDecoder
}
diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go
deleted file mode 100644
index 8da550ce0..000000000
--- a/openapi3filter/validate_request_test.go
+++ /dev/null
@@ -1,223 +0,0 @@
-package openapi3filter
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func setupTestRouter(t *testing.T, spec string) routers.Router {
- t.Helper()
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
-
- return router
-}
-
-func TestValidateRequest(t *testing.T) {
- const spec = `
-openapi: 3.0.0
-info:
- title: 'Validator'
- version: 0.0.1
-paths:
- /category:
- post:
- parameters:
- - name: category
- in: query
- schema:
- type: string
- required: true
- requestBody:
- required: true
- content:
- application/json:
- schema:
- type: object
- required:
- - subCategory
- properties:
- subCategory:
- type: string
- category:
- type: string
- default: Sweets
- responses:
- '201':
- description: Created
- security:
- - apiKey: []
-components:
- securitySchemes:
- apiKey:
- type: apiKey
- name: Api-Key
- in: header
-`
-
- router := setupTestRouter(t, spec)
-
- verifyAPIKeyPresence := func(c context.Context, input *AuthenticationInput) error {
- if input.SecurityScheme.Type == "apiKey" {
- var found bool
- switch input.SecurityScheme.In {
- case "query":
- _, found = input.RequestValidationInput.GetQueryParams()[input.SecurityScheme.Name]
- case "header":
- _, found = input.RequestValidationInput.Request.Header[http.CanonicalHeaderKey(input.SecurityScheme.Name)]
- case "cookie":
- _, err := input.RequestValidationInput.Request.Cookie(input.SecurityScheme.Name)
- found = !errors.Is(err, http.ErrNoCookie)
- }
- if !found {
- return fmt.Errorf("%v not found in %v", input.SecurityScheme.Name, input.SecurityScheme.In)
- }
- }
- return nil
- }
-
- type testRequestBody struct {
- SubCategory string `json:"subCategory"`
- Category string `json:"category,omitempty"`
- }
- type args struct {
- requestBody *testRequestBody
- url string
- apiKey string
- }
- tests := []struct {
- name string
- args args
- expectedModification bool
- expectedErr error
- }{
- {
- name: "Valid request with all fields set",
- args: args{
- requestBody: &testRequestBody{SubCategory: "Chocolate", Category: "Food"},
- url: "/category?category=cookies",
- apiKey: "SomeKey",
- },
- expectedModification: false,
- expectedErr: nil,
- },
- {
- name: "Valid request without certain fields",
- args: args{
- requestBody: &testRequestBody{SubCategory: "Chocolate"},
- url: "/category?category=cookies",
- apiKey: "SomeKey",
- },
- expectedModification: true,
- expectedErr: nil,
- },
- {
- name: "Invalid operation params",
- args: args{
- requestBody: &testRequestBody{SubCategory: "Chocolate"},
- url: "/category?invalidCategory=badCookie",
- apiKey: "SomeKey",
- },
- expectedModification: false,
- expectedErr: &RequestError{},
- },
- {
- name: "Invalid request body",
- args: args{
- requestBody: nil,
- url: "/category?category=cookies",
- apiKey: "SomeKey",
- },
- expectedModification: false,
- expectedErr: &RequestError{},
- },
- {
- name: "Invalid security",
- args: args{
- requestBody: &testRequestBody{SubCategory: "Chocolate"},
- url: "/category?category=cookies",
- apiKey: "",
- },
- expectedModification: false,
- expectedErr: &SecurityRequirementsError{},
- },
- {
- name: "Invalid request body and security",
- args: args{
- requestBody: nil,
- url: "/category?category=cookies",
- apiKey: "",
- },
- expectedModification: false,
- expectedErr: &SecurityRequirementsError{},
- },
- }
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- var requestBody io.Reader
- var originalBodySize int
- if tc.args.requestBody != nil {
- testingBody, err := json.Marshal(tc.args.requestBody)
- require.NoError(t, err)
- requestBody = bytes.NewReader(testingBody)
- originalBodySize = len(testingBody)
- }
- req, err := http.NewRequest(http.MethodPost, tc.args.url, requestBody)
- require.NoError(t, err)
- req.Header.Add("Content-Type", "application/json")
- if tc.args.apiKey != "" {
- req.Header.Add("Api-Key", tc.args.apiKey)
- }
-
- route, pathParams, err := router.FindRoute(req)
- require.NoError(t, err)
-
- validationInput := &RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- Options: &Options{
- AuthenticationFunc: verifyAPIKeyPresence,
- },
- }
- err = ValidateRequest(context.Background(), validationInput)
- assert.IsType(t, tc.expectedErr, err, "ValidateRequest(): error = %v, expectedError %v", err, tc.expectedErr)
- if tc.expectedErr != nil {
- return
- }
- body, err := io.ReadAll(validationInput.Request.Body)
- contentLen := int(validationInput.Request.ContentLength)
- bodySize := len(body)
- assert.NoError(t, err, "unable to read request body: %v", err)
- assert.Equal(t, contentLen, bodySize, "expect ContentLength %d to equal body size %d", contentLen, bodySize)
- bodyModified := originalBodySize != bodySize
- assert.Equal(t, bodyModified, tc.expectedModification, "expect request body modification happened: %t, expected %t", bodyModified, tc.expectedModification)
-
- validationInput.Request.Body, err = validationInput.Request.GetBody()
- assert.NoError(t, err, "unable to re-generate body by GetBody(): %v", err)
- body2, err := io.ReadAll(validationInput.Request.Body)
- assert.NoError(t, err, "unable to read request body: %v", err)
- assert.Equal(t, body, body2, "body by GetBody() is not matched")
- })
- }
-}
diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go
index 08ea4e19d..dad0864d2 100644
--- a/openapi3filter/validate_response.go
+++ b/openapi3filter/validate_response.go
@@ -7,8 +7,6 @@ import (
"fmt"
"io/ioutil"
"net/http"
- "sort"
- "strings"
"github.com/getkin/kin-openapi/openapi3"
)
@@ -19,13 +17,20 @@ import (
//
// Note: One can tune the behavior of uniqueItems: true verification
// by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker
-func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error {
+func ValidateResponse(c context.Context, input *ResponseValidationInput) error {
req := input.RequestValidationInput.Request
switch req.Method {
case "HEAD":
return nil
}
status := input.Status
+ if status < 100 {
+ return &ResponseError{
+ Input: input,
+ Reason: "illegal status code",
+ Err: fmt.Errorf("Status %d", status),
+ }
+ }
// These status codes will never be validated.
// TODO: The list is probably missing some.
@@ -56,6 +61,7 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
if !options.IncludeResponseStatus {
return nil
}
+
return &ResponseError{Input: input, Reason: "status is not supported"}
}
response := responseRef.Value
@@ -63,31 +69,6 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
return &ResponseError{Input: input, Reason: "response has not been resolved"}
}
- opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential options here
- if options.MultiError {
- opts = append(opts, openapi3.MultiErrors())
- }
- if options.customSchemaErrorFunc != nil {
- opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
- }
- if options.ExcludeWriteOnlyValidations {
- opts = append(opts, openapi3.DisableWriteOnlyValidation())
- }
-
- headers := make([]string, 0, len(response.Headers))
- for k := range response.Headers {
- if k != headerCT {
- headers = append(headers, k)
- }
- }
- sort.Strings(headers)
- for _, headerName := range headers {
- headerRef := response.Headers[headerName]
- if err := validateResponseHeader(headerName, headerRef, input, opts); err != nil {
- return err
- }
- }
-
if options.ExcludeResponseBody {
// A user turned off validation of a response's body.
return nil
@@ -99,12 +80,12 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
return nil
}
- inputMIME := input.Header.Get(headerCT)
+ inputMIME := input.Header.Get("Content-Type")
contentType := content.Get(inputMIME)
if contentType == nil {
return &ResponseError{
Input: input,
- Reason: fmt.Sprintf("response header Content-Type has unexpected value: %q", inputMIME),
+ Reason: fmt.Sprintf("input header 'Content-Type' has unexpected value: %q", inputMIME),
}
}
@@ -138,7 +119,7 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
input.SetBodyBytes(data)
encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] }
- _, value, err := decodeBody(bytes.NewBuffer(data), input.Header, contentType.Schema, encFn)
+ value, err := decodeBody(bytes.NewBuffer(data), input.Header, contentType.Schema, encFn)
if err != nil {
return &ResponseError{
Input: input,
@@ -148,77 +129,12 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
}
// Validate data with the schema.
- if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse())...); err != nil {
- schemaId := getSchemaIdentifier(contentType.Schema)
- schemaId = prependSpaceIfNeeded(schemaId)
- return &ResponseError{
- Input: input,
- Reason: fmt.Sprintf("response body doesn't match schema%s", schemaId),
- Err: err,
- }
- }
- return nil
-}
-
-func validateResponseHeader(headerName string, headerRef *openapi3.HeaderRef, input *ResponseValidationInput, opts []openapi3.SchemaValidationOption) error {
- var err error
- var decodedValue interface{}
- var found bool
- var sm *openapi3.SerializationMethod
- dec := &headerParamDecoder{header: input.Header}
-
- if sm, err = headerRef.Value.SerializationMethod(); err != nil {
+ if err := contentType.Schema.Value.VisitJSON(value); err != nil {
return &ResponseError{
Input: input,
- Reason: fmt.Sprintf("unable to get header %q serialization method", headerName),
+ Reason: "response body doesn't match the schema",
Err: err,
}
}
-
- if decodedValue, found, err = decodeValue(dec, headerName, sm, headerRef.Value.Schema, headerRef.Value.Required); err != nil {
- return &ResponseError{
- Input: input,
- Reason: fmt.Sprintf("unable to decode header %q value", headerName),
- Err: err,
- }
- }
-
- if found {
- if err = headerRef.Value.Schema.Value.VisitJSON(decodedValue, opts...); err != nil {
- return &ResponseError{
- Input: input,
- Reason: fmt.Sprintf("response header %q doesn't match schema", headerName),
- Err: err,
- }
- }
- } else if headerRef.Value.Required {
- return &ResponseError{
- Input: input,
- Reason: fmt.Sprintf("response header %q missing", headerName),
- }
- }
return nil
}
-
-// getSchemaIdentifier gets something by which a schema could be identified.
-// A schema by itself doesn't have a true identity field. This function makes
-// a best effort to get a value that can fill that void.
-func getSchemaIdentifier(schema *openapi3.SchemaRef) string {
- var id string
-
- if schema != nil {
- id = strings.TrimSpace(schema.Ref)
- }
- if id == "" && schema.Value != nil {
- id = strings.TrimSpace(schema.Value.Title)
- }
-
- return id
-}
-
-func prependSpaceIfNeeded(value string) string {
- if len(value) > 0 {
- value = " " + value
- }
- return value
-}
diff --git a/openapi3filter/validate_response_test.go b/openapi3filter/validate_response_test.go
deleted file mode 100644
index 5ce657b0b..000000000
--- a/openapi3filter/validate_response_test.go
+++ /dev/null
@@ -1,215 +0,0 @@
-package openapi3filter
-
-import (
- "io"
- "net/http"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-func Test_validateResponseHeader(t *testing.T) {
- type args struct {
- headerName string
- headerRef *openapi3.HeaderRef
- }
- tests := []struct {
- name string
- args args
- isHeaderPresent bool
- headerVals []string
- wantErr bool
- wantErrMsg string
- }{
- {
- name: "test required string header with single string value",
- args: args{
- headerName: "X-Blab",
- headerRef: newHeaderRef(openapi3.NewStringSchema(), true),
- },
- isHeaderPresent: true,
- headerVals: []string{"blab"},
- wantErr: false,
- },
- {
- name: "test required string header with single, empty string value",
- args: args{
- headerName: "X-Blab",
- headerRef: newHeaderRef(openapi3.NewStringSchema(), true),
- },
- isHeaderPresent: true,
- headerVals: []string{""},
- wantErr: true,
- wantErrMsg: `response header "X-Blab" doesn't match schema: Value is not nullable`,
- },
- {
- name: "test optional string header with single string value",
- args: args{
- headerName: "X-Blab",
- headerRef: newHeaderRef(openapi3.NewStringSchema(), false),
- },
- isHeaderPresent: false,
- headerVals: []string{"blab"},
- wantErr: false,
- },
- {
- name: "test required, but missing string header",
- args: args{
- headerName: "X-Blab",
- headerRef: newHeaderRef(openapi3.NewStringSchema(), true),
- },
- isHeaderPresent: false,
- headerVals: nil,
- wantErr: true,
- wantErrMsg: `response header "X-Blab" missing`,
- },
- {
- name: "test integer header with single integer value",
- args: args{
- headerName: "X-Blab",
- headerRef: newHeaderRef(openapi3.NewIntegerSchema(), true),
- },
- isHeaderPresent: true,
- headerVals: []string{"88"},
- wantErr: false,
- },
- {
- name: "test integer header with single string value",
- args: args{
- headerName: "X-Blab",
- headerRef: newHeaderRef(openapi3.NewIntegerSchema(), true),
- },
- isHeaderPresent: true,
- headerVals: []string{"blab"},
- wantErr: true,
- wantErrMsg: `unable to decode header "X-Blab" value: value blab: an invalid integer: invalid syntax`,
- },
- {
- name: "test int64 header with single int64 value",
- args: args{
- headerName: "X-Blab",
- headerRef: newHeaderRef(openapi3.NewInt64Schema(), true),
- },
- isHeaderPresent: true,
- headerVals: []string{"88"},
- wantErr: false,
- },
- {
- name: "test int32 header with single int32 value",
- args: args{
- headerName: "X-Blab",
- headerRef: newHeaderRef(openapi3.NewInt32Schema(), true),
- },
- isHeaderPresent: true,
- headerVals: []string{"88"},
- wantErr: false,
- },
- {
- name: "test float64 header with single float64 value",
- args: args{
- headerName: "X-Blab",
- headerRef: newHeaderRef(openapi3.NewFloat64Schema(), true),
- },
- isHeaderPresent: true,
- headerVals: []string{"88.87"},
- wantErr: false,
- },
- {
- name: "test integer header with multiple csv integer values",
- args: args{
- headerName: "X-blab",
- headerRef: newHeaderRef(newArraySchema(openapi3.NewIntegerSchema()), true),
- },
- isHeaderPresent: true,
- headerVals: []string{"87,88"},
- wantErr: false,
- },
- {
- name: "test integer header with multiple integer values",
- args: args{
- headerName: "X-blab",
- headerRef: newHeaderRef(newArraySchema(openapi3.NewIntegerSchema()), true),
- },
- isHeaderPresent: true,
- headerVals: []string{"87", "88"},
- wantErr: false,
- },
- {
- name: "test non-typed, nullable header with single string value",
- args: args{
- headerName: "X-blab",
- headerRef: newHeaderRef(&openapi3.Schema{Nullable: true}, true),
- },
- isHeaderPresent: true,
- headerVals: []string{"blab"},
- wantErr: false,
- },
- {
- name: "test required non-typed, nullable header not present",
- args: args{
- headerName: "X-blab",
- headerRef: newHeaderRef(&openapi3.Schema{Nullable: true}, true),
- },
- isHeaderPresent: false,
- headerVals: []string{"blab"},
- wantErr: true,
- wantErrMsg: `response header "X-blab" missing`,
- },
- {
- name: "test non-typed, non-nullable header with single string value",
- args: args{
- headerName: "X-blab",
- headerRef: newHeaderRef(&openapi3.Schema{Nullable: false}, true),
- },
- isHeaderPresent: true,
- headerVals: []string{"blab"},
- wantErr: true,
- wantErrMsg: `response header "X-blab" doesn't match schema: Value is not nullable`,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- input := newInputDefault()
- opts := []openapi3.SchemaValidationOption(nil)
- if tt.isHeaderPresent {
- input.Header = map[string][]string{http.CanonicalHeaderKey(tt.args.headerName): tt.headerVals}
- }
-
- err := validateResponseHeader(tt.args.headerName, tt.args.headerRef, input, opts)
- if tt.wantErr {
- require.NotEmpty(t, tt.wantErrMsg, "wanted error message is not populated")
- require.Error(t, err)
- require.Contains(t, err.Error(), tt.wantErrMsg)
- } else {
- require.NoError(t, err)
- }
- })
- }
-}
-
-func newInputDefault() *ResponseValidationInput {
- return &ResponseValidationInput{
- RequestValidationInput: &RequestValidationInput{
- Request: nil,
- PathParams: nil,
- Route: nil,
- },
- Status: 200,
- Header: nil,
- Body: io.NopCloser(strings.NewReader(`{}`)),
- }
-}
-
-func newHeaderRef(schema *openapi3.Schema, required bool) *openapi3.HeaderRef {
- return &openapi3.HeaderRef{Value: &openapi3.Header{Parameter: openapi3.Parameter{Schema: &openapi3.SchemaRef{Value: schema}, Required: required}}}
-}
-
-func newArraySchema(schema *openapi3.Schema) *openapi3.Schema {
- arraySchema := openapi3.NewArraySchema()
- arraySchema.Items = openapi3.NewSchemaRef("", schema)
-
- return arraySchema
-}
diff --git a/openapi3filter/validate_set_default_test.go b/openapi3filter/validate_set_default_test.go
deleted file mode 100644
index 731cbbdca..000000000
--- a/openapi3filter/validate_set_default_test.go
+++ /dev/null
@@ -1,803 +0,0 @@
-package openapi3filter
-
-import (
- "bytes"
- "encoding/json"
- "io/ioutil"
- "net/http"
- "net/url"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
-)
-
-func TestValidatingRequestParameterAndSetDefault(t *testing.T) {
- const spec = `{
- "openapi": "3.0.3",
- "info": {
- "version": "1.0.0",
- "title": "title",
- "description": "desc",
- "contact": {
- "email": "email"
- }
- },
- "paths": {
- "/accounts": {
- "get": {
- "description": "Create a new account",
- "parameters": [
- {
- "in": "query",
- "name": "q1",
- "schema": {
- "type": "string",
- "default": "Q"
- }
- },
- {
- "in": "query",
- "name": "q2",
- "schema": {
- "type": "string",
- "default": "Q"
- }
- },
- {
- "in": "query",
- "name": "q3",
- "schema": {
- "type": "string"
- }
- },
- {
- "in": "header",
- "name": "h1",
- "schema": {
- "type": "boolean",
- "default": true
- }
- },
- {
- "in": "header",
- "name": "h2",
- "schema": {
- "type": "boolean",
- "default": true
- }
- },
- {
- "in": "header",
- "name": "h3",
- "schema": {
- "type": "boolean"
- }
- },
- {
- "in": "cookie",
- "name": "c1",
- "schema": {
- "type": "integer",
- "default": 128
- }
- },
- {
- "in": "cookie",
- "name": "c2",
- "schema": {
- "type": "integer",
- "default": 128
- }
- },
- {
- "in": "cookie",
- "name": "c3",
- "schema": {
- "type": "integer"
- }
- }
- ],
- "responses": {
- "201": {
- "description": "Successfully created a new account"
- },
- "400": {
- "description": "The server could not understand the request due to invalid syntax",
- }
- }
- }
- }
- }
-}
-`
-
- sl := openapi3.NewLoader()
- doc, err := sl.LoadFromData([]byte(spec))
- require.NoError(t, err)
- err = doc.Validate(sl.Context)
- require.NoError(t, err)
- router, err := legacyrouter.NewRouter(doc)
- require.NoError(t, err)
-
- httpReq, err := http.NewRequest(http.MethodGet, "/accounts", nil)
- require.NoError(t, err)
-
- params := &url.Values{
- "q2": []string{"from_request"},
- }
- httpReq.URL.RawQuery = params.Encode()
- httpReq.Header.Set("h2", "false")
- httpReq.AddCookie(&http.Cookie{Name: "c2", Value: "1024"})
-
- route, pathParams, err := router.FindRoute(httpReq)
- require.NoError(t, err)
-
- err = ValidateRequest(sl.Context, &RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- })
- require.NoError(t, err)
-
- // Unset default values in URL were set
- require.Equal(t, "Q", httpReq.URL.Query().Get("q1"))
- // Unset default values in headers were set
- require.Equal(t, "true", httpReq.Header.Get("h1"))
- // Unset default values in cookies were set
- cookie, err := httpReq.Cookie("c1")
- require.NoError(t, err)
- require.Equal(t, "128", cookie.Value)
-
- // All values from request were retained
- require.Equal(t, "from_request", httpReq.URL.Query().Get("q2"))
- require.Equal(t, "false", httpReq.Header.Get("h2"))
- cookie, err = httpReq.Cookie("c2")
- require.NoError(t, err)
- require.Equal(t, "1024", cookie.Value)
-
- // Not set value to parameters without default value
- require.Equal(t, "", httpReq.URL.Query().Get("q3"))
- require.Equal(t, "", httpReq.Header.Get("h3"))
- _, err = httpReq.Cookie("c3")
- require.Equal(t, http.ErrNoCookie, err)
-}
-
-func TestValidateRequestBodyAndSetDefault(t *testing.T) {
- const spec = `{
- "openapi": "3.0.3",
- "info": {
- "version": "1.0.0",
- "title": "title",
- "description": "desc",
- "contact": {
- "email": "email"
- }
- },
- "paths": {
- "/accounts": {
- "post": {
- "description": "Create a new account",
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "required": ["id"],
- "properties": {
- "id": {
- "type": "string",
- "pattern": "[0-9a-v]+$",
- "minLength": 20,
- "maxLength": 20
- },
- "name": {
- "type": "string",
- "default": "default"
- },
- "code": {
- "type": "integer",
- "default": 123
- },
- "all": {
- "type": "boolean",
- "default": false
- },
- "page": {
- "type": "object",
- "properties": {
- "num": {
- "type": "integer",
- "default": 1
- },
- "size": {
- "type": "integer",
- "default": 10
- },
- "order": {
- "type": "string",
- "enum": ["asc", "desc"],
- "default": "desc"
- }
- }
- },
- "filters": {
- "type": "array",
- "nullable": true,
- "items": {
- "type": "object",
- "properties": {
- "field": {
- "type": "string",
- "default": "name"
- },
- "op": {
- "type": "string",
- "enum": ["eq", "ne"],
- "default": "eq"
- },
- "value": {
- "type": "integer",
- "default": 123
- }
- }
- }
- },
- "social_network": {
- "oneOf": [
- {
- "type": "object",
- "required": ["platform"],
- "properties": {
- "platform": {
- "type": "string",
- "enum": [
- "twitter"
- ]
- },
- "tw_link": {
- "type": "string",
- "default": "www.twitter.com"
- }
- }
- },
- {
- "type": "object",
- "required": ["platform"],
- "properties": {
- "platform": {
- "type": "string",
- "enum": [
- "facebook"
- ]
- },
- "fb_link": {
- "type": "string",
- "default": "www.facebook.com"
- }
- }
- }
- ]
- },
- "social_network_2": {
- "anyOf": [
- {
- "type": "object",
- "required": ["platform"],
- "properties": {
- "platform": {
- "type": "string",
- "enum": [
- "twitter"
- ]
- },
- "tw_link": {
- "type": "string",
- "default": "www.twitter.com"
- }
- }
- },
- {
- "type": "object",
- "required": ["platform"],
- "properties": {
- "platform": {
- "type": "string",
- "enum": [
- "facebook"
- ]
- },
- "fb_link": {
- "type": "string",
- "default": "www.facebook.com"
- }
- }
- }
- ]
- },
- "contact": {
- "oneOf": [
- {
- "type": "object",
- "required": ["email"],
- "properties": {
- "email": {
- "type": "string"
- },
- "allow_image": {
- "type": "boolean",
- "default": true
- }
- },
- "additionalProperties": false
- },
- {
- "type": "object",
- "required": ["phone"],
- "properties": {
- "phone": {
- "type": "string"
- },
- "allow_text": {
- "type": "boolean",
- "default": false
- }
- },
- "additionalProperties": false
- }
- ]
- },
- "contact2": {
- "anyOf": [
- {
- "type": "object",
- "required": ["email"],
- "properties": {
- "email": {
- "type": "string"
- },
- "allow_image": {
- "type": "boolean",
- "default": true
- }
- },
- "additionalProperties": false
- },
- {
- "type": "object",
- "required": ["phone"],
- "properties": {
- "phone": {
- "type": "string"
- },
- "allow_text": {
- "type": "boolean",
- "default": false
- }
- },
- "additionalProperties": false
- }
- ]
- }
- }
- }
- }
- }
- },
- "responses": {
- "201": {
- "description": "Successfully created a new account"
- },
- "400": {
- "description": "The server could not understand the request due to invalid syntax",
- }
- }
- }
- }
- }
-}`
- sl := openapi3.NewLoader()
- doc, err := sl.LoadFromData([]byte(spec))
- require.NoError(t, err)
- err = doc.Validate(sl.Context)
- require.NoError(t, err)
- router, err := legacyrouter.NewRouter(doc)
- require.NoError(t, err)
-
- type page struct {
- Num int `json:"num,omitempty"`
- Size int `json:"size,omitempty"`
- Order string `json:"order,omitempty"`
- }
- type filter struct {
- Field string `json:"field,omitempty"`
- OP string `json:"op,omitempty"`
- Value int `json:"value,omitempty"`
- }
- type socialNetwork struct {
- Platform string `json:"platform,omitempty"`
- FBLink string `json:"fb_link,omitempty"`
- TWLink string `json:"tw_link,omitempty"`
- }
- type contact struct {
- Email string `json:"email,omitempty"`
- Phone string `json:"phone,omitempty"`
- }
- type body struct {
- ID string `json:"id,omitempty"`
- Name string `json:"name,omitempty"`
- Code int `json:"code,omitempty"`
- All bool `json:"all,omitempty"`
- Page *page `json:"page,omitempty"`
- Filters []filter `json:"filters,omitempty"`
- SocialNetwork *socialNetwork `json:"social_network,omitempty"`
- SocialNetwork2 *socialNetwork `json:"social_network_2,omitempty"`
- Contact *contact `json:"contact,omitempty"`
- Contact2 *contact `json:"contact2,omitempty"`
- }
-
- testCases := []struct {
- name string
- body body
- bodyAssertion func(t *testing.T, body string)
- }{
- {
- name: "only id",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `{"id":"bt6kdc3d0cvp6u8u3ft0", "name": "default", "code": 123, "all": false}`, body)
- },
- },
- {
- name: "id & name",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Name: "non-default",
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `{"id":"bt6kdc3d0cvp6u8u3ft0", "name": "non-default", "code": 123, "all": false}`, body)
- },
- },
- {
- name: "id & name & code",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Name: "non-default",
- Code: 456,
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `{"id":"bt6kdc3d0cvp6u8u3ft0", "name": "non-default", "code": 456, "all": false}`, body)
- },
- },
- {
- name: "id & name & code & all",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Name: "non-default",
- Code: 456,
- All: true,
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `{"id":"bt6kdc3d0cvp6u8u3ft0", "name": "non-default", "code": 456, "all": true}`, body)
- },
- },
- {
- name: "id & page(num)",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Page: &page{
- Num: 10,
- },
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `
-{
- "id": "bt6kdc3d0cvp6u8u3ft0",
- "name": "default",
- "code": 123,
- "all": false,
- "page": {
- "num": 10,
- "size": 10,
- "order": "desc"
- }
-}
- `, body)
- },
- },
- {
- name: "id & page(num & order)",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Page: &page{
- Num: 10,
- Order: "asc",
- },
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `
-{
- "id": "bt6kdc3d0cvp6u8u3ft0",
- "name": "default",
- "code": 123,
- "all": false,
- "page": {
- "num": 10,
- "size": 10,
- "order": "asc"
- }
-}
- `, body)
- },
- },
- {
- name: "id & page & filters(one element and contains field)",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Page: &page{
- Num: 10,
- Order: "asc",
- },
- Filters: []filter{
- {
- Field: "code",
- },
- },
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `
-{
- "id": "bt6kdc3d0cvp6u8u3ft0",
- "name": "default",
- "code": 123,
- "all": false,
- "page": {
- "num": 10,
- "size": 10,
- "order": "asc"
- },
- "filters": [
- {
- "field": "code",
- "op": "eq",
- "value": 123
- }
- ]
-}
- `, body)
- },
- },
- {
- name: "id & page & filters(one element and contains field & op & value)",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Page: &page{
- Num: 10,
- Order: "asc",
- },
- Filters: []filter{
- {
- Field: "code",
- OP: "ne",
- Value: 456,
- },
- },
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `
-{
- "id": "bt6kdc3d0cvp6u8u3ft0",
- "name": "default",
- "code": 123,
- "all": false,
- "page": {
- "num": 10,
- "size": 10,
- "order": "asc"
- },
- "filters": [
- {
- "field": "code",
- "op": "ne",
- "value": 456
- }
- ]
-}
- `, body)
- },
- },
- {
- name: "id & page & filters(multiple elements)",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Page: &page{
- Num: 10,
- Order: "asc",
- },
- Filters: []filter{
- {
- Value: 456,
- },
- {
- OP: "ne",
- },
- {
- Field: "code",
- Value: 456,
- },
- {
- OP: "ne",
- Value: 789,
- },
- {
- Field: "code",
- OP: "ne",
- Value: 456,
- },
- },
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `
-{
- "id": "bt6kdc3d0cvp6u8u3ft0",
- "name": "default",
- "code": 123,
- "all": false,
- "page": {
- "num": 10,
- "size": 10,
- "order": "asc"
- },
- "filters": [
- {
- "field": "name",
- "op": "eq",
- "value": 456
- },
- {
- "field": "name",
- "op": "ne",
- "value": 123
- },
- {
- "field": "code",
- "op": "eq",
- "value": 456
- },
- {
- "field": "name",
- "op": "ne",
- "value": 789
- },
- {
- "field": "code",
- "op": "ne",
- "value": 456
- }
- ]
-}
- `, body)
- },
- },
- {
- name: "social_network(oneOf)",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- SocialNetwork: &socialNetwork{
- Platform: "facebook",
- },
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `
-{
- "id": "bt6kdc3d0cvp6u8u3ft0",
- "name": "default",
- "code": 123,
- "all": false,
- "social_network": {
- "platform": "facebook",
- "fb_link": "www.facebook.com"
- }
-}
- `, body)
- },
- },
- {
- name: "social_network_2(anyOf)",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- SocialNetwork2: &socialNetwork{
- Platform: "facebook",
- },
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `
-{
- "id": "bt6kdc3d0cvp6u8u3ft0",
- "name": "default",
- "code": 123,
- "all": false,
- "social_network_2": {
- "platform": "facebook",
- "fb_link": "www.facebook.com"
- }
-}
- `, body)
- },
- },
- {
- name: "contact(oneOf)",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Contact: &contact{
- Phone: "123456",
- },
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `
-{
- "id": "bt6kdc3d0cvp6u8u3ft0",
- "name": "default",
- "code": 123,
- "all": false,
- "contact": {
- "phone": "123456",
- "allow_text": false
- }
-}
- `, body)
- },
- },
- {
- name: "contact(anyOf)",
- body: body{
- ID: "bt6kdc3d0cvp6u8u3ft0",
- Contact2: &contact{
- Phone: "123456",
- },
- },
- bodyAssertion: func(t *testing.T, body string) {
- require.JSONEq(t, `
-{
- "id": "bt6kdc3d0cvp6u8u3ft0",
- "name": "default",
- "code": 123,
- "all": false,
- "contact2": {
- "phone": "123456",
- "allow_text": false
- }
-}
- `, body)
- },
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- b, err := json.Marshal(tc.body)
- require.NoError(t, err)
- httpReq, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(b))
- require.NoError(t, err)
- httpReq.Header.Add(headerCT, "application/json")
-
- route, pathParams, err := router.FindRoute(httpReq)
- require.NoError(t, err)
-
- err = ValidateRequest(sl.Context, &RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- })
- require.NoError(t, err)
-
- validatedReqBody, err := ioutil.ReadAll(httpReq.Body)
- require.NoError(t, err)
- tc.bodyAssertion(t, string(validatedReqBody))
- })
- }
-}
diff --git a/openapi3filter/validation_discriminator_test.go b/openapi3filter/validation_discriminator_test.go
deleted file mode 100644
index adabf409d..000000000
--- a/openapi3filter/validation_discriminator_test.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package openapi3filter
-
-import (
- "bytes"
- "net/http"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
-)
-
-func TestValidationWithDiscriminatorSelection(t *testing.T) {
- const spec = `
-openapi: 3.0.0
-info:
- version: 0.2.0
- title: yaAPI
-
-paths:
-
- /blob:
- put:
- operationId: SetObj
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/blob'
- responses:
- '200':
- description: Ok
-
-components:
- schemas:
- blob:
- oneOf:
- - $ref: '#/components/schemas/objA'
- - $ref: '#/components/schemas/objB'
- discriminator:
- propertyName: discr
- mapping:
- objA: '#/components/schemas/objA'
- objB: '#/components/schemas/objB'
- genericObj:
- type: object
- required:
- - discr
- properties:
- discr:
- type: string
- enum:
- - objA
- - objB
- discriminator:
- propertyName: discr
- mapping:
- objA: '#/components/schemas/objA'
- objB: '#/components/schemas/objB'
- objA:
- allOf:
- - $ref: '#/components/schemas/genericObj'
- - type: object
- properties:
- base64:
- type: string
-
- objB:
- allOf:
- - $ref: '#/components/schemas/genericObj'
- - type: object
- properties:
- value:
- type: integer
-`
-
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- router, err := legacyrouter.NewRouter(doc)
- require.NoError(t, err)
-
- body := bytes.NewReader([]byte(`{"discr": "objA", "base64": "S25vY2sgS25vY2ssIE5lbyAuLi4="}`))
- req, err := http.NewRequest("PUT", "/blob", body)
- require.NoError(t, err)
- req.Header.Add(headerCT, "application/json")
-
- route, pathParams, err := router.FindRoute(req)
- require.NoError(t, err)
-
- requestValidationInput := &RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- }
- err = ValidateRequest(loader.Context, requestValidationInput)
- require.NoError(t, err)
-}
diff --git a/openapi3filter/validation_enum_test.go b/openapi3filter/validation_enum_test.go
deleted file mode 100644
index 898c4027a..000000000
--- a/openapi3filter/validation_enum_test.go
+++ /dev/null
@@ -1,175 +0,0 @@
-package openapi3filter
-
-import (
- "bytes"
- "net/http"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
-)
-
-func TestValidationWithIntegerEnum(t *testing.T) {
- const spec = `
-openapi: 3.0.0
-info:
- title: Example integer enum
- version: '0.1'
-paths:
- /sample:
- put:
- requestBody:
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- exenum:
- type: integer
- enum:
- - 0
- - 1
- - 2
- - 3
- example: 0
- nullable: true
- responses:
- '200':
- description: Ok
-`
-
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- router, err := legacyrouter.NewRouter(doc)
- require.NoError(t, err)
-
- tests := []struct {
- data []byte
- wantErr bool
- }{
- {
- []byte(`{"exenum": 1}`),
- false,
- },
- {
- []byte(`{"exenum": "1"}`),
- true,
- },
- {
- []byte(`{"exenum": null}`),
- false,
- },
- {
- []byte(`{}`),
- false,
- },
- }
-
- for _, tt := range tests {
- body := bytes.NewReader(tt.data)
- req, err := http.NewRequest("PUT", "/sample", body)
- require.NoError(t, err)
- req.Header.Add(headerCT, "application/json")
-
- route, pathParams, err := router.FindRoute(req)
- require.NoError(t, err)
-
- requestValidationInput := &RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- }
- err = ValidateRequest(loader.Context, requestValidationInput)
- if tt.wantErr {
- require.Error(t, err)
- } else {
- require.NoError(t, err)
- }
- }
-}
-
-func TestValidationWithStringEnum(t *testing.T) {
- const spec = `
-openapi: 3.0.0
-info:
- title: Example string enum
- version: '0.1'
-paths:
- /sample:
- put:
- requestBody:
- required: true
- content:
- application/json:
- schema:
- type: object
- properties:
- exenum:
- type: string
- enum:
- - "0"
- - "1"
- - "2"
- - "3"
- example: "0"
- responses:
- '200':
- description: Ok
-`
-
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- router, err := legacyrouter.NewRouter(doc)
- require.NoError(t, err)
-
- tests := []struct {
- data []byte
- wantErr bool
- }{
- {
- []byte(`{"exenum": "1"}`),
- false,
- },
- {
- []byte(`{"exenum": 1}`),
- true,
- },
- {
- []byte(`{"exenum": null}`),
- true,
- },
- {
- []byte(`{}`),
- false,
- },
- }
-
- for _, tt := range tests {
- body := bytes.NewReader(tt.data)
- req, err := http.NewRequest("PUT", "/sample", body)
- require.NoError(t, err)
- req.Header.Add(headerCT, "application/json")
-
- route, pathParams, err := router.FindRoute(req)
- require.NoError(t, err)
-
- requestValidationInput := &RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- }
- err = ValidateRequest(loader.Context, requestValidationInput)
- if tt.wantErr {
- require.Error(t, err)
- } else {
- require.NoError(t, err)
- }
- }
-}
diff --git a/openapi3filter/validation_error.go b/openapi3filter/validation_error.go
index 7e685cdef..bfeeaa7da 100644
--- a/openapi3filter/validation_error.go
+++ b/openapi3filter/validation_error.go
@@ -10,25 +10,25 @@ import (
// Based on https://jsonapi.org/format/#error-objects
type ValidationError struct {
// A unique identifier for this particular occurrence of the problem.
- Id string `json:"id,omitempty" yaml:"id,omitempty"`
+ Id string `json:"id,omitempty"`
// The HTTP status code applicable to this problem.
- Status int `json:"status,omitempty" yaml:"status,omitempty"`
+ Status int `json:"status,omitempty"`
// An application-specific error code, expressed as a string value.
- Code string `json:"code,omitempty" yaml:"code,omitempty"`
+ Code string `json:"code,omitempty"`
// A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.
- Title string `json:"title,omitempty" yaml:"title,omitempty"`
+ Title string `json:"title,omitempty"`
// A human-readable explanation specific to this occurrence of the problem.
- Detail string `json:"detail,omitempty" yaml:"detail,omitempty"`
+ Detail string `json:"detail,omitempty"`
// An object containing references to the source of the error
- Source *ValidationErrorSource `json:"source,omitempty" yaml:"source,omitempty"`
+ Source *ValidationErrorSource `json:"source,omitempty"`
}
// ValidationErrorSource struct
type ValidationErrorSource struct {
// A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].
- Pointer string `json:"pointer,omitempty" yaml:"pointer,omitempty"`
+ Pointer string `json:"pointer,omitempty"`
// A string indicating which query parameter caused the error.
- Parameter string `json:"parameter,omitempty" yaml:"parameter,omitempty"`
+ Parameter string `json:"parameter,omitempty"`
}
var _ error = &ValidationError{}
diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go
index 779887db0..49459ef9c 100644
--- a/openapi3filter/validation_error_encoder.go
+++ b/openapi3filter/validation_error_encoder.go
@@ -4,10 +4,10 @@ import (
"context"
"fmt"
"net/http"
+ "regexp"
"strings"
"github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers"
)
// ValidationErrorEncoder wraps a base ErrorEncoder to handle ValidationErrors
@@ -17,8 +17,10 @@ type ValidationErrorEncoder struct {
// Encode implements the ErrorEncoder interface for encoding ValidationErrors
func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http.ResponseWriter) {
- if e, ok := err.(*routers.RouteError); ok {
- cErr := convertRouteError(e)
+ var cErr *ValidationError
+
+ if e, ok := err.(*RouteError); ok {
+ cErr = convertRouteError(e)
enc.Encoder(ctx, cErr, w)
return
}
@@ -29,13 +31,10 @@ func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http
return
}
- var cErr *ValidationError
if e.Err == nil {
cErr = convertBasicRequestError(e)
} else if e.Err == ErrInvalidRequired {
cErr = convertErrInvalidRequired(e)
- } else if e.Err == ErrInvalidEmptyValue {
- cErr = convertErrInvalidEmptyValue(e)
} else if innerErr, ok := e.Err.(*ParseError); ok {
cErr = convertParseError(e, innerErr)
} else if innerErr, ok := e.Err.(*openapi3.SchemaError); ok {
@@ -44,93 +43,95 @@ func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http
if cErr != nil {
enc.Encoder(ctx, cErr, w)
- return
+ } else {
+ enc.Encoder(ctx, err, w)
}
- enc.Encoder(ctx, err, w)
}
-func convertRouteError(e *routers.RouteError) *ValidationError {
- status := http.StatusNotFound
- if e.Error() == routers.ErrMethodNotAllowed.Error() {
- status = http.StatusMethodNotAllowed
+func convertRouteError(e *RouteError) *ValidationError {
+ var cErr *ValidationError
+ switch e.Reason {
+ case "Path doesn't support the HTTP method":
+ cErr = &ValidationError{Status: http.StatusMethodNotAllowed, Title: e.Reason}
+ default:
+ cErr = &ValidationError{Status: http.StatusNotFound, Title: e.Reason}
}
- return &ValidationError{Status: status, Title: e.Error()}
+ return cErr
}
func convertBasicRequestError(e *RequestError) *ValidationError {
- if strings.HasPrefix(e.Reason, prefixInvalidCT) {
- if strings.HasSuffix(e.Reason, `""`) {
- return &ValidationError{
+ var cErr *ValidationError
+ unsupportedContentType := "header 'Content-Type' has unexpected value: "
+ if strings.HasPrefix(e.Reason, unsupportedContentType) {
+ if strings.HasSuffix(e.Reason, `: ""`) {
+ cErr = &ValidationError{
Status: http.StatusUnsupportedMediaType,
- Title: "header Content-Type is required",
+ Title: "header 'Content-Type' is required",
+ }
+ } else {
+ cErr = &ValidationError{
+ Status: http.StatusUnsupportedMediaType,
+ Title: "unsupported content type " + strings.TrimPrefix(e.Reason, unsupportedContentType),
}
}
- return &ValidationError{
- Status: http.StatusUnsupportedMediaType,
- Title: prefixUnsupportedCT + strings.TrimPrefix(e.Reason, prefixInvalidCT),
+ } else {
+ cErr = &ValidationError{
+ Status: http.StatusBadRequest,
+ Title: e.Error(),
}
}
- return &ValidationError{
- Status: http.StatusBadRequest,
- Title: e.Error(),
- }
+ return cErr
}
func convertErrInvalidRequired(e *RequestError) *ValidationError {
- if e.Err == ErrInvalidRequired && e.Parameter != nil {
- return &ValidationError{
+ var cErr *ValidationError
+ if e.Reason == ErrInvalidRequired.Error() && e.Parameter != nil {
+ cErr = &ValidationError{
Status: http.StatusBadRequest,
- Title: fmt.Sprintf("parameter %q in %s is required", e.Parameter.Name, e.Parameter.In),
+ Title: fmt.Sprintf("Parameter '%s' in %s is required", e.Parameter.Name, e.Parameter.In),
}
- }
- return &ValidationError{
- Status: http.StatusBadRequest,
- Title: e.Error(),
- }
-}
-
-func convertErrInvalidEmptyValue(e *RequestError) *ValidationError {
- if e.Err == ErrInvalidEmptyValue && e.Parameter != nil {
- return &ValidationError{
+ } else {
+ cErr = &ValidationError{
Status: http.StatusBadRequest,
- Title: fmt.Sprintf("parameter %q in %s is not allowed to be empty", e.Parameter.Name, e.Parameter.In),
+ Title: e.Error(),
}
}
- return &ValidationError{
- Status: http.StatusBadRequest,
- Title: e.Error(),
- }
+ return cErr
}
func convertParseError(e *RequestError, innerErr *ParseError) *ValidationError {
+ var cErr *ValidationError
// We treat path params of the wrong type like a 404 instead of a 400
if innerErr.Kind == KindInvalidFormat && e.Parameter != nil && e.Parameter.In == "path" {
- return &ValidationError{
+ cErr = &ValidationError{
Status: http.StatusNotFound,
- Title: fmt.Sprintf("resource not found with %q value: %v", e.Parameter.Name, innerErr.Value),
+ Title: fmt.Sprintf("Resource not found with '%s' value: %v", e.Parameter.Name, innerErr.Value),
}
- } else if strings.HasPrefix(innerErr.Reason, prefixUnsupportedCT) {
- return &ValidationError{
+ } else if strings.HasPrefix(innerErr.Reason, "unsupported content type") {
+ cErr = &ValidationError{
Status: http.StatusUnsupportedMediaType,
Title: innerErr.Reason,
}
} else if innerErr.RootCause() != nil {
if rootErr, ok := innerErr.Cause.(*ParseError); ok &&
rootErr.Kind == KindInvalidFormat && e.Parameter.In == "query" {
- return &ValidationError{
+ cErr = &ValidationError{
Status: http.StatusBadRequest,
- Title: fmt.Sprintf("parameter %q in %s is invalid: %v is %s",
+ Title: fmt.Sprintf("Parameter '%s' in %s is invalid: %v is %s",
e.Parameter.Name, e.Parameter.In, rootErr.Value, rootErr.Reason),
}
- }
- return &ValidationError{
- Status: http.StatusBadRequest,
- Title: innerErr.Reason,
+ } else {
+ cErr = &ValidationError{
+ Status: http.StatusBadRequest,
+ Title: innerErr.Reason,
+ }
}
}
- return nil
+ return cErr
}
+var propertyMissingNameRE = regexp.MustCompile(`Property '(?P[^']*)' is missing`)
+
func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *ValidationError {
cErr := &ValidationError{Title: innerErr.Reason}
@@ -147,34 +148,36 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida
}
// Add error source
- if e.Parameter != nil {
+ if e.Parameter != nil && e.Parameter.In == "query" {
// We have a JSONPointer in the query param too so need to
// make sure 'Parameter' check takes priority over 'Pointer'
- cErr.Source = &ValidationErrorSource{Parameter: e.Parameter.Name}
- } else if ptr := innerErr.JSONPointer(); ptr != nil {
- cErr.Source = &ValidationErrorSource{Pointer: toJSONPointer(ptr)}
+ cErr.Source = &ValidationErrorSource{
+ Parameter: e.Parameter.Name,
+ }
+ } else if innerErr.JSONPointer() != nil {
+ pointer := innerErr.JSONPointer()
+
+ cErr.Source = &ValidationErrorSource{
+ Pointer: toJSONPointer(pointer),
+ }
}
// Add details on allowed values for enums
- if innerErr.SchemaField == "enum" {
+ if innerErr.SchemaField == "enum" &&
+ innerErr.Reason == "JSON value is not one of the allowed values" {
enums := make([]string, 0, len(innerErr.Schema.Enum))
for _, enum := range innerErr.Schema.Enum {
enums = append(enums, fmt.Sprintf("%v", enum))
}
- cErr.Detail = fmt.Sprintf("value %v at %s must be one of: %s",
- innerErr.Value,
- toJSONPointer(innerErr.JSONPointer()),
- strings.Join(enums, ", "))
+ cErr.Detail = fmt.Sprintf("Value '%v' at %s must be one of: %s",
+ innerErr.Value, toJSONPointer(innerErr.JSONPointer()), strings.Join(enums, ", "))
value := fmt.Sprintf("%v", innerErr.Value)
- if e.Parameter != nil &&
- (e.Parameter.Explode == nil || *e.Parameter.Explode) &&
+ if (e.Parameter.Explode == nil || *e.Parameter.Explode == true) &&
(e.Parameter.Style == "" || e.Parameter.Style == "form") &&
strings.Contains(value, ",") {
parts := strings.Split(value, ",")
- cErr.Detail = fmt.Sprintf("%s; perhaps you intended '?%s=%s'",
- cErr.Detail,
- e.Parameter.Name,
- strings.Join(parts, "&"+e.Parameter.Name+"="))
+ cErr.Detail = cErr.Detail+"; "+ fmt.Sprintf("perhaps you intended '?%s=%s'",
+ e.Parameter.Name, strings.Join(parts, "&"+e.Parameter.Name+"="))
}
}
return cErr
diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go
index 18368afe7..f67826f72 100644
--- a/openapi3filter/validation_error_test.go
+++ b/openapi3filter/validation_error_test.go
@@ -10,10 +10,8 @@ import (
"net/http/httptest"
"testing"
- "github.com/stretchr/testify/require"
-
"github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers"
+ "github.com/stretchr/testify/require"
)
func newPetstoreRequest(t *testing.T, method, path string, body io.Reader) *http.Request {
@@ -21,7 +19,7 @@ func newPetstoreRequest(t *testing.T, method, path string, body io.Reader) *http
pathPrefix := "v2"
r, err := http.NewRequest(method, fmt.Sprintf("http://%s/%s%s", host, pathPrefix, path), body)
require.NoError(t, err)
- r.Header.Set(headerCT, "application/json")
+ r.Header.Set("Content-Type", "application/json")
r.Header.Set("Authorization", "Bearer magicstring")
r.Host = host
return r
@@ -29,7 +27,7 @@ func newPetstoreRequest(t *testing.T, method, path string, body io.Reader) *http
type validationFields struct {
Handler http.Handler
- File string
+ SwaggerFile string
ErrorEncoder ErrorEncoder
}
type validationArgs struct {
@@ -57,8 +55,7 @@ type validationTest struct {
}
func getValidationTests(t *testing.T) []*validationTest {
- badHost, err := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil)
- require.NoError(t, err)
+ badHost, _ := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil)
badPath := newPetstoreRequest(t, http.MethodGet, "/watdis", nil)
badMethod := newPetstoreRequest(t, http.MethodTrace, "/pet", nil)
@@ -66,19 +63,16 @@ func getValidationTests(t *testing.T) []*validationTest {
missingBody2 := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(``))
noContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`))
- noContentType.Header.Del(headerCT)
+ noContentType.Header.Del("Content-Type")
noContentTypeNeeded := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil)
- noContentTypeNeeded.Header.Del(headerCT)
+ noContentTypeNeeded.Header.Del("Content-Type")
unknownContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`))
- unknownContentType.Header.Set(headerCT, "application/xml")
+ unknownContentType.Header.Set("Content-Type", "application/xml")
unsupportedContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`))
- unsupportedContentType.Header.Set(headerCT, "text/plain")
-
- unsupportedHeaderValue := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`))
- unsupportedHeaderValue.Header.Set("x-environment", "watdis")
+ unsupportedContentType.Header.Set("Content-Type", "text/plain")
return []*validationTest{
//
@@ -90,45 +84,45 @@ func getValidationTests(t *testing.T) []*validationTest {
args: validationArgs{
r: badHost,
},
- wantErrReason: routers.ErrPathNotFound.Error(),
- wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: routers.ErrPathNotFound.Error()},
+ wantErrReason: "Does not match any server",
+ wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: "Does not match any server"},
},
{
name: "error - unknown path",
args: validationArgs{
r: badPath,
},
- wantErrReason: routers.ErrPathNotFound.Error(),
- wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: routers.ErrPathNotFound.Error()},
+ wantErrReason: "Path was not found",
+ wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: "Path was not found"},
},
{
name: "error - unknown method",
args: validationArgs{
r: badMethod,
},
- wantErrReason: routers.ErrMethodNotAllowed.Error(),
+ wantErrReason: "Path doesn't support the HTTP method",
// TODO: By HTTP spec, this should have an Allow header with what is allowed
// but kin-openapi doesn't provide us the requested method or path, so impossible to provide details
wantErrResponse: &ValidationError{Status: http.StatusMethodNotAllowed,
- Title: routers.ErrMethodNotAllowed.Error()},
+ Title: "Path doesn't support the HTTP method"},
},
{
name: "error - missing body on POST",
args: validationArgs{
r: missingBody1,
},
- wantErrBody: "request body has an error: " + ErrInvalidRequired.Error(),
+ wantErrBody: "Request body has an error: must have a value",
wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: "request body has an error: " + ErrInvalidRequired.Error()},
+ Title: "Request body has an error: must have a value"},
},
{
name: "error - empty body on POST",
args: validationArgs{
r: missingBody2,
},
- wantErrBody: "request body has an error: " + ErrInvalidRequired.Error(),
+ wantErrBody: "Request body has an error: must have a value",
wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: "request body has an error: " + ErrInvalidRequired.Error()},
+ Title: "Request body has an error: must have a value"},
},
//
@@ -140,9 +134,9 @@ func getValidationTests(t *testing.T) []*validationTest {
args: validationArgs{
r: noContentType,
},
- wantErrReason: prefixInvalidCT + ` ""`,
+ wantErrReason: "header 'Content-Type' has unexpected value: \"\"",
wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType,
- Title: "header Content-Type is required"},
+ Title: "header 'Content-Type' is required"},
},
{
name: "error - unknown content-type on POST",
@@ -151,18 +145,18 @@ func getValidationTests(t *testing.T) []*validationTest {
},
wantErrReason: "failed to decode request body",
wantErrParseKind: KindUnsupportedFormat,
- wantErrParseReason: prefixUnsupportedCT + ` "application/xml"`,
+ wantErrParseReason: "unsupported content type \"application/xml\"",
wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType,
- Title: prefixUnsupportedCT + ` "application/xml"`},
+ Title: "unsupported content type \"application/xml\""},
},
{
name: "error - unsupported content-type on POST",
args: validationArgs{
r: unsupportedContentType,
},
- wantErrReason: prefixInvalidCT + ` "text/plain"`,
+ wantErrReason: "header 'Content-Type' has unexpected value: \"text/plain\"",
wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType,
- Title: prefixUnsupportedCT + ` "text/plain"`},
+ Title: "unsupported content type \"text/plain\""},
},
{
name: "success - no content-type header required on GET",
@@ -182,13 +176,12 @@ func getValidationTests(t *testing.T) []*validationTest {
},
wantErrParam: "status",
wantErrParamIn: "query",
- wantErrBody: `parameter "status" in query has an error: value is required but missing`,
- wantErrReason: "value is required but missing",
+ wantErrReason: "must have a value",
wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: `parameter "status" in query is required`},
+ Title: "Parameter 'status' in query is required"},
},
{
- name: "error - wrong query string parameter type as integer",
+ name: "error - wrong query string parameter type",
args: validationArgs{
r: newPetstoreRequest(t, http.MethodGet, "/pet/findByIds?ids=1,notAnInt", nil),
},
@@ -196,33 +189,16 @@ func getValidationTests(t *testing.T) []*validationTest {
wantErrParamIn: "query",
// This is a nested ParseError. The outer error is a KindOther with no details.
// So we'd need to look at the inner one which is a KindInvalidFormat. So just check the error body.
- wantErrBody: `parameter "ids" in query has an error: path 1: value notAnInt: an invalid integer: invalid syntax`,
+ wantErrBody: "Parameter 'ids' in query has an error: path 1: value notAnInt: an invalid integer: " +
+ "strconv.ParseFloat: parsing \"notAnInt\": invalid syntax",
// TODO: Should we treat query params of the wrong type like a 404 instead of a 400?
wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: `parameter "ids" in query is invalid: notAnInt is an invalid integer`},
+ Title: "Parameter 'ids' in query is invalid: notAnInt is an invalid integer"},
},
{
name: "success - ignores unknown query string parameter",
args: validationArgs{
- r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=available&wat=isdis", nil),
- },
- },
- {
- name: "error - non required query string has empty value",
- args: validationArgs{
- r: newPetstoreRequest(t, http.MethodGet, "/pets/?tags=", nil),
- },
- wantErrParam: "tags",
- wantErrParamIn: "query",
- wantErrBody: `parameter "tags" in query has an error: empty value is not allowed`,
- wantErrReason: "empty value is not allowed",
- wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: `parameter "tags" in query is not allowed to be empty`},
- },
- {
- name: "success - non required query string has empty value, but has AllowEmptyValue",
- args: validationArgs{
- r: newPetstoreRequest(t, http.MethodGet, "/pets/?status=", nil),
+ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?wat=isdis", nil),
},
},
{
@@ -244,12 +220,12 @@ func getValidationTests(t *testing.T) []*validationTest {
},
wantErrParam: "status",
wantErrParamIn: "query",
- wantErrSchemaReason: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
+ wantErrSchemaReason: "JSON value is not one of the allowed values",
wantErrSchemaPath: "/0",
wantErrSchemaValue: "available,sold",
wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
- Detail: "value available,sold at /0 must be one of: available, pending, sold; " +
+ Title: "JSON value is not one of the allowed values",
+ Detail: "Value 'available,sold' at /0 must be one of: available, pending, sold; " +
// TODO: do we really want to use this heuristic to guess
// that they're using the wrong serialization?
"perhaps you intended '?status=available&status=sold'",
@@ -262,12 +238,12 @@ func getValidationTests(t *testing.T) []*validationTest {
},
wantErrParam: "status",
wantErrParamIn: "query",
- wantErrSchemaReason: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
+ wantErrSchemaReason: "JSON value is not one of the allowed values",
wantErrSchemaPath: "/1",
wantErrSchemaValue: "watdis",
wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
- Detail: "value watdis at /1 must be one of: available, pending, sold",
+ Title: "JSON value is not one of the allowed values",
+ Detail: "Value 'watdis' at /1 must be one of: available, pending, sold",
Source: &ValidationErrorSource{Parameter: "status"}},
},
{
@@ -278,12 +254,12 @@ func getValidationTests(t *testing.T) []*validationTest {
},
wantErrParam: "kind",
wantErrParamIn: "query",
- wantErrSchemaReason: "value is not one of the allowed values [\"dog\",\"cat\",\"turtle\",\"bird,with,commas\"]",
+ wantErrSchemaReason: "JSON value is not one of the allowed values",
wantErrSchemaPath: "/1",
wantErrSchemaValue: "fish,with,commas",
wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: "value is not one of the allowed values [\"dog\",\"cat\",\"turtle\",\"bird,with,commas\"]",
- Detail: "value fish,with,commas at /1 must be one of: dog, cat, turtle, bird,with,commas",
+ Title: "JSON value is not one of the allowed values",
+ Detail: "Value 'fish,with,commas' at /1 must be one of: dog, cat, turtle, bird,with,commas",
// No 'perhaps you intended' because its the right serialization format
Source: &ValidationErrorSource{Parameter: "kind"}},
},
@@ -294,54 +270,21 @@ func getValidationTests(t *testing.T) []*validationTest {
},
},
- //
- // Request header params
- //
- {
- name: "error - invalid enum value for header string parameter",
- args: validationArgs{
- r: unsupportedHeaderValue,
- },
- wantErrParam: "x-environment",
- wantErrParamIn: "header",
- wantErrSchemaReason: "value is not one of the allowed values [\"demo\",\"prod\"]",
- wantErrSchemaPath: "/",
- wantErrSchemaValue: "watdis",
- wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: "value is not one of the allowed values [\"demo\",\"prod\"]",
- Detail: "value watdis at / must be one of: demo, prod",
- Source: &ValidationErrorSource{Parameter: "x-environment"}},
- },
-
//
// Request bodies
//
- {
- name: "error - invalid enum value for header object attribute",
- args: validationArgs{
- r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)),
- },
- wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired",
- wantErrSchemaReason: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
- wantErrSchemaValue: "watdis",
- wantErrSchemaPath: "/status",
- wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
- Title: "value is not one of the allowed values [\"available\",\"pending\",\"sold\"]",
- Detail: "value watdis at /status must be one of: available, pending, sold",
- Source: &ValidationErrorSource{Pointer: "/status"}},
- },
{
name: "error - missing required object attribute",
args: validationArgs{
r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama"}`)),
},
- wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired",
- wantErrSchemaReason: `property "photoUrls" is missing`,
+ wantErrReason: "doesn't match the schema",
+ wantErrSchemaReason: "Property 'photoUrls' is missing",
wantErrSchemaValue: map[string]string{"name": "Bahama"},
wantErrSchemaPath: "/photoUrls",
wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
- Title: `property "photoUrls" is missing`,
+ Title: "Property 'photoUrls' is missing",
Source: &ValidationErrorSource{Pointer: "/photoUrls"}},
},
{
@@ -350,12 +293,12 @@ func getValidationTests(t *testing.T) []*validationTest {
r: newPetstoreRequest(t, http.MethodPost, "/pet",
bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{}}`)),
},
- wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired",
- wantErrSchemaReason: `property "name" is missing`,
+ wantErrReason: "doesn't match the schema",
+ wantErrSchemaReason: "Property 'name' is missing",
wantErrSchemaValue: map[string]string{},
wantErrSchemaPath: "/category/name",
wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
- Title: `property "name" is missing`,
+ Title: "Property 'name' is missing",
Source: &ValidationErrorSource{Pointer: "/category/name"}},
},
{
@@ -364,28 +307,37 @@ func getValidationTests(t *testing.T) []*validationTest {
r: newPetstoreRequest(t, http.MethodPost, "/pet",
bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{"tags": [{}]}}`)),
},
- wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired",
- wantErrSchemaReason: `property "name" is missing`,
+ wantErrReason: "doesn't match the schema",
+ wantErrSchemaReason: "Property 'name' is missing",
wantErrSchemaValue: map[string]string{},
wantErrSchemaPath: "/category/tags/0/name",
wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
- Title: `property "name" is missing`,
+ Title: "Property 'name' is missing",
Source: &ValidationErrorSource{Pointer: "/category/tags/0/name"}},
},
+ {
+ // TODO: Add support for validating readonly properties to upstream validator.
+ name: "error - readonly object attribute",
+ args: validationArgs{
+ r: newPetstoreRequest(t, http.MethodPost, "/pet",
+ bytes.NewBufferString(`{"id":213,"name":"Bahama","photoUrls":[]}}`)),
+ },
+ //wantErr: true,
+ },
{
name: "error - wrong attribute type",
args: validationArgs{
r: newPetstoreRequest(t, http.MethodPost, "/pet",
bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)),
},
- wantErrReason: "doesn't match schema #/components/schemas/PetWithRequired",
- wantErrSchemaReason: "value must be an array",
+ wantErrReason: "doesn't match the schema",
+ wantErrSchemaReason: "Field must be set to array or not be present",
wantErrSchemaPath: "/photoUrls",
- wantErrSchemaValue: "http://cat",
+ wantErrSchemaValue: "string",
// TODO: this shouldn't say "or not be present", but this requires recursively resolving
// innerErr.JSONPointer() against e.RequestBody.Content["application/json"].Schema.Value (.Required, .Properties)
wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
- Title: "value must be an array",
+ Title: "Field must be set to array or not be present",
Source: &ValidationErrorSource{Pointer: "/photoUrls"}},
},
{
@@ -393,15 +345,14 @@ func getValidationTests(t *testing.T) []*validationTest {
args: validationArgs{
r: newPetstoreRequest(t, http.MethodPost, "/pet2", bytes.NewBufferString(`{"name":"Bahama"}`)),
},
- wantErrReason: "doesn't match schema",
+ wantErrReason: "doesn't match the schema",
wantErrSchemaPath: "/",
wantErrSchemaValue: map[string]string{"name": "Bahama"},
- wantErrSchemaReason: `doesn't match all schemas from "allOf"`,
- wantErrSchemaOriginReason: `property "photoUrls" is missing`,
+ wantErrSchemaOriginReason: "Property 'photoUrls' is missing",
wantErrSchemaOriginValue: map[string]string{"name": "Bahama"},
wantErrSchemaOriginPath: "/photoUrls",
wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity,
- Title: `property "photoUrls" is missing`,
+ Title: "Property 'photoUrls' is missing",
Source: &ValidationErrorSource{Pointer: "/photoUrls"}},
},
{
@@ -436,10 +387,9 @@ func getValidationTests(t *testing.T) []*validationTest {
},
wantErrParam: "petId",
wantErrParamIn: "path",
- wantErrBody: `parameter "petId" in path has an error: value is required but missing`,
- wantErrReason: "value is required but missing",
+ wantErrReason: "must have a value",
wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
- Title: `parameter "petId" in path is required`},
+ Title: "Parameter 'petId' in path is required"},
},
{
name: "error - wrong path param type",
@@ -452,7 +402,7 @@ func getValidationTests(t *testing.T) []*validationTest {
wantErrParseValue: "NotAnInt",
wantErrParseReason: "an invalid integer",
wantErrResponse: &ValidationError{Status: http.StatusNotFound,
- Title: `resource not found with "petId" value: NotAnInt`},
+ Title: "Resource not found with 'petId' value: NotAnInt"},
},
{
name: "success - normal case, with path params",
@@ -480,13 +430,13 @@ func TestValidationHandler_validateRequest(t *testing.T) {
req.Equal(tt.wantErrBody, err.Error())
}
- if e, ok := err.(*routers.RouteError); ok {
- req.Equal(tt.wantErrReason, e.Error())
+ if e, ok := err.(*RouteError); ok {
+ req.Equal(tt.wantErrReason, e.Reason)
return
}
e, ok := err.(*RequestError)
- req.True(ok, "not a RequestError: %T -- %#v", err, err)
+ req.True(ok, "error = %v, not a RequestError -- %#v", err, err)
req.Equal(tt.wantErrReason, e.Reason)
@@ -556,12 +506,12 @@ func TestValidationErrorEncoder(t *testing.T) {
}
func buildValidationHandler(tt *validationTest) (*ValidationHandler, error) {
- if tt.fields.File == "" {
- tt.fields.File = "testdata/fixtures/petstore.json"
+ if tt.fields.SwaggerFile == "" {
+ tt.fields.SwaggerFile = "fixtures/petstore.json"
}
h := &ValidationHandler{
Handler: tt.fields.Handler,
- File: tt.fields.File,
+ SwaggerFile: tt.fields.SwaggerFile,
ErrorEncoder: tt.fields.ErrorEncoder,
}
tt.wantErr = tt.wantErr ||
@@ -601,7 +551,7 @@ func runTest_ServeHTTP(t *testing.T, handler http.Handler, encoder ErrorEncoder,
h := &ValidationHandler{
Handler: handler,
ErrorEncoder: encoder,
- File: "testdata/fixtures/petstore.json",
+ SwaggerFile: "fixtures/petstore.json",
}
err := h.Load()
require.NoError(t, err)
@@ -613,7 +563,7 @@ func runTest_ServeHTTP(t *testing.T, handler http.Handler, encoder ErrorEncoder,
func runTest_Middleware(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http.Request) *http.Response {
h := &ValidationHandler{
ErrorEncoder: encoder,
- File: "testdata/fixtures/petstore.json",
+ SwaggerFile: "fixtures/petstore.json",
}
err := h.Load()
require.NoError(t, err)
@@ -660,30 +610,8 @@ func TestValidationHandler_ServeHTTP(t *testing.T) {
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
- require.Equal(t, "[422][][] value must be an array [source pointer=/photoUrls]", string(body))
+ require.Equal(t, "[422][][] Field must be set to array or not be present [source pointer=/photoUrls]", string(body))
})
-
- //t.Run("ignores requests not matching openapi servers if requested", func(t *testing.T) {
- // r := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil)
- // r.URL.Host = "notaserver"
- //
- // handler := &testHandler{}
- // encoder := &mockErrorEncoder{}
- //
- // h := &ValidationHandler{
- // Handler: handler,
- // ErrorEncoder: encoder.Encode,
- // SwaggerFile: "fixtures/petstore.json",
- // IgnoreServerErrors: true,
- // }
- // err := h.Load()
- // require.NoError(t, err)
- // w := httptest.NewRecorder()
- // h.ServeHTTP(w, r)
- //
- // require.True(t, handler.Called)
- // require.False(t, encoder.Called)
- //})
}
func TestValidationHandler_Middleware(t *testing.T) {
@@ -724,6 +652,28 @@ func TestValidationHandler_Middleware(t *testing.T) {
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
- require.Equal(t, "[422][][] value must be an array [source pointer=/photoUrls]", string(body))
+ require.Equal(t, "[422][][] Field must be set to array or not be present [source pointer=/photoUrls]", string(body))
+ })
+
+ t.Run("ignores requests not matching openapi servers if requested", func(t *testing.T) {
+ r := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil)
+ r.URL.Host = "notaserver"
+
+ handler := &testHandler{}
+ encoder := &mockErrorEncoder{}
+
+ h := &ValidationHandler{
+ Handler: handler,
+ ErrorEncoder: encoder.Encode,
+ SwaggerFile: "fixtures/petstore.json",
+ IgnoreServerErrors: true,
+ }
+ err := h.Load()
+ require.NoError(t, err)
+ w := httptest.NewRecorder()
+ h.ServeHTTP(w, r)
+
+ require.True(t, handler.Called)
+ require.False(t, encoder.Called)
})
}
diff --git a/openapi3filter/validation_handler.go b/openapi3filter/validation_handler.go
index abd03bba9..88a7eeb18 100644
--- a/openapi3filter/validation_handler.go
+++ b/openapi3filter/validation_handler.go
@@ -3,20 +3,13 @@ package openapi3filter
import (
"context"
"fmt"
+ "github.com/getkin/kin-openapi/openapi3"
"net/http"
"net/url"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers"
- legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
)
-// AuthenticationFunc allows for custom security requirement validation.
-// A non-nil error fails authentication according to https://spec.openapis.org/oas/v3.1.0#security-requirement-object
-// See ValidateSecurityRequirements
type AuthenticationFunc func(context.Context, *AuthenticationInput) error
-// NoopAuthenticationFunc is an AuthenticationFunc
func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error { return nil }
var _ AuthenticationFunc = NoopAuthenticationFunc
@@ -24,23 +17,19 @@ var _ AuthenticationFunc = NoopAuthenticationFunc
type ValidationHandler struct {
Handler http.Handler
AuthenticationFunc AuthenticationFunc
- File string
+ SwaggerFile string
ErrorEncoder ErrorEncoder
- router routers.Router
+ IgnoreServerErrors bool
+ router *Router
}
func (h *ValidationHandler) Load() error {
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromFile(h.File)
+ h.router = NewRouter()
+
+ err := h.LoadSwagger()
if err != nil {
return err
}
- if err := doc.Validate(loader.Context); err != nil {
- return err
- }
- if h.router, err = legacyrouter.NewRouter(doc); err != nil {
- return err
- }
// set defaults
if h.Handler == nil {
@@ -56,20 +45,20 @@ func (h *ValidationHandler) Load() error {
return nil
}
-//func (h *ValidationHandler) LoadSwagger() error {
-// swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile(h.SwaggerFile)
-// if err != nil {
-// return err
-// }
-// if h.IgnoreServerErrors {
-// // remove servers from the OpenAPI spec if we shouldn't validate them
-// swagger, err = h.removeServers(swagger)
-// if err != nil {
-// return err
-// }
-// }
-// return h.router.AddSwagger(swagger)
-//}
+func (h *ValidationHandler) LoadSwagger() error {
+ swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile(h.SwaggerFile)
+ if err != nil {
+ return err
+ }
+ if h.IgnoreServerErrors {
+ // remove servers from the OpenAPI spec if we shouldn't validate them
+ swagger, err = h.removeServers(swagger)
+ if err != nil {
+ return err
+ }
+ }
+ return h.router.AddSwagger(swagger)
+}
func (h *ValidationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handled := h.before(w, r); handled {
@@ -91,7 +80,8 @@ func (h *ValidationHandler) Middleware(next http.Handler) http.Handler {
}
func (h *ValidationHandler) before(w http.ResponseWriter, r *http.Request) (handled bool) {
- if err := h.validateRequest(r); err != nil {
+ err := h.validateRequest(r)
+ if err != nil {
h.ErrorEncoder(r.Context(), err, w)
return true
}
@@ -100,7 +90,7 @@ func (h *ValidationHandler) before(w http.ResponseWriter, r *http.Request) (hand
func (h *ValidationHandler) validateRequest(r *http.Request) error {
// Find route
- route, pathParams, err := h.router.FindRoute(r)
+ route, pathParams, err := h.router.FindRoute(r.Method, r.URL)
if err != nil {
return err
}
@@ -116,7 +106,8 @@ func (h *ValidationHandler) validateRequest(r *http.Request) error {
Route: route,
Options: options,
}
- if err = ValidateRequest(r.Context(), requestValidationInput); err != nil {
+ err = ValidateRequest(r.Context(), requestValidationInput)
+ if err != nil {
return err
}
@@ -127,7 +118,7 @@ func (h *ValidationHandler) validateRequest(r *http.Request) error {
//
// It also rewrites all the paths to begin with the server path, so that the paths still work.
// This assumes that all servers share the same path (e.g., all have /v1), or return an error.
-func (h *ValidationHandler) removeServers(swagger *openapi3.T) (*openapi3.T, error) {
+func (h *ValidationHandler) removeServers(swagger *openapi3.Swagger) (*openapi3.Swagger, error) {
// collect API pathPrefix path prefixes
prefixes := make(map[string]struct{}, 0) // a "set"
for _, s := range swagger.Servers {
diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go
index d3a1b45bb..6ac05b331 100644
--- a/openapi3filter/validation_test.go
+++ b/openapi3filter/validation_test.go
@@ -1,4 +1,4 @@
-package openapi3filter
+package openapi3filter_test
import (
"bytes"
@@ -14,10 +14,9 @@ import (
"strings"
"testing"
- "github.com/stretchr/testify/require"
-
"github.com/getkin/kin-openapi/openapi3"
- legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
+ "github.com/getkin/kin-openapi/openapi3filter"
+ "github.com/stretchr/testify/require"
)
type ExampleRequest struct {
@@ -46,7 +45,7 @@ func TestFilter(t *testing.T) {
complexArgSchema.Required = []string{"name", "id"}
// Declare router
- doc := &openapi3.T{
+ swagger := &openapi3.Swagger{
OpenAPI: "3.0.0",
Info: &openapi3.Info{
Title: "MyAPI",
@@ -63,10 +62,9 @@ func TestFilter(t *testing.T) {
Parameters: openapi3.Parameters{
{
Value: &openapi3.Parameter{
- In: "path",
- Name: "pathArg",
- Schema: openapi3.NewStringSchema().WithMaxLength(2).NewRef(),
- Required: true,
+ In: "path",
+ Name: "pathArg",
+ Schema: openapi3.NewStringSchema().WithMaxLength(2).NewRef(),
},
},
{
@@ -106,7 +104,7 @@ func TestFilter(t *testing.T) {
).NewRef(),
},
},
- // TODO(decode not): handle decoding "not" Schema
+ // TODO(decode not): handle decoding "not" JSON Schema
// {
// Value: &openapi3.Parameter{
// In: "query",
@@ -135,13 +133,13 @@ func TestFilter(t *testing.T) {
},
},
},
- Responses: openapi3.NewResponses(),
+ Responses: make(openapi3.Responses),
},
},
"/issue151": &openapi3.PathItem{
Get: &openapi3.Operation{
- Responses: openapi3.NewResponses(),
+ Responses: make(openapi3.Responses),
},
Parameters: openapi3.Parameters{
{
@@ -157,36 +155,32 @@ func TestFilter(t *testing.T) {
},
}
- err := doc.Validate(context.Background())
- require.NoError(t, err)
- router, err := legacyrouter.NewRouter(doc)
- require.NoError(t, err)
- expectWithDecoder := func(req ExampleRequest, resp ExampleResponse, decoder ContentParameterDecoder) error {
+ router := openapi3filter.NewRouter().WithSwagger(swagger)
+ expectWithDecoder := func(req ExampleRequest, resp ExampleResponse, decoder openapi3filter.ContentParameterDecoder) error {
t.Logf("Request: %s %s", req.Method, req.URL)
- httpReq, err := http.NewRequest(req.Method, req.URL, marshalReader(req.Body))
- require.NoError(t, err)
- httpReq.Header.Set(headerCT, req.ContentType)
+ httpReq, _ := http.NewRequest(req.Method, req.URL, marshalReader(req.Body))
+ httpReq.Header.Set("Content-Type", req.ContentType)
// Find route
- route, pathParams, err := router.FindRoute(httpReq)
+ route, pathParams, err := router.FindRoute(httpReq.Method, httpReq.URL)
require.NoError(t, err)
// Validate request
- requestValidationInput := &RequestValidationInput{
+ requestValidationInput := &openapi3filter.RequestValidationInput{
Request: httpReq,
PathParams: pathParams,
Route: route,
ParamDecoder: decoder,
}
- if err := ValidateRequest(context.Background(), requestValidationInput); err != nil {
+ if err := openapi3filter.ValidateRequest(context.TODO(), requestValidationInput); err != nil {
return err
}
t.Logf("Response: %d", resp.Status)
- responseValidationInput := &ResponseValidationInput{
+ responseValidationInput := &openapi3filter.ResponseValidationInput{
RequestValidationInput: requestValidationInput,
Status: resp.Status,
Header: http.Header{
- headerCT: []string{
+ "Content-Type": []string{
resp.ContentType,
},
},
@@ -196,23 +190,27 @@ func TestFilter(t *testing.T) {
require.NoError(t, err)
responseValidationInput.SetBodyBytes(data)
}
- err = ValidateResponse(context.Background(), responseValidationInput)
+ err = openapi3filter.ValidateResponse(context.TODO(), responseValidationInput)
require.NoError(t, err)
- return nil
+ return err
}
expect := func(req ExampleRequest, resp ExampleResponse) error {
return expectWithDecoder(req, resp, nil)
}
- resp := ExampleResponse{
+ var err error
+ var req ExampleRequest
+ var resp ExampleResponse
+ resp = ExampleResponse{
Status: 200,
}
-
// Test paths
- req := ExampleRequest{
+
+ req = ExampleRequest{
Method: "POST",
URL: "http://example.com/api/prefix/v/suffix",
}
+
err = expect(req, resp)
require.NoError(t, err)
@@ -222,7 +220,7 @@ func TestFilter(t *testing.T) {
URL: "http://example.com/api/prefix/EXCEEDS_MAX_LENGTH/suffix",
}
err = expect(req, resp)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
// Test query parameter openapi3filter
req = ExampleRequest{
@@ -237,14 +235,14 @@ func TestFilter(t *testing.T) {
URL: "http://example.com/api/prefix/v/suffix?queryArg=EXCEEDS_MAX_LENGTH",
}
err = expect(req, resp)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
req = ExampleRequest{
Method: "GET",
URL: "http://example.com/api/issue151?par2=par1_is_missing",
}
err = expect(req, resp)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
// Test query parameter openapi3filter
req = ExampleRequest{
@@ -266,51 +264,51 @@ func TestFilter(t *testing.T) {
URL: "http://example.com/api/prefix/v/suffix?queryArgAnyOf=123",
}
err = expect(req, resp)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
req = ExampleRequest{
Method: "POST",
URL: "http://example.com/api/prefix/v/suffix?queryArgOneOf=567",
}
err = expect(req, resp)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
req = ExampleRequest{
Method: "POST",
URL: "http://example.com/api/prefix/v/suffix?queryArgOneOf=2017-12-31T11:59:59",
}
err = expect(req, resp)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
req = ExampleRequest{
Method: "POST",
URL: "http://example.com/api/prefix/v/suffix?queryArgAllOf=abdfg",
}
err = expect(req, resp)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
- // TODO(decode not): handle decoding "not" Schema
+ // TODO(decode not): handle decoding "not" JSON Schema
// req = ExampleRequest{
// Method: "POST",
// URL: "http://example.com/api/prefix/v/suffix?queryArgNot=abdfg",
// }
// err = expect(req, resp)
- // require.IsType(t, &RequestError{}, err)
+ // require.IsType(t, &openapi3filter.RequestError{}, err)
- // TODO(decode not): handle decoding "not" Schema
+ // TODO(decode not): handle decoding "not" JSON Schema
// req = ExampleRequest{
// Method: "POST",
// URL: "http://example.com/api/prefix/v/suffix?queryArgNot=123",
// }
// err = expect(req, resp)
- // require.IsType(t, &RequestError{}, err)
+ // require.IsType(t, &openapi3filter.RequestError{}, err)
req = ExampleRequest{
Method: "POST",
URL: "http://example.com/api/prefix/v/suffix?queryArg=EXCEEDS_MAX_LENGTH",
}
err = expect(req, resp)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
req = ExampleRequest{
Method: "POST",
@@ -320,14 +318,14 @@ func TestFilter(t *testing.T) {
Status: 200,
}
err = expect(req, resp)
- // require.IsType(t, &ResponseError{}, err)
+ // require.IsType(t, &openapi3filter.ResponseError{}, err)
require.NoError(t, err)
// Check that content validation works. This should pass, as ID is short
// enough.
req = ExampleRequest{
Method: "POST",
- URL: `http://example.com/api/prefix/v/suffix?contentArg={"name":"bob", "id":"a"}`,
+ URL: "http://example.com/api/prefix/v/suffix?contentArg={\"name\":\"bob\", \"id\":\"a\"}",
}
err = expect(req, resp)
require.NoError(t, err)
@@ -335,10 +333,10 @@ func TestFilter(t *testing.T) {
// Now it should fail due the ID being too long
req = ExampleRequest{
Method: "POST",
- URL: `http://example.com/api/prefix/v/suffix?contentArg={"name":"bob", "id":"EXCEEDS_MAX_LENGTH"}`,
+ URL: "http://example.com/api/prefix/v/suffix?contentArg={\"name\":\"bob\", \"id\":\"EXCEEDS_MAX_LENGTH\"}",
}
err = expect(req, resp)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
// Now, repeat the above two test cases using a custom parameter decoder.
customDecoder := func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error) {
@@ -350,7 +348,7 @@ func TestFilter(t *testing.T) {
req = ExampleRequest{
Method: "POST",
- URL: `http://example.com/api/prefix/v/suffix?contentArg2={"name":"bob", "id":"a"}`,
+ URL: "http://example.com/api/prefix/v/suffix?contentArg2={\"name\":\"bob\", \"id\":\"a\"}",
}
err = expectWithDecoder(req, resp, customDecoder)
require.NoError(t, err)
@@ -358,10 +356,10 @@ func TestFilter(t *testing.T) {
// Now it should fail due the ID being too long
req = ExampleRequest{
Method: "POST",
- URL: `http://example.com/api/prefix/v/suffix?contentArg2={"name":"bob", "id":"EXCEEDS_MAX_LENGTH"}`,
+ URL: "http://example.com/api/prefix/v/suffix?contentArg2={\"name\":\"bob\", \"id\":\"EXCEEDS_MAX_LENGTH\"}",
}
err = expectWithDecoder(req, resp, customDecoder)
- require.IsType(t, &RequestError{}, err)
+ require.IsType(t, &openapi3filter.RequestError{}, err)
}
func marshalReader(value interface{}) io.ReadCloser {
@@ -405,7 +403,7 @@ func TestValidateRequestBody(t *testing.T) {
{
name: "required empty",
body: requiredReqBody,
- wantErr: &RequestError{RequestBody: requiredReqBody, Err: ErrInvalidRequired},
+ wantErr: &openapi3filter.RequestError{RequestBody: requiredReqBody, Err: openapi3filter.ErrInvalidRequired},
},
{
name: "required not empty",
@@ -436,10 +434,10 @@ func TestValidateRequestBody(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/test", tc.data)
if tc.mime != "" {
- req.Header.Set(headerCT, tc.mime)
+ req.Header.Set(http.CanonicalHeaderKey("Content-Type"), tc.mime)
}
- inp := &RequestValidationInput{Request: req}
- err := ValidateRequestBody(context.Background(), inp, tc.body)
+ inp := &openapi3filter.RequestValidationInput{Request: req}
+ err := openapi3filter.ValidateRequestBody(context.Background(), inp, tc.body)
if tc.wantErr == nil {
require.NoError(t, err)
@@ -455,11 +453,11 @@ func matchReqBodyError(want, got error) bool {
if want == got {
return true
}
- wErr, ok := want.(*RequestError)
+ wErr, ok := want.(*openapi3filter.RequestError)
if !ok {
return false
}
- gErr, ok := got.(*RequestError)
+ gErr, ok := got.(*openapi3filter.RequestError)
if !ok {
return false
}
@@ -480,7 +478,8 @@ func toJSON(v interface{}) io.Reader {
return bytes.NewReader(data)
}
-func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *testing.T) {
+// TestOperationOrSwaggerSecurity asserts that the swagger's SecurityRequirements are used if no SecurityRequirements are provided for an operation.
+func TestOperationOrSwaggerSecurity(t *testing.T) {
// Create the security schemes
securitySchemes := []ExampleSecurityScheme{
{
@@ -529,7 +528,8 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test
},
}
- doc := &openapi3.T{
+ // Create the swagger
+ swagger := &openapi3.Swagger{
OpenAPI: "3.0.0",
Info: &openapi3.Info{
Title: "MyAPI",
@@ -541,40 +541,43 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test
securitySchemes[1].Name: {},
},
},
- Components: &openapi3.Components{
+ Components: openapi3.Components{
SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{},
},
}
// Add the security schemes to the components
for _, scheme := range securitySchemes {
- doc.Components.SecuritySchemes[scheme.Name] = &openapi3.SecuritySchemeRef{
+ swagger.Components.SecuritySchemes[scheme.Name] = &openapi3.SecuritySchemeRef{
Value: scheme.Scheme,
}
}
- // Add the paths from the test cases to the spec's paths
+ // Add the paths from the test cases to the swagger's paths
for _, tc := range tc {
var securityRequirements *openapi3.SecurityRequirements = nil
if tc.schemes != nil {
- tempS := openapi3.NewSecurityRequirements()
+ tempS := make(openapi3.SecurityRequirements, 0)
for _, scheme := range *tc.schemes {
- tempS.With(openapi3.SecurityRequirement{scheme.Name: {}})
+ tempS = append(
+ tempS,
+ openapi3.SecurityRequirement{
+ scheme.Name: {},
+ },
+ )
}
- securityRequirements = tempS
+ securityRequirements = &tempS
}
- doc.Paths[tc.name] = &openapi3.PathItem{
+ swagger.Paths[tc.name] = &openapi3.PathItem{
Get: &openapi3.Operation{
Security: securityRequirements,
- Responses: openapi3.NewResponses(),
+ Responses: make(openapi3.Responses),
},
}
}
- err := doc.Validate(context.Background())
- require.NoError(t, err)
- router, err := legacyrouter.NewRouter(doc)
- require.NoError(t, err)
+ // Declare the router
+ router := openapi3filter.NewRouter().WithSwagger(swagger)
// Test each case
for _, path := range tc {
@@ -590,14 +593,15 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test
// Create the request
emptyBody := bytes.NewReader(make([]byte, 0))
- httpReq := httptest.NewRequest(http.MethodGet, path.name, emptyBody)
- route, _, err := router.FindRoute(httpReq)
+ pathURL, err := url.Parse(path.name)
+ require.NoError(t, err)
+ route, _, err := router.FindRoute(http.MethodGet, pathURL)
require.NoError(t, err)
- req := RequestValidationInput{
- Request: httpReq,
+ req := openapi3filter.RequestValidationInput{
+ Request: httptest.NewRequest(http.MethodGet, path.name, emptyBody),
Route: route,
- Options: &Options{
- AuthenticationFunc: func(ctx context.Context, input *AuthenticationInput) error {
+ Options: &openapi3filter.Options{
+ AuthenticationFunc: func(c context.Context, input *openapi3filter.AuthenticationInput) error {
if schemesValidated != nil {
if validated, ok := (*schemesValidated)[input.SecurityScheme]; ok {
if validated {
@@ -618,7 +622,7 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test
}
// Validate the request
- err = ValidateRequest(context.Background(), &req)
+ err = openapi3filter.ValidateRequest(context.TODO(), &req)
require.NoError(t, err)
for securityRequirement, validated := range *schemesValidated {
@@ -663,21 +667,22 @@ func TestAnySecurityRequirementMet(t *testing.T) {
},
}
- doc := openapi3.T{
+ // Create the swagger
+ swagger := openapi3.Swagger{
OpenAPI: "3.0.0",
Info: &openapi3.Info{
Title: "MyAPI",
Version: "0.1",
},
Paths: map[string]*openapi3.PathItem{},
- Components: &openapi3.Components{
+ Components: openapi3.Components{
SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{},
},
}
- // Add the security schemes to the spec's components
+ // Add the security schemes to the swagger's components
for schemeName := range schemes {
- doc.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{
+ swagger.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{
Value: &openapi3.SecurityScheme{
Type: "http",
Scheme: "basic",
@@ -685,27 +690,27 @@ func TestAnySecurityRequirementMet(t *testing.T) {
}
}
- // Add the paths to the spec
+ // Add the paths to the swagger
for _, tc := range tc {
// Create the security requirements from the test cases's schemes
- securityRequirements := openapi3.NewSecurityRequirements()
- for _, scheme := range tc.schemes {
- securityRequirements.With(openapi3.SecurityRequirement{scheme: {}})
+ securityRequirements := make(openapi3.SecurityRequirements, len(tc.schemes))
+ for i, scheme := range tc.schemes {
+ securityRequirements[i] = openapi3.SecurityRequirement{
+ scheme: {},
+ }
}
// Create the path with the security requirements
- doc.Paths[tc.name] = &openapi3.PathItem{
+ swagger.Paths[tc.name] = &openapi3.PathItem{
Get: &openapi3.Operation{
- Security: securityRequirements,
- Responses: openapi3.NewResponses(),
+ Security: &securityRequirements,
+ Responses: make(openapi3.Responses),
},
}
}
- err := doc.Validate(context.Background())
- require.NoError(t, err)
- router, err := legacyrouter.NewRouter(&doc)
- require.NoError(t, err)
+ // Create the router
+ router := openapi3filter.NewRouter().WithSwagger(&swagger)
// Create the authentication function
authFunc := makeAuthFunc(schemes)
@@ -714,18 +719,17 @@ func TestAnySecurityRequirementMet(t *testing.T) {
// Create the request input for the path
tcURL, err := url.Parse(tc.name)
require.NoError(t, err)
- httpReq := httptest.NewRequest(http.MethodGet, tcURL.String(), nil)
- route, _, err := router.FindRoute(httpReq)
+ route, _, err := router.FindRoute(http.MethodGet, tcURL)
require.NoError(t, err)
- req := RequestValidationInput{
+ req := openapi3filter.RequestValidationInput{
Route: route,
- Options: &Options{
+ Options: &openapi3filter.Options{
AuthenticationFunc: authFunc,
},
}
// Validate the security requirements
- err = ValidateSecurityRequirements(context.Background(), &req, *route.Operation.Security)
+ err = openapi3filter.ValidateSecurityRequirements(context.TODO(), &req, *route.Operation.Security)
// If there should have been an error
if tc.error {
@@ -760,21 +764,22 @@ func TestAllSchemesMet(t *testing.T) {
},
}
- doc := openapi3.T{
+ // Create the swagger
+ swagger := openapi3.Swagger{
OpenAPI: "3.0.0",
Info: &openapi3.Info{
Title: "MyAPI",
Version: "0.1",
},
Paths: map[string]*openapi3.PathItem{},
- Components: &openapi3.Components{
+ Components: openapi3.Components{
SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{},
},
}
- // Add the security schemes to the spec's components
+ // Add the security schemes to the swagger's components
for schemeName := range schemes {
- doc.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{
+ swagger.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{
Value: &openapi3.SecurityScheme{
Type: "http",
Scheme: "basic",
@@ -782,7 +787,7 @@ func TestAllSchemesMet(t *testing.T) {
}
}
- // Add the paths to the spec
+ // Add the paths to the swagger
for _, tc := range tc {
// Create the security requirement for the path
securityRequirement := openapi3.SecurityRequirement{}
@@ -794,20 +799,18 @@ func TestAllSchemesMet(t *testing.T) {
}
}
- doc.Paths[tc.name] = &openapi3.PathItem{
+ swagger.Paths[tc.name] = &openapi3.PathItem{
Get: &openapi3.Operation{
Security: &openapi3.SecurityRequirements{
securityRequirement,
},
- Responses: openapi3.NewResponses(),
+ Responses: make(openapi3.Responses),
},
}
}
- err := doc.Validate(context.Background())
- require.NoError(t, err)
- router, err := legacyrouter.NewRouter(&doc)
- require.NoError(t, err)
+ // Create the router from the swagger
+ router := openapi3filter.NewRouter().WithSwagger(&swagger)
// Create the authentication function
authFunc := makeAuthFunc(schemes)
@@ -816,18 +819,17 @@ func TestAllSchemesMet(t *testing.T) {
// Create the request input for the path
tcURL, err := url.Parse(tc.name)
require.NoError(t, err)
- httpReq := httptest.NewRequest(http.MethodGet, tcURL.String(), nil)
- route, _, err := router.FindRoute(httpReq)
+ route, _, err := router.FindRoute(http.MethodGet, tcURL)
require.NoError(t, err)
- req := RequestValidationInput{
+ req := openapi3filter.RequestValidationInput{
Route: route,
- Options: &Options{
+ Options: &openapi3filter.Options{
AuthenticationFunc: authFunc,
},
}
// Validate the security requirements
- err = ValidateSecurityRequirements(context.Background(), &req, *route.Operation.Security)
+ err = openapi3filter.ValidateSecurityRequirements(context.TODO(), &req, *route.Operation.Security)
// If there should have been an error
if tc.error {
@@ -841,8 +843,8 @@ func TestAllSchemesMet(t *testing.T) {
// makeAuthFunc creates an authentication function that accepts the given valid schemes.
// If an invalid or unknown scheme is encountered, an error is returned by the returned function.
// Otherwise the return value of the returned function is nil.
-func makeAuthFunc(schemes map[string]bool) func(ctx context.Context, input *AuthenticationInput) error {
- return func(ctx context.Context, input *AuthenticationInput) error {
+func makeAuthFunc(schemes map[string]bool) func(c context.Context, input *openapi3filter.AuthenticationInput) error {
+ return func(c context.Context, input *openapi3filter.AuthenticationInput) error {
// If the scheme is valid and present in the schemes
valid, present := schemes[input.SecuritySchemeName]
if valid && present {
diff --git a/openapi3filter/zip_file_upload_test.go b/openapi3filter/zip_file_upload_test.go
deleted file mode 100644
index 69c6419cc..000000000
--- a/openapi3filter/zip_file_upload_test.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package openapi3filter_test
-
-import (
- "bytes"
- "context"
- "io"
- "mime/multipart"
- "net/http"
- "net/textproto"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/gorillamux"
-)
-
-func TestValidateZipFileUpload(t *testing.T) {
- const spec = `
-openapi: 3.0.0
-info:
- title: 'Validator'
- version: 0.0.1
-paths:
- /test:
- post:
- requestBody:
- required: true
- content:
- multipart/form-data:
- schema:
- type: object
- required:
- - file
- properties:
- file:
- type: string
- format: binary
- responses:
- '200':
- description: Created
-`
-
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- require.NoError(t, err)
-
- err = doc.Validate(loader.Context)
- require.NoError(t, err)
-
- router, err := gorillamux.NewRouter(doc)
- require.NoError(t, err)
-
- tests := []struct {
- zipData []byte
- wantErr bool
- }{
- {
- []byte{
- 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x7d, 0x23, 0x56, 0xcd, 0xfd, 0x67, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x09, 0x00, 0x1c, 0x00, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x74, 0x78, 0x74, 0x55, 0x54, 0x09, 0x00, 0x03, 0xac, 0xce, 0xb3, 0x63, 0xaf, 0xce, 0xb3, 0x63, 0x75, 0x78, 0x0b, 0x00, 0x01, 0x04, 0xf7, 0x01, 0x00, 0x00, 0x04, 0x14, 0x00, 0x00, 0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x0a, 0x50, 0x4b, 0x01, 0x02, 0x1e, 0x03, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x7d, 0x23, 0x56, 0xcd, 0xfd, 0x67, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x09, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa4, 0x81, 0x00, 0x00, 0x00, 0x00, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x74, 0x78, 0x74, 0x55, 0x54, 0x05, 0x00, 0x03, 0xac, 0xce, 0xb3, 0x63, 0x75, 0x78, 0x0b, 0x00, 0x01, 0x04, 0xf7, 0x01, 0x00, 0x00, 0x04, 0x14, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x4f, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00,
- },
- false,
- },
- {
- []byte{
- 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- }, // No entry
- true,
- },
- }
- for _, tt := range tests {
- body := &bytes.Buffer{}
- writer := multipart.NewWriter(body)
-
- { // Add file data
- h := make(textproto.MIMEHeader)
- h.Set("Content-Disposition", `form-data; name="file"; filename="hello.zip"`)
- h.Set("Content-Type", "application/zip")
-
- fw, err := writer.CreatePart(h)
- require.NoError(t, err)
- _, err = io.Copy(fw, bytes.NewReader(tt.zipData))
-
- require.NoError(t, err)
- }
-
- writer.Close()
-
- req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes()))
- require.NoError(t, err)
-
- req.Header.Set("Content-Type", writer.FormDataContentType())
-
- route, pathParams, err := router.FindRoute(req)
- require.NoError(t, err)
-
- if err = openapi3filter.ValidateRequestBody(
- context.Background(),
- &openapi3filter.RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- },
- route.Operation.RequestBody.Value,
- ); err != nil {
- if !tt.wantErr {
- t.Errorf("got %v", err)
- }
- continue
- }
- if tt.wantErr {
- t.Errorf("want err")
- }
- }
-}
diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go
index eccabd85d..4a80405ba 100644
--- a/openapi3gen/openapi3gen.go
+++ b/openapi3gen/openapi3gen.go
@@ -1,183 +1,94 @@
-// Package openapi3gen generates OpenAPIv3 JSON schemas from Go types.
+// Package openapi3gen generates OpenAPI 3 schemas for Go types.
package openapi3gen
import (
"encoding/json"
- "fmt"
- "math"
"reflect"
"strings"
"time"
+ "github.com/getkin/kin-openapi/jsoninfo"
"github.com/getkin/kin-openapi/openapi3"
)
// CycleError indicates that a type graph has one or more possible cycles.
type CycleError struct{}
-func (err *CycleError) Error() string { return "detected cycle" }
-
-// ExcludeSchemaSentinel indicates that the schema for a specific field should not be included in the final output.
-type ExcludeSchemaSentinel struct{}
-
-func (err *ExcludeSchemaSentinel) Error() string { return "schema excluded" }
-
-// Option allows tweaking SchemaRef generation
-type Option func(*generatorOpt)
-
-// SchemaCustomizerFn is a callback function, allowing
-// the OpenAPI schema definition to be updated with additional
-// properties during the generation process, based on the
-// name of the field, the Go type, and the struct tags.
-// name will be "_root" for the top level object, and tag will be "".
-// A SchemaCustomizerFn can return an ExcludeSchemaSentinel error to
-// indicate that the schema for this field should not be included in
-// the final output
-type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error
-
-type generatorOpt struct {
- useAllExportedFields bool
- throwErrorOnCycle bool
- schemaCustomizer SchemaCustomizerFn
-}
-
-// UseAllExportedFields changes the default behavior of only
-// generating schemas for struct fields with a JSON tag.
-func UseAllExportedFields() Option {
- return func(x *generatorOpt) { x.useAllExportedFields = true }
-}
-
-// ThrowErrorOnCycle changes the default behavior of creating cycle
-// refs to instead error if a cycle is detected.
-func ThrowErrorOnCycle() Option {
- return func(x *generatorOpt) { x.throwErrorOnCycle = true }
+func (err *CycleError) Error() string {
+ return "Detected JSON cycle"
}
-// SchemaCustomizer allows customization of the schema that is generated
-// for a field, for example to support an additional tagging scheme
-func SchemaCustomizer(sc SchemaCustomizerFn) Option {
- return func(x *generatorOpt) { x.schemaCustomizer = sc }
-}
-
-// NewSchemaRefForValue is a shortcut for NewGenerator(...).NewSchemaRefForValue(...)
-func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) {
- g := NewGenerator(opts...)
- return g.NewSchemaRefForValue(value, schemas)
+func NewSchemaRefForValue(value interface{}) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) {
+ g := NewGenerator()
+ ref, err := g.GenerateSchemaRef(reflect.TypeOf(value))
+ for ref := range g.SchemaRefs {
+ ref.Ref = ""
+ }
+ return ref, g.SchemaRefs, err
}
type Generator struct {
- opts generatorOpt
-
Types map[reflect.Type]*openapi3.SchemaRef
// SchemaRefs contains all references and their counts.
// If count is 1, it's not ne
// An OpenAPI identifier has been assigned to each.
SchemaRefs map[*openapi3.SchemaRef]int
-
- // componentSchemaRefs is a set of schemas that must be defined in the components to avoid cycles
- componentSchemaRefs map[string]struct{}
}
-func NewGenerator(opts ...Option) *Generator {
- gOpt := &generatorOpt{}
- for _, f := range opts {
- f(gOpt)
- }
+func NewGenerator() *Generator {
return &Generator{
- Types: make(map[reflect.Type]*openapi3.SchemaRef),
- SchemaRefs: make(map[*openapi3.SchemaRef]int),
- componentSchemaRefs: make(map[string]struct{}),
- opts: *gOpt,
+ Types: make(map[reflect.Type]*openapi3.SchemaRef),
+ SchemaRefs: make(map[*openapi3.SchemaRef]int),
}
}
func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, error) {
- //check generatorOpt consistency here
- return g.generateSchemaRefFor(nil, t, "_root", "")
-}
-
-// NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef, and updates a supplied map with any dependent component schemas if they lead to cycles
-func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas) (*openapi3.SchemaRef, error) {
- ref, err := g.GenerateSchemaRef(reflect.TypeOf(value))
- if err != nil {
- return nil, err
- }
- for ref := range g.SchemaRefs {
- if _, ok := g.componentSchemaRefs[ref.Ref]; ok && schemas != nil {
- schemas[ref.Ref] = &openapi3.SchemaRef{
- Value: ref.Value,
- }
- }
- if strings.HasPrefix(ref.Ref, "#/components/schemas/") {
- ref.Value = nil
- } else {
- ref.Ref = ""
- }
- }
- return ref, nil
+ return g.generateSchemaRefFor(nil, t)
}
-func (g *Generator) generateSchemaRefFor(parents []*theTypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) {
- if ref := g.Types[t]; ref != nil && g.opts.schemaCustomizer == nil {
+func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) {
+ ref := g.Types[t]
+ if ref != nil {
g.SchemaRefs[ref]++
return ref, nil
}
- ref, err := g.generateWithoutSaving(parents, t, name, tag)
- if _, ok := err.(*ExcludeSchemaSentinel); ok {
- // This schema should not be included in the final output
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
+ ref, err := g.generateWithoutSaving(parents, t)
if ref != nil {
g.Types[t] = ref
g.SchemaRefs[ref]++
}
- return ref, nil
+ return ref, err
}
-func getStructField(t reflect.Type, fieldInfo theFieldInfo) reflect.StructField {
- var ff reflect.StructField
- // fieldInfo.Index is an array of indexes starting from the root of the type
- for i := 0; i < len(fieldInfo.Index); i++ {
- ff = t.Field(fieldInfo.Index[i])
- t = ff.Type
- for t.Kind() == reflect.Ptr {
- t = t.Elem()
- }
- }
- return ff
-}
-
-func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) {
- typeInfo := getTypeInfo(t)
+func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) {
+ // Get TypeInfo
+ typeInfo := jsoninfo.GetTypeInfo(t)
for _, parent := range parents {
if parent == typeInfo {
return nil, &CycleError{}
}
}
+ // Doesn't exist.
+ // Create the schema.
if cap(parents) == 0 {
- parents = make([]*theTypeInfo, 0, 4)
+ parents = make([]*jsoninfo.TypeInfo, 0, 4)
}
parents = append(parents, typeInfo)
+ // Ignore pointers
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
+ // Create instance
if strings.HasSuffix(t.Name(), "Ref") {
_, a := t.FieldByName("Ref")
v, b := t.FieldByName("Value")
if a && b {
- vs, err := g.generateSchemaRefFor(parents, v.Type, name, tag)
+ vs, err := g.generateSchemaRefFor(parents, v.Type)
if err != nil {
- if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle {
- g.SchemaRefs[vs]++
- return vs, nil
- }
return nil, err
}
refSchemaRef := RefSchemaRef
@@ -193,57 +104,23 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
}
}
+ // Allocate schema
schema := &openapi3.Schema{}
switch t.Kind() {
case reflect.Func, reflect.Chan:
- return nil, nil // ignore
-
+ return nil, nil
case reflect.Bool:
schema.Type = "boolean"
- case reflect.Int:
- schema.Type = "integer"
- case reflect.Int8:
- schema.Type = "integer"
- schema.Min = &minInt8
- schema.Max = &maxInt8
- case reflect.Int16:
- schema.Type = "integer"
- schema.Min = &minInt16
- schema.Max = &maxInt16
- case reflect.Int32:
- schema.Type = "integer"
- schema.Format = "int32"
- case reflect.Int64:
+ case reflect.Int,
+ reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+ reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
schema.Type = "integer"
schema.Format = "int64"
- case reflect.Uint:
- schema.Type = "integer"
- schema.Min = &zeroInt
- case reflect.Uint8:
- schema.Type = "integer"
- schema.Min = &zeroInt
- schema.Max = &maxUint8
- case reflect.Uint16:
- schema.Type = "integer"
- schema.Min = &zeroInt
- schema.Max = &maxUint16
- case reflect.Uint32:
- schema.Type = "integer"
- schema.Min = &zeroInt
- schema.Max = &maxUint32
- case reflect.Uint64:
- schema.Type = "integer"
- schema.Min = &zeroInt
- schema.Max = &maxUint64
- case reflect.Float32:
- schema.Type = "number"
- schema.Format = "float"
- case reflect.Float64:
+ case reflect.Float32, reflect.Float64:
schema.Type = "number"
- schema.Format = "double"
case reflect.String:
schema.Type = "string"
@@ -251,19 +128,17 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
case reflect.Slice:
if t.Elem().Kind() == reflect.Uint8 {
if t == rawMessageType {
- return &openapi3.SchemaRef{Value: schema}, nil
+ return &openapi3.SchemaRef{
+ Value: schema,
+ }, nil
}
schema.Type = "string"
schema.Format = "byte"
} else {
schema.Type = "array"
- items, err := g.generateSchemaRefFor(parents, t.Elem(), name, tag)
+ items, err := g.generateSchemaRefFor(parents, t.Elem())
if err != nil {
- if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle {
- items = g.generateCycleSchemaRef(t.Elem(), schema)
- } else {
- return nil, err
- }
+ return nil, err
}
if items != nil {
g.SchemaRefs[items]++
@@ -273,17 +148,13 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
case reflect.Map:
schema.Type = "object"
- additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem(), name, tag)
+ additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem())
if err != nil {
- if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle {
- additionalProperties = g.generateCycleSchemaRef(t.Elem(), schema)
- } else {
- return nil, err
- }
+ return nil, err
}
if additionalProperties != nil {
g.SchemaRefs[additionalProperties]++
- schema.AdditionalProperties = openapi3.AdditionalProperties{Schema: additionalProperties}
+ schema.AdditionalProperties = additionalProperties
}
case reflect.Struct:
@@ -292,53 +163,17 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
schema.Format = "date-time"
} else {
for _, fieldInfo := range typeInfo.Fields {
- // Only fields with JSON tag are considered (by default)
- if !fieldInfo.HasJSONTag && !g.opts.useAllExportedFields {
+ // Only fields with JSON tag are considered
+ if !fieldInfo.HasJSONTag {
continue
}
- // If asked, try to use yaml tag
- fieldName, fType := fieldInfo.JSONName, fieldInfo.Type
- if !fieldInfo.HasJSONTag && g.opts.useAllExportedFields {
- // Handle anonymous fields/embedded structs
- if t.Field(fieldInfo.Index[0]).Anonymous {
- ref, err := g.generateSchemaRefFor(parents, fType, fieldName, tag)
- if err != nil {
- if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle {
- ref = g.generateCycleSchemaRef(fType, schema)
- } else {
- return nil, err
- }
- }
- if ref != nil {
- g.SchemaRefs[ref]++
- schema.WithPropertyRef(fieldName, ref)
- }
- } else {
- ff := getStructField(t, fieldInfo)
- if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" {
- fieldName, fType = tag, ff.Type
- }
- }
- }
-
- // extract the field tag if we have a customizer
- var fieldTag reflect.StructTag
- if g.opts.schemaCustomizer != nil {
- ff := getStructField(t, fieldInfo)
- fieldTag = ff.Tag
- }
-
- ref, err := g.generateSchemaRefFor(parents, fType, fieldName, fieldTag)
+ ref, err := g.generateSchemaRefFor(parents, fieldInfo.Type)
if err != nil {
- if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle {
- ref = g.generateCycleSchemaRef(fType, schema)
- } else {
- return nil, err
- }
+ return nil, err
}
if ref != nil {
g.SchemaRefs[ref]++
- schema.WithPropertyRef(fieldName, ref)
+ schema.WithPropertyRef(fieldInfo.JSONName, ref)
}
}
@@ -348,55 +183,13 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
}
}
}
-
- if g.opts.schemaCustomizer != nil {
- if err := g.opts.schemaCustomizer(name, t, tag, schema); err != nil {
- return nil, err
- }
- }
-
return openapi3.NewSchemaRef(t.Name(), schema), nil
}
-func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef {
- var typeName string
- switch t.Kind() {
- case reflect.Ptr:
- return g.generateCycleSchemaRef(t.Elem(), schema)
- case reflect.Slice:
- ref := g.generateCycleSchemaRef(t.Elem(), schema)
- sliceSchema := openapi3.NewSchema()
- sliceSchema.Type = "array"
- sliceSchema.Items = ref
- return openapi3.NewSchemaRef("", sliceSchema)
- case reflect.Map:
- ref := g.generateCycleSchemaRef(t.Elem(), schema)
- mapSchema := openapi3.NewSchema()
- mapSchema.Type = "object"
- mapSchema.AdditionalProperties = openapi3.AdditionalProperties{Schema: ref}
- return openapi3.NewSchemaRef("", mapSchema)
- default:
- typeName = t.Name()
- }
-
- g.componentSchemaRefs[typeName] = struct{}{}
- return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema)
-}
-
var RefSchemaRef = openapi3.NewSchemaRef("Ref",
openapi3.NewObjectSchema().WithProperty("$ref", openapi3.NewStringSchema().WithMinLength(1)))
var (
timeType = reflect.TypeOf(time.Time{})
rawMessageType = reflect.TypeOf(json.RawMessage{})
-
- zeroInt = float64(0)
- maxInt8 = float64(math.MaxInt8)
- minInt8 = float64(math.MinInt8)
- maxInt16 = float64(math.MaxInt16)
- minInt16 = float64(math.MinInt16)
- maxUint8 = float64(math.MaxUint8)
- maxUint16 = float64(math.MaxUint16)
- maxUint32 = float64(math.MaxUint32)
- maxUint64 = float64(math.MaxUint64)
)
diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go
index 9a143e415..d94cfce9f 100644
--- a/openapi3gen/openapi3gen_test.go
+++ b/openapi3gen/openapi3gen_test.go
@@ -2,582 +2,111 @@ package openapi3gen_test
import (
"encoding/json"
- "errors"
- "fmt"
- "reflect"
- "strconv"
- "strings"
"testing"
"time"
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3gen"
+ "github.com/stretchr/testify/require"
)
-func ExampleGenerator_SchemaRefs() {
- type SomeOtherType string
- type Embedded struct {
- Z string `json:"z"`
- }
- type Embedded2 struct {
- A string `json:"a"`
- }
- type SomeStruct struct {
- Bool bool `json:"bool"`
- Int int `json:"int"`
- Int64 int64 `json:"int64"`
- Float64 float64 `json:"float64"`
- String string `json:"string"`
- Bytes []byte `json:"bytes"`
- JSON json.RawMessage `json:"json"`
- Time time.Time `json:"time"`
- Slice []SomeOtherType `json:"slice"`
- Map map[string]*SomeOtherType `json:"map"`
-
- Struct struct {
- X string `json:"x"`
- } `json:"struct"`
-
- EmptyStruct struct {
- Y string
- } `json:"structWithoutFields"`
-
- Embedded `json:"embedded"`
-
- Embedded2
-
- Ptr *SomeOtherType `json:"ptr"`
- }
-
- g := openapi3gen.NewGenerator()
- schemaRef, err := g.NewSchemaRefForValue(&SomeStruct{}, nil)
- if err != nil {
- panic(err)
- }
-
- fmt.Printf("g.SchemaRefs: %d\n", len(g.SchemaRefs))
- var data []byte
- if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil {
- panic(err)
- }
- fmt.Printf("schemaRef: %s\n", data)
- // Output:
- // g.SchemaRefs: 16
- // schemaRef: {
- // "properties": {
- // "a": {
- // "type": "string"
- // },
- // "bool": {
- // "type": "boolean"
- // },
- // "bytes": {
- // "format": "byte",
- // "type": "string"
- // },
- // "embedded": {
- // "properties": {
- // "z": {
- // "type": "string"
- // }
- // },
- // "type": "object"
- // },
- // "float64": {
- // "format": "double",
- // "type": "number"
- // },
- // "int": {
- // "type": "integer"
- // },
- // "int64": {
- // "format": "int64",
- // "type": "integer"
- // },
- // "json": {},
- // "map": {
- // "additionalProperties": {
- // "type": "string"
- // },
- // "type": "object"
- // },
- // "ptr": {
- // "type": "string"
- // },
- // "slice": {
- // "items": {
- // "type": "string"
- // },
- // "type": "array"
- // },
- // "string": {
- // "type": "string"
- // },
- // "struct": {
- // "properties": {
- // "x": {
- // "type": "string"
- // }
- // },
- // "type": "object"
- // },
- // "structWithoutFields": {},
- // "time": {
- // "format": "date-time",
- // "type": "string"
- // }
- // },
- // "type": "object"
- // }
-}
-
-func ExampleThrowErrorOnCycle() {
- type CyclicType0 struct {
- CyclicField *struct {
- CyclicField *CyclicType0 `json:"b"`
- } `json:"a"`
- }
-
- schemas := make(openapi3.Schemas)
- schemaRef, err := openapi3gen.NewSchemaRefForValue(&CyclicType0{}, schemas, openapi3gen.ThrowErrorOnCycle())
- if schemaRef != nil || err == nil {
- panic(`With option ThrowErrorOnCycle, an error is returned when a schema reference cycle is found`)
- }
- if _, ok := err.(*openapi3gen.CycleError); !ok {
- panic(`With option ThrowErrorOnCycle, an error of type CycleError is returned`)
- }
- if len(schemas) != 0 {
- panic(`No references should have been collected at this point`)
- }
-
- if schemaRef, err = openapi3gen.NewSchemaRefForValue(&CyclicType0{}, schemas); err != nil {
- panic(err)
- }
-
- var data []byte
- if data, err = json.MarshalIndent(schemaRef, "", " "); err != nil {
- panic(err)
- }
- fmt.Printf("schemaRef: %s\n", data)
- if data, err = json.MarshalIndent(schemas, "", " "); err != nil {
- panic(err)
- }
- fmt.Printf("schemas: %s\n", data)
- // Output:
- // schemaRef: {
- // "properties": {
- // "a": {
- // "properties": {
- // "b": {
- // "$ref": "#/components/schemas/CyclicType0"
- // }
- // },
- // "type": "object"
- // }
- // },
- // "type": "object"
- // }
- // schemas: {
- // "CyclicType0": {
- // "properties": {
- // "a": {
- // "properties": {
- // "b": {
- // "$ref": "#/components/schemas/CyclicType0"
- // }
- // },
- // "type": "object"
- // }
- // },
- // "type": "object"
- // }
- // }
-}
-
-func TestExportedNonTagged(t *testing.T) {
- type Bla struct {
- A string
- Another string `json:"another"`
- yetAnother string // unused because unexported
- EvenAYaml string `yaml:"even_a_yaml"`
- }
-
- schemaRef, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields())
- require.NoError(t, err)
- require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{
- Type: "object",
- Properties: map[string]*openapi3.SchemaRef{
- "A": {Value: &openapi3.Schema{Type: "string"}},
- "another": {Value: &openapi3.Schema{Type: "string"}},
- "even_a_yaml": {Value: &openapi3.Schema{Type: "string"}},
- }}}, schemaRef)
-}
-
-func ExampleUseAllExportedFields() {
- type UnsignedIntStruct struct {
- UnsignedInt uint `json:"uint"`
- }
-
- schemaRef, err := openapi3gen.NewSchemaRefForValue(&UnsignedIntStruct{}, nil, openapi3gen.UseAllExportedFields())
- if err != nil {
- panic(err)
- }
-
- var data []byte
- if data, err = json.MarshalIndent(schemaRef, "", " "); err != nil {
- panic(err)
- }
- fmt.Printf("schemaRef: %s\n", data)
- // Output:
- // schemaRef: {
- // "properties": {
- // "uint": {
- // "minimum": 0,
- // "type": "integer"
- // }
- // },
- // "type": "object"
- // }
-}
-
-func ExampleGenerator_GenerateSchemaRef() {
- type EmbeddedStruct struct {
- ID string
- }
-
- type ContainerStruct struct {
- Name string
- EmbeddedStruct
- }
-
- instance := &ContainerStruct{
- Name: "Container",
- EmbeddedStruct: EmbeddedStruct{
- ID: "Embedded",
- },
- }
-
- generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields())
-
- schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance))
- if err != nil {
- panic(err)
- }
-
- var data []byte
- if data, err = json.MarshalIndent(schemaRef.Value.Properties["Name"].Value, "", " "); err != nil {
- panic(err)
- }
- fmt.Printf(`schemaRef.Value.Properties["Name"].Value: %s`, data)
- fmt.Println()
- if data, err = json.MarshalIndent(schemaRef.Value.Properties["ID"].Value, "", " "); err != nil {
- panic(err)
- }
- fmt.Printf(`schemaRef.Value.Properties["ID"].Value: %s`, data)
- fmt.Println()
- // Output:
- // schemaRef.Value.Properties["Name"].Value: {
- // "type": "string"
- // }
- // schemaRef.Value.Properties["ID"].Value: {
- // "type": "string"
- // }
-}
-
-func TestEmbeddedPointerStructs(t *testing.T) {
- type EmbeddedStruct struct {
- ID string
- }
-
- type ContainerStruct struct {
- Name string
- *EmbeddedStruct
- }
-
- instance := &ContainerStruct{
- Name: "Container",
- EmbeddedStruct: &EmbeddedStruct{
- ID: "Embedded",
- },
- }
-
- generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields())
-
- schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance))
- require.NoError(t, err)
-
- var ok bool
- _, ok = schemaRef.Value.Properties["Name"]
- require.Equal(t, true, ok)
-
- _, ok = schemaRef.Value.Properties["ID"]
- require.Equal(t, true, ok)
+type CyclicType0 struct {
+ CyclicField *CyclicType1 `json:"a"`
}
-
-// See: https://github.com/getkin/kin-openapi/issues/500
-func TestEmbeddedPointerStructsWithSchemaCustomizer(t *testing.T) {
- type EmbeddedStruct struct {
- ID string
- }
-
- type ContainerStruct struct {
- Name string
- *EmbeddedStruct
- }
-
- instance := &ContainerStruct{
- Name: "Container",
- EmbeddedStruct: &EmbeddedStruct{
- ID: "Embedded",
- },
- }
-
- customizerFn := func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error {
- return nil
- }
- customizerOpt := openapi3gen.SchemaCustomizer(customizerFn)
-
- generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields(), customizerOpt)
-
- schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance))
- require.NoError(t, err)
-
- var ok bool
- _, ok = schemaRef.Value.Properties["Name"]
- require.Equal(t, true, ok)
-
- _, ok = schemaRef.Value.Properties["ID"]
- require.Equal(t, true, ok)
+type CyclicType1 struct {
+ CyclicField *CyclicType0 `json:"b"`
}
-func TestCyclicReferences(t *testing.T) {
- type ObjectDiff struct {
- FieldCycle *ObjectDiff
- SliceCycle []*ObjectDiff
- MapCycle map[*ObjectDiff]*ObjectDiff
- }
-
- instance := &ObjectDiff{
- FieldCycle: nil,
- SliceCycle: nil,
- MapCycle: nil,
- }
-
- generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields())
-
- schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance))
- require.NoError(t, err)
-
- require.NotNil(t, schemaRef.Value.Properties["FieldCycle"])
- require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["FieldCycle"].Ref)
-
- require.NotNil(t, schemaRef.Value.Properties["SliceCycle"])
- require.Equal(t, "array", schemaRef.Value.Properties["SliceCycle"].Value.Type)
- require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["SliceCycle"].Value.Items.Ref)
-
- require.NotNil(t, schemaRef.Value.Properties["MapCycle"])
- require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type)
- require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Schema.Ref)
-}
-
-func ExampleSchemaCustomizer() {
- type NestedInnerBla struct {
- Enum1Field string `json:"enum1" myenumtag:"a,b"`
- }
-
- type InnerBla struct {
- UntaggedStringField string
- AnonStruct struct {
- InnerFieldWithoutTag int
- InnerFieldWithTag int `mymintag:"-1" mymaxtag:"50"`
- NestedInnerBla
- }
- Enum2Field string `json:"enum2" myenumtag:"c,d"`
- }
-
- type Bla struct {
- InnerBla
- EnumField3 string `json:"enum3" myenumtag:"e,f"`
- }
-
- customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error {
- if tag.Get("mymintag") != "" {
- minVal, err := strconv.ParseFloat(tag.Get("mymintag"), 64)
- if err != nil {
- return err
- }
- schema.Min = &minVal
- }
- if tag.Get("mymaxtag") != "" {
- maxVal, err := strconv.ParseFloat(tag.Get("mymaxtag"), 64)
- if err != nil {
- return err
- }
- schema.Max = &maxVal
- }
- if tag.Get("myenumtag") != "" {
- for _, s := range strings.Split(tag.Get("myenumtag"), ",") {
- schema.Enum = append(schema.Enum, s)
- }
- }
- return nil
- })
-
- schemaRef, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer)
- if err != nil {
- panic(err)
- }
-
- var data []byte
- if data, err = json.MarshalIndent(schemaRef, "", " "); err != nil {
- panic(err)
- }
- fmt.Printf("schemaRef: %s\n", data)
- // Output:
- // schemaRef: {
- // "properties": {
- // "AnonStruct": {
- // "properties": {
- // "InnerFieldWithTag": {
- // "maximum": 50,
- // "minimum": -1,
- // "type": "integer"
- // },
- // "InnerFieldWithoutTag": {
- // "type": "integer"
- // },
- // "enum1": {
- // "enum": [
- // "a",
- // "b"
- // ],
- // "type": "string"
- // }
- // },
- // "type": "object"
- // },
- // "UntaggedStringField": {
- // "type": "string"
- // },
- // "enum2": {
- // "enum": [
- // "c",
- // "d"
- // ],
- // "type": "string"
- // },
- // "enum3": {
- // "enum": [
- // "e",
- // "f"
- // ],
- // "type": "string"
- // }
- // },
- // "type": "object"
- // }
-}
-
-func TestSchemaCustomizerError(t *testing.T) {
- customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error {
- return errors.New("test error")
- })
-
- type Bla struct{}
- _, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer)
- require.EqualError(t, err, "test error")
+func TestCyclic(t *testing.T) {
+ schema, refsMap, err := openapi3gen.NewSchemaRefForValue(&CyclicType0{})
+ require.IsType(t, &openapi3gen.CycleError{}, err)
+ require.Nil(t, schema)
+ require.Empty(t, refsMap)
}
-func TestSchemaCustomizerExcludeSchema(t *testing.T) {
- type Bla struct {
- Str string
+func TestSimple(t *testing.T) {
+ type ExampleChild string
+ type Example struct {
+ Bool bool `json:"bool"`
+ Int int `json:"int"`
+ Int64 int64 `json:"int64"`
+ Float64 float64 `json:"float64"`
+ String string `json:"string"`
+ Bytes []byte `json:"bytes"`
+ JSON json.RawMessage `json:"json"`
+ Time time.Time `json:"time"`
+ Slice []*ExampleChild `json:"slice"`
+ Map map[string]*ExampleChild `json:"map"`
+ Struct struct {
+ X string `json:"x"`
+ } `json:"struct"`
+ EmptyStruct struct {
+ X string
+ } `json:"structWithoutFields"`
+ Ptr *ExampleChild `json:"ptr"`
}
- customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error {
- return nil
- })
- schema, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer)
+ schema, refsMap, err := openapi3gen.NewSchemaRefForValue(&Example{})
require.NoError(t, err)
- require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{
- Type: "object",
- Properties: map[string]*openapi3.SchemaRef{
- "Str": {Value: &openapi3.Schema{Type: "string"}},
- }}}, schema)
-
- customizer = openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error {
- return &openapi3gen.ExcludeSchemaSentinel{}
- })
- schema, err = openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer)
+ require.Len(t, refsMap, 14)
+ data, err := json.Marshal(schema)
require.NoError(t, err)
- require.Nil(t, schema)
+ require.JSONEq(t, expectedSimple, string(data))
}
-func ExampleNewSchemaRefForValue_recursive() {
- type RecursiveType struct {
- Field1 string `json:"field1"`
- Field2 string `json:"field2"`
- Field3 string `json:"field3"`
- Components []*RecursiveType `json:"children,omitempty"`
- }
-
- schemas := make(openapi3.Schemas)
- schemaRef, err := openapi3gen.NewSchemaRefForValue(&RecursiveType{}, schemas)
- if err != nil {
- panic(err)
- }
-
- var data []byte
- if data, err = json.MarshalIndent(&schemas, "", " "); err != nil {
- panic(err)
- }
- fmt.Printf("schemas: %s\n", data)
- if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil {
- panic(err)
- }
- fmt.Printf("schemaRef: %s\n", data)
- // Output:
- // schemas: {
- // "RecursiveType": {
- // "properties": {
- // "children": {
- // "items": {
- // "$ref": "#/components/schemas/RecursiveType"
- // },
- // "type": "array"
- // },
- // "field1": {
- // "type": "string"
- // },
- // "field2": {
- // "type": "string"
- // },
- // "field3": {
- // "type": "string"
- // }
- // },
- // "type": "object"
- // }
- // }
- // schemaRef: {
- // "properties": {
- // "children": {
- // "items": {
- // "$ref": "#/components/schemas/RecursiveType"
- // },
- // "type": "array"
- // },
- // "field1": {
- // "type": "string"
- // },
- // "field2": {
- // "type": "string"
- // },
- // "field3": {
- // "type": "string"
- // }
- // },
- // "type": "object"
- // }
+const expectedSimple = `
+{
+ "type": "object",
+ "properties": {
+ "bool": {
+ "type": "boolean"
+ },
+ "int": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "int64": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "float64": {
+ "type": "number"
+ },
+ "time": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "string": {
+ "type": "string"
+ },
+ "bytes": {
+ "type": "string",
+ "format": "byte"
+ },
+ "json": {},
+ "slice": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "map": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "struct": {
+ "type": "object",
+ "properties": {
+ "x": {
+ "type": "string"
+ }
+ }
+ },
+ "structWithoutFields": {},
+ "ptr": {
+ "type": "string"
+ }
+ }
}
+`
diff --git a/openapi3gen/simple_test.go b/openapi3gen/simple_test.go
deleted file mode 100644
index 99e94ae12..000000000
--- a/openapi3gen/simple_test.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package openapi3gen_test
-
-import (
- "encoding/json"
- "fmt"
- "time"
-
- "github.com/getkin/kin-openapi/openapi3gen"
-)
-
-type (
- SomeStruct struct {
- Bool bool `json:"bool"`
- Int int `json:"int"`
- Int64 int64 `json:"int64"`
- Float64 float64 `json:"float64"`
- String string `json:"string"`
- Bytes []byte `json:"bytes"`
- JSON json.RawMessage `json:"json"`
- Time time.Time `json:"time"`
- Slice []SomeOtherType `json:"slice"`
- Map map[string]*SomeOtherType `json:"map"`
-
- Struct struct {
- X string `json:"x"`
- } `json:"struct"`
-
- EmptyStruct struct {
- Y string
- } `json:"structWithoutFields"`
-
- Ptr *SomeOtherType `json:"ptr"`
- }
-
- SomeOtherType string
-)
-
-func Example() {
- schemaRef, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{}, nil)
- if err != nil {
- panic(err)
- }
-
- data, err := json.MarshalIndent(schemaRef, "", " ")
- if err != nil {
- panic(err)
- }
- fmt.Printf("%s\n", data)
- // Output:
- // {
- // "properties": {
- // "bool": {
- // "type": "boolean"
- // },
- // "bytes": {
- // "format": "byte",
- // "type": "string"
- // },
- // "float64": {
- // "format": "double",
- // "type": "number"
- // },
- // "int": {
- // "type": "integer"
- // },
- // "int64": {
- // "format": "int64",
- // "type": "integer"
- // },
- // "json": {},
- // "map": {
- // "additionalProperties": {
- // "type": "string"
- // },
- // "type": "object"
- // },
- // "ptr": {
- // "type": "string"
- // },
- // "slice": {
- // "items": {
- // "type": "string"
- // },
- // "type": "array"
- // },
- // "string": {
- // "type": "string"
- // },
- // "struct": {
- // "properties": {
- // "x": {
- // "type": "string"
- // }
- // },
- // "type": "object"
- // },
- // "structWithoutFields": {},
- // "time": {
- // "format": "date-time",
- // "type": "string"
- // }
- // },
- // "type": "object"
- // }
-}
diff --git a/openapi3gen/type_info.go b/openapi3gen/type_info.go
deleted file mode 100644
index 062882b4c..000000000
--- a/openapi3gen/type_info.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package openapi3gen
-
-import (
- "reflect"
- "sort"
- "sync"
-)
-
-var (
- typeInfos = map[reflect.Type]*theTypeInfo{}
- typeInfosMutex sync.RWMutex
-)
-
-// theTypeInfo contains information about JSON serialization of a type
-type theTypeInfo struct {
- Type reflect.Type
- Fields []theFieldInfo
-}
-
-// getTypeInfo returns theTypeInfo for the given type.
-func getTypeInfo(t reflect.Type) *theTypeInfo {
- for t.Kind() == reflect.Ptr {
- t = t.Elem()
- }
- typeInfosMutex.RLock()
- typeInfo, exists := typeInfos[t]
- typeInfosMutex.RUnlock()
- if exists {
- return typeInfo
- }
- if t.Kind() != reflect.Struct {
- typeInfo = &theTypeInfo{
- Type: t,
- }
- } else {
- // Allocate
- typeInfo = &theTypeInfo{
- Type: t,
- Fields: make([]theFieldInfo, 0, 16),
- }
-
- // Add fields
- typeInfo.Fields = appendFields(nil, nil, t)
-
- // Sort fields
- sort.Sort(sortableFieldInfos(typeInfo.Fields))
- }
-
- // Publish
- typeInfosMutex.Lock()
- typeInfos[t] = typeInfo
- typeInfosMutex.Unlock()
- return typeInfo
-}
diff --git a/routers/legacy/pathpattern/node.go b/pathpattern/node.go
similarity index 94%
rename from routers/legacy/pathpattern/node.go
rename to pathpattern/node.go
index 011dda358..43e2959d4 100644
--- a/routers/legacy/pathpattern/node.go
+++ b/pathpattern/node.go
@@ -1,11 +1,11 @@
// Package pathpattern implements path matching.
//
// Examples of supported patterns:
-// - "/"
-// - "/abc""
-// - "/abc/{variable}" (matches until next '/' or end-of-string)
-// - "/abc/{variable*}" (matches everything, including "/abc" if "/abc" has noot)
-// - "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions)
+// * "/"
+// * "/abc""
+// * "/abc/{variable}" (matches until next '/' or end-of-string)
+// * "/abc/{variable*}" (matches everything, including "/abc" if "/abc" has noot)
+// * "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions)
package pathpattern
import (
@@ -28,8 +28,8 @@ type Options struct {
// PathFromHost converts a host pattern to a path pattern.
//
// Examples:
-// - PathFromHost("some-subdomain.domain.com", false) -> "com/./domain/./some-subdomain"
-// - PathFromHost("some-subdomain.domain.com", true) -> "com/./domain/./subdomain/-/some"
+// * PathFromHost("some-subdomain.domain.com", false) -> "com/./domain/./some-subdomain"
+// * PathFromHost("some-subdomain.domain.com", true) -> "com/./domain/./subdomain/-/some"
func PathFromHost(host string, specialDashes bool) string {
buf := make([]byte, 0, len(host))
end := len(host)
@@ -212,7 +212,7 @@ loop:
// Find variable name
i := strings.IndexByte(remaining, '}')
if i < 0 {
- return nil, fmt.Errorf("missing '}' in: %s", path)
+ return nil, fmt.Errorf("Missing '}' in: %s", path)
}
variableName := strings.TrimSpace(remaining[1:i])
remaining = remaining[i+1:]
@@ -247,7 +247,7 @@ loop:
if suffix.Kind == SuffixKindRegExp {
regExp, err := regexp.Compile(suffix.Pattern)
if err != nil {
- return nil, fmt.Errorf("invalid regular expression in: %s", path)
+ return nil, fmt.Errorf("Invalid regular expression in: %s", path)
}
suffix.regExp = regExp
}
diff --git a/routers/legacy/pathpattern/node_test.go b/pathpattern/node_test.go
similarity index 68%
rename from routers/legacy/pathpattern/node_test.go
rename to pathpattern/node_test.go
index 14d8457ce..6d0ec9d92 100644
--- a/routers/legacy/pathpattern/node_test.go
+++ b/pathpattern/node_test.go
@@ -1,12 +1,14 @@
-package pathpattern
+package pathpattern_test
import (
"testing"
+
+ "github.com/getkin/kin-openapi/pathpattern"
)
func TestPatterns(t *testing.T) {
- DefaultOptions.SupportRegExp = true
- rootNode := &Node{}
+ pathpattern.DefaultOptions.SupportRegExp = true
+ rootNode := &pathpattern.Node{}
add := func(path, value string) {
rootNode.MustAdd(path, value, nil)
}
@@ -22,8 +24,8 @@ func TestPatterns(t *testing.T) {
add("/root/{path*}", "DIRECTORY")
add("/impossible_route", "IMPOSSIBLE")
- add(PathFromHost("www.nike.com", true), "WWW-HOST")
- add(PathFromHost("{other}.nike.com", true), "OTHER-HOST")
+ add(pathpattern.PathFromHost("www.nike.com", true), "WWW-HOST")
+ add(pathpattern.PathFromHost("{other}.nike.com", true), "OTHER-HOST")
expect := func(uri string, expected string, expectedArgs ...string) {
actually := "not found"
@@ -34,11 +36,11 @@ func TestPatterns(t *testing.T) {
}
}
if actually != expected {
- t.Fatalf("Wrong path!\nInput: %s\nExpected: %q\nActually: %q\nTree:\n%s\n\n", uri, expected, actually, rootNode.String())
+ t.Fatalf("Wrong path!\nInput: %s\nExpected: '%s'\nActually: '%s'\nTree:\n%s\n\n", uri, expected, actually, rootNode.String())
return
}
if !argsEqual(expectedArgs, actualArgs) {
- t.Fatalf("Wrong variable values!\nInput: %s\nExpected: %q\nActually: %q\nTree:\n%s\n\n", uri, expectedArgs, actualArgs, rootNode.String())
+ t.Fatalf("Wrong variable values!\nInput: %s\nExpected: '%s'\nActually: '%s'\nTree:\n%s\n\n", uri, expectedArgs, actualArgs, rootNode.String())
return
}
}
@@ -63,9 +65,9 @@ func TestPatterns(t *testing.T) {
expect("/root/", "DIRECTORY", "")
expect("/root/a/b/c", "DIRECTORY", "a/b/c")
- expect(PathFromHost("www.nike.com", true), "WWW-HOST")
- expect(PathFromHost("example.nike.com", true), "OTHER-HOST", "example")
- expect(PathFromHost("subdomain.example.nike.com", true), "not found")
+ expect(pathpattern.PathFromHost("www.nike.com", true), "WWW-HOST")
+ expect(pathpattern.PathFromHost("example.nike.com", true), "OTHER-HOST", "example")
+ expect(pathpattern.PathFromHost("subdomain.example.nike.com", true), "not found")
}
func argsEqual(a, b []string) bool {
diff --git a/refs.sh b/refs.sh
deleted file mode 100755
index 9ade24196..000000000
--- a/refs.sh
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/bin/bash -eux
-set -o pipefail
-
-types=()
-types+=("Callback")
-types+=("Example")
-types+=("Header")
-types+=("Link")
-types+=("Parameter")
-types+=("RequestBody")
-types+=("Response")
-types+=("Schema")
-types+=("SecurityScheme")
-
-cat < 0 {
- if servers, err = makeServers(pathItem.Servers); err != nil {
- return nil, err
- }
- }
-
- operations := pathItem.Operations()
- methods := make([]string, 0, len(operations))
- for method := range operations {
- methods = append(methods, method)
- }
- sort.Strings(methods)
-
- for _, s := range servers {
- muxRoute := muxRouter.Path(s.base + path).Methods(methods...)
- if schemes := s.schemes; len(schemes) != 0 {
- muxRoute.Schemes(schemes...)
- }
- if host := s.host; host != "" {
- muxRoute.Host(host)
- }
- if err := muxRoute.GetError(); err != nil {
- return nil, err
- }
- r.muxes = append(r.muxes, routeMux{
- muxRoute: muxRoute,
- varsUpdater: s.varsUpdater,
- })
- r.routes = append(r.routes, &routers.Route{
- Spec: doc,
- Server: s.server,
- Path: path,
- PathItem: pathItem,
- Method: "",
- Operation: nil,
- })
- }
- }
- return r, nil
-}
-
-// FindRoute extracts the route and parameters of an http.Request
-func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) {
- for i, m := range r.muxes {
- var match mux.RouteMatch
- if m.muxRoute.Match(req, &match) {
- if err := match.MatchErr; err != nil {
- // What then?
- }
- vars := match.Vars
- if f := m.varsUpdater; f != nil {
- f(vars)
- }
- route := *r.routes[i]
- route.Method = req.Method
- route.Operation = route.Spec.Paths[route.Path].GetOperation(route.Method)
- return &route, vars, nil
- }
- switch match.MatchErr {
- case nil:
- case mux.ErrMethodMismatch:
- return nil, nil, routers.ErrMethodNotAllowed
- default: // What then?
- }
- }
- return nil, nil, routers.ErrPathNotFound
-}
-
-func makeServers(in openapi3.Servers) ([]srv, error) {
- servers := make([]srv, 0, len(in))
- for _, server := range in {
- serverURL := server.URL
- if submatch := singleVariableMatcher.FindStringSubmatch(serverURL); submatch != nil {
- sVar := submatch[1]
- sVal := server.Variables[sVar].Default
- serverURL = strings.ReplaceAll(serverURL, "{"+sVar+"}", sVal)
- var varsUpdater varsf
- if lhs := strings.TrimSuffix(serverURL, server.Variables[sVar].Default); lhs != "" {
- varsUpdater = func(vars map[string]string) { vars[sVar] = lhs }
- }
- svr, err := newSrv(serverURL, server, varsUpdater)
- if err != nil {
- return nil, err
- }
-
- servers = append(servers, svr)
- continue
- }
-
- // If a variable represents the port "http://domain.tld:{port}/bla"
- // then url.Parse() cannot parse "http://domain.tld:`bEncode({port})`/bla"
- // and mux is not able to set the {port} variable
- // So we just use the default value for this variable.
- // See https://github.com/getkin/kin-openapi/issues/367
- var varsUpdater varsf
- if lhs := strings.Index(serverURL, ":{"); lhs > 0 {
- rest := serverURL[lhs+len(":{"):]
- rhs := strings.Index(rest, "}")
- portVariable := rest[:rhs]
- portValue := server.Variables[portVariable].Default
- serverURL = strings.ReplaceAll(serverURL, "{"+portVariable+"}", portValue)
- varsUpdater = func(vars map[string]string) {
- vars[portVariable] = portValue
- }
- }
-
- svr, err := newSrv(serverURL, server, varsUpdater)
- if err != nil {
- return nil, err
- }
- servers = append(servers, svr)
- }
- if len(servers) == 0 {
- servers = append(servers, srv{})
- }
-
- return servers, nil
-}
-
-func newSrv(serverURL string, server *openapi3.Server, varsUpdater varsf) (srv, error) {
- var schemes []string
- if strings.Contains(serverURL, "://") {
- scheme0 := strings.Split(serverURL, "://")[0]
- schemes = permutePart(scheme0, server)
- serverURL = strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1)
- }
-
- u, err := url.Parse(bEncode(serverURL))
- if err != nil {
- return srv{}, err
- }
- path := bDecode(u.EscapedPath())
- if len(path) > 0 && path[len(path)-1] == '/' {
- path = path[:len(path)-1]
- }
- svr := srv{
- host: bDecode(u.Host), //u.Hostname()?
- base: path,
- schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624
- server: server,
- varsUpdater: varsUpdater,
- }
- return svr, nil
-}
-
-// Magic strings that temporarily replace "{}" so net/url.Parse() works
-var blURL, brURL = strings.Repeat("-", 50), strings.Repeat("_", 50)
-
-func bEncode(s string) string {
- s = strings.Replace(s, "{", blURL, -1)
- s = strings.Replace(s, "}", brURL, -1)
- return s
-}
-func bDecode(s string) string {
- s = strings.Replace(s, blURL, "{", -1)
- s = strings.Replace(s, brURL, "}", -1)
- return s
-}
-
-func permutePart(part0 string, srv *openapi3.Server) []string {
- type mapAndSlice struct {
- m map[string]struct{}
- s []string
- }
- var2val := make(map[string]mapAndSlice)
- max := 0
- for name0, v := range srv.Variables {
- name := "{" + name0 + "}"
- if !strings.Contains(part0, name) {
- continue
- }
- m := map[string]struct{}{v.Default: {}}
- for _, value := range v.Enum {
- m[value] = struct{}{}
- }
- if l := len(m); l > max {
- max = l
- }
- s := make([]string, 0, len(m))
- for value := range m {
- s = append(s, value)
- }
- var2val[name] = mapAndSlice{m: m, s: s}
- }
- if len(var2val) == 0 {
- return []string{part0}
- }
-
- partsMap := make(map[string]struct{}, max*len(var2val))
- for i := 0; i < max; i++ {
- part := part0
- for name, mas := range var2val {
- part = strings.Replace(part, name, mas.s[i%len(mas.s)], -1)
- }
- partsMap[part] = struct{}{}
- }
- parts := make([]string, 0, len(partsMap))
- for part := range partsMap {
- parts = append(parts, part)
- }
- sort.Strings(parts)
- return parts
-}
diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go
deleted file mode 100644
index 5fb9be2b0..000000000
--- a/routers/gorillamux/router_test.go
+++ /dev/null
@@ -1,509 +0,0 @@
-package gorillamux
-
-import (
- "context"
- "net/http"
- "sort"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers"
-)
-
-func TestRouter(t *testing.T) {
- helloCONNECT := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloDELETE := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloHEAD := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloOPTIONS := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloPATCH := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloPOST := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloPUT := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloTRACE := &openapi3.Operation{Responses: openapi3.NewResponses()}
- paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
- booksPOST := &openapi3.Operation{Responses: openapi3.NewResponses()}
- partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
- doc := &openapi3.T{
- OpenAPI: "3.0.0",
- Info: &openapi3.Info{
- Title: "MyAPI",
- Version: "0.1",
- },
- Paths: openapi3.Paths{
- "/hello": &openapi3.PathItem{
- Connect: helloCONNECT,
- Delete: helloDELETE,
- Get: helloGET,
- Head: helloHEAD,
- Options: helloOPTIONS,
- Patch: helloPATCH,
- Post: helloPOST,
- Put: helloPUT,
- Trace: helloTRACE,
- },
- "/onlyGET": &openapi3.PathItem{
- Get: helloGET,
- },
- "/params/{x}/{y}/{z:.*}": &openapi3.PathItem{
- Get: paramsGET,
- Parameters: openapi3.Parameters{
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")},
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y")},
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")},
- },
- },
- "/books/{bookid}": &openapi3.PathItem{
- Get: paramsGET,
- Parameters: openapi3.Parameters{
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")},
- },
- },
- "/books/{bookid}.json": &openapi3.PathItem{
- Post: booksPOST,
- Parameters: openapi3.Parameters{
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")},
- },
- },
- "/partial": &openapi3.PathItem{
- Get: partialGET,
- },
- },
- }
-
- expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) {
- t.Helper()
- req, err := http.NewRequest(method, uri, nil)
- require.NoError(t, err)
- route, pathParams, err := r.FindRoute(req)
- if err != nil {
- if operation == nil {
- pathItem := doc.Paths[uri]
- if pathItem == nil {
- if err.Error() != routers.ErrPathNotFound.Error() {
- t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrPathNotFound, err)
- }
- return
- }
- if pathItem.GetOperation(method) == nil {
- if err.Error() != routers.ErrMethodNotAllowed.Error() {
- t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrMethodNotAllowed, err)
- }
- }
- } else {
- t.Fatalf("'%s %s': should have returned an operation, but it returned an error: %v", method, uri, err)
- }
- }
- if operation == nil && err == nil {
- t.Fatalf("'%s %s': should have failed, but returned\nroute = %+v\npathParams = %+v", method, uri, route, pathParams)
- }
- if route == nil {
- return
- }
- if route.Operation != operation {
- t.Fatalf("'%s %s': Returned wrong operation (%v)",
- method, uri, route.Operation)
- }
- if len(params) == 0 {
- if len(pathParams) != 0 {
- t.Fatalf("'%s %s': should return no path arguments, but found %+v", method, uri, pathParams)
- }
- } else {
- names := make([]string, 0, len(params))
- for name := range params {
- names = append(names, name)
- }
- sort.Strings(names)
- for _, name := range names {
- expected := params[name]
- actual, exists := pathParams[name]
- if !exists {
- t.Fatalf("'%s %s': path parameter %q should be %q, but it's not defined.", method, uri, name, expected)
- }
- if actual != expected {
- t.Fatalf("'%s %s': path parameter %q should be %q, but it's %q", method, uri, name, expected, actual)
- }
- }
- }
- }
-
- err := doc.Validate(context.Background())
- require.NoError(t, err)
- r, err := NewRouter(doc)
- require.NoError(t, err)
-
- expect(r, http.MethodGet, "/not_existing", nil, nil)
- expect(r, http.MethodDelete, "/hello", helloDELETE, nil)
- expect(r, http.MethodGet, "/hello", helloGET, nil)
- expect(r, http.MethodHead, "/hello", helloHEAD, nil)
- expect(r, http.MethodPatch, "/hello", helloPATCH, nil)
- expect(r, http.MethodPost, "/hello", helloPOST, nil)
- expect(r, http.MethodPut, "/hello", helloPUT, nil)
- expect(r, http.MethodGet, "/params/a/b/", paramsGET, map[string]string{
- "x": "a",
- "y": "b",
- "z": "",
- })
- expect(r, http.MethodGet, "/params/a/b/c%2Fd", paramsGET, map[string]string{
- "x": "a",
- "y": "b",
- "z": "c%2Fd",
- })
- expect(r, http.MethodGet, "/books/War.and.Peace", paramsGET, map[string]string{
- "bookid": "War.and.Peace",
- })
- expect(r, http.MethodPost, "/books/War.and.Peace.json", booksPOST, map[string]string{
- "bookid": "War.and.Peace",
- })
- expect(r, http.MethodPost, "/partial", nil, nil)
-
- doc.Servers = []*openapi3.Server{
- {URL: "https://www.example.com/api/v1"},
- {URL: "{scheme}://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{
- "d0": {Default: "www"},
- "d1": {Default: "example", Enum: []string{"example"}},
- "scheme": {Default: "https", Enum: []string{"https", "http"}},
- }},
- {URL: "http://127.0.0.1:{port}/api/v1", Variables: map[string]*openapi3.ServerVariable{
- "port": {Default: "8000"},
- }},
- }
- err = doc.Validate(context.Background())
- require.NoError(t, err)
- r, err = NewRouter(doc)
- require.NoError(t, err)
- expect(r, http.MethodGet, "/hello", nil, nil)
- expect(r, http.MethodGet, "/api/v1/hello", nil, nil)
- expect(r, http.MethodGet, "www.example.com/api/v1/hello", nil, nil)
- expect(r, http.MethodGet, "https:///api/v1/hello", nil, nil)
- expect(r, http.MethodGet, "https://www.example.com/hello", nil, nil)
- expect(r, http.MethodGet, "https://www.example.com/api/v1/hello", helloGET, nil)
- expect(r, http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{
- "d0": "domain0",
- "d1": "domain1",
- // "scheme": "https", TODO: https://github.com/gorilla/mux/issues/624
- })
- expect(r, http.MethodGet, "http://127.0.0.1:8000/api/v1/hello", helloGET, map[string]string{
- "port": "8000",
- })
-
- doc.Servers = []*openapi3.Server{
- {URL: "{server}", Variables: map[string]*openapi3.ServerVariable{
- "server": {Default: "/api/v1"},
- }},
- }
- err = doc.Validate(context.Background())
- require.NoError(t, err)
- r, err = NewRouter(doc)
- require.NoError(t, err)
- expect(r, http.MethodGet, "https://myserver/api/v1/hello", helloGET, nil)
-
- {
- uri := "https://www.example.com/api/v1/onlyGET"
- expect(r, http.MethodGet, uri, helloGET, nil)
- req, err := http.NewRequest(http.MethodDelete, uri, nil)
- require.NoError(t, err)
- require.NotNil(t, req)
- route, pathParams, err := r.FindRoute(req)
- require.EqualError(t, err, routers.ErrMethodNotAllowed.Error())
- require.Nil(t, route)
- require.Nil(t, pathParams)
- }
-}
-
-func TestPermuteScheme(t *testing.T) {
- scheme0 := "{sche}{me}"
- server := &openapi3.Server{URL: scheme0 + "://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{
- "d0": {Default: "www"},
- "d1": {Default: "example", Enum: []string{"example"}},
- "sche": {Default: "http"},
- "me": {Default: "s", Enum: []string{"", "s"}},
- }}
- err := server.Validate(context.Background())
- require.NoError(t, err)
- perms := permutePart(scheme0, server)
- require.Equal(t, []string{"http", "https"}, perms)
-}
-
-func TestServerPath(t *testing.T) {
- server := &openapi3.Server{URL: "http://example.com"}
- err := server.Validate(context.Background())
- require.NoError(t, err)
-
- _, err = NewRouter(&openapi3.T{Servers: openapi3.Servers{
- server,
- &openapi3.Server{URL: "http://example.com/"},
- &openapi3.Server{URL: "http://example.com/path"},
- newServerWithVariables(
- "{scheme}://localhost",
- map[string]string{
- "scheme": "https",
- }),
- newServerWithVariables(
- "{url}",
- map[string]string{
- "url": "http://example.com/path",
- }),
- newServerWithVariables(
- "http://example.com:{port}/path",
- map[string]string{
- "port": "8088",
- }),
- newServerWithVariables(
- "{server}",
- map[string]string{
- "server": "/",
- }),
- newServerWithVariables(
- "/",
- nil,
- )},
- })
- require.NoError(t, err)
-}
-
-func TestServerOverrideAtPathLevel(t *testing.T) {
- helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
- doc := &openapi3.T{
- OpenAPI: "3.0.0",
- Info: &openapi3.Info{
- Title: "rel",
- Version: "1",
- },
- Servers: openapi3.Servers{
- &openapi3.Server{
- URL: "https://example.com",
- },
- },
- Paths: openapi3.Paths{
- "/hello": &openapi3.PathItem{
- Servers: openapi3.Servers{
- &openapi3.Server{
- URL: "https://another.com",
- },
- },
- Get: helloGET,
- },
- },
- }
- err := doc.Validate(context.Background())
- require.NoError(t, err)
- router, err := NewRouter(doc)
- require.NoError(t, err)
-
- req, err := http.NewRequest(http.MethodGet, "https://another.com/hello", nil)
- require.NoError(t, err)
- route, _, err := router.FindRoute(req)
- require.Equal(t, "/hello", route.Path)
-
- req, err = http.NewRequest(http.MethodGet, "https://example.com/hello", nil)
- require.NoError(t, err)
- route, _, err = router.FindRoute(req)
- require.Nil(t, route)
- require.Error(t, err)
-}
-
-func TestRelativeURL(t *testing.T) {
- helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
- doc := &openapi3.T{
- OpenAPI: "3.0.0",
- Info: &openapi3.Info{
- Title: "rel",
- Version: "1",
- },
- Servers: openapi3.Servers{
- &openapi3.Server{
- URL: "/api/v1",
- },
- },
- Paths: openapi3.Paths{
- "/hello": &openapi3.PathItem{
- Get: helloGET,
- },
- },
- }
- err := doc.Validate(context.Background())
- require.NoError(t, err)
- router, err := NewRouter(doc)
- require.NoError(t, err)
- req, err := http.NewRequest(http.MethodGet, "https://example.com/api/v1/hello", nil)
- require.NoError(t, err)
- route, _, err := router.FindRoute(req)
- require.NoError(t, err)
- require.Equal(t, "/hello", route.Path)
-}
-
-func Test_makeServers(t *testing.T) {
- type testStruct struct {
- name string
- servers openapi3.Servers
- want []srv
- wantErr bool
- initFn func(tt *testStruct)
- }
- tests := []testStruct{
- {
- name: "server is root path",
- servers: openapi3.Servers{
- newServerWithVariables("/", nil),
- },
- want: []srv{{
- schemes: nil,
- host: "",
- base: "",
- server: nil,
- varsUpdater: nil,
- }},
- wantErr: false,
- initFn: func(tt *testStruct) {
- for i, server := range tt.servers {
- tt.want[i].server = server
- }
- },
- },
- {
- name: "server with single variable that evaluates to root path",
- servers: openapi3.Servers{
- newServerWithVariables("{server}", map[string]string{"server": "/"}),
- },
- want: []srv{{
- schemes: nil,
- host: "",
- base: "",
- server: nil,
- varsUpdater: nil,
- }},
- wantErr: false,
- initFn: func(tt *testStruct) {
- for i, server := range tt.servers {
- tt.want[i].server = server
- }
- },
- },
- {
- name: "server is http://localhost:28002",
- servers: openapi3.Servers{
- newServerWithVariables("http://localhost:28002", nil),
- },
- want: []srv{{
- schemes: []string{"http"},
- host: "localhost:28002",
- base: "",
- server: nil,
- varsUpdater: nil,
- }},
- wantErr: false,
- initFn: func(tt *testStruct) {
- for i, server := range tt.servers {
- tt.want[i].server = server
- }
- },
- },
- {
- name: "server with single variable that evaluates to http://localhost:28002",
- servers: openapi3.Servers{
- newServerWithVariables("{server}", map[string]string{"server": "http://localhost:28002"}),
- },
- want: []srv{{
- schemes: []string{"http"},
- host: "localhost:28002",
- base: "",
- server: nil,
- varsUpdater: nil,
- }},
- wantErr: false,
- initFn: func(tt *testStruct) {
- for i, server := range tt.servers {
- tt.want[i].server = server
- }
- },
- },
- {
- name: "server with multiple variables that evaluates to http://localhost:28002",
- servers: openapi3.Servers{
- newServerWithVariables("{scheme}://{host}:{port}", map[string]string{"scheme": "http", "host": "localhost", "port": "28002"}),
- },
- want: []srv{{
- schemes: []string{"http"},
- host: "{host}:28002",
- base: "",
- server: nil,
- varsUpdater: func(vars map[string]string) { vars["port"] = "28002" },
- }},
- wantErr: false,
- initFn: func(tt *testStruct) {
- for i, server := range tt.servers {
- tt.want[i].server = server
- }
- },
- },
- {
- name: "server with unparsable URL fails",
- servers: openapi3.Servers{
- newServerWithVariables("exam^ple.com:443", nil),
- },
- want: nil,
- wantErr: true,
- initFn: nil,
- },
- {
- name: "server with single variable that evaluates to unparsable URL fails",
- servers: openapi3.Servers{
- newServerWithVariables("{server}", map[string]string{"server": "exam^ple.com:443"}),
- },
- want: nil,
- wantErr: true,
- initFn: nil,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if tt.initFn != nil {
- tt.initFn(&tt)
- }
- got, err := makeServers(tt.servers)
- if (err != nil) != tt.wantErr {
- t.Errorf("makeServers() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- assert.Equal(t, len(tt.want), len(got), "expected and actual servers lengths are not equal")
- for i := 0; i < len(tt.want); i++ {
- // Unfortunately using assert.Equals or reflect.DeepEquals isn't
- // an option because function pointers cannot be compared
- assert.Equal(t, tt.want[i].schemes, got[i].schemes)
- assert.Equal(t, tt.want[i].host, got[i].host)
- assert.Equal(t, tt.want[i].host, got[i].host)
- assert.Equal(t, tt.want[i].server, got[i].server)
- if tt.want[i].varsUpdater == nil {
- assert.Nil(t, got[i].varsUpdater, "expected and actual varsUpdater should point to same function")
- } else {
- assert.NotNil(t, got[i].varsUpdater, "expected and actual varsUpdater should point to same function")
- }
- }
- })
- }
-}
-
-func newServerWithVariables(url string, variables map[string]string) *openapi3.Server {
- var serverVariables = map[string]*openapi3.ServerVariable{}
-
- for key, value := range variables {
- serverVariables[key] = newServerVariable(value)
- }
-
- return &openapi3.Server{
- URL: url,
- Description: "",
- Variables: serverVariables,
- }
-}
-
-func newServerVariable(defaultValue string) *openapi3.ServerVariable {
- return &openapi3.ServerVariable{
- Enum: nil,
- Default: defaultValue,
- Description: "",
- }
-}
diff --git a/routers/issue356_test.go b/routers/issue356_test.go
deleted file mode 100644
index de9f06b91..000000000
--- a/routers/issue356_test.go
+++ /dev/null
@@ -1,145 +0,0 @@
-package routers_test
-
-import (
- "context"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers"
- "github.com/getkin/kin-openapi/routers/gorillamux"
- "github.com/getkin/kin-openapi/routers/legacy"
-)
-
-func TestIssue356(t *testing.T) {
- spec := func(servers string) []byte {
- return []byte(`
-openapi: 3.0.0
-info:
- title: Example
- version: '1.0'
- description: test
-servers:
-` + servers + `
-paths:
- /test:
- post:
- responses:
- '201':
- description: Created
- content:
- application/json:
- schema: {type: object}
- requestBody:
- content:
- application/json:
- schema: {type: object}
- description: ''
- description: Create a test object
-`)
- }
-
- for servers, expectError := range map[string]bool{
- `
-- url: http://localhost:3000/base
-- url: /base
-`: false,
-
- `
-- url: /base
-- url: http://localhost:3000/base
-`: false,
-
- `- url: /base`: false,
-
- `- url: http://localhost:3000/base`: true,
-
- ``: true,
- } {
- loader := &openapi3.Loader{Context: context.Background()}
- t.Logf("using servers: %q (%v)", servers, expectError)
- doc, err := loader.LoadFromData(spec(servers))
- require.NoError(t, err)
- err = doc.Validate(context.Background())
- require.NoError(t, err)
- gorillamuxNewRouterWrapped := func(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Router, error) {
- return gorillamux.NewRouter(doc)
- }
-
- for i, newRouter := range []func(*openapi3.T, ...openapi3.ValidationOption) (routers.Router, error){gorillamuxNewRouterWrapped, legacy.NewRouter} {
- t.Logf("using NewRouter from %s", map[int]string{0: "gorillamux", 1: "legacy"}[i])
- router, err := newRouter(doc)
- require.NoError(t, err)
-
- if true {
- t.Logf("using naked newRouter")
- httpReq, err := http.NewRequest(http.MethodPost, "/base/test", strings.NewReader(`{}`))
- require.NoError(t, err)
- httpReq.Header.Set("Content-Type", "application/json")
-
- route, pathParams, err := router.FindRoute(httpReq)
- if expectError {
- require.Error(t, err, routers.ErrPathNotFound)
- return
- }
- require.NoError(t, err)
-
- requestValidationInput := &openapi3filter.RequestValidationInput{
- Request: httpReq,
- PathParams: pathParams,
- Route: route,
- }
- err = openapi3filter.ValidateRequest(context.Background(), requestValidationInput)
- require.NoError(t, err)
- }
-
- if true {
- t.Logf("using httptest.NewServer")
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- route, pathParams, err := router.FindRoute(r)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte(err.Error()))
- return
- }
-
- requestValidationInput := &openapi3filter.RequestValidationInput{
- Request: r,
- PathParams: pathParams,
- Route: route,
- }
- err = openapi3filter.ValidateRequest(r.Context(), requestValidationInput)
- require.NoError(t, err)
-
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte("{}"))
- }))
- defer ts.Close()
-
- req, err := http.NewRequest(http.MethodPost, ts.URL+"/base/test", strings.NewReader(`{}`))
- require.NoError(t, err)
- req.Header.Set("Content-Type", "application/json")
-
- rep, err := http.DefaultClient.Do(req)
- require.NoError(t, err)
- defer rep.Body.Close()
- body, err := ioutil.ReadAll(rep.Body)
- require.NoError(t, err)
-
- if expectError {
- require.Equal(t, 500, rep.StatusCode)
- require.Equal(t, routers.ErrPathNotFound.Error(), string(body))
- return
- }
- require.Equal(t, 200, rep.StatusCode)
- require.Equal(t, "{}", string(body))
- }
- }
- }
-}
diff --git a/routers/legacy/issue444_test.go b/routers/legacy/issue444_test.go
deleted file mode 100644
index c1e9b14f2..000000000
--- a/routers/legacy/issue444_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package legacy_test
-
-import (
- "bytes"
- "context"
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
-)
-
-func TestIssue444(t *testing.T) {
- loader := openapi3.NewLoader()
- oas, err := loader.LoadFromData([]byte(`
-openapi: '3.0.0'
-info:
- title: API
- version: 1.0.0
-paths:
- '/path':
- post:
- requestBody:
- required: true
- content:
- application/x-yaml:
- schema:
- type: object
- responses:
- '200':
- description: x
- content:
- application/json:
- schema:
- type: string
-`))
- require.NoError(t, err)
- router, err := legacyrouter.NewRouter(oas)
- require.NoError(t, err)
-
- r := httptest.NewRequest("POST", "/path", bytes.NewReader([]byte(`
-foo: bar
-`)))
- r.Header.Set("Content-Type", "application/x-yaml")
-
- openapi3.SchemaErrorDetailsDisabled = true
- route, pathParams, err := router.FindRoute(r)
- require.NoError(t, err)
- reqValidationInput := &openapi3filter.RequestValidationInput{
- Request: r,
- PathParams: pathParams,
- Route: route,
- }
- err = openapi3filter.ValidateRequest(context.Background(), reqValidationInput)
- require.NoError(t, err)
-}
diff --git a/routers/legacy/router.go b/routers/legacy/router.go
deleted file mode 100644
index 911422b85..000000000
--- a/routers/legacy/router.go
+++ /dev/null
@@ -1,167 +0,0 @@
-// Package legacy implements a router.
-//
-// It differs from the gorilla/mux router:
-// * it provides granular errors: "path not found", "method not allowed", "variable missing from path"
-// * it does not handle matching routes with extensions (e.g. /books/{id}.json)
-// * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z.*})
-package legacy
-
-import (
- "context"
- "errors"
- "fmt"
- "net/http"
- "strings"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers"
- "github.com/getkin/kin-openapi/routers/legacy/pathpattern"
-)
-
-// Routers maps a HTTP request to a Router.
-type Routers []*Router
-
-// FindRoute extracts the route and parameters of an http.Request
-func (rs Routers) FindRoute(req *http.Request) (routers.Router, *routers.Route, map[string]string, error) {
- for _, router := range rs {
- // Skip routers that have DO NOT have servers
- if len(router.doc.Servers) == 0 {
- continue
- }
- route, pathParams, err := router.FindRoute(req)
- if err == nil {
- return router, route, pathParams, nil
- }
- }
- for _, router := range rs {
- // Skip routers that DO have servers
- if len(router.doc.Servers) > 0 {
- continue
- }
- route, pathParams, err := router.FindRoute(req)
- if err == nil {
- return router, route, pathParams, nil
- }
- }
- return nil, nil, nil, &routers.RouteError{
- Reason: "none of the routers match",
- }
-}
-
-// Router maps a HTTP request to an OpenAPI operation.
-type Router struct {
- doc *openapi3.T
- pathNode *pathpattern.Node
-}
-
-// NewRouter creates a new router.
-//
-// If the given OpenAPIv3 document has servers, router will use them.
-// All operations of the document will be added to the router.
-func NewRouter(doc *openapi3.T, opts ...openapi3.ValidationOption) (routers.Router, error) {
- if err := doc.Validate(context.Background(), opts...); err != nil {
- return nil, fmt.Errorf("validating OpenAPI failed: %w", err)
- }
- router := &Router{doc: doc}
- root := router.node()
- for path, pathItem := range doc.Paths {
- for method, operation := range pathItem.Operations() {
- method = strings.ToUpper(method)
- if err := root.Add(method+" "+path, &routers.Route{
- Spec: doc,
- Path: path,
- PathItem: pathItem,
- Method: method,
- Operation: operation,
- }, nil); err != nil {
- return nil, err
- }
- }
- }
- return router, nil
-}
-
-// AddRoute adds a route in the router.
-func (router *Router) AddRoute(route *routers.Route) error {
- method := route.Method
- if method == "" {
- return errors.New("route is missing method")
- }
- method = strings.ToUpper(method)
- path := route.Path
- if path == "" {
- return errors.New("route is missing path")
- }
- return router.node().Add(method+" "+path, router, nil)
-}
-
-func (router *Router) node() *pathpattern.Node {
- root := router.pathNode
- if root == nil {
- root = &pathpattern.Node{}
- router.pathNode = root
- }
- return root
-}
-
-// FindRoute extracts the route and parameters of an http.Request
-func (router *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) {
- method, url := req.Method, req.URL
- doc := router.doc
-
- // Get server
- servers := doc.Servers
- var server *openapi3.Server
- var remainingPath string
- var pathParams map[string]string
- if len(servers) == 0 {
- remainingPath = url.Path
- } else {
- var paramValues []string
- server, paramValues, remainingPath = servers.MatchURL(url)
- if server == nil {
- return nil, nil, &routers.RouteError{
- Reason: routers.ErrPathNotFound.Error(),
- }
- }
- pathParams = make(map[string]string)
- paramNames, err := server.ParameterNames()
- if err != nil {
- return nil, nil, err
- }
- for i, value := range paramValues {
- name := paramNames[i]
- pathParams[name] = value
- }
- }
-
- // Get PathItem
- root := router.node()
- var route *routers.Route
- node, paramValues := root.Match(method + " " + remainingPath)
- if node != nil {
- route, _ = node.Value.(*routers.Route)
- }
- if route == nil {
- pathItem := doc.Paths[remainingPath]
- if pathItem == nil {
- return nil, nil, &routers.RouteError{Reason: routers.ErrPathNotFound.Error()}
- }
- if pathItem.GetOperation(method) == nil {
- return nil, nil, &routers.RouteError{Reason: routers.ErrMethodNotAllowed.Error()}
- }
- }
-
- if pathParams == nil {
- pathParams = make(map[string]string, len(paramValues))
- }
- paramKeys := node.VariableNames
- for i, value := range paramValues {
- key := paramKeys[i]
- if strings.HasSuffix(key, "*") {
- key = key[:len(key)-1]
- }
- pathParams[key] = value
- }
- return route, pathParams, nil
-}
diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go
deleted file mode 100644
index e9b875986..000000000
--- a/routers/legacy/router_test.go
+++ /dev/null
@@ -1,213 +0,0 @@
-package legacy
-
-import (
- "context"
- "net/http"
- "sort"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/routers"
-)
-
-func TestRouter(t *testing.T) {
- helloCONNECT := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloDELETE := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloHEAD := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloOPTIONS := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloPATCH := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloPOST := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloPUT := &openapi3.Operation{Responses: openapi3.NewResponses()}
- helloTRACE := &openapi3.Operation{Responses: openapi3.NewResponses()}
- paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
- booksPOST := &openapi3.Operation{Responses: openapi3.NewResponses()}
- partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()}
- doc := &openapi3.T{
- OpenAPI: "3.0.0",
- Info: &openapi3.Info{
- Title: "MyAPI",
- Version: "0.1",
- },
- Paths: openapi3.Paths{
- "/hello": &openapi3.PathItem{
- Connect: helloCONNECT,
- Delete: helloDELETE,
- Get: helloGET,
- Head: helloHEAD,
- Options: helloOPTIONS,
- Patch: helloPATCH,
- Post: helloPOST,
- Put: helloPUT,
- Trace: helloTRACE,
- },
- "/onlyGET": &openapi3.PathItem{
- Get: helloGET,
- },
- "/params/{x}/{y}/{z.*}": &openapi3.PathItem{
- Get: paramsGET,
- Parameters: openapi3.Parameters{
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")},
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y")},
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")},
- },
- },
- "/books/{bookid}": &openapi3.PathItem{
- Get: paramsGET,
- Parameters: openapi3.Parameters{
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")},
- },
- },
- "/books/{bookid2}.json": &openapi3.PathItem{
- Post: booksPOST,
- Parameters: openapi3.Parameters{
- &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2")},
- },
- },
- "/partial": &openapi3.PathItem{
- Get: partialGET,
- },
- },
- }
-
- expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) {
- req, err := http.NewRequest(method, uri, nil)
- require.NoError(t, err)
- route, pathParams, err := r.FindRoute(req)
- if err != nil {
- if operation == nil {
- pathItem := doc.Paths[uri]
- if pathItem == nil {
- if err.Error() != routers.ErrPathNotFound.Error() {
- t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrPathNotFound, err)
- }
- return
- }
- if pathItem.GetOperation(method) == nil {
- if err.Error() != routers.ErrMethodNotAllowed.Error() {
- t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrMethodNotAllowed, err)
- }
- }
- } else {
- t.Fatalf("'%s %s': should have returned an operation, but it returned an error: %v", method, uri, err)
- }
- }
- if operation == nil && err == nil {
- t.Fatalf("'%s %s': should have failed, but returned\nroute = %+v\npathParams = %+v", method, uri, route, pathParams)
- }
- if route == nil {
- return
- }
- if route.Operation != operation {
- t.Fatalf("'%s %s': Returned wrong operation (%v)",
- method, uri, route.Operation)
- }
- if len(params) == 0 {
- if len(pathParams) != 0 {
- t.Fatalf("'%s %s': should return no path arguments, but found %+v", method, uri, pathParams)
- }
- } else {
- names := make([]string, 0, len(params))
- for name := range params {
- names = append(names, name)
- }
- sort.Strings(names)
- for _, name := range names {
- expected := params[name]
- actual, exists := pathParams[name]
- if !exists {
- t.Fatalf("'%s %s': path parameter %q should be %q, but it's not defined.", method, uri, name, expected)
- }
- if actual != expected {
- t.Fatalf("'%s %s': path parameter %q should be %q, but it's %q", method, uri, name, expected, actual)
- }
- }
- }
- }
-
- err := doc.Validate(context.Background())
- require.NoError(t, err)
- r, err := NewRouter(doc)
- require.NoError(t, err)
-
- expect(r, http.MethodGet, "/not_existing", nil, nil)
- expect(r, http.MethodDelete, "/hello", helloDELETE, nil)
- expect(r, http.MethodGet, "/hello", helloGET, nil)
- expect(r, http.MethodHead, "/hello", helloHEAD, nil)
- expect(r, http.MethodPatch, "/hello", helloPATCH, nil)
- expect(r, http.MethodPost, "/hello", helloPOST, nil)
- expect(r, http.MethodPut, "/hello", helloPUT, nil)
- expect(r, http.MethodGet, "/params/a/b/", paramsGET, map[string]string{
- "x": "a",
- "y": "b",
- // "z": "",
- })
- expect(r, http.MethodGet, "/params/a/b/c%2Fd", paramsGET, map[string]string{
- "x": "a",
- "y": "b",
- // "z": "c/d",
- })
- expect(r, http.MethodGet, "/books/War.and.Peace", paramsGET, map[string]string{
- "bookid": "War.and.Peace",
- })
- {
- req, err := http.NewRequest(http.MethodPost, "/books/War.and.Peace.json", nil)
- require.NoError(t, err)
- _, _, err = r.FindRoute(req)
- require.EqualError(t, err, routers.ErrPathNotFound.Error())
- }
- expect(r, http.MethodPost, "/partial", nil, nil)
-
- doc.Servers = []*openapi3.Server{
- {URL: "https://www.example.com/api/v1"},
- {URL: "https://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{
- "d0": {Default: "www"},
- "d1": {Default: "example", Enum: []string{"example"}},
- }},
- }
- err = doc.Validate(context.Background())
- require.NoError(t, err)
- r, err = NewRouter(doc)
- require.NoError(t, err)
- expect(r, http.MethodGet, "/hello", nil, nil)
- expect(r, http.MethodGet, "/api/v1/hello", nil, nil)
- expect(r, http.MethodGet, "www.example.com/api/v1/hello", nil, nil)
- expect(r, http.MethodGet, "https:///api/v1/hello", nil, nil)
- expect(r, http.MethodGet, "https://www.example.com/hello", nil, nil)
- expect(r, http.MethodGet, "https://www.example.com/api/v1/hello", helloGET, nil)
- expect(r, http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{
- "d0": "domain0",
- "d1": "domain1",
- })
-
- {
- uri := "https://www.example.com/api/v1/onlyGET"
- expect(r, http.MethodGet, uri, helloGET, nil)
- req, err := http.NewRequest(http.MethodDelete, uri, nil)
- require.NoError(t, err)
- require.NotNil(t, req)
- route, pathParams, err := r.FindRoute(req)
- require.EqualError(t, err, routers.ErrMethodNotAllowed.Error())
- require.Nil(t, route)
- require.Nil(t, pathParams)
- }
-
- schema := &openapi3.Schema{
- Type: "string",
- Example: 3,
- }
- content := openapi3.NewContentWithJSONSchema(schema)
- responses := openapi3.NewResponses()
- responses["default"].Value.Content = content
- doc.Paths["/withExamples"] = &openapi3.PathItem{
- Get: &openapi3.Operation{Responses: responses},
- }
- err = doc.Validate(context.Background())
- require.Error(t, err)
- r, err = NewRouter(doc)
- require.Error(t, err)
- r, err = NewRouter(doc, openapi3.DisableExamplesValidation())
- require.NoError(t, err)
-}
diff --git a/routers/legacy/validate_request_test.go b/routers/legacy/validate_request_test.go
deleted file mode 100644
index 9c15ed44a..000000000
--- a/routers/legacy/validate_request_test.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package legacy_test
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/http"
-
- "github.com/getkin/kin-openapi/openapi3"
- "github.com/getkin/kin-openapi/openapi3filter"
- "github.com/getkin/kin-openapi/routers/legacy"
-)
-
-const spec = `
-openapi: 3.0.0
-info:
- title: My API
- version: 0.0.1
-paths:
- /:
- post:
- responses:
- default:
- description: ''
- requestBody:
- required: true
- content:
- application/json:
- schema:
- oneOf:
- - $ref: '#/components/schemas/Cat'
- - $ref: '#/components/schemas/Dog'
- discriminator:
- propertyName: pet_type
-
-components:
- schemas:
- Pet:
- type: object
- required: [pet_type]
- properties:
- pet_type:
- type: string
- discriminator:
- propertyName: pet_type
-
- Dog:
- allOf:
- - $ref: '#/components/schemas/Pet'
- - type: object
- properties:
- breed:
- type: string
- enum: [Dingo, Husky, Retriever, Shepherd]
- Cat:
- allOf:
- - $ref: '#/components/schemas/Pet'
- - type: object
- properties:
- hunts:
- type: boolean
- age:
- type: integer
-`
-
-func Example() {
- loader := openapi3.NewLoader()
- doc, err := loader.LoadFromData([]byte(spec))
- if err != nil {
- panic(err)
- }
- if err := doc.Validate(loader.Context); err != nil {
- panic(err)
- }
-
- router, err := legacy.NewRouter(doc)
- if err != nil {
- panic(err)
- }
-
- p, err := json.Marshal(map[string]interface{}{
- "pet_type": "Cat",
- "breed": "Dingo",
- "bark": true,
- })
- if err != nil {
- panic(err)
- }
-
- req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(p))
- if err != nil {
- panic(err)
- }
- req.Header.Set("Content-Type", "application/json")
-
- route, pathParams, err := router.FindRoute(req)
- if err != nil {
- panic(err)
- }
-
- requestValidationInput := &openapi3filter.RequestValidationInput{
- Request: req,
- PathParams: pathParams,
- Route: route,
- }
- if err := openapi3filter.ValidateRequest(loader.Context, requestValidationInput); err != nil {
- fmt.Println(err)
- }
- // Output:
- // request body has an error: doesn't match schema: input matches more than one oneOf schemas
-
-}
diff --git a/routers/types.go b/routers/types.go
deleted file mode 100644
index 93746cfe9..000000000
--- a/routers/types.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package routers
-
-import (
- "net/http"
-
- "github.com/getkin/kin-openapi/openapi3"
-)
-
-// Router helps link http.Request.s and an OpenAPIv3 spec
-type Router interface {
- // FindRoute matches an HTTP request with the operation it resolves to.
- // Hosts are matched from the OpenAPIv3 servers key.
- //
- // If you experience ErrPathNotFound and have localhost hosts specified as your servers,
- // turning these server URLs as relative (leaving only the path) should resolve this.
- //
- // See openapi3filter for example uses with request and response validation.
- FindRoute(req *http.Request) (route *Route, pathParams map[string]string, err error)
-}
-
-// Route describes the operation an http.Request can match
-type Route struct {
- Spec *openapi3.T
- Server *openapi3.Server
- Path string
- PathItem *openapi3.PathItem
- Method string
- Operation *openapi3.Operation
-}
-
-// ErrPathNotFound is returned when no route match is found
-var ErrPathNotFound error = &RouteError{"no matching operation was found"}
-
-// ErrMethodNotAllowed is returned when no method of the matched route matches
-var ErrMethodNotAllowed error = &RouteError{"method not allowed"}
-
-// RouteError describes Router errors
-type RouteError struct {
- Reason string
-}
-
-func (e *RouteError) Error() string { return e.Reason }