Skip to content

Commit 9dbb4c3

Browse files
fenollptcdsv
andauthored
openapi3gen: add CreateComponentSchemas option to export object schemas to components (#935)
Co-authored-by: Omer E <[email protected]>
1 parent 8d57cda commit 9dbb4c3

File tree

6 files changed

+528
-8
lines changed

6 files changed

+528
-8
lines changed

.github/docs/openapi3filter_fixtures.txt

Whitespace-only changes.

.github/docs/openapi3gen.txt

+14
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ type ExcludeSchemaSentinel struct{}
2727

2828
func (err *ExcludeSchemaSentinel) Error() string
2929

30+
type ExportComponentSchemasOptions struct {
31+
ExportComponentSchemas bool
32+
ExportTopLevelSchema bool
33+
ExportGenerics bool
34+
}
35+
3036
type Generator struct {
3137
Types map[reflect.Type]*openapi3.SchemaRef
3238

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

59+
func CreateComponentSchemas(exso ExportComponentSchemasOptions) Option
60+
CreateComponents changes the default behavior to add all schemas as
61+
components Reduces duplicate schemas in routes
62+
63+
func CreateTypeNameGenerator(tngnrt TypeNameGenerator) Option
64+
5365
func SchemaCustomizer(sc SchemaCustomizerFn) Option
5466
SchemaCustomizer allows customization of the schema that is generated for a
5567
field, for example to support an additional tagging scheme
@@ -77,3 +89,5 @@ type SetSchemar interface {
7789
their specification. Useful when some custom datatype is needed and/or some
7890
custom logic is needed on how the schema values would be generated
7991

92+
type TypeNameGenerator func(t reflect.Type) string
93+

docs.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ set -o pipefail
33

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

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package subpkg
2+
3+
type Child struct {
4+
Name string `yaml:"name"`
5+
}

openapi3gen/openapi3gen.go

+78-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"math"
88
"reflect"
9+
"regexp"
910
"strings"
1011
"time"
1112

@@ -42,10 +43,20 @@ type SetSchemar interface {
4243
SetSchema(*openapi3.Schema)
4344
}
4445

46+
type ExportComponentSchemasOptions struct {
47+
ExportComponentSchemas bool
48+
ExportTopLevelSchema bool
49+
ExportGenerics bool
50+
}
51+
52+
type TypeNameGenerator func(t reflect.Type) string
53+
4554
type generatorOpt struct {
46-
useAllExportedFields bool
47-
throwErrorOnCycle bool
48-
schemaCustomizer SchemaCustomizerFn
55+
useAllExportedFields bool
56+
throwErrorOnCycle bool
57+
schemaCustomizer SchemaCustomizerFn
58+
exportComponentSchemas ExportComponentSchemasOptions
59+
typeNameGenerator TypeNameGenerator
4960
}
5061

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

68+
func CreateTypeNameGenerator(tngnrt TypeNameGenerator) Option {
69+
return func(x *generatorOpt) { x.typeNameGenerator = tngnrt }
70+
}
71+
5772
// ThrowErrorOnCycle changes the default behavior of creating cycle
5873
// refs to instead error if a cycle is detected.
5974
func ThrowErrorOnCycle() Option {
@@ -66,6 +81,13 @@ func SchemaCustomizer(sc SchemaCustomizerFn) Option {
6681
return func(x *generatorOpt) { x.schemaCustomizer = sc }
6782
}
6883

84+
// CreateComponents changes the default behavior
85+
// to add all schemas as components
86+
// Reduces duplicate schemas in routes
87+
func CreateComponentSchemas(exso ExportComponentSchemasOptions) Option {
88+
return func(x *generatorOpt) { x.exportComponentSchemas = exso }
89+
}
90+
6991
// NewSchemaRefForValue is a shortcut for NewGenerator(...).NewSchemaRefForValue(...)
7092
func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) {
7193
g := NewGenerator(opts...)
@@ -83,6 +105,7 @@ type Generator struct {
83105
SchemaRefs map[*openapi3.SchemaRef]int
84106

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

@@ -111,9 +134,16 @@ func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Sch
111134
return nil, err
112135
}
113136
for ref := range g.SchemaRefs {
114-
if _, ok := g.componentSchemaRefs[ref.Ref]; ok && schemas != nil {
115-
schemas[ref.Ref] = &openapi3.SchemaRef{
116-
Value: ref.Value,
137+
refName := ref.Ref
138+
if g.opts.exportComponentSchemas.ExportComponentSchemas && strings.HasPrefix(refName, "#/components/schemas/") {
139+
refName = strings.TrimPrefix(refName, "#/components/schemas/")
140+
}
141+
142+
if _, ok := g.componentSchemaRefs[refName]; ok && schemas != nil {
143+
if ref.Value != nil && ref.Value.Properties != nil {
144+
schemas[refName] = &openapi3.SchemaRef{
145+
Value: ref.Value,
146+
}
117147
}
118148
}
119149
if strings.HasPrefix(ref.Ref, "#/components/schemas/") {
@@ -298,6 +328,14 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
298328
schema.Type = &openapi3.Types{"string"}
299329
schema.Format = "date-time"
300330
} else {
331+
typeName := g.generateTypeName(t)
332+
333+
if _, ok := g.componentSchemaRefs[typeName]; ok && g.opts.exportComponentSchemas.ExportComponentSchemas {
334+
// Check if we have already parsed this component schema ref based on the name of the struct
335+
// and use that if so
336+
return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema), nil
337+
}
338+
301339
for _, fieldInfo := range typeInfo.Fields {
302340
// Only fields with JSON tag are considered (by default)
303341
if !fieldInfo.HasJSONTag && !g.opts.useAllExportedFields {
@@ -347,6 +385,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
347385
g.SchemaRefs[ref]++
348386
schema.WithPropertyRef(fieldName, ref)
349387
}
388+
350389
}
351390

352391
// Object only if it has properties
@@ -362,6 +401,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
362401
v.SetSchema(schema)
363402
}
364403
}
404+
365405
}
366406

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

413+
if !g.opts.exportComponentSchemas.ExportComponentSchemas || t.Kind() != reflect.Struct {
414+
return openapi3.NewSchemaRef(t.Name(), schema), nil
415+
}
416+
417+
// Best way I could find to check that
418+
// this current type is a generic
419+
isGeneric, err := regexp.Match(`^.*\[.*\]$`, []byte(t.Name()))
420+
if err != nil {
421+
return nil, err
422+
}
423+
424+
if isGeneric && !g.opts.exportComponentSchemas.ExportGenerics {
425+
return openapi3.NewSchemaRef(t.Name(), schema), nil
426+
}
427+
428+
// For structs we add the schemas to the component schemas
429+
if len(parents) > 1 || g.opts.exportComponentSchemas.ExportTopLevelSchema {
430+
typeName := g.generateTypeName(t)
431+
432+
g.componentSchemaRefs[typeName] = struct{}{}
433+
return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema), nil
434+
}
435+
373436
return openapi3.NewSchemaRef(t.Name(), schema), nil
374437
}
375438

439+
func (g *Generator) generateTypeName(t reflect.Type) string {
440+
if g.opts.typeNameGenerator != nil {
441+
return g.opts.typeNameGenerator(t)
442+
}
443+
444+
return t.Name()
445+
}
446+
376447
func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef {
377448
var typeName string
378449
switch t.Kind() {
@@ -391,7 +462,7 @@ func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Sche
391462
mapSchema.AdditionalProperties = openapi3.AdditionalProperties{Schema: ref}
392463
return openapi3.NewSchemaRef("", mapSchema)
393464
default:
394-
typeName = t.Name()
465+
typeName = g.generateTypeName(t)
395466
}
396467

397468
g.componentSchemaRefs[typeName] = struct{}{}

0 commit comments

Comments
 (0)