Skip to content

Commit

Permalink
openapi3gen: add CreateComponentSchemas option to export object schem…
Browse files Browse the repository at this point in the history
…as to components (#935)

Co-authored-by: Omer E <[email protected]>
  • Loading branch information
fenollp and tcdsv authored Apr 6, 2024
1 parent 8d57cda commit 9dbb4c3
Show file tree
Hide file tree
Showing 6 changed files with 528 additions and 8 deletions.
Empty file.
14 changes: 14 additions & 0 deletions .github/docs/openapi3gen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ type ExcludeSchemaSentinel struct{}

func (err *ExcludeSchemaSentinel) Error() string

type ExportComponentSchemasOptions struct {
ExportComponentSchemas bool
ExportTopLevelSchema bool
ExportGenerics bool
}

type Generator struct {
Types map[reflect.Type]*openapi3.SchemaRef

Expand All @@ -50,6 +56,12 @@ func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Sch
type Option func(*generatorOpt)
Option allows tweaking SchemaRef generation

func CreateComponentSchemas(exso ExportComponentSchemasOptions) Option
CreateComponents changes the default behavior to add all schemas as
components Reduces duplicate schemas in routes

func CreateTypeNameGenerator(tngnrt TypeNameGenerator) Option

func SchemaCustomizer(sc SchemaCustomizerFn) Option
SchemaCustomizer allows customization of the schema that is generated for a
field, for example to support an additional tagging scheme
Expand Down Expand Up @@ -77,3 +89,5 @@ type SetSchemar interface {
their specification. Useful when some custom datatype is needed and/or some
custom logic is needed on how the schema values would be generated

type TypeNameGenerator func(t reflect.Type) string

3 changes: 2 additions & 1 deletion docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ set -o pipefail

outdir=.github/docs
mkdir -p "$outdir"
for pkgpath in $(git ls-files | grep / | while read -r path; do dirname "$path"; done | sort -u | grep -vE '[.]git|testdata|cmd/'); do
for pkgpath in $(git ls-files | grep / | while read -r path; do dirname "$path"; done | sort -u | grep -vE '[.]git|testdata|internal|cmd/'); do
echo $pkgpath
go doc -all ./"$pkgpath" | tee "$outdir/${pkgpath////_}.txt"
done

Expand Down
5 changes: 5 additions & 0 deletions openapi3gen/internal/subpkg/sub_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package subpkg

type Child struct {
Name string `yaml:"name"`
}
85 changes: 78 additions & 7 deletions openapi3gen/openapi3gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"math"
"reflect"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -42,10 +43,20 @@ type SetSchemar interface {
SetSchema(*openapi3.Schema)
}

type ExportComponentSchemasOptions struct {
ExportComponentSchemas bool
ExportTopLevelSchema bool
ExportGenerics bool
}

type TypeNameGenerator func(t reflect.Type) string

type generatorOpt struct {
useAllExportedFields bool
throwErrorOnCycle bool
schemaCustomizer SchemaCustomizerFn
useAllExportedFields bool
throwErrorOnCycle bool
schemaCustomizer SchemaCustomizerFn
exportComponentSchemas ExportComponentSchemasOptions
typeNameGenerator TypeNameGenerator
}

// UseAllExportedFields changes the default behavior of only
Expand All @@ -54,6 +65,10 @@ func UseAllExportedFields() Option {
return func(x *generatorOpt) { x.useAllExportedFields = true }
}

func CreateTypeNameGenerator(tngnrt TypeNameGenerator) Option {
return func(x *generatorOpt) { x.typeNameGenerator = tngnrt }
}

// ThrowErrorOnCycle changes the default behavior of creating cycle
// refs to instead error if a cycle is detected.
func ThrowErrorOnCycle() Option {
Expand All @@ -66,6 +81,13 @@ func SchemaCustomizer(sc SchemaCustomizerFn) Option {
return func(x *generatorOpt) { x.schemaCustomizer = sc }
}

// CreateComponents changes the default behavior
// to add all schemas as components
// Reduces duplicate schemas in routes
func CreateComponentSchemas(exso ExportComponentSchemasOptions) Option {
return func(x *generatorOpt) { x.exportComponentSchemas = exso }
}

// NewSchemaRefForValue is a shortcut for NewGenerator(...).NewSchemaRefForValue(...)
func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) {
g := NewGenerator(opts...)
Expand All @@ -83,6 +105,7 @@ type Generator struct {
SchemaRefs map[*openapi3.SchemaRef]int

// componentSchemaRefs is a set of schemas that must be defined in the components to avoid cycles
// or if we have specified create components schemas
componentSchemaRefs map[string]struct{}
}

Expand Down Expand Up @@ -111,9 +134,16 @@ func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Sch
return nil, err
}
for ref := range g.SchemaRefs {
if _, ok := g.componentSchemaRefs[ref.Ref]; ok && schemas != nil {
schemas[ref.Ref] = &openapi3.SchemaRef{
Value: ref.Value,
refName := ref.Ref
if g.opts.exportComponentSchemas.ExportComponentSchemas && strings.HasPrefix(refName, "#/components/schemas/") {
refName = strings.TrimPrefix(refName, "#/components/schemas/")
}

if _, ok := g.componentSchemaRefs[refName]; ok && schemas != nil {
if ref.Value != nil && ref.Value.Properties != nil {
schemas[refName] = &openapi3.SchemaRef{
Value: ref.Value,
}
}
}
if strings.HasPrefix(ref.Ref, "#/components/schemas/") {
Expand Down Expand Up @@ -298,6 +328,14 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
schema.Type = &openapi3.Types{"string"}
schema.Format = "date-time"
} else {
typeName := g.generateTypeName(t)

if _, ok := g.componentSchemaRefs[typeName]; ok && g.opts.exportComponentSchemas.ExportComponentSchemas {
// Check if we have already parsed this component schema ref based on the name of the struct
// and use that if so
return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema), nil
}

for _, fieldInfo := range typeInfo.Fields {
// Only fields with JSON tag are considered (by default)
if !fieldInfo.HasJSONTag && !g.opts.useAllExportedFields {
Expand Down Expand Up @@ -347,6 +385,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
g.SchemaRefs[ref]++
schema.WithPropertyRef(fieldName, ref)
}

}

// Object only if it has properties
Expand All @@ -362,6 +401,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
v.SetSchema(schema)
}
}

}

if g.opts.schemaCustomizer != nil {
Expand All @@ -370,9 +410,40 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
}
}

if !g.opts.exportComponentSchemas.ExportComponentSchemas || t.Kind() != reflect.Struct {
return openapi3.NewSchemaRef(t.Name(), schema), nil
}

// Best way I could find to check that
// this current type is a generic
isGeneric, err := regexp.Match(`^.*\[.*\]$`, []byte(t.Name()))
if err != nil {
return nil, err
}

if isGeneric && !g.opts.exportComponentSchemas.ExportGenerics {
return openapi3.NewSchemaRef(t.Name(), schema), nil
}

// For structs we add the schemas to the component schemas
if len(parents) > 1 || g.opts.exportComponentSchemas.ExportTopLevelSchema {
typeName := g.generateTypeName(t)

g.componentSchemaRefs[typeName] = struct{}{}
return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema), nil
}

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

func (g *Generator) generateTypeName(t reflect.Type) string {
if g.opts.typeNameGenerator != nil {
return g.opts.typeNameGenerator(t)
}

return t.Name()
}

func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef {
var typeName string
switch t.Kind() {
Expand All @@ -391,7 +462,7 @@ func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Sche
mapSchema.AdditionalProperties = openapi3.AdditionalProperties{Schema: ref}
return openapi3.NewSchemaRef("", mapSchema)
default:
typeName = t.Name()
typeName = g.generateTypeName(t)
}

g.componentSchemaRefs[typeName] = struct{}{}
Expand Down
Loading

0 comments on commit 9dbb4c3

Please sign in to comment.