Merge branch 'hoshsadiq-master'
diff --git a/.gitignore b/.gitignore index c1e0636..4c60981 100644 --- a/.gitignore +++ b/.gitignore
@@ -1 +1,2 @@ *.sw[nop] +*.iml
diff --git a/README.md b/README.md index fe43659..e822195 100644 --- a/README.md +++ b/README.md
@@ -190,12 +190,14 @@ **err.Value()**: *interface{}* Returns the value given -**err.Context()**: *gojsonschema.jsonContext* Returns the context. This has a String() method that will print something like this: (root).firstName +**err.Context()**: *gojsonschema.JsonContext* Returns the context. This has a String() method that will print something like this: (root).firstName **err.Field()**: *string* Returns the fieldname in the format firstName, or for embedded properties, person.firstName. This returns the same as the String() method on *err.Context()* but removes the (root). prefix. **err.Description()**: *string* The error description. This is based on the locale you are using. See the beginning of this section for overwriting the locale with a custom implementation. +**err.DescriptionFormat()**: *string* The error description format. This is relevant if you are adding custom validation errors afterwards to the result. + **err.Details()**: *gojsonschema.ErrorDetails* Returns a map[string]interface{} of additional error details specific to the error. For example, GTE errors will have a "min" value, LTE will have a "max" value. See errors.go for a full description of all the error details. Every error always contains a "field" key that holds the value of *err.Field()* Note in most cases, the err.Details() will be used to generate replacement strings in your locales, and not used directly. These strings follow the text/template format i.e. @@ -286,7 +288,56 @@ gojsonschema.FormatCheckers.Add("ValidUserId", ValidUserIdFormatChecker{}) ```` +## Additional custom validation +After the validation has run and you have the results, you may add additional +errors using `Result.AddError`. This is useful to maintain the same format within the resultset instead +of having to add special exceptions for your own errors. Below is an example. +```go +type AnswerInvalidError struct { + gojsonschema.ResultErrorFields +} + +func newAnswerInvalidError(context *gojsonschema.JsonContext, value interface{}, details gojsonschema.ErrorDetails) *AnswerInvalidError { + err := AnswerInvalidError{} + err.SetContext(context) + err.SetType("custom_invalid_error") + // it is important to use SetDescriptionFormat() as this is used to call SetDescription() after it has been parsed + // using the description of err will be overridden by this. + err.SetDescriptionFormat("Answer to the Ultimate Question of Life, the Universe, and Everything is {{.answer}}") + err.SetValue(value) + err.SetDetails(details) + + return &err +} + +func main() { + // ... + schema, err := gojsonschema.NewSchema(schemaLoader) + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + + if true { // some validation + jsonContext := gojsonschema.NewJsonContext("question", nil) + errDetail := gojsonschema.ErrorDetails{ + "answer": 42, + } + result.AddError( + newAnswerInvalidError( + gojsonschema.NewJsonContext("answer", jsonContext), + 52, + errDetail, + ), + errDetail, + ) + } + + return result, err + +} +``` + +This is especially useful if you want to add validation beyond what the +json schema drafts can provide such business specific logic. ## Uses
diff --git a/errors.go b/errors.go index 58cb01f..e583986 100644 --- a/errors.go +++ b/errors.go
@@ -158,7 +158,7 @@ ) // newError takes a ResultError type and sets the type, context, description, details, value, and field -func newError(err ResultError, context *jsonContext, value interface{}, locale locale, details ErrorDetails) { +func newError(err ResultError, context *JsonContext, value interface{}, locale locale, details ErrorDetails) { var t string var d string switch err.(type) { @@ -252,13 +252,14 @@ err.SetContext(context) err.SetValue(value) err.SetDetails(details) + err.SetDescriptionFormat(d) details["field"] = err.Field() if _, exists := details["context"]; !exists && context != nil { details["context"] = context.String() } - err.SetDescription(formatErrorDescription(d, details)) + err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details)) } // formatErrorDescription takes a string in the default text/template
diff --git a/jsonContext.go b/jsonContext.go index fcc8d9d..f40668a 100644 --- a/jsonContext.go +++ b/jsonContext.go
@@ -26,20 +26,20 @@ import "bytes" -// jsonContext implements a persistent linked-list of strings -type jsonContext struct { +// JsonContext implements a persistent linked-list of strings +type JsonContext struct { head string - tail *jsonContext + tail *JsonContext } -func newJsonContext(head string, tail *jsonContext) *jsonContext { - return &jsonContext{head, tail} +func NewJsonContext(head string, tail *JsonContext) *JsonContext { + return &JsonContext{head, tail} } // String displays the context in reverse. // This plays well with the data structure's persistent nature with // Cons and a json document's tree structure. -func (c *jsonContext) String(del ...string) string { +func (c *JsonContext) String(del ...string) string { byteArr := make([]byte, 0, c.stringLen()) buf := bytes.NewBuffer(byteArr) c.writeStringToBuffer(buf, del) @@ -47,7 +47,7 @@ return buf.String() } -func (c *jsonContext) stringLen() int { +func (c *JsonContext) stringLen() int { length := 0 if c.tail != nil { length = c.tail.stringLen() + 1 // add 1 for "." @@ -57,7 +57,7 @@ return length } -func (c *jsonContext) writeStringToBuffer(buf *bytes.Buffer, del []string) { +func (c *JsonContext) writeStringToBuffer(buf *bytes.Buffer, del []string) { if c.tail != nil { c.tail.writeStringToBuffer(buf, del)
diff --git a/result.go b/result.go index 6ad56ae..2e1e8d6 100644 --- a/result.go +++ b/result.go
@@ -40,10 +40,12 @@ Field() string SetType(string) Type() string - SetContext(*jsonContext) - Context() *jsonContext + SetContext(*JsonContext) + Context() *JsonContext SetDescription(string) Description() string + SetDescriptionFormat(string) + DescriptionFormat() string SetValue(interface{}) Value() interface{} SetDetails(ErrorDetails) @@ -55,11 +57,12 @@ // ResultErrorFields implements the ResultError interface, so custom errors // can be defined by just embedding this type ResultErrorFields struct { - errorType string // A string with the type of error (i.e. invalid_type) - context *jsonContext // Tree like notation of the part that failed the validation. ex (root).a.b ... - description string // A human readable error message - value interface{} // Value given by the JSON file that is the source of the error - details ErrorDetails + errorType string // A string with the type of error (i.e. invalid_type) + context *JsonContext // Tree like notation of the part that failed the validation. ex (root).a.b ... + description string // A human readable error message + descriptionFormat string // A format for human readable error message + value interface{} // Value given by the JSON file that is the source of the error + details ErrorDetails } Result struct { @@ -90,11 +93,11 @@ return v.errorType } -func (v *ResultErrorFields) SetContext(context *jsonContext) { +func (v *ResultErrorFields) SetContext(context *JsonContext) { v.context = context } -func (v *ResultErrorFields) Context() *jsonContext { +func (v *ResultErrorFields) Context() *JsonContext { return v.context } @@ -106,6 +109,14 @@ return v.description } +func (v *ResultErrorFields) SetDescriptionFormat(descriptionFormat string) { + v.descriptionFormat = descriptionFormat +} + +func (v *ResultErrorFields) DescriptionFormat() string { + return v.descriptionFormat +} + func (v *ResultErrorFields) SetValue(value interface{}) { v.value = value } @@ -154,8 +165,19 @@ func (v *Result) Errors() []ResultError { return v.errors } +// Add a fully filled error to the error set +// SetDescription() will be called with the result of the parsed err.DescriptionFormat() +func (v *Result) AddError(err ResultError, details ErrorDetails) { + if _, exists := details["context"]; !exists && err.Context() != nil { + details["context"] = err.Context().String() + } -func (v *Result) addError(err ResultError, context *jsonContext, value interface{}, details ErrorDetails) { + err.SetDescription(formatErrorDescription(err.DescriptionFormat(), details)) + + v.errors = append(v.errors, err) +} + +func (v *Result) addInternalError(err ResultError, context *JsonContext, value interface{}, details ErrorDetails) { newError(err, context, value, Locale, details) v.errors = append(v.errors, err) v.score -= 2 // results in a net -1 when added to the +1 we get at the end of the validation function
diff --git a/validation.go b/validation.go index 664da21..54fc7a7 100644 --- a/validation.go +++ b/validation.go
@@ -64,21 +64,21 @@ // begin validation result := &Result{} - context := newJsonContext(STRING_CONTEXT_ROOT, nil) + context := NewJsonContext(STRING_CONTEXT_ROOT, nil) v.rootSchema.validateRecursive(v.rootSchema, root, result, context) return result, nil } -func (v *subSchema) subValidateWithContext(document interface{}, context *jsonContext) *Result { +func (v *subSchema) subValidateWithContext(document interface{}, context *JsonContext) *Result { result := &Result{} v.validateRecursive(v, document, result, context) return result } // Walker function to validate the json recursively against the subSchema -func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateRecursive(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateRecursive %s", context.String()) @@ -94,7 +94,7 @@ // Check for null value if currentNode == nil { if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_NULL) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -126,7 +126,7 @@ givenType = TYPE_NUMBER } - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -155,7 +155,7 @@ case reflect.Slice: if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_ARRAY) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -178,7 +178,7 @@ case reflect.Map: if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_OBJECT) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -203,7 +203,7 @@ for _, pSchema := range currentSubSchema.propertiesChildren { nextNode, ok := castCurrentNode[pSchema.property] if ok { - subContext := newJsonContext(pSchema.property, context) + subContext := NewJsonContext(pSchema.property, context) v.validateRecursive(pSchema, nextNode, result, subContext) } } @@ -213,7 +213,7 @@ case reflect.Bool: if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_BOOLEAN) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -235,7 +235,7 @@ case reflect.String: if currentSubSchema.types.IsTyped() && !currentSubSchema.types.Contains(TYPE_STRING) { - result.addError( + result.addInternalError( new(InvalidTypeError), context, currentNode, @@ -264,7 +264,7 @@ } // Different kinds of validation there, subSchema / common / array / object / string... -func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateSchema(currentSubSchema *subSchema, currentNode interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateSchema %s", context.String()) @@ -288,7 +288,7 @@ } if !validatedAnyOf { - result.addError(new(NumberAnyOfError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(NumberAnyOfError), context, currentNode, ErrorDetails{}) if bestValidationResult != nil { // add error messages of closest matching subSchema as @@ -314,7 +314,7 @@ if nbValidated != 1 { - result.addError(new(NumberOneOfError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(NumberOneOfError), context, currentNode, ErrorDetails{}) if nbValidated == 0 { // add error messages of closest matching subSchema as @@ -337,14 +337,14 @@ } if nbValidated != len(currentSubSchema.allOf) { - result.addError(new(NumberAllOfError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(NumberAllOfError), context, currentNode, ErrorDetails{}) } } if currentSubSchema.not != nil { validationResult := currentSubSchema.not.subValidateWithContext(currentNode, context) if validationResult.Valid() { - result.addError(new(NumberNotError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(NumberNotError), context, currentNode, ErrorDetails{}) } } @@ -357,7 +357,7 @@ case []string: for _, dependOnKey := range dependency { if _, dependencyResolved := currentNode.(map[string]interface{})[dependOnKey]; !dependencyResolved { - result.addError( + result.addInternalError( new(MissingDependencyError), context, currentNode, @@ -380,14 +380,14 @@ if currentSubSchema._then != nil && validationResultIf.Valid() { validationResultThen := currentSubSchema._then.subValidateWithContext(currentNode, context) if !validationResultThen.Valid() { - result.addError(new(ConditionThenError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(ConditionThenError), context, currentNode, ErrorDetails{}) result.mergeErrors(validationResultThen) } } if currentSubSchema._else != nil && !validationResultIf.Valid() { validationResultElse := currentSubSchema._else.subValidateWithContext(currentNode, context) if !validationResultElse.Valid() { - result.addError(new(ConditionElseError), context, currentNode, ErrorDetails{}) + result.addInternalError(new(ConditionElseError), context, currentNode, ErrorDetails{}) result.mergeErrors(validationResultElse) } } @@ -396,7 +396,7 @@ result.incrementScore() } -func (v *subSchema) validateCommon(currentSubSchema *subSchema, value interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateCommon(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateCommon %s", context.String()) @@ -407,10 +407,10 @@ if len(currentSubSchema.enum) > 0 { has, err := currentSubSchema.ContainsEnum(value) if err != nil { - result.addError(new(InternalError), context, value, ErrorDetails{"error": err}) + result.addInternalError(new(InternalError), context, value, ErrorDetails{"error": err}) } if !has { - result.addError( + result.addInternalError( new(EnumError), context, value, @@ -424,7 +424,7 @@ result.incrementScore() } -func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateArray(currentSubSchema *subSchema, value []interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateArray %s", context.String()) @@ -436,7 +436,7 @@ // TODO explain if currentSubSchema.itemsChildrenIsSingleSchema { for i := range value { - subContext := newJsonContext(strconv.Itoa(i), context) + subContext := NewJsonContext(strconv.Itoa(i), context) validationResult := currentSubSchema.itemsChildren[0].subValidateWithContext(value[i], subContext) result.mergeErrors(validationResult) } @@ -447,7 +447,7 @@ // while we have both schemas and values, check them against each other for i := 0; i != nbItems && i != nbValues; i++ { - subContext := newJsonContext(strconv.Itoa(i), context) + subContext := NewJsonContext(strconv.Itoa(i), context) validationResult := currentSubSchema.itemsChildren[i].subValidateWithContext(value[i], subContext) result.mergeErrors(validationResult) } @@ -459,12 +459,12 @@ switch currentSubSchema.additionalItems.(type) { case bool: if !currentSubSchema.additionalItems.(bool) { - result.addError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{}) + result.addInternalError(new(ArrayNoAdditionalItemsError), context, value, ErrorDetails{}) } case *subSchema: additionalItemSchema := currentSubSchema.additionalItems.(*subSchema) for i := nbItems; i != nbValues; i++ { - subContext := newJsonContext(strconv.Itoa(i), context) + subContext := NewJsonContext(strconv.Itoa(i), context) validationResult := additionalItemSchema.subValidateWithContext(value[i], subContext) result.mergeErrors(validationResult) } @@ -476,7 +476,7 @@ // minItems & maxItems if currentSubSchema.minItems != nil { if nbValues < int(*currentSubSchema.minItems) { - result.addError( + result.addInternalError( new(ArrayMinItemsError), context, value, @@ -486,7 +486,7 @@ } if currentSubSchema.maxItems != nil { if nbValues > int(*currentSubSchema.maxItems) { - result.addError( + result.addInternalError( new(ArrayMaxItemsError), context, value, @@ -501,10 +501,10 @@ for _, v := range value { vString, err := marshalToJsonString(v) if err != nil { - result.addError(new(InternalError), context, value, ErrorDetails{"err": err}) + result.addInternalError(new(InternalError), context, value, ErrorDetails{"err": err}) } if isStringInSlice(stringifiedItems, *vString) { - result.addError( + result.addInternalError( new(ItemsMustBeUniqueError), context, value, @@ -518,7 +518,7 @@ result.incrementScore() } -func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string]interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateObject(currentSubSchema *subSchema, value map[string]interface{}, result *Result, context *JsonContext) { if internalLogEnabled { internalLog("validateObject %s", context.String()) @@ -528,7 +528,7 @@ // minProperties & maxProperties: if currentSubSchema.minProperties != nil { if len(value) < int(*currentSubSchema.minProperties) { - result.addError( + result.addInternalError( new(ArrayMinPropertiesError), context, value, @@ -538,7 +538,7 @@ } if currentSubSchema.maxProperties != nil { if len(value) > int(*currentSubSchema.maxProperties) { - result.addError( + result.addInternalError( new(ArrayMaxPropertiesError), context, value, @@ -553,7 +553,7 @@ if ok { result.incrementScore() } else { - result.addError( + result.addInternalError( new(RequiredError), context, value, @@ -584,7 +584,7 @@ if found { if pp_has && !pp_match { - result.addError( + result.addInternalError( new(AdditionalPropertyNotAllowedError), context, value[pk], @@ -595,7 +595,7 @@ } else { if !pp_has || !pp_match { - result.addError( + result.addInternalError( new(AdditionalPropertyNotAllowedError), context, value[pk], @@ -647,7 +647,7 @@ if pp_has && !pp_match { - result.addError( + result.addInternalError( new(InvalidPropertyPatternError), context, value[pk], @@ -664,7 +664,7 @@ result.incrementScore() } -func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key string, value interface{}, result *Result, context *jsonContext) (has bool, matched bool) { +func (v *subSchema) validatePatternProperty(currentSubSchema *subSchema, key string, value interface{}, result *Result, context *JsonContext) (has bool, matched bool) { if internalLogEnabled { internalLog("validatePatternProperty %s", context.String()) @@ -678,7 +678,7 @@ for pk, pv := range currentSubSchema.patternProperties { if matches, _ := regexp.MatchString(pk, key); matches { has = true - subContext := newJsonContext(key, context) + subContext := NewJsonContext(key, context) validationResult := pv.subValidateWithContext(value, subContext) result.mergeErrors(validationResult) if validationResult.Valid() { @@ -696,7 +696,7 @@ return has, true } -func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateString(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) { // Ignore JSON numbers if isJsonNumber(value) { @@ -718,7 +718,7 @@ // minLength & maxLength: if currentSubSchema.minLength != nil { if utf8.RuneCount([]byte(stringValue)) < int(*currentSubSchema.minLength) { - result.addError( + result.addInternalError( new(StringLengthGTEError), context, value, @@ -728,7 +728,7 @@ } if currentSubSchema.maxLength != nil { if utf8.RuneCount([]byte(stringValue)) > int(*currentSubSchema.maxLength) { - result.addError( + result.addInternalError( new(StringLengthLTEError), context, value, @@ -740,7 +740,7 @@ // pattern: if currentSubSchema.pattern != nil { if !currentSubSchema.pattern.MatchString(stringValue) { - result.addError( + result.addInternalError( new(DoesNotMatchPatternError), context, value, @@ -753,7 +753,7 @@ // format if currentSubSchema.format != "" { if !FormatCheckers.IsFormat(currentSubSchema.format, stringValue) { - result.addError( + result.addInternalError( new(DoesNotMatchFormatError), context, value, @@ -765,7 +765,7 @@ result.incrementScore() } -func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{}, result *Result, context *jsonContext) { +func (v *subSchema) validateNumber(currentSubSchema *subSchema, value interface{}, result *Result, context *JsonContext) { // Ignore non numbers if !isJsonNumber(value) { @@ -784,7 +784,7 @@ if currentSubSchema.multipleOf != nil { if q := new(big.Float).Quo(float64Value, currentSubSchema.multipleOf); !q.IsInt() { - result.addError( + result.addInternalError( new(MultipleOfError), context, resultErrorFormatJsonNumber(number), @@ -797,7 +797,7 @@ if currentSubSchema.maximum != nil { if currentSubSchema.exclusiveMaximum { if float64Value.Cmp(currentSubSchema.maximum) >= 0 { - result.addError( + result.addInternalError( new(NumberLTError), context, resultErrorFormatJsonNumber(number), @@ -808,7 +808,7 @@ } } else { if float64Value.Cmp(currentSubSchema.maximum) == 1 { - result.addError( + result.addInternalError( new(NumberLTEError), context, resultErrorFormatJsonNumber(number), @@ -825,7 +825,7 @@ if currentSubSchema.exclusiveMinimum { if float64Value.Cmp(currentSubSchema.minimum) <= 0 { // if float64Value <= *currentSubSchema.minimum { - result.addError( + result.addInternalError( new(NumberGTError), context, resultErrorFormatJsonNumber(number), @@ -836,7 +836,7 @@ } } else { if float64Value.Cmp(currentSubSchema.minimum) == -1 { - result.addError( + result.addInternalError( new(NumberGTEError), context, resultErrorFormatJsonNumber(number), @@ -851,7 +851,7 @@ // format if currentSubSchema.format != "" { if !FormatCheckers.IsFormat(currentSubSchema.format, float64Value) { - result.addError( + result.addInternalError( new(DoesNotMatchFormatError), context, value,