Skip to content
This repository was archived by the owner on Jun 12, 2024. It is now read-only.

Commit 2a90cca

Browse files
authored
Add chi router generator (#20)
**What** Generate writes a `chi` router source code into a given `Writer` reading the YAML definition of the Open API 3.0 spec from a given `Reader`. It supports options to set a package name and error behavior. All included into generation endpoints must have `operationId` and `x-handler-group` attributes. Depending on the options the generator will either produce an error or skip endpoints without these attributes.
1 parent 16b09e2 commit 2a90cca

File tree

3 files changed

+468
-0
lines changed

3 files changed

+468
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ require (
3232
google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 // indirect
3333
google.golang.org/grpc v1.22.0 // indirect
3434
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
35+
gopkg.in/yaml.v2 v2.2.1
3536
)
3637

3738
go 1.13

pkg/generators/router/router.go

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package router
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"strings"
8+
"text/template"
9+
10+
yaml "gopkg.in/yaml.v2"
11+
)
12+
13+
// DefaultPackageName used in the router's source code
14+
const DefaultPackageName = "openapi"
15+
16+
// Options represent all the possible options of the generator
17+
type Options struct {
18+
// PackageName of the generated router source code (`DefaultPackageName` by default)
19+
PackageName string
20+
21+
// FailNoGroup if true the generator returns an error if an endpoint without
22+
// `x-handler-group` attribute was found. Otherwise, this endpoint will be skipped silently.
23+
FailNoGroup bool
24+
25+
// FailNoOperationID if true the generator returns an error if an endpoint without
26+
// `operationId` attribute was found. Otherwise, this endpoint will be skipped silently.
27+
FailNoOperationID bool
28+
}
29+
30+
// Generate writes a chi router source code into `router` reading the YAML definition of the
31+
// Open API 3.0 spec from the `specFile`. It supports options `opts` (see `Options`).
32+
//
33+
// All included into generation endpoints must have `operationId` and `x-handler-group`
34+
// attributes. Depending on the `opts` generator will either produce an error or skip
35+
// endpoints without these attributes.
36+
func Generate(specFile io.Reader, router io.Writer, opts Options) (err error) {
37+
decoder := yaml.NewDecoder(specFile)
38+
spec := spec{}
39+
err = decoder.Decode(&spec)
40+
if err != nil {
41+
return err
42+
}
43+
44+
if opts.PackageName == "" {
45+
opts.PackageName = DefaultPackageName
46+
}
47+
48+
tctx, err := createTemplateCtx(spec, opts)
49+
if err != nil {
50+
return err
51+
}
52+
53+
return routerTemplate.Execute(router, tctx)
54+
}
55+
56+
type templateCtx struct {
57+
PackageName string
58+
Spec spec
59+
Groups map[string]*handlerGroup
60+
PathsByGroups map[string]*pathsInGroup
61+
}
62+
63+
type pathsInGroup struct {
64+
AllowedMethodsByPaths map[string]*methodsInPath
65+
}
66+
67+
type methodsInPath struct {
68+
OperationsByMethods map[string]string
69+
}
70+
71+
type handlerGroup struct {
72+
Endpoints []endpoint
73+
}
74+
75+
type endpoint struct {
76+
Summary string `yaml:"summary"`
77+
Description string `yaml:"description"`
78+
OperationID string `yaml:"operationId"`
79+
Group string `yaml:"x-handler-group"`
80+
}
81+
82+
type path struct {
83+
GET *endpoint `yaml:"get"`
84+
POST *endpoint `yaml:"post"`
85+
PUT *endpoint `yaml:"put"`
86+
PATCH *endpoint `yaml:"patch"`
87+
DELETE *endpoint `yaml:"delete"`
88+
}
89+
90+
type info struct {
91+
Title string `yaml:"title"`
92+
Description string `yaml:"description"`
93+
Version string `yaml:"version"`
94+
}
95+
96+
type spec struct {
97+
Info info `yaml:"info"`
98+
Paths map[string]path `yaml:"paths"`
99+
}
100+
101+
func createTemplateCtx(spec spec, opts Options) (out templateCtx, err error) {
102+
out.PackageName = opts.PackageName
103+
out.Spec = spec
104+
out.Groups = make(map[string]*handlerGroup)
105+
out.PathsByGroups = make(map[string]*pathsInGroup)
106+
107+
for path, definition := range spec.Paths {
108+
err = setEndpoint(&out, opts, http.MethodGet, path, definition.GET)
109+
if err != nil {
110+
return out, err
111+
}
112+
113+
err = setEndpoint(&out, opts, http.MethodPost, path, definition.POST)
114+
if err != nil {
115+
return out, err
116+
}
117+
118+
err = setEndpoint(&out, opts, http.MethodPut, path, definition.PUT)
119+
if err != nil {
120+
return out, err
121+
}
122+
123+
err = setEndpoint(&out, opts, http.MethodPatch, path, definition.PATCH)
124+
if err != nil {
125+
return out, err
126+
}
127+
128+
err = setEndpoint(&out, opts, http.MethodDelete, path, definition.DELETE)
129+
if err != nil {
130+
return out, err
131+
}
132+
}
133+
return out, nil
134+
}
135+
136+
func setEndpoint(out *templateCtx, opts Options, method, path string, e *endpoint) error {
137+
if e == nil {
138+
return nil
139+
}
140+
if e.Group == "" {
141+
if opts.FailNoGroup {
142+
return fmt.Errorf("`%s %s` does not have the `x-handler-group` value", method, path)
143+
}
144+
return nil
145+
}
146+
if e.OperationID == "" {
147+
if opts.FailNoOperationID {
148+
return fmt.Errorf("`%s %s` does not have the `operationId` value", method, path)
149+
}
150+
return nil
151+
}
152+
153+
group := out.Groups[e.Group]
154+
if group == nil {
155+
group = &handlerGroup{}
156+
out.Groups[e.Group] = group
157+
}
158+
group.Endpoints = append(group.Endpoints, *e)
159+
160+
exPathsInGroup := out.PathsByGroups[e.Group]
161+
if exPathsInGroup == nil {
162+
exPathsInGroup = &pathsInGroup{
163+
AllowedMethodsByPaths: make(map[string]*methodsInPath),
164+
}
165+
out.PathsByGroups[e.Group] = exPathsInGroup
166+
}
167+
168+
exMethodsInPath := exPathsInGroup.AllowedMethodsByPaths[path]
169+
if exMethodsInPath == nil {
170+
exMethodsInPath = &methodsInPath{
171+
OperationsByMethods: make(map[string]string, 5), // we have only 5 HTTP methods
172+
}
173+
exPathsInGroup.AllowedMethodsByPaths[path] = exMethodsInPath
174+
}
175+
176+
exMethodsInPath.OperationsByMethods[method] = e.OperationID
177+
178+
return nil
179+
}
180+
181+
func httpMethod(str string) string {
182+
return firstUpper(strings.ToLower(str))
183+
}
184+
func firstLower(str string) string {
185+
return strings.ToLower(str[0:1]) + str[1:]
186+
}
187+
func firstUpper(str string) string {
188+
return strings.ToUpper(str[0:1]) + str[1:]
189+
}
190+
func commentBlock(str string) string {
191+
return "// " + strings.Replace(strings.TrimSpace(str), "\n", "\n// ", -1)
192+
}
193+
194+
// template definitions
195+
var (
196+
fmap = template.FuncMap{
197+
"firstLower": firstLower,
198+
"firstUpper": firstUpper,
199+
"httpMethod": httpMethod,
200+
"commentBlock": commentBlock,
201+
}
202+
routerTemplateSource = `package {{ .PackageName }}
203+
204+
// This file is auto-generated, don't modify it manually
205+
206+
import (
207+
"net/http"
208+
"strings"
209+
210+
"github.com/go-chi/chi"
211+
)
212+
213+
{{range $name, $group := .Groups }}
214+
// {{ $name }}Handler handles the operations of the '{{ $name }}' handler group.
215+
type {{ $name }}Handler interface {
216+
{{- range $idx, $e := $group.Endpoints }}
217+
{{ (printf "%s %s" $e.OperationID $e.Description) | commentBlock }}
218+
{{ $e.OperationID }}(w http.ResponseWriter, r *http.Request)
219+
{{- end}}
220+
}
221+
{{end}}
222+
// NewRouter creates a new router for the spec and the given handlers.
223+
// {{ .Spec.Info.Title }}
224+
//
225+
{{ .Spec.Info.Description | commentBlock }}
226+
//
227+
// {{ .Spec.Info.Version }}
228+
//
229+
func NewRouter(
230+
{{- range $group, $def := .PathsByGroups }}
231+
{{ $group | firstLower}}Handler {{ $group | firstUpper }}Handler,
232+
{{- end}}
233+
) http.Handler {
234+
235+
r := chi.NewRouter()
236+
{{range $group, $pathsInGroup := .PathsByGroups }}
237+
// '{{ $group }}' group
238+
{{ range $path, $methodsInPath := $pathsInGroup.AllowedMethodsByPaths }}
239+
// '{{ $path }}'
240+
r.Options("{{ $path }}", optionsHandlerFunc(
241+
{{- range $method, $operation := $methodsInPath.OperationsByMethods }}
242+
http.Method{{ $method | httpMethod }},
243+
{{- end}}
244+
))
245+
246+
{{- range $method, $operation := $methodsInPath.OperationsByMethods }}
247+
r.{{ $method | httpMethod }}("{{ $path }}", {{ $group | firstLower }}Handler.{{ $operation }})
248+
{{- end}}
249+
{{end}}
250+
{{- end}}
251+
return r
252+
}
253+
254+
func optionsHandlerFunc(allowedMethods ...string) http.HandlerFunc {
255+
return func(w http.ResponseWriter, r *http.Request) {
256+
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
257+
}
258+
}
259+
`
260+
routerTemplate = template.Must(
261+
template.New("router").
262+
Funcs(fmap).
263+
Parse(routerTemplateSource),
264+
)
265+
)

0 commit comments

Comments
 (0)