From 93a629b37c430952bc4318d619439d980ec5d256 Mon Sep 17 00:00:00 2001 From: Ewen Quimerc'h <46993939+EwenQuim@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:58:50 +0200 Subject: [PATCH 01/19] README: add Fuego to dependents (#1017) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 690ef903..41335937 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ The project has received pull requests [from many people](https://github.com/get Please, [give back to this project](https://github.com/sponsors/fenollp) by becoming a sponsor. Here's some projects that depend on _kin-openapi_: + * [github.com/go-fuego/fuego](https://github.com/go-fuego/fuego) - "Framework generating OpenAPI 3 spec from source code" * [github.com/a-h/rest](https://github.com/a-h/rest) - "Generate OpenAPI 3.0 specifications from Go code without annotations or magic comments" * [github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" * [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation" From 75728b3bb6481d777d1bbfd1f539154aa362bb72 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 14 Oct 2024 16:36:18 +0200 Subject: [PATCH 02/19] openapi3: skip a test in CI to avoid 403s from some remote server (#1019) Signed-off-by: Pierre Fenoll --- openapi3/issue495_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openapi3/issue495_test.go b/openapi3/issue495_test.go index ee4cad50..b4d4a202 100644 --- a/openapi3/issue495_test.go +++ b/openapi3/issue495_test.go @@ -1,6 +1,7 @@ package openapi3 import ( + "os" "testing" "github.com/stretchr/testify/require" @@ -113,6 +114,10 @@ paths: sl := NewLoader() sl.IsExternalRefsAllowed = true + if os.Getenv("CI") == "true" { + t.Skip("Running in CI: skipping so we avoid 403 error from remote schema server") + } + doc, err := sl.LoadFromData(spec) require.NoError(t, err) From 56505dc96565e100cd40a741c42a9cae58dd4fff Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Wed, 16 Oct 2024 14:23:59 +0300 Subject: [PATCH 03/19] openapi3: introduce StringMap type to enable unmarshalling of maps with Origin (#1018) --- .github/docs/openapi3.txt | 16 ++++++++++------ README.md | 3 +++ openapi3/discriminator.go | 4 ++-- openapi3/security_scheme.go | 8 ++++---- openapi3/stringmap.go | 4 ++++ 5 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 openapi3/stringmap.go diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index cfccb97a..eddcbfbd 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -400,8 +400,8 @@ func (content Content) Validate(ctx context.Context, opts ...ValidationOption) e type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` - PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` + PropertyName string `json:"propertyName" yaml:"propertyName"` // required + Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` } Discriminator is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object @@ -873,10 +873,10 @@ type NumberFormatValidator = FormatValidator[float64] type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes map[string]string `json:"scopes" yaml:"scopes"` // required + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + Scopes StringMap `json:"scopes" yaml:"scopes"` // required } OAuthFlow is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object @@ -1983,6 +1983,10 @@ func NewRegexpFormatValidator(pattern string) StringFormatValidator NewRegexpFormatValidator creates a new FormatValidator that uses a regular expression to validate the value. +type StringMap map[string]string + StringMap is a map[string]string that ignores the origin in the underlying + json representation. + type T struct { Extensions map[string]any `json:"-" yaml:"-"` diff --git a/README.md b/README.md index 41335937..d135ffe9 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,9 @@ for _, path := range doc.Paths.InMatchingOrder() { ## CHANGELOG: Sub-v1 breaking API changes +### v0.129.0 +* `openapi3.Discriminator.Mapping` and `openapi3.OAuthFlow.Scopes` fields went from a `map[string]string` to the new type `StringMap` + ### v0.127.0 * Downgraded `github.com/gorilla/mux` dep from `1.8.1` to `1.8.0`. diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index e8193bd9..16d24400 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -10,8 +10,8 @@ import ( type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` - PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` + PropertyName string `json:"propertyName" yaml:"propertyName"` // required + Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` } // MarshalJSON returns the JSON encoding of Discriminator. diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index b5c94b61..b37a6eda 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -317,10 +317,10 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes map[string]string `json:"scopes" yaml:"scopes"` // required + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + Scopes StringMap `json:"scopes" yaml:"scopes"` // required } // MarshalJSON returns the JSON encoding of OAuthFlow. diff --git a/openapi3/stringmap.go b/openapi3/stringmap.go new file mode 100644 index 00000000..3819851c --- /dev/null +++ b/openapi3/stringmap.go @@ -0,0 +1,4 @@ +package openapi3 + +// StringMap is a map[string]string that ignores the origin in the underlying json representation. +type StringMap map[string]string From c333b34e43fe1aaeafe23299032470e3f32e02bd Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Mon, 21 Oct 2024 22:52:49 +0300 Subject: [PATCH 04/19] openapi3: reference originating locations in YAML specs - step 1 (#1007) * add origin - step 1 * delete origin from content * point to Origin * make include origin configurable * generic unmarshalStringMap * add origin to more components * revert comments * use const originKey * comment to Scopes * test more specs * Fix Discriminator * update docs * test on a decent set of dedicated specs * remove trainling spaces * add comments * remove trailing whitespace * update docs * dedicated tests * update docs * remove whitespace * rm empty line * rm last newline * add tests * rm unused test files * update deps * fix paths test * test components/security * fix LF * include origin in unmarshal maps * move component unmarshallers to respective files * fix test (json-schema 301) * Add github.com/pb33f/libopenapi (#1004) * Add github.com/pb33f/libopenapi it looks like a reasonable alternative * Update README.md --------- Co-authored-by: Pierre Fenoll * Introduce an option to override the regex implementation (#1006) * make form required field order deterministic (#1008) * openapi2: fix un/marshalling discriminator field (#1011) * fix: issue unmarshalling when discriminator field is set in openapi2.0 * revert original approach * update with different approach * Revert "update with different approach" This reverts commit 2db2b3929adb6a9fc22c9b8300a5689ca4633d98. * v2 schema with discriminator field set as string * update ref link and comment * run docs.sh * README: add Fuego to dependents (#1017) * openapi3: skip a test in CI to avoid 403s from some remote server (#1019) Signed-off-by: Pierre Fenoll * revert fix test (json-schema 301) * openapi3: introduce StringMap type to enable unmarshalling of maps with Origin (#1018) * add origin to more components * Fix Discriminator * update docs * update doccs * merge with stringmap * remove unused Scopes --------- Signed-off-by: Pierre Fenoll Co-authored-by: Jille Timmermans Co-authored-by: Pierre Fenoll Co-authored-by: Alexander Bakker Co-authored-by: Justin Sherrill Co-authored-by: Jay Shah Co-authored-by: Ewen Quimerc'h <46993939+EwenQuim@users.noreply.github.com> --- .github/docs/openapi3.txt | 89 ++++- go.mod | 6 +- go.sum | 14 +- maps.sh | 11 + openapi3/callback.go | 7 + openapi3/components.go | 2 + openapi3/contact.go | 3 + openapi3/content.go | 6 + openapi3/discriminator.go | 3 + openapi3/encoding.go | 3 + openapi3/example.go | 6 + openapi3/external_docs.go | 2 + openapi3/header.go | 6 + openapi3/info.go | 2 + openapi3/license.go | 2 + openapi3/link.go | 9 + openapi3/loader.go | 11 +- openapi3/maplike.go | 33 ++ openapi3/marsh.go | 4 +- openapi3/media_type.go | 2 + openapi3/operation.go | 2 + openapi3/origin.go | 17 + openapi3/origin_test.go | 303 ++++++++++++++++++ openapi3/parameter.go | 8 + openapi3/path_item.go | 2 + openapi3/paths.go | 1 + openapi3/ref.go | 3 +- openapi3/refs.go | 18 ++ openapi3/refs.tmpl | 2 + openapi3/request_body.go | 8 + openapi3/response.go | 9 + openapi3/schema.go | 8 + openapi3/security_requirements.go | 6 + openapi3/security_scheme.go | 13 + openapi3/server.go | 4 + openapi3/stringmap.go | 72 +++++ openapi3/tag.go | 2 + .../origin/additional_properties.yaml | 20 ++ openapi3/testdata/origin/external_docs.yaml | 15 + openapi3/testdata/origin/parameters.yaml | 19 ++ openapi3/testdata/origin/request_body.yaml | 22 ++ openapi3/testdata/origin/security.yaml | 36 +++ openapi3/testdata/origin/simple.yaml | 19 ++ 43 files changed, 816 insertions(+), 14 deletions(-) create mode 100644 openapi3/origin.go create mode 100644 openapi3/origin_test.go create mode 100644 openapi3/testdata/origin/additional_properties.yaml create mode 100644 openapi3/testdata/origin/external_docs.yaml create mode 100644 openapi3/testdata/origin/parameters.yaml create mode 100644 openapi3/testdata/origin/request_body.yaml create mode 100644 openapi3/testdata/origin/security.yaml create mode 100644 openapi3/testdata/origin/simple.yaml diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index eddcbfbd..44ab4d66 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -226,6 +226,7 @@ func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error type Callback struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` // Has unexported fields. } @@ -274,6 +275,7 @@ type CallbackRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Callback @@ -316,6 +318,9 @@ func (m Callbacks) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (callbacks *Callbacks) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Callbacks to a copy of data. + type ComponentRef interface { RefString() string RefPath() *url.URL @@ -324,6 +329,7 @@ type ComponentRef interface { type Components struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` @@ -355,6 +361,7 @@ func (components *Components) Validate(ctx context.Context, opts ...ValidationOp type Contact struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -394,11 +401,15 @@ func NewContentWithSchemaRef(schema *SchemaRef, consumes []string) Content func (content Content) Get(mime string) *MediaType +func (content *Content) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Content to a copy of data. + func (content Content) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Content does not comply with the OpenAPI spec. type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` PropertyName string `json:"propertyName" yaml:"propertyName"` // required Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` @@ -421,6 +432,7 @@ func (discriminator *Discriminator) Validate(ctx context.Context, opts ...Valida type Encoding struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -483,6 +495,7 @@ type ExampleRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Example @@ -525,8 +538,12 @@ func (m Examples) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (examples *Examples) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Examples to a copy of data. + type ExternalDocs struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -593,6 +610,7 @@ type HeaderRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Header @@ -635,8 +653,12 @@ func (m Headers) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (headers *Headers) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Headers to a copy of data. + type Info struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -665,6 +687,7 @@ type IntegerFormatValidator = FormatValidator[int64] type License struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -686,6 +709,7 @@ func (license *License) Validate(ctx context.Context, opts ...ValidationOption) type Link struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` @@ -713,6 +737,7 @@ type LinkRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Link @@ -754,10 +779,16 @@ func (m Links) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (links *Links) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Links to a copy of data. + type Loader struct { // IsExternalRefsAllowed enables visiting other files IsExternalRefsAllowed bool + // IncludeOrigin specifies whether to include the origin of the OpenAPI elements + IncludeOrigin bool + // ReadFromURIFunc allows overriding the any file/URL reading func ReadFromURIFunc ReadFromURIFunc @@ -793,8 +824,15 @@ func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) ResolveRefsIn expands references if for instance spec was just unmarshaled +type Location struct { + Line int `json:"line,omitempty" yaml:"line,omitempty"` + Column int `json:"column,omitempty" yaml:"column,omitempty"` +} + Location is a struct that contains the location of a field. + type MediaType struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Example any `json:"example,omitempty" yaml:"example,omitempty"` @@ -872,6 +910,7 @@ type NumberFormatValidator = FormatValidator[float64] type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` @@ -896,6 +935,7 @@ func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) e type OAuthFlows struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -920,6 +960,7 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) type Operation struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -982,8 +1023,17 @@ func (operation *Operation) Validate(ctx context.Context, opts ...ValidationOpti Validate returns an error if Operation does not comply with the OpenAPI spec. +type Origin struct { + Key *Location `json:"key,omitempty" yaml:"key,omitempty"` + Fields map[string]Location `json:"fields,omitempty" yaml:"fields,omitempty"` +} + Origin contains the origin of a collection. Key is the location of the + collection itself. Fields is a map of the location of each field in the + collection. + type Parameter struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` @@ -1042,6 +1092,7 @@ type ParameterRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Parameter @@ -1099,8 +1150,12 @@ func (m ParametersMap) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (parametersMap *ParametersMap) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets ParametersMap to a copy of data. + type PathItem struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -1140,6 +1195,7 @@ func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption type Paths struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` // Has unexported fields. } @@ -1228,7 +1284,8 @@ func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc documents. type Ref struct { - Ref string `json:"$ref" yaml:"$ref"` + Ref string `json:"$ref" yaml:"$ref"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` } Ref is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object @@ -1252,8 +1309,12 @@ func (m RequestBodies) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (requestBodies *RequestBodies) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets RequestBodies to a copy of data. + type RequestBody struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` @@ -1301,6 +1362,7 @@ type RequestBodyRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *RequestBody @@ -1339,6 +1401,7 @@ func (x *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) type Response struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -1376,10 +1439,14 @@ func (m ResponseBodies) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (responseBodies *ResponseBodies) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets ResponseBodies to a copy of data. + type ResponseRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Response @@ -1418,6 +1485,7 @@ func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) er type Responses struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"-" yaml:"-"` // Has unexported fields. } @@ -1476,6 +1544,7 @@ func (responses *Responses) Value(key string) *ResponseRef type Schema struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` @@ -1690,6 +1759,7 @@ type SchemaRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Schema @@ -1784,6 +1854,9 @@ func (m Schemas) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (schemas *Schemas) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Schemas to a copy of data. + type SecurityRequirement map[string][]string SecurityRequirement is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object @@ -1792,6 +1865,9 @@ func NewSecurityRequirement() SecurityRequirement func (security SecurityRequirement) Authenticate(provider string, scopes ...string) SecurityRequirement +func (security *SecurityRequirement) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets SecurityRequirement to a copy of data. + func (security *SecurityRequirement) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if SecurityRequirement does not comply with the OpenAPI spec. @@ -1808,6 +1884,7 @@ func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) * type SecurityScheme struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -1858,6 +1935,7 @@ type SecuritySchemeRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *SecurityScheme @@ -1901,6 +1979,9 @@ func (m SecuritySchemes) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (securitySchemes *SecuritySchemes) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets SecuritySchemes to a copy of data. + type SerializationMethod struct { Style string Explode bool @@ -1910,6 +1991,7 @@ type SerializationMethod struct { type Server struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` URL string `json:"url" yaml:"url"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -1940,6 +2022,7 @@ func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (e type ServerVariable struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` @@ -1987,6 +2070,9 @@ type StringMap map[string]string StringMap is a map[string]string that ignores the origin in the underlying json representation. +func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets StringMap to a copy of data. + type T struct { Extensions map[string]any `json:"-" yaml:"-"` @@ -2042,6 +2128,7 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error type Tag struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` diff --git a/go.mod b/go.mod index 11bc6d02..7d370323 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,10 @@ module github.com/getkin/kin-openapi -go 1.20 +go 1.22.5 + +replace gopkg.in/yaml.v3 => github.com/oasdiff/yaml3 v0.0.0-20240920135353-c185dc6ea7c6 + +replace github.com/invopop/yaml => github.com/oasdiff/yaml v0.0.0-20240920191703-3e5a9fb5bdf3 require ( github.com/go-openapi/jsonpointer v0.21.0 diff --git a/go.sum b/go.sum index 6b91d0dc..f9643cd0 100644 --- a/go.sum +++ b/go.sum @@ -5,18 +5,23 @@ github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kO github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oasdiff/yaml v0.0.0-20240920191703-3e5a9fb5bdf3 h1:nqCxALSUgWobWkFGIrhLRzR/bpImQdGj+3JS4/scTJo= +github.com/oasdiff/yaml v0.0.0-20240920191703-3e5a9fb5bdf3/go.mod h1:AOyUNV9ElKz7EEZeBm/48U54UtjtgCMT9fFbZEsClQc= +github.com/oasdiff/yaml3 v0.0.0-20240920135353-c185dc6ea7c6 h1:+ZsuDTdapTJxfMQk7SOJiNMg0v36pui01L7FEO615r8= +github.com/oasdiff/yaml3 v0.0.0-20240920135353-c185dc6ea7c6/go.mod h1:lqlOfJRrYpgeWHQj+ky2tf7UJ3PzgHTHRQEpc90nbp0= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -26,7 +31,6 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/maps.sh b/maps.sh index 9cfd0ffd..e2915bf4 100755 --- a/maps.sh +++ b/maps.sh @@ -209,6 +209,17 @@ func (${name} ${type}) UnmarshalJSON(data []byte) (err error) { continue } + if k == originKey { + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + if err = json.Unmarshal(data, &x.Origin); err != nil { + return + } + continue + } + var data []byte if data, err = json.Marshal(v); err != nil { return diff --git a/openapi3/callback.go b/openapi3/callback.go index 34a6bea3..3ae6da69 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -9,6 +9,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callback-object type Callback struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` m map[string]*PathItem } @@ -52,3 +53,9 @@ func (callback *Callback) Validate(ctx context.Context, opts ...ValidationOption return validateExtensions(ctx, callback.Extensions) } + +// UnmarshalJSON sets Callbacks to a copy of data. +func (callbacks *Callbacks) UnmarshalJSON(data []byte) (err error) { + *callbacks, _, err = unmarshalStringMapP[CallbackRef](data) + return +} diff --git a/openapi3/components.go b/openapi3/components.go index 98c4b96c..aecf8b64 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -25,6 +25,7 @@ type ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#components-object type Components struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` @@ -94,6 +95,7 @@ func (components *Components) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "schemas") delete(x.Extensions, "parameters") delete(x.Extensions, "headers") diff --git a/openapi3/contact.go b/openapi3/contact.go index 6c76a6fb..57cc801d 100644 --- a/openapi3/contact.go +++ b/openapi3/contact.go @@ -9,6 +9,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contact-object type Contact struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -50,6 +51,8 @@ func (contact *Contact) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "url") delete(x.Extensions, "email") diff --git a/openapi3/content.go b/openapi3/content.go index 81b070ee..73e301e0 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -122,3 +122,9 @@ func (content Content) Validate(ctx context.Context, opts ...ValidationOption) e } return nil } + +// UnmarshalJSON sets Content to a copy of data. +func (content *Content) UnmarshalJSON(data []byte) (err error) { + *content, _, err = unmarshalStringMapP[MediaType](data) + return +} diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 16d24400..a8ab07b4 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -9,6 +9,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` PropertyName string `json:"propertyName" yaml:"propertyName"` // required Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` @@ -44,6 +45,8 @@ func (discriminator *Discriminator) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "propertyName") delete(x.Extensions, "mapping") if len(x.Extensions) == 0 { diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 1bcdaea5..7eb507e5 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -11,6 +11,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encoding-object type Encoding struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -80,6 +81,8 @@ func (encoding *Encoding) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "contentType") delete(x.Extensions, "headers") delete(x.Extensions, "style") diff --git a/openapi3/example.go b/openapi3/example.go index f9a7a6b0..9d38e434 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -83,3 +83,9 @@ func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) return validateExtensions(ctx, example.Extensions) } + +// UnmarshalJSON sets Examples to a copy of data. +func (examples *Examples) UnmarshalJSON(data []byte) (err error) { + *examples, _, err = unmarshalStringMapP[ExampleRef](data) + return +} diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index bd99511a..f6794141 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -12,6 +12,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object type ExternalDocs struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -49,6 +50,7 @@ func (e *ExternalDocs) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "description") delete(x.Extensions, "url") if len(x.Extensions) == 0 { diff --git a/openapi3/header.go b/openapi3/header.go index dc542874..6b23db52 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -94,3 +94,9 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er } return nil } + +// UnmarshalJSON sets Headers to a copy of data. +func (headers *Headers) UnmarshalJSON(data []byte) (err error) { + *headers, _, err = unmarshalStringMapP[HeaderRef](data) + return +} diff --git a/openapi3/info.go b/openapi3/info.go index e2468285..acca2f4d 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -10,6 +10,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#info-object type Info struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -62,6 +63,7 @@ func (info *Info) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "title") delete(x.Extensions, "description") delete(x.Extensions, "termsOfService") diff --git a/openapi3/license.go b/openapi3/license.go index c4f6c8dc..df9d6c44 100644 --- a/openapi3/license.go +++ b/openapi3/license.go @@ -10,6 +10,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#license-object type License struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -45,6 +46,7 @@ func (license *License) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "url") if len(x.Extensions) == 0 { diff --git a/openapi3/link.go b/openapi3/link.go index 132f6780..dfd320f1 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -11,6 +11,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#link-object type Link struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` @@ -66,6 +67,8 @@ func (link *Link) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "operationRef") delete(x.Extensions, "operationId") delete(x.Extensions, "description") @@ -92,3 +95,9 @@ func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error return validateExtensions(ctx, link.Extensions) } + +// UnmarshalJSON sets Links to a copy of data. +func (links *Links) UnmarshalJSON(data []byte) (err error) { + *links, _, err = unmarshalStringMapP[LinkRef](data) + return +} diff --git a/openapi3/loader.go b/openapi3/loader.go index 31c34076..0f3b8cbd 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -28,6 +28,9 @@ type Loader struct { // IsExternalRefsAllowed enables visiting other files IsExternalRefsAllowed bool + // IncludeOrigin specifies whether to include the origin of the OpenAPI elements + IncludeOrigin bool + // ReadFromURIFunc allows overriding the any file/URL reading func ReadFromURIFunc ReadFromURIFunc @@ -103,7 +106,7 @@ func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, el if err != nil { return nil, err } - if err := unmarshal(data, element); err != nil { + if err := unmarshal(data, element, loader.IncludeOrigin); err != nil { return nil, err } @@ -139,7 +142,7 @@ func (loader *Loader) LoadFromIoReader(reader io.Reader) (*T, error) { func (loader *Loader) LoadFromData(data []byte) (*T, error) { loader.resetVisitedPathItemRefs() doc := &T{} - if err := unmarshal(data, doc); err != nil { + if err := unmarshal(data, doc, loader.IncludeOrigin); err != nil { return nil, err } if err := loader.ResolveRefsIn(doc, nil); err != nil { @@ -168,7 +171,7 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR doc := &T{} loader.visitedDocuments[uri] = doc - if err := unmarshal(data, doc); err != nil { + if err := unmarshal(data, doc, loader.IncludeOrigin); err != nil { return nil, err } @@ -422,7 +425,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv if err2 != nil { return nil, nil, err } - if err2 = unmarshal(data, &cursor); err2 != nil { + if err2 = unmarshal(data, &cursor, loader.IncludeOrigin); err2 != nil { return nil, nil, err } if cursor, err2 = drill(cursor); err2 != nil || cursor == nil { diff --git a/openapi3/maplike.go b/openapi3/maplike.go index 7b8045c6..35b33657 100644 --- a/openapi3/maplike.go +++ b/openapi3/maplike.go @@ -125,6 +125,17 @@ func (responses *Responses) UnmarshalJSON(data []byte) (err error) { continue } + if k == originKey { + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + if err = json.Unmarshal(data, &x.Origin); err != nil { + return + } + continue + } + var data []byte if data, err = json.Marshal(v); err != nil { return @@ -256,6 +267,17 @@ func (callback *Callback) UnmarshalJSON(data []byte) (err error) { continue } + if k == originKey { + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + if err = json.Unmarshal(data, &x.Origin); err != nil { + return + } + continue + } + var data []byte if data, err = json.Marshal(v); err != nil { return @@ -387,6 +409,17 @@ func (paths *Paths) UnmarshalJSON(data []byte) (err error) { continue } + if k == originKey { + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + if err = json.Unmarshal(data, &x.Origin); err != nil { + return + } + continue + } + var data []byte if data, err = json.Marshal(v); err != nil { return diff --git a/openapi3/marsh.go b/openapi3/marsh.go index daa93755..9fdc6dff 100644 --- a/openapi3/marsh.go +++ b/openapi3/marsh.go @@ -16,7 +16,7 @@ func unmarshalError(jsonUnmarshalErr error) error { return jsonUnmarshalErr } -func unmarshal(data []byte, v any) error { +func unmarshal(data []byte, v any, includeOrigin bool) error { var jsonErr, yamlErr error // See https://github.com/getkin/kin-openapi/issues/680 @@ -25,7 +25,7 @@ func unmarshal(data []byte, v any) error { } // UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys - if yamlErr = yaml.Unmarshal(data, v); yamlErr == nil { + if yamlErr = yaml.UnmarshalWithOrigin(data, v, includeOrigin); yamlErr == nil { return nil } diff --git a/openapi3/media_type.go b/openapi3/media_type.go index d4466bcf..d6edaf4d 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -14,6 +14,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object type MediaType struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Example any `json:"example,omitempty" yaml:"example,omitempty"` @@ -101,6 +102,7 @@ func (mediaType *MediaType) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "schema") delete(x.Extensions, "example") delete(x.Extensions, "examples") diff --git a/openapi3/operation.go b/openapi3/operation.go index 40abf73c..7b57e847 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -14,6 +14,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object type Operation struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -116,6 +117,7 @@ func (operation *Operation) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "tags") delete(x.Extensions, "summary") delete(x.Extensions, "description") diff --git a/openapi3/origin.go b/openapi3/origin.go new file mode 100644 index 00000000..b0e4a934 --- /dev/null +++ b/openapi3/origin.go @@ -0,0 +1,17 @@ +package openapi3 + +const originKey = "origin" + +// Origin contains the origin of a collection. +// Key is the location of the collection itself. +// Fields is a map of the location of each field in the collection. +type Origin struct { + Key *Location `json:"key,omitempty" yaml:"key,omitempty"` + Fields map[string]Location `json:"fields,omitempty" yaml:"fields,omitempty"` +} + +// Location is a struct that contains the location of a field. +type Location struct { + Line int `json:"line,omitempty" yaml:"line,omitempty"` + Column int `json:"column,omitempty" yaml:"column,omitempty"` +} diff --git a/openapi3/origin_test.go b/openapi3/origin_test.go new file mode 100644 index 00000000..56d8f1d4 --- /dev/null +++ b/openapi3/origin_test.go @@ -0,0 +1,303 @@ +package openapi3 + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOrigin_All(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + const dir = "testdata/origin/" + items, _ := os.ReadDir(dir) + for _, item := range items { + t.Run(item.Name(), func(t *testing.T) { + doc, err := loader.LoadFromFile(fmt.Sprintf("%s/%s", dir, item.Name())) + require.NoError(t, err) + if doc.Paths == nil { + t.Skip("no paths") + } + require.NotEmpty(t, doc.Paths.Origin) + }) + } +} + +func TestOrigin_Info(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/simple.yaml") + require.NoError(t, err) + + require.NotNil(t, doc.Info.Origin) + require.Equal(t, + &Location{ + Line: 2, + Column: 1, + }, + doc.Info.Origin.Key) + + require.Equal(t, + Location{ + Line: 3, + Column: 3, + }, + doc.Info.Origin.Fields["title"]) + + require.Equal(t, + Location{ + Line: 4, + Column: 3, + }, + doc.Info.Origin.Fields["version"]) +} + +func TestOrigin_Paths(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/simple.yaml") + require.NoError(t, err) + + require.NotNil(t, doc.Paths.Origin) + require.Equal(t, + &Location{ + Line: 5, + Column: 1, + }, + doc.Paths.Origin.Key) + + base := doc.Paths.Find("/partner-api/test/another-method") + + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 13, + Column: 3, + }, + base.Origin.Key) + + require.NotNil(t, base.Get.Origin) + require.Equal(t, + &Location{ + Line: 14, + Column: 5, + }, + base.Get.Origin.Key) +} + +func TestOrigin_RequestBody(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/request_body.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/subscribe").Post.RequestBody.Value + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 8, + Column: 7, + }, + base.Origin.Key) + + require.NotNil(t, base.Content["application/json"].Origin) + require.Equal(t, + &Location{ + Line: 10, + Column: 11, + }, + base.Content["application/json"].Origin.Key) +} + +func TestOrigin_Responses(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/simple.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/partner-api/test/another-method").Get.Responses + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 17, + Column: 7, + }, + base.Origin.Key) + + require.NotNil(t, base.Origin) + require.Nil(t, base.Value("200").Origin) + require.Equal(t, + &Location{ + Line: 18, + Column: 9, + }, + base.Value("200").Value.Origin.Key) + + require.Equal(t, + Location{ + Line: 19, + Column: 11, + }, + base.Value("200").Value.Origin.Fields["description"]) +} + +func TestOrigin_Parameters(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/parameters.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/api/test").Get.Parameters[0].Value + require.NotNil(t, base) + require.Equal(t, + &Location{ + Line: 9, + Column: 11, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 10, + Column: 11, + }, + base.Origin.Fields["in"]) + + require.Equal(t, + Location{ + Line: 9, + Column: 11, + }, + base.Origin.Fields["name"]) +} + +func TestOrigin_SchemaInAdditionalProperties(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/additional_properties.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/partner-api/test/some-method").Get.Responses.Value("200").Value.Content["application/json"].Schema.Value.AdditionalProperties + require.NotNil(t, base) + + require.NotNil(t, base.Schema.Value.Origin) + require.Equal(t, + &Location{ + Line: 14, + Column: 17, + }, + base.Schema.Value.Origin.Key) + + require.Equal(t, + Location{ + Line: 15, + Column: 19, + }, + base.Schema.Value.Origin.Fields["type"]) +} + +func TestOrigin_ExternalDocs(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/external_docs.yaml") + require.NoError(t, err) + + base := doc.ExternalDocs + require.NotNil(t, base.Origin) + + require.Equal(t, + &Location{ + Line: 13, + Column: 1, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 14, + Column: 3, + }, + base.Origin.Fields["description"]) + + require.Equal(t, + Location{ + Line: 15, + Column: 3, + }, + base.Origin.Fields["url"]) +} + +func TestOrigin_Security(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/security.yaml") + require.NoError(t, err) + + base := doc.Components.SecuritySchemes["petstore_auth"].Value + require.NotNil(t, base) + + require.Equal(t, + &Location{ + Line: 29, + Column: 5, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 30, + Column: 7, + }, + base.Origin.Fields["type"]) + + require.Equal(t, + &Location{ + Line: 31, + Column: 7, + }, + base.Flows.Origin.Key) + + require.Equal(t, + &Location{ + Line: 32, + Column: 9, + }, + base.Flows.Implicit.Origin.Key) + + require.Equal(t, + Location{ + Line: 33, + Column: 11, + }, + base.Flows.Implicit.Origin.Fields["authorizationUrl"]) +} diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 34fe2911..d69bd5f5 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -73,6 +73,7 @@ func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOpt // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object type Parameter struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` @@ -216,6 +217,7 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error { } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "in") delete(x.Extensions, "description") @@ -414,3 +416,9 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti return validateExtensions(ctx, parameter.Extensions) } + +// UnmarshalJSON sets ParametersMap to a copy of data. +func (parametersMap *ParametersMap) UnmarshalJSON(data []byte) (err error) { + *parametersMap, _, err = unmarshalStringMapP[ParameterRef](data) + return +} diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 859634fe..2c60472a 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -12,6 +12,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#path-item-object type PathItem struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -98,6 +99,7 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "$ref") delete(x.Extensions, "summary") delete(x.Extensions, "description") diff --git a/openapi3/paths.go b/openapi3/paths.go index 76747412..80675047 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -11,6 +11,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object type Paths struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` m map[string]*PathItem } diff --git a/openapi3/ref.go b/openapi3/ref.go index 07060731..d24f4c9b 100644 --- a/openapi3/ref.go +++ b/openapi3/ref.go @@ -5,5 +5,6 @@ package openapi3 // Ref is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object type Ref struct { - Ref string `json:"$ref" yaml:"$ref"` + Ref string `json:"$ref" yaml:"$ref"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` } diff --git a/openapi3/refs.go b/openapi3/refs.go index d337b0e3..fc11164a 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -19,6 +19,7 @@ type CallbackRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Callback @@ -72,6 +73,7 @@ func (x *CallbackRef) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -155,6 +157,7 @@ type ExampleRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Example @@ -208,6 +211,7 @@ func (x *ExampleRef) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -291,6 +295,7 @@ type HeaderRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Header @@ -344,6 +349,7 @@ func (x *HeaderRef) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -427,6 +433,7 @@ type LinkRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Link @@ -480,6 +487,7 @@ func (x *LinkRef) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -563,6 +571,7 @@ type ParameterRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Parameter @@ -616,6 +625,7 @@ func (x *ParameterRef) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -699,6 +709,7 @@ type RequestBodyRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *RequestBody @@ -752,6 +763,7 @@ func (x *RequestBodyRef) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -835,6 +847,7 @@ type ResponseRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Response @@ -888,6 +901,7 @@ func (x *ResponseRef) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -971,6 +985,7 @@ type SchemaRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Schema @@ -1024,6 +1039,7 @@ func (x *SchemaRef) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -1107,6 +1123,7 @@ type SecuritySchemeRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *SecurityScheme @@ -1160,6 +1177,7 @@ func (x *SecuritySchemeRef) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { diff --git a/openapi3/refs.tmpl b/openapi3/refs.tmpl index a3f5bdab..028deba7 100644 --- a/openapi3/refs.tmpl +++ b/openapi3/refs.tmpl @@ -19,6 +19,7 @@ type {{ $type.Name }}Ref struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *{{ $type.Name }} @@ -72,6 +73,7 @@ func (x *{{ $type.Name }}Ref) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 6d4b8185..39288ee7 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -10,6 +10,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#request-body-object type RequestBody struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` @@ -108,6 +109,7 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "description") delete(x.Extensions, "required") delete(x.Extensions, "content") @@ -136,3 +138,9 @@ func (requestBody *RequestBody) Validate(ctx context.Context, opts ...Validation return validateExtensions(ctx, requestBody.Extensions) } + +// UnmarshalJSON sets RequestBodies to a copy of data. +func (requestBodies *RequestBodies) UnmarshalJSON(data []byte) (err error) { + *requestBodies, _, err = unmarshalStringMapP[RequestBodyRef](data) + return +} diff --git a/openapi3/response.go b/openapi3/response.go index af8fda6f..d4bfe42e 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -12,6 +12,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responses-object type Responses struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"-" yaml:"-"` m map[string]*ResponseRef } @@ -102,6 +103,7 @@ func (responses *Responses) Validate(ctx context.Context, opts ...ValidationOpti // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#response-object type Response struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -171,6 +173,7 @@ func (response *Response) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "description") delete(x.Extensions, "headers") delete(x.Extensions, "content") @@ -225,3 +228,9 @@ func (response *Response) Validate(ctx context.Context, opts ...ValidationOption return validateExtensions(ctx, response.Extensions) } + +// UnmarshalJSON sets ResponseBodies to a copy of data. +func (responseBodies *ResponseBodies) UnmarshalJSON(data []byte) (err error) { + *responseBodies, _, err = unmarshalStringMapP[ResponseRef](data) + return +} diff --git a/openapi3/schema.go b/openapi3/schema.go index f8119606..10a00eac 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -81,6 +81,7 @@ func (s SchemaRefs) JSONLookup(token string) (any, error) { // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object type Schema struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` @@ -411,6 +412,7 @@ func (schema *Schema) UnmarshalJSON(data []byte) error { } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "oneOf") delete(x.Extensions, "anyOf") delete(x.Extensions, "allOf") @@ -2244,3 +2246,9 @@ func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) { func unsupportedFormat(format string) error { return fmt.Errorf("unsupported 'format' value %q", format) } + +// UnmarshalJSON sets Schemas to a copy of data. +func (schemas *Schemas) UnmarshalJSON(data []byte) (err error) { + *schemas, _, err = unmarshalStringMapP[SchemaRef](data) + return +} diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index 87891c95..6af80e8b 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -49,3 +49,9 @@ func (security *SecurityRequirement) Validate(ctx context.Context, opts ...Valid return nil } + +// UnmarshalJSON sets SecurityRequirement to a copy of data. +func (security *SecurityRequirement) UnmarshalJSON(data []byte) (err error) { + *security, _, err = unmarshalStringMap[[]string](data) + return +} diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index b37a6eda..7bc889de 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -12,6 +12,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object type SecurityScheme struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -100,6 +101,7 @@ func (ss *SecurityScheme) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "type") delete(x.Extensions, "description") delete(x.Extensions, "name") @@ -216,6 +218,7 @@ func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object type OAuthFlows struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -270,6 +273,7 @@ func (flows *OAuthFlows) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "implicit") delete(x.Extensions, "password") delete(x.Extensions, "clientCredentials") @@ -316,6 +320,7 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` @@ -359,6 +364,8 @@ func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "authorizationUrl") delete(x.Extensions, "tokenUrl") delete(x.Extensions, "refreshUrl") @@ -427,3 +434,9 @@ func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ... return flow.Validate(ctx, opts...) } + +// UnmarshalJSON sets SecuritySchemes to a copy of data. +func (securitySchemes *SecuritySchemes) UnmarshalJSON(data []byte) (err error) { + *securitySchemes, _, err = unmarshalStringMapP[SecuritySchemeRef](data) + return +} diff --git a/openapi3/server.go b/openapi3/server.go index 7a2007f2..5a817e51 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -52,6 +52,7 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-object type Server struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` URL string `json:"url" yaml:"url"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -115,6 +116,7 @@ func (server *Server) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "url") delete(x.Extensions, "description") delete(x.Extensions, "variables") @@ -235,6 +237,7 @@ func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (e // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object type ServerVariable struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` @@ -276,6 +279,7 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "enum") delete(x.Extensions, "default") delete(x.Extensions, "description") diff --git a/openapi3/stringmap.go b/openapi3/stringmap.go index 3819851c..354a4c8e 100644 --- a/openapi3/stringmap.go +++ b/openapi3/stringmap.go @@ -1,4 +1,76 @@ package openapi3 +import "encoding/json" + // StringMap is a map[string]string that ignores the origin in the underlying json representation. type StringMap map[string]string + +// UnmarshalJSON sets StringMap to a copy of data. +func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) { + *stringMap, _, err = unmarshalStringMap[string](data) + return +} + +// unmarshalStringMapP unmarshals given json into a map[string]*V +func unmarshalStringMapP[V any](data []byte) (map[string]*V, *Origin, error) { + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + return nil, nil, err + } + + origin, err := deepCast[Origin](m[originKey]) + if err != nil { + return nil, nil, err + } + delete(m, originKey) + + result := make(map[string]*V, len(m)) + for k, v := range m { + value, err := deepCast[V](v) + if err != nil { + return nil, nil, err + } + result[k] = value + } + + return result, origin, nil +} + +// unmarshalStringMap unmarshals given json into a map[string]V +func unmarshalStringMap[V any](data []byte) (map[string]V, *Origin, error) { + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + return nil, nil, err + } + + origin, err := deepCast[Origin](m[originKey]) + if err != nil { + return nil, nil, err + } + delete(m, originKey) + + result := make(map[string]V, len(m)) + for k, v := range m { + value, err := deepCast[V](v) + if err != nil { + return nil, nil, err + } + result[k] = *value + } + + return result, origin, nil +} + +// deepCast casts any value to a value of type V. +func deepCast[V any](value any) (*V, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + + var result V + if err = json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} diff --git a/openapi3/tag.go b/openapi3/tag.go index 182d0502..5e4086fe 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -34,6 +34,7 @@ func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error { // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tag-object type Tag struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -75,6 +76,7 @@ func (t *Tag) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "description") delete(x.Extensions, "externalDocs") diff --git a/openapi3/testdata/origin/additional_properties.yaml b/openapi3/testdata/origin/additional_properties.yaml new file mode 100644 index 00000000..8e865bc9 --- /dev/null +++ b/openapi3/testdata/origin/additional_properties.yaml @@ -0,0 +1,20 @@ +openapi: 3.0.1 +info: + title: Test API + version: v1 +paths: + /partner-api/test/some-method: + get: + responses: + "200": + description: Success + content: + application/json: + schema: + additionalProperties: + type: object + properties: + code: + type: integer + text: + type: string \ No newline at end of file diff --git a/openapi3/testdata/origin/external_docs.yaml b/openapi3/testdata/origin/external_docs.yaml new file mode 100644 index 00000000..bfc5ba09 --- /dev/null +++ b/openapi3/testdata/origin/external_docs.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.1 +info: + title: Test API + version: v1 +paths: + /partner-api/test/some-method: + get: + tags: + - Test + responses: + "200": + description: Success +externalDocs: + description: API Documentation + url: https://openweathermap.org/api \ No newline at end of file diff --git a/openapi3/testdata/origin/parameters.yaml b/openapi3/testdata/origin/parameters.yaml new file mode 100644 index 00000000..4d4299e2 --- /dev/null +++ b/openapi3/testdata/origin/parameters.yaml @@ -0,0 +1,19 @@ +info: + title: Tufin + version: 1.0.0 +openapi: 3.0.3 +paths: + /api/test: + get: + parameters: + - name: a + in: query + schema: + type: integer + responses: + 200: + description: OK + post: + responses: + 201: + description: OK diff --git a/openapi3/testdata/origin/request_body.yaml b/openapi3/testdata/origin/request_body.yaml new file mode 100644 index 00000000..09436384 --- /dev/null +++ b/openapi3/testdata/origin/request_body.yaml @@ -0,0 +1,22 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK diff --git a/openapi3/testdata/origin/security.yaml b/openapi3/testdata/origin/security.yaml new file mode 100644 index 00000000..7594e961 --- /dev/null +++ b/openapi3/testdata/origin/security.yaml @@ -0,0 +1,36 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK + security: + - petstore_auth: + - write:pets + - read:pets +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets \ No newline at end of file diff --git a/openapi3/testdata/origin/simple.yaml b/openapi3/testdata/origin/simple.yaml new file mode 100644 index 00000000..53f30805 --- /dev/null +++ b/openapi3/testdata/origin/simple.yaml @@ -0,0 +1,19 @@ +openapi: 3.0.1 +info: + title: Test API + version: v1 +paths: + /partner-api/test/some-method: + get: + tags: + - Test + responses: + "200": + description: Success + /partner-api/test/another-method: + get: + tags: + - Test + responses: + "200": + description: Success From 7145b2c03f7fd4f622d9419f2304345ec00f0db0 Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Wed, 23 Oct 2024 16:41:57 +0300 Subject: [PATCH 05/19] openapi3: reference originating locations in YAML specs - step 2 (#1024) * add origin to example * remove unneeded test * add origin to xml * document origin extra field in example --- .github/docs/openapi3.txt | 2 + openapi3/example.go | 2 + openapi3/origin_test.go | 91 ++++++++++++++++++++------- openapi3/testdata/origin/example.yaml | 19 ++++++ openapi3/testdata/origin/xml.yaml | 26 ++++++++ openapi3/xml.go | 2 + 6 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 openapi3/testdata/origin/example.yaml create mode 100644 openapi3/testdata/origin/xml.yaml diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 44ab4d66..a1631dff 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -468,6 +468,7 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding type Example struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -2240,6 +2241,7 @@ type ValidationOptions struct { type XML struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` diff --git a/openapi3/example.go b/openapi3/example.go index 9d38e434..56aee378 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -10,6 +10,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object type Example struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -59,6 +60,7 @@ func (example *Example) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "summary") delete(x.Extensions, "description") delete(x.Extensions, "value") diff --git a/openapi3/origin_test.go b/openapi3/origin_test.go index 56d8f1d4..d955056a 100644 --- a/openapi3/origin_test.go +++ b/openapi3/origin_test.go @@ -2,33 +2,11 @@ package openapi3 import ( "context" - "fmt" - "os" "testing" "github.com/stretchr/testify/require" ) -func TestOrigin_All(t *testing.T) { - loader := NewLoader() - loader.IsExternalRefsAllowed = true - loader.IncludeOrigin = true - loader.Context = context.Background() - - const dir = "testdata/origin/" - items, _ := os.ReadDir(dir) - for _, item := range items { - t.Run(item.Name(), func(t *testing.T) { - doc, err := loader.LoadFromFile(fmt.Sprintf("%s/%s", dir, item.Name())) - require.NoError(t, err) - if doc.Paths == nil { - t.Skip("no paths") - } - require.NotEmpty(t, doc.Paths.Origin) - }) - } -} - func TestOrigin_Info(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true @@ -301,3 +279,72 @@ func TestOrigin_Security(t *testing.T) { }, base.Flows.Implicit.Origin.Fields["authorizationUrl"]) } + +func TestOrigin_Example(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/example.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/subscribe").Post.RequestBody.Value.Content["application/json"].Examples["bar"].Value + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 14, + Column: 15, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 15, + Column: 17, + }, + base.Origin.Fields["summary"]) + + // Note: + // Example.Value contains an extra field: "origin". + // + // Explanation: + // The example value is defined in the original yaml file as a json object: {"bar": "baz"} + // This json object is also valid in YAML, so yaml.3 decodes it as a map and adds an "origin" field. + require.Contains(t, + base.Value, + originKey) +} + +func TestOrigin_XML(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/xml.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/subscribe").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["name"].Value.XML + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 21, + Column: 19, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 22, + Column: 21, + }, + base.Origin.Fields["namespace"]) + + require.Equal(t, + Location{ + Line: 23, + Column: 21, + }, + base.Origin.Fields["prefix"]) +} diff --git a/openapi3/testdata/origin/example.yaml b/openapi3/testdata/origin/example.yaml new file mode 100644 index 00000000..8b50ef26 --- /dev/null +++ b/openapi3/testdata/origin/example.yaml @@ -0,0 +1,19 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + examples: + bar: + summary: A bar example + value: {"bar": "baz"} + responses: + "200": + description: OK diff --git a/openapi3/testdata/origin/xml.yaml b/openapi3/testdata/origin/xml.yaml new file mode 100644 index 00000000..3193f32f --- /dev/null +++ b/openapi3/testdata/origin/xml.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + id: + type: integer + format: int32 + xml: + attribute: true + name: + type: string + xml: + namespace: http://example.com/schema/sample + prefix: sample + responses: + "200": + description: OK diff --git a/openapi3/xml.go b/openapi3/xml.go index 69d1b348..7acd8188 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -9,6 +9,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object type XML struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` @@ -58,6 +59,7 @@ func (xml *XML) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "namespace") delete(x.Extensions, "prefix") From 4d76e26bdf370a61812e614271792903836304dc Mon Sep 17 00:00:00 2001 From: John Gresty <1815269+jgresty@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:46:43 +0100 Subject: [PATCH 06/19] openapi3: process discriminator mapping values as refs (#1022) * openapi3: make StringMap generic over value This allows us to support more use cases than just a string, allowing us to attach more metadata onto these StringMap types that we cannot encode in just a string. * openapi3: process discriminator mapping values as refs While the type of the discriminator mapping values is a string in the upstream specs, it contains a jsonschema reference to a schema object. It is surprising behaviour that these refs are not handled when calling functions such as InternalizeRefs. This patch adds the data structures to store the ref internally, and updates the Loader and InternalizeRefs to handle this case. There may be several more functions that need to be updated that I am not aware of. Since it is not a full Ref object we have to do some fudging to make it work with all the existing ref handling code. --- .github/docs/openapi3.txt | 25 +++++-- openapi3/discriminator.go | 18 ++++- openapi3/internalize_refs.go | 10 +++ openapi3/internalize_refs_test.go | 1 + openapi3/loader.go | 10 +++ openapi3/schema.go | 4 +- openapi3/security_scheme.go | 8 +- openapi3/stringmap.go | 6 +- openapi3/testdata/discriminator.yml | 24 ++++++ .../discriminator.yml.internalized.yml | 75 +++++++++++++++++++ openapi3/testdata/ext.yml | 17 +++++ 11 files changed, 179 insertions(+), 19 deletions(-) create mode 100644 openapi3/testdata/discriminator.yml create mode 100644 openapi3/testdata/discriminator.yml.internalized.yml create mode 100644 openapi3/testdata/ext.yml diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index a1631dff..b63a994b 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -411,8 +411,8 @@ type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` + PropertyName string `json:"propertyName" yaml:"propertyName"` // required + Mapping StringMap[MappingRef] `json:"mapping,omitempty" yaml:"mapping,omitempty"` } Discriminator is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object @@ -831,6 +831,15 @@ type Location struct { } Location is a struct that contains the location of a field. +type MappingRef SchemaRef + MappingRef is a ref to a Schema objects. Unlike SchemaRefs it is serialised + as a plain string instead of an object with a $ref key, as such it also does + not support extensions. + +func (mr MappingRef) MarshalText() ([]byte, error) + +func (mr *MappingRef) UnmarshalText(data []byte) error + type MediaType struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` @@ -913,10 +922,10 @@ type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes StringMap `json:"scopes" yaml:"scopes"` // required + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + Scopes StringMap[string] `json:"scopes" yaml:"scopes"` // required } OAuthFlow is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object @@ -2067,11 +2076,11 @@ func NewRegexpFormatValidator(pattern string) StringFormatValidator NewRegexpFormatValidator creates a new FormatValidator that uses a regular expression to validate the value. -type StringMap map[string]string +type StringMap[V any] map[string]V StringMap is a map[string]string that ignores the origin in the underlying json representation. -func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) +func (stringMap *StringMap[V]) UnmarshalJSON(data []byte) (err error) UnmarshalJSON sets StringMap to a copy of data. type T struct { diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index a8ab07b4..8d0d9426 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -11,8 +11,22 @@ type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` + PropertyName string `json:"propertyName" yaml:"propertyName"` // required + Mapping StringMap[MappingRef] `json:"mapping,omitempty" yaml:"mapping,omitempty"` +} + +// MappingRef is a ref to a Schema objects. Unlike SchemaRefs it is serialised +// as a plain string instead of an object with a $ref key, as such it also does +// not support extensions. +type MappingRef SchemaRef + +func (mr *MappingRef) UnmarshalText(data []byte) error { + mr.Ref = string(data) + return nil +} + +func (mr MappingRef) MarshalText() ([]byte, error) { + return []byte(mr.Ref), nil } // MarshalJSON returns the JSON encoding of Discriminator. diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index 01f5dad8..da33dacd 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -351,6 +351,16 @@ func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsEx } } } + // Discriminator mapping values are special cases since they are not full + // ref objects but are string references to schema objects. + if s.Discriminator != nil && s.Discriminator.Mapping != nil { + for k, mapRef := range s.Discriminator.Mapping { + s2 := (*SchemaRef)(&mapRef) + isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal) + doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal) + s.Discriminator.Mapping[k] = mapRef + } + } for _, name := range componentNames(s.Properties) { s2 := s.Properties[name] diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go index 6e9853ac..967ebca5 100644 --- a/openapi3/internalize_refs_test.go +++ b/openapi3/internalize_refs_test.go @@ -25,6 +25,7 @@ func TestInternalizeRefs(t *testing.T) { {"testdata/issue831/testref.internalizepath.openapi.yml"}, {"testdata/issue959/openapi.yml"}, {"testdata/interalizationNameCollision/api.yml"}, + {"testdata/discriminator.yml"}, } for _, test := range tests { diff --git a/openapi3/loader.go b/openapi3/loader.go index 0f3b8cbd..4a828866 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -953,6 +953,16 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } } + // Discriminator mapping refs are a special case since they are not full + // ref objects but are plain strings that reference schema objects. + if value.Discriminator != nil && value.Discriminator.Mapping != nil { + for k, v := range value.Discriminator.Mapping { + if err := loader.resolveSchemaRef(doc, (*SchemaRef)(&v), documentPath, visited); err != nil { + return err + } + value.Discriminator.Mapping[k] = v + } + } return nil } diff --git a/openapi3/schema.go b/openapi3/schema.go index 10a00eac..c1c7d23f 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1298,7 +1298,7 @@ func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, valu func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, value any) (err error, run bool) { var visitedOneOf, visitedAnyOf, visitedAllOf bool if v := schema.OneOf; len(v) > 0 { - var discriminatorRef string + var discriminatorRef MappingRef if schema.Discriminator != nil { pn := schema.Discriminator.PropertyName if valuemap, okcheck := value.(map[string]any); okcheck { @@ -1344,7 +1344,7 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val return foundUnresolvedRef(item.Ref), false } - if discriminatorRef != "" && discriminatorRef != item.Ref { + if discriminatorRef.Ref != "" && discriminatorRef.Ref != item.Ref { continue } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 7bc889de..21674df1 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -322,10 +322,10 @@ type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes StringMap `json:"scopes" yaml:"scopes"` // required + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + Scopes StringMap[string] `json:"scopes" yaml:"scopes"` // required } // MarshalJSON returns the JSON encoding of OAuthFlow. diff --git a/openapi3/stringmap.go b/openapi3/stringmap.go index 354a4c8e..b1987c0c 100644 --- a/openapi3/stringmap.go +++ b/openapi3/stringmap.go @@ -3,11 +3,11 @@ package openapi3 import "encoding/json" // StringMap is a map[string]string that ignores the origin in the underlying json representation. -type StringMap map[string]string +type StringMap[V any] map[string]V // UnmarshalJSON sets StringMap to a copy of data. -func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) { - *stringMap, _, err = unmarshalStringMap[string](data) +func (stringMap *StringMap[V]) UnmarshalJSON(data []byte) (err error) { + *stringMap, _, err = unmarshalStringMap[V](data) return } diff --git a/openapi3/testdata/discriminator.yml b/openapi3/testdata/discriminator.yml new file mode 100644 index 00000000..f9ff8846 --- /dev/null +++ b/openapi3/testdata/discriminator.yml @@ -0,0 +1,24 @@ +openapi: 3.1.0 +info: + title: foo + version: 1.0.0 +paths: + /: + get: + operationId: list + responses: + "200": + description: list + content: + application/json: + schema: + type: array + items: + oneOf: + - $ref: "./ext.yml#/schemas/Foo" + - $ref: "./ext.yml#/schemas/Bar" + discriminator: + propertyName: cat + mapping: + foo: "./ext.yml#/schemas/Foo" + bar: "./ext.yml#/schemas/Bar" diff --git a/openapi3/testdata/discriminator.yml.internalized.yml b/openapi3/testdata/discriminator.yml.internalized.yml new file mode 100644 index 00000000..ecd6e4ed --- /dev/null +++ b/openapi3/testdata/discriminator.yml.internalized.yml @@ -0,0 +1,75 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "foo", + "version": "1.0.0" + }, + "paths": { + "/": { + "get": { + "operationId": "list", + "responses": { + "200": { + "description": "list", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ext_schemas_Foo" + }, + { + "$ref": "#/components/schemas/ext_schemas_Bar" + } + ], + "discriminator": { + "propertyName": "cat", + "mapping": { + "foo": "#/components/schemas/ext_schemas_Foo", + "bar": "#/components/schemas/ext_schemas_Bar" + } + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ext_schemas_Foo": { + "type": "object", + "properties": { + "cat": { + "type": "string", + "enum": [ + "foo" + ] + }, + "name": { + "type": "string" + } + } + }, + "ext_schemas_Bar": { + "type": "object", + "properties": { + "cat": { + "type": "string", + "enum": [ + "bar" + ] + }, + "other": { + "type": "string" + } + } + } + } + } +} diff --git a/openapi3/testdata/ext.yml b/openapi3/testdata/ext.yml new file mode 100644 index 00000000..21a7a557 --- /dev/null +++ b/openapi3/testdata/ext.yml @@ -0,0 +1,17 @@ +schemas: + Foo: + type: object + properties: + cat: + type: string + enum: [ "foo" ] + name: + type: string + Bar: + type: object + properties: + cat: + type: string + enum: [ "bar" ] + other: + type: string From c41a06863174e3aa1f5516108781b8c081c9611c Mon Sep 17 00:00:00 2001 From: Oliver Li Date: Sun, 3 Nov 2024 18:04:31 -0500 Subject: [PATCH 07/19] openapi3filter: register decoder for other JSON content types (#1026) --- openapi3filter/req_resp_decoder.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index e151c9ee..19e3609c 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -1264,6 +1264,9 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, func init() { RegisterBodyDecoder("application/json", JSONBodyDecoder) RegisterBodyDecoder("application/json-patch+json", JSONBodyDecoder) + RegisterBodyDecoder("application/ld+json", JSONBodyDecoder) + RegisterBodyDecoder("application/hal+json", JSONBodyDecoder) + RegisterBodyDecoder("application/vnd.api+json", JSONBodyDecoder) RegisterBodyDecoder("application/octet-stream", FileBodyDecoder) RegisterBodyDecoder("application/problem+json", JSONBodyDecoder) RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder) From 344c7d3b13479360c0d041a0069ab37bd6f92ad0 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Fri, 8 Nov 2024 22:31:26 +0100 Subject: [PATCH 08/19] Revert "openapi3: process discriminator mapping values as refs" (#1029) This reverts commit 4d76e26bdf370a61812e614271792903836304dc. --- .github/docs/openapi3.txt | 25 ++----- openapi3/discriminator.go | 18 +---- openapi3/internalize_refs.go | 10 --- openapi3/internalize_refs_test.go | 1 - openapi3/loader.go | 10 --- openapi3/schema.go | 4 +- openapi3/security_scheme.go | 8 +- openapi3/stringmap.go | 6 +- openapi3/testdata/discriminator.yml | 24 ------ .../discriminator.yml.internalized.yml | 75 ------------------- openapi3/testdata/ext.yml | 17 ----- 11 files changed, 19 insertions(+), 179 deletions(-) delete mode 100644 openapi3/testdata/discriminator.yml delete mode 100644 openapi3/testdata/discriminator.yml.internalized.yml delete mode 100644 openapi3/testdata/ext.yml diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index b63a994b..a1631dff 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -411,8 +411,8 @@ type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping StringMap[MappingRef] `json:"mapping,omitempty" yaml:"mapping,omitempty"` + PropertyName string `json:"propertyName" yaml:"propertyName"` // required + Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` } Discriminator is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object @@ -831,15 +831,6 @@ type Location struct { } Location is a struct that contains the location of a field. -type MappingRef SchemaRef - MappingRef is a ref to a Schema objects. Unlike SchemaRefs it is serialised - as a plain string instead of an object with a $ref key, as such it also does - not support extensions. - -func (mr MappingRef) MarshalText() ([]byte, error) - -func (mr *MappingRef) UnmarshalText(data []byte) error - type MediaType struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` @@ -922,10 +913,10 @@ type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes StringMap[string] `json:"scopes" yaml:"scopes"` // required + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + Scopes StringMap `json:"scopes" yaml:"scopes"` // required } OAuthFlow is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object @@ -2076,11 +2067,11 @@ func NewRegexpFormatValidator(pattern string) StringFormatValidator NewRegexpFormatValidator creates a new FormatValidator that uses a regular expression to validate the value. -type StringMap[V any] map[string]V +type StringMap map[string]string StringMap is a map[string]string that ignores the origin in the underlying json representation. -func (stringMap *StringMap[V]) UnmarshalJSON(data []byte) (err error) +func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) UnmarshalJSON sets StringMap to a copy of data. type T struct { diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 8d0d9426..a8ab07b4 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -11,22 +11,8 @@ type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping StringMap[MappingRef] `json:"mapping,omitempty" yaml:"mapping,omitempty"` -} - -// MappingRef is a ref to a Schema objects. Unlike SchemaRefs it is serialised -// as a plain string instead of an object with a $ref key, as such it also does -// not support extensions. -type MappingRef SchemaRef - -func (mr *MappingRef) UnmarshalText(data []byte) error { - mr.Ref = string(data) - return nil -} - -func (mr MappingRef) MarshalText() ([]byte, error) { - return []byte(mr.Ref), nil + PropertyName string `json:"propertyName" yaml:"propertyName"` // required + Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` } // MarshalJSON returns the JSON encoding of Discriminator. diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index da33dacd..01f5dad8 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -351,16 +351,6 @@ func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsEx } } } - // Discriminator mapping values are special cases since they are not full - // ref objects but are string references to schema objects. - if s.Discriminator != nil && s.Discriminator.Mapping != nil { - for k, mapRef := range s.Discriminator.Mapping { - s2 := (*SchemaRef)(&mapRef) - isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal) - doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal) - s.Discriminator.Mapping[k] = mapRef - } - } for _, name := range componentNames(s.Properties) { s2 := s.Properties[name] diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go index 967ebca5..6e9853ac 100644 --- a/openapi3/internalize_refs_test.go +++ b/openapi3/internalize_refs_test.go @@ -25,7 +25,6 @@ func TestInternalizeRefs(t *testing.T) { {"testdata/issue831/testref.internalizepath.openapi.yml"}, {"testdata/issue959/openapi.yml"}, {"testdata/interalizationNameCollision/api.yml"}, - {"testdata/discriminator.yml"}, } for _, test := range tests { diff --git a/openapi3/loader.go b/openapi3/loader.go index 4a828866..0f3b8cbd 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -953,16 +953,6 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } } - // Discriminator mapping refs are a special case since they are not full - // ref objects but are plain strings that reference schema objects. - if value.Discriminator != nil && value.Discriminator.Mapping != nil { - for k, v := range value.Discriminator.Mapping { - if err := loader.resolveSchemaRef(doc, (*SchemaRef)(&v), documentPath, visited); err != nil { - return err - } - value.Discriminator.Mapping[k] = v - } - } return nil } diff --git a/openapi3/schema.go b/openapi3/schema.go index c1c7d23f..10a00eac 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1298,7 +1298,7 @@ func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, valu func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, value any) (err error, run bool) { var visitedOneOf, visitedAnyOf, visitedAllOf bool if v := schema.OneOf; len(v) > 0 { - var discriminatorRef MappingRef + var discriminatorRef string if schema.Discriminator != nil { pn := schema.Discriminator.PropertyName if valuemap, okcheck := value.(map[string]any); okcheck { @@ -1344,7 +1344,7 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val return foundUnresolvedRef(item.Ref), false } - if discriminatorRef.Ref != "" && discriminatorRef.Ref != item.Ref { + if discriminatorRef != "" && discriminatorRef != item.Ref { continue } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 21674df1..7bc889de 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -322,10 +322,10 @@ type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes StringMap[string] `json:"scopes" yaml:"scopes"` // required + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + Scopes StringMap `json:"scopes" yaml:"scopes"` // required } // MarshalJSON returns the JSON encoding of OAuthFlow. diff --git a/openapi3/stringmap.go b/openapi3/stringmap.go index b1987c0c..354a4c8e 100644 --- a/openapi3/stringmap.go +++ b/openapi3/stringmap.go @@ -3,11 +3,11 @@ package openapi3 import "encoding/json" // StringMap is a map[string]string that ignores the origin in the underlying json representation. -type StringMap[V any] map[string]V +type StringMap map[string]string // UnmarshalJSON sets StringMap to a copy of data. -func (stringMap *StringMap[V]) UnmarshalJSON(data []byte) (err error) { - *stringMap, _, err = unmarshalStringMap[V](data) +func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) { + *stringMap, _, err = unmarshalStringMap[string](data) return } diff --git a/openapi3/testdata/discriminator.yml b/openapi3/testdata/discriminator.yml deleted file mode 100644 index f9ff8846..00000000 --- a/openapi3/testdata/discriminator.yml +++ /dev/null @@ -1,24 +0,0 @@ -openapi: 3.1.0 -info: - title: foo - version: 1.0.0 -paths: - /: - get: - operationId: list - responses: - "200": - description: list - content: - application/json: - schema: - type: array - items: - oneOf: - - $ref: "./ext.yml#/schemas/Foo" - - $ref: "./ext.yml#/schemas/Bar" - discriminator: - propertyName: cat - mapping: - foo: "./ext.yml#/schemas/Foo" - bar: "./ext.yml#/schemas/Bar" diff --git a/openapi3/testdata/discriminator.yml.internalized.yml b/openapi3/testdata/discriminator.yml.internalized.yml deleted file mode 100644 index ecd6e4ed..00000000 --- a/openapi3/testdata/discriminator.yml.internalized.yml +++ /dev/null @@ -1,75 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "foo", - "version": "1.0.0" - }, - "paths": { - "/": { - "get": { - "operationId": "list", - "responses": { - "200": { - "description": "list", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/ext_schemas_Foo" - }, - { - "$ref": "#/components/schemas/ext_schemas_Bar" - } - ], - "discriminator": { - "propertyName": "cat", - "mapping": { - "foo": "#/components/schemas/ext_schemas_Foo", - "bar": "#/components/schemas/ext_schemas_Bar" - } - } - } - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "ext_schemas_Foo": { - "type": "object", - "properties": { - "cat": { - "type": "string", - "enum": [ - "foo" - ] - }, - "name": { - "type": "string" - } - } - }, - "ext_schemas_Bar": { - "type": "object", - "properties": { - "cat": { - "type": "string", - "enum": [ - "bar" - ] - }, - "other": { - "type": "string" - } - } - } - } - } -} diff --git a/openapi3/testdata/ext.yml b/openapi3/testdata/ext.yml deleted file mode 100644 index 21a7a557..00000000 --- a/openapi3/testdata/ext.yml +++ /dev/null @@ -1,17 +0,0 @@ -schemas: - Foo: - type: object - properties: - cat: - type: string - enum: [ "foo" ] - name: - type: string - Bar: - type: object - properties: - cat: - type: string - enum: [ "bar" ] - other: - type: string From 4fda0cc458f2eed890cf7e034c7a6838d3f8f5a1 Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Fri, 8 Nov 2024 23:33:29 +0200 Subject: [PATCH 09/19] openapi3: fail to load spec because of schema names in mapping (#1027) --- openapi3/mapping_test.go | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 openapi3/mapping_test.go diff --git a/openapi3/mapping_test.go b/openapi3/mapping_test.go new file mode 100644 index 00000000..0c1c1995 --- /dev/null +++ b/openapi3/mapping_test.go @@ -0,0 +1,58 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMapping(t *testing.T) { + + schema := ` +openapi: 3.0.0 +info: + title: ACME + version: 1.0.0 +components: + schemas: + Pet: + type: object + required: + - petType + properties: + petType: + type: string + discriminator: + propertyName: petType + mapping: + dog: Dog + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + # all other properties specific to a Cat + properties: + name: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + # all other properties specific to a Dog + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + # all other properties specific to a Lizard + properties: + lovesRocks: + type: boolean +` + loader := NewLoader() + loader.IsExternalRefsAllowed = true + _, err := loader.LoadFromData([]byte(schema)) + require.NoError(t, err) +} From e230c133e5cab2b61903b2234826941e4601b3ba Mon Sep 17 00:00:00 2001 From: jayanth-tatina-groww <113498330+jayanth-tatina-groww@users.noreply.github.com> Date: Thu, 14 Nov 2024 02:36:24 +0530 Subject: [PATCH 10/19] openapi2conv: convert schemaRef for additional props (#1030) --- openapi2conv/issue1016_test.go | 81 ++++++++++++++++++++++++++++++++++ openapi2conv/openapi2_conv.go | 4 ++ 2 files changed, 85 insertions(+) create mode 100644 openapi2conv/issue1016_test.go diff --git a/openapi2conv/issue1016_test.go b/openapi2conv/issue1016_test.go new file mode 100644 index 00000000..acd87d07 --- /dev/null +++ b/openapi2conv/issue1016_test.go @@ -0,0 +1,81 @@ +package openapi2conv + +import ( + "context" + "encoding/json" + "testing" + + "github.com/getkin/kin-openapi/openapi2" + "github.com/stretchr/testify/require" +) + +func TestIssue1016(t *testing.T) { + v2 := []byte(` +{ + "basePath": "/v2", + "host": "test.example.com", + "info": { + "title": "MyAPI", + "version": "0.1", + "x-info": "info extension" + }, + "paths": { + "/foo": { + "get": { + "operationId": "getFoo", + "responses": { + "200": { + "description": "returns all information", + "schema": { + "$ref": "#/definitions/PetDirectory" + } + }, + "default": { + "description": "OK" + } + }, + "summary": "get foo" + } + } + }, + "schemes": [ + "http" + ], + "swagger": "2.0", + "definitions": { + "Pet": { + "type": "object", + "required": ["petType"], + "properties": { + "petType": { + "type": "string" + }, + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + }, + "PetDirectory":{ + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Pet" + } + } + } +} +`) + + var doc2 openapi2.T + err := json.Unmarshal(v2, &doc2) + require.NoError(t, err) + + doc3, err := v2v3YAML(v2) + require.NoError(t, err) + + err = doc3.Validate(context.Background()) + require.NoError(t, err) + require.Equal(t, "#/components/schemas/Pet", doc3.Components.Schemas["PetDirectory"].Value.AdditionalProperties.Schema.Ref) +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index ef0a5edd..ec7646d2 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -516,6 +516,10 @@ func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef { AdditionalProperties: schema.Value.AdditionalProperties, } + if schema.Value.AdditionalProperties.Schema != nil { + v3Schema.AdditionalProperties.Schema.Ref = ToV3Ref(schema.Value.AdditionalProperties.Schema.Ref) + } + if schema.Value.Discriminator != "" { v3Schema.Discriminator = &openapi3.Discriminator{ PropertyName: schema.Value.Discriminator, From 02a0d6fb611d6e543ed132f9493f7dbfb3f0c0cc Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Tue, 26 Nov 2024 20:07:45 +0200 Subject: [PATCH 11/19] openapi3: simplify by replacing math.Min with min (#1032) --- openapi3/server.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openapi3/server.go b/openapi3/server.go index 5a817e51..e438e920 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "math" "net/url" "sort" "strings" @@ -174,7 +173,7 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) { } else if ns < 0 { i = np } else { - i = int(math.Min(float64(np), float64(ns))) + i = min(np, ns) } if i < 0 { i = len(input) From be48da5ce74c067be2dfbef240d2594828beb7b0 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Tue, 26 Nov 2024 20:08:31 +0200 Subject: [PATCH 12/19] openapi3: fix deprecation comments (#1034) --- .github/docs/openapi3.txt | 11 +++++++---- openapi3/schema_formats.go | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index a1631dff..4729e0c3 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -128,13 +128,16 @@ func DefineNumberFormatValidator(name string, validator NumberFormatValidator) number format. func DefineStringFormat(name string, pattern string) - DefineStringFormat defines a regexp pattern for a given string - format Deprecated: Use openapi3.DefineStringFormatValidator(name, + DefineStringFormat defines a regexp pattern for a given string format + + Deprecated: Use openapi3.DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) instead. func DefineStringFormatCallback(name string, callback func(string) error) - DefineStringFormatCallback defines a callback function for a given - string format Deprecated: Use openapi3.DefineStringFormatValidator(name, + DefineStringFormatCallback defines a callback function for a given string + format + + Deprecated: Use openapi3.DefineStringFormatValidator(name, NewCallbackValidator(fn)) instead. func DefineStringFormatValidator(name string, validator StringFormatValidator) diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 023c2669..1b76707f 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -131,12 +131,14 @@ func DefineIntegerFormatValidator(name string, validator IntegerFormatValidator) } // DefineStringFormat defines a regexp pattern for a given string format +// // Deprecated: Use openapi3.DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) instead. func DefineStringFormat(name string, pattern string) { DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) } // DefineStringFormatCallback defines a callback function for a given string format +// // Deprecated: Use openapi3.DefineStringFormatValidator(name, NewCallbackValidator(fn)) instead. func DefineStringFormatCallback(name string, callback func(string) error) { DefineStringFormatValidator(name, NewCallbackValidator(callback)) From 793b28df2a1ec5b007a2da6d0918a80f2f70d487 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Tue, 26 Nov 2024 20:09:35 +0200 Subject: [PATCH 13/19] test: fix expected-actual parameters in require.Equal (#1035) --- openapi2conv/issue187_test.go | 6 +++--- openapi3/encoding_test.go | 2 +- openapi3/issue652_test.go | 4 ++-- openapi3/loader_relative_refs_test.go | 4 ++-- openapi3/response_issue224_test.go | 2 +- openapi3filter/validate_request_test.go | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go index 93914d9f..ffd020b6 100644 --- a/openapi2conv/issue187_test.go +++ b/openapi2conv/issue187_test.go @@ -104,7 +104,7 @@ func TestIssue187(t *testing.T) { spec3, err := json.Marshal(doc3) require.NoError(t, err) const expected = `{"components":{"schemas":{"model.ProductSearchAttributeRequest":{"properties":{"filterField":{"type":"string"},"filterKey":{"type":"string"},"type":{"type":"string"},"values":{"$ref":"#/components/schemas/model.ProductSearchAttributeValueRequest"}},"title":"model.ProductSearchAttributeRequest","type":"object"},"model.ProductSearchAttributeValueRequest":{"properties":{"imageUrl":{"type":"string"},"text":{"type":"string"}},"title":"model.ProductSearchAttributeValueRequest","type":"object"}}},"info":{"contact":{"email":"test@test.com","name":"Test"},"description":"Test Golang Application","title":"Test","version":"1.0"},"openapi":"3.0.3","paths":{"/me":{"get":{"operationId":"someTest","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/model.ProductSearchAttributeRequest"}}},"description":"successful operation"}},"summary":"Some test","tags":["probe"]}}}}` - require.JSONEq(t, string(spec3), expected) + require.JSONEq(t, expected, string(spec3)) err = doc3.Validate(context.Background()) require.NoError(t, err) @@ -163,7 +163,7 @@ paths: "200": description: description ` - require.YAMLEq(t, string(spec3), expected) + require.YAMLEq(t, expected, string(spec3)) err = doc3.Validate(context.Background()) require.NoError(t, err) @@ -190,5 +190,5 @@ securityDefinitions: doc2, err := FromV3(doc3) require.NoError(t, err) - require.Equal(t, doc2.SecurityDefinitions["OAuth2Application"].Flow, "application") + require.Equal(t, "application", doc2.SecurityDefinitions["OAuth2Application"].Flow) } diff --git a/openapi3/encoding_test.go b/openapi3/encoding_test.go index 955b21b2..cbf4738e 100644 --- a/openapi3/encoding_test.go +++ b/openapi3/encoding_test.go @@ -91,7 +91,7 @@ func TestEncodingSerializationMethod(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got := tc.enc.SerializationMethod() - require.EqualValues(t, got, tc.want, "got %#v, want %#v", got, tc.want) + require.EqualValues(t, tc.want, got, "got %#v, want %#v", got, tc.want) }) } } diff --git a/openapi3/issue652_test.go b/openapi3/issue652_test.go index 4036b481..738569a4 100644 --- a/openapi3/issue652_test.go +++ b/openapi3/issue652_test.go @@ -23,7 +23,7 @@ func TestIssue652(t *testing.T) { require.Contains(t, spec.Components.Schemas, schemaName) schema := spec.Components.Schemas[schemaName] - assert.Equal(t, schema.Ref, "../definitions.yml#/components/schemas/TestSchema") - assert.Equal(t, schema.Value.Type, &openapi3.Types{"string"}) + assert.Equal(t, "../definitions.yml#/components/schemas/TestSchema", schema.Ref) + assert.Equal(t, &openapi3.Types{"string"}, schema.Value.Type) }) } diff --git a/openapi3/loader_relative_refs_test.go b/openapi3/loader_relative_refs_test.go index cca44ba3..05f9d0e4 100644 --- a/openapi3/loader_relative_refs_test.go +++ b/openapi3/loader_relative_refs_test.go @@ -927,7 +927,7 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { require.Equal(t, "example request", nestedDirPath.Patch.RequestBody.Value.Description) // check response schema and example - require.Equal(t, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type, &Types{"string"}) + require.Equal(t, &Types{"string"}, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) expectedExample := "hello" require.Equal(t, expectedExample, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value) @@ -948,5 +948,5 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { // check response schema and example require.Equal(t, &Types{"string"}, moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) - require.Equal(t, moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value, expectedExample) + require.Equal(t, expectedExample, moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value) } diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go index 2edf894d..265f88a2 100644 --- a/openapi3/response_issue224_test.go +++ b/openapi3/response_issue224_test.go @@ -458,7 +458,7 @@ func TestEmptyResponsesAreInvalid(t *testing.T) { doc, err := loader.LoadFromData(spec) require.NoError(t, err) - require.Equal(t, doc.ExternalDocs.Description, "See AsyncAPI example") + require.Equal(t, "See AsyncAPI example", doc.ExternalDocs.Description) err = doc.Validate(context.Background()) require.EqualError(t, err, `invalid paths: invalid path /pet: invalid operation POST: the responses object MUST contain at least one response code`) diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index 474fab36..82b09129 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -212,7 +212,7 @@ components: assert.NoError(t, err, "unable to read request body: %v", err) assert.Equal(t, contentLen, bodySize, "expect ContentLength %d to equal body size %d", contentLen, bodySize) bodyModified := originalBodySize != bodySize - assert.Equal(t, bodyModified, tc.expectedModification, "expect request body modification happened: %t, expected %t", bodyModified, tc.expectedModification) + assert.Equal(t, tc.expectedModification, bodyModified, "expect request body modification happened: %t, expected %t", bodyModified, tc.expectedModification) validationInput.Request.Body, err = validationInput.Request.GetBody() assert.NoError(t, err, "unable to re-generate body by GetBody(): %v", err) From d819171639b6ddc051a6e8a7fdeffac8e9a75d55 Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Wed, 11 Dec 2024 23:58:18 +0200 Subject: [PATCH 14/19] use forked yaml modules without "replace" (#1038) * fix: thoroughly replace yaml packages with oasdiff's Signed-off-by: Pierre Fenoll * require updated yaml modules * go mod tidy --------- Signed-off-by: Pierre Fenoll Co-authored-by: Pierre Fenoll --- cmd/validate/main.go | 2 +- go.mod | 9 +++------ go.sum | 11 +++++++---- openapi2/marsh.go | 2 +- openapi2/openapi2_test.go | 2 +- openapi2conv/issue187_test.go | 2 +- openapi2conv/issue558_test.go | 2 +- openapi3/additionalProperties_test.go | 2 +- openapi3/issue241_test.go | 2 +- openapi3/issue883_test.go | 20 ++++++++++---------- openapi3/issue972_test.go | 2 +- openapi3/marsh.go | 2 +- openapi3/openapi3_test.go | 2 +- openapi3/schema_test.go | 2 +- openapi3filter/req_resp_decoder.go | 2 +- 15 files changed, 32 insertions(+), 32 deletions(-) diff --git a/cmd/validate/main.go b/cmd/validate/main.go index 874f30c2..75194070 100644 --- a/cmd/validate/main.go +++ b/cmd/validate/main.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" diff --git a/go.mod b/go.mod index 7d370323..48550601 100644 --- a/go.mod +++ b/go.mod @@ -2,18 +2,14 @@ module github.com/getkin/kin-openapi go 1.22.5 -replace gopkg.in/yaml.v3 => github.com/oasdiff/yaml3 v0.0.0-20240920135353-c185dc6ea7c6 - -replace github.com/invopop/yaml => github.com/oasdiff/yaml v0.0.0-20240920191703-3e5a9fb5bdf3 - require ( github.com/go-openapi/jsonpointer v0.21.0 github.com/gorilla/mux v1.8.0 - github.com/invopop/yaml v0.3.1 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 github.com/perimeterx/marshmallow v1.1.5 github.com/stretchr/testify v1.9.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -23,4 +19,5 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f9643cd0..422452cf 100644 --- a/go.sum +++ b/go.sum @@ -18,10 +18,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/oasdiff/yaml v0.0.0-20240920191703-3e5a9fb5bdf3 h1:nqCxALSUgWobWkFGIrhLRzR/bpImQdGj+3JS4/scTJo= -github.com/oasdiff/yaml v0.0.0-20240920191703-3e5a9fb5bdf3/go.mod h1:AOyUNV9ElKz7EEZeBm/48U54UtjtgCMT9fFbZEsClQc= -github.com/oasdiff/yaml3 v0.0.0-20240920135353-c185dc6ea7c6 h1:+ZsuDTdapTJxfMQk7SOJiNMg0v36pui01L7FEO615r8= -github.com/oasdiff/yaml3 v0.0.0-20240920135353-c185dc6ea7c6/go.mod h1:lqlOfJRrYpgeWHQj+ky2tf7UJ3PzgHTHRQEpc90nbp0= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -32,5 +32,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/openapi2/marsh.go b/openapi2/marsh.go index 1a4b0d1d..1745734b 100644 --- a/openapi2/marsh.go +++ b/openapi2/marsh.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" ) func unmarshalError(jsonUnmarshalErr error) error { diff --git a/openapi2/openapi2_test.go b/openapi2/openapi2_test.go index 8a6bf968..d1b3aa56 100644 --- a/openapi2/openapi2_test.go +++ b/openapi2/openapi2_test.go @@ -6,7 +6,7 @@ import ( "os" "reflect" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/getkin/kin-openapi/openapi2" ) diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go index ffd020b6..4cec8753 100644 --- a/openapi2conv/issue187_test.go +++ b/openapi2conv/issue187_test.go @@ -5,7 +5,7 @@ import ( "encoding/json" "testing" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/stretchr/testify/require" "github.com/getkin/kin-openapi/openapi2" diff --git a/openapi2conv/issue558_test.go b/openapi2conv/issue558_test.go index 20673012..b7fb9dc5 100644 --- a/openapi2conv/issue558_test.go +++ b/openapi2conv/issue558_test.go @@ -3,7 +3,7 @@ package openapi2conv import ( "testing" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/stretchr/testify/require" ) diff --git a/openapi3/additionalProperties_test.go b/openapi3/additionalProperties_test.go index 7a9ec747..46cf0dbe 100644 --- a/openapi3/additionalProperties_test.go +++ b/openapi3/additionalProperties_test.go @@ -6,8 +6,8 @@ import ( "os" "testing" + "github.com/oasdiff/yaml3" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "github.com/getkin/kin-openapi/openapi3" ) diff --git a/openapi3/issue241_test.go b/openapi3/issue241_test.go index 0b37b0a9..9a643ede 100644 --- a/openapi3/issue241_test.go +++ b/openapi3/issue241_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" + "github.com/oasdiff/yaml3" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "github.com/getkin/kin-openapi/openapi3" ) diff --git a/openapi3/issue883_test.go b/openapi3/issue883_test.go index 28d71638..7609e6ac 100644 --- a/openapi3/issue883_test.go +++ b/openapi3/issue883_test.go @@ -3,9 +3,9 @@ package openapi3_test import ( "testing" - invopopYaml "github.com/invopop/yaml" + yaml "github.com/oasdiff/yaml" + yamlv3 "github.com/oasdiff/yaml3" "github.com/stretchr/testify/require" - v3 "gopkg.in/yaml.v3" "github.com/getkin/kin-openapi/openapi3" ) @@ -38,8 +38,8 @@ paths: require.NoError(t, err) require.NotNil(t, doc.Paths) - t.Run("Roundtrip invopop/yaml", func(t *testing.T) { - justPaths, err := invopopYaml.Marshal(doc.Paths) + t.Run("Roundtrip using yaml pkg", func(t *testing.T) { + justPaths, err := yaml.Marshal(doc.Paths) require.NoError(t, err) require.NotNil(t, doc.Paths) require.YAMLEq(t, ` @@ -51,13 +51,13 @@ paths: description: OK `[1:], string(justPaths)) - marshalledYaml, err := invopopYaml.Marshal(doc) + marshalledYaml, err := yaml.Marshal(doc) require.NoError(t, err) require.NotNil(t, doc.Paths) require.YAMLEq(t, spec, string(marshalledYaml)) var newDoc openapi3.T - err = invopopYaml.Unmarshal(marshalledYaml, &newDoc) + err = yaml.Unmarshal(marshalledYaml, &newDoc) require.NoError(t, err) require.NotNil(t, newDoc.Paths) require.Equal(t, doc, &newDoc) @@ -76,7 +76,7 @@ paths: description: OK `[1:], string(justPaths)) - justPaths, err = v3.Marshal(doc.Paths) + justPaths, err = yamlv3.Marshal(doc.Paths) require.NoError(t, err) require.NotNil(t, doc.Paths) require.YAMLEq(t, ` @@ -88,14 +88,14 @@ paths: description: OK `[1:], string(justPaths)) - marshalledYaml, err := v3.Marshal(doc) + marshalledYaml, err := yamlv3.Marshal(doc) require.NoError(t, err) require.NotNil(t, doc.Paths) require.YAMLEq(t, spec, string(marshalledYaml)) - t.Skip("TODO: impl https://pkg.go.dev/gopkg.in/yaml.v3#Unmarshaler on maplike types") + t.Skip("TODO: impl https://pkg.go.dev/github.com/oasdiff/yaml3#Unmarshaler on maplike types") var newDoc openapi3.T - err = v3.Unmarshal(marshalledYaml, &newDoc) + err = yamlv3.Unmarshal(marshalledYaml, &newDoc) require.NoError(t, err) require.NotNil(t, newDoc.Paths) require.Equal(t, doc, &newDoc) diff --git a/openapi3/issue972_test.go b/openapi3/issue972_test.go index 3575adc9..5aaaae22 100644 --- a/openapi3/issue972_test.go +++ b/openapi3/issue972_test.go @@ -3,8 +3,8 @@ package openapi3 import ( "testing" + "github.com/oasdiff/yaml3" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" ) func TestIssue972(t *testing.T) { diff --git a/openapi3/marsh.go b/openapi3/marsh.go index 9fdc6dff..2f00828a 100644 --- a/openapi3/marsh.go +++ b/openapi3/marsh.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" ) func unmarshalError(jsonUnmarshalErr error) error { diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 3b68c169..bb42df56 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index d678361b..4332983c 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" + "github.com/oasdiff/yaml3" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) type schemaExample struct { diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 19e3609c..c77289ff 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -17,7 +17,7 @@ import ( "strconv" "strings" - "gopkg.in/yaml.v3" + "github.com/oasdiff/yaml3" "github.com/getkin/kin-openapi/openapi3" ) From b82c647146a1ed15497d7027b07ca4c357e850e7 Mon Sep 17 00:00:00 2001 From: Alexis Goodfellow Date: Wed, 11 Dec 2024 17:01:19 -0500 Subject: [PATCH 15/19] openapi3: update date schema formats to not match months or days of '00' (#1042) * Update date schema formats to not match months or days of '00' * Update schema_issue492_test.go to look for correct DateTime format in error output * Add test cases for '00' months and days in date and date-time objects * Change date/time validation test cases to use EqualError * Fix hour/minute/second matches as well - also update tests --- .github/docs/openapi3.txt | 4 +- openapi3/datetime_schema_test.go | 167 +++++++++++++++++++++++++++++++ openapi3/schema_formats.go | 4 +- openapi3/schema_issue492_test.go | 2 +- 4 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 openapi3/datetime_schema_test.go diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 4729e0c3..47aaeadf 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -35,10 +35,10 @@ const ( FormatOfStringByte = `(^$|^[a-zA-Z0-9+/\-_]*=*$)` // FormatOfStringDate is a RFC3339 date format regexp, for example "2017-07-21". - FormatOfStringDate = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$` + FormatOfStringDate = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])$` // FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z". - FormatOfStringDateTime = `^[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})?$` + FormatOfStringDateTime = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$` ) const ( SerializationSimple = "simple" diff --git a/openapi3/datetime_schema_test.go b/openapi3/datetime_schema_test.go new file mode 100644 index 00000000..fec60576 --- /dev/null +++ b/openapi3/datetime_schema_test.go @@ -0,0 +1,167 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +var DateSpec = []byte(` +components: + schemas: + Server: + properties: + date: + $ref: "#/components/schemas/timestamp" + name: + type: string + type: object + timestamp: + type: string + format: date +openapi: "3.0.1" +paths: {} +info: + version: 1.1.1 + title: title +`[1:]) + +var DateTimeSpec = []byte(` +components: + schemas: + Server: + properties: + datetime: + $ref: "#/components/schemas/timestamp" + name: + type: string + type: object + timestamp: + type: string + format: date-time +openapi: "3.0.1" +paths: {} +info: + version: 1.1.1 + title: title +`[1:]) + +func TestDateZeroMonth(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "date": "2001-00-03", + }) + require.EqualError(t, err, `Error at "/date": string doesn't match the format "date": string doesn't match pattern "`+FormatOfStringDate+`"`) +} + +func TestDateZeroDay(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "date": "2001-02-00", + }) + require.EqualError(t, err, `Error at "/date": string doesn't match the format "date": string doesn't match pattern "`+FormatOfStringDate+`"`) +} + +func TestDateTimeZeroMonth(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2001-00-03T04:05:06.789Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} + +func TestDateTimeZeroDay(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2001-02-00T04:05:06.789Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} + +func TestDateTimeLeapSecond(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2016-12-31T23:59:60.000Z", // exact time of the most recent leap second + }) + require.NoError(t, err) +} + +func TestDateTimeHourOutOfBounds(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2016-12-31T24:00:00.000Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} + +func TestDateTimeMinuteOutOfBounds(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2016-12-31T23:60:00.000Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} + +func TestDateTimeSecondOutOfBounds(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2016-12-31T23:59:61.000Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 1b76707f..489105b4 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -41,10 +41,10 @@ const ( FormatOfStringByte = `(^$|^[a-zA-Z0-9+/\-_]*=*$)` // FormatOfStringDate is a RFC3339 date format regexp, for example "2017-07-21". - FormatOfStringDate = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$` + FormatOfStringDate = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])$` // FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z". - FormatOfStringDateTime = `^[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})?$` + FormatOfStringDateTime = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$` ) func init() { diff --git a/openapi3/schema_issue492_test.go b/openapi3/schema_issue492_test.go index 2fdfcbce..15156722 100644 --- a/openapi3/schema_issue492_test.go +++ b/openapi3/schema_issue492_test.go @@ -46,5 +46,5 @@ info: "name": "kin-openapi", "time": "2001-02-03T04:05:06:789Z", }) - require.ErrorContains(t, err, `Error at "/time": string doesn't match the format "date-time": string doesn't match pattern "^[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})?$"`) + require.EqualError(t, err, `Error at "/time": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) } From 20441ea16ff8894389a5966fee2be217d2845fb4 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Thu, 12 Dec 2024 00:02:47 +0200 Subject: [PATCH 16/19] openapi3,openapi3filter: replace interface{} with any (#1040) --- openapi3/issue82_test.go | 2 +- openapi3/refs_issue247_test.go | 2 +- openapi3filter/validate_request.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openapi3/issue82_test.go b/openapi3/issue82_test.go index 3bf242bc..27b72e28 100644 --- a/openapi3/issue82_test.go +++ b/openapi3/issue82_test.go @@ -8,7 +8,7 @@ import ( ) func TestIssue82(t *testing.T) { - payload := map[string]interface{}{ + payload := map[string]any{ "prop1": "val", "prop3": "val", } diff --git a/openapi3/refs_issue247_test.go b/openapi3/refs_issue247_test.go index 62f056d8..ae6d0c36 100644 --- a/openapi3/refs_issue247_test.go +++ b/openapi3/refs_issue247_test.go @@ -120,7 +120,7 @@ components: require.NoError(t, err) var ptr jsonpointer.Pointer - var v interface{} + var v any var kind reflect.Kind ptr, err = jsonpointer.New("/paths") diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index bf4771a9..42df3176 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -114,7 +114,7 @@ func appendToQueryValues[T any](q url.Values, parameterName string, v []T) { // populateDefaultQueryParameters populates default values inside query parameters, while ensuring types are respected func populateDefaultQueryParameters(q url.Values, parameterName string, value any) { switch t := value.(type) { - case []interface{}: + case []any: appendToQueryValues(q, parameterName, t) default: q.Add(parameterName, fmt.Sprintf("%v", value)) From 325cecc5e4e13ad531ac01968b82e8f6cb4095b3 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Thu, 12 Dec 2024 00:03:47 +0200 Subject: [PATCH 17/19] openapi3filter: simplify ValidateRequest implementation (#1041) --- .github/docs/openapi3filter.txt | 2 +- openapi3filter/validate_request.go | 36 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt index d44ba5ab..1eb4d41e 100644 --- a/.github/docs/openapi3filter.txt +++ b/.github/docs/openapi3filter.txt @@ -93,7 +93,7 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param defined. The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema. -func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err error) +func ValidateRequest(ctx context.Context, input *RequestValidationInput) error 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. diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 42df3176..a28fbccd 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -29,7 +29,7 @@ var ErrInvalidEmptyValue = errors.New("empty value is not allowed") // // Note: One can tune the behavior of uniqueItems: true verification // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker -func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err error) { +func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { var me openapi3.MultiError options := input.Options @@ -49,10 +49,10 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er security = &route.Spec.Security } if security != nil { - if err = ValidateSecurityRequirements(ctx, input, *security); err != nil && !options.MultiError { - return - } - if err != nil { + if err := ValidateSecurityRequirements(ctx, input, *security); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -66,10 +66,10 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er } } - if err = ValidateParameter(ctx, input, parameter); err != nil && !options.MultiError { - return - } - if err != nil { + if err := ValidateParameter(ctx, input, parameter); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -79,10 +79,10 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er if options.ExcludeRequestQueryParams && parameter.Value.In == openapi3.ParameterInQuery { continue } - if err = ValidateParameter(ctx, input, parameter.Value); err != nil && !options.MultiError { - return - } - if err != nil { + if err := ValidateParameter(ctx, input, parameter.Value); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -90,10 +90,10 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er // RequestBody requestBody := operation.RequestBody if requestBody != nil && !options.ExcludeRequestBody { - if err = ValidateRequestBody(ctx, input, requestBody.Value); err != nil && !options.MultiError { - return - } - if err != nil { + if err := ValidateRequestBody(ctx, input, requestBody.Value); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -101,7 +101,7 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er if len(me) > 0 { return me } - return + return nil } // appendToQueryValues adds to query parameters each value in the provided slice From f476f7b5c45634be488e9c09e67dab24251a54f7 Mon Sep 17 00:00:00 2001 From: Aleksey Mikhaylov Date: Tue, 24 Dec 2024 13:18:14 +0300 Subject: [PATCH 18/19] openapi3filter: validation of `x-www-form-urlencoded` with arbitrary nested allOf (#1046) * improper validation of x-www-form-urlencoded with arbitrary nested allOf (#1045) * extend test cases --------- Co-authored-by: Aleksey Mikhaylov --- openapi3filter/issue1045_test.go | 142 +++++++++++++++++++++++++++++ openapi3filter/req_resp_decoder.go | 24 ++--- 2 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 openapi3filter/issue1045_test.go diff --git a/openapi3filter/issue1045_test.go b/openapi3filter/issue1045_test.go new file mode 100644 index 00000000..048dbd37 --- /dev/null +++ b/openapi3filter/issue1045_test.go @@ -0,0 +1,142 @@ +package openapi3filter + +import ( + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue1045(t *testing.T) { + spec := ` +openapi: 3.0.3 +info: + version: 1.0.0 + title: sample api + description: api service paths to test the issue +paths: + /api/path: + post: + summary: path + tags: + - api + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/PathRequest' } + application/x-www-form-urlencoded: + schema: { $ref: '#/components/schemas/PathRequest' } + responses: + '200': + description: Ok + content: + application/json: + schema: { $ref: '#/components/schemas/PathResponse' } +components: + schemas: + Msg_Opt: + properties: + msg: { type: string } + Msg: + allOf: + - $ref: '#/components/schemas/Msg_Opt' + - required: [ msg ] + Name: + properties: + name: { type: string } + required: [ name ] + Id: + properties: + id: + type: string + format: uint64 + required: [ id ] + PathRequest: + type: object + allOf: + - $ref: '#/components/schemas/Msg' + - $ref: '#/components/schemas/Name' + PathResponse: + type: object + allOf: + - $ref: '#/components/schemas/PathRequest' + - $ref: '#/components/schemas/Id' + `[1:] + + loader := openapi3.NewLoader() + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + for _, testcase := range []struct { + name string + endpoint string + ct string + data string + shouldFail bool + }{ + { + name: "json success", + endpoint: "/api/path", + ct: "application/json", + data: `{"msg":"message", "name":"some+name"}`, + shouldFail: false, + }, + { + name: "json failure", + endpoint: "/api/path", + ct: "application/json", + data: `{"name":"some+name"}`, + shouldFail: true, + }, + + // application/x-www-form-urlencoded + { + name: "form success", + endpoint: "/api/path", + ct: "application/x-www-form-urlencoded", + data: "msg=message&name=some+name", + shouldFail: false, + }, + { + name: "form failure", + endpoint: "/api/path", + ct: "application/x-www-form-urlencoded", + data: "name=some+name", + shouldFail: true, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + data := strings.NewReader(testcase.data) + req, err := http.NewRequest("POST", testcase.endpoint, data) + require.NoError(t, err) + req.Header.Add("Content-Type", testcase.ct) + + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + validationInput := &RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + } + err = ValidateRequest(loader.Context, validationInput) + if testcase.shouldFail { + require.Error(t, err, "This test case should fail "+testcase.data) + } else { + require.NoError(t, err, "This test case should pass "+testcase.data) + } + }) + } +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index c77289ff..998a91f8 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -1340,18 +1340,6 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. obj := make(map[string]any) dec := &urlValuesDecoder{values: values} - // Decode schema constructs (allOf, anyOf, oneOf) - if err := decodeSchemaConstructs(dec, schema.Value.AllOf, obj, encFn); err != nil { - return nil, err - } - if err := decodeSchemaConstructs(dec, schema.Value.AnyOf, obj, encFn); err != nil { - return nil, err - } - if err := decodeSchemaConstructs(dec, schema.Value.OneOf, obj, encFn); err != nil { - return nil, err - } - - // Decode properties from the main schema if err := decodeSchemaConstructs(dec, []*openapi3.SchemaRef{schema}, obj, encFn); err != nil { return nil, err } @@ -1363,6 +1351,18 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. // This function is for decoding purposes only and not for validation. func decodeSchemaConstructs(dec *urlValuesDecoder, schemas []*openapi3.SchemaRef, obj map[string]any, encFn EncodingFn) error { for _, schemaRef := range schemas { + + // Decode schema constructs (allOf, anyOf, oneOf) + if err := decodeSchemaConstructs(dec, schemaRef.Value.AllOf, obj, encFn); err != nil { + return err + } + if err := decodeSchemaConstructs(dec, schemaRef.Value.AnyOf, obj, encFn); err != nil { + return err + } + if err := decodeSchemaConstructs(dec, schemaRef.Value.OneOf, obj, encFn); err != nil { + return err + } + for name, prop := range schemaRef.Value.Properties { value, _, err := decodeProperty(dec, name, prop, encFn) if err != nil { From cea0a13b906a708102947f95b9b09d631ff60976 Mon Sep 17 00:00:00 2001 From: Travis Newhouse <12889757+travisnewhouse@users.noreply.github.com> Date: Tue, 24 Dec 2024 02:20:21 -0800 Subject: [PATCH 19/19] openapi2conv: convert references in nested additionalProperties schemas (#1047) * Convert references in nested additionalProperties schemas When the schema specified in additionalProperties contains another nested schema with additionalProperties, the references must be converted at all of levels of the nested schemas. * make toV3AdditionalProperties a non-exported function --- openapi2conv/openapi2_conv.go | 27 ++++-- openapi2conv/openapi2_conv_test.go | 128 +++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 5 deletions(-) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index ec7646d2..8aa45255 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -513,11 +513,7 @@ func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef { MaxProps: schema.Value.MaxProps, AllOf: make(openapi3.SchemaRefs, len(schema.Value.AllOf)), Properties: make(openapi3.Schemas), - AdditionalProperties: schema.Value.AdditionalProperties, - } - - if schema.Value.AdditionalProperties.Schema != nil { - v3Schema.AdditionalProperties.Schema.Ref = ToV3Ref(schema.Value.AdditionalProperties.Schema.Ref) + AdditionalProperties: toV3AdditionalProperties(schema.Value.AdditionalProperties), } if schema.Value.Discriminator != "" { @@ -551,6 +547,27 @@ func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef { } } +func toV3AdditionalProperties(from openapi3.AdditionalProperties) openapi3.AdditionalProperties { + return openapi3.AdditionalProperties{ + Has: from.Has, + Schema: convertRefsInV3SchemaRef(from.Schema), + } +} + +func convertRefsInV3SchemaRef(from *openapi3.SchemaRef) *openapi3.SchemaRef { + if from == nil { + return nil + } + to := *from + to.Ref = ToV3Ref(to.Ref) + if to.Value != nil { + v := *from.Value + to.Value = &v + to.Value.AdditionalProperties = toV3AdditionalProperties(to.Value.AdditionalProperties) + } + return &to +} + var ref2To3 = map[string]string{ "#/definitions/": "#/components/schemas/", "#/responses/": "#/components/responses/", diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index 31777215..659ccb69 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -65,6 +65,134 @@ func TestConvOpenAPIV2ToV3(t *testing.T) { require.JSONEq(t, exampleV3, string(data)) } +func TestConvOpenAPIV2ToV3WithAdditionalPropertiesSchemaRef(t *testing.T) { + v2 := []byte(` +{ + "basePath": "/v2", + "host": "test.example.com", + "info": { + "title": "MyAPI", + "version": "0.1" + }, + "paths": { + "/foo": { + "get": { + "operationId": "getFoo", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "returns all information", + "schema":{ + "type":"object", + "additionalProperties":{ + "$ref":"#/definitions/Foo" + } + } + } + }, + "summary": "get foo" + } + } + }, + "definitions": { + "Foo": { + "type": "object", + "properties": { + "a": { + "type": "string" + } + } + } + }, + "schemes": [ + "http" + ], + "swagger": "2.0" +} +`) + + var doc2 openapi2.T + err := json.Unmarshal(v2, &doc2) + require.NoError(t, err) + + doc3, err := ToV3(&doc2) + require.NoError(t, err) + err = doc3.Validate(context.Background()) + require.NoError(t, err) + + responseSchema := doc3.Paths.Value("/foo").Get.Responses.Value("200").Value.Content.Get("application/json").Schema.Value + require.Equal(t, &openapi3.Types{"object"}, responseSchema.Type) + require.Equal(t, "#/components/schemas/Foo", responseSchema.AdditionalProperties.Schema.Ref) +} + +func TestConvOpenAPIV2ToV3WithNestedAdditionalPropertiesSchemaRef(t *testing.T) { + v2 := []byte(` +{ + "basePath": "/v2", + "host": "test.example.com", + "info": { + "title": "MyAPI", + "version": "0.1" + }, + "paths": { + "/foo": { + "get": { + "operationId": "getFoo", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "returns all information", + "schema":{ + "type":"object", + "additionalProperties":{ + "type":"object", + "additionalProperties":{ + "$ref":"#/definitions/Foo" + } + } + } + } + }, + "summary": "get foo" + } + } + }, + "definitions": { + "Foo": { + "type": "object", + "properties": { + "a": { + "type": "string" + } + } + } + }, + "schemes": [ + "http" + ], + "swagger": "2.0" +} +`) + + var doc2 openapi2.T + err := json.Unmarshal(v2, &doc2) + require.NoError(t, err) + + doc3, err := ToV3(&doc2) + require.NoError(t, err) + err = doc3.Validate(context.Background()) + require.NoError(t, err) + + responseSchema := doc3.Paths.Value("/foo").Get.Responses.Value("200").Value.Content.Get("application/json").Schema.Value + require.Equal(t, &openapi3.Types{"object"}, responseSchema.Type) + require.Equal(t, &openapi3.Types{"object"}, responseSchema.AdditionalProperties.Schema.Value.Type) + require.Equal(t, "#/components/schemas/Foo", responseSchema.AdditionalProperties.Schema.Value.AdditionalProperties.Schema.Ref) +} + const exampleV2 = ` { "basePath": "/v2",