forked from goadesign/goa
-
Notifications
You must be signed in to change notification settings - Fork 0
/
api.go
571 lines (532 loc) · 17 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
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
package apidsl
import (
"fmt"
"reflect"
"regexp"
"strings"
"github.com/goadesign/goa/design"
"github.com/goadesign/goa/dslengine"
)
// API implements the top level API DSL. It defines the API name, default description and other
// default global property values. Here is an example showing all the possible API sub-definitions:
//
// API("API name", func() {
// Title("title") // API title used in documentation
// Description("description") // API description used in documentation
// Version("2.0") // API version being described
// TermsOfService("terms")
// Contact(func() { // API Contact information
// Name("contact name")
// Email("contact email")
// URL("contact URL")
// })
// License(func() { // API Licensing information
// Name("license name")
// URL("license URL")
// })
// Docs(func() {
// Description("doc description")
// URL("doc URL")
// })
// Host("goa.design") // API hostname
// Scheme("http")
// BasePath("/base/:param") // Common base path to all API actions
// Params(func() { // Common parameters to all API actions
// Param("param")
// })
// Security("JWT")
// Origin("http://swagger.goa.design", func() { // Define CORS policy, may be prefixed with "*" wildcard
// Headers("X-Shared-Secret") // One or more authorized headers, use "*" to authorize all
// Methods("GET", "POST") // One or more authorized HTTP methods
// Expose("X-Time") // One or more headers exposed to clients
// MaxAge(600) // How long to cache a prefligh request response
// Credentials() // Sets Access-Control-Allow-Credentials header
// })
// Consumes("application/xml") // Built-in encoders and decoders
// Consumes("application/json")
// Produces("application/gob")
// Produces("application/json", func() { // Custom encoder
// Package("github.com/goadesign/goa/encoding/json")
// })
// ResponseTemplate("static", func() { // Response template for use by actions
// Description("description")
// Status(404)
// MediaType("application/json")
// })
// ResponseTemplate("dynamic", func(arg1, arg2 string) {
// Description(arg1)
// Status(200)
// MediaType(arg2)
// })
// NoExample() // Prevent automatic generation of examples
// Trait("Authenticated", func() { // Traits define DSL that can be run anywhere
// Headers(func() {
// Header("header")
// Required("header")
// })
// })
// }
//
func API(name string, dsl func()) *design.APIDefinition {
if design.Design.Name != "" {
dslengine.ReportError("multiple API definitions, only one is allowed")
return nil
}
if !dslengine.IsTopLevelDefinition() {
dslengine.IncompatibleDSL()
return nil
}
if name == "" {
dslengine.ReportError("API name cannot be empty")
}
design.Design.Name = name
design.Design.DSLFunc = dsl
return design.Design
}
// Version specifies the API version. One design describes one version.
func Version(ver string) {
if api, ok := apiDefinition(); ok {
api.Version = ver
}
}
// Description sets the definition description.
// Description can be called inside API, Resource, Action or MediaType.
func Description(d string) {
switch def := dslengine.CurrentDefinition().(type) {
case *design.APIDefinition:
def.Description = d
case *design.ResourceDefinition:
def.Description = d
case *design.FileServerDefinition:
def.Description = d
case *design.ActionDefinition:
def.Description = d
case *design.MediaTypeDefinition:
def.Description = d
case *design.AttributeDefinition:
def.Description = d
case *design.ResponseDefinition:
def.Description = d
case *design.DocsDefinition:
def.Description = d
case *design.SecuritySchemeDefinition:
def.Description = d
default:
dslengine.IncompatibleDSL()
}
}
// BasePath defines the API base path, i.e. the common path prefix to all the API actions.
// The path may define wildcards (see Routing for a description of the wildcard syntax).
// The corresponding parameters must be described using Params.
func BasePath(val string) {
switch def := dslengine.CurrentDefinition().(type) {
case *design.APIDefinition:
def.BasePath = val
case *design.ResourceDefinition:
def.BasePath = val
if !strings.HasPrefix(val, "//") {
awcs := design.ExtractWildcards(design.Design.BasePath)
wcs := design.ExtractWildcards(val)
for _, awc := range awcs {
for _, wc := range wcs {
if awc == wc {
dslengine.ReportError(`duplicate wildcard "%s" in API and resource base paths`, wc)
}
}
}
}
default:
dslengine.IncompatibleDSL()
}
}
// Origin defines the CORS policy for a given origin. The origin can use a wildcard prefix
// such as "https://*.mydomain.com". The special value "*" defines the policy for all origins
// (in which case there should be only one Origin DSL in the parent resource). Example:
//
// Origin("http://swagger.goa.design", func() { // Define CORS policy, may be prefixed with "*" wildcard
// Headers("X-Shared-Secret") // One or more authorized headers, use "*" to authorize all
// Methods("GET", "POST") // One or more authorized HTTP methods
// Expose("X-Time") // One or more headers exposed to clients
// MaxAge(600) // How long to cache a prefligh request response
// Credentials() // Sets Access-Control-Allow-Credentials header
// })
//
func Origin(origin string, dsl func()) {
cors := &design.CORSDefinition{Origin: origin}
if !dslengine.Execute(dsl, cors) {
return
}
var parent dslengine.Definition
switch def := dslengine.CurrentDefinition().(type) {
case *design.APIDefinition:
parent = def
if def.Origins == nil {
def.Origins = make(map[string]*design.CORSDefinition)
}
def.Origins[origin] = cors
case *design.ResourceDefinition:
parent = def
if def.Origins == nil {
def.Origins = make(map[string]*design.CORSDefinition)
}
def.Origins[origin] = cors
default:
dslengine.IncompatibleDSL()
return
}
cors.Parent = parent
}
// Methods sets the origin allowed methods. Used in Origin DSL.
func Methods(vals ...string) {
if cors, ok := corsDefinition(); ok {
cors.Methods = vals
}
}
// Expose sets the origin exposed headers. Used in Origin DSL.
func Expose(vals ...string) {
if cors, ok := corsDefinition(); ok {
cors.Exposed = vals
}
}
// MaxAge sets the cache expiry for preflight request responses. Used in Origin DSL.
func MaxAge(val uint) {
if cors, ok := corsDefinition(); ok {
cors.MaxAge = val
}
}
// Credentials sets the allow credentials response header. Used in Origin DSL.
func Credentials() {
if cors, ok := corsDefinition(); ok {
cors.Credentials = true
}
}
// TermsOfService describes the API terms of services or links to them.
func TermsOfService(terms string) {
if a, ok := apiDefinition(); ok {
a.TermsOfService = terms
}
}
// Regular expression used to validate RFC1035 hostnames*/
var hostnameRegex = regexp.MustCompile(`^[[:alnum:]][[:alnum:]\-]{0,61}[[:alnum:]]|[[:alpha:]]$`)
// Host sets the API hostname.
func Host(host string) {
if !hostnameRegex.MatchString(host) {
dslengine.ReportError(`invalid hostname value "%s"`, host)
return
}
if a, ok := apiDefinition(); ok {
a.Host = host
}
}
// Scheme sets the API URL schemes.
func Scheme(vals ...string) {
ok := true
for _, v := range vals {
if v != "http" && v != "https" && v != "ws" && v != "wss" {
dslengine.ReportError(`invalid scheme "%s", must be one of "http", "https", "ws" or "wss"`, v)
ok = false
}
}
if !ok {
return
}
switch def := dslengine.CurrentDefinition().(type) {
case *design.APIDefinition:
def.Schemes = append(def.Schemes, vals...)
case *design.ResourceDefinition:
def.Schemes = append(def.Schemes, vals...)
case *design.ActionDefinition:
def.Schemes = append(def.Schemes, vals...)
default:
dslengine.IncompatibleDSL()
}
}
// Contact sets the API contact information.
func Contact(dsl func()) {
contact := new(design.ContactDefinition)
if !dslengine.Execute(dsl, contact) {
return
}
if a, ok := apiDefinition(); ok {
a.Contact = contact
}
}
// License sets the API license information.
func License(dsl func()) {
license := new(design.LicenseDefinition)
if !dslengine.Execute(dsl, license) {
return
}
if a, ok := apiDefinition(); ok {
a.License = license
}
}
// Docs provides external documentation pointers.
func Docs(dsl func()) {
docs := new(design.DocsDefinition)
if !dslengine.Execute(dsl, docs) {
return
}
switch def := dslengine.CurrentDefinition().(type) {
case *design.APIDefinition:
def.Docs = docs
case *design.ActionDefinition:
def.Docs = docs
case *design.FileServerDefinition:
def.Docs = docs
default:
dslengine.IncompatibleDSL()
}
}
// Name sets the contact or license name.
func Name(name string) {
switch def := dslengine.CurrentDefinition().(type) {
case *design.ContactDefinition:
def.Name = name
case *design.LicenseDefinition:
def.Name = name
default:
dslengine.IncompatibleDSL()
}
}
// Email sets the contact email.
func Email(email string) {
if c, ok := contactDefinition(); ok {
c.Email = email
}
}
// URL sets the contact or license URL.
func URL(url string) {
switch def := dslengine.CurrentDefinition().(type) {
case *design.ContactDefinition:
def.URL = url
case *design.LicenseDefinition:
def.URL = url
case *design.DocsDefinition:
def.URL = url
default:
dslengine.IncompatibleDSL()
}
}
// Consumes adds a MIME type to the list of MIME types the APIs supports when accepting requests.
// Consumes may also specify the path of the decoding package.
// The package must expose a DecoderFactory method that returns an object which implements
// goa.DecoderFactory.
func Consumes(args ...interface{}) {
if a, ok := apiDefinition(); ok {
if def := buildEncodingDefinition(false, args...); def != nil {
a.Consumes = append(a.Consumes, def)
}
}
}
// Produces adds a MIME type to the list of MIME types the APIs can encode responses with.
// Produces may also specify the path of the encoding package.
// The package must expose a EncoderFactory method that returns an object which implements
// goa.EncoderFactory.
func Produces(args ...interface{}) {
if a, ok := apiDefinition(); ok {
if def := buildEncodingDefinition(true, args...); def != nil {
a.Produces = append(a.Produces, def)
}
}
}
// buildEncodingDefinition builds up an encoding definition.
func buildEncodingDefinition(encoding bool, args ...interface{}) *design.EncodingDefinition {
var dsl func()
var ok bool
funcName := "Consumes"
if encoding {
funcName = "Produces"
}
if len(args) == 0 {
dslengine.ReportError("missing argument in call to %s", funcName)
return nil
}
if _, ok = args[0].(string); !ok {
dslengine.ReportError("first argument to %s must be a string (MIME type)", funcName)
return nil
}
last := len(args)
if dsl, ok = args[len(args)-1].(func()); ok {
last = len(args) - 1
}
mimeTypes := make([]string, last)
for i := 0; i < last; i++ {
var mimeType string
if mimeType, ok = args[i].(string); !ok {
dslengine.ReportError("argument #%d of %s must be a string (MIME type)", i, funcName)
return nil
}
mimeTypes[i] = mimeType
}
d := &design.EncodingDefinition{MIMETypes: mimeTypes, Encoder: encoding}
if dsl != nil {
dslengine.Execute(dsl, d)
}
return d
}
// Package sets the Go package path to the encoder or decoder. It must be used inside a
// Consumes or Produces DSL.
func Package(path string) {
if e, ok := encodingDefinition(); ok {
e.PackagePath = path
}
}
// Function sets the Go function name used to instantiate the encoder or decoder. Defaults to
// NewEncoder / NewDecoder.
func Function(fn string) {
if e, ok := encodingDefinition(); ok {
e.Function = fn
}
}
// ResponseTemplate defines a response template that action definitions can use to describe their
// responses. The template may specify the HTTP response status, header specification and body media
// type. The template consists of a name and an anonymous function. The function is called when an
// action uses the template to define a response. Response template functions accept string
// parameters they can use to define the response fields. Here is an example of a response template
// definition that uses a function with one argument corresponding to the name of the response body
// media type:
//
// ResponseTemplate(OK, func(mt string) {
// Status(200) // OK response uses status code 200
// Media(mt) // Media type name set by action definition
// Headers(func() {
// Header("X-Request-Id", func() { // X-Request-Id header contains a string
// Pattern("[0-9A-F]+") // Regexp used to validate the response header content
// })
// Required("X-Request-Id") // Header is mandatory
// })
// })
//
// This template can the be used by actions to define the OK response as follows:
//
// Response(OK, "vnd.goa.example")
//
// goa comes with a set of predefined response templates (one per standard HTTP status code). The
// OK template is the only one that accepts an argument. It is used as shown in the example above to
// set the response media type. Other predefined templates do not use arguments. ResponseTemplate
// makes it possible to define additional response templates specific to the API.
func ResponseTemplate(name string, p interface{}) {
if a, ok := apiDefinition(); ok {
if a.Responses == nil {
a.Responses = make(map[string]*design.ResponseDefinition)
}
if a.ResponseTemplates == nil {
a.ResponseTemplates = make(map[string]*design.ResponseTemplateDefinition)
}
if _, ok := a.Responses[name]; ok {
dslengine.ReportError("multiple definitions for response template %s", name)
return
}
if _, ok := a.ResponseTemplates[name]; ok {
dslengine.ReportError("multiple definitions for response template %s", name)
return
}
setupResponseTemplate(a, name, p)
}
}
func setupResponseTemplate(a *design.APIDefinition, name string, p interface{}) {
if f, ok := p.(func()); ok {
r := &design.ResponseDefinition{Name: name}
if dslengine.Execute(f, r) {
a.Responses[name] = r
}
} else if tmpl, ok := p.(func(...string)); ok {
t := func(params ...string) *design.ResponseDefinition {
r := &design.ResponseDefinition{Name: name}
dslengine.Execute(func() { tmpl(params...) }, r)
return r
}
a.ResponseTemplates[name] = &design.ResponseTemplateDefinition{
Name: name,
Template: t,
}
} else {
typ := reflect.TypeOf(p)
if kind := typ.Kind(); kind != reflect.Func {
dslengine.ReportError("dsl must be a function but got %s", kind)
return
}
num := typ.NumIn()
val := reflect.ValueOf(p)
t := func(params ...string) *design.ResponseDefinition {
if len(params) < num {
args := "1 argument"
if num > 0 {
args = fmt.Sprintf("%d arguments", num)
}
dslengine.ReportError("expected at least %s when invoking response template %s", args, name)
return nil
}
r := &design.ResponseDefinition{Name: name}
in := make([]reflect.Value, num)
for i := 0; i < num; i++ {
// type checking
if t := typ.In(i); t.Kind() != reflect.String {
dslengine.ReportError("ResponseTemplate parameters must be strings but type of parameter at position %d is %s", i, t)
return nil
}
// append input arguments
in[i] = reflect.ValueOf(params[i])
}
dslengine.Execute(func() { val.Call(in) }, r)
return r
}
a.ResponseTemplates[name] = &design.ResponseTemplateDefinition{
Name: name,
Template: t,
}
}
}
// Title sets the API title used by generated documentation, JSON Hyper-schema, code comments etc.
func Title(val string) {
if a, ok := apiDefinition(); ok {
a.Title = val
}
}
// Trait defines an API trait. A trait encapsulates arbitrary DSL that gets executed wherever the
// trait is called via the UseTrait function.
func Trait(name string, val ...func()) {
if a, ok := apiDefinition(); ok {
if len(val) < 1 {
dslengine.ReportError("missing trait DSL for %s", name)
return
} else if len(val) > 1 {
dslengine.ReportError("too many arguments given to Trait")
return
}
if _, ok := design.Design.Traits[name]; ok {
dslengine.ReportError("multiple definitions for trait %s%s", name, design.Design.Context())
return
}
trait := &dslengine.TraitDefinition{Name: name, DSLFunc: val[0]}
if a.Traits == nil {
a.Traits = make(map[string]*dslengine.TraitDefinition)
}
a.Traits[name] = trait
}
}
// UseTrait executes the API trait with the given name. UseTrait can be used inside a Resource,
// Action, Type, MediaType or Attribute DSL.
func UseTrait(name string) {
var def dslengine.Definition
switch typedDef := dslengine.CurrentDefinition().(type) {
case *design.ResourceDefinition:
def = typedDef
case *design.ActionDefinition:
def = typedDef
case *design.AttributeDefinition:
def = typedDef
case *design.MediaTypeDefinition:
def = typedDef
default:
dslengine.IncompatibleDSL()
}
if def != nil {
if trait, ok := design.Design.Traits[name]; ok {
dslengine.Execute(trait.DSLFunc, def)
} else {
dslengine.ReportError("unknown trait %s", name)
}
}
}