diff --git a/_testdata/positive/custom_formats.json b/_testdata/positive/custom_formats.json index f13d0ad32..cc8201e1c 100644 --- a/_testdata/positive/custom_formats.json +++ b/_testdata/positive/custom_formats.json @@ -13,6 +13,9 @@ }, { "$ref": "#/components/parameters/Color" + }, + { + "$ref": "#/components/parameters/Hex" } ], "requestBody": { @@ -60,6 +63,15 @@ "type": "string", "format": "rgba" } + }, + "Hex": { + "name": "hex", + "in": "query", + "description": "Hex", + "schema": { + "type": "string", + "format": "hex" + } } }, "schemas": { @@ -90,6 +102,10 @@ "background_color": { "type": "string", "format": "rgba" + }, + "hex_color": { + "type": "string", + "format": "hex" } } } diff --git a/gen/custom_format.go b/gen/custom_format.go new file mode 100644 index 000000000..c4e830967 --- /dev/null +++ b/gen/custom_format.go @@ -0,0 +1,106 @@ +package gen + +import ( + "fmt" + "go/token" + "reflect" + + "github.com/go-faster/errors" + + "github.com/ogen-go/ogen/gen/ir" + "github.com/ogen-go/ogen/internal/xmaps" +) + +func checkImportableType(typ reflect.Type) error { + path := typ.PkgPath() + if path == "main" { + return errors.New("type must be in importable package") + } + + name := typ.Name() + if name == "" { + return errors.New("type must be named or primitive") + } + + if path != "" && !token.IsExported(name) { + return errors.New("type must be exported") + } + + return nil +} + +func (g *Generator) makeCustomFormats() error { + importPaths := map[string]string{} + + makeExternal := func(typ reflect.Type) (ir.ExternalType, error) { + if err := checkImportableType(typ); err != nil { + return ir.ExternalType{}, err + } + + path := typ.PkgPath() + if path == "" { + // Primitive type. + return ir.ExternalType{Type: typ}, nil + } + + importName, ok := importPaths[path] + if !ok { + importName = fmt.Sprintf("custom%d", len(importPaths)) + importPaths[path] = importName + g.imports = append(g.imports, fmt.Sprintf("%s %q", importName, path)) + } + + return ir.ExternalType{ + Pkg: importName, + Type: typ, + }, nil + } + + for _, jsonTyp := range xmaps.SortedKeys(g.opt.CustomFormats) { + formats := g.opt.CustomFormats[jsonTyp] + for _, format := range xmaps.SortedKeys(formats) { + def := formats[format] + + if _, ok := g.customFormats[jsonTyp]; !ok { + g.customFormats[jsonTyp] = map[string]ir.CustomFormat{} + } + + f, err := func() (f ir.CustomFormat, _ error) { + goName, err := pascalNonEmpty(format) + if err != nil { + return f, errors.Wrap(err, "generate go name") + } + + typ, err := makeExternal(def.typ) + if err != nil { + return f, errors.Wrap(err, "format type") + } + + json, err := makeExternal(def.json) + if err != nil { + return f, errors.Wrap(err, "json encoding") + } + + text, err := makeExternal(def.text) + if err != nil { + return f, errors.Wrap(err, "text encoding") + } + + return ir.CustomFormat{ + Name: format, + GoName: goName, + Type: typ, + JSON: json, + Text: text, + }, nil + }() + if err != nil { + return errors.Wrapf(err, "custom format %q:%q", jsonTyp, format) + } + + g.customFormats[jsonTyp][format] = f + } + } + + return nil +} diff --git a/gen/custom_format_test.go b/gen/custom_format_test.go new file mode 100644 index 000000000..63c36ee0a --- /dev/null +++ b/gen/custom_format_test.go @@ -0,0 +1,87 @@ +package gen + +import ( + "fmt" + "reflect" + "testing" + "unsafe" + + "github.com/stretchr/testify/require" + + "github.com/ogen-go/ogen/gen/ir" +) + +func typeOf[T any]() reflect.Type { + return reflect.TypeOf(new(T)).Elem() +} + +type foo struct{} + +type ExportedFunc func() foo + +func Test_checkImportableType(t *testing.T) { + tests := []struct { + typ reflect.Type + wantErr bool + }{ + // Primitive types. + {typeOf[bool](), false}, + {typeOf[int](), false}, + {typeOf[int8](), false}, + {typeOf[int16](), false}, + {typeOf[int32](), false}, + {typeOf[int64](), false}, + {typeOf[uint](), false}, + {typeOf[uint8](), false}, + {typeOf[uint16](), false}, + {typeOf[uint32](), false}, + {typeOf[uint64](), false}, + {typeOf[uintptr](), false}, + {typeOf[float32](), false}, + {typeOf[float64](), false}, + {typeOf[complex64](), false}, + {typeOf[complex128](), false}, + {typeOf[string](), false}, + {typeOf[unsafe.Pointer](), false}, + // Exported types. + {typeOf[Generator](), false}, + {typeOf[ir.Kind](), false}, + {typeOf[ir.CustomFormat](), false}, + {typeOf[ExportedFunc](), false}, + + // Negative cases. + // + // Unexported type. + {typeOf[foo](), true}, + {typeOf[func(foo)](), true}, + {typeOf[func() foo](), true}, + {typeOf[*foo](), true}, + {typeOf[chan foo](), true}, + {typeOf[[]foo](), true}, + {typeOf[map[string]foo](), true}, + // Unnamed type. + {typeOf[struct{}](), true}, + {typeOf[func(struct{})](), true}, + {typeOf[struct{ X int }](), true}, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { + check := require.Error + if !tt.wantErr { + check = require.NoError + } + + err := checkImportableType(tt.typ) + defer func() { + t.Logf("Kind: %q", tt.typ.Kind()) + t.Logf("Package: %q", tt.typ.PkgPath()) + t.Logf("Name: %q", tt.typ.Name()) + if err != nil { + t.Logf("Error: %v", err) + } + }() + + check(t, err) + }) + } +} diff --git a/gen/generator.go b/gen/generator.go index faab9dbb9..1a790e135 100644 --- a/gen/generator.go +++ b/gen/generator.go @@ -1,9 +1,6 @@ package gen import ( - "fmt" - "go/token" - "reflect" "strings" "github.com/go-faster/errors" @@ -12,7 +9,6 @@ import ( "github.com/ogen-go/ogen" "github.com/ogen-go/ogen/gen/ir" - "github.com/ogen-go/ogen/internal/xmaps" "github.com/ogen-go/ogen/internal/xslices" "github.com/ogen-go/ogen/jsonschema" "github.com/ogen-go/ogen/openapi" @@ -86,80 +82,6 @@ func NewGenerator(spec *ogen.Spec, opts Options) (*Generator, error) { return g, nil } -func (g *Generator) makeCustomFormats() error { - importPaths := map[string]string{} - - makeExternal := func(typ reflect.Type) (ir.ExternalType, error) { - path := typ.PkgPath() - if path == "main" { - return ir.ExternalType{}, errors.New("type must be in importable package") - } - if n := typ.Name(); n == "" || !token.IsExported(n) { - return ir.ExternalType{}, errors.New("type must be named and exported") - } - - importName, ok := importPaths[path] - if !ok { - importName = fmt.Sprintf("custom%d", len(importPaths)) - importPaths[path] = importName - g.imports = append(g.imports, fmt.Sprintf("%s %q", importName, path)) - } - - return ir.ExternalType{ - Pkg: importName, - Type: typ, - }, nil - } - - for _, jsonTyp := range xmaps.SortedKeys(g.opt.CustomFormats) { - formats := g.opt.CustomFormats[jsonTyp] - for _, format := range xmaps.SortedKeys(formats) { - def := formats[format] - - if _, ok := g.customFormats[jsonTyp]; !ok { - g.customFormats[jsonTyp] = map[string]ir.CustomFormat{} - } - - f, err := func() (f ir.CustomFormat, _ error) { - goName, err := pascalNonEmpty(format) - if err != nil { - return f, errors.Wrap(err, "generate go name") - } - - typ, err := makeExternal(def.typ) - if err != nil { - return f, errors.Wrap(err, "format type") - } - - json, err := makeExternal(def.json) - if err != nil { - return f, errors.Wrap(err, "json encoding") - } - - text, err := makeExternal(def.text) - if err != nil { - return f, errors.Wrap(err, "text encoding") - } - - return ir.CustomFormat{ - Name: format, - GoName: goName, - Type: typ, - JSON: json, - Text: text, - }, nil - }() - if err != nil { - return errors.Wrapf(err, "custom format %q:%q", jsonTyp, format) - } - - g.customFormats[jsonTyp][format] = f - } - } - - return nil -} - func (g *Generator) makeIR(api *openapi.API) error { if err := g.makeServers(api.Servers); err != nil { return errors.Wrap(err, "servers") diff --git a/gen/ir/custom_format.go b/gen/ir/custom_format.go index 03ded62a5..2af3d5d50 100644 --- a/gen/ir/custom_format.go +++ b/gen/ir/custom_format.go @@ -14,6 +14,10 @@ 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() + } return fmt.Sprintf("%s.%s", c.Pkg, c.Type.Name()) } diff --git a/gen/schema_gen.go b/gen/schema_gen.go index cc7737fae..ee51e6c59 100644 --- a/gen/schema_gen.go +++ b/gen/schema_gen.go @@ -281,15 +281,17 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T if err := t.Validators.SetString(schema); err != nil { return nil, errors.Wrap(err, "string validator") } - 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()), - ) + if t.Validators.String.Set() { + 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()), + ) + } } case jsonschema.Integer: if err := t.Validators.SetInt(schema); err != nil { diff --git a/internal/integration/cmd/customformats/main.go b/internal/integration/cmd/customformats/main.go index dd6bc8fb8..117b50e83 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/hextype" "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" "github.com/ogen-go/ogen/internal/location" @@ -53,16 +54,9 @@ func run(specPath, targetDir string) error { RootURL: u, CustomFormats: gen.CustomFormatsMap{ jsonschema.String: { - "phone": gen.CustomFormat[ - phonetype.Phone, - phonetype.JSONPhoneEncoding, - phonetype.TextPhoneEncoding, - ](), - "rgba": gen.CustomFormat[ - rgbatype.RGBA, - rgbatype.JSONRGBAEncoding, - rgbatype.TextRGBAEncoding, - ](), + "phone": phonetype.PhoneFormat, + "rgba": rgbatype.RGBAFormat, + "hex": hextype.HexFormat, }, }, File: location.NewFile(fileName, specPath, data), diff --git a/internal/integration/customformats/hextype/hex.go b/internal/integration/customformats/hextype/hex.go new file mode 100644 index 000000000..4f6ff2eb9 --- /dev/null +++ b/internal/integration/customformats/hextype/hex.go @@ -0,0 +1,49 @@ +// Package hextype defines a custom format for hexadecimal numbers. +package hextype + +import ( + "strconv" + + "github.com/go-faster/jx" + + "github.com/ogen-go/ogen/gen" +) + +// HexFormat defines a custom format for hexadecimal numbers. +var HexFormat = gen.CustomFormat[ + int64, + JSONHexEncoding, + TextHexEncoding, +]() + +// JSONHexEncoding defines a custom JSON encoding for hexadecimal numbers. +type JSONHexEncoding struct{} + +// EncodeJSON encodes a hexadecimal number as a JSON string. +func (JSONHexEncoding) EncodeJSON(e *jx.Encoder, v int64) { + var t TextHexEncoding + e.Str(t.EncodeText(v)) +} + +// DecodeJSON decodes a hexadecimal number from a JSON string. +func (JSONHexEncoding) DecodeJSON(d *jx.Decoder) (v int64, _ error) { + s, err := d.Str() + if err != nil { + return v, err + } + var t TextHexEncoding + return t.DecodeText(s) +} + +// TextHexEncoding defines a custom text encoding for hexadecimal numbers. +type TextHexEncoding struct{} + +// EncodeText encodes a hexadecimal number as a string. +func (TextHexEncoding) EncodeText(v int64) string { + return strconv.FormatInt(v, 16) +} + +// DecodeText decodes a hexadecimal number from a string. +func (TextHexEncoding) DecodeText(s string) (v int64, _ error) { + return strconv.ParseInt(s, 16, 64) +} diff --git a/internal/integration/customformats/hextype/hex_test.go b/internal/integration/customformats/hextype/hex_test.go new file mode 100644 index 000000000..a88b36c35 --- /dev/null +++ b/internal/integration/customformats/hextype/hex_test.go @@ -0,0 +1,50 @@ +package hextype + +import ( + "fmt" + "testing" + + "github.com/go-faster/jx" + "github.com/stretchr/testify/require" +) + +func TestHex(t *testing.T) { + for i, tt := range []struct { + input string + want int64 + wantErr bool + }{ + {`"1"`, 1, false}, + {`"a"`, 10, false}, + {`"ff"`, 255, false}, + + // Wrong number format. + {`"-"`, 0, true}, + {`"x"`, 0, true}, + {`"fg"`, 0, true}, + {`"0.0"`, 0, true}, + // Wrong JSON type. + {`null`, 0, true}, + {`true`, 0, true}, + {`0`, 0, true}, + // Invalid JSON. + {`"`, 0, true}, + } { + t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) { + a := require.New(t) + + var enc JSONHexEncoding + v, err := enc.DecodeJSON(jx.DecodeStr(tt.input)) + if tt.wantErr { + a.Error(err) + return + } + a.NoError(err) + a.Equal(tt.want, v) + + e := jx.GetEncoder() + enc.EncodeJSON(e, tt.want) + a.JSONEq(tt.input, e.String()) + }) + } +} diff --git a/internal/integration/customformats/phonetype/phone.go b/internal/integration/customformats/phonetype/phone.go index 3536cc32c..a06a4afa9 100644 --- a/internal/integration/customformats/phonetype/phone.go +++ b/internal/integration/customformats/phonetype/phone.go @@ -6,19 +6,32 @@ import ( "github.com/go-faster/errors" "github.com/go-faster/jx" + + "github.com/ogen-go/ogen/gen" ) +// PhoneFormat defines a custom format for phone numbers. +var PhoneFormat = gen.CustomFormat[ + Phone, + JSONPhoneEncoding, + TextPhoneEncoding, +]() + +// Phone is a phone number. type Phone string var phoneRegexp = regexp.MustCompile(`^\+\d+$`) +// JSONPhoneEncoding defines a custom JSON encoding for phone numbers. type JSONPhoneEncoding struct{} +// EncodeJSON encodes a phone number as a JSON string. func (JSONPhoneEncoding) EncodeJSON(e *jx.Encoder, v Phone) { var t TextPhoneEncoding e.Str(t.EncodeText(v)) } +// DecodeJSON decodes a phone number from a JSON string. func (JSONPhoneEncoding) DecodeJSON(d *jx.Decoder) (v Phone, _ error) { s, err := d.Str() if err != nil { @@ -28,12 +41,15 @@ func (JSONPhoneEncoding) DecodeJSON(d *jx.Decoder) (v Phone, _ error) { return t.DecodeText(s) } +// TextPhoneEncoding defines a custom text encoding for phone numbers. type TextPhoneEncoding struct{} +// EncodeText encodes a phone number as a string. func (TextPhoneEncoding) EncodeText(v Phone) string { return string(v) } +// DecodeText decodes a phone number from a string. func (TextPhoneEncoding) DecodeText(s string) (v Phone, _ error) { if !phoneRegexp.MatchString(s) { return v, errors.Errorf("invalid phone %q", s) diff --git a/internal/integration/customformats/rgbatype/rgba.go b/internal/integration/customformats/rgbatype/rgba.go index 9809ff6a5..862d7b139 100644 --- a/internal/integration/customformats/rgbatype/rgba.go +++ b/internal/integration/customformats/rgbatype/rgba.go @@ -5,19 +5,32 @@ import ( "fmt" "github.com/go-faster/jx" + + "github.com/ogen-go/ogen/gen" ) +// RGBAFormat defines a custom format for RGBA colors. +var RGBAFormat = gen.CustomFormat[ + RGBA, + JSONRGBAEncoding, + TextRGBAEncoding, +]() + +// RGBA is a color with red, green, blue, and alpha components. type RGBA struct { R, G, B, A uint8 } +// JSONRGBAEncoding defines a custom JSON encoding for RGBA colors. type JSONRGBAEncoding struct{} +// EncodeJSON encodes an RGBA color as a JSON string. func (JSONRGBAEncoding) EncodeJSON(e *jx.Encoder, v RGBA) { var t TextRGBAEncoding e.Str(t.EncodeText(v)) } +// DecodeJSON decodes an RGBA color from a JSON string. func (JSONRGBAEncoding) DecodeJSON(d *jx.Decoder) (v RGBA, _ error) { s, err := d.Str() if err != nil { @@ -27,12 +40,15 @@ func (JSONRGBAEncoding) DecodeJSON(d *jx.Decoder) (v RGBA, _ error) { return t.DecodeText(s) } +// TextRGBAEncoding defines a custom text encoding for RGBA colors. type TextRGBAEncoding struct{} +// EncodeText encodes an RGBA color as a string. func (TextRGBAEncoding) EncodeText(v RGBA) string { return fmt.Sprintf("rgba(%d,%d,%d,%d)", v.R, v.G, v.B, v.A) } +// DecodeText decodes an RGBA color from a string. func (TextRGBAEncoding) DecodeText(s string) (v RGBA, _ error) { _, err := fmt.Sscanf(s, "rgba(%d,%d,%d,%d)", &v.R, &v.G, &v.B, &v.A) return v, err diff --git a/internal/integration/customformats_test.go b/internal/integration/customformats_test.go index 1bd03fd1e..934bdcfe6 100644 --- a/internal/integration/customformats_test.go +++ b/internal/integration/customformats_test.go @@ -15,12 +15,14 @@ import ( type testCustomFormats struct{} func (t testCustomFormats) PhoneGet(ctx context.Context, req *api.User, params api.PhoneGetParams) (*api.User, error) { - cpy := *req - cpy.HomePhone.SetTo(params.Phone) + req.HomePhone.SetTo(params.Phone) if v, ok := params.Color.Get(); ok { - cpy.BackgroundColor.SetTo(v) + req.BackgroundColor.SetTo(v) } - return &cpy, nil + if v, ok := params.Hex.Get(); ok { + req.HexColor.SetTo(v) + } + return req, nil } func TestCustomFormats(t *testing.T) { @@ -39,7 +41,9 @@ func TestCustomFormats(t *testing.T) { var ( homePhone = phonetype.Phone("+1234567890") backgroundColor = rgbatype.RGBA{R: 255, G: 0, B: 0, A: 255} - u = &api.User{ + hex = int64(100) + + u = &api.User{ ID: 10, Phone: "+1234567890", ProfileColor: rgbatype.RGBA{R: 0, G: 0, B: 0, A: 255}, @@ -49,6 +53,7 @@ func TestCustomFormats(t *testing.T) { u2, err := client.PhoneGet(ctx, u, api.PhoneGetParams{ Phone: homePhone, Color: api.NewOptRgba(backgroundColor), + Hex: api.NewOptHex(hex), }) a.NoError(err) @@ -57,4 +62,5 @@ func TestCustomFormats(t *testing.T) { 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 f6a4159c1..6eeb82570 100644 --- a/internal/integration/test_customformats/oas_cfg_gen.go +++ b/internal/integration/test_customformats/oas_cfg_gen.go @@ -11,23 +11,30 @@ import ( "go.opentelemetry.io/otel/trace" ht "github.com/ogen-go/ogen/http" - custom0 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" - custom1 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" + 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" "github.com/ogen-go/ogen/middleware" "github.com/ogen-go/ogen/ogenerrors" "github.com/ogen-go/ogen/otelogen" ) var ( + formatHex = func() (r struct { + custom0.JSONHexEncoding + custom0.TextHexEncoding + }) { + return r + } formatPhone = func() (r struct { - custom0.JSONPhoneEncoding - custom0.TextPhoneEncoding + custom1.JSONPhoneEncoding + custom1.TextPhoneEncoding }) { return r } formatRgba = func() (r struct { - custom1.JSONRGBAEncoding - custom1.TextRGBAEncoding + custom2.JSONRGBAEncoding + custom2.TextRGBAEncoding }) { return r } diff --git a/internal/integration/test_customformats/oas_client_gen.go b/internal/integration/test_customformats/oas_client_gen.go index a9251c20b..bef2db1ce 100644 --- a/internal/integration/test_customformats/oas_client_gen.go +++ b/internal/integration/test_customformats/oas_client_gen.go @@ -130,6 +130,23 @@ func (c *Client) sendPhoneGet(ctx context.Context, request *User, params PhoneGe return res, errors.Wrap(err, "encode query") } } + { + // Encode "hex" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "hex", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if val, ok := params.Hex.Get(); ok { + return e.EncodeValue(formatHex().EncodeText(val)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } u.RawQuery = q.Values().Encode() stage = "EncodeRequest" diff --git a/internal/integration/test_customformats/oas_handlers_gen.go b/internal/integration/test_customformats/oas_handlers_gen.go index 607aad1cc..5d11f2d2b 100644 --- a/internal/integration/test_customformats/oas_handlers_gen.go +++ b/internal/integration/test_customformats/oas_handlers_gen.go @@ -90,6 +90,10 @@ func (s *Server) handlePhoneGetRequest(args [0]string, w http.ResponseWriter, r Name: "color", In: "query", }: params.Color, + { + Name: "hex", + In: "query", + }: params.Hex, }, Raw: r, } diff --git a/internal/integration/test_customformats/oas_json_gen.go b/internal/integration/test_customformats/oas_json_gen.go index b1c08f703..b899e812d 100644 --- a/internal/integration/test_customformats/oas_json_gen.go +++ b/internal/integration/test_customformats/oas_json_gen.go @@ -9,21 +9,56 @@ import ( "github.com/go-faster/errors" "github.com/go-faster/jx" - custom0 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" - custom1 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" + custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" + custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" "github.com/ogen-go/ogen/validate" ) -// Encode encodes custom0.Phone as json. -func (o OptPhone) Encode(e *jx.Encoder, format func(*jx.Encoder, custom0.Phone)) { +// Encode encodes int64 as json. +func (o OptHex) Encode(e *jx.Encoder, format func(*jx.Encoder, int64)) { + if !o.Set { + return + } + formatHex().EncodeJSON(e, o.Value) +} + +// Decode decodes int64 from json. +func (o *OptHex) Decode(d *jx.Decoder, format func(*jx.Decoder) (int64, error)) error { + if o == nil { + return errors.New("invalid: unable to decode OptHex to nil") + } + o.Set = true + v, err := formatHex().DecodeJSON(d) + if err != nil { + return err + } + o.Value = v + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptHex) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e, formatHex().EncodeJSON) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptHex) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + 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)) { if !o.Set { return } formatPhone().EncodeJSON(e, o.Value) } -// Decode decodes custom0.Phone from json. -func (o *OptPhone) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom0.Phone, error)) error { +// Decode decodes custom1.Phone from json. +func (o *OptPhone) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom1.Phone, error)) error { if o == nil { return errors.New("invalid: unable to decode OptPhone to nil") } @@ -49,16 +84,16 @@ func (s *OptPhone) UnmarshalJSON(data []byte) error { return s.Decode(d, formatPhone().DecodeJSON) } -// Encode encodes custom1.RGBA as json. -func (o OptRgba) Encode(e *jx.Encoder, format func(*jx.Encoder, custom1.RGBA)) { +// Encode encodes custom2.RGBA as json. +func (o OptRgba) Encode(e *jx.Encoder, format func(*jx.Encoder, custom2.RGBA)) { if !o.Set { return } formatRgba().EncodeJSON(e, o.Value) } -// Decode decodes custom1.RGBA from json. -func (o *OptRgba) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom1.RGBA, error)) error { +// Decode decodes custom2.RGBA from json. +func (o *OptRgba) Decode(d *jx.Decoder, format func(*jx.Decoder) (custom2.RGBA, error)) error { if o == nil { return errors.New("invalid: unable to decode OptRgba to nil") } @@ -120,14 +155,21 @@ func (s *User) encodeFields(e *jx.Encoder) { s.BackgroundColor.Encode(e, formatRgba().EncodeJSON) } } + { + if s.HexColor.Set { + e.FieldStart("hex_color") + s.HexColor.Encode(e, formatHex().EncodeJSON) + } + } } -var jsonFieldsNameOfUser = [5]string{ +var jsonFieldsNameOfUser = [6]string{ 0: "id", 1: "phone", 2: "home_phone", 3: "profile_color", 4: "background_color", + 5: "hex_color", } // Decode decodes User from json. @@ -195,6 +237,16 @@ func (s *User) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "decode field \"background_color\"") } + case "hex_color": + if err := func() error { + s.HexColor.Reset() + if err := s.HexColor.Decode(d, formatHex().DecodeJSON); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"hex_color\"") + } default: return d.Skip() } diff --git a/internal/integration/test_customformats/oas_parameters_gen.go b/internal/integration/test_customformats/oas_parameters_gen.go index 48f950db5..f6443d513 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" - custom0 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" - custom1 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" + custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" + custom2 "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,9 +16,11 @@ import ( // PhoneGetParams is parameters of GET /phone operation. type PhoneGetParams struct { // Phone number. - Phone custom0.Phone + Phone custom1.Phone // Color. Color OptRgba + // Hex. + Hex OptHex } func unpackPhoneGetParams(packed middleware.Parameters) (params PhoneGetParams) { @@ -27,7 +29,7 @@ func unpackPhoneGetParams(packed middleware.Parameters) (params PhoneGetParams) Name: "phone", In: "query", } - params.Phone = packed[key].(custom0.Phone) + params.Phone = packed[key].(custom1.Phone) } { key := middleware.ParameterKey{ @@ -38,6 +40,15 @@ func unpackPhoneGetParams(packed middleware.Parameters) (params PhoneGetParams) params.Color = v.(OptRgba) } } + { + key := middleware.ParameterKey{ + Name: "hex", + In: "query", + } + if v, ok := packed[key]; ok { + params.Hex = v.(OptHex) + } + } return params } @@ -89,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 custom1.RGBA + var paramsDotColorVal custom2.RGBA if err := func() error { val, err := d.DecodeValue() if err != nil { @@ -120,5 +131,46 @@ func decodePhoneGetParams(args [0]string, r *http.Request) (params PhoneGetParam Err: err, } } + // Decode query: hex. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "hex", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotHexVal int64 + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := formatHex().DecodeText(val) + if err != nil { + return err + } + + paramsDotHexVal = c + return nil + }(); err != nil { + return err + } + params.Hex.SetTo(paramsDotHexVal) + return nil + }); err != nil { + return err + } + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "hex", + In: "query", + Err: err, + } + } return params, nil } diff --git a/internal/integration/test_customformats/oas_schemas_gen.go b/internal/integration/test_customformats/oas_schemas_gen.go index eaa83a92b..1dc9f68cf 100644 --- a/internal/integration/test_customformats/oas_schemas_gen.go +++ b/internal/integration/test_customformats/oas_schemas_gen.go @@ -3,21 +3,67 @@ package api import ( - custom0 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" - custom1 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" + custom1 "github.com/ogen-go/ogen/internal/integration/customformats/phonetype" + custom2 "github.com/ogen-go/ogen/internal/integration/customformats/rgbatype" ) +// NewOptHex returns new OptHex with value set to v. +func NewOptHex(v int64) OptHex { + return OptHex{ + Value: v, + Set: true, + } +} + +// OptHex is optional int64. +type OptHex struct { + Value int64 + Set bool +} + +// IsSet returns true if OptHex was set. +func (o OptHex) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptHex) Reset() { + var v int64 + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptHex) SetTo(v int64) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptHex) Get() (v int64, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptHex) Or(d int64) int64 { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptPhone returns new OptPhone with value set to v. -func NewOptPhone(v custom0.Phone) OptPhone { +func NewOptPhone(v custom1.Phone) OptPhone { return OptPhone{ Value: v, Set: true, } } -// OptPhone is optional custom0.Phone. +// OptPhone is optional custom1.Phone. type OptPhone struct { - Value custom0.Phone + Value custom1.Phone Set bool } @@ -26,19 +72,19 @@ func (o OptPhone) IsSet() bool { return o.Set } // Reset unsets value. func (o *OptPhone) Reset() { - var v custom0.Phone + var v custom1.Phone o.Value = v o.Set = false } // SetTo sets value to v. -func (o *OptPhone) SetTo(v custom0.Phone) { +func (o *OptPhone) SetTo(v custom1.Phone) { o.Set = true o.Value = v } // Get returns value and boolean that denotes whether value was set. -func (o OptPhone) Get() (v custom0.Phone, ok bool) { +func (o OptPhone) Get() (v custom1.Phone, ok bool) { if !o.Set { return v, false } @@ -46,7 +92,7 @@ func (o OptPhone) Get() (v custom0.Phone, ok bool) { } // Or returns value if set, or given parameter if does not. -func (o OptPhone) Or(d custom0.Phone) custom0.Phone { +func (o OptPhone) Or(d custom1.Phone) custom1.Phone { if v, ok := o.Get(); ok { return v } @@ -54,16 +100,16 @@ func (o OptPhone) Or(d custom0.Phone) custom0.Phone { } // NewOptRgba returns new OptRgba with value set to v. -func NewOptRgba(v custom1.RGBA) OptRgba { +func NewOptRgba(v custom2.RGBA) OptRgba { return OptRgba{ Value: v, Set: true, } } -// OptRgba is optional custom1.RGBA. +// OptRgba is optional custom2.RGBA. type OptRgba struct { - Value custom1.RGBA + Value custom2.RGBA Set bool } @@ -72,19 +118,19 @@ func (o OptRgba) IsSet() bool { return o.Set } // Reset unsets value. func (o *OptRgba) Reset() { - var v custom1.RGBA + var v custom2.RGBA o.Value = v o.Set = false } // SetTo sets value to v. -func (o *OptRgba) SetTo(v custom1.RGBA) { +func (o *OptRgba) SetTo(v custom2.RGBA) { o.Set = true o.Value = v } // Get returns value and boolean that denotes whether value was set. -func (o OptRgba) Get() (v custom1.RGBA, ok bool) { +func (o OptRgba) Get() (v custom2.RGBA, ok bool) { if !o.Set { return v, false } @@ -92,7 +138,7 @@ func (o OptRgba) Get() (v custom1.RGBA, ok bool) { } // Or returns value if set, or given parameter if does not. -func (o OptRgba) Or(d custom1.RGBA) custom1.RGBA { +func (o OptRgba) Or(d custom2.RGBA) custom2.RGBA { if v, ok := o.Get(); ok { return v } @@ -102,10 +148,11 @@ func (o OptRgba) Or(d custom1.RGBA) custom1.RGBA { // Ref: #/components/schemas/User type User struct { ID int64 `json:"id"` - Phone custom0.Phone `json:"phone"` + Phone custom1.Phone `json:"phone"` HomePhone OptPhone `json:"home_phone"` - ProfileColor custom1.RGBA `json:"profile_color"` + ProfileColor custom2.RGBA `json:"profile_color"` BackgroundColor OptRgba `json:"background_color"` + HexColor OptHex `json:"hex_color"` } // GetID returns the value of ID. @@ -114,7 +161,7 @@ func (s *User) GetID() int64 { } // GetPhone returns the value of Phone. -func (s *User) GetPhone() custom0.Phone { +func (s *User) GetPhone() custom1.Phone { return s.Phone } @@ -124,7 +171,7 @@ func (s *User) GetHomePhone() OptPhone { } // GetProfileColor returns the value of ProfileColor. -func (s *User) GetProfileColor() custom1.RGBA { +func (s *User) GetProfileColor() custom2.RGBA { return s.ProfileColor } @@ -133,13 +180,18 @@ func (s *User) GetBackgroundColor() OptRgba { return s.BackgroundColor } +// GetHexColor returns the value of HexColor. +func (s *User) GetHexColor() OptHex { + return s.HexColor +} + // SetID sets the value of ID. func (s *User) SetID(val int64) { s.ID = val } // SetPhone sets the value of Phone. -func (s *User) SetPhone(val custom0.Phone) { +func (s *User) SetPhone(val custom1.Phone) { s.Phone = val } @@ -149,7 +201,7 @@ func (s *User) SetHomePhone(val OptPhone) { } // SetProfileColor sets the value of ProfileColor. -func (s *User) SetProfileColor(val custom1.RGBA) { +func (s *User) SetProfileColor(val custom2.RGBA) { s.ProfileColor = val } @@ -157,3 +209,8 @@ func (s *User) SetProfileColor(val custom1.RGBA) { func (s *User) SetBackgroundColor(val OptRgba) { s.BackgroundColor = val } + +// SetHexColor sets the value of HexColor. +func (s *User) SetHexColor(val OptHex) { + s.HexColor = val +}