-
Notifications
You must be signed in to change notification settings - Fork 22
/
api.go
352 lines (305 loc) · 9.93 KB
/
api.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
package rest
import (
"net/http"
"reflect"
"time"
"github.com/getkin/kin-openapi/openapi3"
)
type APIOpts func(*API)
// WithApplyCustomSchemaToType enables customisation of types in the OpenAPI specification.
// Apply customisation to a specific type by checking the t parameter.
// Apply customisations to all types by ignoring the t parameter.
func WithApplyCustomSchemaToType(f func(t reflect.Type, s *openapi3.Schema)) APIOpts {
return func(api *API) {
api.ApplyCustomSchemaToType = f
}
}
// NewAPI creates a new API from the router.
func NewAPI(name string, opts ...APIOpts) *API {
api := &API{
Name: name,
KnownTypes: defaultKnownTypes,
Routes: make(map[Pattern]MethodToRoute),
// map of model name to schema.
models: make(map[string]*openapi3.Schema),
comments: make(map[string]map[string]string),
}
for _, o := range opts {
o(api)
}
return api
}
var defaultKnownTypes = map[reflect.Type]openapi3.Schema{
reflect.TypeOf(time.Time{}): *openapi3.NewDateTimeSchema(),
reflect.TypeOf(&time.Time{}): *openapi3.NewDateTimeSchema().WithNullable(),
}
// Route models a single API route.
type Route struct {
// Method is the HTTP method of the route, e.g. http.MethodGet
Method Method
// Pattern of the route, e.g. /posts/list, or /users/{id}
Pattern Pattern
// Params of the route.
Params Params
// Models used in the route.
Models Models
// Tags used in the route.
Tags []string
// OperationID for the route.
OperationID string
// Description for the route.
Description string
}
// Params is a route parameter.
type Params struct {
// Path parameters are used in the path of the URL, e.g. /users/{id} would
// have a name of "id".
Path map[string]PathParam
// Query parameters are used in the querystring of the URL, e.g. /users/?sort={sortOrder} would
// have a name of "sort".
Query map[string]QueryParam
}
// PathParam is a paramater that's used in the path of a URL.
type PathParam struct {
// Description of the param.
Description string
// Regexp is a regular expression used to validate the param.
// An empty string means that no validation is applied.
Regexp string
// Type of the param (string, number, integer, boolean).
Type PrimitiveType
// ApplyCustomSchema customises the OpenAPI schema for the path parameter.
ApplyCustomSchema func(s *openapi3.Parameter)
}
// QueryParam is a paramater that's used in the querystring of a URL.
type QueryParam struct {
// Description of the param.
Description string
// Regexp is a regular expression used to validate the param.
// An empty string means that no validation is applied.
Regexp string
// Required sets whether the querystring parameter must be present in the URL.
Required bool
// AllowEmpty sets whether the querystring parameter can be empty.
AllowEmpty bool
// Type of the param (string, number, integer, boolean).
Type PrimitiveType
// ApplyCustomSchema customises the OpenAPI schema for the query parameter.
ApplyCustomSchema func(s *openapi3.Parameter)
}
type PrimitiveType string
const (
PrimitiveTypeString PrimitiveType = "string"
PrimitiveTypeBool PrimitiveType = "boolean"
PrimitiveTypeInteger PrimitiveType = "integer"
PrimitiveTypeFloat64 PrimitiveType = "number"
)
// MethodToRoute maps from a HTTP method to a Route.
type MethodToRoute map[Method]*Route
// Method is the HTTP method of the route, e.g. http.MethodGet
type Method string
// Pattern of the route, e.g. /posts/list, or /users/{id}
type Pattern string
// API is a model of a REST API's routes, along with their
// request and response types.
type API struct {
// Name of the API.
Name string
// Routes of the API.
// From patterns, to methods, to route.
Routes map[Pattern]MethodToRoute
// StripPkgPaths to strip from the type names in the OpenAPI output to avoid
// leaking internal implementation details such as internal repo names.
//
// This increases the risk of type clashes in the OpenAPI output, i.e. two types
// in different namespaces that are set to be stripped, and have the same type Name
// could clash.
//
// Example values could be "github.com/a-h/rest".
StripPkgPaths []string
// Models are the models that are in use in the API.
// It's possible to customise the models prior to generation of the OpenAPI specification
// by editing this value.
models map[string]*openapi3.Schema
// KnownTypes are added to the OpenAPI specification output.
// The default implementation:
// Maps time.Time to a string.
KnownTypes map[reflect.Type]openapi3.Schema
// comments from the package. This can be cleared once the spec has been created.
comments map[string]map[string]string
// ApplyCustomSchemaToType callback to customise the OpenAPI specification for a given type.
// Apply customisation to a specific type by checking the t parameter.
// Apply customisations to all types by ignoring the t parameter.
ApplyCustomSchemaToType func(t reflect.Type, s *openapi3.Schema)
}
// Merge route data into the existing configuration.
// This is typically used by adapters, such as the chiadapter
// to take information that the router already knows and add it
// to the specification.
func (api *API) Merge(r Route) {
toUpdate := api.Route(string(r.Method), string(r.Pattern))
mergeMap(toUpdate.Params.Path, r.Params.Path)
mergeMap(toUpdate.Params.Query, r.Params.Query)
if toUpdate.Models.Request.Type == nil {
toUpdate.Models.Request = r.Models.Request
}
mergeMap(toUpdate.Models.Responses, r.Models.Responses)
}
func mergeMap[TKey comparable, TValue any](into, from map[TKey]TValue) {
for kf, vf := range from {
_, ok := into[kf]
if !ok {
into[kf] = vf
}
}
}
// Spec creates an OpenAPI 3.0 specification document for the API.
func (api *API) Spec() (spec *openapi3.T, err error) {
spec, err = api.createOpenAPI()
if err != nil {
return
}
return
}
// Route upserts a route to the API definition.
func (api *API) Route(method, pattern string) (r *Route) {
methodToRoute, ok := api.Routes[Pattern(pattern)]
if !ok {
methodToRoute = make(MethodToRoute)
api.Routes[Pattern(pattern)] = methodToRoute
}
route, ok := methodToRoute[Method(method)]
if !ok {
route = &Route{
Method: Method(method),
Pattern: Pattern(pattern),
Models: Models{
Responses: make(map[int]Model),
},
Params: Params{
Path: make(map[string]PathParam),
Query: make(map[string]QueryParam),
},
}
methodToRoute[Method(method)] = route
}
return route
}
// Get defines a GET request route for the given pattern.
func (api *API) Get(pattern string) (r *Route) {
return api.Route(http.MethodGet, pattern)
}
// Head defines a HEAD request route for the given pattern.
func (api *API) Head(pattern string) (r *Route) {
return api.Route(http.MethodHead, pattern)
}
// Post defines a POST request route for the given pattern.
func (api *API) Post(pattern string) (r *Route) {
return api.Route(http.MethodPost, pattern)
}
// Put defines a PUT request route for the given pattern.
func (api *API) Put(pattern string) (r *Route) {
return api.Route(http.MethodPut, pattern)
}
// Patch defines a PATCH request route for the given pattern.
func (api *API) Patch(pattern string) (r *Route) {
return api.Route(http.MethodPatch, pattern)
}
// Delete defines a DELETE request route for the given pattern.
func (api *API) Delete(pattern string) (r *Route) {
return api.Route(http.MethodDelete, pattern)
}
// Connect defines a CONNECT request route for the given pattern.
func (api *API) Connect(pattern string) (r *Route) {
return api.Route(http.MethodConnect, pattern)
}
// Options defines an OPTIONS request route for the given pattern.
func (api *API) Options(pattern string) (r *Route) {
return api.Route(http.MethodOptions, pattern)
}
// Trace defines an TRACE request route for the given pattern.
func (api *API) Trace(pattern string) (r *Route) {
return api.Route(http.MethodTrace, pattern)
}
// HasResponseModel configures a response for the route.
// Example:
//
// api.Get("/user").HasResponseModel(http.StatusOK, rest.ModelOf[User]())
func (rm *Route) HasResponseModel(status int, response Model) *Route {
rm.Models.Responses[status] = response
return rm
}
// HasResponseModel configures the request model of the route.
// Example:
//
// api.Post("/user").HasRequestModel(http.StatusOK, rest.ModelOf[User]())
func (rm *Route) HasRequestModel(request Model) *Route {
rm.Models.Request = request
return rm
}
// HasPathParameter configures a path parameter for the route.
func (rm *Route) HasPathParameter(name string, p PathParam) *Route {
rm.Params.Path[name] = p
return rm
}
// HasQueryParameter configures a query parameter for the route.
func (rm *Route) HasQueryParameter(name string, q QueryParam) *Route {
rm.Params.Query[name] = q
return rm
}
// HasTags sets the tags for the route.
func (rm *Route) HasTags(tags []string) *Route {
rm.Tags = append(rm.Tags, tags...)
return rm
}
// HasOperationID sets the OperationID for the route.
func (rm *Route) HasOperationID(operationID string) *Route {
rm.OperationID = operationID
return rm
}
// HasDescription sets the description for the route.
func (rm *Route) HasDescription(description string) *Route {
rm.Description = description
return rm
}
// Models defines the models used by a route.
type Models struct {
Request Model
Responses map[int]Model
}
// ModelOf creates a model of type T.
func ModelOf[T any]() Model {
var t T
m := Model{
Type: reflect.TypeOf(t),
}
if sm, ok := any(t).(CustomSchemaApplier); ok {
m.s = sm.ApplyCustomSchema
}
return m
}
func modelFromType(t reflect.Type) Model {
m := Model{
Type: t,
}
if sm, ok := reflect.New(t).Interface().(CustomSchemaApplier); ok {
m.s = sm.ApplyCustomSchema
}
return m
}
// CustomSchemaApplier is a type that customises its OpenAPI schema.
type CustomSchemaApplier interface {
ApplyCustomSchema(s *openapi3.Schema)
}
var _ CustomSchemaApplier = Model{}
// Model is a model used in one or more routes.
type Model struct {
Type reflect.Type
s func(s *openapi3.Schema)
}
func (m Model) ApplyCustomSchema(s *openapi3.Schema) {
if m.s == nil {
return
}
m.s(s)
}