github.com/goldeneggg/goa@v1.3.1/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 for _, route := range allRoutes { 120 for _, other := range allRoutes { 121 if route == other { 122 continue 123 } 124 if strings.HasPrefix(route.Key, other.Key) { 125 diffs := route.DifferentWildcards(other) 126 if len(diffs) > 0 { 127 var msg string 128 conflicts := make([]string, len(diffs)) 129 for i, d := range diffs { 130 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()) 131 } 132 msg = fmt.Sprintf("%s", strings.Join(conflicts, ", ")) 133 verr.Add(route.Action, 134 `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.`, 135 route.Route.FullPath(), 136 other.Route.FullPath(), 137 other.Resource.Name, 138 other.Action.Name, 139 msg, 140 ) 141 } 142 } 143 } 144 } 145 a.IterateMediaTypes(func(mt *MediaTypeDefinition) error { 146 verr.Merge(mt.Validate()) 147 return nil 148 }) 149 a.IterateUserTypes(func(t *UserTypeDefinition) error { 150 verr.Merge(t.Validate("", a)) 151 return nil 152 }) 153 a.IterateResponses(func(r *ResponseDefinition) error { 154 verr.Merge(r.Validate()) 155 return nil 156 }) 157 for _, dec := range a.Consumes { 158 verr.Merge(dec.Validate()) 159 } 160 for _, enc := range a.Produces { 161 verr.Merge(enc.Validate()) 162 } 163 164 err := verr.AsError() 165 if err == nil { 166 // *ValidationErrors(nil) != error(nil) 167 return nil 168 } 169 return err 170 } 171 172 func (a *APIDefinition) validateContact(verr *dslengine.ValidationErrors) { 173 if a.Contact != nil && a.Contact.URL != "" { 174 if _, err := url.ParseRequestURI(a.Contact.URL); err != nil { 175 verr.Add(a, "invalid contact URL value: %s", err) 176 } 177 } 178 } 179 180 func (a *APIDefinition) validateLicense(verr *dslengine.ValidationErrors) { 181 if a.License != nil && a.License.URL != "" { 182 if _, err := url.ParseRequestURI(a.License.URL); err != nil { 183 verr.Add(a, "invalid license URL value: %s", err) 184 } 185 } 186 } 187 188 func (a *APIDefinition) validateDocs(verr *dslengine.ValidationErrors) { 189 if a.Docs != nil && a.Docs.URL != "" { 190 if _, err := url.ParseRequestURI(a.Docs.URL); err != nil { 191 verr.Add(a, "invalid docs URL value: %s", err) 192 } 193 } 194 } 195 196 func (a *APIDefinition) validateOrigins(verr *dslengine.ValidationErrors) { 197 for _, origin := range a.Origins { 198 verr.Merge(origin.Validate()) 199 } 200 } 201 202 // Validate tests whether the resource definition is consistent: action names are valid and each action is 203 // valid. 204 func (r *ResourceDefinition) Validate() *dslengine.ValidationErrors { 205 verr := new(dslengine.ValidationErrors) 206 if r.Name == "" { 207 verr.Add(r, "Resource name cannot be empty") 208 } 209 r.validateActions(verr) 210 if r.ParentName != "" { 211 r.validateParent(verr) 212 } 213 for _, resp := range r.Responses { 214 verr.Merge(resp.Validate()) 215 } 216 if r.Params != nil { 217 verr.Merge(r.Params.Validate("resource parameters", r)) 218 } 219 for _, origin := range r.Origins { 220 verr.Merge(origin.Validate()) 221 } 222 return verr.AsError() 223 } 224 225 func (r *ResourceDefinition) validateActions(verr *dslengine.ValidationErrors) { 226 found := false 227 for _, a := range r.Actions { 228 if a.Name == r.CanonicalActionName { 229 found = true 230 } 231 verr.Merge(a.Validate()) 232 } 233 for _, f := range r.FileServers { 234 verr.Merge(f.Validate()) 235 } 236 if r.CanonicalActionName != "" && !found { 237 verr.Add(r, `unknown canonical action "%s"`, r.CanonicalActionName) 238 } 239 } 240 241 func (r *ResourceDefinition) validateParent(verr *dslengine.ValidationErrors) { 242 p, ok := Design.Resources[r.ParentName] 243 if !ok { 244 verr.Add(r, "Parent resource named %#v not found", r.ParentName) 245 } else { 246 if p.CanonicalAction() == nil { 247 verr.Add(r, "Parent resource %#v has no canonical action", r.ParentName) 248 } 249 } 250 } 251 252 // Validate makes sure the CORS definition origin is valid. 253 func (cors *CORSDefinition) Validate() *dslengine.ValidationErrors { 254 verr := new(dslengine.ValidationErrors) 255 if !cors.Regexp && strings.Count(cors.Origin, "*") > 1 { 256 verr.Add(cors, "invalid origin, can only contain one wildcard character") 257 } 258 if cors.Regexp { 259 _, err := regexp.Compile(cors.Origin) 260 if err != nil { 261 verr.Add(cors, "invalid origin, should be a valid regular expression") 262 } 263 } 264 return verr 265 } 266 267 // Validate validates the encoding MIME type and Go package path if set. 268 func (enc *EncodingDefinition) Validate() *dslengine.ValidationErrors { 269 verr := new(dslengine.ValidationErrors) 270 if len(enc.MIMETypes) == 0 { 271 verr.Add(enc, "missing MIME type") 272 return verr 273 } 274 for _, m := range enc.MIMETypes { 275 _, _, err := mime.ParseMediaType(m) 276 if err != nil { 277 verr.Add(enc, "invalid MIME type %#v: %s", m, err) 278 } 279 } 280 if len(enc.PackagePath) > 0 { 281 rel := filepath.FromSlash(enc.PackagePath) 282 dir, err := os.Getwd() 283 if err != nil { 284 verr.Add(enc, "couldn't retrieve working directory %s", err) 285 return verr 286 } 287 _, err = build.Default.Import(rel, dir, build.FindOnly) 288 if err != nil { 289 verr.Add(enc, "invalid Go package path %#v: %s", enc.PackagePath, err) 290 return verr 291 } 292 } else { 293 for _, m := range enc.MIMETypes { 294 if _, ok := KnownEncoders[m]; !ok { 295 knownMIMETypes := make([]string, len(KnownEncoders)) 296 i := 0 297 for k := range KnownEncoders { 298 knownMIMETypes[i] = k 299 i++ 300 } 301 sort.Strings(knownMIMETypes) 302 verr.Add(enc, "Encoders not known for all MIME types, use Package to specify encoder Go package. MIME types with known encoders are %s", 303 strings.Join(knownMIMETypes, ", ")) 304 } 305 } 306 } 307 if enc.Function != "" && enc.PackagePath == "" { 308 verr.Add(enc, "Must specify encoder package page with PackagePath") 309 } 310 return verr 311 } 312 313 // Validate tests whether the action definition is consistent: parameters have unique names and it has at least 314 // one response. 315 func (a *ActionDefinition) Validate() *dslengine.ValidationErrors { 316 verr := new(dslengine.ValidationErrors) 317 if a.Name == "" { 318 verr.Add(a, "Action name cannot be empty") 319 } 320 if len(a.Routes) == 0 { 321 verr.Add(a, "No route defined for action") 322 } 323 for i, r := range a.Responses { 324 for j, r2 := range a.Responses { 325 if i != j && r.Status == r2.Status { 326 verr.Add(r, "Multiple response definitions with status code %d", r.Status) 327 } 328 } 329 verr.Merge(r.Validate()) 330 } 331 verr.Merge(a.ValidateParams()) 332 if a.Payload != nil { 333 verr.Merge(a.Payload.Validate("action payload", a)) 334 } 335 if a.Parent == nil { 336 verr.Add(a, "missing parent resource") 337 } 338 if a.Params != nil { 339 for n, p := range a.Params.Type.ToObject() { 340 if p.Type.IsPrimitive() { 341 continue 342 } 343 if p.Type.IsArray() { 344 if p.Type.ToArray().ElemType.Type.IsPrimitive() { 345 continue 346 } 347 } 348 verr.Add(a, "Param %s has an invalid type, action params must be primitives or arrays of primitives", n) 349 } 350 } 351 352 return verr.AsError() 353 } 354 355 // Validate checks the file server is properly initialized. 356 func (f *FileServerDefinition) Validate() *dslengine.ValidationErrors { 357 verr := new(dslengine.ValidationErrors) 358 if f.FilePath == "" { 359 verr.Add(f, "File server must have a non empty file path") 360 } 361 if f.RequestPath == "" { 362 verr.Add(f, "File server must have a non empty route path") 363 } 364 if f.Parent == nil { 365 verr.Add(f, "missing parent resource") 366 } 367 matches := WildcardRegex.FindAllString(f.RequestPath, -1) 368 if len(matches) == 1 { 369 if !strings.HasSuffix(f.RequestPath, matches[0]) { 370 verr.Add(f, "invalid request path %s, must end with a wildcard starting with *", f.RequestPath) 371 } 372 } 373 if len(matches) > 2 { 374 verr.Add(f, "invalid request path, may only contain one wildcard") 375 } 376 377 return verr.AsError() 378 } 379 380 // ValidateParams checks the action parameters (make sure they have names, members and types). 381 func (a *ActionDefinition) ValidateParams() *dslengine.ValidationErrors { 382 verr := new(dslengine.ValidationErrors) 383 if a.Params == nil { 384 return nil 385 } 386 params, ok := a.Params.Type.(Object) 387 if !ok { 388 verr.Add(a, `"Params" field of action is not an object`) 389 } 390 var wcs []string 391 for _, r := range a.Routes { 392 rwcs := ExtractWildcards(r.FullPath()) 393 for _, rwc := range rwcs { 394 found := false 395 for _, wc := range wcs { 396 if rwc == wc { 397 found = true 398 break 399 } 400 } 401 if !found { 402 wcs = append(wcs, rwc) 403 } 404 } 405 } 406 for n, p := range params { 407 if n == "" { 408 verr.Add(a, "action has parameter with no name") 409 } else if p == nil { 410 verr.Add(a, "definition of parameter %s cannot be nil", n) 411 } else if p.Type == nil { 412 verr.Add(a, "type of parameter %s cannot be nil", n) 413 } 414 if p.Type.Kind() == ObjectKind { 415 verr.Add(a, `parameter %s cannot be an object, only action payloads may be of type object`, n) 416 } else if p.Type.Kind() == HashKind { 417 verr.Add(a, `parameter %s cannot be a hash, only action payloads may be of type hash`, n) 418 } 419 ctx := fmt.Sprintf("parameter %s", n) 420 verr.Merge(p.Validate(ctx, a)) 421 } 422 for _, resp := range a.Responses { 423 verr.Merge(resp.Validate()) 424 } 425 return verr.AsError() 426 } 427 428 // validated keeps track of validated attributes to handle cyclical definitions. 429 var validated = make(map[*AttributeDefinition]bool) 430 431 // Validate tests whether the attribute definition is consistent: required fields exist. 432 // Since attributes are unaware of their context, additional context information can be provided 433 // to be used in error messages. 434 // The parent definition context is automatically added to error messages. 435 func (a *AttributeDefinition) Validate(ctx string, parent dslengine.Definition) *dslengine.ValidationErrors { 436 if validated[a] { 437 return nil 438 } 439 validated[a] = true 440 verr := new(dslengine.ValidationErrors) 441 if a.Type == nil { 442 verr.Add(parent, "attribute type is nil") 443 return verr 444 } 445 if ctx != "" { 446 ctx += " - " 447 } 448 // If both Default and Enum are given, make sure the Default value is one of Enum values. 449 // TODO: We only do the default value and enum check just for primitive types. 450 // Issue 388 (https://github.com/goadesign/goa/issues/388) will address this for other types. 451 if a.Type.IsPrimitive() && a.DefaultValue != nil && a.Validation != nil && a.Validation.Values != nil { 452 var found bool 453 for _, e := range a.Validation.Values { 454 if e == a.DefaultValue { 455 found = true 456 break 457 } 458 } 459 if !found { 460 verr.Add(parent, "%sdefault value %#v is not one of the accepted values: %#v", ctx, a.DefaultValue, a.Validation.Values) 461 } 462 } 463 o := a.Type.ToObject() 464 if o != nil { 465 for _, n := range a.AllRequired() { 466 found := false 467 for an := range o { 468 if n == an { 469 found = true 470 break 471 } 472 } 473 if !found { 474 verr.Add(parent, `%srequired field "%s" does not exist`, ctx, n) 475 } 476 } 477 for n, att := range o { 478 ctx = fmt.Sprintf("field %s", n) 479 verr.Merge(att.Validate(ctx, parent)) 480 } 481 } else { 482 if a.Type.IsArray() { 483 elemType := a.Type.ToArray().ElemType 484 verr.Merge(elemType.Validate(ctx, a)) 485 } 486 } 487 488 return verr.AsError() 489 } 490 491 // Validate checks that the response definition is consistent: its status is set and the media 492 // type definition if any is valid. 493 func (r *ResponseDefinition) Validate() *dslengine.ValidationErrors { 494 verr := new(dslengine.ValidationErrors) 495 if r.Headers != nil { 496 verr.Merge(r.Headers.Validate("response headers", r)) 497 } 498 if r.Status == 0 { 499 verr.Add(r, "response status not defined") 500 } 501 return verr.AsError() 502 } 503 504 // Validate checks that the route definition is consistent: it has a parent. 505 func (r *RouteDefinition) Validate() *dslengine.ValidationErrors { 506 verr := new(dslengine.ValidationErrors) 507 if r.Parent == nil { 508 verr.Add(r, "missing route parent action") 509 } 510 return verr.AsError() 511 } 512 513 // Validate checks that the user type definition is consistent: it has a name and the attribute 514 // backing the type is valid. 515 func (u *UserTypeDefinition) Validate(ctx string, parent dslengine.Definition) *dslengine.ValidationErrors { 516 verr := new(dslengine.ValidationErrors) 517 if u.TypeName == "" { 518 verr.Add(parent, "%s - %s", ctx, "User type must have a name") 519 } 520 verr.Merge(u.AttributeDefinition.Validate(ctx, u)) 521 return verr.AsError() 522 } 523 524 // Validate checks that the media type definition is consistent: its identifier is a valid media 525 // type identifier. 526 func (m *MediaTypeDefinition) Validate() *dslengine.ValidationErrors { 527 verr := new(dslengine.ValidationErrors) 528 verr.Merge(m.UserTypeDefinition.Validate("", m)) 529 if m.Type == nil { // TBD move this to somewhere else than validation code 530 m.Type = String 531 } 532 var obj Object 533 if a := m.Type.ToArray(); a != nil { 534 if a.ElemType == nil { 535 verr.Add(m, "array element type is nil") 536 } else { 537 if err := a.ElemType.Validate("array element", m); err != nil { 538 verr.Merge(err) 539 } else { 540 if _, ok := a.ElemType.Type.(*MediaTypeDefinition); !ok { 541 verr.Add(m, "collection media type array element type must be a media type, got %s", a.ElemType.Type.Name()) 542 } else { 543 obj = a.ElemType.Type.ToObject() 544 } 545 } 546 } 547 } else { 548 obj = m.Type.ToObject() 549 } 550 if obj != nil { 551 for n, att := range obj { 552 verr.Merge(att.Validate("attribute "+n, m)) 553 if att.View != "" { 554 cmt, ok := att.Type.(*MediaTypeDefinition) 555 if !ok { 556 verr.Add(m, "attribute %s of media type defines a view for rendering but its type is not MediaTypeDefinition", n) 557 } 558 if _, ok := cmt.Views[att.View]; !ok { 559 verr.Add(m, "attribute %s of media type uses unknown view %#v", n, att.View) 560 } 561 } 562 } 563 } 564 hasDefaultView := false 565 for n, v := range m.Views { 566 if n == "default" { 567 hasDefaultView = true 568 } 569 verr.Merge(v.Validate()) 570 } 571 if !hasDefaultView { 572 verr.Add(m, `media type does not define the default view, use View("default", ...) to define it.`) 573 } 574 575 for _, l := range m.Links { 576 verr.Merge(l.Validate()) 577 } 578 return verr.AsError() 579 } 580 581 // Validate checks that the link definition is consistent: it has a media type or the name of an 582 // attribute part of the parent media type. 583 func (l *LinkDefinition) Validate() *dslengine.ValidationErrors { 584 verr := new(dslengine.ValidationErrors) 585 if l.Name == "" { 586 verr.Add(l, "Links must have a name") 587 } 588 if l.Parent == nil { 589 verr.Add(l, "Link must have a parent media type") 590 } 591 if l.Parent.ToObject() == nil { 592 verr.Add(l, "Link parent media type must be an Object") 593 } 594 att, ok := l.Parent.ToObject()[l.Name] 595 if !ok { 596 verr.Add(l, "Link name must match one of the parent media type attribute names") 597 } else { 598 mediaType, ok := att.Type.(*MediaTypeDefinition) 599 if !ok { 600 verr.Add(l, "attribute type must be a media type") 601 } else { 602 viewFound := false 603 view := l.View 604 for v := range mediaType.Views { 605 if v == view { 606 viewFound = true 607 break 608 } 609 } 610 if !viewFound { 611 verr.Add(l, "view %#v does not exist on target media type %#v", view, mediaType.Identifier) 612 } 613 } 614 } 615 return verr.AsError() 616 } 617 618 // Validate checks that the view definition is consistent: it has a parent media type and the 619 // underlying definition type is consistent. 620 func (v *ViewDefinition) Validate() *dslengine.ValidationErrors { 621 verr := new(dslengine.ValidationErrors) 622 if v.Parent == nil { 623 verr.Add(v, "View must have a parent media type") 624 } 625 verr.Merge(v.AttributeDefinition.Validate("", v)) 626 return verr.AsError() 627 }