Skip to content
Open
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2017,7 +2017,10 @@ output: only-models.gen.go
generate:
models: true
output-options:
# NOTE that this is only required for the `Unreferenced` type
# To keep only specific unreferenced schemas, use preserve-schemas instead of skip-prune:
# preserve-schemas: [Unreferenced]
# Or add x-oapi-codegen-keep-unused: true on a schema in your spec.
# To keep all unreferenced components, use:
skip-prune: true
```

Expand Down Expand Up @@ -4454,6 +4457,7 @@ output-options:
include-operation-ids: []
exclude-operation-ids: []
exclude-schemas: []
preserve-schemas: [] # schema names to keep when pruning unreferenced components
```

Check [the docs](https://pkg.go.dev/github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen#OutputOptions) for more details of usage.
Expand Down
7 changes: 7 additions & 0 deletions configuration-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@
"type": "boolean",
"description": "Whether to skip pruning unused components on the generated code"
},
"preserve-schemas": {
"type": "array",
"description": "Schema names to keep during prune even when unreferenced. Ignored when empty. See also x-oapi-codegen-keep-unused on a schema.",
"items": {
"type": "string"
}
},
"include-tags": {
"type": "array",
"description": "Only include operations that have one of these tags. Ignored when empty.",
Expand Down
2 changes: 1 addition & 1 deletion pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) {
filterOperationsByTag(spec, opts)
filterOperationsByOperationID(spec, opts)
if !opts.OutputOptions.SkipPrune {
pruneUnusedComponents(spec)
pruneUnusedComponents(spec, opts.OutputOptions.PreserveSchemas)
}

// if we are provided an override for the response type suffix update it
Expand Down
2 changes: 2 additions & 0 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ type OutputOptions struct {
SkipFmt bool `yaml:"skip-fmt,omitempty"`
// Whether to skip pruning unused components on the generated code
SkipPrune bool `yaml:"skip-prune,omitempty"`
// PreserveSchemas lists schema names to keep during prune even when unreferenced. Ignored when empty. See also x-oapi-codegen-keep-unused on a schema.
PreserveSchemas []string `yaml:"preserve-schemas,omitempty"`
// Only include operations that have one of these tags. Ignored when empty.
IncludeTags []string `yaml:"include-tags,omitempty"`
// Exclude operations that have one of these tags. Ignored when empty.
Expand Down
2 changes: 2 additions & 0 deletions pkg/codegen/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const (
// extOapiCodegenOnlyHonourGoName is to be used to explicitly enforce the generation of a field as the `x-go-name` extension has describe it.
// This is intended to be used alongside the `allow-unexported-struct-field-names` Compatibility option
extOapiCodegenOnlyHonourGoName = "x-oapi-codegen-only-honour-go-name"
// extKeepUnused marks a schema to be kept during prune even when not referenced (x-oapi-codegen-keep-unused: true).
extKeepUnused = "x-oapi-codegen-keep-unused"
)

func extString(extPropValue any) (string, error) {
Expand Down
28 changes: 24 additions & 4 deletions pkg/codegen/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,14 @@ func removeOrphanedComponents(swagger *openapi3.T, refs []string) int {

for key := range swagger.Components.Schemas {
ref := fmt.Sprintf("#/components/schemas/%s", key)
if !stringInSlice(ref, refs) {
countRemoved++
delete(swagger.Components.Schemas, key)
if stringInSlice(ref, refs) {
continue
}
if schemaRef := swagger.Components.Schemas[key]; schemaRef != nil && schemaKeepUnused(schemaRef) {
continue
}
countRemoved++
delete(swagger.Components.Schemas, key)
}

for key := range swagger.Components.Parameters {
Expand Down Expand Up @@ -474,9 +478,25 @@ func removeOrphanedComponents(swagger *openapi3.T, refs []string) int {
return countRemoved
}

func pruneUnusedComponents(swagger *openapi3.T) {
// schemaKeepUnused returns true if the schema has x-oapi-codegen-keep-unused: true and should not be pruned when unreferenced.
func schemaKeepUnused(schemaRef *openapi3.SchemaRef) bool {
if schemaRef == nil || schemaRef.Value == nil || schemaRef.Value.Extensions == nil {
return false
}
v, ok := schemaRef.Value.Extensions[extKeepUnused]
if !ok {
return false
}
b, ok := v.(bool)
return ok && b
}

func pruneUnusedComponents(swagger *openapi3.T, preserveSchemas []string) {
for {
refs := findComponentRefs(swagger)
for _, name := range preserveSchemas {
refs = append(refs, fmt.Sprintf("#/components/schemas/%s", name))
}
countRemoved := removeOrphanedComponents(swagger, refs)
if countRemoved < 1 {
break
Expand Down
105 changes: 102 additions & 3 deletions pkg/codegen/prune_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestFilterOnlyCat(t *testing.T) {
assert.NotEmpty(t, swagger.Paths.Value("/cat").Get, "GET /cat operation should still be in spec")
assert.Empty(t, swagger.Paths.Value("/dog").Get, "GET /dog should have been removed from spec")

pruneUnusedComponents(swagger)
pruneUnusedComponents(swagger, nil)

assert.Len(t, swagger.Components.Schemas, 3)
}
Expand Down Expand Up @@ -101,7 +101,7 @@ func TestFilterOnlyDog(t *testing.T) {
assert.NotEmpty(t, swagger.Paths.Value("/dog").Get)
assert.Empty(t, swagger.Paths.Value("/cat").Get)

pruneUnusedComponents(swagger)
pruneUnusedComponents(swagger, nil)

assert.Len(t, swagger.Components.Schemas, 3)
}
Expand All @@ -121,7 +121,7 @@ func TestPruningUnusedComponents(t *testing.T) {
assert.Len(t, swagger.Components.Links, 1)
assert.Len(t, swagger.Components.Callbacks, 1)

pruneUnusedComponents(swagger)
pruneUnusedComponents(swagger, nil)

assert.Len(t, swagger.Components.Schemas, 0)
assert.Len(t, swagger.Components.Parameters, 0)
Expand All @@ -136,6 +136,105 @@ func TestPruningUnusedComponents(t *testing.T) {
assert.Len(t, swagger.Components.Callbacks, 0)
}

func TestPrunePreserveSchemas(t *testing.T) {
swagger, err := openapi3.NewLoader().LoadFromData([]byte(prunePreserveSchemasFixture))
assert.NoError(t, err)
assert.Len(t, swagger.Components.Schemas, 3)

pruneUnusedComponents(swagger, []string{"KeepMe"})

// Only KeepMe is in preserve list; others are unreferenced and pruned.
assert.Contains(t, swagger.Components.Schemas, "KeepMe")
assert.NotContains(t, swagger.Components.Schemas, "PruneA")
assert.NotContains(t, swagger.Components.Schemas, "PruneB")
assert.Len(t, swagger.Components.Schemas, 1)
}

const prunePreserveSchemasFixture = `
openapi: 3.0.1
info:
title: Test
version: 1.0.0
paths:
/test:
get:
operationId: test
responses:
'200':
description: ok
content:
application/json:
schema:
type: object
components:
schemas:
KeepMe:
type: object
properties:
id:
type: integer
PruneA:
type: object
properties:
name:
type: string
PruneB:
type: object
properties:
value:
type: string
`

func TestPruneSchemaKeepUnusedExtension(t *testing.T) {
swagger, err := openapi3.NewLoader().LoadFromData([]byte(pruneKeepUnusedExtensionFixture))
assert.NoError(t, err)
assert.Len(t, swagger.Components.Schemas, 3)

pruneUnusedComponents(swagger, nil)

// UnusedSchema has x-oapi-codegen-keep-unused: true so it is kept; UsedSchema is referenced; OtherUnused is pruned.
assert.Contains(t, swagger.Components.Schemas, "UsedSchema")
assert.Contains(t, swagger.Components.Schemas, "UnusedSchema")
assert.NotContains(t, swagger.Components.Schemas, "OtherUnused")
assert.Len(t, swagger.Components.Schemas, 2)
}

const pruneKeepUnusedExtensionFixture = `
openapi: 3.0.1
info:
title: Test
version: 1.0.0
paths:
/test:
get:
operationId: test
responses:
'200':
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/UsedSchema'
components:
schemas:
UsedSchema:
type: object
properties:
id:
type: integer
UnusedSchema:
x-oapi-codegen-keep-unused: true
type: object
properties:
name:
type: string
OtherUnused:
type: object
properties:
value:
type: string
`

const pruneComprehensiveTestFixture = `
openapi: 3.0.1

Expand Down