cloudapi: validate input

Validate incoming requests with openapi3. Remove unsupported
uuid format from the openapi spec. Similarly, change url to uri as
uri is a supported format and url is not.

Co-authored-by: Ondřej Budai <obudai@redhat.com>
Signed-off-by: Ondřej Budai <ondrej@budai.cz>
This commit is contained in:
Chloe Kaubisch 2022-03-10 14:16:36 +00:00
parent f616becf39
commit 13c79294b6
83 changed files with 4942 additions and 549 deletions

3
go.mod
View file

@ -19,7 +19,8 @@ require (
github.com/coreos/go-semver v0.3.0
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
github.com/deepmap/oapi-codegen v1.8.2
github.com/getkin/kin-openapi v0.61.0
github.com/getkin/kin-openapi v0.93.0
github.com/go-openapi/swag v0.21.1 // indirect
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/glog v1.0.0 // indirect

14
go.sum
View file

@ -200,8 +200,9 @@ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getkin/kin-openapi v0.61.0 h1:6awGqF5nG5zkVpMsAih1QH4VgzS8phTxECUWIFo7zko=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.93.0 h1:fc9z+9VADQla6bEb7V+dtZmr9Gj4qB6ZsD8c3pqEK0E=
github.com/getkin/kin-openapi v0.93.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
@ -217,8 +218,9 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
@ -337,6 +339,7 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@ -449,6 +452,8 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -496,8 +501,9 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
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/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@ -549,6 +555,7 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
@ -1224,6 +1231,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=

View file

@ -43,6 +43,7 @@ const (
ErrorInvalidOSTreeParams ServiceErrorCode = 27
ErrorTenantNotFound ServiceErrorCode = 28
ErrorNoGPGKey ServiceErrorCode = 29
ErrorInvalidRequest ServiceErrorCode = 30
// Internal errors, these are bugs
ErrorFailedToInitializeBlueprint ServiceErrorCode = 1000
@ -110,6 +111,7 @@ func getServiceErrors() serviceErrors {
serviceError{ErrorInvalidOSTreeParams, http.StatusBadRequest, "Invalid OSTree parameters or parameter combination"},
serviceError{ErrorTenantNotFound, http.StatusBadRequest, "Tenant not found in JWT claims"},
serviceError{ErrorNoGPGKey, http.StatusBadRequest, "Invalid repository, when check_gpg is set, gpgkey must be specified"},
serviceError{ErrorInvalidRequest, http.StatusBadRequest, "Request could not be validated"},
serviceError{ErrorFailedToInitializeBlueprint, http.StatusInternalServerError, "Failed to initialize blueprint"},
serviceError{ErrorFailedToGenerateManifestSeed, http.StatusInternalServerError, "Failed to generate manifest seed"},

View file

@ -351,6 +351,7 @@ type List struct {
// OSTree defines model for OSTree.
type OSTree struct {
// Can be either a commit (example: 02604b2da6e954bd34b8b82a835e5a77d2b60ffa), or a branch-like reference (example: rhel/8/x86_64/edge)
Parent *string `json:"parent,omitempty"`
Ref *string `json:"ref,omitempty"`
Url *string `json:"url,omitempty"`
@ -661,108 +662,109 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"w1eRWQAA",
}
// GetSwagger returns the content of the embedded swagger specification file

View file

@ -279,6 +279,7 @@ paths:
description: ID of the error
responses:
'200':
description: Error description
content:
application/json:
schema:
@ -445,6 +446,9 @@ components:
properties:
image_builds:
type: array
items:
type: object
x-go-type: interface{}
koji:
$ref: '#/components/schemas/KojiLogs'
KojiLogs:
@ -464,6 +468,9 @@ components:
properties:
manifests:
type: array
items:
type: object
x-go-type: interface{}
ImageStatus:
required:
- status
@ -659,15 +666,15 @@ components:
description: 'Determines whether a valid subscription is required to access this repository.'
baseurl:
type: string
format: url
format: uri
example: 'https://cdn.redhat.com/content/dist/rhel8/8/x86_64/baseos/os/'
mirrorlist:
type: string
format: url
format: uri
example: 'http://mirrorlist.centos.org/?release=8-stream&arch=aarch64&repo=BaseOS'
metalink:
type: string
format: url
format: uri
example: 'https://mirrors.fedoraproject.org/metalink?repo=fedora-32&arch=x86_64'
gpgkey:
type: string
@ -865,13 +872,11 @@ components:
example: 'rhel/8/x86_64/edge'
parent:
type: string
examples:
commit_id:
value: '02604b2da6e954bd34b8b82a835e5a77d2b60ffa'
summary: A commit ID
ref:
value: 'rhel/8/x86_64/edge'
summary: A branch-like ref
description: >
Can be either a commit (example:
02604b2da6e954bd34b8b82a835e5a77d2b60ffa), or a branch-like
reference (example: rhel/8/x86_64/edge)
example: 'rhel/8/x86_64/edge'
Subscription:
type: object
required:
@ -890,10 +895,10 @@ components:
example: 'my-secret-key'
server_url:
type: string
format: uri
example: 'subscription.rhsm.redhat.com'
base_url:
type: string
format: url
example: 'http://cdn.redhat.com/'
insights:
type: boolean
@ -925,7 +930,7 @@ components:
properties:
server:
type: string
format: url
format: uri
example: 'https://koji.fedoraproject.org/kojihub'
task_id:
type: integer

View file

@ -8,6 +8,10 @@ import (
"sync"
"time"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers"
legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@ -30,6 +34,7 @@ type Server struct {
workers *worker.Server
distros *distroregistry.Registry
config ServerConfig
router routers.Router
goroutinesCtx context.Context
goroutinesCtxCancel context.CancelFunc
@ -44,10 +49,26 @@ type ServerConfig struct {
func NewServer(workers *worker.Server, distros *distroregistry.Registry, config ServerConfig) *Server {
ctx, cancel := context.WithCancel(context.Background())
spec, err := GetSwagger()
if err != nil {
panic(err)
}
loader := openapi3.NewLoader()
if err := spec.Validate(loader.Context); err != nil {
panic(err)
}
router, err := legacyrouter.NewRouter(spec)
if err != nil {
panic(err)
}
server := &Server{
workers: workers,
distros: distros,
config: config,
router: router,
goroutinesCtx: ctx,
goroutinesCtxCancel: cancel,
@ -66,11 +87,39 @@ func (s *Server) Handler(path string) http.Handler {
handler := apiHandlers{
server: s,
}
RegisterHandlers(e.Group(path, prometheus.MetricsMiddleware), &handler)
RegisterHandlers(e.Group(path, prometheus.MetricsMiddleware, s.ValidateRequest), &handler)
return e
}
func (s *Server) ValidateRequest(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
request := c.Request()
// extract route and parameters from request
route, params, err := s.router.FindRoute(request)
if err != nil {
return HTTPErrorWithInternal(ErrorResourceNotFound, err)
}
input := &openapi3filter.RequestValidationInput{
Request: request,
PathParams: params,
Route: route,
Options: &openapi3filter.Options{
AuthenticationFunc: openapi3filter.NoopAuthenticationFunc,
},
}
ctx := request.Context()
if err := openapi3filter.ValidateRequest(ctx, input); err != nil {
return HTTPErrorWithInternal(ErrorInvalidRequest, err)
}
return next(c)
}
}
func (s *Server) Shutdown() {
s.goroutinesCtxCancel()
s.goroutinesGroup.Wait()

View file

@ -159,11 +159,11 @@ func TestCompose(t *testing.T) {
}
}`, test_distro.TestArch3Name), http.StatusBadRequest, `
{
"href": "/api/image-builder-composer/v2/errors/4",
"id": "4",
"href": "/api/image-builder-composer/v2/errors/30",
"id": "30",
"kind": "Error",
"code": "IMAGE-BUILDER-COMPOSER-4",
"reason": "Unsupported distribution"
"code": "IMAGE-BUILDER-COMPOSER-30",
"reason": "Request could not be validated"
}`, "operation_id")
// unsupported architecture
@ -207,11 +207,11 @@ func TestCompose(t *testing.T) {
}
}`, test_distro.TestDistroName, test_distro.TestArch3Name), http.StatusBadRequest, `
{
"href": "/api/image-builder-composer/v2/errors/6",
"id": "6",
"href": "/api/image-builder-composer/v2/errors/30",
"id": "30",
"kind": "Error",
"code": "IMAGE-BUILDER-COMPOSER-6",
"reason": "Unsupported image type"
"code": "IMAGE-BUILDER-COMPOSER-30",
"reason": "Request could not be validated"
}`, "operation_id")
// Returns 404, but should be 405; see https://github.com/labstack/echo/issues/1981
@ -945,7 +945,9 @@ func TestImageTypes(t *testing.T) {
"rhsm": false
}],
"upload_options": {
"region": "eu-central-1"
"region": "eu-central-1",
"snapshot_name": "name",
"share_with_accounts": ["123456789012","234567890123"]
}
}
}`, test_distro.TestDistroName, test_distro.TestArch3Name, string(v2.ImageTypesAws)), http.StatusCreated, `

View file

@ -21,6 +21,9 @@ type FieldInfo struct {
}
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:

View file

@ -5,7 +5,7 @@ import (
)
func MarshalRef(value string, otherwise interface{}) ([]byte, error) {
if len(value) > 0 {
if value != "" {
return json.Marshal(&refProps{
Ref: value,
})
@ -17,7 +17,7 @@ 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 {
if ref != "" {
*destRef = ref
return nil
}
@ -26,5 +26,5 @@ func UnmarshalRef(data []byte, destRef *string, destOtherwise interface{}) error
}
type refProps struct {
Ref string `json:"$ref,omitempty"`
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
}

View file

@ -23,7 +23,8 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) {
return ref.Value, nil
}
// Callback is specified by OpenAPI/Swagger standard version 3.0.
// Callback is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callbackObject
type Callback map[string]*PathItem
func (value Callback) Validate(ctx context.Context) error {

View file

@ -8,9 +8,11 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// Components is specified by OpenAPI/Swagger standard version 3.0.
// Components is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject
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"`

View file

@ -6,9 +6,11 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// Discriminator is specified by OpenAPI/Swagger standard version 3.0.
// Discriminator is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminatorObject
type Discriminator struct {
ExtensionProps
PropertyName string `json:"propertyName" yaml:"propertyName"`
Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
}

View file

@ -8,6 +8,7 @@ import (
)
// Encoding is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encodingObject
type Encoding struct {
ExtensionProps

View file

@ -25,6 +25,7 @@ func (e Examples) JSONLookup(token string) (interface{}, error) {
}
// Example is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#exampleObject
type Example struct {
ExtensionProps

View file

@ -1,15 +1,21 @@
package openapi3
import (
"context"
"errors"
"fmt"
"net/url"
"github.com/getkin/kin-openapi/jsoninfo"
)
// ExternalDocs is specified by OpenAPI/Swagger standard version 3.0.
// ExternalDocs is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object
type ExternalDocs struct {
ExtensionProps
Description string `json:"description,omitempty"`
URL string `json:"url,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}
func (e *ExternalDocs) MarshalJSON() ([]byte, error) {
@ -19,3 +25,13 @@ func (e *ExternalDocs) MarshalJSON() ([]byte, error) {
func (e *ExternalDocs) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, e)
}
func (e *ExternalDocs) Validate(ctx context.Context) error {
if e.URL == "" {
return errors.New("url is required")
}
if _, err := url.Parse(e.URL); err != nil {
return fmt.Errorf("url is incorrect: %w", err)
}
return nil
}

View file

@ -2,6 +2,7 @@ package openapi3
import (
"context"
"errors"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
@ -24,17 +25,10 @@ func (h Headers) JSONLookup(token string) (interface{}, error) {
return ref.Value, nil
}
// Header is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#headerObject
type Header struct {
ExtensionProps
// Optional description. Should use CommonMark syntax.
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
Required bool `json:"required,omitempty" yaml:"required,omitempty"`
Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"`
Content Content `json:"content,omitempty" yaml:"content,omitempty"`
Parameter
}
var _ jsonpointer.JSONPointable = (*Header)(nil)
@ -43,10 +37,52 @@ 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 v := value.Schema; v != nil {
if err := v.Validate(ctx); err != nil {
return err
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
@ -61,8 +97,20 @@ func (value Header) JSONLookup(token string) (interface{}, error) {
}
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":

View file

@ -7,9 +7,11 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// Info is specified by OpenAPI/Swagger standard version 3.0.
// Info is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#infoObject
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"`
@ -50,9 +52,11 @@ func (value *Info) Validate(ctx context.Context) error {
return nil
}
// Contact is specified by OpenAPI/Swagger standard version 3.0.
// Contact is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contactObject
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"`
@ -70,9 +74,11 @@ func (value *Contact) Validate(ctx context.Context) error {
return nil
}
// License is specified by OpenAPI/Swagger standard version 3.0.
// License is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#licenseObject
type License struct {
ExtensionProps
Name string `json:"name" yaml:"name"` // Required
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}

View file

@ -0,0 +1,369 @@
package openapi3
import (
"context"
"path/filepath"
"strings"
)
type RefNameResolver func(string) string
// DefaultRefResolver is a default implementation of refNameResolver for the
// InternalizeRefs function.
//
// If a reference points to an element inside a document, it returns the last
// element in the reference using filepath.Base. Otherwise if the reference points
// to a file, it returns the file name trimmed of all extensions.
func DefaultRefNameResolver(ref string) string {
if ref == "" {
return ""
}
split := strings.SplitN(ref, "#", 2)
if len(split) == 2 {
return filepath.Base(split[1])
}
ref = split[0]
for ext := filepath.Ext(ref); len(ext) > 0; ext = filepath.Ext(ref) {
ref = strings.TrimSuffix(ref, ext)
}
return filepath.Base(ref)
}
func schemaNames(s Schemas) []string {
out := make([]string, 0, len(s))
for i := range s {
out = append(out, i)
}
return out
}
func parametersMapNames(s ParametersMap) []string {
out := make([]string, 0, len(s))
for i := range s {
out = append(out, i)
}
return out
}
func isExternalRef(ref string) 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)
}

View file

@ -25,11 +25,13 @@ func (l Links) JSONLookup(token string) (interface{}, error) {
var _ jsonpointer.JSONPointable = (*Links)(nil)
// Link is specified by OpenAPI/Swagger standard version 3.0.
// Link is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#linkObject
type Link struct {
ExtensionProps
OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"`
OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"`
OperationID string `json:"operationId,omitempty" yaml:"operationId,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"`

View file

@ -5,8 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"path/filepath"
@ -31,10 +29,12 @@ type Loader struct {
IsExternalRefsAllowed bool
// ReadFromURIFunc allows overriding the any file/URL reading func
ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
ReadFromURIFunc ReadFromURIFunc
Context context.Context
rootDir string
visitedPathItemRefs map[string]struct{}
visitedDocuments map[string]*T
@ -66,6 +66,7 @@ func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) {
// 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)})
}
@ -118,22 +119,7 @@ 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)
return DefaultReadFromURI(loader, location)
}
// LoadFromData loads a spec from a byte array
@ -305,6 +291,9 @@ func (loader *Loader) resolveComponent(
}
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 {
@ -346,6 +335,14 @@ func (loader *Loader) resolveComponent(
}
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))
@ -372,6 +369,10 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) {
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
}
@ -400,6 +401,14 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) {
}
}
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
@ -459,6 +468,7 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -506,6 +516,7 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -562,6 +573,7 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -617,6 +629,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -686,6 +699,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -749,7 +763,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme
if ref := component.Ref; ref != "" {
if isSingleRefElement(ref) {
var scheme SecurityScheme
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil {
if _, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil {
return err
}
component.Value = &scheme
@ -763,6 +777,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme
return err
}
component.Value = resolved.Value
_ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
return nil
@ -785,7 +800,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP
if ref := component.Ref; ref != "" {
if isSingleRefElement(ref) {
var example Example
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil {
if _, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil {
return err
}
component.Value = &example
@ -799,6 +814,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP
return err
}
component.Value = resolved.Value
_ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
return nil
@ -826,6 +842,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -909,7 +926,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u
if ref := component.Ref; ref != "" {
if isSingleRefElement(ref) {
var link Link
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil {
if _, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil {
return err
}
component.Value = &link
@ -923,6 +940,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u
return err
}
component.Value = resolved.Value
_ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
return nil

View file

@ -0,0 +1,104 @@
package openapi3
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
)
// ReadFromURIFunc defines a function which reads the contents of a resource
// located at a URI.
type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
// ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a
// given URI.
var ErrURINotSupported = errors.New("unsupported URI")
// ReadFromURIs returns a ReadFromURIFunc which tries to read a URI using the
// given reader functions, in the same order. If a reader function does not
// support the URI and returns ErrURINotSupported, the next function is checked
// until a match is found, or the URI is not supported by any.
func ReadFromURIs(readers ...ReadFromURIFunc) ReadFromURIFunc {
return func(loader *Loader, url *url.URL) ([]byte, error) {
for i := range readers {
buf, err := readers[i](loader, url)
if err == ErrURINotSupported {
continue
} else if err != nil {
return nil, err
}
return buf, nil
}
return nil, ErrURINotSupported
}
}
// DefaultReadFromURI returns a caching ReadFromURIFunc which can read remote
// HTTP URIs and local file URIs.
var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile))
// ReadFromHTTP returns a ReadFromURIFunc which uses the given http.Client to
// read the contents from a remote HTTP URI. This client may be customized to
// implement timeouts, RFC 7234 caching, etc.
func ReadFromHTTP(cl *http.Client) ReadFromURIFunc {
return func(loader *Loader, location *url.URL) ([]byte, error) {
if location.Scheme == "" || location.Host == "" {
return nil, ErrURINotSupported
}
req, err := http.NewRequest("GET", location.String(), nil)
if err != nil {
return nil, err
}
resp, err := cl.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode > 399 {
return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
}
}
// ReadFromFile is a ReadFromURIFunc which reads local file URIs.
func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) {
if location.Host != "" {
return nil, ErrURINotSupported
}
if location.Scheme != "" && location.Scheme != "file" {
return nil, ErrURINotSupported
}
return ioutil.ReadFile(location.Path)
}
// URIMapCache returns a ReadFromURIFunc that caches the contents read from URI
// locations in a simple map. This cache implementation is suitable for
// short-lived processes such as command-line tools which process OpenAPI
// documents.
func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc {
cache := map[string][]byte{}
return func(loader *Loader, location *url.URL) (buf []byte, err error) {
if location.Scheme == "" || location.Scheme == "file" {
if !filepath.IsAbs(location.Path) {
// Do not cache relative file paths; this can cause trouble if
// the current working directory changes when processing
// multiple top-level documents.
return reader(loader, location)
}
}
uri := location.String()
var ok bool
if buf, ok = cache[uri]; ok {
return
}
if buf, err = reader(loader, location); err != nil {
return
}
cache[uri] = buf
return
}
}

View file

@ -8,6 +8,7 @@ import (
)
// MediaType is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject
type MediaType struct {
ExtensionProps

View file

@ -9,8 +9,10 @@ import (
)
// T is the root of an OpenAPI v3 document
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oasObject
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
@ -101,5 +103,23 @@ func (value *T) Validate(ctx context.Context) error {
}
}
{
wrap := func(e error) error { return fmt.Errorf("invalid tags: %w", e) }
if v := value.Tags; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
}
}
{
wrap := func(e error) error { return fmt.Errorf("invalid external docs: %w", e) }
if v := value.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
}
}
return nil
}

View file

@ -3,6 +3,7 @@ package openapi3
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/getkin/kin-openapi/jsoninfo"
@ -10,6 +11,7 @@ import (
)
// Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object
type Operation struct {
ExtensionProps
@ -138,5 +140,10 @@ func (value *Operation) Validate(ctx context.Context) error {
} else {
return errors.New("value of responses must be an object")
}
if v := value.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("invalid external docs: %w", err)
}
}
return nil
}

View file

@ -83,8 +83,10 @@ func (value Parameters) Validate(ctx context.Context) error {
}
// Parameter is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.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"`
@ -167,42 +169,42 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, parameter)
}
func (parameter Parameter) JSONLookup(token string) (interface{}, error) {
func (value Parameter) JSONLookup(token string) (interface{}, error) {
switch token {
case "schema":
if parameter.Schema != nil {
if parameter.Schema.Ref != "" {
return &Ref{Ref: parameter.Schema.Ref}, nil
if value.Schema != nil {
if value.Schema.Ref != "" {
return &Ref{Ref: value.Schema.Ref}, nil
}
return parameter.Schema.Value, nil
return value.Schema.Value, nil
}
case "name":
return parameter.Name, nil
return value.Name, nil
case "in":
return parameter.In, nil
return value.In, nil
case "description":
return parameter.Description, nil
return value.Description, nil
case "style":
return parameter.Style, nil
return value.Style, nil
case "explode":
return parameter.Explode, nil
return value.Explode, nil
case "allowEmptyValue":
return parameter.AllowEmptyValue, nil
return value.AllowEmptyValue, nil
case "allowReserved":
return parameter.AllowReserved, nil
return value.AllowReserved, nil
case "deprecated":
return parameter.Deprecated, nil
return value.Deprecated, nil
case "required":
return parameter.Required, nil
return value.Required, nil
case "example":
return parameter.Example, nil
return value.Example, nil
case "examples":
return parameter.Examples, nil
return value.Examples, nil
case "content":
return parameter.Content, nil
return value.Content, nil
}
v, _, err := jsonpointer.GetForToken(parameter.ExtensionProps, token)
v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token)
return v, err
}
@ -251,6 +253,10 @@ func (value *Parameter) Validate(ctx context.Context) error {
return fmt.Errorf("parameter can't have 'in' value %q", value.In)
}
if in == ParameterInPath && !value.Required {
return fmt.Errorf("path parameter %q must be required", value.Name)
}
// Validate a parameter's serialization method.
sm, err := value.SerializationMethod()
if err != nil {
@ -294,6 +300,7 @@ func (value *Parameter) Validate(ctx context.Context) error {
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)

View file

@ -8,8 +8,11 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// PathItem is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathItemObject
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"`

View file

@ -6,7 +6,8 @@ import (
"strings"
)
// Paths is specified by OpenAPI/Swagger standard version 3.0.
// Paths is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object
type Paths map[string]*PathItem
func (value Paths) Validate(ctx context.Context) error {
@ -21,31 +22,60 @@ func (value Paths) Validate(ctx context.Context) error {
pathItem = value[path]
}
normalizedPath, pathParamsCount := normalizeTemplatedPath(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 globalCount uint
var commonParams []string
for _, parameterRef := range pathItem.Parameters {
if parameterRef != nil {
if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
globalCount++
commonParams = append(commonParams, parameter.Name)
}
}
}
for method, operation := range pathItem.Operations() {
var count uint
var setParams []string
for _, parameterRef := range operation.Parameters {
if parameterRef != nil {
if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
count++
setParams = append(setParams, parameter.Name)
}
}
}
if count+globalCount != pathParamsCount {
return fmt.Errorf("operation %s %s must define exactly all path parameters", method, path)
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)
}
}
}
@ -53,6 +83,11 @@ func (value Paths) Validate(ctx context.Context) error {
return err
}
}
if err := value.validateUniqueOperationIDs(); err != nil {
return err
}
return nil
}
@ -75,9 +110,9 @@ func (paths Paths) Find(key string) *PathItem {
return pathItem
}
normalizedPath, expected := normalizeTemplatedPath(key)
normalizedPath, expected, _ := normalizeTemplatedPath(key)
for path, pathItem := range paths {
pathNormalized, got := normalizeTemplatedPath(path)
pathNormalized, got, _ := normalizeTemplatedPath(path)
if got == expected && pathNormalized == normalizedPath {
return pathItem
}
@ -85,43 +120,75 @@ func (paths Paths) Find(key string) *PathItem {
return nil
}
func normalizeTemplatedPath(path string) (string, uint) {
func (value Paths) validateUniqueOperationIDs() error {
operationIDs := make(map[string]string)
for urlPath, pathItem := range value {
if pathItem == nil {
continue
}
for httpMethod, operation := range pathItem.Operations() {
if operation == nil || operation.OperationID == "" {
continue
}
endpoint := httpMethod + " " + urlPath
if endpointDup, ok := operationIDs[operation.OperationID]; ok {
if endpoint > endpointDup { // For make error message a bit more deterministic. May be useful for tests.
endpoint, endpointDup = endpointDup, endpoint
}
return fmt.Errorf("operations %q and %q have the same operation id %q",
endpoint, endpointDup, operation.OperationID)
}
operationIDs[operation.OperationID] = endpoint
}
}
return nil
}
func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) {
if strings.IndexByte(path, '{') < 0 {
return path, 0
return path, 0, nil
}
var buf strings.Builder
buf.Grow(len(path))
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 variables
// 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 == '*' {
buf.WriteRune(cc)
buffTpl.WriteRune(cc)
}
isVariable = false
} else {
// Skip this character
buffVar.WriteRune(c)
continue
}
} else if c == '{' {
// Begin path variable
// The character '{' will be appended
isVariable = true
// The character '{' will be appended
count++
}
// Append the character
buf.WriteRune(c)
buffTpl.WriteRune(c)
cc = c
}
return buf.String(), count
return buffTpl.String(), count, vars
}

View file

@ -8,10 +8,13 @@ import (
)
// Ref is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#referenceObject
type Ref struct {
Ref string `json:"$ref" yaml:"$ref"`
}
// CallbackRef represents either a Callback or a $ref to a Callback.
// When serializing and both fields are set, Ref is preferred over Value.
type CallbackRef struct {
Ref string
Value *Callback
@ -43,6 +46,8 @@ func (value CallbackRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// ExampleRef represents either a Example or a $ref to a Example.
// When serializing and both fields are set, Ref is preferred over Value.
type ExampleRef struct {
Ref string
Value *Example
@ -74,6 +79,8 @@ func (value ExampleRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// HeaderRef represents either a Header or a $ref to a Header.
// When serializing and both fields are set, Ref is preferred over Value.
type HeaderRef struct {
Ref string
Value *Header
@ -105,6 +112,8 @@ func (value HeaderRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// LinkRef represents either a Link or a $ref to a Link.
// When serializing and both fields are set, Ref is preferred over Value.
type LinkRef struct {
Ref string
Value *Link
@ -125,6 +134,8 @@ func (value *LinkRef) Validate(ctx context.Context) error {
return foundUnresolvedRef(value.Ref)
}
// ParameterRef represents either a Parameter or a $ref to a Parameter.
// When serializing and both fields are set, Ref is preferred over Value.
type ParameterRef struct {
Ref string
Value *Parameter
@ -156,6 +167,8 @@ func (value ParameterRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// ResponseRef represents either a Response or a $ref to a Response.
// When serializing and both fields are set, Ref is preferred over Value.
type ResponseRef struct {
Ref string
Value *Response
@ -187,6 +200,8 @@ func (value ResponseRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// RequestBodyRef represents either a RequestBody or a $ref to a RequestBody.
// When serializing and both fields are set, Ref is preferred over Value.
type RequestBodyRef struct {
Ref string
Value *RequestBody
@ -218,6 +233,8 @@ func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// SchemaRef represents either a Schema or a $ref to a Schema.
// When serializing and both fields are set, Ref is preferred over Value.
type SchemaRef struct {
Ref string
Value *Schema
@ -256,6 +273,8 @@ func (value SchemaRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// SecuritySchemeRef represents either a SecurityScheme or a $ref to a SecurityScheme.
// When serializing and both fields are set, Ref is preferred over Value.
type SecuritySchemeRef struct {
Ref string
Value *SecurityScheme

View file

@ -2,6 +2,7 @@ package openapi3
import (
"context"
"errors"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
@ -25,11 +26,13 @@ func (r RequestBodies) JSONLookup(token string) (interface{}, error) {
}
// RequestBody is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#requestBodyObject
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"`
Content Content `json:"content" yaml:"content"`
}
func NewRequestBody() *RequestBody {
@ -98,10 +101,8 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error {
}
func (value *RequestBody) Validate(ctx context.Context) error {
if v := value.Content; v != nil {
if err := v.Validate(ctx); err != nil {
return err
}
if value.Content == nil {
return errors.New("content of the request body is required")
}
return nil
return value.Content.Validate(ctx)
}

View file

@ -11,6 +11,7 @@ import (
)
// Responses is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responsesObject
type Responses map[string]*ResponseRef
var _ jsonpointer.JSONPointable = (*Responses)(nil)
@ -54,8 +55,10 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) {
}
// Response is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responseObject
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"`
@ -104,5 +107,16 @@ func (value *Response) Validate(ctx context.Context) error {
return err
}
}
for _, header := range value.Headers {
if err := header.Validate(ctx); err != nil {
return err
}
}
for _, link := range value.Links {
if err := link.Validate(ctx); err != nil {
return err
}
}
return nil
}

View file

@ -16,6 +16,15 @@ import (
"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
@ -93,6 +102,7 @@ func (s SchemaRefs) JSONLookup(token string) (interface{}, error) {
}
// Schema is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schemaObject
type Schema struct {
ExtensionProps
@ -109,20 +119,18 @@ type Schema struct {
Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
// Object-related, here for struct compactness
AdditionalPropertiesAllowed *bool `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"`
// Array-related, here for struct compactness
UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
// Number-related, here for struct compactness
ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"`
ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"`
// Properties
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"`
AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"`
AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"`
// Number
Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
@ -141,12 +149,13 @@ type Schema struct {
Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"`
// Object
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"`
MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
AdditionalProperties *SchemaRef `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"`
Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
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)
@ -298,73 +307,73 @@ func NewAllOfSchema(schemas ...*Schema) *Schema {
func NewBoolSchema() *Schema {
return &Schema{
Type: "boolean",
Type: TypeBoolean,
}
}
func NewFloat64Schema() *Schema {
return &Schema{
Type: "number",
Type: TypeNumber,
}
}
func NewIntegerSchema() *Schema {
return &Schema{
Type: "integer",
Type: TypeInteger,
}
}
func NewInt32Schema() *Schema {
return &Schema{
Type: "integer",
Type: TypeInteger,
Format: "int32",
}
}
func NewInt64Schema() *Schema {
return &Schema{
Type: "integer",
Type: TypeInteger,
Format: "int64",
}
}
func NewStringSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
}
}
func NewDateTimeSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
Format: "date-time",
}
}
func NewUUIDSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
Format: "uuid",
}
}
func NewBytesSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
Format: "byte",
}
}
func NewArraySchema() *Schema {
return &Schema{
Type: "array",
Type: TypeArray,
}
}
func NewObjectSchema() *Schema {
return &Schema{
Type: "object",
Properties: make(map[string]*SchemaRef),
Type: TypeObject,
Properties: make(Schemas),
}
}
@ -485,7 +494,7 @@ func (schema *Schema) WithProperty(name string, propertySchema *Schema) *Schema
func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema {
properties := schema.Properties
if properties == nil {
properties = make(map[string]*SchemaRef)
properties = make(Schemas)
schema.Properties = properties
}
properties[name] = ref
@ -493,7 +502,7 @@ func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema {
}
func (schema *Schema) WithProperties(properties map[string]*Schema) *Schema {
result := make(map[string]*SchemaRef, len(properties))
result := make(Schemas, len(properties))
for k, v := range properties {
result[k] = &SchemaRef{
Value: v,
@ -638,8 +647,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
schemaType := schema.Type
switch schemaType {
case "":
case "boolean":
case "number":
case TypeBoolean:
case TypeNumber:
if format := schema.Format; len(format) > 0 {
switch format {
case "float", "double":
@ -649,7 +658,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}
}
}
case "integer":
case TypeInteger:
if format := schema.Format; len(format) > 0 {
switch format {
case "int32", "int64":
@ -659,17 +668,21 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}
}
}
case "string":
case TypeString:
if format := schema.Format; len(format) > 0 {
switch format {
// Supported by OpenAPIv3.0.1:
// Supported by OpenAPIv3.0.3:
// https://spec.openapis.org/oas/v3.0.3
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":
// In JSON Draft-07 (not validated yet though):
// https://json-schema.org/draft-07/json-schema-release-notes.html#formats
case "iri", "iri-reference", "uri-template", "idn-email", "idn-hostname":
case "json-pointer", "relative-json-pointer", "regex", "time":
// In JSON Draft 2019-09 (not validated yet though):
// https://json-schema.org/draft/2019-09/release-notes.html#format-vocabulary
case "duration", "uuid":
// Defined in some other specification
case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference":
default:
// Try to check for custom defined formats
if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled {
@ -677,11 +690,16 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}
}
}
case "array":
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 "object":
case TypeObject:
default:
return fmt.Errorf("unsupported 'type' value %q", schemaType)
}
@ -716,6 +734,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}
}
if v := schema.ExternalDocs; v != nil {
if err = v.Validate(ctx); err != nil {
return fmt.Errorf("invalid external docs: %w", err)
}
}
return
}
@ -775,8 +799,6 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
}
switch value := value.(type) {
case nil:
return schema.visitJSONNull(settings)
case bool:
return schema.visitJSONBoolean(settings, value)
case float64:
@ -787,13 +809,22 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
return schema.visitJSONArray(settings, value)
case map[string]interface{}:
return schema.visitJSONObject(settings, value)
default:
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "type",
Reason: fmt.Sprintf("unhandled value of type %T", value),
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),
}
}
@ -820,11 +851,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
if v == nil {
return foundUnresolvedRef(ref.Ref)
}
var oldfailfast bool
oldfailfast, settings.failfast = settings.failfast, true
err := v.visitJSON(settings, value)
settings.failfast = oldfailfast
if err == nil {
if err := v.visitJSON(settings, value); err == nil {
if settings.failfast {
return errSchema
}
@ -837,33 +864,57 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
}
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")
}
discriminatorValString, okcheck := discriminatorVal.(string)
if !okcheck {
return errors.New("descriminator value is not a string")
}
if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; 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)
}
var oldfailfast bool
oldfailfast, settings.failfast = settings.failfast, true
err := v.visitJSON(settings, value)
settings.failfast = oldfailfast
if err == nil {
if schema.Discriminator != nil {
pn := schema.Discriminator.PropertyName
if valuemap, okcheck := value.(map[string]interface{}); okcheck {
if discriminatorVal, okcheck := valuemap[pn]; okcheck == true {
mapref, okcheck := schema.Discriminator.Mapping[discriminatorVal.(string)]
if okcheck && mapref == item.Ref {
ok++
}
}
}
} else {
ok++
}
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
}
@ -874,7 +925,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
}
if ok > 1 {
e.Origin = ErrOneOfConflict
} else if len(validationErrors) == 1 {
e.Origin = validationErrors[0]
}
return e
}
}
@ -886,11 +940,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
if v == nil {
return foundUnresolvedRef(item.Ref)
}
var oldfailfast bool
oldfailfast, settings.failfast = settings.failfast, true
err := v.visitJSON(settings, value)
settings.failfast = oldfailfast
if err == nil {
if err := v.visitJSON(settings, value); err == nil {
ok = true
break
}
@ -912,11 +962,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
if v == nil {
return foundUnresolvedRef(item.Ref)
}
var oldfailfast bool
oldfailfast, settings.failfast = settings.failfast, false
err := v.visitJSON(settings, value)
settings.failfast = oldfailfast
if err != nil {
if err := v.visitJSON(settings, value); err != nil {
if settings.failfast {
return errSchema
}
@ -952,8 +998,8 @@ func (schema *Schema) VisitJSONBoolean(value bool) error {
}
func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) {
if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" {
return schema.expectedType(settings, "boolean")
if schemaType := schema.Type; schemaType != "" && schemaType != TypeBoolean {
return schema.expectedType(settings, TypeBoolean)
}
return
}
@ -982,7 +1028,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
}
me = append(me, err)
}
} else if schemaType != "" && schemaType != "number" {
} else if schemaType != "" && schemaType != TypeNumber {
return schema.expectedType(settings, "number, integer")
}
@ -1046,7 +1092,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
Value: value,
Schema: schema,
SchemaField: "maximum",
Reason: fmt.Sprintf("number must be most %g", *v),
Reason: fmt.Sprintf("number must be at most %g", *v),
}
if !settings.multiError {
return err
@ -1087,8 +1133,8 @@ func (schema *Schema) VisitJSONString(value string) error {
}
func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error {
if schemaType := schema.Type; schemaType != "" && schemaType != "string" {
return schema.expectedType(settings, "string")
if schemaType := schema.Type; schemaType != "" && schemaType != TypeString {
return schema.expectedType(settings, TypeString)
}
var me MultiError
@ -1139,15 +1185,9 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
}
// "pattern"
if pattern := schema.Pattern; pattern != "" && schema.compiledPattern == nil {
if schema.Pattern != "" && schema.compiledPattern == nil {
var err error
if schema.compiledPattern, err = regexp.Compile(pattern); err != nil {
err = &SchemaError{
Value: value,
Schema: schema,
SchemaField: "pattern",
Reason: fmt.Sprintf("cannot compile pattern %q: %v", pattern, err),
}
if err = schema.compilePattern(); err != nil {
if !settings.multiError {
return err
}
@ -1159,7 +1199,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
Value: value,
Schema: schema,
SchemaField: "pattern",
Reason: fmt.Sprintf("string doesn't match the regular expression %q", schema.Pattern),
Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern),
}
if !settings.multiError {
return err
@ -1174,7 +1214,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
switch {
case f.regexp != nil && f.callback == nil:
if cp := f.regexp; !cp.MatchString(value) {
formatErr = fmt.Sprintf("string doesn't match the format %q (regular expression %q)", format, cp.String())
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 {
@ -1212,8 +1252,8 @@ func (schema *Schema) VisitJSONArray(value []interface{}) error {
}
func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error {
if schemaType := schema.Type; schemaType != "" && schemaType != "array" {
return schema.expectedType(settings, "array")
if schemaType := schema.Type; schemaType != "" && schemaType != TypeArray {
return schema.expectedType(settings, TypeArray)
}
var me MultiError
@ -1308,8 +1348,8 @@ func (schema *Schema) VisitJSONObject(value map[string]interface{}) error {
}
func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error {
if schemaType := schema.Type; schemaType != "" && schemaType != "object" {
return schema.expectedType(settings, "object")
if schemaType := schema.Type; schemaType != "" && schemaType != TypeObject {
return schema.expectedType(settings, TypeObject)
}
var me MultiError
@ -1383,7 +1423,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
}
}
allowed := schema.AdditionalPropertiesAllowed
if additionalProperties != nil || allowed == nil || (allowed != nil && *allowed) {
if additionalProperties != nil || allowed == nil || *allowed {
if additionalProperties != nil {
if err := additionalProperties.visitJSON(settings, v); err != nil {
if settings.failfast {
@ -1461,6 +1501,17 @@ func (schema *Schema) expectedType(settings *schemaValidationSettings, typ strin
}
}
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
@ -1470,6 +1521,8 @@ type SchemaError struct {
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)
@ -1545,6 +1598,10 @@ func (err *SchemaError) Error() string {
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)

View file

@ -4,11 +4,12 @@ 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}$`
FormatOfStringForUUIDOfRFC4122 = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$`
)
//FormatCallback custom check on exotic formats
@ -37,24 +38,23 @@ func DefineStringFormatCallback(name string, callback FormatCallback) {
SchemaStringFormats[name] = Format{callback: callback}
}
func validateIP(ip string) (*net.IP, error) {
func validateIP(ip string) error {
parsed := net.ParseIP(ip)
if parsed == nil {
return nil, &SchemaError{
return &SchemaError{
Value: ip,
Reason: "Not an IP address",
}
}
return &parsed, nil
return nil
}
func validateIPv4(ip string) error {
parsed, err := validateIP(ip)
if err != nil {
if err := validateIP(ip); err != nil {
return err
}
if parsed.To4() == nil {
if !(strings.Count(ip, ":") < 2) {
return &SchemaError{
Value: ip,
Reason: "Not an IPv4 address (it's IPv6)",
@ -62,13 +62,13 @@ func validateIPv4(ip string) error {
}
return nil
}
func validateIPv6(ip string) error {
parsed, err := validateIP(ip)
if err != nil {
if err := validateIP(ip); err != nil {
return err
}
if parsed.To4() != nil {
if !(strings.Count(ip, ":") >= 2) {
return &SchemaError{
Value: ip,
Reason: "Not an IPv6 address (it's IPv4)",
@ -90,7 +90,7 @@ func init() {
DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`)
// date-time
DefineStringFormat("date-time", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`)
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})?$`)
}

View file

@ -24,6 +24,8 @@ func (value SecurityRequirements) Validate(ctx context.Context) error {
return nil
}
// SecurityRequirement is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securityRequirementObject
type SecurityRequirement map[string][]string
func NewSecurityRequirement() SecurityRequirement {

View file

@ -25,6 +25,8 @@ func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) {
var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil)
// SecurityScheme is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securitySchemeObject
type SecurityScheme struct {
ExtensionProps
@ -166,8 +168,11 @@ func (value *SecurityScheme) Validate(ctx context.Context) error {
return nil
}
// OAuthFlows is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowsObject
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"`
@ -207,8 +212,11 @@ func (flows *OAuthFlows) Validate(ctx context.Context) error {
return errors.New("no OAuth flow is defined")
}
// OAuthFlow is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowObject
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"`

View file

@ -11,7 +11,7 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// Servers is specified by OpenAPI/Swagger standard version 3.0.
// Servers is specified by OpenAPI/Swagger standard version 3.
type Servers []*Server
// Validate ensures servers are per the OpenAPIv3 specification.
@ -38,9 +38,11 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string)
return nil, nil, ""
}
// Server is specified by OpenAPI/Swagger standard version 3.0.
// Server is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject
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"`
@ -147,9 +149,11 @@ func (value *Server) Validate(ctx context.Context) (err error) {
return
}
// ServerVariable is specified by OpenAPI/Swagger standard version 3.0.
// ServerVariable is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object
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"`

View file

@ -1,6 +1,11 @@
package openapi3
import "github.com/getkin/kin-openapi/jsoninfo"
import (
"context"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
)
// Tags is specified by OpenAPI/Swagger 3.0 standard.
type Tags []*Tag
@ -14,9 +19,20 @@ func (tags Tags) Get(name string) *Tag {
return nil
}
func (tags Tags) Validate(ctx context.Context) error {
for _, v := range tags {
if err := v.Validate(ctx); err != nil {
return err
}
}
return nil
}
// Tag is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject
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"`
@ -29,3 +45,12 @@ func (t *Tag) MarshalJSON() ([]byte, error) {
func (t *Tag) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, t)
}
func (t *Tag) Validate(ctx context.Context) error {
if v := t.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("invalid external docs: %w", err)
}
}
return nil
}

View file

@ -0,0 +1,2 @@
type: string
example: bar

View file

@ -0,0 +1,4 @@
type: object
properties:
bar:
$ref: ../openapi.yml#/components/schemas/Bar

View file

@ -0,0 +1,4 @@
type: object
properties:
foo:
$ref: ../../openapi.yml#/components/schemas/Foo

View file

@ -0,0 +1,15 @@
openapi: "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

View file

@ -0,0 +1,11 @@
get:
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
foo2:
$ref: ../openapi.yml#/components/schemas/Foo2

31
vendor/github.com/getkin/kin-openapi/openapi3/xml.go generated vendored Normal file
View file

@ -0,0 +1,31 @@
package openapi3
import (
"context"
"github.com/getkin/kin-openapi/jsoninfo"
)
// XML is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject
type XML struct {
ExtensionProps
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
Attribute bool `json:"attribute,omitempty" yaml:"attribute,omitempty"`
Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"`
}
func (value *XML) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(value)
}
func (value *XML) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
}
func (value *XML) Validate(ctx context.Context) error {
return nil // TODO
}

View file

@ -0,0 +1,29 @@
package 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,
}
}

View file

@ -0,0 +1,82 @@
package 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"
}

View file

@ -0,0 +1,25 @@
package openapi3filter
import (
"reflect"
"strings"
)
func parseMediaType(contentType string) string {
i := strings.IndexByte(contentType, ';')
if i < 0 {
return contentType
}
return contentType[:i]
}
func isNilValue(value interface{}) bool {
if value == nil {
return true
}
switch reflect.TypeOf(value).Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return reflect.ValueOf(value).IsNil()
}
return false
}

View file

@ -0,0 +1,273 @@
package openapi3filter
import (
"bytes"
"io"
"io/ioutil"
"log"
"net/http"
"github.com/getkin/kin-openapi/routers"
)
// Validator provides HTTP request and response validation middleware.
type Validator struct {
router routers.Router
errFunc ErrFunc
logFunc LogFunc
strict bool
}
// ErrFunc handles errors that may occur during validation.
type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error)
// LogFunc handles log messages that may occur during validation.
type LogFunc func(message string, err error)
// ErrCode is used for classification of different types of errors that may
// occur during validation. These may be used to write an appropriate response
// in ErrFunc.
type ErrCode int
const (
// ErrCodeOK indicates no error. It is also the default value.
ErrCodeOK = 0
// ErrCodeCannotFindRoute happens when the validator fails to resolve the
// request to a defined OpenAPI route.
ErrCodeCannotFindRoute = iota
// ErrCodeRequestInvalid happens when the inbound request does not conform
// to the OpenAPI 3 specification.
ErrCodeRequestInvalid = iota
// ErrCodeResponseInvalid happens when the wrapped handler response does
// not conform to the OpenAPI 3 specification.
ErrCodeResponseInvalid = iota
)
func (e ErrCode) responseText() string {
switch e {
case ErrCodeOK:
return "OK"
case ErrCodeCannotFindRoute:
return "not found"
case ErrCodeRequestInvalid:
return "bad request"
default:
return "server error"
}
}
// NewValidator returns a new response validation middlware, using the given
// routes from an OpenAPI 3 specification.
func NewValidator(router routers.Router, options ...ValidatorOption) *Validator {
v := &Validator{
router: router,
errFunc: func(w http.ResponseWriter, status int, code ErrCode, _ error) {
http.Error(w, code.responseText(), status)
},
logFunc: func(message string, err error) {
log.Printf("%s: %v", message, err)
},
}
for i := range options {
options[i](v)
}
return v
}
// ValidatorOption defines an option that may be specified when creating a
// Validator.
type ValidatorOption func(*Validator)
// OnErr provides a callback that handles writing an HTTP response on a
// validation error. This allows customization of error responses without
// prescribing a particular form. This callback is only called on response
// validator errors in Strict mode.
func OnErr(f ErrFunc) ValidatorOption {
return func(v *Validator) {
v.errFunc = f
}
}
// OnLog provides a callback that handles logging in the Validator. This allows
// the validator to integrate with a services' existing logging system without
// prescribing a particular one.
func OnLog(f LogFunc) ValidatorOption {
return func(v *Validator) {
v.logFunc = f
}
}
// Strict, if set, causes an internal server error to be sent if the wrapped
// handler response fails response validation. If not set, the response is sent
// and the error is only logged.
func Strict(strict bool) ValidatorOption {
return func(v *Validator) {
v.strict = strict
}
}
// Middleware returns an http.Handler which wraps the given handler with
// request and response validation.
func (v *Validator) Middleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route, pathParams, err := v.router.FindRoute(r)
if err != nil {
v.logFunc("validation error: failed to find route for "+r.URL.String(), err)
v.errFunc(w, http.StatusNotFound, ErrCodeCannotFindRoute, err)
return
}
requestValidationInput := &RequestValidationInput{
Request: r,
PathParams: pathParams,
Route: route,
}
if err = ValidateRequest(r.Context(), requestValidationInput); err != nil {
v.logFunc("invalid request", err)
v.errFunc(w, http.StatusBadRequest, ErrCodeRequestInvalid, err)
return
}
var wr responseWrapper
if v.strict {
wr = &strictResponseWrapper{w: w}
} else {
wr = newWarnResponseWrapper(w)
}
h.ServeHTTP(wr, r)
if err = ValidateResponse(r.Context(), &ResponseValidationInput{
RequestValidationInput: requestValidationInput,
Status: wr.statusCode(),
Header: wr.Header(),
Body: ioutil.NopCloser(bytes.NewBuffer(wr.bodyContents())),
}); err != nil {
v.logFunc("invalid response", err)
if v.strict {
v.errFunc(w, http.StatusInternalServerError, ErrCodeResponseInvalid, err)
}
return
}
if err = wr.flushBodyContents(); err != nil {
v.logFunc("failed to write response", err)
}
})
}
type responseWrapper interface {
http.ResponseWriter
// flushBodyContents writes the buffered response to the client, if it has
// not yet been written.
flushBodyContents() error
// statusCode returns the response status code, 0 if not set yet.
statusCode() int
// bodyContents returns the buffered
bodyContents() []byte
}
type warnResponseWrapper struct {
w http.ResponseWriter
headerWritten bool
status int
body bytes.Buffer
tee io.Writer
}
func newWarnResponseWrapper(w http.ResponseWriter) *warnResponseWrapper {
wr := &warnResponseWrapper{
w: w,
}
wr.tee = io.MultiWriter(w, &wr.body)
return wr
}
// Write implements http.ResponseWriter.
func (wr *warnResponseWrapper) Write(b []byte) (int, error) {
if !wr.headerWritten {
wr.WriteHeader(http.StatusOK)
}
return wr.tee.Write(b)
}
// WriteHeader implements http.ResponseWriter.
func (wr *warnResponseWrapper) WriteHeader(status int) {
if !wr.headerWritten {
// If the header hasn't been written, record the status for response
// validation.
wr.status = status
wr.headerWritten = true
}
wr.w.WriteHeader(wr.status)
}
// Header implements http.ResponseWriter.
func (wr *warnResponseWrapper) Header() http.Header {
return wr.w.Header()
}
// Flush implements the optional http.Flusher interface.
func (wr *warnResponseWrapper) Flush() {
// If the wrapped http.ResponseWriter implements optional http.Flusher,
// pass through.
if fl, ok := wr.w.(http.Flusher); ok {
fl.Flush()
}
}
func (wr *warnResponseWrapper) flushBodyContents() error {
return nil
}
func (wr *warnResponseWrapper) statusCode() int {
return wr.status
}
func (wr *warnResponseWrapper) bodyContents() []byte {
return wr.body.Bytes()
}
type strictResponseWrapper struct {
w http.ResponseWriter
headerWritten bool
status int
body bytes.Buffer
}
// Write implements http.ResponseWriter.
func (wr *strictResponseWrapper) Write(b []byte) (int, error) {
if !wr.headerWritten {
wr.WriteHeader(http.StatusOK)
}
return wr.body.Write(b)
}
// WriteHeader implements http.ResponseWriter.
func (wr *strictResponseWrapper) WriteHeader(status int) {
if !wr.headerWritten {
wr.status = status
wr.headerWritten = true
}
}
// Header implements http.ResponseWriter.
func (wr *strictResponseWrapper) Header() http.Header {
return wr.w.Header()
}
func (wr *strictResponseWrapper) flushBodyContents() error {
wr.w.WriteHeader(wr.status)
_, err := wr.w.Write(wr.body.Bytes())
return err
}
func (wr *strictResponseWrapper) statusCode() int {
return wr.status
}
func (wr *strictResponseWrapper) bodyContents() []byte {
return wr.body.Bytes()
}

View file

@ -0,0 +1,24 @@
package 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
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,332 @@
package 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")
// ErrInvalidEmptyValue is returned when a value of a parameter or request body is empty while it's not allowed.
var ErrInvalidEmptyValue = errors.New("empty value is not allowed")
// 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
// 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)
}
}
// 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)
}
}
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 ErrInvalidEmptyValue cause when a value of a required parameter is not defined.
// The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema.
func ValidateParameter(ctx context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error {
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 found bool
var schema *openapi3.Schema
// Validation will ensure that we either have content or schema.
if parameter.Content != nil {
if value, schema, found, err = decodeContentParameter(parameter, input); err != nil {
return &RequestError{Input: input, Parameter: parameter, Err: err}
}
} else {
if value, found, err = decodeStyledParameter(parameter, input); err != nil {
return &RequestError{Input: input, Parameter: parameter, Err: err}
}
schema = parameter.Schema.Value
}
// Validate a parameter's value and presence.
if parameter.Required && !found {
return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired}
}
if isNilValue(value) {
if !parameter.AllowEmptyValue && found {
return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidEmptyValue.Error(), Err: ErrInvalidEmptyValue}
}
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
}

View file

@ -0,0 +1,38 @@
package 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
}

View file

@ -0,0 +1,138 @@
// 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
}

View file

@ -0,0 +1,42 @@
package 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
}

View file

@ -0,0 +1,85 @@
package 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" yaml:"id,omitempty"`
// The HTTP status code applicable to this problem.
Status int `json:"status,omitempty" yaml:"status,omitempty"`
// An application-specific error code, expressed as a string value.
Code string `json:"code,omitempty" yaml:"code,omitempty"`
// A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.
Title string `json:"title,omitempty" yaml:"title,omitempty"`
// A human-readable explanation specific to this occurrence of the problem.
Detail string `json:"detail,omitempty" yaml:"detail,omitempty"`
// An object containing references to the source of the error
Source *ValidationErrorSource `json:"source,omitempty" yaml:"source,omitempty"`
}
// ValidationErrorSource struct
type ValidationErrorSource struct {
// A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].
Pointer string `json:"pointer,omitempty" yaml:"pointer,omitempty"`
// A string indicating which query parameter caused the error.
Parameter string `json:"parameter,omitempty" yaml:"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
}

View file

@ -0,0 +1,185 @@
package 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 e.Err == ErrInvalidEmptyValue {
cErr = convertErrInvalidEmptyValue(e)
} else if innerErr, ok := e.Err.(*ParseError); ok {
cErr = convertParseError(e, innerErr)
} else if innerErr, ok := e.Err.(*openapi3.SchemaError); ok {
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 convertErrInvalidEmptyValue(e *RequestError) *ValidationError {
if e.Err == ErrInvalidEmptyValue && e.Parameter != nil {
return &ValidationError{
Status: http.StatusBadRequest,
Title: fmt.Sprintf("parameter %q in %s is not allowed to be empty", 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, "/")
}

View file

@ -0,0 +1,103 @@
package 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
}

View file

@ -0,0 +1,85 @@
package 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)
}

View file

@ -0,0 +1,328 @@
// 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
}

View file

@ -0,0 +1,167 @@
// 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
}

42
vendor/github.com/getkin/kin-openapi/routers/types.go generated vendored Normal file
View file

@ -0,0 +1,42 @@
package routers
import (
"net/http"
"github.com/getkin/kin-openapi/openapi3"
)
// Router helps link http.Request.s and an OpenAPIv3 spec
type Router interface {
// FindRoute matches an HTTP request with the operation it resolves to.
// Hosts are matched from the OpenAPIv3 servers key.
//
// If you experience ErrPathNotFound and have localhost hosts specified as your servers,
// turning these server URLs as relative (leaving only the path) should resolve this.
//
// See openapi3filter for example uses with request and response validation.
FindRoute(req *http.Request) (route *Route, pathParams map[string]string, err error)
}
// Route describes the operation an http.Request can match
type Route struct {
Spec *openapi3.T
Server *openapi3.Server
Path string
PathItem *openapi3.PathItem
Method string
Operation *openapi3.Operation
}
// ErrPathNotFound is returned when no route match is found
var ErrPathNotFound error = &RouteError{"no matching operation was found"}
// ErrMethodNotAllowed is returned when no method of the matched route matches
var ErrMethodNotAllowed error = &RouteError{"method not allowed"}
// RouteError describes Router errors
type RouteError struct {
Reason string
}
func (e *RouteError) Error() string { return e.Reason }

2
vendor/github.com/go-openapi/swag/.gitattributes generated vendored Normal file
View file

@ -0,0 +1,2 @@
# gofmt always uses LF, whereas Git uses CRLF on Windows.
*.go text eol=lf

View file

@ -20,3 +20,31 @@ linters:
- lll
- gochecknoinits
- gochecknoglobals
- nlreturn
- testpackage
- wrapcheck
- gomnd
- exhaustive
- exhaustivestruct
- goerr113
- wsl
- whitespace
- gofumpt
- godot
- nestif
- godox
- funlen
- gci
- gocognit
- paralleltest
- thelper
- ifshort
- gomoddirectives
- cyclop
- forcetypeassert
- ireturn
- tagliatelle
- varnamelen
- goimports
- tenv
- golint

View file

@ -1,15 +0,0 @@
after_success:
- bash <(curl -s https://codecov.io/bash)
go:
- 1.11.x
- 1.12.x
install:
- GO111MODULE=off go get -u gotest.tools/gotestsum
env:
- GO111MODULE=on
language: go
notifications:
slack:
secure: QUWvCkBBK09GF7YtEvHHVt70JOkdlNBG0nIKu/5qc4/nW5HP8I2w0SEf/XR2je0eED1Qe3L/AfMCWwrEj+IUZc3l4v+ju8X8R3Lomhme0Eb0jd1MTMCuPcBT47YCj0M7RON7vXtbFfm1hFJ/jLe5+9FXz0hpXsR24PJc5ZIi/ogNwkaPqG4BmndzecpSh0vc2FJPZUD9LT0I09REY/vXR0oQAalLkW0asGD5taHZTUZq/kBpsNxaAFrLM23i4mUcf33M5fjLpvx5LRICrX/57XpBrDh2TooBU6Qj3CgoY0uPRYUmSNxbVx1czNzl2JtEpb5yjoxfVPQeg0BvQM00G8LJINISR+ohrjhkZmAqchDupAX+yFrxTtORa78CtnIL6z/aTNlgwwVD8kvL/1pFA/JWYmKDmz93mV/+6wubGzNSQCstzjkFA4/iZEKewKUoRIAi/fxyscP6L/rCpmY/4llZZvrnyTqVbt6URWpopUpH4rwYqreXAtJxJsfBJIeSmUIiDIOMGkCTvyTEW3fWGmGoqWtSHLoaWDyAIGb7azb+KvfpWtEcoPFWfSWU+LGee0A/YsUhBl7ADB9A0CJEuR8q4BPpKpfLwPKSiKSAXL7zDkyjExyhtgqbSl2jS+rKIHOZNL8JkCcTP2MKMVd563C5rC5FMKqu3S9m2b6380E=
script:
- gotestsum -f short-verbose -- -race -coverprofile=coverage.txt -covermode=atomic ./...

View file

@ -2,7 +2,6 @@
[![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/swag/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/go-openapi/swag?status.svg)](http://godoc.org/github.com/go-openapi/swag)
[![GolangCI](https://golangci.com/badges/github.com/go-openapi/swag.svg)](https://golangci.com)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-openapi/swag)](https://goreportcard.com/report/github.com/go-openapi/swag)
Contains a bunch of helper functions for go-openapi and go-swagger projects.

View file

@ -88,7 +88,7 @@ func ConvertFloat64(str string) (float64, error) {
return strconv.ParseFloat(str, 64)
}
// ConvertInt8 turn a string into int8 boolean
// ConvertInt8 turn a string into an int8
func ConvertInt8(str string) (int8, error) {
i, err := strconv.ParseInt(str, 10, 8)
if err != nil {
@ -97,7 +97,7 @@ func ConvertInt8(str string) (int8, error) {
return int8(i), nil
}
// ConvertInt16 turn a string into a int16
// ConvertInt16 turn a string into an int16
func ConvertInt16(str string) (int16, error) {
i, err := strconv.ParseInt(str, 10, 16)
if err != nil {
@ -106,7 +106,7 @@ func ConvertInt16(str string) (int16, error) {
return int16(i), nil
}
// ConvertInt32 turn a string into a int32
// ConvertInt32 turn a string into an int32
func ConvertInt32(str string) (int32, error) {
i, err := strconv.ParseInt(str, 10, 32)
if err != nil {
@ -115,12 +115,12 @@ func ConvertInt32(str string) (int32, error) {
return int32(i), nil
}
// ConvertInt64 turn a string into a int64
// ConvertInt64 turn a string into an int64
func ConvertInt64(str string) (int64, error) {
return strconv.ParseInt(str, 10, 64)
}
// ConvertUint8 turn a string into a uint8
// ConvertUint8 turn a string into an uint8
func ConvertUint8(str string) (uint8, error) {
i, err := strconv.ParseUint(str, 10, 8)
if err != nil {
@ -129,7 +129,7 @@ func ConvertUint8(str string) (uint8, error) {
return uint8(i), nil
}
// ConvertUint16 turn a string into a uint16
// ConvertUint16 turn a string into an uint16
func ConvertUint16(str string) (uint16, error) {
i, err := strconv.ParseUint(str, 10, 16)
if err != nil {
@ -138,7 +138,7 @@ func ConvertUint16(str string) (uint16, error) {
return uint16(i), nil
}
// ConvertUint32 turn a string into a uint32
// ConvertUint32 turn a string into an uint32
func ConvertUint32(str string) (uint32, error) {
i, err := strconv.ParseUint(str, 10, 32)
if err != nil {
@ -147,7 +147,7 @@ func ConvertUint32(str string) (uint32, error) {
return uint32(i), nil
}
// ConvertUint64 turn a string into a uint64
// ConvertUint64 turn a string into an uint64
func ConvertUint64(str string) (uint64, error) {
return strconv.ParseUint(str, 10, 64)
}

View file

@ -181,12 +181,12 @@ func IntValueMap(src map[string]*int) map[string]int {
return dst
}
// Int32 returns a pointer to of the int64 value passed in.
// Int32 returns a pointer to of the int32 value passed in.
func Int32(v int32) *int32 {
return &v
}
// Int32Value returns the value of the int64 pointer passed in or
// Int32Value returns the value of the int32 pointer passed in or
// 0 if the pointer is nil.
func Int32Value(v *int32) int32 {
if v != nil {
@ -195,7 +195,7 @@ func Int32Value(v *int32) int32 {
return 0
}
// Int32Slice converts a slice of int64 values into a slice of
// Int32Slice converts a slice of int32 values into a slice of
// int32 pointers
func Int32Slice(src []int32) []*int32 {
dst := make([]*int32, len(src))
@ -299,13 +299,80 @@ func Int64ValueMap(src map[string]*int64) map[string]int64 {
return dst
}
// Uint returns a pouinter to of the uint value passed in.
// Uint16 returns a pointer to of the uint16 value passed in.
func Uint16(v uint16) *uint16 {
return &v
}
// Uint16Value returns the value of the uint16 pointer passed in or
// 0 if the pointer is nil.
func Uint16Value(v *uint16) uint16 {
if v != nil {
return *v
}
return 0
}
// Uint16Slice converts a slice of uint16 values into a slice of
// uint16 pointers
func Uint16Slice(src []uint16) []*uint16 {
dst := make([]*uint16, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Uint16ValueSlice converts a slice of uint16 pointers into a slice of
// uint16 values
func Uint16ValueSlice(src []*uint16) []uint16 {
dst := make([]uint16, len(src))
for i := 0; i < len(src); i++ {
if src[i] != nil {
dst[i] = *(src[i])
}
}
return dst
}
// Uint16Map converts a string map of uint16 values into a string
// map of uint16 pointers
func Uint16Map(src map[string]uint16) map[string]*uint16 {
dst := make(map[string]*uint16)
for k, val := range src {
v := val
dst[k] = &v
}
return dst
}
// Uint16ValueMap converts a string map of uint16 pointers into a string
// map of uint16 values
func Uint16ValueMap(src map[string]*uint16) map[string]uint16 {
dst := make(map[string]uint16)
for k, val := range src {
if val != nil {
dst[k] = *val
}
}
return dst
}
// Uint returns a pointer to of the uint value passed in.
func Uint(v uint) *uint {
return &v
}
// UintValue returns the value of the uint pouinter passed in or
// 0 if the pouinter is nil.
// UintValue returns the value of the uint pointer passed in or
// 0 if the pointer is nil.
func UintValue(v *uint) uint {
if v != nil {
return *v
@ -313,8 +380,8 @@ func UintValue(v *uint) uint {
return 0
}
// UintSlice converts a slice of uint values uinto a slice of
// uint pouinters
// UintSlice converts a slice of uint values into a slice of
// uint pointers
func UintSlice(src []uint) []*uint {
dst := make([]*uint, len(src))
for i := 0; i < len(src); i++ {
@ -323,7 +390,7 @@ func UintSlice(src []uint) []*uint {
return dst
}
// UintValueSlice converts a slice of uint pouinters uinto a slice of
// UintValueSlice converts a slice of uint pointers into a slice of
// uint values
func UintValueSlice(src []*uint) []uint {
dst := make([]uint, len(src))
@ -335,8 +402,8 @@ func UintValueSlice(src []*uint) []uint {
return dst
}
// UintMap converts a string map of uint values uinto a string
// map of uint pouinters
// UintMap converts a string map of uint values into a string
// map of uint pointers
func UintMap(src map[string]uint) map[string]*uint {
dst := make(map[string]*uint)
for k, val := range src {
@ -346,7 +413,7 @@ func UintMap(src map[string]uint) map[string]*uint {
return dst
}
// UintValueMap converts a string map of uint pouinters uinto a string
// UintValueMap converts a string map of uint pointers into a string
// map of uint values
func UintValueMap(src map[string]*uint) map[string]uint {
dst := make(map[string]uint)
@ -358,13 +425,13 @@ func UintValueMap(src map[string]*uint) map[string]uint {
return dst
}
// Uint32 returns a pouinter to of the uint64 value passed in.
// Uint32 returns a pointer to of the uint32 value passed in.
func Uint32(v uint32) *uint32 {
return &v
}
// Uint32Value returns the value of the uint64 pouinter passed in or
// 0 if the pouinter is nil.
// Uint32Value returns the value of the uint32 pointer passed in or
// 0 if the pointer is nil.
func Uint32Value(v *uint32) uint32 {
if v != nil {
return *v
@ -372,8 +439,8 @@ func Uint32Value(v *uint32) uint32 {
return 0
}
// Uint32Slice converts a slice of uint64 values uinto a slice of
// uint32 pouinters
// Uint32Slice converts a slice of uint32 values into a slice of
// uint32 pointers
func Uint32Slice(src []uint32) []*uint32 {
dst := make([]*uint32, len(src))
for i := 0; i < len(src); i++ {
@ -382,7 +449,7 @@ func Uint32Slice(src []uint32) []*uint32 {
return dst
}
// Uint32ValueSlice converts a slice of uint32 pouinters uinto a slice of
// Uint32ValueSlice converts a slice of uint32 pointers into a slice of
// uint32 values
func Uint32ValueSlice(src []*uint32) []uint32 {
dst := make([]uint32, len(src))
@ -394,8 +461,8 @@ func Uint32ValueSlice(src []*uint32) []uint32 {
return dst
}
// Uint32Map converts a string map of uint32 values uinto a string
// map of uint32 pouinters
// Uint32Map converts a string map of uint32 values into a string
// map of uint32 pointers
func Uint32Map(src map[string]uint32) map[string]*uint32 {
dst := make(map[string]*uint32)
for k, val := range src {
@ -405,7 +472,7 @@ func Uint32Map(src map[string]uint32) map[string]*uint32 {
return dst
}
// Uint32ValueMap converts a string map of uint32 pouinters uinto a string
// Uint32ValueMap converts a string map of uint32 pointers into a string
// map of uint32 values
func Uint32ValueMap(src map[string]*uint32) map[string]uint32 {
dst := make(map[string]uint32)
@ -417,13 +484,13 @@ func Uint32ValueMap(src map[string]*uint32) map[string]uint32 {
return dst
}
// Uint64 returns a pouinter to of the uint64 value passed in.
// Uint64 returns a pointer to of the uint64 value passed in.
func Uint64(v uint64) *uint64 {
return &v
}
// Uint64Value returns the value of the uint64 pouinter passed in or
// 0 if the pouinter is nil.
// Uint64Value returns the value of the uint64 pointer passed in or
// 0 if the pointer is nil.
func Uint64Value(v *uint64) uint64 {
if v != nil {
return *v
@ -431,8 +498,8 @@ func Uint64Value(v *uint64) uint64 {
return 0
}
// Uint64Slice converts a slice of uint64 values uinto a slice of
// uint64 pouinters
// Uint64Slice converts a slice of uint64 values into a slice of
// uint64 pointers
func Uint64Slice(src []uint64) []*uint64 {
dst := make([]*uint64, len(src))
for i := 0; i < len(src); i++ {
@ -441,7 +508,7 @@ func Uint64Slice(src []uint64) []*uint64 {
return dst
}
// Uint64ValueSlice converts a slice of uint64 pouinters uinto a slice of
// Uint64ValueSlice converts a slice of uint64 pointers into a slice of
// uint64 values
func Uint64ValueSlice(src []*uint64) []uint64 {
dst := make([]uint64, len(src))
@ -453,8 +520,8 @@ func Uint64ValueSlice(src []*uint64) []uint64 {
return dst
}
// Uint64Map converts a string map of uint64 values uinto a string
// map of uint64 pouinters
// Uint64Map converts a string map of uint64 values into a string
// map of uint64 pointers
func Uint64Map(src map[string]uint64) map[string]*uint64 {
dst := make(map[string]*uint64)
for k, val := range src {
@ -464,7 +531,7 @@ func Uint64Map(src map[string]uint64) map[string]*uint64 {
return dst
}
// Uint64ValueMap converts a string map of uint64 pouinters uinto a string
// Uint64ValueMap converts a string map of uint64 pointers into a string
// map of uint64 values
func Uint64ValueMap(src map[string]*uint64) map[string]uint64 {
dst := make(map[string]uint64)
@ -476,6 +543,74 @@ func Uint64ValueMap(src map[string]*uint64) map[string]uint64 {
return dst
}
// Float32 returns a pointer to of the float32 value passed in.
func Float32(v float32) *float32 {
return &v
}
// Float32Value returns the value of the float32 pointer passed in or
// 0 if the pointer is nil.
func Float32Value(v *float32) float32 {
if v != nil {
return *v
}
return 0
}
// Float32Slice converts a slice of float32 values into a slice of
// float32 pointers
func Float32Slice(src []float32) []*float32 {
dst := make([]*float32, len(src))
for i := 0; i < len(src); i++ {
dst[i] = &(src[i])
}
return dst
}
// Float32ValueSlice converts a slice of float32 pointers into a slice of
// float32 values
func Float32ValueSlice(src []*float32) []float32 {
dst := make([]float32, len(src))
for i := 0; i < len(src); i++ {
if src[i] != nil {
dst[i] = *(src[i])
}
}
return dst
}
// Float32Map converts a string map of float32 values into a string
// map of float32 pointers
func Float32Map(src map[string]float32) map[string]*float32 {
dst := make(map[string]*float32)
for k, val := range src {
v := val
dst[k] = &v
}
return dst
}
// Float32ValueMap converts a string map of float32 pointers into a string
// map of float32 values
func Float32ValueMap(src map[string]*float32) map[string]float32 {
dst := make(map[string]float32)
for k, val := range src {
if val != nil {
dst[k] = *val
}
}
return dst
}
// Float64 returns a pointer to of the float64 value passed in.
func Float64(v float64) *float64 {
return &v

33
vendor/github.com/go-openapi/swag/file.go generated vendored Normal file
View file

@ -0,0 +1,33 @@
// Copyright 2015 go-swagger maintainers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package swag
import "mime/multipart"
// File represents an uploaded file.
type File struct {
Data multipart.File
Header *multipart.FileHeader
}
// Read bytes from the file
func (f *File) Read(p []byte) (n int, err error) {
return f.Data.Read(p)
}
// Close the file
func (f *File) Close() error {
return f.Data.Close()
}

View file

@ -2,13 +2,17 @@ module github.com/go-openapi/swag
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63
github.com/stretchr/testify v1.3.0
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.2
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.7.6
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/stretchr/testify v1.6.1
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
)
replace github.com/golang/lint => golang.org/x/lint v0.0.0-20190409202823-959b441ac422
replace sourcegraph.com/sourcegraph/go-diff => github.com/sourcegraph/go-diff v0.5.1
go 1.11

View file

@ -1,20 +1,29 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/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 h1:nTT4s92Dgz2HlrB2NaMgvlfqHH39OgMhA7z3PK7PGD4=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
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 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -51,7 +51,7 @@ type ejUnmarshaler interface {
UnmarshalEasyJSON(w *jlexer.Lexer)
}
// WriteJSON writes json data, prefers finding an appropriate interface to short-circuit the marshaller
// WriteJSON writes json data, prefers finding an appropriate interface to short-circuit the marshaler
// so it takes the fastest option available.
func WriteJSON(data interface{}) ([]byte, error) {
if d, ok := data.(ejMarshaler); ok {
@ -65,8 +65,8 @@ func WriteJSON(data interface{}) ([]byte, error) {
return json.Marshal(data)
}
// ReadJSON reads json data, prefers finding an appropriate interface to short-circuit the unmarshaller
// so it takes the fastes option available
// ReadJSON reads json data, prefers finding an appropriate interface to short-circuit the unmarshaler
// so it takes the fastest option available
func ReadJSON(data []byte, value interface{}) error {
trimmedData := bytes.Trim(data, "\x00")
if d, ok := value.(ejUnmarshaler); ok {
@ -189,7 +189,7 @@ func FromDynamicJSON(data, target interface{}) error {
return json.Unmarshal(b, target)
}
// NameProvider represents an object capabale of translating from go property names
// NameProvider represents an object capable of translating from go property names
// to json property names
// This type is thread-safe.
type NameProvider struct {

View file

@ -19,7 +19,9 @@ import (
"io/ioutil"
"log"
"net/http"
"net/url"
"path/filepath"
"runtime"
"strings"
"time"
)
@ -27,6 +29,15 @@ import (
// LoadHTTPTimeout the default timeout for load requests
var LoadHTTPTimeout = 30 * time.Second
// LoadHTTPBasicAuthUsername the username to use when load requests require basic auth
var LoadHTTPBasicAuthUsername = ""
// LoadHTTPBasicAuthPassword the password to use when load requests require basic auth
var LoadHTTPBasicAuthPassword = ""
// LoadHTTPCustomHeaders an optional collection of custom HTTP headers for load requests
var LoadHTTPCustomHeaders = map[string]string{}
// LoadFromFileOrHTTP loads the bytes from a file or a remote http server based on the path passed in
func LoadFromFileOrHTTP(path string) ([]byte, error) {
return LoadStrategy(path, ioutil.ReadFile, loadHTTPBytes(LoadHTTPTimeout))(path)
@ -48,6 +59,26 @@ func LoadStrategy(path string, local, remote func(string) ([]byte, error)) func(
if err != nil {
return nil, err
}
if strings.HasPrefix(pth, `file://`) {
if runtime.GOOS == "windows" {
// support for canonical file URIs on windows.
// Zero tolerance here for dodgy URIs.
u, _ := url.Parse(upth)
if u.Host != "" {
// assume UNC name (volume share)
// file://host/share/folder\... ==> \\host\share\path\folder
// NOTE: UNC port not yet supported
upth = strings.Join([]string{`\`, u.Host, u.Path}, `\`)
} else {
// file:///c:/folder/... ==> just remove the leading slash
upth = strings.TrimPrefix(upth, `file:///`)
}
} else {
upth = strings.TrimPrefix(upth, `file://`)
}
}
return local(filepath.FromSlash(upth))
}
}
@ -55,10 +86,19 @@ func LoadStrategy(path string, local, remote func(string) ([]byte, error)) func(
func loadHTTPBytes(timeout time.Duration) func(path string) ([]byte, error) {
return func(path string) ([]byte, error) {
client := &http.Client{Timeout: timeout}
req, err := http.NewRequest("GET", path, nil)
req, err := http.NewRequest("GET", path, nil) // nolint: noctx
if err != nil {
return nil, err
}
if LoadHTTPBasicAuthUsername != "" && LoadHTTPBasicAuthPassword != "" {
req.SetBasicAuth(LoadHTTPBasicAuthUsername, LoadHTTPBasicAuthPassword)
}
for key, val := range LoadHTTPCustomHeaders {
req.Header.Set(key, val)
}
resp, err := client.Do(req)
defer func() {
if resp != nil {

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build go1.8
// +build go1.8
package swag

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build go1.9
// +build go1.9
package swag

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !go1.8
// +build !go1.8
package swag

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !go1.9
// +build !go1.9
package swag

View file

@ -31,7 +31,7 @@ var isInitialism func(string) bool
// GoNamePrefixFunc sets an optional rule to prefix go names
// which do not start with a letter.
//
// e.g. to help converting "123" into "{prefix}123"
// e.g. to help convert "123" into "{prefix}123"
//
// The default is to prefix with "X"
var GoNamePrefixFunc func(string) string
@ -91,7 +91,7 @@ func init() {
}
const (
//collectionFormatComma = "csv"
// collectionFormatComma = "csv"
collectionFormatSpace = "ssv"
collectionFormatTab = "tsv"
collectionFormatPipe = "pipes"
@ -370,7 +370,7 @@ func IsZero(data interface{}) bool {
// AddInitialisms add additional initialisms
func AddInitialisms(words ...string) {
for _, word := range words {
//commonInitialisms[upper(word)] = true
// commonInitialisms[upper(word)] = true
commonInitialisms.add(upper(word))
}
// sort again

5
vendor/github.com/josharian/intern/README.md generated vendored Normal file
View file

@ -0,0 +1,5 @@
Docs: https://godoc.org/github.com/josharian/intern
See also [Go issue 5160](https://golang.org/issue/5160).
License: MIT

3
vendor/github.com/josharian/intern/go.mod generated vendored Normal file
View file

@ -0,0 +1,3 @@
module github.com/josharian/intern
go 1.5

44
vendor/github.com/josharian/intern/intern.go generated vendored Normal file
View file

@ -0,0 +1,44 @@
// Package intern interns strings.
// Interning is best effort only.
// Interned strings may be removed automatically
// at any time without notification.
// All functions may be called concurrently
// with themselves and each other.
package intern
import "sync"
var (
pool sync.Pool = sync.Pool{
New: func() interface{} {
return make(map[string]string)
},
}
)
// String returns s, interned.
func String(s string) string {
m := pool.Get().(map[string]string)
c, ok := m[s]
if ok {
pool.Put(m)
return c
}
m[s] = s
pool.Put(m)
return s
}
// Bytes returns b converted to a string, interned.
func Bytes(b []byte) string {
m := pool.Get().(map[string]string)
c, ok := m[string(b)]
if ok {
pool.Put(m)
return c
}
s := string(b)
m[s] = s
pool.Put(m)
return s
}

21
vendor/github.com/josharian/intern/license.md generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Josh Bleecher Snyder
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.

View file

@ -4,6 +4,7 @@ package buffer
import (
"io"
"net"
"sync"
)
@ -52,14 +53,12 @@ func putBuf(buf []byte) {
// getBuf gets a chunk from reuse pool or creates a new one if reuse failed.
func getBuf(size int) []byte {
if size < config.PooledSize {
return make([]byte, 0, size)
}
if c := buffers[size]; c != nil {
v := c.Get()
if v != nil {
return v.([]byte)
if size >= config.PooledSize {
if c := buffers[size]; c != nil {
v := c.Get()
if v != nil {
return v.([]byte)
}
}
}
return make([]byte, 0, size)
@ -78,9 +77,12 @@ type Buffer struct {
// EnsureSpace makes sure that the current chunk contains at least s free bytes,
// possibly creating a new chunk.
func (b *Buffer) EnsureSpace(s int) {
if cap(b.Buf)-len(b.Buf) >= s {
return
if cap(b.Buf)-len(b.Buf) < s {
b.ensureSpaceSlow(s)
}
}
func (b *Buffer) ensureSpaceSlow(s int) {
l := len(b.Buf)
if l > 0 {
if cap(b.toPool) != cap(b.Buf) {
@ -105,18 +107,22 @@ func (b *Buffer) EnsureSpace(s int) {
// AppendByte appends a single byte to buffer.
func (b *Buffer) AppendByte(data byte) {
if cap(b.Buf) == len(b.Buf) { // EnsureSpace won't be inlined.
b.EnsureSpace(1)
}
b.EnsureSpace(1)
b.Buf = append(b.Buf, data)
}
// AppendBytes appends a byte slice to buffer.
func (b *Buffer) AppendBytes(data []byte) {
if len(data) <= cap(b.Buf)-len(b.Buf) {
b.Buf = append(b.Buf, data...) // fast path
} else {
b.appendBytesSlow(data)
}
}
func (b *Buffer) appendBytesSlow(data []byte) {
for len(data) > 0 {
if cap(b.Buf) == len(b.Buf) { // EnsureSpace won't be inlined.
b.EnsureSpace(1)
}
b.EnsureSpace(1)
sz := cap(b.Buf) - len(b.Buf)
if sz > len(data) {
@ -128,12 +134,18 @@ func (b *Buffer) AppendBytes(data []byte) {
}
}
// AppendBytes appends a string to buffer.
// AppendString appends a string to buffer.
func (b *Buffer) AppendString(data string) {
if len(data) <= cap(b.Buf)-len(b.Buf) {
b.Buf = append(b.Buf, data...) // fast path
} else {
b.appendStringSlow(data)
}
}
func (b *Buffer) appendStringSlow(data string) {
for len(data) > 0 {
if cap(b.Buf) == len(b.Buf) { // EnsureSpace won't be inlined.
b.EnsureSpace(1)
}
b.EnsureSpace(1)
sz := cap(b.Buf) - len(b.Buf)
if sz > len(data) {
@ -156,18 +168,14 @@ func (b *Buffer) Size() int {
// DumpTo outputs the contents of a buffer to a writer and resets the buffer.
func (b *Buffer) DumpTo(w io.Writer) (written int, err error) {
var n int
for _, buf := range b.bufs {
if err == nil {
n, err = w.Write(buf)
written += n
}
putBuf(buf)
bufs := net.Buffers(b.bufs)
if len(b.Buf) > 0 {
bufs = append(bufs, b.Buf)
}
n, err := bufs.WriteTo(w)
if err == nil {
n, err = w.Write(b.Buf)
written += n
for _, buf := range b.bufs {
putBuf(buf)
}
putBuf(b.toPool)
@ -175,7 +183,7 @@ func (b *Buffer) DumpTo(w io.Writer) (written int, err error) {
b.Buf = nil
b.toPool = nil
return
return int(n), err
}
// BuildBytes creates a single byte slice with all the contents of the buffer. Data is
@ -192,7 +200,7 @@ func (b *Buffer) BuildBytes(reuse ...[]byte) []byte {
var ret []byte
size := b.Size()
// If we got a buffer as argument and it is big enought, reuse it.
// If we got a buffer as argument and it is big enough, reuse it.
if len(reuse) == 1 && cap(reuse[0]) >= size {
ret = reuse[0][:0]
} else {

View file

@ -5,6 +5,7 @@
package jlexer
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
@ -14,6 +15,8 @@ import (
"unicode"
"unicode/utf16"
"unicode/utf8"
"github.com/josharian/intern"
)
// tokenKind determines type of a token.
@ -32,9 +35,10 @@ const (
type token struct {
kind tokenKind // Type of a token.
boolValue bool // Value if a boolean literal token.
byteValue []byte // Raw value of a token.
delimValue byte
boolValue bool // Value if a boolean literal token.
byteValueCloned bool // true if byteValue was allocated and does not refer to original json body
byteValue []byte // Raw value of a token.
delimValue byte
}
// Lexer is a JSON lexer: it iterates over JSON tokens in a byte slice.
@ -240,23 +244,65 @@ func (r *Lexer) fetchNumber() {
// findStringLen tries to scan into the string literal for ending quote char to determine required size.
// The size will be exact if no escapes are present and may be inexact if there are escaped chars.
func findStringLen(data []byte) (isValid, hasEscapes bool, length int) {
delta := 0
for i := 0; i < len(data); i++ {
switch data[i] {
case '\\':
i++
delta++
if i < len(data) && data[i] == 'u' {
delta++
}
case '"':
return true, (delta > 0), (i - delta)
func findStringLen(data []byte) (isValid bool, length int) {
for {
idx := bytes.IndexByte(data, '"')
if idx == -1 {
return false, len(data)
}
if idx == 0 || (idx > 0 && data[idx-1] != '\\') {
return true, length + idx
}
// count \\\\\\\ sequences. even number of slashes means quote is not really escaped
cnt := 1
for idx-cnt-1 >= 0 && data[idx-cnt-1] == '\\' {
cnt++
}
if cnt%2 == 0 {
return true, length + idx
}
length += idx + 1
data = data[idx+1:]
}
}
// unescapeStringToken performs unescaping of string token.
// if no escaping is needed, original string is returned, otherwise - a new one allocated
func (r *Lexer) unescapeStringToken() (err error) {
data := r.token.byteValue
var unescapedData []byte
for {
i := bytes.IndexByte(data, '\\')
if i == -1 {
break
}
escapedRune, escapedBytes, err := decodeEscape(data[i:])
if err != nil {
r.errParse(err.Error())
return err
}
if unescapedData == nil {
unescapedData = make([]byte, 0, len(r.token.byteValue))
}
var d [4]byte
s := utf8.EncodeRune(d[:], escapedRune)
unescapedData = append(unescapedData, data[:i]...)
unescapedData = append(unescapedData, d[:s]...)
data = data[i+escapedBytes:]
}
return false, false, len(data)
if unescapedData != nil {
r.token.byteValue = append(unescapedData, data...)
r.token.byteValueCloned = true
}
return
}
// getu4 decodes \uXXXX from the beginning of s, returning the hex value,
@ -286,36 +332,30 @@ func getu4(s []byte) rune {
return val
}
// processEscape processes a single escape sequence and returns number of bytes processed.
func (r *Lexer) processEscape(data []byte) (int, error) {
// decodeEscape processes a single escape sequence and returns number of bytes processed.
func decodeEscape(data []byte) (decoded rune, bytesProcessed int, err error) {
if len(data) < 2 {
return 0, fmt.Errorf("syntax error at %v", string(data))
return 0, 0, errors.New("incorrect escape symbol \\ at the end of token")
}
c := data[1]
switch c {
case '"', '/', '\\':
r.token.byteValue = append(r.token.byteValue, c)
return 2, nil
return rune(c), 2, nil
case 'b':
r.token.byteValue = append(r.token.byteValue, '\b')
return 2, nil
return '\b', 2, nil
case 'f':
r.token.byteValue = append(r.token.byteValue, '\f')
return 2, nil
return '\f', 2, nil
case 'n':
r.token.byteValue = append(r.token.byteValue, '\n')
return 2, nil
return '\n', 2, nil
case 'r':
r.token.byteValue = append(r.token.byteValue, '\r')
return 2, nil
return '\r', 2, nil
case 't':
r.token.byteValue = append(r.token.byteValue, '\t')
return 2, nil
return '\t', 2, nil
case 'u':
rr := getu4(data)
if rr < 0 {
return 0, errors.New("syntax error")
return 0, 0, errors.New("incorrectly escaped \\uXXXX sequence")
}
read := 6
@ -328,13 +368,10 @@ func (r *Lexer) processEscape(data []byte) (int, error) {
rr = unicode.ReplacementChar
}
}
var d [4]byte
s := utf8.EncodeRune(d[:], rr)
r.token.byteValue = append(r.token.byteValue, d[:s]...)
return read, nil
return rr, read, nil
}
return 0, errors.New("syntax error")
return 0, 0, errors.New("incorrectly escaped bytes")
}
// fetchString scans a string literal token.
@ -342,43 +379,14 @@ func (r *Lexer) fetchString() {
r.pos++
data := r.Data[r.pos:]
isValid, hasEscapes, length := findStringLen(data)
isValid, length := findStringLen(data)
if !isValid {
r.pos += length
r.errParse("unterminated string literal")
return
}
if !hasEscapes {
r.token.byteValue = data[:length]
r.pos += length + 1
return
}
r.token.byteValue = make([]byte, 0, length)
p := 0
for i := 0; i < len(data); {
switch data[i] {
case '"':
r.pos += i + 1
r.token.byteValue = append(r.token.byteValue, data[p:i]...)
i++
return
case '\\':
r.token.byteValue = append(r.token.byteValue, data[p:i]...)
off, err := r.processEscape(data[i:])
if err != nil {
r.errParse(err.Error())
return
}
i += off
p = i
default:
i++
}
}
r.errParse("unterminated string literal")
r.token.byteValue = data[:length]
r.pos += length + 1 // skip closing '"' as well
}
// scanToken scans the next token if no token is currently available in the lexer.
@ -602,7 +610,7 @@ func (r *Lexer) Consumed() {
}
}
func (r *Lexer) unsafeString() (string, []byte) {
func (r *Lexer) unsafeString(skipUnescape bool) (string, []byte) {
if r.token.kind == tokenUndef && r.Ok() {
r.FetchToken()
}
@ -610,6 +618,13 @@ func (r *Lexer) unsafeString() (string, []byte) {
r.errInvalidToken("string")
return "", nil
}
if !skipUnescape {
if err := r.unescapeStringToken(); err != nil {
r.errInvalidToken("string")
return "", nil
}
}
bytes := r.token.byteValue
ret := bytesToStr(r.token.byteValue)
r.consume()
@ -621,13 +636,19 @@ func (r *Lexer) unsafeString() (string, []byte) {
// Warning: returned string may point to the input buffer, so the string should not outlive
// the input buffer. Intended pattern of usage is as an argument to a switch statement.
func (r *Lexer) UnsafeString() string {
ret, _ := r.unsafeString()
ret, _ := r.unsafeString(false)
return ret
}
// UnsafeBytes returns the byte slice if the token is a string literal.
func (r *Lexer) UnsafeBytes() []byte {
_, ret := r.unsafeString()
_, ret := r.unsafeString(false)
return ret
}
// UnsafeFieldName returns current member name string token
func (r *Lexer) UnsafeFieldName(skipUnescape bool) string {
ret, _ := r.unsafeString(skipUnescape)
return ret
}
@ -640,7 +661,34 @@ func (r *Lexer) String() string {
r.errInvalidToken("string")
return ""
}
ret := string(r.token.byteValue)
if err := r.unescapeStringToken(); err != nil {
r.errInvalidToken("string")
return ""
}
var ret string
if r.token.byteValueCloned {
ret = bytesToStr(r.token.byteValue)
} else {
ret = string(r.token.byteValue)
}
r.consume()
return ret
}
// StringIntern reads a string literal, and performs string interning on it.
func (r *Lexer) StringIntern() string {
if r.token.kind == tokenUndef && r.Ok() {
r.FetchToken()
}
if !r.Ok() || r.token.kind != tokenString {
r.errInvalidToken("string")
return ""
}
if err := r.unescapeStringToken(); err != nil {
r.errInvalidToken("string")
return ""
}
ret := intern.Bytes(r.token.byteValue)
r.consume()
return ret
}
@ -839,7 +887,7 @@ func (r *Lexer) Int() int {
}
func (r *Lexer) Uint8Str() uint8 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}
@ -856,7 +904,7 @@ func (r *Lexer) Uint8Str() uint8 {
}
func (r *Lexer) Uint16Str() uint16 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}
@ -873,7 +921,7 @@ func (r *Lexer) Uint16Str() uint16 {
}
func (r *Lexer) Uint32Str() uint32 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}
@ -890,7 +938,7 @@ func (r *Lexer) Uint32Str() uint32 {
}
func (r *Lexer) Uint64Str() uint64 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}
@ -915,7 +963,7 @@ func (r *Lexer) UintptrStr() uintptr {
}
func (r *Lexer) Int8Str() int8 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}
@ -932,7 +980,7 @@ func (r *Lexer) Int8Str() int8 {
}
func (r *Lexer) Int16Str() int16 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}
@ -949,7 +997,7 @@ func (r *Lexer) Int16Str() int16 {
}
func (r *Lexer) Int32Str() int32 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}
@ -966,7 +1014,7 @@ func (r *Lexer) Int32Str() int32 {
}
func (r *Lexer) Int64Str() int64 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}
@ -1004,7 +1052,7 @@ func (r *Lexer) Float32() float32 {
}
func (r *Lexer) Float32Str() float32 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}
@ -1037,7 +1085,7 @@ func (r *Lexer) Float64() float64 {
}
func (r *Lexer) Float64Str() float64 {
s, b := r.unsafeString()
s, b := r.unsafeString(false)
if !r.Ok() {
return 0
}

View file

@ -270,16 +270,25 @@ func (w *Writer) Bool(v bool) {
const chars = "0123456789abcdef"
func isNotEscapedSingleChar(c byte, escapeHTML bool) bool {
// Note: might make sense to use a table if there are more chars to escape. With 4 chars
// it benchmarks the same.
if escapeHTML {
return c != '<' && c != '>' && c != '&' && c != '\\' && c != '"' && c >= 0x20 && c < utf8.RuneSelf
} else {
return c != '\\' && c != '"' && c >= 0x20 && c < utf8.RuneSelf
func getTable(falseValues ...int) [128]bool {
table := [128]bool{}
for i := 0; i < 128; i++ {
table[i] = true
}
for _, v := range falseValues {
table[v] = false
}
return table
}
var (
htmlEscapeTable = getTable(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, '"', '&', '<', '>', '\\')
htmlNoEscapeTable = getTable(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, '"', '\\')
)
func (w *Writer) String(s string) {
w.Buffer.AppendByte('"')
@ -288,15 +297,21 @@ func (w *Writer) String(s string) {
p := 0 // last non-escape symbol
escapeTable := &htmlEscapeTable
if w.NoEscapeHTML {
escapeTable = &htmlNoEscapeTable
}
for i := 0; i < len(s); {
c := s[i]
if isNotEscapedSingleChar(c, !w.NoEscapeHTML) {
// single-width character, no escaping is required
i++
continue
} else if c < utf8.RuneSelf {
// single-with character, need to escape
if c < utf8.RuneSelf {
if escapeTable[c] {
// single-width character, no escaping is required
i++
continue
}
w.Buffer.AppendString(s[p:i])
switch c {
case '\t':

13
vendor/modules.txt vendored
View file

@ -153,15 +153,20 @@ github.com/deepmap/oapi-codegen/pkg/util
github.com/dimchansky/utfbom
# github.com/dougm/pretty v0.0.0-20171025230240-2ee9d7453c02
github.com/dougm/pretty
# github.com/getkin/kin-openapi v0.61.0
# github.com/getkin/kin-openapi v0.93.0
## explicit
github.com/getkin/kin-openapi/jsoninfo
github.com/getkin/kin-openapi/openapi3
github.com/getkin/kin-openapi/openapi3filter
github.com/getkin/kin-openapi/routers
github.com/getkin/kin-openapi/routers/legacy
github.com/getkin/kin-openapi/routers/legacy/pathpattern
# github.com/ghodss/yaml v1.0.0
github.com/ghodss/yaml
# github.com/go-openapi/jsonpointer v0.19.5
github.com/go-openapi/jsonpointer
# github.com/go-openapi/swag v0.19.5
# github.com/go-openapi/swag v0.21.1
## explicit
github.com/go-openapi/swag
# github.com/gobwas/glob v0.2.3
## explicit
@ -255,6 +260,8 @@ github.com/jackc/pgx/v4/pgxpool
github.com/jackc/puddle
# github.com/jmespath/go-jmespath v0.4.0
github.com/jmespath/go-jmespath
# github.com/josharian/intern v1.0.0
github.com/josharian/intern
# github.com/json-iterator/go v1.1.12
github.com/json-iterator/go
# github.com/julienschmidt/httprouter v1.3.0
@ -275,7 +282,7 @@ github.com/labstack/gommon/bytes
github.com/labstack/gommon/color
github.com/labstack/gommon/log
github.com/labstack/gommon/random
# github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e
# github.com/mailru/easyjson v0.7.6
github.com/mailru/easyjson/buffer
github.com/mailru/easyjson/jlexer
github.com/mailru/easyjson/jwriter