Overview
The SpecGenerator generates OpenAPI 3.0.3 specifications at build time by parsing Go source code and extracting endpoint metadata. It produces JSON spec files that can be embedded or served at runtime.
Type Definition
type SpecGenerator struct {
versionConfigs VersionConfigProvider
routeRegistrar RouteRegistrar
vmInitializer VersionManagerInitializer
}
type VersionConfigProvider func() map[string]*APIDocsConfig
type RouteRegistrar func(vm *APIVersionManager) error
type VersionManagerInitializer func() (*APIVersionManager, error)
The spec generator operates in docs-only mode during builds, meaning it doesn’t start an HTTP server. It only parses code and generates documentation.
Constructor Functions
NewSpecGenerator
func NewSpecGenerator(
configs VersionConfigProvider,
routes RouteRegistrar,
) *SpecGenerator
Creates a spec generator with version configs and route registrar.
configs
VersionConfigProvider
required
Function that returns version configurations
Function that registers routes for all versions
Location: core/server/api/spec_generator.go:25
Example:
sg := api.NewSpecGenerator(
func() map[string]*api.APIDocsConfig {
return map[string]*api.APIDocsConfig{
"v1": {Title: "API v1", Version: "v1"},
}
},
func(vm *api.APIVersionManager) error {
return vm.SetVersionRouteRegistrar("v1", registerV1Routes)
},
)
NewSpecGeneratorWithInitializer
func NewSpecGeneratorWithInitializer(
initializer VersionManagerInitializer,
) *SpecGenerator
Creates a spec generator with a custom version manager initializer.
initializer
VersionManagerInitializer
required
Function that creates and configures an APIVersionManager
Location: core/server/api/spec_generator.go:32
Example:
sg := api.NewSpecGeneratorWithInitializer(func() (*api.APIVersionManager, error) {
vm := api.NewAPIVersionManager()
// Custom setup logic
return vm, nil
})
Core Methods
Generate
func (sg *SpecGenerator) Generate(outputDir string, onlyVersion string) error
Generates OpenAPI specs and writes them to disk.
Directory where spec files will be written
Optional: Generate spec for a single version only (empty string = all versions)
Returns:
error - Error if generation or validation fails
Location: core/server/api/spec_generator.go:38
Process:
- Creates output directory if it doesn’t exist
- Temporarily disables embedded spec loading (sets
PB_EXT_DISABLE_OPENAPI_SPECS=1)
- Initializes version manager and registers routes
- Parses source code via AST
- Generates OpenAPI specs for each version
- Writes specs as
{version}.json files
- Validates all generated specs
- Restores environment variables
Example:
sg := api.NewSpecGenerator(getVersionConfigs, registerRoutes)
// Generate all versions
if err := sg.Generate("./specs", ""); err != nil {
log.Fatal(err)
}
// Generate only v1
if err := sg.Generate("./specs", "v1"); err != nil {
log.Fatal(err)
}
Validate
func (sg *SpecGenerator) Validate(specsDir string) error
Validates that all required spec files exist and are properly formatted.
Directory containing spec files to validate
Location: core/server/api/spec_generator.go:131
Checks:
- All configured versions have corresponding spec files
- Spec files are valid JSON
- Required OpenAPI fields are present (openapi, info, paths)
- Version identifiers match file names
Example:
if err := sg.Validate("./specs"); err != nil {
log.Fatalf("Spec validation failed: %v", err)
}
Validation Functions
ValidateSpecs
func ValidateSpecs(specsDir string, versions []string) error
Validates specs for a list of versions.
Directory containing spec files
List of version identifiers to validate
Location: core/server/api/spec_generator.go:156
ValidateSpecFile
func ValidateSpecFile(specPath string, expectedVersion string) error
Validates a single spec file.
Expected version identifier (validates filename matches)
Location: core/server/api/spec_generator.go:171
Validation Checks:
- File exists and is readable
- Valid JSON format
- Contains required OpenAPI fields:
openapi (version string)
info.title
info.version
paths (object)
- Filename matches expected version pattern
Build Integration Examples
// cmd/gen-specs/main.go
package main
import (
"flag"
"log"
"github.com/magooney-loon/pb-ext/core/server/api"
)
func main() {
outputDir := flag.String("output", "./specs", "Output directory")
version := flag.String("version", "", "Generate single version only")
validate := flag.Bool("validate", false, "Validate existing specs")
flag.Parse()
sg := api.NewSpecGenerator(getVersionConfigs, registerAllRoutes)
if *validate {
if err := sg.Validate(*outputDir); err != nil {
log.Fatalf("Validation failed: %v", err)
}
log.Println("✓ All specs valid")
return
}
if err := sg.Generate(*outputDir, *version); err != nil {
log.Fatalf("Generation failed: %v", err)
}
log.Println("✓ Specs generated successfully")
}
Makefile Integration
.PHONY: specs specs-validate specs-clean
specs:
@echo "Generating OpenAPI specs..."
@go run cmd/gen-specs/main.go -output=./specs
specs-validate:
@echo "Validating OpenAPI specs..."
@go run cmd/gen-specs/main.go -validate -output=./specs
specs-clean:
@echo "Cleaning spec files..."
@rm -rf ./specs/*.json
build: specs
@echo "Building with embedded specs..."
@go build -o dist/server cmd/server/main.go
Go Generate
// cmd/server/main.go
//go:generate go run ../gen-specs/main.go -output=../../specs
package main
CI/CD Pipeline
# .github/workflows/build.yml
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Generate OpenAPI specs
run: go run cmd/gen-specs/main.go -output=./specs
- name: Validate specs
run: go run cmd/gen-specs/main.go -validate -output=./specs
- name: Build binary
run: go build -o dist/server cmd/server/main.go
- name: Upload specs artifact
uses: actions/upload-artifact@v3
with:
name: openapi-specs
path: specs/*.json
Complete Example
// cmd/gen-specs/main.go
package main
import (
"log"
"os"
"github.com/magooney-loon/pb-ext/core/server/api"
)
func main() {
// Option 1: Using config provider + route registrar
sg := api.NewSpecGenerator(getVersionConfigs, registerAllRoutes)
// Option 2: Using custom initializer
// sg := api.NewSpecGeneratorWithInitializer(initializeVersionManager)
outputDir := "./specs"
if len(os.Args) > 1 {
outputDir = os.Args[1]
}
log.Println("Generating OpenAPI specs...")
if err := sg.Generate(outputDir, ""); err != nil {
log.Fatalf("Generation failed: %v", err)
}
log.Println("Validating specs...")
if err := sg.Validate(outputDir); err != nil {
log.Fatalf("Validation failed: %v", err)
}
log.Println("✓ Specs generated and validated successfully")
}
func getVersionConfigs() map[string]*api.APIDocsConfig {
return map[string]*api.APIDocsConfig{
"v1": {
Title: "My API v1",
Version: "v1",
BaseURL: "https://api.example.com",
Status: "stable",
PublicSwagger: true,
},
"v2": {
Title: "My API v2",
Version: "v2",
BaseURL: "https://api.example.com",
Status: "beta",
PublicSwagger: false,
},
}
}
func registerAllRoutes(vm *api.APIVersionManager) error {
// Register v1 routes
if err := vm.SetVersionRouteRegistrar("v1", registerV1Routes); err != nil {
return err
}
// Register v2 routes
if err := vm.SetVersionRouteRegistrar("v2", registerV2Routes); err != nil {
return err
}
// Register routes for documentation (no HTTP server)
return vm.RegisterAllVersionRoutesForDocs()
}
func registerV1Routes(router *api.VersionedAPIRouter) {
api := router.SetPrefix("/api/v1")
api.GET("/users", listUsersV1)
api.POST("/users", createUserV1)
}
func registerV2Routes(router *api.VersionedAPIRouter) {
api := router.SetPrefix("/api/v2")
api.GET("/users", listUsersV2)
api.POST("/users", createUserV2)
}
Environment Variables
The spec generator temporarily modifies environment variables during generation:
PB_EXT_DISABLE_OPENAPI_SPECS
Set to "1" during generation to force runtime spec generation (disables disk loading)
Unset during generation to prevent reading from disk
Environment variables are automatically restored after generation completes, even if an error occurs.
Best Practices
- Build-Time Generation: Always generate specs during builds, not at runtime in production
- Version Control: Commit generated specs to version control for reproducibility
- Validation: Always run
Validate() after Generate() in CI/CD
- Single Responsibility: Keep spec generation separate from server startup
- Go Generate: Use
//go:generate directives for automatic regeneration
- Output Directory: Use
./specs or ./openapi as standard output directory
- CI Integration: Generate and validate specs in CI pipeline before builds
Common Patterns
Generate on File Change
# Using watchexec or similar
watchexec -e go -w cmd -w core -- go run cmd/gen-specs/main.go
Version-Specific Generation
// Generate only production version
if err := sg.Generate("./specs", "v1"); err != nil {
log.Fatal(err)
}
// Generate only beta version
if err := sg.Generate("./specs", "v2"); err != nil {
log.Fatal(err)
}
func generateWithPrettyJSON(sg *api.SpecGenerator, outputDir string) error {
if err := sg.Generate(outputDir, ""); err != nil {
return err
}
// Re-format with indentation
files, _ := filepath.Glob(filepath.Join(outputDir, "*.json"))
for _, file := range files {
data, _ := os.ReadFile(file)
var spec map[string]interface{}
json.Unmarshal(data, &spec)
pretty, _ := json.MarshalIndent(spec, "", " ")
os.WriteFile(file, pretty, 0644)
}
return nil
}
Output Structure
specs/
├── v1.json # OpenAPI 3.0.3 spec for v1
├── v2.json # OpenAPI 3.0.3 spec for v2
└── beta.json # OpenAPI 3.0.3 spec for beta
Each spec file contains:
- OpenAPI version (
"3.0.3")
- API info (title, version, description)
- Server URLs
- All endpoint paths and operations
- Component schemas
- Security schemes