Skip to content
This repository was archived by the owner on Apr 14, 2026. It is now read-only.

Commit 45a84e1

Browse files
mromaszewiczclaude
andcommitted
Split runtime into types/params/helpers sub-packages
Restructure the runtime package generation from a single monolithic file into three sub-packages for better organization and independent imports: - runtime/types: custom Go types (Date, Email, UUID, File, Nullable) - runtime/params: parameter serialization/deserialization functions - runtime/helpers: utility functions (MarshalForm) Key changes: - Replace single RuntimePrefix with category-specific prefixes (RuntimeParamsPrefix, RuntimeTypesPrefix, RuntimeHelpersPrefix) - Add {{ typesPrefix }} template function to param templates so params can reference types.Date/types.DateFormat across packages - Update all generators and templates for split prefixes - GenerateRuntime() now returns RuntimeOutput with three fields - CLI --generate-runtime creates three sub-directories - Restructure internal/codegen to codegen/internal with public API - Update webhook example to use shared runtime package - Document runtime-package, webhook, and callback config options Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c7b1de1 commit 45a84e1

415 files changed

Lines changed: 3926 additions & 863 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

experimental/Configuration.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,47 @@ generation:
3030
# Default: false
3131
simple-client: true
3232

33+
# Generate webhook initiator code (sends webhook requests to target URLs).
34+
# Generates a framework-agnostic client that takes the full target URL per-call.
35+
# Default: false
36+
webhook-initiator: true
37+
38+
# Generate webhook receiver code (receives webhook requests).
39+
# Generates framework-specific handler functions. Requires server to be set.
40+
# Default: false
41+
webhook-receiver: true
42+
43+
# Generate callback initiator code (sends callback requests to target URLs).
44+
# Default: false
45+
callback-initiator: true
46+
47+
# Generate callback receiver code (receives callback requests).
48+
# Generates framework-specific handler functions. Requires server to be set.
49+
# Default: false
50+
callback-receiver: true
51+
3352
# Use model types from an external package instead of generating them locally.
3453
# When set, models are imported rather than generated.
3554
# Default: not set (models are generated locally)
3655
models-package:
3756
path: github.com/org/project/models
3857
alias: models # optional, defaults to last segment of path
3958

59+
# Use a shared runtime package instead of embedding helpers in each generated file.
60+
# When set, custom types (Date, Email, UUID, File, Nullable), parameter
61+
# serialization functions, and helper functions (MarshalForm) are NOT embedded
62+
# in the output. Instead, the generated code imports them from three sub-packages:
63+
# <path>/types — custom types (Date, Email, UUID, File, Nullable)
64+
# <path>/params — parameter style/bind functions (StyleSimpleParam, BindFormParam, etc.)
65+
# <path>/helpers — utility functions (MarshalForm)
66+
#
67+
# Generate the runtime package once with:
68+
# oapi-codegen --generate-runtime <base-import-path>
69+
#
70+
# Default: not set (all helpers are embedded in each generated file)
71+
runtime-package:
72+
path: github.com/org/project/runtime
73+
4074
# Output options: control which operations and schemas are included.
4175
output-options:
4276
# Only include operations tagged with one of these tags. Ignored when empty.

experimental/cmd/oapi-codegen/main.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ import (
1515

1616
"gopkg.in/yaml.v3"
1717

18-
"github.com/oapi-codegen/oapi-codegen-exp/experimental/internal/codegen"
18+
"github.com/oapi-codegen/oapi-codegen-exp/experimental/codegen"
1919
)
2020

2121
func main() {
2222
configPath := flag.String("config", "", "path to configuration file")
2323
flagPackage := flag.String("package", "", "Go package name for generated code")
2424
flagOutput := flag.String("output", "", "output file path (default: <spec-basename>.gen.go)")
25+
flagGenerateRuntime := flag.String("generate-runtime", "", "generate runtime sub-packages (types, params, helpers) under the output directory; value is the base import path (no spec required)")
2526
flag.Usage = func() {
2627
fmt.Fprintf(os.Stderr, "Usage: %s [options] <spec-path-or-url>\n\n", os.Args[0])
2728
fmt.Fprintf(os.Stderr, "Arguments:\n")
@@ -31,6 +32,42 @@ func main() {
3132
}
3233
flag.Parse()
3334

35+
// --generate-runtime mode: produce three runtime sub-packages and exit.
36+
if *flagGenerateRuntime != "" {
37+
rt, err := codegen.GenerateRuntime(*flagGenerateRuntime)
38+
if err != nil {
39+
fmt.Fprintf(os.Stderr, "error generating runtime: %v\n", err)
40+
os.Exit(1)
41+
}
42+
// Output directory defaults to current directory.
43+
outputDir := *flagOutput
44+
if outputDir == "" {
45+
outputDir = "."
46+
}
47+
// Write each sub-package into its own subdirectory.
48+
subPkgs := []struct {
49+
dir, file, code string
50+
}{
51+
{"types", "types.gen.go", rt.Types},
52+
{"params", "params.gen.go", rt.Params},
53+
{"helpers", "helpers.gen.go", rt.Helpers},
54+
}
55+
for _, sp := range subPkgs {
56+
dir := filepath.Join(outputDir, sp.dir)
57+
if err := os.MkdirAll(dir, 0755); err != nil {
58+
fmt.Fprintf(os.Stderr, "error creating directory %s: %v\n", dir, err)
59+
os.Exit(1)
60+
}
61+
path := filepath.Join(dir, sp.file)
62+
if err := os.WriteFile(path, []byte(sp.code), 0644); err != nil {
63+
fmt.Fprintf(os.Stderr, "error writing %s: %v\n", path, err)
64+
os.Exit(1)
65+
}
66+
fmt.Printf("Generated %s\n", path)
67+
}
68+
return
69+
}
70+
3471
if flag.NArg() != 1 {
3572
flag.Usage()
3673
os.Exit(1)

experimental/codegen/codegen.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Package codegen provides the public API for oapi-codegen's experimental code generator.
2+
//
3+
// This package re-exports the core types and functions from the internal
4+
// implementation, providing a stable public interface for external consumers.
5+
package codegen
6+
7+
import (
8+
"github.com/pb33f/libopenapi"
9+
10+
impl "github.com/oapi-codegen/oapi-codegen-exp/experimental/codegen/internal"
11+
)
12+
13+
// Configuration is the top-level configuration for code generation.
14+
type Configuration = impl.Configuration
15+
16+
// GenerationOptions controls which parts of the code are generated.
17+
type GenerationOptions = impl.GenerationOptions
18+
19+
// OutputOptions controls filtering of operations and schemas.
20+
type OutputOptions = impl.OutputOptions
21+
22+
// ModelsPackage specifies an external package containing the model types.
23+
type ModelsPackage = impl.ModelsPackage
24+
25+
// TypeMapping allows customizing OpenAPI type/format to Go type mappings.
26+
type TypeMapping = impl.TypeMapping
27+
28+
// NameMangling configures how OpenAPI names are converted to Go identifiers.
29+
type NameMangling = impl.NameMangling
30+
31+
// NameSubstitutions allows direct overrides of generated names.
32+
type NameSubstitutions = impl.NameSubstitutions
33+
34+
// StructTagsConfig configures how struct tags are generated for fields.
35+
type StructTagsConfig = impl.StructTagsConfig
36+
37+
// RuntimePackageConfig specifies an external package containing runtime helpers.
38+
type RuntimePackageConfig = impl.RuntimePackageConfig
39+
40+
// RuntimeOutput holds the generated code for each runtime sub-package.
41+
type RuntimeOutput = impl.RuntimeOutput
42+
43+
// Generate produces Go code from the parsed OpenAPI document.
44+
// specData is the raw spec bytes used to embed the spec in the generated code.
45+
func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (string, error) {
46+
return impl.Generate(doc, specData, cfg)
47+
}
48+
49+
// GenerateRuntime produces standalone Go source files for each of the three
50+
// runtime sub-packages (types, params, helpers). baseImportPath is the base
51+
// import path for the runtime module (e.g., "github.com/org/project/runtime").
52+
func GenerateRuntime(baseImportPath string) (*RuntimeOutput, error) {
53+
return impl.GenerateRuntime(baseImportPath)
54+
}

experimental/internal/codegen/clientgen.go renamed to experimental/codegen/internal/clientgen.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"strings"
77
"text/template"
88

9-
"github.com/oapi-codegen/oapi-codegen-exp/experimental/internal/codegen/templates"
9+
"github.com/oapi-codegen/oapi-codegen-exp/experimental/codegen/internal/templates"
1010
)
1111

1212
// ClientGenerator generates client code from operation descriptors.
@@ -19,8 +19,13 @@ type ClientGenerator struct {
1919

2020
// NewClientGenerator creates a new client generator.
2121
// modelsPackage can be nil if models are in the same package.
22-
func NewClientGenerator(schemaIndex map[string]*SchemaDescriptor, generateSimple bool, modelsPackage *ModelsPackage) (*ClientGenerator, error) {
23-
tmpl := template.New("client").Funcs(templates.Funcs()).Funcs(clientFuncs(schemaIndex, modelsPackage))
22+
// rp holds the package prefixes for runtime sub-packages; all empty when embedded.
23+
func NewClientGenerator(schemaIndex map[string]*SchemaDescriptor, generateSimple bool, modelsPackage *ModelsPackage, rp RuntimePrefixes) (*ClientGenerator, error) {
24+
tmpl := template.New("client").Funcs(templates.Funcs()).Funcs(clientFuncs(schemaIndex, modelsPackage)).Funcs(template.FuncMap{
25+
"runtimeParamsPrefix": func() string { return rp.Params },
26+
"runtimeTypesPrefix": func() string { return rp.Types },
27+
"runtimeHelpersPrefix": func() string { return rp.Helpers },
28+
})
2429

2530
// Parse client templates
2631
for _, ct := range templates.ClientTemplates {

experimental/internal/codegen/clientgen_test.go renamed to experimental/codegen/internal/clientgen_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestClientGenerator(t *testing.T) {
5151
t.Logf("Operations: %v", operationIDs)
5252

5353
// Generate client code
54-
gen, err := NewClientGenerator(schemaIndex, true, nil)
54+
gen, err := NewClientGenerator(schemaIndex, true, nil, RuntimePrefixes{})
5555
require.NoError(t, err, "Failed to create client generator")
5656

5757
clientCode, err := gen.GenerateClient(ops)
@@ -119,23 +119,23 @@ func TestClientGenerator_FormEncoded(t *testing.T) {
119119
require.True(t, hasFormBody, "Expected at least one operation with a form-encoded typed body")
120120

121121
// Generate client code
122-
gen, err := NewClientGenerator(schemaIndex, true, nil)
122+
gen, err := NewClientGenerator(schemaIndex, true, nil, RuntimePrefixes{})
123123
require.NoError(t, err, "Failed to create client generator")
124124

125125
clientCode, err := gen.GenerateClient(ops)
126126
require.NoError(t, err, "Failed to generate client code")
127127

128128
t.Logf("Generated client code:\n%s", clientCode)
129129

130-
// Verify form-encoded body methods reference marshalForm
131-
require.Contains(t, clientCode, "marshalForm(body)")
130+
// Verify form-encoded body methods reference MarshalForm
131+
require.Contains(t, clientCode, "MarshalForm(body)")
132132

133133
// Verify we generate the form helper when needed
134134
ctx.NeedFormHelper(ops)
135135
formHelper, err := generateMarshalFormHelper()
136136
require.NoError(t, err, "Failed to generate form helper")
137137
require.NotEmpty(t, formHelper, "Form helper should be generated when form-encoded bodies exist")
138-
require.Contains(t, formHelper, "func marshalForm(")
138+
require.Contains(t, formHelper, "func MarshalForm(")
139139
require.Contains(t, formHelper, "func marshalFormImpl(")
140140
require.Contains(t, formHelper, "reflect.Value")
141141

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/pb33f/libopenapi"
1010
"github.com/pb33f/libopenapi/datamodel/high/base"
1111

12-
"github.com/oapi-codegen/oapi-codegen-exp/experimental/internal/codegen/templates"
12+
"github.com/oapi-codegen/oapi-codegen-exp/experimental/codegen/internal/templates"
1313
)
1414

1515
// Generate produces Go code from the parsed OpenAPI document.
@@ -20,6 +20,17 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
2020
// Create a single CodegenContext that all generators share.
2121
ctx := NewCodegenContext()
2222

23+
// Configure runtime package prefixes if an external runtime is specified.
24+
var runtimePrefixes RuntimePrefixes
25+
if cfg.Generation.RuntimePackage != nil {
26+
runtimePrefixes = RuntimePrefixes{
27+
Params: "params.",
28+
Types: "types.",
29+
Helpers: "helpers.",
30+
}
31+
ctx.SetRuntimePrefixes(runtimePrefixes.Params, runtimePrefixes.Types, runtimePrefixes.Helpers)
32+
}
33+
2334
// Create content type matcher for filtering request/response bodies
2435
contentTypeMatcher := NewContentTypeMatcher(cfg.ContentTypes)
2536

@@ -102,7 +113,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
102113

103114
ops = FilterOperations(ops, cfg.OutputOptions)
104115

105-
clientGen, err := NewClientGenerator(schemaIndex, cfg.Generation.SimpleClient, cfg.Generation.ModelsPackage)
116+
clientGen, err := NewClientGenerator(schemaIndex, cfg.Generation.SimpleClient, cfg.Generation.ModelsPackage, runtimePrefixes)
106117
if err != nil {
107118
return "", fmt.Errorf("creating client generator: %w", err)
108119
}
@@ -140,7 +151,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
140151
ops = FilterOperations(ops, cfg.OutputOptions)
141152

142153
if len(ops) > 0 {
143-
serverGen, err := NewServerGenerator(cfg.Generation.Server)
154+
serverGen, err := NewServerGenerator(cfg.Generation.Server, runtimePrefixes)
144155
if err != nil {
145156
return "", fmt.Errorf("creating server generator: %w", err)
146157
}
@@ -171,7 +182,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
171182
}
172183

173184
if len(webhookOps) > 0 {
174-
initiatorGen, err := NewInitiatorGenerator("Webhook", schemaIndex, true, cfg.Generation.ModelsPackage)
185+
initiatorGen, err := NewInitiatorGenerator("Webhook", schemaIndex, true, cfg.Generation.ModelsPackage, runtimePrefixes)
175186
if err != nil {
176187
return "", fmt.Errorf("creating webhook initiator generator: %w", err)
177188
}
@@ -202,7 +213,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
202213
}
203214

204215
if len(callbackOps) > 0 {
205-
initiatorGen, err := NewInitiatorGenerator("Callback", schemaIndex, true, cfg.Generation.ModelsPackage)
216+
initiatorGen, err := NewInitiatorGenerator("Callback", schemaIndex, true, cfg.Generation.ModelsPackage, runtimePrefixes)
206217
if err != nil {
207218
return "", fmt.Errorf("creating callback initiator generator: %w", err)
208219
}
@@ -237,7 +248,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
237248
}
238249

239250
if len(webhookOps) > 0 {
240-
receiverGen, err := NewReceiverGenerator("Webhook", cfg.Generation.Server)
251+
receiverGen, err := NewReceiverGenerator("Webhook", cfg.Generation.Server, runtimePrefixes)
241252
if err != nil {
242253
return "", fmt.Errorf("creating webhook receiver generator: %w", err)
243254
}
@@ -288,7 +299,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
288299
}
289300

290301
if len(callbackOps) > 0 {
291-
receiverGen, err := NewReceiverGenerator("Callback", cfg.Generation.Server)
302+
receiverGen, err := NewReceiverGenerator("Callback", cfg.Generation.Server, runtimePrefixes)
292303
if err != nil {
293304
return "", fmt.Errorf("creating callback receiver generator: %w", err)
294305
}
@@ -329,31 +340,47 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
329340

330341
// ── Phase 2: Render helpers ──
331342

332-
// Emit custom type templates (Date, Email, UUID, File, Nullable, etc.)
333-
for _, templateName := range ctx.RequiredCustomTypes() {
334-
typeCode := ctx.loadAndRegisterCustomType(templateName)
335-
if typeCode != "" {
336-
output.AddType(typeCode)
343+
if cfg.Generation.RuntimePackage != nil {
344+
// Runtime package is configured — don't embed helpers, import them.
345+
// Add imports for whichever sub-packages are actually needed.
346+
ctx.AddImport(cfg.Generation.RuntimePackage.TypesImport())
347+
if ctx.HasAnyParams() {
348+
ctx.AddImport(cfg.Generation.RuntimePackage.ParamsImport())
349+
}
350+
if len(ctx.RequiredHelpers()) > 0 {
351+
ctx.AddImport(cfg.Generation.RuntimePackage.HelpersImport())
352+
}
353+
} else {
354+
// Resolve param template dependencies first — this may register
355+
// additional custom types (e.g., Date) that need to be emitted.
356+
ctx.GetRequiredParamTemplates()
357+
358+
// Emit custom type templates (Date, Email, UUID, File, Nullable, etc.)
359+
for _, templateName := range ctx.RequiredCustomTypes() {
360+
typeCode := ctx.loadAndRegisterCustomType(templateName)
361+
if typeCode != "" {
362+
output.AddType(typeCode)
363+
}
337364
}
338-
}
339-
340-
// Emit param functions
341-
paramFuncs, err := generateParamFunctionsFromContext(ctx)
342-
if err != nil {
343-
return "", fmt.Errorf("generating param functions: %w", err)
344-
}
345-
if paramFuncs != "" {
346-
output.AddType(paramFuncs)
347-
}
348365

349-
// Emit helper templates (e.g., marshal_form)
350-
for _, helperName := range ctx.RequiredHelpers() {
351-
helperCode, err := generateHelper(helperName, ctx)
366+
// Emit param functions (typesPrefix is empty when embedded — Date is in same package)
367+
paramFuncs, err := generateParamFunctionsFromContext(ctx, "")
352368
if err != nil {
353-
return "", fmt.Errorf("generating helper %s: %w", helperName, err)
369+
return "", fmt.Errorf("generating param functions: %w", err)
370+
}
371+
if paramFuncs != "" {
372+
output.AddType(paramFuncs)
354373
}
355-
if helperCode != "" {
356-
output.AddType(helperCode)
374+
375+
// Emit helper templates (e.g., marshal_form)
376+
for _, helperName := range ctx.RequiredHelpers() {
377+
helperCode, err := generateHelper(helperName, ctx)
378+
if err != nil {
379+
return "", fmt.Errorf("generating helper %s: %w", helperName, err)
380+
}
381+
if helperCode != "" {
382+
output.AddType(helperCode)
383+
}
357384
}
358385
}
359386

@@ -397,7 +424,8 @@ func generateMarshalFormHelper() (string, error) {
397424
}
398425

399426
// generateParamFunctionsFromContext generates the parameter styling/binding functions based on CodegenContext usage.
400-
func generateParamFunctionsFromContext(ctx *CodegenContext) (string, error) {
427+
// typesPrefix is prepended to Date/DateFormat references in param templates; empty when embedded.
428+
func generateParamFunctionsFromContext(ctx *CodegenContext, typesPrefix string) (string, error) {
401429
if !ctx.HasAnyParams() {
402430
return "", nil
403431
}
@@ -412,7 +440,9 @@ func generateParamFunctionsFromContext(ctx *CodegenContext) (string, error) {
412440
return "", fmt.Errorf("reading param template %s: %w", tmplInfo.Template, err)
413441
}
414442

415-
tmpl, err := template.New(tmplInfo.Name).Parse(string(content))
443+
tmpl, err := template.New(tmplInfo.Name).Funcs(template.FuncMap{
444+
"typesPrefix": func() string { return typesPrefix },
445+
}).Parse(string(content))
416446
if err != nil {
417447
return "", fmt.Errorf("parsing param template %s: %w", tmplInfo.Template, err)
418448
}

0 commit comments

Comments
 (0)