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