diff --git a/dsl.go b/dsl.go index ea92d1282..8799b07d0 100644 --- a/dsl.go +++ b/dsl.go @@ -7,7 +7,6 @@ import ( "github.com/go-faster/jx" jsonv2 "github.com/go-json-experiment/json" - "github.com/ogen-go/ogen/gen/ir" "github.com/ogen-go/ogen/jsonschema" "github.com/ogen-go/ogen/openapi" ) @@ -244,7 +243,7 @@ func (r *RequestBody) AddContent(mt string, s *Schema) *RequestBody { // SetJSONContent sets the given Schema under the JSON MediaType to the Content of the Response. func (r *RequestBody) SetJSONContent(s *Schema) *RequestBody { - return r.AddContent(string(ir.ContentTypeJSON), s) + return r.AddContent("application/json", s) } // initContent ensures the Content map is allocated. @@ -597,7 +596,7 @@ func (p *Parameter) SetIn(i string) *Parameter { return p } -// InPath sets the In of the Parameter to "PathItem". +// InPath sets the In of the Parameter to "path". func (p *Parameter) InPath() *Parameter { return p.SetIn(string(openapi.LocationPath)) } @@ -724,7 +723,7 @@ func (r *Response) AddContent(mt string, s *Schema) *Response { // SetJSONContent sets the given Schema under the JSON MediaType to the Content of the Response. func (r *Response) SetJSONContent(s *Schema) *Response { - return r.AddContent(string(ir.ContentTypeJSON), s) + return r.AddContent("application/json", s) } // initContent ensures the Content map is allocated. diff --git a/gen/gen_contents.go b/gen/gen_contents.go index 6dfeacefe..26bb43110 100644 --- a/gen/gen_contents.go +++ b/gen/gen_contents.go @@ -283,6 +283,7 @@ func (g *Generator) generateContents( } g.log.Info(`Content type is unsupported, set "format" to "binary" to use io.Reader`, + g.zapLocation(media), zap.String("contentType", contentType), ) unsupported = append(unsupported, contentType) diff --git a/gen/gen_headers.go b/gen/gen_headers.go index 9c3e8d2c6..d4d1eea8b 100644 --- a/gen/gen_headers.go +++ b/gen/gen_headers.go @@ -4,13 +4,16 @@ import ( "net/http" "github.com/go-faster/errors" - "go.uber.org/zap" "github.com/ogen-go/ogen/gen/ir" "github.com/ogen-go/ogen/openapi" ) -func (g *Generator) generateHeaders(ctx *genctx, name string, headers map[string]*openapi.Header) (_ map[string]*ir.Parameter, err error) { +func (g *Generator) generateHeaders( + ctx *genctx, + name string, + headers map[string]*openapi.Header, +) (_ map[string]*ir.Parameter, err error) { if len(headers) == 0 { return nil, nil } @@ -19,7 +22,10 @@ func (g *Generator) generateHeaders(ctx *genctx, name string, headers map[string for hname, header := range headers { ctx := ctx.appendPath(hname) if http.CanonicalHeaderKey(hname) == "Content-Type" { - g.log.Warn("Content-Type is described separately and will be ignored in this section.", zap.String("pointer", ctx.JSONPointer())) + g.log.Warn( + "Content-Type is described separately and will be ignored in this section.", + g.zapLocation(header), + ) continue } diff --git a/gen/gen_schema.go b/gen/gen_schema.go index 6026b671d..fccbc748e 100644 --- a/gen/gen_schema.go +++ b/gen/gen_schema.go @@ -39,7 +39,7 @@ func saveSchemaTypes(ctx *genctx, gen *schemaGen) error { } func (g *Generator) generateSchema(ctx *genctx, name string, schema *jsonschema.Schema, optional bool) (*ir.Type, error) { - gen := newSchemaGen(ctx.lookupRef) + gen := newSchemaGen(g.opt.Filename, ctx.lookupRef) gen.log = g.log.Named("schemagen") t, err := gen.generate(name, schema, optional) @@ -99,7 +99,8 @@ func GenerateSchema(schema *jsonschema.Schema, fs FileSystem, opts GenerateSchem local: newTStorage(), } - gen := newSchemaGen(func(ref string) (*ir.Type, bool) { + // TODO(tdakkota): pass input filename + gen := newSchemaGen("", func(ref string) (*ir.Type, bool) { return nil, false }) gen.log = opts.Logger.Named("schemagen") diff --git a/gen/generator.go b/gen/generator.go index c58f40a8e..a6673e4bf 100644 --- a/gen/generator.go +++ b/gen/generator.go @@ -138,14 +138,10 @@ func (g *Generator) makeIR(ops []*openapi.Operation) error { for _, spec := range ops { routePath := spec.Path.String() - log := g.log.With( - zap.String("path", routePath), - zap.String("method", spec.HTTPMethod), - zap.String("operationID", spec.OperationID), - ) + log := g.log.With(g.zapLocation(spec)) if !g.opt.Filters.accept(spec) { - g.log.Info("Skipping filtered operation") + log.Info("Skipping filtered operation") continue } diff --git a/gen/panic.go b/gen/panic.go deleted file mode 100644 index adbad9ae6..000000000 --- a/gen/panic.go +++ /dev/null @@ -1,7 +0,0 @@ -package gen - -import "fmt" - -func unreachable(v interface{}) string { - return fmt.Sprintf("unreachable: %v", v) -} diff --git a/gen/schema_gen.go b/gen/schema_gen.go index d534d3673..450f914b5 100644 --- a/gen/schema_gen.go +++ b/gen/schema_gen.go @@ -17,10 +17,12 @@ type schemaGen struct { localRefs map[string]*ir.Type lookupRef func(ref string) (*ir.Type, bool) nameRef func(ref string) (string, error) - log *zap.Logger + + filename string + log *zap.Logger } -func newSchemaGen(lookupRef func(ref string) (*ir.Type, bool)) *schemaGen { +func newSchemaGen(filename string, lookupRef func(ref string) (*ir.Type, bool)) *schemaGen { return &schemaGen{ side: nil, localRefs: map[string]*ir.Type{}, @@ -32,7 +34,8 @@ func newSchemaGen(lookupRef func(ref string) (*ir.Type, bool)) *schemaGen { } return name, nil }, - log: zap.NewNop(), + filename: filename, + log: zap.NewNop(), } } @@ -276,8 +279,8 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T return g.regtype(name, t), nil case jsonschema.Empty: g.log.Info("Type is not defined, using any", + g.zapLocation(schema), zap.String("name", name), - zap.String("ref", schema.Ref), ) return g.regtype(name, ir.Any(schema)), nil default: diff --git a/gen/schema_gen_test.go b/gen/schema_gen_test.go index f915b1802..ccaec76a3 100644 --- a/gen/schema_gen_test.go +++ b/gen/schema_gen_test.go @@ -15,7 +15,7 @@ func TestSchemaGenAnyWarn(t *testing.T) { a := require.New(t) core, ob := observer.New(zap.InfoLevel) - s := newSchemaGen(func(ref string) (*ir.Type, bool) { + s := newSchemaGen("", func(ref string) (*ir.Type, bool) { return nil, false }) s.log = zap.New(core) diff --git a/gen/utils.go b/gen/utils.go index 2bd3838a8..5da0f7f7b 100644 --- a/gen/utils.go +++ b/gen/utils.go @@ -4,10 +4,17 @@ import ( "fmt" "net/http" + "go.uber.org/zap" + "github.com/ogen-go/ogen/gen/ir" + ogenjson "github.com/ogen-go/ogen/json" "github.com/ogen-go/ogen/jsonschema" ) +func unreachable(v interface{}) string { + return fmt.Sprintf("unreachable: %v", v) +} + func isBinary(s *jsonschema.Schema) bool { if s == nil { return false @@ -67,3 +74,23 @@ func statusText(code int) string { } return fmt.Sprintf("Code%d", code) } + +func (g *Generator) zapLocation(l interface { + Location() (ogenjson.Location, bool) +}) zap.Field { + loc, ok := l.Location() + if !ok { + return zap.Skip() + } + return zap.String("at", loc.WithFilename(g.opt.Filename)) +} + +func (g *schemaGen) zapLocation(l interface { + Location() (ogenjson.Location, bool) +}) zap.Field { + loc, ok := l.Location() + if !ok { + return zap.Skip() + } + return zap.String("at", loc.WithFilename(g.filename)) +} diff --git a/json/location.go b/json/location.go index 4527ccf89..db50291d7 100644 --- a/json/location.go +++ b/json/location.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "sort" - "strconv" "github.com/go-json-experiment/json" ) @@ -94,11 +93,31 @@ type Location struct { // String implements fmt.Stringer. func (l Location) String() string { if l.Line == 0 { - return strconv.Quote(l.JSONPointer) + return l.JSONPointer } return fmt.Sprintf("%d:%d", l.Line, l.Column) } +// WithFilename prints the location with the given filename. +// +// If filename is empty, the location is printed as is. +func (l Location) WithFilename(filename string) string { + if filename != "" { + switch { + case l.Line != 0: + // Line is set, so return "${filename}:". + filename += ":" + case l.JSONPointer != "": + // Line is not set, but JSONPointer is set, so return "${filename}#${JSONPointer}". + filename += "#" + default: + // Neither line nor JSONPointer is set, so return empty string. + return "" + } + } + return filename + l.String() +} + // Locatable is an interface for JSON value location store. type Locatable interface { setLocation(Location) diff --git a/jsonschema/parser.go b/jsonschema/parser.go index c49ac5e8e..7ffe81335 100644 --- a/jsonschema/parser.go +++ b/jsonschema/parser.go @@ -400,6 +400,8 @@ func (p *Parser) extendInfo(schema *RawSchema, s *Schema) *Schema { Mapping: d.Mapping, } } + + s.Locator = schema.Locator return s } diff --git a/jsonschema/schema.go b/jsonschema/schema.go index 826768d85..031b29efc 100644 --- a/jsonschema/schema.go +++ b/jsonschema/schema.go @@ -4,6 +4,8 @@ import ( "regexp" "github.com/go-json-experiment/json" + + ogenjson "github.com/ogen-go/ogen/json" ) // SchemaType is a JSON Schema type. @@ -81,6 +83,8 @@ type Schema struct { // Default schema value. Default interface{} DefaultSet bool + + ogenjson.Locator } // AddExample adds example for this Schema. diff --git a/openapi/example.go b/openapi/example.go index 3c3d0bc18..b11101310 100644 --- a/openapi/example.go +++ b/openapi/example.go @@ -1,12 +1,19 @@ package openapi -import "github.com/go-json-experiment/json" +import ( + "github.com/go-json-experiment/json" + + ogenjson "github.com/ogen-go/ogen/json" +) // Example is an OpenAPI Example. type Example struct { - Ref string `json:"$ref,omitempty"` // ref object - Summary string `json:"summary,omitempty"` - Description string `json:"description,omitempty"` - Value json.RawValue `json:"value,omitempty"` - ExternalValue string `json:"externalValue,omitempty"` + Ref string + + Summary string + Description string + Value json.RawValue + ExternalValue string + + ogenjson.Locator } diff --git a/openapi/mediatype.go b/openapi/mediatype.go index 82dc7d856..f628760b5 100644 --- a/openapi/mediatype.go +++ b/openapi/mediatype.go @@ -3,6 +3,7 @@ package openapi import ( "github.com/go-json-experiment/json" + ogenjson "github.com/ogen-go/ogen/json" "github.com/ogen-go/ogen/jsonschema" ) @@ -12,6 +13,8 @@ type MediaType struct { Example json.RawValue Examples map[string]*Example Encoding map[string]*Encoding + + ogenjson.Locator } // Encoding is Encoding Type Object. @@ -21,4 +24,6 @@ type Encoding struct { Style ParameterStyle Explode bool AllowReserved bool + + ogenjson.Locator } diff --git a/openapi/operation.go b/openapi/operation.go index 2a858f784..28b3ed209 100644 --- a/openapi/operation.go +++ b/openapi/operation.go @@ -1,5 +1,9 @@ package openapi +import ( + ogenjson "github.com/ogen-go/ogen/json" +) + // Operation is an OpenAPI Operation. type Operation struct { OperationID string // optional @@ -23,6 +27,8 @@ type Operation struct { // * default // * 1XX, 2XX, 3XX, 4XX, 5XX Responses map[string]*Response + + ogenjson.Locator } // Path is an operation path. @@ -52,6 +58,8 @@ type RequestBody struct { Description string Required bool Content map[string]*MediaType + + ogenjson.Locator } // Header is an OpenAPI Header definition. @@ -64,4 +72,6 @@ type Response struct { Headers map[string]*Header Content map[string]*MediaType // Links map[string]*Link + + ogenjson.Locator } diff --git a/openapi/parameter.go b/openapi/parameter.go index c10851bd1..9998eb300 100644 --- a/openapi/parameter.go +++ b/openapi/parameter.go @@ -1,6 +1,9 @@ package openapi -import "github.com/ogen-go/ogen/jsonschema" +import ( + ogenjson "github.com/ogen-go/ogen/json" + "github.com/ogen-go/ogen/jsonschema" +) // ParameterLocation defines where OpenAPI parameter is located. type ParameterLocation string @@ -30,16 +33,19 @@ func (l ParameterLocation) Cookie() bool { return l == LocationCookie } // Parameter is an OpenAPI Operation Parameter. type Parameter struct { - Ref string + Ref string + Name string Description string + Deprecated bool Schema *jsonschema.Schema Content *ParameterContent In ParameterLocation Style ParameterStyle Explode bool Required bool - Deprecated bool + + ogenjson.Locator } // ParameterContent describes OpenAPI Parameter content field. diff --git a/openapi/parser/errors.go b/openapi/parser/errors.go index e572137ef..753f1fc12 100644 --- a/openapi/parser/errors.go +++ b/openapi/parser/errors.go @@ -26,33 +26,15 @@ func (e *LocationError) Unwrap() error { return e.err } -func (e *LocationError) fileName() string { - filename := e.file - if filename != "" { - switch { - case e.loc.Line != 0: - // Line is set, so return "${filename}:". - filename += ":" - case e.loc.JSONPointer != "": - // Line is not set, but JSONPointer is set, so return "${filename}#${JSONPointer}". - filename += "#" - default: - // Neither line nor JSONPointer is set, so return empty string. - return "" - } - } - return filename -} - // FormatError implements errors.Formatter. func (e *LocationError) FormatError(p errors.Printer) (next error) { - p.Printf("at %s%s", e.fileName(), e.loc) + p.Printf("at %s", e.loc.WithFilename(e.file)) return e.err } // Error implements error. func (e *LocationError) Error() string { - return fmt.Sprintf("at %s%s: %s", e.fileName(), e.loc, e.err) + return fmt.Sprintf("at %s: %s", e.loc.WithFilename(e.file), e.err) } func (p *parser) wrapLocation(l ogenjson.Locatable, err error) error { diff --git a/openapi/parser/parse_example.go b/openapi/parser/parse_example.go index 0bea0d3e2..a3aecf73a 100644 --- a/openapi/parser/parse_example.go +++ b/openapi/parser/parse_example.go @@ -28,5 +28,6 @@ func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (_ *openapi.Exam Description: e.Description, Value: e.Value, ExternalValue: e.ExternalValue, + Locator: e.Locator, }, nil } diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go index 6f32ccb66..aa87660d8 100644 --- a/openapi/parser/parse_header.go +++ b/openapi/parser/parse_header.go @@ -63,13 +63,14 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) op := &openapi.Header{ Name: name, Description: header.Description, + Deprecated: header.Deprecated, Schema: schema, Content: content, In: locatedIn, Style: inferParamStyle(locatedIn, header.Style), Explode: inferParamExplode(locatedIn, header.Explode), Required: header.Required, - Deprecated: header.Deprecated, + Locator: header.Locator, } if header.Content != nil { diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go index af3081c98..e9fc7e016 100644 --- a/openapi/parser/parse_mediatype.go +++ b/openapi/parser/parse_mediatype.go @@ -89,6 +89,7 @@ func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (_ *openapi.Media Style: inferParamStyle(openapi.LocationQuery, e.Style), Explode: inferParamExplode(openapi.LocationQuery, e.Explode), AllowReserved: e.AllowReserved, + Locator: e.Locator, } encoding.Headers, err = p.parseHeaders(e.Headers, ctx) if err != nil { @@ -143,5 +144,6 @@ func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (_ *openapi.Media Example: m.Example, Examples: examples, Encoding: encodings, + Locator: m.Locator, }, nil } diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go index 8b56c174b..fd8c0efb4 100644 --- a/openapi/parser/parse_parameter.go +++ b/openapi/parser/parse_parameter.go @@ -11,6 +11,28 @@ import ( "github.com/ogen-go/ogen/openapi" ) +func mergeParams(opParams, itemParams []*openapi.Parameter) []*openapi.Parameter { + lookupOp := func(name string, in openapi.ParameterLocation) bool { + for _, param := range opParams { + if param.Name == name && param.In == in { + return true + } + } + return false + } + + for _, param := range itemParams { + // Param defined in operation take precedence over param defined in pathItem. + if lookupOp(param.Name, param.In) { + continue + } + + opParams = append(opParams, param) + } + + return opParams +} + func (p *parser) parseParams(params []*ogen.Parameter) ([]*openapi.Parameter, error) { // Unique parameter is defined by a combination of a name and location. type pnameLoc struct { diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go index 1a5f773e1..29e452749 100644 --- a/openapi/parser/parse_request_body.go +++ b/openapi/parser/parse_request_body.go @@ -35,7 +35,8 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (_ *o return &openapi.RequestBody{ Description: body.Description, - Content: content, Required: body.Required, + Content: content, + Locator: body.Locator, }, nil } diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go index b3ab6a45a..cd13380f4 100644 --- a/openapi/parser/parse_response.go +++ b/openapi/parser/parse_response.go @@ -66,8 +66,9 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (_ *openapi return &openapi.Response{ Description: resp.Description, - Content: content, Headers: headers, + Content: content, + Locator: resp.Locator, }, nil } diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go index ca9e0416b..4271b4e15 100644 --- a/openapi/parser/parse_security.go +++ b/openapi/parser/parse_security.go @@ -149,6 +149,7 @@ func cloneOAuthFlows(flows ogen.OAuthFlows) (r openapi.OAuthFlows) { TokenURL: flow.TokenURL, RefreshURL: flow.RefreshURL, Scopes: make(map[string]string, len(flow.Scopes)), + Locator: flow.Locator, } for k, v := range flow.Scopes { r.Scopes[k] = v @@ -161,6 +162,7 @@ func cloneOAuthFlows(flows ogen.OAuthFlows) (r openapi.OAuthFlows) { Password: cloneFlow(flows.Password), ClientCredentials: cloneFlow(flows.ClientCredentials), AuthorizationCode: cloneFlow(flows.AuthorizationCode), + Locator: flows.Locator, } } @@ -195,6 +197,7 @@ func (p *parser) parseSecurityRequirements(requirements ogen.SecurityRequirement BearerFormat: spec.BearerFormat, Flows: cloneOAuthFlows(flows), OpenIDConnectURL: spec.OpenIDConnectURL, + Locator: spec.Locator, }, }) } diff --git a/openapi/parser/parser.go b/openapi/parser/parser.go index ea2f882ad..27ad1e9f0 100644 --- a/openapi/parser/parser.go +++ b/openapi/parser/parser.go @@ -156,6 +156,7 @@ func (p *parser) parseOp( Description: spec.Description, Deprecated: spec.Deprecated, HTTPMethod: httpMethod, + Locator: spec.Locator, } opParams, err := p.parseParams(spec.Parameters) @@ -200,24 +201,26 @@ func (p *parser) parseOp( return op, nil } -func mergeParams(opParams, itemParams []*openapi.Parameter) []*openapi.Parameter { - lookupOp := func(name string, in openapi.ParameterLocation) bool { - for _, param := range opParams { - if param.Name == name && param.In == in { - return true - } +func forEachOps(item *ogen.PathItem, f func(method string, op ogen.Operation) error) error { + var err error + handle := func(method string, op *ogen.Operation) { + if err != nil || op == nil { + return } - return false - } - for _, param := range itemParams { - // Param defined in operation take precedence over param defined in pathItem. - if lookupOp(param.Name, param.In) { - continue + err = f(method, *op) + if err != nil { + err = errors.Wrap(err, method) } - - opParams = append(opParams, param) } - return opParams + handle("get", item.Get) + handle("put", item.Put) + handle("post", item.Post) + handle("delete", item.Delete) + handle("options", item.Options) + handle("head", item.Head) + handle("patch", item.Patch) + handle("trace", item.Trace) + return err } diff --git a/openapi/parser/utils.go b/openapi/parser/utils.go deleted file mode 100644 index d503d1ae4..000000000 --- a/openapi/parser/utils.go +++ /dev/null @@ -1,30 +0,0 @@ -package parser - -import ( - "github.com/go-faster/errors" - "github.com/ogen-go/ogen" -) - -func forEachOps(item *ogen.PathItem, f func(method string, op ogen.Operation) error) error { - var err error - handle := func(method string, op *ogen.Operation) { - if err != nil || op == nil { - return - } - - err = f(method, *op) - if err != nil { - err = errors.Wrap(err, method) - } - } - - handle("get", item.Get) - handle("put", item.Put) - handle("post", item.Post) - handle("delete", item.Delete) - handle("options", item.Options) - handle("head", item.Head) - handle("patch", item.Patch) - handle("trace", item.Trace) - return err -} diff --git a/openapi/security.go b/openapi/security.go index efce09d60..d3d7e298d 100644 --- a/openapi/security.go +++ b/openapi/security.go @@ -1,5 +1,9 @@ package openapi +import ( + ogenjson "github.com/ogen-go/ogen/json" +) + // SecurityRequirements is parsed security requirements. type SecurityRequirements struct { Scopes []string @@ -17,6 +21,8 @@ type Security struct { BearerFormat string Flows OAuthFlows OpenIDConnectURL string + + ogenjson.Locator } // OAuthFlows allows configuration of the supported OAuth Flows. @@ -25,6 +31,8 @@ type OAuthFlows struct { Password *OAuthFlow ClientCredentials *OAuthFlow AuthorizationCode *OAuthFlow + + ogenjson.Locator } // OAuthFlow is configuration details for a supported OAuth Flow. @@ -33,4 +41,6 @@ type OAuthFlow struct { TokenURL string RefreshURL string Scopes map[string]string // name -> description + + ogenjson.Locator }