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  }