meta.go raw

   1  package data
   2  
   3  import (
   4  	"bytes"
   5  	"encoding/json"
   6  	"fmt"
   7  	"net"
   8  	"reflect"
   9  	"sort"
  10  	"strconv"
  11  	"strings"
  12  )
  13  
  14  // FeedPtr represents the dynamic metadata value in which a feed is providing the value.
  15  type FeedPtr struct {
  16  	FeedID string `json:"feed,omitempty"`
  17  }
  18  
  19  // PulsarMeta is currently only used for validation
  20  type PulsarMeta struct {
  21  	JobID     string  `json:"job_id,omitempty"`
  22  	Bias      string  `json:"bias,omitempty"`
  23  	A5MCutoff float64 `json:"a5m_cutoff,omitempty"`
  24  }
  25  
  26  // Meta contains information on an entity's metadata table. Metadata key/value
  27  // pairs are used by a record's filter pipeline during a dns query.
  28  // All values can be a feed id as well, indicating real-time updates of these values.
  29  // Structure/Precedence of metadata tables:
  30  //   - Record
  31  //   - Meta <- lowest precedence in filter
  32  //   - Region(s)
  33  //   - Meta <- middle precedence in filter chain
  34  //   - ...
  35  //   - Answer(s)
  36  //   - Meta <- highest precedence in filter chain
  37  //   - ...
  38  //   - ...
  39  type Meta struct {
  40  	// STATUS
  41  
  42  	// Indicates whether or not entity is considered 'up'
  43  	// bool or FeedPtr.
  44  	Up interface{} `json:"up,omitempty"`
  45  
  46  	// Indicates the number of active connections.
  47  	// Values must be positive.
  48  	// int or FeedPtr.
  49  	Connections interface{} `json:"connections,omitempty"`
  50  
  51  	// Indicates the number of active requests (HTTP or otherwise).
  52  	// Values must be positive.
  53  	// int or FeedPtr.
  54  	Requests interface{} `json:"requests,omitempty"`
  55  
  56  	// Indicates the "load average".
  57  	// Values must be positive, and will be rounded to the nearest tenth.
  58  	// float64 or FeedPtr.
  59  	LoadAvg interface{} `json:"loadavg,omitempty"`
  60  
  61  	// The Job ID of a Pulsar telemetry gathering job and associated metadata.
  62  	// list of PulsarMeta
  63  	Pulsar interface{} `json:"pulsar,omitempty"`
  64  
  65  	// GEOGRAPHICAL
  66  
  67  	// Must be between -180.0 and +180.0 where negative
  68  	// indicates South and positive indicates North.
  69  	// e.g., the longitude of the datacenter where a server resides.
  70  	// float64 or FeedPtr.
  71  	Latitude interface{} `json:"latitude,omitempty"`
  72  
  73  	// Must be between -180.0 and +180.0 where negative
  74  	// indicates West and positive indicates East.
  75  	// e.g., the longitude of the datacenter where a server resides.
  76  	// float64 or FeedPtr.
  77  	Longitude interface{} `json:"longitude,omitempty"`
  78  
  79  	// Valid geographic regions are: 'US-EAST', 'US-CENTRAL', 'US-WEST',
  80  	// 'EUROPE', 'ASIAPAC', 'SOUTH-AMERICA', 'AFRICA'.
  81  	// e.g., the rough geographic location of the Datacenter where a server resides.
  82  	// []string or FeedPtr.
  83  	Georegion interface{} `json:"georegion,omitempty"`
  84  
  85  	// Countr(ies) must be specified as ISO3166 2-character country code(s).
  86  	// []string or FeedPtr.
  87  	Country interface{} `json:"country,omitempty"`
  88  
  89  	// State(s) must be specified as standard 2-character state code(s).
  90  	// []string or FeedPtr.
  91  	USState interface{} `json:"us_state,omitempty"`
  92  
  93  	// Canadian Province(s) must be specified as standard 2-character province
  94  	// code(s).
  95  	// []string or FeedPtr.
  96  	CAProvince interface{} `json:"ca_province,omitempty"`
  97  
  98  	// INFORMATIONAL
  99  
 100  	// Notes to indicate any necessary details for operators.
 101  	// Up to 256 characters in length.
 102  	// string or FeedPtr.
 103  	Note interface{} `json:"note,omitempty"`
 104  
 105  	// NETWORK
 106  
 107  	// IP (v4 and v6) prefixes in CIDR format ("a.b.c.d/mask").
 108  	// May include up to 1000 prefixes.
 109  	// e.g., "1.2.3.4/24"
 110  	// []string or FeedPtr.
 111  	IPPrefixes interface{} `json:"ip_prefixes,omitempty"`
 112  
 113  	// Autonomous System (AS) number(s).
 114  	// May include up to 1000 AS numbers.
 115  	// []string or FeedPtr.
 116  	ASN interface{} `json:"asn,omitempty"`
 117  
 118  	// TRAFFIC
 119  
 120  	// Indicates the "priority tier".
 121  	// Lower values indicate higher priority.
 122  	// Values must be positive.
 123  	// int or FeedPtr.
 124  	Priority interface{} `json:"priority,omitempty"`
 125  
 126  	// Indicates a weight.
 127  	// Filters that use weights normalize them.
 128  	// Any positive values are allowed.
 129  	// Values between 0 and 100 are recommended for simplicity's sake.
 130  	// float64 or FeedPtr.
 131  	Weight interface{} `json:"weight,omitempty"`
 132  
 133  	// Indicates a cost.
 134  	// Filters that use costs normalize them.
 135  	// Any positive values are allowed.
 136  	// float64 or FeedPtr.
 137  	Cost interface{} `json:"cost,omitempty"`
 138  
 139  	// Indicates a "low watermark" to use for load shedding.
 140  	// The value should depend on the metric used to determine
 141  	// load (e.g., loadavg, connections, etc).
 142  	// int or FeedPtr.
 143  	LowWatermark interface{} `json:"low_watermark,omitempty"`
 144  
 145  	// Indicates a "high watermark" to use for load shedding.
 146  	// The value should depend on the metric used to determine
 147  	// load (e.g., loadavg, connections, etc).
 148  	// int or FeedPtr.
 149  	HighWatermark interface{} `json:"high_watermark,omitempty"`
 150  
 151  	// subdivisions must follow the ISO-3166-2 code for a country and subdivisions
 152  	// map[string]interface{} or FeedPtr.
 153  	Subdivisions interface{} `json:"subdivisions,omitempty"`
 154  
 155  	AdditionalMetadata interface{} `json:"additional_metadata,omitempty"`
 156  }
 157  
 158  // StringMap returns a map[string]interface{} representation of metadata (for use with terraform in nested structures)
 159  func (meta *Meta) StringMap() map[string]interface{} {
 160  	m := make(map[string]interface{})
 161  	v := reflect.Indirect(reflect.ValueOf(meta))
 162  	t := v.Type()
 163  	for i := 0; i < t.NumField(); i++ {
 164  		f := t.Field(i)
 165  		fv := v.Field(i)
 166  		if fv.IsNil() {
 167  			continue
 168  		}
 169  		tag := f.Tag.Get("json")
 170  
 171  		tag = strings.Split(tag, ",")[0]
 172  
 173  		m[tag] = FormatInterface(fv.Interface())
 174  	}
 175  	return m
 176  }
 177  
 178  // FormatInterface takes an interface of types: string, bool, int, float64, []string, map[string]interface{} and FeedPtr, and returns a string representation of said interface
 179  func FormatInterface(i interface{}) string {
 180  	switch v := i.(type) {
 181  	case string:
 182  		return v
 183  	case bool:
 184  		if v {
 185  			return "1"
 186  		}
 187  		return "0"
 188  	case int:
 189  		return strconv.FormatInt(int64(v), 10)
 190  	case float64:
 191  		return strconv.FormatFloat(v, 'f', -1, 64)
 192  	case []string:
 193  		return strings.Join(v, ",")
 194  	case []interface{}:
 195  		slc := make([]string, 0)
 196  		for _, s := range v {
 197  			switch ss := s.(type) {
 198  			// Pulsar
 199  			case map[string]interface{}:
 200  				data, _ := json.Marshal(v)
 201  				return string(data)
 202  			case string:
 203  				slc = append(slc, ss)
 204  			// The ASN field specifically is returned from the API as an integer,
 205  			// which Go treats as a float64 when it parses the json,
 206  			// so this is to account for that field.
 207  			case float64:
 208  				slc = append(slc, strconv.FormatFloat(ss, 'f', -1, 64))
 209  			}
 210  		}
 211  		return strings.Join(slc, ",")
 212  	case map[string]interface{}:
 213  		// Required for Terraform workaround to allow users to submit raw json of feed pointer
 214  		// as value for metadata.  See https://github.com/terraform-providers/terraform-provider-ns1/issues/35
 215  		if val, ok := v["feed"].(string); ok {
 216  			feedPtr := FeedPtr{FeedID: val}
 217  			data, _ := json.Marshal(feedPtr)
 218  			return string(data)
 219  		}
 220  		data, _ := json.Marshal(v)
 221  		return string(data)
 222  	case FeedPtr:
 223  		data, _ := json.Marshal(v)
 224  		return string(data)
 225  	default:
 226  		panic(fmt.Sprintf("expected v to be convertible to a string, got: %+v, %T", v, v))
 227  	}
 228  }
 229  
 230  // ParseType returns an interface containing a string, bool, int, float64, []string, or FeedPtr
 231  // float64 values with no decimal may be returned as integers, but that should be ok because the api won't know the difference
 232  // when it's json encoded
 233  func ParseType(s string) interface{} {
 234  	slc := strings.Split(s, ",")
 235  	if len(slc) > 1 {
 236  		sort.Strings(slc)
 237  		return slc
 238  	}
 239  
 240  	feedptr := FeedPtr{}
 241  	err := json.Unmarshal([]byte(s), &feedptr)
 242  	if err == nil {
 243  		return feedptr
 244  	}
 245  
 246  	f, err := strconv.ParseFloat(s, 64)
 247  	if err == nil {
 248  		if !isIntegral(f) {
 249  			return f
 250  		}
 251  		return int(f)
 252  	}
 253  
 254  	return s
 255  }
 256  
 257  func isIntegral(f float64) bool {
 258  	return f == float64(int(f))
 259  }
 260  
 261  // MetaFromMap creates a *Meta and uses reflection to set fields from a map. This will panic if a value for a key is not a string.
 262  // This it to ensure compatibility with terraform
 263  func MetaFromMap(m map[string]interface{}) *Meta {
 264  	meta := &Meta{}
 265  	mv := reflect.Indirect(reflect.ValueOf(meta))
 266  	mt := mv.Type()
 267  	for k, v := range m {
 268  		name := ToCamel(k)
 269  		switch name {
 270  		case "UsState":
 271  			name = "USState"
 272  		case "Loadavg":
 273  			name = "LoadAvg"
 274  		case "CaProvince":
 275  			name = "CAProvince"
 276  		case "IpPrefixes":
 277  			name = "IPPrefixes"
 278  		case "Asn":
 279  			name = "ASN"
 280  		}
 281  		if _, ok := mt.FieldByName(name); ok {
 282  			fv := mv.FieldByName(name)
 283  			switch name {
 284  			case "Up":
 285  				if v.(string) == "1" || strings.ToLower(v.(string)) == "true" {
 286  					fv.Set(reflect.ValueOf(true))
 287  				} else if v.(string) == "0" || strings.ToLower(v.(string)) == "false" {
 288  					fv.Set(reflect.ValueOf(false))
 289  				} else {
 290  					fv.Set(reflect.ValueOf(ParseType(v.(string))))
 291  				}
 292  			case "ASN":
 293  				// If there is only one ASN, it should still be treated as a string.-
 294  				// otherwise this gets parsed into a float64 and breaks stuff.
 295  				i := strings.Index(v.(string), ",")
 296  				if i == -1 {
 297  					fv.Set(reflect.ValueOf(v.(string)))
 298  				} else {
 299  					fv.Set(reflect.ValueOf(ParseType(v.(string))))
 300  				}
 301  			case "Pulsar":
 302  				var pulsars []map[string]interface{}
 303  				if err := json.Unmarshal([]byte(v.(string)), &pulsars); err == nil {
 304  					fv.Set(reflect.ValueOf(pulsars))
 305  				}
 306  			case "Subdivisions":
 307  				switch v.(type) {
 308  				case string:
 309  					var subMap map[string]interface{}
 310  					json.Unmarshal([]byte(v.(string)), &subMap)
 311  					fv.Set(reflect.ValueOf(subMap))
 312  				case map[string]interface{}:
 313  					fv.Set(reflect.ValueOf(v.(map[string]interface{})))
 314  				}
 315  			case "Note":
 316  				// If it's a Note, just pass the string without any type of parse.
 317  				fv.Set(reflect.ValueOf(v.(string)))
 318  			case "AdditionalMetadata":
 319  				var additional []map[string]interface{}
 320  				if err := json.Unmarshal([]byte(v.(string)), &additional); err == nil {
 321  					fv.Set(reflect.ValueOf(additional))
 322  				}
 323  			default:
 324  				fv.Set(reflect.ValueOf(ParseType(v.(string))))
 325  			}
 326  		}
 327  	}
 328  	return meta
 329  }
 330  
 331  // metaValidation is a validation struct for a metadata field.
 332  // It contains the kinds of types that the field can be, and a list of check functions that will run on the field
 333  type metaValidation struct {
 334  	kinds      []reflect.Kind
 335  	checkFuncs []func(v reflect.Value) error
 336  }
 337  
 338  // validateLatLong makes sure that the given lat/long is within the range 180.0 to -180.0
 339  func validateLatLong(v reflect.Value) error {
 340  	if v.Kind() == reflect.Float64 {
 341  		f := v.Interface().(float64)
 342  		if f < -180.0 || f > 180.0 {
 343  			return fmt.Errorf("latitude/longitude values must be between -180.0 and 180.0, got %f", f)
 344  		}
 345  	}
 346  	return nil
 347  }
 348  
 349  // validateCidr makes sure that the given string is a valid cidr
 350  func validateCidr(v reflect.Value) error {
 351  	if v.Kind() == reflect.String {
 352  		s := v.Interface().(string)
 353  		_, _, err := net.ParseCIDR(s)
 354  		if err != nil {
 355  			return err
 356  		}
 357  	}
 358  	if v.Kind() == reflect.Slice {
 359  		if slc, ok := v.Interface().([]string); ok {
 360  			for _, s := range slc {
 361  				_, _, err := net.ParseCIDR(s)
 362  				if err != nil {
 363  					return fmt.Errorf("%s is not a valid CIDR block", s)
 364  				}
 365  			}
 366  			return nil
 367  		}
 368  		slc := v.Interface().([]interface{})
 369  		for _, s := range slc {
 370  			_, _, err := net.ParseCIDR(s.(string))
 371  			if err != nil {
 372  				return fmt.Errorf("%s is not a valid CIDR block", s.(string))
 373  			}
 374  		}
 375  	}
 376  	return nil
 377  }
 378  
 379  // validatePositiveNumber makes sure that the given number (float or int) is positive
 380  func validatePositiveNumber(fieldName string, v reflect.Value) error {
 381  	i := 0
 382  	if v.Kind() == reflect.Int {
 383  		i = v.Interface().(int)
 384  
 385  	}
 386  
 387  	if v.Kind() == reflect.Float64 {
 388  		i = int(v.Interface().(float64))
 389  	}
 390  
 391  	if i < 0 {
 392  		return fmt.Errorf("%s must be a positive number, was %+v", fieldName, v.Interface())
 393  	}
 394  
 395  	return nil
 396  }
 397  
 398  // geoMap is a map of all of the georegions
 399  var geoMap = map[string]struct{}{
 400  	"US-EAST": {}, "US-CENTRAL": {}, "US-WEST": {},
 401  	"EUROPE": {}, "ASIAPAC": {}, "SOUTH-AMERICA": {}, "AFRICA": {},
 402  }
 403  
 404  // geoKeyString returns a string representation of all of the georegions
 405  func geoKeyString() string {
 406  	length := 0
 407  	slc := make([]string, 0)
 408  	for k := range geoMap {
 409  		slc = append(slc, k)
 410  		length += len(k) + 1
 411  	}
 412  	sort.Strings(slc)
 413  
 414  	b := bytes.NewBuffer(make([]byte, 0, length-1))
 415  
 416  	for _, k := range slc {
 417  		b.WriteString(k + ",")
 418  	}
 419  
 420  	return strings.TrimRight(b.String(), ",")
 421  }
 422  
 423  // validateGeoregion makes sure that the given georegion is correct
 424  func validateGeoregion(v reflect.Value) error {
 425  	if v.Kind() == reflect.String {
 426  		s := v.String()
 427  		if _, ok := geoMap[s]; !ok {
 428  			return fmt.Errorf("georegion must be one or more of %s, found %s", geoKeyString(), s)
 429  		}
 430  	}
 431  
 432  	if v.Kind() == reflect.Slice {
 433  		if slc, ok := v.Interface().([]string); ok {
 434  			for _, s := range slc {
 435  				if _, ok := geoMap[s]; !ok {
 436  					return fmt.Errorf("georegion must be one or more of %s, found %s", geoKeyString(), s)
 437  				}
 438  			}
 439  			return nil
 440  		}
 441  		slc := v.Interface().([]interface{})
 442  		for _, s := range slc {
 443  			if _, ok := geoMap[s.(string)]; !ok {
 444  				return fmt.Errorf("georegion must be one or more of %s, found %s", geoKeyString(), s)
 445  			}
 446  		}
 447  	}
 448  	return nil
 449  }
 450  
 451  // validateCountryStateProvince makes sure that the given field only has two characters
 452  func validateCountryStateProvince(v reflect.Value) error {
 453  	if v.Kind() == reflect.String {
 454  		s := v.String()
 455  		if len(s) != 2 {
 456  			return fmt.Errorf("country/state/province codes must be 2 digits as specified in ISO3166/ISO3166-2, got: %s", s)
 457  		}
 458  	}
 459  
 460  	if v.Kind() == reflect.Slice {
 461  		if slc, ok := v.Interface().([]string); ok {
 462  			for _, s := range slc {
 463  				if len(s) != 2 {
 464  					return fmt.Errorf("country/state/province codes must be 2 digits as specified in ISO3166/ISO3166-2, got: %s", s)
 465  				}
 466  			}
 467  			return nil
 468  		}
 469  		slc := v.Interface().([]interface{})
 470  		for _, s := range slc {
 471  			if len(s.(string)) != 2 {
 472  				return fmt.Errorf("country/state/province codes must be 2 digits as specified in ISO3166/ISO3166-2, got: %s", s)
 473  			}
 474  		}
 475  	}
 476  	return nil
 477  }
 478  
 479  // validateNoteLength validates that a note's length is less than 256 characters
 480  func validateNoteLength(v reflect.Value) error {
 481  	if v.Kind() == reflect.String {
 482  		s := v.String()
 483  		if len(s) > 256 {
 484  			return fmt.Errorf("note length must be less than 256 characters, was %d", len(s))
 485  		}
 486  	}
 487  	return nil
 488  }
 489  
 490  func validatePulsar(v reflect.Value) error {
 491  	var pulsars []*PulsarMeta
 492  
 493  	switch v.Kind() {
 494  	case reflect.Slice:
 495  		// Slice from API
 496  		bs, err := json.Marshal(v.Interface())
 497  		if err != nil {
 498  			return fmt.Errorf("pulsar: unexpected value: `%v`", v.Interface())
 499  		}
 500  		if err := json.Unmarshal(bs, &pulsars); err != nil {
 501  			return fmt.Errorf("pulsar: invalid value: `%v`", v.Interface())
 502  		}
 503  	case reflect.String:
 504  		// String from terraform
 505  		if err := json.Unmarshal([]byte(v.String()), &pulsars); err != nil {
 506  			return fmt.Errorf("pulsar: invalid value: `%v`", v.String())
 507  		}
 508  	}
 509  
 510  	for _, p := range pulsars {
 511  		if p.JobID == "" {
 512  			return fmt.Errorf("pulsar Job ID is required")
 513  		}
 514  	}
 515  	return nil
 516  }
 517  
 518  func validateAdditionalMetadata(v reflect.Value) error {
 519  	// API expects additional_metadata to be array of length 1
 520  	if v.Len() > 1 {
 521  		return fmt.Errorf("unexpected length of `%d`, expected 1", v.Len())
 522  	}
 523  
 524  	return nil
 525  }
 526  
 527  // checkFuncs is shorthand for returning a slice of functions that take a reflect.Value and return an error
 528  func checkFuncs(f ...func(v reflect.Value) error) []func(v reflect.Value) error {
 529  	return f
 530  }
 531  
 532  // kinds is shorthand for returning a slice of reflect.Kind
 533  func kinds(k ...reflect.Kind) []reflect.Kind {
 534  	return k
 535  }
 536  
 537  // validationMap is a map of meta fields to validation types and functions
 538  var validationMap = map[string]metaValidation{
 539  	"Up": {kinds(reflect.Bool), nil},
 540  	"Connections": {kinds(reflect.Int), checkFuncs(
 541  		func(v reflect.Value) error {
 542  			return validatePositiveNumber("Connections", v)
 543  		})},
 544  	"Requests": {kinds(reflect.Int), checkFuncs(
 545  		func(v reflect.Value) error {
 546  			return validatePositiveNumber("Requests", v)
 547  		})},
 548  	"LoadAvg": {kinds(reflect.Float64, reflect.Int), checkFuncs(
 549  		func(v reflect.Value) error {
 550  			return validatePositiveNumber("LoadAvg", v)
 551  		})},
 552  	"Pulsar":     {kinds(reflect.String, reflect.Slice), checkFuncs(validatePulsar)},
 553  	"Latitude":   {kinds(reflect.Float64, reflect.Int), checkFuncs(validateLatLong)},
 554  	"Longitude":  {kinds(reflect.Float64, reflect.Int), checkFuncs(validateLatLong)},
 555  	"Georegion":  {kinds(reflect.String, reflect.Slice), checkFuncs(validateGeoregion)},
 556  	"Country":    {kinds(reflect.String, reflect.Slice), checkFuncs(validateCountryStateProvince)},
 557  	"USState":    {kinds(reflect.String, reflect.Slice), checkFuncs(validateCountryStateProvince)},
 558  	"CAProvince": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCountryStateProvince)},
 559  	"Note":       {kinds(reflect.String), checkFuncs(validateNoteLength)},
 560  	"IPPrefixes": {kinds(reflect.String, reflect.Slice), checkFuncs(validateCidr)},
 561  	"ASN":        {kinds(reflect.String, reflect.Slice), nil},
 562  	"Priority": {kinds(reflect.Int), checkFuncs(
 563  		func(v reflect.Value) error {
 564  			return validatePositiveNumber("Priority", v)
 565  		})},
 566  	"Weight": {kinds(reflect.Float64, reflect.Int), checkFuncs(
 567  		func(v reflect.Value) error {
 568  			return validatePositiveNumber("Weight", v)
 569  		})},
 570  	"Cost": {kinds(reflect.Float64, reflect.Int), checkFuncs(
 571  		func(v reflect.Value) error {
 572  			return validatePositiveNumber("Cost", v)
 573  		})},
 574  	"LowWatermark":       {kinds(reflect.Int), nil},
 575  	"HighWatermark":      {kinds(reflect.Int), nil},
 576  	"Subdivisions":       {kinds(reflect.String, reflect.Map), nil},
 577  	"AdditionalMetadata": {kinds(reflect.String, reflect.Slice), checkFuncs(validateAdditionalMetadata)},
 578  }
 579  
 580  // validate takes a field name, a reflect value, and metaValidation and validates the given field
 581  func validate(name string, v reflect.Value, m metaValidation) (errs []error) {
 582  
 583  	check := true
 584  	// if this is a FeedPtr or a *FeedPtr then we're ok, skip checking the rest of the types
 585  	if v.Kind() == reflect.Struct || v.Kind() == reflect.Invalid {
 586  		check = false
 587  	}
 588  
 589  	if check {
 590  		match := false
 591  		for _, k := range m.kinds {
 592  			if k == v.Kind() {
 593  				match = true
 594  			}
 595  		}
 596  
 597  		if !match {
 598  			errs = append(errs, fmt.Errorf("found type mismatch for meta field '%s'. expected %+v, got: %+v", name, m.kinds, v.Kind()))
 599  		}
 600  
 601  		for _, f := range m.checkFuncs {
 602  			err := f(v)
 603  			if err != nil {
 604  				errs = append(errs, err)
 605  			}
 606  		}
 607  	}
 608  
 609  	if v.Kind() == reflect.Struct {
 610  		if _, ok := v.Interface().(FeedPtr); !ok {
 611  			errs = append(errs, fmt.Errorf("if a meta field is a struct, it must be a FeedPtr, got: %s", v.Type()))
 612  		}
 613  	}
 614  
 615  	return
 616  }
 617  
 618  // Validate validates metadata fields and returns a list of errors if any are found
 619  func (meta *Meta) Validate() (errs []error) {
 620  	mv := reflect.Indirect(reflect.ValueOf(meta))
 621  	mt := mv.Type()
 622  	for i := 0; i < mt.NumField(); i++ {
 623  		fv := mt.Field(i)
 624  		err := validate(fv.Name, mv.Field(i).Elem(), validationMap[fv.Name])
 625  		if err != nil {
 626  			errs = append(errs, err...)
 627  		}
 628  	}
 629  
 630  	return errs
 631  }
 632