Skip to content

Commit db8abb8

Browse files
Add output filtering (oapi-codegen#2201)
* Add output filtering Fixes oapi-codegen#2200 Add output filtering like in V2, and support fetching specs via HTTP. * Fix lint issue --------- Co-authored-by: Marcin Romaszewicz <mromaszewicz@nvidia.com>
1 parent 98e5d1a commit db8abb8

File tree

6 files changed

+506
-5
lines changed

6 files changed

+506
-5
lines changed

experimental/Configuration.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,26 @@ generation:
3737
path: github.com/org/project/models
3838
alias: models # optional, defaults to last segment of path
3939

40+
# Output options: control which operations and schemas are included.
41+
output-options:
42+
# Only include operations tagged with one of these tags. Ignored when empty.
43+
include-tags:
44+
- public
45+
- beta
46+
# Exclude operations tagged with one of these tags. Ignored when empty.
47+
exclude-tags:
48+
- internal
49+
# Only include operations with one of these operation IDs. Ignored when empty.
50+
include-operation-ids:
51+
- listPets
52+
- createPet
53+
# Exclude operations with one of these operation IDs. Ignored when empty.
54+
exclude-operation-ids:
55+
- deprecatedEndpoint
56+
# Exclude schemas with the given names from generation. Ignored when empty.
57+
exclude-schemas:
58+
- InternalConfig
59+
4060
# Type mappings: OpenAPI type/format to Go type.
4161
# User values are merged on top of defaults — you only need to specify overrides.
4262
type-mapping:

experimental/cmd/oapi-codegen/main.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package main
33
import (
44
"flag"
55
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
69
"os"
710
"path/filepath"
811
"strings"
@@ -20,9 +23,9 @@ func main() {
2023
flagPackage := flag.String("package", "", "Go package name for generated code")
2124
flagOutput := flag.String("output", "", "output file path (default: <spec-basename>.gen.go)")
2225
flag.Usage = func() {
23-
fmt.Fprintf(os.Stderr, "Usage: %s [options] <spec-path>\n\n", os.Args[0])
26+
fmt.Fprintf(os.Stderr, "Usage: %s [options] <spec-path-or-url>\n\n", os.Args[0])
2427
fmt.Fprintf(os.Stderr, "Arguments:\n")
25-
fmt.Fprintf(os.Stderr, " spec-path path to OpenAPI spec file\n\n")
28+
fmt.Fprintf(os.Stderr, " spec-path-or-url path or URL to OpenAPI spec file\n\n")
2629
fmt.Fprintf(os.Stderr, "Options:\n")
2730
flag.PrintDefaults()
2831
}
@@ -35,8 +38,8 @@ func main() {
3538

3639
specPath := flag.Arg(0)
3740

38-
// Parse the OpenAPI spec
39-
specData, err := os.ReadFile(specPath)
41+
// Load the OpenAPI spec from file or URL
42+
specData, err := loadSpec(specPath)
4043
if err != nil {
4144
fmt.Fprintf(os.Stderr, "error reading spec: %v\n", err)
4245
os.Exit(1)
@@ -78,7 +81,12 @@ func main() {
7881

7982
// Default output to <spec-basename>.gen.go
8083
if cfg.Output == "" {
81-
base := filepath.Base(specPath)
84+
// For URLs, extract the filename from the URL path
85+
baseName := specPath
86+
if u, err := url.Parse(specPath); err == nil && u.Scheme != "" && u.Host != "" {
87+
baseName = u.Path
88+
}
89+
base := filepath.Base(baseName)
8290
ext := filepath.Ext(base)
8391
cfg.Output = strings.TrimSuffix(base, ext) + ".gen.go"
8492
}
@@ -103,3 +111,31 @@ func main() {
103111

104112
fmt.Printf("Generated %s\n", cfg.Output)
105113
}
114+
115+
// loadSpec loads an OpenAPI spec from a file path or URL.
116+
func loadSpec(specPath string) ([]byte, error) {
117+
u, err := url.Parse(specPath)
118+
if err == nil && u.Scheme != "" && u.Host != "" {
119+
return loadSpecFromURL(u.String())
120+
}
121+
return os.ReadFile(specPath)
122+
}
123+
124+
// loadSpecFromURL fetches an OpenAPI spec from an HTTP(S) URL.
125+
func loadSpecFromURL(specURL string) ([]byte, error) {
126+
resp, err := http.Get(specURL) //nolint:gosec // URL comes from user-provided spec path
127+
if err != nil {
128+
return nil, fmt.Errorf("fetching spec from URL: %w", err)
129+
}
130+
defer func() { _ = resp.Body.Close() }()
131+
132+
if resp.StatusCode != http.StatusOK {
133+
return nil, fmt.Errorf("fetching spec from URL: HTTP %d %s", resp.StatusCode, resp.Status)
134+
}
135+
136+
data, err := io.ReadAll(resp.Body)
137+
if err != nil {
138+
return nil, fmt.Errorf("reading spec from URL: %w", err)
139+
}
140+
return data, nil
141+
}

experimental/internal/codegen/codegen.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
3131
return "", fmt.Errorf("gathering schemas: %w", err)
3232
}
3333

34+
// Filter excluded schemas
35+
schemas = FilterSchemasByName(schemas, cfg.OutputOptions.ExcludeSchemas)
36+
3437
// Pass 2: Compute names for all schemas
3538
converter := NewNameConverter(cfg.NameMangling, cfg.NameSubstitutions)
3639
ComputeSchemaNames(schemas, converter, contentTypeNamer)
@@ -102,6 +105,9 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
102105
return "", fmt.Errorf("gathering operations: %w", err)
103106
}
104107

108+
// Apply operation filters
109+
ops = FilterOperations(ops, cfg.OutputOptions)
110+
105111
// Generate client
106112
clientGen, err := NewClientGenerator(schemaIndex, cfg.Generation.SimpleClient, cfg.Generation.ModelsPackage)
107113
if err != nil {
@@ -158,6 +164,9 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
158164
return "", fmt.Errorf("gathering operations: %w", err)
159165
}
160166

167+
// Apply operation filters
168+
ops = FilterOperations(ops, cfg.OutputOptions)
169+
161170
if len(ops) > 0 {
162171
// Generate server
163172
serverGen, err := NewServerGenerator(cfg.Generation.Server)

experimental/internal/codegen/configuration.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ type Configuration struct {
1515
Output string `yaml:"output"`
1616
// Generation controls which parts of the code are generated
1717
Generation GenerationOptions `yaml:"generation,omitempty"`
18+
// OutputOptions controls filtering of operations and schemas
19+
OutputOptions OutputOptions `yaml:"output-options,omitempty"`
1820
// TypeMapping allows customizing OpenAPI type/format to Go type mappings
1921
TypeMapping TypeMapping `yaml:"type-mapping,omitempty"`
2022
// NameMangling configures how OpenAPI names are converted to Go identifiers
@@ -38,6 +40,20 @@ type Configuration struct {
3840
StructTags StructTagsConfig `yaml:"struct-tags,omitempty"`
3941
}
4042

43+
// OutputOptions controls filtering of which operations and schemas are included in generation.
44+
type OutputOptions struct {
45+
// IncludeTags only includes operations tagged with one of these tags. Ignored when empty.
46+
IncludeTags []string `yaml:"include-tags,omitempty"`
47+
// ExcludeTags excludes operations tagged with one of these tags. Ignored when empty.
48+
ExcludeTags []string `yaml:"exclude-tags,omitempty"`
49+
// IncludeOperationIDs only includes operations with one of these operation IDs. Ignored when empty.
50+
IncludeOperationIDs []string `yaml:"include-operation-ids,omitempty"`
51+
// ExcludeOperationIDs excludes operations with one of these operation IDs. Ignored when empty.
52+
ExcludeOperationIDs []string `yaml:"exclude-operation-ids,omitempty"`
53+
// ExcludeSchemas excludes schemas with the given names from generation. Ignored when empty.
54+
ExcludeSchemas []string `yaml:"exclude-schemas,omitempty"`
55+
}
56+
4157
// ModelsPackage specifies an external package containing the model types.
4258
type ModelsPackage struct {
4359
// Path is the import path for the models package (e.g., "github.com/org/project/models")
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package codegen
2+
3+
// FilterOperationsByTag filters operations based on include/exclude tag lists.
4+
// Exclude is applied first, then include.
5+
func FilterOperationsByTag(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor {
6+
if len(opts.ExcludeTags) > 0 {
7+
tags := sliceToSet(opts.ExcludeTags)
8+
ops = filterOps(ops, func(op *OperationDescriptor) bool {
9+
return !operationHasTag(op, tags)
10+
})
11+
}
12+
if len(opts.IncludeTags) > 0 {
13+
tags := sliceToSet(opts.IncludeTags)
14+
ops = filterOps(ops, func(op *OperationDescriptor) bool {
15+
return operationHasTag(op, tags)
16+
})
17+
}
18+
return ops
19+
}
20+
21+
// FilterOperationsByOperationID filters operations based on include/exclude operation ID lists.
22+
// Exclude is applied first, then include.
23+
func FilterOperationsByOperationID(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor {
24+
if len(opts.ExcludeOperationIDs) > 0 {
25+
ids := sliceToSet(opts.ExcludeOperationIDs)
26+
ops = filterOps(ops, func(op *OperationDescriptor) bool {
27+
return !ids[op.OperationID]
28+
})
29+
}
30+
if len(opts.IncludeOperationIDs) > 0 {
31+
ids := sliceToSet(opts.IncludeOperationIDs)
32+
ops = filterOps(ops, func(op *OperationDescriptor) bool {
33+
return ids[op.OperationID]
34+
})
35+
}
36+
return ops
37+
}
38+
39+
// FilterOperations applies all operation filters (tags, operation IDs) from OutputOptions.
40+
func FilterOperations(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor {
41+
ops = FilterOperationsByTag(ops, opts)
42+
ops = FilterOperationsByOperationID(ops, opts)
43+
return ops
44+
}
45+
46+
// FilterSchemasByName removes schemas whose component name is in the exclude list.
47+
// Only filters top-level component schemas (path: components/schemas/<name>).
48+
func FilterSchemasByName(schemas []*SchemaDescriptor, excludeNames []string) []*SchemaDescriptor {
49+
if len(excludeNames) == 0 {
50+
return schemas
51+
}
52+
excluded := sliceToSet(excludeNames)
53+
result := make([]*SchemaDescriptor, 0, len(schemas))
54+
for _, s := range schemas {
55+
// Check if this is a top-level component schema
56+
if len(s.Path) == 3 && s.Path[0] == "components" && s.Path[1] == "schemas" {
57+
if excluded[s.Path[2]] {
58+
continue
59+
}
60+
}
61+
result = append(result, s)
62+
}
63+
return result
64+
}
65+
66+
// operationHasTag returns true if the operation has any of the given tags.
67+
func operationHasTag(op *OperationDescriptor, tags map[string]bool) bool {
68+
if op == nil || op.Spec == nil {
69+
return false
70+
}
71+
for _, tag := range op.Spec.Tags {
72+
if tags[tag] {
73+
return true
74+
}
75+
}
76+
return false
77+
}
78+
79+
// filterOps returns operations that satisfy the predicate.
80+
func filterOps(ops []*OperationDescriptor, keep func(*OperationDescriptor) bool) []*OperationDescriptor {
81+
result := make([]*OperationDescriptor, 0, len(ops))
82+
for _, op := range ops {
83+
if keep(op) {
84+
result = append(result, op)
85+
}
86+
}
87+
return result
88+
}
89+
90+
// sliceToSet converts a string slice to a set (map[string]bool).
91+
func sliceToSet(items []string) map[string]bool {
92+
m := make(map[string]bool, len(items))
93+
for _, item := range items {
94+
m[item] = true
95+
}
96+
return m
97+
}

0 commit comments

Comments
 (0)