Skip to content

Commit

Permalink
Merge pull request #731 from tdakkota/feat/empty-type-custom-format
Browse files Browse the repository at this point in the history
feat(gen): allow to use empty interface as custom format type
  • Loading branch information
ernado authored Dec 26, 2022
2 parents cf36f2a + 5c430c8 commit 381d15e
Show file tree
Hide file tree
Showing 21 changed files with 640 additions and 103 deletions.
29 changes: 29 additions & 0 deletions _testdata/positive/custom_formats.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -75,6 +101,9 @@
}
},
"schemas": {
"Event": {
"format": "x-my-event"
},
"User": {
"type": "object",
"required": [
Expand Down
11 changes: 10 additions & 1 deletion gen/custom_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 10 additions & 2 deletions gen/ir/custom_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
31 changes: 25 additions & 6 deletions gen/schema_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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),
Expand Down
10 changes: 7 additions & 3 deletions gen/schema_gen_primitive.go
Original file line number Diff line number Diff line change
Expand Up @@ -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][""]
Expand All @@ -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: {
Expand Down
4 changes: 4 additions & 0 deletions internal/integration/cmd/customformats/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
60 changes: 60 additions & 0 deletions internal/integration/customformats/eventtype/eventtype.go
Original file line number Diff line number Diff line change
@@ -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
}
76 changes: 52 additions & 24 deletions internal/integration/customformats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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))
}
25 changes: 16 additions & 9 deletions internal/integration/test_customformats/oas_cfg_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 381d15e

Please sign in to comment.