diff --git a/.github/docs/openapi3filter_fixtures.txt b/.github/docs/openapi3filter_fixtures.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/.github/docs/openapi3gen.txt b/.github/docs/openapi3gen.txt index e692b8594..0626e8721 100644 --- a/.github/docs/openapi3gen.txt +++ b/.github/docs/openapi3gen.txt @@ -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 @@ -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 @@ -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 + diff --git a/docs.sh b/docs.sh index 08d3eb0a5..ca3029a75 100755 --- a/docs.sh +++ b/docs.sh @@ -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 diff --git a/openapi3gen/internal/subpkg/sub_type.go b/openapi3gen/internal/subpkg/sub_type.go new file mode 100644 index 000000000..3dc961dc4 --- /dev/null +++ b/openapi3gen/internal/subpkg/sub_type.go @@ -0,0 +1,5 @@ +package subpkg + +type Child struct { + Name string `yaml:"name"` +} diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index a301337e1..6afed0033 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "reflect" + "regexp" "strings" "time" @@ -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 @@ -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 { @@ -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...) @@ -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{} } @@ -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/") { @@ -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 { @@ -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 @@ -362,6 +401,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type v.SetSchema(schema) } } + } if g.opts.schemaCustomizer != nil { @@ -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() { @@ -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{}{} diff --git a/openapi3gen/openapi3gen_newschemarefforvalue_test.go b/openapi3gen/openapi3gen_newschemarefforvalue_test.go new file mode 100644 index 000000000..1c46f2320 --- /dev/null +++ b/openapi3gen/openapi3gen_newschemarefforvalue_test.go @@ -0,0 +1,429 @@ +package openapi3gen_test + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3gen" + "github.com/getkin/kin-openapi/openapi3gen/internal/subpkg" +) + +// Make sure that custom schema name generator is employed and results produced with it are properly used +func ExampleNewSchemaRefForValue_withSubPackages() { + type Parent struct { + Field1 string `json:"field1"` + Child subpkg.Child `json:"child"` + } + + // these schema names should be returned by name generator + parentSchemaName := "PARENT_TYPE" + childSchemaName := "CHILD_TYPE" + + // sample of a type name generator + typeNameGenerator := func(t reflect.Type) string { + switch t { + case reflect.TypeOf(Parent{}): + return parentSchemaName + case reflect.TypeOf(subpkg.Child{}): + return childSchemaName + } + return t.Name() + } + + schemas := make(openapi3.Schemas) + schemaRef, err := openapi3gen.NewSchemaRefForValue( + &Parent{}, + schemas, + openapi3gen.CreateComponentSchemas(openapi3gen.ExportComponentSchemasOptions{ + ExportComponentSchemas: true, ExportTopLevelSchema: true, + }), + openapi3gen.CreateTypeNameGenerator(typeNameGenerator), + openapi3gen.UseAllExportedFields(), + ) + if err != nil { + panic(err) + } + + var data []byte + if data, err = json.MarshalIndent(&schemas, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemas: %s\n", data) + if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil { + panic(err) + } + + fmt.Printf("schemaRef: %s\n", data) + // Output: + // schemas: { + // "CHILD_TYPE": { + // "properties": { + // "name": { + // "type": "string" + // } + // }, + // "type": "object" + // }, + // "PARENT_TYPE": { + // "properties": { + // "child": { + // "$ref": "#/components/schemas/CHILD_TYPE" + // }, + // "field1": { + // "type": "string" + // } + // }, + // "type": "object" + // } + // } + // schemaRef: { + // "$ref": "#/components/schemas/PARENT_TYPE" + // } + +} + +func ExampleNewSchemaRefForValue_withExportingSchemas() { + type Child struct { + Age string `json:"age"` + } + type AnotherStruct struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Field3 string `json:"field3"` + } + type RecursiveType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Field3 string `json:"field3"` + AnotherStruct AnotherStruct `json:"children,omitempty"` + Child subpkg.Child `json:"child"` + Child2 Child `json:"child2"` + } + + // sample of a type name generator + typeNameGenerator := func(t reflect.Type) string { + packages := strings.Split(t.PkgPath(), "/") + return packages[len(packages)-1] + "_" + t.Name() + } + + schemas := make(openapi3.Schemas) + schemaRef, err := openapi3gen.NewSchemaRefForValue( + &RecursiveType{}, + schemas, + openapi3gen.CreateComponentSchemas(openapi3gen.ExportComponentSchemasOptions{ + ExportComponentSchemas: true, ExportTopLevelSchema: false, + }), + openapi3gen.CreateTypeNameGenerator(typeNameGenerator), + openapi3gen.UseAllExportedFields(), + ) + if err != nil { + panic(err) + } + + var schemasByte []byte + if schemasByte, err = json.MarshalIndent(&schemas, "", " "); err != nil { + panic(err) + } + var schemaRefByte []byte + if schemaRefByte, err = json.MarshalIndent(&schemaRef, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemas: %s\nschemaRef: %s\n", schemasByte, schemaRefByte) + // Output: + // schemas: { + // "openapi3gen_test_AnotherStruct": { + // "properties": { + // "field1": { + // "type": "string" + // }, + // "field2": { + // "type": "string" + // }, + // "field3": { + // "type": "string" + // } + // }, + // "type": "object" + // }, + // "openapi3gen_test_Child": { + // "properties": { + // "age": { + // "type": "string" + // } + // }, + // "type": "object" + // }, + // "subpkg_Child": { + // "properties": { + // "name": { + // "type": "string" + // } + // }, + // "type": "object" + // } + // } + // schemaRef: { + // "properties": { + // "child": { + // "$ref": "#/components/schemas/subpkg_Child" + // }, + // "child2": { + // "$ref": "#/components/schemas/openapi3gen_test_Child" + // }, + // "children": { + // "$ref": "#/components/schemas/openapi3gen_test_AnotherStruct" + // }, + // "field1": { + // "type": "string" + // }, + // "field2": { + // "type": "string" + // }, + // "field3": { + // "type": "string" + // } + // }, + // "type": "object" + // } +} + +func ExampleNewSchemaRefForValue_withExportingSchemasIgnoreTopLevelParent() { + type AnotherStruct struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Field3 string `json:"field3"` + } + type RecursiveType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Field3 string `json:"field3"` + AnotherStruct AnotherStruct `json:"children,omitempty"` + } + + schemas := make(openapi3.Schemas) + schemaRef, err := openapi3gen.NewSchemaRefForValue(&RecursiveType{}, schemas, openapi3gen.CreateComponentSchemas(openapi3gen.ExportComponentSchemasOptions{ + ExportComponentSchemas: true, ExportTopLevelSchema: false, + })) + if err != nil { + panic(err) + } + + var data []byte + if data, err = json.MarshalIndent(&schemas, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemas: %s\n", data) + if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemaRef: %s\n", data) + // Output: + // schemas: { + // "AnotherStruct": { + // "properties": { + // "field1": { + // "type": "string" + // }, + // "field2": { + // "type": "string" + // }, + // "field3": { + // "type": "string" + // } + // }, + // "type": "object" + // } + // } + // schemaRef: { + // "properties": { + // "children": { + // "$ref": "#/components/schemas/AnotherStruct" + // }, + // "field1": { + // "type": "string" + // }, + // "field2": { + // "type": "string" + // }, + // "field3": { + // "type": "string" + // } + // }, + // "type": "object" + // } +} + +func ExampleNewSchemaRefForValue_withExportingSchemasWithGeneric() { + type Child struct { + Age string `json:"age"` + } + type GenericStruct[T any] struct { + GenericField T `json:"genericField"` + Child Child `json:"child"` + } + type AnotherStruct struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Field3 string `json:"field3"` + } + type RecursiveType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Field3 string `json:"field3"` + AnotherStruct AnotherStruct `json:"children,omitempty"` + Child Child `json:"child"` + GenericStruct GenericStruct[string] `json:"genericChild"` + } + + schemas := make(openapi3.Schemas) + schemaRef, err := openapi3gen.NewSchemaRefForValue( + &RecursiveType{}, + schemas, + openapi3gen.CreateComponentSchemas(openapi3gen.ExportComponentSchemasOptions{ + ExportComponentSchemas: true, ExportTopLevelSchema: true, ExportGenerics: false, + }), + openapi3gen.UseAllExportedFields(), + ) + if err != nil { + panic(err) + } + + var data []byte + if data, err = json.MarshalIndent(&schemas, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemas: %s\n", data) + if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemaRef: %s\n", data) + // Output: + // schemas: { + // "AnotherStruct": { + // "properties": { + // "field1": { + // "type": "string" + // }, + // "field2": { + // "type": "string" + // }, + // "field3": { + // "type": "string" + // } + // }, + // "type": "object" + // }, + // "Child": { + // "properties": { + // "age": { + // "type": "string" + // } + // }, + // "type": "object" + // }, + // "RecursiveType": { + // "properties": { + // "child": { + // "$ref": "#/components/schemas/Child" + // }, + // "children": { + // "$ref": "#/components/schemas/AnotherStruct" + // }, + // "field1": { + // "type": "string" + // }, + // "field2": { + // "type": "string" + // }, + // "field3": { + // "type": "string" + // }, + // "genericChild": { + // "properties": { + // "child": { + // "$ref": "#/components/schemas/Child" + // }, + // "genericField": { + // "type": "string" + // } + // }, + // "type": "object" + // } + // }, + // "type": "object" + // } + // } + // schemaRef: { + // "$ref": "#/components/schemas/RecursiveType" + // } +} + +func ExampleNewSchemaRefForValue_withExportingSchemasWithMap() { + type Child struct { + Age string `json:"age"` + } + type MyType struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Map1 map[string]interface{} `json:"anymap"` + Map2 map[string]Child `json:"anymapChild"` + } + + schemas := make(openapi3.Schemas) + schemaRef, err := openapi3gen.NewSchemaRefForValue( + &MyType{}, + schemas, + openapi3gen.CreateComponentSchemas(openapi3gen.ExportComponentSchemasOptions{ + ExportComponentSchemas: true, ExportTopLevelSchema: false, ExportGenerics: true, + }), + openapi3gen.UseAllExportedFields(), + ) + if err != nil { + panic(err) + } + + var data []byte + if data, err = json.MarshalIndent(&schemas, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemas: %s\n", data) + if data, err = json.MarshalIndent(&schemaRef, "", " "); err != nil { + panic(err) + } + fmt.Printf("schemaRef: %s\n", data) + // Output: + // schemas: { + // "Child": { + // "properties": { + // "age": { + // "type": "string" + // } + // }, + // "type": "object" + // } + // } + // schemaRef: { + // "properties": { + // "anymap": { + // "additionalProperties": {}, + // "type": "object" + // }, + // "anymapChild": { + // "additionalProperties": { + // "$ref": "#/components/schemas/Child" + // }, + // "type": "object" + // }, + // "field1": { + // "type": "string" + // }, + // "field2": { + // "type": "string" + // } + // }, + // "type": "object" + // } +}