From 4b1a77981e08e99e33f7556070d33d8912918ecc Mon Sep 17 00:00:00 2001 From: Gord Allott Date: Tue, 8 Sep 2020 10:52:16 +0100 Subject: [PATCH 1/2] Enables jsonpointer support in openapi3 Allows for jsonpointer lookups via github.com/go-openapi/jsonpointer --- go.mod | 1 + go.sum | 14 ++++ openapi3/callback.go | 23 +++++- openapi3/components.go | 18 ++--- openapi3/encoding.go | 10 +-- openapi3/examples.go | 19 +++++ openapi3/header.go | 61 ++++++++++++-- openapi3/link.go | 17 ++++ openapi3/media_type.go | 31 ++++++- openapi3/operation.go | 42 +++++++++- openapi3/parameter.go | 105 +++++++++++++++++++++--- openapi3/refs.go | 93 +++++++++++++++++++++ openapi3/refs_test.go | 134 +++++++++++++++++++++++++++++++ openapi3/request_body.go | 18 +++++ openapi3/response.go | 24 +++++- openapi3/schema.go | 156 +++++++++++++++++++++++++++++++++--- openapi3/security_scheme.go | 17 ++++ 17 files changed, 730 insertions(+), 53 deletions(-) diff --git a/go.mod b/go.mod index cdd681fc3..8ccc13917 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/ghodss/yaml v1.0.0 + github.com/go-openapi/jsonpointer v0.19.5 github.com/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 998d6cb72..60f84360b 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,28 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/openapi3/callback.go b/openapi3/callback.go index 60196ba16..334233104 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -1,6 +1,27 @@ package openapi3 -import "context" +import ( + "context" + "fmt" + + "github.com/go-openapi/jsonpointer" +) + +type Callbacks map[string]*CallbackRef + +var _ jsonpointer.JSONPointable = (*Callbacks)(nil) + +func (c Callbacks) JSONLookup(token string) (interface{}, error) { + ref, ok := c[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} // Callback is specified by OpenAPI/Swagger standard version 3.0. type Callback map[string]*PathItem diff --git a/openapi3/components.go b/openapi3/components.go index 2708f95fb..e01f961d2 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -11,15 +11,15 @@ import ( // Components is specified by OpenAPI/Swagger standard version 3.0. type Components struct { ExtensionProps - Schemas map[string]*SchemaRef `json:"schemas,omitempty" yaml:"schemas,omitempty"` - Parameters map[string]*ParameterRef `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"` - RequestBodies map[string]*RequestBodyRef `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` - Responses map[string]*ResponseRef `json:"responses,omitempty" yaml:"responses,omitempty"` - SecuritySchemes map[string]*SecuritySchemeRef `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` - Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"` - Links map[string]*LinkRef `json:"links,omitempty" yaml:"links,omitempty"` - Callbacks map[string]*CallbackRef `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` + Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` + RequestBodies RequestBodies `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` + Responses Responses `json:"responses,omitempty" yaml:"responses,omitempty"` + SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` + Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` + Links Links `json:"links,omitempty" yaml:"links,omitempty"` + Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` } func NewComponents() Components { diff --git a/openapi3/encoding.go b/openapi3/encoding.go index a60bddf82..16b7a2694 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -11,11 +11,11 @@ import ( type Encoding struct { ExtensionProps - ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` - Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` + Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` } func NewEncoding() *Encoding { diff --git a/openapi3/examples.go b/openapi3/examples.go index d89263ebc..5f6255bf3 100644 --- a/openapi3/examples.go +++ b/openapi3/examples.go @@ -1,9 +1,28 @@ package openapi3 import ( + "fmt" + "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type Examples map[string]*ExampleRef + +var _ jsonpointer.JSONPointable = (*Examples)(nil) + +func (e Examples) JSONLookup(token string) (interface{}, error) { + ref, ok := e[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // Example is specified by OpenAPI/Swagger 3.0 standard. type Example struct { ExtensionProps diff --git a/openapi3/header.go b/openapi3/header.go index 310ef9f92..3adb2ea5a 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -2,23 +2,43 @@ package openapi3 import ( "context" + "fmt" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type Headers map[string]*HeaderRef + +var _ jsonpointer.JSONPointable = (*Headers)(nil) + +func (h Headers) JSONLookup(token string) (interface{}, error) { + ref, ok := h[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + type 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 map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` + 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"` } +var _ jsonpointer.JSONPointable = (*Header)(nil) + func (value *Header) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, value) } @@ -31,3 +51,30 @@ func (value *Header) Validate(c context.Context) error { } return nil } + +func (value Header) JSONLookup(token string) (interface{}, error) { + switch token { + case "schema": + if value.Schema != nil { + if value.Schema.Ref != "" { + return &Ref{Ref: value.Schema.Ref}, nil + } + return value.Schema.Value, nil + } + case "description": + return value.Description, nil + case "deprecated": + return value.Deprecated, nil + case "required": + return value.Required, nil + case "example": + return value.Example, nil + case "examples": + return value.Examples, nil + case "content": + return value.Content, nil + } + + v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token) + return v, err +} diff --git a/openapi3/link.go b/openapi3/link.go index 2c1ec013f..0fe1a1c74 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -6,8 +6,25 @@ import ( "fmt" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type Links map[string]*LinkRef + +func (l Links) JSONLookup(token string) (interface{}, error) { + ref, ok := l[token] + if ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref != nil && ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + +var _ jsonpointer.JSONPointable = (*Links)(nil) + // Link is specified by OpenAPI/Swagger standard version 3.0. type Link struct { ExtensionProps diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 942ecd9e0..6d2f2cb7a 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -4,18 +4,21 @@ import ( "context" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) // MediaType is specified by OpenAPI/Swagger 3.0 standard. type MediaType struct { ExtensionProps - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"` - Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` + Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` } +var _ jsonpointer.JSONPointable = (*MediaType)(nil) + func NewMediaType() *MediaType { return &MediaType{} } @@ -75,3 +78,23 @@ func (mediaType *MediaType) Validate(c context.Context) error { } return nil } + +func (mediaType MediaType) JSONLookup(token string) (interface{}, error) { + switch token { + case "schema": + if mediaType.Schema != nil { + if mediaType.Schema.Ref != "" { + return &Ref{Ref: mediaType.Schema.Ref}, nil + } + return mediaType.Schema.Value, nil + } + case "example": + return mediaType.Example, nil + case "examples": + return mediaType.Examples, nil + case "encoding": + return mediaType.Encoding, nil + } + v, _, err := jsonpointer.GetForToken(mediaType.ExtensionProps, token) + return v, err +} diff --git a/openapi3/operation.go b/openapi3/operation.go index 9e64f031e..08b43127b 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -6,6 +6,7 @@ import ( "strconv" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) // Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard. @@ -34,7 +35,7 @@ type Operation struct { Responses Responses `json:"responses" yaml:"responses"` // Required // Optional callbacks - Callbacks map[string]*CallbackRef `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` @@ -47,6 +48,8 @@ type Operation struct { ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` } +var _ jsonpointer.JSONPointable = (*Operation)(nil) + func NewOperation() *Operation { return &Operation{} } @@ -59,6 +62,43 @@ func (operation *Operation) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, operation) } +func (operation Operation) JSONLookup(token string) (interface{}, error) { + switch token { + case "requestBody": + if operation.RequestBody != nil { + if operation.RequestBody.Ref != "" { + return &Ref{Ref: operation.RequestBody.Ref}, nil + } + return operation.RequestBody.Value, nil + } + case "tags": + return operation.Tags, nil + case "summary": + return operation.Summary, nil + case "description": + return operation.Description, nil + case "operationID": + return operation.OperationID, nil + case "parameters": + return operation.Parameters, nil + case "responses": + return operation.Responses, nil + case "callbacks": + return operation.Callbacks, nil + case "deprecated": + return operation.Deprecated, nil + case "security": + return operation.Security, nil + case "servers": + return operation.Servers, nil + case "externalDocs": + return operation.ExternalDocs, nil + } + + v, _, err := jsonpointer.GetForToken(operation.ExtensionProps, token) + return v, err +} + func (operation *Operation) AddParameter(p *Parameter) { operation.Parameters = append(operation.Parameters, &ParameterRef{ Value: p, diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 1e2f55e17..8603bd7bc 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -4,13 +4,51 @@ import ( "context" "errors" "fmt" + "strconv" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type ParametersMap map[string]*ParameterRef + +var _ jsonpointer.JSONPointable = (*ParametersMap)(nil) + +func (p ParametersMap) JSONLookup(token string) (interface{}, error) { + ref, ok := p[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // Parameters is specified by OpenAPI/Swagger 3.0 standard. type Parameters []*ParameterRef +var _ jsonpointer.JSONPointable = (*Parameters)(nil) + +func (p Parameters) JSONLookup(token string) (interface{}, error) { + index, err := strconv.Atoi(token) + if err != nil { + return nil, err + } + + if index < 0 || index >= len(p) { + return nil, fmt.Errorf("index out of bounds array[0,%d] index '%d'", len(p), index) + } + + ref := p[index] + + if ref != nil && ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + func NewParameters() Parameters { return make(Parameters, 0, 4) } @@ -47,21 +85,23 @@ func (parameters Parameters) Validate(c context.Context) error { // Parameter is specified by OpenAPI/Swagger 3.0 standard. type Parameter struct { ExtensionProps - Name string `json:"name,omitempty" yaml:"name,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples map[string]*ExampleRef `json:"examples,omitempty" yaml:"examples,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` + Content Content `json:"content,omitempty" yaml:"content,omitempty"` } +var _ jsonpointer.JSONPointable = (*Parameter)(nil) + const ( ParameterInPath = "path" ParameterInQuery = "query" @@ -127,6 +167,45 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, parameter) } +func (parameter Parameter) JSONLookup(token string) (interface{}, error) { + switch token { + case "schema": + if parameter.Schema != nil { + if parameter.Schema.Ref != "" { + return &Ref{Ref: parameter.Schema.Ref}, nil + } + return parameter.Schema.Value, nil + } + case "name": + return parameter.Name, nil + case "in": + return parameter.In, nil + case "description": + return parameter.Description, nil + case "style": + return parameter.Style, nil + case "explode": + return parameter.Explode, nil + case "allowEmptyValue": + return parameter.AllowEmptyValue, nil + case "allowReserved": + return parameter.AllowReserved, nil + case "deprecated": + return parameter.Deprecated, nil + case "required": + return parameter.Required, nil + case "example": + return parameter.Example, nil + case "examples": + return parameter.Examples, nil + case "content": + return parameter.Content, nil + } + + v, _, err := jsonpointer.GetForToken(parameter.ExtensionProps, token) + return v, err +} + // SerializationMethod returns a parameter's serialization method. // When a parameter's serialization method is not defined the method returns // the default serialization method corresponding to a parameter's location. diff --git a/openapi3/refs.go b/openapi3/refs.go index 9790b4705..a086e367e 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -4,13 +4,21 @@ import ( "context" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +// Ref is specified by OpenAPI/Swagger 3.0 standard. +type Ref struct { + Ref string `json:"$ref" yaml:"$ref"` +} + type CallbackRef struct { Ref string Value *Callback } +var _ jsonpointer.JSONPointable = (*CallbackRef)(nil) + func (value *CallbackRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -27,11 +35,22 @@ func (value *CallbackRef) Validate(c context.Context) error { return v.Validate(c) } +func (value CallbackRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type ExampleRef struct { Ref string Value *Example } +var _ jsonpointer.JSONPointable = (*ExampleRef)(nil) + func (value *ExampleRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -44,11 +63,22 @@ func (value *ExampleRef) Validate(c context.Context) error { return nil } +func (value ExampleRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type HeaderRef struct { Ref string Value *Header } +var _ jsonpointer.JSONPointable = (*HeaderRef)(nil) + func (value *HeaderRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -64,6 +94,14 @@ func (value *HeaderRef) Validate(c context.Context) error { } return v.Validate(c) } +func (value HeaderRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} type LinkRef struct { Ref string @@ -91,6 +129,8 @@ type ParameterRef struct { Value *Parameter } +var _ jsonpointer.JSONPointable = (*ParameterRef)(nil) + func (value *ParameterRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -107,11 +147,22 @@ func (value *ParameterRef) Validate(c context.Context) error { return v.Validate(c) } +func (value ParameterRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type ResponseRef struct { Ref string Value *Response } +var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) + func (value *ResponseRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -128,11 +179,22 @@ func (value *ResponseRef) Validate(c context.Context) error { return v.Validate(c) } +func (value ResponseRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type RequestBodyRef struct { Ref string Value *RequestBody } +var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) + func (value *RequestBodyRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -149,11 +211,22 @@ func (value *RequestBodyRef) Validate(c context.Context) error { return v.Validate(c) } +func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type SchemaRef struct { Ref string Value *Schema } +var _ jsonpointer.JSONPointable = (*SchemaRef)(nil) + func NewSchemaRef(ref string, value *Schema) *SchemaRef { return &SchemaRef{ Ref: ref, @@ -177,11 +250,22 @@ func (value *SchemaRef) Validate(c context.Context) error { return v.Validate(c) } +func (value SchemaRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} + type SecuritySchemeRef struct { Ref string Value *SecurityScheme } +var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil) + func (value *SecuritySchemeRef) MarshalJSON() ([]byte, error) { return jsoninfo.MarshalRef(value.Ref, value.Value) } @@ -197,3 +281,12 @@ func (value *SecuritySchemeRef) Validate(c context.Context) error { } return v.Validate(c) } + +func (value SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return value.Ref, nil + } + + ptr, _, err := jsonpointer.GetForToken(value.Value, token) + return ptr, err +} diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index ed12b894d..992c0cf42 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -1,8 +1,11 @@ package openapi3 import ( + "reflect" "testing" + "github.com/go-openapi/jsonpointer" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -109,3 +112,134 @@ components: _, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) require.EqualError(t, err, `invalid response: value MUST be a JSON object`) } + +func TestIssue247(t *testing.T) { + spec := `openapi: 3.0.2 +info: + title: Swagger Petstore - OpenAPI 3.0 + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.5 +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io +- name: store + description: Operations about user +- name: user + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: http://swagger.io +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + ` + root, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) + require.NoError(t, err) + + ptr, err := jsonpointer.New("/paths/~1pet/put/responses/200/content") + require.NoError(t, err) + v, kind, err := ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.TypeOf(Content{}).Kind(), kind) + assert.IsType(t, Content{}, v) + + ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content/application~1json/schema") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + assert.IsType(t, &Ref{}, v) + assert.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) + + ptr, err = jsonpointer.New("/components/schemas/Pets/items") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + require.IsType(t, &Ref{}, v) + assert.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) + + ptr, err = jsonpointer.New("/components/schemas/Error/properties/code") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + require.IsType(t, &Schema{}, v) + assert.Equal(t, "integer", v.(*Schema).Type) + +} diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 6d649ca4b..ad871e8fd 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -2,10 +2,28 @@ package openapi3 import ( "context" + "fmt" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type RequestBodies map[string]*RequestBodyRef + +var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) + +func (r RequestBodies) JSONLookup(token string) (interface{}, error) { + ref, ok := r[token] + if ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref != nil && ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // RequestBody is specified by OpenAPI/Swagger 3.0 standard. type RequestBody struct { ExtensionProps diff --git a/openapi3/response.go b/openapi3/response.go index a89b28e2f..7c4da1dc2 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -3,14 +3,18 @@ package openapi3 import ( "context" "errors" + "fmt" "strconv" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) // Responses is specified by OpenAPI/Swagger 3.0 standard. type Responses map[string]*ResponseRef +var _ jsonpointer.JSONPointable = (*Responses)(nil) + func NewResponses() Responses { r := make(Responses) r["default"] = &ResponseRef{Value: NewResponse().WithDescription("")} @@ -37,13 +41,25 @@ func (responses Responses) Validate(c context.Context) error { return nil } +func (responses Responses) JSONLookup(token string) (interface{}, error) { + ref, ok := responses[token] + if ok == false { + return nil, fmt.Errorf("invalid token reference: %q", token) + } + + if ref != nil && ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // Response is specified by OpenAPI/Swagger 3.0 standard. type Response struct { ExtensionProps - Description *string `json:"description,omitempty" yaml:"description,omitempty"` - Headers map[string]*HeaderRef `json:"headers,omitempty" yaml:"headers,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` - Links map[string]*LinkRef `json:"links,omitempty" yaml:"links,omitempty"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` + Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Links Links `json:"links,omitempty" yaml:"links,omitempty"` } func NewResponse() *Response { diff --git a/openapi3/schema.go b/openapi3/schema.go index d020c0e3c..ad5488470 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -13,6 +13,7 @@ import ( "unicode/utf16" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) var ( @@ -50,13 +51,51 @@ func Uint64Ptr(value uint64) *uint64 { return &value } +type Schemas map[string]*SchemaRef + +var _ jsonpointer.JSONPointable = (*Schemas)(nil) + +func (s Schemas) JSONLookup(token string) (interface{}, error) { + ref, ok := s[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + +type SchemaRefs []*SchemaRef + +var _ jsonpointer.JSONPointable = (*SchemaRefs)(nil) + +func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { + i, err := strconv.Atoi(token) + if err != nil { + return nil, err + } + + if i >= len(s) { + return nil, fmt.Errorf("index out of range: %d", i) + } + + ref := s[i] + + if ref == nil || ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + // Schema is specified by OpenAPI/Swagger 3.0 standard. type Schema struct { ExtensionProps - OneOf []*SchemaRef `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` - AnyOf []*SchemaRef `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` - AllOf []*SchemaRef `json:"allOf,omitempty" yaml:"allOf,omitempty"` + OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` + AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` + AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Title string `json:"title,omitempty" yaml:"title,omitempty"` @@ -99,14 +138,16 @@ type Schema struct { Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` // Object - Required []string `json:"required,omitempty" yaml:"required,omitempty"` - Properties map[string]*SchemaRef `json:"properties,omitempty" yaml:"properties,omitempty"` - MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` - MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` - AdditionalProperties *SchemaRef `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"` - Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + 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"` } +var _ jsonpointer.JSONPointable = (*Schema)(nil) + func NewSchema() *Schema { return &Schema{} } @@ -119,6 +160,103 @@ func (schema *Schema) UnmarshalJSON(data []byte) error { return jsoninfo.UnmarshalStrictStruct(data, schema) } +func (schema Schema) JSONLookup(token string) (interface{}, error) { + switch token { + case "additionalProperties": + if schema.AdditionalProperties != nil { + if schema.AdditionalProperties.Ref != "" { + return &Ref{Ref: schema.AdditionalProperties.Ref}, nil + } + return schema.AdditionalProperties.Value, nil + } + case "not": + if schema.Not != nil { + if schema.Not.Ref != "" { + return &Ref{Ref: schema.Not.Ref}, nil + } + return schema.Not.Value, nil + } + case "items": + if schema.Items != nil { + if schema.Items.Ref != "" { + return &Ref{Ref: schema.Items.Ref}, nil + } + return schema.Items.Value, nil + } + case "oneOf": + return schema.OneOf, nil + case "anyOf": + return schema.AnyOf, nil + case "allOf": + return schema.AllOf, nil + case "type": + return schema.Type, nil + case "title": + return schema.Title, nil + case "format": + return schema.Format, nil + case "description": + return schema.Description, nil + case "enum": + return schema.Enum, nil + case "default": + return schema.Default, nil + case "example": + return schema.Example, nil + case "externalDocs": + return schema.ExternalDocs, nil + case "additionalPropertiesAllowed": + return schema.AdditionalPropertiesAllowed, nil + case "uniqueItems": + return schema.UniqueItems, nil + case "exclusiveMin": + return schema.ExclusiveMin, nil + case "exclusiveMax": + return schema.ExclusiveMax, nil + case "nullable": + return schema.Nullable, nil + case "readOnly": + return schema.ReadOnly, nil + case "writeOnly": + return schema.WriteOnly, nil + case "allowEmptyValue": + return schema.AllowEmptyValue, nil + case "xml": + return schema.XML, nil + case "deprecated": + return schema.Deprecated, nil + case "min": + return schema.Min, nil + case "max": + return schema.Max, nil + case "multipleOf": + return schema.MultipleOf, nil + case "minLength": + return schema.MinLength, nil + case "maxLength": + return schema.MaxLength, nil + case "pattern": + return schema.Pattern, nil + case "minItems": + return schema.MinItems, nil + case "maxItems": + return schema.MaxItems, nil + case "required": + return schema.Required, nil + case "properties": + return schema.Properties, nil + case "minProps": + return schema.MinProps, nil + case "maxProps": + return schema.MaxProps, nil + case "discriminator": + return schema.Discriminator, nil + } + + v, _, err := jsonpointer.GetForToken(schema.ExtensionProps, token) + return v, err +} + func (schema *Schema) NewRef() *SchemaRef { return &SchemaRef{ Value: schema, diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 0e991fb67..8dcb23086 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -6,8 +6,25 @@ import ( "fmt" "github.com/getkin/kin-openapi/jsoninfo" + "github.com/go-openapi/jsonpointer" ) +type SecuritySchemes map[string]*SecuritySchemeRef + +func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) { + ref, ok := s[token] + if ref == nil || ok == false { + return nil, fmt.Errorf("object has no field %q", token) + } + + if ref.Ref != "" { + return &Ref{Ref: ref.Ref}, nil + } + return ref.Value, nil +} + +var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) + type SecurityScheme struct { ExtensionProps From c6063561aaa85b757de0597db132dc4d73453b24 Mon Sep 17 00:00:00 2001 From: Gord Allott Date: Mon, 7 Dec 2020 14:34:11 +0000 Subject: [PATCH 2/2] use int64 for array indexes --- openapi3/refs_test.go | 31 +++++++++++++++++++++++++++++++ openapi3/schema.go | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index 992c0cf42..0c8b84570 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -207,6 +207,12 @@ components: format: int32 message: type: string + OneOfTest: + type: object + oneOf: + - type: string + - type: integer + format: int32 ` root, err := NewSwaggerLoader().LoadSwaggerFromData([]byte(spec)) require.NoError(t, err) @@ -242,4 +248,29 @@ components: require.IsType(t, &Schema{}, v) assert.Equal(t, "integer", v.(*Schema).Type) + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/0") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + require.IsType(t, &Schema{}, v) + assert.Equal(t, "string", v.(*Schema).Type) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/1") + require.NoError(t, err) + v, kind, err = ptr.Get(root) + assert.NoError(t, err) + assert.Equal(t, reflect.Ptr, kind) + require.IsType(t, &Schema{}, v) + assert.Equal(t, "integer", v.(*Schema).Type) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/5") + require.NoError(t, err) + _, _, err = ptr.Get(root) + assert.Error(t, err) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/-1") + require.NoError(t, err) + _, _, err = ptr.Get(root) + assert.Error(t, err) } diff --git a/openapi3/schema.go b/openapi3/schema.go index ad5488470..228ce0623 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -72,12 +72,12 @@ type SchemaRefs []*SchemaRef var _ jsonpointer.JSONPointable = (*SchemaRefs)(nil) func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { - i, err := strconv.Atoi(token) + i, err := strconv.ParseUint(token, 10, 64) if err != nil { return nil, err } - if i >= len(s) { + if i >= uint64(len(s)) { return nil, fmt.Errorf("index out of range: %d", i) }