From d2d60e3522053e12d5c09f21667635bd8101d884 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 13 Sep 2022 15:21:18 +0300 Subject: [PATCH 1/5] refactor(jsonschema): merge extensions and locator into one type --- jsonschema/extensions.go | 151 +++++++++++++++++++++++++++++++++++++++ jsonschema/parser.go | 29 ++++---- jsonschema/raw_schema.go | 11 +-- 3 files changed, 167 insertions(+), 24 deletions(-) create mode 100644 jsonschema/extensions.go diff --git a/jsonschema/extensions.go b/jsonschema/extensions.go new file mode 100644 index 000000000..6b5d88f46 --- /dev/null +++ b/jsonschema/extensions.go @@ -0,0 +1,151 @@ +package jsonschema + +import ( + "encoding/json" + "reflect" + "strings" + + "github.com/go-faster/errors" + "github.com/go-faster/jx" + yaml "github.com/go-faster/yamlx" + + "github.com/ogen-go/ogen/internal/location" +) + +// Extensions map is "^x-" fields list. +type Extensions map[string]*yaml.Node + +func isExtensionKey(key string) bool { + return strings.HasPrefix(key, "x-") +} + +// MarshalYAML implements yaml.Marshaler. +func (p Extensions) MarshalYAML() (any, error) { + content := make([]*yaml.Node, 0, len(p)*2) + for key, val := range p { + if !isExtensionKey(key) { + continue + } + content = append(content, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, + val, + ) + } + + return &yaml.Node{ + Kind: yaml.MappingNode, + Content: content, + }, nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (p *Extensions) UnmarshalYAML(node *yaml.Node) error { + if node.Kind != yaml.MappingNode { + return &yaml.UnmarshalError{ + Node: node, + Type: reflect.TypeOf(p), + Err: errors.Errorf("cannot unmarshal %s into %T", node.ShortTag(), p), + } + } + m := *p + if m == nil { + m = make(Extensions, len(node.Content)/2) + *p = m + } + for i := 0; i < len(node.Content); i += 2 { + var ( + keyNode = node.Content[i] + value = node.Content[i+1] + key string + ) + if err := keyNode.Decode(&key); err != nil { + return err + } + if isExtensionKey(key) { + m[key] = value + } + } + return nil +} + +// MarshalJSON implements json.Marshaler. +func (p Extensions) MarshalJSON() ([]byte, error) { + e := &jx.Encoder{} + + e.ObjStart() + for key, val := range p { + if !isExtensionKey(key) { + continue + } + + e.FieldStart(key) + b, err := convertYAMLtoRawJSON(val) + if err != nil { + return nil, errors.Wrap(err, "marshal") + } + e.Raw(b) + } + e.ObjEnd() + return e.Bytes(), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (p *Extensions) UnmarshalJSON(data []byte) error { + m := *p + if m == nil { + m = make(Extensions) + *p = m + } + + d := jx.DecodeBytes(data) + return d.Obj(func(d *jx.Decoder, key string) error { + if !isExtensionKey(key) { + return d.Skip() + } + + b, err := d.Raw() + if err != nil { + return err + } + + value, err := convertJSONToRawYAML(json.RawMessage(b)) + if err != nil { + return err + } + m[key] = value + + return nil + }) +} + +// OpenAPICommon is common fields for OpenAPI objects. +type OpenAPICommon struct { + Extensions + location.Locator +} + +// MarshalYAML implements yaml.Marshaler. +func (p OpenAPICommon) MarshalYAML() (any, error) { + return p.Extensions.MarshalYAML() +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (p *OpenAPICommon) UnmarshalYAML(node *yaml.Node) error { + if err := p.Extensions.UnmarshalYAML(node); err != nil { + return errors.Wrap(err, "unmarshal extensions") + } + if err := p.Locator.UnmarshalYAML(node); err != nil { + return errors.Wrap(err, "unmarshal locator") + } + return nil +} + +// MarshalJSON implements json.Marshaler. +func (p OpenAPICommon) MarshalJSON() ([]byte, error) { + return p.Extensions.MarshalJSON() +} + +// UnmarshalJSON implements json.Unmarshaler. +func (p *OpenAPICommon) UnmarshalJSON(data []byte) error { + return p.Extensions.UnmarshalJSON(data) +} diff --git a/jsonschema/parser.go b/jsonschema/parser.go index 04e99f954..1e8b3e4ef 100644 --- a/jsonschema/parser.go +++ b/jsonschema/parser.go @@ -7,7 +7,6 @@ import ( "regexp" "github.com/go-faster/errors" - "github.com/go-faster/jx" "github.com/ogen-go/ogen/internal/jsonpointer" "github.com/ogen-go/ogen/internal/location" @@ -57,7 +56,7 @@ func (p *Parser) Resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, erro func (p *Parser) parse(schema *RawSchema, ctx *jsonpointer.ResolveCtx) (_ *Schema, rerr error) { if schema != nil { defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), schema.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), schema.Common.Locator, rerr) }() } return p.parse1(schema, ctx, func(s *Schema) *Schema { @@ -73,7 +72,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun if schema != nil && s != nil { if enum := schema.Enum; len(enum) > 0 { - loc := schema.Locator.Field("enum") + loc := schema.Common.Locator.Field("enum") for i := range enum { for j := range enum { if i == j { @@ -112,15 +111,13 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun return nil }(); err != nil { err := errors.Wrap(err, "parse default") - return nil, p.wrapField("default", ctx.LastLoc(), schema.Locator, err) + return nil, p.wrapField("default", ctx.LastLoc(), schema.Common.Locator, err) } } - if a, ok := schema.XAnnotations["x-ogen-name"]; ok { - name, err := jx.DecodeBytes(a).Str() - if err != nil { - return nil, errors.Wrapf(err, "decode %q", a) + if a, ok := schema.Common.Extensions["x-ogen-name"]; ok { + if err := a.Decode(&s.XOgenName); err != nil { + return nil, errors.Wrap(err, "parse x-ogen-name") } - s.XOgenName = name } } @@ -132,7 +129,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo return nil, nil } wrapField := func(field string, err error) error { - return p.wrapField(field, ctx.LastLoc(), schema.Locator, err) + return p.wrapField(field, ctx.LastLoc(), schema.Common.Locator, err) } validateMinMax := func(prop string, min, max *uint64) (rerr error) { @@ -180,7 +177,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo case len(schema.OneOf) > 0: s := hook(&Schema{}) - schemas, err := p.parseMany(schema.OneOf, schema.Locator, ctx) + schemas, err := p.parseMany(schema.OneOf, schema.Common.Locator, ctx) if err != nil { return nil, wrapField("oneOf", err) } @@ -208,7 +205,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo Pattern: schema.Pattern, }) - schemas, err := p.parseMany(schema.AnyOf, schema.Locator, ctx) + schemas, err := p.parseMany(schema.AnyOf, schema.Common.Locator, ctx) if err != nil { return nil, wrapField("anyOf", err) } @@ -218,7 +215,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo case len(schema.AllOf) > 0: s := hook(&Schema{}) - schemas, err := p.parseMany(schema.AllOf, schema.Locator, ctx) + schemas, err := p.parseMany(schema.AllOf, schema.Common.Locator, ctx) if err != nil { return nil, wrapField("allOf", err) } @@ -302,7 +299,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo } if pp := schema.PatternProperties; len(pp) > 0 { - ppLoc := schema.Locator.Field("patternProperties") + ppLoc := schema.Common.Locator.Field("patternProperties") patterns := make([]PatternProperty, len(pp)) for idx, prop := range pp { @@ -326,7 +323,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo s.PatternProperties = patterns } - propsLoc := schema.Locator.Field("properties") + propsLoc := schema.Common.Locator.Field("properties") for _, propSpec := range schema.Properties { prop, err := p.parse(propSpec.Schema, ctx) if err != nil { @@ -489,6 +486,6 @@ func (p *Parser) extendInfo(schema *RawSchema, s *Schema) *Schema { } } - s.Locator = schema.Locator + s.Locator = schema.Common.Locator return s } diff --git a/jsonschema/raw_schema.go b/jsonschema/raw_schema.go index 90d040fbe..564f92c40 100644 --- a/jsonschema/raw_schema.go +++ b/jsonschema/raw_schema.go @@ -1,11 +1,5 @@ package jsonschema -import ( - "encoding/json" - - "github.com/ogen-go/ogen/internal/location" -) - // RawSchema is unparsed JSON Schema. type RawSchema struct { Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` @@ -45,8 +39,7 @@ type RawSchema struct { XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"` Example Example `json:"example,omitempty" yaml:"example,omitempty"` - XAnnotations map[string]json.RawMessage `json:"-" yaml:"-"` - Locator location.Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // Discriminator discriminates types for OneOf, AllOf, AnyOf. @@ -57,6 +50,8 @@ type Discriminator struct { PropertyName string `json:"propertyName" yaml:"propertyName"` // An object to hold mappings between payload values and schema names or references. Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` + + Common OpenAPICommon `json:"-" yaml:",inline"` } // XML is a metadata object that allows for more fine-tuned XML model definitions. From fd2aea05e2570e717c13a6529296b86482ad1cad Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 13 Sep 2022 15:22:01 +0300 Subject: [PATCH 2/5] feat: parse extensions --- dsl_test.go | 30 ++++++---- location_test.go | 38 ++++++------- schema.go | 8 ++- spec_backcomp.go => schema_backcomp.go | 5 +- security_scheme.go | 6 +- spec.go | 79 ++++++++++++++++++++------ 6 files changed, 111 insertions(+), 55 deletions(-) rename spec_backcomp.go => schema_backcomp.go (97%) diff --git a/dsl_test.go b/dsl_test.go index db793ee2f..9dc8ceca4 100644 --- a/dsl_test.go +++ b/dsl_test.go @@ -18,6 +18,11 @@ const ( ) var ( + _extensions = ogen.Extensions(nil) + _common = ogen.OpenAPICommon{ + Extensions: _extensions, + } + // reusable query param _queryParam = ogen.NewParameter(). InQuery(). @@ -103,8 +108,8 @@ func TestBuilder(t *testing.T) { Version: "0.1.0", }, Servers: []ogen.Server{ - {"staging", "staging.api.com", nil}, - {"production", "api.com", nil}, + {"staging", "staging.api.com", nil, _extensions}, + {"production", "api.com", nil, _extensions}, }, Paths: map[string]*ogen.PathItem{ pathWithID: { @@ -212,6 +217,7 @@ func TestBuilder(t *testing.T) { }, }, }, + Extensions: _extensions, } // referenced path path := ogen.NewPathItem(). @@ -328,18 +334,22 @@ func TestBuilder(t *testing.T) { SetHead(ogen.NewOperation().SetOperationID("head").SetResponses(ogen.Responses{"resp": ogen.NewResponse()})). SetPatch(ogen.NewOperation().SetOperationID("patch").AddParameters(ogen.NewParameter().InHeader().SetDeprecated(true))). SetTrace(ogen.NewOperation().SetOperationID("trace")). - SetServers([]ogen.Server{{"url1", "desc1", nil}}). + SetServers([]ogen.Server{{"url1", "desc1", nil, nil}}). AddServers(ogen.NewServer().SetDescription("desc2").SetURL("url2")). SetParameters([]*ogen.Parameter{_queryParam.Parameter}) assert.Equal(t, &ogen.PathItem{ - Put: &ogen.Operation{OperationID: "put", Tags: []string{"tag1", "tag2"}}, - Delete: &ogen.Operation{OperationID: "delete", Summary: "summary"}, - Options: &ogen.Operation{OperationID: "options", Parameters: []*ogen.Parameter{_cookieParam.Parameter}}, - Head: &ogen.Operation{OperationID: "head", Responses: ogen.Responses{"resp": &ogen.Response{}}}, - Patch: &ogen.Operation{OperationID: "patch", Parameters: []*ogen.Parameter{{In: "header", Deprecated: true}}}, - Trace: &ogen.Operation{OperationID: "trace"}, - Servers: []ogen.Server{{"url1", "desc1", nil}, {"url2", "desc2", nil}}, + Put: &ogen.Operation{OperationID: "put", Tags: []string{"tag1", "tag2"}}, + Delete: &ogen.Operation{OperationID: "delete", Summary: "summary"}, + Options: &ogen.Operation{OperationID: "options", Parameters: []*ogen.Parameter{_cookieParam.Parameter}}, + Head: &ogen.Operation{OperationID: "head", Responses: ogen.Responses{"resp": &ogen.Response{}}}, + Patch: &ogen.Operation{OperationID: "patch", Parameters: []*ogen.Parameter{{In: "header", Deprecated: true}}}, + Trace: &ogen.Operation{OperationID: "trace"}, + Servers: []ogen.Server{ + {"url1", "desc1", nil, nil}, + {"url2", "desc2", nil, nil}, + }, Parameters: []*ogen.Parameter{_queryParam.Parameter}, + Common: _common, }, pi) mlt := uint64(1) diff --git a/location_test.go b/location_test.go index 3d61afc71..b7053b67f 100644 --- a/location_test.go +++ b/location_test.go @@ -138,26 +138,26 @@ func TestLocation(t *testing.T) { schema = media.Schema ) // Compare PathItem. - equalLoc(&foo.Locator, 8, 13) + equalLoc(&foo.Common.Locator, 8, 13) // Compare post - equalLoc(&post.Locator, 9, 15) + equalLoc(&post.Common.Locator, 9, 15) // Compare Parameters. - equalLoc(&post.Parameters[0].Locator, 11, 11) - equalLoc(&post.Parameters[1].Locator, 18, 11) + equalLoc(&post.Parameters[0].Common.Locator, 11, 11) + equalLoc(&post.Parameters[1].Common.Locator, 18, 11) // Compare RequestBody. - equalLoc(&body.Locator, 26, 24) - equalLoc(&media.Locator, 28, 33) - equalLoc(&schema.Locator, 29, 25) + equalLoc(&body.Common.Locator, 26, 24) + equalLoc(&media.Common.Locator, 28, 33) + equalLoc(&schema.Common.Locator, 29, 25) // Compare get. - equalLoc(&get.Locator, 48, 14) + equalLoc(&get.Common.Locator, 48, 14) var user = locationSpec.Components.Schemas["User"] - equalLoc(&user.Locator, 59, 15) - equalLoc(&user.Properties[0].Schema.Locator, 62, 19) + equalLoc(&user.Common.Locator, 59, 15) + equalLoc(&user.Properties[0].Schema.Common.Locator, 62, 19) }) t.Run("YAML", func(t *testing.T) { a := assert.New(t) @@ -175,20 +175,20 @@ func TestLocation(t *testing.T) { ) // FIXME(tdakkota): parser sets map/seq location to the first element. // Compare PathItem and Operation. - equalLoc(&foo.Locator, 7, 5) - equalLoc(&post.Locator, 8, 7) + equalLoc(&foo.Common.Locator, 7, 5) + equalLoc(&post.Common.Locator, 8, 7) // Compare Parameters. - equalLoc(&post.Parameters[0].Locator, 9, 11) - equalLoc(&post.Parameters[1].Locator, 13, 11) + equalLoc(&post.Parameters[0].Common.Locator, 9, 11) + equalLoc(&post.Parameters[1].Common.Locator, 13, 11) // Compare RequestBody. - equalLoc(&body.Locator, 18, 9) - equalLoc(&requestMedia.Locator, 20, 13) - equalLoc(&requestSchema.Locator, 21, 15) + equalLoc(&body.Common.Locator, 18, 9) + equalLoc(&requestMedia.Common.Locator, 20, 13) + equalLoc(&requestSchema.Common.Locator, 21, 15) var user = locationSpec.Components.Schemas["User"] - equalLoc(&user.Locator, 36, 7) - equalLoc(&user.Properties[0].Schema.Locator, 39, 11) + equalLoc(&user.Common.Locator, 36, 7) + equalLoc(&user.Properties[0].Schema.Common.Locator, 39, 11) }) } diff --git a/schema.go b/schema.go index b6e5ae18b..2e22c0a45 100644 --- a/schema.go +++ b/schema.go @@ -7,6 +7,8 @@ import ( "github.com/go-faster/errors" "github.com/go-faster/jx" yaml "github.com/go-faster/yamlx" + + "github.com/ogen-go/ogen/jsonschema" ) // The Schema Object allows the definition of input and output data types. @@ -239,7 +241,7 @@ type Schema struct { // described is not a string. ContentMediaType string `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common jsonschema.OpenAPICommon `json:"-" yaml:",inline"` } // Property is item of Properties. @@ -500,6 +502,8 @@ type Discriminator struct { PropertyName string `json:"propertyName" yaml:"propertyName"` // An object to hold mappings between payload values and schema names or references. Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` + + Common OpenAPICommon `json:"-" yaml:",inline"` } // XML is a metadata object that allows for more fine-tuned XML model definitions. @@ -532,4 +536,6 @@ type XML struct { // // Default value is false. Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"` + + Common OpenAPICommon `json:"-" yaml:",inline"` } diff --git a/spec_backcomp.go b/schema_backcomp.go similarity index 97% rename from spec_backcomp.go rename to schema_backcomp.go index 99823be38..860ecf7ed 100644 --- a/spec_backcomp.go +++ b/schema_backcomp.go @@ -1,8 +1,6 @@ package ogen import ( - "encoding/json" - "github.com/ogen-go/ogen/jsonschema" ) @@ -56,8 +54,7 @@ func (s *Schema) ToJSONSchema() *jsonschema.RawSchema { Discriminator: s.Discriminator.ToJSONSchema(), XML: s.XML.ToJSONSchema(), Example: s.Example, - XAnnotations: map[string]json.RawMessage{}, - Locator: s.Locator, + Common: s.Common, } } diff --git a/security_scheme.go b/security_scheme.go index b21c675b5..c6c1c2742 100644 --- a/security_scheme.go +++ b/security_scheme.go @@ -25,7 +25,7 @@ type SecurityScheme struct { // This MUST be in the form of a URL. The OpenID Connect standard requires the use of TLS. OpenIDConnectURL string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // OAuthFlows allows configuration of the supported OAuth Flows. @@ -41,7 +41,7 @@ type OAuthFlows struct { // Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0. AuthorizationCode *OAuthFlow `json:"authorizationCode" yaml:"authorizationCode"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // OAuthFlow is configuration details for a supported OAuth Flow. @@ -61,7 +61,7 @@ type OAuthFlow struct { // A map between the scope name and a short description for it. The map MAY be empty. Scopes map[string]string `json:"scopes" yaml:"scopes"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // SecurityRequirements lists the required security schemes to execute this operation. diff --git a/spec.go b/spec.go index 83f7ef5b5..adc022677 100644 --- a/spec.go +++ b/spec.go @@ -21,6 +21,13 @@ type ( // RawValue is a raw JSON value. RawValue = jsonschema.RawValue + // Extensions is a map of OpenAPI extensions. + // + // See https://spec.openapis.org/oas/v3.1.0#specification-extensions. + Extensions = jsonschema.Extensions + // OpenAPICommon is a common OpenAPI object fields (extensions and locator). + OpenAPICommon = jsonschema.OpenAPICommon + // Locator stores location of JSON value. Locator = location.Locator ) @@ -71,6 +78,9 @@ type Spec struct { // Additional external documentation. ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // Specification extensions. + Extensions Extensions `json:"-" yaml:",inline"` + // Raw YAML node. Used by '$ref' resolvers. Raw *yaml.Node `json:"-" yaml:"-"` } @@ -138,6 +148,9 @@ type Info struct { License *License `json:"license,omitempty" yaml:"license,omitempty"` // REQUIRED. The version of the OpenAPI document. Version string `json:"version" yaml:"version"` + + // Specification extensions. + Extensions Extensions `json:"-" yaml:",inline"` } // Contact information for the exposed API. @@ -150,6 +163,9 @@ type Contact struct { URL string `json:"url,omitempty" yaml:"url,omitempty"` // The email address of the contact person/organization. Email string `json:"email,omitempty" yaml:"email,omitempty"` + + // Specification extensions. + Extensions Extensions `json:"-" yaml:",inline"` } // License information for the exposed API. @@ -162,6 +178,9 @@ type License struct { Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // A URL to the license used for the API. URL string `json:"url,omitempty" yaml:"url,omitempty"` + + // Specification extensions. + Extensions Extensions `json:"-" yaml:",inline"` } // Server represents a Server. @@ -177,6 +196,9 @@ type Server struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` // A map between a variable name and its value. The value is used for substitution in the server's URL template. Variables map[string]ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` + + // Specification extensions. + Extensions Extensions `json:"-" yaml:",inline"` } // ServerVariable describes an object representing a Server Variable for server URL template substitution. @@ -193,6 +215,9 @@ type ServerVariable struct { Default string `json:"default" yaml:"default"` // An optional description for the server variable. CommonMark syntax MAY be used for rich text representation. Description string `json:"description,omitempty" yaml:"description,omitempty"` + + // Specification extensions. + Extensions Extensions `json:"-" yaml:",inline"` } // Components Holds a set of reusable objects for different aspects of the OAS. @@ -201,18 +226,28 @@ type ServerVariable struct { // // See https://spec.openapis.org/oas/v3.1.0#components-object. type Components struct { - Schemas map[string]*Schema `json:"schemas,omitempty" yaml:"schemas,omitempty"` - Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` - Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` - RequestBodies map[string]*RequestBody `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` - Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + // An object to hold reusable Schema Objects. + Schemas map[string]*Schema `json:"schemas,omitempty" yaml:"schemas,omitempty"` + // An object to hold reusable Response Objects. + Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` + // An object to hold reusable Parameter Objects. + Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + // An object to hold reusable Example Objects. + Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` + // An object to hold reusable Request Body Objects. + RequestBodies map[string]*RequestBody `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"` + // An object to hold reusable Header Objects. + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + // An object to hold reusable Security Scheme Objects. SecuritySchemes map[string]*SecurityScheme `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` - Links map[string]*Link `json:"links,omitempty" yaml:"links,omitempty"` - Callbacks map[string]*Callback `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` - PathItems map[string]*PathItem `json:"pathItems,omitempty" yaml:"pathItems,omitempty"` + // An object to hold reusable Link Objects. + Links map[string]*Link `json:"links,omitempty" yaml:"links,omitempty"` + // An object to hold reusable Callback Objects. + Callbacks map[string]*Callback `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` + // An object to hold reusable Path Item Objects. + PathItems map[string]*PathItem `json:"pathItems,omitempty" yaml:"pathItems,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // Init initializes all fields. @@ -293,7 +328,7 @@ type PathItem struct { // a combination of a name and location. Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // Operation describes a single API operation on a path. @@ -347,7 +382,7 @@ type Operation struct { // it will be overridden by this value. Servers []Server `json:"servers,omitempty" yaml:"servers,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // ExternalDocumentation describes a reference to external resource for extended documentation. @@ -358,6 +393,9 @@ type ExternalDocumentation struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` // REQUIRED. The URL for the target documentation. This MUST be in the form of a URL. URL string `json:"url" yaml:"url"` + + // Specification extensions. + Extensions Extensions `json:"-" yaml:",inline"` } // Parameter describes a single operation parameter. @@ -414,7 +452,7 @@ type Parameter struct { // The map MUST only contain one entry. Content map[string]Media `json:"content,omitempty" yaml:"content,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // RequestBody describes a single request body. @@ -438,7 +476,7 @@ type RequestBody struct { // Determines if the request body is required in the request. Defaults to false. Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // Media provides schema and examples for the media type identified by its key. @@ -460,7 +498,7 @@ type Media struct { // type is multipart or application/x-www-form-urlencoded. Encoding map[string]Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // Encoding describes single encoding definition applied to a single schema property. @@ -491,7 +529,7 @@ type Encoding struct { // is not application/x-www-form-urlencoded. AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // Responses is a container for the expected responses of an operation. @@ -536,7 +574,7 @@ type Response struct { // of the names for Component Objects. Links map[string]*Link `json:"links,omitempty" yaml:"links,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // Callback is a map of possible out-of band callbacks related to the parent operation. @@ -569,7 +607,7 @@ type Example struct { // A URI that points to the literal example. ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` - Locator Locator `json:"-" yaml:",inline"` + Common OpenAPICommon `json:"-" yaml:",inline"` } // Link describes a possible design-time link for a response. @@ -592,6 +630,8 @@ type Link struct { // The key is the parameter name to be used, whereas the value can be a constant or an expression to be // evaluated and passed to the linked operation. Parameters map[string]RawValue `json:"parameters,omitempty" yaml:"parameters,omitempty"` + + Common OpenAPICommon `json:"-" yaml:",inline"` } // Header describes header response. @@ -615,4 +655,7 @@ type Tag struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` // Additional external documentation for this tag. ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + + // Specification extensions. + Extensions Extensions `json:"-" yaml:",inline"` } From d51b0e5ebf0fb14c4392e2560746b6f9f359c083 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 13 Sep 2022 15:23:02 +0300 Subject: [PATCH 3/5] refactor(parser): update parser due to structure changes --- openapi/parser/parse_components.go | 28 ++++++++++--------- openapi/parser/parse_example.go | 7 ++--- openapi/parser/parse_header.go | 15 ++++++----- openapi/parser/parse_mediatype.go | 14 +++++----- openapi/parser/parse_parameter.go | 26 +++++++++--------- openapi/parser/parse_path_item.go | 18 +++++++------ openapi/parser/parse_request_body.go | 11 ++++---- openapi/parser/parse_response.go | 13 ++++----- openapi/parser/parse_security.go | 40 ++++++++++++++-------------- 9 files changed, 92 insertions(+), 80 deletions(-) diff --git a/openapi/parser/parse_components.go b/openapi/parser/parse_components.go index a9728b8a8..c998fcb72 100644 --- a/openapi/parser/parse_components.go +++ b/openapi/parser/parse_components.go @@ -40,34 +40,35 @@ func validateComponentsKeys(p *parser, c *ogen.Components) error { if c == nil { return nil } - if err := validateComponentsKey(p, c.Schemas, c.Locator.Field("schemas")); err != nil { + locator := c.Common.Locator + if err := validateComponentsKey(p, c.Schemas, locator.Field("schemas")); err != nil { return errors.Wrap(err, "schemas") } - if err := validateComponentsKey(p, c.Responses, c.Locator.Field("responses")); err != nil { + if err := validateComponentsKey(p, c.Responses, locator.Field("responses")); err != nil { return errors.Wrap(err, "responses") } - if err := validateComponentsKey(p, c.Parameters, c.Locator.Field("parameters")); err != nil { + if err := validateComponentsKey(p, c.Parameters, locator.Field("parameters")); err != nil { return errors.Wrap(err, "parameters") } - if err := validateComponentsKey(p, c.Examples, c.Locator.Field("examples")); err != nil { + if err := validateComponentsKey(p, c.Examples, locator.Field("examples")); err != nil { return errors.Wrap(err, "examples") } - if err := validateComponentsKey(p, c.RequestBodies, c.Locator.Field("requestBodies")); err != nil { + if err := validateComponentsKey(p, c.RequestBodies, locator.Field("requestBodies")); err != nil { return errors.Wrap(err, "requestBodies") } - if err := validateComponentsKey(p, c.Headers, c.Locator.Field("headers")); err != nil { + if err := validateComponentsKey(p, c.Headers, locator.Field("headers")); err != nil { return errors.Wrap(err, "headers") } - if err := validateComponentsKey(p, c.SecuritySchemes, c.Locator.Field("securitySchemes")); err != nil { + if err := validateComponentsKey(p, c.SecuritySchemes, locator.Field("securitySchemes")); err != nil { return errors.Wrap(err, "securitySchemes") } - if err := validateComponentsKey(p, c.Links, c.Locator.Field("links")); err != nil { + if err := validateComponentsKey(p, c.Links, locator.Field("links")); err != nil { return errors.Wrap(err, "links") } - if err := validateComponentsKey(p, c.Callbacks, c.Locator.Field("callbacks")); err != nil { + if err := validateComponentsKey(p, c.Callbacks, locator.Field("callbacks")); err != nil { return errors.Wrap(err, "callbacks") } - if err := validateComponentsKey(p, c.PathItems, c.Locator.Field("pathItems")); err != nil { + if err := validateComponentsKey(p, c.PathItems, locator.Field("pathItems")); err != nil { return errors.Wrap(err, "pathItems") } return nil @@ -83,8 +84,9 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer RequestBodies: map[string]*openapi.RequestBody{}, }, nil } + locator := c.Common.Locator defer func() { - rerr = p.wrapLocation("", c.Locator, rerr) + rerr = p.wrapLocation("", locator, rerr) }() if err := validateComponentsKeys(p, c); err != nil { @@ -99,9 +101,9 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer RequestBodies: make(map[string]*openapi.RequestBody, len(c.RequestBodies)), } wrapErr := func(component, name string, err error) error { - loc := c.Locator.Field(component).Field(name) + locator := locator.Field(component).Field(name) err = errors.Wrapf(err, "%s: %q", component, name) - return p.wrapLocation("", loc, err) + return p.wrapLocation("", locator, err) } for name := range c.Schemas { diff --git a/openapi/parser/parse_example.go b/openapi/parser/parse_example.go index e7b95de8e..0c6c6bc66 100644 --- a/openapi/parser/parse_example.go +++ b/openapi/parser/parse_example.go @@ -10,13 +10,14 @@ func (p *parser) parseExample(e *ogen.Example, ctx *jsonpointer.ResolveCtx) (_ * if e == nil { return nil, nil } + locator := e.Common.Locator defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), e.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr) }() if ref := e.Ref; ref != "" { resolved, err := p.resolveExample(ref, ctx) if err != nil { - return nil, p.wrapRef(ctx.LastLoc(), e.Locator, err) + return nil, p.wrapRef(ctx.LastLoc(), locator, err) } return resolved, nil } @@ -26,6 +27,6 @@ func (p *parser) parseExample(e *ogen.Example, ctx *jsonpointer.ResolveCtx) (_ * Description: e.Description, Value: e.Value, ExternalValue: e.ExternalValue, - Locator: e.Locator, + Locator: locator, }, nil } diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go index cb2864a6e..2f726f594 100644 --- a/openapi/parser/parse_header.go +++ b/openapi/parser/parse_header.go @@ -28,13 +28,14 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer. if header == nil { return nil, errors.New("header object is empty or null") } + locator := header.Common.Locator defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), header.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr) }() if ref := header.Ref; ref != "" { resolved, err := p.resolveHeader(name, ref, ctx) if err != nil { - return nil, p.wrapRef(ctx.LastLoc(), header.Locator, err) + return nil, p.wrapRef(ctx.LastLoc(), locator, err) } return resolved, nil } @@ -44,7 +45,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer. return nil } err := errors.Errorf(`%q MUST NOT be specified, got %q`, name, got) - return p.wrapField(name, ctx.LastLoc(), header.Locator, err) + return p.wrapField(name, ctx.LastLoc(), locator, err) } if err := mustNotSpecified("in", header.In); err != nil { return nil, err @@ -61,13 +62,13 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer. schema, err := p.parseSchema(header.Schema, ctx) if err != nil { err := errors.Wrap(err, "schema") - return nil, p.wrapField("schema", ctx.LastLoc(), header.Locator, err) + return nil, p.wrapField("schema", ctx.LastLoc(), locator, err) } - content, err := p.parseParameterContent(header.Content, header.Locator.Field("content"), ctx) + content, err := p.parseParameterContent(header.Content, locator.Field("content"), ctx) if err != nil { err := errors.Wrap(err, "content") - return nil, p.wrapField("content", ctx.LastLoc(), header.Locator, err) + return nil, p.wrapField("content", ctx.LastLoc(), locator, err) } op := &openapi.Header{ @@ -80,7 +81,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer. Style: inferParamStyle(locatedIn, header.Style), Explode: inferParamExplode(locatedIn, header.Explode), Required: header.Required, - Locator: header.Locator, + Locator: locator, } // TODO: Validate content? diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go index 2479f2bcd..42ca5e342 100644 --- a/openapi/parser/parse_mediatype.go +++ b/openapi/parser/parse_mediatype.go @@ -78,8 +78,9 @@ func (p *parser) parseParameterContent( } func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.ResolveCtx) (_ *openapi.MediaType, rerr error) { + locator := m.Common.Locator defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), m.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr) }() s, err := p.parseSchema(m.Schema, ctx) @@ -100,8 +101,9 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv } parseEncoding := func(name string, prop jsonschema.Property, e ogen.Encoding) (rerr error) { + locator := e.Common.Locator defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), e.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr) }() encoding := &openapi.Encoding{ @@ -110,11 +112,11 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv Style: inferParamStyle(openapi.LocationQuery, e.Style), Explode: inferParamExplode(openapi.LocationQuery, e.Explode), AllowReserved: e.AllowReserved, - Locator: e.Locator, + Locator: locator, } encoding.Headers, err = p.parseHeaders(e.Headers, ctx) if err != nil { - return p.wrapField("headers", ctx.LastLoc(), e.Locator, err) + return p.wrapField("headers", ctx.LastLoc(), locator, err) } encodings[name] = encoding @@ -134,7 +136,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv return nil } - encodingLoc := m.Locator.Field("encoding") + encodingLoc := locator.Field("encoding") for name, e := range m.Encoding { prop, ok := names[name] if !ok { @@ -175,6 +177,6 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv Example: json.RawMessage(m.Example), Examples: examples, Encoding: encodings, - Locator: m.Locator, + Locator: locator, }, nil } diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go index dff19c1a6..1dd44822c 100644 --- a/openapi/parser/parse_parameter.go +++ b/openapi/parser/parse_parameter.go @@ -69,7 +69,7 @@ func (p *parser) parseParams( } if _, ok := unique[ploc]; ok { err := errors.Errorf("duplicate parameter: %q in %q", param.Name, param.In) - return nil, p.wrapLocation(ctx.LastLoc(), spec.Locator, err) + return nil, p.wrapLocation(ctx.LastLoc(), spec.Common.Locator, err) } unique[ploc] = struct{}{} @@ -85,16 +85,17 @@ func (p *parser) validateParameter( param *ogen.Parameter, file string, ) error { + locator := param.Common.Locator switch { case param.Schema != nil && param.Content != nil: err := errors.New("parameter MUST contain either a schema property, or a content property, but not both") - return p.wrapField("schema", file, param.Locator, err) + return p.wrapField("schema", file, locator, err) case param.Schema == nil && param.Content == nil: return errors.New("parameter MUST contain either a schema property, or a content property") case param.Content != nil && len(param.Content) < 1: // https://github.com/OAI/OpenAPI-Specification/discussions/2875 err := errors.New("content must have at least one entry") - return p.wrapField("content", file, param.Locator, err) + return p.wrapField("content", file, locator, err) } // Path parameters are always required. @@ -102,12 +103,12 @@ func (p *parser) validateParameter( case openapi.LocationPath: if !param.Required { err := errors.New("path parameters must be required") - return p.wrapField("required", file, param.Locator, err) + return p.wrapField("required", file, locator, err) } case openapi.LocationHeader: if !httpguts.ValidHeaderFieldName(name) { err := errors.Errorf("invalid header name %q", name) - return p.wrapField("name", file, param.Locator, err) + return p.wrapField("name", file, locator, err) } } return nil @@ -117,13 +118,14 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC if param == nil { return nil, errors.New("parameter object is empty or null") } + locator := param.Common.Locator defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), param.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr) }() if ref := param.Ref; ref != "" { parsed, err := p.resolveParameter(ref, ctx) if err != nil { - return nil, p.wrapRef(ctx.LastLoc(), param.Locator, err) + return nil, p.wrapRef(ctx.LastLoc(), locator, err) } return parsed, nil } @@ -138,7 +140,7 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC locatedIn, exists := types[strings.ToLower(param.In)] if !exists { err := errors.Errorf("unknown parameter location %q", param.In) - return nil, p.wrapField("in", ctx.LastLoc(), param.Locator, err) + return nil, p.wrapField("in", ctx.LastLoc(), locator, err) } if err := p.validateParameter(param.Name, locatedIn, param, ctx.LastLoc()); err != nil { @@ -148,13 +150,13 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC schema, err := p.parseSchema(param.Schema, ctx) if err != nil { err := errors.Wrap(err, "schema") - return nil, p.wrapField("schema", ctx.LastLoc(), param.Locator, err) + return nil, p.wrapField("schema", ctx.LastLoc(), locator, err) } - content, err := p.parseParameterContent(param.Content, param.Locator.Field("content"), ctx) + content, err := p.parseParameterContent(param.Content, locator.Field("content"), ctx) if err != nil { err := errors.Wrap(err, "content") - return nil, p.wrapField("content", ctx.LastLoc(), param.Locator, err) + return nil, p.wrapField("content", ctx.LastLoc(), locator, err) } op := &openapi.Parameter{ @@ -168,7 +170,7 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC Explode: inferParamExplode(locatedIn, param.Explode), Required: param.Required, AllowReserved: param.AllowReserved, - Locator: param.Locator, + Locator: locator, } // TODO: Validate content? diff --git a/openapi/parser/parse_path_item.go b/openapi/parser/parse_path_item.go index c87670962..1aac12352 100644 --- a/openapi/parser/parse_path_item.go +++ b/openapi/parser/parse_path_item.go @@ -20,19 +20,20 @@ func (p *parser) parsePathItem( if item == nil { return nil, errors.New("pathItem object is empty or null") } + locator := item.Common.Locator defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), item.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr) }() if ref := item.Ref; ref != "" { ops, err := p.resolvePathItem(path, ref, operationIDs, ctx) if err != nil { - return nil, p.wrapRef(ctx.LastLoc(), item.Locator, err) + return nil, p.wrapRef(ctx.LastLoc(), locator, err) } return ops, nil } - itemParams, err := p.parseParams(item.Parameters, item.Locator.Field("parameters"), ctx) + itemParams, err := p.parseParams(item.Parameters, locator.Field("parameters"), ctx) if err != nil { return nil, errors.Wrap(err, "parameters") } @@ -69,8 +70,9 @@ func (p *parser) parseOp( itemParams []*openapi.Parameter, ctx *jsonpointer.ResolveCtx, ) (_ *openapi.Operation, err error) { + locator := spec.Common.Locator defer func() { - err = p.wrapLocation(ctx.LastLoc(), spec.Locator, err) + err = p.wrapLocation(ctx.LastLoc(), locator, err) }() op := &openapi.Operation{ @@ -79,10 +81,10 @@ func (p *parser) parseOp( Description: spec.Description, Deprecated: spec.Deprecated, HTTPMethod: httpMethod, - Locator: spec.Locator, + Locator: locator, } - opParams, err := p.parseParams(spec.Parameters, spec.Locator.Field("parameters"), ctx) + opParams, err := p.parseParams(spec.Parameters, locator.Field("parameters"), ctx) if err != nil { return nil, errors.Wrap(err, "parameters") } @@ -103,7 +105,7 @@ func (p *parser) parseOp( } { - locator := spec.Locator.Field("responses") + locator := locator.Field("responses") op.Responses, err = p.parseResponses(spec.Responses, locator, ctx) if err != nil { err := errors.Wrap(err, "responses") @@ -127,7 +129,7 @@ func (p *parser) parseOp( if spec.Security != nil { // Use operation level security. security = spec.Security - securityParent = spec.Locator + securityParent = locator } if err := parseSecurity(security, securityParent.Field("security")); err != nil { return nil, err diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go index d0a90a586..b536def93 100644 --- a/openapi/parser/parse_request_body.go +++ b/openapi/parser/parse_request_body.go @@ -12,13 +12,14 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *jsonpointer.Resol if body == nil { return nil, errors.New("requestBody object is empty or null") } + locator := body.Common.Locator defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), body.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr) }() if ref := body.Ref; ref != "" { resolved, err := p.resolveRequestBody(ref, ctx) if err != nil { - return nil, p.wrapRef(ctx.LastLoc(), body.Locator, err) + return nil, p.wrapRef(ctx.LastLoc(), locator, err) } return resolved, nil } @@ -29,7 +30,7 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *jsonpointer.Resol return nil, errors.New("content must have at least one entry") } - content, err := p.parseContent(body.Content, body.Locator.Field("content"), ctx) + content, err := p.parseContent(body.Content, locator.Field("content"), ctx) if err != nil { return nil, errors.Wrap(err, "parse content") } @@ -37,13 +38,13 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *jsonpointer.Resol return content, nil }() if err != nil { - return nil, p.wrapField("content", ctx.LastLoc(), body.Locator, err) + return nil, p.wrapField("content", ctx.LastLoc(), locator, err) } return &openapi.RequestBody{ Description: body.Description, Required: body.Required, Content: content, - Locator: body.Locator, + Locator: locator, }, nil } diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go index d40596b71..432aa8cf0 100644 --- a/openapi/parser/parse_response.go +++ b/openapi/parser/parse_response.go @@ -42,34 +42,35 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *jsonpointer.ResolveCtx) if resp == nil { return nil, errors.New("response object is empty or null") } + locator := resp.Common.Locator defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), resp.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr) }() if ref := resp.Ref; ref != "" { resolved, err := p.resolveResponse(ref, ctx) if err != nil { - return nil, p.wrapRef(ctx.LastLoc(), resp.Locator, err) + return nil, p.wrapRef(ctx.LastLoc(), locator, err) } return resolved, nil } - content, err := p.parseContent(resp.Content, resp.Locator.Field("content"), ctx) + content, err := p.parseContent(resp.Content, locator.Field("content"), ctx) if err != nil { err := errors.Wrap(err, "content") - return nil, p.wrapField("content", ctx.LastLoc(), resp.Locator, err) + return nil, p.wrapField("content", ctx.LastLoc(), locator, err) } headers, err := p.parseHeaders(resp.Headers, ctx) if err != nil { err := errors.Wrap(err, "headers") - return nil, p.wrapField("headers", ctx.LastLoc(), resp.Locator, err) + return nil, p.wrapField("headers", ctx.LastLoc(), locator, err) } return &openapi.Response{ Description: resp.Description, Headers: headers, Content: content, - Locator: resp.Locator, + Locator: locator, }, nil } diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go index ca9176ff9..c723aff1f 100644 --- a/openapi/parser/parse_security.go +++ b/openapi/parser/parse_security.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/go-faster/errors" + "golang.org/x/exp/maps" "github.com/ogen-go/ogen" "github.com/ogen-go/ogen/internal/jsonpointer" @@ -19,14 +20,15 @@ func (p *parser) parseSecurityScheme( if scheme == nil { return nil, errors.New("securityScheme is empty or null") } + locator := scheme.Common.Locator defer func() { - rerr = p.wrapLocation(ctx.LastLoc(), scheme.Locator, rerr) + rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr) }() if ref := scheme.Ref; ref != "" { resolved, err := p.resolveSecurityScheme(ref, ctx) if err != nil { - return nil, p.wrapRef(ctx.LastLoc(), scheme.Locator, err) + return nil, p.wrapRef(ctx.LastLoc(), locator, err) } return resolved, nil } @@ -38,11 +40,11 @@ func (p *parser) parseSecurityScheme( case "query", "header", "cookie": default: err := errors.Errorf(`invalid "in": %q`, scheme.In) - return p.wrapField("in", ctx.LastLoc(), scheme.Locator, err) + return p.wrapField("in", ctx.LastLoc(), locator, err) } if scheme.Name == "" { err := errors.New(`"name" is required and MUST be a non-empty string`) - return p.wrapField("name", ctx.LastLoc(), scheme.Locator, err) + return p.wrapField("name", ctx.LastLoc(), locator, err) } return nil case "http": @@ -64,23 +66,23 @@ func (p *parser) parseSecurityScheme( "vapid": default: err := errors.Errorf(`invalid "scheme": %q`, scheme.Scheme) - return p.wrapField("scheme", ctx.LastLoc(), scheme.Locator, err) + return p.wrapField("scheme", ctx.LastLoc(), locator, err) } return nil case "mutualTLS": return nil case "oauth2": err := p.validateOAuthFlows(scheme.Flows, ctx.LastLoc()) - return p.wrapField("flows", ctx.LastLoc(), scheme.Locator, err) + return p.wrapField("flows", ctx.LastLoc(), locator, err) case "openIdConnect": if _, err := url.ParseRequestURI(scheme.OpenIDConnectURL); err != nil { err := errors.Wrap(err, `"openIdConnectUrl" MUST be in the form of a URL`) - return p.wrapField("openIdConnectUrl", ctx.LastLoc(), scheme.Locator, err) + return p.wrapField("openIdConnectUrl", ctx.LastLoc(), locator, err) } return nil default: err := errors.Errorf("unknown security scheme type %q", scheme.Type) - return p.wrapField("type", ctx.LastLoc(), scheme.Locator, err) + return p.wrapField("type", ctx.LastLoc(), locator, err) } }(); err != nil { return nil, errors.Wrap(err, scheme.Type) @@ -113,16 +115,18 @@ func (p *parser) validateOAuthFlows(flows *ogen.OAuthFlows, loc string) (rerr er if flows == nil { return errors.New("oAuthFlows is empty or null") } + locator := flows.Common.Locator defer func() { - rerr = p.wrapLocation(loc, flows.Locator, rerr) + rerr = p.wrapLocation(loc, locator, rerr) }() check := func(flow *ogen.OAuthFlow, authURL, tokenURL bool) (rerr error) { if flow == nil { return nil } + locator := flow.Common.Locator defer func() { - rerr = p.wrapLocation(loc, flow.Locator, rerr) + rerr = p.wrapLocation(loc, locator, rerr) }() checkURL := func(name, input string, check bool) error { @@ -131,7 +135,7 @@ func (p *parser) validateOAuthFlows(flows *ogen.OAuthFlows, loc string) (rerr er } if _, err := url.ParseRequestURI(input); err != nil { err := errors.Wrapf(err, `%q MUST be in the form of a URL`, name) - return p.wrapField(name, loc, flow.Locator, err) + return p.wrapField(name, loc, locator, err) } return nil } @@ -156,17 +160,13 @@ func cloneOAuthFlows(flows ogen.OAuthFlows) (r openapi.OAuthFlows) { if flow == nil { return nil } - r := &openapi.OAuthFlow{ + return &openapi.OAuthFlow{ AuthorizationURL: flow.AuthorizationURL, TokenURL: flow.TokenURL, RefreshURL: flow.RefreshURL, - Scopes: make(map[string]string, len(flow.Scopes)), - Locator: flow.Locator, + Scopes: maps.Clone(flow.Scopes), + Locator: flow.Common.Locator, } - for k, v := range flow.Scopes { - r.Scopes[k] = v - } - return r } return openapi.OAuthFlows{ @@ -174,7 +174,7 @@ func cloneOAuthFlows(flows ogen.OAuthFlows) (r openapi.OAuthFlows) { Password: cloneFlow(flows.Password), ClientCredentials: cloneFlow(flows.ClientCredentials), AuthorizationCode: cloneFlow(flows.AuthorizationCode), - Locator: flows.Locator, + Locator: flows.Common.Locator, } } @@ -231,7 +231,7 @@ func (p *parser) parseSecurityRequirements( BearerFormat: spec.BearerFormat, Flows: cloneOAuthFlows(flows), OpenIDConnectURL: spec.OpenIDConnectURL, - Locator: spec.Locator, + Locator: spec.Common.Locator, }, }) } From 19e884d199cb9208f5ca32112d44bbd40a729637 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 13 Sep 2022 16:28:10 +0300 Subject: [PATCH 4/5] fix(jsonschema): remove pointer to let yaml `inline` directive work --- jsonschema/extensions.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/jsonschema/extensions.go b/jsonschema/extensions.go index 6b5d88f46..fa6c7206e 100644 --- a/jsonschema/extensions.go +++ b/jsonschema/extensions.go @@ -13,7 +13,7 @@ import ( ) // Extensions map is "^x-" fields list. -type Extensions map[string]*yaml.Node +type Extensions map[string]yaml.Node func isExtensionKey(key string) bool { return strings.HasPrefix(key, "x-") @@ -23,12 +23,13 @@ func isExtensionKey(key string) bool { func (p Extensions) MarshalYAML() (any, error) { content := make([]*yaml.Node, 0, len(p)*2) for key, val := range p { + val := val if !isExtensionKey(key) { continue } content = append(content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, - val, + &val, ) } @@ -61,8 +62,9 @@ func (p *Extensions) UnmarshalYAML(node *yaml.Node) error { if err := keyNode.Decode(&key); err != nil { return err } - if isExtensionKey(key) { - m[key] = value + // FIXME(tdakkota): use *yamlx.Node instead of yaml.Node + if isExtensionKey(key) && value != nil { + m[key] = *value } } return nil @@ -74,12 +76,13 @@ func (p Extensions) MarshalJSON() ([]byte, error) { e.ObjStart() for key, val := range p { + val := val if !isExtensionKey(key) { continue } e.FieldStart(key) - b, err := convertYAMLtoRawJSON(val) + b, err := convertYAMLtoRawJSON(&val) if err != nil { return nil, errors.Wrap(err, "marshal") } @@ -112,7 +115,7 @@ func (p *Extensions) UnmarshalJSON(data []byte) error { if err != nil { return err } - m[key] = value + m[key] = *value return nil }) From a6622e87b7d4e4c58117636ec36c9837784ac563 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 13 Sep 2022 19:05:44 +0300 Subject: [PATCH 5/5] test: add some tests to ensure extensions parsing --- jsonschema/parser_test.go | 43 +++++++++++++++++++++++++++++++++++++++ spec_test.go | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 spec_test.go diff --git a/jsonschema/parser_test.go b/jsonschema/parser_test.go index 505db4cc9..5e001c85b 100644 --- a/jsonschema/parser_test.go +++ b/jsonschema/parser_test.go @@ -1,14 +1,17 @@ package jsonschema import ( + "fmt" "strconv" "strings" "testing" "github.com/go-faster/errors" + yaml "github.com/go-faster/yamlx" "github.com/stretchr/testify/require" "github.com/ogen-go/ogen/internal/jsonpointer" + "github.com/ogen-go/ogen/internal/location" ) type components map[string]*RawSchema @@ -345,6 +348,46 @@ func TestSchemaReferencedArray(t *testing.T) { require.Equal(t, expect, out) } +func TestSchemaExtensions(t *testing.T) { + tests := []struct { + raw string + expect *Schema + expectErr bool + }{ + { + `{"type": "string", "x-ogen-name": "foo"}`, + &Schema{ + Type: String, + XOgenName: "foo", + }, + false, + }, + {`{"type": "string", "x-ogen-name": {}}`, nil, true}, + } + + for i, tt := range tests { + tt := tt + t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { + a := require.New(t) + + var raw RawSchema + a.NoError(yaml.Unmarshal([]byte(tt.raw), &raw)) + + out, err := NewParser(Settings{ + Filename: "test.yaml", + }).Parse(&raw) + if tt.expectErr { + a.Error(err) + return + } + a.NoError(err) + // Zero locator to simplify comparison. + out.Locator = location.Locator{} + a.Equal(tt.expect, out) + }) + } +} + func TestInvalidMultipleOf(t *testing.T) { values := []int{0, -1, -10} parser := NewParser(Settings{ diff --git a/spec_test.go b/spec_test.go new file mode 100644 index 000000000..bd15bb713 --- /dev/null +++ b/spec_test.go @@ -0,0 +1,43 @@ +package ogen_test + +import ( + "testing" + + yaml "github.com/go-faster/yamlx" + "github.com/stretchr/testify/require" + + "github.com/ogen-go/ogen" +) + +func encodeDecode[T any](a *require.Assertions, input T) (result T) { + data, err := yaml.Marshal(input) + a.NoError(err) + + a.NoError(yaml.Unmarshal(data, &result)) + return result +} + +func TestExtensionParsing(t *testing.T) { + a := require.New(t) + + { + var ( + input = `{"url": "/api/v1", "x-ogen-name": "foo"}` + s ogen.Server + ) + a.NoError(yaml.Unmarshal([]byte(input), &s)) + a.Equal("foo", s.Extensions["x-ogen-name"].Value) + s2 := encodeDecode(a, s) + a.Equal("foo", s2.Extensions["x-ogen-name"].Value) + } + + { + var ( + input = `{"description": "foo", "x-ogen-extension": "bar"}` + s ogen.Response + ) + a.NoError(yaml.Unmarshal([]byte(input), &s)) + a.Equal("bar", s.Common.Extensions["x-ogen-extension"].Value) + // FIXME(tdakkota): encodeDecode doesn't work for this type + } +}