github.com/zak-blake/goa@v1.4.1/design/apidsl/media_type.go (about) 1 package apidsl 2 3 import ( 4 "fmt" 5 "mime" 6 "strings" 7 8 "github.com/goadesign/goa/design" 9 "github.com/goadesign/goa/dslengine" 10 ) 11 12 // Counter used to create unique media type names for identifier-less media types. 13 var mediaTypeCount int 14 15 // MediaType is a top level DSL which can also be used in ResponseTemplate. 16 // 17 // MediaType implements the media type definition DSL. A media type definition describes the 18 // representation of a resource used in a response body. 19 // 20 // Media types are defined with a unique identifier as defined by RFC6838. The identifier also 21 // defines the default value for the Content-Type header of responses. The ContentType DSL allows 22 // overridding the default as shown in the example below. 23 // 24 // The media type definition includes a listing of all the potential attributes that can appear in 25 // the body. Views specify which of the attributes are actually rendered so that the same media type 26 // definition may represent multiple rendering of a given resource representation. 27 // 28 // All media types must define a view named "default". This view is used to render the media type in 29 // response bodies when no other view is specified. 30 // 31 // A media type definition may also define links to other media types. This is done by first 32 // defining an attribute for the linked-to media type and then referring to that attribute in the 33 // Links DSL. Views may then elect to render one or the other or both. Links are rendered using the 34 // special "link" view. Media types that are linked to must define that view. Here is an example 35 // showing all the possible media type sub-definitions: 36 // 37 // MediaType("application/vnd.goa.example.bottle", func() { 38 // Description("A bottle of wine") 39 // TypeName("BottleMedia") // Override default generated name 40 // ContentType("application/json") // Override default Content-Type header value 41 // Attributes(func() { 42 // Attribute("id", Integer, "ID of bottle") 43 // Attribute("href", String, "API href of bottle") 44 // Attribute("account", Account, "Owner account") 45 // Attribute("origin", Origin, "Details on wine origin") 46 // Links(func() { 47 // Link("account") // Defines link to Account media type 48 // Link("origin", "tiny") // Set view used to render link if not "link" 49 // }) 50 // Required("id", "href") 51 // }) 52 // View("default", func() { 53 // Attribute("id") 54 // Attribute("href") 55 // Attribute("links") // Renders links 56 // }) 57 // View("extended", func() { 58 // Attribute("id") 59 // Attribute("href") 60 // Attribute("account") // Renders account inline 61 // Attribute("origin") // Renders origin inline 62 // Attribute("links") // Renders links 63 // }) 64 // }) 65 // 66 // This function returns the media type definition so it can be referred to throughout the apidsl. 67 func MediaType(identifier string, apidsl func()) *design.MediaTypeDefinition { 68 if design.Design.MediaTypes == nil { 69 design.Design.MediaTypes = make(map[string]*design.MediaTypeDefinition) 70 } 71 72 if !dslengine.IsTopLevelDefinition() { 73 dslengine.IncompatibleDSL() 74 return nil 75 } 76 77 // Validate Media Type 78 identifier, params, err := mime.ParseMediaType(identifier) 79 if err != nil { 80 dslengine.ReportError("invalid media type identifier %#v: %s", 81 identifier, err) 82 // We don't return so that other errors may be 83 // captured in this one run. 84 identifier = "text/plain" 85 } 86 canonicalID := design.CanonicalIdentifier(identifier) 87 // Validate that media type identifier doesn't clash 88 if _, ok := design.Design.MediaTypes[canonicalID]; ok { 89 dslengine.ReportError("media type %#v with canonical identifier %#v is defined twice", identifier, canonicalID) 90 return nil 91 } 92 identifier = mime.FormatMediaType(identifier, params) 93 lastPart := identifier 94 lastPartIndex := strings.LastIndex(identifier, "/") 95 if lastPartIndex > -1 { 96 lastPart = identifier[lastPartIndex+1:] 97 } 98 plusIndex := strings.Index(lastPart, "+") 99 if plusIndex > 0 { 100 lastPart = lastPart[:plusIndex] 101 } 102 lastPart = strings.TrimPrefix(lastPart, "vnd.") 103 elems := strings.Split(lastPart, ".") 104 for i, e := range elems { 105 elems[i] = strings.Title(e) 106 } 107 typeName := strings.Join(elems, "") 108 if typeName == "" { 109 mediaTypeCount++ 110 typeName = fmt.Sprintf("MediaType%d", mediaTypeCount) 111 } 112 // Now save the type in the API media types map 113 mt := design.NewMediaTypeDefinition(typeName, identifier, apidsl) 114 design.Design.MediaTypes[canonicalID] = mt 115 return mt 116 } 117 118 // Media sets a response media type by name or by reference using a value returned by MediaType: 119 // 120 // Response("NotFound", func() { 121 // Status(404) 122 // Media("application/json") 123 // }) 124 // 125 // If Media uses a media type defined in the design then it may optionally specify a view name: 126 // 127 // Response("OK", func() { 128 // Status(200) 129 // Media(BottleMedia, "tiny") 130 // }) 131 // 132 // Specifying a media type is useful for responses that always return the same view. 133 // 134 // Media can be used inside Response or ResponseTemplate. 135 func Media(val interface{}, viewName ...string) { 136 if r, ok := responseDefinition(); ok { 137 if m, ok := val.(*design.MediaTypeDefinition); ok { 138 if m != nil { 139 r.MediaType = m.Identifier 140 } 141 } else if identifier, ok := val.(string); ok { 142 r.MediaType = identifier 143 } else { 144 dslengine.ReportError("media type must be a string or a pointer to MediaTypeDefinition, got %#v", val) 145 } 146 if len(viewName) == 1 { 147 r.ViewName = viewName[0] 148 } else if len(viewName) > 1 { 149 dslengine.ReportError("too many arguments given to DefaultMedia") 150 } 151 } 152 } 153 154 // Reference sets a type or media type reference. The value itself can be a type or a media type. 155 // The reference type attributes define the default properties for attributes with the same name in 156 // the type using the reference. So for example if a type is defined as such: 157 // 158 // var Bottle = Type("bottle", func() { 159 // Attribute("name", func() { 160 // MinLength(3) 161 // }) 162 // Attribute("vintage", Integer, func() { 163 // Minimum(1970) 164 // }) 165 // Attribute("somethingelse") 166 // }) 167 // 168 // Declaring the following media type: 169 // 170 // var BottleMedia = MediaType("vnd.goa.bottle", func() { 171 // Reference(Bottle) 172 // Attributes(func() { 173 // Attribute("id", Integer) 174 // Attribute("name") 175 // Attribute("vintage") 176 // }) 177 // }) 178 // 179 // defines the "name" and "vintage" attributes with the same type and validations as defined in 180 // the Bottle type. 181 func Reference(t design.DataType) { 182 switch def := dslengine.CurrentDefinition().(type) { 183 case *design.MediaTypeDefinition: 184 def.Reference = t 185 case *design.AttributeDefinition: 186 def.Reference = t 187 default: 188 dslengine.IncompatibleDSL() 189 } 190 } 191 192 // TypeName can be used in: MediaType 193 // 194 // TypeName makes it possible to set the Go struct name for a media type in the 195 // generated code. By default goagen uses the identifier to compute a valid Go 196 // identifier. This function makes it possible to override that and provide a 197 // custom name. name must be a valid Go identifier. 198 func TypeName(name string) { 199 switch def := dslengine.CurrentDefinition().(type) { 200 case *design.MediaTypeDefinition: 201 def.TypeName = name 202 default: 203 dslengine.IncompatibleDSL() 204 } 205 } 206 207 // ContentType sets the value of the Content-Type response header. By default the ID of the media 208 // type is used. 209 // 210 // ContentType("application/json") 211 // 212 func ContentType(typ string) { 213 if mt, ok := mediaTypeDefinition(); ok { 214 mt.ContentType = typ 215 } 216 } 217 218 // View can be used in: MediaType, Response 219 // 220 // View adds a new view to a media type. A view has a name and lists attributes that are 221 // rendered when the view is used to produce a response. The attribute names must appear in the 222 // media type definition. If an attribute is itself a media type then the view may specify which 223 // view to use when rendering the attribute using the View function in the View apidsl. If not 224 // specified then the view named "default" is used. Examples: 225 // 226 // View("default", func() { 227 // Attribute("id") // "id" and "name" must be media type attributes 228 // Attribute("name") 229 // }) 230 // 231 // View("extended", func() { 232 // Attribute("id") 233 // Attribute("name") 234 // Attribute("origin", func() { 235 // View("extended") // Use view "extended" to render attribute "origin" 236 // }) 237 // }) 238 func View(name string, apidsl ...func()) { 239 switch def := dslengine.CurrentDefinition().(type) { 240 case *design.MediaTypeDefinition: 241 mt := def 242 243 if !mt.Type.IsObject() && !mt.Type.IsArray() { 244 dslengine.ReportError("cannot define view on non object and non collection media types") 245 return 246 } 247 if mt.Views == nil { 248 mt.Views = make(map[string]*design.ViewDefinition) 249 } else { 250 if _, ok := mt.Views[name]; ok { 251 dslengine.ReportError("multiple definitions for view %#v in media type %#v", name, mt.TypeName) 252 return 253 } 254 } 255 at := &design.AttributeDefinition{} 256 ok := false 257 if len(apidsl) > 0 { 258 ok = dslengine.Execute(apidsl[0], at) 259 } else if mt.Type.IsArray() { 260 // inherit view from collection element if present 261 elem := mt.Type.ToArray().ElemType 262 if elem != nil { 263 if pa, ok2 := elem.Type.(*design.MediaTypeDefinition); ok2 { 264 if v, ok2 := pa.Views[name]; ok2 { 265 at = v.AttributeDefinition 266 ok = true 267 } else { 268 dslengine.ReportError("unknown view %#v", name) 269 return 270 } 271 } 272 } 273 } 274 if ok { 275 view, err := buildView(name, mt, at) 276 if err != nil { 277 dslengine.ReportError(err.Error()) 278 return 279 } 280 mt.Views[name] = view 281 } 282 283 case *design.AttributeDefinition: 284 def.View = name 285 286 default: 287 dslengine.IncompatibleDSL() 288 } 289 } 290 291 // buildView builds a view definition given an attribute and a corresponding media type. 292 func buildView(name string, mt *design.MediaTypeDefinition, at *design.AttributeDefinition) (*design.ViewDefinition, error) { 293 if at.Type == nil || !at.Type.IsObject() { 294 return nil, fmt.Errorf("invalid view DSL") 295 } 296 o := at.Type.ToObject() 297 if o != nil { 298 mto := mt.Type.ToObject() 299 if mto == nil { 300 mto = mt.Type.ToArray().ElemType.Type.ToObject() 301 } 302 for n, cat := range o { 303 if existing, ok := mto[n]; ok { 304 dup := design.DupAtt(existing) 305 dup.View = cat.View 306 o[n] = dup 307 } else if n != "links" { 308 return nil, fmt.Errorf("unknown attribute %#v", n) 309 } 310 } 311 } 312 return &design.ViewDefinition{ 313 AttributeDefinition: at, 314 Name: name, 315 Parent: mt, 316 }, nil 317 } 318 319 // Attributes implements the media type attributes apidsl. See MediaType. 320 func Attributes(apidsl func()) { 321 if mt, ok := mediaTypeDefinition(); ok { 322 dslengine.Execute(apidsl, mt) 323 } 324 } 325 326 // Links implements the media type links apidsl. See MediaType. 327 func Links(apidsl func()) { 328 if mt, ok := mediaTypeDefinition(); ok { 329 dslengine.Execute(apidsl, mt) 330 } 331 } 332 333 // Link adds a link to a media type. At the minimum a link has a name corresponding to one of the 334 // media type attribute names. A link may also define the view used to render the linked-to 335 // attribute. The default view used to render links is "link". Examples: 336 // 337 // Link("origin") // Use the "link" view of the "origin" attribute 338 // Link("account", "tiny") // Use the "tiny" view of the "account" attribute 339 func Link(name string, view ...string) { 340 if mt, ok := mediaTypeDefinition(); ok { 341 if mt.Links == nil { 342 mt.Links = make(map[string]*design.LinkDefinition) 343 } else { 344 if _, ok := mt.Links[name]; ok { 345 dslengine.ReportError("duplicate definition for link %#v", name) 346 return 347 } 348 } 349 link := &design.LinkDefinition{Name: name, Parent: mt} 350 if len(view) > 1 { 351 dslengine.ReportError("invalid syntax in Link definition for %#v, allowed syntax is Link(name) or Link(name, view)", name) 352 } 353 if len(view) > 0 { 354 link.View = view[0] 355 } else { 356 link.View = "link" 357 } 358 mt.Links[name] = link 359 } 360 } 361 362 // CollectionOf creates a collection media type from its element media type and an optional 363 // identifier. A collection media type represents the content of responses that return a collection 364 // of resources such as "list" actions. This function can be called from any place where a media 365 // type can be used. 366 // 367 // If an identifier isn't provided then the resulting media type identifier is built from the 368 // element media type by appending the media type parameter "type" with value "collection". 369 // 370 // Examples: 371 // 372 // // Define a collection media type using the default generated identifier 373 // // (e.g. "vnd.goa.bottle; type=collection" assuming the identifier of BottleMedia 374 // // is "vnd.goa.bottle") and the default views (i.e. inherited from the BottleMedia 375 // // views). 376 // var col = CollectionOf(BottleMedia) 377 // 378 // // Another collection media type using the same element media type but defining a 379 // // different default view. 380 // var col2 = CollectionOf(BottleMedia, "vnd.goa.bottle.alternate; type=collection;", func() { 381 // View("default", func() { 382 // Attribute("id") 383 // Attribute("name") 384 // }) 385 // }) 386 func CollectionOf(v interface{}, paramAndDSL ...interface{}) *design.MediaTypeDefinition { 387 var m *design.MediaTypeDefinition 388 var ok bool 389 m, ok = v.(*design.MediaTypeDefinition) 390 if !ok { 391 if id, ok := v.(string); ok { 392 m = design.Design.MediaTypes[design.CanonicalIdentifier(id)] 393 } 394 } 395 if m == nil { 396 dslengine.ReportError("invalid CollectionOf argument: not a media type and not a known media type identifier") 397 // don't return nil to avoid panics, the error will get reported at the end 398 return design.NewMediaTypeDefinition("InvalidCollection", "text/plain", nil) 399 } 400 id := m.Identifier 401 mediatype, params, err := mime.ParseMediaType(id) 402 if err != nil { 403 dslengine.ReportError("invalid media type identifier %#v: %s", id, err) 404 // don't return nil to avoid panics, the error will get reported at the end 405 return design.NewMediaTypeDefinition("InvalidCollection", "text/plain", nil) 406 } 407 hasType := false 408 for param := range params { 409 if param == "type" { 410 hasType = true 411 break 412 } 413 } 414 if !hasType { 415 params["type"] = "collection" 416 } 417 id = mime.FormatMediaType(mediatype, params) 418 p, apidsl := parseCollectionOfDSL(paramAndDSL...) 419 if p != "" { 420 id = p 421 } 422 canonical := design.CanonicalIdentifier(id) 423 if mt, ok := design.GeneratedMediaTypes[canonical]; ok { 424 // Already have a type for this collection, reuse it. 425 return mt 426 } 427 mt := design.NewMediaTypeDefinition("", id, func() { 428 if mt, ok := mediaTypeDefinition(); ok { 429 // Cannot compute collection type name before element media type DSL has executed 430 // since the DSL may modify element type name via the TypeName function. 431 mt.TypeName = m.TypeName + "Collection" 432 mt.AttributeDefinition = &design.AttributeDefinition{Type: ArrayOf(m)} 433 if apidsl != nil { 434 dslengine.Execute(apidsl, mt) 435 } 436 if mt.Views == nil { 437 // If the apidsl didn't create any views (or there is no apidsl at all) 438 // then inherit the views from the collection element. 439 mt.Views = make(map[string]*design.ViewDefinition) 440 for n, v := range m.Views { 441 mt.Views[n] = v 442 } 443 } 444 } 445 }) 446 // Do not execute the apidsl right away, will be done last to make sure the element apidsl has run 447 // first. 448 design.GeneratedMediaTypes[canonical] = mt 449 return mt 450 } 451 452 func parseCollectionOfDSL(paramAndDSL ...interface{}) (string, func()) { 453 var param string 454 var dsl func() 455 var ok bool 456 if len(paramAndDSL) > 0 { 457 d := paramAndDSL[len(paramAndDSL)-1] 458 if dsl, ok = d.(func()); ok { 459 paramAndDSL = paramAndDSL[:len(paramAndDSL)-1] 460 } 461 for _, p := range paramAndDSL { 462 param, ok = p.(string) 463 if !ok { 464 dslengine.ReportError("invalid CollectionOf argument, must be a string or a DSL function", p) 465 return "", nil 466 } 467 } 468 } 469 return param, dsl 470 }