From 787fc8f062b7204fed75d14d4c10b01a9b0c5713 Mon Sep 17 00:00:00 2001 From: Jay Shah Date: Tue, 17 Sep 2024 11:19:57 -0400 Subject: [PATCH 1/7] fix: issue unmarshalling when discriminator field is set in openapi2.0 --- .github/docs/openapi3.txt | 2 +- openapi2/issues1010_test.go | 97 ++++++++++++++++++++++++++++++++++ openapi3/discriminator_test.go | 13 ++++- openapi3/schema.go | 32 +++++++++-- 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 openapi2/issues1010_test.go diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index cfccb97aa..0b5b06e35 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -1524,7 +1524,7 @@ type Schema struct { MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` - Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + Discriminator any `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` } Schema is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object diff --git a/openapi2/issues1010_test.go b/openapi2/issues1010_test.go new file mode 100644 index 000000000..dfb6fd2a6 --- /dev/null +++ b/openapi2/issues1010_test.go @@ -0,0 +1,97 @@ +package openapi2 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue1010(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/Pet" + } + }, + "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" + } + }, + "discriminator": "petType" + }, + "Dog": { + "allOf": [ + { + "$ref": "#/definitions/Pet" + }, + { + "type": "object", + "properties": { + "breed": { + "type": "string" + } + } + } + ] + }, + "Cat": { + "allOf": [ + { + "$ref": "#/definitions/Pet" + }, + { + "type": "object", + "properties": { + "color": { + "type": "string" + } + } + } + ] + } + } +} +`) + + var doc2 T + err := json.Unmarshal(v2, &doc2) + require.NoError(t, err) +} diff --git a/openapi3/discriminator_test.go b/openapi3/discriminator_test.go index d56791439..7157fd2bf 100644 --- a/openapi3/discriminator_test.go +++ b/openapi3/discriminator_test.go @@ -1,6 +1,7 @@ package openapi3 import ( + "encoding/json" "testing" "github.com/stretchr/testify/require" @@ -52,5 +53,15 @@ func TestParsingDiscriminator(t *testing.T) { err = doc.Validate(loader.Context) require.NoError(t, err) - require.Len(t, doc.Components.Schemas["MyResponseType"].Value.Discriminator.Mapping, 2) + discriminatorMap, ok := doc.Components.Schemas["MyResponseType"].Value.Discriminator.(map[string]interface{}) + require.True(t, ok) + + marshaledDiscriminator, err := json.Marshal(discriminatorMap) + require.NoError(t, err) + + var discriminator *Discriminator + err = json.Unmarshal(marshaledDiscriminator, &discriminator) + require.NoError(t, err) + + require.Len(t, discriminator.Mapping, 2) } diff --git a/openapi3/schema.go b/openapi3/schema.go index f81196066..b9776ba37 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -129,7 +129,7 @@ type Schema struct { MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` - Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + Discriminator any `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` } type Types []string @@ -1298,7 +1298,33 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val if v := schema.OneOf; len(v) > 0 { var discriminatorRef string if schema.Discriminator != nil { - pn := schema.Discriminator.PropertyName + var discriminator *Discriminator + if descriminatorValuemap, okcheck := schema.Discriminator.(map[string]any); okcheck { + marshaledDiscriminator, err := json.Marshal(descriminatorValuemap) + if err != nil { + return &SchemaError{ + Schema: schema, + SchemaField: "discriminator", + Reason: fmt.Sprintf("unable to marshal the discriminator field in schema: %v", err), + }, false + } + + if err := json.Unmarshal(marshaledDiscriminator, &discriminator); err != nil { + return &SchemaError{ + Schema: schema, + SchemaField: "discriminator", + Reason: fmt.Sprintf("unable to unmarshall the discriminator field in schema: %v", err), + }, false + } + } else { + return &SchemaError{ + Schema: schema, + SchemaField: "discriminator", + Reason: fmt.Sprintf("discriminator is expected to be an object, but received an unknown type"), + }, false + } + + pn := discriminator.PropertyName if valuemap, okcheck := value.(map[string]any); okcheck { discriminatorVal, okcheck := valuemap[pn] if !okcheck { @@ -1319,7 +1345,7 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val }, false } - if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck { + if discriminatorRef, okcheck = discriminator.Mapping[discriminatorValString]; len(discriminator.Mapping) > 0 && !okcheck { return &SchemaError{ Value: discriminatorVal, Schema: schema, From 2e539ed641bac8935388cd679a6cbdd78154669b Mon Sep 17 00:00:00 2001 From: Jay Shah Date: Sun, 22 Sep 2024 20:03:29 -0400 Subject: [PATCH 2/7] revert original approach --- .github/docs/openapi3.txt | 2 +- openapi3/discriminator_test.go | 13 +------------ openapi3/schema.go | 32 +++----------------------------- 3 files changed, 5 insertions(+), 42 deletions(-) diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 0b5b06e35..cfccb97aa 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -1524,7 +1524,7 @@ type Schema struct { MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` - Discriminator any `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` } Schema is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object diff --git a/openapi3/discriminator_test.go b/openapi3/discriminator_test.go index 7157fd2bf..d56791439 100644 --- a/openapi3/discriminator_test.go +++ b/openapi3/discriminator_test.go @@ -1,7 +1,6 @@ package openapi3 import ( - "encoding/json" "testing" "github.com/stretchr/testify/require" @@ -53,15 +52,5 @@ func TestParsingDiscriminator(t *testing.T) { err = doc.Validate(loader.Context) require.NoError(t, err) - discriminatorMap, ok := doc.Components.Schemas["MyResponseType"].Value.Discriminator.(map[string]interface{}) - require.True(t, ok) - - marshaledDiscriminator, err := json.Marshal(discriminatorMap) - require.NoError(t, err) - - var discriminator *Discriminator - err = json.Unmarshal(marshaledDiscriminator, &discriminator) - require.NoError(t, err) - - require.Len(t, discriminator.Mapping, 2) + require.Len(t, doc.Components.Schemas["MyResponseType"].Value.Discriminator.Mapping, 2) } diff --git a/openapi3/schema.go b/openapi3/schema.go index b9776ba37..f81196066 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -129,7 +129,7 @@ type Schema struct { MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` - Discriminator any `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` } type Types []string @@ -1298,33 +1298,7 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val if v := schema.OneOf; len(v) > 0 { var discriminatorRef string if schema.Discriminator != nil { - var discriminator *Discriminator - if descriminatorValuemap, okcheck := schema.Discriminator.(map[string]any); okcheck { - marshaledDiscriminator, err := json.Marshal(descriminatorValuemap) - if err != nil { - return &SchemaError{ - Schema: schema, - SchemaField: "discriminator", - Reason: fmt.Sprintf("unable to marshal the discriminator field in schema: %v", err), - }, false - } - - if err := json.Unmarshal(marshaledDiscriminator, &discriminator); err != nil { - return &SchemaError{ - Schema: schema, - SchemaField: "discriminator", - Reason: fmt.Sprintf("unable to unmarshall the discriminator field in schema: %v", err), - }, false - } - } else { - return &SchemaError{ - Schema: schema, - SchemaField: "discriminator", - Reason: fmt.Sprintf("discriminator is expected to be an object, but received an unknown type"), - }, false - } - - pn := discriminator.PropertyName + pn := schema.Discriminator.PropertyName if valuemap, okcheck := value.(map[string]any); okcheck { discriminatorVal, okcheck := valuemap[pn] if !okcheck { @@ -1345,7 +1319,7 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val }, false } - if discriminatorRef, okcheck = discriminator.Mapping[discriminatorValString]; len(discriminator.Mapping) > 0 && !okcheck { + if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck { return &SchemaError{ Value: discriminatorVal, Schema: schema, From 2db2b3929adb6a9fc22c9b8300a5689ca4633d98 Mon Sep 17 00:00:00 2001 From: Jay Shah Date: Sun, 22 Sep 2024 20:07:22 -0400 Subject: [PATCH 3/7] update with different approach --- openapi2/issues1010_test.go | 1 + openapi3/schema.go | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/openapi2/issues1010_test.go b/openapi2/issues1010_test.go index dfb6fd2a6..9b7a93748 100644 --- a/openapi2/issues1010_test.go +++ b/openapi2/issues1010_test.go @@ -94,4 +94,5 @@ func TestIssue1010(t *testing.T) { var doc2 T err := json.Unmarshal(v2, &doc2) require.NoError(t, err) + require.Equal(t, "petType", doc2.Definitions["Pet"].Value.Discriminator.PropertyName) } diff --git a/openapi3/schema.go b/openapi3/schema.go index f81196066..fc201e500 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -404,12 +404,42 @@ func (schema Schema) MarshalYAML() (any, error) { // UnmarshalJSON sets Schema to a copy of data. func (schema *Schema) UnmarshalJSON(data []byte) error { - type SchemaBis Schema + type Alias Schema + type SchemaBis struct { + Alias + Discriminator json.RawMessage `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + } + var x SchemaBis if err := json.Unmarshal(data, &x); err != nil { return unmarshalError(err) } - _ = json.Unmarshal(data, &x.Extensions) + + type DiscriminatorObject struct { + Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + } + + if len(x.Discriminator) > 0 { + var ds *string + if err := json.Unmarshal(x.Discriminator, &ds); err == nil { + // In OpenAPI 2, the Discriminator is a string field, while in OpenAPI 3, + // it corresponds to the Discriminator.PropertyName field. + // If the OpenAPI 2 Discriminator (ds) is not nil, we assign it to + // the OpenAPI 3 equivalent, which is Discriminator.PropertyName. + if ds != nil { + x.Alias.Discriminator = &Discriminator{ + PropertyName: *ds, + } + } + } else { + var do DiscriminatorObject + if err := json.Unmarshal(x.Discriminator, &do.Discriminator); err == nil && do.Discriminator != nil { + x.Alias.Discriminator = do.Discriminator + } + } + } + + _ = json.Unmarshal(data, &x.Alias.Extensions) delete(x.Extensions, "oneOf") delete(x.Extensions, "anyOf") @@ -464,7 +494,7 @@ func (schema *Schema) UnmarshalJSON(data []byte) error { x.Extensions = nil } - *schema = Schema(x) + *schema = Schema(x.Alias) if schema.Format == "date" { // This is a fix for: https://github.com/getkin/kin-openapi/issues/697 From 4a49549a79efafdb6b6d4e4fa715abf0216ed26a Mon Sep 17 00:00:00 2001 From: Jay Shah Date: Mon, 30 Sep 2024 08:13:22 -0400 Subject: [PATCH 4/7] Revert "update with different approach" This reverts commit 2db2b3929adb6a9fc22c9b8300a5689ca4633d98. --- openapi2/issues1010_test.go | 1 - openapi3/schema.go | 36 +++--------------------------------- 2 files changed, 3 insertions(+), 34 deletions(-) diff --git a/openapi2/issues1010_test.go b/openapi2/issues1010_test.go index 9b7a93748..dfb6fd2a6 100644 --- a/openapi2/issues1010_test.go +++ b/openapi2/issues1010_test.go @@ -94,5 +94,4 @@ func TestIssue1010(t *testing.T) { var doc2 T err := json.Unmarshal(v2, &doc2) require.NoError(t, err) - require.Equal(t, "petType", doc2.Definitions["Pet"].Value.Discriminator.PropertyName) } diff --git a/openapi3/schema.go b/openapi3/schema.go index fc201e500..f81196066 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -404,42 +404,12 @@ func (schema Schema) MarshalYAML() (any, error) { // UnmarshalJSON sets Schema to a copy of data. func (schema *Schema) UnmarshalJSON(data []byte) error { - type Alias Schema - type SchemaBis struct { - Alias - Discriminator json.RawMessage `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` - } - + type SchemaBis Schema var x SchemaBis if err := json.Unmarshal(data, &x); err != nil { return unmarshalError(err) } - - type DiscriminatorObject struct { - Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` - } - - if len(x.Discriminator) > 0 { - var ds *string - if err := json.Unmarshal(x.Discriminator, &ds); err == nil { - // In OpenAPI 2, the Discriminator is a string field, while in OpenAPI 3, - // it corresponds to the Discriminator.PropertyName field. - // If the OpenAPI 2 Discriminator (ds) is not nil, we assign it to - // the OpenAPI 3 equivalent, which is Discriminator.PropertyName. - if ds != nil { - x.Alias.Discriminator = &Discriminator{ - PropertyName: *ds, - } - } - } else { - var do DiscriminatorObject - if err := json.Unmarshal(x.Discriminator, &do.Discriminator); err == nil && do.Discriminator != nil { - x.Alias.Discriminator = do.Discriminator - } - } - } - - _ = json.Unmarshal(data, &x.Alias.Extensions) + _ = json.Unmarshal(data, &x.Extensions) delete(x.Extensions, "oneOf") delete(x.Extensions, "anyOf") @@ -494,7 +464,7 @@ func (schema *Schema) UnmarshalJSON(data []byte) error { x.Extensions = nil } - *schema = Schema(x.Alias) + *schema = Schema(x) if schema.Format == "date" { // This is a fix for: https://github.com/getkin/kin-openapi/issues/697 From 913e56aa8e18366ad7673cdbb2bef9cdbfe9bb1d Mon Sep 17 00:00:00 2001 From: Jay Shah Date: Mon, 30 Sep 2024 17:41:05 -0400 Subject: [PATCH 5/7] v2 schema with discriminator field set as string --- openapi2/helpers.go | 15 ++ openapi2/issues1010_test.go | 1 + openapi2/openapi2.go | 30 ++-- openapi2/parameter.go | 46 +++--- openapi2/ref.go | 9 ++ openapi2/refs.go | 104 +++++++++++++ openapi2/response.go | 8 +- openapi2/schema.go | 269 ++++++++++++++++++++++++++++++++++ openapi2conv/openapi2_conv.go | 196 +++++++++++++++++++------ 9 files changed, 588 insertions(+), 90 deletions(-) create mode 100644 openapi2/helpers.go create mode 100644 openapi2/ref.go create mode 100644 openapi2/refs.go create mode 100644 openapi2/schema.go diff --git a/openapi2/helpers.go b/openapi2/helpers.go new file mode 100644 index 000000000..777a2f0bb --- /dev/null +++ b/openapi2/helpers.go @@ -0,0 +1,15 @@ +package openapi2 + +import ( + "net/url" +) + +// copyURI makes a copy of the pointer. +func copyURI(u *url.URL) *url.URL { + if u == nil { + return nil + } + + c := *u // shallow-copy + return &c +} diff --git a/openapi2/issues1010_test.go b/openapi2/issues1010_test.go index dfb6fd2a6..2f84dc946 100644 --- a/openapi2/issues1010_test.go +++ b/openapi2/issues1010_test.go @@ -94,4 +94,5 @@ func TestIssue1010(t *testing.T) { var doc2 T err := json.Unmarshal(v2, &doc2) require.NoError(t, err) + require.Equal(t, "petType", doc2.Definitions["Pet"].Value.Discriminator) } diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 2d922c639..bd3375339 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -10,21 +10,21 @@ import ( type T struct { Extensions map[string]any `json:"-" yaml:"-"` - Swagger string `json:"swagger" yaml:"swagger"` // required - Info openapi3.Info `json:"info" yaml:"info"` // required - ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` - Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` - Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` - Host string `json:"host,omitempty" yaml:"host,omitempty"` - BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` - Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` - Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` - Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` - SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` - Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` - Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + Swagger string `json:"swagger" yaml:"swagger"` // required + Info openapi3.Info `json:"info" yaml:"info"` // required + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` + Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` + Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` + Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` + Definitions map[string]*SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` + Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` + SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` + Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` + Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } // MarshalJSON returns the JSON encoding of T. diff --git a/openapi2/parameter.go b/openapi2/parameter.go index c701705bb..7f2bddb2c 100644 --- a/openapi2/parameter.go +++ b/openapi2/parameter.go @@ -28,29 +28,29 @@ type Parameter struct { Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` - Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` - Format string `json:"format,omitempty" yaml:"format,omitempty"` - Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` - ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` - ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` - Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` - Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` - Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` - MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` - MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` - MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` - MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` - Default any `json:"default,omitempty" yaml:"default,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` + Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` } // MarshalJSON returns the JSON encoding of Parameter. diff --git a/openapi2/ref.go b/openapi2/ref.go new file mode 100644 index 000000000..477aa99e3 --- /dev/null +++ b/openapi2/ref.go @@ -0,0 +1,9 @@ +package openapi2 + +//go:generate go run refsgenerator.go + +// 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 // TODO: Update this Link +type Ref struct { + Ref string `json:"$ref" yaml:"$ref"` +} diff --git a/openapi2/refs.go b/openapi2/refs.go new file mode 100644 index 000000000..5109e9883 --- /dev/null +++ b/openapi2/refs.go @@ -0,0 +1,104 @@ +package openapi2 + +import ( + "encoding/json" + "net/url" + "sort" + "strings" + + "github.com/go-openapi/jsonpointer" + "github.com/perimeterx/marshmallow" +) + +// SchemaRef represents either a Schema or a $ref to a Schema. +// When serializing and both fields are set, Ref is preferred over Value. +type SchemaRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + + Ref string + Value *Schema + extra []string + + refPath *url.URL +} + +var _ jsonpointer.JSONPointable = (*SchemaRef)(nil) + +func (x *SchemaRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } + +// RefString returns the $ref value. +func (x *SchemaRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *SchemaRef) CollectionName() string { return "schemas" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *SchemaRef) RefPath() *url.URL { return copyURI(x.refPath) } + +func (x *SchemaRef) setRefPath(u *url.URL) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { + return + } + + x.refPath = copyURI(u) +} + +// MarshalYAML returns the YAML encoding of SchemaRef. +func (x SchemaRef) MarshalYAML() (any, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value.MarshalYAML() +} + +// MarshalJSON returns the JSON encoding of SchemaRef. +func (x SchemaRef) MarshalJSON() ([]byte, error) { + y, err := x.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(y) +} + +// UnmarshalJSON sets SchemaRef to a copy of data. +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 + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } + } + return nil + } + return json.Unmarshal(data, &x.Value) +} + +// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (x *SchemaRef) JSONLookup(token string) (any, error) { + if token == "$ref" { + return x.Ref, nil + } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + + ptr, _, err := jsonpointer.GetForToken(x.Value, token) + return ptr, err +} diff --git a/openapi2/response.go b/openapi2/response.go index 5306beb15..3a2983ce1 100644 --- a/openapi2/response.go +++ b/openapi2/response.go @@ -11,10 +11,10 @@ type Response struct { Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` - Examples map[string]any `json:"examples,omitempty" yaml:"examples,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Examples map[string]any `json:"examples,omitempty" yaml:"examples,omitempty"` } // MarshalJSON returns the JSON encoding of Response. diff --git a/openapi2/schema.go b/openapi2/schema.go new file mode 100644 index 000000000..64bed2751 --- /dev/null +++ b/openapi2/schema.go @@ -0,0 +1,269 @@ +package openapi2 + +import ( + "encoding/json" + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +type ( + Schemas map[string]*SchemaRef + SchemaRefs []*SchemaRef +) + +// Schema is specified by OpenAPI/Swagger 2.0 standard. +// See https://swagger.io/specification/v2/#schema-object +type Schema struct { + Extensions map[string]any `json:"-" yaml:"-"` + + AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` + Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` + Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + + // Array-related, here for struct compactness + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + // Number-related, here for struct compactness + ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + // Properties + ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + XML *openapi3.XML `json:"xml,omitempty" yaml:"xml,omitempty"` + + // Number + Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Max *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + + // String + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + + // Array + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + + // Object + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` + MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + AdditionalProperties openapi3.AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + Discriminator string `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` +} + +// MarshalJSON returns the JSON encoding of Schema. +func (schema Schema) MarshalJSON() ([]byte, error) { + m, err := schema.MarshalYAML() + if err != nil { + return nil, err + } + + return json.Marshal(m) +} + +// MarshalYAML returns the YAML encoding of Schema. +func (schema Schema) MarshalYAML() (any, error) { + m := make(map[string]any, 36+len(schema.Extensions)) + for k, v := range schema.Extensions { + m[k] = v + } + + if x := schema.AllOf; len(x) != 0 { + m["allOf"] = x + } + if x := schema.Not; x != nil { + m["not"] = x + } + if x := schema.Type; x != nil { + m["type"] = x + } + if x := schema.Title; len(x) != 0 { + m["title"] = x + } + if x := schema.Format; len(x) != 0 { + m["format"] = x + } + if x := schema.Description; len(x) != 0 { + m["description"] = x + } + if x := schema.Enum; len(x) != 0 { + m["enum"] = x + } + if x := schema.Default; x != nil { + m["default"] = x + } + if x := schema.Example; x != nil { + m["example"] = x + } + if x := schema.ExternalDocs; x != nil { + m["externalDocs"] = x + } + + // Array-related + if x := schema.UniqueItems; x { + m["uniqueItems"] = x + } + // Number-related + if x := schema.ExclusiveMin; x { + m["exclusiveMinimum"] = x + } + if x := schema.ExclusiveMax; x { + m["exclusiveMaximum"] = x + } + if x := schema.ReadOnly; x { + m["readOnly"] = x + } + if x := schema.WriteOnly; x { + m["writeOnly"] = x + } + if x := schema.AllowEmptyValue; x { + m["allowEmptyValue"] = x + } + if x := schema.Deprecated; x { + m["deprecated"] = x + } + if x := schema.XML; x != nil { + m["xml"] = x + } + + // Number + if x := schema.Min; x != nil { + m["minimum"] = x + } + if x := schema.Max; x != nil { + m["maximum"] = x + } + if x := schema.MultipleOf; x != nil { + m["multipleOf"] = x + } + + // String + if x := schema.MinLength; x != 0 { + m["minLength"] = x + } + if x := schema.MaxLength; x != nil { + m["maxLength"] = x + } + if x := schema.Pattern; x != "" { + m["pattern"] = x + } + + // Array + if x := schema.MinItems; x != 0 { + m["minItems"] = x + } + if x := schema.MaxItems; x != nil { + m["maxItems"] = x + } + if x := schema.Items; x != nil { + m["items"] = x + } + + // Object + if x := schema.Required; len(x) != 0 { + m["required"] = x + } + if x := schema.Properties; len(x) != 0 { + m["properties"] = x + } + if x := schema.MinProps; x != 0 { + m["minProperties"] = x + } + if x := schema.MaxProps; x != nil { + m["maxProperties"] = x + } + if x := schema.AdditionalProperties; x.Has != nil || x.Schema != nil { + m["additionalProperties"] = &x + } + if x := schema.Discriminator; x != "" { + m["discriminator"] = x + } + + return m, nil +} + +// UnmarshalJSON sets Schema to a copy of data. +func (schema *Schema) UnmarshalJSON(data []byte) error { + type SchemaBis Schema + var x SchemaBis + if err := json.Unmarshal(data, &x); err != nil { + return unmarshalError(err) + } + _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, "oneOf") + delete(x.Extensions, "anyOf") + delete(x.Extensions, "allOf") + delete(x.Extensions, "not") + delete(x.Extensions, "type") + delete(x.Extensions, "title") + delete(x.Extensions, "format") + delete(x.Extensions, "description") + delete(x.Extensions, "enum") + delete(x.Extensions, "default") + delete(x.Extensions, "example") + delete(x.Extensions, "externalDocs") + + // Array-related + delete(x.Extensions, "uniqueItems") + // Number-related + delete(x.Extensions, "exclusiveMinimum") + delete(x.Extensions, "exclusiveMaximum") + // Properties + delete(x.Extensions, "nullable") + delete(x.Extensions, "readOnly") + delete(x.Extensions, "writeOnly") + delete(x.Extensions, "allowEmptyValue") + delete(x.Extensions, "deprecated") + delete(x.Extensions, "xml") + + // Number + delete(x.Extensions, "minimum") + delete(x.Extensions, "maximum") + delete(x.Extensions, "multipleOf") + + // String + delete(x.Extensions, "minLength") + delete(x.Extensions, "maxLength") + delete(x.Extensions, "pattern") + + // Array + delete(x.Extensions, "minItems") + delete(x.Extensions, "maxItems") + delete(x.Extensions, "items") + + // Object + delete(x.Extensions, "required") + delete(x.Extensions, "properties") + delete(x.Extensions, "minProperties") + delete(x.Extensions, "maxProperties") + delete(x.Extensions, "additionalProperties") + delete(x.Extensions, "discriminator") + + if len(x.Extensions) == 0 { + x.Extensions = nil + } + + *schema = Schema(x) + + if schema.Format == "date" { + // This is a fix for: https://github.com/getkin/kin-openapi/issues/697 + if eg, ok := schema.Example.(string); ok { + schema.Example = strings.TrimSuffix(eg, "T00:00:00Z") + } + } + return nil +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index fa161bcad..ef0a5eddc 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -272,7 +272,6 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete MinLength: parameter.MinLength, MaxLength: parameter.MaxLength, Default: parameter.Default, - Items: parameter.Items, MinItems: parameter.MinItems, MaxItems: parameter.MaxItems, Pattern: parameter.Pattern, @@ -281,6 +280,10 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete MultipleOf: parameter.MultipleOf, Required: required, }} + if parameter.Items != nil { + schemaRef.Value.Items = ToV3SchemaRef(parameter.Items) + } + schemaRefMap := make(map[string]*openapi3.SchemaRef, 1) schemaRefMap[parameter.Name] = schemaRef return nil, nil, schemaRefMap, nil @@ -301,7 +304,7 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete Description: parameter.Description, Required: required, Extensions: stripNonExtensions(parameter.Extensions), - Schema: ToV3SchemaRef(&openapi3.SchemaRef{Value: &openapi3.Schema{ + Schema: ToV3SchemaRef(&openapi2.SchemaRef{Value: &openapi2.Schema{ Type: parameter.Type, Format: parameter.Format, Enum: parameter.Enum, @@ -349,7 +352,7 @@ func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool, c sort.Strings(requireds) schema := &openapi3.Schema{ Type: &openapi3.Types{"object"}, - Properties: ToV3Schemas(bodies), + Properties: bodies, Required: requireds, } return &openapi3.RequestBodyRef{ @@ -456,7 +459,7 @@ func ToV3Headers(defs map[string]*openapi2.Header) openapi3.Headers { return headers } -func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef { +func ToV3Schemas(defs map[string]*openapi2.SchemaRef) map[string]*openapi3.SchemaRef { schemas := make(map[string]*openapi3.SchemaRef, len(defs)) for name, schema := range defs { schemas[name] = ToV3SchemaRef(schema) @@ -464,34 +467,84 @@ func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.Schem return schemas } -func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { +func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef { + if schema == nil { + return &openapi3.SchemaRef{} + } + if ref := schema.Ref; ref != "" { return &openapi3.SchemaRef{Ref: ToV3Ref(ref)} } + if schema.Value == nil { - return schema + return &openapi3.SchemaRef{ + Extensions: schema.Extensions, + } + } + + v3Schema := &openapi3.Schema{ + Extensions: schema.Extensions, + Type: schema.Value.Type, + Title: schema.Value.Title, + Format: schema.Value.Format, + Description: schema.Value.Description, + Enum: schema.Value.Enum, + Default: schema.Value.Default, + Example: schema.Value.Example, + ExternalDocs: schema.Value.ExternalDocs, + UniqueItems: schema.Value.UniqueItems, + ExclusiveMin: schema.Value.ExclusiveMin, + ExclusiveMax: schema.Value.ExclusiveMax, + ReadOnly: schema.Value.ReadOnly, + WriteOnly: schema.Value.WriteOnly, + AllowEmptyValue: schema.Value.AllowEmptyValue, + Deprecated: schema.Value.Deprecated, + XML: schema.Value.XML, + Min: schema.Value.Min, + Max: schema.Value.Max, + MultipleOf: schema.Value.MultipleOf, + MinLength: schema.Value.MinLength, + MaxLength: schema.Value.MaxLength, + Pattern: schema.Value.Pattern, + MinItems: schema.Value.MinItems, + MaxItems: schema.Value.MaxItems, + Required: schema.Value.Required, + MinProps: schema.Value.MinProps, + MaxProps: schema.Value.MaxProps, + AllOf: make(openapi3.SchemaRefs, len(schema.Value.AllOf)), + Properties: make(openapi3.Schemas), + AdditionalProperties: schema.Value.AdditionalProperties, + } + + if schema.Value.Discriminator != "" { + v3Schema.Discriminator = &openapi3.Discriminator{ + PropertyName: schema.Value.Discriminator, + } } + if schema.Value.Items != nil { - schema.Value.Items = ToV3SchemaRef(schema.Value.Items) + v3Schema.Items = ToV3SchemaRef(schema.Value.Items) } if schema.Value.Type.Is("file") { - schema.Value.Format, schema.Value.Type = "binary", &openapi3.Types{"string"} + v3Schema.Format, v3Schema.Type = "binary", &openapi3.Types{"string"} } for k, v := range schema.Value.Properties { - schema.Value.Properties[k] = ToV3SchemaRef(v) - } - if v := schema.Value.AdditionalProperties.Schema; v != nil { - schema.Value.AdditionalProperties.Schema = ToV3SchemaRef(v) + v3Schema.Properties[k] = ToV3SchemaRef(v) } for i, v := range schema.Value.AllOf { - schema.Value.AllOf[i] = ToV3SchemaRef(v) + v3Schema.AllOf[i] = ToV3SchemaRef(v) } if val, ok := schema.Value.Extensions["x-nullable"]; ok { - schema.Value.Nullable, _ = val.(bool) - delete(schema.Value.Extensions, "x-nullable") + if nullable, valid := val.(bool); valid { + v3Schema.Nullable = nullable + delete(v3Schema.Extensions, "x-nullable") + } } - return schema + return &openapi3.SchemaRef{ + Extensions: schema.Extensions, + Value: v3Schema, + } } var ref2To3 = map[string]string{ @@ -742,8 +795,8 @@ func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, c return } -func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) { - v2Defs := make(map[string]*openapi3.SchemaRef) +func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi2.SchemaRef, map[string]*openapi2.Parameter) { + v2Defs := make(map[string]*openapi2.SchemaRef) v2Params := make(map[string]*openapi2.Parameter) for name, schema := range schemas { schemaConv, parameterConv := FromV3SchemaRef(schema, components) @@ -759,7 +812,7 @@ func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3. return v2Defs, v2Params } -func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) { +func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi2.SchemaRef, *openapi2.Parameter) { if ref := schema.Ref; ref != "" { name := getParameterNameFromNewRef(ref) if val, ok := components.Schemas[name]; ok { @@ -769,10 +822,12 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components } } - return &openapi3.SchemaRef{Ref: FromV3Ref(ref)}, nil + return &openapi2.SchemaRef{Ref: FromV3Ref(ref)}, nil } if schema.Value == nil { - return schema, nil + return &openapi2.SchemaRef{ + Extensions: schema.Extensions, + }, nil } if schema.Value != nil { @@ -789,19 +844,19 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components } } return nil, &openapi2.Parameter{ - In: "formData", - Name: originalName, - Description: schema.Value.Description, - Type: paramType, - Enum: schema.Value.Enum, - Minimum: schema.Value.Min, - Maximum: schema.Value.Max, - ExclusiveMin: schema.Value.ExclusiveMin, - ExclusiveMax: schema.Value.ExclusiveMax, - MinLength: schema.Value.MinLength, - MaxLength: schema.Value.MaxLength, - Default: schema.Value.Default, - Items: schema.Value.Items, + In: "formData", + Name: originalName, + Description: schema.Value.Description, + Type: paramType, + Enum: schema.Value.Enum, + Minimum: schema.Value.Min, + Maximum: schema.Value.Max, + ExclusiveMin: schema.Value.ExclusiveMin, + ExclusiveMax: schema.Value.ExclusiveMax, + MinLength: schema.Value.MinLength, + MaxLength: schema.Value.MaxLength, + Default: schema.Value.Default, + // Items: schema.Value.Items, MinItems: schema.Value.MinItems, MaxItems: schema.Value.MaxItems, AllowEmptyValue: schema.Value.AllowEmptyValue, @@ -812,32 +867,72 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components } } } + + v2Schema := &openapi2.Schema{ + Extensions: schema.Value.Extensions, + Type: schema.Value.Type, + Title: schema.Value.Title, + Format: schema.Value.Format, + Description: schema.Value.Description, + Enum: schema.Value.Enum, + Default: schema.Value.Default, + Example: schema.Value.Example, + ExternalDocs: schema.Value.ExternalDocs, + UniqueItems: schema.Value.UniqueItems, + ExclusiveMin: schema.Value.ExclusiveMin, + ExclusiveMax: schema.Value.ExclusiveMax, + ReadOnly: schema.Value.ReadOnly, + WriteOnly: schema.Value.WriteOnly, + AllowEmptyValue: schema.Value.AllowEmptyValue, + Deprecated: schema.Value.Deprecated, + XML: schema.Value.XML, + Min: schema.Value.Min, + Max: schema.Value.Max, + MultipleOf: schema.Value.MultipleOf, + MinLength: schema.Value.MinLength, + MaxLength: schema.Value.MaxLength, + Pattern: schema.Value.Pattern, + MinItems: schema.Value.MinItems, + MaxItems: schema.Value.MaxItems, + Required: schema.Value.Required, + MinProps: schema.Value.MinProps, + MaxProps: schema.Value.MaxProps, + Properties: make(openapi2.Schemas), + AllOf: make(openapi2.SchemaRefs, len(schema.Value.AllOf)), + AdditionalProperties: schema.Value.AdditionalProperties, + } + if v := schema.Value.Items; v != nil { - schema.Value.Items, _ = FromV3SchemaRef(v, components) + v2Schema.Items, _ = FromV3SchemaRef(v, components) } + keys := make([]string, 0, len(schema.Value.Properties)) for k := range schema.Value.Properties { keys = append(keys, k) } sort.Strings(keys) for _, key := range keys { - schema.Value.Properties[key], _ = FromV3SchemaRef(schema.Value.Properties[key], components) - } - if v := schema.Value.AdditionalProperties.Schema; v != nil { - schema.Value.AdditionalProperties.Schema, _ = FromV3SchemaRef(v, components) + property, _ := FromV3SchemaRef(schema.Value.Properties[key], components) + if property != nil { + v2Schema.Properties[key] = property + } } + for i, v := range schema.Value.AllOf { - schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components) + v2Schema.AllOf[i], _ = FromV3SchemaRef(v, components) } if schema.Value.PermitsNull() { schema.Value.Nullable = false if schema.Value.Extensions == nil { - schema.Value.Extensions = make(map[string]any) + v2Schema.Extensions = make(map[string]any) } - schema.Value.Extensions["x-nullable"] = true + v2Schema.Extensions["x-nullable"] = true } - return schema, nil + return &openapi2.SchemaRef{ + Extensions: schema.Extensions, + Value: v2Schema, + }, nil } func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements { @@ -906,6 +1001,11 @@ func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameter break } } + + var v2Items *openapi2.SchemaRef + if val.Items != nil { + v2Items, _ = FromV3SchemaRef(val.Items, nil) + } parameter := &openapi2.Parameter{ Name: propName, Description: val.Description, @@ -918,7 +1018,7 @@ func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameter MinLength: val.MinLength, MaxLength: val.MaxLength, Default: val.Default, - Items: val.Items, + Items: v2Items, MinItems: val.MinItems, MaxItems: val.MaxItems, Maximum: val.Max, @@ -1029,12 +1129,12 @@ func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components Extensions: stripNonExtensions(parameter.Extensions), } if schemaRef := parameter.Schema; schemaRef != nil { - schemaRef, _ = FromV3SchemaRef(schemaRef, components) - if ref := schemaRef.Ref; ref != "" { - result.Schema = &openapi3.SchemaRef{Ref: FromV3Ref(ref)} + schemaRefV2, _ := FromV3SchemaRef(schemaRef, components) + if ref := schemaRefV2.Ref; ref != "" { + result.Schema = &openapi2.SchemaRef{Ref: FromV3Ref(ref)} return result, nil } - schema := schemaRef.Value + schema := schemaRefV2.Value result.Type = schema.Type result.Format = schema.Format result.Enum = schema.Enum From 4a9a5ff9de5257091b4e89d2f14e6c9a36b6023f Mon Sep 17 00:00:00 2001 From: Jay Shah Date: Tue, 1 Oct 2024 13:41:27 -0400 Subject: [PATCH 6/7] update ref link and comment --- openapi2/ref.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi2/ref.go b/openapi2/ref.go index 477aa99e3..16a614eb9 100644 --- a/openapi2/ref.go +++ b/openapi2/ref.go @@ -3,7 +3,7 @@ package openapi2 //go:generate go run refsgenerator.go // 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 // TODO: Update this Link +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#reference-object type Ref struct { Ref string `json:"$ref" yaml:"$ref"` } From c222bf1ed1003272574c5cd6657a6f4f7658d61a Mon Sep 17 00:00:00 2001 From: Jay Shah Date: Tue, 1 Oct 2024 14:47:30 -0400 Subject: [PATCH 7/7] run docs.sh --- .github/docs/openapi2.txt | 191 ++++++++++++++++++++++++++-------- .github/docs/openapi2conv.txt | 8 +- openapi2/ref.go | 2 +- 3 files changed, 154 insertions(+), 47 deletions(-) diff --git a/.github/docs/openapi2.txt b/.github/docs/openapi2.txt index aec6439de..9025f5a83 100644 --- a/.github/docs/openapi2.txt +++ b/.github/docs/openapi2.txt @@ -47,29 +47,29 @@ type Parameter struct { Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` - Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` - Format string `json:"format,omitempty" yaml:"format,omitempty"` - Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` - ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` - ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` - Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` - Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` - Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` - MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` - MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` - MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` - MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` - Default any `json:"default,omitempty" yaml:"default,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` + Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` } func (parameter Parameter) MarshalJSON() ([]byte, error) @@ -113,15 +113,21 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) func (pathItem *PathItem) UnmarshalJSON(data []byte) error UnmarshalJSON sets PathItem to a copy of data. +type Ref struct { + Ref string `json:"$ref" yaml:"$ref"` +} + Ref is specified by OpenAPI/Swagger 2.0 standard. See + https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#reference-object + type Response struct { Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` - Examples map[string]any `json:"examples,omitempty" yaml:"examples,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Examples map[string]any `json:"examples,omitempty" yaml:"examples,omitempty"` } func (response Response) MarshalJSON() ([]byte, error) @@ -130,6 +136,107 @@ func (response Response) MarshalJSON() ([]byte, error) func (response *Response) UnmarshalJSON(data []byte) error UnmarshalJSON sets Response to a copy of data. +type Schema struct { + Extensions map[string]any `json:"-" yaml:"-"` + + AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` + Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` + Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + + // Array-related, here for struct compactness + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + // Number-related, here for struct compactness + ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + // Properties + ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + XML *openapi3.XML `json:"xml,omitempty" yaml:"xml,omitempty"` + + // Number + Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Max *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + + // String + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + + // Array + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + + // Object + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` + MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + AdditionalProperties openapi3.AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + Discriminator string `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` +} + Schema is specified by OpenAPI/Swagger 2.0 standard. See + https://swagger.io/specification/v2/#schema-object + +func (schema Schema) MarshalJSON() ([]byte, error) + MarshalJSON returns the JSON encoding of Schema. + +func (schema Schema) MarshalYAML() (any, error) + MarshalYAML returns the YAML encoding of Schema. + +func (schema *Schema) UnmarshalJSON(data []byte) error + UnmarshalJSON sets Schema to a copy of data. + +type SchemaRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + + Ref string + Value *Schema + + // Has unexported fields. +} + SchemaRef represents either a Schema or a $ref to a Schema. When serializing + and both fields are set, Ref is preferred over Value. + +func (x *SchemaRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *SchemaRef) JSONLookup(token string) (any, error) + JSONLookup implements + https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable + +func (x SchemaRef) MarshalJSON() ([]byte, error) + MarshalJSON returns the JSON encoding of SchemaRef. + +func (x SchemaRef) MarshalYAML() (any, error) + MarshalYAML returns the YAML encoding of SchemaRef. + +func (x *SchemaRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *SchemaRef) RefString() string + RefString returns the $ref value. + +func (x *SchemaRef) UnmarshalJSON(data []byte) error + UnmarshalJSON sets SchemaRef to a copy of data. + +type SchemaRefs []*SchemaRef + +type Schemas map[string]*SchemaRef + type SecurityRequirements []map[string][]string type SecurityScheme struct { @@ -157,21 +264,21 @@ func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error type T struct { Extensions map[string]any `json:"-" yaml:"-"` - Swagger string `json:"swagger" yaml:"swagger"` // required - Info openapi3.Info `json:"info" yaml:"info"` // required - ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` - Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` - Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` - Host string `json:"host,omitempty" yaml:"host,omitempty"` - BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` - Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` - Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` - Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` - SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` - Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` - Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + Swagger string `json:"swagger" yaml:"swagger"` // required + Info openapi3.Info `json:"info" yaml:"info"` // required + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` + Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` + Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` + Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` + Definitions map[string]*SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` + Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` + SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` + Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` + Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } T is the root of an OpenAPI v2 document diff --git a/.github/docs/openapi2conv.txt b/.github/docs/openapi2conv.txt index 2f9f6deca..e24925aa3 100644 --- a/.github/docs/openapi2conv.txt +++ b/.github/docs/openapi2conv.txt @@ -16,8 +16,8 @@ func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, med func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameters func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) (*openapi2.Response, error) func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error) -func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) -func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) +func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi2.SchemaRef, *openapi2.Parameter) +func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi2.SchemaRef, map[string]*openapi2.Parameter) func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) func ToV3(doc2 *openapi2.T) (*openapi3.T, error) @@ -29,8 +29,8 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) func ToV3Ref(ref string) string func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.ResponseRef, error) -func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef -func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef +func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef +func ToV3Schemas(defs map[string]*openapi2.SchemaRef) map[string]*openapi3.SchemaRef func ToV3SecurityRequirements(requirements openapi2.SecurityRequirements) openapi3.SecurityRequirements func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.SecuritySchemeRef, error) func ToV3WithLoader(doc2 *openapi2.T, loader *openapi3.Loader, location *url.URL) (*openapi3.T, error) diff --git a/openapi2/ref.go b/openapi2/ref.go index 16a614eb9..e591d143e 100644 --- a/openapi2/ref.go +++ b/openapi2/ref.go @@ -2,7 +2,7 @@ package openapi2 //go:generate go run refsgenerator.go -// Ref is specified by OpenAPI/Swagger 3.0 standard. +// Ref is specified by OpenAPI/Swagger 2.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#reference-object type Ref struct { Ref string `json:"$ref" yaml:"$ref"`