From c333b34e43fe1aaeafe23299032470e3f32e02bd Mon Sep 17 00:00:00 2001 From: Reuven Harrison Date: Mon, 21 Oct 2024 22:52:49 +0300 Subject: [PATCH] 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 eddcbfbd9..44ab4d663 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 11bc6d02b..7d370323d 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 6b91d0dc9..f9643cd07 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 9cfd0ffdc..e2915bf41 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 34a6bea35..3ae6da692 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 98c4b96c1..aecf8b648 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 6c76a6fb6..57cc801dd 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 81b070eec..73e301e05 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 16d244008..a8ab07b45 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 1bcdaea5e..7eb507e51 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 f9a7a6b07..9d38e4343 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 bd99511a5..f6794141e 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 dc542874d..6b23db52e 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 e2468285c..acca2f4d4 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 c4f6c8dc3..df9d6c446 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 132f67803..dfd320f1d 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 31c340761..0f3b8cbd3 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 7b8045c67..35b336571 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 daa937551..9fdc6dffb 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 d4466bcf5..d6edaf4d2 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 40abf73c0..7b57e847c 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 000000000..b0e4a9345 --- /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 000000000..56d8f1d44 --- /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 34fe29118..d69bd5f59 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 859634fe6..2c60472a4 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 76747412b..806750474 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 07060731f..d24f4c9b1 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 d337b0e3d..fc11164a5 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 a3f5bdab7..028deba7d 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 6d4b8185e..39288ee77 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 af8fda6f7..d4bfe42e0 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 f81196066..10a00eace 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 87891c954..6af80e8b3 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 b37a6eda2..7bc889dec 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 7a2007f20..5a817e511 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 3819851c9..354a4c8ec 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 182d0502a..5e4086fef 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 000000000..8e865bc92 --- /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 000000000..bfc5ba09e --- /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 000000000..4d4299e2c --- /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 000000000..094363842 --- /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 000000000..7594e961a --- /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 000000000..53f308052 --- /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