Skip to content
This repository was archived by the owner on Apr 14, 2026. It is now read-only.
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
10 changes: 9 additions & 1 deletion experimental/internal/codegen/clientgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ func clientFuncs(schemaIndex map[string]*SchemaDescriptor, modelsPackage *Models
"isSimpleOperation": isSimpleOperation,
"simpleOperationSuccessResponse": simpleOperationSuccessResponse,
"errorResponseForOperation": errorResponseForOperation,
"defaultTypedBody": func(op *OperationDescriptor) *RequestBodyDescriptor {
return op.DefaultTypedBody()
},
"goTypeForContent": func(content *ResponseContentDescriptor) string {
return goTypeForContent(content, schemaIndex, modelsPackage)
},
Expand Down Expand Up @@ -96,6 +99,11 @@ func isSimpleOperation(op *OperationDescriptor) bool {
return false
}

// If the operation has a body, it must have a typed body for the simple client
if op.HasBody && !op.HasTypedBody() {
return false
}

// Count success responses (2xx or default that could be success)
var successResponses []*ResponseDescriptor
for _, r := range op.Responses {
Expand Down Expand Up @@ -261,7 +269,7 @@ func (g *ClientGenerator) GenerateRequestBodyTypes(ops []*OperationDescriptor) s

for _, op := range ops {
for _, body := range op.Bodies {
if !body.IsJSON {
if !body.GenerateTyped {
continue
}
// Get the underlying type for this request body
Expand Down
64 changes: 63 additions & 1 deletion experimental/internal/codegen/clientgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestClientGenerator(t *testing.T) {
paramTracker := NewParamUsageTracker()

// Gather operations
ops, err := GatherOperations(doc, paramTracker)
ops, err := GatherOperations(doc, paramTracker, NewContentTypeMatcher(DefaultContentTypes()))
require.NoError(t, err, "Failed to gather operations")
require.Len(t, ops, 4, "Expected 4 operations")

Expand Down Expand Up @@ -80,6 +80,68 @@ func TestClientGenerator(t *testing.T) {
require.Contains(t, clientCode, "NewSimpleClient")
}

func TestClientGenerator_FormEncoded(t *testing.T) {
// Read the comprehensive spec which includes form-encoded bodies
specPath := "test/files/comprehensive.yaml"
specData, err := os.ReadFile(specPath)
require.NoError(t, err, "Failed to read comprehensive spec")

doc, err := libopenapi.NewDocument(specData)
require.NoError(t, err, "Failed to parse comprehensive spec")

contentTypeMatcher := NewContentTypeMatcher(DefaultContentTypes())
schemas, err := GatherSchemas(doc, contentTypeMatcher)
require.NoError(t, err, "Failed to gather schemas")

converter := NewNameConverter(NameMangling{}, NameSubstitutions{})
contentTypeNamer := NewContentTypeShortNamer(DefaultContentTypeShortNames())
ComputeSchemaNames(schemas, converter, contentTypeNamer)

schemaIndex := make(map[string]*SchemaDescriptor)
for _, s := range schemas {
schemaIndex[s.Path.String()] = s
}

paramTracker := NewParamUsageTracker()
ops, err := GatherOperations(doc, paramTracker, contentTypeMatcher)
require.NoError(t, err, "Failed to gather operations")

// Verify we have an operation with a form-encoded body
var hasFormBody bool
for _, op := range ops {
for _, body := range op.Bodies {
if body.IsFormEncoded && body.GenerateTyped {
hasFormBody = true
break
}
}
}
require.True(t, hasFormBody, "Expected at least one operation with a form-encoded typed body")

// Generate client code
gen, err := NewClientGenerator(schemaIndex, true, nil)
require.NoError(t, err, "Failed to create client generator")

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

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

// Verify form-encoded body methods reference marshalForm
require.Contains(t, clientCode, "marshalForm(body)")

// Verify we generate the form helper when needed
formHelper, err := generateFormHelper(ops)
require.NoError(t, err, "Failed to generate form helper")
require.NotEmpty(t, formHelper, "Form helper should be generated when form-encoded bodies exist")
require.Contains(t, formHelper, "func marshalForm(")
require.Contains(t, formHelper, "func marshalFormImpl(")
require.Contains(t, formHelper, "reflect.Value")

// Verify it generates WithFormdataBody method
require.Contains(t, clientCode, "WithFormdataBody")
}

func TestIsSimpleOperation(t *testing.T) {
tests := []struct {
name string
Expand Down
85 changes: 79 additions & 6 deletions experimental/internal/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
paramTracker := NewParamUsageTracker()

// Gather operations
ops, err := GatherOperations(doc, paramTracker)
ops, err := GatherOperations(doc, paramTracker, contentTypeMatcher)
if err != nil {
return "", fmt.Errorf("gathering operations: %w", err)
}
Expand Down Expand Up @@ -145,6 +145,18 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
for _, imp := range paramTracker.GetRequiredImports() {
output.AddImport(imp.Path, imp.Alias)
}

// Generate form helper if any operation has form-encoded bodies
formHelper, err := generateFormHelper(ops)
if err != nil {
return "", fmt.Errorf("generating form helper: %w", err)
}
if formHelper != "" {
output.AddType(formHelper)
for _, imp := range templates.MarshalFormHelperTemplate.Imports {
output.AddImport(imp.Path, imp.Alias)
}
}
}

// Track whether shared error types have been generated to avoid duplication.
Expand All @@ -159,7 +171,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
paramTracker := NewParamUsageTracker()

// Gather operations
ops, err := GatherOperations(doc, paramTracker)
ops, err := GatherOperations(doc, paramTracker, contentTypeMatcher)
if err != nil {
return "", fmt.Errorf("gathering operations: %w", err)
}
Expand Down Expand Up @@ -216,7 +228,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
if cfg.Generation.WebhookInitiator {
paramTracker := NewParamUsageTracker()

webhookOps, err := GatherWebhookOperations(doc, paramTracker)
webhookOps, err := GatherWebhookOperations(doc, paramTracker, contentTypeMatcher)
if err != nil {
return "", fmt.Errorf("gathering webhook operations: %w", err)
}
Expand Down Expand Up @@ -253,14 +265,26 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
for _, imp := range paramTracker.GetRequiredImports() {
output.AddImport(imp.Path, imp.Alias)
}

// Generate form helper if any webhook operation has form-encoded bodies
formHelper, err := generateFormHelper(webhookOps)
if err != nil {
return "", fmt.Errorf("generating form helper: %w", err)
}
if formHelper != "" {
output.AddType(formHelper)
for _, imp := range templates.MarshalFormHelperTemplate.Imports {
output.AddImport(imp.Path, imp.Alias)
}
}
}
}

// Generate callback initiator code if requested
if cfg.Generation.CallbackInitiator {
paramTracker := NewParamUsageTracker()

callbackOps, err := GatherCallbackOperations(doc, paramTracker)
callbackOps, err := GatherCallbackOperations(doc, paramTracker, contentTypeMatcher)
if err != nil {
return "", fmt.Errorf("gathering callback operations: %w", err)
}
Expand Down Expand Up @@ -297,6 +321,18 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
for _, imp := range paramTracker.GetRequiredImports() {
output.AddImport(imp.Path, imp.Alias)
}

// Generate form helper if any callback operation has form-encoded bodies
formHelper, err := generateFormHelper(callbackOps)
if err != nil {
return "", fmt.Errorf("generating form helper: %w", err)
}
if formHelper != "" {
output.AddType(formHelper)
for _, imp := range templates.MarshalFormHelperTemplate.Imports {
output.AddImport(imp.Path, imp.Alias)
}
}
}
}

Expand All @@ -308,7 +344,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri

paramTracker := NewParamUsageTracker()

webhookOps, err := GatherWebhookOperations(doc, paramTracker)
webhookOps, err := GatherWebhookOperations(doc, paramTracker, contentTypeMatcher)
if err != nil {
return "", fmt.Errorf("gathering webhook operations: %w", err)
}
Expand Down Expand Up @@ -378,7 +414,7 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri

paramTracker := NewParamUsageTracker()

callbackOps, err := GatherCallbackOperations(doc, paramTracker)
callbackOps, err := GatherCallbackOperations(doc, paramTracker, contentTypeMatcher)
if err != nil {
return "", fmt.Errorf("gathering callback operations: %w", err)
}
Expand Down Expand Up @@ -442,6 +478,43 @@ func Generate(doc libopenapi.Document, specData []byte, cfg Configuration) (stri
return output.Format()
}

// hasFormEncodedBodies returns true if any operation has a form-encoded typed request body.
func hasFormEncodedBodies(ops []*OperationDescriptor) bool {
for _, op := range ops {
for _, body := range op.Bodies {
if body.IsFormEncoded && body.GenerateTyped {
return true
}
}
}
return false
}

// generateFormHelper generates the marshalForm helper function if needed.
func generateFormHelper(ops []*OperationDescriptor) (string, error) {
if !hasFormEncodedBodies(ops) {
return "", nil
}

tmplInfo := templates.MarshalFormHelperTemplate
content, err := templates.TemplateFS.ReadFile("files/" + tmplInfo.Template)
if err != nil {
return "", fmt.Errorf("reading form helper template: %w", err)
}

tmpl, err := template.New(tmplInfo.Name).Parse(string(content))
if err != nil {
return "", fmt.Errorf("parsing form helper template: %w", err)
}

var result strings.Builder
if err := tmpl.Execute(&result, nil); err != nil {
return "", fmt.Errorf("executing form helper template: %w", err)
}

return result.String(), nil
}

// generateParamFunctionsFromTracker generates the parameter styling/binding functions based on usage.
func generateParamFunctionsFromTracker(tracker *ParamUsageTracker) (string, error) {
if !tracker.HasAnyUsage() {
Expand Down
1 change: 1 addition & 0 deletions experimental/internal/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ func DefaultContentTypes() []string {
return []string{
`^application/json$`,
`^application/.*\+json$`,
`^application/x-www-form-urlencoded$`,
}
}

Expand Down
2 changes: 1 addition & 1 deletion experimental/internal/codegen/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func gatherTestOps(t *testing.T) []*OperationDescriptor {
require.NoError(t, err)

tracker := NewParamUsageTracker()
ops, err := GatherOperations(doc, tracker)
ops, err := GatherOperations(doc, tracker, NewContentTypeMatcher(DefaultContentTypes()))
require.NoError(t, err)
return ops
}
Expand Down
35 changes: 23 additions & 12 deletions experimental/internal/codegen/gather_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (
)

// GatherOperations traverses an OpenAPI document and collects all operations.
func GatherOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker) ([]*OperationDescriptor, error) {
// contentTypeMatcher determines which content types get typed request body methods.
func GatherOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker, contentTypeMatcher *ContentTypeMatcher) ([]*OperationDescriptor, error) {
model, err := doc.BuildV3Model()
if err != nil {
return nil, fmt.Errorf("building v3 model: %w", err)
Expand All @@ -21,14 +22,16 @@ func GatherOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker)
}

g := &operationGatherer{
paramTracker: paramTracker,
paramTracker: paramTracker,
contentTypeMatcher: contentTypeMatcher,
}

return g.gatherFromDocument(&model.Model)
}

type operationGatherer struct {
paramTracker *ParamUsageTracker
paramTracker *ParamUsageTracker
contentTypeMatcher *ContentTypeMatcher
}

func (g *operationGatherer) gatherFromDocument(doc *v3.Document) ([]*OperationDescriptor, error) {
Expand Down Expand Up @@ -317,16 +320,22 @@ func (g *operationGatherer) gatherRequestBodies(operationID string, bodyRef *v3.
bodyRequired = *bodyRef.Required
}

generateTyped := false
if g.contentTypeMatcher != nil {
generateTyped = g.contentTypeMatcher.Matches(contentType)
}

desc := &RequestBodyDescriptor{
ContentType: contentType,
Required: bodyRequired,
Schema: schemaDesc,

NameTag: nameTag,
GoTypeName: goTypeName,
FuncSuffix: funcSuffix,
IsDefault: isDefault,
IsJSON: IsMediaTypeJSON(contentType),
NameTag: nameTag,
GoTypeName: goTypeName,
FuncSuffix: funcSuffix,
IsDefault: isDefault,
IsFormEncoded: contentType == "application/x-www-form-urlencoded",
GenerateTyped: generateTyped,
}

// Gather encoding options for form data
Expand Down Expand Up @@ -568,7 +577,7 @@ func sortPathParamsByPath(path string, params []*ParameterDescriptor) ([]*Parame
}

// GatherWebhookOperations traverses an OpenAPI document and collects operations from webhooks.
func GatherWebhookOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker) ([]*OperationDescriptor, error) {
func GatherWebhookOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker, contentTypeMatcher *ContentTypeMatcher) ([]*OperationDescriptor, error) {
model, err := doc.BuildV3Model()
if err != nil {
return nil, fmt.Errorf("building v3 model: %w", err)
Expand All @@ -578,14 +587,15 @@ func GatherWebhookOperations(doc libopenapi.Document, paramTracker *ParamUsageTr
}

g := &operationGatherer{
paramTracker: paramTracker,
paramTracker: paramTracker,
contentTypeMatcher: contentTypeMatcher,
}

return g.gatherWebhooks(&model.Model)
}

// GatherCallbackOperations traverses an OpenAPI document and collects operations from callbacks.
func GatherCallbackOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker) ([]*OperationDescriptor, error) {
func GatherCallbackOperations(doc libopenapi.Document, paramTracker *ParamUsageTracker, contentTypeMatcher *ContentTypeMatcher) ([]*OperationDescriptor, error) {
model, err := doc.BuildV3Model()
if err != nil {
return nil, fmt.Errorf("building v3 model: %w", err)
Expand All @@ -595,7 +605,8 @@ func GatherCallbackOperations(doc libopenapi.Document, paramTracker *ParamUsageT
}

g := &operationGatherer{
paramTracker: paramTracker,
paramTracker: paramTracker,
contentTypeMatcher: contentTypeMatcher,
}

return g.gatherCallbacks(&model.Model)
Expand Down
2 changes: 1 addition & 1 deletion experimental/internal/codegen/initiatorgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (g *InitiatorGenerator) GenerateRequestBodyTypes(ops []*OperationDescriptor

for _, op := range ops {
for _, body := range op.Bodies {
if !body.IsJSON {
if !body.GenerateTyped {
continue
}
var targetType string
Expand Down
Loading