Skip to content

Commit

Permalink
Merge pull request #730 from tdakkota/feat/custom-format-primitive-types
Browse files Browse the repository at this point in the history
feat(gen): allow to define custom format for primitive types
  • Loading branch information
ernado authored Dec 22, 2022
2 parents f701b6a + a896e22 commit 040369b
Show file tree
Hide file tree
Showing 18 changed files with 603 additions and 146 deletions.
16 changes: 16 additions & 0 deletions _testdata/positive/custom_formats.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
},
{
"$ref": "#/components/parameters/Color"
},
{
"$ref": "#/components/parameters/Hex"
}
],
"requestBody": {
Expand Down Expand Up @@ -60,6 +63,15 @@
"type": "string",
"format": "rgba"
}
},
"Hex": {
"name": "hex",
"in": "query",
"description": "Hex",
"schema": {
"type": "string",
"format": "hex"
}
}
},
"schemas": {
Expand Down Expand Up @@ -90,6 +102,10 @@
"background_color": {
"type": "string",
"format": "rgba"
},
"hex_color": {
"type": "string",
"format": "hex"
}
}
}
Expand Down
106 changes: 106 additions & 0 deletions gen/custom_format.go
Original file line number Diff line number Diff line change
@@ -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
}
87 changes: 87 additions & 0 deletions gen/custom_format_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
78 changes: 0 additions & 78 deletions gen/generator.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package gen

import (
"fmt"
"go/token"
"reflect"
"strings"

"github.com/go-faster/errors"
Expand All @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
4 changes: 4 additions & 0 deletions gen/ir/custom_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down
20 changes: 11 additions & 9 deletions gen/schema_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 4 additions & 10 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/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"
Expand Down Expand Up @@ -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),
Expand Down
Loading

0 comments on commit 040369b

Please sign in to comment.