github.com/NewMillennialGames/goa@v1.4.0/design/apidsl/attribute.go (about)

     1  package apidsl
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/goadesign/goa/design"
    11  	"github.com/goadesign/goa/dslengine"
    12  )
    13  
    14  // Attribute can be used in: View, Type, Attribute, Attributes
    15  //
    16  // Attribute implements the attribute definition DSL. An attribute describes a data structure
    17  // recursively. Attributes are used for describing request headers, parameters and payloads -
    18  // response bodies and headers - media types	 and types. An attribute definition is recursive:
    19  // attributes may include other attributes. At the basic level an attribute has a name,
    20  // a type and optionally a default value and validation rules. The type of an attribute can be one of:
    21  //
    22  // * The primitive types Boolean, Integer, Number, DateTime, UUID or String.
    23  //
    24  // * A type defined via the Type function.
    25  //
    26  // * A media type defined via the MediaType function.
    27  //
    28  // * An object described recursively with child attributes.
    29  //
    30  // * An array defined using the ArrayOf function.
    31  //
    32  // * An hashmap defined using the HashOf function.
    33  //
    34  // * The special type Any to indicate that the attribute may take any of the types listed above.
    35  //
    36  // Attributes can be defined using the Attribute, Param, Member or Header functions depending
    37  // on where the definition appears. The syntax for all these DSL is the same.
    38  // Here are some examples:
    39  //
    40  //	Attribute("name")					// Defines an attribute of type String
    41  //
    42  //	Attribute("name", func() {
    43  //		Pattern("^foo")					// Adds a validation rule to the attribute
    44  //	})
    45  //
    46  //	Attribute("name", Integer)				// Defines an attribute of type Integer
    47  //
    48  //	Attribute("name", Integer, func() {
    49  //		Default(42)					// With a default value
    50  //	})
    51  //
    52  //	Attribute("name", Integer, "description")		// Specifies a description
    53  //
    54  //	Attribute("name", Integer, "description", func() {
    55  //		Enum(1, 2)					// And validation rules
    56  //	})
    57  //
    58  // Nested attributes:
    59  //
    60  //	Attribute("nested", func() {
    61  //		Description("description")
    62  //		Attribute("child")
    63  //		Attribute("child2", func() {
    64  //			// ....
    65  //		})
    66  //		Required("child")
    67  //	})
    68  //
    69  // Here are all the valid usage of the Attribute function:
    70  //
    71  //	Attribute(name string, dataType DataType, description string, dsl func())
    72  //
    73  //	Attribute(name string, dataType DataType, description string)
    74  //
    75  //	Attribute(name string, dataType DataType, dsl func())
    76  //
    77  //	Attribute(name string, dataType DataType)
    78  //
    79  //	Attribute(name string, dsl func())	// dataType is String or Object (if DSL defines child attributes)
    80  //
    81  //	Attribute(name string)			// dataType is String
    82  func Attribute(name string, args ...interface{}) {
    83  	var parent *design.AttributeDefinition
    84  
    85  	switch def := dslengine.CurrentDefinition().(type) {
    86  	case *design.AttributeDefinition:
    87  		parent = def
    88  	case *design.MediaTypeDefinition:
    89  		parent = def.AttributeDefinition
    90  	case design.ContainerDefinition:
    91  		parent = def.Attribute()
    92  	case *design.APIDefinition:
    93  		if def.Params == nil {
    94  			def.Params = new(design.AttributeDefinition)
    95  		}
    96  		parent = def.Params
    97  	case *design.ResourceDefinition:
    98  		if def.Params == nil {
    99  			def.Params = new(design.AttributeDefinition)
   100  		}
   101  		parent = def.Params
   102  	default:
   103  		dslengine.IncompatibleDSL()
   104  	}
   105  
   106  	if parent != nil {
   107  		if parent.Type == nil {
   108  			parent.Type = make(design.Object)
   109  		}
   110  		if _, ok := parent.Type.(design.Object); !ok {
   111  			dslengine.ReportError("can't define child attributes on attribute of type %s", parent.Type.Name())
   112  			return
   113  		}
   114  
   115  		baseAttr := attributeFromRef(name, parent.Reference)
   116  		dataType, description, dsl := parseAttributeArgs(baseAttr, args...)
   117  		if baseAttr != nil {
   118  			if description != "" {
   119  				baseAttr.Description = description
   120  			}
   121  			if dataType != nil {
   122  				baseAttr.Type = dataType
   123  			}
   124  		} else {
   125  			baseAttr = &design.AttributeDefinition{
   126  				Type:        dataType,
   127  				Description: description,
   128  			}
   129  		}
   130  		baseAttr.Reference = parent.Reference
   131  		if dsl != nil {
   132  			dslengine.Execute(dsl, baseAttr)
   133  		}
   134  		if baseAttr.Type == nil {
   135  			// DSL did not contain an "Attribute" declaration
   136  			baseAttr.Type = design.String
   137  		}
   138  		parent.Type.(design.Object)[name] = baseAttr
   139  	}
   140  }
   141  
   142  // attributeFromRef returns a base attribute given a reference data type.
   143  // It takes care of running the DSL on the reference type if it hasn't run yet.
   144  func attributeFromRef(name string, ref design.DataType) *design.AttributeDefinition {
   145  	if ref == nil {
   146  		return nil
   147  	}
   148  	switch t := ref.(type) {
   149  	case *design.UserTypeDefinition:
   150  		if t.DSLFunc != nil {
   151  			dsl := t.DSLFunc
   152  			t.DSLFunc = nil
   153  			dslengine.Execute(dsl, t.AttributeDefinition)
   154  		}
   155  		if att, ok := t.ToObject()[name]; ok {
   156  			return design.DupAtt(att)
   157  		}
   158  	case *design.MediaTypeDefinition:
   159  		if t.DSLFunc != nil {
   160  			dsl := t.DSLFunc
   161  			t.DSLFunc = nil
   162  			dslengine.Execute(dsl, t)
   163  		}
   164  		if att, ok := t.ToObject()[name]; ok {
   165  			return design.DupAtt(att)
   166  		}
   167  	case design.Object:
   168  		if att, ok := t[name]; ok {
   169  			return design.DupAtt(att)
   170  		}
   171  	}
   172  	return nil
   173  }
   174  
   175  func parseAttributeArgs(baseAttr *design.AttributeDefinition, args ...interface{}) (design.DataType, string, func()) {
   176  	var (
   177  		dataType    design.DataType
   178  		description string
   179  		dsl         func()
   180  		ok          bool
   181  	)
   182  
   183  	parseDataType := func(expected string, index int) {
   184  		if name, ok2 := args[index].(string); ok2 {
   185  			// Lookup type by name
   186  			if dataType, ok = design.Design.Types[name]; !ok {
   187  				var mt *design.MediaTypeDefinition
   188  				if mt = design.Design.MediaTypeWithIdentifier(name); mt == nil {
   189  					dataType = design.String // not nil to avoid panics
   190  					dslengine.InvalidArgError(expected, args[index])
   191  				} else {
   192  					dataType = mt
   193  				}
   194  			}
   195  			return
   196  		}
   197  		if dataType, ok = args[index].(design.DataType); !ok {
   198  			dslengine.InvalidArgError(expected, args[index])
   199  		}
   200  	}
   201  	parseDescription := func(expected string, index int) {
   202  		if description, ok = args[index].(string); !ok {
   203  			dslengine.InvalidArgError(expected, args[index])
   204  		}
   205  	}
   206  	parseDSL := func(index int, success, failure func()) {
   207  		if dsl, ok = args[index].(func()); ok {
   208  			success()
   209  		} else {
   210  			failure()
   211  		}
   212  	}
   213  
   214  	success := func() {}
   215  
   216  	switch len(args) {
   217  	case 0:
   218  		if baseAttr != nil {
   219  			dataType = baseAttr.Type
   220  		} else {
   221  			dataType = design.String
   222  		}
   223  	case 1:
   224  		success = func() {
   225  			if baseAttr != nil {
   226  				dataType = baseAttr.Type
   227  			}
   228  		}
   229  		parseDSL(0, success, func() { parseDataType("type, type name or func()", 0) })
   230  	case 2:
   231  		parseDataType("type or type name", 0)
   232  		parseDSL(1, success, func() { parseDescription("string or func()", 1) })
   233  	case 3:
   234  		parseDataType("type or type name", 0)
   235  		parseDescription("string", 1)
   236  		parseDSL(2, success, func() { dslengine.InvalidArgError("func()", args[2]) })
   237  	default:
   238  		dslengine.ReportError("too many arguments in call to Attribute")
   239  	}
   240  
   241  	return dataType, description, dsl
   242  }
   243  
   244  // Header can be used in: Headers, APIKeySecurity, JWTSecurity
   245  //
   246  // Header is an alias of Attribute for the most part.
   247  //
   248  // Within an APIKeySecurity or JWTSecurity definition, Header
   249  // defines that an implementation must check the given header to get
   250  // the API Key.  In this case, no `args` parameter is necessary.
   251  func Header(name string, args ...interface{}) {
   252  	if _, ok := dslengine.CurrentDefinition().(*design.SecuritySchemeDefinition); ok {
   253  		if len(args) != 0 {
   254  			dslengine.ReportError("do not specify args")
   255  			return
   256  		}
   257  		inHeader(name)
   258  		return
   259  	}
   260  
   261  	Attribute(name, args...)
   262  }
   263  
   264  // Member can be used in: Payload
   265  //
   266  // Member is an alias of Attribute.
   267  func Member(name string, args ...interface{}) {
   268  	Attribute(name, args...)
   269  }
   270  
   271  // Param can be used in: Params
   272  //
   273  // Param is an alias of Attribute.
   274  func Param(name string, args ...interface{}) {
   275  	Attribute(name, args...)
   276  }
   277  
   278  // Default can be used in: Attribute
   279  //
   280  // Default sets the default value for an attribute.
   281  // See http://json-schema.org/latest/json-schema-validation.html#anchor10.
   282  func Default(def interface{}) {
   283  	if a, ok := attributeDefinition(); ok {
   284  		if a.Type != nil {
   285  			if !a.Type.CanHaveDefault() {
   286  				dslengine.ReportError("%s type cannot have a default value", qualifiedTypeName(a.Type))
   287  			} else if !a.Type.IsCompatible(def) {
   288  				dslengine.ReportError("default value %#v is incompatible with attribute of type %s",
   289  					def, qualifiedTypeName(a.Type))
   290  			} else {
   291  				a.SetDefault(def)
   292  			}
   293  		} else {
   294  			a.SetDefault(def)
   295  		}
   296  	}
   297  }
   298  
   299  // Example can be used in: Attribute, Header, Param, HashOf, ArrayOf
   300  //
   301  // Example sets the example of an attribute to be used for the documentation:
   302  //
   303  //	Attributes(func() {
   304  //		Attribute("ID", Integer, func() {
   305  //			Example(1)
   306  //		})
   307  //		Attribute("name", String, func() {
   308  //			Example("Cabernet Sauvignon")
   309  //		})
   310  //		Attribute("price", String) //If no Example() is provided, goa generates one that fits your specification
   311  //	})
   312  //
   313  // If you do not want an auto-generated example for an attribute, add NoExample() to it.
   314  func Example(exp interface{}) {
   315  	if a, ok := attributeDefinition(); ok {
   316  		if pass := a.SetExample(exp); !pass {
   317  			dslengine.ReportError("example value %#v is incompatible with attribute of type %s",
   318  				exp, a.Type.Name())
   319  		}
   320  	}
   321  }
   322  
   323  // ReadOnly can be used in: Attribute
   324  // ReadOnly sets the readOnly property of an attribute to true. It is used when attributes are computed in the API and
   325  // are not expected from the client
   326  func ReadOnly() {
   327  	if a, ok := attributeDefinition(); ok {
   328  		a.SetReadOnly()
   329  	}
   330  }
   331  
   332  // NoExample can be used in: Attribute, Header, Param, HashOf, ArrayOf
   333  //
   334  // NoExample sets the example of an attribute to be blank for the documentation. It is used when
   335  // users don't want any custom or auto-generated example
   336  func NoExample() {
   337  	switch def := dslengine.CurrentDefinition().(type) {
   338  	case *design.APIDefinition:
   339  		def.NoExamples = true
   340  	case *design.AttributeDefinition:
   341  		def.SetExample(nil)
   342  	default:
   343  		dslengine.IncompatibleDSL()
   344  	}
   345  }
   346  
   347  // Enum can be used in: Attribute, Header, Param, HashOf, ArrayOf
   348  //
   349  // Enum adds a "enum" validation to the attribute.
   350  // See http://json-schema.org/latest/json-schema-validation.html#anchor76.
   351  func Enum(val ...interface{}) {
   352  	if a, ok := attributeDefinition(); ok {
   353  		ok := true
   354  		for i, v := range val {
   355  			// When can a.Type be nil? glad you asked
   356  			// There are two ways to write an Attribute declaration with the DSL that
   357  			// don't set the type: with one argument - just the name - in which case the type
   358  			// is set to String or with two arguments - the name and DSL. In this latter form
   359  			// the type can end up being either String - if the DSL does not define any
   360  			// attribute - or object if it does.
   361  			// Why allowing this? because it's not always possible to specify the type of an
   362  			// object - an object may just be declared inline to represent a substructure.
   363  			// OK then why not assuming object and not allowing for string? because the DSL
   364  			// where there's only one argument and the type is string implicitly is very
   365  			// useful and common, for example to list attributes that refer to other attributes
   366  			// such as responses that refer to responses defined at the API level or links that
   367  			// refer to the media type attributes. So if the form that takes a DSL always ended
   368  			// up defining an object we'd have a weird situation where one arg is string and
   369  			// two args is object. Breaks the least surprise principle. Soooo long story
   370  			// short the lesser evil seems to be to allow the ambiguity. Also tests like the
   371  			// one below are really a convenience to the user and not a fundamental feature
   372  			// - not checking in the case the type is not known yet is OK.
   373  			if a.Type != nil && !a.Type.IsCompatible(v) {
   374  				dslengine.ReportError("value %#v at index %d is incompatible with attribute of type %s",
   375  					v, i, a.Type.Name())
   376  				ok = false
   377  			}
   378  		}
   379  		if ok {
   380  			a.AddValues(val)
   381  		}
   382  	}
   383  }
   384  
   385  // SupportedValidationFormats lists the supported formats for use with the
   386  // Format DSL.
   387  var SupportedValidationFormats = []string{
   388  	"cidr",
   389  	"date",
   390  	"date-time",
   391  	"email",
   392  	"hostname",
   393  	"ipv4",
   394  	"ipv6",
   395  	"ip",
   396  	"mac",
   397  	"regexp",
   398  	"rfc1123",
   399  	"uri",
   400  }
   401  
   402  // Format can be used in: Attribute, Header, Param, HashOf, ArrayOf
   403  //
   404  // Format adds a "format" validation to the attribute.
   405  // See http://json-schema.org/latest/json-schema-validation.html#anchor104.
   406  // The formats supported by goa are:
   407  //
   408  // "date": RFC3339 date
   409  //
   410  // "date-time": RFC3339 date time
   411  //
   412  // "email": RFC5322 email address
   413  //
   414  // "hostname": RFC1035 internet host name
   415  //
   416  // "ipv4", "ipv6", "ip": RFC2373 IPv4, IPv6 address or either
   417  //
   418  // "uri": RFC3986 URI
   419  //
   420  // "mac": IEEE 802 MAC-48, EUI-48 or EUI-64 MAC address
   421  //
   422  // "cidr": RFC4632 or RFC4291 CIDR notation IP address
   423  //
   424  // "regexp": RE2 regular expression
   425  //
   426  // "rfc1123": RFC1123 date time
   427  func Format(f string) {
   428  	if a, ok := attributeDefinition(); ok {
   429  		if a.Type != nil && a.Type.Kind() != design.StringKind {
   430  			incompatibleAttributeType("format", a.Type.Name(), "a string")
   431  		} else {
   432  			supported := false
   433  			for _, s := range SupportedValidationFormats {
   434  				if s == f {
   435  					supported = true
   436  					break
   437  				}
   438  			}
   439  			if !supported {
   440  				dslengine.ReportError("unsupported format %#v, supported formats are: %s",
   441  					f, strings.Join(SupportedValidationFormats, ", "))
   442  			} else {
   443  				if a.Validation == nil {
   444  					a.Validation = &dslengine.ValidationDefinition{}
   445  				}
   446  				a.Validation.Format = f
   447  			}
   448  		}
   449  	}
   450  }
   451  
   452  // Pattern can be used in: Attribute, Header, Param, HashOf, ArrayOf
   453  //
   454  // Pattern adds a "pattern" validation to the attribute.
   455  // See http://json-schema.org/latest/json-schema-validation.html#anchor33.
   456  func Pattern(p string) {
   457  	if a, ok := attributeDefinition(); ok {
   458  		if a.Type != nil && a.Type.Kind() != design.StringKind {
   459  			incompatibleAttributeType("pattern", a.Type.Name(), "a string")
   460  		} else {
   461  			_, err := regexp.Compile(p)
   462  			if err != nil {
   463  				dslengine.ReportError("invalid pattern %#v, %s", p, err)
   464  			} else {
   465  				if a.Validation == nil {
   466  					a.Validation = &dslengine.ValidationDefinition{}
   467  				}
   468  				a.Validation.Pattern = p
   469  			}
   470  		}
   471  	}
   472  }
   473  
   474  // Minimum can be used in: Attribute, Header, Param, HashOf, ArrayOf
   475  //
   476  // Minimum adds a "minimum" validation to the attribute.
   477  // See http://json-schema.org/latest/json-schema-validation.html#anchor21.
   478  func Minimum(val interface{}) {
   479  	if a, ok := attributeDefinition(); ok {
   480  		if a.Type != nil && a.Type.Kind() != design.IntegerKind && a.Type.Kind() != design.NumberKind {
   481  			incompatibleAttributeType("minimum", a.Type.Name(), "an integer or a number")
   482  		} else {
   483  			var f float64
   484  			switch v := val.(type) {
   485  			case float32, float64, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64:
   486  				f = reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0.0))).Float()
   487  			case string:
   488  				var err error
   489  				f, err = strconv.ParseFloat(v, 64)
   490  				if err != nil {
   491  					dslengine.ReportError("invalid number value %#v", v)
   492  					return
   493  				}
   494  			default:
   495  				dslengine.ReportError("invalid number value %#v", v)
   496  				return
   497  			}
   498  			if a.Validation == nil {
   499  				a.Validation = &dslengine.ValidationDefinition{}
   500  			}
   501  			a.Validation.Minimum = &f
   502  		}
   503  	}
   504  }
   505  
   506  // Maximum can be used in: Attribute, Header, Param, HashOf, ArrayOf
   507  //
   508  // Maximum adds a "maximum" validation to the attribute.
   509  // See http://json-schema.org/latest/json-schema-validation.html#anchor17.
   510  func Maximum(val interface{}) {
   511  	if a, ok := attributeDefinition(); ok {
   512  		if a.Type != nil && a.Type.Kind() != design.IntegerKind && a.Type.Kind() != design.NumberKind {
   513  			incompatibleAttributeType("maximum", a.Type.Name(), "an integer or a number")
   514  		} else {
   515  			var f float64
   516  			switch v := val.(type) {
   517  			case float32, float64, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64:
   518  				f = reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0.0))).Float()
   519  			case string:
   520  				var err error
   521  				f, err = strconv.ParseFloat(v, 64)
   522  				if err != nil {
   523  					dslengine.ReportError("invalid number value %#v", v)
   524  					return
   525  				}
   526  			default:
   527  				dslengine.ReportError("invalid number value %#v", v)
   528  				return
   529  			}
   530  			if a.Validation == nil {
   531  				a.Validation = &dslengine.ValidationDefinition{}
   532  			}
   533  			a.Validation.Maximum = &f
   534  		}
   535  	}
   536  }
   537  
   538  // MinLength can be used in: Attribute, Header, Param, HashOf, ArrayOf
   539  //
   540  // MinLength adds a "minItems" validation to the attribute.
   541  // See http://json-schema.org/latest/json-schema-validation.html#anchor45.
   542  func MinLength(val int) {
   543  	if a, ok := attributeDefinition(); ok {
   544  		if a.Type != nil && a.Type.Kind() != design.StringKind && a.Type.Kind() != design.ArrayKind && a.Type.Kind() != design.HashKind {
   545  			incompatibleAttributeType("minimum length", a.Type.Name(), "a string or an array")
   546  		} else {
   547  			if a.Validation == nil {
   548  				a.Validation = &dslengine.ValidationDefinition{}
   549  			}
   550  			a.Validation.MinLength = &val
   551  		}
   552  	}
   553  }
   554  
   555  // MaxLength can be used in: Attribute, Header, Param, HashOf, ArrayOf
   556  //
   557  // MaxLength adds a "maxItems" validation to the attribute.
   558  // See http://json-schema.org/latest/json-schema-validation.html#anchor42.
   559  func MaxLength(val int) {
   560  	if a, ok := attributeDefinition(); ok {
   561  		if a.Type != nil && a.Type.Kind() != design.StringKind && a.Type.Kind() != design.ArrayKind {
   562  			incompatibleAttributeType("maximum length", a.Type.Name(), "a string or an array")
   563  		} else {
   564  			if a.Validation == nil {
   565  				a.Validation = &dslengine.ValidationDefinition{}
   566  			}
   567  			a.Validation.MaxLength = &val
   568  		}
   569  	}
   570  }
   571  
   572  // Required can be used in: Attributes, Headers, Payload, Type, Params
   573  //
   574  // Required adds a "required" validation to the attribute.
   575  // See http://json-schema.org/latest/json-schema-validation.html#anchor61.
   576  func Required(names ...string) {
   577  	var at *design.AttributeDefinition
   578  
   579  	switch def := dslengine.CurrentDefinition().(type) {
   580  	case *design.AttributeDefinition:
   581  		at = def
   582  	case *design.MediaTypeDefinition:
   583  		at = def.AttributeDefinition
   584  	default:
   585  		dslengine.IncompatibleDSL()
   586  		return
   587  	}
   588  
   589  	if at.Type != nil && at.Type.Kind() != design.ObjectKind {
   590  		incompatibleAttributeType("required", at.Type.Name(), "an object")
   591  	} else {
   592  		if at.Validation == nil {
   593  			at.Validation = &dslengine.ValidationDefinition{}
   594  		}
   595  		at.Validation.AddRequired(names)
   596  	}
   597  }
   598  
   599  // incompatibleAttributeType reports an error for validations defined on
   600  // incompatible attributes (e.g. max value on string).
   601  func incompatibleAttributeType(validation, actual, expected string) {
   602  	dslengine.ReportError("invalid %s validation definition: attribute must be %s (but type is %s)",
   603  		validation, expected, actual)
   604  }
   605  
   606  // qualifiedTypeName returns the qualified type name for the given data type.
   607  // This is useful in reporting types in error messages.
   608  // (e.g) array<string>, hash<string, string>, hash<string, array<int>>
   609  func qualifiedTypeName(t design.DataType) string {
   610  	switch t.Kind() {
   611  	case design.DateTimeKind:
   612  		return "datetime"
   613  	case design.ArrayKind:
   614  		return fmt.Sprintf("%s<%s>", t.Name(), qualifiedTypeName(t.ToArray().ElemType.Type))
   615  	case design.HashKind:
   616  		h := t.ToHash()
   617  		return fmt.Sprintf("%s<%s, %s>",
   618  			t.Name(),
   619  			qualifiedTypeName(h.KeyType.Type),
   620  			qualifiedTypeName(h.ElemType.Type),
   621  		)
   622  	}
   623  	return t.Name()
   624  }