diff --git a/_testdata/positive/custom_formats.json b/_testdata/positive/custom_formats.json index cc8201e1c..c1745d0e0 100644 --- a/_testdata/positive/custom_formats.json +++ b/_testdata/positive/custom_formats.json @@ -5,6 +5,32 @@ "version": "v0.1.0" }, "paths": { + "/event": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + } + }, + "responses": { + "200": { + "description": "description", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + } + } + } + } + }, "/phone": { "get": { "parameters": [ @@ -75,6 +101,9 @@ } }, "schemas": { + "Event": { + "format": "x-my-event" + }, "User": { "type": "object", "required": [ diff --git a/gen/custom_format.go b/gen/custom_format.go index c4e830967..eb6415b36 100644 --- a/gen/custom_format.go +++ b/gen/custom_format.go @@ -19,7 +19,16 @@ func checkImportableType(typ reflect.Type) error { name := typ.Name() if name == "" { - return errors.New("type must be named or primitive") + switch typ.Kind() { + case reflect.Interface: + if typ.NumMethod() == 0 { + // Allow empty interface. + break + } + fallthrough + default: + return errors.New("type must be named or primitive") + } } if path != "" && !token.IsExported(name) { diff --git a/gen/ir/custom_format.go b/gen/ir/custom_format.go index 2af3d5d50..c680694cf 100644 --- a/gen/ir/custom_format.go +++ b/gen/ir/custom_format.go @@ -15,8 +15,16 @@ type ExternalType struct { // Go returns valid Go type for this ExternalType. func (c ExternalType) Go() string { if c.Pkg == "" { - // Primitive type. - return c.Type.Name() + switch t := c.Type; t.Kind() { + case reflect.Interface: + if t.NumMethod() != 0 { + panic(fmt.Sprintf("unexpected interface type: %v", t)) + } + return "any" + default: + // Primitive type. + return t.Name() + } } return fmt.Sprintf("%s.%s", c.Pkg, c.Type.Name()) } diff --git a/gen/schema_gen.go b/gen/schema_gen.go index ee51e6c59..af9335804 100644 --- a/gen/schema_gen.go +++ b/gen/schema_gen.go @@ -276,6 +276,12 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T return nil, errors.Wrap(err, "primitive") } + fields := []zap.Field{ + zapPosition(schema), + zap.String("type", string(schema.Type)), + zap.String("format", schema.Format), + zap.String("go_type", t.Go()), + } switch schema.Type { case jsonschema.String: if err := t.Validators.SetString(schema); err != nil { @@ -285,26 +291,39 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T switch t.Primitive { case ir.String, ir.ByteSlice: default: - g.log.Warn("String validator cannot be applied to non-string type and will be ignored", - zapPosition(schema), - zap.String("type", string(schema.Type)), - zap.String("format", schema.Format), - zap.String("go_type", t.Go()), - ) + g.log.Warn("String validator cannot be applied to generated type and will be ignored", fields...) } } case jsonschema.Integer: if err := t.Validators.SetInt(schema); err != nil { return nil, errors.Wrap(err, "int validator") } + if t.Validators.Int.Set() { + switch t.Primitive { + case ir.String, ir.ByteSlice: + default: + g.log.Warn("Int validator cannot be applied to generated type and will be ignored", fields...) + } + } case jsonschema.Number: if err := t.Validators.SetFloat(schema); err != nil { return nil, errors.Wrap(err, "float validator") } + if t.Validators.Float.Set() { + switch t.Primitive { + case ir.String, ir.ByteSlice: + default: + g.log.Warn("Float validator cannot be applied to generated type and will be ignored", fields...) + } + } } return g.regtype(name, t), nil case jsonschema.Empty: + if format, ok := g.customFormats[schema.Type][schema.Format]; ok { + return g.customFormat(format, schema), nil + } + g.log.Info("Type is not defined, using any", zapPosition(schema), zap.String("name", name), diff --git a/gen/schema_gen_primitive.go b/gen/schema_gen_primitive.go index 3c091bb11..f2295c018 100644 --- a/gen/schema_gen_primitive.go +++ b/gen/schema_gen_primitive.go @@ -153,9 +153,7 @@ func (g *schemaGen) parseSimple(schema *jsonschema.Schema) *ir.Type { t, found := mapping[schema.Type][schema.Format] if !found { if custom, ok := g.customFormats[schema.Type][schema.Format]; ok { - typ := ir.Primitive(ir.Custom, schema) - typ.CustomFormat = &custom - return typ + return g.customFormat(custom, schema) } // Fallback to default. t = mapping[schema.Type][""] @@ -164,6 +162,12 @@ func (g *schemaGen) parseSimple(schema *jsonschema.Schema) *ir.Type { return ir.Primitive(t, schema) } +func (g *schemaGen) customFormat(custom ir.CustomFormat, schema *jsonschema.Schema) *ir.Type { + typ := ir.Primitive(ir.Custom, schema) + typ.CustomFormat = &custom + return typ +} + func TypeFormatMapping() map[jsonschema.SchemaType]map[string]ir.PrimitiveType { return map[jsonschema.SchemaType]map[string]ir.PrimitiveType{ jsonschema.Integer: { diff --git a/internal/integration/cmd/customformats/main.go b/internal/integration/cmd/customformats/main.go index 117b50e83..1b74bad5e 100644 --- a/internal/integration/cmd/customformats/main.go +++ b/internal/integration/cmd/customformats/main.go @@ -11,6 +11,7 @@ import ( "github.com/ogen-go/ogen" "github.com/ogen-go/ogen/gen" "github.com/ogen-go/ogen/gen/genfs" + "github.com/ogen-go/ogen/internal/integration/customformats/eventtype" "github.com/ogen-go/ogen/internal/integration/customformats/hextype" "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" @@ -58,6 +59,9 @@ func run(specPath, targetDir string) error { "rgba": rgbatype.RGBAFormat, "hex": hextype.HexFormat, }, + jsonschema.Empty: { + "x-my-event": eventtype.EventFormat, + }, }, File: location.NewFile(fileName, specPath, data), Logger: l, diff --git a/internal/integration/customformats/eventtype/eventtype.go b/internal/integration/customformats/eventtype/eventtype.go new file mode 100644 index 000000000..27811bdc3 --- /dev/null +++ b/internal/integration/customformats/eventtype/eventtype.go @@ -0,0 +1,60 @@ +// Package eventtype defines a custom format for event types. +package eventtype + +import ( + "encoding/json" + + "github.com/go-faster/jx" + + "github.com/ogen-go/ogen/gen" +) + +type Event = any + +// EventFormat defines a custom format for Event. +var EventFormat = gen.CustomFormat[ + Event, + JSONEventEncoding, + TextEventEncoding, +]() + +// JSONEventEncoding defines a custom JSON encoding for hexadecimal numbers. +type JSONEventEncoding struct{} + +// EncodeJSON encodes a hexadecimal number as a JSON string. +func (JSONEventEncoding) EncodeJSON(e *jx.Encoder, v Event) { + b, err := json.Marshal(v) + if err != nil { + e.Null() + return + } + e.Raw(b) +} + +// DecodeJSON decodes a hexadecimal number from a JSON string. +func (JSONEventEncoding) DecodeJSON(d *jx.Decoder) (v Event, _ error) { + r, err := d.Raw() + if err != nil { + return v, err + } + err = json.Unmarshal(r, &v) + return v, err +} + +// TextEventEncoding defines a custom text encoding for hexadecimal numbers. +type TextEventEncoding struct{} + +// EncodeText encodes a hexadecimal number as a string. +func (TextEventEncoding) EncodeText(v Event) string { + b, err := json.Marshal(v) + if err != nil { + return "" + } + return string(b) +} + +// DecodeText decodes a hexadecimal number from a string. +func (TextEventEncoding) DecodeText(s string) (v Event, _ error) { + err := json.Unmarshal([]byte(s), &v) + return v, err +} diff --git a/internal/integration/customformats_test.go b/internal/integration/customformats_test.go index 934bdcfe6..229e483b6 100644 --- a/internal/integration/customformats_test.go +++ b/internal/integration/customformats_test.go @@ -5,6 +5,7 @@ import ( "net/http/httptest" "testing" + "github.com/go-faster/errors" "github.com/stretchr/testify/require" "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" @@ -14,6 +15,13 @@ import ( type testCustomFormats struct{} +func (t testCustomFormats) EventPost(ctx context.Context, req any) (any, error) { + if req == nil { + return nil, errors.New("empty request") + } + return req, nil +} + func (t testCustomFormats) PhoneGet(ctx context.Context, req *api.User, params api.PhoneGetParams) (*api.User, error) { req.HomePhone.SetTo(params.Phone) if v, ok := params.Color.Get(); ok { @@ -26,41 +34,61 @@ func (t testCustomFormats) PhoneGet(ctx context.Context, req *api.User, params a } func TestCustomFormats(t *testing.T) { - a := require.New(t) ctx := context.Background() srv, err := api.NewServer(testCustomFormats{}) - a.NoError(err) + require.NoError(t, err) s := httptest.NewServer(srv) defer s.Close() client, err := api.NewClient(s.URL, api.WithClient(s.Client())) - a.NoError(err) + require.NoError(t, err) - var ( - homePhone = phonetype.Phone("+1234567890") - backgroundColor = rgbatype.RGBA{R: 255, G: 0, B: 0, A: 255} - hex = int64(100) + t.Run("EventPost", func(t *testing.T) { + a := require.New(t) - u = &api.User{ - ID: 10, - Phone: "+1234567890", - ProfileColor: rgbatype.RGBA{R: 0, G: 0, B: 0, A: 255}, + for _, val := range []any{ + true, + float64(42), + "string", + []any{float64(1), float64(2), float64(3)}, + map[string]any{ + "key": []any{"value", "value2"}, + }, + } { + result, err := client.EventPost(ctx, val) + a.NoError(err) + a.Equal(val, result) } - ) + }) + t.Run("Phone", func(t *testing.T) { + a := require.New(t) + + var ( + homePhone = phonetype.Phone("+1234567890") + backgroundColor = rgbatype.RGBA{R: 255, G: 0, B: 0, A: 255} + hex = int64(100) + + u = &api.User{ + ID: 10, + Phone: "+1234567890", + ProfileColor: rgbatype.RGBA{R: 0, G: 0, B: 0, A: 255}, + } + ) + + u2, err := client.PhoneGet(ctx, u, api.PhoneGetParams{ + Phone: homePhone, + Color: api.NewOptRgba(backgroundColor), + Hex: api.NewOptHex(hex), + }) + a.NoError(err) - u2, err := client.PhoneGet(ctx, u, api.PhoneGetParams{ - Phone: homePhone, - Color: api.NewOptRgba(backgroundColor), - Hex: api.NewOptHex(hex), + a.Equal(u.ID, u2.ID) + a.Equal(u.Phone, u2.Phone) + a.Equal(u.ProfileColor, u2.ProfileColor) + a.Equal(homePhone, u2.HomePhone.Or("")) + a.Equal(backgroundColor, u2.BackgroundColor.Or(rgbatype.RGBA{})) + a.Equal(hex, u2.HexColor.Or(0)) }) - a.NoError(err) - - a.Equal(u.ID, u2.ID) - a.Equal(u.Phone, u2.Phone) - a.Equal(u.ProfileColor, u2.ProfileColor) - a.Equal(homePhone, u2.HomePhone.Or("")) - a.Equal(backgroundColor, u2.BackgroundColor.Or(rgbatype.RGBA{})) - a.Equal(hex, u2.HexColor.Or(0)) } diff --git a/internal/integration/test_customformats/oas_cfg_gen.go b/internal/integration/test_customformats/oas_cfg_gen.go index 6eeb82570..d6f6ca41f 100644 --- a/internal/integration/test_customformats/oas_cfg_gen.go +++ b/internal/integration/test_customformats/oas_cfg_gen.go @@ -11,9 +11,10 @@ import ( "go.opentelemetry.io/otel/trace" ht "github.com/ogen-go/ogen/http" - custom0 "github.com/ogen-go/ogen/internal/integration/customformats/hextype" - custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" - custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" + custom0 "github.com/ogen-go/ogen/internal/integration/customformats/eventtype" + custom1 "github.com/ogen-go/ogen/internal/integration/customformats/hextype" + custom2 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" + custom3 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" "github.com/ogen-go/ogen/middleware" "github.com/ogen-go/ogen/ogenerrors" "github.com/ogen-go/ogen/otelogen" @@ -21,20 +22,26 @@ import ( var ( formatHex = func() (r struct { - custom0.JSONHexEncoding - custom0.TextHexEncoding + custom1.JSONHexEncoding + custom1.TextHexEncoding }) { return r } formatPhone = func() (r struct { - custom1.JSONPhoneEncoding - custom1.TextPhoneEncoding + custom2.JSONPhoneEncoding + custom2.TextPhoneEncoding }) { return r } formatRgba = func() (r struct { - custom2.JSONRGBAEncoding - custom2.TextRGBAEncoding + custom3.JSONRGBAEncoding + custom3.TextRGBAEncoding + }) { + return r + } + formatXMyEvent = func() (r struct { + custom0.JSONEventEncoding + custom0.TextEventEncoding }) { return r } diff --git a/internal/integration/test_customformats/oas_client_gen.go b/internal/integration/test_customformats/oas_client_gen.go index 591592793..55819021a 100644 --- a/internal/integration/test_customformats/oas_client_gen.go +++ b/internal/integration/test_customformats/oas_client_gen.go @@ -56,6 +56,72 @@ func (c *Client) requestURL(ctx context.Context) *url.URL { return u } +// EventPost invokes POST /event operation. +// +// POST /event +func (c *Client) EventPost(ctx context.Context, request any) (any, error) { + res, err := c.sendEventPost(ctx, request) + _ = res + return res, err +} + +func (c *Client) sendEventPost(ctx context.Context, request any) (res any, err error) { + var otelAttrs []attribute.KeyValue + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, otelAttrs...) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, "EventPost", + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, otelAttrs...) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + u.Path += "/event" + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u, nil) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeEventPostRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + defer resp.Body.Close() + + stage = "DecodeResponse" + result, err := decodeEventPostResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // PhoneGet invokes GET /phone operation. // // GET /phone diff --git a/internal/integration/test_customformats/oas_handlers_gen.go b/internal/integration/test_customformats/oas_handlers_gen.go index 5d11f2d2b..19c10bab7 100644 --- a/internal/integration/test_customformats/oas_handlers_gen.go +++ b/internal/integration/test_customformats/oas_handlers_gen.go @@ -14,6 +14,101 @@ import ( "github.com/ogen-go/ogen/ogenerrors" ) +// handleEventPostRequest handles POST /event operation. +// +// POST /event +func (s *Server) handleEventPostRequest(args [0]string, w http.ResponseWriter, r *http.Request) { + var otelAttrs []attribute.KeyValue + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), "EventPost", + serverSpanKind, + ) + defer span.End() + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + s.duration.Record(ctx, elapsedDuration.Microseconds(), otelAttrs...) + }() + + // Increment request counter. + s.requests.Add(ctx, 1, otelAttrs...) + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + s.errors.Add(ctx, 1, otelAttrs...) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: "EventPost", + ID: "", + } + ) + request, close, err := s.decodeEventPostRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response any + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: "EventPost", + OperationID: "", + Body: request, + Params: middleware.Parameters{}, + Raw: r, + } + + type ( + Request = any + Params = struct{} + Response = any + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + nil, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.EventPost(ctx, request) + return response, err + }, + ) + } else { + response, err = s.h.EventPost(ctx, request) + } + if err != nil { + recordError("Internal", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + if err := encodeEventPostResponse(response, w, span); err != nil { + recordError("EncodeResponse", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } +} + // handlePhoneGetRequest handles GET /phone operation. // // GET /phone diff --git a/internal/integration/test_customformats/oas_json_gen.go b/internal/integration/test_customformats/oas_json_gen.go index b899e812d..aaa254277 100644 --- a/internal/integration/test_customformats/oas_json_gen.go +++ b/internal/integration/test_customformats/oas_json_gen.go @@ -9,8 +9,8 @@ import ( "github.com/go-faster/errors" "github.com/go-faster/jx" - custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" - custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" + custom2 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" + custom3 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" "github.com/ogen-go/ogen/validate" ) @@ -49,16 +49,16 @@ func (s *OptHex) UnmarshalJSON(data []byte) error { return s.Decode(d, formatHex().DecodeJSON) } -// Encode encodes custom1.Phone as json. -func (o OptPhone) Encode(e *jx.Encoder, format func(*jx.Encoder, custom1.Phone)) { +// Encode encodes custom2.Phone as json. +func (o OptPhone) Encode(e *jx.Encoder, format func(*jx.Encoder, custom2.Phone)) { if !o.Set { return } formatPhone().EncodeJSON(e, o.Value) } -// Decode decodes custom1.Phone from json. -func (o *OptPhone) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom1.Phone, error)) error { +// Decode decodes custom2.Phone from json. +func (o *OptPhone) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom2.Phone, error)) error { if o == nil { return errors.New("invalid: unable to decode OptPhone to nil") } @@ -84,16 +84,16 @@ func (s *OptPhone) UnmarshalJSON(data []byte) error { return s.Decode(d, formatPhone().DecodeJSON) } -// Encode encodes custom2.RGBA as json. -func (o OptRgba) Encode(e *jx.Encoder, format func(*jx.Encoder, custom2.RGBA)) { +// Encode encodes custom3.RGBA as json. +func (o OptRgba) Encode(e *jx.Encoder, format func(*jx.Encoder, custom3.RGBA)) { if !o.Set { return } formatRgba().EncodeJSON(e, o.Value) } -// Decode decodes custom2.RGBA from json. -func (o *OptRgba) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom2.RGBA, error)) error { +// Decode decodes custom3.RGBA from json. +func (o *OptRgba) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom3.RGBA, error)) error { if o == nil { return errors.New("invalid: unable to decode OptRgba to nil") } diff --git a/internal/integration/test_customformats/oas_parameters_gen.go b/internal/integration/test_customformats/oas_parameters_gen.go index f6443d513..0722248d1 100644 --- a/internal/integration/test_customformats/oas_parameters_gen.go +++ b/internal/integration/test_customformats/oas_parameters_gen.go @@ -5,8 +5,8 @@ package api import ( "net/http" - custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" - custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" + custom2 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" + custom3 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" "github.com/ogen-go/ogen/middleware" "github.com/ogen-go/ogen/ogenerrors" "github.com/ogen-go/ogen/uri" @@ -16,7 +16,7 @@ import ( // PhoneGetParams is parameters of GET /phone operation. type PhoneGetParams struct { // Phone number. - Phone custom1.Phone + Phone custom2.Phone // Color. Color OptRgba // Hex. @@ -29,7 +29,7 @@ func unpackPhoneGetParams(packed middleware.Parameters) (params PhoneGetParams) Name: "phone", In: "query", } - params.Phone = packed[key].(custom1.Phone) + params.Phone = packed[key].(custom2.Phone) } { key := middleware.ParameterKey{ @@ -100,7 +100,7 @@ func decodePhoneGetParams(args [0]string, r *http.Request) (params PhoneGetParam if err := q.HasParam(cfg); err == nil { if err := q.DecodeParam(cfg, func(d uri.Decoder) error { - var paramsDotColorVal custom2.RGBA + var paramsDotColorVal custom3.RGBA if err := func() error { val, err := d.DecodeValue() if err != nil { diff --git a/internal/integration/test_customformats/oas_request_decoders_gen.go b/internal/integration/test_customformats/oas_request_decoders_gen.go index fb414e574..335e185ba 100644 --- a/internal/integration/test_customformats/oas_request_decoders_gen.go +++ b/internal/integration/test_customformats/oas_request_decoders_gen.go @@ -15,6 +15,71 @@ import ( "github.com/ogen-go/ogen/validate" ) +func (s *Server) decodeEventPostRequest(r *http.Request) ( + req any, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = multierr.Append(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = multierr.Append(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, close, validate.ErrBodyRequired + } + + var request any + buf, err := io.ReadAll(r.Body) + if err != nil { + return req, close, err + } + + if len(buf) == 0 { + return req, close, validate.ErrBodyRequired + } + + d := jx.DecodeBytes(buf) + if err := func() error { + v, err := formatXMyEvent().DecodeJSON(d) + request = v + if err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, close, err + } + return request, close, nil + default: + return req, close, validate.InvalidContentType(ct) + } +} + func (s *Server) decodePhoneGetRequest(r *http.Request) ( req *User, close func() error, diff --git a/internal/integration/test_customformats/oas_request_encoders_gen.go b/internal/integration/test_customformats/oas_request_encoders_gen.go index 3cc388499..fb00aa192 100644 --- a/internal/integration/test_customformats/oas_request_encoders_gen.go +++ b/internal/integration/test_customformats/oas_request_encoders_gen.go @@ -11,6 +11,20 @@ import ( ht "github.com/ogen-go/ogen/http" ) +func encodeEventPostRequest( + req any, + r *http.Request, +) error { + const contentType = "application/json" + e := jx.GetEncoder() + { + formatXMyEvent().EncodeJSON(e, req) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + func encodePhoneGetRequest( req *User, r *http.Request, diff --git a/internal/integration/test_customformats/oas_response_decoders_gen.go b/internal/integration/test_customformats/oas_response_decoders_gen.go index da3613dc5..874ffadf3 100644 --- a/internal/integration/test_customformats/oas_response_decoders_gen.go +++ b/internal/integration/test_customformats/oas_response_decoders_gen.go @@ -14,6 +14,49 @@ import ( "github.com/ogen-go/ogen/validate" ) +func decodeEventPostResponse(resp *http.Response) (res any, err error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + + d := jx.DecodeBytes(buf) + var response any + if err := func() error { + v, err := formatXMyEvent().DecodeJSON(d) + response = v + if err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + return res, validate.UnexpectedStatusCode(resp.StatusCode) +} + func decodePhoneGetResponse(resp *http.Response) (res *User, err error) { switch resp.StatusCode { case 200: diff --git a/internal/integration/test_customformats/oas_response_encoders_gen.go b/internal/integration/test_customformats/oas_response_encoders_gen.go index a3aafa726..17ec5c7a5 100644 --- a/internal/integration/test_customformats/oas_response_encoders_gen.go +++ b/internal/integration/test_customformats/oas_response_encoders_gen.go @@ -11,6 +11,19 @@ import ( "go.opentelemetry.io/otel/trace" ) +func encodeEventPostResponse(response any, w http.ResponseWriter, span trace.Span) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := jx.GetEncoder() + formatXMyEvent().EncodeJSON(e, response) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + return nil +} + func encodePhoneGetResponse(response *User, w http.ResponseWriter, span trace.Span) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) diff --git a/internal/integration/test_customformats/oas_router_gen.go b/internal/integration/test_customformats/oas_router_gen.go index 8109c363f..0b6bc247e 100644 --- a/internal/integration/test_customformats/oas_router_gen.go +++ b/internal/integration/test_customformats/oas_router_gen.go @@ -41,23 +41,53 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } switch elem[0] { - case '/': // Prefix: "/phone" - if l := len("/phone"); len(elem) >= l && elem[0:l] == "/phone" { + case '/': // Prefix: "/" + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { elem = elem[l:] } else { break } if len(elem) == 0 { - // Leaf node. - switch r.Method { - case "GET": - s.handlePhoneGetRequest([0]string{}, w, r) - default: - s.notAllowed(w, r, "GET") + break + } + switch elem[0] { + case 'e': // Prefix: "event" + if l := len("event"); len(elem) >= l && elem[0:l] == "event" { + elem = elem[l:] + } else { + break } - return + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleEventPostRequest([0]string{}, w, r) + default: + s.notAllowed(w, r, "POST") + } + + return + } + case 'p': // Prefix: "phone" + if l := len("phone"); len(elem) >= l && elem[0:l] == "phone" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "GET": + s.handlePhoneGetRequest([0]string{}, w, r) + default: + s.notAllowed(w, r, "GET") + } + + return + } } } } @@ -122,24 +152,56 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } switch elem[0] { - case '/': // Prefix: "/phone" - if l := len("/phone"); len(elem) >= l && elem[0:l] == "/phone" { + case '/': // Prefix: "/" + if l := len("/"); len(elem) >= l && elem[0:l] == "/" { elem = elem[l:] } else { break } if len(elem) == 0 { - switch method { - case "GET": - // Leaf: PhoneGet - r.name = "PhoneGet" - r.operationID = "" - r.args = args - r.count = 0 - return r, true - default: - return + break + } + switch elem[0] { + case 'e': // Prefix: "event" + if l := len("event"); len(elem) >= l && elem[0:l] == "event" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "POST": + // Leaf: EventPost + r.name = "EventPost" + r.operationID = "" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + case 'p': // Prefix: "phone" + if l := len("phone"); len(elem) >= l && elem[0:l] == "phone" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + switch method { + case "GET": + // Leaf: PhoneGet + r.name = "PhoneGet" + r.operationID = "" + r.args = args + r.count = 0 + return r, true + default: + return + } } } } diff --git a/internal/integration/test_customformats/oas_schemas_gen.go b/internal/integration/test_customformats/oas_schemas_gen.go index 1dc9f68cf..265f8f750 100644 --- a/internal/integration/test_customformats/oas_schemas_gen.go +++ b/internal/integration/test_customformats/oas_schemas_gen.go @@ -3,8 +3,8 @@ package api import ( - custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" - custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" + custom2 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" + custom3 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" ) // NewOptHex returns new OptHex with value set to v. @@ -54,16 +54,16 @@ func (o OptHex) Or(d int64) int64 { } // NewOptPhone returns new OptPhone with value set to v. -func NewOptPhone(v custom1.Phone) OptPhone { +func NewOptPhone(v custom2.Phone) OptPhone { return OptPhone{ Value: v, Set: true, } } -// OptPhone is optional custom1.Phone. +// OptPhone is optional custom2.Phone. type OptPhone struct { - Value custom1.Phone + Value custom2.Phone Set bool } @@ -72,19 +72,19 @@ func (o OptPhone) IsSet() bool { return o.Set } // Reset unsets value. func (o *OptPhone) Reset() { - var v custom1.Phone + var v custom2.Phone o.Value = v o.Set = false } // SetTo sets value to v. -func (o *OptPhone) SetTo(v custom1.Phone) { +func (o *OptPhone) SetTo(v custom2.Phone) { o.Set = true o.Value = v } // Get returns value and boolean that denotes whether value was set. -func (o OptPhone) Get() (v custom1.Phone, ok bool) { +func (o OptPhone) Get() (v custom2.Phone, ok bool) { if !o.Set { return v, false } @@ -92,7 +92,7 @@ func (o OptPhone) Get() (v custom1.Phone, ok bool) { } // Or returns value if set, or given parameter if does not. -func (o OptPhone) Or(d custom1.Phone) custom1.Phone { +func (o OptPhone) Or(d custom2.Phone) custom2.Phone { if v, ok := o.Get(); ok { return v } @@ -100,16 +100,16 @@ func (o OptPhone) Or(d custom1.Phone) custom1.Phone { } // NewOptRgba returns new OptRgba with value set to v. -func NewOptRgba(v custom2.RGBA) OptRgba { +func NewOptRgba(v custom3.RGBA) OptRgba { return OptRgba{ Value: v, Set: true, } } -// OptRgba is optional custom2.RGBA. +// OptRgba is optional custom3.RGBA. type OptRgba struct { - Value custom2.RGBA + Value custom3.RGBA Set bool } @@ -118,19 +118,19 @@ func (o OptRgba) IsSet() bool { return o.Set } // Reset unsets value. func (o *OptRgba) Reset() { - var v custom2.RGBA + var v custom3.RGBA o.Value = v o.Set = false } // SetTo sets value to v. -func (o *OptRgba) SetTo(v custom2.RGBA) { +func (o *OptRgba) SetTo(v custom3.RGBA) { o.Set = true o.Value = v } // Get returns value and boolean that denotes whether value was set. -func (o OptRgba) Get() (v custom2.RGBA, ok bool) { +func (o OptRgba) Get() (v custom3.RGBA, ok bool) { if !o.Set { return v, false } @@ -138,7 +138,7 @@ func (o OptRgba) Get() (v custom2.RGBA, ok bool) { } // Or returns value if set, or given parameter if does not. -func (o OptRgba) Or(d custom2.RGBA) custom2.RGBA { +func (o OptRgba) Or(d custom3.RGBA) custom3.RGBA { if v, ok := o.Get(); ok { return v } @@ -148,9 +148,9 @@ func (o OptRgba) Or(d custom2.RGBA) custom2.RGBA { // Ref: #/components/schemas/User type User struct { ID int64 `json:"id"` - Phone custom1.Phone `json:"phone"` + Phone custom2.Phone `json:"phone"` HomePhone OptPhone `json:"home_phone"` - ProfileColor custom2.RGBA `json:"profile_color"` + ProfileColor custom3.RGBA `json:"profile_color"` BackgroundColor OptRgba `json:"background_color"` HexColor OptHex `json:"hex_color"` } @@ -161,7 +161,7 @@ func (s *User) GetID() int64 { } // GetPhone returns the value of Phone. -func (s *User) GetPhone() custom1.Phone { +func (s *User) GetPhone() custom2.Phone { return s.Phone } @@ -171,7 +171,7 @@ func (s *User) GetHomePhone() OptPhone { } // GetProfileColor returns the value of ProfileColor. -func (s *User) GetProfileColor() custom2.RGBA { +func (s *User) GetProfileColor() custom3.RGBA { return s.ProfileColor } @@ -191,7 +191,7 @@ func (s *User) SetID(val int64) { } // SetPhone sets the value of Phone. -func (s *User) SetPhone(val custom1.Phone) { +func (s *User) SetPhone(val custom2.Phone) { s.Phone = val } @@ -201,7 +201,7 @@ func (s *User) SetHomePhone(val OptPhone) { } // SetProfileColor sets the value of ProfileColor. -func (s *User) SetProfileColor(val custom2.RGBA) { +func (s *User) SetProfileColor(val custom3.RGBA) { s.ProfileColor = val } diff --git a/internal/integration/test_customformats/oas_server_gen.go b/internal/integration/test_customformats/oas_server_gen.go index a79255dbf..c78d04bcd 100644 --- a/internal/integration/test_customformats/oas_server_gen.go +++ b/internal/integration/test_customformats/oas_server_gen.go @@ -8,6 +8,10 @@ import ( // Handler handles operations described by OpenAPI v3 specification. type Handler interface { + // EventPost implements POST /event operation. + // + // POST /event + EventPost(ctx context.Context, req any) (any, error) // PhoneGet implements GET /phone operation. // // GET /phone diff --git a/internal/integration/test_customformats/oas_unimplemented_gen.go b/internal/integration/test_customformats/oas_unimplemented_gen.go index f447223b7..0c5d9b0ad 100644 --- a/internal/integration/test_customformats/oas_unimplemented_gen.go +++ b/internal/integration/test_customformats/oas_unimplemented_gen.go @@ -13,6 +13,13 @@ type UnimplementedHandler struct{} var _ Handler = UnimplementedHandler{} +// EventPost implements POST /event operation. +// +// POST /event +func (UnimplementedHandler) EventPost(ctx context.Context, req any) (r any, _ error) { + return r, ht.ErrNotImplemented +} + // PhoneGet implements GET /phone operation. // // GET /phone