github.com/furusax0621/goa-v1@v1.4.3/design/validation.go (about) 1 package design 2 3 import ( 4 "fmt" 5 "go/build" 6 "mime" 7 "net/url" 8 "os" 9 "path/filepath" 10 "regexp" 11 "sort" 12 "strings" 13 14 "github.com/goadesign/goa/dslengine" 15 ) 16 17 type routeInfo struct { 18 Key string 19 Resource *ResourceDefinition 20 Action *ActionDefinition 21 Route *RouteDefinition 22 Wildcards []*wildCardInfo 23 } 24 25 type wildCardInfo struct { 26 Name string 27 Orig dslengine.Definition 28 } 29 30 func newRouteInfo(resource *ResourceDefinition, action *ActionDefinition, route *RouteDefinition) *routeInfo { 31 vars := route.Params() 32 wi := make([]*wildCardInfo, len(vars)) 33 for i, v := range vars { 34 var orig dslengine.Definition 35 if strings.Contains(route.Path, v) { 36 orig = route 37 } else if strings.Contains(resource.BasePath, v) { 38 orig = resource 39 } else { 40 orig = Design 41 } 42 wi[i] = &wildCardInfo{Name: v, Orig: orig} 43 } 44 key := WildcardRegex.ReplaceAllLiteralString(route.FullPath(), "*") 45 return &routeInfo{ 46 Key: key, 47 Resource: resource, 48 Action: action, 49 Route: route, 50 Wildcards: wi, 51 } 52 } 53 54 // DifferentWildcards returns the list of wildcards in other that have a different name from the 55 // wildcard in target at the same position. 56 func (r *routeInfo) DifferentWildcards(other *routeInfo) (res [][2]*wildCardInfo) { 57 for i, wc := range other.Wildcards { 58 if r.Wildcards[i].Name != wc.Name { 59 res = append(res, [2]*wildCardInfo{r.Wildcards[i], wc}) 60 } 61 } 62 return 63 } 64 65 // Validate tests whether the API definition is consistent: all resource parent names resolve to 66 // an actual resource. 67 func (a *APIDefinition) Validate() error { 68 69 // This is a little bit hacky but we need the generated media types DSLs to run first so 70 // that their views are defined otherwise we risk running into validation errors where an 71 // attribute defined on a non generated media type uses a generated mediatype (i.e. 72 // CollectionOf(Foo)) with a specific view that hasn't been set yet. 73 // TBD: Maybe GeneratedMediaTypes should not be a separate DSL root. 74 for _, mt := range GeneratedMediaTypes { 75 dslengine.Execute(mt.DSLFunc, mt) 76 mt.DSLFunc = nil // So that it doesn't run again when the generated media types DSL root is executed 77 } 78 79 verr := new(dslengine.ValidationErrors) 80 if a.Params != nil { 81 verr.Merge(a.Params.Validate("base parameters", a)) 82 } 83 84 a.validateContact(verr) 85 a.validateLicense(verr) 86 a.validateDocs(verr) 87 a.validateOrigins(verr) 88 89 var allRoutes []*routeInfo 90 a.IterateResources(func(r *ResourceDefinition) error { 91 verr.Merge(r.Validate()) 92 r.IterateActions(func(ac *ActionDefinition) error { 93 if ac.Docs != nil && ac.Docs.URL != "" { 94 if _, err := url.ParseRequestURI(ac.Docs.URL); err != nil { 95 verr.Add(ac, "invalid action docs URL value: %s", err) 96 } 97 } 98 for _, ro := range ac.Routes { 99 if ro.IsAbsolute() { 100 continue 101 } 102 info := newRouteInfo(r, ac, ro) 103 allRoutes = append(allRoutes, info) 104 rwcs := ExtractWildcards(ac.Parent.FullPath()) 105 wcs := ExtractWildcards(ro.Path) 106 for _, rwc := range rwcs { 107 for _, wc := range wcs { 108 if rwc == wc { 109 verr.Add(ac, `duplicate wildcard "%s" in resource base path "%s" and action route "%s"`, 110 wc, ac.Parent.FullPath(), ro.Path) 111 } 112 } 113 } 114 } 115 return nil 116 }) 117 return nil 118 }) 119 120 a.validateRoutes(verr, allRoutes) 121 122 a.IterateMediaTypes(func(mt *MediaTypeDefinition) error { 123 verr.Merge(mt.Validate()) 124 return nil 125 }) 126 a.IterateUserTypes(func(t *UserTypeDefinition) error { 127 verr.Merge(t.Validate("", a)) 128 return nil 129 }) 130 a.IterateResponses(func(r *ResponseDefinition) error { 131 verr.Merge(r.Validate()) 132 return nil 133 }) 134 for _, dec := range a.Consumes { 135 verr.Merge(dec.Validate()) 136 } 137 for _, enc := range a.Produces { 138 verr.Merge(enc.Validate()) 139 } 140 141 err := verr.AsError() 142 if err == nil { 143 // *ValidationErrors(nil) != error(nil) 144 return nil 145 } 146 return err 147 } 148 149 func (a *APIDefinition) validateRoutes(verr *dslengine.ValidationErrors, routes []*routeInfo) { 150 for _, route := range routes { 151 for _, other := range routes { 152 if route == other { 153 continue 154 } 155 if route.Route.Verb != other.Route.Verb { 156 continue 157 } 158 if strings.HasPrefix(route.Key, other.Key) { 159 diffs := route.DifferentWildcards(other) 160 if len(diffs) > 0 { 161 var msg string 162 conflicts := make([]string, len(diffs)) 163 for i, d := range diffs { 164 conflicts[i] = fmt.Sprintf(`"%s" from %s and "%s" from %s`, d[0].Name, d[0].Orig.Context(), d[1].Name, d[1].Orig.Context()) 165 } 166 msg = fmt.Sprintf("%s", strings.Join(conflicts, ", ")) 167 verr.Add(route.Action, 168 `route "%s" conflicts with route "%s" of %s action %s. Make sure wildcards at the same positions have the same name. Conflicting wildcards are %s.`, 169 route.Route.FullPath(), 170 other.Route.FullPath(), 171 other.Resource.Name, 172 other.Action.Name, 173 msg, 174 ) 175 } 176 } 177 } 178 } 179 } 180 181 func (a *APIDefinition) validateContact(verr *dslengine.ValidationErrors) { 182 if a.Contact != nil && a.Contact.URL != "" { 183 if _, err := url.ParseRequestURI(a.Contact.URL); err != nil { 184 verr.Add(a, "invalid contact URL value: %s", err) 185 } 186 } 187 } 188 189 func (a *APIDefinition) validateLicense(verr *dslengine.ValidationErrors) { 190 if a.License != nil && a.License.URL != "" { 191 if _, err := url.ParseRequestURI(a.License.URL); err != nil { 192 verr.Add(a, "invalid license URL value: %s", err) 193 } 194 } 195 } 196 197 func (a *APIDefinition) validateDocs(verr *dslengine.ValidationErrors) { 198 if a.Docs != nil && a.Docs.URL != "" { 199 if _, err := url.ParseRequestURI(a.Docs.URL); err != nil { 200 verr.Add(a, "invalid docs URL value: %s", err) 201 } 202 } 203 } 204 205 func (a *APIDefinition) validateOrigins(verr *dslengine.ValidationErrors) { 206 for _, origin := range a.Origins { 207 verr.Merge(origin.Validate()) 208 } 209 } 210 211 // Validate tests whether the resource definition is consistent: action names are valid and each action is 212 // valid. 213 func (r *ResourceDefinition) Validate() *dslengine.ValidationErrors { 214 verr := new(dslengine.ValidationErrors) 215 if r.Name == "" { 216 verr.Add(r, "Resource name cannot be empty") 217 } 218 r.validateActions(verr) 219 if r.ParentName != "" { 220 r.validateParent(verr) 221 } 222 for _, resp := range r.Responses { 223 verr.Merge(resp.Validate()) 224 } 225 if r.Params != nil { 226 verr.Merge(r.Params.Validate("resource parameters", r)) 227 } 228 for _, origin := range r.Origins { 229 verr.Merge(origin.Validate()) 230 } 231 return verr.AsError() 232 } 233 234 func (r *ResourceDefinition) validateActions(verr *dslengine.ValidationErrors) { 235 found := false 236 for _, a := range r.Actions { 237 if a.Name == r.CanonicalActionName { 238 found = true 239 } 240 verr.Merge(a.Validate()) 241 } 242 for _, f := range r.FileServers { 243 verr.Merge(f.Validate()) 244 } 245 if r.CanonicalActionName != "" && !found { 246 verr.Add(r, `unknown canonical action "%s"`, r.CanonicalActionName) 247 } 248 } 249 250 func (r *ResourceDefinition) validateParent(verr *dslengine.ValidationErrors) { 251 p, ok := Design.Resources[r.ParentName] 252 if !ok { 253 verr.Add(r, "Parent resource named %#v not found", r.ParentName) 254 } else { 255 if p.CanonicalAction() == nil { 256 verr.Add(r, "Parent resource %#v has no canonical action", r.ParentName) 257 } 258 } 259 } 260 261 // Validate makes sure the CORS definition origin is valid. 262 func (cors *CORSDefinition) Validate() *dslengine.ValidationErrors { 263 verr := new(dslengine.ValidationErrors) 264 if !cors.Regexp && strings.Count(cors.Origin, "*") > 1 { 265 verr.Add(cors, "invalid origin, can only contain one wildcard character") 266 } 267 if cors.Regexp { 268 _, err := regexp.Compile(cors.Origin) 269 if err != nil { 270 verr.Add(cors, "invalid origin, should be a valid regular expression") 271 } 272 } 273 return verr 274 } 275 276 // Validate validates the encoding MIME type and Go package path if set. 277 func (enc *EncodingDefinition) Validate() *dslengine.ValidationErrors { 278 verr := new(dslengine.ValidationErrors) 279 if len(enc.MIMETypes) == 0 { 280 verr.Add(enc, "missing MIME type") 281 return verr 282 } 283 for _, m := range enc.MIMETypes { 284 _, _, err := mime.ParseMediaType(m) 285 if err != nil { 286 verr.Add(enc, "invalid MIME type %#v: %s", m, err) 287 } 288 } 289 if len(enc.PackagePath) > 0 { 290 rel := filepath.FromSlash(enc.PackagePath) 291 dir, err := os.Getwd() 292 if err != nil { 293 verr.Add(enc, "couldn't retrieve working directory %s", err) 294 return verr 295 } 296 _, err = build.Default.Import(rel, dir, build.FindOnly) 297 if err != nil { 298 verr.Add(enc, "invalid Go package path %#v: %s", enc.PackagePath, err) 299 return verr 300 } 301 } else { 302 for _, m := range enc.MIMETypes { 303 if _, ok := KnownEncoders[m]; !ok { 304 knownMIMETypes := make([]string, len(KnownEncoders)) 305 i := 0 306 for k := range KnownEncoders { 307 knownMIMETypes[i] = k 308 i++ 309 } 310 sort.Strings(knownMIMETypes) 311 verr.Add(enc, "Encoders not known for all MIME types, use Package to specify encoder Go package. MIME types with known encoders are %s", 312 strings.Join(knownMIMETypes, ", ")) 313 } 314 } 315 } 316 if enc.Function != "" && enc.PackagePath == "" { 317 verr.Add(enc, "Must specify encoder package page with PackagePath") 318 } 319 return verr 320 } 321 322 // Validate tests whether the action definition is consistent: parameters have unique names and it has at least 323 // one response. 324 func (a *ActionDefinition) Validate() *dslengine.ValidationErrors { 325 verr := new(dslengine.ValidationErrors) 326 if a.Name == "" { 327 verr.Add(a, "Action name cannot be empty") 328 } 329 if len(a.Routes) == 0 { 330 verr.Add(a, "No route defined for action") 331 } 332 for i, r := range a.Responses { 333 for j, r2 := range a.Responses { 334 if i != j && r.Status == r2.Status { 335 verr.Add(r, "Multiple response definitions with status code %d", r.Status) 336 } 337 } 338 verr.Merge(r.Validate()) 339 if HasFile(r.Type) { 340 verr.Add(a, "Response %s contains an invalid type, action responses cannot contain a file", i) 341 } 342 } 343 verr.Merge(a.ValidateParams()) 344 if a.Payload != nil { 345 verr.Merge(a.Payload.Validate("action payload", a)) 346 if HasFile(a.Payload.Type) && a.PayloadMultipart != true { 347 verr.Add(a, "Payload %s contains an invalid type, action payloads cannot contain a file", a.Payload.TypeName) 348 } 349 } 350 if a.Parent == nil { 351 verr.Add(a, "missing parent resource") 352 } 353 if a.Params != nil { 354 for n, p := range a.Params.Type.ToObject() { 355 if p.Type.IsPrimitive() { 356 if HasFile(p.Type) { 357 verr.Add(a, "Param %s has an invalid type, action params cannot be a file", n) 358 } 359 continue 360 } 361 if p.Type.IsArray() { 362 if p.Type.ToArray().ElemType.Type.IsPrimitive() { 363 if HasFile(p.Type.ToArray().ElemType.Type) { 364 verr.Add(a, "Param %s has an invalid type, action params cannot be a file array", n) 365 } 366 continue 367 } 368 } 369 verr.Add(a, "Param %s has an invalid type, action params must be primitives or arrays of primitives", n) 370 } 371 } 372 373 return verr.AsError() 374 } 375 376 // Validate checks the file server is properly initialized. 377 func (f *FileServerDefinition) Validate() *dslengine.ValidationErrors { 378 verr := new(dslengine.ValidationErrors) 379 if f.FilePath == "" { 380 verr.Add(f, "File server must have a non empty file path") 381 } 382 if f.RequestPath == "" { 383 verr.Add(f, "File server must have a non empty route path") 384 } 385 if f.Parent == nil { 386 verr.Add(f, "missing parent resource") 387 } 388 matches := WildcardRegex.FindAllString(f.RequestPath, -1) 389 if len(matches) == 1 { 390 if !strings.HasSuffix(f.RequestPath, matches[0]) { 391 verr.Add(f, "invalid request path %s, must end with a wildcard starting with *", f.RequestPath) 392 } 393 } 394 if len(matches) > 2 { 395 verr.Add(f, "invalid request path, may only contain one wildcard") 396 } 397 398 return verr.AsError() 399 } 400 401 // ValidateParams checks the action parameters (make sure they have names, members and types). 402 func (a *ActionDefinition) ValidateParams() *dslengine.ValidationErrors { 403 verr := new(dslengine.ValidationErrors) 404 if a.Params == nil { 405 return nil 406 } 407 params, ok := a.Params.Type.(Object) 408 if !ok { 409 verr.Add(a, `"Params" field of action is not an object`) 410 } 411 var wcs []string 412 for _, r := range a.Routes { 413 rwcs := ExtractWildcards(r.FullPath()) 414 for _, rwc := range rwcs { 415 found := false 416 for _, wc := range wcs { 417 if rwc == wc { 418 found = true 419 break 420 } 421 } 422 if !found { 423 wcs = append(wcs, rwc) 424 } 425 } 426 } 427 for n, p := range params { 428 if n == "" { 429 verr.Add(a, "action has parameter with no name") 430 } else if p == nil { 431 verr.Add(a, "definition of parameter %s cannot be nil", n) 432 } else if p.Type == nil { 433 verr.Add(a, "type of parameter %s cannot be nil", n) 434 } 435 if p.Type.Kind() == ObjectKind { 436 verr.Add(a, `parameter %s cannot be an object, only action payloads may be of type object`, n) 437 } else if p.Type.Kind() == HashKind { 438 verr.Add(a, `parameter %s cannot be a hash, only action payloads may be of type hash`, n) 439 } 440 ctx := fmt.Sprintf("parameter %s", n) 441 verr.Merge(p.Validate(ctx, a)) 442 } 443 for _, resp := range a.Responses { 444 verr.Merge(resp.Validate()) 445 } 446 return verr.AsError() 447 } 448 449 // validated keeps track of validated attributes to handle cyclical definitions. 450 var validated = make(map[*AttributeDefinition]bool) 451 452 // Validate tests whether the attribute definition is consistent: required fields exist. 453 // Since attributes are unaware of their context, additional context information can be provided 454 // to be used in error messages. 455 // The parent definition context is automatically added to error messages. 456 func (a *AttributeDefinition) Validate(ctx string, parent dslengine.Definition) *dslengine.ValidationErrors { 457 if validated[a] { 458 return nil 459 } 460 validated[a] = true 461 verr := new(dslengine.ValidationErrors) 462 if a.Type == nil { 463 verr.Add(parent, "attribute type is nil") 464 return verr 465 } 466 if ctx != "" { 467 ctx += " - " 468 } 469 // If both Default and Enum are given, make sure the Default value is one of Enum values. 470 // TODO: We only do the default value and enum check just for primitive types. 471 // Issue 388 (https://github.com/goadesign/goa/issues/388) will address this for other types. 472 if a.Type.IsPrimitive() && a.DefaultValue != nil && a.Validation != nil && a.Validation.Values != nil { 473 var found bool 474 for _, e := range a.Validation.Values { 475 if e == a.DefaultValue { 476 found = true 477 break 478 } 479 } 480 if !found { 481 verr.Add(parent, "%sdefault value %#v is not one of the accepted values: %#v", ctx, a.DefaultValue, a.Validation.Values) 482 } 483 } 484 o := a.Type.ToObject() 485 if o != nil { 486 for _, n := range a.AllRequired() { 487 found := false 488 for an := range o { 489 if n == an { 490 found = true 491 break 492 } 493 } 494 if !found { 495 verr.Add(parent, `%srequired field "%s" does not exist`, ctx, n) 496 } 497 } 498 for n, att := range o { 499 ctx = fmt.Sprintf("field %s", n) 500 verr.Merge(att.Validate(ctx, parent)) 501 } 502 } else { 503 if a.Type.IsArray() { 504 elemType := a.Type.ToArray().ElemType 505 verr.Merge(elemType.Validate(ctx, a)) 506 } 507 } 508 509 return verr.AsError() 510 } 511 512 // Validate checks that the response definition is consistent: its status is set and the media 513 // type definition if any is valid. 514 func (r *ResponseDefinition) Validate() *dslengine.ValidationErrors { 515 verr := new(dslengine.ValidationErrors) 516 if r.Headers != nil { 517 verr.Merge(r.Headers.Validate("response headers", r)) 518 } 519 if r.Status == 0 { 520 verr.Add(r, "response status not defined") 521 } 522 return verr.AsError() 523 } 524 525 // Validate checks that the route definition is consistent: it has a parent. 526 func (r *RouteDefinition) Validate() *dslengine.ValidationErrors { 527 verr := new(dslengine.ValidationErrors) 528 if r.Parent == nil { 529 verr.Add(r, "missing route parent action") 530 } 531 return verr.AsError() 532 } 533 534 // Validate checks that the user type definition is consistent: it has a name and the attribute 535 // backing the type is valid. 536 func (u *UserTypeDefinition) Validate(ctx string, parent dslengine.Definition) *dslengine.ValidationErrors { 537 verr := new(dslengine.ValidationErrors) 538 if u.TypeName == "" { 539 verr.Add(parent, "%s - %s", ctx, "User type must have a name") 540 } 541 verr.Merge(u.AttributeDefinition.Validate(ctx, u)) 542 return verr.AsError() 543 } 544 545 // Validate checks that the media type definition is consistent: its identifier is a valid media 546 // type identifier. 547 func (m *MediaTypeDefinition) Validate() *dslengine.ValidationErrors { 548 verr := new(dslengine.ValidationErrors) 549 verr.Merge(m.UserTypeDefinition.Validate("", m)) 550 if m.Type == nil { // TBD move this to somewhere else than validation code 551 m.Type = String 552 } 553 var obj Object 554 if a := m.Type.ToArray(); a != nil { 555 if a.ElemType == nil { 556 verr.Add(m, "array element type is nil") 557 } else { 558 if err := a.ElemType.Validate("array element", m); err != nil { 559 verr.Merge(err) 560 } else { 561 if _, ok := a.ElemType.Type.(*MediaTypeDefinition); !ok { 562 verr.Add(m, "collection media type array element type must be a media type, got %s", a.ElemType.Type.Name()) 563 } else { 564 obj = a.ElemType.Type.ToObject() 565 } 566 } 567 } 568 } else { 569 obj = m.Type.ToObject() 570 } 571 if obj != nil { 572 for n, att := range obj { 573 verr.Merge(att.Validate("attribute "+n, m)) 574 if att.View != "" { 575 cmt, ok := att.Type.(*MediaTypeDefinition) 576 if !ok { 577 verr.Add(m, "attribute %s of media type defines a view for rendering but its type is not MediaTypeDefinition", n) 578 } 579 if _, ok := cmt.Views[att.View]; !ok { 580 verr.Add(m, "attribute %s of media type uses unknown view %#v", n, att.View) 581 } 582 } 583 } 584 } 585 hasDefaultView := false 586 for n, v := range m.Views { 587 if n == "default" { 588 hasDefaultView = true 589 } 590 verr.Merge(v.Validate()) 591 } 592 if !hasDefaultView { 593 verr.Add(m, `media type does not define the default view, use View("default", ...) to define it.`) 594 } 595 596 for _, l := range m.Links { 597 verr.Merge(l.Validate()) 598 } 599 return verr.AsError() 600 } 601 602 // Validate checks that the link definition is consistent: it has a media type or the name of an 603 // attribute part of the parent media type. 604 func (l *LinkDefinition) Validate() *dslengine.ValidationErrors { 605 verr := new(dslengine.ValidationErrors) 606 if l.Name == "" { 607 verr.Add(l, "Links must have a name") 608 } 609 if l.Parent == nil { 610 verr.Add(l, "Link must have a parent media type") 611 } 612 if l.Parent.ToObject() == nil { 613 verr.Add(l, "Link parent media type must be an Object") 614 } 615 att, ok := l.Parent.ToObject()[l.Name] 616 if !ok { 617 verr.Add(l, "Link name must match one of the parent media type attribute names") 618 } else { 619 mediaType, ok := att.Type.(*MediaTypeDefinition) 620 if !ok { 621 verr.Add(l, "attribute type must be a media type") 622 } else { 623 viewFound := false 624 view := l.View 625 for v := range mediaType.Views { 626 if v == view { 627 viewFound = true 628 break 629 } 630 } 631 if !viewFound { 632 verr.Add(l, "view %#v does not exist on target media type %#v", view, mediaType.Identifier) 633 } 634 } 635 } 636 return verr.AsError() 637 } 638 639 // Validate checks that the view definition is consistent: it has a parent media type and the 640 // underlying definition type is consistent. 641 func (v *ViewDefinition) Validate() *dslengine.ValidationErrors { 642 verr := new(dslengine.ValidationErrors) 643 if v.Parent == nil { 644 verr.Add(v, "View must have a parent media type") 645 } 646 verr.Merge(v.AttributeDefinition.Validate("", v)) 647 return verr.AsError() 648 }