diff --git a/pkg/cloudcommon/validators/choices.go b/pkg/cloudcommon/validators/choices.go new file mode 100644 index 0000000000..83188a7ef0 --- /dev/null +++ b/pkg/cloudcommon/validators/choices.go @@ -0,0 +1,31 @@ +package validators + +import ( + "strings" +) + +type Empty struct{} +type Choices map[string]Empty + +func NewChoices(choices ...string) Choices { + cs := Choices{} + for _, choice := range choices { + cs[choice] = Empty{} + } + return cs +} + +func (cs Choices) Has(choice string) bool { + _, ok := cs[choice] + return ok +} + +func (cs Choices) String() string { + choices := make([]string, len(cs)) + i := 0 + for choice, _ := range cs { + choices[i] = choice + i++ + } + return strings.Join(choices, "|") +} diff --git a/pkg/cloudcommon/validators/doc.go b/pkg/cloudcommon/validators/doc.go new file mode 100644 index 0000000000..6a69b4ede8 --- /dev/null +++ b/pkg/cloudcommon/validators/doc.go @@ -0,0 +1 @@ +package validators // import "yunion.io/x/onecloud/pkg/cloudcommon/validators" diff --git a/pkg/cloudcommon/validators/errors.go b/pkg/cloudcommon/validators/errors.go new file mode 100644 index 0000000000..57c7271ece --- /dev/null +++ b/pkg/cloudcommon/validators/errors.go @@ -0,0 +1,106 @@ +package validators + +import ( + "fmt" +) + +type ErrType uintptr + +const ( + ERR_SUCCESS ErrType = iota + ERR_GENERAL // uncategorized error + ERR_MISSING_KEY + ERR_INVALID_TYPE + ERR_INVALID_CHOICE + ERR_NOT_IN_RANGE + ERR_INVALID_VALUE + ERR_MODEL_MANAGER + ERR_MODEL_NOT_FOUND +) + +var errTypeToString = map[ErrType]string{ + ERR_SUCCESS: "No error", + ERR_GENERAL: "General error", + ERR_MISSING_KEY: "Missing_key error", + ERR_INVALID_TYPE: "Invalid type error", + ERR_INVALID_CHOICE: "Invalid choice error", + ERR_NOT_IN_RANGE: "Not in range error", + ERR_INVALID_VALUE: "Invalid value error", + ERR_MODEL_MANAGER: "Model manager error", + ERR_MODEL_NOT_FOUND: "Model not found error", +} + +func (errType ErrType) String() string { + s, ok := errTypeToString[errType] + if ok { + return s + } + return "unknown error" +} + +type ValidateError struct { + ErrType ErrType + Msg string +} + +func (ve *ValidateError) Error() string { + return ve.Msg +} + +// TODO let each validator provide the error +func newMissingKeyError(key string) error { + msg := fmt.Sprintf("missing %q", key) + return newError(ERR_MISSING_KEY, msg) +} + +func newGeneralError(key string, err error) error { + msg := fmt.Sprintf("general error for %q: %s", key, err) + return newError(ERR_GENERAL, msg) +} + +func newInvalidTypeError(key string, typ string, err error) error { + msg := fmt.Sprintf("expecting %s type for %q: %s", typ, key, err) + return newError(ERR_INVALID_TYPE, msg) +} + +func newInvalidChoiceError(key string, choices Choices, choice string) error { + msg := fmt.Sprintf("invalid %q, want %s, got %s", key, choices, choice) + return newError(ERR_INVALID_CHOICE, msg) +} + +func newNotInRangeError(key string, value, lower, upper int64) error { + msg := fmt.Sprintf("invalid %q: %d, want [%d,%d]", key, value, lower, upper) + return newError(ERR_NOT_IN_RANGE, msg) +} + +func newInvalidValueError(key string, value string) error { + msg := fmt.Sprintf("invalid %q: %s", key, value) + return newError(ERR_INVALID_VALUE, msg) +} + +func newModelManagerError(modelKeyword string) error { + msg := fmt.Sprintf("internal error: getting model manager for %q failed", + modelKeyword) + return newError(ERR_MODEL_MANAGER, msg) +} + +func newModelNotFoundError(modelKeyword, idOrName string, err error) error { + msg := fmt.Sprintf("cannot find %q with id/name %q: %s", + modelKeyword, idOrName, err) + return newError(ERR_MODEL_NOT_FOUND, msg) +} + +func newError(typ ErrType, msg string) error { + return &ValidateError{ + ErrType: typ, + Msg: msg, + } +} + +func IsModelNotFoundError(err error) bool { + ve, ok := err.(*ValidateError) + if ok && ve.ErrType == ERR_MODEL_NOT_FOUND { + return true + } + return false +} diff --git a/pkg/cloudcommon/validators/validators.go b/pkg/cloudcommon/validators/validators.go new file mode 100644 index 0000000000..305beb3a2c --- /dev/null +++ b/pkg/cloudcommon/validators/validators.go @@ -0,0 +1,427 @@ +package validators + +// TODO +// +// email +// uuid +// uri + +import ( + "math" + "net" + "reflect" + "regexp" + + "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/pkg/gotypes" + "yunion.io/x/pkg/util/regutils" + "yunion.io/x/sqlchemy" +) + +type ValidatorFunc func(*jsonutils.JSONDict) error + +type IValidatorBase interface { + Validate(data *jsonutils.JSONDict) error +} + +type IValidator interface { + IValidatorBase + getValue() interface{} + setDefault(data *jsonutils.JSONDict) bool +} + +type Validator struct { + parent IValidator + Key string + optional bool + defaultVal interface{} + value jsonutils.JSONObject +} + +func (v *Validator) Optional(optional bool) IValidator { + v.optional = optional + return v.parent +} + +func (v *Validator) Default(defaultVal interface{}) IValidator { + v.defaultVal = defaultVal + return v.parent +} + +func (v *Validator) getValue() interface{} { + return nil +} + +func (v *Validator) setDefault(data *jsonutils.JSONDict) bool { + switch v.defaultVal.(type) { + case string: + s := v.defaultVal.(string) + v.value = jsonutils.NewString(s) + data.Set(v.Key, v.value) + return true + case int, int32, int64, uint, uint32, uint64: + value := reflect.ValueOf(v.defaultVal) + value64 := value.Convert(gotypes.Int64Type) + defaultVal64 := value64.Interface().(int64) + v.value = jsonutils.NewInt(defaultVal64) + data.Set(v.Key, v.value) + return true + } + return false +} + +func (v *Validator) Validate(data *jsonutils.JSONDict) error { + err, _ := v.validateEx(data) + return err +} + +func (v *Validator) validateEx(data *jsonutils.JSONDict) (err error, isSet bool) { + if !data.Contains(v.Key) { + if v.defaultVal != nil { + isSet = v.parent.setDefault(data) + return nil, isSet + } + if !v.optional { + err = newMissingKeyError(v.Key) + return + } + return + } + value, err := data.Get(v.Key) + if err != nil { + return + } + v.value = value + isSet = true + return +} + +type ValidatorStringChoices struct { + Validator + Choices Choices + defaultVal string + Value string +} + +func NewStringChoicesValidator(key string, choices Choices) *ValidatorStringChoices { + v := &ValidatorStringChoices{ + Validator: Validator{Key: key}, + Choices: choices, + } + v.parent = v + return v +} + +func (v *ValidatorStringChoices) Default(s string) IValidator { + if v.Choices.Has(s) { + return v.Validator.Default(s) + } + panic("invalid default for " + v.Key) +} + +func (v *ValidatorStringChoices) getValue() interface{} { + return v.Value +} + +func (v *ValidatorStringChoices) Validate(data *jsonutils.JSONDict) error { + if err, isSet := v.Validator.validateEx(data); err != nil || !isSet { + return err + } + s, err := v.value.GetString() + if err != nil { + return newGeneralError(v.Key, err) + } + if !v.Choices.Has(s) { + return newInvalidChoiceError(v.Key, v.Choices, s) + } + // in case it's stringified from v.value + data.Set(v.Key, jsonutils.NewString(s)) + v.Value = s + return nil +} + +type ValidatorRange struct { + Validator + Lower int64 + Upper int64 + Value int64 +} + +func (v *ValidatorRange) getValue() interface{} { + return v.Value +} + +func (v *ValidatorRange) Default(i int64) IValidator { + if i >= v.Lower && i <= v.Upper { + return v.Validator.Default(i) + } + panic("invalid default for " + v.Key) +} +func (v *ValidatorRange) Validate(data *jsonutils.JSONDict) error { + if err, isSet := v.Validator.validateEx(data); err != nil || !isSet { + return err + } + i, err := v.value.Int() + if err != nil { + return newInvalidTypeError(v.Key, "integer", err) + } + if i < v.Lower || i > v.Upper { + return newNotInRangeError(v.Key, i, v.Lower, v.Upper) + } + data.Set(v.Key, jsonutils.NewInt(i)) + v.Value = i + return nil +} + +func NewRangeValidator(key string, lower int64, upper int64) *ValidatorRange { + v := &ValidatorRange{ + Validator: Validator{Key: key}, + Lower: lower, + Upper: upper, + } + v.parent = v + return v +} + +func NewPortValidator(key string) *ValidatorRange { + return NewRangeValidator(key, 1, 65535) +} + +func NewNonNegativeValidator(key string) *ValidatorRange { + return NewRangeValidator(key, 0, math.MaxInt64) +} + +type ValidatorModelIdOrName struct { + Validator + ModelKeyword string + ProjectId string + ModelManager db.IModelManager + Model db.IModel + modelIdKey string +} + +func (v *ValidatorModelIdOrName) getValue() interface{} { + return v.Model +} + +func NewModelIdOrNameValidator(key string, modelKeyword string, projectId string) *ValidatorModelIdOrName { + v := &ValidatorModelIdOrName{ + Validator: Validator{Key: key}, + ProjectId: projectId, + ModelKeyword: modelKeyword, + modelIdKey: key + "_id", + } + v.parent = v + return v +} + +func (v *ValidatorModelIdOrName) ModelIdKey(modelIdKey string) *ValidatorModelIdOrName { + v.modelIdKey = modelIdKey + return v +} + +func (v *ValidatorModelIdOrName) validate(data *jsonutils.JSONDict) error { + if err, isSet := v.Validator.validateEx(data); err != nil || !isSet { + return err + } + modelIdOrName, err := v.value.GetString() + if err != nil { + return err + } + + modelManager := db.GetModelManager(v.ModelKeyword) + if modelManager == nil { + return newModelManagerError(v.ModelKeyword) + } + v.ModelManager = modelManager + model, err := modelManager.FetchByIdOrName(v.ProjectId, modelIdOrName) + if err != nil { + return newModelNotFoundError(v.ModelKeyword, modelIdOrName, err) + } + v.Model = model + return nil +} + +func (v *ValidatorModelIdOrName) Validate(data *jsonutils.JSONDict) error { + err := v.validate(data) + if err != nil { + return err + } + if v.Model != nil { + if len(v.modelIdKey) > 0 { + data.Remove(v.Key) + data.Set(v.modelIdKey, jsonutils.NewString(v.Model.GetId())) + } + } + return nil +} + +func (v *ValidatorModelIdOrName) QueryFilter(q *sqlchemy.SQuery, data *jsonutils.JSONDict) (*sqlchemy.SQuery, error) { + err := v.validate(data) + if err != nil { + if IsModelNotFoundError(err) { + // hack + q = q.Equals(v.modelIdKey, "0") + q = q.Equals(v.modelIdKey, "1") + return q, nil + } + return nil, err + } + if v.Model != nil { + q = q.Equals(v.modelIdKey, v.Model.GetId()) + } + return q, nil +} + +type ValidatorRegexp struct { + Validator + Regexp *regexp.Regexp + Value string + allowEmpty bool +} + +func (v *ValidatorRegexp) AllowEmpty(allowEmpty bool) *ValidatorRegexp { + v.allowEmpty = allowEmpty + return v +} + +func (v *ValidatorRegexp) getValue() interface{} { + return v.Value +} + +func (v *ValidatorRegexp) Validate(data *jsonutils.JSONDict) error { + if err, isSet := v.Validator.validateEx(data); err != nil || !isSet { + return err + } + value, err := v.value.GetString() + if err != nil { + return newInvalidTypeError(v.Key, "string", err) + } + if v.allowEmpty && len(value) == 0 { + return nil + } + if !v.Regexp.MatchString(value) { + return newInvalidValueError(v.Key, value) + } + v.Value = value + return nil +} + +func NewRegexpValidator(key string, regexp *regexp.Regexp) *ValidatorRegexp { + v := &ValidatorRegexp{ + Validator: Validator{Key: key}, + Regexp: regexp, + } + v.parent = v + return v +} + +type ValidatorDomainName struct { + ValidatorRegexp +} + +func NewDomainNameValidator(key string) *ValidatorDomainName { + v := &ValidatorDomainName{ + ValidatorRegexp: *NewRegexpValidator(key, regutils.DOMAINNAME_REG), + } + v.parent = v + return v +} + +type ValidatorURLPath struct { + ValidatorRegexp +} + +// URI Path as defined in https://tools.ietf.org/html/rfc3986#section-3.3 +var regexpURLPath = regexp.MustCompile(`^(?:/[a-zA-Z0-9.%$&'()*+,;=!~_-]*)*$`) + +func NewURLPathValidator(key string) *ValidatorURLPath { + v := &ValidatorURLPath{ + ValidatorRegexp: *NewRegexpValidator(key, regexpURLPath), + } + v.parent = v + return v +} + +type ValidatorStruct struct { + Validator + Value interface{} +} + +func (v *ValidatorStruct) getValue() interface{} { + return v.Value +} + +func (v *ValidatorStruct) Validate(data *jsonutils.JSONDict) error { + if err, isSet := v.Validator.validateEx(data); err != nil || !isSet { + return err + } + err := v.value.Unmarshal(v.Value) + if err != nil { + return newGeneralError(v.Key, err) + } + if valueValidator, ok := v.Value.(IValidatorBase); ok { + err = valueValidator.Validate(data) + if err != nil { + return err + } + } + return nil +} + +func NewStructValidator(key string, value interface{}) *ValidatorStruct { + v := &ValidatorStruct{ + Validator: Validator{Key: key}, + Value: value, + } + v.parent = v + return v +} + +type ValidatorIPv4Addr struct { + Validator + IP net.IP +} + +func (v *ValidatorIPv4Addr) getValue() interface{} { + return v.IP +} + +func (v *ValidatorIPv4Addr) setDefault(data *jsonutils.JSONDict) bool { + if v.defaultVal == nil { + return false + } + defaultIP, ok := v.defaultVal.(net.IP) + if !ok { + return false + } + value := jsonutils.NewString(defaultIP.String()) + v.value = value + data.Set(v.Key, value) + return true +} + +func (v *ValidatorIPv4Addr) Validate(data *jsonutils.JSONDict) error { + if err, isSet := v.Validator.validateEx(data); err != nil || !isSet { + return err + } + s, err := v.value.GetString() + if err != nil { + return newInvalidTypeError(v.Key, "string", err) + } + ip := net.ParseIP(s).To4() + if ip == nil { + return newInvalidValueError(v.Key, s) + } + v.IP = ip + return nil +} + +func NewIPv4AddrValidator(key string) *ValidatorIPv4Addr { + v := &ValidatorIPv4Addr{ + Validator: Validator{Key: key}, + } + v.parent = v + return v +} diff --git a/pkg/cloudcommon/validators/validators_test.go b/pkg/cloudcommon/validators/validators_test.go new file mode 100644 index 0000000000..1323554b5b --- /dev/null +++ b/pkg/cloudcommon/validators/validators_test.go @@ -0,0 +1,500 @@ +package validators + +// TODO +// +// - strict type, no implicit conversion +// - test model validator +// - invalid default for string choice, range + +import ( + "net" + "reflect" + "testing" + + "yunion.io/x/jsonutils" +) + +func TestURLPathRegexp(t *testing.T) { + cases := []struct { + in string + match bool + }{ + {in: "", match: true}, + {in: "/", match: true}, + {in: "/p", match: true}, + {in: "/p/", match: true}, + {in: "p", match: false}, + {in: "p/", match: false}, + {in: "/p?", match: false}, + {in: "/p#", match: false}, + } + for _, c := range cases { + got := regexpURLPath.MatchString(c.in) + if got != c.match { + t.Errorf("%q match, want %v, got %v", c.in, c.match, got) + } + } +} + +type C struct { + Name string + In string + Out string + Optional bool + Default interface{} + ValueWant interface{} + Err ErrType +} + +func testS(t *testing.T, v IValidator, c *C) { + j, _ := jsonutils.ParseString(c.In) + jd := j.(*jsonutils.JSONDict) + err := v.Validate(jd) + if err != nil { + verr, ok := err.(*ValidateError) + if ok { + if verr.ErrType != c.Err { + t.Errorf("error want %q, got %q", + c.Err, verr.ErrType) + } + } else { + t.Errorf("want error type ValidateError") + } + } else { + if c.Err != ERR_SUCCESS { + t.Errorf("expect error: %s", c.Err) + } + } + jWant, _ := jsonutils.ParseString(c.Out) + if !reflect.DeepEqual(j, jWant) { + t.Errorf("json out want %s, got %s", + jWant.String(), j.String()) + } + value := v.getValue() + if !reflect.DeepEqual(value, c.ValueWant) { + t.Errorf("value want %#v, got %#v", c.ValueWant, value) + } +} + +func TestStringChoicesValidator(t *testing.T) { + choices := NewChoices("choice0", "choice1", "100") + cases := []*C{ + { + Name: "missing non-optional", + In: `{}`, + Out: `{}`, + Optional: false, + Err: ERR_MISSING_KEY, + ValueWant: "", + }, + { + Name: "missing optional", + In: `{}`, + Out: `{}`, + Optional: true, + ValueWant: "", + }, + { + Name: "missing with default", + In: `{}`, + Out: `{s: "choice0"}`, + Default: "choice0", + ValueWant: "choice0", + }, + { + Name: "stringified", + In: `{"s": 100}`, + Out: `{s: "100"}`, + ValueWant: "100", + }, + { + Name: "stringified invalid choice", + In: `{"s": 101}`, + Out: `{"s": 101}`, + Err: ERR_INVALID_CHOICE, + ValueWant: "", + }, + { + Name: "good choice", + In: `{"s": "choice1"}`, + Out: `{"s": "choice1"}`, + ValueWant: "choice1", + }, + { + Name: "bad choice", + In: `{"s": "badchoice"}`, + Out: `{"s": "badchoice"}`, + Err: ERR_INVALID_CHOICE, + ValueWant: "", + }, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + v := NewStringChoicesValidator("s", choices) + if c.Default != nil { + s := c.Default.(string) + v.Default(s) + } + if c.Optional { + v.Optional(true) + } + testS(t, v, c) + }) + } +} + +func TestRangeValidator(t *testing.T) { + cases := []*C{ + { + Name: "missing non-optional", + In: `{}`, + Out: `{}`, + Err: ERR_MISSING_KEY, + ValueWant: int64(0), + }, + { + Name: "missing optional", + In: `{}`, + Out: `{}`, + Optional: true, + ValueWant: int64(0), + }, + { + Name: "missing with default", + In: `{}`, + Out: `{s: 1}`, + Default: int64(1), + ValueWant: int64(1), + }, + { + Name: "parsed", + In: `{s: "100"}`, + Out: `{s: 100}`, + ValueWant: int64(100), + }, + { + Name: "parsed invalid int", + In: `{s: "abc"}`, + Out: `{s: "abc"}`, + Err: ERR_INVALID_TYPE, + ValueWant: int64(0), + }, + { + Name: "parsed not in range", + In: `{s: "65536"}`, + Out: `{s: "65536"}`, + Err: ERR_NOT_IN_RANGE, + ValueWant: int64(0), + }, + { + Name: "in range", + In: `{s: 100}`, + Out: `{s: 100}`, + ValueWant: int64(100), + }, + { + Name: "not in range", + In: `{s: 0}`, + Out: `{s: 0}`, + Err: ERR_NOT_IN_RANGE, + ValueWant: int64(0), + }, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + v := NewPortValidator("s") + if c.Default != nil { + i := c.Default.(int64) + v.Default(i) + } + if c.Optional { + v.Optional(true) + } + testS(t, v, c) + }) + } +} + +func TestRegexValidator(t *testing.T) { + type RegexC struct { + *C + AllowEmpty bool + } + cases := []*RegexC{ + { + C: &C{ + Name: "missing non-optional", + In: `{}`, + Out: `{}`, + Err: ERR_MISSING_KEY, + ValueWant: "", + }, + }, + { + C: &C{ + Name: "missing optional", + In: `{}`, + Out: `{}`, + Optional: true, + ValueWant: "", + }, + }, + { + C: &C{ + Name: "missing with default", + In: `{}`, + Out: `{s: "example.com"}`, + Default: "example.com", + ValueWant: "example.com", + }, + }, + { + C: &C{ + Name: "valid", + In: `{s: "a.example.com"}`, + Out: `{s: "a.example.com"}`, + ValueWant: "a.example.com", + }, + }, + { + C: &C{ + Name: "valid (allow empty)", + In: `{s: ""}`, + Out: `{s: ""}`, + ValueWant: "", + }, + AllowEmpty: true, + }, + { + C: &C{ + Name: "invalid", + In: `{s: "/.example.com"}`, + Out: `{s: "/.example.com"}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + { + C: &C{ + Name: "invalid (disallow empty)", + In: `{s: ""}`, + Out: `{s: ""}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + v := NewDomainNameValidator("s") + if c.Default != nil { + i := c.Default.(string) + v.Default(i) + } + if c.Optional { + v.Optional(true) + } + if c.AllowEmpty { + v.AllowEmpty(true) + } + testS(t, v, c.C) + }) + } +} + +func TestIPv4Validator(t *testing.T) { + var nilIP net.IP + localIP := net.IPv4(127, 0, 0, 1).To4() + cases := []*C{ + { + Name: "missing non-optional", + In: `{}`, + Out: `{}`, + Err: ERR_MISSING_KEY, + ValueWant: nilIP, + }, + { + Name: "missing optional", + In: `{}`, + Out: `{}`, + Optional: true, + ValueWant: nilIP, + }, + { + Name: "missing with default", + In: `{}`, + Out: `{s: "127.0.0.1"}`, + Default: localIP, + ValueWant: localIP, + }, + { + Name: "valid", + In: `{s: "127.0.0.1"}`, + Out: `{s: "127.0.0.1"}`, + ValueWant: localIP, + }, + { + Name: "valid (v4 in v6)", + In: `{s: "::ffff:127.0.0.1"}`, + Out: `{s: "::ffff:127.0.0.1"}`, + ValueWant: localIP, + }, + { + Name: "invalid", + In: `{s: "127.0.0"}`, + Out: `{s: "127.0.0"}`, + Err: ERR_INVALID_VALUE, + ValueWant: nilIP, + }, + { + Name: "invalid (empty string)", + In: `{s: ""}`, + Out: `{s: ""}`, + Err: ERR_INVALID_VALUE, + ValueWant: nilIP, + }, + { + Name: "invalid (wrong type)", + In: `{s: 100}`, + Out: `{s: 100}`, + Err: ERR_INVALID_VALUE, + ValueWant: nilIP, + }, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + v := NewIPv4AddrValidator("s") + if c.Default != nil { + i := c.Default.(net.IP) + v.Default(i) + } + if c.Optional { + v.Optional(true) + } + testS(t, v, c) + }) + } +} + +type TestStruct struct { + F0 string + F1 int + F2 bool +} + +type TestVStruct TestStruct + +func (v *TestVStruct) Validate(data *jsonutils.JSONDict) error { + if v.F2 { + return nil + } else { + return newInvalidValueError("F2", v.F0) + } +} + +func TestStructValidator(t *testing.T) { + type StructC struct { + *C + Value interface{} + } + defaultVal := &TestStruct{ + F0: "holy", + F1: 100, + F2: true, + } + var defaultValMiss *TestStruct + { + defaultValMissCopy := *defaultVal + defaultValMissCopy.F2 = false + defaultValMiss = &defaultValMissCopy + } + defaultValJsonStr := `{s: {"F0": "holy", "F1": 100, "F2": true}}` + defaultValJsonStrMiss := `{s: {"F0": "holy", "F1": 100}}` + defaultValJsonStrMore := `{s: {"F0": "holy", "F1": 100, "F2": true, "Foo": "bar"}}` + + defaultVVal := (*TestVStruct)(defaultVal) + var defaultVValBad *TestVStruct + { + defaultVValBadCopy := *defaultVVal + defaultVValBadCopy.F2 = false + defaultVValBad = &defaultVValBadCopy + } + defaultVValJsonStrBad := `{s: {"F0": "holy", "F1": 100, "F2": false}}` + cases := []*StructC{ + { + C: &C{ + Name: "missing non-optional", + In: `{}`, + Out: `{}`, + Err: ERR_MISSING_KEY, + ValueWant: &TestStruct{}, + }, + Value: &TestStruct{}, + }, + { + C: &C{ + Name: "missing optional", + In: `{}`, + Out: `{}`, + Optional: true, + ValueWant: &TestStruct{}, + }, + Value: &TestStruct{}, + }, + { + C: &C{ + Name: "valid", + In: defaultValJsonStr, + Out: defaultValJsonStr, + ValueWant: defaultVal, + }, + Value: &TestStruct{}, + }, + { + C: &C{ + Name: "valid (missing fields)", + In: defaultValJsonStrMiss, + Out: defaultValJsonStrMiss, + ValueWant: defaultValMiss, + }, + Value: &TestStruct{}, + }, + { + C: &C{ + Name: "valid (more fields)", + In: defaultValJsonStrMore, + Out: defaultValJsonStrMore, + ValueWant: defaultVal, + }, + Value: &TestStruct{}, + }, + { + C: &C{ + Name: "valid (struct says valid)", + In: defaultValJsonStr, + Out: defaultValJsonStr, + ValueWant: defaultVVal, + }, + Value: &TestVStruct{}, + }, + { + C: &C{ + Name: "invalid (struct says invalid)", + In: defaultVValJsonStrBad, + Out: defaultVValJsonStrBad, + ValueWant: defaultVValBad, + Err: ERR_INVALID_VALUE, + }, + Value: &TestVStruct{}, + }, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + v := NewStructValidator("s", c.Value) + if c.Optional { + v.Optional(true) + } + testS(t, v, c.C) + }) + } +}