github.com/goldeneggg/goa@v1.3.1/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  	case design.Object:
   166  		if att, ok := t[name]; ok {
   167  			return design.DupAtt(att)
   168  		}
   169  	}
   170  	return nil
   171  }
   172  
   173  func parseAttributeArgs(baseAttr *design.AttributeDefinition, args ...interface{}) (design.DataType, string, func()) {
   174  	var (
   175  		dataType    design.DataType
   176  		description string
   177  		dsl         func()
   178  		ok          bool
   179  	)
   180  
   181  	parseDataType := func(expected string, index int) {
   182  		if name, ok2 := args[index].(string); ok2 {
   183  			// Lookup type by name
   184  			if dataType, ok = design.Design.Types[name]; !ok {
   185  				var mt *design.MediaTypeDefinition
   186  				if mt = design.Design.MediaTypeWithIdentifier(name); mt == nil {
   187  					dataType = design.String // not nil to avoid panics
   188  					dslengine.InvalidArgError(expected, args[index])
   189  				} else {
   190  					dataType = mt
   191  				}
   192  			}
   193  			return
   194  		}
   195  		if dataType, ok = args[index].(design.DataType); !ok {
   196  			dslengine.InvalidArgError(expected, args[index])
   197  		}
   198  	}
   199  	parseDescription := func(expected string, index int) {
   200  		if description, ok = args[index].(string); !ok {
   201  			dslengine.InvalidArgError(expected, args[index])
   202  		}
   203  	}
   204  	parseDSL := func(index int, success, failure func()) {
   205  		if dsl, ok = args[index].(func()); ok {
   206  			success()
   207  		} else {
   208  			failure()
   209  		}
   210  	}
   211  
   212  	success := func() {}
   213  
   214  	switch len(args) {
   215  	case 0:
   216  		if baseAttr != nil {
   217  			dataType = baseAttr.Type
   218  		} else {
   219  			dataType = design.String
   220  		}
   221  	case 1:
   222  		success = func() {
   223  			if baseAttr != nil {
   224  				dataType = baseAttr.Type
   225  			}
   226  		}
   227  		parseDSL(0, success, func() { parseDataType("type, type name or func()", 0) })
   228  	case 2:
   229  		parseDataType("type or type name", 0)
   230  		parseDSL(1, success, func() { parseDescription("string or func()", 1) })
   231  	case 3:
   232  		parseDataType("type or type name", 0)
   233  		parseDescription("string", 1)
   234  		parseDSL(2, success, func() { dslengine.InvalidArgError("func()", args[2]) })
   235  	default:
   236  		dslengine.ReportError("too many arguments in call to Attribute")
   237  	}
   238  
   239  	return dataType, description, dsl
   240  }
   241  
   242  // Header is an alias of Attribute for the most part.
   243  //
   244  // Within an APIKeySecurity or JWTSecurity definition, Header
   245  // defines that an implementation must check the given header to get
   246  // the API Key.  In this case, no `args` parameter is necessary.
   247  func Header(name string, args ...interface{}) {
   248  	if _, ok := dslengine.CurrentDefinition().(*design.SecuritySchemeDefinition); ok {
   249  		if len(args) != 0 {
   250  			dslengine.ReportError("do not specify args")
   251  			return
   252  		}
   253  		inHeader(name)
   254  		return
   255  	}
   256  
   257  	Attribute(name, args...)
   258  }
   259  
   260  // Member is an alias of Attribute.
   261  func Member(name string, args ...interface{}) {
   262  	Attribute(name, args...)
   263  }
   264  
   265  // Param is an alias of Attribute.
   266  func Param(name string, args ...interface{}) {
   267  	Attribute(name, args...)
   268  }
   269  
   270  // Default sets the default value for an attribute.
   271  // See http://json-schema.org/latest/json-schema-validation.html#anchor10.
   272  func Default(def interface{}) {
   273  	if a, ok := attributeDefinition(); ok {
   274  		if a.Type != nil {
   275  			if !a.Type.CanHaveDefault() {
   276  				dslengine.ReportError("%s type cannot have a default value", qualifiedTypeName(a.Type))
   277  			} else if !a.Type.IsCompatible(def) {
   278  				dslengine.ReportError("default value %#v is incompatible with attribute of type %s",
   279  					def, qualifiedTypeName(a.Type))
   280  			} else {
   281  				a.SetDefault(def)
   282  			}
   283  		} else {
   284  			a.SetDefault(def)
   285  		}
   286  	}
   287  }
   288  
   289  // Example sets the example of an attribute to be used for the documentation:
   290  //
   291  //	Attributes(func() {
   292  //		Attribute("ID", Integer, func() {
   293  //			Example(1)
   294  //		})
   295  //		Attribute("name", String, func() {
   296  //			Example("Cabernet Sauvignon")
   297  //		})
   298  //		Attribute("price", String) //If no Example() is provided, goa generates one that fits your specification
   299  //	})
   300  //
   301  // If you do not want an auto-generated example for an attribute, add NoExample() to it.
   302  func Example(exp interface{}) {
   303  	if a, ok := attributeDefinition(); ok {
   304  		if pass := a.SetExample(exp); !pass {
   305  			dslengine.ReportError("example value %#v is incompatible with attribute of type %s",
   306  				exp, a.Type.Name())
   307  		}
   308  	}
   309  }
   310  
   311  // NoExample sets the example of an attribute to be blank for the documentation. It is used when
   312  // users don't want any custom or auto-generated example
   313  func NoExample() {
   314  	switch def := dslengine.CurrentDefinition().(type) {
   315  	case *design.APIDefinition:
   316  		def.NoExamples = true
   317  	case *design.AttributeDefinition:
   318  		def.SetExample(nil)
   319  	default:
   320  		dslengine.IncompatibleDSL()
   321  	}
   322  }
   323  
   324  // Enum adds a "enum" validation to the attribute.
   325  // See http://json-schema.org/latest/json-schema-validation.html#anchor76.
   326  func Enum(val ...interface{}) {
   327  	if a, ok := attributeDefinition(); ok {
   328  		ok := true
   329  		for i, v := range val {
   330  			// When can a.Type be nil? glad you asked
   331  			// There are two ways to write an Attribute declaration with the DSL that
   332  			// don't set the type: with one argument - just the name - in which case the type
   333  			// is set to String or with two arguments - the name and DSL. In this latter form
   334  			// the type can end up being either String - if the DSL does not define any
   335  			// attribute - or object if it does.
   336  			// Why allowing this? because it's not always possible to specify the type of an
   337  			// object - an object may just be declared inline to represent a substructure.
   338  			// OK then why not assuming object and not allowing for string? because the DSL
   339  			// where there's only one argument and the type is string implicitly is very
   340  			// useful and common, for example to list attributes that refer to other attributes
   341  			// such as responses that refer to responses defined at the API level or links that
   342  			// refer to the media type attributes. So if the form that takes a DSL always ended
   343  			// up defining an object we'd have a weird situation where one arg is string and
   344  			// two args is object. Breaks the least surprise principle. Soooo long story
   345  			// short the lesser evil seems to be to allow the ambiguity. Also tests like the
   346  			// one below are really a convenience to the user and not a fundamental feature
   347  			// - not checking in the case the type is not known yet is OK.
   348  			if a.Type != nil && !a.Type.IsCompatible(v) {
   349  				dslengine.ReportError("value %#v at index %d is incompatible with attribute of type %s",
   350  					v, i, a.Type.Name())
   351  				ok = false
   352  			}
   353  		}
   354  		if ok {
   355  			a.AddValues(val)
   356  		}
   357  	}
   358  }
   359  
   360  // SupportedValidationFormats lists the supported formats for use with the
   361  // Format DSL.
   362  var SupportedValidationFormats = []string{
   363  	"cidr",
   364  	"date-time",
   365  	"email",
   366  	"hostname",
   367  	"ipv4",
   368  	"ipv6",
   369  	"ip",
   370  	"mac",
   371  	"regexp",
   372  	"rfc1123",
   373  	"uri",
   374  }
   375  
   376  // Format adds a "format" validation to the attribute.
   377  // See http://json-schema.org/latest/json-schema-validation.html#anchor104.
   378  // The formats supported by goa are:
   379  //
   380  // "date-time": RFC3339 date time
   381  //
   382  // "email": RFC5322 email address
   383  //
   384  // "hostname": RFC1035 internet host name
   385  //
   386  // "ipv4", "ipv6", "ip": RFC2373 IPv4, IPv6 address or either
   387  //
   388  // "uri": RFC3986 URI
   389  //
   390  // "mac": IEEE 802 MAC-48, EUI-48 or EUI-64 MAC address
   391  //
   392  // "cidr": RFC4632 or RFC4291 CIDR notation IP address
   393  //
   394  // "regexp": RE2 regular expression
   395  //
   396  // "rfc1123": RFC1123 date time
   397  func Format(f string) {
   398  	if a, ok := attributeDefinition(); ok {
   399  		if a.Type != nil && a.Type.Kind() != design.StringKind {
   400  			incompatibleAttributeType("format", a.Type.Name(), "a string")
   401  		} else {
   402  			supported := false
   403  			for _, s := range SupportedValidationFormats {
   404  				if s == f {
   405  					supported = true
   406  					break
   407  				}
   408  			}
   409  			if !supported {
   410  				dslengine.ReportError("unsupported format %#v, supported formats are: %s",
   411  					f, strings.Join(SupportedValidationFormats, ", "))
   412  			} else {
   413  				if a.Validation == nil {
   414  					a.Validation = &dslengine.ValidationDefinition{}
   415  				}
   416  				a.Validation.Format = f
   417  			}
   418  		}
   419  	}
   420  }
   421  
   422  // Pattern adds a "pattern" validation to the attribute.
   423  // See http://json-schema.org/latest/json-schema-validation.html#anchor33.
   424  func Pattern(p string) {
   425  	if a, ok := attributeDefinition(); ok {
   426  		if a.Type != nil && a.Type.Kind() != design.StringKind {
   427  			incompatibleAttributeType("pattern", a.Type.Name(), "a string")
   428  		} else {
   429  			_, err := regexp.Compile(p)
   430  			if err != nil {
   431  				dslengine.ReportError("invalid pattern %#v, %s", p, err)
   432  			} else {
   433  				if a.Validation == nil {
   434  					a.Validation = &dslengine.ValidationDefinition{}
   435  				}
   436  				a.Validation.Pattern = p
   437  			}
   438  		}
   439  	}
   440  }
   441  
   442  // Minimum adds a "minimum" validation to the attribute.
   443  // See http://json-schema.org/latest/json-schema-validation.html#anchor21.
   444  func Minimum(val interface{}) {
   445  	if a, ok := attributeDefinition(); ok {
   446  		if a.Type != nil && a.Type.Kind() != design.IntegerKind && a.Type.Kind() != design.NumberKind {
   447  			incompatibleAttributeType("minimum", a.Type.Name(), "an integer or a number")
   448  		} else {
   449  			var f float64
   450  			switch v := val.(type) {
   451  			case float32, float64, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64:
   452  				f = reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0.0))).Float()
   453  			case string:
   454  				var err error
   455  				f, err = strconv.ParseFloat(v, 64)
   456  				if err != nil {
   457  					dslengine.ReportError("invalid number value %#v", v)
   458  					return
   459  				}
   460  			default:
   461  				dslengine.ReportError("invalid number value %#v", v)
   462  				return
   463  			}
   464  			if a.Validation == nil {
   465  				a.Validation = &dslengine.ValidationDefinition{}
   466  			}
   467  			a.Validation.Minimum = &f
   468  		}
   469  	}
   470  }
   471  
   472  // Maximum adds a "maximum" validation to the attribute.
   473  // See http://json-schema.org/latest/json-schema-validation.html#anchor17.
   474  func Maximum(val interface{}) {
   475  	if a, ok := attributeDefinition(); ok {
   476  		if a.Type != nil && a.Type.Kind() != design.IntegerKind && a.Type.Kind() != design.NumberKind {
   477  			incompatibleAttributeType("maximum", a.Type.Name(), "an integer or a number")
   478  		} else {
   479  			var f float64
   480  			switch v := val.(type) {
   481  			case float32, float64, int, int8, int16, int32, int64, uint8, uint16, uint32, uint64:
   482  				f = reflect.ValueOf(v).Convert(reflect.TypeOf(float64(0.0))).Float()
   483  			case string:
   484  				var err error
   485  				f, err = strconv.ParseFloat(v, 64)
   486  				if err != nil {
   487  					dslengine.ReportError("invalid number value %#v", v)
   488  					return
   489  				}
   490  			default:
   491  				dslengine.ReportError("invalid number value %#v", v)
   492  				return
   493  			}
   494  			if a.Validation == nil {
   495  				a.Validation = &dslengine.ValidationDefinition{}
   496  			}
   497  			a.Validation.Maximum = &f
   498  		}
   499  	}
   500  }
   501  
   502  // MinLength adds a "minItems" validation to the attribute.
   503  // See http://json-schema.org/latest/json-schema-validation.html#anchor45.
   504  func MinLength(val int) {
   505  	if a, ok := attributeDefinition(); ok {
   506  		if a.Type != nil && a.Type.Kind() != design.StringKind && a.Type.Kind() != design.ArrayKind && a.Type.Kind() != design.HashKind {
   507  			incompatibleAttributeType("minimum length", a.Type.Name(), "a string or an array")
   508  		} else {
   509  			if a.Validation == nil {
   510  				a.Validation = &dslengine.ValidationDefinition{}
   511  			}
   512  			a.Validation.MinLength = &val
   513  		}
   514  	}
   515  }
   516  
   517  // MaxLength adds a "maxItems" validation to the attribute.
   518  // See http://json-schema.org/latest/json-schema-validation.html#anchor42.
   519  func MaxLength(val int) {
   520  	if a, ok := attributeDefinition(); ok {
   521  		if a.Type != nil && a.Type.Kind() != design.StringKind && a.Type.Kind() != design.ArrayKind {
   522  			incompatibleAttributeType("maximum length", a.Type.Name(), "a string or an array")
   523  		} else {
   524  			if a.Validation == nil {
   525  				a.Validation = &dslengine.ValidationDefinition{}
   526  			}
   527  			a.Validation.MaxLength = &val
   528  		}
   529  	}
   530  }
   531  
   532  // Required adds a "required" validation to the attribute.
   533  // See http://json-schema.org/latest/json-schema-validation.html#anchor61.
   534  func Required(names ...string) {
   535  	var at *design.AttributeDefinition
   536  
   537  	switch def := dslengine.CurrentDefinition().(type) {
   538  	case *design.AttributeDefinition:
   539  		at = def
   540  	case *design.MediaTypeDefinition:
   541  		at = def.AttributeDefinition
   542  	default:
   543  		dslengine.IncompatibleDSL()
   544  		return
   545  	}
   546  
   547  	if at.Type != nil && at.Type.Kind() != design.ObjectKind {
   548  		incompatibleAttributeType("required", at.Type.Name(), "an object")
   549  	} else {
   550  		if at.Validation == nil {
   551  			at.Validation = &dslengine.ValidationDefinition{}
   552  		}
   553  		at.Validation.AddRequired(names)
   554  	}
   555  }
   556  
   557  // incompatibleAttributeType reports an error for validations defined on
   558  // incompatible attributes (e.g. max value on string).
   559  func incompatibleAttributeType(validation, actual, expected string) {
   560  	dslengine.ReportError("invalid %s validation definition: attribute must be %s (but type is %s)",
   561  		validation, expected, actual)
   562  }
   563  
   564  // qualifiedTypeName returns the qualified type name for the given data type.
   565  // This is useful in reporting types in error messages.
   566  // (e.g) array<string>, hash<string, string>, hash<string, array<int>>
   567  func qualifiedTypeName(t design.DataType) string {
   568  	switch t.Kind() {
   569  	case design.DateTimeKind:
   570  		return "datetime"
   571  	case design.ArrayKind:
   572  		return fmt.Sprintf("%s<%s>", t.Name(), qualifiedTypeName(t.ToArray().ElemType.Type))
   573  	case design.HashKind:
   574  		h := t.ToHash()
   575  		return fmt.Sprintf("%s<%s, %s>",
   576  			t.Name(),
   577  			qualifiedTypeName(h.KeyType.Type),
   578  			qualifiedTypeName(h.ElemType.Type),
   579  		)
   580  	}
   581  	return t.Name()
   582  }