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

Commit 584c77c

Browse files
mromaszewiczclaude
andcommitted
experimental: reduce redundancy in codegen internals
Build V3 model once in Generate() and pass *v3.Document to all gather functions instead of rebuilding it 2-7x per run. Cache gathered operations so client+server and initiator+receiver share a single gather call. Replace hardcoded schemaToGoType with TypeMapping-aware resolveType on operationGatherer so parameter types respect the user type-mapping config. This also fixes an inconsistency where parameters used uuid.UUID (import) while models used the runtime UUID type. Deduplicate InitiatorGenerator and ClientGenerator by extracting generateRequestBodyTypes as a shared function, introducing a loadTemplates helper with templateEntry, and adding RuntimePrefixes.FuncMap() to eliminate repeated inline FuncMap construction across all four generators. Fix pre-existing lint issues: QF1003 tagged switch in issue3_test, ST1023 redundant type declarations in six test files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cc017aa commit 584c77c

17 files changed

Lines changed: 281 additions & 267 deletions

File tree

experimental/codegen/internal/clientgen.go

Lines changed: 117 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,61 @@ import (
66
"strings"
77
"text/template"
88

9+
"github.com/pb33f/libopenapi/datamodel/high/base"
10+
911
"github.com/oapi-codegen/oapi-codegen-exp/experimental/codegen/internal/templates"
1012
)
1113

14+
// templateEntry describes a single template to load.
15+
type templateEntry struct {
16+
Name string // Template name for ExecuteTemplate
17+
Template string // Path within the templates FS (relative to "files/")
18+
}
19+
20+
// loadTemplates parses one or more sets of template entries into the given template.
21+
// Each set is a slice of templateEntry. All entries are loaded in order.
22+
func loadTemplates(tmpl *template.Template, sets ...[]templateEntry) error {
23+
for _, set := range sets {
24+
for _, entry := range set {
25+
content, err := templates.TemplateFS.ReadFile("files/" + entry.Template)
26+
if err != nil {
27+
return fmt.Errorf("reading template %s: %w", entry.Template, err)
28+
}
29+
if _, err := tmpl.New(entry.Name).Parse(string(content)); err != nil {
30+
return fmt.Errorf("parsing template %s: %w", entry.Template, err)
31+
}
32+
}
33+
}
34+
return nil
35+
}
36+
37+
// clientTemplateEntries converts ClientTemplates map to a slice of templateEntry.
38+
func clientTemplateEntries() []templateEntry {
39+
entries := make([]templateEntry, 0, len(templates.ClientTemplates))
40+
for _, ct := range templates.ClientTemplates {
41+
entries = append(entries, templateEntry{Name: ct.Name, Template: ct.Template})
42+
}
43+
return entries
44+
}
45+
46+
// initiatorTemplateEntries converts InitiatorTemplates map to a slice of templateEntry.
47+
func initiatorTemplateEntries() []templateEntry {
48+
entries := make([]templateEntry, 0, len(templates.InitiatorTemplates))
49+
for _, it := range templates.InitiatorTemplates {
50+
entries = append(entries, templateEntry{Name: it.Name, Template: it.Template})
51+
}
52+
return entries
53+
}
54+
55+
// sharedServerTemplateEntries converts SharedServerTemplates map to a slice of templateEntry.
56+
func sharedServerTemplateEntries() []templateEntry {
57+
entries := make([]templateEntry, 0, len(templates.SharedServerTemplates))
58+
for _, st := range templates.SharedServerTemplates {
59+
entries = append(entries, templateEntry{Name: st.Name, Template: st.Template})
60+
}
61+
return entries
62+
}
63+
1264
// ClientGenerator generates client code from operation descriptors.
1365
type ClientGenerator struct {
1466
tmpl *template.Template
@@ -20,35 +72,11 @@ type ClientGenerator struct {
2072
// NewClientGenerator creates a new client generator.
2173
// modelsPackage can be nil if models are in the same package.
2274
// 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-
})
29-
30-
// Parse client templates
31-
for _, ct := range templates.ClientTemplates {
32-
content, err := templates.TemplateFS.ReadFile("files/" + ct.Template)
33-
if err != nil {
34-
return nil, err
35-
}
36-
_, err = tmpl.New(ct.Name).Parse(string(content))
37-
if err != nil {
38-
return nil, err
39-
}
40-
}
75+
func NewClientGenerator(schemaIndex map[string]*SchemaDescriptor, generateSimple bool, modelsPackage *ModelsPackage, rp RuntimePrefixes, typeMapping TypeMapping) (*ClientGenerator, error) {
76+
tmpl := template.New("client").Funcs(templates.Funcs()).Funcs(clientFuncs(schemaIndex, modelsPackage, typeMapping)).Funcs(rp.FuncMap())
4177

42-
// Parse shared templates (param_types is shared with server)
43-
for _, st := range templates.SharedServerTemplates {
44-
content, err := templates.TemplateFS.ReadFile("files/" + st.Template)
45-
if err != nil {
46-
return nil, err
47-
}
48-
_, err = tmpl.New(st.Name).Parse(string(content))
49-
if err != nil {
50-
return nil, err
51-
}
78+
if err := loadTemplates(tmpl, clientTemplateEntries(), sharedServerTemplateEntries()); err != nil {
79+
return nil, err
5280
}
5381

5482
return &ClientGenerator{
@@ -60,7 +88,7 @@ func NewClientGenerator(schemaIndex map[string]*SchemaDescriptor, generateSimple
6088
}
6189

6290
// clientFuncs returns template functions specific to client generation.
63-
func clientFuncs(schemaIndex map[string]*SchemaDescriptor, modelsPackage *ModelsPackage) template.FuncMap {
91+
func clientFuncs(schemaIndex map[string]*SchemaDescriptor, modelsPackage *ModelsPackage, typeMapping TypeMapping) template.FuncMap {
6492
return template.FuncMap{
6593
"pathFmt": pathFmt,
6694
"isSimpleOperation": isSimpleOperation,
@@ -70,7 +98,7 @@ func clientFuncs(schemaIndex map[string]*SchemaDescriptor, modelsPackage *Models
7098
return op.DefaultTypedBody()
7199
},
72100
"goTypeForContent": func(content *ResponseContentDescriptor) string {
73-
return goTypeForContent(content, schemaIndex, modelsPackage)
101+
return goTypeForContent(content, schemaIndex, modelsPackage, typeMapping)
74102
},
75103
"modelsPkg": func() string {
76104
return modelsPackage.Prefix()
@@ -170,7 +198,7 @@ func errorResponseForOperation(op *OperationDescriptor) *ResponseDescriptor {
170198

171199
// goTypeForContent returns the Go type for a response content descriptor.
172200
// If modelsPackage is set, type names are prefixed with the package name.
173-
func goTypeForContent(content *ResponseContentDescriptor, schemaIndex map[string]*SchemaDescriptor, modelsPackage *ModelsPackage) string {
201+
func goTypeForContent(content *ResponseContentDescriptor, schemaIndex map[string]*SchemaDescriptor, modelsPackage *ModelsPackage, typeMapping TypeMapping) string {
174202
if content == nil || content.Schema == nil {
175203
return "any"
176204
}
@@ -205,14 +233,61 @@ func goTypeForContent(content *ResponseContentDescriptor, schemaIndex map[string
205233
return pkgPrefix + content.Schema.StableName
206234
}
207235

208-
// Try to derive from the schema itself
236+
// Try to derive from the schema itself using TypeMapping
209237
if content.Schema.Schema != nil {
210-
return schemaToGoType(content.Schema.Schema)
238+
return resolveSchemaType(content.Schema.Schema, typeMapping)
239+
}
240+
241+
return "any"
242+
}
243+
244+
// resolveSchemaType converts a schema to a Go type string using the provided TypeMapping.
245+
// This is a standalone helper used as a last-resort fallback in goTypeForContent.
246+
func resolveSchemaType(schema *base.Schema, tm TypeMapping) string {
247+
if schema == nil {
248+
return "any"
249+
}
250+
251+
// Check for array
252+
if schema.Items != nil && schema.Items.A != nil {
253+
itemType := "any"
254+
if itemSchema := schema.Items.A.Schema(); itemSchema != nil {
255+
itemType = resolveSchemaType(itemSchema, tm)
256+
}
257+
return "[]" + itemType
258+
}
259+
260+
// Check explicit type
261+
for _, t := range schema.Type {
262+
switch t {
263+
case "string":
264+
return resolveFormatType(tm.String, schema.Format)
265+
case "integer":
266+
return resolveFormatType(tm.Integer, schema.Format)
267+
case "number":
268+
return resolveFormatType(tm.Number, schema.Format)
269+
case "boolean":
270+
return tm.Boolean.Default.Type
271+
case "array":
272+
return "[]any"
273+
case "object":
274+
return "map[string]any"
275+
}
211276
}
212277

213278
return "any"
214279
}
215280

281+
// resolveFormatType looks up a type from a FormatMapping, falling back to its default.
282+
func resolveFormatType(fm FormatMapping, format string) string {
283+
if format != "" {
284+
if spec, ok := fm.Formats[format]; ok {
285+
return spec.Type
286+
}
287+
}
288+
return fm.Default.Type
289+
}
290+
216291
// GenerateBase generates the base client types and helpers.
217292
func (g *ClientGenerator) GenerateBase() (string, error) {
218293
var buf bytes.Buffer
@@ -269,8 +344,14 @@ func (g *ClientGenerator) GenerateParamTypes(ops []*OperationDescriptor) (string
269344

270345
// GenerateRequestBodyTypes generates type aliases for request bodies.
271346
func (g *ClientGenerator) GenerateRequestBodyTypes(ops []*OperationDescriptor) string {
347+
return generateRequestBodyTypes(ops, g.schemaIndex, g.modelsPackage)
348+
}
349+
350+
// generateRequestBodyTypes generates type aliases for request bodies.
351+
// This is shared between ClientGenerator and InitiatorGenerator.
352+
func generateRequestBodyTypes(ops []*OperationDescriptor, schemaIndex map[string]*SchemaDescriptor, modelsPackage *ModelsPackage) string {
272353
var buf bytes.Buffer
273-
pkgPrefix := g.modelsPackage.Prefix()
354+
pkgPrefix := modelsPackage.Prefix()
274355

275356
for _, op := range ops {
276357
for _, body := range op.Bodies {
@@ -282,7 +363,7 @@ func (g *ClientGenerator) GenerateRequestBodyTypes(ops []*OperationDescriptor) s
282363
if body.Schema != nil {
283364
if body.Schema.Ref != "" {
284365
// Reference to a component schema
285-
if target, ok := g.schemaIndex[body.Schema.Ref]; ok {
366+
if target, ok := schemaIndex[body.Schema.Ref]; ok {
286367
targetType = pkgPrefix + target.ShortName
287368
}
288369
} else if body.Schema.ShortName != "" {
@@ -293,8 +374,7 @@ func (g *ClientGenerator) GenerateRequestBodyTypes(ops []*OperationDescriptor) s
293374
targetType = "any"
294375
}
295376

296-
// Generate type alias: type addPetJSONRequestBody = models.NewPet
297-
buf.WriteString(fmt.Sprintf("type %s = %s\n\n", body.GoTypeName, targetType))
377+
fmt.Fprintf(&buf, "type %s = %s\n\n", body.GoTypeName, targetType)
298378
}
299379
}
300380

experimental/codegen/internal/clientgen_test.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ func TestClientGenerator(t *testing.T) {
1414
specData, err := os.ReadFile(specPath)
1515
require.NoError(t, err, "Failed to read petstore spec")
1616

17-
// Parse the spec
17+
// Parse the spec and build model once
1818
doc, err := libopenapi.NewDocument(specData)
1919
require.NoError(t, err, "Failed to parse petstore spec")
20+
model, err := doc.BuildV3Model()
21+
require.NoError(t, err, "Failed to build v3 model")
22+
v3Doc := &model.Model
2023

2124
// Gather schemas to build schema index
2225
contentTypeMatcher := NewContentTypeMatcher(DefaultContentTypes())
23-
schemas, err := GatherSchemas(doc, contentTypeMatcher, OutputOptions{})
26+
schemas, err := GatherSchemas(v3Doc, contentTypeMatcher, OutputOptions{})
2427
require.NoError(t, err, "Failed to gather schemas")
2528

2629
// Compute names for schemas
@@ -38,7 +41,7 @@ func TestClientGenerator(t *testing.T) {
3841
ctx := NewCodegenContext()
3942

4043
// Gather operations
41-
ops, err := GatherOperations(doc, ctx, NewContentTypeMatcher(DefaultContentTypes()))
44+
ops, err := GatherOperations(v3Doc, ctx, NewContentTypeMatcher(DefaultContentTypes()), DefaultTypeMapping)
4245
require.NoError(t, err, "Failed to gather operations")
4346
require.Len(t, ops, 4, "Expected 4 operations")
4447

@@ -51,7 +54,7 @@ func TestClientGenerator(t *testing.T) {
5154
t.Logf("Operations: %v", operationIDs)
5255

5356
// Generate client code
54-
gen, err := NewClientGenerator(schemaIndex, true, nil, RuntimePrefixes{})
57+
gen, err := NewClientGenerator(schemaIndex, true, nil, RuntimePrefixes{}, DefaultTypeMapping)
5558
require.NoError(t, err, "Failed to create client generator")
5659

5760
clientCode, err := gen.GenerateClient(ops)
@@ -88,9 +91,12 @@ func TestClientGenerator_FormEncoded(t *testing.T) {
8891

8992
doc, err := libopenapi.NewDocument(specData)
9093
require.NoError(t, err, "Failed to parse comprehensive spec")
94+
model, err := doc.BuildV3Model()
95+
require.NoError(t, err, "Failed to build v3 model")
96+
v3Doc := &model.Model
9197

9298
contentTypeMatcher := NewContentTypeMatcher(DefaultContentTypes())
93-
schemas, err := GatherSchemas(doc, contentTypeMatcher, OutputOptions{})
99+
schemas, err := GatherSchemas(v3Doc, contentTypeMatcher, OutputOptions{})
94100
require.NoError(t, err, "Failed to gather schemas")
95101

96102
converter := NewNameConverter(NameMangling{}, NameSubstitutions{})
@@ -103,7 +109,7 @@ func TestClientGenerator_FormEncoded(t *testing.T) {
103109
}
104110

105111
ctx := NewCodegenContext()
106-
ops, err := GatherOperations(doc, ctx, contentTypeMatcher)
112+
ops, err := GatherOperations(v3Doc, ctx, contentTypeMatcher, DefaultTypeMapping)
107113
require.NoError(t, err, "Failed to gather operations")
108114

109115
// Verify we have an operation with a form-encoded body
@@ -119,7 +125,7 @@ func TestClientGenerator_FormEncoded(t *testing.T) {
119125
require.True(t, hasFormBody, "Expected at least one operation with a form-encoded typed body")
120126

121127
// Generate client code
122-
gen, err := NewClientGenerator(schemaIndex, true, nil, RuntimePrefixes{})
128+
gen, err := NewClientGenerator(schemaIndex, true, nil, RuntimePrefixes{}, DefaultTypeMapping)
123129
require.NoError(t, err, "Failed to create client generator")
124130

125131
clientCode, err := gen.GenerateClient(ops)

0 commit comments

Comments
 (0)