Skip to content

Commit

Permalink
Add an openapi3gen example + options (#320)
Browse files Browse the repository at this point in the history
  • Loading branch information
fenollp authored Mar 4, 2021
1 parent f598766 commit 96aeb23
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 120 deletions.
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

0 comments on commit 96aeb23

Please sign in to comment.