Skip to content

Commit f8484b5

Browse files
committed
fix(gin): address more code review feedback from PR #13
- Add lazy initialization in gin.Extractor to handle zero-value usage - Fix nil-deref in schema_extractor when SchemaRef.Value is nil for custom types - Fix binding tag required check to handle multiple comma-separated rules - Add proper support for ShouldBindQuery (BindingSourceQuery) - Separate FormParams and FileParams from QueryParams - Add BindingSource type to track binding source (json, xml, yaml, query, form, multipart) - Update generator to handle form/file uploads with correct content types - Update tests to check new FormParams and FileParams fields Signed-off-by: spencercjh <spencercjh@gmail.com>
1 parent e8a58ef commit f8484b5

6 files changed

Lines changed: 159 additions & 37 deletions

File tree

internal/extractor/gin/generator.go

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -224,25 +224,8 @@ func (g *Generator) buildOperation(route *Route, handlerInfo *HandlerInfo, schem
224224
})
225225
}
226226

227-
// Request body
228-
if handlerInfo.BodyType != "" && handlerInfo.BodyType != ginHType {
229-
var schemaRef *openapi3.SchemaRef
230-
if _, exists := schemas[handlerInfo.BodyType]; exists {
231-
schemaRef = &openapi3.SchemaRef{Ref: "#/components/schemas/" + handlerInfo.BodyType}
232-
} else {
233-
// Fallback to generic object if schema not found
234-
schemaRef = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}}}
235-
}
236-
operation.RequestBody = &openapi3.RequestBodyRef{
237-
Value: &openapi3.RequestBody{
238-
Content: openapi3.Content{
239-
"application/json": {
240-
Schema: schemaRef,
241-
},
242-
},
243-
},
244-
}
245-
}
227+
// Handle request body / form parameters based on binding source
228+
g.buildRequestBody(operation, handlerInfo, schemas)
246229

247230
// Responses
248231
operation.Responses = openapi3.NewResponses()
@@ -289,6 +272,89 @@ func (g *Generator) buildOperation(route *Route, handlerInfo *HandlerInfo, schem
289272
return operation
290273
}
291274

275+
// buildRequestBody builds the request body or form parameters based on binding source.
276+
func (g *Generator) buildRequestBody(operation *openapi3.Operation, handlerInfo *HandlerInfo, schemas openapi3.Schemas) {
277+
// Handle form parameters (application/x-www-form-urlencoded)
278+
if len(handlerInfo.FormParams) > 0 {
279+
for _, param := range handlerInfo.FormParams {
280+
operation.Parameters = append(operation.Parameters, &openapi3.ParameterRef{
281+
Value: &openapi3.Parameter{
282+
Name: param.Name,
283+
In: "query",
284+
Required: param.Required,
285+
Description: "Form parameter",
286+
Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}},
287+
},
288+
})
289+
}
290+
}
291+
292+
// Handle file upload parameters (multipart/form-data)
293+
if len(handlerInfo.FileParams) > 0 {
294+
content := make(openapi3.Content)
295+
schema := &openapi3.Schema{
296+
Type: &openapi3.Types{"object"},
297+
Properties: make(openapi3.Schemas),
298+
}
299+
for _, param := range handlerInfo.FileParams {
300+
schema.Properties[param.Name] = &openapi3.SchemaRef{
301+
Value: &openapi3.Schema{
302+
Type: &openapi3.Types{"string"},
303+
Format: "binary",
304+
},
305+
}
306+
if param.Required {
307+
schema.Required = append(schema.Required, param.Name)
308+
}
309+
}
310+
content["multipart/form-data"] = &openapi3.MediaType{Schema: &openapi3.SchemaRef{Value: schema}}
311+
operation.RequestBody = &openapi3.RequestBodyRef{
312+
Value: &openapi3.RequestBody{
313+
Required: true,
314+
Content: content,
315+
},
316+
}
317+
return
318+
}
319+
320+
// Handle body binding based on source
321+
if handlerInfo.BodyType == "" || handlerInfo.BodyType == ginHType {
322+
return
323+
}
324+
325+
var schemaRef *openapi3.SchemaRef
326+
if _, exists := schemas[handlerInfo.BodyType]; exists {
327+
schemaRef = &openapi3.SchemaRef{Ref: "#/components/schemas/" + handlerInfo.BodyType}
328+
} else {
329+
// Fallback to generic object if schema not found
330+
schemaRef = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}}}
331+
}
332+
333+
// Determine content type based on binding source
334+
contentType := "application/json"
335+
switch handlerInfo.BindingSrc {
336+
case BindingSourceXML:
337+
contentType = "application/xml"
338+
case BindingSourceYAML:
339+
contentType = "application/x-yaml"
340+
case BindingSourceQuery:
341+
// Query binding - don't create request body, parameters are in query
342+
return
343+
case BindingSourceForm:
344+
contentType = "application/x-www-form-urlencoded"
345+
}
346+
347+
operation.RequestBody = &openapi3.RequestBodyRef{
348+
Value: &openapi3.RequestBody{
349+
Content: openapi3.Content{
350+
contentType: {
351+
Schema: schemaRef,
352+
},
353+
},
354+
},
355+
}
356+
}
357+
292358
// setOperationForMethod sets the operation for the given HTTP method.
293359
func setOperationForMethod(pathItem *openapi3.PathItem, method string, operation *openapi3.Operation) {
294360
switch method {

internal/extractor/gin/gin.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ type Extractor struct {
2020
generator *Generator
2121
}
2222

23+
// ensureInitialized lazily initializes the extractor fields if needed.
24+
// This handles the case where the extractor is used as a zero-value.
25+
func (e *Extractor) ensureInitialized() {
26+
if e.detector == nil {
27+
e.detector = NewDetector()
28+
}
29+
if e.patcher == nil {
30+
e.patcher = NewPatcher()
31+
}
32+
if e.generator == nil {
33+
e.generator = NewGenerator()
34+
}
35+
}
36+
2337
// NewExtractor creates a new Extractor instance.
2438
func NewExtractor() *Extractor {
2539
return &Extractor{
@@ -36,11 +50,13 @@ func (e *Extractor) Name() string {
3650

3751
// Detect implements extractor.Extractor.Detect.
3852
func (e *Extractor) Detect(projectPath string) (*extractor.ProjectInfo, error) {
53+
e.ensureInitialized()
3954
return e.detector.Detect(projectPath)
4055
}
4156

4257
// Patch implements extractor.Extractor.Patch.
4358
func (e *Extractor) Patch(projectPath string, opts *extractor.PatchOptions) (*extractor.PatchResult, error) {
59+
e.ensureInitialized()
4460
// Gin projects don't need patching
4561
info := &extractor.ProjectInfo{
4662
Framework: ExtractorName,
@@ -52,6 +68,7 @@ func (e *Extractor) Patch(projectPath string, opts *extractor.PatchOptions) (*ex
5268

5369
// Generate implements extractor.Extractor.Generate.
5470
func (e *Extractor) Generate(ctx context.Context, projectPath string, info *extractor.ProjectInfo, opts *extractor.GenerateOptions) (*extractor.GenerateResult, error) {
71+
e.ensureInitialized()
5572
return e.generator.Generate(ctx, projectPath, info, opts)
5673
}
5774

internal/extractor/gin/handler_analyzer.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ func (a *HandlerAnalyzer) AnalyzeHandler(fn *ast.FuncDecl) (*HandlerInfo, error)
3131
PathParams: []ParamInfo{},
3232
QueryParams: []ParamInfo{},
3333
HeaderParams: []ParamInfo{},
34+
FormParams: []ParamInfo{},
35+
FileParams: []ParamInfo{},
3436
Responses: []ResponseInfo{},
3537
}
3638

@@ -184,13 +186,14 @@ func (a *HandlerAnalyzer) parseHandlerCall(call *ast.CallExpr, info *HandlerInfo
184186
case "GetHeader":
185187
a.extractHeaderParam(call, info)
186188
case "ShouldBindJSON", "BindJSON", "ShouldBind", "Bind":
187-
a.extractBodyType(call, info, varTypeMap)
189+
a.extractBodyType(call, info, varTypeMap, BindingSourceJSON)
188190
case "ShouldBindXML", "BindXML":
189-
a.extractBodyType(call, info, varTypeMap)
191+
a.extractBodyType(call, info, varTypeMap, BindingSourceXML)
190192
case "ShouldBindYAML", "BindYAML":
191-
a.extractBodyType(call, info, varTypeMap)
193+
a.extractBodyType(call, info, varTypeMap, BindingSourceYAML)
192194
case "ShouldBindQuery", "BindQuery":
193-
a.extractBodyType(call, info, varTypeMap)
195+
// Query binding extracts query parameters, not body
196+
a.extractQueryBinding(call, info, varTypeMap)
194197
case "PostForm":
195198
a.extractFormParam(call, info, true)
196199
case "DefaultPostForm":
@@ -251,13 +254,27 @@ func (a *HandlerAnalyzer) extractHeaderParam(call *ast.CallExpr, info *HandlerIn
251254
}
252255

253256
// extractBodyType extracts body type from binding calls like c.ShouldBindJSON().
254-
func (a *HandlerAnalyzer) extractBodyType(call *ast.CallExpr, info *HandlerInfo, varTypeMap map[string]string) {
257+
func (a *HandlerAnalyzer) extractBodyType(call *ast.CallExpr, info *HandlerInfo, varTypeMap map[string]string, source BindingSource) {
255258
if len(call.Args) < 1 {
256259
return
257260
}
258261
if typeName := extractTypeFromArg(call.Args[0], varTypeMap); typeName != "" {
259262
info.BodyType = typeName
260-
slog.Debug("Extracted body type", "type", typeName)
263+
info.BindingSrc = source
264+
slog.Debug("Extracted body type", "type", typeName, "source", source)
265+
}
266+
}
267+
268+
// extractQueryBinding handles ShouldBindQuery by extracting query parameters from struct tags.
269+
// For now, we track that this is a query binding so the generator can handle it properly.
270+
func (a *HandlerAnalyzer) extractQueryBinding(call *ast.CallExpr, info *HandlerInfo, varTypeMap map[string]string) {
271+
if len(call.Args) < 1 {
272+
return
273+
}
274+
if typeName := extractTypeFromArg(call.Args[0], varTypeMap); typeName != "" {
275+
info.BodyType = typeName
276+
info.BindingSrc = BindingSourceQuery
277+
slog.Debug("Extracted query binding type", "type", typeName)
261278
}
262279
}
263280

@@ -267,7 +284,7 @@ func (a *HandlerAnalyzer) extractFormParam(call *ast.CallExpr, info *HandlerInfo
267284
return
268285
}
269286
if name := extractStringLiteral(call.Args[0]); name != "" {
270-
info.QueryParams = append(info.QueryParams, ParamInfo{
287+
info.FormParams = append(info.FormParams, ParamInfo{
271288
Name: name,
272289
GoType: "string",
273290
Required: required,
@@ -281,7 +298,7 @@ func (a *HandlerAnalyzer) extractFileParam(call *ast.CallExpr, info *HandlerInfo
281298
return
282299
}
283300
if name := extractStringLiteral(call.Args[0]); name != "" {
284-
info.QueryParams = append(info.QueryParams, ParamInfo{
301+
info.FileParams = append(info.FileParams, ParamInfo{
285302
Name: name,
286303
GoType: "file",
287304
Required: true,

internal/extractor/gin/handler_analyzer_more_test.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,25 +130,29 @@ func uploadFile(c *gin.Context) {
130130

131131
info, _ := analyzer.AnalyzeHandler(handlerDecl)
132132

133-
// Check for form parameters
133+
// Check for file parameter in FileParams
134134
foundFile := false
135-
foundUserID := false
136-
for _, param := range info.QueryParams {
135+
for _, param := range info.FileParams {
137136
if param.Name == "file" {
138137
foundFile = true
139138
if param.GoType != "file" {
140139
t.Errorf("expected file type for 'file', got '%s'", param.GoType)
141140
}
142141
}
142+
}
143+
if !foundFile {
144+
t.Error("expected 'file' parameter in FileParams")
145+
}
146+
147+
// Check for form parameter in FormParams
148+
foundUserID := false
149+
for _, param := range info.FormParams {
143150
if param.Name == "userId" {
144151
foundUserID = true
145152
}
146153
}
147-
if !foundFile {
148-
t.Error("expected 'file' parameter")
149-
}
150154
if !foundUserID {
151-
t.Error("expected 'userId' parameter")
155+
t.Error("expected 'userId' parameter in FormParams")
152156
}
153157
}
154158

internal/extractor/gin/schema_extractor.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ func (e *SchemaExtractor) extractStructSchema(typeSpec *ast.TypeSpec) (*openapi3
106106
// Parse struct tags
107107
if field.Tag != nil {
108108
tag := strings.Trim(field.Tag.Value, "`")
109-
propertyName = e.applyTags(fieldSchemaRef.Value, tag, fieldName, schema)
109+
// Guard against nil Value when field is a $ref to custom type
110+
if fieldSchemaRef.Value != nil {
111+
propertyName = e.applyTags(fieldSchemaRef.Value, tag, fieldName, schema)
112+
}
110113
}
111114

112115
// Skip fields with json:"-"
@@ -260,9 +263,9 @@ func (e *SchemaExtractor) applyTags(schema *openapi3.Schema, tag, fieldName stri
260263
return propertyName
261264
}
262265

263-
// Parse binding tag
266+
// Parse binding tag (handle multiple comma-separated rules like "required,email")
264267
if bindingTag := extractTagValue(tag, "binding"); bindingTag != "" {
265-
if bindingTag == "required" {
268+
if strings.Contains(bindingTag, "required") {
266269
parentSchema.Required = append(parentSchema.Required, propertyName)
267270
}
268271
}

internal/extractor/gin/types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,27 @@ type Route struct {
2828
Middlewares []string // Middleware names
2929
}
3030

31+
// BindingSource indicates the source of request binding.
32+
type BindingSource string
33+
34+
const (
35+
BindingSourceJSON BindingSource = "json" // application/json
36+
BindingSourceXML BindingSource = "xml" // application/xml
37+
BindingSourceYAML BindingSource = "yaml" // application/x-yaml
38+
BindingSourceQuery BindingSource = "query" // query parameters
39+
BindingSourceForm BindingSource = "form" // application/x-www-form-urlencoded
40+
BindingSourceMultipart BindingSource = "multipart" // multipart/form-data
41+
)
42+
3143
// HandlerInfo contains information extracted from a handler function.
3244
type HandlerInfo struct {
3345
PathParams []ParamInfo // Path parameters (c.Param)
3446
QueryParams []ParamInfo // Query parameters (c.Query)
3547
HeaderParams []ParamInfo // Header parameters (c.GetHeader)
48+
FormParams []ParamInfo // Form parameters (c.PostForm)
49+
FileParams []ParamInfo // File upload parameters (c.FormFile)
3650
BodyType string // Request body type (from ShouldBindJSON)
51+
BindingSrc BindingSource // Source of binding (json, xml, query, form, etc.)
3752
Responses []ResponseInfo // Response info (from c.JSON calls)
3853
}
3954

0 commit comments

Comments
 (0)