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