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  }