diff --git a/.github/workflows/go-santhoshtekuri.yml b/.github/workflows/go-santhoshtekuri.yml new file mode 100644 index 000000000..3b21e9e25 --- /dev/null +++ b/.github/workflows/go-santhoshtekuri.yml @@ -0,0 +1,88 @@ +name: go-santhoshtekuri +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.14', '1.x'] + # Locked at https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on + os: + - ubuntu-20.04 + - windows-2019 + - macos-10.15 + 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@v2 + 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@v2 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-go-${{ matrix.go }}-mod-${{ hashFiles('**/go.sum') }} + if: matrix.go != '1.14' + + + - uses: actions/checkout@v2 + + - run: go mod download && go mod tidy && go mod verify + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + + - run: go vet ./... + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + + - run: go fmt ./... + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + + - run: go test -tags santhoshtekuri ./... + - run: go test -tags santhoshtekuri -v -run TestRaceyPatternSchema -race ./... + env: + CGO_ENABLED: '1' + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + - run: | + cp openapi3/testdata/load_with_go_embed_test.go openapi3/ + cat go.mod | sed 's%go 1.14%go 1.16%' >gomod && mv gomod go.mod + go test -tags santhoshtekuri ./... + if: matrix.go != '1.14' + + + - 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 "'[%].'" diff --git a/.github/workflows/go-xeiuupv.yml b/.github/workflows/go-xeiuupv.yml new file mode 100644 index 000000000..2fd327b29 --- /dev/null +++ b/.github/workflows/go-xeiuupv.yml @@ -0,0 +1,88 @@ +name: go-xeipuuv +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.14', '1.x'] + # Locked at https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on + os: + - ubuntu-20.04 + - windows-2019 + - macos-10.15 + 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@v2 + 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@v2 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod }} + key: ${{ runner.os }}-go-${{ matrix.go }}-mod-${{ hashFiles('**/go.sum') }} + if: matrix.go != '1.14' + + + - uses: actions/checkout@v2 + + - run: go mod download && go mod tidy && go mod verify + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + + - run: go vet ./... + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + + - run: go fmt ./... + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + + - run: go test -tags xeipuuv ./... + - run: go test -tags xeipuuv -v -run TestRaceyPatternSchema -race ./... + env: + CGO_ENABLED: '1' + - if: runner.os == 'Linux' + run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] + - run: | + cp openapi3/testdata/load_with_go_embed_test.go openapi3/ + cat go.mod | sed 's%go 1.14%go 1.16%' >gomod && mv gomod go.mod + go test -tags xeipuuv ./... + if: matrix.go != '1.14' + + + - 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 "'[%].'" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4a5b87c21..ac25b13da 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -64,8 +64,8 @@ jobs: - if: runner.os == 'Linux' run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]] - - run: go test ./... - - run: go test -v -run TestRaceyPatternSchema -race ./... + - run: go test -tags legacy ./... + - run: go test -tags legacy -v -run TestRaceyPatternSchema -race ./... env: CGO_ENABLED: '1' - if: runner.os == 'Linux' @@ -73,7 +73,7 @@ jobs: - run: | cp openapi3/testdata/load_with_go_embed_test.go openapi3/ cat go.mod | sed 's%go 1.14%go 1.16%' >gomod && mv gomod go.mod - go test ./... + go test -tags legacy ./... if: matrix.go != '1.14' diff --git a/WIP b/WIP new file mode 100644 index 000000000..272cc3dd9 --- /dev/null +++ b/WIP @@ -0,0 +1,7 @@ +https://pkg.go.dev/github.com/santhosh-tekuri/jsonschema/v4#Compiler + (*Compiler).AddResource and (*Compiler).Compile() both require filenames / URLs + How to map that to components $ref? + +https://pkg.go.dev/github.com/qri-io/jsonschema?utm_source=godoc + 1 schema = 1 json.Unmarshal + How to add multiple component schemas? diff --git a/go.mod b/go.mod index f84f470c1..9f6c08872 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,9 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/go-openapi/jsonpointer v0.19.5 github.com/gorilla/mux v1.8.0 + github.com/santhosh-tekuri/jsonschema/v4 v4.0.0 github.com/stretchr/testify v1.5.1 + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonschema v1.2.0 gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 2b289d716..d5b0da09f 100644 --- a/go.sum +++ b/go.sum @@ -19,10 +19,19 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 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/santhosh-tekuri/jsonschema/v4 v4.0.0 h1:5yvbsmEfOQE2+YuRudUhXPzMQK6P74AR3LafM5zrzf0= +github.com/santhosh-tekuri/jsonschema/v4 v4.0.0/go.mod h1:P2UNYJ2xnL4zRyQ2obmD35HiejSJhHTKwMcckhPucG4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 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= diff --git a/openapi3/callback.go b/openapi3/callback.go index 8995e4792..4ea58f0cf 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -7,6 +7,7 @@ import ( "github.com/go-openapi/jsonpointer" ) +// Callbacks represents components' callback mapping type Callbacks map[string]*CallbackRef var _ jsonpointer.JSONPointable = (*Callbacks)(nil) @@ -26,8 +27,9 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) { // Callback is specified by OpenAPI/Swagger standard version 3.0. type Callback map[string]*PathItem -func (value Callback) Validate(ctx context.Context) error { - for _, v := range value { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (cb Callback) Validate(ctx context.Context) error { + for _, v := range cb { if err := v.Validate(ctx); err != nil { return err } diff --git a/openapi3/components.go b/openapi3/components.go index 7acafabf9..c5d7a6ad2 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -2,8 +2,6 @@ package openapi3 import ( "context" - "fmt" - "regexp" "github.com/getkin/kin-openapi/jsoninfo" ) @@ -34,6 +32,7 @@ func (components *Components) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, components) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (components *Components) Validate(ctx context.Context) (err error) { for k, v := range components.Schemas { if err = ValidateIdentifier(k); err != nil { @@ -91,17 +90,3 @@ func (components *Components) Validate(ctx context.Context) (err error) { return } - -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) - -func ValidateIdentifier(value string) error { - if IdentifierRegExp.MatchString(value) { - return nil - } - return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (regexp: %q)", value, identifierPattern) -} diff --git a/openapi3/content.go b/openapi3/content.go index 5edb7d3fa..4a2ccb444 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -104,8 +104,9 @@ func (content Content) Get(mime string) *MediaType { return content["*/*"] } -func (value Content) Validate(ctx context.Context) error { - for _, v := range value { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (content Content) Validate(ctx context.Context) error { + for _, v := range content { // Validate MediaType if err := v.Validate(ctx); err != nil { return err diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 82ad7040b..162cb76fc 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -13,14 +13,15 @@ type Discriminator struct { Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` } -func (value *Discriminator) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(value) +func (discr *Discriminator) MarshalJSON() ([]byte, error) { + return jsoninfo.MarshalStrictStruct(discr) } -func (value *Discriminator) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +func (discr *Discriminator) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, discr) } -func (value *Discriminator) Validate(ctx context.Context) error { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (discr *Discriminator) Validate(ctx context.Context) error { return nil } diff --git a/openapi3/encoding.go b/openapi3/encoding.go index ad48b9160..5ae480ef0 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -61,11 +61,12 @@ func (encoding *Encoding) SerializationMethod() *SerializationMethod { return sm } -func (value *Encoding) Validate(ctx context.Context) error { - if value == nil { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (encoding *Encoding) Validate(ctx context.Context) error { + if encoding == nil { return nil } - for k, v := range value.Headers { + for k, v := range encoding.Headers { if err := ValidateIdentifier(k); err != nil { return nil } @@ -75,7 +76,7 @@ func (value *Encoding) Validate(ctx context.Context) error { } // Validate a media types's serialization method. - sm := value.SerializationMethod() + sm := encoding.SerializationMethod() switch { case sm.Style == SerializationForm && sm.Explode, sm.Style == SerializationForm && !sm.Explode, diff --git a/openapi3/encoding_test.go b/openapi3/encoding_test.go index 5c354540d..1157ee9ea 100644 --- a/openapi3/encoding_test.go +++ b/openapi3/encoding_test.go @@ -16,17 +16,17 @@ func TestEncodingJSON(t *testing.T) { require.NotEmpty(t, data) t.Log("Unmarshal *openapi3.Encoding from JSON") - docA := &Encoding{} - err = json.Unmarshal(encodingJSON, &docA) + enc := &Encoding{} + err = json.Unmarshal(encodingJSON, &enc) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Validate *openapi3.Encoding") - err = docA.Validate(context.Background()) + err = enc.Validate(context.Background()) require.NoError(t, err) t.Log("Ensure representations match") - dataA, err := json.Marshal(docA) + dataA, err := json.Marshal(enc) require.NoError(t, err) require.JSONEq(t, string(data), string(encodingJSON)) require.JSONEq(t, string(data), string(dataA)) diff --git a/openapi3/examples.go b/openapi3/examples.go index f7f90ce54..a04b3377b 100644 --- a/openapi3/examples.go +++ b/openapi3/examples.go @@ -48,6 +48,7 @@ func (example *Example) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, example) } -func (value *Example) Validate(ctx context.Context) error { - return nil // TODO +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (example *Example) Validate(ctx context.Context) error { + return nil // TODO https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#example-object } diff --git a/openapi3/header.go b/openapi3/header.go index 5fdc31771..cffbd4fdf 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -9,12 +9,13 @@ import ( "github.com/go-openapi/jsonpointer" ) +// Headers represents components' header mapping type Headers map[string]*HeaderRef var _ jsonpointer.JSONPointable = (*Headers)(nil) -func (h Headers) JSONLookup(token string) (interface{}, error) { - ref, ok := h[token] +func (hs Headers) JSONLookup(token string) (interface{}, error) { + ref, ok := hs[token] if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } @@ -33,33 +34,34 @@ type Header struct { var _ jsonpointer.JSONPointable = (*Header)(nil) -func (value *Header) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, value) +func (h *Header) UnmarshalJSON(data []byte) error { + return jsoninfo.UnmarshalStrictStruct(data, h) } // SerializationMethod returns a header's serialization method. -func (value *Header) SerializationMethod() (*SerializationMethod, error) { - style := value.Style +func (h *Header) SerializationMethod() (*SerializationMethod, error) { + style := h.Style if style == "" { style = SerializationSimple } explode := false - if value.Explode != nil { - explode = *value.Explode + if h.Explode != nil { + explode = *h.Explode } return &SerializationMethod{Style: style, Explode: explode}, nil } -func (value *Header) Validate(ctx context.Context) error { - if value.Name != "" { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (h *Header) Validate(ctx context.Context) error { + if h.Name != "" { return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map") } - if value.In != "" { + if h.In != "" { return errors.New("header 'in' MUST NOT be specified, it is implicitly in header") } // Validate a parameter's serialization method. - sm, err := value.SerializationMethod() + sm, err := h.SerializationMethod() if err != nil { return err } @@ -70,17 +72,17 @@ func (value *Header) Validate(ctx context.Context) error { return fmt.Errorf("header schema is invalid: %v", e) } - if (value.Schema == nil) == (value.Content == nil) { - e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", value) + if (h.Schema == nil) == (h.Content == nil) { + e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", h) return fmt.Errorf("header schema is invalid: %v", e) } - if schema := value.Schema; schema != nil { + if schema := h.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { return fmt.Errorf("header schema is invalid: %v", err) } } - if content := value.Content; content != nil { + if content := h.Content; content != nil { if err := content.Validate(ctx); err != nil { return fmt.Errorf("header content is invalid: %v", err) } @@ -88,41 +90,41 @@ func (value *Header) Validate(ctx context.Context) error { return nil } -func (value Header) JSONLookup(token string) (interface{}, error) { +func (h Header) JSONLookup(token string) (interface{}, error) { switch token { case "schema": - if value.Schema != nil { - if value.Schema.Ref != "" { - return &Ref{Ref: value.Schema.Ref}, nil + if h.Schema != nil { + if h.Schema.Ref != "" { + return &Ref{Ref: h.Schema.Ref}, nil } - return value.Schema.Value, nil + return h.Schema.Value, nil } case "name": - return value.Name, nil + return h.Name, nil case "in": - return value.In, nil + return h.In, nil case "description": - return value.Description, nil + return h.Description, nil case "style": - return value.Style, nil + return h.Style, nil case "explode": - return value.Explode, nil - case "allowEmptyValue": - return value.AllowEmptyValue, nil + return h.Explode, nil + case "allowEmptyh": + return h.AllowEmptyValue, nil case "allowReserved": - return value.AllowReserved, nil + return h.AllowReserved, nil case "deprecated": - return value.Deprecated, nil + return h.Deprecated, nil case "required": - return value.Required, nil + return h.Required, nil case "example": - return value.Example, nil + return h.Example, nil case "examples": - return value.Examples, nil + return h.Examples, nil case "content": - return value.Content, nil + return h.Content, nil } - v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(h.ExtensionProps, token) return v, err } diff --git a/openapi3/info.go b/openapi3/info.go index 2adffff1a..ff2830c9f 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -26,6 +26,7 @@ func (value *Info) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *Info) Validate(ctx context.Context) error { if contact := value.Contact; contact != nil { if err := contact.Validate(ctx); err != nil { @@ -66,6 +67,7 @@ func (value *Contact) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *Contact) Validate(ctx context.Context) error { return nil } @@ -85,6 +87,7 @@ func (value *License) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *License) Validate(ctx context.Context) error { if value.Name == "" { return errors.New("value of license name must be a non-empty string") diff --git a/openapi3/link.go b/openapi3/link.go index 7d627b8bc..e0df4ca8a 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -9,6 +9,7 @@ import ( "github.com/go-openapi/jsonpointer" ) +// Links repressents component's link mapping type Links map[string]*LinkRef func (l Links) JSONLookup(token string) (interface{}, error) { @@ -44,6 +45,7 @@ func (value *Link) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *Link) Validate(ctx context.Context) error { if value.OperationID == "" && value.OperationRef == "" { return errors.New("missing operationId or operationRef on link") diff --git a/openapi3/loader_paths_test.go b/openapi3/loader_paths_test.go index 584f00e85..80e759601 100644 --- a/openapi3/loader_paths_test.go +++ b/openapi3/loader_paths_test.go @@ -29,11 +29,13 @@ paths: loader := NewLoader() doc, err := loader.LoadFromData([]byte(strings.Replace(spec, "PATH", path, 1))) require.NoError(t, err) + err = doc.Validate(loader.Context) if expectedErr != "" { require.EqualError(t, err, expectedErr) } else { require.NoError(t, err) } + } } diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go index 8fee2f4c2..e0584e084 100644 --- a/openapi3/loader_read_from_uri_func_test.go +++ b/openapi3/loader_read_from_uri_func_test.go @@ -19,7 +19,8 @@ func TestLoaderReadFromURIFunc(t *testing.T) { doc, err := loader.LoadFromFile("recursiveRef/openapi.yml") require.NoError(t, err) require.NotNil(t, doc) - require.NoError(t, doc.Validate(loader.Context)) + 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) } diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go index bd6590364..12af688d1 100644 --- a/openapi3/loader_recursive_ref_test.go +++ b/openapi3/loader_recursive_ref_test.go @@ -12,6 +12,7 @@ func TestLoaderSupportsRecursiveReference(t *testing.T) { doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml") require.NoError(t, err) require.NotNil(t, doc) - require.NoError(t, doc.Validate(loader.Context)) + 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) } diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 2dd0842f6..8c1fcbd3e 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -67,11 +67,12 @@ func (mediaType *MediaType) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, mediaType) } -func (value *MediaType) Validate(ctx context.Context) error { - if value == nil { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (mediaType *MediaType) Validate(ctx context.Context) error { + if mediaType == nil { return nil } - if schema := value.Schema; schema != nil { + if schema := mediaType.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { return err } diff --git a/openapi3/media_type_test.go b/openapi3/media_type_test.go index 099c4b667..405e6ed9f 100644 --- a/openapi3/media_type_test.go +++ b/openapi3/media_type_test.go @@ -15,17 +15,17 @@ func TestMediaTypeJSON(t *testing.T) { require.NotEmpty(t, data) t.Log("Unmarshal *openapi3.MediaType from JSON") - docA := &MediaType{} - err = json.Unmarshal(mediaTypeJSON, &docA) + mt := &MediaType{} + err = json.Unmarshal(mediaTypeJSON, &mt) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Validate *openapi3.MediaType") - err = docA.Validate(context.Background()) + err = mt.Validate(context.Background()) require.NoError(t, err) t.Log("Ensure representations match") - dataA, err := json.Marshal(docA) + dataA, err := json.Marshal(mt) require.NoError(t, err) require.JSONEq(t, string(data), string(mediaTypeJSON)) require.JSONEq(t, string(data), string(dataA)) diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index ee6887727..905f39f3e 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -19,6 +19,8 @@ type T struct { Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + + refd, refdAsReq, refdAsRep schemaLoader } func (doc *T) MarshalJSON() ([]byte, error) { @@ -29,6 +31,17 @@ func (doc *T) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, doc) } +// CompileSchemas needs to be called before any use of VisitData() +func (doc *T) CompileSchemas() error { + if err := doc.compileSchemas(newSchemaValidationSettings(VisitAsRequest())); err != nil { + return err + } + if err := doc.compileSchemas(newSchemaValidationSettings(VisitAsResponse())); err != nil { + return err + } + return doc.compileSchemas(newSchemaValidationSettings()) +} + func (doc *T) AddOperation(path string, method string, operation *Operation) { paths := doc.Paths if paths == nil { @@ -47,8 +60,10 @@ func (doc *T) AddServer(server *Server) { doc.Servers = append(doc.Servers, server) } -func (value *T) Validate(ctx context.Context) error { - if value.OpenAPI == "" { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +// Validation ends with a call to CompileSchemas() +func (doc *T) Validate(ctx context.Context) error { + if doc.OpenAPI == "" { return errors.New("value of openapi must be a non-empty string") } @@ -56,14 +71,14 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid components: %v", e) } - if err := value.Components.Validate(ctx); err != nil { + if err := doc.Components.Validate(ctx); err != nil { return wrap(err) } } { wrap := func(e error) error { return fmt.Errorf("invalid info: %v", e) } - if v := value.Info; v != nil { + if v := doc.Info; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } @@ -74,7 +89,7 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid paths: %v", e) } - if v := value.Paths; v != nil { + if v := doc.Paths; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } @@ -85,7 +100,7 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid security: %v", e) } - if v := value.Security; v != nil { + if v := doc.Security; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } @@ -94,12 +109,12 @@ func (value *T) Validate(ctx context.Context) error { { wrap := func(e error) error { return fmt.Errorf("invalid servers: %v", e) } - if v := value.Servers; v != nil { + if v := doc.Servers; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } } } - return nil + return doc.CompileSchemas() } diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 38d488d4e..2e40c3b67 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -416,6 +416,7 @@ components: } else { require.NoError(t, err) } + }) } } diff --git a/openapi3/operation.go b/openapi3/operation.go index 0de7c421a..dbeae3ba9 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -120,18 +120,19 @@ func (operation *Operation) AddResponse(status int, response *Response) { } } -func (value *Operation) Validate(ctx context.Context) error { - if v := value.Parameters; v != nil { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (operation *Operation) Validate(ctx context.Context) error { + if v := operation.Parameters; v != nil { if err := v.Validate(ctx); err != nil { return err } } - if v := value.RequestBody; v != nil { + if v := operation.RequestBody; v != nil { if err := v.Validate(ctx); err != nil { return err } } - if v := value.Responses; v != nil { + if v := operation.Responses; v != nil { if err := v.Validate(ctx); err != nil { return err } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 2081e4e1d..22650ff72 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -10,12 +10,13 @@ import ( "github.com/go-openapi/jsonpointer" ) +// ParametersMap represents the parameter components type ParametersMap map[string]*ParameterRef var _ jsonpointer.JSONPointable = (*ParametersMap)(nil) -func (p ParametersMap) JSONLookup(token string) (interface{}, error) { - ref, ok := p[token] +func (pm ParametersMap) JSONLookup(token string) (interface{}, error) { + ref, ok := pm[token] if ref == nil || ok == false { return nil, fmt.Errorf("object has no field %q", token) } @@ -31,17 +32,17 @@ type Parameters []*ParameterRef var _ jsonpointer.JSONPointable = (*Parameters)(nil) -func (p Parameters) JSONLookup(token string) (interface{}, error) { +func (parameters 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)) + if index < 0 || index >= len(parameters) { + return nil, fmt.Errorf("index %d out of bounds of array of length %d", index, len(parameters)) } - ref := p[index] + ref := parameters[index] if ref != nil && ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil @@ -64,9 +65,10 @@ func (parameters Parameters) GetByInAndName(in string, name string) *Parameter { return nil } -func (value Parameters) Validate(ctx context.Context) error { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (parameters Parameters) Validate(ctx context.Context) error { dupes := make(map[string]struct{}) - for _, item := range value { + for _, item := range parameters { if v := item.Value; v != nil { key := v.In + ":" + v.Name if _, ok := dupes[key]; ok { @@ -237,11 +239,12 @@ func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error) } } -func (value *Parameter) Validate(ctx context.Context) error { - if value.Name == "" { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (parameter *Parameter) Validate(ctx context.Context) error { + if parameter.Name == "" { return errors.New("parameter name can't be blank") } - in := value.In + in := parameter.In switch in { case ParameterInPath, @@ -249,56 +252,56 @@ func (value *Parameter) Validate(ctx context.Context) error { ParameterInHeader, ParameterInCookie: default: - return fmt.Errorf("parameter can't have 'in' value %q", value.In) + return fmt.Errorf("parameter can't have 'in' value %q", parameter.In) } // Validate a parameter's serialization method. - sm, err := value.SerializationMethod() + sm, err := parameter.SerializationMethod() if err != nil { return err } var smSupported bool switch { - case value.In == ParameterInPath && sm.Style == SerializationSimple && !sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationSimple && sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationLabel && !sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationLabel && sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationMatrix && !sm.Explode, - value.In == ParameterInPath && sm.Style == SerializationMatrix && sm.Explode, - - value.In == ParameterInQuery && sm.Style == SerializationForm && sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationForm && !sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && !sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && !sm.Explode, - value.In == ParameterInQuery && sm.Style == SerializationDeepObject && sm.Explode, - - value.In == ParameterInHeader && sm.Style == SerializationSimple && !sm.Explode, - value.In == ParameterInHeader && sm.Style == SerializationSimple && sm.Explode, - - value.In == ParameterInCookie && sm.Style == SerializationForm && !sm.Explode, - value.In == ParameterInCookie && sm.Style == SerializationForm && sm.Explode: + case parameter.In == ParameterInPath && sm.Style == SerializationSimple && !sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationSimple && sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationLabel && !sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationLabel && sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationMatrix && !sm.Explode, + parameter.In == ParameterInPath && sm.Style == SerializationMatrix && sm.Explode, + + parameter.In == ParameterInQuery && sm.Style == SerializationForm && sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationForm && !sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && !sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && !sm.Explode, + parameter.In == ParameterInQuery && sm.Style == SerializationDeepObject && sm.Explode, + + parameter.In == ParameterInHeader && sm.Style == SerializationSimple && !sm.Explode, + parameter.In == ParameterInHeader && sm.Style == SerializationSimple && sm.Explode, + + parameter.In == ParameterInCookie && sm.Style == SerializationForm && !sm.Explode, + parameter.In == ParameterInCookie && sm.Style == SerializationForm && sm.Explode: 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: %v", value.Name, e) + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) } - if (value.Schema == nil) == (value.Content == nil) { + 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: %v", value.Name, e) + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e) } - if schema := value.Schema; schema != nil { + if schema := parameter.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { - return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, err) + return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err) } } - if content := value.Content; content != nil { + if content := parameter.Content; content != nil { if err := content.Validate(ctx); err != nil { - return fmt.Errorf("parameter %q content is invalid: %v", value.Name, err) + return fmt.Errorf("parameter %q content is invalid: %v", parameter.Name, err) } } return nil diff --git a/openapi3/path_item.go b/openapi3/path_item.go index a66502046..88938b7dd 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -116,8 +116,9 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) { } } -func (value *PathItem) Validate(ctx context.Context) error { - for _, operation := range value.Operations() { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (pathItem *PathItem) Validate(ctx context.Context) error { + for _, operation := range pathItem.Operations() { if err := operation.Validate(ctx); err != nil { return err } diff --git a/openapi3/paths.go b/openapi3/paths.go index bdb87ae7d..ec0da0aca 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -9,16 +9,17 @@ import ( // Paths is specified by OpenAPI/Swagger standard version 3.0. type Paths map[string]*PathItem -func (value Paths) Validate(ctx context.Context) error { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (paths Paths) Validate(ctx context.Context) error { normalizedPaths := make(map[string]string) - for path, pathItem := range value { + for path, pathItem := range paths { if path == "" || path[0] != '/' { return fmt.Errorf("path %q does not start with a forward slash (/)", path) } if pathItem == nil { - value[path] = &PathItem{} - pathItem = value[path] + paths[path] = &PathItem{} + pathItem = paths[path] } normalizedPath, _, varsInPath := normalizeTemplatedPath(path) diff --git a/openapi3/paths_test.go b/openapi3/paths_test.go index 402288b67..5344e6899 100644 --- a/openapi3/paths_test.go +++ b/openapi3/paths_test.go @@ -7,7 +7,8 @@ import ( "github.com/stretchr/testify/require" ) -var emptyPathSpec = ` +func TestPathValidate(t *testing.T) { + const emptyPathSpec = ` openapi: "3.0.0" info: version: 1.0.0 @@ -19,8 +20,6 @@ servers: paths: /pets: ` - -func TestPathValidate(t *testing.T) { doc, err := NewLoader().LoadFromData([]byte(emptyPathSpec)) require.NoError(t, err) err = doc.Paths.Validate(context.Background()) diff --git a/openapi3/race_test.go b/openapi3/race_test.go index 4ac31c38e..2237c2b05 100644 --- a/openapi3/race_test.go +++ b/openapi3/race_test.go @@ -18,7 +18,7 @@ func TestRaceyPatternSchema(t *testing.T) { require.NoError(t, err) visit := func() { - err := schema.VisitJSONString("test") + err := schema.VisitData(nil, "test") require.NoError(t, err) } diff --git a/openapi3/refs.go b/openapi3/refs.go index 4b64035f8..0d611eab7 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -27,6 +27,7 @@ func (value *CallbackRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *CallbackRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -58,6 +59,7 @@ func (value *ExampleRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *ExampleRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -89,6 +91,7 @@ func (value *HeaderRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *HeaderRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -118,6 +121,7 @@ func (value *LinkRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *LinkRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -140,6 +144,7 @@ func (value *ParameterRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *ParameterRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -171,6 +176,7 @@ func (value *ResponseRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *ResponseRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -202,6 +208,7 @@ func (value *RequestBodyRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *RequestBodyRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -240,6 +247,7 @@ func (value *SchemaRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *SchemaRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) @@ -271,6 +279,7 @@ func (value *SecuritySchemeRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (value *SecuritySchemeRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 66b512fa0..d25441bee 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -97,8 +97,9 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, requestBody) } -func (value *RequestBody) Validate(ctx context.Context) error { - if v := value.Content; v != nil { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (requestBody *RequestBody) Validate(ctx context.Context) error { + if v := requestBody.Content; v != nil { if err := v.Validate(ctx); err != nil { return err } diff --git a/openapi3/response.go b/openapi3/response.go index 2ab33aca2..09c0d84e9 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -29,11 +29,12 @@ func (responses Responses) Get(status int) *ResponseRef { return responses[strconv.FormatInt(int64(status), 10)] } -func (value Responses) Validate(ctx context.Context) error { - if len(value) == 0 { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (responses Responses) Validate(ctx context.Context) error { + if len(responses) == 0 { return errors.New("the responses object MUST contain at least one response code") } - for _, v := range value { + for _, v := range responses { if err := v.Validate(ctx); err != nil { return err } @@ -94,12 +95,13 @@ func (response *Response) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, response) } -func (value *Response) Validate(ctx context.Context) error { - if value.Description == nil { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (response *Response) Validate(ctx context.Context) error { + if response.Description == nil { return errors.New("a short description of the response is required") } - if content := value.Content; content != nil { + if content := response.Content; content != nil { if err := content.Validate(ctx); err != nil { return err } diff --git a/openapi3/schema.go b/openapi3/schema.go index 6878ad35d..930ab4b22 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1,38 +1,17 @@ package openapi3 import ( - "bytes" "context" - "encoding/json" "errors" "fmt" - "math" - "math/big" "regexp" "strconv" - "unicode/utf16" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) -var ( - // SchemaErrorDetailsDisabled disables printing of details about schema errors. - SchemaErrorDetailsDisabled = false - - //SchemaFormatValidationDisabled disables validation of schema type formats. - SchemaFormatValidationDisabled = false - - errSchema = errors.New("input does not match the schema") - - // ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema - ErrOneOfConflict = errors.New("input matches more than one oneOf schemas") - - // 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") -) +var errSchema = errors.New("input does not match the schema") // Float64Ptr is a helper for defining OpenAPI schemas. func Float64Ptr(value float64) *float64 { @@ -150,6 +129,12 @@ type Schema struct { var _ jsonpointer.JSONPointable = (*Schema)(nil) +// VisitData validates given data against schema, using components from doc if given. +// It will use #/components/schemas if given doc is non-nil and doc.CompileSchemas() was called. +func (schema *Schema) VisitData(doc *T, data interface{}, opts ...SchemaValidationOption) (err error) { + return schema.visitData(doc, data, opts...) +} + func NewSchema() *Schema { return &Schema{} } @@ -578,1023 +563,7 @@ func (schema *Schema) IsEmpty() bool { return true } -func (value *Schema) Validate(ctx context.Context) error { - return value.validate(ctx, []*Schema{}) -} - -func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { - for _, existing := range stack { - if existing == schema { - 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 - } - } - - for _, item := range schema.AnyOf { - v := item.Value - if v == nil { - return foundUnresolvedRef(item.Ref) - } - if err = v.validate(ctx, stack); err != nil { - return - } - } - - for _, item := range schema.AllOf { - v := item.Value - if v == nil { - return foundUnresolvedRef(item.Ref) - } - if err = v.validate(ctx, stack); err != nil { - return - } - } - - if ref := schema.Not; ref != nil { - v := ref.Value - if v == nil { - return foundUnresolvedRef(ref.Ref) - } - if err = v.validate(ctx, stack); err != nil { - return - } - } - - schemaType := schema.Type - switch schemaType { - case "": - case "boolean": - case "number": - if format := schema.Format; len(format) > 0 { - switch format { - case "float", "double": - default: - if !SchemaFormatValidationDisabled { - return unsupportedFormat(format) - } - } - } - case "integer": - if format := schema.Format; len(format) > 0 { - switch format { - case "int32", "int64": - default: - if !SchemaFormatValidationDisabled { - return unsupportedFormat(format) - } - } - } - case "string": - if format := schema.Format; len(format) > 0 { - switch format { - // Supported by OpenAPIv3.0.1: - case "byte", "binary", "date", "date-time", "password": - // 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 && !SchemaFormatValidationDisabled { - return unsupportedFormat(format) - } - } - } - if schema.Pattern != "" { - if err = schema.compilePattern(); err != nil { - return err - } - } - case "array": - if schema.Items == nil { - return errors.New("when schema type is 'array', schema 'items' must be non-null") - } - case "object": - default: - return fmt.Errorf("unsupported 'type' value %q", schemaType) - } - - if ref := schema.Items; ref != nil { - v := ref.Value - if v == nil { - return foundUnresolvedRef(ref.Ref) - } - if err = v.validate(ctx, stack); err != nil { - return - } - } - - for _, ref := range schema.Properties { - v := ref.Value - if v == nil { - return foundUnresolvedRef(ref.Ref) - } - if err = v.validate(ctx, stack); err != nil { - return - } - } - - 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 - } - } - - return -} - -func (schema *Schema) IsMatching(value interface{}) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONBoolean(value bool) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONNumber(value float64) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONString(value string) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONArray(value []interface{}) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) IsMatchingJSONObject(value map[string]interface{}) bool { - settings := newSchemaValidationSettings(FailFast()) - return schema.visitJSON(settings, value) == nil -} - -func (schema *Schema) VisitJSON(value interface{}, opts ...SchemaValidationOption) error { - settings := newSchemaValidationSettings(opts...) - return schema.visitJSON(settings, value) -} - -func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interface{}) (err error) { - switch value := value.(type) { - case nil: - return schema.visitJSONNull(settings) - case float64: - if math.IsNaN(value) { - return ErrSchemaInputNaN - } - if math.IsInf(value, 0) { - return ErrSchemaInputInf - } - } - - if schema.IsEmpty() { - return - } - if err = schema.visitSetOperations(settings, value); err != nil { - return - } - - switch value := value.(type) { - case nil: - return schema.visitJSONNull(settings) - case bool: - return schema.visitJSONBoolean(settings, value) - case float64: - return schema.visitJSONNumber(settings, value) - case string: - return schema.visitJSONString(settings, value) - case []interface{}: - return schema.visitJSONArray(settings, value) - case map[string]interface{}: - return schema.visitJSONObject(settings, value) - default: - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "type", - Reason: fmt.Sprintf("unhandled value of type %T", value), - } - } -} - -func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) { - if enum := schema.Enum; len(enum) != 0 { - for _, v := range enum { - if value == v { - return - } - } - if settings.failfast { - return errSchema - } - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "enum", - Reason: "value is not one of the allowed values", - } - } - - if ref := schema.Not; ref != nil { - v := ref.Value - if v == nil { - return foundUnresolvedRef(ref.Ref) - } - err := v.visitJSON(settings, value) - if err == nil { - if settings.failfast { - return errSchema - } - return &SchemaError{ - 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 errors.New("input does not contain the discriminator property") - } - - if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorVal.(string)]; len(schema.Discriminator.Mapping) > 0 && !okcheck { - return errors.New("input does not contain a valid discriminator value") - } - } - } - - ok := 0 - validationErrors := []error{} - for _, item := range v { - v := item.Value - if v == nil { - return foundUnresolvedRef(item.Ref) - } - - if discriminatorRef != "" && discriminatorRef != item.Ref { - continue - } - - err := v.visitJSON(settings, value) - if err != nil { - validationErrors = append(validationErrors, err) - continue - } - - ok++ - } - - if ok != 1 { - if len(validationErrors) > 1 { - errorMessage := "" - for _, err := range validationErrors { - if errorMessage != "" { - errorMessage += " Or " - } - errorMessage += err.Error() - } - return errors.New("doesn't match schema due to: " + errorMessage) - } - if settings.failfast { - return errSchema - } - e := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "oneOf", - } - if ok > 1 { - e.Origin = ErrOneOfConflict - } else if len(validationErrors) == 1 { - e.Origin = validationErrors[0] - } - - return e - } - } - - if v := schema.AnyOf; len(v) > 0 { - ok := false - for _, item := range v { - v := item.Value - if v == nil { - return foundUnresolvedRef(item.Ref) - } - err := v.visitJSON(settings, value) - if err == nil { - ok = true - break - } - } - if !ok { - if settings.failfast { - return errSchema - } - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "anyOf", - } - } - } - - for _, item := range schema.AllOf { - v := item.Value - if v == nil { - return foundUnresolvedRef(item.Ref) - } - err := v.visitJSON(settings, value) - if err != nil { - if settings.failfast { - return errSchema - } - return &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "allOf", - Origin: err, - } - } - } - return -} - -func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) { - if schema.Nullable { - return - } - if settings.failfast { - return errSchema - } - return &SchemaError{ - 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) -} - -func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) { - if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" { - return schema.expectedType(settings, "boolean") - } - return -} - -func (schema *Schema) VisitJSONNumber(value float64) error { - settings := newSchemaValidationSettings() - return schema.visitJSONNumber(settings, value) -} - -func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error { - var me MultiError - schemaType := schema.Type - if schemaType == "integer" { - if bigFloat := big.NewFloat(value); !bigFloat.IsInt() { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "type", - Reason: "Value must be an integer", - } - if !settings.multiError { - return err - } - me = append(me, err) - } - } else if schemaType != "" && schemaType != "number" { - return schema.expectedType(settings, "number, integer") - } - - // "exclusiveMinimum" - if v := schema.ExclusiveMin; v && !(*schema.Min < value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "exclusiveMinimum", - Reason: fmt.Sprintf("number must be more than %g", *schema.Min), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "exclusiveMaximum" - if v := schema.ExclusiveMax; v && !(*schema.Max > value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "exclusiveMaximum", - Reason: fmt.Sprintf("number must be less than %g", *schema.Max), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "minimum" - if v := schema.Min; v != nil && !(*v <= value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minimum", - Reason: fmt.Sprintf("number must be at least %g", *v), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "maximum" - if v := schema.Max; v != nil && !(*v >= value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maximum", - Reason: fmt.Sprintf("number must be most %g", *v), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "multipleOf" - if v := schema.MultipleOf; v != nil { - // "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 { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "multipleOf", - } - if !settings.multiError { - return err - } - me = append(me, err) - } - } - - if len(me) > 0 { - return me - } - - return nil -} - -func (schema *Schema) VisitJSONString(value string) error { - settings := newSchemaValidationSettings() - return schema.visitJSONString(settings, value) -} - -func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error { - if schemaType := schema.Type; schemaType != "" && schemaType != "string" { - return schema.expectedType(settings, "string") - } - - var me MultiError - - // "minLength" and "maxLength" - minLength := schema.MinLength - maxLength := schema.MaxLength - if minLength != 0 || maxLength != nil { - // JSON schema string lengths are UTF-16, not UTF-8! - length := int64(0) - for _, r := range value { - if utf16.IsSurrogate(r) { - length += 2 - } else { - length++ - } - } - if minLength != 0 && length < int64(minLength) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minLength", - Reason: fmt.Sprintf("minimum string length is %d", minLength), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - if maxLength != nil && length > int64(*maxLength) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maxLength", - Reason: fmt.Sprintf("maximum string length is %d", *maxLength), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - } - - // "pattern" - if schema.Pattern != "" && schema.compiledPattern == nil { - var err error - if err = schema.compilePattern(); err != nil { - if !settings.multiError { - return 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 %q", schema.Pattern), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "format" - var formatErr string - 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) { - formatErr = fmt.Sprintf("string doesn't match the format %q (regular expression %q)", format, cp.String()) - } - case f.regexp == nil && f.callback != nil: - if err := f.callback(value); err != nil { - formatErr = err.Error() - } - default: - formatErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format) - } - } - } - if formatErr != "" { - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "format", - Reason: formatErr, - } - if !settings.multiError { - return err - } - me = append(me, err) - - } - - if len(me) > 0 { - return me - } - - return nil -} - -func (schema *Schema) VisitJSONArray(value []interface{}) error { - settings := newSchemaValidationSettings() - return schema.visitJSONArray(settings, value) -} - -func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error { - if schemaType := schema.Type; schemaType != "" && schemaType != "array" { - return schema.expectedType(settings, "array") - } - - var me MultiError - - lenValue := int64(len(value)) - - // "minItems" - if v := schema.MinItems; v != 0 && lenValue < int64(v) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minItems", - Reason: fmt.Sprintf("minimum number of items is %d", v), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "maxItems" - if v := schema.MaxItems; v != nil && lenValue > int64(*v) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maxItems", - Reason: fmt.Sprintf("maximum number of items is %d", *v), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "uniqueItems" - if sliceUniqueItemsChecker == nil { - sliceUniqueItemsChecker = isSliceOfUniqueItems - } - if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "uniqueItems", - Reason: "duplicate items found", - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "items" - if itemSchemaRef := schema.Items; itemSchemaRef != nil { - itemSchema := itemSchemaRef.Value - if itemSchema == nil { - 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 len(me) > 0 { - return me - } - - return nil -} - -func (schema *Schema) VisitJSONObject(value map[string]interface{}) error { - settings := newSchemaValidationSettings() - return schema.visitJSONObject(settings, value) -} - -func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error { - if schemaType := schema.Type; schemaType != "" && schemaType != "object" { - return schema.expectedType(settings, "object") - } - - var me MultiError - - // "properties" - properties := schema.Properties - lenValue := int64(len(value)) - - // "minProperties" - if v := schema.MinProps; v != 0 && lenValue < int64(v) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "minProperties", - Reason: fmt.Sprintf("there must be at least %d properties", v), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "maxProperties" - if v := schema.MaxProps; v != nil && lenValue > int64(*v) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "maxProperties", - Reason: fmt.Sprintf("there must be at most %d properties", *v), - } - if !settings.multiError { - return err - } - me = append(me, err) - } - - // "additionalProperties" - var additionalProperties *Schema - if ref := schema.AdditionalProperties; ref != nil { - additionalProperties = ref.Value - } - for k, v := range value { - if properties != nil { - propertyRef := properties[k] - if propertyRef != nil { - p := propertyRef.Value - if p == nil { - return foundUnresolvedRef(propertyRef.Ref) - } - if err := p.visitJSON(settings, v); err != nil { - if settings.failfast { - 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) - } - continue - } - } - 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 { - 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) - } - } - continue - } - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "properties", - Reason: fmt.Sprintf("property %q is unsupported", k), - } - if !settings.multiError { - return err - } - 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 { - return errSchema - } - err := markSchemaErrorKey(&SchemaError{ - Value: value, - Schema: schema, - SchemaField: "required", - Reason: fmt.Sprintf("property %q is missing", k), - }, k) - if !settings.multiError { - return err - } - me = append(me, err) - } - } - - if len(me) > 0 { - return me - } - - return nil -} - -func (schema *Schema) expectedType(settings *schemaValidationSettings, typ string) error { - if settings.failfast { - return errSchema - } - return &SchemaError{ - 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", - Reason: fmt.Sprintf("cannot compile pattern %q: %v", schema.Pattern, err), - } - } - return nil -} - -type SchemaError struct { - Value interface{} - reversePath []string - Schema *Schema - SchemaField string - Reason string - Origin error -} - -func markSchemaErrorKey(err error, key string) error { - 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 { - if v, ok := err.(*SchemaError); ok { - v.reversePath = append(v.reversePath, strconv.FormatInt(int64(index), 10)) - return v - } - if v, ok := err.(MultiError); ok { - for _, e := range v { - _ = markSchemaErrorIndex(e, index) - } - return v - } - return err -} - -func (err *SchemaError) JSONPointer() []string { - reversePath := err.reversePath - path := append([]string(nil), reversePath...) - for left, right := 0, len(path)-1; left < right; left, right = left+1, right-1 { - path[left], path[right] = path[right], path[left] - } - return path -} - -func (err *SchemaError) Error() string { - 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 - for i := len(reversePath) - 1; i >= 0; i-- { - buf.WriteByte('/') - buf.WriteString(reversePath[i]) - } - buf.WriteString(`": `) - } - reason := err.Reason - if reason == "" { - buf.WriteString(`Doesn't match schema "`) - buf.WriteString(err.SchemaField) - buf.WriteString(`"`) - } else { - buf.WriteString(reason) - } - if !SchemaErrorDetailsDisabled { - buf.WriteString("\nSchema:\n ") - encoder := json.NewEncoder(buf) - encoder.SetIndent(" ", " ") - if err := encoder.Encode(err.Schema); err != nil { - panic(err) - } - buf.WriteString("\nValue:\n ") - if err := encoder.Encode(err.Value); err != nil { - panic(err) - } - } - return buf.String() -} - -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{}{} - } - return s == len(m) -} - -// SliceUniqueItemsChecker is an function used to check if an given slice -// have unique items. -type SliceUniqueItemsChecker func(items []interface{}) bool - -// By default using predefined func isSliceOfUniqueItems which make use of -// json.Marshal to generate a key for map used to check if a given slice -// have unique items. -var sliceUniqueItemsChecker SliceUniqueItemsChecker = isSliceOfUniqueItems - -// RegisterArrayUniqueItemsChecker is used to register a customized function -// used to check if JSON array have unique items. -func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) { - sliceUniqueItemsChecker = fn -} - -func unsupportedFormat(format string) error { - return fmt.Errorf("unsupported 'format' value %q", format) +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (schema *Schema) Validate(ctx context.Context) error { + return schema.validate(ctx, []*Schema{}) } diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 1eb41509e..55dbf844c 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -1,3 +1,5 @@ +// +build legacy + package openapi3 import ( @@ -6,11 +8,6 @@ import ( "regexp" ) -const ( - // FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122 - 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 custom check on exotic formats type FormatCallback func(Val string) error @@ -103,3 +100,9 @@ func DefineIPv4Format() { func DefineIPv6Format() { DefineStringFormatCallback("ipv6", validateIPv6) } + +// DefineUUIDFormat defines a string format for UUID v1-v5 as specified by RFC4122 +func DefineUUIDFormat() { + const FormatOfStringForUUIDOfRFC4122 = `^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$` + DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) +} diff --git a/openapi3/schema_formats_xeipuuv.go b/openapi3/schema_formats_xeipuuv.go new file mode 100644 index 000000000..7f246d999 --- /dev/null +++ b/openapi3/schema_formats_xeipuuv.go @@ -0,0 +1,52 @@ +// +build xeipuuv + +package openapi3 + +import ( + "regexp" + + "github.com/xeipuuv/gojsonschema" + // https://github.com/xeipuuv/gojsonschema/pull/297/files discriminator support +) + +func init() { + // gojsonschema.FormatCheckers = gojsonschema.FormatCheckerChain{} FIXME https://github.com/xeipuuv/gojsonschema/pull/326 + gojsonschema.FormatCheckers.Add("byte", byteFormatChecker{}) + gojsonschema.FormatCheckers.Add("date", gojsonschema.DateFormatChecker{}) + gojsonschema.FormatCheckers.Add("date-time", gojsonschema.DateTimeFormatChecker{}) +} + +type byteFormatChecker struct{} + +var _ gojsonschema.FormatChecker = (*byteFormatChecker)(nil) +var reByteFormatChecker = regexp.MustCompile(`(^$|^[a-zA-Z0-9+/\-_]*=*$)`) + +// IsFormat supports base64 and base64url. Padding ('=') is supported. +func (byteFormatChecker) IsFormat(input interface{}) bool { + asString, ok := input.(string) + if !ok { + return true + } + + return reByteFormatChecker.MatchString(asString) +} + +// DefineEmailFormat opts-in to checking email format (outside of OpenAPIv3 spec) +func DefineEmailFormat() { + gojsonschema.FormatCheckers.Add("email", gojsonschema.EmailFormatChecker{}) +} + +// DefineUUIDFormat opts-in to checking uuid format v1-v5 as specified by RFC4122 (outside of OpenAPIv3 spec) +func DefineUUIDFormat() { + gojsonschema.FormatCheckers.Add("uuid", gojsonschema.UUIDFormatChecker{}) +} + +// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec +func DefineIPv4Format() { + gojsonschema.FormatCheckers.Add("ipv4", gojsonschema.IPV4FormatChecker{}) +} + +// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec +func DefineIPv6Format() { + gojsonschema.FormatCheckers.Add("ipv6", gojsonschema.IPV6FormatChecker{}) +} diff --git a/openapi3/schema_from_openapi.go b/openapi3/schema_from_openapi.go new file mode 100644 index 000000000..25f0b9c17 --- /dev/null +++ b/openapi3/schema_from_openapi.go @@ -0,0 +1,147 @@ +package openapi3 + +type schemaJSON = map[string]interface{} +type schemasJSON = map[string]schemaJSON + +func (s *SchemaRef) fromOpenAPISchema(settings *schemaValidationSettings) (schema schemaJSON) { + if ref := s.Ref; ref != "" { + return schemaJSON{"$ref": ref} + } + return s.Value.fromOpenAPISchema(settings) +} + +func (s *Schema) fromOpenAPISchema(settings *schemaValidationSettings) (schema schemaJSON) { + schema = make(schemaJSON) + + if sEnum := s.Enum; len(sEnum) != 0 { + schema["enum"] = sEnum + } + + if sMinLength := s.MinLength; sMinLength != 0 { + schema["minLength"] = sMinLength + } + if sMaxLength := s.MaxLength; nil != sMaxLength { + schema["maxLength"] = *sMaxLength + } + + if sFormat := s.Format; sFormat != "" { + schema["format"] = sFormat + } + if sPattern := s.Pattern; sPattern != "" { + schema["pattern"] = sPattern + } + + if nil != s.Min { + schema["minimum"] = *s.Min + } + if nil != s.Max { + schema["maximum"] = *s.Max + } + if sExMin := s.ExclusiveMin; sExMin { + schema["exclusiveMinimum"] = sExMin + } + if sExMax := s.ExclusiveMax; sExMax { + schema["exclusiveMaximum"] = sExMax + } + if nil != s.MultipleOf { + schema["multipleOf"] = *s.MultipleOf + } + + if sUniq := s.UniqueItems; sUniq { + schema["uniqueItems"] = sUniq + } + if sMinItems := s.MinItems; sMinItems != 0 { + schema["minItems"] = sMinItems + } + if nil != s.MaxItems { + schema["maxItems"] = *s.MaxItems + } + if sItems := s.Items; nil != sItems { + if sItems.Value != nil && sItems.Value.IsEmpty() { + schema["items"] = []schemaJSON{} + } else { + schema["items"] = []schemaJSON{sItems.fromOpenAPISchema(settings)} + } + } + + if sMinProps := s.MinProps; sMinProps != 0 { + schema["minProperties"] = sMinProps + } + if nil != s.MaxProps { + schema["maxProperties"] = *s.MaxProps + } + + if sRequired := s.Required; len(sRequired) != 0 { + required := make([]string, 0, len(sRequired)) + for _, propName := range sRequired { + prop := s.Properties[propName] + switch { + case settings.asreq && prop != nil && prop.Value.ReadOnly: + case settings.asrep && prop != nil && prop.Value.WriteOnly: + default: + required = append(required, propName) + } + } + schema["required"] = required + } + + if count := len(s.Properties); count != 0 { + properties := make(schemasJSON, count) + for propName, prop := range s.Properties { + properties[propName] = prop.fromOpenAPISchema(settings) + } + schema["properties"] = properties + } + + if sAddProps := s.AdditionalPropertiesAllowed; sAddProps != nil { + // TODO: complete handling + schema["additionalProperties"] = sAddProps + } + + if sAllOf := s.AllOf; len(sAllOf) != 0 { + allOf := make([]schemaJSON, 0, len(sAllOf)) + for _, sOf := range sAllOf { + allOf = append(allOf, sOf.fromOpenAPISchema(settings)) + } + schema["allOf"] = allOf + } + if sAnyOf := s.AnyOf; len(sAnyOf) != 0 { + anyOf := make([]schemaJSON, 0, len(sAnyOf)) + for _, sOf := range sAnyOf { + anyOf = append(anyOf, sOf.fromOpenAPISchema(settings)) + } + schema["anyOf"] = anyOf + } + if sOneOf := s.OneOf; len(sOneOf) != 0 { + oneOf := make([]schemaJSON, 0, len(sOneOf)) + for _, sOf := range sOneOf { + oneOf = append(oneOf, sOf.fromOpenAPISchema(settings)) + } + schema["oneOf"] = oneOf + } + + if sType := s.Type; sType != "" { + schema["type"] = []string{s.Type} + } + + if sNot := s.Not; sNot != nil { + schema["not"] = sNot.fromOpenAPISchema(settings) + } + + if s.IsEmpty() { + schema = schemaJSON{"not": schemaJSON{"type": "null"}} + } + + if s.Nullable { + schema = schemaJSON{"anyOf": []schemaJSON{ + {"type": "null"}, + schema, + }} + } + + schema["$schema"] = "http://json-schema.org/draft-04/schema#" + //FIXME + //https://github.com/openapi-contrib/openapi-schema-to-json-schema/blob/45c080c38027c30652263b4cc44cd3534f5ccc1b/lib/converters/schema.js + //https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schemaObject + return +} diff --git a/openapi3/schema_issue289_test.go b/openapi3/schema_issue289_test.go index 6ab6b63d5..9e5190336 100644 --- a/openapi3/schema_issue289_test.go +++ b/openapi3/schema_issue289_test.go @@ -27,13 +27,21 @@ func TestIssue289(t *testing.T) { 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" +info: + title: An API + version: v1 +paths: {} `) - s, err := NewLoader().LoadFromData(spec) + loader := NewLoader() + doc, err := loader.LoadFromData(spec) require.NoError(t, err) - err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitData(doc, map[string]interface{}{ "name": "kin-openapi", "address": "127.0.0.1", }) - require.EqualError(t, err, ErrOneOfConflict.Error()) + require.Contains(t, err.Error(), "oneOf") } diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go index 03fb670b1..f8588ca82 100644 --- a/openapi3/schema_oneOf_test.go +++ b/openapi3/schema_oneOf_test.go @@ -1,12 +1,19 @@ package openapi3 import ( + "strings" "testing" "github.com/stretchr/testify/require" ) -var oneofSpec = []byte(`components: +const oneofSpec = ` +openapi: "3.0.1" +info: + title: An API + version: v1 +paths: {} +components: schemas: Cat: type: object @@ -18,11 +25,11 @@ var oneofSpec = []byte(`components: $type: type: string enum: - - cat + - cat required: - - name - - scratches - - $type + - name + - scratches + - $type Dog: type: object properties: @@ -33,24 +40,30 @@ var oneofSpec = []byte(`components: $type: type: string enum: - - dog + - dog required: - - name - - barks - - $type + - name + - barks + - $type Animal: type: object oneOf: - - $ref: "#/components/schemas/Cat" - - $ref: "#/components/schemas/Dog" + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" discriminator: propertyName: $type mapping: cat: "#/components/schemas/Cat" dog: "#/components/schemas/Dog" -`) +` -var oneofNoDiscriminatorSpec = []byte(`components: +const oneofNoDiscriminatorSpec = ` +openapi: "3.0.1" +info: + title: An API + version: v1 +paths: {} +components: schemas: Cat: type: object @@ -60,8 +73,8 @@ var oneofNoDiscriminatorSpec = []byte(`components: scratches: type: boolean required: - - name - - scratches + - name + - scratches Dog: type: object properties: @@ -70,49 +83,63 @@ var oneofNoDiscriminatorSpec = []byte(`components: barks: type: boolean required: - - name - - barks + - name + - barks Animal: type: object oneOf: - - $ref: "#/components/schemas/Cat" - - $ref: "#/components/schemas/Dog" -`) + - $ref: "#/components/schemas/Cat" + - $ref: "#/components/schemas/Dog" +` -func TestVisitJSON_OneOf_MissingDiscriptorProperty(t *testing.T) { - s, err := NewLoader().LoadFromData(oneofSpec) +func TestVisitData_OneOf_MissingDiscriptorProperty(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(oneofSpec)) require.NoError(t, err) - err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err = doc.Validate(loader.Context) + require.NoError(t, err) + err = doc.Components.Schemas["Animal"].Value.VisitData(doc, map[string]interface{}{ "name": "snoopy", }) require.EqualError(t, err, "input does not contain the discriminator property") } -func TestVisitJSON_OneOf_MissingDiscriptorValue(t *testing.T) { - s, err := NewLoader().LoadFromData(oneofSpec) +func TestVisitData_OneOf_MissingDiscriptorValue(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(oneofSpec)) + require.NoError(t, err) + err = doc.Validate(loader.Context) require.NoError(t, err) - err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err = doc.Components.Schemas["Animal"].Value.VisitData(doc, map[string]interface{}{ "name": "snoopy", "$type": "snake", }) require.EqualError(t, err, "input does not contain a valid discriminator value") } -func TestVisitJSON_OneOf_MissingField(t *testing.T) { - s, err := NewLoader().LoadFromData(oneofSpec) +func TestVisitData_OneOf_MissingField(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(oneofSpec)) require.NoError(t, err) - err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err = doc.Validate(loader.Context) + require.NoError(t, err) + err = doc.Components.Schemas["Animal"].Value.VisitData(doc, map[string]interface{}{ "name": "snoopy", "$type": "dog", }) - require.EqualError(t, err, "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") + require.Contains(t, err.Error(), "barks") + require.True(t, strings.Contains(err.Error(), "is required") || strings.Contains(err.Error(), "is missing")) } -func TestVisitJSON_OneOf_NoDiscriptor_MissingField(t *testing.T) { - s, err := NewLoader().LoadFromData(oneofNoDiscriminatorSpec) +func TestVisitData_OneOf_NoDiscriptor_MissingField(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(oneofNoDiscriminatorSpec)) + require.NoError(t, err) + err = doc.Validate(loader.Context) require.NoError(t, err) - err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err = doc.Components.Schemas["Animal"].Value.VisitData(doc, 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") + require.Contains(t, err.Error(), "scratches") + require.True(t, strings.Contains(err.Error(), "is required") || strings.Contains(err.Error(), "is missing")) } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index f724f08e2..7a18354a5 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -14,7 +14,6 @@ import ( ) type schemaExample struct { - Title string Schema *Schema Serialization interface{} AllValid []interface{} @@ -22,9 +21,9 @@ type schemaExample struct { } func TestSchemas(t *testing.T) { - DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) - for _, example := range schemaExamples { - t.Run(example.Title, testSchema(t, example)) + DefineUUIDFormat() + for title, example := range schemaExamples { + t.Run(title, testSchema(t, example)) } } @@ -55,7 +54,7 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) { } // NaN and Inf aren't valid JSON but are handled for _, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} { - err := schema.VisitJSON(value) + err := schema.VisitData(nil, value) require.Error(t, err) } } @@ -67,12 +66,11 @@ func validateSchema(t *testing.T, schema *Schema, value interface{}, opts ...Sch var val interface{} err = json.Unmarshal(data, &val) require.NoError(t, err) - return schema.VisitJSON(val, opts...) + return schema.VisitData(nil, val, opts...) } -var schemaExamples = []schemaExample{ - { - Title: "EMPTY SCHEMA", +var schemaExamples = map[string]schemaExample{ + "EMPTY SCHEMA": { Schema: &Schema{}, Serialization: map[string]interface{}{ // This OA3 schema is exactly this draft-04 schema: @@ -91,8 +89,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "JUST NULLABLE", + "JUST NULLABLE": { Schema: NewSchema().WithNullable(), Serialization: map[string]interface{}{ // This OA3 schema is exactly both this draft-04 schema: {} and: @@ -113,8 +110,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "NULLABLE BOOLEAN", + "NULLABLE BOOLEAN": { Schema: NewBoolSchema().WithNullable(), Serialization: map[string]interface{}{ "nullable": true, @@ -135,8 +131,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "NULLABLE ANYOF", + "NULLABLE ANYOF": { Schema: NewAnyOfSchema( NewIntegerSchema(), NewFloat64Schema(), @@ -161,8 +156,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "BOOLEAN", + "BOOLEAN": { Schema: NewBoolSchema(), Serialization: map[string]interface{}{ "type": "boolean", @@ -180,8 +174,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "NUMBER", + "NUMBER": { Schema: NewFloat64Schema(). WithMin(2.5). WithMax(3.5), @@ -207,8 +200,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "INTEGER", + "INTEGER": { Schema: NewInt64Schema(). WithMin(2). WithMax(5), @@ -235,8 +227,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "STRING", + "STRING": { Schema: NewStringSchema(). WithMinLength(2). WithMaxLength(3). @@ -264,8 +255,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "STRING: optional format 'uuid'", + "STRING: optional format 'uuid'": { Schema: NewUUIDSchema(), Serialization: map[string]interface{}{ "type": "string", @@ -285,8 +275,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "STRING: format 'date-time'", + "STRING: format 'date-time'": { Schema: NewDateTimeSchema(), Serialization: map[string]interface{}{ "type": "string", @@ -310,8 +299,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "STRING: format 'date-time'", + "STRING: format 'byte'": { Schema: NewBytesSchema(), Serialization: map[string]interface{}{ "type": "string", @@ -342,8 +330,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "ARRAY", + "ARRAY": { Schema: &Schema{ Type: "array", MinItems: 2, @@ -382,8 +369,8 @@ var schemaExamples = []schemaExample{ }, }, }, - { - Title: "ARRAY : items format 'object'", + + "ARRAY : items format 'object'": { Schema: &Schema{ Type: "array", UniqueItems: true, @@ -439,8 +426,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "ARRAY : items format 'object' and object with a property of array type ", + "ARRAY : items format 'object' and object with a property of array type ": { Schema: &Schema{ Type: "array", UniqueItems: true, @@ -525,8 +511,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "ARRAY : items format 'array'", + "ARRAY : items format 'array'": { Schema: &Schema{ Type: "array", UniqueItems: true, @@ -569,8 +554,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "ARRAY : items format 'array' and array with object type items", + "ARRAY : items format 'array' and array with object type items": { Schema: &Schema{ Type: "array", UniqueItems: true, @@ -673,8 +657,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "OBJECT", + "OBJECT": { Schema: &Schema{ Type: "object", MaxProps: Uint64Ptr(2), @@ -718,7 +701,8 @@ var schemaExamples = []schemaExample{ }, }, }, - { + + "OBJECT: 2": { Schema: &Schema{ Type: "object", AdditionalProperties: &SchemaRef{ @@ -746,7 +730,8 @@ var schemaExamples = []schemaExample{ }, }, }, - { + + "OBJECT: 3": { Schema: &Schema{ Type: "object", AdditionalPropertiesAllowed: BoolPtr(true), @@ -764,8 +749,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "NOT", + "NOT": { Schema: &Schema{ Not: &SchemaRef{ Value: &Schema{ @@ -801,8 +785,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "ANY OF", + "ANY OF": { Schema: &Schema{ AnyOf: []*SchemaRef{ { @@ -842,8 +825,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "ALL OF", + "ALL OF": { Schema: &Schema{ AllOf: []*SchemaRef{ { @@ -883,8 +865,7 @@ var schemaExamples = []schemaExample{ }, }, - { - Title: "ONE OF", + "ONE OF": { Schema: &Schema{ OneOf: []*SchemaRef{ { @@ -926,37 +907,41 @@ var schemaExamples = []schemaExample{ } type schemaTypeExample struct { - Title string Schema *Schema AllValid []string AllInvalid []string } func TestTypes(t *testing.T) { - for _, example := range typeExamples { - t.Run(example.Title, testType(t, example)) + for title, example := range typeExamples { + testType(t, title, example) } } -func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { - return func(t *testing.T) { - baseSchema := example.Schema - for _, typ := range example.AllValid { +func testType(t *testing.T, title string, example schemaTypeExample) { + baseSchema := example.Schema + for _, typ := range example.AllValid { + t.Run(title+"_Valid_"+typ, func(t *testing.T) { schema := baseSchema.WithFormat(typ) err := schema.Validate(context.Background()) require.NoError(t, err) - } - for _, typ := range example.AllInvalid { + }) + } + for _, typ := range example.AllInvalid { + t.Run(title+"_Invalid_"+typ, func(t *testing.T) { schema := baseSchema.WithFormat(typ) err := schema.Validate(context.Background()) - require.Error(t, err) - } + if SchemaFormatValidationDisabled { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) } } -var typeExamples = []schemaTypeExample{ - { - Title: "STRING", +var typeExamples = map[string]schemaTypeExample{ + "STRING": { Schema: NewStringSchema(), AllValid: []string{ "", @@ -973,8 +958,7 @@ var typeExamples = []schemaTypeExample{ }, }, - { - Title: "NUMBER", + "NUMBER": { Schema: NewFloat64Schema(), AllValid: []string{ "", @@ -986,8 +970,7 @@ var typeExamples = []schemaTypeExample{ }, }, - { - Title: "INTEGER", + "INTEGER": { Schema: NewIntegerSchema(), AllValid: []string{ "", @@ -1001,8 +984,8 @@ var typeExamples = []schemaTypeExample{ } func TestSchemaErrors(t *testing.T) { - for _, example := range schemaErrorExamples { - t.Run(example.Title, testSchemaError(t, example)) + for title, example := range schemaErrorExamples { + t.Run(title, testSchemaError(t, example)) } } @@ -1014,14 +997,12 @@ func testSchemaError(t *testing.T, example schemaErrorExample) func(*testing.T) } type schemaErrorExample struct { - Title string Error *SchemaError Want string } -var schemaErrorExamples = []schemaErrorExample{ - { - Title: "SIMPLE", +var schemaErrorExamples = map[string]schemaErrorExample{ + "SIMPLE": { Error: &SchemaError{ Value: 1, Schema: &Schema{}, @@ -1029,8 +1010,8 @@ var schemaErrorExamples = []schemaErrorExample{ }, Want: "SIMPLE", }, - { - Title: "NEST", + + "NEST": { Error: &SchemaError{ Value: 1, Schema: &Schema{}, @@ -1046,15 +1027,14 @@ 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)) + for title, example := range schemaMultiErrorExamples { + t.Run(title, testSchemaMultiError(t, example)) } } @@ -1088,9 +1068,8 @@ func testSchemaMultiError(t *testing.T, example schemaMultiErrorExample) func(*t } } -var schemaMultiErrorExamples = []schemaMultiErrorExample{ - { - Title: "STRING", +var schemaMultiErrorExamples = map[string]schemaMultiErrorExample{ + "STRING": { Schema: NewStringSchema(). WithMinLength(2). WithMaxLength(3). @@ -1104,8 +1083,8 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ {&SchemaError{SchemaField: "maxLength"}, &SchemaError{SchemaField: "pattern"}}, }, }, - { - Title: "NUMBER", + + "NUMBER": { Schema: NewIntegerSchema(). WithMin(1). WithMax(10), @@ -1118,8 +1097,8 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ {&SchemaError{SchemaField: "type"}, &SchemaError{SchemaField: "maximum"}}, }, }, - { - Title: "ARRAY: simple", + + "ARRAY: simple": { Schema: NewArraySchema(). WithMinItems(2). WithMaxItems(2). @@ -1142,8 +1121,8 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ }, }, }, - { - Title: "ARRAY: object", + + "ARRAY: object": { Schema: NewArraySchema(). WithItems(NewObjectSchema(). WithProperties(map[string]*Schema{ @@ -1166,8 +1145,8 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ }, }, }, - { - Title: "OBJECT", + + "OBJECT": { Schema: NewObjectSchema(). WithProperties(map[string]*Schema{ "key1": NewStringSchema(), @@ -1196,6 +1175,10 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ func TestIssue283(t *testing.T) { const api = ` openapi: "3.0.1" +info: + title: An API + version: v1 +paths: {} components: schemas: Test: @@ -1207,17 +1190,20 @@ components: type: boolean type: object ` - data := map[string]interface{}{ + loader := NewLoader() + doc, err := loader.LoadFromData([]byte(api)) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Test"].Value.VisitData(doc, 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"`) + require.Contains(t, err.Error(), `ownerName`) + require.True(t, strings.Contains(err.Error(), `schema "not"`) || strings.Contains(err.Error(), `schema (not)`)) } func TestValidationFailsOnInvalidPattern(t *testing.T) { @@ -1225,7 +1211,6 @@ func TestValidationFailsOnInvalidPattern(t *testing.T) { Pattern: "[", Type: "string", } - - var err = schema.Validate(context.Background()) + err := schema.Validate(context.Background()) require.Error(t, err) } diff --git a/openapi3/schemas_legacy.go b/openapi3/schemas_legacy.go new file mode 100644 index 000000000..c3e99de14 --- /dev/null +++ b/openapi3/schemas_legacy.go @@ -0,0 +1,996 @@ +// +build legacy + +package openapi3 + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "math" + "math/big" + "regexp" + "strconv" + "unicode/utf16" +) + +var ( + // SchemaErrorDetailsDisabled disables printing of details about schema errors. + SchemaErrorDetailsDisabled = false + + //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") + + // 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") +) + +type schemaLoader = struct{} + +func (doc *T) compileSchemas(settings *schemaValidationSettings) (err error) { + return +} + +func (schema *Schema) visitData(doc *T, data interface{}, opts ...SchemaValidationOption) (err error) { + settings := newSchemaValidationSettings(opts...) + return schema.visitJSON(settings, data) +} + +func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { + for _, existing := range stack { + if existing == schema { + 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 + } + } + + for _, item := range schema.AnyOf { + v := item.Value + if v == nil { + return foundUnresolvedRef(item.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + for _, item := range schema.AllOf { + v := item.Value + if v == nil { + return foundUnresolvedRef(item.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + if ref := schema.Not; ref != nil { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + schemaType := schema.Type + switch schemaType { + case "": + case "boolean": + case "number": + if format := schema.Format; len(format) > 0 { + switch format { + case "float", "double": + default: + if !SchemaFormatValidationDisabled { + return unsupportedFormat(format) + } + } + } + case "integer": + if format := schema.Format; len(format) > 0 { + switch format { + case "int32", "int64": + default: + if !SchemaFormatValidationDisabled { + return unsupportedFormat(format) + } + } + } + case "string": + if format := schema.Format; len(format) > 0 { + switch format { + // Supported by OpenAPIv3.0.1: + case "byte", "binary", "date", "date-time", "password": + // 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 && !SchemaFormatValidationDisabled { + return unsupportedFormat(format) + } + } + } + if schema.Pattern != "" { + if err = schema.compilePattern(); err != nil { + return err + } + } + case "array": + if schema.Items == nil { + return errors.New("when schema type is 'array', schema 'items' must be non-null") + } + case "object": + default: + return fmt.Errorf("unsupported 'type' value %q", schemaType) + } + + if ref := schema.Items; ref != nil { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + for _, ref := range schema.Properties { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + 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 + } + } + + return +} + +func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interface{}) (err error) { + switch value := value.(type) { + case nil: + return schema.visitJSONNull(settings) + case float64: + if math.IsNaN(value) { + return ErrSchemaInputNaN + } + if math.IsInf(value, 0) { + return ErrSchemaInputInf + } + } + + if schema.IsEmpty() { + return + } + if err = schema.visitSetOperations(settings, value); err != nil { + return + } + + switch value := value.(type) { + case nil: + return schema.visitJSONNull(settings) + case bool: + return schema.visitJSONBoolean(settings, value) + case float64: + return schema.visitJSONNumber(settings, value) + case string: + return schema.visitJSONString(settings, value) + case []interface{}: + return schema.visitJSONArray(settings, value) + case map[string]interface{}: + return schema.visitJSONObject(settings, value) + default: + return &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "type", + Reason: fmt.Sprintf("unhandled value of type %T", value), + } + } +} + +func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) { + if enum := schema.Enum; len(enum) != 0 { + for _, v := range enum { + if value == v { + return + } + } + if settings.failfast { + return errSchema + } + return &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "enum", + Reason: "value is not one of the allowed values", + } + } + + if ref := schema.Not; ref != nil { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err := v.visitJSON(settings, value); err == nil { + if settings.failfast { + return errSchema + } + return &SchemaError{ + 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 errors.New("input does not contain the discriminator property") + } + + if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorVal.(string)]; len(schema.Discriminator.Mapping) > 0 && !okcheck { + return errors.New("input does not contain a valid discriminator value") + } + } + } + + ok := 0 + validationErrors := []error{} + for _, item := range v { + v := item.Value + if v == nil { + return foundUnresolvedRef(item.Ref) + } + + if discriminatorRef != "" && discriminatorRef != item.Ref { + continue + } + + if err := v.visitJSON(settings, value); err != nil { + validationErrors = append(validationErrors, err) + continue + } + + ok++ + } + + if ok != 1 { + if len(validationErrors) > 1 { + errorMessage := "" + for _, err := range validationErrors { + if errorMessage != "" { + errorMessage += " Or " + } + errorMessage += err.Error() + } + return errors.New("doesn't match schema due to: " + errorMessage) + } + if settings.failfast { + return errSchema + } + e := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "oneOf", + } + if ok > 1 { + e.Origin = ErrOneOfConflict + } else if len(validationErrors) == 1 { + e.Origin = validationErrors[0] + } + + return e + } + } + + if v := schema.AnyOf; len(v) > 0 { + ok := false + for _, item := range v { + v := item.Value + if v == nil { + return foundUnresolvedRef(item.Ref) + } + if err := v.visitJSON(settings, value); err == nil { + ok = true + break + } + } + if !ok { + if settings.failfast { + return errSchema + } + return &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "anyOf", + } + } + } + + for _, item := range schema.AllOf { + v := item.Value + if v == nil { + return foundUnresolvedRef(item.Ref) + } + if err := v.visitJSON(settings, value); err != nil { + if settings.failfast { + return errSchema + } + return &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "allOf", + Origin: err, + } + } + } + return +} + +func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) { + if schema.Nullable { + return + } + if settings.failfast { + return errSchema + } + return &SchemaError{ + Value: nil, + Schema: schema, + SchemaField: "nullable", + Reason: "Value is not nullable", + } +} + +func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) { + if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" { + return schema.expectedType(settings, "boolean") + } + return +} + +func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error { + var me MultiError + schemaType := schema.Type + if schemaType == "integer" { + if bigFloat := big.NewFloat(value); !bigFloat.IsInt() { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "type", + Reason: "Value must be an integer", + } + if !settings.multiError { + return err + } + me = append(me, err) + } + } else if schemaType != "" && schemaType != "number" { + return schema.expectedType(settings, "number, integer") + } + + // "exclusiveMinimum" + if v := schema.ExclusiveMin; v && !(*schema.Min < value) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "exclusiveMinimum", + Reason: fmt.Sprintf("number must be more than %g", *schema.Min), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "exclusiveMaximum" + if v := schema.ExclusiveMax; v && !(*schema.Max > value) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "exclusiveMaximum", + Reason: fmt.Sprintf("number must be less than %g", *schema.Max), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "minimum" + if v := schema.Min; v != nil && !(*v <= value) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "minimum", + Reason: fmt.Sprintf("number must be at least %g", *v), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "maximum" + if v := schema.Max; v != nil && !(*v >= value) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "maximum", + Reason: fmt.Sprintf("number must be most %g", *v), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "multipleOf" + if v := schema.MultipleOf; v != nil { + // "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 { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "multipleOf", + } + if !settings.multiError { + return err + } + me = append(me, err) + } + } + + if len(me) > 0 { + return me + } + + return nil +} + +func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error { + if schemaType := schema.Type; schemaType != "" && schemaType != "string" { + return schema.expectedType(settings, "string") + } + + var me MultiError + + // "minLength" and "maxLength" + minLength := schema.MinLength + maxLength := schema.MaxLength + if minLength != 0 || maxLength != nil { + // JSON schema string lengths are UTF-16, not UTF-8! + length := int64(0) + for _, r := range value { + if utf16.IsSurrogate(r) { + length += 2 + } else { + length++ + } + } + if minLength != 0 && length < int64(minLength) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "minLength", + Reason: fmt.Sprintf("minimum string length is %d", minLength), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + if maxLength != nil && length > int64(*maxLength) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "maxLength", + Reason: fmt.Sprintf("maximum string length is %d", *maxLength), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + } + + // "pattern" + if schema.Pattern != "" && schema.compiledPattern == nil { + var err error + if err = schema.compilePattern(); err != nil { + if !settings.multiError { + return 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 %q", schema.Pattern), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "format" + var formatErr string + 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) { + formatErr = fmt.Sprintf("string doesn't match the format %q (regular expression %q)", format, cp.String()) + } + case f.regexp == nil && f.callback != nil: + if err := f.callback(value); err != nil { + formatErr = err.Error() + } + default: + formatErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format) + } + } + } + if formatErr != "" { + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "format", + Reason: formatErr, + } + if !settings.multiError { + return err + } + me = append(me, err) + + } + + if len(me) > 0 { + return me + } + + return nil +} + +func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error { + if schemaType := schema.Type; schemaType != "" && schemaType != "array" { + return schema.expectedType(settings, "array") + } + + var me MultiError + + lenValue := int64(len(value)) + + // "minItems" + if v := schema.MinItems; v != 0 && lenValue < int64(v) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "minItems", + Reason: fmt.Sprintf("minimum number of items is %d", v), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "maxItems" + if v := schema.MaxItems; v != nil && lenValue > int64(*v) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "maxItems", + Reason: fmt.Sprintf("maximum number of items is %d", *v), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "uniqueItems" + if sliceUniqueItemsChecker == nil { + sliceUniqueItemsChecker = isSliceOfUniqueItems + } + if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "uniqueItems", + Reason: "duplicate items found", + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "items" + if itemSchemaRef := schema.Items; itemSchemaRef != nil { + itemSchema := itemSchemaRef.Value + if itemSchema == nil { + 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 len(me) > 0 { + return me + } + + return nil +} + +func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error { + if schemaType := schema.Type; schemaType != "" && schemaType != "object" { + return schema.expectedType(settings, "object") + } + + var me MultiError + + // "properties" + properties := schema.Properties + lenValue := int64(len(value)) + + // "minProperties" + if v := schema.MinProps; v != 0 && lenValue < int64(v) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "minProperties", + Reason: fmt.Sprintf("there must be at least %d properties", v), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "maxProperties" + if v := schema.MaxProps; v != nil && lenValue > int64(*v) { + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "maxProperties", + Reason: fmt.Sprintf("there must be at most %d properties", *v), + } + if !settings.multiError { + return err + } + me = append(me, err) + } + + // "additionalProperties" + var additionalProperties *Schema + if ref := schema.AdditionalProperties; ref != nil { + additionalProperties = ref.Value + } + for k, v := range value { + if properties != nil { + propertyRef := properties[k] + if propertyRef != nil { + p := propertyRef.Value + if p == nil { + return foundUnresolvedRef(propertyRef.Ref) + } + if err := p.visitJSON(settings, v); err != nil { + if settings.failfast { + 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) + } + continue + } + } + 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 { + 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) + } + } + continue + } + if settings.failfast { + return errSchema + } + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "properties", + Reason: fmt.Sprintf("property %q is unsupported", k), + } + if !settings.multiError { + return err + } + 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 { + return errSchema + } + err := markSchemaErrorKey(&SchemaError{ + Value: value, + Schema: schema, + SchemaField: "required", + Reason: fmt.Sprintf("property %q is missing", k), + }, k) + if !settings.multiError { + return err + } + me = append(me, err) + } + } + + if len(me) > 0 { + return me + } + + return nil +} + +func (schema *Schema) expectedType(settings *schemaValidationSettings, typ string) error { + if settings.failfast { + return errSchema + } + return &SchemaError{ + 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", + Reason: fmt.Sprintf("cannot compile pattern %q: %v", schema.Pattern, err), + } + } + return nil +} + +type SchemaError struct { + Value interface{} + reversePath []string + Schema *Schema + SchemaField string + Reason string + Origin error +} + +func markSchemaErrorKey(err error, key string) error { + 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 { + if v, ok := err.(*SchemaError); ok { + v.reversePath = append(v.reversePath, strconv.FormatInt(int64(index), 10)) + return v + } + if v, ok := err.(MultiError); ok { + for _, e := range v { + _ = markSchemaErrorIndex(e, index) + } + return v + } + return err +} + +func (err *SchemaError) JSONPointer() []string { + reversePath := err.reversePath + path := append([]string(nil), reversePath...) + for left, right := 0, len(path)-1; left < right; left, right = left+1, right-1 { + path[left], path[right] = path[right], path[left] + } + return path +} + +func (err *SchemaError) Error() string { + 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 + for i := len(reversePath) - 1; i >= 0; i-- { + buf.WriteByte('/') + buf.WriteString(reversePath[i]) + } + buf.WriteString(`": `) + } + reason := err.Reason + if reason == "" { + buf.WriteString(`Doesn't match schema "`) + buf.WriteString(err.SchemaField) + buf.WriteString(`"`) + } else { + buf.WriteString(reason) + } + if !SchemaErrorDetailsDisabled { + buf.WriteString("\nSchema:\n ") + encoder := json.NewEncoder(buf) + encoder.SetIndent(" ", " ") + if err := encoder.Encode(err.Schema); err != nil { + panic(err) + } + buf.WriteString("\nValue:\n ") + if err := encoder.Encode(err.Value); err != nil { + panic(err) + } + } + return buf.String() +} + +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{}{} + } + return s == len(m) +} + +// SliceUniqueItemsChecker is an function used to check if an given slice +// have unique items. +type SliceUniqueItemsChecker func(items []interface{}) bool + +// By default using predefined func isSliceOfUniqueItems which make use of +// json.Marshal to generate a key for map used to check if a given slice +// have unique items. +var sliceUniqueItemsChecker SliceUniqueItemsChecker = isSliceOfUniqueItems + +// RegisterArrayUniqueItemsChecker is used to register a customized function +// used to check if JSON array have unique items. +func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) { + sliceUniqueItemsChecker = fn +} + +func unsupportedFormat(format string) error { + return fmt.Errorf("unsupported 'format' value %q", format) +} diff --git a/openapi3/schemas_santhoshtekuri.go b/openapi3/schemas_santhoshtekuri.go new file mode 100644 index 000000000..5ba8af45d --- /dev/null +++ b/openapi3/schemas_santhoshtekuri.go @@ -0,0 +1,158 @@ +// +build santhoshtekuri + +package openapi3 + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "regexp" + "strings" + + jsonschema "github.com/santhosh-tekuri/jsonschema/v4" +) + +// // SchemaFormatValidationDisabled FIXME: drop +// var SchemaFormatValidationDisabled = true + +// type schemaLoader = *gojsonschema.SchemaLoader + +func (doc *T) compileSchemas(settings *schemaValidationSettings) (err error) { + docSchemas := doc.Components.Schemas + schemas := make(map[string][]byte, len(docSchemas)) + for name, docSchema := range docSchemas { + schema := docSchema.Value.fromOpenAPISchema(settings) + var schemaBytes []byte + if schemaBytes, err = json.Marshal(schema);err!=nil{ + return + } + schemas[name] =schemaBytes + } + + refd:=jsonschema.NewCompiler() + refd.Draft=jsonschema.Draft4 + for name,schemaBytes:=range schemas{ + r:= + } + + return +} + +func (schema *Schema) visitData(doc *T, data interface{}, opts ...SchemaValidationOption) (err error) { + return +} + +func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { + for _, existing := range stack { + if existing == schema { + 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 + } + } + + for _, item := range schema.AnyOf { + v := item.Value + if v == nil { + return foundUnresolvedRef(item.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + for _, item := range schema.AllOf { + v := item.Value + if v == nil { + return foundUnresolvedRef(item.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + if ref := schema.Not; ref != nil { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + schemaType := schema.Type + // NOTE: any format is valid, as per: + // > However, to support documentation needs, the format property is an open string-valued property, and can have any value. + switch schemaType { + case "": + case "boolean": + case "number": + case "integer": + case "string": + case "array": + if schema.Items == nil { + return errors.New("when schema type is 'array', schema 'items' must be non-null") + } + case "object": + default: + return fmt.Errorf("unsupported 'type' value %q", schemaType) + } + + if pattern := schema.Pattern; pattern != "" { + if _, err = regexp.Compile(pattern); err != nil { + return &SchemaError{ + Schema: schema, + SchemaField: "pattern", + Reason: fmt.Sprintf("cannot compile pattern %q: %v", pattern, err), + } + } + } + + if ref := schema.Items; ref != nil { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + for _, ref := range schema.Properties { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + 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 + } + } + + return +} diff --git a/openapi3/schemas_xeipuuv.go b/openapi3/schemas_xeipuuv.go new file mode 100644 index 000000000..e6e348148 --- /dev/null +++ b/openapi3/schemas_xeipuuv.go @@ -0,0 +1,286 @@ +// +build xeipuuv + +package openapi3 + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/xeipuuv/gojsonschema" +) + +// SchemaFormatValidationDisabled FIXME: drop +var SchemaFormatValidationDisabled = true + +type schemaLoader = *gojsonschema.SchemaLoader + +func (doc *T) compileSchemas(settings *schemaValidationSettings) (err error) { + docSchemas := doc.Components.Schemas + schemas := make(schemasJSON, len(docSchemas)) + for name, docSchema := range docSchemas { + schemas[name] = docSchema.Value.fromOpenAPISchema(settings) + } + //FIXME merge loops + refd := gojsonschema.NewSchemaLoader() + for name, schema := range schemas { + absRef := "#/components/schemas/" + name + sl := gojsonschema.NewGoLoader(schema) + if err = refd.AddSchema(absRef, sl); err != nil { + return + } + } + + switch { + case settings.asreq: + doc.refdAsReq = refd + case settings.asrep: + doc.refdAsRep = refd + default: + doc.refd = refd + } + return +} + +func (schema *Schema) visitData(doc *T, data interface{}, opts ...SchemaValidationOption) (err error) { + settings := newSchemaValidationSettings(opts...) + ls := gojsonschema.NewGoLoader(schema.fromOpenAPISchema(settings)) + ld := gojsonschema.NewGoLoader(data) + + var res *gojsonschema.Result + if doc != nil { + if doc.refdAsReq == nil || doc.refdAsRep == nil || doc.refd == nil { + panic(`func (*T) CompileSchemas() error must be called first`) + } + var whole *gojsonschema.Schema + switch { + case settings.asreq: + whole, err = doc.refdAsReq.Compile(ls) + case settings.asrep: + whole, err = doc.refdAsRep.Compile(ls) + default: + whole, err = doc.refd.Compile(ls) + } + if err != nil { + return + } + res, err = whole.Validate(ld) + } else { + res, err = gojsonschema.Validate(ls, ld) + } + if err != nil { + return + } + + if !res.Valid() { + err := SchemaValidationError(res.Errors()) + if settings.multiError { + return err.asMultiError() + } + return err + } + return +} + +func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { + for _, existing := range stack { + if existing == schema { + 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 + } + } + + for _, item := range schema.AnyOf { + v := item.Value + if v == nil { + return foundUnresolvedRef(item.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + for _, item := range schema.AllOf { + v := item.Value + if v == nil { + return foundUnresolvedRef(item.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + if ref := schema.Not; ref != nil { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + schemaType := schema.Type + // NOTE: any format is valid, as per: + // > However, to support documentation needs, the format property is an open string-valued property, and can have any value. + switch schemaType { + case "": + case "boolean": + case "number": + case "integer": + case "string": + case "array": + if schema.Items == nil { + return errors.New("when schema type is 'array', schema 'items' must be non-null") + } + case "object": + default: + return fmt.Errorf("unsupported 'type' value %q", schemaType) + } + + if pattern := schema.Pattern; pattern != "" { + if _, err = regexp.Compile(pattern); err != nil { + return &SchemaError{ + Schema: schema, + SchemaField: "pattern", + Reason: fmt.Sprintf("cannot compile pattern %q: %v", pattern, err), + } + } + } + + if ref := schema.Items; ref != nil { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + for _, ref := range schema.Properties { + v := ref.Value + if v == nil { + return foundUnresolvedRef(ref.Ref) + } + if err = v.validate(ctx, stack); err != nil { + return + } + } + + 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 + } + } + + return +} + +// SchemaValidationError is a collection of errors +type SchemaValidationError []gojsonschema.ResultError + +var _ error = (*SchemaValidationError)(nil) + +func (e SchemaValidationError) Error() string { + var buff strings.Builder + for i, re := range []gojsonschema.ResultError(e) { + if i != 0 { + buff.WriteString("\n") + } + buff.WriteString(re.String()) + } + return buff.String() +} + +// Errors unwraps into much detailed errors. +// See https://pkg.go.dev/github.com/xeipuuv/gojsonschema#ResultError +func (e SchemaValidationError) Errors() []gojsonschema.ResultError { + return e +} + +// JSONPointer returns a dot (.) delimited "JSON path" to the context of the first error. +func (e SchemaValidationError) JSONPointer() string { + return []gojsonschema.ResultError(e)[0].Field() +} + +func (e SchemaValidationError) asMultiError() MultiError { + errs := make([]error, 0, len(e)) + for _, re := range e { + errs = append(errs, errors.New(re.String())) + } + return errs +} + +type SchemaError struct { + Value interface{} + reversePath []string //FIXME + Schema *Schema + SchemaField string + Reason string + Origin error //FIXME +} + +func (err *SchemaError) JSONPointer() []string { + return nil //FIXME +} + +func (err *SchemaError) Error() string { + // 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 + // for i := len(reversePath) - 1; i >= 0; i-- { + // buf.WriteByte('/') + // buf.WriteString(reversePath[i]) + // } + // buf.WriteString(`": `) + // } + reason := err.Reason + if reason == "" { + buf.WriteString(`Doesn't match schema "`) + buf.WriteString(err.SchemaField) + buf.WriteString(`"`) + } else { + buf.WriteString(reason) + } + { // if !SchemaErrorDetailsDisabled { + buf.WriteString("\nSchema:\n ") + encoder := json.NewEncoder(buf) + encoder.SetIndent(" ", " ") + if err := encoder.Encode(err.Schema); err != nil { + panic(err) + } + buf.WriteString("\nValue:\n ") + if err := encoder.Encode(err.Value); err != nil { + panic(err) + } + } + return buf.String() +} diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index ce6fcc6f1..f5529410b 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -15,8 +15,9 @@ func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) * return srs } -func (value SecurityRequirements) Validate(ctx context.Context) error { - for _, item := range value { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (srs SecurityRequirements) Validate(ctx context.Context) error { + for _, item := range srs { if err := item.Validate(ctx); err != nil { return err } @@ -38,6 +39,7 @@ func (security SecurityRequirement) Authenticate(provider string, scopes ...stri return security } -func (value SecurityRequirement) Validate(ctx context.Context) error { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (security SecurityRequirement) Validate(ctx context.Context) error { return nil } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 990f258d4..3bad2416c 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -103,15 +103,16 @@ func (ss *SecurityScheme) WithBearerFormat(value string) *SecurityScheme { return ss } -func (value *SecurityScheme) Validate(ctx context.Context) error { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (ss *SecurityScheme) Validate(ctx context.Context) error { hasIn := false hasBearerFormat := false hasFlow := false - switch value.Type { + switch ss.Type { case "apiKey": hasIn = true case "http": - scheme := value.Scheme + scheme := ss.Scheme switch scheme { case "bearer": hasBearerFormat = true @@ -122,46 +123,46 @@ func (value *SecurityScheme) Validate(ctx context.Context) error { case "oauth2": hasFlow = true case "openIdConnect": - if value.OpenIdConnectUrl == "" { - return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", value.Name) + if ss.OpenIdConnectUrl == "" { + return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", ss.Name) } default: - return fmt.Errorf("security scheme 'type' can't be %q", value.Type) + return fmt.Errorf("security scheme 'type' can't be %q", ss.Type) } // Validate "in" and "name" if hasIn { - switch value.In { + 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", value.In) + return fmt.Errorf("security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not %q", ss.In) } - if value.Name == "" { + if ss.Name == "" { return errors.New("security scheme of type 'apiKey' should have 'name'") } - } else if len(value.In) > 0 { - return fmt.Errorf("security scheme of type %q can't have 'in'", value.Type) - } else if len(value.Name) > 0 { + } else if len(ss.In) > 0 { + return fmt.Errorf("security scheme of type %q can't have 'in'", ss.Type) + } else if len(ss.Name) > 0 { 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(value.BearerFormat) > 0 { - return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", value.Type) + if !hasBearerFormat && len(ss.BearerFormat) > 0 { + return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", ss.Type) } // Validate "flow" if hasFlow { - flow := value.Flows + flow := ss.Flows if flow == nil { - return fmt.Errorf("security scheme of type %q should have 'flows'", value.Type) + return fmt.Errorf("security scheme of type %q should have 'flows'", ss.Type) } if err := flow.Validate(ctx); err != nil { return fmt.Errorf("security scheme 'flow' is invalid: %v", err) } - } else if value.Flows != nil { - return fmt.Errorf("security scheme of type %q can't have 'flows'", value.Type) + } else if ss.Flows != nil { + return fmt.Errorf("security scheme of type %q can't have 'flows'", ss.Type) } return nil } @@ -191,18 +192,19 @@ func (flows *OAuthFlows) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, flows) } +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. func (flows *OAuthFlows) Validate(ctx context.Context) error { if v := flows.Implicit; v != nil { - return v.Validate(ctx, oAuthFlowTypeImplicit) + return v.validate(ctx, oAuthFlowTypeImplicit) } if v := flows.Password; v != nil { - return v.Validate(ctx, oAuthFlowTypePassword) + return v.validate(ctx, oAuthFlowTypePassword) } if v := flows.ClientCredentials; v != nil { - return v.Validate(ctx, oAuthFlowTypeClientCredentials) + return v.validate(ctx, oAuthFlowTypeClientCredentials) } if v := flows.AuthorizationCode; v != nil { - return v.Validate(ctx, oAuthFlowAuthorizationCode) + return v.validate(ctx, oAuthFlowAuthorizationCode) } return errors.New("no OAuth flow is defined") } @@ -223,7 +225,7 @@ func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, flow) } -func (flow *OAuthFlow) Validate(ctx context.Context, typ oAuthFlowType) error { +func (flow *OAuthFlow) validate(ctx 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 '") diff --git a/openapi3/server.go b/openapi3/server.go index 4415bd08f..3f12291d4 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -14,9 +14,9 @@ import ( // Servers is specified by OpenAPI/Swagger standard version 3.0. type Servers []*Server -// Validate ensures servers are per the OpenAPIv3 specification. -func (value Servers) Validate(ctx context.Context) error { - for _, v := range value { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (servers Servers) Validate(ctx context.Context) error { + for _, v := range servers { if err := v.Validate(ctx); err != nil { return err } @@ -125,19 +125,20 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) { return params, input, true } -func (value *Server) Validate(ctx context.Context) (err error) { - if value.URL == "" { +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (server *Server) Validate(ctx context.Context) (err error) { + if server.URL == "" { return errors.New("value of url must be a non-empty string") } - opening, closing := strings.Count(value.URL, "{"), strings.Count(value.URL, "}") + opening, closing := strings.Count(server.URL, "{"), strings.Count(server.URL, "}") if opening != closing { return errors.New("server URL has mismatched { and }") } - if opening != len(value.Variables) { + if opening != len(server.Variables) { return errors.New("server has undeclared variables") } - for name, v := range value.Variables { - if !strings.Contains(value.URL, fmt.Sprintf("{%s}", name)) { + for name, v := range server.Variables { + if !strings.Contains(server.URL, fmt.Sprintf("{%s}", name)) { return errors.New("server has undeclared variables") } if err = v.Validate(ctx); err != nil { @@ -163,9 +164,10 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, serverVariable) } -func (value *ServerVariable) Validate(ctx context.Context) error { - if value.Default == "" { - data, err := value.MarshalJSON() +// Validate goes through the receiver value and its descendants and errors on any non compliance to the OpenAPIv3 specification. +func (serverVariable *ServerVariable) Validate(ctx context.Context) error { + if serverVariable.Default == "" { + data, err := serverVariable.MarshalJSON() if err != nil { return err } diff --git a/openapi3/unique_items_checker_test.go b/openapi3/unique_items_checker_test.go index 85147c67a..0174da135 100644 --- a/openapi3/unique_items_checker_test.go +++ b/openapi3/unique_items_checker_test.go @@ -1,3 +1,5 @@ +// +build legacy + package openapi3_test import ( @@ -19,7 +21,7 @@ func TestRegisterArrayUniqueItemsChecker(t *testing.T) { ) // Fist checked by predefined function - err := schema.VisitJSON(val) + err := schema.VisitData(nil, val) require.NoError(t, err) // Register a function will always return false when check if a @@ -30,7 +32,7 @@ func TestRegisterArrayUniqueItemsChecker(t *testing.T) { }) defer openapi3.RegisterArrayUniqueItemsChecker(nil) // Reset for other tests - err = schema.VisitJSON(val) + err = schema.VisitData(nil, val) require.Error(t, err) require.True(t, strings.HasPrefix(err.Error(), "duplicate items found")) } diff --git a/openapi3/validate_identifier.go b/openapi3/validate_identifier.go new file mode 100644 index 000000000..e4c83cfa4 --- /dev/null +++ b/openapi3/validate_identifier.go @@ -0,0 +1,23 @@ +package openapi3 + +import ( + "fmt" + "regexp" +) + +var identifierRegExp *regexp.Regexp + +// ValidateIdentifier verifies whether Component object key matches identifier pattern according to OpenAPIv3 +func ValidateIdentifier(value string) (err error) { + const re = `^[a-zA-Z0-9._-]+$` + if identifierRegExp == nil { + if identifierRegExp, err = regexp.Compile(re); err != nil { + return + } + } + + if identifierRegExp.MatchString(value) { + return nil + } + return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (regexp: %q)", value, re) +} diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 7ae863b82..54024f63c 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -914,16 +914,16 @@ func TestDecodeParameter(t *testing.T) { Title: "MyAPI", Version: "0.1", } - spec := &openapi3.T{OpenAPI: "3.0.0", Info: info} + doc := &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()) + doc.AddOperation(path, http.MethodGet, op) + err = doc.Validate(context.Background()) require.NoError(t, err) - router, err := legacyrouter.NewRouter(spec) + router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) route, pathParams, err := router.FindRoute(req) diff --git a/openapi3filter/unpack_errors_test.go b/openapi3filter/unpack_errors_test.go index 4242177f9..ddc480c5b 100644 --- a/openapi3filter/unpack_errors_test.go +++ b/openapi3filter/unpack_errors_test.go @@ -13,10 +13,14 @@ import ( ) func Example() { - doc, err := openapi3.NewLoader().LoadFromFile("./testdata/petstore.yaml") + loader := openapi3.NewLoader() + doc, err := loader.LoadFromFile("./testdata/petstore.yaml") if err != nil { panic(err) } + if err = doc.Validate(loader.Context); err != nil { + panic(err) + } router, err := gorillamux.NewRouter(doc) if err != nil { diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go index 454a927e9..a8dd08237 100644 --- a/openapi3filter/validate_readonly_test.go +++ b/openapi3filter/validate_readonly_test.go @@ -68,8 +68,10 @@ func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { 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) diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 2f9a5f14c..a4f28fea1 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -154,7 +154,13 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param opts = make([]openapi3.SchemaValidationOption, 0, 1) opts = append(opts, openapi3.MultiErrors()) } - if err = schema.VisitJSON(value, opts...); err != nil { + + var spec *openapi3.T + if input.Route != nil { + spec = input.Route.Spec + } + + if err = schema.VisitData(spec, value, opts...); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } return nil @@ -237,8 +243,12 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req opts = append(opts, openapi3.MultiErrors()) } - // Validate JSON with the schema - if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { + var spec *openapi3.T + if input.Route != nil { + spec = input.Route.Spec + } + + if err := contentType.Schema.Value.VisitData(spec, value, opts...); err != nil { return &RequestError{ Input: input, RequestBody: requestBody, diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index b70938e7c..37e9b0c96 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -126,8 +126,12 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error opts = append(opts, openapi3.MultiErrors()) } - // Validate data with the schema. - if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { + var spec *openapi3.T + if input.RequestValidationInput.Route != nil { + spec = input.RequestValidationInput.Route.Spec + } + + if err := contentType.Schema.Value.VisitData(spec, value, opts...); err != nil { return &ResponseError{ Input: input, Reason: "response body doesn't match the schema", diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index d4d0a12f1..e0d684f56 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -159,6 +159,7 @@ func TestFilter(t *testing.T) { require.NoError(t, err) router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) + expectWithDecoder := func(req ExampleRequest, resp ExampleResponse, decoder ContentParameterDecoder) error { t.Logf("Request: %s %s", req.Method, req.URL) httpReq, err := http.NewRequest(req.Method, req.URL, marshalReader(req.Body)) @@ -198,6 +199,7 @@ func TestFilter(t *testing.T) { require.NoError(t, err) return err } + expect := func(req ExampleRequest, resp ExampleResponse) error { return expectWithDecoder(req, resp, nil) }