• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pace / bricks / 12827718001

17 Jan 2025 10:57AM UTC coverage: 56.909% (-0.3%) from 57.237%
12827718001

Pull #384

github

monstermunchkin
Satisfy linters
Pull Request #384: Extend linting

478 of 946 new or added lines in 109 files covered. (50.53%)

133 existing lines in 53 files now uncovered.

5667 of 9958 relevant lines covered (56.91%)

21.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.97
/http/jsonapi/generator/generate_handler.go
1
// Copyright © 2018 by PACE Telematics GmbH. All rights reserved.
2

3
package generator
4

5
import (
6
        "fmt"
7
        "net/url"
8
        "path/filepath"
9
        "regexp"
10
        "sort"
11
        "strconv"
12
        "strings"
13

14
        "github.com/dave/jennifer/jen"
15
        "github.com/getkin/kin-openapi/openapi3"
16
        "golang.org/x/text/cases"
17
        "golang.org/x/text/language"
18

19
        "github.com/pace/bricks/maintenance/log"
20
)
21

22
const (
23
        pkgGorillaMux     = "github.com/gorilla/mux"
24
        pkgJSONAPIRuntime = "github.com/pace/bricks/http/jsonapi/runtime"
25
        pkgJSONAPIMetrics = "github.com/pace/bricks/maintenance/metric/jsonapi"
26
        pkgMaintErrors    = "github.com/pace/bricks/maintenance/errors"
27
        pkgSentry         = "github.com/getsentry/sentry-go"
28
        pkgOAuth2         = "github.com/pace/bricks/http/oauth2"
29
        pkgOIDC           = "github.com/pace/bricks/http/oidc"
30
        pkgAPIKey         = "github.com/pace/bricks/http/security/apikey" //nolint:gosec
31
        pkgDecimal        = "github.com/shopspring/decimal"
32
)
33

34
const (
35
        serviceInterface = "Service"
36
        rootRouterName   = "router"
37
        jsonapiContent   = "application/vnd.api+json"
38
)
39

40
var noValidation = map[string]string{"valid": "-"}
41

42
// List of responses that will be handled on the framework level and
43
// are therefore not handled by the user.
44
var generatorResponseBlacklist = map[string]bool{
45
        "401": true, // if no bearer token is provided
46
        "406": true, // if accept header is unacceptable
47
        "415": true, // if media type is invalid
48
        "422": true, // handled by go-validator
49
        "500": true, // if service returns an error
50

51
        // TODO(vil): maybe more 500 errors depending on the context result
52
        // e.g. Temporary errors (implementing the temporary interface)
53
        // result in retry later / also rate limiting
54
}
55

56
type routeGeneratorFunc func([]*route, *openapi3.Swagger) error
57

58
// BuildHandler generates the request handlers based on gorilla mux.
59
func (g *Generator) BuildHandler(schema *openapi3.Swagger) error {
5✔
60
        paths := schema.Paths
5✔
61
        // sort by key
5✔
62
        keys := make([]string, 0, len(paths))
5✔
63
        for k := range paths {
46✔
64
                keys = append(keys, k)
41✔
65
        }
41✔
66

67
        sort.Stable(sort.StringSlice(keys))
5✔
68

5✔
69
        var routes []*route
5✔
70

5✔
71
        for _, pattern := range keys {
46✔
72
                path := paths[pattern]
41✔
73

41✔
74
                if err := g.buildPath(pattern, path, &routes, schema.Components.SecuritySchemes); err != nil {
41✔
75
                        return err
×
76
                }
×
77
        }
78

79
        funcs := []routeGeneratorFunc{
5✔
80
                g.generateRequestResponseTypes,
5✔
81
                g.buildServiceInterface,
5✔
82
                g.buildRouterHelpers,
5✔
83
                g.buildRouter,
5✔
84
                g.buildRouterWithFallbackAsArg,
5✔
85
        }
5✔
86
        for _, fn := range funcs {
30✔
87
                if err := fn(routes, schema); err != nil {
25✔
UNCOV
88
                        return err
×
89
                }
×
90
        }
91

92
        return nil
5✔
93
}
94

95
func (g *Generator) buildPath(pattern string, pathItem *openapi3.PathItem, routes *[]*route, secSchemes map[string]*openapi3.SecuritySchemeRef) error {
41✔
96
        operations := []struct {
41✔
97
                method    string
41✔
98
                operation *openapi3.Operation
41✔
99
        }{
41✔
100
                {"Connect", pathItem.Connect},
41✔
101
                {"Delete", pathItem.Delete},
41✔
102
                {"Get", pathItem.Get},
41✔
103
                {"Head", pathItem.Head},
41✔
104
                {"Options", pathItem.Options},
41✔
105
                {"Patch", pathItem.Patch},
41✔
106
                {"Post", pathItem.Post},
41✔
107
                {"Put", pathItem.Put},
41✔
108
                {"Trace", pathItem.Trace},
41✔
109
        }
41✔
110

41✔
111
        for _, op := range operations {
410✔
112
                // since the list contains all operations some can be nil
369✔
113
                if op.operation == nil {
685✔
114
                        continue
316✔
115
                }
116

117
                route, err := g.buildHandler(op.method, op.operation, pattern, pathItem, secSchemes)
53✔
118
                if err != nil {
53✔
119
                        return err
×
120
                }
×
121

122
                if err := route.parseURL(); err != nil {
53✔
UNCOV
123
                        return err
×
124
                }
×
125

126
                *routes = append(*routes, route)
53✔
127
        }
128

129
        return nil
41✔
130
}
131

132
func (g *Generator) generateRequestResponseTypes(routes []*route, schema *openapi3.Swagger) error {
5✔
133
        for _, route := range routes {
58✔
134
                // generate ...ResponseWriter for each route
53✔
135
                if err := g.generateResponseInterface(route, schema); err != nil {
53✔
UNCOV
136
                        return err
×
137
                }
×
138

139
                // generate ...Request for each route
140
                if err := g.generateRequestStruct(route, schema); err != nil {
53✔
UNCOV
141
                        return err
×
142
                }
×
143
        }
144

145
        return nil
5✔
146
}
147

148
func (g *Generator) generateResponseInterface(route *route, _ *openapi3.Swagger) error {
53✔
149
        methods := []jen.Code{
53✔
150
                jen.Qual("net/http", "ResponseWriter"),
53✔
151
        }
53✔
152

53✔
153
        // sort by key
53✔
154
        keys := make([]string, 0, len(route.operation.Responses))
53✔
155
        for k := range route.operation.Responses {
355✔
156
                keys = append(keys, k)
302✔
157
        }
302✔
158

159
        sort.Stable(sort.StringSlice(keys))
53✔
160

53✔
161
        for _, code := range keys {
355✔
162
                response := route.operation.Responses[code]
302✔
163

302✔
164
                // don't generate response helpers for things that are handled by the framework
302✔
165
                if generatorResponseBlacklist[code] {
478✔
166
                        continue
176✔
167
                }
168

169
                // error responses have an error message parameter
170
                codeNum, err := strconv.Atoi(code)
126✔
171
                if err != nil {
126✔
NEW
172
                        return fmt.Errorf("failed to parse response code %s: %w", code, err)
×
173
                }
×
174

175
                // generate method name
176
                var methodName string
126✔
177
                if response.Ref != "" {
192✔
178
                        methodName = generateMethodName(filepath.Base(response.Ref))
66✔
179
                } else {
126✔
180
                        methodName = generateMethodName(response.Value.Description)
60✔
181
                }
60✔
182

183
                method := jen.Id(methodName)
126✔
184
                if codeNum >= 400 {
196✔
185
                        method.Params(jen.Error())
70✔
186

70✔
187
                        defer func() { // defer to put methods after type
140✔
188
                                // generate the method as function for the implementing type
70✔
189
                                g.addGoDoc(methodName, fmt.Sprintf("responds with jsonapi error (HTTP code %d)", codeNum))
70✔
190
                                g.goSource.Func().Params(jen.Id("w").Op("*").Id(route.responseTypeImpl)).
70✔
191
                                        Id(methodName).Params(jen.Id("err").Error()).Block(
70✔
192
                                        jen.Qual(pkgJSONAPIRuntime, "WriteError").Call(
70✔
193
                                                jen.Id("w"),
70✔
194
                                                jen.Lit(codeNum),
70✔
195
                                                jen.Id("err"),
70✔
196
                                        ),
70✔
197
                                )
70✔
198
                        }()
70✔
199
                } else if mt := response.Value.Content.Get(jsonapiContent); mt != nil {
93✔
200
                        typeReference, err := g.generateTypeReference(route.serviceFunc+methodName,
37✔
201
                                mt.Schema, false)
37✔
202
                        if err != nil {
37✔
203
                                return err
×
204
                        }
×
205

206
                        method.Params(typeReference)
37✔
207

37✔
208
                        defer func() { // defer to put methods after type
74✔
209
                                // generate the method as function for the implementing type
37✔
210
                                g.addGoDoc(methodName, fmt.Sprintf("responds with jsonapi marshaled data (HTTP code %d)", codeNum))
37✔
211
                                g.goSource.Func().Params(jen.Id("w").Op("*").Id(route.responseTypeImpl)).
37✔
212
                                        Id(methodName).Params(jen.Id("data").Add(typeReference)).Block(
37✔
213
                                        jen.Qual(pkgJSONAPIRuntime, "Marshal").Call(
37✔
214
                                                jen.Id("w"),
37✔
215
                                                jen.Id("data"),
37✔
216
                                                jen.Lit(codeNum),
37✔
217
                                        ),
37✔
218
                                )
37✔
219
                        }()
37✔
220
                } else {
19✔
221
                        method.Params()
19✔
222

19✔
223
                        defer func() { // defer to put methods after type
38✔
224
                                // get mime type if any
19✔
225
                                mime := "application/vnd.api+json"
19✔
226
                                for m := range response.Value.Content {
22✔
227
                                        mime = m
3✔
228
                                        break // only the first mime type will be respected
3✔
229
                                }
230

231
                                // generate the method as function for the implementing type
232
                                g.addGoDoc(methodName, fmt.Sprintf("responds with empty response (HTTP code %d)", codeNum))
19✔
233
                                g.goSource.Func().Params(jen.Id("w").Op("*").Id(route.responseTypeImpl)).
19✔
234
                                        Id(methodName).Params().BlockFunc(func(g *jen.Group) {
38✔
235
                                        // set the content type for the response (prevents the go guess work -> improves performance)
19✔
236
                                        g.Id("w").Dot("Header").Call().Dot("Set").Call(jen.Lit("Content-Type"), jen.Lit(mime))
19✔
237
                                        g.Id("w").Dot("WriteHeader").Call(jen.Lit(codeNum))
19✔
238
                                })
19✔
239
                        }()
240
                }
241

242
                methods = append(methods, method)
126✔
243
        }
244

245
        // Comment and type
246
        g.addGoDoc(route.responseType, "is a standard http.ResponseWriter extended with methods\n"+
53✔
247
                "to generate the respective responses easily")
53✔
248
        g.goSource.Type().Id(route.responseType).Interface(methods...)
53✔
249

53✔
250
        // Implementation type
53✔
251
        g.goSource.Type().Id(route.responseTypeImpl).Struct(
53✔
252
                jen.Qual("net/http", "ResponseWriter"),
53✔
253
        )
53✔
254

53✔
255
        return nil
53✔
256
}
257

258
func (g *Generator) generateRequestStruct(route *route, _ *openapi3.Swagger) error {
53✔
259
        body := route.operation.RequestBody
53✔
260

53✔
261
        // add http request
53✔
262
        fields := []jen.Code{
53✔
263
                jen.Id("Request").Op("*").Qual("net/http", "Request").Tag(noValidation),
53✔
264
        }
53✔
265

53✔
266
        // add request type
53✔
267
        if body != nil {
74✔
268
                if mt := body.Value.Content.Get(jsonapiContent); mt != nil {
41✔
269
                        ref, err := g.generateTypeReference(route.serviceFunc+"Content", mt.Schema, true)
20✔
270
                        if err != nil {
20✔
271
                                return err
×
272
                        }
×
273
                        // the content has noValidation to only check parametes first
274
                        // then unmarshal and then check the content after
275
                        fields = append(fields, jen.Id("Content").Add(ref).Tag(noValidation))
20✔
276
                }
277
        }
278

279
        // add parameters
280
        for _, param := range route.operation.Parameters {
140✔
281
                paramName := generateParamName(param)
87✔
282
                paramStmt := jen.Id(paramName)
87✔
283

87✔
284
                tags := make(map[string]string)
87✔
285
                if param.Value.Required {
121✔
286
                        tags["valid"] = "required"
34✔
287
                } else {
87✔
288
                        tags["valid"] = "optional"
53✔
289
                }
53✔
290

291
                // add go type
292
                if param.Value.Schema.Ref != "" {
92✔
293
                        paramStmt.Id(goNameHelper(filepath.Base(param.Value.Schema.Ref)))
5✔
294
                } else {
87✔
295
                        tg := g.goType(paramStmt, param.Value.Schema.Value, tags)
82✔
296
                        tg.isParam = true
82✔
297

82✔
298
                        if err := tg.invoke(); err != nil {
82✔
UNCOV
299
                                return err
×
300
                        }
×
301
                }
302

303
                fields = append(fields, paramStmt.Tag(tags))
87✔
304
        }
305

306
        // add comment and generate type
307
        if body != nil {
74✔
308
                g.addGoDoc(route.requestType, body.Value.Description)
21✔
309
        } else {
53✔
310
                g.addGoDoc(route.requestType, "is a standard http.Request extended with the\n"+
32✔
311
                        "un-marshaled content object")
32✔
312
        }
32✔
313

314
        g.goSource.Type().Id(route.requestType).Struct(fields...)
53✔
315

53✔
316
        return nil
53✔
317
}
318

319
func (g *Generator) buildServiceInterface(routes []*route, schema *openapi3.Swagger) error {
5✔
320
        for _, route := range routes {
58✔
321
                if err := g.buildSubServiceInterface(route, schema); err != nil {
53✔
322
                        return err
×
323
                }
×
324
        }
325

326
        subServices := make([]jen.Code, 0)
5✔
327
        for _, route := range routes {
58✔
328
                subServices = append(subServices, jen.Id(generateSubServiceName(route.handler)))
53✔
329
        }
53✔
330

331
        g.goSource.Line()
5✔
332
        g.goSource.Line()
5✔
333
        g.goSource.Comment("Legacy Interface.")
5✔
334
        g.goSource.Comment("Use this if you want to fully implement a service.")
5✔
335
        g.goSource.Type().Id(serviceInterface).Interface(subServices...)
5✔
336

5✔
337
        return nil
5✔
338
}
339

340
func (g *Generator) buildSubServiceInterface(route *route, _ *openapi3.Swagger) error {
53✔
341
        methods := make([]jen.Code, 0)
53✔
342

53✔
343
        if route.operation.Description != "" {
95✔
344
                methods = append(methods, jen.Comment(fmt.Sprintf("%s %s\n\n%s", route.serviceFunc, route.operation.Summary, route.operation.Description)))
42✔
345
        } else {
53✔
346
                methods = append(methods, jen.Comment(fmt.Sprintf("%s %s", route.serviceFunc, route.operation.Summary)))
11✔
347
        }
11✔
348

349
        methods = append(methods, jen.Id(route.serviceFunc).Params(
53✔
350
                jen.Qual("context", "Context"),
53✔
351
                jen.Id(route.responseType),
53✔
352
                jen.Op("*").Id(route.requestType),
53✔
353
        ).Id("error"))
53✔
354

53✔
355
        g.goSource.Line().Commentf("%s interface for %s handler", serviceInterface, route.handler)
53✔
356
        g.goSource.Type().Id(generateSubServiceName(route.handler)).Interface(methods...)
53✔
357

53✔
358
        return nil
53✔
359
}
360

361
func (g *Generator) buildRouter(routes []*route, schema *openapi3.Swagger) error {
5✔
362
        routerBody, err := g.buildRouterBodyWithFallback(routes, schema, jen.Id(rootRouterName).Dot("NotFoundHandler"))
5✔
363
        if err != nil {
5✔
364
                return nil
×
365
        }
×
366

367
        g.addGoDoc("Router", "implements: "+schema.Info.Title+"\n\n"+schema.Info.Description)
5✔
368

5✔
369
        serviceInterfaceVariable := jen.Id("service").Interface()
5✔
370
        if hasSecuritySchema(schema) {
8✔
371
                g.goSource.Func().Id("Router").Params(
3✔
372
                        serviceInterfaceVariable, jen.Id("authBackend").Id(authBackendInterface)).Op("*").Qual(pkgGorillaMux, "Router").Block(routerBody...)
3✔
373
        } else {
5✔
374
                g.goSource.Func().Id("Router").Params(
2✔
375
                        serviceInterfaceVariable).Op("*").Qual(pkgGorillaMux, "Router").Block(routerBody...)
2✔
376
        }
2✔
377

378
        return nil
5✔
379
}
380

381
func (g *Generator) buildRouterWithFallbackAsArg(routes []*route, schema *openapi3.Swagger) error {
5✔
382
        routerBody, err := g.buildRouterBodyWithFallback(routes, schema, jen.Id("fallback"))
5✔
383
        if err != nil {
5✔
384
                return nil
×
385
        }
×
386

387
        g.addGoDoc("Router", "implements: "+schema.Info.Title+"\n\n"+schema.Info.Description)
5✔
388

5✔
389
        serviceInterfaceVariable := jen.Id("service").Interface()
5✔
390
        if hasSecuritySchema(schema) {
8✔
391
                g.goSource.Func().Id("RouterWithFallback").Params(
3✔
392
                        serviceInterfaceVariable, jen.Id("authBackend").Id(authBackendInterface), jen.Id("fallback").Qual("net/http", "Handler")).Op("*").Qual(pkgGorillaMux, "Router").Block(routerBody...)
3✔
393
        } else {
5✔
394
                g.goSource.Func().Id("RouterWithFallback").Params(
2✔
395
                        serviceInterfaceVariable, jen.Id("fallback").Qual("net/http", "Handler")).Op("*").Qual(pkgGorillaMux, "Router").Block(routerBody...)
2✔
396
        }
2✔
397

398
        return nil
5✔
399
}
400

401
func (g *Generator) buildRouterHelpers(routes []*route, schema *openapi3.Swagger) error {
5✔
402
        needsSecurity := hasSecuritySchema(schema)
5✔
403

5✔
404
        // sort the routes with query parameter to the top
5✔
405
        sortableRoutes := sortableRouteList(routes)
5✔
406
        sort.Stable(&sortableRoutes)
5✔
407

5✔
408
        fallbackName := "fallback"
5✔
409
        fallback := jen.Id(fallbackName).Qual("net/http", "Handler")
5✔
410
        // add all route handlers
5✔
411
        for i := 0; i < len(sortableRoutes); i++ {
58✔
412
                route := sortableRoutes[i]
53✔
413

53✔
414
                var routeCallParams *jen.Statement
53✔
415
                if needsSecurity {
98✔
416
                        routeCallParams = jen.List(jen.Id("service"), jen.Id("authBackend"))
45✔
417
                } else {
53✔
418
                        routeCallParams = jen.List(jen.Id("service"))
8✔
419
                }
8✔
420

421
                primaryHandler := jen.Id(route.handler).Call(routeCallParams)
53✔
422
                fallbackHandler := jen.Id(fallbackName)
53✔
423
                ifElse := make([]jen.Code, 0)
53✔
424

53✔
425
                for _, handler := range []jen.Code{primaryHandler, fallbackHandler} {
159✔
426
                        block := jen.Return(handler)
106✔
427
                        ifElse = append(ifElse, block)
106✔
428
                }
106✔
429

430
                if len(ifElse) < 1 {
53✔
431
                        panic("if-else slice should contain two elements, one with the service interface being called and one passing the NotFoundHandler")
×
432
                }
433

434
                implGuard := jen.If(
53✔
435
                        jen.List(jen.Id("service"), jen.Id("ok")).Op(":=").Id("service").Assert(jen.Id(generateSubServiceName(route.handler))),
53✔
436
                        jen.Id("ok")).Block(ifElse[0]).Else().Block(ifElse[1])
53✔
437

53✔
438
                comment := jen.Commentf("%s helper that checks if the given service fulfills the interface. Returns fallback handler if not, otherwise returns matching handler.", generateHandlerTypeAssertionHelperName(route.handler))
53✔
439

53✔
440
                var callParams *jen.Statement
53✔
441
                if needsSecurity {
98✔
442
                        callParams = jen.List(jen.Id("service").Id("interface{}"), fallback, jen.Id("authBackend").Id(authBackendInterface))
45✔
443
                } else {
53✔
444
                        callParams = jen.List(jen.Id("service").Id("interface{}"), fallback)
8✔
445
                }
8✔
446

447
                helper := jen.Func().Id(generateHandlerTypeAssertionHelperName(route.handler)).
53✔
448
                        Params(callParams).Qual("net/http", "Handler").Block(implGuard).Line().Line()
53✔
449

53✔
450
                g.goSource.Line().Add(comment)
53✔
451
                g.goSource.Add(helper)
53✔
452
        }
453

454
        return nil
5✔
455
}
456

457
func (g *Generator) buildRouterBodyWithFallback(routes []*route, schema *openapi3.Swagger, fallback jen.Code) ([]jen.Code, error) {
10✔
458
        needsSecurity := hasSecuritySchema(schema)
10✔
459
        startInd := 0
10✔
460

10✔
461
        var routeStmts []jen.Code //nolint:prealloc
10✔
462

10✔
463
        if needsSecurity {
16✔
464
                startInd++
6✔
465
                routeStmts = make([]jen.Code, 2, (len(routes)+2)*len(schema.Servers)+2)
6✔
466
                // Init Authentication
6✔
467
                var names []string
6✔
468
                for name := range schema.Components.SecuritySchemes {
22✔
469
                        names = append(names, name)
16✔
470
                }
16✔
471

472
                sort.Stable(sort.StringSlice(names))
6✔
473

6✔
474
                caser := cases.Title(language.Und, cases.NoLower)
6✔
475
                for _, name := range names {
22✔
476
                        routeStmts = append(routeStmts, jen.Id("authBackend").Dot("Init"+caser.String(name)).Call(jen.Id("cfg"+caser.String(name))))
16✔
477
                }
16✔
478
        } else {
4✔
479
                routeStmts = make([]jen.Code, 1, (len(routes)+2)*len(schema.Servers)+1)
4✔
480
        }
4✔
481
        // create new router
482
        routeStmts[startInd] = jen.Id(rootRouterName).Op(":=").Qual(pkgGorillaMux, "NewRouter").Call()
10✔
483

10✔
484
        // Note: we don't restrict host, scheme and port to ease development
10✔
485
        pathsIdx := make(map[string]struct{})
10✔
486

10✔
487
        var paths []string
10✔
488

10✔
489
        for _, server := range schema.Servers {
24✔
490
                serverURL, err := url.Parse(server.URL)
14✔
491
                if err != nil {
14✔
492
                        return nil, err
×
493
                }
×
494

495
                if _, ok := pathsIdx[serverURL.Path]; !ok {
26✔
496
                        paths = append(paths, serverURL.Path)
12✔
497
                }
12✔
498

499
                pathsIdx[serverURL.Path] = struct{}{}
14✔
500
        }
501

502
        // but generate subrouters for each server
503
        for i, path := range paths {
22✔
504
                subrouterID := fmt.Sprintf("s%d", i+1)
12✔
505

12✔
506
                // init and return the router
12✔
507
                routeStmts = append(routeStmts, jen.Comment(fmt.Sprintf("Subrouter %s - Path: %s", subrouterID, path)))
12✔
508
                routeStmts = append(routeStmts, jen.Id(subrouterID).Op(":=").Id("router").
12✔
509
                        Dot("PathPrefix").Call(jen.Lit(path)).Dot("Subrouter").Call())
12✔
510

12✔
511
                // sort the routes with query parameter to the top
12✔
512
                sortableRoutes := sortableRouteList(routes)
12✔
513
                sort.Stable(&sortableRoutes)
12✔
514

12✔
515
                // add all route handlers
12✔
516
                for i := 0; i < len(sortableRoutes); i++ {
126✔
517
                        route := sortableRoutes[i]
114✔
518

114✔
519
                        var routeCallParams *jen.Statement
114✔
520
                        if needsSecurity {
204✔
521
                                routeCallParams = jen.List(jen.Id("service"), fallback, jen.Id("authBackend"))
90✔
522
                        } else {
114✔
523
                                routeCallParams = jen.List(jen.Id("service"), fallback)
24✔
524
                        }
24✔
525

526
                        helper := jen.Id(generateHandlerTypeAssertionHelperName(route.handler)).Call(routeCallParams)
114✔
527
                        routeStmt := jen.Id(subrouterID).Dot("Methods").Call(jen.Lit(route.method)).
114✔
528
                                Dot("Path").Call(jen.Lit(route.url.Path))
114✔
529

114✔
530
                        // add query parameters for route matching
114✔
531
                        if len(route.queryValues) > 0 {
118✔
532
                                for key, value := range route.queryValues {
8✔
533
                                        if len(value) != 1 {
4✔
534
                                                panic("query paths can only handle one query parameter with the same name!")
×
535
                                        }
536

537
                                        routeStmt.Dot("Queries").Call(jen.Lit(key), jen.Lit(value[0]))
4✔
538
                                }
539
                        }
540

541
                        // add the name to build routes
542
                        routeStmt.Dot("Name").Call(jen.Lit(route.serviceFunc))
114✔
543

114✔
544
                        routeStmt.Dot("Handler").Call(helper)
114✔
545

114✔
546
                        routeStmts = append(routeStmts, routeStmt)
114✔
547
                }
548
        }
549

550
        // return
551
        routeStmts = append(routeStmts, jen.Return(jen.Id("router")))
10✔
552

10✔
553
        return routeStmts, nil
10✔
554
}
555

556
func (g *Generator) buildHandler(method string, op *openapi3.Operation, pattern string, _ *openapi3.PathItem, secSchemes map[string]*openapi3.SecuritySchemeRef) (*route, error) {
53✔
557
        needsSecurity := len(secSchemes) > 0
53✔
558
        route := &route{
53✔
559
                method:    strings.ToUpper(method),
53✔
560
                pattern:   pattern,
53✔
561
                operation: op,
53✔
562
        }
53✔
563

53✔
564
        // avoid ruby style path parameters
53✔
565
        if strings.Contains(pattern, "/:") {
53✔
566
                log.Warnf("Note: Don't use ruby style path parameters: %s", pattern)
×
567
        }
×
568

569
        // use OperationID for go function names or generate the name
570
        caser := cases.Title(language.Und, cases.NoLower)
53✔
571

53✔
572
        oid := caser.String(op.OperationID)
53✔
573
        if oid == "" {
53✔
574
                log.Warnf("Note: Avoid automatic method name generation for path (use OperationID): %s", pattern)
×
NEW
575

×
576
                oid = generateName(method, op, pattern)
×
577
        }
×
578

579
        handler := oid + "Handler"
53✔
580
        route.handler = handler
53✔
581
        route.serviceFunc = oid
53✔
582
        route.responseType = oid + "ResponseWriter"
53✔
583
        route.responseTypeImpl = strings.ToLower(oid[:1]) + oid[1:] + "ResponseWriter"
53✔
584
        route.requestType = oid + "Request"
53✔
585

53✔
586
        // check if handler has request body
53✔
587
        var requestBody bool
53✔
588

53✔
589
        if body := op.RequestBody; body != nil {
74✔
590
                if mt := body.Value.Content.Get(jsonapiContent); mt != nil {
41✔
591
                        requestBody = true
20✔
592
                }
20✔
593
        }
594

595
        // generate handler function
596
        gen := g // generator is used less frequent then the jen group, make available with longer name
53✔
597

53✔
598
        var auth *jen.Group
53✔
599

53✔
600
        if needsSecurity {
98✔
601
                if op.Security != nil {
83✔
602
                        var err error
38✔
603

38✔
604
                        auth, err = generateAuthorization(op, secSchemes)
38✔
605
                        if err != nil {
38✔
606
                                return nil, err
×
607
                        }
×
608
                }
609
        }
610

611
        g.addGoDoc(handler, fmt.Sprintf(`handles request/response marshaling and validation for
53✔
612
        %s %s`,
53✔
613
                method, pattern))
53✔
614

53✔
615
        var params *jen.Statement
53✔
616
        if needsSecurity {
98✔
617
                params = jen.List(jen.Id("service").Id(generateSubServiceName(route.handler)), jen.Id("authBackend").Id(authBackendInterface))
45✔
618
        } else {
53✔
619
                params = jen.List(jen.Id("service").Id(generateSubServiceName(route.handler)))
8✔
620
        }
8✔
621

622
        g.goSource.Func().Id(handler).Params(params).Qual("net/http", "Handler").Block(
53✔
623
                jen.Return().Qual("net/http", "HandlerFunc").Call(
53✔
624
                        jen.Func().Params(
53✔
625
                                jen.Id("w").Qual("net/http", "ResponseWriter"),
53✔
626
                                jen.Id("r").Op("*").Qual("net/http", "Request"),
53✔
627
                        ).BlockFunc(func(g *jen.Group) {
106✔
628
                                // recover panics
53✔
629
                                g.Defer().Qual(pkgMaintErrors, "HandleRequest").Call(jen.Lit(handler), jen.Id("w"), jen.Id("r"))
53✔
630

53✔
631
                                g.Line().Comment("Trace the service function handler execution")
53✔
632
                                g.Id("span").Op(":=").Qual(pkgSentry, "StartSpan").Call(
53✔
633
                                        jen.Id("r").Dot("Context").Call(), jen.Lit("http.server"), jen.Qual(pkgSentry, "WithDescription").Call(jen.Lit(handler)))
53✔
634
                                g.Defer().Id("span").Dot("Finish").Call()
53✔
635
                                g.Line().Empty()
53✔
636

53✔
637
                                // set tracing context
53✔
638
                                g.Id("ctx").Op(":=").Id("span").Dot("Context").Call()
53✔
639

53✔
640
                                g.Id("r").Op("=").Id("r.WithContext").Call(jen.Id("ctx"))
53✔
641

53✔
642
                                g.Add(auth)
53✔
643

53✔
644
                                g.Line().Comment("Setup context, response writer and request type")
53✔
645

53✔
646
                                // response writer
53✔
647
                                g.Id("writer").Op(":=").Id(route.responseTypeImpl).
53✔
648
                                        Block(jen.Id("ResponseWriter").Op(":").
53✔
649
                                                Qual(pkgJSONAPIMetrics, "NewMetric").Call(
53✔
650
                                                jen.Lit(gen.serviceName),
53✔
651
                                                jen.Lit(route.pattern),
53✔
652
                                                jen.Id("w"),
53✔
653
                                                jen.Id("r")).Op(","))
53✔
654

53✔
655
                                // request
53✔
656
                                g.Id("request").Op(":=").Id(route.requestType).
53✔
657
                                        Block(jen.Id("Request").Op(":").Id("r").Op(","))
53✔
658

53✔
659
                                // vars in case parameters are given
53✔
660
                                g.Line().Comment("Scan and validate incoming request parameters")
53✔
661

53✔
662
                                if len(route.operation.Parameters) > 0 {
94✔
663
                                        // path parameters need the vars
41✔
664
                                        needVars := false
41✔
665

41✔
666
                                        for _, param := range route.operation.Parameters {
128✔
667
                                                if param.Value.In == "path" {
122✔
668
                                                        needVars = true
35✔
669
                                                }
35✔
670
                                        }
671

672
                                        if needVars {
70✔
673
                                                g.Id("vars").Op(":=").Qual(pkgGorillaMux, "Vars").Call(jen.Id("r"))
29✔
674
                                        }
29✔
675

676
                                        // all parameters need to be parsed
677
                                        g.If().Op("!").Qual(pkgJSONAPIRuntime, "ScanParameters").CallFunc(func(g *jen.Group) {
82✔
678
                                                g.Id("w")
41✔
679
                                                g.Id("r")
41✔
680

41✔
681
                                                caser := cases.Title(language.Und, cases.NoLower)
41✔
682

41✔
683
                                                for _, param := range route.operation.Parameters {
128✔
684
                                                        name := generateParamName(param)
87✔
685
                                                        g.Op("&").Qual(pkgJSONAPIRuntime, "ScanParameter").BlockFunc(func(g *jen.Group) {
174✔
686
                                                                g.Id("Data").Op(":").Op("&").Id("request").Dot(name).Op(",")
87✔
687
                                                                g.Id("Location").Op(":").Qual(pkgJSONAPIRuntime, "ScanIn"+caser.String(param.Value.In)).Op(",")
87✔
688
                                                                if param.Value.In == "path" {
122✔
689
                                                                        g.Id("Input").Op(":").Id("vars").Index(jen.Lit(param.Value.Name)).Op(",")
35✔
690
                                                                }
35✔
691
                                                                g.Id("Name").Op(":").Lit(param.Value.Name).Op(",")
87✔
692
                                                        })
693
                                                }
694
                                        }).Block(jen.Return())
695
                                }
696

697
                                // validate parameters / body
698
                                if requestBody || len(route.operation.Parameters) > 0 {
102✔
699
                                        g.If().Op("!").Qual(pkgJSONAPIRuntime, "ValidateParameters").Call(
49✔
700
                                                jen.Id("w"),
49✔
701
                                                jen.Id("r"),
49✔
702
                                                jen.Op("&").Id("request"),
49✔
703
                                        ).Block(
49✔
704
                                                jen.Return().Comment("invalid request stop further processing"),
49✔
705
                                        )
49✔
706
                                }
49✔
707

708
                                // invoke service and handle error with internal server error response
709
                                invokeService := jen.Comment("Invoke service that implements the business logic").Line().
53✔
710
                                        Id("err").Op(":=").Id("service").Dot(route.serviceFunc).Call(
53✔
711
                                        jen.Id("ctx"),
53✔
712
                                        jen.Op("&").Id("writer"),
53✔
713
                                        jen.Op("&").Id("request"),
53✔
714
                                ).Line().Select().Block(
53✔
715
                                        jen.Case(jen.Op("<-").Id("ctx").Dot("Done").Call()),
53✔
716
                                        jen.If().Id("ctx").Dot("Err").Call().Op("!=").Nil().
53✔
717
                                                Block(
53✔
718
                                                        jen.Comment("Context cancellation should not be reported if it's the request context"),
53✔
719
                                                        jen.Id("w").Dot("WriteHeader").Call(jen.Lit(499)),
53✔
720
                                                        jen.If().Id("err").Op("!=").Nil().Op("&&").Op("!").Parens(
53✔
721
                                                                jen.Qual("errors", "Is").Call(jen.Id("err"), jen.Qual("context", "Canceled")).Op("||").
53✔
722
                                                                        Qual("errors", "Is").Call(jen.Id("err"), jen.Qual("context", "DeadlineExceeded")),
53✔
723
                                                        ).Block(
53✔
724
                                                                jen.Comment("Report unclean error handling (err != context err) to sentry"),
53✔
725
                                                                jen.Qual(pkgMaintErrors, "Handle").Call(jen.Id("ctx"), jen.Id("err")),
53✔
726
                                                        ),
53✔
727
                                                ),
53✔
728
                                        jen.Default(),
53✔
729
                                        jen.If().Id("err").Op("!=").Nil().Block(
53✔
730
                                                jen.Qual(pkgMaintErrors, "HandleError").Call(jen.Id("err"),
53✔
731
                                                        jen.Lit(handler),
53✔
732
                                                        jen.Id("w"),
53✔
733
                                                        jen.Id("r")),
53✔
734
                                        ),
53✔
735
                                )
53✔
736

53✔
737
                                // if there is a request body unmarshal it then call the service
53✔
738
                                // otherwise directly call the service
53✔
739
                                if requestBody {
73✔
740
                                        g.Line().Comment("Unmarshal the service request body")
20✔
741

20✔
742
                                        isArray := false
20✔
743

20✔
744
                                        mt := op.RequestBody.Value.Content.Get(jsonapiContent)
20✔
745
                                        if mt != nil {
40✔
746
                                                data := mt.Schema.Value.Properties["data"]
20✔
747
                                                if data != nil && data.Value.Type == "array" {
25✔
748
                                                        if data.Ref != "" && data.Value.Items.Ref != "" {
6✔
749
                                                                isArray = true
1✔
750
                                                        }
1✔
751
                                                }
752
                                        }
753

754
                                        if isArray {
21✔
755
                                                typeName := nameFromSchemaRef(mt.Schema.Value.Properties["data"].Value.Items)
1✔
756
                                                g.List(jen.Id("ok"), jen.Id("data")).Op(":=").
1✔
757
                                                        Qual(pkgJSONAPIRuntime, "UnmarshalMany").
1✔
758
                                                        Call(
1✔
759
                                                                jen.Id("w"),
1✔
760
                                                                jen.Id("r"),
1✔
761
                                                                jen.Qual("reflect", "TypeOf").Call(jen.New(jen.Id(typeName))),
1✔
762
                                                        )
1✔
763
                                                g.If(jen.Id("ok")).Block(
1✔
764
                                                        jen.Comment("Move the data"),
1✔
765
                                                        jen.For(jen.List(jen.Id("_"), jen.Id("elem")).Op(":=").Range().Call(jen.Id("data"))).
1✔
766
                                                                Block(
1✔
767
                                                                        jen.Id("request").Dot("Content").
1✔
768
                                                                                Op("=").
1✔
769
                                                                                Append(
1✔
770
                                                                                        jen.Id("request").Dot("Content"),
1✔
771
                                                                                        jen.Id("elem").Assert(jen.Id("*"+typeName)),
1✔
772
                                                                                ),
1✔
773
                                                                ),
1✔
774
                                                        invokeService,
1✔
775
                                                )
1✔
776
                                        } else {
20✔
777
                                                g.If(jen.Qual(pkgJSONAPIRuntime, "Unmarshal").Call(
19✔
778
                                                        jen.Id("w"),
19✔
779
                                                        jen.Id("r"),
19✔
780
                                                        jen.Op("&").Id("request").Dot("Content"))).Block(invokeService)
19✔
781
                                        }
19✔
782
                                } else {
33✔
783
                                        g.Line().Add(invokeService)
33✔
784
                                }
33✔
785
                        }),
786
                ),
787
        )
788

789
        return route, nil
53✔
790
}
791

792
func generateAuthorization(op *openapi3.Operation, secSchemes map[string]*openapi3.SecuritySchemeRef) (*jen.Group, error) {
38✔
793
        req := *op.Security
38✔
794
        r := &jen.Group{}
38✔
795

38✔
796
        if len(req[0]) == 0 {
38✔
797
                return r, nil
×
798
        }
×
799

800
        multipleSecSchemes := len(req[0]) > 1
38✔
801

38✔
802
        var err error
38✔
803

38✔
804
        if multipleSecSchemes {
41✔
805
                r, err = generateAuthorizationForMultipleSecSchemas(op, secSchemes)
3✔
806
        } else {
38✔
807
                r, err = generateAuthorizationForSingleSecSchema(op, secSchemes)
35✔
808
        }
35✔
809

810
        if err != nil {
38✔
811
                return nil, err
×
812
        }
×
813

814
        return r, nil
38✔
815
}
816

817
func generateAuthorizationForSingleSecSchema(op *openapi3.Operation, schemas map[string]*openapi3.SecuritySchemeRef) (*jen.Group, error) {
35✔
818
        req := *op.Security
35✔
819
        r := &jen.Group{}
35✔
820

35✔
821
        if len(req[0]) == 0 {
35✔
822
                return nil, nil
×
823
        }
×
824

825
        caser := cases.Title(language.Und, cases.NoLower)
35✔
826

35✔
827
        for name, secConfig := range (*op.Security)[0] {
70✔
828
                securityScheme := schemas[name]
35✔
829
                switch securityScheme.Value.Type {
35✔
830
                case "oauth2", "openIdConnect":
35✔
831
                        if len(secConfig) > 0 {
70✔
832
                                r.Line().List(jen.Id("ctx"), jen.Id("ok")).Op(":=").Id("authBackend."+authFuncPrefix+caser.String(name)).Call(jen.Id("r"), jen.Id("w"), jen.Lit(secConfig[0]))
35✔
833
                        } else {
35✔
834
                                r.Line().List(jen.Id("ctx"), jen.Id("ok")).Op(":=").Id("authBackend."+authFuncPrefix+caser.String(name)).Call(jen.Id("r"), jen.Id("w"), jen.Lit(""))
×
835
                        }
×
836
                case "apiKey":
×
837
                        if len(secConfig) > 0 {
×
838
                                return nil, fmt.Errorf("security config for api key authorization needs %d values but had: %d", 0, len(secConfig))
×
839
                        }
×
840

841
                        r.Line().List(jen.Id("ctx"), jen.Id("ok")).Op(":=").Id("authBackend."+authFuncPrefix+caser.String(name)).Call(jen.Id("r"), jen.Id("w"))
×
842
                default:
×
843
                        return nil, fmt.Errorf("security Scheme of type %q is not suppported", securityScheme.Value.Type)
×
844
                }
845
        }
846

847
        r.Line().If(jen.Op("!").Id("ok")).Block(jen.Return())
35✔
848

35✔
849
        return r, nil
35✔
850
}
851

852
func generateAuthorizationForMultipleSecSchemas(op *openapi3.Operation, secSchemes map[string]*openapi3.SecuritySchemeRef) (*jen.Group, error) {
3✔
853
        orderedSec := make([][]string, len((*op.Security)[0]))
3✔
854
        i := 0
3✔
855

3✔
856
        // Security Schemes are sorted for a reliable order of the code
3✔
857
        for name, val := range (*op.Security)[0] {
10✔
858
                vals := []string{name}
7✔
859
                orderedSec[i] = append(vals, val...)
7✔
860

7✔
861
                i++
7✔
862
        }
7✔
863

864
        sort.Slice(orderedSec, func(i, j int) bool {
7✔
865
                return orderedSec[i][0] < orderedSec[j][0]
4✔
866
        })
4✔
867

868
        r := &jen.Group{}
3✔
869
        last := &jen.Group{}
3✔
870
        last.Qual("net/http", "Error").Call(jen.Id("w"), jen.Lit("Authorization Error"), jen.Qual("net/http", "StatusUnauthorized"))
3✔
871
        last.Line().Return()
3✔
872

3✔
873
        caser := cases.Title(language.Und, cases.NoLower)
3✔
874

3✔
875
        r.Line().Var().Id("ok").Id("bool")
3✔
876

3✔
877
        for _, val := range orderedSec {
10✔
878
                name := val[0]
7✔
879
                securityScheme := secSchemes[name]
7✔
880
                innerBlock := &jen.Group{}
7✔
881
                innerBlock.Line().List(jen.Id("ctx"), jen.Id("ok")).Op("=").Id("authBackend." + authFuncPrefix + caser.String(name))
7✔
882

7✔
883
                switch securityScheme.Value.Type {
7✔
884
                case "oauth2", "openIdConnect":
4✔
885
                        if len(val) >= 2 {
6✔
886
                                innerBlock.Call(jen.Id("r"), jen.Id("w"), jen.Lit(val[1]))
2✔
887
                        } else {
4✔
888
                                innerBlock.Call(jen.Id("r"), jen.Id("w"), jen.Lit(""))
2✔
889
                        }
2✔
890
                case "apiKey":
3✔
891
                        if len(val) > 1 {
3✔
892
                                return nil, fmt.Errorf("security config for api key authorization needs %d values but had: %d", 0, len(val))
×
893
                        }
×
894

895
                        innerBlock.Call(jen.Id("r"), jen.Id("w"))
3✔
896
                default:
×
897
                        return nil, fmt.Errorf("security Scheme of type %q is not suppported", securityScheme.Value.Type)
×
898
                }
899

900
                innerBlock.Line().If(jen.Op("!").Id("ok")).Block(jen.Return())
7✔
901
                r.Line().If(jen.Id("authBackend." + authCanAuthFuncPrefix + caser.String(name))).Call(jen.Id("r")).Block(innerBlock).Else()
7✔
902
        }
903

904
        r.Block(last)
3✔
905

3✔
906
        return r, nil
3✔
907
}
908

909
var asciiName = regexp.MustCompile("([^a-zA-Z]+)")
910

NEW
911
func generateName(method string, _ *openapi3.Operation, pattern string) string {
×
912
        name := method
×
913
        parts := strings.Split(asciiName.ReplaceAllString(pattern, "/"), "/")
×
NEW
914

×
915
        for _, part := range parts {
×
916
                name += goNameHelper(part)
×
917
        }
×
918

UNCOV
919
        return goNameHelper(name)
×
920
}
921

922
func generateMethodName(description string) string {
300✔
923
        parts := strings.Split(asciiName.ReplaceAllString(description, " "), " ")
300✔
924
        for i := 0; i < len(parts); i++ {
823✔
925
                parts[i] = goNameHelper(parts[i])
523✔
926
        }
523✔
927

928
        return goNameHelper(strings.Join(parts, ""))
300✔
929
}
930

931
func generateParamName(param *openapi3.ParameterRef) string {
174✔
932
        return "Param" + generateMethodName(param.Value.Name)
174✔
933
}
174✔
934

935
func generateSubServiceName(handler string) string {
212✔
936
        return fmt.Sprintf("%s%s", handler, serviceInterface)
212✔
937
}
212✔
938

939
func generateHandlerTypeAssertionHelperName(handler string) string {
220✔
940
        return fmt.Sprintf("%sWithFallbackHelper", handler)
220✔
941
}
220✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc