Skip to content

Commit 381d15e

Browse files
authored
Merge pull request #731 from tdakkota/feat/empty-type-custom-format
feat(gen): allow to use empty interface as custom format type
2 parents cf36f2a + 5c430c8 commit 381d15e

21 files changed

+640
-103
lines changed

_testdata/positive/custom_formats.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,32 @@
55
"version": "v0.1.0"
66
},
77
"paths": {
8+
"/event": {
9+
"post": {
10+
"requestBody": {
11+
"required": true,
12+
"content": {
13+
"application/json": {
14+
"schema": {
15+
"$ref": "#/components/schemas/Event"
16+
}
17+
}
18+
}
19+
},
20+
"responses": {
21+
"200": {
22+
"description": "description",
23+
"content": {
24+
"application/json": {
25+
"schema": {
26+
"$ref": "#/components/schemas/Event"
27+
}
28+
}
29+
}
30+
}
31+
}
32+
}
33+
},
834
"/phone": {
935
"get": {
1036
"parameters": [
@@ -75,6 +101,9 @@
75101
}
76102
},
77103
"schemas": {
104+
"Event": {
105+
"format": "x-my-event"
106+
},
78107
"User": {
79108
"type": "object",
80109
"required": [

gen/custom_format.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,16 @@ func checkImportableType(typ reflect.Type) error {
1919

2020
name := typ.Name()
2121
if name == "" {
22-
return errors.New("type must be named or primitive")
22+
switch typ.Kind() {
23+
case reflect.Interface:
24+
if typ.NumMethod() == 0 {
25+
// Allow empty interface.
26+
break
27+
}
28+
fallthrough
29+
default:
30+
return errors.New("type must be named or primitive")
31+
}
2332
}
2433

2534
if path != "" && !token.IsExported(name) {

gen/ir/custom_format.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,16 @@ type ExternalType struct {
1515
// Go returns valid Go type for this ExternalType.
1616
func (c ExternalType) Go() string {
1717
if c.Pkg == "" {
18-
// Primitive type.
19-
return c.Type.Name()
18+
switch t := c.Type; t.Kind() {
19+
case reflect.Interface:
20+
if t.NumMethod() != 0 {
21+
panic(fmt.Sprintf("unexpected interface type: %v", t))
22+
}
23+
return "any"
24+
default:
25+
// Primitive type.
26+
return t.Name()
27+
}
2028
}
2129
return fmt.Sprintf("%s.%s", c.Pkg, c.Type.Name())
2230
}

gen/schema_gen.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
276276
return nil, errors.Wrap(err, "primitive")
277277
}
278278

279+
fields := []zap.Field{
280+
zapPosition(schema),
281+
zap.String("type", string(schema.Type)),
282+
zap.String("format", schema.Format),
283+
zap.String("go_type", t.Go()),
284+
}
279285
switch schema.Type {
280286
case jsonschema.String:
281287
if err := t.Validators.SetString(schema); err != nil {
@@ -285,26 +291,39 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
285291
switch t.Primitive {
286292
case ir.String, ir.ByteSlice:
287293
default:
288-
g.log.Warn("String validator cannot be applied to non-string type and will be ignored",
289-
zapPosition(schema),
290-
zap.String("type", string(schema.Type)),
291-
zap.String("format", schema.Format),
292-
zap.String("go_type", t.Go()),
293-
)
294+
g.log.Warn("String validator cannot be applied to generated type and will be ignored", fields...)
294295
}
295296
}
296297
case jsonschema.Integer:
297298
if err := t.Validators.SetInt(schema); err != nil {
298299
return nil, errors.Wrap(err, "int validator")
299300
}
301+
if t.Validators.Int.Set() {
302+
switch t.Primitive {
303+
case ir.String, ir.ByteSlice:
304+
default:
305+
g.log.Warn("Int validator cannot be applied to generated type and will be ignored", fields...)
306+
}
307+
}
300308
case jsonschema.Number:
301309
if err := t.Validators.SetFloat(schema); err != nil {
302310
return nil, errors.Wrap(err, "float validator")
303311
}
312+
if t.Validators.Float.Set() {
313+
switch t.Primitive {
314+
case ir.String, ir.ByteSlice:
315+
default:
316+
g.log.Warn("Float validator cannot be applied to generated type and will be ignored", fields...)
317+
}
318+
}
304319
}
305320

306321
return g.regtype(name, t), nil
307322
case jsonschema.Empty:
323+
if format, ok := g.customFormats[schema.Type][schema.Format]; ok {
324+
return g.customFormat(format, schema), nil
325+
}
326+
308327
g.log.Info("Type is not defined, using any",
309328
zapPosition(schema),
310329
zap.String("name", name),

gen/schema_gen_primitive.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,7 @@ func (g *schemaGen) parseSimple(schema *jsonschema.Schema) *ir.Type {
153153
t, found := mapping[schema.Type][schema.Format]
154154
if !found {
155155
if custom, ok := g.customFormats[schema.Type][schema.Format]; ok {
156-
typ := ir.Primitive(ir.Custom, schema)
157-
typ.CustomFormat = &custom
158-
return typ
156+
return g.customFormat(custom, schema)
159157
}
160158
// Fallback to default.
161159
t = mapping[schema.Type][""]
@@ -164,6 +162,12 @@ func (g *schemaGen) parseSimple(schema *jsonschema.Schema) *ir.Type {
164162
return ir.Primitive(t, schema)
165163
}
166164

165+
func (g *schemaGen) customFormat(custom ir.CustomFormat, schema *jsonschema.Schema) *ir.Type {
166+
typ := ir.Primitive(ir.Custom, schema)
167+
typ.CustomFormat = &custom
168+
return typ
169+
}
170+
167171
func TypeFormatMapping() map[jsonschema.SchemaType]map[string]ir.PrimitiveType {
168172
return map[jsonschema.SchemaType]map[string]ir.PrimitiveType{
169173
jsonschema.Integer: {

internal/integration/cmd/customformats/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/ogen-go/ogen"
1212
"github.com/ogen-go/ogen/gen"
1313
"github.com/ogen-go/ogen/gen/genfs"
14+
"github.com/ogen-go/ogen/internal/integration/customformats/eventtype"
1415
"github.com/ogen-go/ogen/internal/integration/customformats/hextype"
1516
"github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
1617
"github.com/ogen-go/ogen/internal/integration/customformats/rgbatype"
@@ -58,6 +59,9 @@ func run(specPath, targetDir string) error {
5859
"rgba": rgbatype.RGBAFormat,
5960
"hex": hextype.HexFormat,
6061
},
62+
jsonschema.Empty: {
63+
"x-my-event": eventtype.EventFormat,
64+
},
6165
},
6266
File: location.NewFile(fileName, specPath, data),
6367
Logger: l,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Package eventtype defines a custom format for event types.
2+
package eventtype
3+
4+
import (
5+
"encoding/json"
6+
7+
"github.com/go-faster/jx"
8+
9+
"github.com/ogen-go/ogen/gen"
10+
)
11+
12+
type Event = any
13+
14+
// EventFormat defines a custom format for Event.
15+
var EventFormat = gen.CustomFormat[
16+
Event,
17+
JSONEventEncoding,
18+
TextEventEncoding,
19+
]()
20+
21+
// JSONEventEncoding defines a custom JSON encoding for hexadecimal numbers.
22+
type JSONEventEncoding struct{}
23+
24+
// EncodeJSON encodes a hexadecimal number as a JSON string.
25+
func (JSONEventEncoding) EncodeJSON(e *jx.Encoder, v Event) {
26+
b, err := json.Marshal(v)
27+
if err != nil {
28+
e.Null()
29+
return
30+
}
31+
e.Raw(b)
32+
}
33+
34+
// DecodeJSON decodes a hexadecimal number from a JSON string.
35+
func (JSONEventEncoding) DecodeJSON(d *jx.Decoder) (v Event, _ error) {
36+
r, err := d.Raw()
37+
if err != nil {
38+
return v, err
39+
}
40+
err = json.Unmarshal(r, &v)
41+
return v, err
42+
}
43+
44+
// TextEventEncoding defines a custom text encoding for hexadecimal numbers.
45+
type TextEventEncoding struct{}
46+
47+
// EncodeText encodes a hexadecimal number as a string.
48+
func (TextEventEncoding) EncodeText(v Event) string {
49+
b, err := json.Marshal(v)
50+
if err != nil {
51+
return ""
52+
}
53+
return string(b)
54+
}
55+
56+
// DecodeText decodes a hexadecimal number from a string.
57+
func (TextEventEncoding) DecodeText(s string) (v Event, _ error) {
58+
err := json.Unmarshal([]byte(s), &v)
59+
return v, err
60+
}

internal/integration/customformats_test.go

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http/httptest"
66
"testing"
77

8+
"github.com/go-faster/errors"
89
"github.com/stretchr/testify/require"
910

1011
"github.com/ogen-go/ogen/internal/integration/customformats/phonetype"
@@ -14,6 +15,13 @@ import (
1415

1516
type testCustomFormats struct{}
1617

18+
func (t testCustomFormats) EventPost(ctx context.Context, req any) (any, error) {
19+
if req == nil {
20+
return nil, errors.New("empty request")
21+
}
22+
return req, nil
23+
}
24+
1725
func (t testCustomFormats) PhoneGet(ctx context.Context, req *api.User, params api.PhoneGetParams) (*api.User, error) {
1826
req.HomePhone.SetTo(params.Phone)
1927
if v, ok := params.Color.Get(); ok {
@@ -26,41 +34,61 @@ func (t testCustomFormats) PhoneGet(ctx context.Context, req *api.User, params a
2634
}
2735

2836
func TestCustomFormats(t *testing.T) {
29-
a := require.New(t)
3037
ctx := context.Background()
3138

3239
srv, err := api.NewServer(testCustomFormats{})
33-
a.NoError(err)
40+
require.NoError(t, err)
3441

3542
s := httptest.NewServer(srv)
3643
defer s.Close()
3744

3845
client, err := api.NewClient(s.URL, api.WithClient(s.Client()))
39-
a.NoError(err)
46+
require.NoError(t, err)
4047

41-
var (
42-
homePhone = phonetype.Phone("+1234567890")
43-
backgroundColor = rgbatype.RGBA{R: 255, G: 0, B: 0, A: 255}
44-
hex = int64(100)
48+
t.Run("EventPost", func(t *testing.T) {
49+
a := require.New(t)
4550

46-
u = &api.User{
47-
ID: 10,
48-
Phone: "+1234567890",
49-
ProfileColor: rgbatype.RGBA{R: 0, G: 0, B: 0, A: 255},
51+
for _, val := range []any{
52+
true,
53+
float64(42),
54+
"string",
55+
[]any{float64(1), float64(2), float64(3)},
56+
map[string]any{
57+
"key": []any{"value", "value2"},
58+
},
59+
} {
60+
result, err := client.EventPost(ctx, val)
61+
a.NoError(err)
62+
a.Equal(val, result)
5063
}
51-
)
64+
})
65+
t.Run("Phone", func(t *testing.T) {
66+
a := require.New(t)
67+
68+
var (
69+
homePhone = phonetype.Phone("+1234567890")
70+
backgroundColor = rgbatype.RGBA{R: 255, G: 0, B: 0, A: 255}
71+
hex = int64(100)
72+
73+
u = &api.User{
74+
ID: 10,
75+
Phone: "+1234567890",
76+
ProfileColor: rgbatype.RGBA{R: 0, G: 0, B: 0, A: 255},
77+
}
78+
)
79+
80+
u2, err := client.PhoneGet(ctx, u, api.PhoneGetParams{
81+
Phone: homePhone,
82+
Color: api.NewOptRgba(backgroundColor),
83+
Hex: api.NewOptHex(hex),
84+
})
85+
a.NoError(err)
5286

53-
u2, err := client.PhoneGet(ctx, u, api.PhoneGetParams{
54-
Phone: homePhone,
55-
Color: api.NewOptRgba(backgroundColor),
56-
Hex: api.NewOptHex(hex),
87+
a.Equal(u.ID, u2.ID)
88+
a.Equal(u.Phone, u2.Phone)
89+
a.Equal(u.ProfileColor, u2.ProfileColor)
90+
a.Equal(homePhone, u2.HomePhone.Or(""))
91+
a.Equal(backgroundColor, u2.BackgroundColor.Or(rgbatype.RGBA{}))
92+
a.Equal(hex, u2.HexColor.Or(0))
5793
})
58-
a.NoError(err)
59-
60-
a.Equal(u.ID, u2.ID)
61-
a.Equal(u.Phone, u2.Phone)
62-
a.Equal(u.ProfileColor, u2.ProfileColor)
63-
a.Equal(homePhone, u2.HomePhone.Or(""))
64-
a.Equal(backgroundColor, u2.BackgroundColor.Or(rgbatype.RGBA{}))
65-
a.Equal(hex, u2.HexColor.Or(0))
6694
}

internal/integration/test_customformats/oas_cfg_gen.go

Lines changed: 16 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)