pax_global_header00006660000000000000000000000064141523640720014516gustar00rootroot0000000000000052 comment=a2254d99982d94a6774ef4cbcdf1ccb03d56bcff kin-openapi-0.85.0/000077500000000000000000000000001415236407200140225ustar00rootroot00000000000000kin-openapi-0.85.0/.github/000077500000000000000000000000001415236407200153625ustar00rootroot00000000000000kin-openapi-0.85.0/.github/workflows/000077500000000000000000000000001415236407200174175ustar00rootroot00000000000000kin-openapi-0.85.0/.github/workflows/go.yml000066400000000000000000000051371415236407200205550ustar00rootroot00000000000000name: go on: pull_request: push: jobs: build-and-test: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GO111MODULE: 'on' CGO_ENABLED: '0' strategy: fail-fast: true matrix: go: ['1.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 ./... - run: go test -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 ./... 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 "'[%].'" kin-openapi-0.85.0/.gitignore000066400000000000000000000005651415236407200160200ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ # Macos file system .DS_Store # IntelliJ / GoLand .idea /openapi3/load_with_go_embed_test.go kin-openapi-0.85.0/LICENSE000066400000000000000000000020721415236407200150300ustar00rootroot00000000000000MIT License Copyright (c) 2017-2018 the project authors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kin-openapi-0.85.0/README.md000066400000000000000000000241461415236407200153100ustar00rootroot00000000000000[![CI](https://github.com/getkin/kin-openapi/workflows/go/badge.svg)](https://github.com/getkin/kin-openapi/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/getkin/kin-openapi)](https://goreportcard.com/report/github.com/getkin/kin-openapi) [![GoDoc](https://godoc.org/github.com/getkin/kin-openapi?status.svg)](https://godoc.org/github.com/getkin/kin-openapi) [![Join Gitter Chat Channel -](https://badges.gitter.im/getkin/kin.svg)](https://gitter.im/getkin/kin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # Introduction A [Go](https://golang.org) project for handling [OpenAPI](https://www.openapis.org/) files. We target the latest OpenAPI version (currently 3), but the project contains support for older OpenAPI versions too. Licensed under the [MIT License](LICENSE). ## Contributors and users The project has received pull requests from many people. Thanks to everyone! Here's some projects that depend on _kin-openapi_: * [https://github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" * [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation" * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - Generate Go server boilerplate from an OpenAPIv3 spec document * [github.com/dunglas/vulcain](https://github.com/dunglas/vulcain) - "Use HTTP/2 Server Push to create fast and idiomatic client-driven REST APIs" * [github.com/danielgtaylor/restish](https://github.com/danielgtaylor/restish) - "...a CLI for interacting with REST-ish HTTP APIs with some nice features built-in" * [github.com/goadesign/goa](https://github.com/goadesign/goa) - "Goa is a framework for building micro-services and APIs in Go using a unique design-first approach." * [github.com/hashicorp/nomad-openapi](https://github.com/hashicorp/nomad-openapi) - "Nomad is an easy-to-use, flexible, and performant workload orchestrator that can deploy a mix of microservice, batch, containerized, and non-containerized applications. Nomad is easy to operate and scale and has native Consul and Vault integrations." * (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request) ## Alternatives * [go-swagger](https://github.com/go-swagger/go-swagger) stated [*OpenAPIv3 won't be supported*](https://github.com/go-swagger/go-swagger/issues/1122#issuecomment-575968499) * [swaggo](https://github.com/swaggo/swag) has an [open issue on OpenAPIv3](https://github.com/swaggo/swag/issues/386) * [go-openapi](https://github.com/go-openapi)'s [spec3](https://github.com/go-openapi/spec3) * an iteration on [spec](https://github.com/go-openapi/spec) (for OpenAPIv2) * see [README](https://github.com/go-openapi/spec3/tree/3fab9faa9094e06ebd19ded7ea96d156c2283dca#oai-object-model---) for the missing parts * See [https://github.com/OAI](https://github.com/OAI)'s [great tooling list](https://github.com/OAI/OpenAPI-Specification/blob/master/IMPLEMENTATIONS.md) # Structure * _openapi2_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi2)) * Support for OpenAPI 2 files, including serialization, deserialization, and validation. * _openapi2conv_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi2conv)) * Converts OpenAPI 2 files into OpenAPI 3 files. * _openapi3_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi3)) * Support for OpenAPI 3 files, including serialization, deserialization, and validation. * _openapi3filter_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi3filter)) * Validates HTTP requests and responses * Provides a [gorilla/mux](https://github.com/gorilla/mux) router for OpenAPI operations * _openapi3gen_ ([godoc](https://godoc.org/github.com/getkin/kin-openapi/openapi3gen)) * Generates `*openapi3.Schema` values for Go types. # Some recipes ## Loading OpenAPI document Use `openapi3.Loader`, which resolves all references: ```go doc, err := openapi3.NewLoader().LoadFromFile("swagger.json") ``` ## Getting OpenAPI operation that matches request ```go loader := openapi3.NewLoader() doc, _ := loader.LoadFromData([]byte(`...`)) _ := doc.Validate(loader.Context) router, _ := gorillamux.NewRouter(doc) route, pathParams, _ := router.FindRoute(httpRequest) // Do something with route.Operation ``` ## Validating HTTP requests/responses ```go package main import ( "bytes" "context" "encoding/json" "log" "net/http" "github.com/getkin/kin-openapi/openapi3filter" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) func main() { ctx := context.Background() loader := &openapi3.Loader{Context: ctx} doc, _ := loader.LoadFromFile("openapi3_spec.json") _ := doc.Validate(ctx) router, _ := legacyrouter.NewRouter(doc) httpReq, _ := http.NewRequest(http.MethodGet, "/items", nil) // Find route route, pathParams, _ := router.FindRoute(httpReq) // Validate request requestValidationInput := &openapi3filter.RequestValidationInput{ Request: httpReq, PathParams: pathParams, Route: route, } if err := openapi3filter.ValidateRequest(ctx, requestValidationInput); err != nil { panic(err) } var ( respStatus = 200 respContentType = "application/json" respBody = bytes.NewBufferString(`{}`) ) log.Println("Response:", respStatus) responseValidationInput := &openapi3filter.ResponseValidationInput{ RequestValidationInput: requestValidationInput, Status: respStatus, Header: http.Header{"Content-Type": []string{respContentType}}, } if respBody != nil { data, _ := json.Marshal(respBody) responseValidationInput.SetBodyBytes(data) } // Validate response. if err := openapi3filter.ValidateResponse(ctx, responseValidationInput); err != nil { panic(err) } } ``` ## Custom content type for body of HTTP request/response By default, the library parses a body of HTTP request and response if it has one of the next content types: `"text/plain"` or `"application/json"`. To support other content types you must register decoders for them: ```go func main() { // ... // Register a body's decoder for content type "application/xml". openapi3filter.RegisterBodyDecoder("application/xml", xmlBodyDecoder) // Now you can validate HTTP request that contains a body with content type "application/xml". requestValidationInput := &openapi3filter.RequestValidationInput{ Request: httpReq, PathParams: pathParams, Route: route, } if err := openapi3filter.ValidateRequest(ctx, requestValidationInput); err != nil { panic(err) } // ... // And you can validate HTTP response that contains a body with content type "application/xml". if err := openapi3filter.ValidateResponse(ctx, responseValidationInput); err != nil { panic(err) } } func xmlBodyDecoder(body []byte) (interface{}, error) { // Decode body to a primitive, []inteface{}, or map[string]interface{}. } ``` ## Custom function to check uniqueness of array items By defaut, the library check unique items by below predefined function ```go func isSliceOfUniqueItems(xs []interface{}) bool { s := len(xs) m := make(map[string]struct{}, s) for _, x := range xs { key, _ := json.Marshal(&x) m[string(key)] = struct{}{} } return s == len(m) } ``` In the predefined function using `json.Marshal` to generate a string can be used as a map key which is to support check the uniqueness of an array when the array items are objects or arrays. You can register you own function according to your input data to get better performance: ```go func main() { // ... // Register a customized function used to check uniqueness of array. openapi3.RegisterArrayUniqueItemsChecker(arrayUniqueItemsChecker) // ... other validate codes } func arrayUniqueItemsChecker(items []interface{}) bool { // Check the uniqueness of the input slice } ``` ## Sub-v0 breaking API changes ### v0.84.0 * The prototype of `openapi3gen.NewSchemaRefForValue` changed: * It no longer returns a map but that is still accessible under the field `(*Generator).SchemaRefs`. * It now takes in an additional argument (basically `doc.Components.Schemas`) which gets written to so `$ref` cycles can be properly handled. ### v0.61.0 * Renamed `openapi2.Swagger` to `openapi2.T`. * Renamed `openapi2conv.FromV3Swagger` to `openapi2conv.FromV3`. * Renamed `openapi2conv.ToV3Swagger` to `openapi2conv.ToV3`. * Renamed `openapi3.LoadSwaggerFromData` to `openapi3.LoadFromData`. * Renamed `openapi3.LoadSwaggerFromDataWithPath` to `openapi3.LoadFromDataWithPath`. * Renamed `openapi3.LoadSwaggerFromFile` to `openapi3.LoadFromFile`. * Renamed `openapi3.LoadSwaggerFromURI` to `openapi3.LoadFromURI`. * Renamed `openapi3.NewSwaggerLoader` to `openapi3.NewLoader`. * Renamed `openapi3.Swagger` to `openapi3.T`. * Renamed `openapi3.SwaggerLoader` to `openapi3.Loader`. * Renamed `openapi3filter.ValidationHandler.SwaggerFile` to `openapi3filter.ValidationHandler.File`. * Renamed `routers.Route.Swagger` to `routers.Route.Spec`. ### v0.51.0 * Type `openapi3filter.Route` moved to `routers` (and `Route.Handler` was dropped. See https://github.com/getkin/kin-openapi/issues/329) * Type `openapi3filter.RouteError` moved to `routers` (so did `ErrPathNotFound` and `ErrMethodNotAllowed` which are now `RouteError`s) * Routers' `FindRoute(...)` method now takes only one argument: `*http.Request` * `getkin/kin-openapi/openapi3filter.Router` moved to `getkin/kin-openapi/routers/legacy` * `openapi3filter.NewRouter()` and its related `WithSwaggerFromFile(string)`, `WithSwagger(*openapi3.Swagger)`, `AddSwaggerFromFile(string)` and `AddSwagger(*openapi3.Swagger)` are all replaced with a single `.NewRouter(*openapi3.Swagger)` * NOTE: the `NewRouter(doc)` call now requires that the user ensures `doc` is valid (`doc.Validate() != nil`). This used to be asserted. ### v0.47.0 Field `(*openapi3.SwaggerLoader).LoadSwaggerFromURIFunc` of type `func(*openapi3.SwaggerLoader, *url.URL) (*openapi3.Swagger, error)` was removed after the addition of the field `(*openapi3.SwaggerLoader).ReadFromURIFunc` of type `func(*openapi3.SwaggerLoader, *url.URL) ([]byte, error)`. kin-openapi-0.85.0/go.mod000066400000000000000000000003411415236407200151260ustar00rootroot00000000000000module github.com/getkin/kin-openapi go 1.14 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/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.3.0 ) kin-openapi-0.85.0/go.sum000066400000000000000000000053061415236407200151610ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= 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/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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= kin-openapi-0.85.0/jsoninfo/000077500000000000000000000000001415236407200156475ustar00rootroot00000000000000kin-openapi-0.85.0/jsoninfo/doc.go000066400000000000000000000001541415236407200167430ustar00rootroot00000000000000// Package jsoninfo provides information and functions for marshalling/unmarshalling JSON. package jsoninfo kin-openapi-0.85.0/jsoninfo/field_info.go000066400000000000000000000047641415236407200203070ustar00rootroot00000000000000package jsoninfo import ( "reflect" "strings" "unicode" "unicode/utf8" ) // FieldInfo contains information about JSON serialization of a field. type FieldInfo struct { MultipleFields bool // Whether multiple Go fields share this JSON name HasJSONTag bool TypeIsMarshaller bool TypeIsUnmarshaller bool JSONOmitEmpty bool JSONString bool Index []int Type reflect.Type JSONName string } func AppendFields(fields []FieldInfo, parentIndex []int, t reflect.Type) []FieldInfo { if t.Kind() == reflect.Ptr { t = t.Elem() } // For each field numField := t.NumField() iteration: for i := 0; i < numField; i++ { f := t.Field(i) index := make([]int, 0, len(parentIndex)+1) index = append(index, parentIndex...) index = append(index, i) // See whether this is an embedded field if f.Anonymous { if f.Tag.Get("json") == "-" { continue } fields = AppendFields(fields, index, f.Type) continue iteration } // Ignore certain types switch f.Type.Kind() { case reflect.Func, reflect.Chan: continue iteration } // Is it a private (lowercase) field? firstRune, _ := utf8.DecodeRuneInString(f.Name) if unicode.IsLower(firstRune) { continue iteration } // Declare a field field := FieldInfo{ Index: index, Type: f.Type, JSONName: f.Name, } // Read "json" tag jsonTag := f.Tag.Get("json") // Read our custom "multijson" tag that // allows multiple fields with the same name. if v := f.Tag.Get("multijson"); v != "" { field.MultipleFields = true jsonTag = v } // Handle "-" if jsonTag == "-" { continue } // Parse the tag if jsonTag != "" { field.HasJSONTag = true for i, part := range strings.Split(jsonTag, ",") { if i == 0 { if part != "" { field.JSONName = part } } else { switch part { case "omitempty": field.JSONOmitEmpty = true case "string": field.JSONString = true } } } } _, field.TypeIsMarshaller = field.Type.MethodByName("MarshalJSON") _, field.TypeIsUnmarshaller = field.Type.MethodByName("UnmarshalJSON") // Field is done fields = append(fields, field) } return fields } type sortableFieldInfos []FieldInfo func (list sortableFieldInfos) Len() int { return len(list) } func (list sortableFieldInfos) Less(i, j int) bool { return list[i].JSONName < list[j].JSONName } func (list sortableFieldInfos) Swap(i, j int) { a, b := list[i], list[j] list[i], list[j] = b, a } kin-openapi-0.85.0/jsoninfo/marshal.go000066400000000000000000000077001415236407200176310ustar00rootroot00000000000000package jsoninfo import ( "encoding/json" "fmt" "reflect" ) // MarshalStrictStruct function: // * Marshals struct fields, ignoring MarshalJSON() and fields without 'json' tag. // * Correctly handles StrictStruct semantics. func MarshalStrictStruct(value StrictStruct) ([]byte, error) { encoder := NewObjectEncoder() if err := value.EncodeWith(encoder, value); err != nil { return nil, err } return encoder.Bytes() } type ObjectEncoder struct { result map[string]json.RawMessage } func NewObjectEncoder() *ObjectEncoder { return &ObjectEncoder{ result: make(map[string]json.RawMessage, 8), } } // Bytes returns the result of encoding. func (encoder *ObjectEncoder) Bytes() ([]byte, error) { return json.Marshal(encoder.result) } // EncodeExtension adds a key/value to the current JSON object. func (encoder *ObjectEncoder) EncodeExtension(key string, value interface{}) error { data, err := json.Marshal(value) if err != nil { return err } encoder.result[key] = data return nil } // EncodeExtensionMap adds all properties to the result. func (encoder *ObjectEncoder) EncodeExtensionMap(value map[string]json.RawMessage) error { if value != nil { result := encoder.result for k, v := range value { result[k] = v } } return nil } func (encoder *ObjectEncoder) EncodeStructFieldsAndExtensions(value interface{}) error { reflection := reflect.ValueOf(value) // Follow "encoding/json" semantics if reflection.Kind() != reflect.Ptr { // Panic because this is a clear programming error panic(fmt.Errorf("value %s is not a pointer", reflection.Type().String())) } if reflection.IsNil() { // Panic because this is a clear programming error panic(fmt.Errorf("value %s is nil", reflection.Type().String())) } // Take the element reflection = reflection.Elem() // Obtain typeInfo typeInfo := GetTypeInfo(reflection.Type()) // Declare result result := encoder.result // Supported fields iteration: for _, field := range typeInfo.Fields { // Fields without JSON tag are ignored if !field.HasJSONTag { continue } // Marshal fieldValue := reflection.FieldByIndex(field.Index) if v, ok := fieldValue.Interface().(json.Marshaler); ok { if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { if field.JSONOmitEmpty { continue iteration } result[field.JSONName] = []byte("null") continue } fieldData, err := v.MarshalJSON() if err != nil { return err } result[field.JSONName] = fieldData continue } switch fieldValue.Kind() { case reflect.Ptr, reflect.Interface: if fieldValue.IsNil() { if field.JSONOmitEmpty { continue iteration } result[field.JSONName] = []byte("null") continue } case reflect.Struct: case reflect.Map: if field.JSONOmitEmpty && (fieldValue.IsNil() || fieldValue.Len() == 0) { continue iteration } case reflect.Slice: if field.JSONOmitEmpty && fieldValue.Len() == 0 { continue iteration } case reflect.Bool: x := fieldValue.Bool() if field.JSONOmitEmpty && !x { continue iteration } s := "false" if x { s = "true" } result[field.JSONName] = []byte(s) continue iteration case reflect.Int64, reflect.Int, reflect.Int32: if field.JSONOmitEmpty && fieldValue.Int() == 0 { continue iteration } case reflect.Uint64, reflect.Uint, reflect.Uint32: if field.JSONOmitEmpty && fieldValue.Uint() == 0 { continue iteration } case reflect.Float64: if field.JSONOmitEmpty && fieldValue.Float() == 0.0 { continue iteration } case reflect.String: if field.JSONOmitEmpty && len(fieldValue.String()) == 0 { continue iteration } default: panic(fmt.Errorf("field %q has unsupported type %s", field.JSONName, field.Type.String())) } // No special treament is needed // Use plain old "encoding/json".Marshal fieldData, err := json.Marshal(fieldValue.Addr().Interface()) if err != nil { return err } result[field.JSONName] = fieldData } return nil } kin-openapi-0.85.0/jsoninfo/marshal_ref.go000066400000000000000000000011071415236407200204600ustar00rootroot00000000000000package jsoninfo import ( "encoding/json" ) func MarshalRef(value string, otherwise interface{}) ([]byte, error) { if len(value) > 0 { return json.Marshal(&refProps{ Ref: value, }) } return json.Marshal(otherwise) } func UnmarshalRef(data []byte, destRef *string, destOtherwise interface{}) error { refProps := &refProps{} if err := json.Unmarshal(data, refProps); err == nil { ref := refProps.Ref if len(ref) > 0 { *destRef = ref return nil } } return json.Unmarshal(data, destOtherwise) } type refProps struct { Ref string `json:"$ref,omitempty"` } kin-openapi-0.85.0/jsoninfo/marshal_test.go000066400000000000000000000073221415236407200206700ustar00rootroot00000000000000package jsoninfo_test import ( "encoding/json" "testing" "time" "github.com/getkin/kin-openapi/jsoninfo" "github.com/getkin/kin-openapi/openapi3" ) type Simple struct { openapi3.ExtensionProps Bool bool `json:"bool"` Int int `json:"int"` Int64 int64 `json:"int64"` Float64 float64 `json:"float64"` Time time.Time `json:"time"` String string `json:"string"` Bytes []byte `json:"bytes"` } type SimpleOmitEmpty struct { openapi3.ExtensionProps Bool bool `json:"bool,omitempty"` Int int `json:"int,omitempty"` Int64 int64 `json:"int64,omitempty"` Float64 float64 `json:"float64,omitempty"` Time time.Time `json:"time,omitempty"` String string `json:"string,omitempty"` Bytes []byte `json:"bytes,omitempty"` } type SimplePtrOmitEmpty struct { openapi3.ExtensionProps Bool *bool `json:"bool,omitempty"` Int *int `json:"int,omitempty"` Int64 *int64 `json:"int64,omitempty"` Float64 *float64 `json:"float64,omitempty"` Time *time.Time `json:"time,omitempty"` String *string `json:"string,omitempty"` Bytes *[]byte `json:"bytes,omitempty"` } type OriginalNameType struct { openapi3.ExtensionProps Field string `json:",omitempty"` } type RootType struct { openapi3.ExtensionProps EmbeddedType0 EmbeddedType1 } type EmbeddedType0 struct { openapi3.ExtensionProps Field0 string `json:"embedded0,omitempty"` } type EmbeddedType1 struct { openapi3.ExtensionProps Field1 string `json:"embedded1,omitempty"` } // Example describes expected outcome of: // 1.Marshal JSON // 2.Unmarshal value // 3.Marshal value type Example struct { NoMarshal bool NoUnmarshal bool Value jsoninfo.StrictStruct JSON interface{} } var Examples = []Example{ // Primitives { Value: &SimpleOmitEmpty{}, JSON: Object{ "time": time.Unix(0, 0), }, }, { Value: &SimpleOmitEmpty{}, JSON: Object{ "bool": true, "int": 42, "int64": 42, "float64": 3.14, "string": "abc", "bytes": []byte{1, 2, 3}, "time": time.Unix(1, 0), }, }, // Pointers { Value: &SimplePtrOmitEmpty{}, JSON: Object{}, }, { Value: &SimplePtrOmitEmpty{}, JSON: Object{ "bool": true, "int": 42, "int64": 42, "float64": 3.14, "string": "abc", "bytes": []byte{1, 2, 3}, "time": time.Unix(1, 0), }, }, // JSON tag "fieldName" { Value: &Simple{}, JSON: Object{ "bool": false, "int": 0, "int64": 0, "float64": 0, "string": "", "bytes": []byte{}, "time": time.Unix(0, 0), }, }, // JSON tag ",omitempty" { Value: &OriginalNameType{}, JSON: Object{ "Field": "abc", }, }, // Embedding { Value: &RootType{}, JSON: Object{}, }, { Value: &RootType{}, JSON: Object{ "embedded0": "0", "embedded1": "1", "x-other": "abc", }, }, } type Object map[string]interface{} func TestExtensions(t *testing.T) { for _, example := range Examples { // Define JSON that will be unmarshalled expectedData, err := json.Marshal(example.JSON) if err != nil { panic(err) } expected := string(expectedData) // Define value that will marshalled x := example.Value // Unmarshal if !example.NoUnmarshal { t.Logf("Unmarshalling %T", x) if err := jsoninfo.UnmarshalStrictStruct(expectedData, x); err != nil { t.Fatalf("Error unmarshalling %T: %v", x, err) } t.Logf("Marshalling %T", x) } // Marshal if !example.NoMarshal { data, err := jsoninfo.MarshalStrictStruct(x) if err != nil { t.Fatalf("Error marshalling: %v", err) } actually := string(data) if actually != expected { t.Fatalf("Error!\nExpected: %s\nActually: %s", expected, actually) } } } } kin-openapi-0.85.0/jsoninfo/strict_struct.go000066400000000000000000000002541415236407200211130ustar00rootroot00000000000000package jsoninfo type StrictStruct interface { EncodeWith(encoder *ObjectEncoder, value interface{}) error DecodeWith(decoder *ObjectDecoder, value interface{}) error } kin-openapi-0.85.0/jsoninfo/type_info.go000066400000000000000000000024311415236407200201720ustar00rootroot00000000000000package jsoninfo import ( "reflect" "sort" "sync" ) var ( typeInfos = map[reflect.Type]*TypeInfo{} typeInfosMutex sync.RWMutex ) // TypeInfo contains information about JSON serialization of a type type TypeInfo struct { Type reflect.Type Fields []FieldInfo } func GetTypeInfoForValue(value interface{}) *TypeInfo { return GetTypeInfo(reflect.TypeOf(value)) } // GetTypeInfo returns TypeInfo for the given type. func GetTypeInfo(t reflect.Type) *TypeInfo { for t.Kind() == reflect.Ptr { t = t.Elem() } typeInfosMutex.RLock() typeInfo, exists := typeInfos[t] typeInfosMutex.RUnlock() if exists { return typeInfo } if t.Kind() != reflect.Struct { typeInfo = &TypeInfo{ Type: t, } } else { // Allocate typeInfo = &TypeInfo{ Type: t, Fields: make([]FieldInfo, 0, 16), } // Add fields typeInfo.Fields = AppendFields(nil, nil, t) // Sort fields sort.Sort(sortableFieldInfos(typeInfo.Fields)) } // Publish typeInfosMutex.Lock() typeInfos[t] = typeInfo typeInfosMutex.Unlock() return typeInfo } // FieldNames returns all field names func (typeInfo *TypeInfo) FieldNames() []string { fields := typeInfo.Fields names := make([]string, 0, len(fields)) for _, field := range fields { names = append(names, field.JSONName) } return names } kin-openapi-0.85.0/jsoninfo/unmarshal.go000066400000000000000000000065751415236407200202050ustar00rootroot00000000000000package jsoninfo import ( "encoding/json" "fmt" "reflect" ) // UnmarshalStrictStruct function: // * Unmarshals struct fields, ignoring UnmarshalJSON(...) and fields without 'json' tag. // * Correctly handles StrictStruct func UnmarshalStrictStruct(data []byte, value StrictStruct) error { decoder, err := NewObjectDecoder(data) if err != nil { return err } return value.DecodeWith(decoder, value) } type ObjectDecoder struct { Data []byte remainingFields map[string]json.RawMessage } func NewObjectDecoder(data []byte) (*ObjectDecoder, error) { var remainingFields map[string]json.RawMessage if err := json.Unmarshal(data, &remainingFields); err != nil { return nil, fmt.Errorf("failed to unmarshal extension properties: %v (%s)", err, data) } return &ObjectDecoder{ Data: data, remainingFields: remainingFields, }, nil } // DecodeExtensionMap returns all properties that were not decoded previously. func (decoder *ObjectDecoder) DecodeExtensionMap() map[string]json.RawMessage { return decoder.remainingFields } func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) error { reflection := reflect.ValueOf(value) if reflection.Kind() != reflect.Ptr { panic(fmt.Errorf("value %T is not a pointer", value)) } if reflection.IsNil() { panic(fmt.Errorf("value %T is nil", value)) } reflection = reflection.Elem() for (reflection.Kind() == reflect.Interface || reflection.Kind() == reflect.Ptr) && !reflection.IsNil() { reflection = reflection.Elem() } reflectionType := reflection.Type() if reflectionType.Kind() != reflect.Struct { panic(fmt.Errorf("value %T is not a struct", value)) } typeInfo := GetTypeInfo(reflectionType) // Supported fields fields := typeInfo.Fields remainingFields := decoder.remainingFields for fieldIndex, field := range fields { // Fields without JSON tag are ignored if !field.HasJSONTag { continue } // Get data fieldData, exists := remainingFields[field.JSONName] if !exists { continue } // Unmarshal if field.TypeIsUnmarshaller { fieldType := field.Type isPtr := false if fieldType.Kind() == reflect.Ptr { fieldType = fieldType.Elem() isPtr = true } fieldValue := reflect.New(fieldType) if err := fieldValue.Interface().(json.Unmarshaler).UnmarshalJSON(fieldData); err != nil { if field.MultipleFields { i := fieldIndex + 1 if i < len(fields) && fields[i].JSONName == field.JSONName { continue } } return fmt.Errorf("failed to unmarshal property %q (%s): %v", field.JSONName, fieldValue.Type().String(), err) } if !isPtr { fieldValue = fieldValue.Elem() } reflection.FieldByIndex(field.Index).Set(fieldValue) // Remove the field from remaining fields delete(remainingFields, field.JSONName) } else { fieldPtr := reflection.FieldByIndex(field.Index) if fieldPtr.Kind() != reflect.Ptr || fieldPtr.IsNil() { fieldPtr = fieldPtr.Addr() } if err := json.Unmarshal(fieldData, fieldPtr.Interface()); err != nil { if field.MultipleFields { i := fieldIndex + 1 if i < len(fields) && fields[i].JSONName == field.JSONName { continue } } return fmt.Errorf("failed to unmarshal property %q (%s): %v", field.JSONName, fieldPtr.Type().String(), err) } // Remove the field from remaining fields delete(remainingFields, field.JSONName) } } return nil } kin-openapi-0.85.0/jsoninfo/unmarshal_test.go000066400000000000000000000100651415236407200212310ustar00rootroot00000000000000package jsoninfo import ( "errors" "testing" "github.com/stretchr/testify/require" ) func TestNewObjectDecoder(t *testing.T) { data := []byte(` { "field1": 1, "field2": 2 } `) t.Run("test new object decoder", func(t *testing.T) { decoder, err := NewObjectDecoder(data) require.NoError(t, err) require.NotNil(t, decoder) require.Equal(t, data, decoder.Data) require.Equal(t, 2, len(decoder.DecodeExtensionMap())) }) } type mockStrictStruct struct { EncodeWithFn func(encoder *ObjectEncoder, value interface{}) error DecodeWithFn func(decoder *ObjectDecoder, value interface{}) error } func (m *mockStrictStruct) EncodeWith(encoder *ObjectEncoder, value interface{}) error { return m.EncodeWithFn(encoder, value) } func (m *mockStrictStruct) DecodeWith(decoder *ObjectDecoder, value interface{}) error { return m.DecodeWithFn(decoder, value) } func TestUnmarshalStrictStruct(t *testing.T) { data := []byte(` { "field1": 1, "field2": 2 } `) t.Run("test unmarshal with StrictStruct without err", func(t *testing.T) { decodeWithFnCalled := 0 mockStruct := &mockStrictStruct{ EncodeWithFn: func(encoder *ObjectEncoder, value interface{}) error { return nil }, DecodeWithFn: func(decoder *ObjectDecoder, value interface{}) error { decodeWithFnCalled++ return nil }, } err := UnmarshalStrictStruct(data, mockStruct) require.NoError(t, err) require.Equal(t, 1, decodeWithFnCalled) }) t.Run("test unmarshal with StrictStruct with err", func(t *testing.T) { decodeWithFnCalled := 0 mockStruct := &mockStrictStruct{ EncodeWithFn: func(encoder *ObjectEncoder, value interface{}) error { return nil }, DecodeWithFn: func(decoder *ObjectDecoder, value interface{}) error { decodeWithFnCalled++ return errors.New("unable to decode the value") }, } err := UnmarshalStrictStruct(data, mockStruct) require.Error(t, err) require.Equal(t, 1, decodeWithFnCalled) }) } func TestDecodeStructFieldsAndExtensions(t *testing.T) { data := []byte(` { "field1": "field1", "field2": "field2" } `) decoder, err := NewObjectDecoder(data) require.NoError(t, err) require.NotNil(t, decoder) t.Run("value is not pointer", func(t *testing.T) { var value interface{} require.Panics(t, func() { _ = decoder.DecodeStructFieldsAndExtensions(value) }, "value is not a pointer") }) t.Run("value is nil", func(t *testing.T) { var value *string = nil require.Panics(t, func() { _ = decoder.DecodeStructFieldsAndExtensions(value) }, "value is nil") }) t.Run("value is not struct", func(t *testing.T) { var value = "simple string" require.Panics(t, func() { _ = decoder.DecodeStructFieldsAndExtensions(&value) }, "value is not struct") }) t.Run("successfully decoded with all fields", func(t *testing.T) { d, err := NewObjectDecoder(data) require.NoError(t, err) require.NotNil(t, d) var value = struct { Field1 string `json:"field1"` Field2 string `json:"field2"` }{} err = d.DecodeStructFieldsAndExtensions(&value) require.NoError(t, err) require.Equal(t, "field1", value.Field1) require.Equal(t, "field2", value.Field2) require.Equal(t, 0, len(d.DecodeExtensionMap())) }) t.Run("successfully decoded with renaming field", func(t *testing.T) { d, err := NewObjectDecoder(data) require.NoError(t, err) require.NotNil(t, d) var value = struct { Field1 string `json:"field1"` }{} err = d.DecodeStructFieldsAndExtensions(&value) require.NoError(t, err) require.Equal(t, "field1", value.Field1) require.Equal(t, 1, len(d.DecodeExtensionMap())) }) t.Run("un-successfully decoded due to data mismatch", func(t *testing.T) { d, err := NewObjectDecoder(data) require.NoError(t, err) require.NotNil(t, d) var value = struct { Field1 int `json:"field1"` }{} err = d.DecodeStructFieldsAndExtensions(&value) require.Error(t, err) require.EqualError(t, err, `failed to unmarshal property "field1" (*int): json: cannot unmarshal string into Go value of type int`) require.Equal(t, 0, value.Field1) require.Equal(t, 2, len(d.DecodeExtensionMap())) }) } kin-openapi-0.85.0/jsoninfo/unsupported_properties_error.go000066400000000000000000000021521415236407200242530ustar00rootroot00000000000000package jsoninfo import ( "encoding/json" "fmt" "sort" ) // UnsupportedPropertiesError is a helper for extensions that want to refuse // unsupported JSON object properties. // // It produces a helpful error message. type UnsupportedPropertiesError struct { Value interface{} UnsupportedProperties map[string]json.RawMessage } func NewUnsupportedPropertiesError(v interface{}, m map[string]json.RawMessage) error { return &UnsupportedPropertiesError{ Value: v, UnsupportedProperties: m, } } func (err *UnsupportedPropertiesError) Error() string { m := err.UnsupportedProperties typeInfo := GetTypeInfoForValue(err.Value) if m == nil || typeInfo == nil { return fmt.Sprintf("invalid %T", *err) } keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) supported := typeInfo.FieldNames() if len(supported) == 0 { return fmt.Sprintf("type \"%T\" doesn't take any properties. Unsupported properties: %+v", err.Value, keys) } return fmt.Sprintf("unsupported properties: %+v (supported properties are: %+v)", keys, supported) } kin-openapi-0.85.0/openapi2/000077500000000000000000000000001415236407200155375ustar00rootroot00000000000000kin-openapi-0.85.0/openapi2/doc.go000066400000000000000000000005021415236407200166300ustar00rootroot00000000000000// Package openapi2 parses and writes OpenAPIv2 specification documents. // // Does not cover all elements of OpenAPIv2. // When OpenAPI version 3 is backwards-compatible with version 2, version 3 elements have been used. // // See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md package openapi2 kin-openapi-0.85.0/openapi2/openapi2.go000066400000000000000000000261161415236407200176110ustar00rootroot00000000000000package openapi2 import ( "fmt" "net/http" "sort" "github.com/getkin/kin-openapi/jsoninfo" "github.com/getkin/kin-openapi/openapi3" ) // T is the root of an OpenAPI v2 document type T struct { openapi3.ExtensionProps Swagger string `json:"swagger" yaml:"swagger"` Info openapi3.Info `json:"info" yaml:"info"` ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` Host string `json:"host,omitempty" yaml:"host,omitempty"` BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty,noref" yaml:"definitions,omitempty,noref"` Parameters map[string]*Parameter `json:"parameters,omitempty,noref" yaml:"parameters,omitempty,noref"` Responses map[string]*Response `json:"responses,omitempty,noref" yaml:"responses,omitempty,noref"` SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } func (doc *T) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(doc) } func (doc *T) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, doc) } func (doc *T) AddOperation(path string, method string, operation *Operation) { paths := doc.Paths if paths == nil { paths = make(map[string]*PathItem, 8) doc.Paths = paths } pathItem := paths[path] if pathItem == nil { pathItem = &PathItem{} paths[path] = pathItem } pathItem.SetOperation(method, operation) } type PathItem struct { openapi3.ExtensionProps Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` } func (pathItem *PathItem) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(pathItem) } func (pathItem *PathItem) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, pathItem) } func (pathItem *PathItem) Operations() map[string]*Operation { operations := make(map[string]*Operation, 8) if v := pathItem.Delete; v != nil { operations[http.MethodDelete] = v } if v := pathItem.Get; v != nil { operations[http.MethodGet] = v } if v := pathItem.Head; v != nil { operations[http.MethodHead] = v } if v := pathItem.Options; v != nil { operations[http.MethodOptions] = v } if v := pathItem.Patch; v != nil { operations[http.MethodPatch] = v } if v := pathItem.Post; v != nil { operations[http.MethodPost] = v } if v := pathItem.Put; v != nil { operations[http.MethodPut] = v } return operations } func (pathItem *PathItem) GetOperation(method string) *Operation { switch method { case http.MethodDelete: return pathItem.Delete case http.MethodGet: return pathItem.Get case http.MethodHead: return pathItem.Head case http.MethodOptions: return pathItem.Options case http.MethodPatch: return pathItem.Patch case http.MethodPost: return pathItem.Post case http.MethodPut: return pathItem.Put default: panic(fmt.Errorf("unsupported HTTP method %q", method)) } } func (pathItem *PathItem) SetOperation(method string, operation *Operation) { switch method { case http.MethodDelete: pathItem.Delete = operation case http.MethodGet: pathItem.Get = operation case http.MethodHead: pathItem.Head = operation case http.MethodOptions: pathItem.Options = operation case http.MethodPatch: pathItem.Patch = operation case http.MethodPost: pathItem.Post = operation case http.MethodPut: pathItem.Put = operation default: panic(fmt.Errorf("unsupported HTTP method %q", method)) } } type Operation struct { openapi3.ExtensionProps Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` Responses map[string]*Response `json:"responses" yaml:"responses"` Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` } func (operation *Operation) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(operation) } func (operation *Operation) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, operation) } type Parameters []*Parameter var _ sort.Interface = Parameters{} func (ps Parameters) Len() int { return len(ps) } func (ps Parameters) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] } func (ps Parameters) Less(i, j int) bool { if ps[i].Name != ps[j].Name { return ps[i].Name < ps[j].Name } if ps[i].In != ps[j].In { return ps[i].In < ps[j].In } return ps[i].Ref < ps[j].Ref } type Parameter struct { openapi3.ExtensionProps Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` } func (parameter *Parameter) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(parameter) } func (parameter *Parameter) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, parameter) } type Response struct { openapi3.ExtensionProps Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` } func (response *Response) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(response) } func (response *Response) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, response) } type Header struct { openapi3.ExtensionProps Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` } func (header *Header) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(header) } func (header *Header) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, header) } type SecurityRequirements []map[string][]string type SecurityScheme struct { openapi3.ExtensionProps Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Flow string `json:"flow,omitempty" yaml:"flow,omitempty"` AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"` Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } func (securityScheme *SecurityScheme) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(securityScheme) } func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, securityScheme) } kin-openapi-0.85.0/openapi2/openapi2_test.go000066400000000000000000000021171415236407200206430ustar00rootroot00000000000000package openapi2_test import ( "encoding/json" "fmt" "io/ioutil" "reflect" "github.com/getkin/kin-openapi/openapi2" "github.com/ghodss/yaml" ) func Example() { input, err := ioutil.ReadFile("testdata/swagger.json") if err != nil { panic(err) } var doc openapi2.T if err = json.Unmarshal(input, &doc); err != nil { panic(err) } if doc.ExternalDocs.Description != "Find out more about Swagger" { panic(`doc.ExternalDocs was parsed incorrectly!`) } outputJSON, err := json.Marshal(doc) if err != nil { panic(err) } var docAgainFromJSON openapi2.T if err = json.Unmarshal(outputJSON, &docAgainFromJSON); err != nil { panic(err) } if !reflect.DeepEqual(doc, docAgainFromJSON) { fmt.Println("objects doc & docAgainFromJSON should be the same") } outputYAML, err := yaml.Marshal(doc) if err != nil { panic(err) } var docAgainFromYAML openapi2.T if err = yaml.Unmarshal(outputYAML, &docAgainFromYAML); err != nil { panic(err) } if !reflect.DeepEqual(doc, docAgainFromYAML) { fmt.Println("objects doc & docAgainFromYAML should be the same") } // Output: } kin-openapi-0.85.0/openapi2/testdata/000077500000000000000000000000001415236407200173505ustar00rootroot00000000000000kin-openapi-0.85.0/openapi2/testdata/swagger.json000066400000000000000000000321521415236407200217050ustar00rootroot00000000000000{"swagger":"2.0","info":{"title":"Swagger Petstore","description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.","termsOfService":"http://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"1.0.3"},"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"},"schemes":["https","http"],"host":"petstore.swagger.io","basePath":"/v2","paths":{"/pet":{"post":{"summary":"Add a new pet to the store","tags":["pet"],"operationId":"addPet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"put":{"summary":"Update an existing pet","tags":["pet"],"operationId":"updatePet","parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"405":{"description":"Validation exception"}},"consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByStatus":{"get":{"summary":"Finds Pets by status","description":"Multiple status values can be provided with comma separated strings","tags":["pet"],"operationId":"findPetsByStatus","parameters":[{"in":"query","name":"status","description":"Status values that need to be considered for filter","required":true,"type":"array","items":{"default":"available","enum":["available","pending","sold"],"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid status value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByTags":{"get":{"summary":"Finds Pets by tags","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","tags":["pet"],"operationId":"findPetsByTags","parameters":[{"in":"query","name":"tags","description":"Tags to filter by","required":true,"type":"array","items":{"type":"string"}}],"responses":{"200":{"description":"successful operation","schema":{"items":{"$ref":"#/definitions/Pet"},"type":"array"}},"400":{"description":"Invalid tag value"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}":{"delete":{"summary":"Deletes a pet","tags":["pet"],"operationId":"deletePet","parameters":[{"in":"header","name":"api_key","type":"string"},{"in":"path","name":"petId","description":"Pet id to delete","required":true,"type":"integer","format":"int64"}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]},"get":{"summary":"Find pet by ID","description":"Returns a single pet","tags":["pet"],"operationId":"getPetById","parameters":[{"in":"path","name":"petId","description":"ID of pet to return","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Pet"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"produces":["application/json","application/xml"],"security":[{"api_key":[]}]},"post":{"summary":"Updates a pet in the store with form data","tags":["pet"],"operationId":"updatePetWithForm","parameters":[{"in":"path","name":"petId","description":"ID of pet that needs to be updated","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"name","description":"Updated name of the pet","type":"string"},{"in":"formData","name":"status","description":"Updated status of the pet","type":"string"}],"responses":{"405":{"description":"Invalid input"}},"consumes":["application/x-www-form-urlencoded"],"produces":["application/json","application/xml"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}/uploadImage":{"post":{"summary":"uploads an image","tags":["pet"],"operationId":"uploadFile","parameters":[{"in":"path","name":"petId","description":"ID of pet to update","required":true,"type":"integer","format":"int64"},{"in":"formData","name":"additionalMetadata","description":"Additional data to pass to server","type":"string"},{"in":"formData","name":"file","description":"file to upload","type":"file"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ApiResponse"}}},"consumes":["multipart/form-data"],"produces":["application/json"],"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/store/inventory":{"get":{"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities","tags":["store"],"operationId":"getInventory","responses":{"200":{"description":"successful operation","schema":{"additionalProperties":{"format":"int32","type":"integer"},"type":"object"}}},"produces":["application/json"],"security":[{"api_key":[]}]}},"/store/order":{"post":{"summary":"Place an order for a pet","tags":["store"],"operationId":"placeOrder","parameters":[{"in":"body","name":"body","description":"order placed for purchasing the pet","required":true,"schema":{"$ref":"#/definitions/Order"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid Order"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/store/order/{orderId}":{"delete":{"summary":"Delete purchase order by ID","description":"For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors","tags":["store"],"operationId":"deleteOrder","parameters":[{"in":"path","name":"orderId","description":"ID of the order that needs to be deleted","required":true,"type":"integer","format":"int64","minimum":1}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Find purchase order by ID","description":"For valid response try integer IDs with value \u003e= 1 and \u003c= 10. Other values will generated exceptions","tags":["store"],"operationId":"getOrderById","parameters":[{"in":"path","name":"orderId","description":"ID of pet that needs to be fetched","required":true,"type":"integer","format":"int64","minimum":1,"maximum":10}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}},"produces":["application/json","application/xml"]}},"/user":{"post":{"summary":"Create user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"createUser","parameters":[{"in":"body","name":"body","description":"Created user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithArray":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithArrayInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/createWithList":{"post":{"summary":"Creates list of users with given input array","tags":["user"],"operationId":"createUsersWithListInput","parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"items":{"$ref":"#/definitions/User"},"type":"array"}}],"responses":{"default":{"description":"successful operation"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}},"/user/login":{"get":{"summary":"Logs user into the system","tags":["user"],"operationId":"loginUser","parameters":[{"in":"query","name":"username","description":"The user name for login","required":true,"type":"string"},{"in":"query","name":"password","description":"The password for login in clear text","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"string"},"headers":{"X-Expires-After":{"description":"date in UTC when token expires","type":"string"},"X-Rate-Limit":{"description":"calls per hour allowed by the user","type":"integer"}}},"400":{"description":"Invalid username/password supplied"}},"produces":["application/json","application/xml"]}},"/user/logout":{"get":{"summary":"Logs out current logged in user session","tags":["user"],"operationId":"logoutUser","responses":{"default":{"description":"successful operation"}},"produces":["application/json","application/xml"]}},"/user/{username}":{"delete":{"summary":"Delete user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"deleteUser","parameters":[{"in":"path","name":"username","description":"The name that needs to be deleted","required":true,"type":"string"}],"responses":{"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"get":{"summary":"Get user by user name","tags":["user"],"operationId":"getUserByName","parameters":[{"in":"path","name":"username","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}},"produces":["application/json","application/xml"]},"put":{"summary":"Updated user","description":"This can only be done by the logged in user.","tags":["user"],"operationId":"updateUser","parameters":[{"in":"path","name":"username","description":"name that need to be updated","required":true,"type":"string"},{"in":"body","name":"body","description":"Updated user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"400":{"description":"Invalid user supplied"},"404":{"description":"User not found"}},"consumes":["application/json"],"produces":["application/json","application/xml"]}}},"definitions":{"ApiResponse":{"properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"},"type":{"type":"string"}},"type":"object"},"Category":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Category"}},"Order":{"properties":{"complete":{"type":"boolean"},"id":{"format":"int64","type":"integer"},"petId":{"format":"int64","type":"integer"},"quantity":{"format":"int32","type":"integer"},"shipDate":{"format":"date-time","type":"string"},"status":{"description":"Order Status","enum":["placed","approved","delivered"],"type":"string"}},"type":"object","xml":{"name":"Order"}},"Pet":{"properties":{"category":{"$ref":"#/definitions/Category"},"id":{"format":"int64","type":"integer"},"name":{"example":"doggie","type":"string"},"photoUrls":{"items":{"type":"string","xml":{"name":"photoUrl"}},"type":"array","xml":{"wrapped":true}},"status":{"description":"pet status in the store","enum":["available","pending","sold"],"type":"string"},"tags":{"items":{"$ref":"#/definitions/Tag"},"type":"array","xml":{"wrapped":true}}},"required":["name","photoUrls"],"type":"object","xml":{"name":"Pet"}},"Tag":{"properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"}},"type":"object","xml":{"name":"Tag"}},"User":{"properties":{"email":{"type":"string"},"firstName":{"type":"string"},"id":{"format":"int64","type":"integer"},"lastName":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"description":"User Status","format":"int32","type":"integer"},"username":{"type":"string"}},"type":"object","xml":{"name":"User"}}},"securityDefinitions":{"api_key":{"type":"apiKey","in":"header","name":"api_key"},"petstore_auth":{"type":"oauth2","flow":"implicit","authorizationUrl":"https://petstore.swagger.io/oauth/authorize","scopes":{"read:pets":"read your pets","write:pets":"modify pets in your account"}}},"tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"store","description":"Access to Petstore orders"},{"name":"user","description":"Operations about user","externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}]}kin-openapi-0.85.0/openapi2conv/000077500000000000000000000000001415236407200164255ustar00rootroot00000000000000kin-openapi-0.85.0/openapi2conv/doc.go000066400000000000000000000001421415236407200175160ustar00rootroot00000000000000// Package openapi2conv converts an OpenAPI v2 specification document to v3. package openapi2conv kin-openapi-0.85.0/openapi2conv/issue187_test.go000066400000000000000000000114431415236407200214060ustar00rootroot00000000000000package openapi2conv import ( "context" "encoding/json" "testing" "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" "github.com/ghodss/yaml" "github.com/stretchr/testify/require" ) func v2v3JSON(spec2 []byte) (doc3 *openapi3.T, err error) { var doc2 openapi2.T if err = json.Unmarshal(spec2, &doc2); err != nil { return } doc3, err = ToV3(&doc2) return } func v2v3YAML(spec2 []byte) (doc3 *openapi3.T, err error) { var doc2 openapi2.T if err = yaml.Unmarshal(spec2, &doc2); err != nil { return } doc3, err = ToV3(&doc2) return } func TestIssue187(t *testing.T) { spec := ` { "swagger": "2.0", "info": { "description": "Test Golang Application", "version": "1.0", "title": "Test", "contact": { "name": "Test", "email": "test@test.com" } }, "paths": { "/me": { "get": { "description": "", "operationId": "someTest", "summary": "Some test", "tags": ["probe"], "produces": ["application/json"], "responses": { "200": { "description": "successful operation", "schema": {"$ref": "#/definitions/model.ProductSearchAttributeRequest"} } } } } }, "host": "", "basePath": "/test", "definitions": { "model.ProductSearchAttributeRequest": { "type": "object", "properties": { "filterField": { "type": "string" }, "filterKey": { "type": "string" }, "type": { "type": "string" }, "values": { "$ref": "#/definitions/model.ProductSearchAttributeValueRequest" } }, "title": "model.ProductSearchAttributeRequest" }, "model.ProductSearchAttributeValueRequest": { "type": "object", "properties": { "imageUrl": { "type": "string" }, "text": { "type": "string" } }, "title": "model.ProductSearchAttributeValueRequest" } } } ` doc3, err := v2v3JSON([]byte(spec)) require.NoError(t, err) spec3, err := json.Marshal(doc3) require.NoError(t, err) const expected = `{"components":{"schemas":{"model.ProductSearchAttributeRequest":{"properties":{"filterField":{"type":"string"},"filterKey":{"type":"string"},"type":{"type":"string"},"values":{"$ref":"#/components/schemas/model.ProductSearchAttributeValueRequest"}},"title":"model.ProductSearchAttributeRequest","type":"object"},"model.ProductSearchAttributeValueRequest":{"properties":{"imageUrl":{"type":"string"},"text":{"type":"string"}},"title":"model.ProductSearchAttributeValueRequest","type":"object"}}},"info":{"contact":{"email":"test@test.com","name":"Test"},"description":"Test Golang Application","title":"Test","version":"1.0"},"openapi":"3.0.3","paths":{"/me":{"get":{"operationId":"someTest","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/model.ProductSearchAttributeRequest"}}},"description":"successful operation"}},"summary":"Some test","tags":["probe"]}}}}` require.Equal(t, string(spec3), expected) err = doc3.Validate(context.Background()) require.NoError(t, err) } func TestIssue237(t *testing.T) { spec := ` swagger: '2.0' info: version: 1.0.0 title: title paths: /test: get: parameters: - in: body schema: $ref: '#/definitions/TestRef' responses: '200': description: description definitions: TestRef: type: object allOf: - $ref: '#/definitions/TestRef2' TestRef2: type: object ` doc3, err := v2v3YAML([]byte(spec)) require.NoError(t, err) spec3, err := yaml.Marshal(doc3) require.NoError(t, err) const expected = `components: schemas: TestRef: allOf: - $ref: '#/components/schemas/TestRef2' type: object TestRef2: type: object info: title: title version: 1.0.0 openapi: 3.0.3 paths: /test: get: requestBody: content: '*/*': schema: $ref: '#/components/schemas/TestRef' responses: "200": description: description ` require.Equal(t, string(spec3), expected) err = doc3.Validate(context.Background()) require.NoError(t, err) } func TestPR449(t *testing.T) { spec := ` swagger: '2.0' info: version: 1.0.0 title: title securityDefinitions: OAuth2Application: type: "oauth2" flow: "application" tokenUrl: "example.com/oauth2/token" ` doc3, err := v2v3YAML([]byte(spec)) require.NoError(t, err) require.NotNil(t, doc3.Components.SecuritySchemes["OAuth2Application"].Value.Flows.ClientCredentials) _, err = yaml.Marshal(doc3) require.NoError(t, err) doc2, err := FromV3(doc3) require.NoError(t, err) require.Equal(t, doc2.SecurityDefinitions["OAuth2Application"].Flow, "application") } kin-openapi-0.85.0/openapi2conv/issue440_test.go000066400000000000000000000022071415236407200213740ustar00rootroot00000000000000package openapi2conv import ( "context" "encoding/json" "os" "testing" "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) func TestIssue440(t *testing.T) { doc2file, err := os.Open("testdata/swagger.json") require.NoError(t, err) defer doc2file.Close() var doc2 openapi2.T err = json.NewDecoder(doc2file).Decode(&doc2) require.NoError(t, err) doc3, err := ToV3(&doc2) require.NoError(t, err) err = doc3.Validate(context.Background()) require.NoError(t, err) require.Equal(t, openapi3.Servers{ {URL: "https://petstore.swagger.io/v2"}, {URL: "http://petstore.swagger.io/v2"}, }, doc3.Servers) doc2.Host = "your-bot-domain.de" doc2.Schemes = nil doc2.BasePath = "" doc3, err = ToV3(&doc2) require.NoError(t, err) err = doc3.Validate(context.Background()) require.NoError(t, err) require.Equal(t, openapi3.Servers{ {URL: "https://your-bot-domain.de/"}, }, doc3.Servers) doc2.Host = "https://your-bot-domain.de" doc2.Schemes = nil doc2.BasePath = "" doc3, err = ToV3(&doc2) require.Error(t, err) require.Contains(t, err.Error(), `invalid host`) } kin-openapi-0.85.0/openapi2conv/openapi2_conv.go000066400000000000000000001024371415236407200215250ustar00rootroot00000000000000package openapi2conv import ( "encoding/json" "errors" "fmt" "net/url" "sort" "strings" "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" ) // ToV3 converts an OpenAPIv2 spec to an OpenAPIv3 spec func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { stripNonCustomExtensions(doc2.Extensions) doc3 := &openapi3.T{ OpenAPI: "3.0.3", Info: &doc2.Info, Components: openapi3.Components{}, Tags: doc2.Tags, ExtensionProps: doc2.ExtensionProps, ExternalDocs: doc2.ExternalDocs, } if host := doc2.Host; host != "" { if strings.Contains(host, "/") { err := fmt.Errorf("invalid host %q. This MUST be the host only and does not include the scheme nor sub-paths.", host) return nil, err } schemes := doc2.Schemes if len(schemes) == 0 { schemes = []string{"https"} } basePath := doc2.BasePath if basePath == "" { basePath = "/" } for _, scheme := range schemes { u := url.URL{ Scheme: scheme, Host: host, Path: basePath, } doc3.AddServer(&openapi3.Server{URL: u.String()}) } } doc3.Components.Schemas = make(map[string]*openapi3.SchemaRef) if parameters := doc2.Parameters; len(parameters) != 0 { doc3.Components.Parameters = make(map[string]*openapi3.ParameterRef) doc3.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) for k, parameter := range parameters { v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(&doc3.Components, parameter, doc2.Consumes) switch { case err != nil: return nil, err case v3RequestBody != nil: doc3.Components.RequestBodies[k] = v3RequestBody case v3SchemaMap != nil: for _, v3Schema := range v3SchemaMap { doc3.Components.Schemas[k] = v3Schema } default: doc3.Components.Parameters[k] = v3Parameter } } } if paths := doc2.Paths; len(paths) != 0 { doc3Paths := make(map[string]*openapi3.PathItem, len(paths)) for path, pathItem := range paths { r, err := ToV3PathItem(doc2, &doc3.Components, pathItem, doc2.Consumes) if err != nil { return nil, err } doc3Paths[path] = r } doc3.Paths = doc3Paths } if responses := doc2.Responses; len(responses) != 0 { doc3.Components.Responses = make(map[string]*openapi3.ResponseRef, len(responses)) for k, response := range responses { r, err := ToV3Response(response) if err != nil { return nil, err } doc3.Components.Responses[k] = r } } for key, schema := range ToV3Schemas(doc2.Definitions) { doc3.Components.Schemas[key] = schema } if m := doc2.SecurityDefinitions; len(m) != 0 { doc3SecuritySchemes := make(map[string]*openapi3.SecuritySchemeRef) for k, v := range m { r, err := ToV3SecurityScheme(v) if err != nil { return nil, err } doc3SecuritySchemes[k] = r } doc3.Components.SecuritySchemes = doc3SecuritySchemes } doc3.Security = ToV3SecurityRequirements(doc2.Security) { sl := openapi3.NewLoader() if err := sl.ResolveRefsIn(doc3, nil); err != nil { return nil, err } } return doc3, nil } func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) { stripNonCustomExtensions(pathItem.Extensions) doc3 := &openapi3.PathItem{ ExtensionProps: pathItem.ExtensionProps, } for method, operation := range pathItem.Operations() { doc3Operation, err := ToV3Operation(doc2, components, pathItem, operation, consumes) if err != nil { return nil, err } doc3.SetOperation(method, doc3Operation) } for _, parameter := range pathItem.Parameters { v3Parameter, v3RequestBody, v3Schema, err := ToV3Parameter(components, parameter, consumes) switch { case err != nil: return nil, err case v3RequestBody != nil: return nil, errors.New("pathItem must not have a body parameter") case v3Schema != nil: return nil, errors.New("pathItem must not have a schema parameter") default: doc3.Parameters = append(doc3.Parameters, v3Parameter) } } return doc3, nil } func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, operation *openapi2.Operation, consumes []string) (*openapi3.Operation, error) { if operation == nil { return nil, nil } stripNonCustomExtensions(operation.Extensions) doc3 := &openapi3.Operation{ OperationID: operation.OperationID, Summary: operation.Summary, Description: operation.Description, Tags: operation.Tags, ExtensionProps: operation.ExtensionProps, } if v := operation.Security; v != nil { doc3Security := ToV3SecurityRequirements(*v) doc3.Security = &doc3Security } if len(operation.Consumes) > 0 { consumes = operation.Consumes } var reqBodies []*openapi3.RequestBodyRef formDataSchemas := make(map[string]*openapi3.SchemaRef) for _, parameter := range operation.Parameters { v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(components, parameter, consumes) switch { case err != nil: return nil, err case v3RequestBody != nil: reqBodies = append(reqBodies, v3RequestBody) case v3SchemaMap != nil: for key, v3Schema := range v3SchemaMap { formDataSchemas[key] = v3Schema } default: doc3.Parameters = append(doc3.Parameters, v3Parameter) } } var err error if doc3.RequestBody, err = onlyOneReqBodyParam(reqBodies, formDataSchemas, components, consumes); err != nil { return nil, err } if responses := operation.Responses; responses != nil { doc3Responses := make(openapi3.Responses, len(responses)) for k, response := range responses { doc3, err := ToV3Response(response) if err != nil { return nil, err } doc3Responses[k] = doc3 } doc3.Responses = doc3Responses } return doc3, nil } func getParameterNameFromOldRef(ref string) string { cleanPath := strings.TrimPrefix(ref, "#/parameters/") pathSections := strings.SplitN(cleanPath, "/", 1) return pathSections[0] } func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Parameter, consumes []string) (*openapi3.ParameterRef, *openapi3.RequestBodyRef, map[string]*openapi3.SchemaRef, error) { if ref := parameter.Ref; ref != "" { if strings.HasPrefix(ref, "#/parameters/") { name := getParameterNameFromOldRef(ref) if _, ok := components.RequestBodies[name]; ok { v3Ref := strings.Replace(ref, "#/parameters/", "#/components/requestBodies/", 1) return nil, &openapi3.RequestBodyRef{Ref: v3Ref}, nil, nil } else if schema, ok := components.Schemas[name]; ok { schemaRefMap := make(map[string]*openapi3.SchemaRef) if val, ok := schema.Value.Extensions["x-formData-name"]; ok { name = val.(string) } v3Ref := strings.Replace(ref, "#/parameters/", "#/components/schemas/", 1) schemaRefMap[name] = &openapi3.SchemaRef{Ref: v3Ref} return nil, nil, schemaRefMap, nil } } return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil, nil } stripNonCustomExtensions(parameter.Extensions) switch parameter.In { case "body": result := &openapi3.RequestBody{ Description: parameter.Description, Required: parameter.Required, ExtensionProps: parameter.ExtensionProps, } if parameter.Name != "" { if result.Extensions == nil { result.Extensions = make(map[string]interface{}) } result.Extensions["x-originalParamName"] = parameter.Name } if schemaRef := parameter.Schema; schemaRef != nil { // Assuming JSON result.WithSchemaRef(ToV3SchemaRef(schemaRef), consumes) } return nil, &openapi3.RequestBodyRef{Value: result}, nil, nil case "formData": format, typ := parameter.Format, parameter.Type if typ == "file" { format, typ = "binary", "string" } if parameter.ExtensionProps.Extensions == nil { parameter.ExtensionProps.Extensions = make(map[string]interface{}) } parameter.ExtensionProps.Extensions["x-formData-name"] = parameter.Name var required []string if parameter.Required { required = []string{parameter.Name} } schemaRef := &openapi3.SchemaRef{ Value: &openapi3.Schema{ Description: parameter.Description, Type: typ, ExtensionProps: parameter.ExtensionProps, Format: format, Enum: parameter.Enum, Min: parameter.Minimum, Max: parameter.Maximum, ExclusiveMin: parameter.ExclusiveMin, ExclusiveMax: parameter.ExclusiveMax, MinLength: parameter.MinLength, MaxLength: parameter.MaxLength, Default: parameter.Default, Items: parameter.Items, MinItems: parameter.MinItems, MaxItems: parameter.MaxItems, Pattern: parameter.Pattern, AllowEmptyValue: parameter.AllowEmptyValue, UniqueItems: parameter.UniqueItems, MultipleOf: parameter.MultipleOf, Required: required, }, } schemaRefMap := make(map[string]*openapi3.SchemaRef) schemaRefMap[parameter.Name] = schemaRef return nil, nil, schemaRefMap, nil default: required := parameter.Required if parameter.In == openapi3.ParameterInPath { required = true } var schemaRefRef string if schemaRef := parameter.Schema; schemaRef != nil && schemaRef.Ref != "" { schemaRefRef = schemaRef.Ref } result := &openapi3.Parameter{ In: parameter.In, Name: parameter.Name, Description: parameter.Description, Required: required, ExtensionProps: parameter.ExtensionProps, Schema: ToV3SchemaRef(&openapi3.SchemaRef{Value: &openapi3.Schema{ Type: parameter.Type, Format: parameter.Format, Enum: parameter.Enum, Min: parameter.Minimum, Max: parameter.Maximum, ExclusiveMin: parameter.ExclusiveMin, ExclusiveMax: parameter.ExclusiveMax, MinLength: parameter.MinLength, MaxLength: parameter.MaxLength, Default: parameter.Default, Items: parameter.Items, MinItems: parameter.MinItems, MaxItems: parameter.MaxItems, Pattern: parameter.Pattern, AllowEmptyValue: parameter.AllowEmptyValue, UniqueItems: parameter.UniqueItems, MultipleOf: parameter.MultipleOf, }, Ref: schemaRefRef, }), } return &openapi3.ParameterRef{Value: result}, nil, nil, nil } } func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool, consumes []string) *openapi3.RequestBodyRef { if len(bodies) != len(reqs) { panic(`request bodies and them being required must match`) } requireds := make([]string, 0, len(reqs)) for propName, req := range reqs { if _, ok := bodies[propName]; !ok { panic(`request bodies and them being required must match`) } if req { requireds = append(requireds, propName) } } schema := &openapi3.Schema{ Type: "object", Properties: ToV3Schemas(bodies), Required: requireds, } return &openapi3.RequestBodyRef{ Value: openapi3.NewRequestBody().WithSchema(schema, consumes), } } func getParameterNameFromNewRef(ref string) string { cleanPath := strings.TrimPrefix(ref, "#/components/schemas/") pathSections := strings.SplitN(cleanPath, "/", 1) return pathSections[0] } func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[string]*openapi3.SchemaRef, components *openapi3.Components, consumes []string) (*openapi3.RequestBodyRef, error) { if len(bodies) > 1 { return nil, errors.New("multiple body parameters cannot exist for the same operation") } if len(bodies) != 0 && len(formDataSchemas) != 0 { return nil, errors.New("body and form parameters cannot exist together for the same operation") } for _, requestBodyRef := range bodies { return requestBodyRef, nil } if len(formDataSchemas) > 0 { formDataParams := make(map[string]*openapi3.SchemaRef, len(formDataSchemas)) formDataReqs := make(map[string]bool, len(formDataSchemas)) for formDataName, formDataSchema := range formDataSchemas { if formDataSchema.Ref != "" { name := getParameterNameFromNewRef(formDataSchema.Ref) if schema := components.Schemas[name]; schema != nil && schema.Value != nil { if tempName, ok := schema.Value.Extensions["x-formData-name"]; ok { name = tempName.(string) } formDataParams[name] = formDataSchema formDataReqs[name] = false for _, req := range schema.Value.Required { if name == req { formDataReqs[name] = true } } } } else if formDataSchema.Value != nil { formDataParams[formDataName] = formDataSchema formDataReqs[formDataName] = false for _, req := range formDataSchema.Value.Required { if formDataName == req { formDataReqs[formDataName] = true } } } } return formDataBody(formDataParams, formDataReqs, consumes), nil } return nil, nil } func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) { if ref := response.Ref; ref != "" { return &openapi3.ResponseRef{Ref: ToV3Ref(ref)}, nil } stripNonCustomExtensions(response.Extensions) result := &openapi3.Response{ Description: &response.Description, ExtensionProps: response.ExtensionProps, } if schemaRef := response.Schema; schemaRef != nil { result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef)) } return &openapi3.ResponseRef{Value: result}, nil } func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef { schemas := make(map[string]*openapi3.SchemaRef, len(defs)) for name, schema := range defs { schemas[name] = ToV3SchemaRef(schema) } return schemas } func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { if ref := schema.Ref; ref != "" { return &openapi3.SchemaRef{Ref: ToV3Ref(ref)} } if schema.Value == nil { return schema } if schema.Value.Items != nil { schema.Value.Items = ToV3SchemaRef(schema.Value.Items) } for k, v := range schema.Value.Properties { schema.Value.Properties[k] = ToV3SchemaRef(v) } if v := schema.Value.AdditionalProperties; v != nil { schema.Value.AdditionalProperties = ToV3SchemaRef(v) } for i, v := range schema.Value.AllOf { schema.Value.AllOf[i] = ToV3SchemaRef(v) } return schema } var ref2To3 = map[string]string{ "#/definitions/": "#/components/schemas/", "#/responses/": "#/components/responses/", "#/parameters/": "#/components/parameters/", } func ToV3Ref(ref string) string { for old, new := range ref2To3 { if strings.HasPrefix(ref, old) { ref = strings.Replace(ref, old, new, 1) } } return ref } func FromV3Ref(ref string) string { for new, old := range ref2To3 { if strings.HasPrefix(ref, old) { ref = strings.Replace(ref, old, new, 1) } else if strings.HasPrefix(ref, "#/components/requestBodies/") { ref = strings.Replace(ref, "#/components/requestBodies/", "#/parameters/", 1) } } return ref } func ToV3SecurityRequirements(requirements openapi2.SecurityRequirements) openapi3.SecurityRequirements { if requirements == nil { return nil } result := make(openapi3.SecurityRequirements, len(requirements)) for i, item := range requirements { result[i] = item } return result } func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.SecuritySchemeRef, error) { if securityScheme == nil { return nil, nil } stripNonCustomExtensions(securityScheme.Extensions) result := &openapi3.SecurityScheme{ Description: securityScheme.Description, ExtensionProps: securityScheme.ExtensionProps, } switch securityScheme.Type { case "basic": result.Type = "http" result.Scheme = "basic" case "apiKey": result.Type = "apiKey" result.In = securityScheme.In result.Name = securityScheme.Name case "oauth2": result.Type = "oauth2" flows := &openapi3.OAuthFlows{} result.Flows = flows scopesMap := make(map[string]string) for scope, desc := range securityScheme.Scopes { scopesMap[scope] = desc } flow := &openapi3.OAuthFlow{ AuthorizationURL: securityScheme.AuthorizationURL, TokenURL: securityScheme.TokenURL, Scopes: scopesMap, } switch securityScheme.Flow { case "implicit": flows.Implicit = flow case "accessCode": flows.AuthorizationCode = flow case "password": flows.Password = flow case "application": flows.ClientCredentials = flow default: return nil, fmt.Errorf("unsupported flow %q", securityScheme.Flow) } } return &openapi3.SecuritySchemeRef{ Ref: ToV3Ref(securityScheme.Ref), Value: result, }, nil } // FromV3 converts an OpenAPIv3 spec to an OpenAPIv2 spec func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { doc2Responses, err := FromV3Responses(doc3.Components.Responses, &doc3.Components) if err != nil { return nil, err } stripNonCustomExtensions(doc3.Extensions) schemas, parameters := FromV3Schemas(doc3.Components.Schemas, &doc3.Components) doc2 := &openapi2.T{ Swagger: "2.0", Info: *doc3.Info, Definitions: schemas, Parameters: parameters, Responses: doc2Responses, Tags: doc3.Tags, ExtensionProps: doc3.ExtensionProps, ExternalDocs: doc3.ExternalDocs, } isHTTPS := false isHTTP := false servers := doc3.Servers for i, server := range servers { parsedURL, err := url.Parse(server.URL) if err == nil { // See which schemes seem to be supported if parsedURL.Scheme == "https" { isHTTPS = true } else if parsedURL.Scheme == "http" { isHTTP = true } // The first server is assumed to provide the base path if i == 0 { doc2.Host = parsedURL.Host doc2.BasePath = parsedURL.Path } } } if isHTTPS { doc2.Schemes = append(doc2.Schemes, "https") } if isHTTP { doc2.Schemes = append(doc2.Schemes, "http") } for path, pathItem := range doc3.Paths { if pathItem == nil { continue } doc2.AddOperation(path, "GET", nil) stripNonCustomExtensions(pathItem.Extensions) addPathExtensions(doc2, path, pathItem.ExtensionProps) for method, operation := range pathItem.Operations() { if operation == nil { continue } doc2Operation, err := FromV3Operation(doc3, operation) if err != nil { return nil, err } doc2.AddOperation(path, method, doc2Operation) } params := openapi2.Parameters{} for _, param := range pathItem.Parameters { p, err := FromV3Parameter(param, &doc3.Components) if err != nil { return nil, err } params = append(params, p) } sort.Sort(params) doc2.Paths[path].Parameters = params } for name, param := range doc3.Components.Parameters { if doc2.Parameters[name], err = FromV3Parameter(param, &doc3.Components); err != nil { return nil, err } } for name, requestBodyRef := range doc3.Components.RequestBodies { bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, &doc3.Components) if err != nil { return nil, err } if len(formDataParameters) != 0 { for _, param := range formDataParameters { doc2.Parameters[param.Name] = param } } else if len(bodyOrRefParameters) != 0 { for _, param := range bodyOrRefParameters { doc2.Parameters[name] = param } } if len(consumes) != 0 { doc2.Consumes = consumesToArray(consumes) } } if m := doc3.Components.SecuritySchemes; m != nil { doc2SecuritySchemes := make(map[string]*openapi2.SecurityScheme) for id, securityScheme := range m { v, err := FromV3SecurityScheme(doc3, securityScheme) if err != nil { return nil, err } doc2SecuritySchemes[id] = v } doc2.SecurityDefinitions = doc2SecuritySchemes } doc2.Security = FromV3SecurityRequirements(doc3.Security) return doc2, nil } func consumesToArray(consumes map[string]struct{}) []string { consumesArr := make([]string, 0, len(consumes)) for key := range consumes { consumesArr = append(consumesArr, key) } sort.Strings(consumesArr) return consumesArr } func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, components *openapi3.Components) ( bodyOrRefParameters openapi2.Parameters, formParameters openapi2.Parameters, consumes map[string]struct{}, err error, ) { if ref := requestBodyRef.Ref; ref != "" { bodyOrRefParameters = append(bodyOrRefParameters, &openapi2.Parameter{Ref: FromV3Ref(ref)}) return } //Only select one formData or request body for an individual requesstBody as OpenAPI 2 does not support multiples if requestBodyRef.Value != nil { for contentType, mediaType := range requestBodyRef.Value.Content { if consumes == nil { consumes = make(map[string]struct{}) } consumes[contentType] = struct{}{} if formParams := FromV3RequestBodyFormData(mediaType); len(formParams) != 0 { formParameters = formParams } else { paramName := name if originalName, ok := requestBodyRef.Value.Extensions["x-originalParamName"]; ok { json.Unmarshal(originalName.(json.RawMessage), ¶mName) } var r *openapi2.Parameter if r, err = FromV3RequestBody(paramName, requestBodyRef, mediaType, components); err != nil { return } bodyOrRefParameters = append(bodyOrRefParameters, r) } } } return } func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) { v2Defs := make(map[string]*openapi3.SchemaRef) v2Params := make(map[string]*openapi2.Parameter) for name, schema := range schemas { schemaConv, parameterConv := FromV3SchemaRef(schema, components) if schemaConv != nil { v2Defs[name] = schemaConv } else if parameterConv != nil { if parameterConv.Name == "" { parameterConv.Name = name } v2Params[name] = parameterConv } } return v2Defs, v2Params } func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) { if ref := schema.Ref; ref != "" { name := getParameterNameFromNewRef(ref) if val, ok := components.Schemas[name]; ok { if val.Value.Format == "binary" { v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1) return nil, &openapi2.Parameter{Ref: v2Ref} } } return &openapi3.SchemaRef{Ref: FromV3Ref(ref)}, nil } if schema.Value == nil { return schema, nil } if schema.Value != nil { if schema.Value.Type == "string" && schema.Value.Format == "binary" { paramType := "file" required := false value, _ := schema.Value.Extensions["x-formData-name"] var originalName string json.Unmarshal(value.(json.RawMessage), &originalName) for _, prop := range schema.Value.Required { if originalName == prop { required = true } } return nil, &openapi2.Parameter{ In: "formData", Name: originalName, Description: schema.Value.Description, Type: paramType, Enum: schema.Value.Enum, Minimum: schema.Value.Min, Maximum: schema.Value.Max, ExclusiveMin: schema.Value.ExclusiveMin, ExclusiveMax: schema.Value.ExclusiveMax, MinLength: schema.Value.MinLength, MaxLength: schema.Value.MaxLength, Default: schema.Value.Default, Items: schema.Value.Items, MinItems: schema.Value.MinItems, MaxItems: schema.Value.MaxItems, AllowEmptyValue: schema.Value.AllowEmptyValue, UniqueItems: schema.Value.UniqueItems, MultipleOf: schema.Value.MultipleOf, ExtensionProps: schema.Value.ExtensionProps, Required: required, } } } if v := schema.Value.Items; v != nil { schema.Value.Items, _ = FromV3SchemaRef(v, components) } keys := make([]string, 0, len(schema.Value.Properties)) for k := range schema.Value.Properties { keys = append(keys, k) } sort.Strings(keys) for _, key := range keys { schema.Value.Properties[key], _ = FromV3SchemaRef(schema.Value.Properties[key], components) } if v := schema.Value.AdditionalProperties; v != nil { schema.Value.AdditionalProperties, _ = FromV3SchemaRef(v, components) } for i, v := range schema.Value.AllOf { schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components) } return schema, nil } func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements { if requirements == nil { return nil } result := make([]map[string][]string, 0, len(requirements)) for _, item := range requirements { result = append(result, item) } return result } func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) { stripNonCustomExtensions(pathItem.Extensions) result := &openapi2.PathItem{ ExtensionProps: pathItem.ExtensionProps, } for method, operation := range pathItem.Operations() { r, err := FromV3Operation(doc3, operation) if err != nil { return nil, err } result.SetOperation(method, r) } for _, parameter := range pathItem.Parameters { p, err := FromV3Parameter(parameter, &doc3.Components) if err != nil { return nil, err } result.Parameters = append(result.Parameters, p) } return result, nil } func findNameForRequestBody(operation *openapi3.Operation) string { nameSearch: for _, name := range attemptedBodyParameterNames { for _, parameterRef := range operation.Parameters { parameter := parameterRef.Value if parameter != nil && parameter.Name == name { continue nameSearch } } return name } return "" } func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameters { parameters := openapi2.Parameters{} for propName, schemaRef := range mediaType.Schema.Value.Properties { if ref := schemaRef.Ref; ref != "" { v2Ref := strings.Replace(ref, "#/components/schemas/", "#/parameters/", 1) parameters = append(parameters, &openapi2.Parameter{Ref: v2Ref}) continue } val := schemaRef.Value typ := val.Type if val.Format == "binary" { typ = "file" } required := false for _, name := range val.Required { if name == propName { required = true break } } parameter := &openapi2.Parameter{ Name: propName, Description: val.Description, Type: typ, In: "formData", ExtensionProps: val.ExtensionProps, Enum: val.Enum, ExclusiveMin: val.ExclusiveMin, ExclusiveMax: val.ExclusiveMax, MinLength: val.MinLength, MaxLength: val.MaxLength, Default: val.Default, Items: val.Items, MinItems: val.MinItems, MaxItems: val.MaxItems, Maximum: val.Max, Minimum: val.Min, Pattern: val.Pattern, // CollectionFormat: val.CollectionFormat, // Format: val.Format, AllowEmptyValue: val.AllowEmptyValue, Required: required, UniqueItems: val.UniqueItems, MultipleOf: val.MultipleOf, } parameters = append(parameters, parameter) } return parameters } func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2.Operation, error) { if operation == nil { return nil, nil } stripNonCustomExtensions(operation.Extensions) result := &openapi2.Operation{ OperationID: operation.OperationID, Summary: operation.Summary, Description: operation.Description, Tags: operation.Tags, ExtensionProps: operation.ExtensionProps, } if v := operation.Security; v != nil { resultSecurity := FromV3SecurityRequirements(*v) result.Security = &resultSecurity } for _, parameter := range operation.Parameters { r, err := FromV3Parameter(parameter, &doc3.Components) if err != nil { return nil, err } result.Parameters = append(result.Parameters, r) } if v := operation.RequestBody; v != nil { // Find parameter name that we can use for the body name := findNameForRequestBody(operation) if name == "" { return nil, errors.New("could not find a name for request body") } bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, &doc3.Components) if err != nil { return nil, err } if len(formDataParameters) != 0 { result.Parameters = append(result.Parameters, formDataParameters...) } else if len(bodyOrRefParameters) != 0 { for _, param := range bodyOrRefParameters { result.Parameters = append(result.Parameters, param) break // add a single request body } } if len(consumes) != 0 { result.Consumes = consumesToArray(consumes) } } sort.Sort(result.Parameters) if responses := operation.Responses; responses != nil { resultResponses, err := FromV3Responses(responses, &doc3.Components) if err != nil { return nil, err } result.Responses = resultResponses } return result, nil } func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, mediaType *openapi3.MediaType, components *openapi3.Components) (*openapi2.Parameter, error) { requestBody := requestBodyRef.Value stripNonCustomExtensions(requestBody.Extensions) result := &openapi2.Parameter{ In: "body", Name: name, Description: requestBody.Description, Required: requestBody.Required, ExtensionProps: requestBody.ExtensionProps, } if mediaType != nil { result.Schema, _ = FromV3SchemaRef(mediaType.Schema, components) } return result, nil } func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components) (*openapi2.Parameter, error) { if ref := ref.Ref; ref != "" { return &openapi2.Parameter{Ref: FromV3Ref(ref)}, nil } parameter := ref.Value if parameter == nil { return nil, nil } stripNonCustomExtensions(parameter.Extensions) result := &openapi2.Parameter{ Description: parameter.Description, In: parameter.In, Name: parameter.Name, Required: parameter.Required, ExtensionProps: parameter.ExtensionProps, } if schemaRef := parameter.Schema; schemaRef != nil { schemaRef, _ = FromV3SchemaRef(schemaRef, components) if ref := schemaRef.Ref; ref != "" { result.Schema = &openapi3.SchemaRef{Ref: FromV3Ref(ref)} return result, nil } schema := schemaRef.Value result.Type = schema.Type result.Format = schema.Format result.Enum = schema.Enum result.Minimum = schema.Min result.Maximum = schema.Max result.ExclusiveMin = schema.ExclusiveMin result.ExclusiveMax = schema.ExclusiveMax result.MinLength = schema.MinLength result.MaxLength = schema.MaxLength result.Pattern = schema.Pattern result.Default = schema.Default result.Items = schema.Items result.MinItems = schema.MinItems result.MaxItems = schema.MaxItems result.AllowEmptyValue = schema.AllowEmptyValue // result.CollectionFormat = schema.CollectionFormat result.UniqueItems = schema.UniqueItems result.MultipleOf = schema.MultipleOf } return result, nil } func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error) { v2Responses := make(map[string]*openapi2.Response, len(responses)) for k, response := range responses { r, err := FromV3Response(response, components) if err != nil { return nil, err } v2Responses[k] = r } return v2Responses, nil } func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) (*openapi2.Response, error) { if ref := ref.Ref; ref != "" { return &openapi2.Response{Ref: FromV3Ref(ref)}, nil } response := ref.Value if response == nil { return nil, nil } description := "" if desc := response.Description; desc != nil { description = *desc } stripNonCustomExtensions(response.Extensions) result := &openapi2.Response{ Description: description, ExtensionProps: response.ExtensionProps, } if content := response.Content; content != nil { if ct := content["application/json"]; ct != nil { result.Schema, _ = FromV3SchemaRef(ct.Schema, components) } } return result, nil } func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) { securityScheme := ref.Value if securityScheme == nil { return nil, nil } stripNonCustomExtensions(securityScheme.Extensions) result := &openapi2.SecurityScheme{ Ref: FromV3Ref(ref.Ref), Description: securityScheme.Description, ExtensionProps: securityScheme.ExtensionProps, } switch securityScheme.Type { case "http": switch securityScheme.Scheme { case "basic": result.Type = "basic" default: result.Type = "apiKey" result.In = "header" result.Name = "Authorization" } case "apiKey": result.Type = "apiKey" result.In = securityScheme.In result.Name = securityScheme.Name case "oauth2": result.Type = "oauth2" flows := securityScheme.Flows if flows != nil { var flow *openapi3.OAuthFlow // TODO: Is this the right priority? What if multiple defined? if flow = flows.Implicit; flow != nil { result.Flow = "implicit" } else if flow = flows.AuthorizationCode; flow != nil { result.Flow = "accessCode" } else if flow = flows.Password; flow != nil { result.Flow = "password" } else if flow = flows.ClientCredentials; flow != nil { result.Flow = "application" } else { return nil, nil } for scope, desc := range flow.Scopes { result.Scopes[scope] = desc } } default: return nil, fmt.Errorf("unsupported security scheme type %q", securityScheme.Type) } return result, nil } var attemptedBodyParameterNames = []string{ "body", "requestBody", } func stripNonCustomExtensions(extensions map[string]interface{}) { for extName := range extensions { if !strings.HasPrefix(extName, "x-") { delete(extensions, extName) } } } func addPathExtensions(doc2 *openapi2.T, path string, extensionProps openapi3.ExtensionProps) { paths := doc2.Paths if paths == nil { paths = make(map[string]*openapi2.PathItem, 8) doc2.Paths = paths } pathItem := paths[path] if pathItem == nil { pathItem = &openapi2.PathItem{} paths[path] = pathItem } pathItem.ExtensionProps = extensionProps } kin-openapi-0.85.0/openapi2conv/openapi2_conv_test.go000066400000000000000000000333721415236407200225650ustar00rootroot00000000000000package openapi2conv import ( "context" "encoding/json" "testing" "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) func TestConvOpenAPIV3ToV2(t *testing.T) { var doc3 openapi3.T err := json.Unmarshal([]byte(exampleV3), &doc3) require.NoError(t, err) { // Refs need resolving before we can Validate sl := openapi3.NewLoader() err = sl.ResolveRefsIn(&doc3, nil) require.NoError(t, err) err = doc3.Validate(context.Background()) require.NoError(t, err) } doc2, err := FromV3(&doc3) require.NoError(t, err) data, err := json.Marshal(doc2) require.NoError(t, err) require.JSONEq(t, exampleV2, string(data)) } func TestConvOpenAPIV2ToV3(t *testing.T) { var doc2 openapi2.T err := json.Unmarshal([]byte(exampleV2), &doc2) require.NoError(t, err) doc3, err := ToV3(&doc2) require.NoError(t, err) err = doc3.Validate(context.Background()) require.NoError(t, err) data, err := json.Marshal(doc3) require.NoError(t, err) require.JSONEq(t, exampleV3, string(data)) } const exampleV2 = ` { "basePath": "/v2", "consumes": [ "application/json", "application/xml" ], "definitions": { "Error": { "description": "Error response.", "properties": { "message": { "type": "string" } }, "required": [ "message" ], "type": "object" }, "Item": { "additionalProperties": true, "properties": { "foo": { "type": "string" }, "quux": { "$ref": "#/definitions/ItemExtension" } }, "type": "object" }, "ItemExtension": { "description": "It could be anything.", "type": "boolean" }, "foo": { "description": "foo description", "enum": [ "bar", "baz" ], "type": "string" } }, "externalDocs": { "description": "Example Documentation", "url": "https://example/doc/" }, "host": "test.example.com", "info": { "title": "MyAPI", "version": "0.1", "x-info": "info extension" }, "parameters": { "banana": { "in": "path", "name": "banana", "required": true, "type": "string" }, "post_form_ref": { "description": "param description", "in": "formData", "name": "fileUpload2", "required": true, "type": "file", "x-formData-name": "fileUpload2", "x-mimetype": "text/plain" }, "put_body": { "in": "body", "name": "banana", "required": true, "schema": { "type": "string" }, "x-originalParamName": "banana" } }, "paths": { "/another/{banana}/{id}": { "parameters": [ { "$ref": "#/parameters/banana" }, { "in": "path", "name": "id", "required": true, "type": "integer" } ] }, "/example": { "delete": { "description": "example delete", "operationId": "example-delete", "parameters": [ { "description": "Only return results that intersect the provided bounding box.", "in": "query", "items": { "type": "number" }, "maxItems": 4, "minItems": 4, "name": "bbox", "type": "array" }, { "in": "query", "name": "x", "type": "string", "x-parameter": "parameter extension 1" }, { "default": 250, "description": "The y parameter", "in": "query", "maximum": 10000, "minimum": 1, "name": "y", "type": "integer" } ], "responses": { "200": { "description": "ok", "schema": { "items": { "$ref": "#/definitions/Item" }, "type": "array" } }, "404": { "description": "404 response" }, "default": { "description": "default response", "x-response": "response extension 1" } }, "security": [ { "get_security_0": [ "scope0", "scope1" ], "get_security_1": [] } ], "summary": "example get", "tags": [ "Example" ] }, "get": { "description": "example get", "responses": { "403": { "$ref": "#/responses/ForbiddenError" }, "404": { "description": "404 response" }, "default": { "description": "default response" } }, "x-operation": "operation extension 1" }, "head": { "description": "example head", "responses": { "default": { "description": "default response" } } }, "options": { "description": "example options", "responses": { "default": { "description": "default response" } } }, "patch": { "consumes": [ "application/json", "application/xml" ], "description": "example patch", "parameters": [ { "in": "body", "name": "patch_body", "schema": { "allOf": [ { "$ref": "#/definitions/Item" } ] }, "x-originalParamName": "patch_body", "x-requestBody": "requestbody extension 1" } ], "responses": { "default": { "description": "default response" } } }, "post": { "consumes": [ "multipart/form-data" ], "description": "example post", "parameters": [ { "$ref": "#/parameters/post_form_ref" }, { "description": "param description", "in": "formData", "name": "fileUpload", "type": "file", "x-formData-name": "fileUpload", "x-mimetype": "text/plain" }, { "description": "File Id", "in": "query", "name": "id", "type": "integer" }, { "description": "Description of file contents", "in": "formData", "name": "note", "type": "integer", "x-formData-name": "note" } ], "responses": { "default": { "description": "default response" } } }, "put": { "description": "example put", "parameters": [ { "$ref": "#/parameters/put_body" } ], "responses": { "default": { "description": "default response" } } }, "x-path": "path extension 1", "x-path2": "path extension 2" }, "/foo": { "get": { "operationId": "getFoo", "consumes": [ "application/json", "application/xml" ], "parameters": [ { "x-originalParamName": "foo", "in": "body", "name": "foo", "schema": { "$ref": "#/definitions/foo" } } ], "responses": { "default": { "description": "OK", "schema": { "$ref": "#/definitions/foo" } } }, "summary": "get foo" } } }, "responses": { "ForbiddenError": { "description": "Insufficient permission to perform the requested action.", "schema": { "$ref": "#/definitions/Error" } } }, "schemes": [ "https" ], "security": [ { "default_security_0": [ "scope0", "scope1" ], "default_security_1": [] } ], "swagger": "2.0", "tags": [ { "description": "An example tag.", "name": "Example" } ], "x-root": "root extension 1", "x-root2": "root extension 2" } ` const exampleV3 = ` { "components": { "parameters": { "banana": { "in": "path", "name": "banana", "required": true, "schema": { "type": "string" } } }, "requestBodies": { "put_body": { "content": { "application/json": { "schema": { "type": "string" } }, "application/xml": { "schema": { "type": "string" } } }, "required": true, "x-originalParamName": "banana" } }, "responses": { "ForbiddenError": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }, "description": "Insufficient permission to perform the requested action." } }, "schemas": { "Error": { "description": "Error response.", "properties": { "message": { "type": "string" } }, "required": [ "message" ], "type": "object" }, "Item": { "additionalProperties": true, "properties": { "foo": { "type": "string" }, "quux": { "$ref": "#/components/schemas/ItemExtension" } }, "type": "object" }, "ItemExtension": { "description": "It could be anything.", "type": "boolean" }, "post_form_ref": { "description": "param description", "format": "binary", "required": [ "fileUpload2" ], "type": "string", "x-formData-name": "fileUpload2", "x-mimetype": "text/plain" }, "foo": { "description": "foo description", "enum": [ "bar", "baz" ], "type": "string" } } }, "externalDocs": { "description": "Example Documentation", "url": "https://example/doc/" }, "info": { "title": "MyAPI", "version": "0.1", "x-info": "info extension" }, "openapi": "3.0.3", "paths": { "/another/{banana}/{id}": { "parameters": [ { "$ref": "#/components/parameters/banana" }, { "in": "path", "name": "id", "required": true, "schema": { "type": "integer" } } ] }, "/example": { "delete": { "description": "example delete", "operationId": "example-delete", "parameters": [ { "description": "Only return results that intersect the provided bounding box.", "in": "query", "name": "bbox", "schema": { "items": { "type": "number" }, "maxItems": 4, "minItems": 4, "type": "array" } }, { "in": "query", "name": "x", "schema": { "type": "string" }, "x-parameter": "parameter extension 1" }, { "description": "The y parameter", "in": "query", "name": "y", "schema": { "default": 250, "maximum": 10000, "minimum": 1, "type": "integer" } } ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { "$ref": "#/components/schemas/Item" }, "type": "array" } } }, "description": "ok" }, "404": { "description": "404 response" }, "default": { "description": "default response", "x-response": "response extension 1" } }, "security": [ { "get_security_0": [ "scope0", "scope1" ], "get_security_1": [] } ], "summary": "example get", "tags": [ "Example" ] }, "get": { "description": "example get", "responses": { "403": { "$ref": "#/components/responses/ForbiddenError" }, "404": { "description": "404 response" }, "default": { "description": "default response" } }, "x-operation": "operation extension 1" }, "head": { "description": "example head", "responses": { "default": { "description": "default response" } } }, "options": { "description": "example options", "responses": { "default": { "description": "default response" } } }, "patch": { "description": "example patch", "requestBody": { "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Item" } ] } }, "application/xml": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Item" } ] } } }, "x-originalParamName": "patch_body", "x-requestBody": "requestbody extension 1" }, "responses": { "default": { "description": "default response" } } }, "post": { "description": "example post", "parameters": [ { "description": "File Id", "in": "query", "name": "id", "schema": { "type": "integer" } } ], "requestBody": { "content": { "multipart/form-data": { "schema": { "properties": { "fileUpload": { "description": "param description", "format": "binary", "type": "string", "x-formData-name": "fileUpload", "x-mimetype": "text/plain" }, "fileUpload2": { "$ref": "#/components/schemas/post_form_ref" }, "note": { "description": "Description of file contents", "type": "integer", "x-formData-name": "note" } }, "required": [ "fileUpload2" ], "type": "object" } } } }, "responses": { "default": { "description": "default response" } } }, "put": { "description": "example put", "requestBody": { "$ref": "#/components/requestBodies/put_body" }, "responses": { "default": { "description": "default response" } } }, "x-path": "path extension 1", "x-path2": "path extension 2" }, "/foo": { "get": { "operationId": "getFoo", "requestBody": { "x-originalParamName": "foo", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/foo" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/foo" } } } }, "responses": { "default": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/foo" } } }, "description": "OK" } }, "summary": "get foo" } } }, "security": [ { "default_security_0": [ "scope0", "scope1" ], "default_security_1": [] } ], "servers": [ { "url": "https://test.example.com/v2" } ], "tags": [ { "description": "An example tag.", "name": "Example" } ], "x-root": "root extension 1", "x-root2": "root extension 2" } ` kin-openapi-0.85.0/openapi2conv/testdata/000077500000000000000000000000001415236407200202365ustar00rootroot00000000000000kin-openapi-0.85.0/openapi2conv/testdata/swagger.json000077700000000000000000000000001415236407200310712../../openapi2/testdata/swagger.jsonustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/000077500000000000000000000000001415236407200155405ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/callback.go000066400000000000000000000012751415236407200176300ustar00rootroot00000000000000package openapi3 import ( "context" "fmt" "github.com/go-openapi/jsonpointer" ) type Callbacks map[string]*CallbackRef var _ jsonpointer.JSONPointable = (*Callbacks)(nil) func (c Callbacks) JSONLookup(token string) (interface{}, error) { ref, ok := c[token] if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } if ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } // Callback is specified by OpenAPI/Swagger standard version 3.0. type Callback map[string]*PathItem func (value Callback) Validate(ctx context.Context) error { for _, v := range value { if err := v.Validate(ctx); err != nil { return err } } return nil } kin-openapi-0.85.0/openapi3/components.go000066400000000000000000000056441415236407200202650ustar00rootroot00000000000000package openapi3 import ( "context" "fmt" "regexp" "github.com/getkin/kin-openapi/jsoninfo" ) // Components is specified by OpenAPI/Swagger standard version 3.0. type Components struct { ExtensionProps Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` RequestBodies RequestBodies `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` Responses Responses `json:"responses,omitempty" yaml:"responses,omitempty"` SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` Links Links `json:"links,omitempty" yaml:"links,omitempty"` Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` } func NewComponents() Components { return Components{} } func (components *Components) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(components) } func (components *Components) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, components) } func (components *Components) Validate(ctx context.Context) (err error) { for k, v := range components.Schemas { if err = ValidateIdentifier(k); err != nil { return } if err = v.Validate(ctx); err != nil { return } } for k, v := range components.Parameters { if err = ValidateIdentifier(k); err != nil { return } if err = v.Validate(ctx); err != nil { return } } for k, v := range components.RequestBodies { if err = ValidateIdentifier(k); err != nil { return } if err = v.Validate(ctx); err != nil { return } } for k, v := range components.Responses { if err = ValidateIdentifier(k); err != nil { return } if err = v.Validate(ctx); err != nil { return } } for k, v := range components.Headers { if err = ValidateIdentifier(k); err != nil { return } if err = v.Validate(ctx); err != nil { return } } for k, v := range components.SecuritySchemes { if err = ValidateIdentifier(k); err != nil { return } if err = v.Validate(ctx); err != nil { return } } 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) } kin-openapi-0.85.0/openapi3/content.go000066400000000000000000000055651415236407200175540ustar00rootroot00000000000000package openapi3 import ( "context" "strings" ) // Content is specified by OpenAPI/Swagger 3.0 standard. type Content map[string]*MediaType func NewContent() Content { return make(map[string]*MediaType, 4) } func NewContentWithSchema(schema *Schema, consumes []string) Content { if len(consumes) == 0 { return Content{ "*/*": NewMediaType().WithSchema(schema), } } content := make(map[string]*MediaType, len(consumes)) for _, mediaType := range consumes { content[mediaType] = NewMediaType().WithSchema(schema) } return content } func NewContentWithSchemaRef(schema *SchemaRef, consumes []string) Content { if len(consumes) == 0 { return Content{ "*/*": NewMediaType().WithSchemaRef(schema), } } content := make(map[string]*MediaType, len(consumes)) for _, mediaType := range consumes { content[mediaType] = NewMediaType().WithSchemaRef(schema) } return content } func NewContentWithJSONSchema(schema *Schema) Content { return Content{ "application/json": NewMediaType().WithSchema(schema), } } func NewContentWithJSONSchemaRef(schema *SchemaRef) Content { return Content{ "application/json": NewMediaType().WithSchemaRef(schema), } } func NewContentWithFormDataSchema(schema *Schema) Content { return Content{ "multipart/form-data": NewMediaType().WithSchema(schema), } } func NewContentWithFormDataSchemaRef(schema *SchemaRef) Content { return Content{ "multipart/form-data": NewMediaType().WithSchemaRef(schema), } } func (content Content) Get(mime string) *MediaType { // If the mime is empty then short-circuit to the wildcard. // We do this here so that we catch only the specific case of // and empty mime rather than a present, but invalid, mime type. if mime == "" { return content["*/*"] } // Start by making the most specific match possible // by using the mime type in full. if v := content[mime]; v != nil { return v } // If an exact match is not found then we strip all // metadata from the mime type and only use the x/y // portion. i := strings.IndexByte(mime, ';') if i < 0 { // If there is no metadata then preserve the full mime type // string for later wildcard searches. i = len(mime) } mime = mime[:i] if v := content[mime]; v != nil { return v } // If the x/y pattern has no specific match then we // try the x/* pattern. i = strings.IndexByte(mime, '/') if i < 0 { // In the case that the given mime type is not valid because it is // missing the subtype we return nil so that this does not accidentally // resolve with the wildcard. return nil } mime = mime[:i] + "/*" if v := content[mime]; v != nil { return v } // Finally, the most generic match of */* is returned // as a catch-all. return content["*/*"] } func (value Content) Validate(ctx context.Context) error { for _, v := range value { // Validate MediaType if err := v.Validate(ctx); err != nil { return err } } return nil } kin-openapi-0.85.0/openapi3/content_test.go000066400000000000000000000050461415236407200206050ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestContent_Get(t *testing.T) { fallback := NewMediaType() wildcard := NewMediaType() stripped := NewMediaType() fullMatch := NewMediaType() content := Content{ "*/*": fallback, "application/*": wildcard, "application/json": stripped, "application/json;encoding=utf-8": fullMatch, } contentWithoutWildcards := Content{ "application/json": stripped, "application/json;encoding=utf-8": fullMatch, } tests := []struct { name string content Content mime string want *MediaType }{ { name: "missing", content: contentWithoutWildcards, mime: "text/plain;encoding=utf-8", want: nil, }, { name: "full match", content: content, mime: "application/json;encoding=utf-8", want: fullMatch, }, { name: "stripped match", content: content, mime: "application/json;encoding=utf-16", want: stripped, }, { name: "wildcard match", content: content, mime: "application/yaml;encoding=utf-16", want: wildcard, }, { name: "fallback match", content: content, mime: "text/plain;encoding=utf-16", want: fallback, }, { name: "invalid mime type", content: content, mime: "text;encoding=utf16", want: nil, }, { name: "missing no encoding", content: contentWithoutWildcards, mime: "text/plain", want: nil, }, { name: "stripped match no encoding", content: content, mime: "application/json", want: stripped, }, { name: "wildcard match no encoding", content: content, mime: "application/yaml", want: wildcard, }, { name: "fallback match no encoding", content: content, mime: "text/plain", want: fallback, }, { name: "invalid mime type no encoding", content: content, mime: "text", want: nil, }, { name: "missing mime type", content: content, mime: "", want: fallback, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Using require.True here because require.Same is not yet released. // We're comparing pointer values and the require.Equal will // dereference and compare the pointed to values rather than check // if the memory addresses are the same. Once require.Same is released // this test should convert to using that. require.True(t, tt.want == tt.content.Get(tt.mime)) }) } } kin-openapi-0.85.0/openapi3/discriminator.go000066400000000000000000000012311415236407200207330ustar00rootroot00000000000000package openapi3 import ( "context" "github.com/getkin/kin-openapi/jsoninfo" ) // Discriminator is specified by OpenAPI/Swagger standard version 3.0. type Discriminator struct { ExtensionProps PropertyName string `json:"propertyName" yaml:"propertyName"` Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` } func (value *Discriminator) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(value) } func (value *Discriminator) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } func (value *Discriminator) Validate(ctx context.Context) error { return nil } kin-openapi-0.85.0/openapi3/discriminator_test.go000066400000000000000000000017371415236407200220050ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestParsingDiscriminator(t *testing.T) { const spec = ` { "openapi": "3.0.0", "info": { "version": "1.0.0", "title": "title", "description": "desc", "contact": { "email": "email" } }, "paths": {}, "components": { "schemas": { "MyResponseType": { "discriminator": { "mapping": { "cat": "#/components/schemas/Cat", "dog": "#/components/schemas/Dog" }, "propertyName": "pet_type" }, "oneOf": [ { "$ref": "#/components/schemas/Cat" }, { "$ref": "#/components/schemas/Dog" } ] }, "Cat": {"enum": ["chat"]}, "Dog": {"enum": ["chien"]} } } } ` loader := NewLoader() doc, err := loader.LoadFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) require.Equal(t, 2, len(doc.Components.Schemas["MyResponseType"].Value.Discriminator.Mapping)) } kin-openapi-0.85.0/openapi3/doc.go000066400000000000000000000002571415236407200166400ustar00rootroot00000000000000// Package openapi3 parses and writes OpenAPI 3 specification documents. // // See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md package openapi3 kin-openapi-0.85.0/openapi3/encoding.go000066400000000000000000000051261415236407200176610ustar00rootroot00000000000000package openapi3 import ( "context" "fmt" "github.com/getkin/kin-openapi/jsoninfo" ) // Encoding is specified by OpenAPI/Swagger 3.0 standard. type Encoding struct { ExtensionProps ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` Style string `json:"style,omitempty" yaml:"style,omitempty"` Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` } func NewEncoding() *Encoding { return &Encoding{} } func (encoding *Encoding) WithHeader(name string, header *Header) *Encoding { return encoding.WithHeaderRef(name, &HeaderRef{ Value: header, }) } func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding { headers := encoding.Headers if headers == nil { headers = make(map[string]*HeaderRef) encoding.Headers = headers } headers[name] = ref return encoding } func (encoding *Encoding) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(encoding) } func (encoding *Encoding) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, encoding) } // SerializationMethod returns a serialization method of request body. // When serialization method is not defined the method returns the default serialization method. func (encoding *Encoding) SerializationMethod() *SerializationMethod { sm := &SerializationMethod{Style: SerializationForm, Explode: true} if encoding != nil { if encoding.Style != "" { sm.Style = encoding.Style } if encoding.Explode != nil { sm.Explode = *encoding.Explode } } return sm } func (value *Encoding) Validate(ctx context.Context) error { if value == nil { return nil } for k, v := range value.Headers { if err := ValidateIdentifier(k); err != nil { return nil } if err := v.Validate(ctx); err != nil { return nil } } // Validate a media types's serialization method. sm := value.SerializationMethod() switch { case sm.Style == SerializationForm && sm.Explode, sm.Style == SerializationForm && !sm.Explode, sm.Style == SerializationSpaceDelimited && sm.Explode, sm.Style == SerializationSpaceDelimited && !sm.Explode, sm.Style == SerializationPipeDelimited && sm.Explode, sm.Style == SerializationPipeDelimited && !sm.Explode, sm.Style == SerializationDeepObject && sm.Explode: // it is a valid default: return fmt.Errorf("serialization method with style=%q and explode=%v is not supported by media type", sm.Style, sm.Explode) } return nil } kin-openapi-0.85.0/openapi3/encoding_test.go000066400000000000000000000045761415236407200207300ustar00rootroot00000000000000package openapi3 import ( "context" "encoding/json" "reflect" "testing" "github.com/stretchr/testify/require" ) func TestEncodingJSON(t *testing.T) { t.Log("Marshal *openapi3.Encoding to JSON") data, err := json.Marshal(encoding()) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Unmarshal *openapi3.Encoding from JSON") docA := &Encoding{} err = json.Unmarshal(encodingJSON, &docA) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Validate *openapi3.Encoding") err = docA.Validate(context.Background()) require.NoError(t, err) t.Log("Ensure representations match") dataA, err := json.Marshal(docA) require.NoError(t, err) require.JSONEq(t, string(data), string(encodingJSON)) require.JSONEq(t, string(data), string(dataA)) } var encodingJSON = []byte(` { "contentType": "application/json", "headers": { "someHeader": {} }, "style": "form", "explode": true, "allowReserved": true } `) func encoding() *Encoding { explode := true return &Encoding{ ContentType: "application/json", Headers: map[string]*HeaderRef{ "someHeader": { Value: &Header{}, }, }, Style: "form", Explode: &explode, AllowReserved: true, } } func TestEncodingSerializationMethod(t *testing.T) { boolPtr := func(b bool) *bool { return &b } testCases := []struct { name string enc *Encoding want *SerializationMethod }{ { name: "default", want: &SerializationMethod{Style: SerializationForm, Explode: true}, }, { name: "encoding with style", enc: &Encoding{Style: SerializationSpaceDelimited}, want: &SerializationMethod{Style: SerializationSpaceDelimited, Explode: true}, }, { name: "encoding with explode", enc: &Encoding{Explode: boolPtr(true)}, want: &SerializationMethod{Style: SerializationForm, Explode: true}, }, { name: "encoding with no explode", enc: &Encoding{Explode: boolPtr(false)}, want: &SerializationMethod{Style: SerializationForm, Explode: false}, }, { name: "encoding with style and explode ", enc: &Encoding{Style: SerializationSpaceDelimited, Explode: boolPtr(false)}, want: &SerializationMethod{Style: SerializationSpaceDelimited, Explode: false}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got := tc.enc.SerializationMethod() require.True(t, reflect.DeepEqual(got, tc.want), "got %#v, want %#v", got, tc.want) }) } } kin-openapi-0.85.0/openapi3/errors.go000066400000000000000000000017121415236407200174040ustar00rootroot00000000000000package openapi3 import ( "bytes" "errors" ) // MultiError is a collection of errors, intended for when // multiple issues need to be reported upstream type MultiError []error func (me MultiError) Error() string { buff := &bytes.Buffer{} for _, e := range me { buff.WriteString(e.Error()) buff.WriteString(" | ") } return buff.String() } //Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()` //It will also return true if any of the contained errors match target func (me MultiError) Is(target error) bool { if _, ok := target.(MultiError); ok { return true } for _, e := range me { if errors.Is(e, target) { return true } } return false } //As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type func (me MultiError) As(target interface{}) bool { for _, e := range me { if errors.As(e, target) { return true } } return false } kin-openapi-0.85.0/openapi3/example_test.go000066400000000000000000000021031415236407200205550ustar00rootroot00000000000000package openapi3 import ( "encoding/json" "testing" "github.com/stretchr/testify/require" ) func TestExampleJSON(t *testing.T) { t.Log("Marshal *openapi3.Example to JSON") data, err := json.Marshal(example()) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Unmarshal *openapi3.Example from JSON") docA := &Example{} err = json.Unmarshal(exampleJSON, &docA) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Ensure representations match") dataA, err := json.Marshal(docA) require.NoError(t, err) require.JSONEq(t, string(data), string(exampleJSON)) require.JSONEq(t, string(data), string(dataA)) } var exampleJSON = []byte(` { "summary": "An example of a cat", "value": { "name": "Fluffy", "petType": "Cat", "color": "White", "gender": "male", "breed": "Persian" } } `) func example() *Example { value := map[string]string{ "name": "Fluffy", "petType": "Cat", "color": "White", "gender": "male", "breed": "Persian", } return &Example{ Summary: "An example of a cat", Value: value, } } kin-openapi-0.85.0/openapi3/examples.go000066400000000000000000000024151415236407200177070ustar00rootroot00000000000000package openapi3 import ( "context" "fmt" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) type Examples map[string]*ExampleRef var _ jsonpointer.JSONPointable = (*Examples)(nil) func (e Examples) JSONLookup(token string) (interface{}, error) { ref, ok := e[token] if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } if ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } // Example is specified by OpenAPI/Swagger 3.0 standard. type Example struct { ExtensionProps Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Value interface{} `json:"value,omitempty" yaml:"value,omitempty"` ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` } func NewExample(value interface{}) *Example { return &Example{ Value: value, } } func (example *Example) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(example) } func (example *Example) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, example) } func (value *Example) Validate(ctx context.Context) error { return nil // TODO } kin-openapi-0.85.0/openapi3/extension.go000066400000000000000000000021361415236407200201050ustar00rootroot00000000000000package openapi3 import ( "github.com/getkin/kin-openapi/jsoninfo" ) // ExtensionProps provides support for OpenAPI extensions. // It reads/writes all properties that begin with "x-". type ExtensionProps struct { Extensions map[string]interface{} `json:"-" yaml:"-"` } // Assert that the type implements the interface var _ jsoninfo.StrictStruct = &ExtensionProps{} // EncodeWith will be invoked by package "jsoninfo" func (props *ExtensionProps) EncodeWith(encoder *jsoninfo.ObjectEncoder, value interface{}) error { for k, v := range props.Extensions { if err := encoder.EncodeExtension(k, v); err != nil { return err } } return encoder.EncodeStructFieldsAndExtensions(value) } // DecodeWith will be invoked by package "jsoninfo" func (props *ExtensionProps) DecodeWith(decoder *jsoninfo.ObjectDecoder, value interface{}) error { if err := decoder.DecodeStructFieldsAndExtensions(value); err != nil { return err } source := decoder.DecodeExtensionMap() result := make(map[string]interface{}, len(source)) for k, v := range source { result[k] = v } props.Extensions = result return nil } kin-openapi-0.85.0/openapi3/extension_test.go000066400000000000000000000055331415236407200211500ustar00rootroot00000000000000package openapi3 import ( "encoding/json" "fmt" "testing" "github.com/getkin/kin-openapi/jsoninfo" "github.com/stretchr/testify/require" ) func ExampleExtensionProps_DecodeWith() { loader := NewLoader() loader.IsExternalRefsAllowed = true spec, err := loader.LoadFromFile("testdata/testref.openapi.json") if err != nil { panic(err) } dec, err := jsoninfo.NewObjectDecoder(spec.Info.Extensions["x-my-extension"].(json.RawMessage)) if err != nil { panic(err) } var value struct { Key int `json:"k"` } if err = spec.Info.DecodeWith(dec, &value); err != nil { panic(err) } fmt.Println(value.Key) // Output: 42 } func TestExtensionProps_EncodeWith(t *testing.T) { t.Run("successfully encoded", func(t *testing.T) { encoder := jsoninfo.NewObjectEncoder() var extensionProps = ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", }, } var value = struct { Field1 string `json:"field1"` Field2 string `json:"field2"` }{} err := extensionProps.EncodeWith(encoder, &value) require.NoError(t, err) }) } func TestExtensionProps_DecodeWith(t *testing.T) { data := []byte(` { "field1": "value1", "field2": "value2" } `) t.Run("successfully decode all the fields", func(t *testing.T) { decoder, err := jsoninfo.NewObjectDecoder(data) require.NoError(t, err) var extensionProps = &ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", "field2": "value1", }, } var value = struct { Field1 string `json:"field1"` Field2 string `json:"field2"` }{} err = extensionProps.DecodeWith(decoder, &value) require.NoError(t, err) require.Equal(t, 0, len(extensionProps.Extensions)) require.Equal(t, "value1", value.Field1) require.Equal(t, "value2", value.Field2) }) t.Run("successfully decode some of the fields", func(t *testing.T) { decoder, err := jsoninfo.NewObjectDecoder(data) require.NoError(t, err) var extensionProps = &ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", "field2": "value2", }, } var value = &struct { Field1 string `json:"field1"` }{} err = extensionProps.DecodeWith(decoder, value) require.NoError(t, err) require.Equal(t, 1, len(extensionProps.Extensions)) require.Equal(t, "value1", value.Field1) }) t.Run("successfully decode none of the fields", func(t *testing.T) { decoder, err := jsoninfo.NewObjectDecoder(data) require.NoError(t, err) var extensionProps = &ExtensionProps{ Extensions: map[string]interface{}{ "field1": "value1", "field2": "value2", }, } var value = struct { Field3 string `json:"field3"` Field4 string `json:"field4"` }{} err = extensionProps.DecodeWith(decoder, &value) require.NoError(t, err) require.Equal(t, 2, len(extensionProps.Extensions)) require.Empty(t, value.Field3) require.Empty(t, value.Field4) }) } kin-openapi-0.85.0/openapi3/external_docs.go000066400000000000000000000007511415236407200207240ustar00rootroot00000000000000package openapi3 import ( "github.com/getkin/kin-openapi/jsoninfo" ) // ExternalDocs is specified by OpenAPI/Swagger standard version 3.0. type ExternalDocs struct { ExtensionProps Description string `json:"description,omitempty"` URL string `json:"url,omitempty"` } func (e *ExternalDocs) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(e) } func (e *ExternalDocs) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, e) } kin-openapi-0.85.0/openapi3/header.go000066400000000000000000000064521415236407200173260ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "fmt" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) type Headers map[string]*HeaderRef var _ jsonpointer.JSONPointable = (*Headers)(nil) func (h Headers) JSONLookup(token string) (interface{}, error) { ref, ok := h[token] if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } if ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } // Header is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#headerObject type Header struct { Parameter } var _ jsonpointer.JSONPointable = (*Header)(nil) func (value *Header) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } // SerializationMethod returns a header's serialization method. func (value *Header) SerializationMethod() (*SerializationMethod, error) { style := value.Style if style == "" { style = SerializationSimple } explode := false if value.Explode != nil { explode = *value.Explode } return &SerializationMethod{Style: style, Explode: explode}, nil } func (value *Header) Validate(ctx context.Context) error { if value.Name != "" { return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map") } if value.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() if err != nil { return err } if smSupported := false || sm.Style == SerializationSimple && !sm.Explode || sm.Style == SerializationSimple && sm.Explode; !smSupported { e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a header parameter", sm.Style, sm.Explode) return fmt.Errorf("header schema is invalid: %v", e) } if (value.Schema == nil) == (value.Content == nil) { e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", value) return fmt.Errorf("header schema is invalid: %v", e) } if schema := value.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 err := content.Validate(ctx); err != nil { return fmt.Errorf("header content is invalid: %v", err) } } return nil } func (value 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 } return value.Schema.Value, nil } case "name": return value.Name, nil case "in": return value.In, nil case "description": return value.Description, nil case "style": return value.Style, nil case "explode": return value.Explode, nil case "allowEmptyValue": return value.AllowEmptyValue, nil case "allowReserved": return value.AllowReserved, nil case "deprecated": return value.Deprecated, nil case "required": return value.Required, nil case "example": return value.Example, nil case "examples": return value.Examples, nil case "content": return value.Content, nil } v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token) return v, err } kin-openapi-0.85.0/openapi3/info.go000066400000000000000000000047461415236407200170350ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "github.com/getkin/kin-openapi/jsoninfo" ) // Info is specified by OpenAPI/Swagger standard version 3.0. type Info struct { ExtensionProps Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"` License *License `json:"license,omitempty" yaml:"license,omitempty"` Version string `json:"version" yaml:"version"` // Required } func (value *Info) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(value) } func (value *Info) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } func (value *Info) Validate(ctx context.Context) error { if contact := value.Contact; contact != nil { if err := contact.Validate(ctx); err != nil { return err } } if license := value.License; license != nil { if err := license.Validate(ctx); err != nil { return err } } if value.Version == "" { return errors.New("value of version must be a non-empty string") } if value.Title == "" { return errors.New("value of title must be a non-empty string") } return nil } // Contact is specified by OpenAPI/Swagger standard version 3.0. type Contact struct { ExtensionProps Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` Email string `json:"email,omitempty" yaml:"email,omitempty"` } func (value *Contact) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(value) } func (value *Contact) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } func (value *Contact) Validate(ctx context.Context) error { return nil } // License is specified by OpenAPI/Swagger standard version 3.0. type License struct { ExtensionProps Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` } func (value *License) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(value) } func (value *License) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } func (value *License) Validate(ctx context.Context) error { if value.Name == "" { return errors.New("value of license name must be a non-empty string") } return nil } kin-openapi-0.85.0/openapi3/internalize_refs.go000066400000000000000000000245641415236407200214450ustar00rootroot00000000000000package openapi3 import ( "context" "path/filepath" "strings" ) type RefNameResolver func(string) string // DefaultRefResolver is a default implementation of refNameResolver for the // InternalizeRefs function. // // If a reference points to an element inside a document, it returns the last // element in the reference using filepath.Base. Otherwise if the reference points // to a file, it returns the file name trimmed of all extensions. func DefaultRefNameResolver(ref string) string { if ref == "" { return "" } split := strings.SplitN(ref, "#", 2) if len(split) == 2 { return filepath.Base(split[1]) } ref = split[0] for ext := filepath.Ext(ref); len(ext) > 0; ext = filepath.Ext(ref) { ref = strings.TrimSuffix(ref, ext) } return filepath.Base(ref) } func schemaNames(s Schemas) []string { out := make([]string, 0, len(s)) for i := range s { out = append(out, i) } return out } func parametersMapNames(s ParametersMap) []string { out := make([]string, 0, len(s)) for i := range s { out = append(out, i) } return out } func isExternalRef(ref string) bool { return ref != "" && !strings.HasPrefix(ref, "#/components/") } func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver) { if s == nil || !isExternalRef(s.Ref) { return } name := refNameResolver(s.Ref) if _, ok := doc.Components.Schemas[name]; ok { s.Ref = "#/components/schemas/" + name return } if doc.Components.Schemas == nil { doc.Components.Schemas = make(Schemas) } doc.Components.Schemas[name] = s.Value.NewRef() s.Ref = "#/components/schemas/" + name } func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolver) { if p == nil || !isExternalRef(p.Ref) { return } name := refNameResolver(p.Ref) if _, ok := doc.Components.Parameters[name]; ok { p.Ref = "#/components/parameters/" + name return } if doc.Components.Parameters == nil { doc.Components.Parameters = make(ParametersMap) } doc.Components.Parameters[name] = &ParameterRef{Value: p.Value} p.Ref = "#/components/parameters/" + name } func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver) { if h == nil || !isExternalRef(h.Ref) { return } name := refNameResolver(h.Ref) if _, ok := doc.Components.Headers[name]; ok { h.Ref = "#/components/headers/" + name return } if doc.Components.Headers == nil { doc.Components.Headers = make(Headers) } doc.Components.Headers[name] = &HeaderRef{Value: h.Value} h.Ref = "#/components/headers/" + name } func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameResolver) { if r == nil || !isExternalRef(r.Ref) { return } name := refNameResolver(r.Ref) if _, ok := doc.Components.RequestBodies[name]; ok { r.Ref = "#/components/requestBodies/" + name return } if doc.Components.RequestBodies == nil { doc.Components.RequestBodies = make(RequestBodies) } doc.Components.RequestBodies[name] = &RequestBodyRef{Value: r.Value} r.Ref = "#/components/requestBodies/" + name } func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver) { if r == nil || !isExternalRef(r.Ref) { return } name := refNameResolver(r.Ref) if _, ok := doc.Components.Responses[name]; ok { r.Ref = "#/components/responses/" + name return } if doc.Components.Responses == nil { doc.Components.Responses = make(Responses) } doc.Components.Responses[name] = &ResponseRef{Value: r.Value} r.Ref = "#/components/responses/" + name } func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver RefNameResolver) { if ss == nil || !isExternalRef(ss.Ref) { return } name := refNameResolver(ss.Ref) if _, ok := doc.Components.SecuritySchemes[name]; ok { ss.Ref = "#/components/securitySchemes/" + name return } if doc.Components.SecuritySchemes == nil { doc.Components.SecuritySchemes = make(SecuritySchemes) } doc.Components.SecuritySchemes[name] = &SecuritySchemeRef{Value: ss.Value} ss.Ref = "#/components/securitySchemes/" + name } func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver) { if e == nil || !isExternalRef(e.Ref) { return } name := refNameResolver(e.Ref) if _, ok := doc.Components.Examples[name]; ok { e.Ref = "#/components/examples/" + name return } if doc.Components.Examples == nil { doc.Components.Examples = make(Examples) } doc.Components.Examples[name] = &ExampleRef{Value: e.Value} e.Ref = "#/components/examples/" + name } func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver) { if l == nil || !isExternalRef(l.Ref) { return } name := refNameResolver(l.Ref) if _, ok := doc.Components.Links[name]; ok { l.Ref = "#/components/links/" + name return } if doc.Components.Links == nil { doc.Components.Links = make(Links) } doc.Components.Links[name] = &LinkRef{Value: l.Value} l.Ref = "#/components/links/" + name } func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver) { if c == nil || !isExternalRef(c.Ref) { return } name := refNameResolver(c.Ref) if _, ok := doc.Components.Callbacks[name]; ok { c.Ref = "#/components/callbacks/" + name } if doc.Components.Callbacks == nil { doc.Components.Callbacks = make(Callbacks) } doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value} c.Ref = "#/components/callbacks/" + name } func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver) { if s == nil { return } for _, list := range []SchemaRefs{s.AllOf, s.AnyOf, s.OneOf} { for _, s2 := range list { doc.addSchemaToSpec(s2, refNameResolver) if s2 != nil { doc.derefSchema(s2.Value, refNameResolver) } } } for _, s2 := range s.Properties { doc.addSchemaToSpec(s2, refNameResolver) if s2 != nil { doc.derefSchema(s2.Value, refNameResolver) } } for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties, s.Items} { doc.addSchemaToSpec(ref, refNameResolver) if ref != nil { doc.derefSchema(ref.Value, refNameResolver) } } } func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver) { for _, h := range hs { doc.addHeaderToSpec(h, refNameResolver) doc.derefParameter(h.Value.Parameter, refNameResolver) } } func (doc *T) derefExamples(es Examples, refNameResolver RefNameResolver) { for _, e := range es { doc.addExampleToSpec(e, refNameResolver) } } func (doc *T) derefContent(c Content, refNameResolver RefNameResolver) { for _, mediatype := range c { doc.addSchemaToSpec(mediatype.Schema, refNameResolver) if mediatype.Schema != nil { doc.derefSchema(mediatype.Schema.Value, refNameResolver) } doc.derefExamples(mediatype.Examples, refNameResolver) for _, e := range mediatype.Encoding { doc.derefHeaders(e.Headers, refNameResolver) } } } func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver) { for _, l := range ls { doc.addLinkToSpec(l, refNameResolver) } } func (doc *T) derefResponses(es Responses, refNameResolver RefNameResolver) { for _, e := range es { doc.addResponseToSpec(e, refNameResolver) if e.Value != nil { doc.derefHeaders(e.Value.Headers, refNameResolver) doc.derefContent(e.Value.Content, refNameResolver) doc.derefLinks(e.Value.Links, refNameResolver) } } } func (doc *T) derefParameter(p Parameter, refNameResolver RefNameResolver) { doc.addSchemaToSpec(p.Schema, refNameResolver) doc.derefContent(p.Content, refNameResolver) if p.Schema != nil { doc.derefSchema(p.Schema.Value, refNameResolver) } } func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver) { doc.derefContent(r.Content, refNameResolver) } func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver) { for _, ops := range paths { // inline full operations ops.Ref = "" for _, op := range ops.Operations() { doc.addRequestBodyToSpec(op.RequestBody, refNameResolver) if op.RequestBody != nil && op.RequestBody.Value != nil { doc.derefRequestBody(*op.RequestBody.Value, refNameResolver) } for _, cb := range op.Callbacks { doc.addCallbackToSpec(cb, refNameResolver) if cb.Value != nil { doc.derefPaths(*cb.Value, refNameResolver) } } doc.derefResponses(op.Responses, refNameResolver) for _, param := range op.Parameters { doc.addParameterToSpec(param, refNameResolver) if param.Value != nil { doc.derefParameter(*param.Value, refNameResolver) } } } } } // InternalizeRefs removes all references to external files from the spec and moves them // to the components section. // // refNameResolver takes in references to returns a name to store the reference under locally. // It MUST return a unique name for each reference type. // A default implementation is provided that will suffice for most use cases. See the function // documention for more details. // // Example: // // doc.InternalizeRefs(context.Background(), nil) func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) { if refNameResolver == nil { refNameResolver = DefaultRefNameResolver } // Handle components section names := schemaNames(doc.Components.Schemas) for _, name := range names { schema := doc.Components.Schemas[name] doc.addSchemaToSpec(schema, refNameResolver) if schema != nil { schema.Ref = "" // always dereference the top level doc.derefSchema(schema.Value, refNameResolver) } } names = parametersMapNames(doc.Components.Parameters) for _, name := range names { p := doc.Components.Parameters[name] doc.addParameterToSpec(p, refNameResolver) if p != nil && p.Value != nil { p.Ref = "" // always dereference the top level doc.derefParameter(*p.Value, refNameResolver) } } doc.derefHeaders(doc.Components.Headers, refNameResolver) for _, req := range doc.Components.RequestBodies { doc.addRequestBodyToSpec(req, refNameResolver) if req != nil && req.Value != nil { req.Ref = "" // always dereference the top level doc.derefRequestBody(*req.Value, refNameResolver) } } doc.derefResponses(doc.Components.Responses, refNameResolver) for _, ss := range doc.Components.SecuritySchemes { doc.addSecuritySchemeToSpec(ss, refNameResolver) } doc.derefExamples(doc.Components.Examples, refNameResolver) doc.derefLinks(doc.Components.Links, refNameResolver) for _, cb := range doc.Components.Callbacks { doc.addCallbackToSpec(cb, refNameResolver) if cb != nil && cb.Value != nil { cb.Ref = "" // always dereference the top level doc.derefPaths(*cb.Value, refNameResolver) } } doc.derefPaths(doc.Paths, refNameResolver) } kin-openapi-0.85.0/openapi3/internalize_refs_test.go000066400000000000000000000031041415236407200224670ustar00rootroot00000000000000package openapi3 import ( "context" "regexp" "testing" "github.com/stretchr/testify/require" ) func TestInternalizeRefs(t *testing.T) { var regexpRef = regexp.MustCompile(`"\$ref":`) var regexpRefInternal = regexp.MustCompile(`"\$ref":"\#`) tests := []struct { filename string }{ {"testdata/testref.openapi.yml"}, {"testdata/recursiveRef/openapi.yml"}, {"testdata/spec.yaml"}, {"testdata/callbacks.yml"}, } for _, test := range tests { t.Run(test.filename, func(t *testing.T) { // Load in the reference spec from the testdata sl := NewLoader() sl.IsExternalRefsAllowed = true doc, err := sl.LoadFromFile(test.filename) require.NoError(t, err, "loading test file") // Internalize the references doc.InternalizeRefs(context.Background(), DefaultRefNameResolver) // Validate the internalized spec err = doc.Validate(context.Background()) require.Nil(t, err, "validating internalized spec") data, err := doc.MarshalJSON() require.NoError(t, err, "marshalling internalized spec") // run a static check over the file, making sure each occurence of a // reference is followed by a # numRefs := len(regexpRef.FindAll(data, -1)) numInternalRefs := len(regexpRefInternal.FindAll(data, -1)) require.Equal(t, numRefs, numInternalRefs, "checking all references are internal") // load from data, but with the path set to the current directory doc2, err := sl.LoadFromData(data) require.NoError(t, err, "reloading spec") err = doc2.Validate(context.Background()) require.Nil(t, err, "validating reloaded spec") }) } } kin-openapi-0.85.0/openapi3/issue301_test.go000066400000000000000000000015021415236407200205000ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestIssue301(t *testing.T) { sl := NewLoader() sl.IsExternalRefsAllowed = true doc, err := sl.LoadFromFile("testdata/callbacks.yml") require.NoError(t, err) err = doc.Validate(sl.Context) require.NoError(t, err) transCallbacks := doc.Paths["/trans"].Post.Callbacks["transactionCallback"].Value require.Equal(t, "object", (*transCallbacks)["http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}"].Post.RequestBody. Value.Content["application/json"].Schema. Value.Type) otherCallbacks := doc.Paths["/other"].Post.Callbacks["myEvent"].Value require.Equal(t, "boolean", (*otherCallbacks)["{$request.query.queryUrl}"].Post.RequestBody. Value.Content["application/json"].Schema. Value.Type) } kin-openapi-0.85.0/openapi3/issue341_test.go000066400000000000000000000013771415236407200205160ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestIssue341(t *testing.T) { sl := NewLoader() sl.IsExternalRefsAllowed = true doc, err := sl.LoadFromFile("testdata/main.yaml") require.NoError(t, err) err = doc.Validate(sl.Context) require.NoError(t, err) err = sl.ResolveRefsIn(doc, nil) require.NoError(t, err) bs, err := doc.MarshalJSON() require.NoError(t, err) require.Equal(t, []byte(`{"components":{},"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"get":{"responses":{"200":{"$ref":"#/components/responses/testpath_200_response"}}}}}}`), bs) require.Equal(t, "string", doc.Paths["/testpath"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) } kin-openapi-0.85.0/openapi3/issue344_test.go000066400000000000000000000006521415236407200205140ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestIssue344(t *testing.T) { sl := NewLoader() sl.IsExternalRefsAllowed = true doc, err := sl.LoadFromFile("testdata/spec.yaml") require.NoError(t, err) err = doc.Validate(sl.Context) require.NoError(t, err) require.Equal(t, "string", doc.Components.Schemas["Test"].Value.Properties["test"].Value.Properties["name"].Value.Type) } kin-openapi-0.85.0/openapi3/issue376_test.go000066400000000000000000000043051415236407200205200ustar00rootroot00000000000000package openapi3 import ( "fmt" "testing" "github.com/stretchr/testify/require" ) func TestIssue376(t *testing.T) { spec := []byte(` openapi: 3.0.0 components: schemas: schema1: type: object additionalProperties: type: string schema2: type: object properties: prop: $ref: '#/components/schemas/schema1/additionalProperties' paths: {} info: title: An API version: 1.2.3.4 `) loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) require.Equal(t, "An API", doc.Info.Title) require.Equal(t, 2, len(doc.Components.Schemas)) require.Equal(t, 0, len(doc.Paths)) require.Equal(t, "string", doc.Components.Schemas["schema2"].Value.Properties["prop"].Value.Type) } func TestMultijsonTagSerialization(t *testing.T) { spec := []byte(` openapi: 3.0.0 components: schemas: unset: type: number #empty-object: # TODO additionalProperties: {} object: additionalProperties: {type: string} boolean: additionalProperties: false paths: {} info: title: An API version: 1.2.3.4 `) loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) for propName, propSchema := range doc.Components.Schemas { ap := propSchema.Value.AdditionalProperties apa := propSchema.Value.AdditionalPropertiesAllowed encoded, err := propSchema.MarshalJSON() require.NoError(t, err) require.Equal(t, string(encoded), map[string]string{ "unset": `{"type":"number"}`, // TODO: "empty-object":`{"additionalProperties":{}}`, "object": `{"additionalProperties":{"type":"string"}}`, "boolean": `{"additionalProperties":false}`, }[propName]) if propName == "unset" { require.True(t, ap == nil && apa == nil) continue } apStr := "" if ap != nil { apStr = fmt.Sprintf("{Ref:%s Value.Type:%v}", (*ap).Ref, (*ap).Value.Type) } apaStr := "" if apa != nil { apaStr = fmt.Sprintf("%v", *apa) } require.Truef(t, (ap != nil && apa == nil) || (ap == nil && apa != nil), "%s: isnil(%s) xor isnil(%s)", propName, apaStr, apStr) } } kin-openapi-0.85.0/openapi3/issue382_test.go000066400000000000000000000005031415236407200205110ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestOverridingGlobalParametersValidation(t *testing.T) { loader := NewLoader() doc, err := loader.LoadFromFile("testdata/Test_param_override.yml") require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) } kin-openapi-0.85.0/openapi3/link.go000066400000000000000000000033121415236407200170230ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "fmt" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) type Links map[string]*LinkRef func (l Links) JSONLookup(token string) (interface{}, error) { ref, ok := l[token] if ok == false { return nil, fmt.Errorf("object has no field %q", token) } if ref != nil && ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } var _ jsonpointer.JSONPointable = (*Links)(nil) // Link is specified by OpenAPI/Swagger standard version 3.0. type Link struct { ExtensionProps OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` Server *Server `json:"server,omitempty" yaml:"server,omitempty"` RequestBody interface{} `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` } func (value *Link) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(value) } func (value *Link) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } func (value *Link) Validate(ctx context.Context) error { if value.OperationID == "" && value.OperationRef == "" { return errors.New("missing operationId or operationRef on link") } if value.OperationID != "" && value.OperationRef != "" { return fmt.Errorf("operationId %q and operationRef %q are mutually exclusive", value.OperationID, value.OperationRef) } return nil } kin-openapi-0.85.0/openapi3/loader.go000066400000000000000000000711051415236407200173410ustar00rootroot00000000000000package openapi3 import ( "context" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/url" "path" "path/filepath" "reflect" "strconv" "strings" "github.com/ghodss/yaml" ) func foundUnresolvedRef(ref string) error { return fmt.Errorf("found unresolved ref: %q", ref) } func failedToResolveRefFragmentPart(value, what string) error { return fmt.Errorf("failed to resolve %q in fragment in URI: %q", what, value) } // Loader helps deserialize an OpenAPIv3 document type Loader struct { // IsExternalRefsAllowed enables visiting other files IsExternalRefsAllowed bool // ReadFromURIFunc allows overriding the any file/URL reading func ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error) Context context.Context rootDir string visitedPathItemRefs map[string]struct{} visitedDocuments map[string]*T visitedExample map[*Example]struct{} visitedHeader map[*Header]struct{} visitedLink map[*Link]struct{} visitedParameter map[*Parameter]struct{} visitedRequestBody map[*RequestBody]struct{} visitedResponse map[*Response]struct{} visitedSchema map[*Schema]struct{} visitedSecurityScheme map[*SecurityScheme]struct{} } // NewLoader returns an empty Loader func NewLoader() *Loader { return &Loader{} } func (loader *Loader) resetVisitedPathItemRefs() { loader.visitedPathItemRefs = make(map[string]struct{}) } // LoadFromURI loads a spec from a remote URL func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) { loader.resetVisitedPathItemRefs() return loader.loadFromURIInternal(location) } // LoadFromFile loads a spec from a local file path func (loader *Loader) LoadFromFile(location string) (*T, error) { loader.rootDir = path.Dir(location) return loader.LoadFromURI(&url.URL{Path: filepath.ToSlash(location)}) } func (loader *Loader) loadFromURIInternal(location *url.URL) (*T, error) { data, err := loader.readURL(location) if err != nil { return nil, err } return loader.loadFromDataWithPathInternal(data, location) } func (loader *Loader) allowsExternalRefs(ref string) (err error) { if !loader.IsExternalRefsAllowed { err = fmt.Errorf("encountered disallowed external reference: %q", ref) } return } // loadSingleElementFromURI reads the data from ref and unmarshals to the passed element. func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, element interface{}) (*url.URL, error) { if err := loader.allowsExternalRefs(ref); err != nil { return nil, err } parsedURL, err := url.Parse(ref) if err != nil { return nil, err } if fragment := parsedURL.Fragment; fragment != "" { return nil, fmt.Errorf("unexpected ref fragment %q", fragment) } resolvedPath, err := resolvePath(rootPath, parsedURL) if err != nil { return nil, fmt.Errorf("could not resolve path: %v", err) } data, err := loader.readURL(resolvedPath) if err != nil { return nil, err } if err := yaml.Unmarshal(data, element); err != nil { return nil, err } return resolvedPath, nil } func (loader *Loader) readURL(location *url.URL) ([]byte, error) { if f := loader.ReadFromURIFunc; f != nil { return f(loader, location) } if location.Scheme != "" && location.Host != "" { resp, err := http.Get(location.String()) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode > 399 { return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode) } return ioutil.ReadAll(resp.Body) } if location.Scheme != "" || location.Host != "" || location.RawQuery != "" { return nil, fmt.Errorf("unsupported URI: %q", location.String()) } return ioutil.ReadFile(location.Path) } // LoadFromData loads a spec from a byte array func (loader *Loader) LoadFromData(data []byte) (*T, error) { loader.resetVisitedPathItemRefs() doc := &T{} if err := yaml.Unmarshal(data, doc); err != nil { return nil, err } if err := loader.ResolveRefsIn(doc, nil); err != nil { return nil, err } return doc, nil } // LoadFromDataWithPath takes the OpenAPI document data in bytes and a path where the resolver can find referred // elements and returns a *T with all resolved data or an error if unable to load data or resolve refs. func (loader *Loader) LoadFromDataWithPath(data []byte, location *url.URL) (*T, error) { loader.resetVisitedPathItemRefs() return loader.loadFromDataWithPathInternal(data, location) } func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.URL) (*T, error) { if loader.visitedDocuments == nil { loader.visitedDocuments = make(map[string]*T) } uri := location.String() if doc, ok := loader.visitedDocuments[uri]; ok { return doc, nil } doc := &T{} loader.visitedDocuments[uri] = doc if err := yaml.Unmarshal(data, doc); err != nil { return nil, err } if err := loader.ResolveRefsIn(doc, location); err != nil { return nil, err } return doc, nil } // ResolveRefsIn expands references if for instance spec was just unmarshalled func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { if loader.visitedPathItemRefs == nil { loader.resetVisitedPathItemRefs() } // Visit all components components := doc.Components for _, component := range components.Headers { if err = loader.resolveHeaderRef(doc, component, location); err != nil { return } } for _, component := range components.Parameters { if err = loader.resolveParameterRef(doc, component, location); err != nil { return } } for _, component := range components.RequestBodies { if err = loader.resolveRequestBodyRef(doc, component, location); err != nil { return } } for _, component := range components.Responses { if err = loader.resolveResponseRef(doc, component, location); err != nil { return } } for _, component := range components.Schemas { if err = loader.resolveSchemaRef(doc, component, location); err != nil { return } } for _, component := range components.SecuritySchemes { if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil { return } } for _, component := range components.Examples { if err = loader.resolveExampleRef(doc, component, location); err != nil { return } } for _, component := range components.Callbacks { if err = loader.resolveCallbackRef(doc, component, location); err != nil { return } } // Visit all operations for entrypoint, pathItem := range doc.Paths { if pathItem == nil { continue } if err = loader.resolvePathItemRef(doc, entrypoint, pathItem, location); err != nil { return } } return } func join(basePath *url.URL, relativePath *url.URL) (*url.URL, error) { if basePath == nil { return relativePath, nil } newPath, err := url.Parse(basePath.String()) if err != nil { return nil, fmt.Errorf("cannot copy path: %q", basePath.String()) } newPath.Path = path.Join(path.Dir(newPath.Path), relativePath.Path) return newPath, nil } func resolvePath(basePath *url.URL, componentPath *url.URL) (*url.URL, error) { if componentPath.Scheme == "" && componentPath.Host == "" { // support absolute paths if componentPath.Path[0] == '/' { return componentPath, nil } return join(basePath, componentPath) } return componentPath, nil } func isSingleRefElement(ref string) bool { return !strings.Contains(ref, "#") } func (loader *Loader) resolveComponent( doc *T, ref string, path *url.URL, resolved interface{}, ) ( componentPath *url.URL, err error, ) { if doc, ref, componentPath, err = loader.resolveRef(doc, ref, path); err != nil { return nil, err } parsedURL, err := url.Parse(ref) if err != nil { return nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) } fragment := parsedURL.Fragment if !strings.HasPrefix(fragment, "/") { return nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) } drill := func(cursor interface{}) (interface{}, error) { for _, pathPart := range strings.Split(fragment[1:], "/") { pathPart = unescapeRefString(pathPart) if cursor, err = drillIntoField(cursor, pathPart); err != nil { e := failedToResolveRefFragmentPart(ref, pathPart) return nil, fmt.Errorf("%s: %s", e.Error(), err.Error()) } if cursor == nil { return nil, failedToResolveRefFragmentPart(ref, pathPart) } } return cursor, nil } var cursor interface{} if cursor, err = drill(doc); err != nil { if path == nil { return nil, err } var err2 error data, err2 := loader.readURL(path) if err2 != nil { return nil, err } if err2 = yaml.Unmarshal(data, &cursor); err2 != nil { return nil, err } if cursor, err2 = drill(cursor); err2 != nil || cursor == nil { return nil, err } err = nil } switch { case reflect.TypeOf(cursor) == reflect.TypeOf(resolved): reflect.ValueOf(resolved).Elem().Set(reflect.ValueOf(cursor).Elem()) return componentPath, nil case reflect.TypeOf(cursor) == reflect.TypeOf(map[string]interface{}{}): codec := func(got, expect interface{}) error { enc, err := json.Marshal(got) if err != nil { return err } if err = json.Unmarshal(enc, expect); err != nil { return err } return nil } if err := codec(cursor, resolved); err != nil { return nil, fmt.Errorf("bad data in %q", ref) } return componentPath, nil default: return nil, fmt.Errorf("bad data in %q", ref) } } func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { // Special case due to multijson if s, ok := cursor.(*SchemaRef); ok && fieldName == "additionalProperties" { if ap := s.Value.AdditionalPropertiesAllowed; ap != nil { return *ap, nil } return s.Value.AdditionalProperties, nil } switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() { case reflect.Map: elementValue := val.MapIndex(reflect.ValueOf(fieldName)) if !elementValue.IsValid() { return nil, fmt.Errorf("map key %q not found", fieldName) } return elementValue.Interface(), nil case reflect.Slice: i, err := strconv.ParseUint(fieldName, 10, 32) if err != nil { return nil, err } index := int(i) if 0 > index || index >= val.Len() { return nil, errors.New("slice index out of bounds") } return val.Index(index).Interface(), nil case reflect.Struct: hasFields := false for i := 0; i < val.NumField(); i++ { hasFields = true field := val.Type().Field(i) tagValue := field.Tag.Get("yaml") yamlKey := strings.Split(tagValue, ",")[0] if yamlKey == "-" { tagValue := field.Tag.Get("multijson") yamlKey = strings.Split(tagValue, ",")[0] } if yamlKey == fieldName { return val.Field(i).Interface(), nil } } // if cursor is a "ref wrapper" struct (e.g. RequestBodyRef), if _, ok := val.Type().FieldByName("Value"); ok { // try digging into its Value field return drillIntoField(val.FieldByName("Value").Interface(), fieldName) } if hasFields { if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "ExtensionProps" { extensions := val.Field(0).Interface().(ExtensionProps).Extensions if enc, ok := extensions[fieldName]; ok { var dec interface{} if err := json.Unmarshal(enc.(json.RawMessage), &dec); err != nil { return nil, err } return dec, nil } } } return nil, fmt.Errorf("struct field %q not found", fieldName) default: return nil, errors.New("not a map, slice nor struct") } } func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef string) *url.URL { if loader.rootDir == "" { return current } return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)} } func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { if ref != "" && ref[0] == '#' { return doc, ref, path, nil } if err := loader.allowsExternalRefs(ref); err != nil { return nil, "", nil, err } parsedURL, err := url.Parse(ref) if err != nil { return nil, "", nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) } fragment := parsedURL.Fragment parsedURL.Fragment = "" var resolvedPath *url.URL if resolvedPath, err = resolvePath(path, parsedURL); err != nil { return nil, "", nil, fmt.Errorf("error resolving path: %v", err) } if doc, err = loader.loadFromURIInternal(resolvedPath); err != nil { return nil, "", nil, fmt.Errorf("error resolving reference %q: %v", ref, err) } return doc, "#" + fragment, resolvedPath, nil } func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedHeader == nil { loader.visitedHeader = make(map[*Header]struct{}) } if _, ok := loader.visitedHeader[component.Value]; ok { return nil } loader.visitedHeader[component.Value] = struct{}{} } if component == nil { return errors.New("invalid header: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var header Header if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { return err } component.Value = &header } else { var resolved HeaderRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveHeaderRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value if value == nil { return nil } if schema := value.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } } return nil } func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedParameter == nil { loader.visitedParameter = make(map[*Parameter]struct{}) } if _, ok := loader.visitedParameter[component.Value]; ok { return nil } loader.visitedParameter[component.Value] = struct{}{} } if component == nil { return errors.New("invalid parameter: value MUST be an object") } ref := component.Ref if ref != "" { if isSingleRefElement(ref) { var param Parameter if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { return err } component.Value = ¶m } else { var resolved ParameterRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveParameterRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value if value == nil { return nil } if value.Content != nil && value.Schema != nil { return errors.New("cannot contain both schema and content in a parameter") } for _, contentType := range value.Content { if schema := contentType.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } } } if schema := value.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } } return nil } func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedRequestBody == nil { loader.visitedRequestBody = make(map[*RequestBody]struct{}) } if _, ok := loader.visitedRequestBody[component.Value]; ok { return nil } loader.visitedRequestBody[component.Value] = struct{}{} } if component == nil { return errors.New("invalid requestBody: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var requestBody RequestBody if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { return err } component.Value = &requestBody } else { var resolved RequestBodyRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err = loader.resolveRequestBodyRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value if value == nil { return nil } for _, contentType := range value.Content { for name, example := range contentType.Examples { if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err } contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } } } return nil } func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedResponse == nil { loader.visitedResponse = make(map[*Response]struct{}) } if _, ok := loader.visitedResponse[component.Value]; ok { return nil } loader.visitedResponse[component.Value] = struct{}{} } if component == nil { return errors.New("invalid response: value MUST be an object") } ref := component.Ref if ref != "" { if isSingleRefElement(ref) { var resp Response if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { return err } component.Value = &resp } else { var resolved ResponseRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveResponseRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value if value == nil { return nil } for _, header := range value.Headers { if err := loader.resolveHeaderRef(doc, header, documentPath); err != nil { return err } } for _, contentType := range value.Content { if contentType == nil { continue } for name, example := range contentType.Examples { if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err } contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath); err != nil { return err } contentType.Schema = schema } } for _, link := range value.Links { if err := loader.resolveLinkRef(doc, link, documentPath); err != nil { return err } } return nil } func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedSchema == nil { loader.visitedSchema = make(map[*Schema]struct{}) } if _, ok := loader.visitedSchema[component.Value]; ok { return nil } loader.visitedSchema[component.Value] = struct{}{} } if component == nil { return errors.New("invalid schema: value MUST be an object") } ref := component.Ref if ref != "" { if isSingleRefElement(ref) { var schema Schema if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { return err } component.Value = &schema } else { var resolved SchemaRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveSchemaRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value if value == nil { return nil } // ResolveRefs referred schemas if v := value.Items; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } for _, v := range value.Properties { if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } if v := value.AdditionalProperties; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } if v := value.Not; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } for _, v := range value.AllOf { if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } for _, v := range value.AnyOf { if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } for _, v := range value.OneOf { if err := loader.resolveSchemaRef(doc, v, documentPath); err != nil { return err } } return nil } func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedSecurityScheme == nil { loader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) } if _, ok := loader.visitedSecurityScheme[component.Value]; ok { return nil } loader.visitedSecurityScheme[component.Value] = struct{}{} } if component == nil { return errors.New("invalid securityScheme: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var scheme SecurityScheme if _, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { return err } component.Value = &scheme } else { var resolved SecuritySchemeRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveSecuritySchemeRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value _ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } return nil } func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedExample == nil { loader.visitedExample = make(map[*Example]struct{}) } if _, ok := loader.visitedExample[component.Value]; ok { return nil } loader.visitedExample[component.Value] = struct{}{} } if component == nil { return errors.New("invalid example: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var example Example if _, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { return err } component.Value = &example } else { var resolved ExampleRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveExampleRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value _ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } return nil } func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) { if component == nil { return errors.New("invalid callback: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var resolved Callback if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil { return err } component.Value = &resolved } else { var resolved CallbackRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveCallbackRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } value := component.Value if value == nil { return nil } for entrypoint, pathItem := range *value { entrypoint, pathItem := entrypoint, pathItem err = func() (err error) { key := "-" if documentPath != nil { key = documentPath.EscapedPath() } key += entrypoint if _, ok := loader.visitedPathItemRefs[key]; ok { return nil } loader.visitedPathItemRefs[key] = struct{}{} if pathItem == nil { return errors.New("invalid path item: value MUST be an object") } ref := pathItem.Ref if ref != "" { if isSingleRefElement(ref) { var p PathItem if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { return err } *pathItem = p } else { if doc, ref, documentPath, err = loader.resolveRef(doc, ref, documentPath); err != nil { return } rest := strings.TrimPrefix(ref, "#/components/callbacks/") if rest == ref { return fmt.Errorf(`expected prefix "#/components/callbacks/" in URI %q`, ref) } id := unescapeRefString(rest) definitions := doc.Components.Callbacks if definitions == nil { return failedToResolveRefFragmentPart(ref, "callbacks") } resolved := definitions[id] if resolved == nil { return failedToResolveRefFragmentPart(ref, id) } for _, p := range *resolved.Value { *pathItem = *p break } } } return loader.resolvePathItemRefContinued(doc, pathItem, documentPath) }() if err != nil { return err } } return nil } func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if loader.visitedLink == nil { loader.visitedLink = make(map[*Link]struct{}) } if _, ok := loader.visitedLink[component.Value]; ok { return nil } loader.visitedLink[component.Value] = struct{}{} } if component == nil { return errors.New("invalid link: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var link Link if _, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { return err } component.Value = &link } else { var resolved LinkRef componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveLinkRef(doc, &resolved, componentPath); err != nil { return err } component.Value = resolved.Value _ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref) } } return nil } func (loader *Loader) resolvePathItemRef(doc *T, entrypoint string, pathItem *PathItem, documentPath *url.URL) (err error) { key := "_" if documentPath != nil { key = documentPath.EscapedPath() } key += entrypoint if _, ok := loader.visitedPathItemRefs[key]; ok { return nil } loader.visitedPathItemRefs[key] = struct{}{} if pathItem == nil { return errors.New("invalid path item: value MUST be an object") } ref := pathItem.Ref if ref != "" { if isSingleRefElement(ref) { var p PathItem if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { return err } *pathItem = p } else { if doc, ref, documentPath, err = loader.resolveRef(doc, ref, documentPath); err != nil { return } rest := strings.TrimPrefix(ref, "#/paths/") if rest == ref { return fmt.Errorf(`expected prefix "#/paths/" in URI %q`, ref) } id := unescapeRefString(rest) definitions := doc.Paths if definitions == nil { return failedToResolveRefFragmentPart(ref, "paths") } resolved := definitions[id] if resolved == nil { return failedToResolveRefFragmentPart(ref, id) } *pathItem = *resolved } } return loader.resolvePathItemRefContinued(doc, pathItem, documentPath) } func (loader *Loader) resolvePathItemRefContinued(doc *T, pathItem *PathItem, documentPath *url.URL) (err error) { for _, parameter := range pathItem.Parameters { if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil { return } } for _, operation := range pathItem.Operations() { for _, parameter := range operation.Parameters { if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil { return } } if requestBody := operation.RequestBody; requestBody != nil { if err = loader.resolveRequestBodyRef(doc, requestBody, documentPath); err != nil { return } } for _, response := range operation.Responses { if err = loader.resolveResponseRef(doc, response, documentPath); err != nil { return } } for _, callback := range operation.Callbacks { if err = loader.resolveCallbackRef(doc, callback, documentPath); err != nil { return } } } return } func unescapeRefString(ref string) string { return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1) } kin-openapi-0.85.0/openapi3/loader_empty_response_description_test.go000066400000000000000000000037471415236407200261460ustar00rootroot00000000000000package openapi3 import ( "encoding/json" "strings" "testing" "github.com/stretchr/testify/require" ) func TestJSONSpecResponseDescriptionEmptiness(t *testing.T) { const spec = ` { "info": { "description": "A sample API to illustrate OpenAPI concepts", "title": "Sample API", "version": "1.0.0" }, "openapi": "3.0.0", "paths": { "/path1": { "get": { "responses": { "200": { "description": "" } } } } } } ` { spec := []byte(spec) loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) got := doc.Paths["/path1"].Get.Responses["200"].Value.Description expected := "" require.Equal(t, &expected, got) t.Log("Empty description provided: valid spec") err = doc.Validate(loader.Context) require.NoError(t, err) } { spec := []byte(strings.Replace(spec, `"description": ""`, `"description": "My response"`, 1)) loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) got := doc.Paths["/path1"].Get.Responses["200"].Value.Description expected := "My response" require.Equal(t, &expected, got) t.Log("Non-empty description provided: valid spec") err = doc.Validate(loader.Context) require.NoError(t, err) } noDescriptionIsInvalid := func(data []byte) *T { loader := NewLoader() doc, err := loader.LoadFromData(data) require.NoError(t, err) got := doc.Paths["/path1"].Get.Responses["200"].Value.Description require.Nil(t, got) t.Log("No description provided: invalid spec") err = doc.Validate(loader.Context) require.Error(t, err) return doc } var docWithNoResponseDescription *T { spec := []byte(strings.Replace(spec, `"description": ""`, ``, 1)) docWithNoResponseDescription = noDescriptionIsInvalid(spec) } str, err := json.Marshal(docWithNoResponseDescription) require.NoError(t, err) require.NotEmpty(t, str) t.Log("Reserialization does not set description") _ = noDescriptionIsInvalid(str) } kin-openapi-0.85.0/openapi3/loader_http_error_test.go000066400000000000000000000057031415236407200226510ustar00rootroot00000000000000package openapi3 import ( "fmt" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/require" ) func TestLoadReferenceFromRemoteURLFailsWithHttpError(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, "") })) defer ts.Close() spec := []byte(` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": { "default": { "description": "test", "headers": { "X-TEST-HEADER": { "$ref": "` + ts.URL + `/components.openapi.json#/components/headers/CustomTestHeader" } } } } } } } }`) loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.Nil(t, doc) require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL, ts.URL)) doc, err = loader.LoadFromData(spec) require.Nil(t, doc) require.EqualError(t, err, fmt.Sprintf("error resolving reference \"%s/components.openapi.json#/components/headers/CustomTestHeader\": error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL, ts.URL)) } func TestLoadFromRemoteURLFailsWithHttpError(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, "") })) defer ts.Close() spec := []byte(` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": { "default": { "description": "test", "headers": { "X-TEST-HEADER": { "$ref": "` + ts.URL + `/components.openapi.json" } } } } } } } }`) loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.Nil(t, doc) require.EqualError(t, err, fmt.Sprintf("error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL)) doc, err = loader.LoadFromData(spec) require.Nil(t, doc) require.EqualError(t, err, fmt.Sprintf("error loading \"%s/components.openapi.json\": request returned status code 400", ts.URL)) } kin-openapi-0.85.0/openapi3/loader_issue212_test.go000066400000000000000000000042701415236407200220340ustar00rootroot00000000000000package openapi3 import ( "encoding/json" "testing" "github.com/stretchr/testify/require" ) func TestIssue212(t *testing.T) { spec := ` openapi: 3.0.1 info: title: 'test' version: 1.0.0 servers: - url: /api paths: /available-products: get: operationId: getAvailableProductCollection responses: "200": description: test content: application/json: schema: type: array items: $ref: "#/components/schemas/AvailableProduct" components: schemas: AvailableProduct: type: object properties: id: type: string type: type: string name: type: string media: type: object properties: documents: type: array items: allOf: - $ref: "#/components/schemas/AvailableProduct/properties/previewImage/allOf/0" - type: object properties: uri: type: string pattern: ^\/documents\/[0-9a-f]{64}$ previewImage: allOf: - type: object required: - id - uri properties: id: type: string uri: type: string - type: object properties: uri: type: string pattern: ^\/images\/[0-9a-f]{64}$ ` loader := NewLoader() doc, err := loader.LoadFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) expected, err := json.Marshal(&Schema{ Type: "object", Required: []string{"id", "uri"}, Properties: Schemas{ "id": {Value: &Schema{Type: "string"}}, "uri": {Value: &Schema{Type: "string"}}, }, }, ) require.NoError(t, err) got, err := json.Marshal(doc.Components.Schemas["AvailableProduct"].Value.Properties["media"].Value.Properties["documents"].Value.Items.Value.AllOf[0].Value) require.NoError(t, err) require.Equal(t, expected, got) } kin-openapi-0.85.0/openapi3/loader_issue220_test.go000066400000000000000000000011751415236407200220340ustar00rootroot00000000000000package openapi3 import ( "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestIssue220(t *testing.T) { for _, specPath := range []string{ "testdata/my-openapi.json", filepath.FromSlash("testdata/my-openapi.json"), } { t.Logf("specPath: %q", specPath) loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile(specPath) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) require.Equal(t, "integer", doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["bar"].Value.Type) } } kin-openapi-0.85.0/openapi3/loader_issue235_test.go000066400000000000000000000012011415236407200220300ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestIssue235OK(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/issue235.spec0.yml") require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) } func TestIssue235CircularDep(t *testing.T) { t.Skip("TODO: return an error on circular dependencies between external files of a spec") loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/issue235.spec0-typo.yml") require.Nil(t, doc) require.Error(t, err) } kin-openapi-0.85.0/openapi3/loader_outside_refs_test.go000066400000000000000000000010371415236407200231500ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestLoadOutsideRefs(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/303bis/service.yaml") require.NoError(t, err) require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) require.Equal(t, "string", doc.Paths["/service"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Items.Value.AllOf[0].Value.Properties["created_at"].Value.Type) } kin-openapi-0.85.0/openapi3/loader_paths_test.go000066400000000000000000000013731415236407200215770ustar00rootroot00000000000000package openapi3 import ( "strings" "testing" "github.com/stretchr/testify/require" ) func TestPathsMustStartWithSlash(t *testing.T) { spec := ` openapi: "3.0" info: version: "1.0" title: sample basePath: /adc/v1 paths: PATH: get: responses: 200: description: description ` for path, expectedErr := range map[string]string{ "foo/bar": "invalid paths: path \"foo/bar\" does not start with a forward slash (/)", "/foo/bar": "", } { 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) } } } kin-openapi-0.85.0/openapi3/loader_read_from_uri_func_test.go000066400000000000000000000047261415236407200243150ustar00rootroot00000000000000package openapi3 import ( "fmt" "io/ioutil" "net/url" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func TestLoaderReadFromURIFunc(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true loader.ReadFromURIFunc = func(loader *Loader, url *url.URL) ([]byte, error) { return ioutil.ReadFile(filepath.Join("testdata", url.Path)) } doc, err := loader.LoadFromFile("recursiveRef/openapi.yml") require.NoError(t, err) require.NotNil(t, doc) require.NoError(t, doc.Validate(loader.Context)) require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example) } type multipleSourceLoaderExample struct { Sources map[string][]byte } func (l *multipleSourceLoaderExample) LoadFromURI( loader *Loader, location *url.URL, ) ([]byte, error) { source := l.resolveSourceFromURI(location) if source == nil { return nil, fmt.Errorf("unsupported URI: %q", location.String()) } return source, nil } func (l *multipleSourceLoaderExample) resolveSourceFromURI(location fmt.Stringer) []byte { return l.Sources[location.String()] } func TestResolveSchemaExternalRef(t *testing.T) { rootLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "spec.json"} externalLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "external.json"} rootSpec := []byte(fmt.Sprintf( `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Root":{"allOf":[{"$ref":"%s#/components/schemas/External"}]}}}}`, externalLocation.String(), )) externalSpec := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"External Spec"},"paths":{},"components":{"schemas":{"External":{"type":"string"}}}}`) multipleSourceLoader := &multipleSourceLoaderExample{ Sources: map[string][]byte{ rootLocation.String(): rootSpec, externalLocation.String(): externalSpec, }, } loader := &Loader{ IsExternalRefsAllowed: true, ReadFromURIFunc: multipleSourceLoader.LoadFromURI, } doc, err := loader.LoadFromURI(rootLocation) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) refRootVisited := doc.Components.Schemas["Root"].Value.AllOf[0] require.Equal(t, fmt.Sprintf("%s#/components/schemas/External", externalLocation.String()), refRootVisited.Ref) require.NotNil(t, refRootVisited.Value) } kin-openapi-0.85.0/openapi3/loader_recursive_ref_test.go000066400000000000000000000022511415236407200233170ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestLoaderSupportsRecursiveReference(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml") require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example) } func TestIssue447(t *testing.T) { loader := NewLoader() doc, err := loader.LoadFromData([]byte(` openapi: 3.0.1 info: title: Recursive refs example version: "1.0" paths: {} components: schemas: Complex: type: object properties: parent: $ref: '#/components/schemas/Complex' `)) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) require.Equal(t, "object", doc.Components. // Complex Schemas["Complex"]. // parent Value.Properties["parent"]. // parent Value.Properties["parent"]. // parent Value.Properties["parent"]. // type Value.Type) } kin-openapi-0.85.0/openapi3/loader_relative_refs_test.go000066400000000000000000000654371415236407200233250ustar00rootroot00000000000000package openapi3 import ( "fmt" "net/url" "testing" "github.com/stretchr/testify/require" ) type refTestDataEntry struct { name string contentTemplate string testFunc func(t *testing.T, doc *T) } type refTestDataEntryWithErrorMessage struct { name string contentTemplate string errorMessage *string testFunc func(t *testing.T, doc *T) } var refTestDataEntries = []refTestDataEntry{ { name: "SchemaRef", contentTemplate: externalSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Schemas["TestSchema"].Value.Type) require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type) }, }, { name: "ResponseRef", contentTemplate: externalResponseRefTemplate, testFunc: func(t *testing.T, doc *T) { desc := "description" require.Equal(t, &desc, doc.Components.Responses["TestResponse"].Value.Description) }, }, { name: "ParameterRef", contentTemplate: externalParameterRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Parameters["TestParameter"].Value.Name) require.Equal(t, "id", doc.Components.Parameters["TestParameter"].Value.Name) }, }, { name: "ExampleRef", contentTemplate: externalExampleRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Examples["TestExample"].Value.Description) require.Equal(t, "description", doc.Components.Examples["TestExample"].Value.Description) }, }, { name: "RequestBodyRef", contentTemplate: externalRequestBodyRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.RequestBodies["TestRequestBody"].Value.Content) }, }, { name: "SecuritySchemeRef", contentTemplate: externalSecuritySchemeRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Description) require.Equal(t, "description", doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Description) }, }, { name: "ExternalHeaderRef", contentTemplate: externalHeaderRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Headers["TestHeader"].Value.Description) require.Equal(t, "description", doc.Components.Headers["TestHeader"].Value.Description) }, }, { name: "PathParameterRef", contentTemplate: externalPathParameterRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test/{id}"].Parameters[0].Value.Name) require.Equal(t, "id", doc.Paths["/test/{id}"].Parameters[0].Value.Name) }, }, { name: "PathOperationParameterRef", contentTemplate: externalPathOperationParameterRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test/{id}"].Get.Parameters[0].Value) require.Equal(t, "id", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Name) }, }, { name: "PathOperationRequestBodyRef", contentTemplate: externalPathOperationRequestBodyRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value) require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content) }, }, { name: "PathOperationResponseRef", contentTemplate: externalPathOperationResponseRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) desc := "description" require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) }, }, { name: "PathOperationParameterSchemaRef", contentTemplate: externalPathOperationParameterSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value) require.Equal(t, "string", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Schema.Value.Type) require.Equal(t, "id", doc.Paths["/test/{id}"].Get.Parameters[0].Value.Name) }, }, { name: "PathOperationParameterRefWithContentInQuery", contentTemplate: externalPathOperationParameterWithContentInQueryTemplate, testFunc: func(t *testing.T, doc *T) { schemaRef := doc.Paths["/test/{id}"].Get.Parameters[0].Value.Content["application/json"].Schema require.NotNil(t, schemaRef.Value) require.Equal(t, "string", schemaRef.Value.Type) }, }, { name: "PathOperationRequestBodyExampleRef", contentTemplate: externalPathOperationRequestBodyExampleRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value) require.Equal(t, "description", doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Examples["application/json"].Value.Description) }, }, { name: "PathOperationReqestBodyContentSchemaRef", contentTemplate: externalPathOperationReqestBodyContentSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value) require.Equal(t, "string", doc.Paths["/test"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Type) }, }, { name: "PathOperationResponseExampleRef", contentTemplate: externalPathOperationResponseExampleRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) desc := "testdescription" require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Examples["application/json"].Value.Description) }, }, { name: "PathOperationResponseSchemaRef", contentTemplate: externalPathOperationResponseSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value) desc := "testdescription" require.Equal(t, &desc, doc.Paths["/test"].Post.Responses["default"].Value.Description) require.Equal(t, "string", doc.Paths["/test"].Post.Responses["default"].Value.Content["application/json"].Schema.Value.Type) }, }, { name: "ComponentHeaderSchemaRef", contentTemplate: externalComponentHeaderSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Headers["TestHeader"].Value) require.Equal(t, "string", doc.Components.Headers["TestHeader"].Value.Schema.Value.Type) }, }, { name: "RequestResponseHeaderRef", contentTemplate: externalRequestResponseHeaderRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) }, }, } var refTestDataEntriesResponseError = []refTestDataEntryWithErrorMessage{ { name: "CannotContainBothSchemaAndContentInAParameter", contentTemplate: externalCannotContainBothSchemaAndContentInAParameter, errorMessage: &(&struct{ x string }{"cannot contain both schema and content in a parameter"}).x, testFunc: func(t *testing.T, doc *T) { }, }, } func TestLoadFromDataWithExternalRef(t *testing.T) { for _, td := range refTestDataEntries { t.Logf("testcase %q", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json")) loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) td.testFunc(t, doc) } } func TestLoadFromDataWithExternalRefResponseError(t *testing.T) { for _, td := range refTestDataEntriesResponseError { t.Logf("testcase %q", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "components.openapi.json")) loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.EqualError(t, err, *td.errorMessage) td.testFunc(t, doc) } } func TestLoadFromDataWithExternalNestedRef(t *testing.T) { for _, td := range refTestDataEntries { t.Logf("testcase %q", td.name) spec := []byte(fmt.Sprintf(td.contentTemplate, "nesteddir/nestedcomponents.openapi.json")) loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) td.testFunc(t, doc) } } const externalSchemaRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "schemas": { "TestSchema": { "$ref": "%s#/components/schemas/CustomTestSchema" } } } }` const externalResponseRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "responses": { "TestResponse": { "$ref": "%s#/components/responses/CustomTestResponse" } } } }` const externalParameterRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "parameters": { "TestParameter": { "$ref": "%s#/components/parameters/CustomTestParameter" } } } }` const externalExampleRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "examples": { "TestExample": { "$ref": "%s#/components/examples/CustomTestExample" } } } }` const externalRequestBodyRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "requestBodies": { "TestRequestBody": { "$ref": "%s#/components/requestBodies/CustomTestRequestBody" } } } }` const externalSecuritySchemeRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "securitySchemes": { "TestSecurityScheme": { "$ref": "%s#/components/securitySchemes/CustomTestSecurityScheme" } } } }` const externalHeaderRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "headers": { "TestHeader": { "$ref": "%s#/components/headers/CustomTestHeader" } } } }` const externalPathParameterRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test/{id}": { "parameters": [ { "$ref": "%s#/components/parameters/CustomTestParameter" } ] } } }` const externalPathOperationParameterRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test/{id}": { "get": { "responses": {}, "parameters": [ { "$ref": "%s#/components/parameters/CustomTestParameter" } ] } } } }` const externalPathOperationRequestBodyRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": {}, "requestBody": { "$ref": "%s#/components/requestBodies/CustomTestRequestBody" } } } } }` const externalPathOperationResponseRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": { "default": { "$ref": "%s#/components/responses/CustomTestResponse" } } } } } }` const externalPathOperationParameterSchemaRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test/{id}": { "get": { "responses": {}, "parameters": [ { "$ref": "#/components/parameters/CustomTestParameter" } ] } } }, "components": { "parameters": { "CustomTestParameter": { "name": "id", "in": "header", "schema": { "$ref": "%s#/components/schemas/CustomTestSchema" } } } } }` const externalPathOperationParameterWithContentInQueryTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test/{id}": { "get": { "responses": {}, "parameters": [ { "$ref": "#/components/parameters/CustomTestParameter" } ] } } }, "components": { "parameters": { "CustomTestParameter": { "content": { "application/json": { "schema": { "$ref": "%s#/components/schemas/CustomTestSchema" } } } } } } }` const externalCannotContainBothSchemaAndContentInAParameter = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test/{id}": { "get": { "responses": {}, "parameters": [ { "$ref": "#/components/parameters/CustomTestParameter" } ] } } }, "components": { "parameters": { "CustomTestParameter": { "content": { "application/json": { "schema": { "$ref": "%s#/components/schemas/CustomTestSchema" } } }, "schema": { "$ref": "%s#/components/schemas/CustomTestSchema" } } } } }` const externalPathOperationRequestBodyExampleRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": {}, "requestBody": { "$ref": "#/components/requestBodies/CustomTestRequestBody" } } } }, "components": { "requestBodies": { "CustomTestRequestBody": { "content": { "application/json": { "examples": { "application/json": { "$ref": "%s#/components/examples/CustomTestExample" } } } } } } } }` const externalPathOperationReqestBodyContentSchemaRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": {}, "requestBody": { "$ref": "#/components/requestBodies/CustomTestRequestBody" } } } }, "components": { "requestBodies": { "CustomTestRequestBody": { "content": { "application/json": { "schema": { "$ref": "%s#/components/schemas/CustomTestSchema" } } } } } } }` const externalPathOperationResponseExampleRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": { "default": { "$ref": "#/components/responses/CustomTestResponse" } } } } }, "components": { "responses": { "CustomTestResponse": { "description": "testdescription", "content": { "application/json": { "examples": { "application/json": { "$ref": "%s#/components/examples/CustomTestExample" } } } } } } } }` const externalPathOperationResponseSchemaRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": { "default": { "$ref": "#/components/responses/CustomTestResponse" } } } } }, "components": { "responses": { "CustomTestResponse": { "description": "testdescription", "content": { "application/json": { "schema": { "$ref": "%s#/components/schemas/CustomTestSchema" } } } } } } }` const externalComponentHeaderSchemaRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "headers": { "TestHeader": { "$ref": "#/components/headers/CustomTestHeader" }, "CustomTestHeader": { "name": "X-TEST-HEADER", "in": "header", "schema": { "$ref": "%s#/components/schemas/CustomTestSchema" } } } } }` const externalRequestResponseHeaderRefTemplate = ` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": { "default": { "description": "test", "headers": { "X-TEST-HEADER": { "$ref": "%s#/components/headers/CustomTestHeader" } } } } } } } }` // Relative Schema Documents Tests var relativeDocRefsTestDataEntries = []refTestDataEntry{ { name: "SchemaRef", contentTemplate: relativeSchemaDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Schemas["TestSchema"].Value.Type) require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type) }, }, { name: "ResponseRef", contentTemplate: relativeResponseDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { desc := "description" require.Equal(t, &desc, doc.Components.Responses["TestResponse"].Value.Description) }, }, { name: "ParameterRef", contentTemplate: relativeParameterDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Parameters["TestParameter"].Value.Name) require.Equal(t, "param", doc.Components.Parameters["TestParameter"].Value.Name) require.Equal(t, true, doc.Components.Parameters["TestParameter"].Value.Required) }, }, { name: "ExampleRef", contentTemplate: relativeExampleDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, "param", doc.Components.Examples["TestExample"].Value.Summary) require.NotNil(t, "param", doc.Components.Examples["TestExample"].Value.Value) require.Equal(t, "An example", doc.Components.Examples["TestExample"].Value.Summary) }, }, { name: "RequestRef", contentTemplate: relativeRequestDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, "param", doc.Components.RequestBodies["TestRequestBody"].Value.Description) require.Equal(t, "example request", doc.Components.RequestBodies["TestRequestBody"].Value.Description) }, }, { name: "HeaderRef", contentTemplate: relativeHeaderDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, "param", doc.Components.Headers["TestHeader"].Value.Description) require.Equal(t, "description", doc.Components.Headers["TestHeader"].Value.Description) }, }, { name: "HeaderRef", contentTemplate: relativeHeaderDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, "param", doc.Components.Headers["TestHeader"].Value.Description) require.Equal(t, "description", doc.Components.Headers["TestHeader"].Value.Description) }, }, { name: "SecuritySchemeRef", contentTemplate: relativeSecuritySchemeDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Type) require.NotNil(t, doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme) require.Equal(t, "http", doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Type) require.Equal(t, "basic", doc.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme) }, }, { name: "PathRef", contentTemplate: relativePathDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths["/pets"]) require.NotNil(t, doc.Paths["/pets"].Get.Responses["200"]) require.NotNil(t, doc.Paths["/pets"].Get.Responses["200"].Value.Content["application/json"]) }, }, } func TestLoadSpecWithRelativeDocumentRefs(t *testing.T) { for _, td := range relativeDocRefsTestDataEntries { t.Logf("testcase %q", td.name) spec := []byte(td.contentTemplate) loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/"}) require.NoError(t, err) td.testFunc(t, doc) } } const relativeSchemaDocsRefTemplate = ` openapi: 3.0.0 info: title: "" version: "1.0" paths: {} components: schemas: TestSchema: $ref: relativeDocs/CustomTestSchema.yml ` const relativeResponseDocsRefTemplate = ` openapi: 3.0.0 info: title: "" version: "1.0" paths: {} components: responses: TestResponse: $ref: relativeDocs/CustomTestResponse.yml ` const relativeParameterDocsRefTemplate = ` openapi: 3.0.0 info: title: "" version: "1.0" paths: {} components: parameters: TestParameter: $ref: relativeDocs/CustomTestParameter.yml ` const relativeExampleDocsRefTemplate = ` openapi: 3.0.0 info: title: "" version: "1.0" paths: {} components: examples: TestExample: $ref: relativeDocs/CustomTestExample.yml ` const relativeRequestDocsRefTemplate = ` openapi: 3.0.0 info: title: "" version: "1.0" paths: {} components: requestBodies: TestRequestBody: $ref: relativeDocs/CustomTestRequestBody.yml ` const relativeHeaderDocsRefTemplate = ` openapi: 3.0.0 info: title: "" version: "1.0" paths: {} components: headers: TestHeader: $ref: relativeDocs/CustomTestHeader.yml ` const relativeSecuritySchemeDocsRefTemplate = ` openapi: 3.0.0 info: title: "" version: "1.0" paths: {} components: securitySchemes: TestSecurityScheme: $ref: relativeDocs/CustomTestSecurityScheme.yml ` const relativePathDocsRefTemplate = ` openapi: 3.0.0 info: title: "" version: "2.0" paths: /pets: $ref: relativeDocs/CustomTestPath.yml ` func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/relativeDocsUseDocumentPath/openapi/openapi.yml") require.NoError(t, err) // path in nested directory // check parameter nestedDirPath := doc.Paths["/pets/{id}"] require.Equal(t, "param", nestedDirPath.Patch.Parameters[0].Value.Name) require.Equal(t, "path", nestedDirPath.Patch.Parameters[0].Value.In) require.Equal(t, true, nestedDirPath.Patch.Parameters[0].Value.Required) // check header require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description) require.Equal(t, "header1", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Another"].Value.Description) require.Equal(t, "header2", nestedDirPath.Patch.Responses["200"].Value.Headers["X-And-Another"].Value.Description) // check request body require.Equal(t, "example request", nestedDirPath.Patch.RequestBody.Value.Description) // check response schema and example require.Equal(t, nestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Schema.Value.Type, "string") expectedExample := "hello" require.Equal(t, expectedExample, nestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Examples["CustomTestExample"].Value.Value) // path in more nested directory // check parameter moreNestedDirPath := doc.Paths["/pets/{id}/{city}"] require.Equal(t, "param", moreNestedDirPath.Patch.Parameters[0].Value.Name) require.Equal(t, "path", moreNestedDirPath.Patch.Parameters[0].Value.In) require.Equal(t, true, moreNestedDirPath.Patch.Parameters[0].Value.Required) // check header require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description) require.Equal(t, "header1", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Another"].Value.Description) require.Equal(t, "header2", nestedDirPath.Patch.Responses["200"].Value.Headers["X-And-Another"].Value.Description) // check request body require.Equal(t, "example request", moreNestedDirPath.Patch.RequestBody.Value.Description) // check response schema and example require.Equal(t, "string", moreNestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Schema.Value.Type) require.Equal(t, moreNestedDirPath.Patch.Responses["200"].Value.Content["application/json"].Examples["CustomTestExample"].Value.Value, expectedExample) } kin-openapi-0.85.0/openapi3/loader_test.go000066400000000000000000000336141415236407200204030ustar00rootroot00000000000000package openapi3 import ( "errors" "fmt" "net" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/stretchr/testify/require" ) const addr = "localhost:7965" func TestLoadYAML(t *testing.T) { spec := []byte(` openapi: 3.0.0 info: title: An API version: v1 components: schemas: NewItem: required: [name] properties: name: {type: string} tag: {type: string} ErrorModel: type: object required: [code, message] properties: code: {type: integer} message: {type: string} paths: /items: put: description: '' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NewItem' responses: default: &defaultResponse # a YAML ref description: unexpected error content: application/json: schema: $ref: '#/components/schemas/ErrorModel' `) loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) require.Equal(t, "An API", doc.Info.Title) require.Equal(t, 2, len(doc.Components.Schemas)) require.Equal(t, 1, len(doc.Paths)) def := doc.Paths["/items"].Put.Responses.Default().Value desc := "unexpected error" require.Equal(t, &desc, def.Description) err = doc.Validate(loader.Context) require.NoError(t, err) } func ExampleLoader() { const source = `{"info":{"description":"An API"}}` doc, err := NewLoader().LoadFromData([]byte(source)) if err != nil { panic(err) } fmt.Print(doc.Info.Description) // Output: An API } func TestResolveSchemaRef(t *testing.T) { source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1",description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`) loader := NewLoader() doc, err := loader.LoadFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) refAVisited := doc.Components.Schemas["A"].Value.AllOf[0] require.Equal(t, "#/components/schemas/B", refAVisited.Ref) require.NotNil(t, refAVisited.Value) } func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) { source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{"/foo":{"post":{"requestBody":{"content":{"application/json":{"schema":null}}}}}}}`) loader := NewLoader() doc, err := loader.LoadFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) require.EqualError(t, err, `invalid paths: found unresolved ref: ""`) } func TestResolveResponseExampleRef(t *testing.T) { source := []byte(` openapi: 3.0.1 info: title: My API version: 1.0.0 components: examples: test: value: error: false paths: /: get: responses: 200: description: A test response content: application/json: examples: test: $ref: '#/components/examples/test'`) loader := NewLoader() doc, err := loader.LoadFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) example := doc.Paths["/"].Get.Responses.Get(200).Value.Content.Get("application/json").Examples["test"] require.NotNil(t, example.Value) require.Equal(t, example.Value.Value.(map[string]interface{})["error"].(bool), false) } func TestLoadErrorOnRefMisuse(t *testing.T) { spec := []byte(` openapi: '3.0.0' servers: [{url: /}] info: title: Some API version: '1' components: schemas: Thing: {type: string} paths: /items: put: description: '' requestBody: # Uses a schema ref instead of a requestBody ref. $ref: '#/components/schemas/Thing' responses: '201': description: '' content: application/json: schema: $ref: '#/components/schemas/Thing' `) loader := NewLoader() _, err := loader.LoadFromData(spec) require.Error(t, err) } func TestLoadPathParamRef(t *testing.T) { spec := []byte(` openapi: '3.0.0' info: title: '' version: '1' components: parameters: testParam: name: test in: query schema: type: string paths: '/': parameters: - $ref: '#/components/parameters/testParam' get: responses: '200': description: Test call. `) loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) require.NotNil(t, doc.Paths["/"].Parameters[0].Value) } func TestLoadRequestExampleRef(t *testing.T) { spec := []byte(` openapi: '3.0.0' info: title: '' version: '1' components: examples: test: value: hello: world paths: '/': post: requestBody: content: application/json: examples: test: $ref: "#/components/examples/test" responses: '200': description: Test call. `) loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) require.NotNil(t, doc.Paths["/"].Post.RequestBody.Value.Content.Get("application/json").Examples["test"]) } func createTestServer(t *testing.T, handler http.Handler) *httptest.Server { ts := httptest.NewUnstartedServer(handler) l, err := net.Listen("tcp", addr) require.NoError(t, err) ts.Listener.Close() ts.Listener = l return ts } func TestLoadFromRemoteURL(t *testing.T) { fs := http.FileServer(http.Dir("testdata")) ts := createTestServer(t, fs) ts.Start() defer ts.Close() loader := NewLoader() loader.IsExternalRefsAllowed = true url, err := url.Parse("http://" + addr + "/test.openapi.json") require.NoError(t, err) doc, err := loader.LoadFromURI(url) require.NoError(t, err) require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type) } func TestLoadWithReferenceInReference(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/refInRef/openapi.json") require.NoError(t, err) require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) require.Equal(t, "string", doc.Paths["/api/test/ref/in/ref"].Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) } func TestLoadFileWithExternalSchemaRef(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/testref.openapi.json") require.NoError(t, err) require.NotNil(t, doc.Components.Schemas["AnotherTestSchema"].Value.Type) } func TestLoadFileWithExternalSchemaRefSingleComponent(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/testrefsinglecomponent.openapi.json") require.NoError(t, err) require.NotNil(t, doc.Components.Responses["SomeResponse"]) desc := "this is a single response definition" require.Equal(t, &desc, doc.Components.Responses["SomeResponse"].Value.Description) } func TestLoadRequestResponseHeaderRef(t *testing.T) { spec := []byte(` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": { "default": { "description": "test", "headers": { "X-TEST-HEADER": { "$ref": "#/components/headers/TestHeader" } } } } } } }, "components": { "headers": { "TestHeader": { "description": "testheader" } } } }`) loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) require.Equal(t, "testheader", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) } func TestLoadFromDataWithExternalRequestResponseHeaderRemoteRef(t *testing.T) { spec := []byte(` { "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": { "/test": { "post": { "responses": { "default": { "description": "test", "headers": { "X-TEST-HEADER": { "$ref": "http://` + addr + `/components.openapi.json#/components/headers/CustomTestHeader" } } } } } } } }`) fs := http.FileServer(http.Dir("testdata")) ts := createTestServer(t, fs) ts.Start() defer ts.Close() loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromDataWithPath(spec, &url.URL{Path: "testdata/testfilename.openapi.json"}) require.NoError(t, err) require.NotNil(t, doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) require.Equal(t, "description", doc.Paths["/test"].Post.Responses["default"].Value.Headers["X-TEST-HEADER"].Value.Description) } func TestLoadYamlFile(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/test.openapi.yml") require.NoError(t, err) require.Equal(t, "OAI Specification in YAML", doc.Info.Title) } func TestLoadYamlFileWithExternalSchemaRef(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/testref.openapi.yml") require.NoError(t, err) require.NotNil(t, doc.Components.Schemas["AnotherTestSchema"].Value.Type) } func TestLoadYamlFileWithExternalPathRef(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/pathref.openapi.yml") require.NoError(t, err) require.NotNil(t, doc.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) require.Equal(t, "string", doc.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) } func TestResolveResponseLinkRef(t *testing.T) { source := []byte(` openapi: 3.0.1 info: title: My API version: 1.0.0 components: links: Father: description: link to to the father operationId: getUserById parameters: "id": "$response.body#/fatherId" paths: /users/{id}: get: operationId: getUserById, parameters: - name: id, in: path schema: type: string responses: 200: description: A test response content: application/json: links: father: $ref: '#/components/links/Father' `) loader := NewLoader() doc, err := loader.LoadFromData(source) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) response := doc.Paths[`/users/{id}`].Get.Responses.Get(200).Value link := response.Links[`father`].Value require.NotNil(t, link) require.Equal(t, "getUserById", link.OperationID) require.Equal(t, "link to to the father", link.Description) } func TestResolveNonComponentsRef(t *testing.T) { spec := []byte(` openapi: 3.0.0 info: title: An API version: v1 components: schemas: NewItem: required: [name] properties: name: {type: string} tag: {type: string} ErrorModel: type: object required: [code, message] properties: code: {type: integer} message: {type: string} paths: /items: put: description: '' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NewItem' responses: default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/ErrorModel' post: description: '' requestBody: required: true content: application/json: schema: $ref: '#/paths/~1items/put/requestBody/content/application~1json/schema' responses: default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/ErrorModel' `) loader := NewLoader() doc, err := loader.LoadFromData(spec) require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) } func TestServersVariables(t *testing.T) { const spec = ` openapi: 3.0.1 info: title: My API version: 1.0.0 paths: {} servers: - @@@ ` for value, expected := range map[string]error{ `{url: /}`: nil, `{url: "http://{x}.{y}.example.com"}`: errors.New("invalid servers: server has undeclared variables"), `{url: "http://{x}.y}.example.com"}`: errors.New("invalid servers: server URL has mismatched { and }"), `{url: "http://{x.example.com"}`: errors.New("invalid servers: server URL has mismatched { and }"), `{url: "http://{x}.example.com", variables: {x: {default: "www"}}}`: nil, `{url: "http://{x}.example.com", variables: {x: {default: "www", enum: ["www"]}}}`: nil, `{url: "http://{x}.example.com", variables: {x: {enum: ["www"]}}}`: errors.New(`invalid servers: field default is required in {"enum":["www"]}`), `{url: "http://www.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), `{url: "http://{y}.example.com", variables: {x: {enum: ["www"]}}}`: errors.New("invalid servers: server has undeclared variables"), } { t.Run(value, func(t *testing.T) { loader := NewLoader() doc, err := loader.LoadFromData([]byte(strings.Replace(spec, "@@@", value, 1))) require.NoError(t, err) err = doc.Validate(loader.Context) require.Equal(t, expected, err) }) } } kin-openapi-0.85.0/openapi3/media_type.go000066400000000000000000000047031415236407200202130ustar00rootroot00000000000000package openapi3 import ( "context" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) // MediaType is specified by OpenAPI/Swagger 3.0 standard. type MediaType struct { ExtensionProps Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` } var _ jsonpointer.JSONPointable = (*MediaType)(nil) func NewMediaType() *MediaType { return &MediaType{} } func (mediaType *MediaType) WithSchema(schema *Schema) *MediaType { if schema == nil { mediaType.Schema = nil } else { mediaType.Schema = &SchemaRef{Value: schema} } return mediaType } func (mediaType *MediaType) WithSchemaRef(schema *SchemaRef) *MediaType { mediaType.Schema = schema return mediaType } func (mediaType *MediaType) WithExample(name string, value interface{}) *MediaType { example := mediaType.Examples if example == nil { example = make(map[string]*ExampleRef) mediaType.Examples = example } example[name] = &ExampleRef{ Value: NewExample(value), } return mediaType } func (mediaType *MediaType) WithEncoding(name string, enc *Encoding) *MediaType { encoding := mediaType.Encoding if encoding == nil { encoding = make(map[string]*Encoding) mediaType.Encoding = encoding } encoding[name] = enc return mediaType } func (mediaType *MediaType) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(mediaType) } func (mediaType *MediaType) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, mediaType) } func (value *MediaType) Validate(ctx context.Context) error { if value == nil { return nil } if schema := value.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { return err } } return nil } func (mediaType MediaType) JSONLookup(token string) (interface{}, error) { switch token { case "schema": if mediaType.Schema != nil { if mediaType.Schema.Ref != "" { return &Ref{Ref: mediaType.Schema.Ref}, nil } return mediaType.Schema.Value, nil } case "example": return mediaType.Example, nil case "examples": return mediaType.Examples, nil case "encoding": return mediaType.Encoding, nil } v, _, err := jsonpointer.GetForToken(mediaType.ExtensionProps, token) return v, err } kin-openapi-0.85.0/openapi3/media_type_test.go000066400000000000000000000027361415236407200212560ustar00rootroot00000000000000package openapi3 import ( "context" "encoding/json" "testing" "github.com/stretchr/testify/require" ) func TestMediaTypeJSON(t *testing.T) { t.Log("Marshal *openapi3.MediaType to JSON") data, err := json.Marshal(mediaType()) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Unmarshal *openapi3.MediaType from JSON") docA := &MediaType{} err = json.Unmarshal(mediaTypeJSON, &docA) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Validate *openapi3.MediaType") err = docA.Validate(context.Background()) require.NoError(t, err) t.Log("Ensure representations match") dataA, err := json.Marshal(docA) require.NoError(t, err) require.JSONEq(t, string(data), string(mediaTypeJSON)) require.JSONEq(t, string(data), string(dataA)) } var mediaTypeJSON = []byte(` { "schema": { "description": "Some schema" }, "encoding": { "someEncoding": { "contentType": "application/xml; charset=utf-8" } }, "examples": { "someExample": { "value": { "name": "Some example" } } } } `) func mediaType() *MediaType { example := map[string]string{"name": "Some example"} return &MediaType{ Schema: &SchemaRef{ Value: &Schema{ Description: "Some schema", }, }, Encoding: map[string]*Encoding{ "someEncoding": { ContentType: "application/xml; charset=utf-8", }, }, Examples: map[string]*ExampleRef{ "someExample": { Value: NewExample(example), }, }, } } kin-openapi-0.85.0/openapi3/openapi3.go000066400000000000000000000052651415236407200176150ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "fmt" "github.com/getkin/kin-openapi/jsoninfo" ) // T is the root of an OpenAPI v3 document type T struct { ExtensionProps OpenAPI string `json:"openapi" yaml:"openapi"` // Required Components Components `json:"components,omitempty" yaml:"components,omitempty"` Info *Info `json:"info" yaml:"info"` // Required Paths Paths `json:"paths" yaml:"paths"` // Required Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } func (doc *T) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(doc) } func (doc *T) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, doc) } func (doc *T) AddOperation(path string, method string, operation *Operation) { paths := doc.Paths if paths == nil { paths = make(Paths) doc.Paths = paths } pathItem := paths[path] if pathItem == nil { pathItem = &PathItem{} paths[path] = pathItem } pathItem.SetOperation(method, operation) } func (doc *T) AddServer(server *Server) { doc.Servers = append(doc.Servers, server) } func (value *T) Validate(ctx context.Context) error { if value.OpenAPI == "" { return errors.New("value of openapi must be a non-empty string") } // NOTE: only mention info/components/paths/... key in this func's errors. { wrap := func(e error) error { return fmt.Errorf("invalid components: %v", e) } if err := value.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 err := v.Validate(ctx); err != nil { return wrap(err) } } else { return wrap(errors.New("must be an object")) } } { wrap := func(e error) error { return fmt.Errorf("invalid paths: %v", e) } if v := value.Paths; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } } else { return wrap(errors.New("must be an object")) } } { wrap := func(e error) error { return fmt.Errorf("invalid security: %v", e) } if v := value.Security; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } } } { wrap := func(e error) error { return fmt.Errorf("invalid servers: %v", e) } if v := value.Servers; v != nil { if err := v.Validate(ctx); err != nil { return wrap(err) } } } return nil } kin-openapi-0.85.0/openapi3/openapi3_test.go000066400000000000000000000224511415236407200206500ustar00rootroot00000000000000package openapi3 import ( "context" "encoding/json" "strings" "testing" "github.com/ghodss/yaml" "github.com/stretchr/testify/require" ) func TestRefsJSON(t *testing.T) { loader := NewLoader() t.Log("Marshal *T to JSON") data, err := json.Marshal(spec()) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Unmarshal *T from JSON") docA := &T{} err = json.Unmarshal(specJSON, &docA) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Resolve refs in unmarshalled *T") err = loader.ResolveRefsIn(docA, nil) require.NoError(t, err) t.Log("Resolve refs in marshalled *T") docB, err := loader.LoadFromData(data) require.NoError(t, err) require.NotEmpty(t, docB) t.Log("Validate *T") err = docA.Validate(loader.Context) require.NoError(t, err) err = docB.Validate(loader.Context) require.NoError(t, err) t.Log("Ensure representations match") dataA, err := json.Marshal(docA) require.NoError(t, err) dataB, err := json.Marshal(docB) require.NoError(t, err) require.JSONEq(t, string(data), string(specJSON)) require.JSONEq(t, string(data), string(dataA)) require.JSONEq(t, string(data), string(dataB)) } func TestRefsYAML(t *testing.T) { loader := NewLoader() t.Log("Marshal *T to YAML") data, err := yaml.Marshal(spec()) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Unmarshal *T from YAML") docA := &T{} err = yaml.Unmarshal(specYAML, &docA) require.NoError(t, err) require.NotEmpty(t, data) t.Log("Resolve refs in unmarshalled *T") err = loader.ResolveRefsIn(docA, nil) require.NoError(t, err) t.Log("Resolve refs in marshalled *T") docB, err := loader.LoadFromData(data) require.NoError(t, err) require.NotEmpty(t, docB) t.Log("Validate *T") err = docA.Validate(loader.Context) require.NoError(t, err) err = docB.Validate(loader.Context) require.NoError(t, err) t.Log("Ensure representations match") dataA, err := yaml.Marshal(docA) require.NoError(t, err) dataB, err := yaml.Marshal(docB) require.NoError(t, err) eqYAML(t, data, specYAML) eqYAML(t, data, dataA) eqYAML(t, data, dataB) } func eqYAML(t *testing.T, expected, actual []byte) { var e, a interface{} err := yaml.Unmarshal(expected, &e) require.NoError(t, err) err = yaml.Unmarshal(actual, &a) require.NoError(t, err) require.Equal(t, e, a) } var specYAML = []byte(` openapi: '3.0' info: title: MyAPI version: '0.1' paths: "/hello": parameters: - "$ref": "#/components/parameters/someParameter" post: parameters: - "$ref": "#/components/parameters/someParameter" requestBody: "$ref": "#/components/requestBodies/someRequestBody" responses: '200': "$ref": "#/components/responses/someResponse" components: parameters: someParameter: description: Some parameter name: example in: query schema: "$ref": "#/components/schemas/someSchema" requestBodies: someRequestBody: description: Some request body responses: someResponse: description: Some response schemas: someSchema: description: Some schema headers: otherHeader: schema: {type: string} someHeader: "$ref": "#/components/headers/otherHeader" examples: otherExample: value: name: Some example someExample: "$ref": "#/components/examples/otherExample" securitySchemes: otherSecurityScheme: description: Some security scheme type: apiKey in: query name: token someSecurityScheme: "$ref": "#/components/securitySchemes/otherSecurityScheme" `) var specJSON = []byte(` { "openapi": "3.0", "info": { "title": "MyAPI", "version": "0.1" }, "paths": { "/hello": { "parameters": [ { "$ref": "#/components/parameters/someParameter" } ], "post": { "parameters": [ { "$ref": "#/components/parameters/someParameter" } ], "requestBody": { "$ref": "#/components/requestBodies/someRequestBody" }, "responses": { "200": { "$ref": "#/components/responses/someResponse" } } } } }, "components": { "parameters": { "someParameter": { "description": "Some parameter", "name": "example", "in": "query", "schema": { "$ref": "#/components/schemas/someSchema" } } }, "requestBodies": { "someRequestBody": { "description": "Some request body" } }, "responses": { "someResponse": { "description": "Some response" } }, "schemas": { "someSchema": { "description": "Some schema" } }, "headers": { "otherHeader": { "schema": { "type": "string" } }, "someHeader": { "$ref": "#/components/headers/otherHeader" } }, "examples": { "otherExample": { "value": { "name": "Some example" } }, "someExample": { "$ref": "#/components/examples/otherExample" } }, "securitySchemes": { "otherSecurityScheme": { "description": "Some security scheme", "type": "apiKey", "in": "query", "name": "token" }, "someSecurityScheme": { "$ref": "#/components/securitySchemes/otherSecurityScheme" } } } } `) func spec() *T { parameter := &Parameter{ Description: "Some parameter", Name: "example", In: "query", Schema: &SchemaRef{ Ref: "#/components/schemas/someSchema", }, } requestBody := &RequestBody{ Description: "Some request body", } responseDescription := "Some response" response := &Response{ Description: &responseDescription, } schema := &Schema{ Description: "Some schema", } example := map[string]string{"name": "Some example"} return &T{ OpenAPI: "3.0", Info: &Info{ Title: "MyAPI", Version: "0.1", }, Paths: Paths{ "/hello": &PathItem{ Post: &Operation{ Parameters: Parameters{ { Ref: "#/components/parameters/someParameter", Value: parameter, }, }, RequestBody: &RequestBodyRef{ Ref: "#/components/requestBodies/someRequestBody", Value: requestBody, }, Responses: Responses{ "200": &ResponseRef{ Ref: "#/components/responses/someResponse", Value: response, }, }, }, Parameters: Parameters{ { Ref: "#/components/parameters/someParameter", Value: parameter, }, }, }, }, Components: Components{ Parameters: map[string]*ParameterRef{ "someParameter": { Value: parameter, }, }, RequestBodies: map[string]*RequestBodyRef{ "someRequestBody": { Value: requestBody, }, }, Responses: map[string]*ResponseRef{ "someResponse": { Value: response, }, }, Schemas: map[string]*SchemaRef{ "someSchema": { Value: schema, }, }, Headers: map[string]*HeaderRef{ "someHeader": { Ref: "#/components/headers/otherHeader", }, "otherHeader": { Value: &Header{Parameter{Schema: &SchemaRef{Value: NewStringSchema()}}}, }, }, Examples: map[string]*ExampleRef{ "someExample": { Ref: "#/components/examples/otherExample", }, "otherExample": { Value: NewExample(example), }, }, SecuritySchemes: map[string]*SecuritySchemeRef{ "someSecurityScheme": { Ref: "#/components/securitySchemes/otherSecurityScheme", }, "otherSecurityScheme": { Value: &SecurityScheme{ Description: "Some security scheme", Type: "apiKey", In: "query", Name: "token", }, }, }, }, } } func TestValidation(t *testing.T) { info := ` info: title: "Hello World REST APIs" version: "1.0" ` paths := ` paths: "/api/v2/greetings.json": get: operationId: listGreetings responses: 200: description: "List different greetings" "/api/v2/greetings/{id}.json": parameters: - name: id in: path required: true schema: type: string example: "greeting" get: operationId: showGreeting responses: 200: description: "Get a single greeting object" ` spec := ` openapi: 3.0.2 ` + info + paths + ` components: schemas: GreetingObject: properties: id: type: string type: type: string default: "greeting" attributes: properties: description: type: string ` tests := map[string]string{ spec: "", strings.Replace(spec, `openapi: 3.0.2`, ``, 1): "value of openapi must be a non-empty string", strings.Replace(spec, `openapi: 3.0.2`, `openapi: ''`, 1): "value of openapi must be a non-empty string", strings.Replace(spec, info, ``, 1): "invalid info: must be an object", strings.Replace(spec, paths, ``, 1): "invalid paths: must be an object", } for spec, expectedErr := range tests { t.Run(expectedErr, func(t *testing.T) { doc := &T{} err := yaml.Unmarshal([]byte(spec), &doc) require.NoError(t, err) err = doc.Validate(context.Background()) if expectedErr != "" { require.EqualError(t, err, expectedErr) } else { require.NoError(t, err) } }) } } kin-openapi-0.85.0/openapi3/operation.go000066400000000000000000000073441415236407200200770ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "strconv" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) // Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard. type Operation struct { ExtensionProps // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` // Optional short summary. Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` // Optional description. Should use CommonMark syntax. Description string `json:"description,omitempty" yaml:"description,omitempty"` // Optional operation ID. OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` // Optional parameters. Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` // Optional body parameter. RequestBody *RequestBodyRef `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` // Responses. Responses Responses `json:"responses" yaml:"responses"` // Required // Optional callbacks Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` // Optional security requirements that overrides top-level security. Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` // Optional servers that overrides top-level servers. Servers *Servers `json:"servers,omitempty" yaml:"servers,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } var _ jsonpointer.JSONPointable = (*Operation)(nil) func NewOperation() *Operation { return &Operation{} } func (operation *Operation) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(operation) } func (operation *Operation) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, operation) } func (operation Operation) JSONLookup(token string) (interface{}, error) { switch token { case "requestBody": if operation.RequestBody != nil { if operation.RequestBody.Ref != "" { return &Ref{Ref: operation.RequestBody.Ref}, nil } return operation.RequestBody.Value, nil } case "tags": return operation.Tags, nil case "summary": return operation.Summary, nil case "description": return operation.Description, nil case "operationID": return operation.OperationID, nil case "parameters": return operation.Parameters, nil case "responses": return operation.Responses, nil case "callbacks": return operation.Callbacks, nil case "deprecated": return operation.Deprecated, nil case "security": return operation.Security, nil case "servers": return operation.Servers, nil case "externalDocs": return operation.ExternalDocs, nil } v, _, err := jsonpointer.GetForToken(operation.ExtensionProps, token) return v, err } func (operation *Operation) AddParameter(p *Parameter) { operation.Parameters = append(operation.Parameters, &ParameterRef{ Value: p, }) } func (operation *Operation) AddResponse(status int, response *Response) { responses := operation.Responses if responses == nil { responses = NewResponses() operation.Responses = responses } code := "default" if status != 0 { code = strconv.FormatInt(int64(status), 10) } responses[code] = &ResponseRef{ Value: response, } } func (value *Operation) Validate(ctx context.Context) error { if v := value.Parameters; v != nil { if err := v.Validate(ctx); err != nil { return err } } if v := value.RequestBody; v != nil { if err := v.Validate(ctx); err != nil { return err } } if v := value.Responses; v != nil { if err := v.Validate(ctx); err != nil { return err } } else { return errors.New("value of responses must be an object") } return nil } kin-openapi-0.85.0/openapi3/operation_test.go000066400000000000000000000034011415236407200211240ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "testing" "github.com/stretchr/testify/require" ) var operation *Operation func initOperation() { operation = NewOperation() operation.Description = "Some description" operation.Summary = "Some summary" operation.Tags = []string{"tag1", "tag2"} } func TestAddParameter(t *testing.T) { initOperation() operation.AddParameter(NewQueryParameter("param1")) operation.AddParameter(NewCookieParameter("param2")) require.Equal(t, "param1", operation.Parameters.GetByInAndName("query", "param1").Name) require.Equal(t, "param2", operation.Parameters.GetByInAndName("cookie", "param2").Name) } func TestAddResponse(t *testing.T) { initOperation() operation.AddResponse(200, NewResponse()) operation.AddResponse(400, NewResponse()) require.NotNil(t, "status 200", operation.Responses.Get(200).Value) require.NotNil(t, "status 400", operation.Responses.Get(400).Value) } func operationWithoutResponses() *Operation { initOperation() return operation } func operationWithResponses() *Operation { initOperation() operation.AddResponse(200, NewResponse().WithDescription("some response")) return operation } func TestOperationValidation(t *testing.T) { tests := []struct { name string input *Operation expectedError error }{ { "when no Responses object is provided", operationWithoutResponses(), errors.New("value of responses must be an object"), }, { "when a Responses object is provided", operationWithResponses(), nil, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { c := context.Background() validationErr := test.input.Validate(c) require.Equal(t, test.expectedError, validationErr, "expected errors (or lack of) to match") }) } } kin-openapi-0.85.0/openapi3/parameter.go000066400000000000000000000214261415236407200200540ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "fmt" "strconv" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) type ParametersMap map[string]*ParameterRef var _ jsonpointer.JSONPointable = (*ParametersMap)(nil) func (p ParametersMap) JSONLookup(token string) (interface{}, error) { ref, ok := p[token] if ref == nil || ok == false { return nil, fmt.Errorf("object has no field %q", token) } if ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } // Parameters is specified by OpenAPI/Swagger 3.0 standard. type Parameters []*ParameterRef var _ jsonpointer.JSONPointable = (*Parameters)(nil) func (p Parameters) JSONLookup(token string) (interface{}, error) { index, err := strconv.Atoi(token) if err != nil { return nil, err } if index < 0 || index >= len(p) { return nil, fmt.Errorf("index %d out of bounds of array of length %d", index, len(p)) } ref := p[index] if ref != nil && ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } func NewParameters() Parameters { return make(Parameters, 0, 4) } func (parameters Parameters) GetByInAndName(in string, name string) *Parameter { for _, item := range parameters { if v := item.Value; v != nil { if v.Name == name && v.In == in { return v } } } return nil } func (value Parameters) Validate(ctx context.Context) error { dupes := make(map[string]struct{}) for _, item := range value { if v := item.Value; v != nil { key := v.In + ":" + v.Name if _, ok := dupes[key]; ok { return fmt.Errorf("more than one %q parameter has name %q", v.In, v.Name) } dupes[key] = struct{}{} } if err := item.Validate(ctx); err != nil { return err } } return nil } // Parameter is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#parameterObject type Parameter struct { ExtensionProps Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Style string `json:"style,omitempty" yaml:"style,omitempty"` Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` Content Content `json:"content,omitempty" yaml:"content,omitempty"` } var _ jsonpointer.JSONPointable = (*Parameter)(nil) const ( ParameterInPath = "path" ParameterInQuery = "query" ParameterInHeader = "header" ParameterInCookie = "cookie" ) func NewPathParameter(name string) *Parameter { return &Parameter{ Name: name, In: ParameterInPath, Required: true, } } func NewQueryParameter(name string) *Parameter { return &Parameter{ Name: name, In: ParameterInQuery, } } func NewHeaderParameter(name string) *Parameter { return &Parameter{ Name: name, In: ParameterInHeader, } } func NewCookieParameter(name string) *Parameter { return &Parameter{ Name: name, In: ParameterInCookie, } } func (parameter *Parameter) WithDescription(value string) *Parameter { parameter.Description = value return parameter } func (parameter *Parameter) WithRequired(value bool) *Parameter { parameter.Required = value return parameter } func (parameter *Parameter) WithSchema(value *Schema) *Parameter { if value == nil { parameter.Schema = nil } else { parameter.Schema = &SchemaRef{ Value: value, } } return parameter } func (parameter *Parameter) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(parameter) } func (parameter *Parameter) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, parameter) } func (value Parameter) JSONLookup(token string) (interface{}, error) { switch token { case "schema": if value.Schema != nil { if value.Schema.Ref != "" { return &Ref{Ref: value.Schema.Ref}, nil } return value.Schema.Value, nil } case "name": return value.Name, nil case "in": return value.In, nil case "description": return value.Description, nil case "style": return value.Style, nil case "explode": return value.Explode, nil case "allowEmptyValue": return value.AllowEmptyValue, nil case "allowReserved": return value.AllowReserved, nil case "deprecated": return value.Deprecated, nil case "required": return value.Required, nil case "example": return value.Example, nil case "examples": return value.Examples, nil case "content": return value.Content, nil } v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token) return v, err } // SerializationMethod returns a parameter's serialization method. // When a parameter's serialization method is not defined the method returns // the default serialization method corresponding to a parameter's location. func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error) { switch parameter.In { case ParameterInPath, ParameterInHeader: style := parameter.Style if style == "" { style = SerializationSimple } explode := false if parameter.Explode != nil { explode = *parameter.Explode } return &SerializationMethod{Style: style, Explode: explode}, nil case ParameterInQuery, ParameterInCookie: style := parameter.Style if style == "" { style = SerializationForm } explode := true if parameter.Explode != nil { explode = *parameter.Explode } return &SerializationMethod{Style: style, Explode: explode}, nil default: return nil, fmt.Errorf("unexpected parameter's 'in': %q", parameter.In) } } func (value *Parameter) Validate(ctx context.Context) error { if value.Name == "" { return errors.New("parameter name can't be blank") } in := value.In switch in { case ParameterInPath, ParameterInQuery, ParameterInHeader, ParameterInCookie: default: return fmt.Errorf("parameter can't have 'in' value %q", value.In) } // Validate a parameter's serialization method. sm, err := value.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: 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) } if (value.Schema == nil) == (value.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) } if schema := value.Schema; schema != nil { if err := schema.Validate(ctx); err != nil { return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, err) } } if content := value.Content; content != nil { if err := content.Validate(ctx); err != nil { return fmt.Errorf("parameter %q content is invalid: %v", value.Name, err) } } return nil } kin-openapi-0.85.0/openapi3/parameter_issue223_test.go000066400000000000000000000050701415236407200225470ustar00rootroot00000000000000package openapi3 import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestPathParametersMatchPath(t *testing.T) { spec := ` openapi: "3.0.0" info: version: 1.0.0 title: Swagger Petstore license: name: MIT servers: - url: http://petstore.swagger.io/v1 paths: /pets: get: summary: List all pets operationId: listPets tags: - pets responses: '200': description: A paged array of pets headers: x-next: description: A link to the next page of responses schema: type: string content: application/json: schema: $ref: "#/components/schemas/Pets" default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error" post: summary: Create a pet operationId: createPets tags: - pets responses: '201': description: Null response default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error" /pets/{petId}: get: summary: Info for a specific pet operationId: showPetById tags: - pets # <------------------ no parameters responses: '200': description: Expected response to a valid request content: application/json: schema: $ref: "#/components/schemas/Pet" default: description: unexpected error content: application/json: schema: $ref: "#/components/schemas/Error" components: schemas: Pet: type: object required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string Pets: type: array items: $ref: "#/components/schemas/Pet" Error: type: object required: - code - message properties: code: type: integer format: int32 message: type: string ` doc, err := NewLoader().LoadFromData([]byte(spec)) require.NoError(t, err) err = doc.Validate(context.Background()) require.EqualError(t, err, `invalid paths: operation GET /pets/{petId} must define exactly all path parameters (missing: [petId])`) } kin-openapi-0.85.0/openapi3/path_item.go000066400000000000000000000070531415236407200200460ustar00rootroot00000000000000package openapi3 import ( "context" "fmt" "net/http" "github.com/getkin/kin-openapi/jsoninfo" ) type PathItem struct { ExtensionProps Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Connect *Operation `json:"connect,omitempty" yaml:"connect,omitempty"` Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` Trace *Operation `json:"trace,omitempty" yaml:"trace,omitempty"` Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` } func (pathItem *PathItem) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(pathItem) } func (pathItem *PathItem) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, pathItem) } func (pathItem *PathItem) Operations() map[string]*Operation { operations := make(map[string]*Operation, 4) if v := pathItem.Connect; v != nil { operations[http.MethodConnect] = v } if v := pathItem.Delete; v != nil { operations[http.MethodDelete] = v } if v := pathItem.Get; v != nil { operations[http.MethodGet] = v } if v := pathItem.Head; v != nil { operations[http.MethodHead] = v } if v := pathItem.Options; v != nil { operations[http.MethodOptions] = v } if v := pathItem.Patch; v != nil { operations[http.MethodPatch] = v } if v := pathItem.Post; v != nil { operations[http.MethodPost] = v } if v := pathItem.Put; v != nil { operations[http.MethodPut] = v } if v := pathItem.Trace; v != nil { operations[http.MethodTrace] = v } return operations } func (pathItem *PathItem) GetOperation(method string) *Operation { switch method { case http.MethodConnect: return pathItem.Connect case http.MethodDelete: return pathItem.Delete case http.MethodGet: return pathItem.Get case http.MethodHead: return pathItem.Head case http.MethodOptions: return pathItem.Options case http.MethodPatch: return pathItem.Patch case http.MethodPost: return pathItem.Post case http.MethodPut: return pathItem.Put case http.MethodTrace: return pathItem.Trace default: panic(fmt.Errorf("unsupported HTTP method %q", method)) } } func (pathItem *PathItem) SetOperation(method string, operation *Operation) { switch method { case http.MethodConnect: pathItem.Connect = operation case http.MethodDelete: pathItem.Delete = operation case http.MethodGet: pathItem.Get = operation case http.MethodHead: pathItem.Head = operation case http.MethodOptions: pathItem.Options = operation case http.MethodPatch: pathItem.Patch = operation case http.MethodPost: pathItem.Post = operation case http.MethodPut: pathItem.Put = operation case http.MethodTrace: pathItem.Trace = operation default: panic(fmt.Errorf("unsupported HTTP method %q", method)) } } func (value *PathItem) Validate(ctx context.Context) error { for _, operation := range value.Operations() { if err := operation.Validate(ctx); err != nil { return err } } return nil } kin-openapi-0.85.0/openapi3/paths.go000066400000000000000000000077071415236407200172210ustar00rootroot00000000000000package openapi3 import ( "context" "fmt" "strings" ) // Paths is specified by OpenAPI/Swagger standard version 3.0. type Paths map[string]*PathItem func (value Paths) Validate(ctx context.Context) error { normalizedPaths := make(map[string]string) for path, pathItem := range value { 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] } normalizedPath, _, varsInPath := normalizeTemplatedPath(path) if oldPath, ok := normalizedPaths[normalizedPath]; ok { return fmt.Errorf("conflicting paths %q and %q", path, oldPath) } normalizedPaths[path] = path var commonParams []string for _, parameterRef := range pathItem.Parameters { if parameterRef != nil { if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath { commonParams = append(commonParams, parameter.Name) } } } for method, operation := range pathItem.Operations() { var setParams []string for _, parameterRef := range operation.Parameters { if parameterRef != nil { if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath { setParams = append(setParams, parameter.Name) } } } if expected := len(setParams) + len(commonParams); expected != len(varsInPath) { expected -= len(varsInPath) if expected < 0 { expected *= -1 } missing := make(map[string]struct{}, expected) definedParams := append(setParams, commonParams...) for _, name := range definedParams { if _, ok := varsInPath[name]; !ok { missing[name] = struct{}{} } } for name := range varsInPath { got := false for _, othername := range definedParams { if othername == name { got = true break } } if !got { missing[name] = struct{}{} } } if len(missing) != 0 { missings := make([]string, 0, len(missing)) for name := range missing { missings = append(missings, name) } return fmt.Errorf("operation %s %s must define exactly all path parameters (missing: %v)", method, path, missings) } } } if err := pathItem.Validate(ctx); err != nil { return err } } return nil } // Find returns a path that matches the key. // // The method ignores differences in template variable names (except possible "*" suffix). // // For example: // // paths := openapi3.Paths { // "/person/{personName}": &openapi3.PathItem{}, // } // pathItem := path.Find("/person/{name}") // // would return the correct path item. func (paths Paths) Find(key string) *PathItem { // Try directly access the map pathItem := paths[key] if pathItem != nil { return pathItem } normalizedPath, expected, _ := normalizeTemplatedPath(key) for path, pathItem := range paths { pathNormalized, got, _ := normalizeTemplatedPath(path) if got == expected && pathNormalized == normalizedPath { return pathItem } } return nil } func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) { if strings.IndexByte(path, '{') < 0 { return path, 0, nil } var buffTpl strings.Builder buffTpl.Grow(len(path)) var ( cc rune count uint isVariable bool vars = make(map[string]struct{}) buffVar strings.Builder ) for i, c := range path { if isVariable { if c == '}' { // End path variable isVariable = false vars[buffVar.String()] = struct{}{} buffVar = strings.Builder{} // First append possible '*' before this character // The character '}' will be appended if i > 0 && cc == '*' { buffTpl.WriteRune(cc) } } else { buffVar.WriteRune(c) continue } } else if c == '{' { // Begin path variable isVariable = true // The character '{' will be appended count++ } // Append the character buffTpl.WriteRune(c) cc = c } return buffTpl.String(), count, vars } kin-openapi-0.85.0/openapi3/paths_test.go000066400000000000000000000007271415236407200202530ustar00rootroot00000000000000package openapi3 import ( "context" "testing" "github.com/stretchr/testify/require" ) var emptyPathSpec = ` openapi: "3.0.0" info: version: 1.0.0 title: Swagger Petstore license: name: MIT servers: - url: http://petstore.swagger.io/v1 paths: /pets: ` func TestPathValidate(t *testing.T) { doc, err := NewLoader().LoadFromData([]byte(emptyPathSpec)) require.NoError(t, err) err = doc.Paths.Validate(context.Background()) require.NoError(t, err) } kin-openapi-0.85.0/openapi3/race_test.go000066400000000000000000000007141415236407200200420ustar00rootroot00000000000000package openapi3_test import ( "context" "testing" "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) func TestRaceyPatternSchema(t *testing.T) { schema := openapi3.Schema{ Pattern: "^test|for|race|condition$", Type: "string", } err := schema.Validate(context.Background()) require.NoError(t, err) visit := func() { err := schema.VisitJSONString("test") require.NoError(t, err) } go visit() visit() } kin-openapi-0.85.0/openapi3/refs.go000066400000000000000000000147221415236407200170340ustar00rootroot00000000000000package openapi3 import ( "context" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) // Ref is specified by OpenAPI/Swagger 3.0 standard. type Ref struct { Ref string `json:"$ref" yaml:"$ref"` } type CallbackRef struct { Ref string Value *Callback } var _ jsonpointer.JSONPointable = (*CallbackRef)(nil) func (value *CallbackRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } func (value *CallbackRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } func (value *CallbackRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) } return foundUnresolvedRef(value.Ref) } func (value CallbackRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil } ptr, _, err := jsonpointer.GetForToken(value.Value, token) return ptr, err } type ExampleRef struct { Ref string Value *Example } var _ jsonpointer.JSONPointable = (*ExampleRef)(nil) func (value *ExampleRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } func (value *ExampleRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } func (value *ExampleRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) } return foundUnresolvedRef(value.Ref) } func (value ExampleRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil } ptr, _, err := jsonpointer.GetForToken(value.Value, token) return ptr, err } type HeaderRef struct { Ref string Value *Header } var _ jsonpointer.JSONPointable = (*HeaderRef)(nil) func (value *HeaderRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } func (value *HeaderRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } func (value *HeaderRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) } return foundUnresolvedRef(value.Ref) } func (value HeaderRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil } ptr, _, err := jsonpointer.GetForToken(value.Value, token) return ptr, err } type LinkRef struct { Ref string Value *Link } func (value *LinkRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } func (value *LinkRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } func (value *LinkRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) } return foundUnresolvedRef(value.Ref) } type ParameterRef struct { Ref string Value *Parameter } var _ jsonpointer.JSONPointable = (*ParameterRef)(nil) func (value *ParameterRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } func (value *ParameterRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } func (value *ParameterRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) } return foundUnresolvedRef(value.Ref) } func (value ParameterRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil } ptr, _, err := jsonpointer.GetForToken(value.Value, token) return ptr, err } type ResponseRef struct { Ref string Value *Response } var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) func (value *ResponseRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } func (value *ResponseRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } func (value *ResponseRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) } return foundUnresolvedRef(value.Ref) } func (value ResponseRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil } ptr, _, err := jsonpointer.GetForToken(value.Value, token) return ptr, err } type RequestBodyRef struct { Ref string Value *RequestBody } var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) func (value *RequestBodyRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } func (value *RequestBodyRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } func (value *RequestBodyRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) } return foundUnresolvedRef(value.Ref) } func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil } ptr, _, err := jsonpointer.GetForToken(value.Value, token) return ptr, err } type SchemaRef struct { Ref string Value *Schema } var _ jsonpointer.JSONPointable = (*SchemaRef)(nil) func NewSchemaRef(ref string, value *Schema) *SchemaRef { return &SchemaRef{ Ref: ref, Value: value, } } func (value *SchemaRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } func (value *SchemaRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } func (value *SchemaRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) } return foundUnresolvedRef(value.Ref) } func (value SchemaRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil } ptr, _, err := jsonpointer.GetForToken(value.Value, token) return ptr, err } type SecuritySchemeRef struct { Ref string Value *SecurityScheme } var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil) func (value *SecuritySchemeRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } func (value *SecuritySchemeRef) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) } func (value *SecuritySchemeRef) Validate(ctx context.Context) error { if v := value.Value; v != nil { return v.Validate(ctx) } return foundUnresolvedRef(value.Ref) } func (value SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { return value.Ref, nil } ptr, _, err := jsonpointer.GetForToken(value.Value, token) return ptr, err } kin-openapi-0.85.0/openapi3/refs_test.go000066400000000000000000000151201415236407200200640ustar00rootroot00000000000000package openapi3 import ( "reflect" "testing" "github.com/go-openapi/jsonpointer" "github.com/stretchr/testify/require" ) func TestIssue222(t *testing.T) { spec := ` openapi: 3.0.0 info: version: 1.0.0 title: Swagger Petstore license: name: MIT servers: - url: 'http://petstore.swagger.io/v1' paths: /pets: get: summary: List all pets operationId: listPets tags: - pets parameters: - name: limit in: query description: How many items to return at one time (max 100) required: false schema: type: integer format: int32 responses: '200': # <--------------- PANIC HERE post: summary: Create a pet operationId: createPets tags: - pets responses: '201': description: Null response default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error' '/pets/{petId}': get: summary: Info for a specific pet operationId: showPetById tags: - pets parameters: - name: petId in: path required: true description: The id of the pet to retrieve schema: type: string responses: '200': description: Expected response to a valid request content: application/json: schema: $ref: '#/components/schemas/Pet' default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: Pet: type: object required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string Pets: type: array items: $ref: '#/components/schemas/Pet' Error: type: object required: - code - message properties: code: type: integer format: int32 message: type: string ` _, err := NewLoader().LoadFromData([]byte(spec)) require.EqualError(t, err, `invalid response: value MUST be an object`) } func TestIssue247(t *testing.T) { spec := `openapi: 3.0.2 info: title: Swagger Petstore - OpenAPI 3.0 license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html version: 1.0.5 servers: - url: /api/v3 tags: - name: pet description: Everything about your Pets externalDocs: description: Find out more url: http://swagger.io - name: store description: Operations about user - name: user description: Access to Petstore orders externalDocs: description: Find out more about our store url: http://swagger.io paths: /pet: put: tags: - pet summary: Update an existing pet description: Update an existing pet by Id operationId: updatePet requestBody: description: Update an existent pet in the store content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Pet' required: true responses: "200": description: Successful operation content: application/xml: schema: $ref: '#/components/schemas/Pet' application/json: schema: $ref: '#/components/schemas/Pet' "400": description: Invalid ID supplied "404": description: Pet not found "405": description: Validation exception security: - petstore_auth: - write:pets - read:pets components: schemas: Pet: type: object required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string Pets: type: array items: $ref: '#/components/schemas/Pet' Error: type: object required: - code - message properties: code: type: integer format: int32 message: type: string OneOfTest: type: object oneOf: - type: string - type: integer format: int32 ` root, err := NewLoader().LoadFromData([]byte(spec)) require.NoError(t, err) ptr, err := jsonpointer.New("/paths/~1pet/put/responses/200/content") require.NoError(t, err) v, kind, err := ptr.Get(root) require.NoError(t, err) require.Equal(t, reflect.TypeOf(Content{}).Kind(), kind) require.IsType(t, Content{}, v) ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content/application~1json/schema") require.NoError(t, err) v, kind, err = ptr.Get(root) require.NoError(t, err) require.Equal(t, reflect.Ptr, kind) require.IsType(t, &Ref{}, v) require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) ptr, err = jsonpointer.New("/components/schemas/Pets/items") require.NoError(t, err) v, kind, err = ptr.Get(root) require.NoError(t, err) require.Equal(t, reflect.Ptr, kind) require.IsType(t, &Ref{}, v) require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) ptr, err = jsonpointer.New("/components/schemas/Error/properties/code") require.NoError(t, err) v, kind, err = ptr.Get(root) require.NoError(t, err) require.Equal(t, reflect.Ptr, kind) require.IsType(t, &Schema{}, v) require.Equal(t, "integer", v.(*Schema).Type) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/0") require.NoError(t, err) v, kind, err = ptr.Get(root) require.NoError(t, err) require.Equal(t, reflect.Ptr, kind) require.IsType(t, &Schema{}, v) require.Equal(t, "string", v.(*Schema).Type) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/1") require.NoError(t, err) v, kind, err = ptr.Get(root) require.NoError(t, err) require.Equal(t, reflect.Ptr, kind) require.IsType(t, &Schema{}, v) require.Equal(t, "integer", v.(*Schema).Type) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/5") require.NoError(t, err) _, _, err = ptr.Get(root) require.Error(t, err) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/-1") require.NoError(t, err) _, _, err = ptr.Get(root) require.Error(t, err) } kin-openapi-0.85.0/openapi3/request_body.go000066400000000000000000000055001415236407200205740ustar00rootroot00000000000000package openapi3 import ( "context" "fmt" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) type RequestBodies map[string]*RequestBodyRef var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) func (r RequestBodies) JSONLookup(token string) (interface{}, error) { ref, ok := r[token] if ok == false { return nil, fmt.Errorf("object has no field %q", token) } if ref != nil && ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } // RequestBody is specified by OpenAPI/Swagger 3.0 standard. type RequestBody struct { ExtensionProps Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` Content Content `json:"content,omitempty" yaml:"content,omitempty"` } func NewRequestBody() *RequestBody { return &RequestBody{} } func (requestBody *RequestBody) WithDescription(value string) *RequestBody { requestBody.Description = value return requestBody } func (requestBody *RequestBody) WithRequired(value bool) *RequestBody { requestBody.Required = value return requestBody } func (requestBody *RequestBody) WithContent(content Content) *RequestBody { requestBody.Content = content return requestBody } func (requestBody *RequestBody) WithSchemaRef(value *SchemaRef, consumes []string) *RequestBody { requestBody.Content = NewContentWithSchemaRef(value, consumes) return requestBody } func (requestBody *RequestBody) WithSchema(value *Schema, consumes []string) *RequestBody { requestBody.Content = NewContentWithSchema(value, consumes) return requestBody } func (requestBody *RequestBody) WithJSONSchemaRef(value *SchemaRef) *RequestBody { requestBody.Content = NewContentWithJSONSchemaRef(value) return requestBody } func (requestBody *RequestBody) WithJSONSchema(value *Schema) *RequestBody { requestBody.Content = NewContentWithJSONSchema(value) return requestBody } func (requestBody *RequestBody) WithFormDataSchemaRef(value *SchemaRef) *RequestBody { requestBody.Content = NewContentWithFormDataSchemaRef(value) return requestBody } func (requestBody *RequestBody) WithFormDataSchema(value *Schema) *RequestBody { requestBody.Content = NewContentWithFormDataSchema(value) return requestBody } func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType { m := requestBody.Content if m == nil { return nil } return m[mediaType] } func (requestBody *RequestBody) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(requestBody) } 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 { if err := v.Validate(ctx); err != nil { return err } } return nil } kin-openapi-0.85.0/openapi3/response.go000066400000000000000000000052151415236407200177300ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "fmt" "strconv" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) // Responses is specified by OpenAPI/Swagger 3.0 standard. type Responses map[string]*ResponseRef var _ jsonpointer.JSONPointable = (*Responses)(nil) func NewResponses() Responses { r := make(Responses) r["default"] = &ResponseRef{Value: NewResponse().WithDescription("")} return r } func (responses Responses) Default() *ResponseRef { return responses["default"] } 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 { return errors.New("the responses object MUST contain at least one response code") } for _, v := range value { if err := v.Validate(ctx); err != nil { return err } } return nil } func (responses Responses) JSONLookup(token string) (interface{}, error) { ref, ok := responses[token] if ok == false { return nil, fmt.Errorf("invalid token reference: %q", token) } if ref != nil && ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } // Response is specified by OpenAPI/Swagger 3.0 standard. type Response struct { ExtensionProps Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` Content Content `json:"content,omitempty" yaml:"content,omitempty"` Links Links `json:"links,omitempty" yaml:"links,omitempty"` } func NewResponse() *Response { return &Response{} } func (response *Response) WithDescription(value string) *Response { response.Description = &value return response } func (response *Response) WithContent(content Content) *Response { response.Content = content return response } func (response *Response) WithJSONSchema(schema *Schema) *Response { response.Content = NewContentWithJSONSchema(schema) return response } func (response *Response) WithJSONSchemaRef(schema *SchemaRef) *Response { response.Content = NewContentWithJSONSchemaRef(schema) return response } func (response *Response) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(response) } func (response *Response) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, response) } func (value *Response) Validate(ctx context.Context) error { if value.Description == nil { return errors.New("a short description of the response is required") } if content := value.Content; content != nil { if err := content.Validate(ctx); err != nil { return err } } return nil } kin-openapi-0.85.0/openapi3/response_issue224_test.go000066400000000000000000000345341415236407200224350ustar00rootroot00000000000000package openapi3 import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestEmptyResponsesAreInvalid(t *testing.T) { spec := `{ "openapi": "3.0.0", "servers": [ { "url": "http://petstore.swagger.io/v2" } ], "info": { "description": ":dog: :cat: :rabbit: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key to test the authorization filters.", "version": "1.0.0", "title": "Swagger Petstore", "termsOfService": "http://swagger.io/terms/", "contact": { "email": "apiteam@swagger.io" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" } }, "tags": [ { "name": "pet", "description": "Everything about your Pets", "externalDocs": { "description": "Find out more", "url": "http://swagger.io" } }, { "name": "store", "description": "Access to Petstore orders" }, { "name": "user", "description": "Operations about user", "externalDocs": { "description": "Find out more about our store", "url": "http://swagger.io" } } ], "paths": { "/pet": { "post": { "tags": [ "pet" ], "summary": "Add a new pet to the store", "description": "", "operationId": "addPet", "responses": { }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "requestBody": { "$ref": "#/components/requestBodies/Pet" }, "parameters": [] }, "put": { "tags": [ "pet" ], "summary": "Update an existing pet", "description": "", "operationId": "updatePet", "responses": { }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "requestBody": { "$ref": "#/components/requestBodies/Pet" }, "parameters": [] } }, "/pet/{petId}": { "get": { "tags": [ "pet" ], "summary": "Find pet by ID", "description": "Returns a single pet", "operationId": "getPetById", "parameters": [ { "name": "petId", "in": "path", "description": "ID of pet to return", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { }, "security": [ { "api_key": [] } ] }, "post": { "tags": [ "pet" ], "summary": "Updates a pet in the store with form data", "description": "", "operationId": "updatePetWithForm", "parameters": [ { "name": "petId", "in": "path", "description": "ID of pet that needs to be updated", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "requestBody": { "content": { "application/x-www-form-urlencoded": { "schema": { "type": "object", "properties": { "name": { "description": "Updated name of the pet", "type": "string" }, "status": { "description": "Updated status of the pet", "type": "string" } } } } } } }, "delete": { "tags": [ "pet" ], "summary": "Deletes a pet", "description": "", "operationId": "deletePet", "parameters": [ { "name": "api_key", "in": "header", "required": false, "schema": { "type": "string" } }, { "name": "petId", "in": "path", "description": "Pet id to delete", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] } } }, "externalDocs": { "description": "See AsyncAPI example", "url": "https://mermade.github.io/shins/asyncapi.html" }, "components": { "schemas": { "Order": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "petId": { "type": "integer", "format": "int64" }, "quantity": { "type": "integer", "format": "int32" }, "shipDate": { "type": "string", "format": "date-time" }, "status": { "type": "string", "description": "Order Status", "enum": [ "placed", "approved", "delivered" ] }, "complete": { "type": "boolean", "default": false } }, "xml": { "name": "Order" } }, "Category": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" } }, "xml": { "name": "Category" } }, "User": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "username": { "type": "string" }, "firstName": { "type": "string" }, "lastName": { "type": "string" }, "email": { "type": "string" }, "password": { "type": "string" }, "phone": { "type": "string" }, "userStatus": { "type": "integer", "format": "int32", "description": "User Status" } }, "xml": { "name": "User" } }, "Tag": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" } }, "xml": { "name": "Tag" } }, "Pet": { "type": "object", "required": [ "name", "photoUrls" ], "properties": { "id": { "type": "integer", "format": "int64" }, "category": { "$ref": "#/components/schemas/Category" }, "name": { "type": "string", "example": "doggie" }, "photoUrls": { "type": "array", "xml": { "name": "photoUrl", "wrapped": true }, "items": { "type": "string" } }, "tags": { "type": "array", "xml": { "name": "tag", "wrapped": true }, "items": { "$ref": "#/components/schemas/Tag" } }, "status": { "type": "string", "description": "pet status in the store", "enum": [ "available", "pending", "sold" ] } }, "xml": { "name": "Pet" } }, "ApiResponse": { "type": "object", "properties": { "code": { "type": "integer", "format": "int32" }, "type": { "type": "string" }, "message": { "type": "string" } } } }, "requestBodies": { "Pet": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } } }, "description": "Pet object that needs to be added to the store", "required": true }, "UserArray": { "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/User" } } } }, "description": "List of user object", "required": true } }, "securitySchemes": { "petstore_auth": { "type": "oauth2", "flows": { "implicit": { "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", "scopes": { "write:pets": "modify pets in your account", "read:pets": "read your pets" } } } }, "api_key": { "type": "apiKey", "name": "api_key", "in": "header" } }, "links": {}, "callbacks": {} }, "security": [] } ` doc, err := NewLoader().LoadFromData([]byte(spec)) require.NoError(t, err) require.Equal(t, doc.ExternalDocs.Description, "See AsyncAPI example") err = doc.Validate(context.Background()) require.EqualError(t, err, `invalid paths: the responses object MUST contain at least one response code`) } kin-openapi-0.85.0/openapi3/schema.go000066400000000000000000001150371415236407200173360ustar00rootroot00000000000000package 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" ) const ( TypeArray = "array" TypeBoolean = "boolean" TypeInteger = "integer" TypeNumber = "number" TypeObject = "object" TypeString = "string" ) 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") ) // Float64Ptr is a helper for defining OpenAPI schemas. func Float64Ptr(value float64) *float64 { return &value } // BoolPtr is a helper for defining OpenAPI schemas. func BoolPtr(value bool) *bool { return &value } // Int64Ptr is a helper for defining OpenAPI schemas. func Int64Ptr(value int64) *int64 { return &value } // Uint64Ptr is a helper for defining OpenAPI schemas. func Uint64Ptr(value uint64) *uint64 { return &value } type Schemas map[string]*SchemaRef var _ jsonpointer.JSONPointable = (*Schemas)(nil) func (s Schemas) JSONLookup(token string) (interface{}, error) { ref, ok := s[token] if ref == nil || ok == false { return nil, fmt.Errorf("object has no field %q", token) } if ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } type SchemaRefs []*SchemaRef var _ jsonpointer.JSONPointable = (*SchemaRefs)(nil) func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { i, err := strconv.ParseUint(token, 10, 64) if err != nil { return nil, err } if i >= uint64(len(s)) { return nil, fmt.Errorf("index out of range: %d", i) } ref := s[i] if ref == nil || ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } // Schema is specified by OpenAPI/Swagger 3.0 standard. type Schema struct { ExtensionProps OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Title string `json:"title,omitempty" yaml:"title,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` // Array-related, here for struct compactness UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` // Number-related, here for struct compactness ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` // Properties Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"` Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` // Number Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` Max *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` // String MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` compiledPattern *regexp.Regexp // Array MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` // Object Required []string `json:"required,omitempty" yaml:"required,omitempty"` Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` AdditionalPropertiesAllowed *bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // In this order... AdditionalProperties *SchemaRef `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // ...for multijson Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` } var _ jsonpointer.JSONPointable = (*Schema)(nil) func NewSchema() *Schema { return &Schema{} } func (schema *Schema) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(schema) } func (schema *Schema) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, schema) } func (schema Schema) JSONLookup(token string) (interface{}, error) { switch token { case "additionalProperties": if schema.AdditionalProperties != nil { if schema.AdditionalProperties.Ref != "" { return &Ref{Ref: schema.AdditionalProperties.Ref}, nil } return schema.AdditionalProperties.Value, nil } case "not": if schema.Not != nil { if schema.Not.Ref != "" { return &Ref{Ref: schema.Not.Ref}, nil } return schema.Not.Value, nil } case "items": if schema.Items != nil { if schema.Items.Ref != "" { return &Ref{Ref: schema.Items.Ref}, nil } return schema.Items.Value, nil } case "oneOf": return schema.OneOf, nil case "anyOf": return schema.AnyOf, nil case "allOf": return schema.AllOf, nil case "type": return schema.Type, nil case "title": return schema.Title, nil case "format": return schema.Format, nil case "description": return schema.Description, nil case "enum": return schema.Enum, nil case "default": return schema.Default, nil case "example": return schema.Example, nil case "externalDocs": return schema.ExternalDocs, nil case "additionalPropertiesAllowed": return schema.AdditionalPropertiesAllowed, nil case "uniqueItems": return schema.UniqueItems, nil case "exclusiveMin": return schema.ExclusiveMin, nil case "exclusiveMax": return schema.ExclusiveMax, nil case "nullable": return schema.Nullable, nil case "readOnly": return schema.ReadOnly, nil case "writeOnly": return schema.WriteOnly, nil case "allowEmptyValue": return schema.AllowEmptyValue, nil case "xml": return schema.XML, nil case "deprecated": return schema.Deprecated, nil case "min": return schema.Min, nil case "max": return schema.Max, nil case "multipleOf": return schema.MultipleOf, nil case "minLength": return schema.MinLength, nil case "maxLength": return schema.MaxLength, nil case "pattern": return schema.Pattern, nil case "minItems": return schema.MinItems, nil case "maxItems": return schema.MaxItems, nil case "required": return schema.Required, nil case "properties": return schema.Properties, nil case "minProps": return schema.MinProps, nil case "maxProps": return schema.MaxProps, nil case "discriminator": return schema.Discriminator, nil } v, _, err := jsonpointer.GetForToken(schema.ExtensionProps, token) return v, err } func (schema *Schema) NewRef() *SchemaRef { return &SchemaRef{ Value: schema, } } func NewOneOfSchema(schemas ...*Schema) *Schema { refs := make([]*SchemaRef, 0, len(schemas)) for _, schema := range schemas { refs = append(refs, &SchemaRef{Value: schema}) } return &Schema{ OneOf: refs, } } func NewAnyOfSchema(schemas ...*Schema) *Schema { refs := make([]*SchemaRef, 0, len(schemas)) for _, schema := range schemas { refs = append(refs, &SchemaRef{Value: schema}) } return &Schema{ AnyOf: refs, } } func NewAllOfSchema(schemas ...*Schema) *Schema { refs := make([]*SchemaRef, 0, len(schemas)) for _, schema := range schemas { refs = append(refs, &SchemaRef{Value: schema}) } return &Schema{ AllOf: refs, } } func NewBoolSchema() *Schema { return &Schema{ Type: TypeBoolean, } } func NewFloat64Schema() *Schema { return &Schema{ Type: TypeNumber, } } func NewIntegerSchema() *Schema { return &Schema{ Type: TypeInteger, } } func NewInt32Schema() *Schema { return &Schema{ Type: TypeInteger, Format: "int32", } } func NewInt64Schema() *Schema { return &Schema{ Type: TypeInteger, Format: "int64", } } func NewStringSchema() *Schema { return &Schema{ Type: TypeString, } } func NewDateTimeSchema() *Schema { return &Schema{ Type: TypeString, Format: "date-time", } } func NewUUIDSchema() *Schema { return &Schema{ Type: TypeString, Format: "uuid", } } func NewBytesSchema() *Schema { return &Schema{ Type: TypeString, Format: "byte", } } func NewArraySchema() *Schema { return &Schema{ Type: TypeArray, } } func NewObjectSchema() *Schema { return &Schema{ Type: TypeObject, Properties: make(Schemas), } } func (schema *Schema) WithNullable() *Schema { schema.Nullable = true return schema } func (schema *Schema) WithMin(value float64) *Schema { schema.Min = &value return schema } func (schema *Schema) WithMax(value float64) *Schema { schema.Max = &value return schema } func (schema *Schema) WithExclusiveMin(value bool) *Schema { schema.ExclusiveMin = value return schema } func (schema *Schema) WithExclusiveMax(value bool) *Schema { schema.ExclusiveMax = value return schema } func (schema *Schema) WithEnum(values ...interface{}) *Schema { schema.Enum = values return schema } func (schema *Schema) WithDefault(defaultValue interface{}) *Schema { schema.Default = defaultValue return schema } func (schema *Schema) WithFormat(value string) *Schema { schema.Format = value return schema } func (schema *Schema) WithLength(i int64) *Schema { n := uint64(i) schema.MinLength = n schema.MaxLength = &n return schema } func (schema *Schema) WithMinLength(i int64) *Schema { n := uint64(i) schema.MinLength = n return schema } func (schema *Schema) WithMaxLength(i int64) *Schema { n := uint64(i) schema.MaxLength = &n return schema } func (schema *Schema) WithLengthDecodedBase64(i int64) *Schema { n := uint64(i) v := (n*8 + 5) / 6 schema.MinLength = v schema.MaxLength = &v return schema } func (schema *Schema) WithMinLengthDecodedBase64(i int64) *Schema { n := uint64(i) schema.MinLength = (n*8 + 5) / 6 return schema } func (schema *Schema) WithMaxLengthDecodedBase64(i int64) *Schema { n := uint64(i) schema.MinLength = (n*8 + 5) / 6 return schema } func (schema *Schema) WithPattern(pattern string) *Schema { schema.Pattern = pattern schema.compiledPattern = nil return schema } func (schema *Schema) WithItems(value *Schema) *Schema { schema.Items = &SchemaRef{ Value: value, } return schema } func (schema *Schema) WithMinItems(i int64) *Schema { n := uint64(i) schema.MinItems = n return schema } func (schema *Schema) WithMaxItems(i int64) *Schema { n := uint64(i) schema.MaxItems = &n return schema } func (schema *Schema) WithUniqueItems(unique bool) *Schema { schema.UniqueItems = unique return schema } func (schema *Schema) WithProperty(name string, propertySchema *Schema) *Schema { return schema.WithPropertyRef(name, &SchemaRef{ Value: propertySchema, }) } func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema { properties := schema.Properties if properties == nil { properties = make(Schemas) schema.Properties = properties } properties[name] = ref return schema } func (schema *Schema) WithProperties(properties map[string]*Schema) *Schema { result := make(Schemas, len(properties)) for k, v := range properties { result[k] = &SchemaRef{ Value: v, } } schema.Properties = result return schema } func (schema *Schema) WithMinProperties(i int64) *Schema { n := uint64(i) schema.MinProps = n return schema } func (schema *Schema) WithMaxProperties(i int64) *Schema { n := uint64(i) schema.MaxProps = &n return schema } func (schema *Schema) WithAnyAdditionalProperties() *Schema { schema.AdditionalProperties = nil t := true schema.AdditionalPropertiesAllowed = &t return schema } func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema { if v == nil { schema.AdditionalProperties = nil } else { schema.AdditionalProperties = &SchemaRef{ Value: v, } } return schema } func (schema *Schema) IsEmpty() bool { if schema.Type != "" || schema.Format != "" || len(schema.Enum) != 0 || schema.UniqueItems || schema.ExclusiveMin || schema.ExclusiveMax || schema.Nullable || schema.ReadOnly || schema.WriteOnly || schema.AllowEmptyValue || schema.Min != nil || schema.Max != nil || schema.MultipleOf != nil || schema.MinLength != 0 || schema.MaxLength != nil || schema.Pattern != "" || schema.MinItems != 0 || schema.MaxItems != nil || len(schema.Required) != 0 || schema.MinProps != 0 || schema.MaxProps != nil { return false } if n := schema.Not; n != nil && !n.Value.IsEmpty() { return false } if ap := schema.AdditionalProperties; ap != nil && !ap.Value.IsEmpty() { return false } if apa := schema.AdditionalPropertiesAllowed; apa != nil && !*apa { return false } if items := schema.Items; items != nil && !items.Value.IsEmpty() { return false } for _, s := range schema.Properties { if !s.Value.IsEmpty() { return false } } for _, s := range schema.OneOf { if !s.Value.IsEmpty() { return false } } for _, s := range schema.AnyOf { if !s.Value.IsEmpty() { return false } } for _, s := range schema.AllOf { if !s.Value.IsEmpty() { return false } } 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 TypeBoolean: case TypeNumber: if format := schema.Format; len(format) > 0 { switch format { case "float", "double": default: if !SchemaFormatValidationDisabled { return unsupportedFormat(format) } } } case TypeInteger: if format := schema.Format; len(format) > 0 { switch format { case "int32", "int64": default: if !SchemaFormatValidationDisabled { return unsupportedFormat(format) } } } case TypeString: 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 TypeArray: if schema.Items == nil { return errors.New("when schema type is 'array', schema 'items' must be non-null") } case TypeObject: 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) case map[interface{}]interface{}: // for YAML cf. issue #444 values := make(map[string]interface{}, len(value)) for key, v := range value { if k, ok := key.(string); ok { values[k] = v } } if len(value) == len(values) { return schema.visitJSONObject(settings, values) } } 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(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 != TypeBoolean { return schema.expectedType(settings, TypeBoolean) } 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 != TypeNumber { 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 != TypeString { return schema.expectedType(settings, TypeString) } 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 "%s"`, 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 "%s")`, 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 != TypeArray { return schema.expectedType(settings, TypeArray) } 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 != TypeObject { return schema.expectedType(settings, TypeObject) } 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 } var _ interface{ Unwrap() error } = SchemaError{} 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 (err SchemaError) Unwrap() error { return err.Origin } 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) } kin-openapi-0.85.0/openapi3/schema_formats.go000066400000000000000000000051701415236407200210650ustar00rootroot00000000000000package openapi3 import ( "fmt" "net" "regexp" "strings" ) 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 type Format struct { regexp *regexp.Regexp callback FormatCallback } //SchemaStringFormats allows for validating strings format var SchemaStringFormats = make(map[string]Format, 8) //DefineStringFormat Defines a new regexp pattern for a given format func DefineStringFormat(name string, pattern string) { re, err := regexp.Compile(pattern) if err != nil { err := fmt.Errorf("format %q has invalid pattern %q: %v", name, pattern, err) panic(err) } SchemaStringFormats[name] = Format{regexp: re} } // DefineStringFormatCallback adds a validation function for a specific schema format entry func DefineStringFormatCallback(name string, callback FormatCallback) { SchemaStringFormats[name] = Format{callback: callback} } func validateIP(ip string) error { parsed := net.ParseIP(ip) if parsed == nil { return &SchemaError{ Value: ip, Reason: "Not an IP address", } } return nil } func validateIPv4(ip string) error { if err := validateIP(ip); err != nil { return err } if !(strings.Count(ip, ":") < 2) { return &SchemaError{ Value: ip, Reason: "Not an IPv4 address (it's IPv6)", } } return nil } func validateIPv6(ip string) error { if err := validateIP(ip); err != nil { return err } if !(strings.Count(ip, ":") >= 2) { return &SchemaError{ Value: ip, Reason: "Not an IPv6 address (it's IPv4)", } } return nil } func init() { // This pattern catches only some suspiciously wrong-looking email addresses. // Use DefineStringFormat(...) if you need something stricter. DefineStringFormat("email", `^[^@]+@[^@<>",\s]+$`) // Base64 // The pattern supports base64 and b./ase64url. Padding ('=') is supported. DefineStringFormat("byte", `(^$|^[a-zA-Z0-9+/\-_]*=*$)`) // date DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`) // date-time DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`) } // DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec func DefineIPv4Format() { DefineStringFormatCallback("ipv4", validateIPv4) } // DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec func DefineIPv6Format() { DefineStringFormatCallback("ipv6", validateIPv6) } kin-openapi-0.85.0/openapi3/schema_formats_test.go000066400000000000000000000026101415236407200221200ustar00rootroot00000000000000package openapi3 import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestIssue430(t *testing.T) { schema := NewOneOfSchema( NewStringSchema().WithFormat("ipv4"), NewStringSchema().WithFormat("ipv6"), ) err := schema.Validate(context.Background()) require.NoError(t, err) data := map[string]bool{ "127.0.1.1": true, // https://stackoverflow.com/a/48519490/1418165 // v4 "192.168.0.1": true, // "192.168.0.1:80" doesn't parse per net.ParseIP() // v6 "::FFFF:C0A8:1": false, "::FFFF:C0A8:0001": false, "0000:0000:0000:0000:0000:FFFF:C0A8:1": false, // "::FFFF:C0A8:1%1" doesn't parse per net.ParseIP() "::FFFF:192.168.0.1": false, // "[::FFFF:C0A8:1]:80" doesn't parse per net.ParseIP() // "[::FFFF:C0A8:1%1]:80" doesn't parse per net.ParseIP() } for datum := range data { err = schema.VisitJSON(datum) require.Error(t, err, ErrOneOfConflict.Error()) } DefineIPv4Format() DefineIPv6Format() for datum, isV4 := range data { err = schema.VisitJSON(datum) require.NoError(t, err) if isV4 { require.Nil(t, validateIPv4(datum), "%q should be IPv4", datum) require.NotNil(t, validateIPv6(datum), "%q should not be IPv6", datum) } else { require.NotNil(t, validateIPv4(datum), "%q should not be IPv4", datum) require.Nil(t, validateIPv6(datum), "%q should be IPv6", datum) } } } kin-openapi-0.85.0/openapi3/schema_issue289_test.go000066400000000000000000000020061415236407200220370ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) func TestIssue289(t *testing.T) { spec := []byte(`components: schemas: Server: properties: address: oneOf: - $ref: "#/components/schemas/ip-address" - $ref: "#/components/schemas/domain-name" name: type: string type: object domain-name: maxLength: 10 minLength: 5 pattern: "((([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.)*([a-zA-Z0-9_]([a-zA-Z0-9\\-_]){0,61})?[a-zA-Z0-9]\\.?)|\\." type: string ip-address: pattern: "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$" type: string openapi: "3.0.1" `) s, err := NewLoader().LoadFromData(spec) require.NoError(t, err) err = s.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ "name": "kin-openapi", "address": "127.0.0.1", }) require.EqualError(t, err, ErrOneOfConflict.Error()) } kin-openapi-0.85.0/openapi3/schema_oneOf_test.go000066400000000000000000000076251415236407200215260ustar00rootroot00000000000000package openapi3 import ( "testing" "github.com/stretchr/testify/require" ) var oneofSpec = []byte(`components: schemas: Cat: type: object properties: name: type: string scratches: type: boolean $type: type: string enum: - cat required: - name - scratches - $type Dog: type: object properties: name: type: string barks: type: boolean $type: type: string enum: - dog required: - name - barks - $type Animal: type: object oneOf: - $ref: "#/components/schemas/Cat" - $ref: "#/components/schemas/Dog" discriminator: propertyName: $type mapping: cat: "#/components/schemas/Cat" dog: "#/components/schemas/Dog" `) var oneofNoDiscriminatorSpec = []byte(`components: schemas: Cat: type: object properties: name: type: string scratches: type: boolean required: - name - scratches Dog: type: object properties: name: type: string barks: type: boolean required: - name - barks Animal: type: object oneOf: - $ref: "#/components/schemas/Cat" - $ref: "#/components/schemas/Dog" `) func TestVisitJSON_OneOf_MissingDiscriptorProperty(t *testing.T) { s, err := NewLoader().LoadFromData(oneofSpec) require.NoError(t, err) err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ "name": "snoopy", }) require.EqualError(t, err, "input does not contain the discriminator property") } func TestVisitJSON_OneOf_MissingDiscriptorValue(t *testing.T) { s, err := NewLoader().LoadFromData(oneofSpec) require.NoError(t, err) err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ "name": "snoopy", "$type": "snake", }) require.EqualError(t, err, "input does not contain a valid discriminator value") } func TestVisitJSON_OneOf_MissingField(t *testing.T) { s, err := NewLoader().LoadFromData(oneofSpec) require.NoError(t, err) err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ "name": "snoopy", "$type": "dog", }) require.EqualError(t, err, "Error at \"/barks\": property \"barks\" is missing\nSchema:\n {\n \"properties\": {\n \"$type\": {\n \"enum\": [\n \"dog\"\n ],\n \"type\": \"string\"\n },\n \"barks\": {\n \"type\": \"boolean\"\n },\n \"name\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"barks\",\n \"$type\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"$type\": \"dog\",\n \"name\": \"snoopy\"\n }\n") } func TestVisitJSON_OneOf_NoDiscriptor_MissingField(t *testing.T) { s, err := NewLoader().LoadFromData(oneofNoDiscriminatorSpec) require.NoError(t, err) err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ "name": "snoopy", }) require.EqualError(t, err, "doesn't match schema due to: Error at \"/scratches\": property \"scratches\" is missing\nSchema:\n {\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"scratches\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"name\",\n \"scratches\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"name\": \"snoopy\"\n }\n Or Error at \"/barks\": property \"barks\" is missing\nSchema:\n {\n \"properties\": {\n \"barks\": {\n \"type\": \"boolean\"\n },\n \"name\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"barks\"\n ],\n \"type\": \"object\"\n }\n\nValue:\n {\n \"name\": \"snoopy\"\n }\n") } kin-openapi-0.85.0/openapi3/schema_test.go000066400000000000000000000577761415236407200204140ustar00rootroot00000000000000package openapi3 import ( "context" "encoding/base64" "encoding/json" "fmt" "math" "reflect" "strings" "testing" "github.com/stretchr/testify/require" ) type schemaExample struct { Title string Schema *Schema Serialization interface{} AllValid []interface{} AllInvalid []interface{} } func TestSchemas(t *testing.T) { DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) for _, example := range schemaExamples { t.Run(example.Title, testSchema(t, example)) } } func testSchema(t *testing.T, example schemaExample) func(*testing.T) { return func(t *testing.T) { schema := example.Schema if serialized := example.Serialization; serialized != nil { jsonSerialized, err := json.Marshal(serialized) require.NoError(t, err) jsonSchema, err := json.Marshal(schema) require.NoError(t, err) require.JSONEq(t, string(jsonSerialized), string(jsonSchema)) var dataUnserialized Schema err = json.Unmarshal(jsonSerialized, &dataUnserialized) require.NoError(t, err) var dataSchema Schema err = json.Unmarshal(jsonSchema, &dataSchema) require.NoError(t, err) require.Equal(t, dataUnserialized, dataSchema) } for _, value := range example.AllValid { err := validateSchema(t, schema, value) require.NoError(t, err) } for _, value := range example.AllInvalid { err := validateSchema(t, schema, value) require.Error(t, err) } // NaN and Inf aren't valid JSON but are handled for _, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} { err := schema.VisitJSON(value) require.Error(t, err) } } } func validateSchema(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error { data, err := json.Marshal(value) require.NoError(t, err) var val interface{} err = json.Unmarshal(data, &val) require.NoError(t, err) return schema.VisitJSON(val, opts...) } var schemaExamples = []schemaExample{ { Title: "EMPTY SCHEMA", Schema: &Schema{}, Serialization: map[string]interface{}{ // This OA3 schema is exactly this draft-04 schema: // {"not": {"type": "null"}} }, AllValid: []interface{}{ false, true, 3.14, "", []interface{}{}, map[string]interface{}{}, }, AllInvalid: []interface{}{ nil, }, }, { Title: "JUST NULLABLE", Schema: NewSchema().WithNullable(), Serialization: map[string]interface{}{ // This OA3 schema is exactly both this draft-04 schema: {} and: // {anyOf: [type:string, type:number, type:integer, type:boolean // ,{type:array, items:{}}, type:object]} "nullable": true, }, AllValid: []interface{}{ nil, false, true, 0, 0.0, 3.14, "", []interface{}{}, map[string]interface{}{}, }, }, { Title: "NULLABLE BOOLEAN", Schema: NewBoolSchema().WithNullable(), Serialization: map[string]interface{}{ "nullable": true, "type": "boolean", }, AllValid: []interface{}{ nil, false, true, }, AllInvalid: []interface{}{ 0, 0.0, 3.14, "", []interface{}{}, map[string]interface{}{}, }, }, { Title: "NULLABLE ANYOF", Schema: NewAnyOfSchema( NewIntegerSchema(), NewFloat64Schema(), ).WithNullable(), Serialization: map[string]interface{}{ "nullable": true, "anyOf": []interface{}{ map[string]interface{}{"type": "integer"}, map[string]interface{}{"type": "number"}, }, }, AllValid: []interface{}{ nil, 42, 4.2, }, AllInvalid: []interface{}{ true, []interface{}{42}, "bla", map[string]interface{}{}, }, }, { Title: "BOOLEAN", Schema: NewBoolSchema(), Serialization: map[string]interface{}{ "type": "boolean", }, AllValid: []interface{}{ false, true, }, AllInvalid: []interface{}{ nil, 3.14, "", []interface{}{}, map[string]interface{}{}, }, }, { Title: "NUMBER", Schema: NewFloat64Schema(). WithMin(2.5). WithMax(3.5), Serialization: map[string]interface{}{ "type": "number", "minimum": 2.5, "maximum": 3.5, }, AllValid: []interface{}{ 2.5, 3.14, 3.5, }, AllInvalid: []interface{}{ nil, false, true, 2.4, 3.6, "", []interface{}{}, map[string]interface{}{}, }, }, { Title: "INTEGER", Schema: NewInt64Schema(). WithMin(2). WithMax(5), Serialization: map[string]interface{}{ "type": "integer", "format": "int64", "minimum": 2, "maximum": 5, }, AllValid: []interface{}{ 2, 5, }, AllInvalid: []interface{}{ nil, false, true, 1, 6, 3.5, "", []interface{}{}, map[string]interface{}{}, }, }, { Title: "STRING", Schema: NewStringSchema(). WithMinLength(2). WithMaxLength(3). WithPattern("^[abc]+$"), Serialization: map[string]interface{}{ "type": "string", "minLength": 2, "maxLength": 3, "pattern": "^[abc]+$", }, AllValid: []interface{}{ "ab", "abc", }, AllInvalid: []interface{}{ nil, false, true, 3.14, "a", "xy", "aaaa", []interface{}{}, map[string]interface{}{}, }, }, { Title: "STRING: optional format 'uuid'", Schema: NewUUIDSchema(), Serialization: map[string]interface{}{ "type": "string", "format": "uuid", }, AllValid: []interface{}{ "dd7d8481-81a3-407f-95f0-a2f1cb382a4b", "dcba3901-2fba-48c1-9db2-00422055804e", "ace8e3be-c254-4c10-8859-1401d9a9d52a", }, AllInvalid: []interface{}{ nil, "g39840b1-d0ef-446d-e555-48fcca50a90a", "4cf3i040-ea14-4daa-b0b5-ea9329473519", "aaf85740-7e27-4b4f-b4554-a03a43b1f5e3", "56f5bff4-z4b6-48e6-a10d-b6cf66a83b04", }, }, { Title: "STRING: format 'date-time'", Schema: NewDateTimeSchema(), Serialization: map[string]interface{}{ "type": "string", "format": "date-time", }, AllValid: []interface{}{ "2017-12-31T11:59:59", "2017-12-31T11:59:59Z", "2017-12-31T11:59:59-11:30", "2017-12-31T11:59:59+11:30", "2017-12-31T11:59:59.999+11:30", "2017-12-31T11:59:59.999Z", }, AllInvalid: []interface{}{ nil, 3.14, "2017-12-31", "2017-12-31T11:59:59\n", "2017-12-31T11:59:59.+11:30", "2017-12-31T11:59:59.Z", }, }, { Title: "STRING: format 'date-time'", Schema: NewBytesSchema(), Serialization: map[string]interface{}{ "type": "string", "format": "byte", }, AllValid: []interface{}{ "", base64.StdEncoding.EncodeToString(func() []byte { data := make([]byte, 0, 1024) for i := 0; i < cap(data); i++ { data = append(data, byte(i)) } return data }()), base64.URLEncoding.EncodeToString(func() []byte { data := make([]byte, 0, 1024) for i := 0; i < cap(data); i++ { data = append(data, byte(i)) } return data }()), }, AllInvalid: []interface{}{ nil, " ", "\n", "%", }, }, { Title: "ARRAY", Schema: &Schema{ Type: "array", MinItems: 2, MaxItems: Uint64Ptr(3), UniqueItems: true, Items: NewFloat64Schema().NewRef(), }, Serialization: map[string]interface{}{ "type": "array", "minItems": 2, "maxItems": 3, "uniqueItems": true, "items": map[string]interface{}{ "type": "number", }, }, AllValid: []interface{}{ []interface{}{ 1, 2, }, []interface{}{ 1, 2, 3, }, }, AllInvalid: []interface{}{ nil, 3.14, []interface{}{ 1, }, []interface{}{ 42, 42, }, []interface{}{ 1, 2, 3, 4, }, }, }, { Title: "ARRAY : items format 'object'", Schema: &Schema{ Type: "array", UniqueItems: true, Items: (&Schema{ Type: "object", Properties: Schemas{ "key1": NewFloat64Schema().NewRef(), }, }).NewRef(), }, Serialization: map[string]interface{}{ "type": "array", "uniqueItems": true, "items": map[string]interface{}{ "properties": map[string]interface{}{ "key1": map[string]interface{}{ "type": "number", }, }, "type": "object", }, }, AllValid: []interface{}{ []interface{}{ map[string]interface{}{ "key1": 1, "key2": 1, // Additioanl properties will make object different // By default additionalProperties is true }, map[string]interface{}{ "key1": 1, }, }, []interface{}{ map[string]interface{}{ "key1": 1, }, map[string]interface{}{ "key1": 2, }, }, }, AllInvalid: []interface{}{ []interface{}{ map[string]interface{}{ "key1": 1, }, map[string]interface{}{ "key1": 1, }, }, }, }, { Title: "ARRAY : items format 'object' and object with a property of array type ", Schema: &Schema{ Type: "array", UniqueItems: true, Items: (&Schema{ Type: "object", Properties: Schemas{ "key1": (&Schema{ Type: "array", UniqueItems: true, Items: NewFloat64Schema().NewRef(), }).NewRef(), }, }).NewRef(), }, Serialization: map[string]interface{}{ "type": "array", "uniqueItems": true, "items": map[string]interface{}{ "properties": map[string]interface{}{ "key1": map[string]interface{}{ "type": "array", "uniqueItems": true, "items": map[string]interface{}{ "type": "number", }, }, }, "type": "object", }, }, AllValid: []interface{}{ []interface{}{ map[string]interface{}{ "key1": []interface{}{ 1, 2, }, }, map[string]interface{}{ "key1": []interface{}{ 3, 4, }, }, }, []interface{}{ // Slice have items with the same value but with different index will treated as different slices map[string]interface{}{ "key1": []interface{}{ 10, 9, }, }, map[string]interface{}{ "key1": []interface{}{ 9, 10, }, }, }, }, AllInvalid: []interface{}{ []interface{}{ // Violate outer array uniqueItems: true map[string]interface{}{ "key1": []interface{}{ 9, 9, }, }, map[string]interface{}{ "key1": []interface{}{ 9, 9, }, }, }, []interface{}{ // Violate inner(array in object) array uniqueItems: true map[string]interface{}{ "key1": []interface{}{ 9, 9, }, }, map[string]interface{}{ "key1": []interface{}{ 8, 8, }, }, }, }, }, { Title: "ARRAY : items format 'array'", Schema: &Schema{ Type: "array", UniqueItems: true, Items: (&Schema{ Type: "array", UniqueItems: true, Items: NewFloat64Schema().NewRef(), }).NewRef(), }, Serialization: map[string]interface{}{ "type": "array", "uniqueItems": true, "items": map[string]interface{}{ "items": map[string]interface{}{ "type": "number", }, "uniqueItems": true, "type": "array", }, }, AllValid: []interface{}{ []interface{}{ []interface{}{1, 2}, []interface{}{3, 4}, }, []interface{}{ // Slice have items with the same value but with different index will treated as different slices []interface{}{1, 2}, []interface{}{2, 1}, }, }, AllInvalid: []interface{}{ []interface{}{ // Violate outer array uniqueItems: true []interface{}{8, 9}, []interface{}{8, 9}, }, []interface{}{ // Violate inner array uniqueItems: true []interface{}{9, 9}, []interface{}{8, 8}, }, }, }, { Title: "ARRAY : items format 'array' and array with object type items", Schema: &Schema{ Type: "array", UniqueItems: true, Items: (&Schema{ Type: "array", UniqueItems: true, Items: (&Schema{ Type: "object", Properties: Schemas{ "key1": NewFloat64Schema().NewRef(), }, }).NewRef(), }).NewRef(), }, Serialization: map[string]interface{}{ "type": "array", "uniqueItems": true, "items": map[string]interface{}{ "items": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "key1": map[string]interface{}{ "type": "number", }, }, }, "uniqueItems": true, "type": "array", }, }, AllValid: []interface{}{ []interface{}{ []interface{}{ map[string]interface{}{ "key1": 1, }, }, []interface{}{ map[string]interface{}{ "key1": 2, }, }, }, []interface{}{ // Slice have items with the same value but with different index will treated as different slices []interface{}{ map[string]interface{}{ "key1": 1, }, map[string]interface{}{ "key1": 2, }, }, []interface{}{ map[string]interface{}{ "key1": 2, }, map[string]interface{}{ "key1": 1, }, }, }, }, AllInvalid: []interface{}{ []interface{}{ // Violate outer array uniqueItems: true []interface{}{ map[string]interface{}{ "key1": 1, }, map[string]interface{}{ "key1": 2, }, }, []interface{}{ map[string]interface{}{ "key1": 1, }, map[string]interface{}{ "key1": 2, }, }, }, []interface{}{ // Violate inner array uniqueItems: true []interface{}{ map[string]interface{}{ "key1": 1, }, map[string]interface{}{ "key1": 1, }, }, []interface{}{ map[string]interface{}{ "key1": 2, }, map[string]interface{}{ "key1": 2, }, }, }, }, }, { Title: "OBJECT", Schema: &Schema{ Type: "object", MaxProps: Uint64Ptr(2), Properties: Schemas{ "numberProperty": NewFloat64Schema().NewRef(), }, }, Serialization: map[string]interface{}{ "type": "object", "maxProperties": 2, "properties": map[string]interface{}{ "numberProperty": map[string]interface{}{ "type": "number", }, }, }, AllValid: []interface{}{ map[string]interface{}{}, map[string]interface{}{ "numberProperty": 3.14, }, map[string]interface{}{ "numberProperty": 3.14, "some prop": nil, }, }, AllInvalid: []interface{}{ nil, false, true, 3.14, "", []interface{}{}, map[string]interface{}{ "numberProperty": "abc", }, map[string]interface{}{ "numberProperty": 3.14, "some prop": 42, "third": "prop", }, }, }, { Schema: &Schema{ Type: "object", AdditionalProperties: &SchemaRef{ Value: &Schema{ Type: "number", }, }, }, Serialization: map[string]interface{}{ "type": "object", "additionalProperties": map[string]interface{}{ "type": "number", }, }, AllValid: []interface{}{ map[string]interface{}{}, map[string]interface{}{ "x": 3.14, "y": 3.14, }, }, AllInvalid: []interface{}{ map[string]interface{}{ "x": "abc", }, }, }, { Schema: &Schema{ Type: "object", AdditionalPropertiesAllowed: BoolPtr(true), }, Serialization: map[string]interface{}{ "type": "object", "additionalProperties": true, }, AllValid: []interface{}{ map[string]interface{}{}, map[string]interface{}{ "x": false, "y": 3.14, }, }, }, { Title: "NOT", Schema: &Schema{ Not: &SchemaRef{ Value: &Schema{ Enum: []interface{}{ nil, true, 3.14, "not this", }, }, }, }, Serialization: map[string]interface{}{ "not": map[string]interface{}{ "enum": []interface{}{ nil, true, 3.14, "not this", }, }, }, AllValid: []interface{}{ false, 2, "abc", }, AllInvalid: []interface{}{ nil, true, 3.14, "not this", }, }, { Title: "ANY OF", Schema: &Schema{ AnyOf: []*SchemaRef{ { Value: NewFloat64Schema(). WithMin(1). WithMax(2), }, { Value: NewFloat64Schema(). WithMin(2). WithMax(3), }, }, }, Serialization: map[string]interface{}{ "anyOf": []interface{}{ map[string]interface{}{ "type": "number", "minimum": 1, "maximum": 2, }, map[string]interface{}{ "type": "number", "minimum": 2, "maximum": 3, }, }, }, AllValid: []interface{}{ 1, 2, 3, }, AllInvalid: []interface{}{ 0, 4, }, }, { Title: "ALL OF", Schema: &Schema{ AllOf: []*SchemaRef{ { Value: NewFloat64Schema(). WithMin(1). WithMax(2), }, { Value: NewFloat64Schema(). WithMin(2). WithMax(3), }, }, }, Serialization: map[string]interface{}{ "allOf": []interface{}{ map[string]interface{}{ "type": "number", "minimum": 1, "maximum": 2, }, map[string]interface{}{ "type": "number", "minimum": 2, "maximum": 3, }, }, }, AllValid: []interface{}{ 2, }, AllInvalid: []interface{}{ 0, 1, 3, 4, }, }, { Title: "ONE OF", Schema: &Schema{ OneOf: []*SchemaRef{ { Value: NewFloat64Schema(). WithMin(1). WithMax(2), }, { Value: NewFloat64Schema(). WithMin(2). WithMax(3), }, }, }, Serialization: map[string]interface{}{ "oneOf": []interface{}{ map[string]interface{}{ "type": "number", "minimum": 1, "maximum": 2, }, map[string]interface{}{ "type": "number", "minimum": 2, "maximum": 3, }, }, }, AllValid: []interface{}{ 1, 3, }, AllInvalid: []interface{}{ 0, 2, 4, }, }, } 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)) } } func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { return func(t *testing.T) { baseSchema := example.Schema for _, typ := range example.AllValid { schema := baseSchema.WithFormat(typ) err := schema.Validate(context.Background()) require.NoError(t, err) } for _, typ := range example.AllInvalid { schema := baseSchema.WithFormat(typ) err := schema.Validate(context.Background()) require.Error(t, err) } } } var typeExamples = []schemaTypeExample{ { Title: "STRING", Schema: NewStringSchema(), AllValid: []string{ "", "byte", "binary", "date", "date-time", "password", // Not supported but allowed: "uri", }, AllInvalid: []string{ "code/golang", }, }, { Title: "NUMBER", Schema: NewFloat64Schema(), AllValid: []string{ "", "float", "double", }, AllInvalid: []string{ "f32", }, }, { Title: "INTEGER", Schema: NewIntegerSchema(), AllValid: []string{ "", "int32", "int64", }, AllInvalid: []string{ "uint8", }, }, } func TestSchemaErrors(t *testing.T) { for _, example := range schemaErrorExamples { t.Run(example.Title, testSchemaError(t, example)) } } func testSchemaError(t *testing.T, example schemaErrorExample) func(*testing.T) { return func(t *testing.T) { msg := example.Error.Error() require.True(t, strings.Contains(msg, example.Want)) } } type schemaErrorExample struct { Title string Error *SchemaError Want string } var schemaErrorExamples = []schemaErrorExample{ { Title: "SIMPLE", Error: &SchemaError{ Value: 1, Schema: &Schema{}, Reason: "SIMPLE", }, Want: "SIMPLE", }, { Title: "NEST", Error: &SchemaError{ Value: 1, Schema: &Schema{}, Reason: "PARENT", Origin: &SchemaError{ Value: 1, Schema: &Schema{}, Reason: "NEST", }, }, Want: "NEST", }, } type schemaMultiErrorExample struct { Title string Schema *Schema Values []interface{} ExpectedErrors []MultiError } func TestSchemasMultiError(t *testing.T) { for _, example := range schemaMultiErrorExamples { t.Run(example.Title, testSchemaMultiError(t, example)) } } func testSchemaMultiError(t *testing.T, example schemaMultiErrorExample) func(*testing.T) { return func(t *testing.T) { schema := example.Schema for i, value := range example.Values { err := validateSchema(t, schema, value, MultiErrors()) require.Error(t, err) require.IsType(t, MultiError{}, err) merr, _ := err.(MultiError) expected := example.ExpectedErrors[i] require.True(t, len(merr) > 0) require.Len(t, merr, len(expected)) for _, e := range merr { require.IsType(t, &SchemaError{}, e) var found bool scherr, _ := e.(*SchemaError) for _, expectedErr := range expected { expectedScherr, _ := expectedErr.(*SchemaError) if reflect.DeepEqual(expectedScherr.reversePath, scherr.reversePath) && expectedScherr.SchemaField == scherr.SchemaField { found = true break } } require.True(t, found, fmt.Sprintf("missing %s error on %s", scherr.SchemaField, strings.Join(scherr.JSONPointer(), "."))) } } } } var schemaMultiErrorExamples = []schemaMultiErrorExample{ { Title: "STRING", Schema: NewStringSchema(). WithMinLength(2). WithMaxLength(3). WithPattern("^[abc]+$"), Values: []interface{}{ "f", "foobar", }, ExpectedErrors: []MultiError{ {&SchemaError{SchemaField: "minLength"}, &SchemaError{SchemaField: "pattern"}}, {&SchemaError{SchemaField: "maxLength"}, &SchemaError{SchemaField: "pattern"}}, }, }, { Title: "NUMBER", Schema: NewIntegerSchema(). WithMin(1). WithMax(10), Values: []interface{}{ 0.5, 10.1, }, ExpectedErrors: []MultiError{ {&SchemaError{SchemaField: "type"}, &SchemaError{SchemaField: "minimum"}}, {&SchemaError{SchemaField: "type"}, &SchemaError{SchemaField: "maximum"}}, }, }, { Title: "ARRAY: simple", Schema: NewArraySchema(). WithMinItems(2). WithMaxItems(2). WithItems(NewStringSchema(). WithPattern("^[abc]+$")), Values: []interface{}{ []interface{}{"foo"}, []interface{}{"foo", "bar", "fizz"}, }, ExpectedErrors: []MultiError{ { &SchemaError{SchemaField: "minItems"}, &SchemaError{SchemaField: "pattern", reversePath: []string{"0"}}, }, { &SchemaError{SchemaField: "maxItems"}, &SchemaError{SchemaField: "pattern", reversePath: []string{"0"}}, &SchemaError{SchemaField: "pattern", reversePath: []string{"1"}}, &SchemaError{SchemaField: "pattern", reversePath: []string{"2"}}, }, }, }, { Title: "ARRAY: object", Schema: NewArraySchema(). WithItems(NewObjectSchema(). WithProperties(map[string]*Schema{ "key1": NewStringSchema(), "key2": NewIntegerSchema(), }), ), Values: []interface{}{ []interface{}{ map[string]interface{}{ "key1": 100, // not a string "key2": "not an integer", }, }, }, ExpectedErrors: []MultiError{ { &SchemaError{SchemaField: "type", reversePath: []string{"key1", "0"}}, &SchemaError{SchemaField: "type", reversePath: []string{"key2", "0"}}, }, }, }, { Title: "OBJECT", Schema: NewObjectSchema(). WithProperties(map[string]*Schema{ "key1": NewStringSchema(), "key2": NewIntegerSchema(), "key3": NewArraySchema(). WithItems(NewStringSchema(). WithPattern("^[abc]+$")), }), Values: []interface{}{ map[string]interface{}{ "key1": 100, // not a string "key2": "not an integer", "key3": []interface{}{"abc", "def"}, }, }, ExpectedErrors: []MultiError{ { &SchemaError{SchemaField: "type", reversePath: []string{"key1"}}, &SchemaError{SchemaField: "type", reversePath: []string{"key2"}}, &SchemaError{SchemaField: "pattern", reversePath: []string{"1", "key3"}}, }, }, }, } func TestIssue283(t *testing.T) { const api = ` openapi: "3.0.1" components: schemas: Test: properties: name: type: string ownerName: not: type: boolean type: object ` data := map[string]interface{}{ "name": "kin-openapi", "ownerName": true, } s, err := NewLoader().LoadFromData([]byte(api)) require.NoError(t, err) require.NotNil(t, s) err = s.Components.Schemas["Test"].Value.VisitJSON(data) require.NotNil(t, err) require.NotEqual(t, errSchema, err) require.Contains(t, err.Error(), `Error at "/ownerName": Doesn't match schema "not"`) } func TestValidationFailsOnInvalidPattern(t *testing.T) { schema := Schema{ Pattern: "[", Type: "string", } var err = schema.Validate(context.Background()) require.Error(t, err) } kin-openapi-0.85.0/openapi3/schema_validation_settings.go000066400000000000000000000020071415236407200234600ustar00rootroot00000000000000package openapi3 // SchemaValidationOption describes options a user has when validating request / response bodies. type SchemaValidationOption func(*schemaValidationSettings) type schemaValidationSettings struct { failfast bool multiError bool asreq, asrep bool // exclusive (XOR) fields } // FailFast returns schema validation errors quicker. func FailFast() SchemaValidationOption { return func(s *schemaValidationSettings) { s.failfast = true } } func MultiErrors() SchemaValidationOption { return func(s *schemaValidationSettings) { s.multiError = true } } func VisitAsRequest() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false } } func VisitAsResponse() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true } } func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings { settings := &schemaValidationSettings{} for _, opt := range opts { opt(settings) } return settings } kin-openapi-0.85.0/openapi3/security_requirements.go000066400000000000000000000017631415236407200225500ustar00rootroot00000000000000package openapi3 import ( "context" ) type SecurityRequirements []SecurityRequirement func NewSecurityRequirements() *SecurityRequirements { return &SecurityRequirements{} } func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) *SecurityRequirements { *srs = append(*srs, securityRequirement) return srs } func (value SecurityRequirements) Validate(ctx context.Context) error { for _, item := range value { if err := item.Validate(ctx); err != nil { return err } } return nil } type SecurityRequirement map[string][]string func NewSecurityRequirement() SecurityRequirement { return make(SecurityRequirement) } func (security SecurityRequirement) Authenticate(provider string, scopes ...string) SecurityRequirement { if len(scopes) == 0 { scopes = []string{} // Forces the variable to be encoded as an array instead of null } security[provider] = scopes return security } func (value SecurityRequirement) Validate(ctx context.Context) error { return nil } kin-openapi-0.85.0/openapi3/security_requirements_test.go000066400000000000000000000025551415236407200236070ustar00rootroot00000000000000package openapi3 import ( "encoding/json" "testing" "github.com/stretchr/testify/require" ) func TestSecurityRequirementsEncoding(t *testing.T) { tests := []struct { requirements *SecurityRequirements json string }{ { requirements: NewSecurityRequirements(), json: `[]`, }, { requirements: NewSecurityRequirements().With(NewSecurityRequirement()), json: `[{}]`, }, } for _, test := range tests { b, err := json.Marshal(test.requirements) require.NoError(t, err) require.Equal(t, test.json, string(b), "incorrect requirements encoding") } } func TestSecurityRequirementEncoding(t *testing.T) { tests := []struct { requirement SecurityRequirement json string }{ { requirement: NewSecurityRequirement(), json: `{}`, }, { requirement: NewSecurityRequirement().Authenticate("provider"), json: `{"provider":[]}`, }, { requirement: NewSecurityRequirement().Authenticate("provider", "scope1"), json: `{"provider":["scope1"]}`, }, { requirement: NewSecurityRequirement().Authenticate("provider", "scope1", "scope2"), json: `{"provider":["scope1","scope2"]}`, }, } for _, test := range tests { b, err := json.Marshal(test.requirement) require.NoError(t, err) require.Equal(t, test.json, string(b), "incorrect requirements encoding") } } kin-openapi-0.85.0/openapi3/security_scheme.go000066400000000000000000000155011415236407200212640ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "fmt" "github.com/getkin/kin-openapi/jsoninfo" "github.com/go-openapi/jsonpointer" ) type SecuritySchemes map[string]*SecuritySchemeRef func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) { ref, ok := s[token] if ref == nil || ok == false { return nil, fmt.Errorf("object has no field %q", token) } if ref.Ref != "" { return &Ref{Ref: ref.Ref}, nil } return ref.Value, nil } var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) type SecurityScheme struct { ExtensionProps Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"` OpenIdConnectUrl string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` } func NewSecurityScheme() *SecurityScheme { return &SecurityScheme{} } func NewCSRFSecurityScheme() *SecurityScheme { return &SecurityScheme{ Type: "apiKey", In: "header", Name: "X-XSRF-TOKEN", } } func NewOIDCSecurityScheme(oidcUrl string) *SecurityScheme { return &SecurityScheme{ Type: "openIdConnect", OpenIdConnectUrl: oidcUrl, } } func NewJWTSecurityScheme() *SecurityScheme { return &SecurityScheme{ Type: "http", Scheme: "bearer", BearerFormat: "JWT", } } func (ss *SecurityScheme) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(ss) } func (ss *SecurityScheme) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, ss) } func (ss *SecurityScheme) WithType(value string) *SecurityScheme { ss.Type = value return ss } func (ss *SecurityScheme) WithDescription(value string) *SecurityScheme { ss.Description = value return ss } func (ss *SecurityScheme) WithName(value string) *SecurityScheme { ss.Name = value return ss } func (ss *SecurityScheme) WithIn(value string) *SecurityScheme { ss.In = value return ss } func (ss *SecurityScheme) WithScheme(value string) *SecurityScheme { ss.Scheme = value return ss } func (ss *SecurityScheme) WithBearerFormat(value string) *SecurityScheme { ss.BearerFormat = value return ss } func (value *SecurityScheme) Validate(ctx context.Context) error { hasIn := false hasBearerFormat := false hasFlow := false switch value.Type { case "apiKey": hasIn = true case "http": scheme := value.Scheme switch scheme { case "bearer": hasBearerFormat = true case "basic", "negotiate", "digest": default: return fmt.Errorf("security scheme of type 'http' has invalid 'scheme' value %q", scheme) } case "oauth2": hasFlow = true case "openIdConnect": if value.OpenIdConnectUrl == "" { return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", value.Name) } default: return fmt.Errorf("security scheme 'type' can't be %q", value.Type) } // Validate "in" and "name" if hasIn { switch value.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) } if value.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 { 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) } // Validate "flow" if hasFlow { flow := value.Flows if flow == nil { return fmt.Errorf("security scheme of type %q should have 'flows'", value.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) } return nil } type OAuthFlows struct { ExtensionProps Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` } type oAuthFlowType int const ( oAuthFlowTypeImplicit oAuthFlowType = iota oAuthFlowTypePassword oAuthFlowTypeClientCredentials oAuthFlowAuthorizationCode ) func (flows *OAuthFlows) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(flows) } func (flows *OAuthFlows) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, flows) } func (flows *OAuthFlows) Validate(ctx context.Context) error { if v := flows.Implicit; v != nil { return v.Validate(ctx, oAuthFlowTypeImplicit) } if v := flows.Password; v != nil { return v.Validate(ctx, oAuthFlowTypePassword) } if v := flows.ClientCredentials; v != nil { return v.Validate(ctx, oAuthFlowTypeClientCredentials) } if v := flows.AuthorizationCode; v != nil { return v.Validate(ctx, oAuthFlowAuthorizationCode) } return errors.New("no OAuth flow is defined") } type OAuthFlow struct { ExtensionProps AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` Scopes map[string]string `json:"scopes" yaml:"scopes"` } func (flow *OAuthFlow) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(flow) } func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, flow) } 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 '") } } if typ != oAuthFlowTypeImplicit { if v := flow.TokenURL; v == "" { return errors.New("an OAuth flow is missing 'tokenUrl in not implicit'") } } if v := flow.Scopes; v == nil { return errors.New("an OAuth flow is missing 'scopes'") } return nil } kin-openapi-0.85.0/openapi3/security_scheme_test.go000066400000000000000000000077601415236407200223330ustar00rootroot00000000000000package openapi3 import ( "context" "testing" "github.com/stretchr/testify/require" ) type securitySchemeExample struct { title string raw []byte valid bool } func TestSecuritySchemaExample(t *testing.T) { for _, example := range securitySchemeExamples { t.Run(example.title, testSecuritySchemaExample(t, example)) } } func testSecuritySchemaExample(t *testing.T, e securitySchemeExample) func(*testing.T) { return func(t *testing.T) { var err error ss := &SecurityScheme{} err = ss.UnmarshalJSON(e.raw) require.NoError(t, err) err = ss.Validate(context.Background()) if e.valid { require.NoError(t, err) } else { require.Error(t, err) } } } // from https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#fixed-fields-23 var securitySchemeExamples = []securitySchemeExample{ { title: "Basic Authentication Sample", raw: []byte(` { "type": "http", "scheme": "basic" } `), valid: true, }, { title: "Negotiate Authentication Sample", raw: []byte(` { "type": "http", "scheme": "negotiate" } `), valid: true, }, { title: "Unknown http Authentication Sample", raw: []byte(` { "type": "http", "scheme": "notvalid" } `), valid: false, }, { title: "API Key Sample", raw: []byte(` { "type": "apiKey", "name": "api_key", "in": "header" } `), valid: true, }, { title: "apiKey with bearerFormat", raw: []byte(` { "type": "apiKey", "in": "header", "name": "X-API-KEY", "bearerFormat": "Arbitrary text" } `), valid: false, }, { title: "Bearer Sample with arbitrary format", raw: []byte(` { "type": "http", "scheme": "bearer", "bearerFormat": "Arbitrary text" } `), valid: true, }, { title: "Implicit OAuth2 Sample", raw: []byte(` { "type": "oauth2", "flows": { "implicit": { "authorizationUrl": "https://example.com/api/oauth/dialog", "scopes": { "write:pets": "modify pets in your account", "read:pets": "read your pets" } } } } `), valid: true, }, { title: "OAuth Flow Object Sample", raw: []byte(` { "type": "oauth2", "flows": { "implicit": { "authorizationUrl": "https://example.com/api/oauth/dialog", "scopes": { "write:pets": "modify pets in your account", "read:pets": "read your pets" } }, "authorizationCode": { "authorizationUrl": "https://example.com/api/oauth/dialog", "tokenUrl": "https://example.com/api/oauth/token", "scopes": { "write:pets": "modify pets in your account", "read:pets": "read your pets" } } } } `), valid: true, }, { title: "OAuth Flow Object clientCredentials/password", raw: []byte(` { "type": "oauth2", "flows": { "clientCredentials": { "tokenUrl": "https://example.com/api/oauth/token", "scopes": { "write:pets": "modify pets in your account" } }, "password": { "tokenUrl": "https://example.com/api/oauth/token", "scopes": { "read:pets": "read your pets" } } } } `), valid: true, }, { title: "Invalid Basic", raw: []byte(` { "type": "https", "scheme": "basic" } `), valid: false, }, { title: "Apikey Cookie", raw: []byte(` { "type": "apiKey", "in": "cookie", "name": "somecookie" } `), valid: true, }, { title: "OAuth Flow Object with no scopes", raw: []byte(` { "type": "oauth2", "flows": { "password": { "tokenUrl": "https://example.com/api/oauth/token" } } } `), valid: false, }, { title: "OAuth Flow Object with empty scopes", raw: []byte(` { "type": "oauth2", "flows": { "password": { "tokenUrl": "https://example.com/api/oauth/token", "scopes": {} } } } `), valid: true, }, { title: "OIDC Type With URL", raw: []byte(` { "type": "openIdConnect", "openIdConnectUrl": "https://example.com/.well-known/openid-configuration" } `), valid: true, }, { title: "OIDC Type Without URL", raw: []byte(` { "type": "openIdConnect", "openIdConnectUrl": "" } `), valid: false, }, } kin-openapi-0.85.0/openapi3/serialization_method.go000066400000000000000000000007461415236407200223130ustar00rootroot00000000000000package openapi3 const ( SerializationSimple = "simple" SerializationLabel = "label" SerializationMatrix = "matrix" SerializationForm = "form" SerializationSpaceDelimited = "spaceDelimited" SerializationPipeDelimited = "pipeDelimited" SerializationDeepObject = "deepObject" ) // SerializationMethod describes a serialization method of HTTP request's parameters and body. type SerializationMethod struct { Style string Explode bool } kin-openapi-0.85.0/openapi3/server.go000066400000000000000000000103721415236407200174000ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "fmt" "math" "net/url" "strings" "github.com/getkin/kin-openapi/jsoninfo" ) // 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 { if err := v.Validate(ctx); err != nil { return err } } return nil } func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) { rawURL := parsedURL.String() if i := strings.IndexByte(rawURL, '?'); i >= 0 { rawURL = rawURL[:i] } for _, server := range servers { pathParams, remaining, ok := server.MatchRawURL(rawURL) if ok { return server, pathParams, remaining } } return nil, nil, "" } // Server is specified by OpenAPI/Swagger standard version 3.0. type Server struct { ExtensionProps URL string `json:"url" yaml:"url"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` } func (server *Server) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(server) } func (server *Server) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, server) } func (server Server) ParameterNames() ([]string, error) { pattern := server.URL var params []string for len(pattern) > 0 { i := strings.IndexByte(pattern, '{') if i < 0 { break } pattern = pattern[i+1:] i = strings.IndexByte(pattern, '}') if i < 0 { return nil, errors.New("missing '}'") } params = append(params, strings.TrimSpace(pattern[:i])) pattern = pattern[i+1:] } return params, nil } func (server Server) MatchRawURL(input string) ([]string, string, bool) { pattern := server.URL var params []string for len(pattern) > 0 { c := pattern[0] if len(pattern) == 1 && c == '/' { break } if c == '{' { // Find end of pattern i := strings.IndexByte(pattern, '}') if i < 0 { return nil, "", false } pattern = pattern[i+1:] // Find next matching pattern character or next '/' whichever comes first np := -1 if len(pattern) > 0 { np = strings.IndexByte(input, pattern[0]) } ns := strings.IndexByte(input, '/') if np < 0 { i = ns } else if ns < 0 { i = np } else { i = int(math.Min(float64(np), float64(ns))) } if i < 0 { i = len(input) } params = append(params, input[:i]) input = input[i:] continue } if len(input) == 0 || input[0] != c { return nil, "", false } pattern = pattern[1:] input = input[1:] } if input == "" { input = "/" } if input[0] != '/' { return nil, "", false } return params, input, true } func (value *Server) Validate(ctx context.Context) (err error) { if value.URL == "" { return errors.New("value of url must be a non-empty string") } opening, closing := strings.Count(value.URL, "{"), strings.Count(value.URL, "}") if opening != closing { return errors.New("server URL has mismatched { and }") } if opening != len(value.Variables) { return errors.New("server has undeclared variables") } for name, v := range value.Variables { if !strings.Contains(value.URL, fmt.Sprintf("{%s}", name)) { return errors.New("server has undeclared variables") } if err = v.Validate(ctx); err != nil { return } } return } // ServerVariable is specified by OpenAPI/Swagger standard version 3.0. type ServerVariable struct { ExtensionProps Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` } func (serverVariable *ServerVariable) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(serverVariable) } 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() if err != nil { return err } return fmt.Errorf("field default is required in %s", data) } return nil } kin-openapi-0.85.0/openapi3/server_test.go000066400000000000000000000064341415236407200204430ustar00rootroot00000000000000package openapi3 import ( "context" "errors" "testing" "github.com/stretchr/testify/require" ) func TestServerParamNames(t *testing.T) { server := &Server{ URL: "http://{x}.{y}.example.com", } values, err := server.ParameterNames() require.NoError(t, err) require.Exactly(t, []string{"x", "y"}, values) } func TestServerParamValuesWithPath(t *testing.T) { server := &Server{ URL: "http://{arg0}.{arg1}.example.com/a/{arg3}-version/{arg4}c{arg5}", } for input, expected := range map[string]*serverMatch{ "http://x.example.com/a/b": nil, "http://x.y.example.com/": nil, "http://x.y.example.com/a/": nil, "http://x.y.example.com/a/c": nil, "http://baddomain.com/.example.com/a/1.0.0-version/c/d": nil, "http://baddomain.com/.example.com/a/1.0.0/2/2.0.0-version/c": nil, "http://x.y.example.com/a/b-version/prefixedc": newServerMatch("/", "x", "y", "b", "prefixed", ""), "http://x.y.example.com/a/b-version/c": newServerMatch("/", "x", "y", "b", "", ""), "http://x.y.example.com/a/b-version/c/": newServerMatch("/", "x", "y", "b", "", ""), "http://x.y.example.com/a/b-version/c/d": newServerMatch("/d", "x", "y", "b", "", ""), "http://domain0.domain1.example.com/a/b-version/c/d": newServerMatch("/d", "domain0", "domain1", "b", "", ""), "http://domain0.domain1.example.com/a/1.0.0-version/c/d": newServerMatch("/d", "domain0", "domain1", "1.0.0", "", ""), } { t.Run(input, testServerParamValues(t, server, input, expected)) } } func TestServerParamValuesNoPath(t *testing.T) { server := &Server{ URL: "https://{arg0}.{arg1}.example.com/", } for input, expected := range map[string]*serverMatch{ "https://domain0.domain1.example.com/": newServerMatch("/", "domain0", "domain1"), } { t.Run(input, testServerParamValues(t, server, input, expected)) } } func validServer() *Server { return &Server{ URL: "http://my.cool.website", } } func invalidServer() *Server { return &Server{} } func TestServerValidation(t *testing.T) { tests := []struct { name string input *Server expectedError error }{ { "when no URL is provided", invalidServer(), errors.New("value of url must be a non-empty string"), }, { "when a URL is provided", validServer(), nil, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { c := context.Background() validationErr := test.input.Validate(c) require.Equal(t, test.expectedError, validationErr, "expected errors (or lack of) to match") }) } } func testServerParamValues(t *testing.T, server *Server, input string, expected *serverMatch) func(*testing.T) { return func(t *testing.T) { args, remaining, ok := server.MatchRawURL(input) if expected == nil { require.False(t, ok) return } require.True(t, ok) actual := &serverMatch{ Remaining: remaining, Args: args, } require.Equal(t, expected, actual) } } type serverMatch struct { Remaining string Args []string } func newServerMatch(remaining string, args ...string) *serverMatch { return &serverMatch{ Remaining: remaining, Args: args, } } kin-openapi-0.85.0/openapi3/tag.go000066400000000000000000000014361415236407200166460ustar00rootroot00000000000000package openapi3 import "github.com/getkin/kin-openapi/jsoninfo" // Tags is specified by OpenAPI/Swagger 3.0 standard. type Tags []*Tag func (tags Tags) Get(name string) *Tag { for _, tag := range tags { if tag.Name == name { return tag } } return nil } // Tag is specified by OpenAPI/Swagger 3.0 standard. type Tag struct { ExtensionProps Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } func (t *Tag) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalStrictStruct(t) } func (t *Tag) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, t) } kin-openapi-0.85.0/openapi3/testdata/000077500000000000000000000000001415236407200173515ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/303bis/000077500000000000000000000000001415236407200203545ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/303bis/common/000077500000000000000000000000001415236407200216445ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/303bis/common/properties.yaml000066400000000000000000000004671415236407200247330ustar00rootroot00000000000000timestamp: type: string description: Date and time in ISO 8601 format. example: "2020-04-09T18:14:30Z" readOnly: true nullable: true timestamps: type: object properties: created_at: $ref: "#/timestamp" deleted_at: $ref: "#/timestamp" updated_at: $ref: "#/timestamp" kin-openapi-0.85.0/openapi3/testdata/303bis/service.yaml000066400000000000000000000011051415236407200226750ustar00rootroot00000000000000openapi: 3.0.0 info: title: 'some service spec' version: 1.2.3 paths: /service: get: tags: - services/service summary: List services description: List services. operationId: list-services responses: "200": description: OK content: application/json: schema: type: array items: $ref: "#/components/schemas/model_service" components: schemas: model_service: allOf: - $ref: "common/properties.yaml#/timestamps" kin-openapi-0.85.0/openapi3/testdata/Test_param_override.yml000066400000000000000000000015771415236407200241040ustar00rootroot00000000000000openapi: 3.0.0 info: title: customer version: '1.0' servers: - url: 'httpbin.kwaf-demo.test' paths: '/customers/{customer_id}': parameters: - schema: type: integer name: customer_id in: path required: true get: parameters: - schema: type: integer maximum: 100 name: customer_id in: path required: true summary: customer tags: [] responses: '200': description: OK content: application/json: schema: type: object properties: customer_id: type: integer customer_name: type: string operationId: get-customers-customer_id description: Retrieve a specific customer by ID components: schemas: {} kin-openapi-0.85.0/openapi3/testdata/callback-transactioned.yml000066400000000000000000000003711415236407200244650ustar00rootroot00000000000000post: requestBody: description: Callback payload content: 'application/json': schema: $ref: 'callbacks.yml#/components/schemas/SomePayload' responses: '200': description: callback successfully processed kin-openapi-0.85.0/openapi3/testdata/callbacks.yml000066400000000000000000000033011415236407200220100ustar00rootroot00000000000000openapi: 3.1.0 info: title: Callback refd version: 1.2.3 paths: /trans: post: description: '' requestBody: description: '' content: 'application/json': schema: properties: id: {type: string} email: {format: email} responses: '201': description: subscription successfully created content: application/json: schema: type: object callbacks: transactionCallback: 'http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}': $ref: callback-transactioned.yml /other: post: description: '' parameters: - name: queryUrl in: query required: true description: | bla bla bla schema: type: string format: uri example: https://example.com responses: '201': description: '' content: application/json: schema: type: object callbacks: myEvent: $ref: '#/components/callbacks/MyCallbackEvent' components: schemas: SomePayload: {type: object} SomeOtherPayload: {type: boolean} callbacks: MyCallbackEvent: '{$request.query.queryUrl}': post: requestBody: description: Callback payload content: 'application/json': schema: $ref: '#/components/schemas/SomeOtherPayload' responses: '200': description: callback successfully processed kin-openapi-0.85.0/openapi3/testdata/circularref.openapi.yml000066400000000000000000000005731415236407200240340ustar00rootroot00000000000000--- openapi: 3.0.0 info: title: 'OAI Specification in YAML' version: 0.0.1 paths: /test: get: responses: "200": $ref: '#/components/responses/GetTestOK' components: responses: GetTestOK: description: OK content: application/json: schema: $ref: 'pathref.openapi.yml#/components/schemas/TestSchema' kin-openapi-0.85.0/openapi3/testdata/components.openapi.json000066400000000000000000000032621415236407200240660ustar00rootroot00000000000000{ "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "schemas": { "Name": { "type": "string" }, "CustomTestSchema": { "$ref": "#/components/schemas/Name" } }, "responses": { "Name": { "description": "description" }, "CustomTestResponse": { "$ref": "#/components/responses/Name" } }, "parameters": { "Name": { "name": "id", "in": "header" }, "CustomTestParameter": { "$ref": "#/components/parameters/Name" } }, "examples": { "Name": { "description": "description" }, "CustomTestExample": { "$ref": "#/components/examples/Name" } }, "requestBodies": { "Name": { "content": {} }, "CustomTestRequestBody": { "$ref": "#/components/requestBodies/Name" } }, "headers": { "Name": { "description": "description" }, "CustomTestHeader": { "$ref": "#/components/headers/Name" } }, "securitySchemes": { "Name": { "type": "cookie", "description": "description" }, "CustomTestSecurityScheme": { "$ref": "#/components/securitySchemes/Name" } } } }kin-openapi-0.85.0/openapi3/testdata/components.openapi.yml000066400000000000000000000016541415236407200237210ustar00rootroot00000000000000--- openapi: 3.0.0 info: title: '' version: '1' paths: {} components: schemas: Name: type: string CustomTestSchema: "$ref": "#/components/schemas/Name" responses: Name: description: description CustomTestResponse: "$ref": "#/components/responses/Name" parameters: Name: name: id in: header CustomTestParameter: "$ref": "#/components/parameters/Name" examples: Name: description: description CustomTestExample: "$ref": "#/components/examples/Name" requestBodies: Name: content: {} CustomTestRequestBody: "$ref": "#/components/requestBodies/Name" headers: Name: description: description CustomTestHeader: "$ref": "#/components/headers/Name" securitySchemes: Name: type: cookie description: description CustomTestSecurityScheme: "$ref": "#/components/securitySchemes/Name" kin-openapi-0.85.0/openapi3/testdata/ext.json000066400000000000000000000006021415236407200210420ustar00rootroot00000000000000{ "$schema": "http://json-schema.org/draft-04/schema#", "definitions": { "a": { "type": "string" }, "b": { "type": "object", "description": "I use a local reference.", "properties": { "name": { "$ref": "#/definitions/a" } } } } } kin-openapi-0.85.0/openapi3/testdata/issue235.spec0-typo.yml000066400000000000000000000007661415236407200234710ustar00rootroot00000000000000openapi: 3.0.0 info: title: 'OAI Specification in YAML' version: 0.0.1 paths: /test: get: responses: "200": $ref: '#/components/responses/GetTestOK' components: responses: GetTestOK: description: OK content: application/json: schema: $ref: '#/components/schemas/ObjectA' schemas: ObjectA: type: object properties: object_b: $ref: 'issue235.spec0-typo.yml#/components/schemas/ObjectD' kin-openapi-0.85.0/openapi3/testdata/issue235.spec0.yml000066400000000000000000000007611415236407200224730ustar00rootroot00000000000000openapi: 3.0.0 info: title: 'OAI Specification in YAML' version: 0.0.1 paths: /test: get: responses: "200": $ref: '#/components/responses/GetTestOK' components: responses: GetTestOK: description: OK content: application/json: schema: $ref: '#/components/schemas/ObjectA' schemas: ObjectA: type: object properties: object_b: $ref: 'issue235.spec1.yml#/components/schemas/ObjectD' kin-openapi-0.85.0/openapi3/testdata/issue235.spec1.yml000066400000000000000000000003651415236407200224740ustar00rootroot00000000000000components: schemas: ObjectD: type: object properties: result: $ref: '#/components/schemas/ObjectE' ObjectE: properties: name: $ref: issue235.spec2.yml#/components/schemas/ObjectX kin-openapi-0.85.0/openapi3/testdata/issue235.spec2.yml000066400000000000000000000001561415236407200224730ustar00rootroot00000000000000components: schemas: ObjectX: type: object properties: name: type: string kin-openapi-0.85.0/openapi3/testdata/load_with_go_embed_test.go000066400000000000000000000013431415236407200245330ustar00rootroot00000000000000package openapi3_test import ( "embed" "fmt" "net/url" "github.com/getkin/kin-openapi/openapi3" ) //go:embed testdata/recursiveRef/* var fs embed.FS func Example() { loader := openapi3.NewLoader() loader.IsExternalRefsAllowed = true loader.ReadFromURIFunc = func(loader *openapi3.Loader, uri *url.URL) ([]byte, error) { return fs.ReadFile(uri.Path) } doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml") if err != nil { panic(err) } if err = doc.Validate(loader.Context); err != nil { panic(err) } fmt.Println(doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Type) // Output: string } kin-openapi-0.85.0/openapi3/testdata/main.yaml000066400000000000000000000001751415236407200211640ustar00rootroot00000000000000openapi: "3.0.0" info: title: "test file" version: "n/a" paths: /testpath: $ref: "testpath.yaml#/paths/~1testpath" kin-openapi-0.85.0/openapi3/testdata/my-openapi.json000066400000000000000000000004471415236407200223270ustar00rootroot00000000000000{ "openapi": "3.0.2", "info": { "title": "My API", "version": "0.1.0" }, "paths": { "/foo": { "get": { "responses": { "200": { "$ref": "my-other-openapi.json#/components/responses/DefaultResponse" } } } } } } kin-openapi-0.85.0/openapi3/testdata/my-other-openapi.json000066400000000000000000000011471415236407200234440ustar00rootroot00000000000000{ "openapi": "3.0.2", "info": { "title": "My other API", "version": "0.1.0" }, "components": { "schemas": { "DefaultObject": { "type": "object", "properties": { "foo": { "type": "string" }, "bar": { "type": "integer" } } } }, "responses": { "DefaultResponse": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DefaultObject" } } } } } } } kin-openapi-0.85.0/openapi3/testdata/nesteddir/000077500000000000000000000000001415236407200213325ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/nesteddir/nestedcomponents.openapi.json000066400000000000000000000024751415236407200272570ustar00rootroot00000000000000{ "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "schemas": { "CustomTestSchema": { "$ref": "nestedcomponentsref.openapi.json#/components/schemas/Name" } }, "responses": { "CustomTestResponse": { "$ref": "nestedcomponentsref.openapi.json#/components/responses/Name" } }, "parameters": { "CustomTestParameter": { "$ref": "nestedcomponentsref.openapi.json#/components/parameters/Name" } }, "examples": { "CustomTestExample": { "$ref": "nestedcomponentsref.openapi.json#/components/examples/Name" } }, "requestBodies": { "CustomTestRequestBody": { "$ref": "nestedcomponentsref.openapi.json#/components/requestBodies/Name" } }, "headers": { "CustomTestHeader": { "$ref": "nestedcomponentsref.openapi.json#/components/headers/Name" } }, "securitySchemes": { "CustomTestSecurityScheme": { "$ref": "nestedcomponentsref.openapi.json#/components/securitySchemes/Name" } } } }kin-openapi-0.85.0/openapi3/testdata/nesteddir/nestedcomponentsref.openapi.json000066400000000000000000000017101415236407200277430ustar00rootroot00000000000000{ "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "schemas": { "Name": { "type": "string" } }, "responses": { "Name": { "description": "description" } }, "parameters": { "Name": { "name": "id", "in": "header" } }, "examples": { "Name": { "description": "description" } }, "requestBodies": { "Name": { "content": {} } }, "headers": { "Name": { "description": "description" } }, "securitySchemes": { "Name": { "type": "cookie", "description": "description" } } } }kin-openapi-0.85.0/openapi3/testdata/pathref.openapi.yml000066400000000000000000000003131415236407200231540ustar00rootroot00000000000000--- openapi: 3.0.0 info: title: 'OAI Specification in YAML' version: 0.0.1 paths: /test: $ref: 'circularref.openapi.yml#/paths/~1test' components: schemas: TestSchema: type: string kin-openapi-0.85.0/openapi3/testdata/recursiveRef/000077500000000000000000000000001415236407200220155ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/recursiveRef/components/000077500000000000000000000000001415236407200242025ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/recursiveRef/components/Bar.yml000066400000000000000000000000321415236407200254240ustar00rootroot00000000000000type: string example: bar kin-openapi-0.85.0/openapi3/testdata/recursiveRef/components/Foo.yml000066400000000000000000000001211415236407200254420ustar00rootroot00000000000000type: object properties: bar: $ref: ../openapi.yml#/components/schemas/Bar kin-openapi-0.85.0/openapi3/testdata/recursiveRef/components/Foo/000077500000000000000000000000001415236407200247255ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml000066400000000000000000000001241415236407200262520ustar00rootroot00000000000000type: object properties: foo: $ref: ../../openapi.yml#/components/schemas/Foo kin-openapi-0.85.0/openapi3/testdata/recursiveRef/openapi.yml000066400000000000000000000004141415236407200241720ustar00rootroot00000000000000openapi: "3.0.3" info: title: Recursive refs example version: "1.0" paths: /foo: $ref: ./paths/foo.yml components: schemas: Foo: $ref: ./components/Foo.yml Foo2: $ref: ./components/Foo/Foo2.yml Bar: $ref: ./components/Bar.yml kin-openapi-0.85.0/openapi3/testdata/recursiveRef/paths/000077500000000000000000000000001415236407200231345ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/recursiveRef/paths/foo.yml000066400000000000000000000003611415236407200244420ustar00rootroot00000000000000get: responses: "200": description: OK content: application/json: schema: type: object properties: foo2: $ref: ../openapi.yml#/components/schemas/Foo2 kin-openapi-0.85.0/openapi3/testdata/refInRef/000077500000000000000000000000001415236407200210515ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/refInRef/messages/000077500000000000000000000000001415236407200226605ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/refInRef/messages/definitions.json000066400000000000000000000001121415236407200260600ustar00rootroot00000000000000{ "definitions": { "External": { "type": "string" } } } kin-openapi-0.85.0/openapi3/testdata/refInRef/messages/request.json000066400000000000000000000002711415236407200252430ustar00rootroot00000000000000{ "type": "object", "required": [ "definition_reference" ], "properties": { "definition_reference": { "$ref": "definitions.json#/definitions/External" } } } kin-openapi-0.85.0/openapi3/testdata/refInRef/messages/response.json000066400000000000000000000001611415236407200254070ustar00rootroot00000000000000{ "type": "object", "properties": { "id": { "type": "integer", "format": "int32" } } } kin-openapi-0.85.0/openapi3/testdata/refInRef/openapi.json000066400000000000000000000012751415236407200234040ustar00rootroot00000000000000{ "openapi": "3.0.3", "info": { "title": "Reference in reference example", "version": "1.0.0" }, "paths": { "/api/test/ref/in/ref": { "post": { "requestBody": { "content": { "application/json": { "schema": { "$ref": "messages/request.json" } } } }, "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "$ref": "messages/response.json" } } } } } } } } } kin-openapi-0.85.0/openapi3/testdata/relativeDocs/000077500000000000000000000000001415236407200217755ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocs/CustomTestExample.yml000066400000000000000000000001151415236407200261430ustar00rootroot00000000000000summary: An example value: | { "example": "hello" }kin-openapi-0.85.0/openapi3/testdata/relativeDocs/CustomTestHeader.yml000066400000000000000000000000301415236407200257340ustar00rootroot00000000000000description: descriptionkin-openapi-0.85.0/openapi3/testdata/relativeDocs/CustomTestParameter.yml000066400000000000000000000000441415236407200264710ustar00rootroot00000000000000name: param in: path required: true kin-openapi-0.85.0/openapi3/testdata/relativeDocs/CustomTestPath.yml000066400000000000000000000004741415236407200254540ustar00rootroot00000000000000get: operationId: findAllDefinitions responses: "200": content: application/json: schema: properties: pets: items: type: array properties: repository: name: string kin-openapi-0.85.0/openapi3/testdata/relativeDocs/CustomTestRequestBody.yml000066400000000000000000000000341415236407200270160ustar00rootroot00000000000000description: example requestkin-openapi-0.85.0/openapi3/testdata/relativeDocs/CustomTestResponse.yml000066400000000000000000000000301415236407200263420ustar00rootroot00000000000000description: descriptionkin-openapi-0.85.0/openapi3/testdata/relativeDocs/CustomTestSchema.yml000066400000000000000000000000141415236407200257460ustar00rootroot00000000000000type: stringkin-openapi-0.85.0/openapi3/testdata/relativeDocs/CustomTestSecurityScheme.yml000066400000000000000000000000301415236407200275000ustar00rootroot00000000000000type: http scheme: basickin-openapi-0.85.0/openapi3/testdata/relativeDocs/openapi/000077500000000000000000000000001415236407200234305ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocs/openapi/openapi.yml000066400000000000000000000002361415236407200256070ustar00rootroot00000000000000openapi: 3.0.0 info: title: "" version: "1.0" paths: {} components: responses: TestResponse: $ref: responses/nesteddir/CustomTestResponse.yml kin-openapi-0.85.0/openapi3/testdata/relativeDocs/openapi/responses/000077500000000000000000000000001415236407200254515ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocs/openapi/responses/custom/000077500000000000000000000000001415236407200267635ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocs/openapi/responses/custom/CustomTestResponse.yml000066400000000000000000000001561415236407200333410ustar00rootroot00000000000000description: description content: application/json: schema: $ref: "../../../CustomTestSchema.yml" kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/000077500000000000000000000000001415236407200247665ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestExample.yml000066400000000000000000000000401415236407200311310ustar00rootroot00000000000000summary: An example value: hellokin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml000066400000000000000000000000241415236407200307300ustar00rootroot00000000000000description: header kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml000066400000000000000000000000251415236407200310120ustar00rootroot00000000000000description: header1 kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml000066400000000000000000000000371415236407200315130ustar00rootroot00000000000000header: description: header1 kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml000066400000000000000000000000251415236407200310130ustar00rootroot00000000000000description: header2 kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml000066400000000000000000000000371415236407200315140ustar00rootroot00000000000000header: description: header2 kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestParameter.yml000066400000000000000000000000441415236407200314620ustar00rootroot00000000000000name: param in: path required: true kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestRequestBody.yml000066400000000000000000000000341415236407200320070ustar00rootroot00000000000000description: example requestkin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestResponse.yml000066400000000000000000000000301415236407200313330ustar00rootroot00000000000000description: descriptionkin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestSchema.yml000066400000000000000000000000141415236407200307370ustar00rootroot00000000000000type: stringkin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/000077500000000000000000000000001415236407200264215ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/openapi.yml000066400000000000000000000003241415236407200305760ustar00rootroot00000000000000openapi: 3.0.0 info: title: "" version: "1.0" paths: /pets/{id}: $ref: "paths/nesteddir/CustomTestPath.yml" /pets/{id}/{city}: $ref: "paths/nesteddir/morenested/CustomTestPath.yml" components: {} kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/000077500000000000000000000000001415236407200275405ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/000077500000000000000000000000001415236407200315215ustar00rootroot00000000000000CustomTestPath.yml000066400000000000000000000012131415236407200351110ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddirpatch: description: "Modify a pet" parameters: - $ref: "../../../CustomTestParameter.yml" requestBody: $ref: "../../../CustomTestRequestBody.yml" responses: 200: description: "success" headers: X-Rate-Limit-Reset: $ref: "../../../CustomTestHeader.yml" X-Another: $ref: ../../../CustomTestHeader1.yml X-And-Another: $ref: ../../../CustomTestHeader2.yml content: application/json: schema: $ref: "../../../CustomTestSchema.yml" examples: CustomTestExample: $ref: "../../../CustomTestExample.yml" kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/000077500000000000000000000000001415236407200336665ustar00rootroot00000000000000CustomTestPath.yml000066400000000000000000000012721415236407200372630ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenestedpatch: description: "Modify a pet" parameters: - $ref: "../../../../CustomTestParameter.yml" requestBody: $ref: "../../../../CustomTestRequestBody.yml" responses: 200: description: "success" headers: X-Rate-Limit-Reset: $ref: "../../../../CustomTestHeader.yml" X-Another: $ref: '../../../../CustomTestHeader1bis.yml#/header' X-And-Another: $ref: '../../../../CustomTestHeader2bis.yml#/header' content: application/json: schema: $ref: "../../../../CustomTestSchema.yml" examples: CustomTestExample: $ref: "../../../../CustomTestExample.yml" kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/responses/000077500000000000000000000000001415236407200304425ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/responses/nesteddir/000077500000000000000000000000001415236407200324235ustar00rootroot00000000000000CustomTestResponse.yml000066400000000000000000000003751415236407200367250ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3/testdata/relativeDocsUseDocumentPath/openapi/responses/nesteddirdescription: description content: application/json: schema: $ref: "../../../CustomTestSchema.yml" example: $ref: "../../../CustomTestExample.yml" headers: X-Rate-Limit-Reset: $ref: "../../../CustomTestHeader.yml" kin-openapi-0.85.0/openapi3/testdata/singleresponse.openapi.json000066400000000000000000000000741415236407200247370ustar00rootroot00000000000000{ "description": "this is a single response definition" } kin-openapi-0.85.0/openapi3/testdata/singleresponse.openapi.yml000066400000000000000000000000661415236407200245700ustar00rootroot00000000000000--- description: this is a single response definition kin-openapi-0.85.0/openapi3/testdata/spec.yaml000066400000000000000000000003351415236407200211700ustar00rootroot00000000000000openapi: 3.0.1 info: version: 1.0.0 title: Some Swagger license: name: MIT paths: {} components: schemas: Test: type: object properties: test: $ref: 'ext.json#/definitions/b' kin-openapi-0.85.0/openapi3/testdata/test.openapi.json000066400000000000000000000003661415236407200226620ustar00rootroot00000000000000{ "openapi": "3.0.0", "info": { "title": "", "version": "0.0.1" }, "paths": {}, "components": { "schemas": { "TestSchema": { "type": "string" } } } } kin-openapi-0.85.0/openapi3/testdata/test.openapi.yml000066400000000000000000000002231415236407200225020ustar00rootroot00000000000000--- openapi: 3.0.0 info: title: 'OAI Specification in YAML' version: 0.0.1 paths: {} components: schemas: TestSchema: type: string kin-openapi-0.85.0/openapi3/testdata/testpath.yaml000066400000000000000000000004521415236407200220720ustar00rootroot00000000000000paths: /testpath: get: responses: "200": $ref: "#/components/responses/testpath_200_response" components: responses: testpath_200_response: description: a custom response content: application/json: schema: type: string kin-openapi-0.85.0/openapi3/testdata/testref.openapi.json000066400000000000000000000005641415236407200233570ustar00rootroot00000000000000{ "openapi": "3.0.0", "info": { "title": "OAI Specification w/ refs in JSON", "x-my-extension": {"k": 42}, "version": "1" }, "paths": {}, "components": { "schemas": { "AnotherTestSchema": { "$ref": "components.openapi.json#/components/schemas/CustomTestSchema" } } } }kin-openapi-0.85.0/openapi3/testdata/testref.openapi.yml000066400000000000000000000003641415236407200232050ustar00rootroot00000000000000--- openapi: 3.0.0 info: title: 'OAI Specification w/ refs in YAML' # x-my-extension: {k: 42}, version: '1' paths: {} components: schemas: AnotherTestSchema: $ref: 'components.openapi.yml#/components/schemas/CustomTestSchema' kin-openapi-0.85.0/openapi3/testdata/testrefsinglecomponent.openapi.json000066400000000000000000000004131415236407200264750ustar00rootroot00000000000000{ "openapi": "3.0.0", "info": { "title": "", "version": "1" }, "paths": {}, "components": { "responses": { "SomeResponse": { "$ref": "singleresponse.openapi.json" } } } } kin-openapi-0.85.0/openapi3/testdata/testrefsinglecomponent.openapi.yml000066400000000000000000000002631415236407200263300ustar00rootroot00000000000000--- openapi: 3.0.0 info: title: 'OAI Specification w/ refs in YAML' version: '1' paths: {} components: responses: SomeResponse: "$ref": singleresponse.openapi.yml kin-openapi-0.85.0/openapi3/unique_items_checker_test.go000066400000000000000000000016761415236407200233330ustar00rootroot00000000000000package openapi3_test import ( "strings" "testing" "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) func TestRegisterArrayUniqueItemsChecker(t *testing.T) { var ( schema = openapi3.Schema{ Type: "array", UniqueItems: true, Items: openapi3.NewStringSchema().NewRef(), } val = []interface{}{"1", "2", "3"} ) // Fist checked by predefined function err := schema.VisitJSON(val) require.NoError(t, err) // Register a function will always return false when check if a // slice has unique items, then use a slice indeed has unique // items to verify that check unique items will failed. openapi3.RegisterArrayUniqueItemsChecker(func(items []interface{}) bool { return false }) defer openapi3.RegisterArrayUniqueItemsChecker(nil) // Reset for other tests err = schema.VisitJSON(val) require.Error(t, err) require.True(t, strings.HasPrefix(err.Error(), "duplicate items found")) } kin-openapi-0.85.0/openapi3filter/000077500000000000000000000000001415236407200167465ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3filter/authentication_input.go000066400000000000000000000013141415236407200235320ustar00rootroot00000000000000package openapi3filter import ( "fmt" "github.com/getkin/kin-openapi/openapi3" ) type AuthenticationInput struct { RequestValidationInput *RequestValidationInput SecuritySchemeName string SecurityScheme *openapi3.SecurityScheme Scopes []string } func (input *AuthenticationInput) NewError(err error) error { if err == nil { if len(input.Scopes) == 0 { err = fmt.Errorf("security requirement %q failed", input.SecuritySchemeName) } else { err = fmt.Errorf("security requirement %q (scopes: %+v) failed", input.SecuritySchemeName, input.Scopes) } } return &RequestError{ Input: input.RequestValidationInput, Reason: "authorization failed", Err: err, } } kin-openapi-0.85.0/openapi3filter/errors.go000066400000000000000000000034561415236407200206210ustar00rootroot00000000000000package openapi3filter import ( "fmt" "github.com/getkin/kin-openapi/openapi3" ) var _ error = &RequestError{} // RequestError is returned by ValidateRequest when request does not match OpenAPI spec type RequestError struct { Input *RequestValidationInput Parameter *openapi3.Parameter RequestBody *openapi3.RequestBody Reason string Err error } var _ interface{ Unwrap() error } = RequestError{} func (err *RequestError) Error() string { reason := err.Reason if e := err.Err; e != nil { if len(reason) == 0 { reason = e.Error() } else { reason += ": " + e.Error() } } if v := err.Parameter; v != nil { return fmt.Sprintf("parameter %q in %s has an error: %s", v.Name, v.In, reason) } else if v := err.RequestBody; v != nil { return fmt.Sprintf("request body has an error: %s", reason) } else { return reason } } func (err RequestError) Unwrap() error { return err.Err } var _ error = &ResponseError{} // ResponseError is returned by ValidateResponse when response does not match OpenAPI spec type ResponseError struct { Input *ResponseValidationInput Reason string Err error } var _ interface{ Unwrap() error } = ResponseError{} func (err *ResponseError) Error() string { reason := err.Reason if e := err.Err; e != nil { if len(reason) == 0 { reason = e.Error() } else { reason += ": " + e.Error() } } return reason } func (err ResponseError) Unwrap() error { return err.Err } var _ error = &SecurityRequirementsError{} // SecurityRequirementsError is returned by ValidateSecurityRequirements // when no requirement is met. type SecurityRequirementsError struct { SecurityRequirements openapi3.SecurityRequirements Errors []error } func (err *SecurityRequirementsError) Error() string { return "Security requirements failed" } kin-openapi-0.85.0/openapi3filter/fixtures/000077500000000000000000000000001415236407200206175ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3filter/fixtures/petstore.json000066400000000000000000001013521415236407200233610ustar00rootroot00000000000000{ "openapi": "3.0.0", "servers": [ { "url": "https://petstore.swagger.io/v2" }, { "url": "http://petstore.swagger.io/v2" } ], "info": { "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", "version": "1.0.0", "title": "Swagger Petstore", "termsOfService": "http://swagger.io/terms/", "contact": { "email": "apiteam@swagger.io" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" } }, "tags": [ { "name": "pet", "description": "Everything about your Pets", "externalDocs": { "description": "Find out more", "url": "http://swagger.io" } }, { "name": "store", "description": "Access to Petstore orders" }, { "name": "user", "description": "Operations about user", "externalDocs": { "description": "Find out more about our store", "url": "http://swagger.io" } } ], "paths": { "/pet": { "post": { "tags": [ "pet" ], "summary": "Add a new pet to the store", "description": "", "operationId": "addPet", "responses": { "405": { "description": "Invalid input" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "requestBody": { "$ref": "#/components/requestBodies/PetWithRequired" }, "parameters": [ { "schema": { "type": "string", "enum": [ "demo", "prod" ] }, "in": "header", "name": "x-environment", "description": "Where to send the data for processing" } ] }, "patch": { "tags": [ "pet" ], "summary": "Update an existing pet", "description": "", "operationId": "updatePet", "responses": { "400": { "description": "Invalid ID supplied" }, "404": { "description": "Pet not found" }, "405": { "description": "Validation exception" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "requestBody": { "$ref": "#/components/requestBodies/Pet" } } }, "/pet2": { "post": { "tags": [ "pet" ], "summary": "Add a new pet to the store", "description": "", "operationId": "addPet", "responses": { "405": { "description": "Invalid input" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "requestBody": { "$ref": "#/components/requestBodies/PetAllOfRequiredProperties" } } }, "/pet/findByStatus": { "get": { "tags": [ "pet" ], "summary": "Finds Pets by status", "description": "Multiple status values can be provided with comma separated strings", "operationId": "findPetsByStatus", "parameters": [ { "name": "status", "in": "query", "description": "Status values that need to be considered for filter", "required": true, "explode": true, "schema": { "type": "array", "items": { "type": "string", "enum": [ "available", "pending", "sold" ], "default": "available" } } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "type": "array", "items": { "allOf": [ {"$ref": "#/components/schemas/Pet"}, {"$ref": "#/components/schemas/PetRequiredProperties"} ] } } }, "application/json": { "schema": { "type": "array", "items": { "allOf": [ {"$ref": "#/components/schemas/Pet"}, {"$ref": "#/components/schemas/PetRequiredProperties"} ] } } } } }, "400": { "description": "Invalid status value" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] } }, "/pet/findByTags": { "get": { "tags": [ "pet" ], "summary": "Finds Pets by tags", "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", "operationId": "findPetsByTags", "parameters": [ { "name": "tags", "in": "query", "description": "Tags to filter by", "required": true, "explode": true, "schema": { "type": "array", "items": { "type": "string" } } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } }, "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } } } }, "400": { "description": "Invalid tag value" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "deprecated": true } }, "/pet/findByIds": { "get": { "tags": [ "pet" ], "summary": "Finds Pets by IDs", "description": "Muliple IDs can be provided with comma separated strings. Use id1, id2, id3 for testing.", "operationId": "findPetsByIds", "parameters": [ { "name": "ids", "in": "query", "description": "IDs to filter by", "required": true, "explode": false, "schema": { "type": "array", "items": { "type": "integer", "format": "int64" } } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } }, "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } } } }, "400": { "description": "Invalid ID value" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "deprecated": true } }, "/pet/findByKind": { "get": { "tags": [ "pet" ], "summary": "Finds Pets by Kind", "description": "Muliple kinds can be provided with comma separated strings. Use id1, id2, id3 for testing.", "operationId": "findPetsByKind", "parameters": [ { "name": "kind", "in": "query", "description": "Kinds to filter by", "required": true, "explode": false, "style": "pipeDelimited", "schema": { "type": "array", "items": { "type": "string", "enum": [ "dog", "cat", "turtle", "bird,with,commas" ] } } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } }, "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } } } } }, "400": { "description": "Invalid ID value" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "deprecated": true } }, "/pet/{petId}": { "get": { "tags": [ "pet" ], "summary": "Find pet by ID", "description": "Returns a single pet", "operationId": "getPetById", "parameters": [ { "name": "petId", "in": "path", "description": "ID of pet to return", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } } }, "400": { "description": "Invalid ID supplied" }, "404": { "description": "Pet not found" } }, "security": [ { "api_key": [] } ] }, "post": { "tags": [ "pet" ], "summary": "Updates a pet in the store with form data", "description": "", "operationId": "updatePetWithForm", "parameters": [ { "name": "petId", "in": "path", "description": "ID of pet that needs to be updated", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "405": { "description": "Invalid input" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "requestBody": { "content": { "application/x-www-form-urlencoded": { "schema": { "type": "object", "properties": { "name": { "description": "Updated name of the pet", "type": "string" }, "status": { "description": "Updated status of the pet", "type": "string" } } } } } } }, "delete": { "tags": [ "pet" ], "summary": "Deletes a pet", "description": "", "operationId": "deletePet", "parameters": [ { "name": "api_key", "in": "header", "required": false, "schema": { "type": "string" } }, { "name": "petId", "in": "path", "description": "Pet id to delete", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "400": { "description": "Invalid ID supplied" }, "404": { "description": "Pet not found" } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ] } }, "/pet/{petId}/uploadImage": { "post": { "tags": [ "pet" ], "summary": "uploads an image", "description": "", "operationId": "uploadFile", "parameters": [ { "name": "petId", "in": "path", "description": "ID of pet to update", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiResponse" } } } } }, "security": [ { "petstore_auth": [ "write:pets", "read:pets" ] } ], "requestBody": { "content": { "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } } } } }, "/store/inventory": { "get": { "tags": [ "store" ], "summary": "Returns pet inventories by status", "description": "Returns a map of status codes to quantities", "operationId": "getInventory", "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": { "type": "integer", "format": "int32" } } } } } }, "security": [ { "api_key": [] } ] } }, "/store/order": { "post": { "tags": [ "store" ], "summary": "Place an order for a pet", "description": "", "operationId": "placeOrder", "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/Order" } }, "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } } }, "400": { "description": "Invalid Order" } }, "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } }, "description": "order placed for purchasing the pet", "required": true } } }, "/store/order/{orderId}": { "get": { "tags": [ "store" ], "summary": "Find purchase order by ID", "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", "operationId": "getOrderById", "parameters": [ { "name": "orderId", "in": "path", "description": "ID of pet that needs to be fetched", "required": true, "schema": { "type": "integer", "format": "int64", "minimum": 1, "maximum": 10 } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/Order" } }, "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } } }, "400": { "description": "Invalid ID supplied" }, "404": { "description": "Order not found" } } }, "delete": { "tags": [ "store" ], "summary": "Delete purchase order by ID", "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", "operationId": "deleteOrder", "parameters": [ { "name": "orderId", "in": "path", "description": "ID of the order that needs to be deleted", "required": true, "schema": { "type": "integer", "format": "int64", "minimum": 1 } } ], "responses": { "400": { "description": "Invalid ID supplied" }, "404": { "description": "Order not found" } } } }, "/user": { "post": { "tags": [ "user" ], "summary": "Create user", "description": "This can only be done by the logged in user.", "operationId": "createUser", "responses": { "default": { "description": "successful operation" } }, "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } }, "description": "Created user object", "required": true } } }, "/user/createWithArray": { "post": { "tags": [ "user" ], "summary": "Creates list of users with given input array", "description": "", "operationId": "createUsersWithArrayInput", "responses": { "default": { "description": "successful operation" } }, "requestBody": { "$ref": "#/components/requestBodies/UserArray" } } }, "/user/createWithList": { "post": { "tags": [ "user" ], "summary": "Creates list of users with given input array", "description": "", "operationId": "createUsersWithListInput", "responses": { "default": { "description": "successful operation" } }, "requestBody": { "$ref": "#/components/requestBodies/UserArray" } } }, "/user/login": { "get": { "tags": [ "user" ], "summary": "Logs user into the system", "description": "", "operationId": "loginUser", "parameters": [ { "name": "username", "in": "query", "description": "The user name for login", "required": true, "schema": { "type": "string" } }, { "name": "password", "in": "query", "description": "The password for login in clear text", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "successful operation", "headers": { "X-Rate-Limit": { "description": "calls per hour allowed by the user", "schema": { "type": "integer", "format": "int32" } }, "X-Expires-After": { "description": "date in UTC when token expires", "schema": { "type": "string", "format": "date-time" } } }, "content": { "application/xml": { "schema": { "type": "string" } }, "application/json": { "schema": { "type": "string" } } } }, "400": { "description": "Invalid username/password supplied" } } } }, "/user/logout": { "get": { "tags": [ "user" ], "summary": "Logs out current logged in user session", "description": "", "operationId": "logoutUser", "responses": { "default": { "description": "successful operation" } } } }, "/user/{username}": { "get": { "tags": [ "user" ], "summary": "Get user by user name", "description": "", "operationId": "getUserByName", "parameters": [ { "name": "username", "in": "path", "description": "The name that needs to be fetched. Use user1 for testing. ", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "successful operation", "content": { "application/xml": { "schema": { "$ref": "#/components/schemas/User" } }, "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } }, "400": { "description": "Invalid username supplied" }, "404": { "description": "User not found" } } }, "put": { "tags": [ "user" ], "summary": "Updated user", "description": "This can only be done by the logged in user.", "operationId": "updateUser", "parameters": [ { "name": "username", "in": "path", "description": "name that need to be updated", "required": true, "schema": { "type": "string" } } ], "responses": { "400": { "description": "Invalid user supplied" }, "404": { "description": "User not found" } }, "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } }, "description": "Updated user object", "required": true } }, "delete": { "tags": [ "user" ], "summary": "Delete user", "description": "This can only be done by the logged in user.", "operationId": "deleteUser", "parameters": [ { "name": "username", "in": "path", "description": "The name that needs to be deleted", "required": true, "schema": { "type": "string" } } ], "responses": { "400": { "description": "Invalid username supplied" }, "404": { "description": "User not found" } } } } }, "externalDocs": { "description": "Find out more about Swagger", "url": "http://swagger.io" }, "components": { "schemas": { "Order": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "petId": { "type": "integer", "format": "int64" }, "quantity": { "type": "integer", "format": "int32" }, "shipDate": { "type": "string", "format": "date-time" }, "status": { "type": "string", "description": "Order Status", "enum": [ "placed", "approved", "delivered" ] }, "complete": { "type": "boolean", "default": false } }, "xml": { "name": "Order" } }, "User": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "username": { "type": "string" }, "firstName": { "type": "string" }, "lastName": { "type": "string" }, "email": { "type": "string" }, "password": { "type": "string" }, "phone": { "type": "string" }, "userStatus": { "type": "integer", "format": "int32", "description": "User Status" } }, "xml": { "name": "User" } }, "Category": { "type": "object", "required": [ "name" ], "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" }, "tags": { "type": "array", "xml": { "name": "tag", "wrapped": true }, "items": { "$ref": "#/components/schemas/Tag" } } }, "xml": { "name": "Category" } }, "Tag": { "type": "object", "required": [ "name" ], "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" } }, "xml": { "name": "Tag" } }, "ApiResponse": { "type": "object", "properties": { "code": { "type": "integer", "format": "int32" }, "type": { "type": "string" }, "message": { "type": "string" } } }, "Pet": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64", "readOnly": true }, "category": { "$ref": "#/components/schemas/Category" }, "name": { "type": "string", "example": "doggie" }, "photoUrls": { "type": "array", "xml": { "name": "photoUrl", "wrapped": true }, "items": { "type": "string" } }, "tags": { "type": "array", "xml": { "name": "tag", "wrapped": true }, "items": { "$ref": "#/components/schemas/Tag" } }, "status": { "type": "string", "description": "pet status in the store", "enum": [ "available", "pending", "sold" ] } }, "xml": { "name": "Pet" } }, "PetRequiredProperties": { "type": "object", "required": [ "name", "photoUrls" ] }, "PetWithRequired": { "type": "object", "required": [ "name", "photoUrls" ], "properties": { "id": { "type": "integer", "format": "int64", "readOnly": true }, "category": { "$ref": "#/components/schemas/Category" }, "name": { "type": "string", "example": "doggie" }, "photoUrls": { "type": "array", "xml": { "name": "photoUrl", "wrapped": true }, "items": { "type": "string" } }, "tags": { "type": "array", "xml": { "name": "tag", "wrapped": true }, "items": { "$ref": "#/components/schemas/Tag" } }, "status": { "type": "string", "description": "pet status in the store", "enum": [ "available", "pending", "sold" ] } }, "xml": { "name": "Pet" } } }, "requestBodies": { "PetAllOfRequiredProperties": { "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Pet" }, { "$ref": "#/components/schemas/PetRequiredProperties" } ] } }, "application/xml": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Pet" }, { "$ref": "#/components/schemas/PetRequiredProperties" } ] } } }, "required": true }, "Pet": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/Pet" } } }, "description": "Pet object that needs to be added to the store", "required": true }, "PetWithRequired": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PetWithRequired" } }, "application/xml": { "schema": { "$ref": "#/components/schemas/PetWithRequired" } } }, "description": "Pet object that needs to be added to the store", "required": true }, "UserArray": { "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/User" } } } }, "description": "List of user object", "required": true } }, "securitySchemes": { "petstore_auth": { "type": "oauth2", "flows": { "implicit": { "authorizationUrl": "https://petstore.swagger.io/oauth/dialog", "scopes": { "write:pets": "modify pets in your account", "read:pets": "read your pets" } } } }, "api_key": { "type": "apiKey", "name": "api_key", "in": "header" } } } } kin-openapi-0.85.0/openapi3filter/internal.go000066400000000000000000000003101415236407200211030ustar00rootroot00000000000000package openapi3filter import ( "strings" ) func parseMediaType(contentType string) string { i := strings.IndexByte(contentType, ';') if i < 0 { return contentType } return contentType[:i] } kin-openapi-0.85.0/openapi3filter/issue436_test.go000066400000000000000000000054561415236407200217330ustar00rootroot00000000000000package openapi3filter_test import ( "bytes" "context" "io" "mime/multipart" "net/http" "net/textproto" "strings" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" "github.com/getkin/kin-openapi/routers/gorillamux" ) func Example_validateMultipartFormData() { const spec = ` openapi: 3.0.0 info: title: 'Validator' version: 0.0.1 paths: /test: post: requestBody: required: true content: multipart/form-data: schema: type: object required: - file properties: file: type: string format: binary categories: type: array items: $ref: "#/components/schemas/Category" responses: '200': description: Created components: schemas: Category: type: object properties: name: type: string required: - name ` loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(spec)) if err != nil { panic(err) } if err = doc.Validate(loader.Context); err != nil { panic(err) } router, err := gorillamux.NewRouter(doc) if err != nil { panic(err) } body := &bytes.Buffer{} writer := multipart.NewWriter(body) { // Add a single "categories" item as part data h := make(textproto.MIMEHeader) h.Set("Content-Disposition", `form-data; name="categories"`) h.Set("Content-Type", "application/json") fw, err := writer.CreatePart(h) if err != nil { panic(err) } if _, err = io.Copy(fw, strings.NewReader(`{"name": "foo"}`)); err != nil { panic(err) } } { // Add a single "categories" item as part data, again h := make(textproto.MIMEHeader) h.Set("Content-Disposition", `form-data; name="categories"`) h.Set("Content-Type", "application/json") fw, err := writer.CreatePart(h) if err != nil { panic(err) } if _, err = io.Copy(fw, strings.NewReader(`{"name": "bar"}`)); err != nil { panic(err) } } { // Add file data fw, err := writer.CreateFormFile("file", "hello.txt") if err != nil { panic(err) } if _, err = io.Copy(fw, strings.NewReader("hello")); err != nil { panic(err) } } writer.Close() req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes())) if err != nil { panic(err) } req.Header.Set("Content-Type", writer.FormDataContentType()) route, pathParams, err := router.FindRoute(req) if err != nil { panic(err) } if err = openapi3filter.ValidateRequestBody( context.Background(), &openapi3filter.RequestValidationInput{ Request: req, PathParams: pathParams, Route: route, }, route.Operation.RequestBody.Value, ); err != nil { panic(err) } // Output: } kin-openapi-0.85.0/openapi3filter/options.go000066400000000000000000000013321415236407200207670ustar00rootroot00000000000000package openapi3filter // DefaultOptions do not set an AuthenticationFunc. // A spec with security schemes defined will not pass validation // unless an AuthenticationFunc is defined. var DefaultOptions = &Options{} // Options used by ValidateRequest and ValidateResponse type Options struct { // Set ExcludeRequestBody so ValidateRequest skips request body validation ExcludeRequestBody bool // Set ExcludeResponseBody so ValidateResponse skips response body validation ExcludeResponseBody bool // Set IncludeResponseStatus so ValidateResponse fails on response // status not defined in OpenAPI spec IncludeResponseStatus bool MultiError bool // See NoopAuthenticationFunc AuthenticationFunc AuthenticationFunc } kin-openapi-0.85.0/openapi3filter/req_resp_decoder.go000066400000000000000000000746141415236407200226160ustar00rootroot00000000000000package openapi3filter import ( "encoding/json" "errors" "fmt" "io" "io/ioutil" "mime" "mime/multipart" "net/http" "net/url" "regexp" "strconv" "strings" "gopkg.in/yaml.v2" "github.com/getkin/kin-openapi/openapi3" ) // ParseErrorKind describes a kind of ParseError. // The type simplifies comparison of errors. type ParseErrorKind int const ( // KindOther describes an untyped parsing error. KindOther ParseErrorKind = iota // KindUnsupportedFormat describes an error that happens when a value has an unsupported format. KindUnsupportedFormat // KindInvalidFormat describes an error that happens when a value does not conform a format // that is required by a serialization method. KindInvalidFormat ) // ParseError describes errors which happens while parse operation's parameters, requestBody, or response. type ParseError struct { Kind ParseErrorKind Value interface{} Reason string Cause error path []interface{} } var _ interface{ Unwrap() error } = ParseError{} func (e *ParseError) Error() string { var msg []string if p := e.Path(); len(p) > 0 { var arr []string for _, v := range p { arr = append(arr, fmt.Sprintf("%v", v)) } msg = append(msg, fmt.Sprintf("path %v", strings.Join(arr, "."))) } msg = append(msg, e.innerError()) return strings.Join(msg, ": ") } func (e *ParseError) innerError() string { var msg []string if e.Value != nil { msg = append(msg, fmt.Sprintf("value %v", e.Value)) } if e.Reason != "" { msg = append(msg, e.Reason) } if e.Cause != nil { if v, ok := e.Cause.(*ParseError); ok { msg = append(msg, v.innerError()) } else { msg = append(msg, e.Cause.Error()) } } return strings.Join(msg, ": ") } // RootCause returns a root cause of ParseError. func (e *ParseError) RootCause() error { if v, ok := e.Cause.(*ParseError); ok { return v.RootCause() } return e.Cause } func (e ParseError) Unwrap() error { return e.Cause } // Path returns a path to the root cause. func (e *ParseError) Path() []interface{} { var path []interface{} if v, ok := e.Cause.(*ParseError); ok { p := v.Path() if len(p) > 0 { path = append(path, p...) } } if len(e.path) > 0 { path = append(path, e.path...) } return path } func invalidSerializationMethodErr(sm *openapi3.SerializationMethod) error { return fmt.Errorf("invalid serialization method: style=%q, explode=%v", sm.Style, sm.Explode) } // Decodes a parameter defined via the content property as an object. It uses // the user specified decoder, or our build-in decoder for application/json func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationInput) ( value interface{}, schema *openapi3.Schema, err error) { var paramValues []string var found bool switch param.In { case openapi3.ParameterInPath: var paramValue string if paramValue, found = input.PathParams[param.Name]; found { paramValues = []string{paramValue} } case openapi3.ParameterInQuery: paramValues, found = input.GetQueryParams()[param.Name] case openapi3.ParameterInHeader: if paramValue := input.Request.Header.Get(http.CanonicalHeaderKey(param.Name)); paramValue != "" { paramValues = []string{paramValue} found = true } case openapi3.ParameterInCookie: var cookie *http.Cookie if cookie, err = input.Request.Cookie(param.Name); err == http.ErrNoCookie { found = false } else if err != nil { return } else { paramValues = []string{cookie.Value} found = true } default: err = fmt.Errorf("unsupported parameter.in: %q", param.In) return } if !found { if param.Required { err = fmt.Errorf("parameter %q is required, but missing", param.Name) } return } decoder := input.ParamDecoder if decoder == nil { decoder = defaultContentParameterDecoder } value, schema, err = decoder(param, paramValues) return } func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) ( outValue interface{}, outSchema *openapi3.Schema, err error) { // Only query parameters can have multiple values. if len(values) > 1 && param.In != openapi3.ParameterInQuery { err = fmt.Errorf("%s parameter %q cannot have multiple values", param.In, param.Name) return } content := param.Content if content == nil { err = fmt.Errorf("parameter %q expected to have content", param.Name) return } // We only know how to decode a parameter if it has one content, application/json if len(content) != 1 { err = fmt.Errorf("multiple content types for parameter %q", param.Name) return } mt := content.Get("application/json") if mt == nil { err = fmt.Errorf("parameter %q has no content schema", param.Name) return } outSchema = mt.Schema.Value if len(values) == 1 { if err = json.Unmarshal([]byte(values[0]), &outValue); err != nil { err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return } } else { outArray := make([]interface{}, 0, len(values)) for _, v := range values { var item interface{} if err = json.Unmarshal([]byte(v), &item); err != nil { err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return } outArray = append(outArray, item) } outValue = outArray } return } type valueDecoder interface { DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) } // decodeStyledParameter returns a value of an operation's parameter from HTTP request for // parameters defined using the style format. // The function returns ParseError when HTTP request contains an invalid value of a parameter. func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationInput) (interface{}, error) { sm, err := param.SerializationMethod() if err != nil { return nil, err } var dec valueDecoder switch param.In { case openapi3.ParameterInPath: if len(input.PathParams) == 0 { return nil, nil } dec = &pathParamDecoder{pathParams: input.PathParams} case openapi3.ParameterInQuery: if len(input.GetQueryParams()) == 0 { return nil, nil } dec = &urlValuesDecoder{values: input.GetQueryParams()} case openapi3.ParameterInHeader: dec = &headerParamDecoder{header: input.Request.Header} case openapi3.ParameterInCookie: dec = &cookieParamDecoder{req: input.Request} default: return nil, fmt.Errorf("unsupported parameter's 'in': %s", param.In) } return decodeValue(dec, param.Name, sm, param.Schema, param.Required) } func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (interface{}, error) { if len(schema.Value.AllOf) > 0 { var value interface{} var err error for _, sr := range schema.Value.AllOf { value, err = decodeValue(dec, param, sm, sr, required) if value == nil || err != nil { break } } return value, err } if len(schema.Value.AnyOf) > 0 { for _, sr := range schema.Value.AnyOf { value, _ := decodeValue(dec, param, sm, sr, required) if value != nil { return value, nil } } if required { return nil, fmt.Errorf("decoding anyOf for parameter %q failed", param) } return nil, nil } if len(schema.Value.OneOf) > 0 { isMatched := 0 var value interface{} for _, sr := range schema.Value.OneOf { v, _ := decodeValue(dec, param, sm, sr, required) if v != nil { value = v isMatched++ } } if isMatched == 1 { return value, nil } else if isMatched > 1 { return nil, fmt.Errorf("decoding oneOf failed: %d schemas matched", isMatched) } if required { return nil, fmt.Errorf("decoding oneOf failed: %q is required", param) } return nil, nil } if schema.Value.Not != nil { // TODO(decode not): handle decoding "not" JSON Schema return nil, errors.New("not implemented: decoding 'not'") } if schema.Value.Type != "" { var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) switch schema.Value.Type { case "array": decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { return dec.DecodeArray(param, sm, schema) } case "object": decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { return dec.DecodeObject(param, sm, schema) } default: decodeFn = dec.DecodePrimitive } return decodeFn(param, sm, schema) } return nil, nil } // pathParamDecoder decodes values of path parameters. type pathParamDecoder struct { pathParams map[string]string } func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { var prefix string switch sm.Style { case "simple": // A prefix is empty for style "simple". case "label": prefix = "." case "matrix": prefix = ";" + param + "=" default: return nil, invalidSerializationMethodErr(sm) } if d.pathParams == nil { // HTTP request does not contains a value of the target path parameter. return nil, nil } raw, ok := d.pathParams[param] if !ok || raw == "" { // HTTP request does not contains a value of the target path parameter. return nil, nil } src, err := cutPrefix(raw, prefix) if err != nil { return nil, err } return parsePrimitive(src, schema) } func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { var prefix, delim string switch { case sm.Style == "simple": delim = "," case sm.Style == "label" && !sm.Explode: prefix = "." delim = "," case sm.Style == "label" && sm.Explode: prefix = "." delim = "." case sm.Style == "matrix" && !sm.Explode: prefix = ";" + param + "=" delim = "," case sm.Style == "matrix" && sm.Explode: prefix = ";" + param + "=" delim = ";" + param + "=" default: return nil, invalidSerializationMethodErr(sm) } if d.pathParams == nil { // HTTP request does not contains a value of the target path parameter. return nil, nil } raw, ok := d.pathParams[param] if !ok || raw == "" { // HTTP request does not contains a value of the target path parameter. return nil, nil } src, err := cutPrefix(raw, prefix) if err != nil { return nil, err } return parseArray(strings.Split(src, delim), schema) } func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { var prefix, propsDelim, valueDelim string switch { case sm.Style == "simple" && !sm.Explode: propsDelim = "," valueDelim = "," case sm.Style == "simple" && sm.Explode: propsDelim = "," valueDelim = "=" case sm.Style == "label" && !sm.Explode: prefix = "." propsDelim = "," valueDelim = "," case sm.Style == "label" && sm.Explode: prefix = "." propsDelim = "." valueDelim = "=" case sm.Style == "matrix" && !sm.Explode: prefix = ";" + param + "=" propsDelim = "," valueDelim = "," case sm.Style == "matrix" && sm.Explode: prefix = ";" propsDelim = ";" valueDelim = "=" default: return nil, invalidSerializationMethodErr(sm) } if d.pathParams == nil { // HTTP request does not contains a value of the target path parameter. return nil, nil } raw, ok := d.pathParams[param] if !ok || raw == "" { // HTTP request does not contains a value of the target path parameter. return nil, nil } src, err := cutPrefix(raw, prefix) if err != nil { return nil, err } props, err := propsFromString(src, propsDelim, valueDelim) if err != nil { return nil, err } return makeObject(props, schema) } // cutPrefix validates that a raw value of a path parameter has the specified prefix, // and returns a raw value without the prefix. func cutPrefix(raw, prefix string) (string, error) { if prefix == "" { return raw, nil } if len(raw) < len(prefix) || raw[:len(prefix)] != prefix { return "", &ParseError{ Kind: KindInvalidFormat, Value: raw, Reason: fmt.Sprintf("a value must be prefixed with %q", prefix), } } return raw[len(prefix):], nil } // urlValuesDecoder decodes values of query parameters. type urlValuesDecoder struct { values url.Values } func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { if sm.Style != "form" { return nil, invalidSerializationMethodErr(sm) } values := d.values[param] if len(values) == 0 { // HTTP request does not contain a value of the target query parameter. return nil, nil } return parsePrimitive(values[0], schema) } func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { if sm.Style == "deepObject" { return nil, invalidSerializationMethodErr(sm) } values := d.values[param] if len(values) == 0 { // HTTP request does not contain a value of the target query parameter. return nil, nil } if !sm.Explode { var delim string switch sm.Style { case "form": delim = "," case "spaceDelimited": delim = " " case "pipeDelimited": delim = "|" } values = strings.Split(values[0], delim) } return parseArray(values, schema) } func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { var propsFn func(url.Values) (map[string]string, error) switch sm.Style { case "form": propsFn = func(params url.Values) (map[string]string, error) { if len(params) == 0 { // HTTP request does not contain query parameters. return nil, nil } if sm.Explode { props := make(map[string]string) for key, values := range params { props[key] = values[0] } return props, nil } values := params[param] if len(values) == 0 { // HTTP request does not contain a value of the target query parameter. return nil, nil } return propsFromString(values[0], ",", ",") } case "deepObject": propsFn = func(params url.Values) (map[string]string, error) { props := make(map[string]string) for key, values := range params { groups := regexp.MustCompile(fmt.Sprintf("%s\\[(.+?)\\]", param)).FindAllStringSubmatch(key, -1) if len(groups) == 0 { // A query parameter's name does not match the required format, so skip it. continue } props[groups[0][1]] = values[0] } if len(props) == 0 { // HTTP request does not contain query parameters encoded by rules of style "deepObject". return nil, nil } return props, nil } default: return nil, invalidSerializationMethodErr(sm) } props, err := propsFn(d.values) if err != nil { return nil, err } if props == nil { return nil, nil } return makeObject(props, schema) } // headerParamDecoder decodes values of header parameters. type headerParamDecoder struct { header http.Header } func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { if sm.Style != "simple" { return nil, invalidSerializationMethodErr(sm) } raw := d.header.Get(http.CanonicalHeaderKey(param)) return parsePrimitive(raw, schema) } func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { if sm.Style != "simple" { return nil, invalidSerializationMethodErr(sm) } raw := d.header.Get(http.CanonicalHeaderKey(param)) if raw == "" { // HTTP request does not contains a corresponding header return nil, nil } return parseArray(strings.Split(raw, ","), schema) } func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { if sm.Style != "simple" { return nil, invalidSerializationMethodErr(sm) } valueDelim := "," if sm.Explode { valueDelim = "=" } raw := d.header.Get(http.CanonicalHeaderKey(param)) if raw == "" { // HTTP request does not contain a corresponding header. return nil, nil } props, err := propsFromString(raw, ",", valueDelim) if err != nil { return nil, err } return makeObject(props, schema) } // cookieParamDecoder decodes values of cookie parameters. type cookieParamDecoder struct { req *http.Request } func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { if sm.Style != "form" { return nil, invalidSerializationMethodErr(sm) } cookie, err := d.req.Cookie(param) if err == http.ErrNoCookie { // HTTP request does not contain a corresponding cookie. return nil, nil } if err != nil { return nil, fmt.Errorf("decoding param %q: %s", param, err) } return parsePrimitive(cookie.Value, schema) } func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { if sm.Style != "form" || sm.Explode { return nil, invalidSerializationMethodErr(sm) } cookie, err := d.req.Cookie(param) if err == http.ErrNoCookie { // HTTP request does not contain a corresponding cookie. return nil, nil } if err != nil { return nil, fmt.Errorf("decoding param %q: %s", param, err) } return parseArray(strings.Split(cookie.Value, ","), schema) } func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { if sm.Style != "form" || sm.Explode { return nil, invalidSerializationMethodErr(sm) } cookie, err := d.req.Cookie(param) if err == http.ErrNoCookie { // HTTP request does not contain a corresponding cookie. return nil, nil } if err != nil { return nil, fmt.Errorf("decoding param %q: %s", param, err) } props, err := propsFromString(cookie.Value, ",", ",") if err != nil { return nil, err } return makeObject(props, schema) } // propsFromString returns a properties map that is created by splitting a source string by propDelim and valueDelim. // The source string must have a valid format: pairs separated by . // The function returns an error when the source string has an invalid format. func propsFromString(src, propDelim, valueDelim string) (map[string]string, error) { props := make(map[string]string) pairs := strings.Split(src, propDelim) // When propDelim and valueDelim is equal the source string follow the next rule: // every even item of pairs is a properies's name, and the subsequent odd item is a property's value. if propDelim == valueDelim { // Taking into account the rule above, a valid source string must be splitted by propDelim // to an array with an even number of items. if len(pairs)%2 != 0 { return nil, &ParseError{ Kind: KindInvalidFormat, Value: src, Reason: fmt.Sprintf("a value must be a list of object's properties in format \"name%svalue\" separated by %s", valueDelim, propDelim), } } for i := 0; i < len(pairs)/2; i++ { props[pairs[i*2]] = pairs[i*2+1] } return props, nil } // When propDelim and valueDelim is not equal the source string follow the next rule: // every item of pairs is a string that follows format . for _, pair := range pairs { prop := strings.Split(pair, valueDelim) if len(prop) != 2 { return nil, &ParseError{ Kind: KindInvalidFormat, Value: src, Reason: fmt.Sprintf("a value must be a list of object's properties in format \"name%svalue\" separated by %s", valueDelim, propDelim), } } props[prop[0]] = prop[1] } return props, nil } // makeObject returns an object that contains properties from props. // A value of every property is parsed as a primitive value. // The function returns an error when an error happened while parse object's properties. func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]interface{}, error) { obj := make(map[string]interface{}) for propName, propSchema := range schema.Value.Properties { value, err := parsePrimitive(props[propName], propSchema) if err != nil { if v, ok := err.(*ParseError); ok { return nil, &ParseError{path: []interface{}{propName}, Cause: v} } return nil, fmt.Errorf("property %q: %s", propName, err) } obj[propName] = value } return obj, nil } // parseArray returns an array that contains items from a raw array. // Every item is parsed as a primitive value. // The function returns an error when an error happened while parse array's items. func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, error) { var value []interface{} for i, v := range raw { item, err := parsePrimitive(v, schemaRef.Value.Items) if err != nil { if v, ok := err.(*ParseError); ok { return nil, &ParseError{path: []interface{}{i}, Cause: v} } return nil, fmt.Errorf("item %d: %s", i, err) } value = append(value, item) } return value, nil } // parsePrimitive returns a value that is created by parsing a source string to a primitive type // that is specified by a schema. The function returns nil when the source string is empty. // The function panics when a schema has a non primitive type. func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) { if raw == "" { return nil, nil } switch schema.Value.Type { case "integer": v, err := strconv.ParseFloat(raw, 64) if err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid integer", Cause: err} } return v, nil case "number": v, err := strconv.ParseFloat(raw, 64) if err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid number", Cause: err} } return v, nil case "boolean": v, err := strconv.ParseBool(raw) if err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid number", Cause: err} } return v, nil case "string": return raw, nil default: panic(fmt.Sprintf("schema has non primitive type %q", schema.Value.Type)) } } // EncodingFn is a function that returns an encoding of a request body's part. type EncodingFn func(partName string) *openapi3.Encoding // BodyDecoder is an interface to decode a body of a request or response. // An implementation must return a value that is a primitive, []interface{}, or map[string]interface{}. type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (interface{}, error) // bodyDecoders contains decoders for supported content types of a body. // By default, there is content type "application/json" is supported only. var bodyDecoders = make(map[string]BodyDecoder) // RegisteredBodyDecoder returns the registered body decoder for the given content type. // // If no decoder was registered for the given content type, nil is returned. // This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines. func RegisteredBodyDecoder(contentType string) BodyDecoder { return bodyDecoders[contentType] } // RegisterBodyDecoder registers a request body's decoder for a content type. // // If a decoder for the specified content type already exists, the function replaces // it with the specified decoder. // This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines. func RegisterBodyDecoder(contentType string, decoder BodyDecoder) { if contentType == "" { panic("contentType is empty") } if decoder == nil { panic("decoder is not defined") } bodyDecoders[contentType] = decoder } // UnregisterBodyDecoder dissociates a body decoder from a content type. // // Decoding this content type will result in an error. // This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines. func UnregisterBodyDecoder(contentType string) { if contentType == "" { panic("contentType is empty") } delete(bodyDecoders, contentType) } var headerCT = http.CanonicalHeaderKey("Content-Type") const prefixUnsupportedCT = "unsupported content type" // decodeBody returns a decoded body. // The function returns ParseError when a body is invalid. func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { contentType := header.Get(headerCT) if contentType == "" { if _, ok := body.(*multipart.Part); ok { contentType = "text/plain" } } mediaType := parseMediaType(contentType) decoder, ok := bodyDecoders[mediaType] if !ok { return nil, &ParseError{ Kind: KindUnsupportedFormat, Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, mediaType), } } value, err := decoder(body, header, schema, encFn) if err != nil { return nil, err } return value, nil } func init() { RegisterBodyDecoder("text/plain", plainBodyDecoder) RegisterBodyDecoder("application/json", jsonBodyDecoder) RegisterBodyDecoder("application/x-yaml", yamlBodyDecoder) RegisterBodyDecoder("application/yaml", yamlBodyDecoder) RegisterBodyDecoder("application/problem+json", jsonBodyDecoder) RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder) RegisterBodyDecoder("multipart/form-data", multipartBodyDecoder) RegisterBodyDecoder("application/octet-stream", FileBodyDecoder) } func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { data, err := ioutil.ReadAll(body) if err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} } return string(data), nil } func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { var value interface{} if err := json.NewDecoder(body).Decode(&value); err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} } return value, nil } func yamlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { var value interface{} if err := yaml.NewDecoder(body).Decode(&value); err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} } return value, nil } func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { // Validate schema of request body. // By the OpenAPI 3 specification request body's schema must have type "object". // Properties of the schema describes individual parts of request body. if schema.Value.Type != "object" { return nil, errors.New("unsupported schema of request body") } for propName, propSchema := range schema.Value.Properties { switch propSchema.Value.Type { case "object": return nil, fmt.Errorf("unsupported schema of request body's property %q", propName) case "array": items := propSchema.Value.Items.Value if items.Type != "string" && items.Type != "integer" && items.Type != "number" && items.Type != "boolean" { return nil, fmt.Errorf("unsupported schema of request body's property %q", propName) } } } // Parse form. b, err := ioutil.ReadAll(body) if err != nil { return nil, err } values, err := url.ParseQuery(string(b)) if err != nil { return nil, err } // Make an object value from form values. obj := make(map[string]interface{}) dec := &urlValuesDecoder{values: values} for name, prop := range schema.Value.Properties { var ( value interface{} enc *openapi3.Encoding ) if encFn != nil { enc = encFn(name) } sm := enc.SerializationMethod() if value, err = decodeValue(dec, name, sm, prop, false); err != nil { return nil, err } obj[name] = value } return obj, nil } func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { if schema.Value.Type != "object" { return nil, errors.New("unsupported schema of request body") } // Parse form. values := make(map[string][]interface{}) contentType := header.Get(headerCT) _, params, err := mime.ParseMediaType(contentType) if err != nil { return nil, err } mr := multipart.NewReader(body, params["boundary"]) for { var part *multipart.Part if part, err = mr.NextPart(); err == io.EOF { break } if err != nil { return nil, err } var ( name = part.FormName() enc *openapi3.Encoding ) if encFn != nil { enc = encFn(name) } subEncFn := func(string) *openapi3.Encoding { return enc } // If the property's schema has type "array" it is means that the form contains a few parts with the same name. // Every such part has a type that is defined by an items schema in the property's schema. var valueSchema *openapi3.SchemaRef var exists bool valueSchema, exists = schema.Value.Properties[name] if !exists { anyProperties := schema.Value.AdditionalPropertiesAllowed if anyProperties != nil { switch *anyProperties { case true: //additionalProperties: true continue default: //additionalProperties: false return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } } if schema.Value.AdditionalProperties == nil { return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name] if !exists { return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } } if valueSchema.Value.Type == "array" { valueSchema = valueSchema.Value.Items } var value interface{} if value, err = decodeBody(part, http.Header(part.Header), valueSchema, subEncFn); err != nil { if v, ok := err.(*ParseError); ok { return nil, &ParseError{path: []interface{}{name}, Cause: v} } return nil, fmt.Errorf("part %s: %s", name, err) } values[name] = append(values[name], value) } allTheProperties := make(map[string]*openapi3.SchemaRef) for k, v := range schema.Value.Properties { allTheProperties[k] = v } if schema.Value.AdditionalProperties != nil { for k, v := range schema.Value.AdditionalProperties.Value.Properties { allTheProperties[k] = v } } // Make an object value from form values. obj := make(map[string]interface{}) for name, prop := range allTheProperties { vv := values[name] if len(vv) == 0 { continue } if prop.Value.Type == "array" { obj[name] = vv } else { obj[name] = vv[0] } } return obj, nil } // FileBodyDecoder is a body decoder that decodes a file body to a string. func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { data, err := ioutil.ReadAll(body) if err != nil { return nil, err } return string(data), nil } kin-openapi-0.85.0/openapi3filter/req_resp_decoder_test.go000066400000000000000000001244711415236407200236520ustar00rootroot00000000000000package openapi3filter import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "net/textproto" "net/url" "reflect" "strings" "testing" "github.com/getkin/kin-openapi/openapi3" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) func TestDecodeParameter(t *testing.T) { var ( boolPtr = func(b bool) *bool { return &b } explode = boolPtr(true) noExplode = boolPtr(false) arrayOf = func(items *openapi3.SchemaRef) *openapi3.SchemaRef { return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "array", Items: items}} } objectOf = func(args ...interface{}) *openapi3.SchemaRef { s := &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "object", Properties: make(map[string]*openapi3.SchemaRef)}} if len(args)%2 != 0 { panic("invalid arguments. must be an odd number of arguments") } for i := 0; i < len(args)/2; i++ { propName := args[i*2].(string) propSchema := args[i*2+1].(*openapi3.SchemaRef) s.Value.Properties[propName] = propSchema } return s } integerSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "integer"}} numberSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "number"}} booleanSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "boolean"}} stringSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "string"}} allofSchema = &openapi3.SchemaRef{ Value: &openapi3.Schema{ AllOf: []*openapi3.SchemaRef{ integerSchema, numberSchema, }}} anyofSchema = &openapi3.SchemaRef{ Value: &openapi3.Schema{ AnyOf: []*openapi3.SchemaRef{ integerSchema, stringSchema, }}} oneofSchema = &openapi3.SchemaRef{ Value: &openapi3.Schema{ OneOf: []*openapi3.SchemaRef{ booleanSchema, integerSchema, }}} arraySchema = arrayOf(stringSchema) objectSchema = objectOf("id", stringSchema, "name", stringSchema) ) type testCase struct { name string param *openapi3.Parameter path string query string header string cookie string want interface{} err error } testGroups := []struct { name string testCases []testCase }{ { name: "path primitive", testCases: []testCase{ { name: "simple", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: stringSchema}, path: "/foo", want: "foo", }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: stringSchema}, path: "/foo", want: "foo", }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringSchema}, path: "/.foo", want: "foo", }, { name: "label invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringSchema}, path: "/foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "label explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringSchema}, path: "/.foo", want: "foo", }, { name: "label explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringSchema}, path: "/foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "matrix", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringSchema}, path: "/;param=foo", want: "foo", }, { name: "matrix invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringSchema}, path: "/foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "matrix explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringSchema}, path: "/;param=foo", want: "foo", }, { name: "matrix explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringSchema}, path: "/foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "path", Schema: stringSchema}, path: "/foo", want: "foo", }, { name: "string", param: &openapi3.Parameter{Name: "param", In: "path", Schema: stringSchema}, path: "/foo", want: "foo", }, { name: "integer", param: &openapi3.Parameter{Name: "param", In: "path", Schema: integerSchema}, path: "/1", want: float64(1), }, { name: "integer invalid", param: &openapi3.Parameter{Name: "param", In: "path", Schema: integerSchema}, path: "/foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "number", param: &openapi3.Parameter{Name: "param", In: "path", Schema: numberSchema}, path: "/1.1", want: 1.1, }, { name: "number invalid", param: &openapi3.Parameter{Name: "param", In: "path", Schema: numberSchema}, path: "/foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "boolean", param: &openapi3.Parameter{Name: "param", In: "path", Schema: booleanSchema}, path: "/true", want: true, }, { name: "boolean invalid", param: &openapi3.Parameter{Name: "param", In: "path", Schema: booleanSchema}, path: "/foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, }, }, { name: "path array", testCases: []testCase{ { name: "simple", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: arraySchema}, path: "/foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: arraySchema}, path: "/foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema}, path: "/.foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "label invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: arraySchema}, path: "/foo,bar", err: &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"}, }, { name: "label explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema}, path: "/.foo.bar", want: []interface{}{"foo", "bar"}, }, { name: "label explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: arraySchema}, path: "/foo.bar", err: &ParseError{Kind: KindInvalidFormat, Value: "foo.bar"}, }, { name: "matrix", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema}, path: "/;param=foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "matrix invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: arraySchema}, path: "/foo,bar", err: &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"}, }, { name: "matrix explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema}, path: "/;param=foo;param=bar", want: []interface{}{"foo", "bar"}, }, { name: "matrix explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: arraySchema}, path: "/foo,bar", err: &ParseError{Kind: KindInvalidFormat, Value: "foo,bar"}, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arraySchema}, path: "/foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "invalid integer items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(integerSchema)}, path: "/1,foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(numberSchema)}, path: "/1.1,foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(booleanSchema)}, path: "/true,foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, { name: "path object", testCases: []testCase{ { name: "simple", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: objectSchema}, path: "/id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: objectSchema}, path: "/id=foo,name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema}, path: "/.id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "label invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema}, path: "/id,foo,name,bar", err: &ParseError{Kind: KindInvalidFormat, Value: "id,foo,name,bar"}, }, { name: "label explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema}, path: "/.id=foo.name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "label explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema}, path: "/id=foo.name=bar", err: &ParseError{Kind: KindInvalidFormat, Value: "id=foo.name=bar"}, }, { name: "matrix", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema}, path: "/;param=id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "matrix invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema}, path: "/id,foo,name,bar", err: &ParseError{Kind: KindInvalidFormat, Value: "id,foo,name,bar"}, }, { name: "matrix explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema}, path: "/;id=foo;name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "matrix explode invalid", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema}, path: "/id=foo;name=bar", err: &ParseError{Kind: KindInvalidFormat, Value: "id=foo;name=bar"}, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectSchema}, path: "/id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "invalid integer prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", integerSchema)}, path: "/foo,bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", numberSchema)}, path: "/foo,bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", booleanSchema)}, path: "/foo,bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, { name: "query primitive", testCases: []testCase{ { name: "form", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: stringSchema}, query: "param=foo", want: "foo", }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: stringSchema}, query: "param=foo", want: "foo", }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: stringSchema}, query: "param=foo", want: "foo", }, { name: "string", param: &openapi3.Parameter{Name: "param", In: "query", Schema: stringSchema}, query: "param=foo", want: "foo", }, { name: "integer", param: &openapi3.Parameter{Name: "param", In: "query", Schema: integerSchema}, query: "param=1", want: float64(1), }, { name: "integer invalid", param: &openapi3.Parameter{Name: "param", In: "query", Schema: integerSchema}, query: "param=foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "number", param: &openapi3.Parameter{Name: "param", In: "query", Schema: numberSchema}, query: "param=1.1", want: 1.1, }, { name: "number invalid", param: &openapi3.Parameter{Name: "param", In: "query", Schema: numberSchema}, query: "param=foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "boolean", param: &openapi3.Parameter{Name: "param", In: "query", Schema: booleanSchema}, query: "param=true", want: true, }, { name: "boolean invalid", param: &openapi3.Parameter{Name: "param", In: "query", Schema: booleanSchema}, query: "param=foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, }, }, { name: "query Allof", testCases: []testCase{ { name: "allofSchema integer and number", param: &openapi3.Parameter{Name: "param", In: "query", Schema: allofSchema}, query: "param=1", want: float64(1), }, { name: "allofSchema string", param: &openapi3.Parameter{Name: "param", In: "query", Schema: allofSchema}, query: "param=abdf", err: &ParseError{Kind: KindInvalidFormat, Value: "abdf"}, }, }, }, { name: "query Anyof", testCases: []testCase{ { name: "anyofSchema integer", param: &openapi3.Parameter{Name: "param", In: "query", Schema: anyofSchema}, query: "param=1", want: float64(1), }, { name: "anyofSchema string", param: &openapi3.Parameter{Name: "param", In: "query", Schema: anyofSchema}, query: "param=abdf", want: "abdf", }, }, }, { name: "query Oneof", testCases: []testCase{ { name: "oneofSchema boolean", param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema}, query: "param=true", want: true, }, { name: "oneofSchema int", param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema}, query: "param=1122", want: float64(1122), }, { name: "oneofSchema string", param: &openapi3.Parameter{Name: "param", In: "query", Schema: oneofSchema}, query: "param=abcd", want: nil, }, }, }, { name: "query array", testCases: []testCase{ { name: "form", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: arraySchema}, query: "param=foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: arraySchema}, query: "param=foo¶m=bar", want: []interface{}{"foo", "bar"}, }, { name: "spaceDelimited", param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: noExplode, Schema: arraySchema}, query: "param=foo bar", want: []interface{}{"foo", "bar"}, }, { name: "spaceDelimited explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: explode, Schema: arraySchema}, query: "param=foo¶m=bar", want: []interface{}{"foo", "bar"}, }, { name: "pipeDelimited", param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: noExplode, Schema: arraySchema}, query: "param=foo|bar", want: []interface{}{"foo", "bar"}, }, { name: "pipeDelimited explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: explode, Schema: arraySchema}, query: "param=foo¶m=bar", want: []interface{}{"foo", "bar"}, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arraySchema}, query: "param=foo¶m=bar", want: []interface{}{"foo", "bar"}, }, { name: "invalid integer items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(integerSchema)}, query: "param=1¶m=foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(numberSchema)}, query: "param=1.1¶m=foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(booleanSchema)}, query: "param=true¶m=foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, { name: "query object", testCases: []testCase{ { name: "form", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: objectSchema}, query: "param=id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: objectSchema}, query: "id=foo&name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "deepObject explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectSchema}, query: "param[id]=foo¶m[name]=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectSchema}, query: "id=foo&name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "invalid integer prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", integerSchema)}, query: "foo=bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", numberSchema)}, query: "foo=bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", booleanSchema)}, query: "foo=bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, { name: "header primitive", testCases: []testCase{ { name: "simple", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: stringSchema}, header: "X-Param:foo", want: "foo", }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: stringSchema}, header: "X-Param:foo", want: "foo", }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: stringSchema}, header: "X-Param:foo", want: "foo", }, { name: "string", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: stringSchema}, header: "X-Param:foo", want: "foo", }, { name: "integer", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema}, header: "X-Param:1", want: float64(1), }, { name: "integer invalid", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: integerSchema}, header: "X-Param:foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "number", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: numberSchema}, header: "X-Param:1.1", want: 1.1, }, { name: "number invalid", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: numberSchema}, header: "X-Param:foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "boolean", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: booleanSchema}, header: "X-Param:true", want: true, }, { name: "boolean invalid", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: booleanSchema}, header: "X-Param:foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, }, }, { name: "header array", testCases: []testCase{ { name: "simple", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: arraySchema}, header: "X-Param:foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: arraySchema}, header: "X-Param:foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arraySchema}, header: "X-Param:foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "invalid integer items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(integerSchema)}, header: "X-Param:1,foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(numberSchema)}, header: "X-Param:1.1,foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(booleanSchema)}, header: "X-Param:true,foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, { name: "header object", testCases: []testCase{ { name: "simple", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: objectSchema}, header: "X-Param:id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: objectSchema}, header: "X-Param:id=foo,name=bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectSchema}, header: "X-Param:id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "invalid integer prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", integerSchema)}, header: "X-Param:foo,bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", numberSchema)}, header: "X-Param:foo,bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", booleanSchema)}, header: "X-Param:foo,bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, { name: "cookie primitive", testCases: []testCase{ { name: "form", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: stringSchema}, cookie: "X-Param:foo", want: "foo", }, { name: "form explode", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: explode, Schema: stringSchema}, cookie: "X-Param:foo", want: "foo", }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: stringSchema}, cookie: "X-Param:foo", want: "foo", }, { name: "string", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: stringSchema}, cookie: "X-Param:foo", want: "foo", }, { name: "integer", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: integerSchema}, cookie: "X-Param:1", want: float64(1), }, { name: "integer invalid", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: integerSchema}, cookie: "X-Param:foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "number", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: numberSchema}, cookie: "X-Param:1.1", want: 1.1, }, { name: "number invalid", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: numberSchema}, cookie: "X-Param:foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, { name: "boolean", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: booleanSchema}, cookie: "X-Param:true", want: true, }, { name: "boolean invalid", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Schema: booleanSchema}, cookie: "X-Param:foo", err: &ParseError{Kind: KindInvalidFormat, Value: "foo"}, }, }, }, { name: "cookie array", testCases: []testCase{ { name: "form", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arraySchema}, cookie: "X-Param:foo,bar", want: []interface{}{"foo", "bar"}, }, { name: "invalid integer items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(integerSchema)}, cookie: "X-Param:1,foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(numberSchema)}, cookie: "X-Param:1.1,foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(booleanSchema)}, cookie: "X-Param:true,foo", err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, { name: "cookie object", testCases: []testCase{ { name: "form", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectSchema}, cookie: "X-Param:id,foo,name,bar", want: map[string]interface{}{"id": "foo", "name": "bar"}, }, { name: "invalid integer prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", integerSchema)}, cookie: "X-Param:foo,bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", numberSchema)}, cookie: "X-Param:foo,bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", booleanSchema)}, cookie: "X-Param:foo,bar", err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, } for _, tg := range testGroups { t.Run(tg.name, func(t *testing.T) { for _, tc := range tg.testCases { t.Run(tc.name, func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://test.org/test"+tc.path, nil) require.NoError(t, err, "failed to create a test request") if tc.query != "" { query := req.URL.Query() for _, param := range strings.Split(tc.query, "&") { v := strings.Split(param, "=") query.Add(v[0], v[1]) } req.URL.RawQuery = query.Encode() } if tc.header != "" { v := strings.Split(tc.header, ":") req.Header.Add(v[0], v[1]) } if tc.cookie != "" { v := strings.Split(tc.cookie, ":") req.AddCookie(&http.Cookie{Name: v[0], Value: v[1]}) } path := "/test" if tc.path != "" { path += "/{" + tc.param.Name + "}" } info := &openapi3.Info{ Title: "MyAPI", Version: "0.1", } spec := &openapi3.T{OpenAPI: "3.0.0", Info: info} op := &openapi3.Operation{ OperationID: "test", Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, Responses: openapi3.NewResponses(), } spec.AddOperation(path, http.MethodGet, op) err = spec.Validate(context.Background()) require.NoError(t, err) router, err := legacyrouter.NewRouter(spec) require.NoError(t, err) route, pathParams, err := router.FindRoute(req) require.NoError(t, err) input := &RequestValidationInput{Request: req, PathParams: pathParams, Route: route} got, err := decodeStyledParameter(tc.param, input) if tc.err != nil { require.Error(t, err) require.Truef(t, matchParseError(err, tc.err), "got error:\n%v\nwant error:\n%v", err, tc.err) return } require.NoError(t, err) require.Truef(t, reflect.DeepEqual(got, tc.want), "got %v, want %v", got, tc.want) }) } }) } } func TestDecodeBody(t *testing.T) { boolPtr := func(b bool) *bool { return &b } urlencodedForm := make(url.Values) urlencodedForm.Set("a", "a1") urlencodedForm.Set("b", "10") urlencodedForm.Add("c", "c1") urlencodedForm.Add("c", "c2") urlencodedSpaceDelim := make(url.Values) urlencodedSpaceDelim.Set("a", "a1") urlencodedSpaceDelim.Set("b", "10") urlencodedSpaceDelim.Add("c", "c1 c2") urlencodedPipeDelim := make(url.Values) urlencodedPipeDelim.Set("a", "a1") urlencodedPipeDelim.Set("b", "10") urlencodedPipeDelim.Add("c", "c1|c2") d, err := json.Marshal(map[string]interface{}{"d1": "d1"}) require.NoError(t, err) multipartForm, multipartFormMime, err := newTestMultipartForm([]*testFormPart{ {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, {name: "b", contentType: "application/json", data: strings.NewReader("10")}, {name: "c", contentType: "text/plain", data: strings.NewReader("c1")}, {name: "c", contentType: "text/plain", data: strings.NewReader("c2")}, {name: "d", contentType: "application/json", data: bytes.NewReader(d)}, {name: "f", contentType: "application/octet-stream", data: strings.NewReader("foo"), filename: "f1"}, {name: "g", data: strings.NewReader("g1")}, }) require.NoError(t, err) multipartFormExtraPart, multipartFormMimeExtraPart, err := newTestMultipartForm([]*testFormPart{ {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, {name: "x", contentType: "text/plain", data: strings.NewReader("x1")}, }) require.NoError(t, err) multipartAnyAdditionalProps, multipartMimeAnyAdditionalProps, err := newTestMultipartForm([]*testFormPart{ {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, {name: "x", contentType: "text/plain", data: strings.NewReader("x1")}, }) require.NoError(t, err) multipartAdditionalProps, multipartMimeAdditionalProps, err := newTestMultipartForm([]*testFormPart{ {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, {name: "x", contentType: "text/plain", data: strings.NewReader("x1")}, }) require.NoError(t, err) multipartAdditionalPropsErr, multipartMimeAdditionalPropsErr, err := newTestMultipartForm([]*testFormPart{ {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, {name: "x", contentType: "text/plain", data: strings.NewReader("x1")}, {name: "y", contentType: "text/plain", data: strings.NewReader("y1")}, }) require.NoError(t, err) testCases := []struct { name string mime string body io.Reader schema *openapi3.Schema encoding map[string]*openapi3.Encoding want interface{} wantErr error }{ { name: prefixUnsupportedCT, mime: "application/xml", wantErr: &ParseError{Kind: KindUnsupportedFormat}, }, { name: "invalid body data", mime: "application/json", body: strings.NewReader("invalid"), wantErr: &ParseError{Kind: KindInvalidFormat}, }, { name: "plain text", mime: "text/plain", body: strings.NewReader("text"), want: "text", }, { name: "json", mime: "application/json", body: strings.NewReader("\"foo\""), want: "foo", }, { name: "x-yaml", mime: "application/x-yaml", body: strings.NewReader("foo"), want: "foo", }, { name: "yaml", mime: "application/yaml", body: strings.NewReader("foo"), want: "foo", }, { name: "urlencoded form", mime: "application/x-www-form-urlencoded", body: strings.NewReader(urlencodedForm.Encode()), schema: openapi3.NewObjectSchema(). WithProperty("a", openapi3.NewStringSchema()). WithProperty("b", openapi3.NewIntegerSchema()). WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())), want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}}, }, { name: "urlencoded space delimited", mime: "application/x-www-form-urlencoded", body: strings.NewReader(urlencodedSpaceDelim.Encode()), schema: openapi3.NewObjectSchema(). WithProperty("a", openapi3.NewStringSchema()). WithProperty("b", openapi3.NewIntegerSchema()). WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())), encoding: map[string]*openapi3.Encoding{ "c": {Style: openapi3.SerializationSpaceDelimited, Explode: boolPtr(false)}, }, want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}}, }, { name: "urlencoded pipe delimited", mime: "application/x-www-form-urlencoded", body: strings.NewReader(urlencodedPipeDelim.Encode()), schema: openapi3.NewObjectSchema(). WithProperty("a", openapi3.NewStringSchema()). WithProperty("b", openapi3.NewIntegerSchema()). WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())), encoding: map[string]*openapi3.Encoding{ "c": {Style: openapi3.SerializationPipeDelimited, Explode: boolPtr(false)}, }, want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}}, }, { name: "multipart", mime: multipartFormMime, body: multipartForm, schema: openapi3.NewObjectSchema(). WithProperty("a", openapi3.NewStringSchema()). WithProperty("b", openapi3.NewIntegerSchema()). WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())). WithProperty("d", openapi3.NewObjectSchema().WithProperty("d1", openapi3.NewStringSchema())). WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")). WithProperty("g", openapi3.NewStringSchema()), want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"}, }, { name: "multipartExtraPart", mime: multipartFormMimeExtraPart, body: multipartFormExtraPart, schema: openapi3.NewObjectSchema(). WithProperty("a", openapi3.NewStringSchema()), want: map[string]interface{}{"a": "a1"}, wantErr: &ParseError{Kind: KindOther}, }, { name: "multipartAnyAdditionalProperties", mime: multipartMimeAnyAdditionalProps, body: multipartAnyAdditionalProps, schema: openapi3.NewObjectSchema(). WithAnyAdditionalProperties(). WithProperty("a", openapi3.NewStringSchema()), want: map[string]interface{}{"a": "a1"}, }, { name: "multipartWithAdditionalProperties", mime: multipartMimeAdditionalProps, body: multipartAdditionalProps, schema: openapi3.NewObjectSchema(). WithAdditionalProperties(openapi3.NewObjectSchema(). WithProperty("x", openapi3.NewStringSchema())). WithProperty("a", openapi3.NewStringSchema()), want: map[string]interface{}{"a": "a1", "x": "x1"}, }, { name: "multipartWithAdditionalPropertiesError", mime: multipartMimeAdditionalPropsErr, body: multipartAdditionalPropsErr, schema: openapi3.NewObjectSchema(). WithAdditionalProperties(openapi3.NewObjectSchema(). WithProperty("x", openapi3.NewStringSchema())). WithProperty("a", openapi3.NewStringSchema()), want: map[string]interface{}{"a": "a1", "x": "x1"}, wantErr: &ParseError{Kind: KindOther}, }, { name: "file", mime: "application/octet-stream", body: strings.NewReader("foo"), want: "foo", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { h := make(http.Header) h.Set(headerCT, tc.mime) var schemaRef *openapi3.SchemaRef if tc.schema != nil { schemaRef = tc.schema.NewRef() } encFn := func(name string) *openapi3.Encoding { if tc.encoding == nil { return nil } return tc.encoding[name] } got, err := decodeBody(tc.body, h, schemaRef, encFn) if tc.wantErr != nil { require.Error(t, err) require.Truef(t, matchParseError(err, tc.wantErr), "got error:\n%v\nwant error:\n%v", err, tc.wantErr) return } require.NoError(t, err) require.Truef(t, reflect.DeepEqual(got, tc.want), "got %v, want %v", got, tc.want) }) } } type testFormPart struct { name string contentType string data io.Reader filename string } func newTestMultipartForm(parts []*testFormPart) (io.Reader, string, error) { form := &bytes.Buffer{} w := multipart.NewWriter(form) defer w.Close() for _, p := range parts { var disp string if p.filename == "" { disp = fmt.Sprintf("form-data; name=%q", p.name) } else { disp = fmt.Sprintf("form-data; name=%q; filename=%q", p.name, p.filename) } h := make(textproto.MIMEHeader) h.Set(headerCT, p.contentType) h.Set("Content-Disposition", disp) pw, err := w.CreatePart(h) if err != nil { return nil, "", err } if _, err = io.Copy(pw, p.data); err != nil { return nil, "", err } } return form, w.FormDataContentType(), nil } func TestRegisterAndUnregisterBodyDecoder(t *testing.T) { var decoder BodyDecoder decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (decoded interface{}, err error) { var data []byte if data, err = ioutil.ReadAll(body); err != nil { return } return strings.Split(string(data), ","), nil } contentType := "text/csv" h := make(http.Header) h.Set(headerCT, contentType) originalDecoder := RegisteredBodyDecoder(contentType) require.Nil(t, originalDecoder) RegisterBodyDecoder(contentType, decoder) require.Equal(t, fmt.Sprintf("%v", decoder), fmt.Sprintf("%v", RegisteredBodyDecoder(contentType))) body := strings.NewReader("foo,bar") schema := openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema()).NewRef() encFn := func(string) *openapi3.Encoding { return nil } got, err := decodeBody(body, h, schema, encFn) require.NoError(t, err) require.Equal(t, []string{"foo", "bar"}, got) UnregisterBodyDecoder(contentType) originalDecoder = RegisteredBodyDecoder(contentType) require.Nil(t, originalDecoder) _, err = decodeBody(body, h, schema, encFn) require.Equal(t, &ParseError{ Kind: KindUnsupportedFormat, Reason: prefixUnsupportedCT + ` "text/csv"`, }, err) } func matchParseError(got, want error) bool { wErr, ok := want.(*ParseError) if !ok { return false } gErr, ok := got.(*ParseError) if !ok { return false } if wErr.Kind != gErr.Kind { return false } if !reflect.DeepEqual(wErr.Value, gErr.Value) { return false } if !reflect.DeepEqual(wErr.Path(), gErr.Path()) { return false } if wErr.Cause != nil { return matchParseError(gErr.Cause, wErr.Cause) } return true } kin-openapi-0.85.0/openapi3filter/testdata/000077500000000000000000000000001415236407200205575ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3filter/testdata/petstore.yaml000066400000000000000000000046721415236407200233210ustar00rootroot00000000000000openapi: "3.0.0" info: description: "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters." version: "1.0.0" title: "Swagger Petstore" termsOfService: "http://swagger.io/terms/" contact: email: "apiteam@swagger.io" license: name: "Apache 2.0" url: "http://www.apache.org/licenses/LICENSE-2.0.html" tags: - name: "pet" description: "Everything about your Pets" externalDocs: description: "Find out more" url: "http://swagger.io" - name: "store" description: "Access to Petstore orders" - name: "user" description: "Operations about user" externalDocs: description: "Find out more about our store" url: "http://swagger.io" paths: /pet: post: tags: - "pet" summary: "Add a new pet to the store" description: "" operationId: "addPet" requestBody: required: true content: 'application/json': schema: $ref: '#/components/schemas/Pet' responses: "405": description: "Invalid input" components: schemas: Category: type: "object" properties: id: type: "integer" format: "int64" name: type: "string" xml: name: "Category" Tag: type: "object" properties: id: type: "integer" format: "int64" name: type: "string" xml: name: "Tag" Pet: type: "object" required: - "name" - "photoUrls" properties: id: type: "integer" format: "int64" category: $ref: "#/components/schemas/Category" name: type: "string" example: "doggie" photoUrls: type: "array" xml: name: "photoUrl" wrapped: true items: type: "string" tags: type: "array" xml: name: "tag" wrapped: true items: $ref: "#/components/schemas/Tag" status: type: "string" description: "pet status in the store" enum: - "available" - "pending" - "sold" xml: name: "Pet" kin-openapi-0.85.0/openapi3filter/unpack_errors_test.go000066400000000000000000000102061415236407200232100ustar00rootroot00000000000000package openapi3filter_test import ( "fmt" "net/http" "net/http/httptest" "sort" "strings" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" "github.com/getkin/kin-openapi/routers/gorillamux" ) func Example() { doc, err := openapi3.NewLoader().LoadFromFile("./testdata/petstore.yaml") if err != nil { panic(err) } router, err := gorillamux.NewRouter(doc) if err != nil { panic(err) } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { route, pathParams, err := router.FindRoute(r) if err != nil { fmt.Println(err.Error()) w.WriteHeader(http.StatusInternalServerError) return } err = openapi3filter.ValidateRequest(r.Context(), &openapi3filter.RequestValidationInput{ Request: r, PathParams: pathParams, Route: route, Options: &openapi3filter.Options{ MultiError: true, }, }) switch err := err.(type) { case nil: case openapi3.MultiError: issues := convertError(err) names := make([]string, 0, len(issues)) for k := range issues { names = append(names, k) } sort.Strings(names) for _, k := range names { msgs := issues[k] fmt.Println("===== Start New Error =====") fmt.Println(k + ":") for _, msg := range msgs { fmt.Printf("\t%s\n", msg) } } w.WriteHeader(http.StatusBadRequest) default: fmt.Println(err.Error()) w.WriteHeader(http.StatusBadRequest) } })) defer ts.Close() // (note invalid type for name and invalid status) body := strings.NewReader(`{"name": 100, "photoUrls": [], "status": "invalidStatus"}`) req, err := http.NewRequest("POST", ts.URL+"/pet", body) if err != nil { panic(err) } req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Printf("response: %d %s\n", resp.StatusCode, resp.Body) // Output: // ===== Start New Error ===== // @body.name: // Error at "/name": Field must be set to string or not be present // Schema: // { // "example": "doggie", // "type": "string" // } // // Value: // "number, integer" // // ===== Start New Error ===== // @body.status: // Error at "/status": value is not one of the allowed values // Schema: // { // "description": "pet status in the store", // "enum": [ // "available", // "pending", // "sold" // ], // "type": "string" // } // // Value: // "invalidStatus" // // response: 400 {} } const ( prefixBody = "@body" unknown = "@unknown" ) func convertError(me openapi3.MultiError) map[string][]string { issues := make(map[string][]string) for _, err := range me { switch err := err.(type) { case *openapi3.SchemaError: // Can inspect schema validation errors here, e.g. err.Value field := prefixBody if path := err.JSONPointer(); len(path) > 0 { field = fmt.Sprintf("%s.%s", field, strings.Join(path, ".")) } if _, ok := issues[field]; !ok { issues[field] = make([]string, 0, 3) } issues[field] = append(issues[field], err.Error()) case *openapi3filter.RequestError: // possible there were multiple issues that failed validation if err, ok := err.Err.(openapi3.MultiError); ok { for k, v := range convertError(err) { if _, ok := issues[k]; !ok { issues[k] = make([]string, 0, 3) } issues[k] = append(issues[k], v...) } continue } // check if invalid HTTP parameter if err.Parameter != nil { prefix := err.Parameter.In name := fmt.Sprintf("%s.%s", prefix, err.Parameter.Name) if _, ok := issues[name]; !ok { issues[name] = make([]string, 0, 3) } issues[name] = append(issues[name], err.Error()) continue } // check if requestBody if err.RequestBody != nil { if _, ok := issues[prefixBody]; !ok { issues[prefixBody] = make([]string, 0, 3) } issues[prefixBody] = append(issues[prefixBody], err.Error()) continue } default: reasons, ok := issues[unknown] if !ok { reasons = make([]string, 0, 3) } reasons = append(reasons, err.Error()) issues[unknown] = reasons } } return issues } kin-openapi-0.85.0/openapi3filter/validate_readonly_test.go000066400000000000000000000042651415236407200240310ustar00rootroot00000000000000package openapi3filter import ( "bytes" "encoding/json" "net/http" "testing" "github.com/getkin/kin-openapi/openapi3" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) { const spec = `{ "openapi": "3.0.3", "info": { "version": "1.0.0", "title": "title", "description": "desc", "contact": { "email": "email" } }, "paths": { "/accounts": { "post": { "description": "Create a new account", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["_id"], "properties": { "_id": { "type": "string", "description": "Unique identifier for this object.", "pattern": "[0-9a-v]+$", "minLength": 20, "maxLength": 20, "readOnly": true } } } } } }, "responses": { "201": { "description": "Successfully created a new account" }, "400": { "description": "The server could not understand the request due to invalid syntax", } } } } } } ` type Request struct { ID string `json:"_id"` } 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) b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"}) require.NoError(t, err) httpReq, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(b)) require.NoError(t, err) httpReq.Header.Add(headerCT, "application/json") route, pathParams, err := router.FindRoute(httpReq) require.NoError(t, err) err = ValidateRequest(sl.Context, &RequestValidationInput{ Request: httpReq, PathParams: pathParams, Route: route, }) require.NoError(t, err) } kin-openapi-0.85.0/openapi3filter/validate_request.go000066400000000000000000000217331415236407200226440ustar00rootroot00000000000000package openapi3filter import ( "bytes" "context" "errors" "fmt" "io/ioutil" "net/http" "sort" "github.com/getkin/kin-openapi/openapi3" ) // ErrAuthenticationServiceMissing is returned when no authentication service // is defined for the request validator var ErrAuthenticationServiceMissing = errors.New("missing AuthenticationFunc") // ErrInvalidRequired is returned when a required value of a parameter or request body is not defined. var ErrInvalidRequired = errors.New("value is required but missing") // ValidateRequest is used to validate the given input according to previous // loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a // non-nil error will be returned. // // Note: One can tune the behavior of uniqueItems: true verification // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { var ( err error me openapi3.MultiError ) options := input.Options if options == nil { options = DefaultOptions } route := input.Route operation := route.Operation operationParameters := operation.Parameters pathItemParameters := route.PathItem.Parameters // For each parameter of the PathItem for _, parameterRef := range pathItemParameters { parameter := parameterRef.Value if operationParameters != nil { if override := operationParameters.GetByInAndName(parameter.In, parameter.Name); override != nil { continue } } if err = ValidateParameter(ctx, input, parameter); err != nil && !options.MultiError { return err } if err != nil { me = append(me, err) } } // For each parameter of the Operation for _, parameter := range operationParameters { if err = ValidateParameter(ctx, input, parameter.Value); err != nil && !options.MultiError { return err } if err != nil { me = append(me, err) } } // RequestBody requestBody := operation.RequestBody if requestBody != nil && !options.ExcludeRequestBody { if err = ValidateRequestBody(ctx, input, requestBody.Value); err != nil && !options.MultiError { return err } if err != nil { me = append(me, err) } } // Security security := operation.Security // If there aren't any security requirements for the operation if security == nil { // Use the global security requirements. security = &route.Spec.Security } if security != nil { if err = ValidateSecurityRequirements(ctx, input, *security); err != nil && !options.MultiError { return err } if err != nil { me = append(me, err) } } if len(me) > 0 { return me } return nil } // ValidateParameter validates a parameter's value by JSON schema. // The function returns RequestError with a ParseError cause when unable to parse a value. // The function returns RequestError with ErrInvalidRequired cause when a value of a required parameter is not defined. // The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema. func ValidateParameter(ctx context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error { if parameter.Schema == nil && parameter.Content == nil { // We have no schema for the parameter. Assume that everything passes // a schema-less check, but this could also be an error. The OpenAPI // validation allows this to happen. return nil } options := input.Options if options == nil { options = DefaultOptions } var value interface{} var err error var schema *openapi3.Schema // Validation will ensure that we either have content or schema. if parameter.Content != nil { if value, schema, err = decodeContentParameter(parameter, input); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } } else { if value, err = decodeStyledParameter(parameter, input); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } schema = parameter.Schema.Value } // Validate a parameter's value. if value == nil { if parameter.Required { return &RequestError{Input: input, Parameter: parameter, Err: ErrInvalidRequired} } return nil } if schema == nil { // A parameter's schema is not defined so skip validation of a parameter's value. return nil } var opts []openapi3.SchemaValidationOption if options.MultiError { opts = make([]openapi3.SchemaValidationOption, 0, 1) opts = append(opts, openapi3.MultiErrors()) } if err = schema.VisitJSON(value, opts...); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } return nil } const prefixInvalidCT = "header Content-Type has unexpected value" // ValidateRequestBody validates data of a request's body. // // The function returns RequestError with ErrInvalidRequired cause when a value is required but not defined. // The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema. func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, requestBody *openapi3.RequestBody) error { var ( req = input.Request data []byte ) options := input.Options if options == nil { options = DefaultOptions } if req.Body != http.NoBody && req.Body != nil { defer req.Body.Close() var err error if data, err = ioutil.ReadAll(req.Body); err != nil { return &RequestError{ Input: input, RequestBody: requestBody, Reason: "reading failed", Err: err, } } // Put the data back into the input req.Body = ioutil.NopCloser(bytes.NewReader(data)) } if len(data) == 0 { if requestBody.Required { return &RequestError{Input: input, RequestBody: requestBody, Err: ErrInvalidRequired} } return nil } content := requestBody.Content if len(content) == 0 { // A request's body does not have declared content, so skip validation. return nil } inputMIME := req.Header.Get(headerCT) contentType := requestBody.Content.Get(inputMIME) if contentType == nil { return &RequestError{ Input: input, RequestBody: requestBody, Reason: fmt.Sprintf("%s %q", prefixInvalidCT, inputMIME), } } if contentType.Schema == nil { // A JSON schema that describes the received data is not declared, so skip validation. return nil } encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] } value, err := decodeBody(bytes.NewReader(data), req.Header, contentType.Schema, encFn) if err != nil { return &RequestError{ Input: input, RequestBody: requestBody, Reason: "failed to decode request body", Err: err, } } opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here opts = append(opts, openapi3.VisitAsRequest()) if options.MultiError { opts = append(opts, openapi3.MultiErrors()) } // Validate JSON with the schema if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { return &RequestError{ Input: input, RequestBody: requestBody, Reason: "doesn't match the schema", Err: err, } } return nil } // ValidateSecurityRequirements goes through multiple OpenAPI 3 security // requirements in order and returns nil on the first valid requirement. // If no requirement is met, errors are returned in order. func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error { if len(srs) == 0 { return nil } var errs []error for _, sr := range srs { if err := validateSecurityRequirement(ctx, input, sr); err != nil { if len(errs) == 0 { errs = make([]error, 0, len(srs)) } errs = append(errs, err) continue } return nil } return &SecurityRequirementsError{ SecurityRequirements: srs, Errors: errs, } } // validateSecurityRequirement validates a single OpenAPI 3 security requirement func validateSecurityRequirement(ctx context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error { doc := input.Route.Spec securitySchemes := doc.Components.SecuritySchemes // Ensure deterministic order names := make([]string, 0, len(securityRequirement)) for name := range securityRequirement { names = append(names, name) } sort.Strings(names) // Get authentication function options := input.Options if options == nil { options = DefaultOptions } f := options.AuthenticationFunc if f == nil { return ErrAuthenticationServiceMissing } // For each scheme for the requirement for _, name := range names { var securityScheme *openapi3.SecurityScheme if securitySchemes != nil { if ref := securitySchemes[name]; ref != nil { securityScheme = ref.Value } } if securityScheme == nil { return &RequestError{ Input: input, Err: fmt.Errorf("security scheme %q is not declared", name), } } scopes := securityRequirement[name] if err := f(ctx, &AuthenticationInput{ RequestValidationInput: input, SecuritySchemeName: name, SecurityScheme: securityScheme, Scopes: scopes, }); err != nil { return err } } return nil } kin-openapi-0.85.0/openapi3filter/validate_request_input.go000066400000000000000000000022241415236407200240550ustar00rootroot00000000000000package openapi3filter import ( "net/http" "net/url" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" ) // A ContentParameterDecoder takes a parameter definition from the OpenAPI spec, // and the value which we received for it. It is expected to return the // value unmarshaled into an interface which can be traversed for // validation, it should also return the schema to be used for validating the // object, since there can be more than one in the content spec. // // If a query parameter appears multiple times, values[] will have more // than one value, but for all other parameter types it should have just // one. type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error) type RequestValidationInput struct { Request *http.Request PathParams map[string]string QueryParams url.Values Route *routers.Route Options *Options ParamDecoder ContentParameterDecoder } func (input *RequestValidationInput) GetQueryParams() url.Values { q := input.QueryParams if q == nil { q = input.Request.URL.Query() input.QueryParams = q } return q } kin-openapi-0.85.0/openapi3filter/validate_response.go000066400000000000000000000071411415236407200230070ustar00rootroot00000000000000// Package openapi3filter validates that requests and inputs request an OpenAPI 3 specification file. package openapi3filter import ( "bytes" "context" "fmt" "io/ioutil" "net/http" "github.com/getkin/kin-openapi/openapi3" ) // ValidateResponse is used to validate the given input according to previous // loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a // non-nil error will be returned. // // Note: One can tune the behavior of uniqueItems: true verification // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error { req := input.RequestValidationInput.Request switch req.Method { case "HEAD": return nil } status := input.Status // These status codes will never be validated. // TODO: The list is probably missing some. switch status { case http.StatusNotModified, http.StatusPermanentRedirect, http.StatusTemporaryRedirect, http.StatusMovedPermanently: return nil } route := input.RequestValidationInput.Route options := input.Options if options == nil { options = DefaultOptions } // Find input for the current status responses := route.Operation.Responses if len(responses) == 0 { return nil } responseRef := responses.Get(status) // Response if responseRef == nil { responseRef = responses.Default() // Default input } if responseRef == nil { // By default, status that is not documented is allowed. if !options.IncludeResponseStatus { return nil } return &ResponseError{Input: input, Reason: "status is not supported"} } response := responseRef.Value if response == nil { return &ResponseError{Input: input, Reason: "response has not been resolved"} } if options.ExcludeResponseBody { // A user turned off validation of a response's body. return nil } content := response.Content if len(content) == 0 || options.ExcludeResponseBody { // An operation does not contains a validation schema for responses with this status code. return nil } inputMIME := input.Header.Get(headerCT) contentType := content.Get(inputMIME) if contentType == nil { return &ResponseError{ Input: input, Reason: fmt.Sprintf("response header Content-Type has unexpected value: %q", inputMIME), } } if contentType.Schema == nil { // An operation does not contains a validation schema for responses with this status code. return nil } // Read response's body. body := input.Body // Response would contain partial or empty input body // after we begin reading. // Ensure that this doesn't happen. input.Body = nil // Ensure we close the reader defer body.Close() // Read all data, err := ioutil.ReadAll(body) if err != nil { return &ResponseError{ Input: input, Reason: "failed to read response body", Err: err, } } // Put the data back into the response. input.SetBodyBytes(data) encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] } value, err := decodeBody(bytes.NewBuffer(data), input.Header, contentType.Schema, encFn) if err != nil { return &ResponseError{ Input: input, Reason: "failed to decode response body", Err: err, } } opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here opts = append(opts, openapi3.VisitAsRequest()) if options.MultiError { opts = append(opts, openapi3.MultiErrors()) } // Validate data with the schema. if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { return &ResponseError{ Input: input, Reason: "response body doesn't match the schema", Err: err, } } return nil } kin-openapi-0.85.0/openapi3filter/validate_response_input.go000066400000000000000000000015041415236407200242230ustar00rootroot00000000000000package openapi3filter import ( "bytes" "io" "io/ioutil" "net/http" ) type ResponseValidationInput struct { RequestValidationInput *RequestValidationInput Status int Header http.Header Body io.ReadCloser Options *Options } func (input *ResponseValidationInput) SetBodyBytes(value []byte) *ResponseValidationInput { input.Body = ioutil.NopCloser(bytes.NewReader(value)) return input } var JSONPrefixes = []string{ ")]}',\n", } // TrimJSONPrefix trims one of the possible prefixes func TrimJSONPrefix(data []byte) []byte { search: for _, prefix := range JSONPrefixes { if len(data) < len(prefix) { continue } for i, b := range data[:len(prefix)] { if b != prefix[i] { continue search } } return data[len(prefix):] } return data } kin-openapi-0.85.0/openapi3filter/validation_discriminator_test.go000066400000000000000000000042561415236407200254240ustar00rootroot00000000000000package openapi3filter import ( "bytes" "net/http" "testing" "github.com/getkin/kin-openapi/openapi3" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) func TestValidationWithDiscriminatorSelection(t *testing.T) { const spec = ` openapi: 3.0.0 info: version: 0.2.0 title: yaAPI paths: /blob: put: operationId: SetObj requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/blob' responses: '200': description: Ok components: schemas: blob: oneOf: - $ref: '#/components/schemas/objA' - $ref: '#/components/schemas/objB' discriminator: propertyName: discr mapping: objA: '#/components/schemas/objA' objB: '#/components/schemas/objB' genericObj: type: object required: - discr properties: discr: type: string enum: - objA - objB discriminator: propertyName: discr mapping: objA: '#/components/schemas/objA' objB: '#/components/schemas/objB' objA: allOf: - $ref: '#/components/schemas/genericObj' - type: object properties: base64: type: string objB: allOf: - $ref: '#/components/schemas/genericObj' - type: object properties: value: type: integer ` loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(spec)) require.NoError(t, err) router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) body := bytes.NewReader([]byte(`{"discr": "objA", "base64": "S25vY2sgS25vY2ssIE5lbyAuLi4="}`)) req, err := http.NewRequest("PUT", "/blob", body) require.NoError(t, err) req.Header.Add(headerCT, "application/json") route, pathParams, err := router.FindRoute(req) require.NoError(t, err) requestValidationInput := &RequestValidationInput{ Request: req, PathParams: pathParams, Route: route, } err = ValidateRequest(loader.Context, requestValidationInput) require.NoError(t, err) } kin-openapi-0.85.0/openapi3filter/validation_error.go000066400000000000000000000046731415236407200226520ustar00rootroot00000000000000package openapi3filter import ( "bytes" "strconv" ) // ValidationError struct provides granular error information // useful for communicating issues back to end user and developer. // Based on https://jsonapi.org/format/#error-objects type ValidationError struct { // A unique identifier for this particular occurrence of the problem. Id string `json:"id,omitempty"` // The HTTP status code applicable to this problem. Status int `json:"status,omitempty"` // An application-specific error code, expressed as a string value. Code string `json:"code,omitempty"` // A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization. Title string `json:"title,omitempty"` // A human-readable explanation specific to this occurrence of the problem. Detail string `json:"detail,omitempty"` // An object containing references to the source of the error Source *ValidationErrorSource `json:"source,omitempty"` } // ValidationErrorSource struct type ValidationErrorSource struct { // A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute]. Pointer string `json:"pointer,omitempty"` // A string indicating which query parameter caused the error. Parameter string `json:"parameter,omitempty"` } var _ error = &ValidationError{} // Error implements the error interface. func (e *ValidationError) Error() string { b := new(bytes.Buffer) b.WriteString("[") if e.Status != 0 { b.WriteString(strconv.Itoa(e.Status)) } b.WriteString("]") b.WriteString("[") if e.Code != "" { b.WriteString(e.Code) } b.WriteString("]") b.WriteString("[") if e.Id != "" { b.WriteString(e.Id) } b.WriteString("]") b.WriteString(" ") if e.Title != "" { b.WriteString(e.Title) b.WriteString(" ") } if e.Detail != "" { b.WriteString("| ") b.WriteString(e.Detail) b.WriteString(" ") } if e.Source != nil { b.WriteString("[source ") if e.Source.Parameter != "" { b.WriteString("parameter=") b.WriteString(e.Source.Parameter) } else if e.Source.Pointer != "" { b.WriteString("pointer=") b.WriteString(e.Source.Pointer) } b.WriteString("]") } if b.Len() == 0 { return "no error" } return b.String() } // StatusCode implements the StatusCoder interface for DefaultErrorEncoder func (e *ValidationError) StatusCode() int { return e.Status } kin-openapi-0.85.0/openapi3filter/validation_error_encoder.go000066400000000000000000000117511415236407200243440ustar00rootroot00000000000000package openapi3filter import ( "context" "fmt" "net/http" "strings" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" ) // ValidationErrorEncoder wraps a base ErrorEncoder to handle ValidationErrors type ValidationErrorEncoder struct { Encoder ErrorEncoder } // Encode implements the ErrorEncoder interface for encoding ValidationErrors func (enc *ValidationErrorEncoder) Encode(ctx context.Context, err error, w http.ResponseWriter) { if e, ok := err.(*routers.RouteError); ok { cErr := convertRouteError(e) enc.Encoder(ctx, cErr, w) return } e, ok := err.(*RequestError) if !ok { enc.Encoder(ctx, err, w) return } var cErr *ValidationError if e.Err == nil { cErr = convertBasicRequestError(e) } else if e.Err == ErrInvalidRequired { cErr = convertErrInvalidRequired(e) } else if innerErr, ok := e.Err.(*ParseError); ok { cErr = convertParseError(e, innerErr) } else if innerErr, ok := e.Err.(*openapi3.SchemaError); ok { cErr = convertSchemaError(e, innerErr) } if cErr != nil { enc.Encoder(ctx, cErr, w) return } enc.Encoder(ctx, err, w) } func convertRouteError(e *routers.RouteError) *ValidationError { status := http.StatusNotFound if e.Error() == routers.ErrMethodNotAllowed.Error() { status = http.StatusMethodNotAllowed } return &ValidationError{Status: status, Title: e.Error()} } func convertBasicRequestError(e *RequestError) *ValidationError { if strings.HasPrefix(e.Reason, prefixInvalidCT) { if strings.HasSuffix(e.Reason, `""`) { return &ValidationError{ Status: http.StatusUnsupportedMediaType, Title: "header Content-Type is required", } } return &ValidationError{ Status: http.StatusUnsupportedMediaType, Title: prefixUnsupportedCT + strings.TrimPrefix(e.Reason, prefixInvalidCT), } } return &ValidationError{ Status: http.StatusBadRequest, Title: e.Error(), } } func convertErrInvalidRequired(e *RequestError) *ValidationError { if e.Err == ErrInvalidRequired && e.Parameter != nil { return &ValidationError{ Status: http.StatusBadRequest, Title: fmt.Sprintf("parameter %q in %s is required", e.Parameter.Name, e.Parameter.In), } } return &ValidationError{ Status: http.StatusBadRequest, Title: e.Error(), } } func convertParseError(e *RequestError, innerErr *ParseError) *ValidationError { // We treat path params of the wrong type like a 404 instead of a 400 if innerErr.Kind == KindInvalidFormat && e.Parameter != nil && e.Parameter.In == "path" { return &ValidationError{ Status: http.StatusNotFound, Title: fmt.Sprintf("resource not found with %q value: %v", e.Parameter.Name, innerErr.Value), } } else if strings.HasPrefix(innerErr.Reason, prefixUnsupportedCT) { return &ValidationError{ Status: http.StatusUnsupportedMediaType, Title: innerErr.Reason, } } else if innerErr.RootCause() != nil { if rootErr, ok := innerErr.Cause.(*ParseError); ok && rootErr.Kind == KindInvalidFormat && e.Parameter.In == "query" { return &ValidationError{ Status: http.StatusBadRequest, Title: fmt.Sprintf("parameter %q in %s is invalid: %v is %s", e.Parameter.Name, e.Parameter.In, rootErr.Value, rootErr.Reason), } } return &ValidationError{ Status: http.StatusBadRequest, Title: innerErr.Reason, } } return nil } func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *ValidationError { cErr := &ValidationError{Title: innerErr.Reason} // Handle "Origin" error if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok { cErr = convertSchemaError(e, originErr) } // Add http status code if e.Parameter != nil { cErr.Status = http.StatusBadRequest } else if e.RequestBody != nil { cErr.Status = http.StatusUnprocessableEntity } // Add error source if e.Parameter != nil { // We have a JSONPointer in the query param too so need to // make sure 'Parameter' check takes priority over 'Pointer' cErr.Source = &ValidationErrorSource{Parameter: e.Parameter.Name} } else if ptr := innerErr.JSONPointer(); ptr != nil { cErr.Source = &ValidationErrorSource{Pointer: toJSONPointer(ptr)} } // Add details on allowed values for enums if innerErr.SchemaField == "enum" { enums := make([]string, 0, len(innerErr.Schema.Enum)) for _, enum := range innerErr.Schema.Enum { enums = append(enums, fmt.Sprintf("%v", enum)) } cErr.Detail = fmt.Sprintf("value %v at %s must be one of: %s", innerErr.Value, toJSONPointer(innerErr.JSONPointer()), strings.Join(enums, ", ")) value := fmt.Sprintf("%v", innerErr.Value) if e.Parameter != nil && (e.Parameter.Explode == nil || *e.Parameter.Explode) && (e.Parameter.Style == "" || e.Parameter.Style == "form") && strings.Contains(value, ",") { parts := strings.Split(value, ",") cErr.Detail = fmt.Sprintf("%s; perhaps you intended '?%s=%s'", cErr.Detail, e.Parameter.Name, strings.Join(parts, "&"+e.Parameter.Name+"=")) } } return cErr } func toJSONPointer(reversePath []string) string { return "/" + strings.Join(reversePath, "/") } kin-openapi-0.85.0/openapi3filter/validation_error_test.go000066400000000000000000000572631415236407200237140ustar00rootroot00000000000000package openapi3filter import ( "bytes" "context" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" "github.com/stretchr/testify/require" ) func newPetstoreRequest(t *testing.T, method, path string, body io.Reader) *http.Request { host := "petstore.swagger.io" pathPrefix := "v2" r, err := http.NewRequest(method, fmt.Sprintf("http://%s/%s%s", host, pathPrefix, path), body) require.NoError(t, err) r.Header.Set(headerCT, "application/json") r.Header.Set("Authorization", "Bearer magicstring") r.Host = host return r } type validationFields struct { Handler http.Handler File string ErrorEncoder ErrorEncoder } type validationArgs struct { r *http.Request } type validationTest struct { name string fields validationFields args validationArgs wantErr bool wantErrBody string wantErrReason string wantErrSchemaReason string wantErrSchemaPath string wantErrSchemaValue interface{} wantErrSchemaOriginReason string wantErrSchemaOriginPath string wantErrSchemaOriginValue interface{} wantErrParam string wantErrParamIn string wantErrParseKind ParseErrorKind wantErrParseValue interface{} wantErrParseReason string wantErrResponse *ValidationError } func getValidationTests(t *testing.T) []*validationTest { badHost, err := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil) require.NoError(t, err) badPath := newPetstoreRequest(t, http.MethodGet, "/watdis", nil) badMethod := newPetstoreRequest(t, http.MethodTrace, "/pet", nil) missingBody1 := newPetstoreRequest(t, http.MethodPost, "/pet", nil) missingBody2 := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(``)) noContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) noContentType.Header.Del(headerCT) noContentTypeNeeded := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil) noContentTypeNeeded.Header.Del(headerCT) unknownContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) unknownContentType.Header.Set(headerCT, "application/xml") unsupportedContentType := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) unsupportedContentType.Header.Set(headerCT, "text/plain") unsupportedHeaderValue := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{}`)) unsupportedHeaderValue.Header.Set("x-environment", "watdis") return []*validationTest{ // // Basics // { name: "error - unknown host", args: validationArgs{ r: badHost, }, wantErrReason: routers.ErrPathNotFound.Error(), wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: routers.ErrPathNotFound.Error()}, }, { name: "error - unknown path", args: validationArgs{ r: badPath, }, wantErrReason: routers.ErrPathNotFound.Error(), wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: routers.ErrPathNotFound.Error()}, }, { name: "error - unknown method", args: validationArgs{ r: badMethod, }, wantErrReason: routers.ErrMethodNotAllowed.Error(), // TODO: By HTTP spec, this should have an Allow header with what is allowed // but kin-openapi doesn't provide us the requested method or path, so impossible to provide details wantErrResponse: &ValidationError{Status: http.StatusMethodNotAllowed, Title: routers.ErrMethodNotAllowed.Error()}, }, { name: "error - missing body on POST", args: validationArgs{ r: missingBody1, }, wantErrBody: "request body has an error: " + ErrInvalidRequired.Error(), wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: "request body has an error: " + ErrInvalidRequired.Error()}, }, { name: "error - empty body on POST", args: validationArgs{ r: missingBody2, }, wantErrBody: "request body has an error: " + ErrInvalidRequired.Error(), wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: "request body has an error: " + ErrInvalidRequired.Error()}, }, // // Content-Type // { name: "error - missing content-type on POST", args: validationArgs{ r: noContentType, }, wantErrReason: prefixInvalidCT + ` ""`, wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType, Title: "header Content-Type is required"}, }, { name: "error - unknown content-type on POST", args: validationArgs{ r: unknownContentType, }, wantErrReason: "failed to decode request body", wantErrParseKind: KindUnsupportedFormat, wantErrParseReason: prefixUnsupportedCT + ` "application/xml"`, wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType, Title: prefixUnsupportedCT + ` "application/xml"`}, }, { name: "error - unsupported content-type on POST", args: validationArgs{ r: unsupportedContentType, }, wantErrReason: prefixInvalidCT + ` "text/plain"`, wantErrResponse: &ValidationError{Status: http.StatusUnsupportedMediaType, Title: prefixUnsupportedCT + ` "text/plain"`}, }, { name: "success - no content-type header required on GET", args: validationArgs{ r: noContentTypeNeeded, }, }, // // Query strings // { name: "error - missing required query string parameter", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus", nil), }, wantErrParam: "status", wantErrParamIn: "query", wantErrBody: `parameter "status" in query has an error: value is required but missing`, wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "status" in query is required`}, }, { name: "error - wrong query string parameter type", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByIds?ids=1,notAnInt", nil), }, wantErrParam: "ids", wantErrParamIn: "query", // This is a nested ParseError. The outer error is a KindOther with no details. // So we'd need to look at the inner one which is a KindInvalidFormat. So just check the error body. wantErrBody: `parameter "ids" in query has an error: path 1: value notAnInt: an invalid integer: ` + "strconv.ParseFloat: parsing \"notAnInt\": invalid syntax", // TODO: Should we treat query params of the wrong type like a 404 instead of a 400? wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "ids" in query is invalid: notAnInt is an invalid integer`}, }, { name: "success - ignores unknown query string parameter", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?wat=isdis", nil), }, }, { name: "success - normal case, query strings", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=available", nil), }, }, { name: "success - normal case, query strings, array", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=available&status=sold", nil), }, }, { name: "error - invalid query string array serialization", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=available,sold", nil), }, wantErrParam: "status", wantErrParamIn: "query", wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaPath: "/0", wantErrSchemaValue: "available,sold", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: "value is not one of the allowed values", Detail: "value available,sold at /0 must be one of: available, pending, sold; " + // TODO: do we really want to use this heuristic to guess // that they're using the wrong serialization? "perhaps you intended '?status=available&status=sold'", Source: &ValidationErrorSource{Parameter: "status"}}, }, { name: "error - invalid enum value for query string parameter", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold&status=watdis", nil), }, wantErrParam: "status", wantErrParamIn: "query", wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaPath: "/1", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: "value is not one of the allowed values", Detail: "value watdis at /1 must be one of: available, pending, sold", Source: &ValidationErrorSource{Parameter: "status"}}, }, { name: "error - invalid enum value, allowing commas (without 'perhaps you intended' recommendation)", args: validationArgs{ // fish,with,commas isn't an enum value r: newPetstoreRequest(t, http.MethodGet, "/pet/findByKind?kind=dog|fish,with,commas", nil), }, wantErrParam: "kind", wantErrParamIn: "query", wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaPath: "/1", wantErrSchemaValue: "fish,with,commas", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: "value is not one of the allowed values", Detail: "value fish,with,commas at /1 must be one of: dog, cat, turtle, bird,with,commas", // No 'perhaps you intended' because its the right serialization format Source: &ValidationErrorSource{Parameter: "kind"}}, }, { name: "success - valid enum value, allowing commas", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/findByKind?kind=dog|bird,with,commas", nil), }, }, // // Request header params // { name: "error - invalid enum value for header string parameter", args: validationArgs{ r: unsupportedHeaderValue, }, wantErrParam: "x-environment", wantErrParamIn: "header", wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaPath: "/", wantErrSchemaValue: "watdis", wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: "value is not one of the allowed values", Detail: "value watdis at / must be one of: demo, prod", Source: &ValidationErrorSource{Parameter: "x-environment"}}, }, // // Request bodies // { name: "error - invalid enum value for header object attribute", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"status":"watdis"}`)), }, wantErrReason: "doesn't match the schema", wantErrSchemaReason: "value is not one of the allowed values", wantErrSchemaValue: "watdis", wantErrSchemaPath: "/status", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, Title: "value is not one of the allowed values", Detail: "value watdis at /status must be one of: available, pending, sold", Source: &ValidationErrorSource{Pointer: "/status"}}, }, { name: "error - missing required object attribute", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama"}`)), }, wantErrReason: "doesn't match the schema", wantErrSchemaReason: `property "photoUrls" is missing`, wantErrSchemaValue: map[string]string{"name": "Bahama"}, wantErrSchemaPath: "/photoUrls", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, Title: `property "photoUrls" is missing`, Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, }, { name: "error - missing required nested object attribute", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{}}`)), }, wantErrReason: "doesn't match the schema", wantErrSchemaReason: `property "name" is missing`, wantErrSchemaValue: map[string]string{}, wantErrSchemaPath: "/category/name", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, Title: `property "name" is missing`, Source: &ValidationErrorSource{Pointer: "/category/name"}}, }, { name: "error - missing required deeply nested object attribute", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":[],"category":{"tags": [{}]}}`)), }, wantErrReason: "doesn't match the schema", wantErrSchemaReason: `property "name" is missing`, wantErrSchemaValue: map[string]string{}, wantErrSchemaPath: "/category/tags/0/name", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, Title: `property "name" is missing`, Source: &ValidationErrorSource{Pointer: "/category/tags/0/name"}}, }, { // TODO: Add support for validating readonly properties to upstream validator. name: "error - readonly object attribute", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"id":213,"name":"Bahama","photoUrls":[]}}`)), }, //wantErr: true, }, { name: "error - wrong attribute type", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)), }, wantErrReason: "doesn't match the schema", wantErrSchemaReason: "Field must be set to array or not be present", wantErrSchemaPath: "/photoUrls", wantErrSchemaValue: "string", // TODO: this shouldn't say "or not be present", but this requires recursively resolving // innerErr.JSONPointer() against e.RequestBody.Content["application/json"].Schema.Value (.Required, .Properties) wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, Title: "Field must be set to array or not be present", Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, }, { name: "error - missing required object attribute from allOf required overlay", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet2", bytes.NewBufferString(`{"name":"Bahama"}`)), }, wantErrReason: "doesn't match the schema", wantErrSchemaPath: "/", wantErrSchemaValue: map[string]string{"name": "Bahama"}, wantErrSchemaOriginReason: `property "photoUrls" is missing`, wantErrSchemaOriginValue: map[string]string{"name": "Bahama"}, wantErrSchemaOriginPath: "/photoUrls", wantErrResponse: &ValidationError{Status: http.StatusUnprocessableEntity, Title: `property "photoUrls" is missing`, Source: &ValidationErrorSource{Pointer: "/photoUrls"}}, }, { name: "success - ignores unknown object attribute", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"wat":"isdis","name":"Bahama","photoUrls":[]}`)), }, }, { name: "success - normal case, POST", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":[]}`)), }, }, { name: "success - required properties are not required on PATCH if required overlaid using allOf elsewhere", args: validationArgs{ r: newPetstoreRequest(t, http.MethodPatch, "/pet", bytes.NewBufferString(`{}`)), }, }, // // Path params // { name: "error - missing path param", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/", nil), }, wantErrParam: "petId", wantErrParamIn: "path", wantErrBody: `parameter "petId" in path has an error: value is required but missing`, wantErrResponse: &ValidationError{Status: http.StatusBadRequest, Title: `parameter "petId" in path is required`}, }, { name: "error - wrong path param type", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/NotAnInt", nil), }, wantErrParam: "petId", wantErrParamIn: "path", wantErrParseKind: KindInvalidFormat, wantErrParseValue: "NotAnInt", wantErrParseReason: "an invalid integer", wantErrResponse: &ValidationError{Status: http.StatusNotFound, Title: `resource not found with "petId" value: NotAnInt`}, }, { name: "success - normal case, with path params", args: validationArgs{ r: newPetstoreRequest(t, http.MethodGet, "/pet/23", nil), }, }, } } func TestValidationHandler_validateRequest(t *testing.T) { tests := getValidationTests(t) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := require.New(t) h, err := buildValidationHandler(tt) req.NoError(err) err = h.validateRequest(tt.args.r) req.Equal(tt.wantErr, err != nil) if err != nil { if tt.wantErrBody != "" { req.Equal(tt.wantErrBody, err.Error()) } if e, ok := err.(*routers.RouteError); ok { req.Equal(tt.wantErrReason, e.Error()) return } e, ok := err.(*RequestError) req.True(ok, "not a RequestError: %T -- %#v", err, err) req.Equal(tt.wantErrReason, e.Reason) if e.Parameter != nil { req.Equal(tt.wantErrParam, e.Parameter.Name) req.Equal(tt.wantErrParamIn, e.Parameter.In) } else { req.False(tt.wantErrParam != "" || tt.wantErrParamIn != "", "error = %v, no Parameter -- %#v", e, e) } if innerErr, ok := e.Err.(*openapi3.SchemaError); ok { req.Equal(tt.wantErrSchemaReason, innerErr.Reason) pointer := toJSONPointer(innerErr.JSONPointer()) req.Equal(tt.wantErrSchemaPath, pointer) req.Equal(fmt.Sprintf("%v", tt.wantErrSchemaValue), fmt.Sprintf("%v", innerErr.Value)) if originErr, ok := innerErr.Origin.(*openapi3.SchemaError); ok { req.Equal(tt.wantErrSchemaOriginReason, originErr.Reason) pointer := toJSONPointer(originErr.JSONPointer()) req.Equal(tt.wantErrSchemaOriginPath, pointer) req.Equal(fmt.Sprintf("%v", tt.wantErrSchemaOriginValue), fmt.Sprintf("%v", originErr.Value)) } } else { req.False(tt.wantErrSchemaReason != "" || tt.wantErrSchemaPath != "", "error = %v, not a SchemaError -- %#v", e.Err, e.Err) req.False(tt.wantErrSchemaOriginReason != "" || tt.wantErrSchemaOriginPath != "", "error = %v, not a SchemaError with Origin -- %#v", e.Err, e.Err) } if innerErr, ok := e.Err.(*ParseError); ok { req.Equal(tt.wantErrParseKind, innerErr.Kind) req.Equal(tt.wantErrParseValue, innerErr.Value) req.Equal(tt.wantErrParseReason, innerErr.Reason) } else { req.False(tt.wantErrParseValue != nil || tt.wantErrParseReason != "", "error = %v, not a ParseError -- %#v", e.Err, e.Err) } } }) } } func TestValidationErrorEncoder(t *testing.T) { tests := getValidationTests(t) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockEncoder := &mockErrorEncoder{} encoder := &ValidationErrorEncoder{Encoder: mockEncoder.Encode} req := require.New(t) h, err := buildValidationHandler(tt) req.NoError(err) err = h.validateRequest(tt.args.r) req.Equal(tt.wantErr, err != nil) if err != nil { encoder.Encode(tt.args.r.Context(), err, httptest.NewRecorder()) if tt.wantErrResponse != mockEncoder.Err { req.Equal(tt.wantErrResponse, mockEncoder.Err) } } }) } } func buildValidationHandler(tt *validationTest) (*ValidationHandler, error) { if tt.fields.File == "" { tt.fields.File = "fixtures/petstore.json" } h := &ValidationHandler{ Handler: tt.fields.Handler, File: tt.fields.File, ErrorEncoder: tt.fields.ErrorEncoder, } tt.wantErr = tt.wantErr || (tt.wantErrBody != "") || (tt.wantErrReason != "") || (tt.wantErrSchemaReason != "") || (tt.wantErrSchemaPath != "") || (tt.wantErrParseValue != nil) || (tt.wantErrParseReason != "") err := h.Load() return h, err } type testHandler struct { Called bool } func (h *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.Called = true } type mockErrorEncoder struct { Called bool Ctx context.Context Err error W http.ResponseWriter } func (e *mockErrorEncoder) Encode(ctx context.Context, err error, w http.ResponseWriter) { e.Called = true e.Ctx = ctx e.Err = err e.W = w } func runTest_ServeHTTP(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http.Request) *http.Response { h := &ValidationHandler{ Handler: handler, ErrorEncoder: encoder, File: "fixtures/petstore.json", } err := h.Load() require.NoError(t, err) w := httptest.NewRecorder() h.ServeHTTP(w, req) return w.Result() } func runTest_Middleware(t *testing.T, handler http.Handler, encoder ErrorEncoder, req *http.Request) *http.Response { h := &ValidationHandler{ ErrorEncoder: encoder, File: "fixtures/petstore.json", } err := h.Load() require.NoError(t, err) w := httptest.NewRecorder() h.Middleware(handler).ServeHTTP(w, req) return w.Result() } func TestValidationHandler_ServeHTTP(t *testing.T) { t.Run("errors on invalid requests", func(t *testing.T) { httpCtx := context.WithValue(context.Background(), "pig", "tails") r, err := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil) require.NoError(t, err) r = r.WithContext(httpCtx) handler := &testHandler{} encoder := &mockErrorEncoder{} runTest_ServeHTTP(t, handler, encoder.Encode, r) require.False(t, handler.Called) require.True(t, encoder.Called) require.Equal(t, httpCtx, encoder.Ctx) require.NotNil(t, encoder.Err) }) t.Run("passes valid requests through", func(t *testing.T) { r := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil) handler := &testHandler{} encoder := &mockErrorEncoder{} runTest_ServeHTTP(t, handler, encoder.Encode, r) require.True(t, handler.Called) require.False(t, encoder.Called) }) t.Run("uses error encoder", func(t *testing.T) { r := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)) handler := &testHandler{} encoder := &ValidationErrorEncoder{Encoder: (ErrorEncoder)(DefaultErrorEncoder)} resp := runTest_ServeHTTP(t, handler, encoder.Encode, r) body, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) require.Equal(t, "[422][][] Field must be set to array or not be present [source pointer=/photoUrls]", string(body)) }) } func TestValidationHandler_Middleware(t *testing.T) { t.Run("errors on invalid requests", func(t *testing.T) { httpCtx := context.WithValue(context.Background(), "pig", "tails") r, err := http.NewRequest(http.MethodGet, "http://unknown-host.com/v2/pet", nil) require.NoError(t, err) r = r.WithContext(httpCtx) handler := &testHandler{} encoder := &mockErrorEncoder{} runTest_Middleware(t, handler, encoder.Encode, r) require.False(t, handler.Called) require.True(t, encoder.Called) require.Equal(t, httpCtx, encoder.Ctx) require.NotNil(t, encoder.Err) }) t.Run("passes valid requests through", func(t *testing.T) { r := newPetstoreRequest(t, http.MethodGet, "/pet/findByStatus?status=sold", nil) handler := &testHandler{} encoder := &mockErrorEncoder{} runTest_Middleware(t, handler, encoder.Encode, r) require.True(t, handler.Called) require.False(t, encoder.Called) }) t.Run("uses error encoder", func(t *testing.T) { r := newPetstoreRequest(t, http.MethodPost, "/pet", bytes.NewBufferString(`{"name":"Bahama","photoUrls":"http://cat"}`)) handler := &testHandler{} encoder := &ValidationErrorEncoder{Encoder: (ErrorEncoder)(DefaultErrorEncoder)} resp := runTest_Middleware(t, handler, encoder.Encode, r) body, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) require.Equal(t, "[422][][] Field must be set to array or not be present [source pointer=/photoUrls]", string(body)) }) } kin-openapi-0.85.0/openapi3filter/validation_handler.go000066400000000000000000000045231415236407200231300ustar00rootroot00000000000000package openapi3filter import ( "context" "net/http" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" ) type AuthenticationFunc func(context.Context, *AuthenticationInput) error func NoopAuthenticationFunc(context.Context, *AuthenticationInput) error { return nil } var _ AuthenticationFunc = NoopAuthenticationFunc type ValidationHandler struct { Handler http.Handler AuthenticationFunc AuthenticationFunc File string ErrorEncoder ErrorEncoder router routers.Router } func (h *ValidationHandler) Load() error { loader := openapi3.NewLoader() doc, err := loader.LoadFromFile(h.File) if err != nil { return err } if err := doc.Validate(loader.Context); err != nil { return err } if h.router, err = legacyrouter.NewRouter(doc); err != nil { return err } // set defaults if h.Handler == nil { h.Handler = http.DefaultServeMux } if h.AuthenticationFunc == nil { h.AuthenticationFunc = NoopAuthenticationFunc } if h.ErrorEncoder == nil { h.ErrorEncoder = DefaultErrorEncoder } return nil } func (h *ValidationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if handled := h.before(w, r); handled { return } // TODO: validateResponse h.Handler.ServeHTTP(w, r) } // Middleware implements gorilla/mux MiddlewareFunc func (h *ValidationHandler) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if handled := h.before(w, r); handled { return } // TODO: validateResponse next.ServeHTTP(w, r) }) } func (h *ValidationHandler) before(w http.ResponseWriter, r *http.Request) (handled bool) { if err := h.validateRequest(r); err != nil { h.ErrorEncoder(r.Context(), err, w) return true } return false } func (h *ValidationHandler) validateRequest(r *http.Request) error { // Find route route, pathParams, err := h.router.FindRoute(r) if err != nil { return err } options := &Options{ AuthenticationFunc: h.AuthenticationFunc, } // Validate request requestValidationInput := &RequestValidationInput{ Request: r, PathParams: pathParams, Route: route, Options: options, } if err = ValidateRequest(r.Context(), requestValidationInput); err != nil { return err } return nil } kin-openapi-0.85.0/openapi3filter/validation_kit.go000066400000000000000000000070401415236407200222770ustar00rootroot00000000000000package openapi3filter import ( "context" "encoding/json" "net/http" ) /////////////////////////////////////////////////////////////////////////////////// // We didn't want to tie kin-openapi too tightly with go-kit. // This file contains the ErrorEncoder and DefaultErrorEncoder function // borrowed from this project. // // The MIT License (MIT) // // Copyright (c) 2015 Peter Bourgon // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. /////////////////////////////////////////////////////////////////////////////////// // ErrorEncoder is responsible for encoding an error to the ResponseWriter. // Users are encouraged to use custom ErrorEncoders to encode HTTP errors to // their clients, and will likely want to pass and check for their own error // types. See the example shipping/handling service. type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter) // StatusCoder is checked by DefaultErrorEncoder. If an error value implements // StatusCoder, the StatusCode will be used when encoding the error. By default, // StatusInternalServerError (500) is used. type StatusCoder interface { StatusCode() int } // Headerer is checked by DefaultErrorEncoder. If an error value implements // Headerer, the provided headers will be applied to the response writer, after // the Content-Type is set. type Headerer interface { Headers() http.Header } // DefaultErrorEncoder writes the error to the ResponseWriter, by default a // content type of text/plain, a body of the plain text of the error, and a // status code of 500. If the error implements Headerer, the provided headers // will be applied to the response. If the error implements json.Marshaler, and // the marshaling succeeds, a content type of application/json and the JSON // encoded form of the error will be used. If the error implements StatusCoder, // the provided StatusCode will be used instead of 500. func DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter) { contentType, body := "text/plain; charset=utf-8", []byte(err.Error()) if marshaler, ok := err.(json.Marshaler); ok { if jsonBody, marshalErr := marshaler.MarshalJSON(); marshalErr == nil { contentType, body = "application/json; charset=utf-8", jsonBody } } w.Header().Set("Content-Type", contentType) if headerer, ok := err.(Headerer); ok { for k, values := range headerer.Headers() { for _, v := range values { w.Header().Add(k, v) } } } code := http.StatusInternalServerError if sc, ok := err.(StatusCoder); ok { code = sc.StatusCode() } w.WriteHeader(code) w.Write(body) } kin-openapi-0.85.0/openapi3filter/validation_test.go000066400000000000000000000536421415236407200225000ustar00rootroot00000000000000package openapi3filter import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "net/url" "reflect" "strings" "testing" "github.com/getkin/kin-openapi/openapi3" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) type ExampleRequest struct { Method string URL string ContentType string Body interface{} } type ExampleResponse struct { Status int ContentType string Body interface{} } type ExampleSecurityScheme struct { Name string Scheme *openapi3.SecurityScheme } func TestFilter(t *testing.T) { // Declare a schema for an object with name and id properties complexArgSchema := openapi3.NewObjectSchema(). WithProperty("name", openapi3.NewStringSchema()). WithProperty("id", openapi3.NewStringSchema().WithMaxLength(2)) complexArgSchema.Required = []string{"name", "id"} // Declare router doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", Version: "0.1", }, Servers: openapi3.Servers{ { URL: "http://example.com/api/", }, }, Paths: openapi3.Paths{ "/prefix/{pathArg}/suffix": &openapi3.PathItem{ Post: &openapi3.Operation{ Parameters: openapi3.Parameters{ { Value: &openapi3.Parameter{ In: "path", Name: "pathArg", Schema: openapi3.NewStringSchema().WithMaxLength(2).NewRef(), }, }, { Value: &openapi3.Parameter{ In: "query", Name: "queryArg", Schema: openapi3.NewStringSchema().WithMaxLength(2).NewRef(), }, }, { Value: &openapi3.Parameter{ In: "query", Name: "queryArgAnyOf", Schema: openapi3.NewAnyOfSchema( openapi3.NewStringSchema().WithMaxLength(2), openapi3.NewDateTimeSchema(), ).NewRef(), }, }, { Value: &openapi3.Parameter{ In: "query", Name: "queryArgOneOf", Schema: openapi3.NewOneOfSchema( openapi3.NewStringSchema().WithMaxLength(2), openapi3.NewInt32Schema(), ).NewRef(), }, }, { Value: &openapi3.Parameter{ In: "query", Name: "queryArgAllOf", Schema: openapi3.NewAllOfSchema( openapi3.NewDateTimeSchema(), openapi3.NewStringSchema(), ).NewRef(), }, }, // TODO(decode not): handle decoding "not" Schema // { // Value: &openapi3.Parameter{ // In: "query", // Name: "queryArgNot", // Schema: &openapi3.SchemaRef{ // Value: &openapi3.Schema{ // Not: &openapi3.SchemaRef{ // Value: openapi3.NewInt32Schema(), // }}}, // }, // }, { Value: &openapi3.Parameter{ In: "query", Name: "contentArg", Content: openapi3.NewContentWithJSONSchema(complexArgSchema), }, }, { Value: &openapi3.Parameter{ In: "query", Name: "contentArg2", Content: openapi3.Content{ "application/something_funny": openapi3.NewMediaType().WithSchema(complexArgSchema), }, }, }, }, Responses: openapi3.NewResponses(), }, }, "/issue151": &openapi3.PathItem{ Get: &openapi3.Operation{ Responses: openapi3.NewResponses(), }, Parameters: openapi3.Parameters{ { Value: &openapi3.Parameter{ In: "query", Name: "par1", Required: true, Schema: openapi3.NewIntegerSchema().NewRef(), }, }, }, }, }, } err := doc.Validate(context.Background()) require.NoError(t, err) router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) expectWithDecoder := func(req ExampleRequest, resp ExampleResponse, decoder ContentParameterDecoder) error { t.Logf("Request: %s %s", req.Method, req.URL) httpReq, err := http.NewRequest(req.Method, req.URL, marshalReader(req.Body)) require.NoError(t, err) httpReq.Header.Set(headerCT, req.ContentType) // Find route route, pathParams, err := router.FindRoute(httpReq) require.NoError(t, err) // Validate request requestValidationInput := &RequestValidationInput{ Request: httpReq, PathParams: pathParams, Route: route, ParamDecoder: decoder, } if err := ValidateRequest(context.Background(), requestValidationInput); err != nil { return err } t.Logf("Response: %d", resp.Status) responseValidationInput := &ResponseValidationInput{ RequestValidationInput: requestValidationInput, Status: resp.Status, Header: http.Header{ headerCT: []string{ resp.ContentType, }, }, } if resp.Body != nil { data, err := json.Marshal(resp.Body) require.NoError(t, err) responseValidationInput.SetBodyBytes(data) } err = ValidateResponse(context.Background(), responseValidationInput) require.NoError(t, err) return err } expect := func(req ExampleRequest, resp ExampleResponse) error { return expectWithDecoder(req, resp, nil) } resp := ExampleResponse{ Status: 200, } // Test paths req := ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix", } err = expect(req, resp) require.NoError(t, err) // Test query parameter openapi3filter req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/EXCEEDS_MAX_LENGTH/suffix", } err = expect(req, resp) require.IsType(t, &RequestError{}, err) // Test query parameter openapi3filter req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArg=a", } err = expect(req, resp) require.NoError(t, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArg=EXCEEDS_MAX_LENGTH", } err = expect(req, resp) require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "GET", URL: "http://example.com/api/issue151?par2=par1_is_missing", } err = expect(req, resp) require.IsType(t, &RequestError{}, err) // Test query parameter openapi3filter req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArgAnyOf=ae&queryArgOneOf=ac&queryArgAllOf=2017-12-31T11:59:59", } err = expect(req, resp) require.NoError(t, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArgAnyOf=2017-12-31T11:59:59", } err = expect(req, resp) require.NoError(t, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArgAnyOf=123", } err = expect(req, resp) require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArgOneOf=567", } err = expect(req, resp) require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArgOneOf=2017-12-31T11:59:59", } err = expect(req, resp) require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArgAllOf=abdfg", } err = expect(req, resp) require.IsType(t, &RequestError{}, err) // TODO(decode not): handle decoding "not" Schema // req = ExampleRequest{ // Method: "POST", // URL: "http://example.com/api/prefix/v/suffix?queryArgNot=abdfg", // } // err = expect(req, resp) // require.IsType(t, &RequestError{}, err) // TODO(decode not): handle decoding "not" Schema // req = ExampleRequest{ // Method: "POST", // URL: "http://example.com/api/prefix/v/suffix?queryArgNot=123", // } // err = expect(req, resp) // require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?queryArg=EXCEEDS_MAX_LENGTH", } err = expect(req, resp) require.IsType(t, &RequestError{}, err) req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix", } resp = ExampleResponse{ Status: 200, } err = expect(req, resp) // require.IsType(t, &ResponseError{}, err) require.NoError(t, err) // Check that content validation works. This should pass, as ID is short // enough. req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?contentArg={\"name\":\"bob\", \"id\":\"a\"}", } err = expect(req, resp) require.NoError(t, err) // Now it should fail due the ID being too long req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?contentArg={\"name\":\"bob\", \"id\":\"EXCEEDS_MAX_LENGTH\"}", } err = expect(req, resp) require.IsType(t, &RequestError{}, err) // Now, repeat the above two test cases using a custom parameter decoder. customDecoder := func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error) { var value interface{} err := json.Unmarshal([]byte(values[0]), &value) schema := param.Content.Get("application/something_funny").Schema.Value return value, schema, err } req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?contentArg2={\"name\":\"bob\", \"id\":\"a\"}", } err = expectWithDecoder(req, resp, customDecoder) require.NoError(t, err) // Now it should fail due the ID being too long req = ExampleRequest{ Method: "POST", URL: "http://example.com/api/prefix/v/suffix?contentArg2={\"name\":\"bob\", \"id\":\"EXCEEDS_MAX_LENGTH\"}", } err = expectWithDecoder(req, resp, customDecoder) require.IsType(t, &RequestError{}, err) } func marshalReader(value interface{}) io.ReadCloser { if value == nil { return nil } data, err := json.Marshal(value) if err != nil { panic(err) } return ioutil.NopCloser(bytes.NewReader(data)) } func TestValidateRequestBody(t *testing.T) { requiredReqBody := openapi3.NewRequestBody(). WithContent(openapi3.NewContentWithJSONSchema(openapi3.NewStringSchema())). WithRequired(true) plainTextContent := openapi3.NewContent() plainTextContent["text/plain"] = openapi3.NewMediaType().WithSchema(openapi3.NewStringSchema()) testCases := []struct { name string body *openapi3.RequestBody mime string data io.Reader wantErr error }{ { name: "non required empty", body: openapi3.NewRequestBody(). WithContent(openapi3.NewContentWithJSONSchema(openapi3.NewStringSchema())), }, { name: "non required not empty", body: openapi3.NewRequestBody(). WithContent(openapi3.NewContentWithJSONSchema(openapi3.NewStringSchema())), mime: "application/json", data: toJSON("foo"), }, { name: "required empty", body: requiredReqBody, wantErr: &RequestError{RequestBody: requiredReqBody, Err: ErrInvalidRequired}, }, { name: "required not empty", body: requiredReqBody, mime: "application/json", data: toJSON("foo"), }, { name: "not JSON data", body: openapi3.NewRequestBody().WithContent(plainTextContent).WithRequired(true), mime: "text/plain", data: strings.NewReader("foo"), }, { name: "not declared content", body: openapi3.NewRequestBody().WithRequired(true), mime: "application/json", data: toJSON("foo"), }, { name: "not declared schema", body: openapi3.NewRequestBody().WithJSONSchemaRef(nil), mime: "application/json", data: toJSON("foo"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/test", tc.data) if tc.mime != "" { req.Header.Set(headerCT, tc.mime) } inp := &RequestValidationInput{Request: req} err := ValidateRequestBody(context.Background(), inp, tc.body) if tc.wantErr == nil { require.NoError(t, err) return } require.True(t, matchReqBodyError(tc.wantErr, err), "got error:\n%s\nwant error\n%s", err, tc.wantErr) }) } } func matchReqBodyError(want, got error) bool { if want == got { return true } wErr, ok := want.(*RequestError) if !ok { return false } gErr, ok := got.(*RequestError) if !ok { return false } if !reflect.DeepEqual(wErr.RequestBody, gErr.RequestBody) { return false } if wErr.Err != nil { return matchReqBodyError(wErr.Err, gErr.Err) } return false } func toJSON(v interface{}) io.Reader { data, err := json.Marshal(v) if err != nil { panic(err) } return bytes.NewReader(data) } func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *testing.T) { // Create the security schemes securitySchemes := []ExampleSecurityScheme{ { Name: "apikey", Scheme: &openapi3.SecurityScheme{ Type: "apiKey", Name: "apikey", In: "cookie", }, }, { Name: "http-basic", Scheme: &openapi3.SecurityScheme{ Type: "http", Scheme: "basic", }, }, } // Create the test cases tc := []struct { name string schemes *[]ExampleSecurityScheme expectedSchemes *[]ExampleSecurityScheme }{ { name: "/inherited-security", schemes: nil, expectedSchemes: &[]ExampleSecurityScheme{ securitySchemes[1], }, }, { name: "/overwrite-without-security", schemes: &[]ExampleSecurityScheme{}, expectedSchemes: &[]ExampleSecurityScheme{}, }, { name: "/overwrite-with-security", schemes: &[]ExampleSecurityScheme{ securitySchemes[0], }, expectedSchemes: &[]ExampleSecurityScheme{ securitySchemes[0], }, }, } doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", Version: "0.1", }, Paths: map[string]*openapi3.PathItem{}, Security: openapi3.SecurityRequirements{ { securitySchemes[1].Name: {}, }, }, Components: openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, } // Add the security schemes to the components for _, scheme := range securitySchemes { doc.Components.SecuritySchemes[scheme.Name] = &openapi3.SecuritySchemeRef{ Value: scheme.Scheme, } } // Add the paths from the test cases to the spec's paths for _, tc := range tc { var securityRequirements *openapi3.SecurityRequirements = nil if tc.schemes != nil { tempS := openapi3.NewSecurityRequirements() for _, scheme := range *tc.schemes { tempS.With(openapi3.SecurityRequirement{scheme.Name: {}}) } securityRequirements = tempS } doc.Paths[tc.name] = &openapi3.PathItem{ Get: &openapi3.Operation{ Security: securityRequirements, Responses: openapi3.NewResponses(), }, } } err := doc.Validate(context.Background()) require.NoError(t, err) router, err := legacyrouter.NewRouter(doc) require.NoError(t, err) // Test each case for _, path := range tc { // Make a map of the schemes and whether they're var schemesValidated *map[*openapi3.SecurityScheme]bool = nil if path.expectedSchemes != nil { temp := make(map[*openapi3.SecurityScheme]bool) schemesValidated = &temp for _, scheme := range *path.expectedSchemes { (*schemesValidated)[scheme.Scheme] = false } } // Create the request emptyBody := bytes.NewReader(make([]byte, 0)) httpReq := httptest.NewRequest(http.MethodGet, path.name, emptyBody) route, _, err := router.FindRoute(httpReq) require.NoError(t, err) req := RequestValidationInput{ Request: httpReq, Route: route, Options: &Options{ AuthenticationFunc: func(ctx context.Context, input *AuthenticationInput) error { if schemesValidated != nil { if validated, ok := (*schemesValidated)[input.SecurityScheme]; ok { if validated { t.Fatalf("The path %q had the schemes %v named %q validated more than once", path.name, input.SecurityScheme, input.SecuritySchemeName) } (*schemesValidated)[input.SecurityScheme] = true return nil } } t.Fatalf("The path %q had the schemes %v named %q", path.name, input.SecurityScheme, input.SecuritySchemeName) return nil }, }, } // Validate the request err = ValidateRequest(context.Background(), &req) require.NoError(t, err) for securityRequirement, validated := range *schemesValidated { if !validated { t.Fatalf("The security requirement %v was exepected to be validated but wasn't", securityRequirement) } } } } // TestAlternateRequirementMet asserts that ValidateSecurityRequirements succeeds if any SecurityRequirement is met and otherwise doesn't. func TestAnySecurityRequirementMet(t *testing.T) { // Create of a map of scheme names and whether they are valid schemes := map[string]bool{ "a": true, "b": true, "c": false, "d": false, } // Create the test cases tc := []struct { name string schemes []string error bool }{ { name: "/ok1", schemes: []string{"a", "b"}, error: false, }, { name: "/ok2", schemes: []string{"a", "c"}, error: false, }, { name: "/error", schemes: []string{"c", "d"}, error: true, }, } doc := openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", Version: "0.1", }, Paths: map[string]*openapi3.PathItem{}, Components: openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, } // Add the security schemes to the spec's components for schemeName := range schemes { doc.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{ Value: &openapi3.SecurityScheme{ Type: "http", Scheme: "basic", }, } } // Add the paths to the spec for _, tc := range tc { // Create the security requirements from the test cases's schemes securityRequirements := openapi3.NewSecurityRequirements() for _, scheme := range tc.schemes { securityRequirements.With(openapi3.SecurityRequirement{scheme: {}}) } // Create the path with the security requirements doc.Paths[tc.name] = &openapi3.PathItem{ Get: &openapi3.Operation{ Security: securityRequirements, Responses: openapi3.NewResponses(), }, } } err := doc.Validate(context.Background()) require.NoError(t, err) router, err := legacyrouter.NewRouter(&doc) require.NoError(t, err) // Create the authentication function authFunc := makeAuthFunc(schemes) for _, tc := range tc { // Create the request input for the path tcURL, err := url.Parse(tc.name) require.NoError(t, err) httpReq := httptest.NewRequest(http.MethodGet, tcURL.String(), nil) route, _, err := router.FindRoute(httpReq) require.NoError(t, err) req := RequestValidationInput{ Route: route, Options: &Options{ AuthenticationFunc: authFunc, }, } // Validate the security requirements err = ValidateSecurityRequirements(context.Background(), &req, *route.Operation.Security) // If there should have been an error if tc.error { require.Errorf(t, err, "an error is expected for path %q", tc.name) } else { require.NoErrorf(t, err, "an error wasn't expected for path %q", tc.name) } } } // TestAllSchemesMet asserts that ValidateSecurityRequirement succeeds if all the SecuritySchemes of a SecurityRequirement are met and otherwise doesn't. func TestAllSchemesMet(t *testing.T) { // Create of a map of scheme names and whether they are met schemes := map[string]bool{ "a": true, "b": true, "c": false, } // Create the test cases tc := []struct { name string error bool }{ { name: "/ok", error: false, }, { name: "/error", error: true, }, } doc := openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", Version: "0.1", }, Paths: map[string]*openapi3.PathItem{}, Components: openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, } // Add the security schemes to the spec's components for schemeName := range schemes { doc.Components.SecuritySchemes[schemeName] = &openapi3.SecuritySchemeRef{ Value: &openapi3.SecurityScheme{ Type: "http", Scheme: "basic", }, } } // Add the paths to the spec for _, tc := range tc { // Create the security requirement for the path securityRequirement := openapi3.SecurityRequirement{} for scheme, valid := range schemes { // If the scheme is valid or the test case is meant to return an error if valid || tc.error { // Add the scheme to the security requirement securityRequirement[scheme] = []string{} } } doc.Paths[tc.name] = &openapi3.PathItem{ Get: &openapi3.Operation{ Security: &openapi3.SecurityRequirements{ securityRequirement, }, Responses: openapi3.NewResponses(), }, } } err := doc.Validate(context.Background()) require.NoError(t, err) router, err := legacyrouter.NewRouter(&doc) require.NoError(t, err) // Create the authentication function authFunc := makeAuthFunc(schemes) for _, tc := range tc { // Create the request input for the path tcURL, err := url.Parse(tc.name) require.NoError(t, err) httpReq := httptest.NewRequest(http.MethodGet, tcURL.String(), nil) route, _, err := router.FindRoute(httpReq) require.NoError(t, err) req := RequestValidationInput{ Route: route, Options: &Options{ AuthenticationFunc: authFunc, }, } // Validate the security requirements err = ValidateSecurityRequirements(context.Background(), &req, *route.Operation.Security) // If there should have been an error if tc.error { require.Error(t, err) } else { require.NoError(t, err) } } } // makeAuthFunc creates an authentication function that accepts the given valid schemes. // If an invalid or unknown scheme is encountered, an error is returned by the returned function. // Otherwise the return value of the returned function is nil. func makeAuthFunc(schemes map[string]bool) func(ctx context.Context, input *AuthenticationInput) error { return func(ctx context.Context, input *AuthenticationInput) error { // If the scheme is valid and present in the schemes valid, present := schemes[input.SecuritySchemeName] if valid && present { return nil } // If the scheme is present in che schemes if present { // Return an unmet scheme error return fmt.Errorf("security scheme for %q wasn't met", input.SecuritySchemeName) } // Return an unknown scheme error return fmt.Errorf("security scheme for %q is unknown", input.SecuritySchemeName) } } kin-openapi-0.85.0/openapi3gen/000077500000000000000000000000001415236407200162325ustar00rootroot00000000000000kin-openapi-0.85.0/openapi3gen/openapi3gen.go000066400000000000000000000254411415236407200207770ustar00rootroot00000000000000// Package openapi3gen generates OpenAPIv3 JSON schemas from Go types. package openapi3gen import ( "encoding/json" "fmt" "math" "reflect" "strings" "time" "github.com/getkin/kin-openapi/jsoninfo" "github.com/getkin/kin-openapi/openapi3" ) // CycleError indicates that a type graph has one or more possible cycles. type CycleError struct{} func (err *CycleError) Error() string { return "detected cycle" } // Option allows tweaking SchemaRef generation type Option func(*generatorOpt) // SchemaCustomizerFn is a callback function, allowing // the OpenAPI schema definition to be updated with additional // properties during the generation process, based on the // name of the field, the Go type, and the struct tags. // name will be "_root" for the top level object, and tag will be "" type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error type generatorOpt struct { useAllExportedFields bool throwErrorOnCycle bool schemaCustomizer SchemaCustomizerFn } // UseAllExportedFields changes the default behavior of only // generating schemas for struct fields with a JSON tag. func UseAllExportedFields() Option { return func(x *generatorOpt) { x.useAllExportedFields = true } } // ThrowErrorOnCycle changes the default behavior of creating cycle // refs to instead error if a cycle is detected. func ThrowErrorOnCycle() Option { return func(x *generatorOpt) { x.throwErrorOnCycle = true } } // SchemaCustomizer allows customization of the schema that is generated // for a field, for example to support an additional tagging scheme func SchemaCustomizer(sc SchemaCustomizerFn) Option { return func(x *generatorOpt) { x.schemaCustomizer = sc } } // NewSchemaRefForValue is a shortcut for NewGenerator(...).NewSchemaRefForValue(...) func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) { g := NewGenerator(opts...) return g.NewSchemaRefForValue(value, schemas) } type Generator struct { opts generatorOpt Types map[reflect.Type]*openapi3.SchemaRef // SchemaRefs contains all references and their counts. // If count is 1, it's not ne // An OpenAPI identifier has been assigned to each. SchemaRefs map[*openapi3.SchemaRef]int // componentSchemaRefs is a set of schemas that must be defined in the components to avoid cycles componentSchemaRefs map[string]struct{} } func NewGenerator(opts ...Option) *Generator { gOpt := &generatorOpt{} for _, f := range opts { f(gOpt) } return &Generator{ Types: make(map[reflect.Type]*openapi3.SchemaRef), SchemaRefs: make(map[*openapi3.SchemaRef]int), componentSchemaRefs: make(map[string]struct{}), opts: *gOpt, } } func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, error) { //check generatorOpt consistency here return g.generateSchemaRefFor(nil, t, "_root", "") } // NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef, and updates a supplied map with any dependent component schemas if they lead to cycles func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas) (*openapi3.SchemaRef, error) { ref, err := g.GenerateSchemaRef(reflect.TypeOf(value)) if err != nil { return nil, err } for ref := range g.SchemaRefs { if _, ok := g.componentSchemaRefs[ref.Ref]; ok && schemas != nil { schemas[ref.Ref] = &openapi3.SchemaRef{ Value: ref.Value, } } if strings.HasPrefix(ref.Ref, "#/components/schemas/") { ref.Value = nil } else { ref.Ref = "" } } return ref, nil } func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { if ref := g.Types[t]; ref != nil && g.opts.schemaCustomizer == nil { g.SchemaRefs[ref]++ return ref, nil } ref, err := g.generateWithoutSaving(parents, t, name, tag) if err != nil { return nil, err } if ref != nil { g.Types[t] = ref g.SchemaRefs[ref]++ } return ref, nil } func getStructField(t reflect.Type, fieldInfo jsoninfo.FieldInfo) reflect.StructField { var ff reflect.StructField // fieldInfo.Index is an array of indexes starting from the root of the type for i := 0; i < len(fieldInfo.Index); i++ { ff = t.Field(fieldInfo.Index[i]) t = ff.Type } return ff } func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { typeInfo := jsoninfo.GetTypeInfo(t) for _, parent := range parents { if parent == typeInfo { return nil, &CycleError{} } } if cap(parents) == 0 { parents = make([]*jsoninfo.TypeInfo, 0, 4) } parents = append(parents, typeInfo) for t.Kind() == reflect.Ptr { t = t.Elem() } if strings.HasSuffix(t.Name(), "Ref") { _, a := t.FieldByName("Ref") v, b := t.FieldByName("Value") if a && b { vs, err := g.generateSchemaRefFor(parents, v.Type, name, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { g.SchemaRefs[vs]++ return vs, nil } return nil, err } refSchemaRef := RefSchemaRef g.SchemaRefs[refSchemaRef]++ ref := openapi3.NewSchemaRef(t.Name(), &openapi3.Schema{ OneOf: []*openapi3.SchemaRef{ refSchemaRef, vs, }, }) g.SchemaRefs[ref]++ return ref, nil } } schema := &openapi3.Schema{} switch t.Kind() { case reflect.Func, reflect.Chan: return nil, nil // ignore case reflect.Bool: schema.Type = "boolean" case reflect.Int: schema.Type = "integer" case reflect.Int8: schema.Type = "integer" schema.Min = &minInt8 schema.Max = &maxInt8 case reflect.Int16: schema.Type = "integer" schema.Min = &minInt16 schema.Max = &maxInt16 case reflect.Int32: schema.Type = "integer" schema.Format = "int32" case reflect.Int64: schema.Type = "integer" schema.Format = "int64" case reflect.Uint: schema.Type = "integer" schema.Min = &zeroInt case reflect.Uint8: schema.Type = "integer" schema.Min = &zeroInt schema.Max = &maxUint8 case reflect.Uint16: schema.Type = "integer" schema.Min = &zeroInt schema.Max = &maxUint16 case reflect.Uint32: schema.Type = "integer" schema.Min = &zeroInt schema.Max = &maxUint32 case reflect.Uint64: schema.Type = "integer" schema.Min = &zeroInt schema.Max = &maxUint64 case reflect.Float32: schema.Type = "number" schema.Format = "float" case reflect.Float64: schema.Type = "number" schema.Format = "double" case reflect.String: schema.Type = "string" case reflect.Slice: if t.Elem().Kind() == reflect.Uint8 { if t == rawMessageType { return &openapi3.SchemaRef{Value: schema}, nil } schema.Type = "string" schema.Format = "byte" } else { schema.Type = "array" items, err := g.generateSchemaRefFor(parents, t.Elem(), name, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { items = g.generateCycleSchemaRef(t.Elem(), schema) } else { return nil, err } } if items != nil { g.SchemaRefs[items]++ schema.Items = items } } case reflect.Map: schema.Type = "object" additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem(), name, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { additionalProperties = g.generateCycleSchemaRef(t.Elem(), schema) } else { return nil, err } } if additionalProperties != nil { g.SchemaRefs[additionalProperties]++ schema.AdditionalProperties = additionalProperties } case reflect.Struct: if t == timeType { schema.Type = "string" schema.Format = "date-time" } else { for _, fieldInfo := range typeInfo.Fields { // Only fields with JSON tag are considered (by default) if !fieldInfo.HasJSONTag && !g.opts.useAllExportedFields { continue } // If asked, try to use yaml tag fieldName, fType := fieldInfo.JSONName, fieldInfo.Type if !fieldInfo.HasJSONTag && g.opts.useAllExportedFields { // Handle anonymous fields/embedded structs if t.Field(fieldInfo.Index[0]).Anonymous { ref, err := g.generateSchemaRefFor(parents, fType, fieldName, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { ref = g.generateCycleSchemaRef(fType, schema) } else { return nil, err } } if ref != nil { g.SchemaRefs[ref]++ schema.WithPropertyRef(fieldName, ref) } } else { ff := getStructField(t, fieldInfo) if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" { fieldName, fType = tag, ff.Type } } } // extract the field tag if we have a customizer var fieldTag reflect.StructTag if g.opts.schemaCustomizer != nil { ff := getStructField(t, fieldInfo) fieldTag = ff.Tag } ref, err := g.generateSchemaRefFor(parents, fType, fieldName, fieldTag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { ref = g.generateCycleSchemaRef(fType, schema) } else { return nil, err } } if ref != nil { g.SchemaRefs[ref]++ schema.WithPropertyRef(fieldName, ref) } } // Object only if it has properties if schema.Properties != nil { schema.Type = "object" } } } if g.opts.schemaCustomizer != nil { if err := g.opts.schemaCustomizer(name, t, tag, schema); err != nil { return nil, err } } return openapi3.NewSchemaRef(t.Name(), schema), nil } func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef { var typeName string switch t.Kind() { case reflect.Ptr: return g.generateCycleSchemaRef(t.Elem(), schema) case reflect.Slice: ref := g.generateCycleSchemaRef(t.Elem(), schema) sliceSchema := openapi3.NewSchema() sliceSchema.Type = "array" sliceSchema.Items = ref return openapi3.NewSchemaRef("", sliceSchema) case reflect.Map: ref := g.generateCycleSchemaRef(t.Elem(), schema) mapSchema := openapi3.NewSchema() mapSchema.Type = "object" mapSchema.AdditionalProperties = ref return openapi3.NewSchemaRef("", mapSchema) default: typeName = t.Name() } g.componentSchemaRefs[typeName] = struct{}{} return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema) } var RefSchemaRef = openapi3.NewSchemaRef("Ref", openapi3.NewObjectSchema().WithProperty("$ref", openapi3.NewStringSchema().WithMinLength(1))) var ( timeType = reflect.TypeOf(time.Time{}) rawMessageType = reflect.TypeOf(json.RawMessage{}) zeroInt = float64(0) maxInt8 = float64(math.MaxInt8) minInt8 = float64(math.MinInt8) maxInt16 = float64(math.MaxInt16) minInt16 = float64(math.MinInt16) maxUint8 = float64(math.MaxUint8) maxUint16 = float64(math.MaxUint16) maxUint32 = float64(math.MaxUint32) maxUint64 = float64(math.MaxUint64) ) kin-openapi-0.85.0/openapi3gen/openapi3gen_test.go000066400000000000000000000300031415236407200220240ustar00rootroot00000000000000package openapi3gen_test import ( "encoding/json" "errors" "fmt" "reflect" "strconv" "strings" "testing" "time" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3gen" "github.com/stretchr/testify/require" ) func ExampleGenerator_SchemaRefs() { type SomeOtherType string type SomeStruct struct { Bool bool `json:"bool"` Int int `json:"int"` Int64 int64 `json:"int64"` Float64 float64 `json:"float64"` String string `json:"string"` Bytes []byte `json:"bytes"` JSON json.RawMessage `json:"json"` Time time.Time `json:"time"` Slice []SomeOtherType `json:"slice"` Map map[string]*SomeOtherType `json:"map"` Struct struct { X string `json:"x"` } `json:"struct"` EmptyStruct struct { Y string } `json:"structWithoutFields"` Ptr *SomeOtherType `json:"ptr"` } g := openapi3gen.NewGenerator() schemaRef, err := g.NewSchemaRefForValue(&SomeStruct{}, nil) if err != nil { panic(err) } fmt.Printf("g.SchemaRefs: %d\n", len(g.SchemaRefs)) var data []byte if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil { panic(err) } fmt.Printf("schemaRef: %s\n", data) // Output: // g.SchemaRefs: 15 // schemaRef: { // "properties": { // "bool": { // "type": "boolean" // }, // "bytes": { // "format": "byte", // "type": "string" // }, // "float64": { // "format": "double", // "type": "number" // }, // "int": { // "type": "integer" // }, // "int64": { // "format": "int64", // "type": "integer" // }, // "json": {}, // "map": { // "additionalProperties": { // "type": "string" // }, // "type": "object" // }, // "ptr": { // "type": "string" // }, // "slice": { // "items": { // "type": "string" // }, // "type": "array" // }, // "string": { // "type": "string" // }, // "struct": { // "properties": { // "x": { // "type": "string" // } // }, // "type": "object" // }, // "structWithoutFields": {}, // "time": { // "format": "date-time", // "type": "string" // } // }, // "type": "object" // } } func ExampleThrowErrorOnCycle() { type CyclicType0 struct { CyclicField *struct { CyclicField *CyclicType0 `json:"b"` } `json:"a"` } schemas := make(openapi3.Schemas) schemaRef, err := openapi3gen.NewSchemaRefForValue(&CyclicType0{}, schemas, openapi3gen.ThrowErrorOnCycle()) if schemaRef != nil || err == nil { panic(`With option ThrowErrorOnCycle, an error is returned when a schema reference cycle is found`) } if _, ok := err.(*openapi3gen.CycleError); !ok { panic(`With option ThrowErrorOnCycle, an error of type CycleError is returned`) } if len(schemas) != 0 { panic(`No references should have been collected at this point`) } if schemaRef, err = openapi3gen.NewSchemaRefForValue(&CyclicType0{}, schemas); err != nil { panic(err) } var data []byte if data, err = json.MarshalIndent(schemaRef, "", " "); err != nil { panic(err) } fmt.Printf("schemaRef: %s\n", data) if data, err = json.MarshalIndent(schemas, "", " "); err != nil { panic(err) } fmt.Printf("schemas: %s\n", data) // Output: // schemaRef: { // "properties": { // "a": { // "properties": { // "b": { // "$ref": "#/components/schemas/CyclicType0" // } // }, // "type": "object" // } // }, // "type": "object" // } // schemas: { // "CyclicType0": { // "properties": { // "a": { // "properties": { // "b": { // "$ref": "#/components/schemas/CyclicType0" // } // }, // "type": "object" // } // }, // "type": "object" // } // } } func TestExportedNonTagged(t *testing.T) { type Bla struct { A string Another string `json:"another"` yetAnother string // unused because unexported EvenAYaml string `yaml:"even_a_yaml"` } schemaRef, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields()) require.NoError(t, err) require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{ Type: "object", Properties: map[string]*openapi3.SchemaRef{ "A": {Value: &openapi3.Schema{Type: "string"}}, "another": {Value: &openapi3.Schema{Type: "string"}}, "even_a_yaml": {Value: &openapi3.Schema{Type: "string"}}, }}}, schemaRef) } func ExampleUseAllExportedFields() { type UnsignedIntStruct struct { UnsignedInt uint `json:"uint"` } schemaRef, err := openapi3gen.NewSchemaRefForValue(&UnsignedIntStruct{}, nil, openapi3gen.UseAllExportedFields()) if err != nil { panic(err) } var data []byte if data, err = json.MarshalIndent(schemaRef, "", " "); err != nil { panic(err) } fmt.Printf("schemaRef: %s\n", data) // Output: // schemaRef: { // "properties": { // "uint": { // "minimum": 0, // "type": "integer" // } // }, // "type": "object" // } } func ExampleGenerator_GenerateSchemaRef() { type EmbeddedStruct struct { ID string } type ContainerStruct struct { Name string EmbeddedStruct } instance := &ContainerStruct{ Name: "Container", EmbeddedStruct: EmbeddedStruct{ ID: "Embedded", }, } generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields()) schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) if err != nil { panic(err) } var data []byte if data, err = json.MarshalIndent(schemaRef.Value.Properties["Name"].Value, "", " "); err != nil { panic(err) } fmt.Printf(`schemaRef.Value.Properties["Name"].Value: %s`, data) fmt.Println() if data, err = json.MarshalIndent(schemaRef.Value.Properties["ID"].Value, "", " "); err != nil { panic(err) } fmt.Printf(`schemaRef.Value.Properties["ID"].Value: %s`, data) fmt.Println() // Output: // schemaRef.Value.Properties["Name"].Value: { // "type": "string" // } // schemaRef.Value.Properties["ID"].Value: { // "type": "string" // } } func TestEmbeddedPointerStructs(t *testing.T) { type EmbeddedStruct struct { ID string } type ContainerStruct struct { Name string *EmbeddedStruct } instance := &ContainerStruct{ Name: "Container", EmbeddedStruct: &EmbeddedStruct{ ID: "Embedded", }, } generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields()) schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) require.NoError(t, err) var ok bool _, ok = schemaRef.Value.Properties["Name"] require.Equal(t, true, ok) _, ok = schemaRef.Value.Properties["ID"] require.Equal(t, true, ok) } func TestCyclicReferences(t *testing.T) { type ObjectDiff struct { FieldCycle *ObjectDiff SliceCycle []*ObjectDiff MapCycle map[*ObjectDiff]*ObjectDiff } instance := &ObjectDiff{ FieldCycle: nil, SliceCycle: nil, MapCycle: nil, } generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields()) schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) require.NoError(t, err) require.NotNil(t, schemaRef.Value.Properties["FieldCycle"]) require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["FieldCycle"].Ref) require.NotNil(t, schemaRef.Value.Properties["SliceCycle"]) require.Equal(t, "array", schemaRef.Value.Properties["SliceCycle"].Value.Type) require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["SliceCycle"].Value.Items.Ref) require.NotNil(t, schemaRef.Value.Properties["MapCycle"]) require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type) require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref) } func ExampleSchemaCustomizer() { type NestedInnerBla struct { Enum1Field string `json:"enum1" myenumtag:"a,b"` } type InnerBla struct { UntaggedStringField string AnonStruct struct { InnerFieldWithoutTag int InnerFieldWithTag int `mymintag:"-1" mymaxtag:"50"` NestedInnerBla } Enum2Field string `json:"enum2" myenumtag:"c,d"` } type Bla struct { InnerBla EnumField3 string `json:"enum3" myenumtag:"e,f"` } customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { if tag.Get("mymintag") != "" { minVal, err := strconv.ParseFloat(tag.Get("mymintag"), 64) if err != nil { return err } schema.Min = &minVal } if tag.Get("mymaxtag") != "" { maxVal, err := strconv.ParseFloat(tag.Get("mymaxtag"), 64) if err != nil { return err } schema.Max = &maxVal } if tag.Get("myenumtag") != "" { for _, s := range strings.Split(tag.Get("myenumtag"), ",") { schema.Enum = append(schema.Enum, s) } } return nil }) schemaRef, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer) if err != nil { panic(err) } var data []byte if data, err = json.MarshalIndent(schemaRef, "", " "); err != nil { panic(err) } fmt.Printf("schemaRef: %s\n", data) // Output: // schemaRef: { // "properties": { // "AnonStruct": { // "properties": { // "InnerFieldWithTag": { // "maximum": 50, // "minimum": -1, // "type": "integer" // }, // "InnerFieldWithoutTag": { // "type": "integer" // }, // "enum1": { // "enum": [ // "a", // "b" // ], // "type": "string" // } // }, // "type": "object" // }, // "UntaggedStringField": { // "type": "string" // }, // "enum2": { // "enum": [ // "c", // "d" // ], // "type": "string" // }, // "enum3": { // "enum": [ // "e", // "f" // ], // "type": "string" // } // }, // "type": "object" // } } func TestSchemaCustomizerError(t *testing.T) { customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { return errors.New("test error") }) type Bla struct{} _, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer) require.EqualError(t, err, "test error") } func ExampleNewSchemaRefForValue_recursive() { type RecursiveType struct { Field1 string `json:"field1"` Field2 string `json:"field2"` Field3 string `json:"field3"` Components []*RecursiveType `json:"children,omitempty"` } schemas := make(openapi3.Schemas) schemaRef, err := openapi3gen.NewSchemaRefForValue(&RecursiveType{}, schemas) if err != nil { panic(err) } var data []byte if data, err = json.MarshalIndent(&schemas, "", " "); err != nil { panic(err) } fmt.Printf("schemas: %s\n", data) if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil { panic(err) } fmt.Printf("schemaRef: %s\n", data) // Output: // schemas: { // "RecursiveType": { // "properties": { // "children": { // "items": { // "$ref": "#/components/schemas/RecursiveType" // }, // "type": "array" // }, // "field1": { // "type": "string" // }, // "field2": { // "type": "string" // }, // "field3": { // "type": "string" // } // }, // "type": "object" // } // } // schemaRef: { // "properties": { // "children": { // "items": { // "$ref": "#/components/schemas/RecursiveType" // }, // "type": "array" // }, // "field1": { // "type": "string" // }, // "field2": { // "type": "string" // }, // "field3": { // "type": "string" // } // }, // "type": "object" // } } kin-openapi-0.85.0/openapi3gen/simple_test.go000066400000000000000000000042341415236407200211140ustar00rootroot00000000000000package openapi3gen_test import ( "encoding/json" "fmt" "time" "github.com/getkin/kin-openapi/openapi3gen" ) type ( SomeStruct struct { Bool bool `json:"bool"` Int int `json:"int"` Int64 int64 `json:"int64"` Float64 float64 `json:"float64"` String string `json:"string"` Bytes []byte `json:"bytes"` JSON json.RawMessage `json:"json"` Time time.Time `json:"time"` Slice []SomeOtherType `json:"slice"` Map map[string]*SomeOtherType `json:"map"` Struct struct { X string `json:"x"` } `json:"struct"` EmptyStruct struct { Y string } `json:"structWithoutFields"` Ptr *SomeOtherType `json:"ptr"` } SomeOtherType string ) func Example() { schemaRef, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{}, nil) if err != nil { panic(err) } data, err := json.MarshalIndent(schemaRef, "", " ") if err != nil { panic(err) } fmt.Printf("%s\n", data) // Output: // { // "properties": { // "bool": { // "type": "boolean" // }, // "bytes": { // "format": "byte", // "type": "string" // }, // "float64": { // "format": "double", // "type": "number" // }, // "int": { // "type": "integer" // }, // "int64": { // "format": "int64", // "type": "integer" // }, // "json": {}, // "map": { // "additionalProperties": { // "type": "string" // }, // "type": "object" // }, // "ptr": { // "type": "string" // }, // "slice": { // "items": { // "type": "string" // }, // "type": "array" // }, // "string": { // "type": "string" // }, // "struct": { // "properties": { // "x": { // "type": "string" // } // }, // "type": "object" // }, // "structWithoutFields": {}, // "time": { // "format": "date-time", // "type": "string" // } // }, // "type": "object" // } } kin-openapi-0.85.0/routers/000077500000000000000000000000001415236407200155255ustar00rootroot00000000000000kin-openapi-0.85.0/routers/gorillamux/000077500000000000000000000000001415236407200177105ustar00rootroot00000000000000kin-openapi-0.85.0/routers/gorillamux/example_test.go000066400000000000000000000030341415236407200227310ustar00rootroot00000000000000package gorillamux_test import ( "context" "fmt" "net/http" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" "github.com/getkin/kin-openapi/routers/gorillamux" ) func Example() { ctx := context.Background() loader := &openapi3.Loader{Context: ctx, IsExternalRefsAllowed: true} doc, err := loader.LoadFromFile("../../openapi3/testdata/pathref.openapi.yml") if err != nil { panic(err) } if err = doc.Validate(ctx); err != nil { panic(err) } router, err := gorillamux.NewRouter(doc) if err != nil { panic(err) } httpReq, err := http.NewRequest(http.MethodGet, "/test", nil) if err != nil { panic(err) } route, pathParams, err := router.FindRoute(httpReq) if err != nil { panic(err) } requestValidationInput := &openapi3filter.RequestValidationInput{ Request: httpReq, PathParams: pathParams, Route: route, } if err := openapi3filter.ValidateRequest(ctx, requestValidationInput); err != nil { panic(err) } responseValidationInput := &openapi3filter.ResponseValidationInput{ RequestValidationInput: requestValidationInput, Status: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, } responseValidationInput.SetBodyBytes([]byte(`{}`)) err = openapi3filter.ValidateResponse(ctx, responseValidationInput) fmt.Println(err) // Output: // response body doesn't match the schema: Field must be set to string or not be present // Schema: // { // "type": "string" // } // // Value: // "object" } kin-openapi-0.85.0/routers/gorillamux/router.go000066400000000000000000000127501415236407200215640ustar00rootroot00000000000000// Package gorillamux implements a router. // // It differs from the legacy router: // * it provides somewhat granular errors: "path not found", "method not allowed". // * it handles matching routes with extensions (e.g. /books/{id}.json) // * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z:.*}) package gorillamux import ( "net/http" "net/url" "sort" "strings" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" "github.com/gorilla/mux" ) // Router helps link http.Request.s and an OpenAPIv3 spec type Router struct { muxes []*mux.Route routes []*routers.Route } // NewRouter creates a gorilla/mux router. // Assumes spec is .Validate()d // TODO: Handle/HandlerFunc + ServeHTTP (When there is a match, the route variables can be retrieved calling mux.Vars(request)) func NewRouter(doc *openapi3.T) (routers.Router, error) { type srv struct { schemes []string host, base string server *openapi3.Server } servers := make([]srv, 0, len(doc.Servers)) for _, server := range doc.Servers { serverURL := server.URL var schemes []string var u *url.URL var err error if strings.Contains(serverURL, "://") { scheme0 := strings.Split(serverURL, "://")[0] schemes = permutePart(scheme0, server) u, err = url.Parse(bEncode(strings.Replace(serverURL, scheme0+"://", schemes[0]+"://", 1))) } else { u, err = url.Parse(bEncode(serverURL)) } if err != nil { return nil, err } path := bDecode(u.EscapedPath()) if len(path) > 0 && path[len(path)-1] == '/' { path = path[:len(path)-1] } servers = append(servers, srv{ host: bDecode(u.Host), //u.Hostname()? base: path, schemes: schemes, // scheme: []string{scheme0}, TODO: https://github.com/gorilla/mux/issues/624 server: server, }) } if len(servers) == 0 { servers = append(servers, srv{}) } muxRouter := mux.NewRouter().UseEncodedPath() r := &Router{} for _, path := range orderedPaths(doc.Paths) { pathItem := doc.Paths[path] operations := pathItem.Operations() methods := make([]string, 0, len(operations)) for method := range operations { methods = append(methods, method) } sort.Strings(methods) for _, s := range servers { muxRoute := muxRouter.Path(s.base + path).Methods(methods...) if schemes := s.schemes; len(schemes) != 0 { muxRoute.Schemes(schemes...) } if host := s.host; host != "" { muxRoute.Host(host) } if err := muxRoute.GetError(); err != nil { return nil, err } r.muxes = append(r.muxes, muxRoute) r.routes = append(r.routes, &routers.Route{ Spec: doc, Server: s.server, Path: path, PathItem: pathItem, Method: "", Operation: nil, }) } } return r, nil } // FindRoute extracts the route and parameters of an http.Request func (r *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) { for i, muxRoute := range r.muxes { var match mux.RouteMatch if muxRoute.Match(req, &match) { if err := match.MatchErr; err != nil { // What then? } route := r.routes[i] route.Method = req.Method route.Operation = route.Spec.Paths[route.Path].GetOperation(route.Method) return route, match.Vars, nil } switch match.MatchErr { case nil: case mux.ErrMethodMismatch: return nil, nil, routers.ErrMethodNotAllowed default: // What then? } } return nil, nil, routers.ErrPathNotFound } func orderedPaths(paths map[string]*openapi3.PathItem) []string { // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#pathsObject // When matching URLs, concrete (non-templated) paths would be matched // before their templated counterparts. // NOTE: sorting by number of variables ASC then by lexicographical // order seems to be a good heuristic. vars := make(map[int][]string) max := 0 for path := range paths { count := strings.Count(path, "}") vars[count] = append(vars[count], path) if count > max { max = count } } ordered := make([]string, 0, len(paths)) for c := 0; c <= max; c++ { if ps, ok := vars[c]; ok { sort.Strings(ps) ordered = append(ordered, ps...) } } return ordered } // Magic strings that temporarily replace "{}" so net/url.Parse() works var blURL, brURL = strings.Repeat("-", 50), strings.Repeat("_", 50) func bEncode(s string) string { s = strings.Replace(s, "{", blURL, -1) s = strings.Replace(s, "}", brURL, -1) return s } func bDecode(s string) string { s = strings.Replace(s, blURL, "{", -1) s = strings.Replace(s, brURL, "}", -1) return s } func permutePart(part0 string, srv *openapi3.Server) []string { type mapAndSlice struct { m map[string]struct{} s []string } var2val := make(map[string]mapAndSlice) max := 0 for name0, v := range srv.Variables { name := "{" + name0 + "}" if !strings.Contains(part0, name) { continue } m := map[string]struct{}{v.Default: {}} for _, value := range v.Enum { m[value] = struct{}{} } if l := len(m); l > max { max = l } s := make([]string, 0, len(m)) for value := range m { s = append(s, value) } var2val[name] = mapAndSlice{m: m, s: s} } if len(var2val) == 0 { return []string{part0} } partsMap := make(map[string]struct{}, max*len(var2val)) for i := 0; i < max; i++ { part := part0 for name, mas := range var2val { part = strings.Replace(part, name, mas.s[i%len(mas.s)], -1) } partsMap[part] = struct{}{} } parts := make([]string, 0, len(partsMap)) for part := range partsMap { parts = append(parts, part) } sort.Strings(parts) return parts } kin-openapi-0.85.0/routers/gorillamux/router_test.go000066400000000000000000000177251415236407200226320ustar00rootroot00000000000000package gorillamux import ( "context" "net/http" "sort" "testing" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" "github.com/stretchr/testify/require" ) func TestRouter(t *testing.T) { helloCONNECT := &openapi3.Operation{Responses: openapi3.NewResponses()} helloDELETE := &openapi3.Operation{Responses: openapi3.NewResponses()} helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} helloHEAD := &openapi3.Operation{Responses: openapi3.NewResponses()} helloOPTIONS := &openapi3.Operation{Responses: openapi3.NewResponses()} helloPATCH := &openapi3.Operation{Responses: openapi3.NewResponses()} helloPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} helloPUT := &openapi3.Operation{Responses: openapi3.NewResponses()} helloTRACE := &openapi3.Operation{Responses: openapi3.NewResponses()} paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()} booksPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()} doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", Version: "0.1", }, Paths: openapi3.Paths{ "/hello": &openapi3.PathItem{ Connect: helloCONNECT, Delete: helloDELETE, Get: helloGET, Head: helloHEAD, Options: helloOPTIONS, Patch: helloPATCH, Post: helloPOST, Put: helloPUT, Trace: helloTRACE, }, "/onlyGET": &openapi3.PathItem{ Get: helloGET, }, "/params/{x}/{y}/{z:.*}": &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")}, }, }, "/books/{bookid}": &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, }, "/books/{bookid2}.json": &openapi3.PathItem{ Post: booksPOST, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2")}, }, }, "/partial": &openapi3.PathItem{ Get: partialGET, }, }, } expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { req, err := http.NewRequest(method, uri, nil) require.NoError(t, err) route, pathParams, err := r.FindRoute(req) if err != nil { if operation == nil { pathItem := doc.Paths[uri] if pathItem == nil { if err.Error() != routers.ErrPathNotFound.Error() { t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrPathNotFound, err) } return } if pathItem.GetOperation(method) == nil { if err.Error() != routers.ErrMethodNotAllowed.Error() { t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrMethodNotAllowed, err) } } } else { t.Fatalf("'%s %s': should have returned an operation, but it returned an error: %v", method, uri, err) } } if operation == nil && err == nil { t.Fatalf("'%s %s': should have failed, but returned\nroute = %+v\npathParams = %+v", method, uri, route, pathParams) } if route == nil { return } if route.Operation != operation { t.Fatalf("'%s %s': Returned wrong operation (%v)", method, uri, route.Operation) } if len(params) == 0 { if len(pathParams) != 0 { t.Fatalf("'%s %s': should return no path arguments, but found %+v", method, uri, pathParams) } } else { names := make([]string, 0, len(params)) for name := range params { names = append(names, name) } sort.Strings(names) for _, name := range names { expected := params[name] actual, exists := pathParams[name] if !exists { t.Fatalf("'%s %s': path parameter %q should be %q, but it's not defined.", method, uri, name, expected) } if actual != expected { t.Fatalf("'%s %s': path parameter %q should be %q, but it's %q", method, uri, name, expected, actual) } } } } err := doc.Validate(context.Background()) require.NoError(t, err) r, err := NewRouter(doc) require.NoError(t, err) expect(r, http.MethodGet, "/not_existing", nil, nil) expect(r, http.MethodDelete, "/hello", helloDELETE, nil) expect(r, http.MethodGet, "/hello", helloGET, nil) expect(r, http.MethodHead, "/hello", helloHEAD, nil) expect(r, http.MethodPatch, "/hello", helloPATCH, nil) expect(r, http.MethodPost, "/hello", helloPOST, nil) expect(r, http.MethodPut, "/hello", helloPUT, nil) expect(r, http.MethodGet, "/params/a/b/", paramsGET, map[string]string{ "x": "a", "y": "b", "z": "", }) expect(r, http.MethodGet, "/params/a/b/c%2Fd", paramsGET, map[string]string{ "x": "a", "y": "b", "z": "c%2Fd", }) expect(r, http.MethodGet, "/books/War.and.Peace", paramsGET, map[string]string{ "bookid": "War.and.Peace", }) expect(r, http.MethodPost, "/books/War.and.Peace.json", booksPOST, map[string]string{ "bookid2": "War.and.Peace", }) expect(r, http.MethodPost, "/partial", nil, nil) doc.Servers = []*openapi3.Server{ {URL: "https://www.example.com/api/v1"}, {URL: "{scheme}://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{ "d0": {Default: "www"}, "d1": {Default: "example", Enum: []string{"example"}}, "scheme": {Default: "https", Enum: []string{"https", "http"}}, }}, } err = doc.Validate(context.Background()) require.NoError(t, err) r, err = NewRouter(doc) require.NoError(t, err) expect(r, http.MethodGet, "/hello", nil, nil) expect(r, http.MethodGet, "/api/v1/hello", nil, nil) expect(r, http.MethodGet, "www.example.com/api/v1/hello", nil, nil) expect(r, http.MethodGet, "https:///api/v1/hello", nil, nil) expect(r, http.MethodGet, "https://www.example.com/hello", nil, nil) expect(r, http.MethodGet, "https://www.example.com/api/v1/hello", helloGET, nil) expect(r, http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{ "d0": "domain0", "d1": "domain1", // "scheme": "https", TODO: https://github.com/gorilla/mux/issues/624 }) { uri := "https://www.example.com/api/v1/onlyGET" expect(r, http.MethodGet, uri, helloGET, nil) req, err := http.NewRequest(http.MethodDelete, uri, nil) require.NoError(t, err) require.NotNil(t, req) route, pathParams, err := r.FindRoute(req) require.EqualError(t, err, routers.ErrMethodNotAllowed.Error()) require.Nil(t, route) require.Nil(t, pathParams) } } func TestPermuteScheme(t *testing.T) { scheme0 := "{sche}{me}" server := &openapi3.Server{URL: scheme0 + "://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{ "d0": {Default: "www"}, "d1": {Default: "example", Enum: []string{"example"}}, "sche": {Default: "http"}, "me": {Default: "s", Enum: []string{"", "s"}}, }} err := server.Validate(context.Background()) require.NoError(t, err) perms := permutePart(scheme0, server) require.Equal(t, []string{"http", "https"}, perms) } func TestServerPath(t *testing.T) { server := &openapi3.Server{URL: "http://example.com"} err := server.Validate(context.Background()) require.NoError(t, err) _, err = NewRouter(&openapi3.T{Servers: openapi3.Servers{ server, &openapi3.Server{URL: "http://example.com/"}, &openapi3.Server{URL: "http://example.com/path"}}, }) require.NoError(t, err) } func TestRelativeURL(t *testing.T) { helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} doc := &openapi3.T{ Servers: openapi3.Servers{ &openapi3.Server{ URL: "/api/v1", }, }, Paths: openapi3.Paths{ "/hello": &openapi3.PathItem{ Get: helloGET, }, }, } router, err := NewRouter(doc) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "https://example.com/api/v1/hello", nil) require.NoError(t, err) route, _, err := router.FindRoute(req) require.NoError(t, err) require.Equal(t, "/hello", route.Path) } kin-openapi-0.85.0/routers/legacy/000077500000000000000000000000001415236407200167715ustar00rootroot00000000000000kin-openapi-0.85.0/routers/legacy/issue444_test.go000066400000000000000000000025011415236407200217410ustar00rootroot00000000000000package legacy_test import ( "bytes" "context" "net/http/httptest" "testing" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" legacyrouter "github.com/getkin/kin-openapi/routers/legacy" "github.com/stretchr/testify/require" ) func TestIssue444(t *testing.T) { loader := openapi3.NewLoader() oas, err := loader.LoadFromData([]byte(` openapi: '3.0.0' info: title: API version: 1.0.0 paths: '/path': post: requestBody: required: true content: application/x-yaml: schema: type: object responses: '200': description: x content: application/json: schema: type: string `)) require.NoError(t, err) router, err := legacyrouter.NewRouter(oas) require.NoError(t, err) r := httptest.NewRequest("POST", "/path", bytes.NewReader([]byte(` foo: bar `))) r.Header.Set("Content-Type", "application/x-yaml") openapi3.SchemaErrorDetailsDisabled = true route, pathParams, err := router.FindRoute(r) require.NoError(t, err) reqValidationInput := &openapi3filter.RequestValidationInput{ Request: r, PathParams: pathParams, Route: route, } err = openapi3filter.ValidateRequest(context.Background(), reqValidationInput) require.NoError(t, err) } kin-openapi-0.85.0/routers/legacy/pathpattern/000077500000000000000000000000001415236407200213235ustar00rootroot00000000000000kin-openapi-0.85.0/routers/legacy/pathpattern/node.go000066400000000000000000000201211415236407200225730ustar00rootroot00000000000000// Package pathpattern implements path matching. // // Examples of supported patterns: // * "/" // * "/abc"" // * "/abc/{variable}" (matches until next '/' or end-of-string) // * "/abc/{variable*}" (matches everything, including "/abc" if "/abc" has noot) // * "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions) package pathpattern import ( "bytes" "fmt" "regexp" "sort" "strings" ) var DefaultOptions = &Options{ SupportWildcard: true, } type Options struct { SupportWildcard bool SupportRegExp bool } // PathFromHost converts a host pattern to a path pattern. // // Examples: // * PathFromHost("some-subdomain.domain.com", false) -> "com/./domain/./some-subdomain" // * PathFromHost("some-subdomain.domain.com", true) -> "com/./domain/./subdomain/-/some" func PathFromHost(host string, specialDashes bool) string { buf := make([]byte, 0, len(host)) end := len(host) // Go from end to start for start := end - 1; start >= 0; start-- { switch host[start] { case '.': buf = append(buf, host[start+1:end]...) buf = append(buf, '/', '.', '/') end = start case '-': if specialDashes { buf = append(buf, host[start+1:end]...) buf = append(buf, '/', '-', '/') end = start } } } buf = append(buf, host[:end]...) return string(buf) } type Node struct { VariableNames []string Value interface{} Suffixes SuffixList } func (currentNode *Node) String() string { buf := bytes.NewBuffer(make([]byte, 0, 255)) currentNode.toBuffer(buf, "") return buf.String() } func (currentNode *Node) toBuffer(buf *bytes.Buffer, linePrefix string) { if value := currentNode.Value; value != nil { buf.WriteString(linePrefix) buf.WriteString("VALUE: ") fmt.Fprint(buf, value) buf.WriteString("\n") } suffixes := currentNode.Suffixes if len(suffixes) > 0 { newLinePrefix := linePrefix + " " for _, suffix := range suffixes { buf.WriteString(linePrefix) buf.WriteString("PATTERN: ") buf.WriteString(suffix.String()) buf.WriteString("\n") suffix.Node.toBuffer(buf, newLinePrefix) } } } type SuffixKind int // Note that order is important! const ( // SuffixKindConstant matches a constant string SuffixKindConstant = SuffixKind(iota) // SuffixKindRegExp matches a regular expression SuffixKindRegExp // SuffixKindVariable matches everything until '/' SuffixKindVariable // SuffixKindEverything matches everything (until end-of-string) SuffixKindEverything ) // Suffix describes condition that type Suffix struct { Kind SuffixKind Pattern string // compiled regular expression regExp *regexp.Regexp // Next node Node *Node } func EqualSuffix(a, b Suffix) bool { return a.Kind == b.Kind && a.Pattern == b.Pattern } func (suffix Suffix) String() string { switch suffix.Kind { case SuffixKindConstant: return suffix.Pattern case SuffixKindVariable: return "{_}" case SuffixKindEverything: return "{_*}" default: return "{_|" + suffix.Pattern + "}" } } type SuffixList []Suffix func (list SuffixList) Less(i, j int) bool { a, b := list[i], list[j] ak, bk := a.Kind, b.Kind if ak < bk { return true } else if bk < ak { return false } return a.Pattern > b.Pattern } func (list SuffixList) Len() int { return len(list) } func (list SuffixList) Swap(i, j int) { a, b := list[i], list[j] list[i], list[j] = b, a } func (currentNode *Node) MustAdd(path string, value interface{}, options *Options) { node, err := currentNode.CreateNode(path, options) if err != nil { panic(err) } node.Value = value } func (currentNode *Node) Add(path string, value interface{}, options *Options) error { node, err := currentNode.CreateNode(path, options) if err != nil { return err } node.Value = value return nil } func (currentNode *Node) CreateNode(path string, options *Options) (*Node, error) { if options == nil { options = DefaultOptions } for strings.HasSuffix(path, "/") { path = path[:len(path)-1] } remaining := path var variableNames []string loop: for { //remaining = strings.TrimPrefix(remaining, "/") if len(remaining) == 0 { // This node is the right one // Check whether another route already leads to this node currentNode.VariableNames = variableNames return currentNode, nil } suffix := Suffix{} var i int if strings.HasPrefix(remaining, "/") { remaining = remaining[1:] suffix.Kind = SuffixKindConstant suffix.Pattern = "/" } else { i = strings.IndexAny(remaining, "/{") if i < 0 { i = len(remaining) } if i > 0 { // Constant string pattern suffix.Kind = SuffixKindConstant suffix.Pattern = remaining[:i] remaining = remaining[i:] } else if remaining[0] == '{' { // This is probably a variable suffix.Kind = SuffixKindVariable // Find variable name i := strings.IndexByte(remaining, '}') if i < 0 { return nil, fmt.Errorf("missing '}' in: %s", path) } variableName := strings.TrimSpace(remaining[1:i]) remaining = remaining[i+1:] if options.SupportRegExp { // See if it has regular expression i = strings.IndexByte(variableName, '|') if i >= 0 { suffix.Kind = SuffixKindRegExp suffix.Pattern = strings.TrimSpace(variableName[i+1:]) variableName = strings.TrimSpace(variableName[:i]) } } if suffix.Kind == SuffixKindVariable && options.SupportWildcard { if strings.HasSuffix(variableName, "*") { suffix.Kind = SuffixKindEverything } } variableNames = append(variableNames, variableName) } } // Find existing matcher for _, existing := range currentNode.Suffixes { if EqualSuffix(existing, suffix) { currentNode = existing.Node continue loop } } // Compile regular expression if suffix.Kind == SuffixKindRegExp { regExp, err := regexp.Compile(suffix.Pattern) if err != nil { return nil, fmt.Errorf("invalid regular expression in: %s", path) } suffix.regExp = regExp } // Create new node newNode := &Node{} suffix.Node = newNode currentNode.Suffixes = append(currentNode.Suffixes, suffix) sort.Sort(currentNode.Suffixes) currentNode = newNode continue loop } } func (currentNode *Node) Match(path string) (*Node, []string) { for strings.HasSuffix(path, "/") { path = path[:len(path)-1] } variableValues := make([]string, 0, 8) return currentNode.matchRemaining(path, variableValues) } func (currentNode *Node) matchRemaining(remaining string, paramValues []string) (*Node, []string) { // Check if this node matches if len(remaining) == 0 && currentNode.Value != nil { return currentNode, paramValues } // See if any suffix matches for _, suffix := range currentNode.Suffixes { var resultNode *Node var resultValues []string switch suffix.Kind { case SuffixKindConstant: pattern := suffix.Pattern if strings.HasPrefix(remaining, pattern) { newRemaining := remaining[len(pattern):] resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, paramValues) } else if len(remaining) == 0 && pattern == "/" { resultNode, resultValues = suffix.Node.matchRemaining(remaining, paramValues) } case SuffixKindVariable: i := strings.IndexByte(remaining, '/') if i < 0 { i = len(remaining) } newParamValues := append(paramValues, remaining[:i]) newRemaining := remaining[i:] resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, newParamValues) case SuffixKindEverything: newParamValues := append(paramValues, remaining) resultNode, resultValues = suffix.Node, newParamValues case SuffixKindRegExp: i := strings.IndexByte(remaining, '/') if i < 0 { i = len(remaining) } paramValue := remaining[:i] regExp := suffix.regExp if regExp.MatchString(paramValue) { matches := regExp.FindStringSubmatch(paramValue) if len(matches) > 1 { paramValue = matches[1] } newParamValues := append(paramValues, paramValue) newRemaining := remaining[i:] resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, newParamValues) } } if resultNode != nil && resultNode.Value != nil { // This suffix matched return resultNode, resultValues } } // No suffix matched return nil, nil } kin-openapi-0.85.0/routers/legacy/pathpattern/node_test.go000066400000000000000000000045041415236407200236410ustar00rootroot00000000000000package pathpattern import ( "testing" ) func TestPatterns(t *testing.T) { DefaultOptions.SupportRegExp = true rootNode := &Node{} add := func(path, value string) { rootNode.MustAdd(path, value, nil) } add("GET /abc", "GET METHOD") add("POST /abc", "POST METHOD") add("/abc", "SIMPLE") add("/abc/fixedString", "FIXED STRING") add("/abc/{param}", "FILE") add("/abc/{param*}", "DEEP FILE") add("/abc/{fileName|(.*)\\.jpeg}", "JPEG") add("/abc/{fileName|some_prefix_(.*)\\.jpeg}", "PREFIXED JPEG") add("/root/{path*}", "DIRECTORY") add("/impossible_route", "IMPOSSIBLE") add(PathFromHost("www.nike.com", true), "WWW-HOST") add(PathFromHost("{other}.nike.com", true), "OTHER-HOST") expect := func(uri string, expected string, expectedArgs ...string) { actually := "not found" node, actualArgs := rootNode.Match(uri) if node != nil { if s, ok := node.Value.(string); ok { actually = s } } if actually != expected { t.Fatalf("Wrong path!\nInput: %s\nExpected: %q\nActually: %q\nTree:\n%s\n\n", uri, expected, actually, rootNode.String()) return } if !argsEqual(expectedArgs, actualArgs) { t.Fatalf("Wrong variable values!\nInput: %s\nExpected: %q\nActually: %q\nTree:\n%s\n\n", uri, expectedArgs, actualArgs, rootNode.String()) return } } expect("", "not found") expect("/", "not found") expect("GET /abc", "GET METHOD") expect("GET /abc/", "GET METHOD") expect("POST /abc", "POST METHOD") expect("/url_without_handler", "not found") expect("/abc", "SIMPLE") expect("/abc/fixedString", "FIXED STRING") expect("/abc/09az", "FILE", "09az") expect("/abc/09az/1/2/3", "DEEP FILE", "09az/1/2/3") expect("/abc/09az/1/2/3/", "DEEP FILE", "09az/1/2/3") expect("/abc/someFile.jpeg", "JPEG", "someFile") expect("/abc/someFile.old.jpeg", "JPEG", "someFile.old") expect("/abc/some_prefix_someFile.jpeg", "PREFIXED JPEG", "someFile") expect("/root", "DIRECTORY", "") expect("/root/", "DIRECTORY", "") expect("/root/a/b/c", "DIRECTORY", "a/b/c") expect(PathFromHost("www.nike.com", true), "WWW-HOST") expect(PathFromHost("example.nike.com", true), "OTHER-HOST", "example") expect(PathFromHost("subdomain.example.nike.com", true), "not found") } func argsEqual(a, b []string) bool { if len(a) != len(b) { return false } for i, ai := range a { if ai != b[i] { return false } } return true } kin-openapi-0.85.0/routers/legacy/router.go000066400000000000000000000106021415236407200206370ustar00rootroot00000000000000// Package legacy implements a router. // // It differs from the gorilla/mux router: // * it provides granular errors: "path not found", "method not allowed", "variable missing from path" // * it does not handle matching routes with extensions (e.g. /books/{id}.json) // * it handles path patterns with a different syntax (e.g. /params/{x}/{y}/{z.*}) package legacy import ( "context" "errors" "fmt" "net/http" "strings" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" "github.com/getkin/kin-openapi/routers/legacy/pathpattern" ) // Routers maps a HTTP request to a Router. type Routers []*Router // FindRoute extracts the route and parameters of an http.Request func (rs Routers) FindRoute(req *http.Request) (routers.Router, *routers.Route, map[string]string, error) { for _, router := range rs { // Skip routers that have DO NOT have servers if len(router.doc.Servers) == 0 { continue } route, pathParams, err := router.FindRoute(req) if err == nil { return router, route, pathParams, nil } } for _, router := range rs { // Skip routers that DO have servers if len(router.doc.Servers) > 0 { continue } route, pathParams, err := router.FindRoute(req) if err == nil { return router, route, pathParams, nil } } return nil, nil, nil, &routers.RouteError{ Reason: "none of the routers match", } } // Router maps a HTTP request to an OpenAPI operation. type Router struct { doc *openapi3.T pathNode *pathpattern.Node } // NewRouter creates a new router. // // If the given OpenAPIv3 document has servers, router will use them. // All operations of the document will be added to the router. func NewRouter(doc *openapi3.T) (routers.Router, error) { if err := doc.Validate(context.Background()); err != nil { return nil, fmt.Errorf("validating OpenAPI failed: %v", err) } router := &Router{doc: doc} root := router.node() for path, pathItem := range doc.Paths { for method, operation := range pathItem.Operations() { method = strings.ToUpper(method) if err := root.Add(method+" "+path, &routers.Route{ Spec: doc, Path: path, PathItem: pathItem, Method: method, Operation: operation, }, nil); err != nil { return nil, err } } } return router, nil } // AddRoute adds a route in the router. func (router *Router) AddRoute(route *routers.Route) error { method := route.Method if method == "" { return errors.New("route is missing method") } method = strings.ToUpper(method) path := route.Path if path == "" { return errors.New("route is missing path") } return router.node().Add(method+" "+path, router, nil) } func (router *Router) node() *pathpattern.Node { root := router.pathNode if root == nil { root = &pathpattern.Node{} router.pathNode = root } return root } // FindRoute extracts the route and parameters of an http.Request func (router *Router) FindRoute(req *http.Request) (*routers.Route, map[string]string, error) { method, url := req.Method, req.URL doc := router.doc // Get server servers := doc.Servers var server *openapi3.Server var remainingPath string var pathParams map[string]string if len(servers) == 0 { remainingPath = url.Path } else { var paramValues []string server, paramValues, remainingPath = servers.MatchURL(url) if server == nil { return nil, nil, &routers.RouteError{ Reason: routers.ErrPathNotFound.Error(), } } pathParams = make(map[string]string, 8) paramNames, err := server.ParameterNames() if err != nil { return nil, nil, err } for i, value := range paramValues { name := paramNames[i] pathParams[name] = value } } // Get PathItem root := router.node() var route *routers.Route node, paramValues := root.Match(method + " " + remainingPath) if node != nil { route, _ = node.Value.(*routers.Route) } if route == nil { pathItem := doc.Paths[remainingPath] if pathItem == nil { return nil, nil, &routers.RouteError{Reason: routers.ErrPathNotFound.Error()} } if pathItem.GetOperation(method) == nil { return nil, nil, &routers.RouteError{Reason: routers.ErrMethodNotAllowed.Error()} } } if pathParams == nil { pathParams = make(map[string]string, len(paramValues)) } paramKeys := node.VariableNames for i, value := range paramValues { key := paramKeys[i] if strings.HasSuffix(key, "*") { key = key[:len(key)-1] } pathParams[key] = value } return route, pathParams, nil } kin-openapi-0.85.0/routers/legacy/router_test.go000066400000000000000000000147421415236407200217070ustar00rootroot00000000000000package legacy import ( "context" "net/http" "sort" "testing" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" "github.com/stretchr/testify/require" ) func TestRouter(t *testing.T) { helloCONNECT := &openapi3.Operation{Responses: openapi3.NewResponses()} helloDELETE := &openapi3.Operation{Responses: openapi3.NewResponses()} helloGET := &openapi3.Operation{Responses: openapi3.NewResponses()} helloHEAD := &openapi3.Operation{Responses: openapi3.NewResponses()} helloOPTIONS := &openapi3.Operation{Responses: openapi3.NewResponses()} helloPATCH := &openapi3.Operation{Responses: openapi3.NewResponses()} helloPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} helloPUT := &openapi3.Operation{Responses: openapi3.NewResponses()} helloTRACE := &openapi3.Operation{Responses: openapi3.NewResponses()} paramsGET := &openapi3.Operation{Responses: openapi3.NewResponses()} booksPOST := &openapi3.Operation{Responses: openapi3.NewResponses()} partialGET := &openapi3.Operation{Responses: openapi3.NewResponses()} doc := &openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "MyAPI", Version: "0.1", }, Paths: openapi3.Paths{ "/hello": &openapi3.PathItem{ Connect: helloCONNECT, Delete: helloDELETE, Get: helloGET, Head: helloHEAD, Options: helloOPTIONS, Patch: helloPATCH, Post: helloPOST, Put: helloPUT, Trace: helloTRACE, }, "/onlyGET": &openapi3.PathItem{ Get: helloGET, }, "/params/{x}/{y}/{z.*}": &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("x")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("y")}, &openapi3.ParameterRef{Value: openapi3.NewPathParameter("z")}, }, }, "/books/{bookid}": &openapi3.PathItem{ Get: paramsGET, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid")}, }, }, "/books/{bookid2}.json": &openapi3.PathItem{ Post: booksPOST, Parameters: openapi3.Parameters{ &openapi3.ParameterRef{Value: openapi3.NewPathParameter("bookid2")}, }, }, "/partial": &openapi3.PathItem{ Get: partialGET, }, }, } expect := func(r routers.Router, method string, uri string, operation *openapi3.Operation, params map[string]string) { req, err := http.NewRequest(method, uri, nil) require.NoError(t, err) route, pathParams, err := r.FindRoute(req) if err != nil { if operation == nil { pathItem := doc.Paths[uri] if pathItem == nil { if err.Error() != routers.ErrPathNotFound.Error() { t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrPathNotFound, err) } return } if pathItem.GetOperation(method) == nil { if err.Error() != routers.ErrMethodNotAllowed.Error() { t.Fatalf("'%s %s': should have returned %q, but it returned an error: %v", method, uri, routers.ErrMethodNotAllowed, err) } } } else { t.Fatalf("'%s %s': should have returned an operation, but it returned an error: %v", method, uri, err) } } if operation == nil && err == nil { t.Fatalf("'%s %s': should have failed, but returned\nroute = %+v\npathParams = %+v", method, uri, route, pathParams) } if route == nil { return } if route.Operation != operation { t.Fatalf("'%s %s': Returned wrong operation (%v)", method, uri, route.Operation) } if len(params) == 0 { if len(pathParams) != 0 { t.Fatalf("'%s %s': should return no path arguments, but found %+v", method, uri, pathParams) } } else { names := make([]string, 0, len(params)) for name := range params { names = append(names, name) } sort.Strings(names) for _, name := range names { expected := params[name] actual, exists := pathParams[name] if !exists { t.Fatalf("'%s %s': path parameter %q should be %q, but it's not defined.", method, uri, name, expected) } if actual != expected { t.Fatalf("'%s %s': path parameter %q should be %q, but it's %q", method, uri, name, expected, actual) } } } } err := doc.Validate(context.Background()) require.NoError(t, err) r, err := NewRouter(doc) require.NoError(t, err) expect(r, http.MethodGet, "/not_existing", nil, nil) expect(r, http.MethodDelete, "/hello", helloDELETE, nil) expect(r, http.MethodGet, "/hello", helloGET, nil) expect(r, http.MethodHead, "/hello", helloHEAD, nil) expect(r, http.MethodPatch, "/hello", helloPATCH, nil) expect(r, http.MethodPost, "/hello", helloPOST, nil) expect(r, http.MethodPut, "/hello", helloPUT, nil) expect(r, http.MethodGet, "/params/a/b/", paramsGET, map[string]string{ "x": "a", "y": "b", // "z": "", }) expect(r, http.MethodGet, "/params/a/b/c%2Fd", paramsGET, map[string]string{ "x": "a", "y": "b", // "z": "c/d", }) expect(r, http.MethodGet, "/books/War.and.Peace", paramsGET, map[string]string{ "bookid": "War.and.Peace", }) { req, err := http.NewRequest(http.MethodPost, "/books/War.and.Peace.json", nil) require.NoError(t, err) _, _, err = r.FindRoute(req) require.EqualError(t, err, routers.ErrPathNotFound.Error()) } expect(r, http.MethodPost, "/partial", nil, nil) doc.Servers = []*openapi3.Server{ {URL: "https://www.example.com/api/v1"}, {URL: "https://{d0}.{d1}.com/api/v1/", Variables: map[string]*openapi3.ServerVariable{ "d0": {Default: "www"}, "d1": {Default: "example", Enum: []string{"example"}}, }}, } err = doc.Validate(context.Background()) require.NoError(t, err) r, err = NewRouter(doc) require.NoError(t, err) expect(r, http.MethodGet, "/hello", nil, nil) expect(r, http.MethodGet, "/api/v1/hello", nil, nil) expect(r, http.MethodGet, "www.example.com/api/v1/hello", nil, nil) expect(r, http.MethodGet, "https:///api/v1/hello", nil, nil) expect(r, http.MethodGet, "https://www.example.com/hello", nil, nil) expect(r, http.MethodGet, "https://www.example.com/api/v1/hello", helloGET, nil) expect(r, http.MethodGet, "https://domain0.domain1.com/api/v1/hello", helloGET, map[string]string{ "d0": "domain0", "d1": "domain1", }) { uri := "https://www.example.com/api/v1/onlyGET" expect(r, http.MethodGet, uri, helloGET, nil) req, err := http.NewRequest(http.MethodDelete, uri, nil) require.NoError(t, err) require.NotNil(t, req) route, pathParams, err := r.FindRoute(req) require.EqualError(t, err, routers.ErrMethodNotAllowed.Error()) require.Nil(t, route) require.Nil(t, pathParams) } } kin-openapi-0.85.0/routers/legacy/validate_request_test.go000066400000000000000000000043421415236407200237230ustar00rootroot00000000000000package legacy_test import ( "bytes" "encoding/json" "fmt" "net/http" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" "github.com/getkin/kin-openapi/routers/legacy" ) const spec = ` openapi: 3.0.0 info: title: My API version: 0.0.1 paths: /: post: responses: default: description: '' requestBody: required: true content: application/json: schema: oneOf: - $ref: '#/components/schemas/Cat' - $ref: '#/components/schemas/Dog' discriminator: propertyName: pet_type components: schemas: Pet: type: object required: [pet_type] properties: pet_type: type: string discriminator: propertyName: pet_type Dog: allOf: - $ref: '#/components/schemas/Pet' - type: object properties: breed: type: string enum: [Dingo, Husky, Retriever, Shepherd] Cat: allOf: - $ref: '#/components/schemas/Pet' - type: object properties: hunts: type: boolean age: type: integer ` func Example() { loader := openapi3.NewLoader() doc, err := loader.LoadFromData([]byte(spec)) if err != nil { panic(err) } if err := doc.Validate(loader.Context); err != nil { panic(err) } router, err := legacy.NewRouter(doc) if err != nil { panic(err) } p, err := json.Marshal(map[string]interface{}{ "pet_type": "Cat", "breed": "Dingo", "bark": true, }) if err != nil { panic(err) } req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(p)) if err != nil { panic(err) } req.Header.Set("Content-Type", "application/json") route, pathParams, err := router.FindRoute(req) if err != nil { panic(err) } requestValidationInput := &openapi3filter.RequestValidationInput{ Request: req, PathParams: pathParams, Route: route, } if err := openapi3filter.ValidateRequest(loader.Context, requestValidationInput); err != nil { fmt.Println(err) } // Output: // request body has an error: doesn't match the schema: input matches more than one oneOf schemas } kin-openapi-0.85.0/routers/types.go000066400000000000000000000016101415236407200172160ustar00rootroot00000000000000package routers import ( "net/http" "github.com/getkin/kin-openapi/openapi3" ) // Router helps link http.Request.s and an OpenAPIv3 spec type Router interface { FindRoute(req *http.Request) (route *Route, pathParams map[string]string, err error) } // Route describes the operation an http.Request can match type Route struct { Spec *openapi3.T Server *openapi3.Server Path string PathItem *openapi3.PathItem Method string Operation *openapi3.Operation } // ErrPathNotFound is returned when no route match is found var ErrPathNotFound error = &RouteError{"no matching operation was found"} // ErrMethodNotAllowed is returned when no method of the matched route matches var ErrMethodNotAllowed error = &RouteError{"method not allowed"} // RouteError describes Router errors type RouteError struct { Reason string } func (e *RouteError) Error() string { return e.Reason }