Skip to content
Merged
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
20 changes: 20 additions & 0 deletions experimental/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ generation:
path: github.com/org/project/models
alias: models # optional, defaults to last segment of path

# Output options: control which operations and schemas are included.
output-options:
# Only include operations tagged with one of these tags. Ignored when empty.
include-tags:
- public
- beta
# Exclude operations tagged with one of these tags. Ignored when empty.
exclude-tags:
- internal
# Only include operations with one of these operation IDs. Ignored when empty.
include-operation-ids:
- listPets
- createPet
# Exclude operations with one of these operation IDs. Ignored when empty.
exclude-operation-ids:
- deprecatedEndpoint
# Exclude schemas with the given names from generation. Ignored when empty.
exclude-schemas:
- InternalConfig

# Type mappings: OpenAPI type/format to Go type.
# User values are merged on top of defaults — you only need to specify overrides.
type-mapping:
Expand Down
46 changes: 41 additions & 5 deletions experimental/cmd/oapi-codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package main
import (
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
Expand All @@ -20,9 +23,9 @@ func main() {
flagPackage := flag.String("package", "", "Go package name for generated code")
flagOutput := flag.String("output", "", "output file path (default: <spec-basename>.gen.go)")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] <spec-path>\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Usage: %s [options] <spec-path-or-url>\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Arguments:\n")
fmt.Fprintf(os.Stderr, " spec-path path to OpenAPI spec file\n\n")
fmt.Fprintf(os.Stderr, " spec-path-or-url path or URL to OpenAPI spec file\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
}
Expand All @@ -35,8 +38,8 @@ func main() {

specPath := flag.Arg(0)

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

// Default output to <spec-basename>.gen.go
if cfg.Output == "" {
base := filepath.Base(specPath)
// For URLs, extract the filename from the URL path
baseName := specPath
if u, err := url.Parse(specPath); err == nil && u.Scheme != "" && u.Host != "" {
baseName = u.Path
}
base := filepath.Base(baseName)
ext := filepath.Ext(base)
cfg.Output = strings.TrimSuffix(base, ext) + ".gen.go"
}
Expand All @@ -103,3 +111,31 @@ func main() {

fmt.Printf("Generated %s\n", cfg.Output)
}

// loadSpec loads an OpenAPI spec from a file path or URL.
func loadSpec(specPath string) ([]byte, error) {
u, err := url.Parse(specPath)
if err == nil && u.Scheme != "" && u.Host != "" {
return loadSpecFromURL(u.String())
}
return os.ReadFile(specPath)
}

// loadSpecFromURL fetches an OpenAPI spec from an HTTP(S) URL.
func loadSpecFromURL(specURL string) ([]byte, error) {
resp, err := http.Get(specURL) //nolint:gosec // URL comes from user-provided spec path
if err != nil {
return nil, fmt.Errorf("fetching spec from URL: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("fetching spec from URL: HTTP %d %s", resp.StatusCode, resp.Status)
}

data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading spec from URL: %w", err)
}
return data, nil
}
9 changes: 9 additions & 0 deletions experimental/internal/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
return "", fmt.Errorf("gathering schemas: %w", err)
}

// Filter excluded schemas
schemas = FilterSchemasByName(schemas, cfg.OutputOptions.ExcludeSchemas)

// Pass 2: Compute names for all schemas
converter := NewNameConverter(cfg.NameMangling, cfg.NameSubstitutions)
ComputeSchemaNames(schemas, converter, contentTypeNamer)
Expand Down Expand Up @@ -102,6 +105,9 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
return "", fmt.Errorf("gathering operations: %w", err)
}

// Apply operation filters
ops = FilterOperations(ops, cfg.OutputOptions)

// Generate client
clientGen, err := NewClientGenerator(schemaIndex, cfg.Generation.SimpleClient, cfg.Generation.ModelsPackage)
if err != nil {
Expand Down Expand Up @@ -158,6 +164,9 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
return "", fmt.Errorf("gathering operations: %w", err)
}

// Apply operation filters
ops = FilterOperations(ops, cfg.OutputOptions)

if len(ops) > 0 {
// Generate server
serverGen, err := NewServerGenerator(cfg.Generation.Server)
Expand Down
16 changes: 16 additions & 0 deletions experimental/internal/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type Configuration struct {
Output string `yaml:"output"`
// Generation controls which parts of the code are generated
Generation GenerationOptions `yaml:"generation,omitempty"`
// OutputOptions controls filtering of operations and schemas
OutputOptions OutputOptions `yaml:"output-options,omitempty"`
// TypeMapping allows customizing OpenAPI type/format to Go type mappings
TypeMapping TypeMapping `yaml:"type-mapping,omitempty"`
// NameMangling configures how OpenAPI names are converted to Go identifiers
Expand All @@ -38,6 +40,20 @@ type Configuration struct {
StructTags StructTagsConfig `yaml:"struct-tags,omitempty"`
}

// OutputOptions controls filtering of which operations and schemas are included in generation.
type OutputOptions struct {
// IncludeTags only includes operations tagged with one of these tags. Ignored when empty.
IncludeTags []string `yaml:"include-tags,omitempty"`
// ExcludeTags excludes operations tagged with one of these tags. Ignored when empty.
ExcludeTags []string `yaml:"exclude-tags,omitempty"`
// IncludeOperationIDs only includes operations with one of these operation IDs. Ignored when empty.
IncludeOperationIDs []string `yaml:"include-operation-ids,omitempty"`
// ExcludeOperationIDs excludes operations with one of these operation IDs. Ignored when empty.
ExcludeOperationIDs []string `yaml:"exclude-operation-ids,omitempty"`
// ExcludeSchemas excludes schemas with the given names from generation. Ignored when empty.
ExcludeSchemas []string `yaml:"exclude-schemas,omitempty"`
}

// ModelsPackage specifies an external package containing the model types.
type ModelsPackage struct {
// Path is the import path for the models package (e.g., "github.com/org/project/models")
Expand Down
97 changes: 97 additions & 0 deletions experimental/internal/codegen/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package codegen

// FilterOperationsByTag filters operations based on include/exclude tag lists.
// Exclude is applied first, then include.
func FilterOperationsByTag(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor {
if len(opts.ExcludeTags) > 0 {
tags := sliceToSet(opts.ExcludeTags)
ops = filterOps(ops, func(op *OperationDescriptor) bool {
return !operationHasTag(op, tags)
})
}
if len(opts.IncludeTags) > 0 {
tags := sliceToSet(opts.IncludeTags)
ops = filterOps(ops, func(op *OperationDescriptor) bool {
return operationHasTag(op, tags)
})
}
return ops
}

// FilterOperationsByOperationID filters operations based on include/exclude operation ID lists.
// Exclude is applied first, then include.
func FilterOperationsByOperationID(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor {
if len(opts.ExcludeOperationIDs) > 0 {
ids := sliceToSet(opts.ExcludeOperationIDs)
ops = filterOps(ops, func(op *OperationDescriptor) bool {
return !ids[op.OperationID]
})
}
if len(opts.IncludeOperationIDs) > 0 {
ids := sliceToSet(opts.IncludeOperationIDs)
ops = filterOps(ops, func(op *OperationDescriptor) bool {
return ids[op.OperationID]
})
}
return ops
}

// FilterOperations applies all operation filters (tags, operation IDs) from OutputOptions.
func FilterOperations(ops []*OperationDescriptor, opts OutputOptions) []*OperationDescriptor {
ops = FilterOperationsByTag(ops, opts)
ops = FilterOperationsByOperationID(ops, opts)
return ops
}

// FilterSchemasByName removes schemas whose component name is in the exclude list.
// Only filters top-level component schemas (path: components/schemas/<name>).
func FilterSchemasByName(schemas []*SchemaDescriptor, excludeNames []string) []*SchemaDescriptor {
if len(excludeNames) == 0 {
return schemas
}
excluded := sliceToSet(excludeNames)
result := make([]*SchemaDescriptor, 0, len(schemas))
for _, s := range schemas {
// Check if this is a top-level component schema
if len(s.Path) == 3 && s.Path[0] == "components" && s.Path[1] == "schemas" {
if excluded[s.Path[2]] {
continue
}
}
result = append(result, s)
}
return result
}

// operationHasTag returns true if the operation has any of the given tags.
func operationHasTag(op *OperationDescriptor, tags map[string]bool) bool {
if op == nil || op.Spec == nil {
return false
}
for _, tag := range op.Spec.Tags {
if tags[tag] {
return true
}
}
return false
}

// filterOps returns operations that satisfy the predicate.
func filterOps(ops []*OperationDescriptor, keep func(*OperationDescriptor) bool) []*OperationDescriptor {
result := make([]*OperationDescriptor, 0, len(ops))
for _, op := range ops {
if keep(op) {
result = append(result, op)
}
}
return result
}

// sliceToSet converts a string slice to a set (map[string]bool).
func sliceToSet(items []string) map[string]bool {
m := make(map[string]bool, len(items))
for _, item := range items {
m[item] = true
}
return m
}
Loading