From e8eeadf08a4662efd51a67e457bf6fdb74919b72 Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Tekuri Date: Tue, 10 Jan 2023 13:11:53 +0530 Subject: [PATCH] errors: captures error message fields in struct closes Issue #44 --- compiler.go | 20 -- errors.go | 11 +- example_extension_test.go | 11 +- extension.go | 8 +- msg/msg.go | 380 ++++++++++++++++++++++++++++++++++++++ output.go | 4 +- schema.go | 127 ++++++------- 7 files changed, 465 insertions(+), 96 deletions(-) create mode 100644 msg/msg.go diff --git a/compiler.go b/compiler.go index 3f3cc80..6ca6b3e 100644 --- a/compiler.go +++ b/compiler.go @@ -371,26 +371,6 @@ func (c *Compiler) compileMap(r *resource, stack []schemaRef, sref schemaRef, re if e, ok := m["enum"]; ok { s.Enum = e.([]interface{}) - allPrimitives := true - for _, item := range s.Enum { - switch jsonType(item) { - case "object", "array": - allPrimitives = false - break - } - } - s.enumError = "enum failed" - if allPrimitives { - if len(s.Enum) == 1 { - s.enumError = fmt.Sprintf("value must be %#v", s.Enum[0]) - } else { - strEnum := make([]string, len(s.Enum)) - for i, item := range s.Enum { - strEnum[i] = fmt.Sprintf("%#v", item) - } - s.enumError = fmt.Sprintf("value must be one of %s", strings.Join(strEnum, ", ")) - } - } } s.Minimum = loadRat("minimum") diff --git a/errors.go b/errors.go index deaded8..821423b 100644 --- a/errors.go +++ b/errors.go @@ -3,6 +3,8 @@ package jsonschema import ( "fmt" "strings" + + "github.com/santhosh-tekuri/jsonschema/v5/msg" ) // InvalidJSONTypeError is the error type returned by ValidateInterface. @@ -69,7 +71,7 @@ type ValidationError struct { KeywordLocation string // validation path of validating keyword or schema AbsoluteKeywordLocation string // absolute location of validating keyword or schema InstanceLocation string // location of the json value within the instance being validated - Message string // describes error + Message fmt.Stringer // captures the message and data used in constructing it Causes []*ValidationError // nested validation errors } @@ -81,10 +83,11 @@ func (ve *ValidationError) add(causes ...error) error { } func (ve *ValidationError) causes(err error) error { - if err := err.(*ValidationError); err.Message == "" { - ve.Causes = err.Causes + var e = err.(*ValidationError) + if _, ok := e.Message.(msg.Empty); ok { + ve.Causes = e.Causes } else { - ve.add(err) + ve.add(e) } return ve } diff --git a/example_extension_test.go b/example_extension_test.go index 29e0784..cdf7db5 100644 --- a/example_extension_test.go +++ b/example_extension_test.go @@ -31,6 +31,15 @@ func (powerOfCompiler) Compile(ctx jsonschema.CompilerContext, m map[string]inte return nil, nil } +type powerOfSchemaMsg struct { + got interface{} + want int64 +} + +func (m powerOfSchemaMsg) String() string { + return fmt.Sprintf("%v not powerOf %v", m.got, m.want) +} + type powerOfSchema int64 func (s powerOfSchema) Validate(ctx jsonschema.ValidationContext, v interface{}) error { @@ -42,7 +51,7 @@ func (s powerOfSchema) Validate(ctx jsonschema.ValidationContext, v interface{}) n = n / pow } if n != 1 { - return ctx.Error("powerOf", "%v not powerOf %v", v, pow) + return ctx.Error("powerOf", powerOfSchemaMsg{got: v, want: pow}) } return nil default: diff --git a/extension.go b/extension.go index 452ba11..7167a51 100644 --- a/extension.go +++ b/extension.go @@ -1,5 +1,7 @@ package jsonschema +import "fmt" + // ExtCompiler compiles custom keyword(s) into ExtSchema. type ExtCompiler interface { // Compile compiles the custom keywords in schema m and returns its compiled representation. @@ -75,7 +77,7 @@ type ValidationContext struct { result validationResult validate func(sch *Schema, schPath string, v interface{}, vpath string) error validateInplace func(sch *Schema, schPath string) error - validationError func(keywordPath string, format string, a ...interface{}) *ValidationError + validationError func(keywordPath string, msg fmt.Stringer) *ValidationError } // EvaluatedProp marks given property of object as evaluated. @@ -104,8 +106,8 @@ func (ctx ValidationContext) Validate(s *Schema, spath string, v interface{}, vp // Error used to construct validation error by extensions. // // keywordPath is relative-json-pointer to keyword. -func (ctx ValidationContext) Error(keywordPath string, format string, a ...interface{}) *ValidationError { - return ctx.validationError(keywordPath, format, a...) +func (ctx ValidationContext) Error(keywordPath string, msg fmt.Stringer) *ValidationError { + return ctx.validationError(keywordPath, msg) } // Group is used by extensions to group multiple errors as causes to parent error. diff --git a/msg/msg.go b/msg/msg.go new file mode 100644 index 0000000..a8b4373 --- /dev/null +++ b/msg/msg.go @@ -0,0 +1,380 @@ +package msg + +import "fmt" +import "math/big" +import "strings" + +// Empty captures error fields for empty message. +type Empty struct{} + +func (Empty) String() string { + return "" +} + +// False captures error fields for false boolean schema. +type False struct{} + +func (False) String() string { + return "not allowed" +} + +// Type captures error fields for 'type'. +type Type struct { + Got string // type of the value we got + Want []string // types that are allowed +} + +func (d Type) String() string { + return fmt.Sprintf("expected %s, but got %s", strings.Join(d.Want, " or "), d.Got) +} + +// Format captures error fields for 'format'. +type Format struct { + Got interface{} // the value we got + Want string // format that is allowed +} + +func (d Format) String() string { + var got = d.Got + if v, ok := got.(string); ok { + got = quote(v) + } + return fmt.Sprintf("%v is not valid %s", got, quote(d.Want)) +} + +// MinProperties captures error fields for 'minProperties'. +type MinProperties struct { + Got int // num properties we got + Want int // min properties allowed +} + +func (d MinProperties) String() string { + return fmt.Sprintf("minimum %d properties allowed, but found %d properties", d.Want, d.Got) +} + +// MaxProperties captures error fields for 'maxProperties'. +type MaxProperties struct { + Got int // num properties we got + Want int // max properties allowed +} + +func (d MaxProperties) String() string { + return fmt.Sprintf("maximum %d properties allowed, but found %d properties", d.Want, d.Got) +} + +// Required captures error fields for 'required'. +type Required struct { + Want []string // properties that are missing +} + +func (d Required) String() string { + return fmt.Sprintf("missing properties: %s", strings.Join(d.Want, ", ")) +} + +// AdditionalProperties captures error fields for 'additionalProperties'. +type AdditionalProperties struct { + Got []string // additional properties we got +} + +func (d AdditionalProperties) String() string { + pnames := make([]string, 0, len(d.Got)) + for _, pname := range d.Got { + pnames = append(pnames, quote(pname)) + } + return fmt.Sprintf("additionalProperties %s not allowed", strings.Join(pnames, ", ")) +} + +// DependentRequired captures error fields for 'dependentRequired', 'dependencies'. +type DependentRequired struct { + Want string // property that is required + Got string // property that requires Want +} + +func (d DependentRequired) String() string { + return fmt.Sprintf("property %s is required, if %s property exists", quote(d.Want), quote(d.Got)) +} + +// MinItems captures error fields for 'minItems'. +type MinItems struct { + Got int // num items we got + Want int // min items allowed +} + +func (d MinItems) String() string { + return fmt.Sprintf("minimum %d items required, but found %d items", d.Want, d.Got) +} + +// MaxItems captures error fields for 'maxItems'. +type MaxItems struct { + Got int // num items we got + Want int // max items allowed +} + +func (d MaxItems) String() string { + return fmt.Sprintf("maximum %d items required, but found %d items", d.Want, d.Got) +} + +// MinContains captures error fields for 'minContains'. +type MinContains struct { + Got []int // item indexes matching contains schema + Want int // min items allowed matching contains schema +} + +func (d MinContains) String() string { + return fmt.Sprintf("minimum %d valid items required, but found %d valid items", d.Want, len(d.Got)) +} + +// MaxContains captures error fields for 'maxContains'. +type MaxContains struct { + Got []int // item indexes matching contains schema + Want int // max items allowed matching contains schema +} + +func (d MaxContains) String() string { + return fmt.Sprintf("maximum %d valid items required, but found %d valid items", d.Want, len(d.Got)) +} + +// UniqueItems captures error fields for 'uniqueItems'. +type UniqueItems struct { + Got [2]int // item indexes that are not unique +} + +func (d UniqueItems) String() string { + return fmt.Sprintf("items at index %d and %d are equal", d.Got[0], d.Got[1]) +} + +// OneOf captures error fields for 'oneOf'. +type OneOf struct { + Got []int // subschema indexes that matched +} + +func (d OneOf) String() string { + if len(d.Got) == 0 { + return "oneOf failed" + } + return fmt.Sprintf("valid against subschemas %d and %d", d.Got[0], d.Got[1]) +} + +// AnyOf captures error fields for 'anyOf'. +type AnyOf struct{} + +func (AnyOf) String() string { + return "anyOf failed" +} + +// AllOf captures error fields for 'allOf'. +type AllOf struct { + Got []int // subschema indexes that did not match +} + +func (d AllOf) String() string { + got := fmt.Sprintf("%v", d.Got) + got = got[1 : len(got)-1] + return fmt.Sprintf("invalid against subschemas %v", got) +} + +// Not captures error fields for 'not'. +type Not struct{} + +func (Not) String() string { + return "not failed" +} + +// Schema captures error fields for top schema, '$ref', '$recursiveRef', '$dynamicRef'. +type Schema struct { + Want string // url of schema that did not match +} + +func (d Schema) String() string { + return fmt.Sprintf("doesn't validate with %s", quote(d.Want)) +} + +// AdditionalItems captures error fields for 'additionalItems'. +type AdditionalItems struct { + Got int // num items we got + Want int // num items allowed +} + +func (d AdditionalItems) String() string { + return fmt.Sprintf("only %d items are allowed, but found %d items", d.Want, d.Got) +} + +// MinLength captures error fields for 'minLength'. +type MinLength struct { + Got int // length of string we got + Want int // min length of string allowed +} + +func (d MinLength) String() string { + return fmt.Sprintf("length must be >= %d, but got %d", d.Want, d.Got) +} + +// MaxLength captures error fields for 'maxLength'. +type MaxLength struct { + Got int // length of string we got + Want int // max length of string allowed +} + +func (d MaxLength) String() string { + return fmt.Sprintf("length must be <= %d, but got %d", d.Want, d.Got) +} + +// Pattern captures error fields for 'pattern'. +type Pattern struct { + Got string // string value we got + Want string // regex that should match +} + +func (d Pattern) String() string { + return fmt.Sprintf("%s does not match pattern %s", quote(d.Got), quote(d.Want)) +} + +// Minimum captures error fields for 'minimum'. +type Minimum struct { + Got interface{} // number we got + Want *big.Rat // min number allowed +} + +func (d Minimum) String() string { + want, _ := d.Want.Float64() + return fmt.Sprintf("must be >= %v but found %v", want, d.Got) +} + +// Maximum captures error fields for 'maximum'. +type Maximum struct { + Got interface{} // number we got + Want *big.Rat // max number allowed +} + +func (d Maximum) String() string { + want, _ := d.Want.Float64() + return fmt.Sprintf("must be <= %v but found %v", want, d.Got) +} + +// ExclusiveMinimum captures error fields for 'exclusiveMinimum'. +type ExclusiveMinimum struct { + Got interface{} // number we got + Want *big.Rat // exclusive min number allowed +} + +func (d ExclusiveMinimum) String() string { + want, _ := d.Want.Float64() + return fmt.Sprintf("must be > %v but found %v", want, d.Got) +} + +// ExclusiveMaximum captures error fields for 'exclusiveMaximum'. +type ExclusiveMaximum struct { + Got interface{} // number we got + Want *big.Rat // exclusive max number allowed +} + +func (d ExclusiveMaximum) String() string { + want, _ := d.Want.Float64() + return fmt.Sprintf("must be < %v but found %v", want, d.Got) +} + +// MultipleOf captures error fields for 'multipleOf'. +type MultipleOf struct { + Got interface{} // number we got + Want *big.Rat // only multiple of this allowed +} + +func (d MultipleOf) String() string { + want, _ := d.Want.Float64() + return fmt.Sprintf("%v not multipleOf %v", d.Got, want) +} + +// Then captures error fields for 'then'. +type Then struct{} + +func (Then) String() string { + return "if-then failed" +} + +// Else captures error fields for 'else'. +type Else struct{} + +func (Else) String() string { + return "if-else failed" +} + +// Const captures error fields for 'const'. +type Const struct { + Got interface{} // value we got + Want interface{} // value allowed +} + +func (d Const) String() string { + switch d.Want.(type) { + case map[string]interface{}, []interface{}: + return "const failed" + default: + return fmt.Sprintf("value must be %#v", d.Want) + } +} + +// Enum captures error fields for 'enum'. +type Enum struct { + Got interface{} // value we got + Want []interface{} // list of values allowed +} + +func (d Enum) String() string { + allPrimitives := true + for _, item := range d.Want { + switch item.(type) { + case map[string]interface{}, []interface{}: + allPrimitives = false + break + } + } + if allPrimitives { + if len(d.Want) == 1 { + return fmt.Sprintf("value must be %#v", d.Want[0]) + } else { + strEnum := make([]string, len(d.Want)) + for i, item := range d.Want { + strEnum[i] = fmt.Sprintf("%#v", item) + } + return fmt.Sprintf("value must be one of %s", strings.Join(strEnum, ", ")) + } + } + return "enum failed" +} + +// ContentEncoding captures error fields for 'contentEncoding'. +type ContentEncoding struct { + Got string // value we got + Want string // content encoding of the value allowed +} + +func (d ContentEncoding) String() string { + return fmt.Sprintf("value is not %s encoded", d.Want) +} + +// ContentMediaType captures error fields for 'contentMediaType'. +type ContentMediaType struct { + Got []byte // decoded value we got + Want string // media type of value allowed +} + +func (d ContentMediaType) String() string { + return fmt.Sprintf("value is not of mediatype %s", quote(d.Want)) +} + +// ContentSchema captures error fields for 'contentSchema'. +type ContentSchema struct { + Got []byte // decoded value we got +} + +func (ContentSchema) String() string { + return "value is not valid json" +} + +// quote returns single-quoted string +func quote(s string) string { + s = fmt.Sprintf("%q", s) + s = strings.ReplaceAll(s, `\"`, `"`) + s = strings.ReplaceAll(s, `'`, `\'`) + return "'" + s[1:len(s)-1] + "'" +} diff --git a/output.go b/output.go index d65ae2a..59b3922 100644 --- a/output.go +++ b/output.go @@ -35,7 +35,7 @@ func (ve *ValidationError) BasicOutput() Basic { KeywordLocation: ve.KeywordLocation, AbsoluteKeywordLocation: ve.AbsoluteKeywordLocation, InstanceLocation: ve.InstanceLocation, - Error: ve.Message, + Error: ve.Message.String(), }) for _, cause := range ve.Causes { flatten(cause) @@ -63,7 +63,7 @@ func (ve *ValidationError) DetailedOutput() Detailed { for _, cause := range ve.Causes { errors = append(errors, cause.DetailedOutput()) } - var message = ve.Message + var message = ve.Message.String() if len(ve.Causes) > 0 { message = "" } diff --git a/schema.go b/schema.go index 0c8d8a3..51f19d3 100644 --- a/schema.go +++ b/schema.go @@ -10,6 +10,8 @@ import ( "strconv" "strings" "unicode/utf8" + + "github.com/santhosh-tekuri/jsonschema/v5/msg" ) // A Schema represents compiled version of json-schema. @@ -32,7 +34,6 @@ type Schema struct { Types []string // allowed types. Constant []interface{} // first element in slice is constant value. note: slice is used to capture nil constant. Enum []interface{} // allowed values. - enumError string // error message for enum fail. captured here to avoid constructing error message every time. Not *Schema AllOf []*Schema AnyOf []*Schema @@ -179,7 +180,7 @@ func (s *Schema) validateValue(v interface{}, vloc string) (err error) { KeywordLocation: "", AbsoluteKeywordLocation: s.Location, InstanceLocation: vloc, - Message: fmt.Sprintf("doesn't validate with %s", s.Location), + Message: msg.Schema{Want: s.Location}, } return ve.causes(err) } @@ -188,12 +189,12 @@ func (s *Schema) validateValue(v interface{}, vloc string) (err error) { // validate validates given value v with this schema. func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interface{}, vloc string) (result validationResult, err error) { - validationError := func(keywordPath string, format string, a ...interface{}) *ValidationError { + validationError := func(keywordPath string, msg fmt.Stringer) *ValidationError { return &ValidationError{ KeywordLocation: keywordLocation(scope, keywordPath), AbsoluteKeywordLocation: joinPtr(s.Location, keywordPath), InstanceLocation: vloc, - Message: fmt.Sprintf(format, a...), + Message: msg, } } @@ -247,7 +248,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if s.Always != nil { if !*s.Always { - return result, validationError("", "not allowed") + return result, validationError("", msg.False{}) } return result, nil } @@ -268,7 +269,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa } } if !matched { - return result, validationError("type", "expected %s, but got %s", strings.Join(s.Types, " or "), vType) + return result, validationError("type", msg.Type{Got: vType, Want: s.Types}) } } @@ -276,12 +277,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if len(s.Constant) > 0 { if !equals(v, s.Constant[0]) { - switch jsonType(s.Constant[0]) { - case "object", "array": - errors = append(errors, validationError("const", "const failed")) - default: - errors = append(errors, validationError("const", "value must be %#v", s.Constant[0])) - } + errors = append(errors, validationError("const", msg.Const{Got: v, Want: s.Constant[0]})) } } @@ -294,35 +290,31 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa } } if !matched { - errors = append(errors, validationError("enum", s.enumError)) + errors = append(errors, validationError("enum", msg.Enum{Got: v, Want: s.Enum})) } } if s.format != nil && !s.format(v) { - var val = v - if v, ok := v.(string); ok { - val = quote(v) - } - errors = append(errors, validationError("format", "%v is not valid %s", val, quote(s.Format))) + errors = append(errors, validationError("format", msg.Format{Got: v, Want: s.Format})) } switch v := v.(type) { case map[string]interface{}: if s.MinProperties != -1 && len(v) < s.MinProperties { - errors = append(errors, validationError("minProperties", "minimum %d properties allowed, but found %d properties", s.MinProperties, len(v))) + errors = append(errors, validationError("minProperties", msg.MinProperties{Got: len(v), Want: s.MinProperties})) } if s.MaxProperties != -1 && len(v) > s.MaxProperties { - errors = append(errors, validationError("maxProperties", "maximum %d properties allowed, but found %d properties", s.MaxProperties, len(v))) + errors = append(errors, validationError("maxProperties", msg.MaxProperties{Got: len(v), Want: s.MaxProperties})) } if len(s.Required) > 0 { var missing []string for _, pname := range s.Required { if _, ok := v[pname]; !ok { - missing = append(missing, quote(pname)) + missing = append(missing, pname) } } if len(missing) > 0 { - errors = append(errors, validationError("required", "missing properties: %s", strings.Join(missing, ", "))) + errors = append(errors, validationError("required", msg.Required{Want: missing})) } } @@ -346,7 +338,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if s.RegexProperties { for pname := range v { if !isRegex(pname) { - errors = append(errors, validationError("", "patternProperty %s is not valid regex", quote(pname))) + errors = append(errors, validationError("", msg.Format{Got: pname, Want: "regex"})) } } } @@ -363,7 +355,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if s.AdditionalProperties != nil { if allowed, ok := s.AdditionalProperties.(bool); ok { if !allowed && len(result.unevalProps) > 0 { - errors = append(errors, validationError("additionalProperties", "additionalProperties %s not allowed", result.unevalPnames())) + errors = append(errors, validationError("additionalProperties", msg.AdditionalProperties{Got: result.unevalPnames()})) } } else { schema := s.AdditionalProperties.(*Schema) @@ -387,7 +379,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa case []string: for i, pname := range dvalue { if _, ok := v[pname]; !ok { - errors = append(errors, validationError("dependencies/"+escape(dname)+"/"+strconv.Itoa(i), "property %s is required, if %s property exists", quote(pname), quote(dname))) + errors = append(errors, validationError("dependencies/"+escape(dname)+"/"+strconv.Itoa(i), msg.DependentRequired{Got: dname, Want: pname})) } } } @@ -397,7 +389,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if _, ok := v[dname]; ok { for i, pname := range dvalue { if _, ok := v[pname]; !ok { - errors = append(errors, validationError("dependentRequired/"+escape(dname)+"/"+strconv.Itoa(i), "property %s is required, if %s property exists", quote(pname), quote(dname))) + errors = append(errors, validationError("dependentRequired/"+escape(dname)+"/"+strconv.Itoa(i), msg.DependentRequired{Got: dname, Want: pname})) } } } @@ -412,16 +404,16 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa case []interface{}: if s.MinItems != -1 && len(v) < s.MinItems { - errors = append(errors, validationError("minItems", "minimum %d items required, but found %d items", s.MinItems, len(v))) + errors = append(errors, validationError("minItems", msg.MinItems{Got: len(v), Want: s.MinItems})) } if s.MaxItems != -1 && len(v) > s.MaxItems { - errors = append(errors, validationError("maxItems", "maximum %d items required, but found %d items", s.MaxItems, len(v))) + errors = append(errors, validationError("maxItems", msg.MaxItems{Got: len(v), Want: s.MaxItems})) } if s.UniqueItems { for i := 1; i < len(v); i++ { for j := 0; j < i; j++ { if equals(v[i], v[j]) { - errors = append(errors, validationError("uniqueItems", "items at index %d and %d are equal", j, i)) + errors = append(errors, validationError("uniqueItems", msg.UniqueItems{Got: [2]int{j, i}})) } } } @@ -456,7 +448,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if additionalItems { result.unevalItems = nil } else if len(v) > len(items) { - errors = append(errors, validationError("additionalItems", "only %d items are allowed, but found %d items", len(items), len(v))) + errors = append(errors, validationError("additionalItems", msg.AdditionalItems{Got: len(v), Want: len(items)})) } } } @@ -480,23 +472,23 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa // contains + minContains + maxContains if s.Contains != nil && (s.MinContains != -1 || s.MaxContains != -1) { - matched := 0 + var matched []int var causes []error for i, item := range v { if err := validate(s.Contains, "contains", item, strconv.Itoa(i)); err != nil { causes = append(causes, err) } else { - matched++ + matched = append(matched, i) if s.ContainsEval { delete(result.unevalItems, i) } } } - if s.MinContains != -1 && matched < s.MinContains { - errors = append(errors, validationError("minContains", "valid must be >= %d, but got %d", s.MinContains, matched).add(causes...)) + if s.MinContains != -1 && len(matched) < s.MinContains { + errors = append(errors, validationError("minContains", msg.MinContains{Got: matched, Want: s.MinContains}).add(causes...)) } - if s.MaxContains != -1 && matched > s.MaxContains { - errors = append(errors, validationError("maxContains", "valid must be <= %d, but got %d", s.MaxContains, matched)) + if s.MaxContains != -1 && len(matched) > s.MaxContains { + errors = append(errors, validationError("maxContains", msg.MaxContains{Got: matched, Want: s.MaxContains})) } } @@ -505,15 +497,15 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if s.MinLength != -1 || s.MaxLength != -1 { length := utf8.RuneCount([]byte(v)) if s.MinLength != -1 && length < s.MinLength { - errors = append(errors, validationError("minLength", "length must be >= %d, but got %d", s.MinLength, length)) + errors = append(errors, validationError("minLength", msg.MinLength{Got: length, Want: s.MinLength})) } if s.MaxLength != -1 && length > s.MaxLength { - errors = append(errors, validationError("maxLength", "length must be <= %d, but got %d", s.MaxLength, length)) + errors = append(errors, validationError("maxLength", msg.MaxLength{Got: length, Want: s.MaxLength})) } } if s.Pattern != nil && !s.Pattern.MatchString(v) { - errors = append(errors, validationError("pattern", "does not match pattern %s", quote(s.Pattern.String()))) + errors = append(errors, validationError("pattern", msg.Pattern{Got: v, Want: s.Pattern.String()})) } // contentEncoding + contentMediaType @@ -523,7 +515,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if s.decoder != nil { b, err := s.decoder(v) if err != nil { - errors = append(errors, validationError("contentEncoding", "value is not %s encoded", s.ContentEncoding)) + errors = append(errors, validationError("contentEncoding", msg.ContentEncoding{Got: v, Want: s.ContentEncoding})) } else { content, decoded = b, true } @@ -533,13 +525,13 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa content = []byte(v) } if err := s.mediaType(content); err != nil { - errors = append(errors, validationError("contentMediaType", "value is not of mediatype %s", quote(s.ContentMediaType))) + errors = append(errors, validationError("contentMediaType", msg.ContentMediaType{Got: content, Want: s.ContentMediaType})) } } if decoded && s.ContentSchema != nil { contentJSON, err := unmarshal(bytes.NewReader(content)) if err != nil { - errors = append(errors, validationError("contentSchema", "value is not valid json")) + errors = append(errors, validationError("contentSchema", msg.ContentSchema{Got: content})) } else { err := validate(s.ContentSchema, "contentSchema", contentJSON, "") if err != nil { @@ -558,25 +550,21 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa } return numVal } - f64 := func(r *big.Rat) float64 { - f, _ := r.Float64() - return f - } if s.Minimum != nil && num().Cmp(s.Minimum) < 0 { - errors = append(errors, validationError("minimum", "must be >= %v but found %v", f64(s.Minimum), v)) + errors = append(errors, validationError("minimum", msg.Minimum{Got: v, Want: s.Minimum})) } if s.ExclusiveMinimum != nil && num().Cmp(s.ExclusiveMinimum) <= 0 { - errors = append(errors, validationError("exclusiveMinimum", "must be > %v but found %v", f64(s.ExclusiveMinimum), v)) + errors = append(errors, validationError("exclusiveMinimum", msg.ExclusiveMinimum{Got: v, Want: s.ExclusiveMinimum})) } if s.Maximum != nil && num().Cmp(s.Maximum) > 0 { - errors = append(errors, validationError("maximum", "must be <= %v but found %v", f64(s.Maximum), v)) + errors = append(errors, validationError("maximum", msg.Maximum{Got: v, Want: s.Maximum})) } if s.ExclusiveMaximum != nil && num().Cmp(s.ExclusiveMaximum) >= 0 { - errors = append(errors, validationError("exclusiveMaximum", "must be < %v but found %v", f64(s.ExclusiveMaximum), v)) + errors = append(errors, validationError("exclusiveMaximum", msg.ExclusiveMaximum{Got: v, Want: s.ExclusiveMaximum})) } if s.MultipleOf != nil { if q := new(big.Rat).Quo(num(), s.MultipleOf); !q.IsInt() { - errors = append(errors, validationError("multipleOf", "%v not multipleOf %v", v, f64(s.MultipleOf))) + errors = append(errors, validationError("multipleOf", msg.MultipleOf{Got: v, Want: s.MultipleOf})) } } } @@ -589,7 +577,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if s.url() == sch.url() { url = sch.loc() } - return validationError(refPath, "doesn't validate with %s", quote(url)).causes(err) + return validationError(refPath, msg.Schema{Want: url}).causes(err) } } return nil @@ -635,13 +623,20 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa } if s.Not != nil && validateInplace(s.Not, "not") == nil { - errors = append(errors, validationError("not", "not failed")) + errors = append(errors, validationError("not", msg.Not{})) } - for i, sch := range s.AllOf { - schPath := "allOf/" + strconv.Itoa(i) - if err := validateInplace(sch, schPath); err != nil { - errors = append(errors, validationError(schPath, "allOf failed").add(err)) + if len(s.AllOf) > 0 { + var failed []int + var causes []error + for i, sch := range s.AllOf { + if err := validateInplace(sch, "allOf/"+strconv.Itoa(i)); err != nil { + failed = append(failed, i) + causes = append(causes, err) + } + } + if len(failed) > 0 { + errors = append(errors, validationError("allOf", msg.AllOf{Got: failed}).add(causes...)) } } @@ -656,7 +651,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa } } if !matched { - errors = append(errors, validationError("anyOf", "anyOf failed").add(causes...)) + errors = append(errors, validationError("anyOf", msg.AnyOf{}).add(causes...)) } } @@ -668,7 +663,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if matched == -1 { matched = i } else { - errors = append(errors, validationError("oneOf", "valid against schemas at indexes %d and %d", matched, i)) + errors = append(errors, validationError("oneOf", msg.OneOf{Got: []int{matched, i}})) break } } else { @@ -676,7 +671,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa } } if matched == -1 { - errors = append(errors, validationError("oneOf", "oneOf failed").add(causes...)) + errors = append(errors, validationError("oneOf", msg.OneOf{}).add(causes...)) } } @@ -688,13 +683,13 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa if err == nil { if s.Then != nil { if err := validateInplace(s.Then, "then"); err != nil { - errors = append(errors, validationError("then", "if-then failed").add(err)) + errors = append(errors, validationError("then", msg.Then{}).add(err)) } } } else { if s.Else != nil { if err := validateInplace(s.Else, "else"); err != nil { - errors = append(errors, validationError("else", "if-else failed").add(err)) + errors = append(errors, validationError("else", msg.Else{}).add(err)) } } } @@ -738,7 +733,7 @@ func (s *Schema) validate(scope []schemaRef, vscope int, spath string, v interfa case 1: return result, errors[0] default: - return result, validationError("", "").add(errors...) // empty message, used just for wrapping + return result, validationError("", msg.Empty{}).add(errors...) // empty message, used just for wrapping } } @@ -747,12 +742,12 @@ type validationResult struct { unevalItems map[int]struct{} } -func (vr validationResult) unevalPnames() string { +func (vr validationResult) unevalPnames() []string { pnames := make([]string, 0, len(vr.unevalProps)) for pname := range vr.unevalProps { - pnames = append(pnames, quote(pname)) + pnames = append(pnames, pname) } - return strings.Join(pnames, ", ") + return pnames } // jsonType returns the json type of given value v.