Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an openapi3gen example + options #320

Merged
merged 5 commits into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions jsoninfo/field_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ iteration:

// Read our custom "multijson" tag that
// allows multiple fields with the same name.
if v := f.Tag.Get("multijson"); len(v) > 0 {
if v := f.Tag.Get("multijson"); v != "" {
field.MultipleFields = true
jsonTag = v
}
Expand All @@ -74,11 +74,11 @@ iteration:
}

// Parse the tag
if len(jsonTag) > 0 {
if jsonTag != "" {
field.HasJSONTag = true
for i, part := range strings.Split(jsonTag, ",") {
if i == 0 {
if len(part) > 0 {
if part != "" {
field.JSONName = part
}
} else {
Expand All @@ -92,12 +92,8 @@ iteration:
}
}

if _, ok := field.Type.MethodByName("MarshalJSON"); ok {
field.TypeIsMarshaller = true
}
if _, ok := field.Type.MethodByName("UnmarshalJSON"); ok {
field.TypeIsUnmarshaller = true
}
_, field.TypeIsMarshaller = field.Type.MethodByName("MarshalJSON")
_, field.TypeIsUnmarshaller = field.Type.MethodByName("UnmarshalJSON")

// Field is done
fields = append(fields, field)
Expand Down
114 changes: 90 additions & 24 deletions openapi3gen/openapi3gen.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Package openapi3gen generates OpenAPI 3 schemas for Go types.
// Package openapi3gen generates OpenAPIv3 JSON schemas from Go types.
package openapi3gen

import (
"encoding/json"
"math"
"reflect"
"strings"
"time"
Expand All @@ -18,8 +19,22 @@ func (err *CycleError) Error() string {
return "Detected JSON cycle"
}

func NewSchemaRefForValue(value interface{}) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) {
g := NewGenerator()
// Option allows tweaking SchemaRef generation
type Option func(*generatorOpt)

type generatorOpt struct {
useAllExportedFields bool
}

// UseAllExportedFields changes the default behavior of only
// generating schemas for struct fields with a JSON tag.
func UseAllExportedFields() Option {
return func(x *generatorOpt) { x.useAllExportedFields = true }
}

// NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef.
func NewSchemaRefForValue(value interface{}, opts ...Option) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) {
g := NewGenerator(opts...)
ref, err := g.GenerateSchemaRef(reflect.TypeOf(value))
for ref := range g.SchemaRefs {
ref.Ref = ""
Expand All @@ -28,6 +43,8 @@ func NewSchemaRefForValue(value interface{}) (*openapi3.SchemaRef, map[*openapi3
}

type Generator struct {
opts generatorOpt

Types map[reflect.Type]*openapi3.SchemaRef

// SchemaRefs contains all references and their counts.
Expand All @@ -36,20 +53,25 @@ type Generator struct {
SchemaRefs map[*openapi3.SchemaRef]int
}

func NewGenerator() *Generator {
func NewGenerator(opts ...Option) *Generator {
gOpt := &generatorOpt{}
for _, f := range opts {
f(gOpt)
}
return &Generator{
Types: make(map[reflect.Type]*openapi3.SchemaRef),
SchemaRefs: make(map[*openapi3.SchemaRef]int),
opts: *gOpt,
}
}

func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, error) {
//check generatorOpt consistency here
return g.generateSchemaRefFor(nil, t)
}

func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) {
ref := g.Types[t]
if ref != nil {
if ref := g.Types[t]; ref != nil {
g.SchemaRefs[ref]++
return ref, nil
}
Expand All @@ -62,27 +84,22 @@ func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect
}

func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) {
// Get TypeInfo
typeInfo := jsoninfo.GetTypeInfo(t)
for _, parent := range parents {
if parent == typeInfo {
return nil, &CycleError{}
}
}

// Doesn't exist.
// Create the schema.
if cap(parents) == 0 {
parents = make([]*jsoninfo.TypeInfo, 0, 4)
}
parents = append(parents, typeInfo)

// Ignore pointers
for t.Kind() == reflect.Ptr {
t = t.Elem()
}

// Create instance
if strings.HasSuffix(t.Name(), "Ref") {
_, a := t.FieldByName("Ref")
v, b := t.FieldByName("Value")
Expand All @@ -104,33 +121,62 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec
}
}

// Allocate schema
schema := &openapi3.Schema{}

switch t.Kind() {
case reflect.Func, reflect.Chan:
return nil, nil
return nil, nil // ignore

case reflect.Bool:
schema.Type = "boolean"

case reflect.Int,
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
case reflect.Int:
schema.Type = "integer"
case reflect.Int8:
schema.Type = "integer"
schema.Min = &minInt8
schema.Max = &maxInt8
case reflect.Int16:
schema.Type = "integer"
schema.Min = &minInt16
schema.Max = &maxInt16
case reflect.Int32:
schema.Type = "integer"
schema.Format = "int32"
case reflect.Int64:
schema.Type = "integer"
schema.Format = "int64"
case reflect.Uint8:
schema.Type = "integer"
schema.Min = &zeroInt
schema.Max = &maxUint8
case reflect.Uint16:
schema.Type = "integer"
schema.Min = &zeroInt
schema.Max = &maxUint16
case reflect.Uint32:
schema.Type = "integer"
schema.Min = &zeroInt
schema.Max = &maxUint32
case reflect.Uint64:
schema.Type = "integer"
schema.Min = &zeroInt
schema.Max = &maxUint64

case reflect.Float32, reflect.Float64:
case reflect.Float32:
schema.Type = "number"
schema.Format = "float"
case reflect.Float64:
schema.Type = "number"
schema.Format = "double"

case reflect.String:
schema.Type = "string"

case reflect.Slice:
if t.Elem().Kind() == reflect.Uint8 {
if t == rawMessageType {
return &openapi3.SchemaRef{
Value: schema,
}, nil
return &openapi3.SchemaRef{Value: schema}, nil
}
schema.Type = "string"
schema.Format = "byte"
Expand Down Expand Up @@ -163,17 +209,26 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec
schema.Format = "date-time"
} else {
for _, fieldInfo := range typeInfo.Fields {
// Only fields with JSON tag are considered
if !fieldInfo.HasJSONTag {
// Only fields with JSON tag are considered (by default)
if !fieldInfo.HasJSONTag && !g.opts.useAllExportedFields {
continue
}
ref, err := g.generateSchemaRefFor(parents, fieldInfo.Type)
// If asked, try to use yaml tag
name, fType := fieldInfo.JSONName, fieldInfo.Type
if !fieldInfo.HasJSONTag && g.opts.useAllExportedFields {
ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1])
if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" {
name, fType = tag, ff.Type
}
}

ref, err := g.generateSchemaRefFor(parents, fType)
if err != nil {
return nil, err
}
if ref != nil {
g.SchemaRefs[ref]++
schema.WithPropertyRef(fieldInfo.JSONName, ref)
schema.WithPropertyRef(name, ref)
}
}

Expand All @@ -183,6 +238,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec
}
}
}

return openapi3.NewSchemaRef(t.Name(), schema), nil
}

Expand All @@ -192,4 +248,14 @@ var RefSchemaRef = openapi3.NewSchemaRef("Ref",
var (
timeType = reflect.TypeOf(time.Time{})
rawMessageType = reflect.TypeOf(json.RawMessage{})

zeroInt = float64(0)
maxInt8 = float64(math.MaxInt8)
minInt8 = float64(math.MinInt8)
maxInt16 = float64(math.MaxInt16)
minInt16 = float64(math.MinInt16)
maxUint8 = float64(math.MaxUint8)
maxUint16 = float64(math.MaxUint16)
maxUint32 = float64(math.MaxUint32)
maxUint64 = float64(math.MaxUint64)
)
104 changes: 17 additions & 87 deletions openapi3gen/openapi3gen_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package openapi3gen

import (
"encoding/json"
"testing"
"time"

"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
)

Expand All @@ -16,96 +15,27 @@ type CyclicType1 struct {
}

func TestCyclic(t *testing.T) {
schema, refsMap, err := NewSchemaRefForValue(&CyclicType0{})
schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{})
require.IsType(t, &CycleError{}, err)
require.Nil(t, schema)
require.Nil(t, schemaRef)
require.Empty(t, refsMap)
}

func TestSimple(t *testing.T) {
type ExampleChild string
type Example struct {
Bool bool `json:"bool"`
Int int `json:"int"`
Int64 int64 `json:"int64"`
Float64 float64 `json:"float64"`
String string `json:"string"`
Bytes []byte `json:"bytes"`
JSON json.RawMessage `json:"json"`
Time time.Time `json:"time"`
Slice []*ExampleChild `json:"slice"`
Map map[string]*ExampleChild `json:"map"`
Struct struct {
X string `json:"x"`
} `json:"struct"`
EmptyStruct struct {
X string
} `json:"structWithoutFields"`
Ptr *ExampleChild `json:"ptr"`
func TestExportedNonTagged(t *testing.T) {
type Bla struct {
A string
Another string `json:"another"`
yetAnother string
EvenAYaml string `yaml:"even_a_yaml"`
}

schema, refsMap, err := NewSchemaRefForValue(&Example{})
schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields())
require.NoError(t, err)
require.Len(t, refsMap, 14)
data, err := json.Marshal(schema)
require.NoError(t, err)
require.JSONEq(t, expectedSimple, string(data))
}

const expectedSimple = `
{
"type": "object",
"properties": {
"bool": {
"type": "boolean"
},
"int": {
"type": "integer",
"format": "int64"
},
"int64": {
"type": "integer",
"format": "int64"
},
"float64": {
"type": "number"
},
"time": {
"type": "string",
"format": "date-time"
},
"string": {
"type": "string"
},
"bytes": {
"type": "string",
"format": "byte"
},
"json": {},
"slice": {
"type": "array",
"items": {
"type": "string"
}
},
"map": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"struct": {
"type": "object",
"properties": {
"x": {
"type": "string"
}
}
},
"structWithoutFields": {},
"ptr": {
"type": "string"
}
}
require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{
Type: "object",
Properties: map[string]*openapi3.SchemaRef{
"A": {Value: &openapi3.Schema{Type: "string"}},
"another": {Value: &openapi3.Schema{Type: "string"}},
"even_a_yaml": {Value: &openapi3.Schema{Type: "string"}},
}}}, schemaRef)
}
`
Loading