From 236504d018ad71c3e253a3b0233484afa8705f8f Mon Sep 17 00:00:00 2001 From: tdakkota Date: Tue, 5 Jul 2022 07:02:38 +0300 Subject: [PATCH] feat(parser): wrap errors with object location --- openapi/parser/errors.go | 51 ++++++++++++++++++++++++++++ openapi/parser/parse_example.go | 5 ++- openapi/parser/parse_header.go | 5 ++- openapi/parser/parse_mediatype.go | 6 +++- openapi/parser/parse_parameter.go | 5 ++- openapi/parser/parse_request_body.go | 6 +++- openapi/parser/parse_response.go | 5 ++- openapi/parser/parse_schema.go | 7 +++- openapi/parser/parse_security.go | 33 ++++++++++-------- openapi/parser/parser.go | 4 +++ 10 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 openapi/parser/errors.go diff --git a/openapi/parser/errors.go b/openapi/parser/errors.go new file mode 100644 index 000000000..1040c90fe --- /dev/null +++ b/openapi/parser/errors.go @@ -0,0 +1,51 @@ +package parser + +import ( + "fmt" + + "github.com/go-faster/errors" + + ogenjson "github.com/ogen-go/ogen/json" +) + +var _ interface { + error + errors.Wrapper + errors.Formatter +} = (*LocationError)(nil) + +// LocationError is a wrapper for an error that has a location. +type LocationError struct { + loc ogenjson.Location + err error +} + +// Unwrap implements errors.Wrapper. +func (e *LocationError) Unwrap() error { + return e.err +} + +// FormatError implements errors.Formatter. +func (e *LocationError) FormatError(p errors.Printer) (next error) { + p.Printf("at %s", e.loc) + return e.err +} + +// Error implements error. +func (e *LocationError) Error() string { + return fmt.Sprintf("at %s: %s", e.loc, e.err) +} + +func (p *parser) wrapLocation(l ogenjson.Locatable, err error) error { + if err == nil { + return nil + } + loc, ok := l.Location() + if !ok { + return err + } + return &LocationError{ + loc: loc, + err: err, + } +} diff --git a/openapi/parser/parse_example.go b/openapi/parser/parse_example.go index c625c2ece..0bea0d3e2 100644 --- a/openapi/parser/parse_example.go +++ b/openapi/parser/parse_example.go @@ -7,7 +7,7 @@ import ( "github.com/ogen-go/ogen/openapi" ) -func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (*openapi.Example, error) { +func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (_ *openapi.Example, rerr error) { if e == nil { return nil, nil } @@ -19,6 +19,9 @@ func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (*openapi.Exampl } return ex, nil } + defer func() { + rerr = p.wrapLocation(e, rerr) + }() return &openapi.Example{ Summary: e.Summary, diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go index 19c24d296..39e63cc72 100644 --- a/openapi/parser/parse_header.go +++ b/openapi/parser/parse_header.go @@ -23,7 +23,7 @@ func (p *parser) parseHeaders(headers map[string]*ogen.Header, ctx *resolveCtx) return result, nil } -func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (*openapi.Header, error) { +func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (_ *openapi.Header, rerr error) { if header == nil { return nil, errors.New("header object is empty or null") } @@ -34,6 +34,9 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) } return parsed, nil } + defer func() { + rerr = p.wrapLocation(header, rerr) + }() if header.In != "" { return nil, errors.Errorf(`"in" MUST NOT be specified, got %q`, header.In) diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go index 643a3cf39..92ae81ce2 100644 --- a/openapi/parser/parse_mediatype.go +++ b/openapi/parser/parse_mediatype.go @@ -56,7 +56,11 @@ func (p *parser) parseParameterContent(content map[string]ogen.Media, ctx *resol panic("unreachable") } -func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (*openapi.MediaType, error) { +func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (_ *openapi.MediaType, rerr error) { + defer func() { + rerr = p.wrapLocation(&m, rerr) + }() + s, err := p.parseSchema(m.Schema, ctx) if err != nil { return nil, errors.Wrap(err, "schema") diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go index 787c7b5a4..e66e69d90 100644 --- a/openapi/parser/parse_parameter.go +++ b/openapi/parser/parse_parameter.go @@ -73,7 +73,7 @@ func validateParameter(name string, locatedIn openapi.ParameterLocation, param * return nil } -func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (*openapi.Parameter, error) { +func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (_ *openapi.Parameter, rerr error) { if param == nil { return nil, errors.New("parameter object is empty or null") } @@ -84,6 +84,9 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (*openap } return parsed, nil } + defer func() { + rerr = p.wrapLocation(param, rerr) + }() types := map[string]openapi.ParameterLocation{ "query": openapi.LocationQuery, diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go index beb2a1d25..dc99afeed 100644 --- a/openapi/parser/parse_request_body.go +++ b/openapi/parser/parse_request_body.go @@ -7,7 +7,7 @@ import ( "github.com/ogen-go/ogen/openapi" ) -func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (*openapi.RequestBody, error) { +func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (_ *openapi.RequestBody, rerr error) { if body == nil { return nil, errors.New("requestBody object is empty or null") } @@ -19,6 +19,10 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (*ope return reqBody, nil } + defer func() { + rerr = p.wrapLocation(body, rerr) + }() + if len(body.Content) < 1 { // See https://github.com/OAI/OpenAPI-Specification/discussions/2875. return nil, errors.New("content must have at least one entry") diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go index 7badfd83a..04b7a1b77 100644 --- a/openapi/parser/parse_response.go +++ b/openapi/parser/parse_response.go @@ -38,7 +38,7 @@ func (p *parser) parseResponses(responses ogen.Responses) (_ map[string]*openapi return result, nil } -func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (*openapi.Response, error) { +func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (_ *openapi.Response, rerr error) { if resp == nil { return nil, errors.New("response object is empty or null") } @@ -50,6 +50,9 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (*openapi.R return resp, nil } + defer func() { + rerr = p.wrapLocation(resp, rerr) + }() content, err := p.parseContent(resp.Content, ctx) if err != nil { diff --git a/openapi/parser/parse_schema.go b/openapi/parser/parse_schema.go index 4f987bfee..7d5b2af22 100644 --- a/openapi/parser/parse_schema.go +++ b/openapi/parser/parse_schema.go @@ -10,7 +10,12 @@ import ( "github.com/ogen-go/ogen/jsonschema" ) -func (p *parser) parseSchema(schema *ogen.Schema, ctx *resolveCtx) (*jsonschema.Schema, error) { +func (p *parser) parseSchema(schema *ogen.Schema, ctx *resolveCtx) (_ *jsonschema.Schema, rerr error) { + if schema != nil { + defer func() { + rerr = p.wrapLocation(schema, rerr) + }() + } s := schema.ToJSONSchema() if loc := ctx.lastLoc(); s != nil && s.Ref != "" && loc != "" { base, err := url.Parse(loc) diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go index 2b82d44ab..dc16d3713 100644 --- a/openapi/parser/parse_security.go +++ b/openapi/parser/parse_security.go @@ -11,31 +11,34 @@ import ( ) func (p *parser) parseSecurityScheme( - s *ogen.SecurityScheme, + scheme *ogen.SecurityScheme, scopes []string, ctx *resolveCtx, -) (*ogen.SecurityScheme, error) { - if s == nil { +) (_ *ogen.SecurityScheme, rerr error) { + if scheme == nil { return nil, errors.New("securityScheme is empty or null") } - if ref := s.Ref; ref != "" { + if ref := scheme.Ref; ref != "" { sch, err := p.resolveSecurityScheme(ref, ctx) if err != nil { return nil, errors.Wrap(err, "resolve security schema") } return sch, nil } + defer func() { + rerr = p.wrapLocation(scheme, rerr) + }() if err := func() error { - switch s.Type { + switch scheme.Type { case "apiKey": - switch s.In { + switch scheme.In { case "query", "header", "cookie": default: - return errors.Errorf(`invalid "in": %q`, s.In) + return errors.Errorf(`invalid "in": %q`, scheme.In) } - if s.Name == "" { + if scheme.Name == "" { return errors.New(`"name" is required and MUST be a non-empty string`) } return nil @@ -45,7 +48,7 @@ func (p *parser) parseSecurityScheme( // Probably such validation is too strict. // Values from https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml. - switch strings.ToLower(s.Scheme) { + switch strings.ToLower(scheme.Scheme) { case "basic", "bearer", "digest", @@ -57,26 +60,26 @@ func (p *parser) parseSecurityScheme( "scram-sha-256", "vapid": default: - return errors.Errorf(`invalid "scheme": %q`, s.Scheme) + return errors.Errorf(`invalid "scheme": %q`, scheme.Scheme) } return nil case "mutualTLS": return nil case "oauth2": - return validateOAuthFlows(scopes, s.Flows) + return validateOAuthFlows(scopes, scheme.Flows) case "openIdConnect": - if _, err := url.ParseRequestURI(s.OpenIDConnectURL); err != nil { + if _, err := url.ParseRequestURI(scheme.OpenIDConnectURL); err != nil { return errors.Wrap(err, `"openIdConnectUrl" MUST be in the form of a URL`) } return nil default: - return errors.Errorf("unknown security scheme type %q", s.Type) + return errors.Errorf("unknown security scheme type %q", scheme.Type) } }(); err != nil { - return nil, errors.Wrap(err, s.Type) + return nil, errors.Wrap(err, scheme.Type) } - return s, nil + return scheme, nil } func validateOAuthFlows(scopes []string, flows *ogen.OAuthFlows) error { diff --git a/openapi/parser/parser.go b/openapi/parser/parser.go index c1dfa9623..833b5ac63 100644 --- a/openapi/parser/parser.go +++ b/openapi/parser/parser.go @@ -130,6 +130,10 @@ func (p *parser) parseOps() error { } func (p *parser) parseOp(path, httpMethod string, spec ogen.Operation, itemParams []*openapi.Parameter) (_ *openapi.Operation, err error) { + defer func() { + err = p.wrapLocation(&spec, err) + }() + op := &openapi.Operation{ OperationID: spec.OperationID, Summary: spec.Summary,