diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 2f9768a..1c298fd 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -76,7 +76,9 @@ func InvalidDeepObject(param *v3.Parameter, qp *helpers.QueryParam) *ValidationE } } -func QueryParameterMissing(param *v3.Parameter) *ValidationError { +func QueryParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "required") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -86,10 +88,23 @@ func QueryParameterMissing(param *v3.Parameter) *ValidationError { SpecLine: param.GoLow().Required.KeyNode.Line, SpecCol: param.GoLow().Required.KeyNode.Column, HowToFix: HowToFixMissingValue, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Required query parameter '%s' is missing", param.Name), + FieldName: param.Name, + FieldPath: "", + InstancePath: []string{}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func HeaderParameterMissing(param *v3.Parameter) *ValidationError { +func HeaderParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/required", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -99,10 +114,20 @@ func HeaderParameterMissing(param *v3.Parameter) *ValidationError { SpecLine: param.GoLow().Required.KeyNode.Line, SpecCol: param.GoLow().Required.KeyNode.Column, HowToFix: HowToFixMissingValue, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Required header parameter '%s' is missing", param.Name), + FieldName: param.Name, + FieldPath: "", + InstancePath: []string{}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string) *ValidationError { +func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -112,15 +137,25 @@ func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string) *Validation SpecLine: param.GoLow().Schema.Value.Schema().Type.KeyNode.Line, SpecCol: param.GoLow().Schema.Value.Schema().Type.KeyNode.Line, HowToFix: HowToFixInvalidEncoding, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Header value '%s' cannot be decoded as object (malformed encoding)", val), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") + + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -131,12 +166,21 @@ func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectQueryParamArrayBoolean( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -147,10 +191,19 @@ func IncorrectQueryParamArrayBoolean( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } -func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64) *ValidationError { +func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "maxItems") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -161,10 +214,19 @@ func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expec SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixInvalidMaxItems, expected), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array has %d items, but maximum is %d", actual, expected), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64) *ValidationError { +func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expected, actual int64, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "minItems") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -175,10 +237,19 @@ func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expec SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixInvalidMinItems, expected), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array has %d items, but minimum is %d", actual, expected), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, duplicates string) *ValidationError { +func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, duplicates string, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "uniqueItems") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -188,12 +259,21 @@ func IncorrectParamArrayUniqueItems(param *v3.Parameter, sch *base.Schema, dupli SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: sch, HowToFix: "Ensure the array values are all unique", + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array contains duplicate values: %s", duplicates), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectCookieParamArrayBoolean( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -204,12 +284,21 @@ func IncorrectCookieParamArrayBoolean( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } func IncorrectQueryParamArrayInteger( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -220,12 +309,21 @@ func IncorrectQueryParamArrayInteger( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid integer", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } func IncorrectQueryParamArrayNumber( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -236,12 +334,21 @@ func IncorrectQueryParamArrayNumber( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } func IncorrectCookieParamArrayNumber( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -252,10 +359,22 @@ func IncorrectCookieParamArrayNumber( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } -func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/content/application~1json/schema", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -266,10 +385,19 @@ func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema SpecCol: param.GoLow().FindContent(helpers.JSONContentType).ValueNode.Column, Context: sch, HowToFix: HowToFixInvalidJSON, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not valid JSON", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -281,10 +409,19 @@ func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema) * ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid boolean", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -296,10 +433,19 @@ func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema) ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid integer", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidQueryParamNumber(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidQueryParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -311,15 +457,25 @@ func InvalidQueryParamNumber(param *v3.Parameter, ef string, sch *base.Schema) * ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid number", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") + + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -331,10 +487,17 @@ func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema) * ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedItemsSchema string) *ValidationError { var enums []string // look at that model fly! for i := range param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.Value { @@ -342,6 +505,9 @@ func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Sche fmt.Sprint(param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.Value[i].Value.Value)) } validEnums := strings.Join(enums, ", ") + + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/enum") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -352,10 +518,22 @@ func IncorrectQueryParamEnumArray(param *v3.Parameter, ef string, sch *base.Sche SpecCol: param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.KeyNode.Line, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } -func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + escapedPath = strings.TrimPrefix(escapedPath, "~1") + keywordLocation := fmt.Sprintf("/paths/%s/%s/parameters/%s/allowReserved", escapedPath, strings.ToLower(operation), param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -366,10 +544,19 @@ func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema) * SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixReservedValues, url.QueryEscape(ef)), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' contains reserved characters but allowReserved is false", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -381,10 +568,19 @@ func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema) ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid integer", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -396,10 +592,19 @@ func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema) ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid number", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -410,10 +615,19 @@ func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid integer", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -424,10 +638,19 @@ func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid number", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -439,10 +662,19 @@ func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema) ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid boolean", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectCookieParamBool(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectCookieParamBool(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -453,15 +685,25 @@ func IncorrectCookieParamBool(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid boolean", ef), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") + + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationCookie, @@ -472,12 +714,21 @@ func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema) SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectHeaderParamArrayBoolean( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -488,12 +739,21 @@ func IncorrectHeaderParamArrayBoolean( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } func IncorrectHeaderParamArrayNumber( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, operation string, renderedItemsSchema string, ) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/type") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -504,10 +764,21 @@ func IncorrectHeaderParamArrayNumber( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedItemsSchema, + }}, } } -func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema) *ValidationError { +func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -518,15 +789,27 @@ func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema) SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *ValidationError { +func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/enum", escapedPath, param.Name) + var enums []string for i := range sch.Enum { enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -537,10 +820,21 @@ func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema) *V SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' does not match any enum values: [%s]", ef, validEnums), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schema) *ValidationError { +func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -552,10 +846,21 @@ func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schem ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid integer", item), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema) *ValidationError { +func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema, pathTemplate string, renderedSchema string) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -566,12 +871,23 @@ func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema SpecCol: param.GoLow().Schema.KeyNode.Column, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Value '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectPathParamArrayNumber( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -582,12 +898,23 @@ func IncorrectPathParamArrayNumber( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid number", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectPathParamArrayInteger( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -598,12 +925,23 @@ func IncorrectPathParamArrayInteger( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid integer", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } func IncorrectPathParamArrayBoolean( - param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, + param *v3.Parameter, item string, sch *base.Schema, itemsSchema *base.Schema, pathTemplate string, renderedSchema string, ) *ValidationError { + escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") + keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -614,10 +952,24 @@ func IncorrectPathParamArrayBoolean( SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Array item '%s' is not a valid boolean", item), + FieldName: param.Name, + InstancePath: []string{param.Name, "[item]"}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, } } -func PathParameterMissing(param *v3.Parameter) *ValidationError { +func PathParameterMissing(param *v3.Parameter, pathTemplate string, actualPath string) *ValidationError { + actualSegments := strings.Split(strings.Trim(actualPath, "/"), "/") + + encodedPath := strings.ReplaceAll(pathTemplate, "~", "~0") + encodedPath = strings.ReplaceAll(encodedPath, "/", "~1") + encodedPath = strings.TrimPrefix(encodedPath, "~1") + keywordLoc := fmt.Sprintf("/paths/%s/parameters/%s/required", encodedPath, param.Name) + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationPath, @@ -627,5 +979,12 @@ func PathParameterMissing(param *v3.Parameter) *ValidationError { SpecLine: param.GoLow().Required.KeyNode.Line, SpecCol: param.GoLow().Required.KeyNode.Column, HowToFix: HowToFixMissingValue, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Required path parameter '%s' is missing from path '%s'", param.Name, actualPath), + FieldName: param.Name, + FieldPath: "", + InstancePath: actualSegments, + KeywordLocation: keywordLoc, + }}, } } diff --git a/errors/parameter_errors_test.go b/errors/parameter_errors_test.go index 4f8304a..4fb5b9b 100644 --- a/errors/parameter_errors_test.go +++ b/errors/parameter_errors_test.go @@ -119,7 +119,7 @@ func TestQueryParameterMissing(t *testing.T) { param := createMockParameterWithSchema() // Call the function - err := QueryParameterMissing(param) + err := QueryParameterMissing(param, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -134,7 +134,7 @@ func TestHeaderParameterMissing(t *testing.T) { param := createMockParameterWithSchema() // Call the function - err := HeaderParameterMissing(param) + err := HeaderParameterMissing(param, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -150,7 +150,7 @@ func TestHeaderParameterCannotBeDecoded(t *testing.T) { val := "malformed_header_value" // Call the function - err := HeaderParameterCannotBeDecoded(param, val) + err := HeaderParameterCannotBeDecoded(param, val, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -187,7 +187,7 @@ func TestIncorrectHeaderParamEnum(t *testing.T) { schema := base.NewSchema(s) // Call the function with an invalid enum value - err := IncorrectHeaderParamEnum(param, "invalidEnum", schema) + err := IncorrectHeaderParamEnum(param, "invalidEnum", schema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -223,7 +223,7 @@ func TestIncorrectQueryParamArrayBoolean(t *testing.T) { schema := base.NewSchema(s) // Call the function with an invalid boolean value in the array - err := IncorrectQueryParamArrayBoolean(param, "notBoolean", schema, schema.Items.A.Schema()) + err := IncorrectQueryParamArrayBoolean(param, "notBoolean", schema, schema.Items.A.Schema(), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -314,7 +314,7 @@ func TestIncorrectCookieParamArrayBoolean(t *testing.T) { itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid boolean value in the array - err := IncorrectCookieParamArrayBoolean(param, "notBoolean", s, itemsSchema) + err := IncorrectCookieParamArrayBoolean(param, "notBoolean", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -375,7 +375,7 @@ func TestIncorrectQueryParamArrayInteger(t *testing.T) { itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid number value in the array - err := IncorrectQueryParamArrayInteger(param, "notNumber", s, itemsSchema) + err := IncorrectQueryParamArrayInteger(param, "notNumber", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -394,7 +394,7 @@ func TestIncorrectQueryParamArrayNumber(t *testing.T) { itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid number value in the array - err := IncorrectQueryParamArrayNumber(param, "notNumber", s, itemsSchema) + err := IncorrectQueryParamArrayNumber(param, "notNumber", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -455,7 +455,7 @@ func TestIncorrectCookieParamArrayNumber(t *testing.T) { itemsSchema := base.NewSchema(baseSchema.Items.Value.A.Schema()) // Call the function with an invalid number value in the cookie array - err := IncorrectCookieParamArrayNumber(param, "notNumber", s, itemsSchema) + err := IncorrectCookieParamArrayNumber(param, "notNumber", s, itemsSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -531,7 +531,7 @@ func TestIncorrectParamEncodingJSON(t *testing.T) { baseSchema := createMockLowBaseSchema() // Call the function with an invalid JSON value - err := IncorrectParamEncodingJSON(param, "invalidJSON", base.NewSchema(baseSchema)) + err := IncorrectParamEncodingJSON(param, "invalidJSON", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -560,7 +560,7 @@ func TestIncorrectQueryParamBool(t *testing.T) { }) // Call the function with an invalid boolean value - err := IncorrectQueryParamBool(param, "notBoolean", base.NewSchema(baseSchema)) + err := IncorrectQueryParamBool(param, "notBoolean", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -576,7 +576,7 @@ func TestInvalidQueryParamNumber(t *testing.T) { baseSchema := createMockLowBaseSchema() // Call the function with an invalid number value - err := InvalidQueryParamNumber(param, "notNumber", base.NewSchema(baseSchema)) + err := InvalidQueryParamNumber(param, "notNumber", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -592,7 +592,7 @@ func TestInvalidQueryParamInteger(t *testing.T) { baseSchema := createMockLowBaseSchema() // Call the function with an invalid number value - err := InvalidQueryParamInteger(param, "notNumber", base.NewSchema(baseSchema)) + err := InvalidQueryParamInteger(param, "notNumber", base.NewSchema(baseSchema), "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -617,7 +617,7 @@ func TestIncorrectQueryParamEnum(t *testing.T) { param.GoLow().Schema.Value.Schema().Enum.KeyNode = &yaml.Node{} // Call the function with an invalid enum value - err := IncorrectQueryParamEnum(param, "invalidEnum", highSchema) + err := IncorrectQueryParamEnum(param, "invalidEnum", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -646,7 +646,7 @@ func TestIncorrectQueryParamEnumArray(t *testing.T) { } // Call the function with an invalid enum value - err := IncorrectQueryParamEnumArray(param, "invalidEnum", highSchema) + err := IncorrectQueryParamEnumArray(param, "invalidEnum", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -669,7 +669,7 @@ func TestIncorrectReservedValues(t *testing.T) { param := createMockParameter() param.Name = "borked::?^&*" - err := IncorrectReservedValues(param, "borked::?^&*", highSchema) + err := IncorrectReservedValues(param, "borked::?^&*", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -692,7 +692,7 @@ func TestInvalidHeaderParamInteger(t *testing.T) { param := createMockParameter() param.Name = "bunny" - err := InvalidHeaderParamInteger(param, "bunmy", highSchema) + err := InvalidHeaderParamInteger(param, "bunmy", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -715,7 +715,7 @@ func TestInvalidHeaderParamNumber(t *testing.T) { param := createMockParameter() param.Name = "bunny" - err := InvalidHeaderParamNumber(param, "bunmy", highSchema) + err := InvalidHeaderParamNumber(param, "bunmy", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -738,7 +738,7 @@ func TestInvalidCookieParamNumber(t *testing.T) { param := createMockParameter() param.Name = "cookies" - err := InvalidCookieParamNumber(param, "milky", highSchema) + err := InvalidCookieParamNumber(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -761,7 +761,7 @@ func TestInvalidCookieParamInteger(t *testing.T) { param := createMockParameter() param.Name = "cookies" - err := InvalidCookieParamInteger(param, "milky", highSchema) + err := InvalidCookieParamInteger(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -784,7 +784,7 @@ func TestIncorrectHeaderParamBool(t *testing.T) { param := createMockParameter() param.Name = "cookies" - err := IncorrectHeaderParamBool(param, "milky", highSchema) + err := IncorrectHeaderParamBool(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -807,7 +807,7 @@ func TestIncorrectCookieParamBool(t *testing.T) { param := createMockParameter() param.Name = "cookies" - err := IncorrectCookieParamBool(param, "milky", highSchema) + err := IncorrectCookieParamBool(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -837,7 +837,7 @@ items: } param.GoLow().Schema.Value.Schema().Enum.KeyNode = &yaml.Node{} - err := IncorrectCookieParamEnum(param, "milky", highSchema) + err := IncorrectCookieParamEnum(param, "milky", highSchema, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -863,7 +863,7 @@ func TestIncorrectHeaderParamArrayBoolean(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectHeaderParamArrayBoolean(param, "milky", highSchema, nil) + err := IncorrectHeaderParamArrayBoolean(param, "milky", highSchema, nil, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -889,7 +889,7 @@ func TestIncorrectHeaderParamArrayNumber(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectHeaderParamArrayNumber(param, "milky", highSchema, nil) + err := IncorrectHeaderParamArrayNumber(param, "milky", highSchema, nil, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -914,7 +914,7 @@ func TestIncorrectPathParamBool(t *testing.T) { param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectPathParamBool(param, "milky", highSchema) + err := IncorrectPathParamBool(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -945,7 +945,7 @@ items: } param.GoLow().Schema.Value.Schema().Enum.KeyNode = &yaml.Node{} - err := IncorrectPathParamEnum(param, "milky", highSchema) + err := IncorrectPathParamEnum(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -970,7 +970,7 @@ func TestIncorrectPathParamNumber(t *testing.T) { param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectPathParamNumber(param, "milky", highSchema) + err := IncorrectPathParamNumber(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -995,7 +995,7 @@ func TestIncorrectPathParamInteger(t *testing.T) { param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectPathParamInteger(param, "milky", highSchema) + err := IncorrectPathParamInteger(param, "milky", highSchema, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -1021,7 +1021,7 @@ func TestIncorrectPathParamArrayNumber(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectPathParamArrayNumber(param, "milky", highSchema, nil) + err := IncorrectPathParamArrayNumber(param, "milky", highSchema, nil, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -1047,7 +1047,7 @@ func TestIncorrectPathParamArrayInteger(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectPathParamArrayInteger(param, "milky", highSchema, nil) + err := IncorrectPathParamArrayInteger(param, "milky", highSchema, nil, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -1073,7 +1073,7 @@ func TestIncorrectPathParamArrayBoolean(t *testing.T) { param := createMockParameter() param.Name = "bubbles" - err := IncorrectPathParamArrayBoolean(param, "milky", highSchema, nil) + err := IncorrectPathParamArrayBoolean(param, "milky", highSchema, nil, "/test-path", "{}") // Validate the error require.NotNil(t, err) @@ -1098,7 +1098,7 @@ func TestPathParameterMissing(t *testing.T) { param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := PathParameterMissing(param) + err := PathParameterMissing(param, "/test/{testQueryParam}", "/test/") // Validate the error require.NotNil(t, err) @@ -1124,7 +1124,7 @@ items: param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectParamArrayMaxNumItems(param, param.Schema.Schema(), 10, 25) + err := IncorrectParamArrayMaxNumItems(param, param.Schema.Schema(), 10, 25, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -1150,7 +1150,7 @@ items: param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectParamArrayMinNumItems(param, param.Schema.Schema(), 10, 5) + err := IncorrectParamArrayMinNumItems(param, param.Schema.Schema(), 10, 5, "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) @@ -1176,7 +1176,7 @@ items: param.Schema = base.CreateSchemaProxy(highSchema) param.GoLow().Schema.KeyNode = &yaml.Node{} - err := IncorrectParamArrayUniqueItems(param, param.Schema.Schema(), "fish, cake") + err := IncorrectParamArrayUniqueItems(param, param.Schema.Schema(), "fish, cake", "/test-path", "get", "{}") // Validate the error require.NotNil(t, err) diff --git a/errors/validation_error.go b/errors/validation_error.go index 4c590ae..dd848c7 100644 --- a/errors/validation_error.go +++ b/errors/validation_error.go @@ -9,29 +9,25 @@ import ( "github.com/santhosh-tekuri/jsonschema/v6" ) -// SchemaValidationFailure is a wrapper around the jsonschema.ValidationError object, to provide a more -// user-friendly way to break down what went wrong. +// SchemaValidationFailure describes any failure that occurs when validating data +// against either an OpenAPI or JSON Schema. It aims to be a more user-friendly +// representation of the error than what is provided by the jsonschema library. type SchemaValidationFailure struct { // Reason is a human-readable message describing the reason for the error. Reason string `json:"reason,omitempty" yaml:"reason,omitempty"` - // Location is the XPath-like location of the validation failure - Location string `json:"location,omitempty" yaml:"location,omitempty"` + // InstancePath is the raw path segments from the root to the failing field + InstancePath []string `json:"instancePath,omitempty" yaml:"instancePath,omitempty"` // FieldName is the name of the specific field that failed validation (last segment of the path) FieldName string `json:"fieldName,omitempty" yaml:"fieldName,omitempty"` - // FieldPath is the JSONPath representation of the field location (e.g., "$.user.email") + // FieldPath is the JSONPath representation of the field location that failed validation (e.g., "$.user.email") FieldPath string `json:"fieldPath,omitempty" yaml:"fieldPath,omitempty"` - // InstancePath is the raw path segments from the root to the failing field - InstancePath []string `json:"instancePath,omitempty" yaml:"instancePath,omitempty"` - - // DeepLocation is the path to the validation failure as exposed by the jsonschema library. - DeepLocation string `json:"deepLocation,omitempty" yaml:"deepLocation,omitempty"` - - // AbsoluteLocation is the absolute path to the validation failure as exposed by the jsonschema library. - AbsoluteLocation string `json:"absoluteLocation,omitempty" yaml:"absoluteLocation,omitempty"` + // KeywordLocation is the JSON Pointer (RFC 6901) path to the schema keyword that failed validation + // (e.g., "/properties/age/minimum") + KeywordLocation string `json:"keywordLocation,omitempty" yaml:"keywordLocation,omitempty"` // Line is the line number where the violation occurred. This may a local line number // if the validation is a schema (only schemas are validated locally, so the line number will be relative to @@ -46,19 +42,25 @@ type SchemaValidationFailure struct { // ReferenceSchema is the schema that was referenced in the validation failure. ReferenceSchema string `json:"referenceSchema,omitempty" yaml:"referenceSchema,omitempty"` - // ReferenceObject is the object that was referenced in the validation failure. + // ReferenceObject is the object that failed schema validation ReferenceObject string `json:"referenceObject,omitempty" yaml:"referenceObject,omitempty"` // ReferenceExample is an example object generated from the schema that was referenced in the validation failure. ReferenceExample string `json:"referenceExample,omitempty" yaml:"referenceExample,omitempty"` - // The original error object, which is a jsonschema.ValidationError object. - OriginalError *jsonschema.ValidationError `json:"-" yaml:"-"` + // The original jsonschema.ValidationError object, if the schema failure originated from the jsonschema library. + OriginalJsonSchemaError *jsonschema.ValidationError `json:"-" yaml:"-"` + + // Context is the raw schema object that failed validation (for programmatic access) + Context interface{} `json:"-" yaml:"-"` } // Error returns a string representation of the error func (s *SchemaValidationFailure) Error() string { - return fmt.Sprintf("Reason: %s, Location: %s", s.Reason, s.Location) + if s.FieldPath != "" { + return fmt.Sprintf("Reason: %s, FieldPath: %s", s.Reason, s.FieldPath) + } + return fmt.Sprintf("Reason: %s", s.Reason) } // ValidationError is a struct that contains all the information about a validation error. @@ -97,7 +99,7 @@ type ValidationError struct { ParameterName string `json:"parameterName,omitempty" yaml:"parameterName,omitempty"` // SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors - // This is only populated whe the validation type is against a schema. + // This is only populated when the validation type is against a schema. SchemaValidationErrors []*SchemaValidationFailure `json:"validationErrors,omitempty" yaml:"validationErrors,omitempty"` // Context is the object that the validation error occurred on. This is usually a pointer to a schema diff --git a/errors/validation_error_test.go b/errors/validation_error_test.go index 749dedf..c68ca4c 100644 --- a/errors/validation_error_test.go +++ b/errors/validation_error_test.go @@ -13,11 +13,11 @@ import ( func TestSchemaValidationFailure_Error(t *testing.T) { // Test the Error method of SchemaValidationFailure s := &SchemaValidationFailure{ - Reason: "Invalid type", - Location: "/path/to/property", + Reason: "Invalid type", + FieldPath: "$.path.to.property", } - expectedError := "Reason: Invalid type, Location: /path/to/property" + expectedError := "Reason: Invalid type, FieldPath: $.path.to.property" require.Equal(t, expectedError, s.Error()) } @@ -48,8 +48,8 @@ func TestValidationError_Error_WithSpecLineAndColumn(t *testing.T) { func TestValidationError_Error_WithSchemaValidationErrors(t *testing.T) { // Test the Error method of ValidationError with SchemaValidationErrors schemaError := &SchemaValidationFailure{ - Reason: "Invalid enum value", - Location: "/path/to/enum", + Reason: "Invalid enum value", + FieldPath: "$.path.to.enum", } v := &ValidationError{ Message: "Enum validation failed", @@ -64,8 +64,8 @@ func TestValidationError_Error_WithSchemaValidationErrors(t *testing.T) { func TestValidationError_Error_WithSchemaValidationErrors_AndSpecLineColumn(t *testing.T) { // Test the Error method of ValidationError with SchemaValidationErrors and SpecLine and SpecCol schemaError := &SchemaValidationFailure{ - Reason: "Invalid enum value", - Location: "/path/to/enum", + Reason: "Invalid enum value", + FieldPath: "$.path.to.enum", } v := &ValidationError{ Message: "Enum validation failed", diff --git a/go.mod b/go.mod index 3936942..736e573 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v4 v4.0.0-rc.2 - golang.org/x/text v0.30.0 + golang.org/x/text v0.31.0 ) require ( @@ -19,6 +19,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pb33f/ordered-map/v2 v2.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.46.0 // indirect + golang.org/x/net v0.47.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f304134..ea0532c 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -72,6 +72,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/helpers/json_pointer.go b/helpers/json_pointer.go new file mode 100644 index 0000000..50eecac --- /dev/null +++ b/helpers/json_pointer.go @@ -0,0 +1,40 @@ +// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package helpers + +import ( + "fmt" + "strings" +) + +// EscapeJSONPointerSegment escapes a single segment for use in a JSON Pointer (RFC 6901). +// It replaces '~' with '~0' and '/' with '~1'. +func EscapeJSONPointerSegment(segment string) string { + escaped := strings.ReplaceAll(segment, "~", "~0") + escaped = strings.ReplaceAll(escaped, "/", "~1") + return escaped +} + +// ConstructParameterJSONPointer constructs a full JSON Pointer path for a parameter +// in the OpenAPI specification. +// Format: /paths/{path}/{method}/parameters/{paramName}/schema/{keyword} +// The path segment is automatically escaped according to RFC 6901. +// The keyword can be a simple keyword like "type" or a nested path like "items/type". +func ConstructParameterJSONPointer(pathTemplate, method, paramName, keyword string) string { + escapedPath := EscapeJSONPointerSegment(pathTemplate) + escapedPath = strings.TrimPrefix(escapedPath, "~1") // Remove leading slash encoding + method = strings.ToLower(method) + return fmt.Sprintf("/paths/%s/%s/parameters/%s/schema/%s", escapedPath, method, paramName, keyword) +} + +// ConstructResponseHeaderJSONPointer constructs a full JSON Pointer path for a response header +// in the OpenAPI specification. +// Format: /paths/{path}/{method}/responses/{statusCode}/headers/{headerName}/{keyword} +// The path segment is automatically escaped according to RFC 6901. +func ConstructResponseHeaderJSONPointer(pathTemplate, method, statusCode, headerName, keyword string) string { + escapedPath := EscapeJSONPointerSegment(pathTemplate) + escapedPath = strings.TrimPrefix(escapedPath, "~1") // Remove leading slash encoding + method = strings.ToLower(method) + return fmt.Sprintf("/paths/%s/%s/responses/%s/headers/%s/%s", escapedPath, method, statusCode, headerName, keyword) +} diff --git a/helpers/json_pointer_test.go b/helpers/json_pointer_test.go new file mode 100644 index 0000000..5e08b2d --- /dev/null +++ b/helpers/json_pointer_test.go @@ -0,0 +1,149 @@ +// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEscapeJSONPointerSegment(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "no special characters", + input: "simple", + expected: "simple", + }, + { + name: "tilde only", + input: "some~thing", + expected: "some~0thing", + }, + { + name: "slash only", + input: "path/to/something", + expected: "path~1to~1something", + }, + { + name: "both tilde and slash", + input: "path/with~special/chars~", + expected: "path~1with~0special~1chars~0", + }, + { + name: "path template", + input: "/users/{id}", + expected: "~1users~1{id}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := EscapeJSONPointerSegment(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestConstructParameterJSONPointer(t *testing.T) { + tests := []struct { + name string + pathTemplate string + method string + paramName string + keyword string + expected string + }{ + { + name: "simple path with query parameter type", + pathTemplate: "/users", + method: "GET", + paramName: "limit", + keyword: "type", + expected: "/paths/users/get/parameters/limit/schema/type", + }, + { + name: "path with parameter and enum keyword", + pathTemplate: "/users/{id}", + method: "POST", + paramName: "status", + keyword: "enum", + expected: "/paths/users~1{id}/post/parameters/status/schema/enum", + }, + { + name: "path with tilde character", + pathTemplate: "/some~path", + method: "PUT", + paramName: "value", + keyword: "format", + expected: "/paths/some~0path/put/parameters/value/schema/format", + }, + { + name: "path with multiple slashes", + pathTemplate: "/api/v1/users/{userId}/posts/{postId}", + method: "DELETE", + paramName: "filter", + keyword: "required", + expected: "/paths/api~1v1~1users~1{userId}~1posts~1{postId}/delete/parameters/filter/schema/required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConstructParameterJSONPointer(tt.pathTemplate, tt.method, tt.paramName, tt.keyword) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestConstructResponseHeaderJSONPointer(t *testing.T) { + tests := []struct { + name string + pathTemplate string + method string + statusCode string + headerName string + keyword string + expected string + }{ + { + name: "simple response header", + pathTemplate: "/health", + method: "GET", + statusCode: "200", + headerName: "X-Request-ID", + keyword: "required", + expected: "/paths/health/get/responses/200/headers/X-Request-ID/required", + }, + { + name: "path with parameter", + pathTemplate: "/users/{id}", + method: "POST", + statusCode: "201", + headerName: "Location", + keyword: "schema", + expected: "/paths/users~1{id}/post/responses/201/headers/Location/schema", + }, + { + name: "path with tilde and slash", + pathTemplate: "/some~path/to/resource", + method: "PUT", + statusCode: "204", + headerName: "ETag", + keyword: "type", + expected: "/paths/some~0path~1to~1resource/put/responses/204/headers/ETag/type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConstructResponseHeaderJSONPointer(tt.pathTemplate, tt.method, tt.statusCode, tt.headerName, tt.keyword) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/parameters/cookie_parameters.go b/parameters/cookie_parameters.go index 8f74b9f..cd85f44 100644 --- a/parameters/cookie_parameters.go +++ b/parameters/cookie_parameters.go @@ -4,6 +4,7 @@ package parameters import ( + "encoding/json" "fmt" "net/http" "strconv" @@ -43,6 +44,7 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, // extract params for the operation params := helpers.ExtractParamsForOperation(request, pathItem) var validationErrors []*errors.ValidationError + operation := strings.ToLower(request.Method) for _, p := range params { if p.In == helpers.Cookie { for _, cookie := range request.Cookies() { @@ -52,6 +54,15 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, if p.Schema != nil { sch = p.Schema.Schema() } + + // Render schema once for ReferenceSchema field in errors + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + pType := sch.Type for _, ty := range pType { @@ -59,7 +70,7 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, case helpers.Integer: if _, err := strconv.ParseInt(cookie.Value, 10, 64); err != nil { validationErrors = append(validationErrors, - errors.InvalidCookieParamInteger(p, strings.ToLower(cookie.Value), sch)) + errors.InvalidCookieParamInteger(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) break } // check if enum is in range @@ -73,13 +84,13 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch)) + errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } } case helpers.Number: if _, err := strconv.ParseFloat(cookie.Value, 64); err != nil { validationErrors = append(validationErrors, - errors.InvalidCookieParamNumber(p, strings.ToLower(cookie.Value), sch)) + errors.InvalidCookieParamNumber(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) break } // check if enum is in range @@ -93,13 +104,13 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch)) + errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } } case helpers.Boolean: if _, err := strconv.ParseBool(cookie.Value); err != nil { validationErrors = append(validationErrors, - errors.IncorrectCookieParamBool(p, strings.ToLower(cookie.Value), sch)) + errors.IncorrectCookieParamBool(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } case helpers.Object: if !p.IsExploded() { @@ -125,7 +136,7 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, // only check if items is a schema, not a boolean if sch.Items.IsA() { validationErrors = append(validationErrors, - ValidateCookieArray(sch, p, cookie.Value)...) + ValidateCookieArray(sch, p, cookie.Value, pathValue, operation, renderedSchema)...) } } @@ -143,7 +154,7 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch)) + errors.IncorrectCookieParamEnum(p, strings.ToLower(cookie.Value), sch, pathValue, operation, renderedSchema)) } } } diff --git a/parameters/header_parameters.go b/parameters/header_parameters.go index fed3fc8..78c3a73 100644 --- a/parameters/header_parameters.go +++ b/parameters/header_parameters.go @@ -4,6 +4,7 @@ package parameters import ( + "encoding/json" "fmt" "net/http" "strconv" @@ -46,6 +47,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, var validationErrors []*errors.ValidationError seenHeaders := make(map[string]bool) + operation := strings.ToLower(request.Method) for _, p := range params { if p.In == helpers.Header { @@ -56,6 +58,15 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, if p.Schema != nil { sch = p.Schema.Schema() } + + // Render schema once for ReferenceSchema field in errors + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + pType := sch.Type for _, ty := range pType { @@ -63,7 +74,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, case helpers.Integer: if _, err := strconv.ParseInt(param, 10, 64); err != nil { validationErrors = append(validationErrors, - errors.InvalidHeaderParamInteger(p, strings.ToLower(param), sch)) + errors.InvalidHeaderParamInteger(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) break } // check if the param is within the enum @@ -77,14 +88,14 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(param), sch)) + errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } } case helpers.Number: if _, err := strconv.ParseFloat(param, 64); err != nil { validationErrors = append(validationErrors, - errors.InvalidHeaderParamNumber(p, strings.ToLower(param), sch)) + errors.InvalidHeaderParamNumber(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) break } // check if the param is within the enum @@ -98,14 +109,14 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectCookieParamEnum(p, strings.ToLower(param), sch)) + errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } } case helpers.Boolean: if _, err := strconv.ParseBool(param); err != nil { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamBool(p, strings.ToLower(param), sch)) + errors.IncorrectHeaderParamBool(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } case helpers.Object: @@ -123,7 +134,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, if len(encodedObj) == 0 { validationErrors = append(validationErrors, - errors.HeaderParameterCannotBeDecoded(p, strings.ToLower(param))) + errors.HeaderParameterCannotBeDecoded(p, strings.ToLower(param), pathValue, operation, renderedSchema)) break } @@ -144,7 +155,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, if !p.IsExploded() { // only unexploded arrays are supported for cookie params if sch.Items.IsA() { validationErrors = append(validationErrors, - ValidateHeaderArray(sch, p, param)...) + ValidateHeaderArray(sch, p, param, pathValue, operation, renderedSchema)...) } } @@ -162,7 +173,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch)) + errors.IncorrectHeaderParamEnum(p, strings.ToLower(param), sch, pathValue, operation, renderedSchema)) } } } @@ -172,11 +183,23 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, validationErrors = append(validationErrors, ValidateSingleParameterSchema(sch, param, p.Name, - lowbase.SchemaLabel, p.Name, helpers.ParameterValidation, helpers.ParameterValidationHeader, v.options)...) + lowbase.SchemaLabel, p.Name, helpers.ParameterValidation, helpers.ParameterValidationHeader, v.options, + pathValue, + operation)...) } } else { if p.Required != nil && *p.Required { - validationErrors = append(validationErrors, errors.HeaderParameterMissing(p)) + // Render schema for missing required parameter + var renderedSchema string + if p.Schema != nil { + sch := p.Schema.Schema() + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + } + validationErrors = append(validationErrors, errors.HeaderParameterMissing(p, pathValue, operation, renderedSchema)) } } } diff --git a/parameters/path_parameters.go b/parameters/path_parameters.go index 31f7e3c..3522c1d 100644 --- a/parameters/path_parameters.go +++ b/parameters/path_parameters.go @@ -4,6 +4,7 @@ package parameters import ( + "encoding/json" "fmt" "net/http" "net/url" @@ -46,6 +47,9 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p submittedSegments := strings.Split(paths.StripRequestPath(request, v.document), helpers.Slash) pathSegments := strings.Split(pathValue, helpers.Slash) + // get the operation method for error reporting + operation := strings.ToLower(request.Method) + // extract params for the operation params := helpers.ExtractParamsForOperation(request, pathItem) var validationErrors []*errors.ValidationError @@ -129,7 +133,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p if decodedParamValue == "" { // Mandatory path parameter cannot be empty if p.Required != nil && *p.Required { - validationErrors = append(validationErrors, errors.PathParameterMissing(p)) + validationErrors = append(validationErrors, errors.PathParameterMissing(p, pathValue, request.URL.Path)) break } continue @@ -138,6 +142,14 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p // extract the schema from the parameter sch := p.Schema.Schema() + // Render schema once for ReferenceSchema field in errors + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + // check enum (if present) enumCheck := func(decodedValue string) { matchFound := false @@ -149,7 +161,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectPathParamEnum(p, strings.ToLower(decodedValue), sch)) + errors.IncorrectPathParamEnum(p, strings.ToLower(decodedValue), sch, pathValue, renderedSchema)) } } @@ -176,11 +188,13 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p helpers.ParameterValidation, helpers.ParameterValidationPath, v.options, + pathValue, + operation, )...) case helpers.Integer: // simple use case is already handled in find param. - rawParamValue, paramValueParsed, err := v.resolveInteger(sch, p, isLabel, isMatrix, decodedParamValue) + rawParamValue, paramValueParsed, err := v.resolveInteger(sch, p, isLabel, isMatrix, decodedParamValue, pathValue, renderedSchema) if err != nil { validationErrors = append(validationErrors, err...) break @@ -199,11 +213,13 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p helpers.ParameterValidation, helpers.ParameterValidationPath, v.options, + pathValue, + operation, )...) case helpers.Number: // simple use case is already handled in find param. - rawParamValue, paramValueParsed, err := v.resolveNumber(sch, p, isLabel, isMatrix, decodedParamValue) + rawParamValue, paramValueParsed, err := v.resolveNumber(sch, p, isLabel, isMatrix, decodedParamValue, pathValue, renderedSchema) if err != nil { validationErrors = append(validationErrors, err...) break @@ -222,19 +238,21 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p helpers.ParameterValidation, helpers.ParameterValidationPath, v.options, + pathValue, + operation, )...) case helpers.Boolean: if isLabel && p.Style == helpers.LabelStyle { if _, err := strconv.ParseBool(decodedParamValue[1:]); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamBool(p, decodedParamValue[1:], sch)) + errors.IncorrectPathParamBool(p, decodedParamValue[1:], sch, pathValue, renderedSchema)) } } if isSimple { if _, err := strconv.ParseBool(decodedParamValue); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamBool(p, decodedParamValue, sch)) + errors.IncorrectPathParamBool(p, decodedParamValue, sch, pathValue, renderedSchema)) } } if isMatrix && p.Style == helpers.MatrixStyle { @@ -242,7 +260,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p decodedForMatrix := strings.Replace(decodedParamValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) if _, err := strconv.ParseBool(decodedForMatrix); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamBool(p, decodedForMatrix, sch)) + errors.IncorrectPathParamBool(p, decodedForMatrix, sch, pathValue, renderedSchema)) } } case helpers.Object: @@ -290,6 +308,15 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p // extract the items schema in order to validate the array items. if sch.Items != nil && sch.Items.IsA() { iSch := sch.Items.A.Schema() + + // Render items schema once for ReferenceSchema field in array errors + var renderedItemsSchema string + if iSch != nil { + rendered, _ := iSch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedItemsSchema = string(schemaBytes) + } + for n := range iSch.Type { // determine how to explode the array var arrayValues []string @@ -317,14 +344,14 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p for pv := range arrayValues { if _, err := strconv.ParseInt(arrayValues[pv], 10, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamArrayInteger(p, arrayValues[pv], sch, iSch)) + errors.IncorrectPathParamArrayInteger(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) } } case helpers.Number: for pv := range arrayValues { if _, err := strconv.ParseFloat(arrayValues[pv], 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamArrayNumber(p, arrayValues[pv], sch, iSch)) + errors.IncorrectPathParamArrayNumber(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) } } case helpers.Boolean: @@ -332,7 +359,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p bc := len(validationErrors) if _, err := strconv.ParseBool(arrayValues[pv]); err != nil { validationErrors = append(validationErrors, - errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch)) + errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) continue } if len(validationErrors) == bc { @@ -340,7 +367,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p // need to catch this edge case. if arrayValues[pv] == "0" || arrayValues[pv] == "1" { validationErrors = append(validationErrors, - errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch)) + errors.IncorrectPathParamArrayBoolean(p, arrayValues[pv], sch, iSch, pathValue, renderedItemsSchema)) continue } } @@ -364,11 +391,11 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p return true, nil } -func (v *paramValidator) resolveNumber(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string) (string, float64, []*errors.ValidationError) { +func (v *paramValidator) resolveNumber(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string, pathValue string, renderedSchema string) (string, float64, []*errors.ValidationError) { if isLabel && p.Style == helpers.LabelStyle { paramValueParsed, err := strconv.ParseFloat(paramValue[1:], 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue[1:], paramValueParsed, nil } @@ -377,22 +404,22 @@ func (v *paramValidator) resolveNumber(sch *base.Schema, p *v3.Parameter, isLabe paramValue = strings.Replace(paramValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) paramValueParsed, err := strconv.ParseFloat(paramValue, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } paramValueParsed, err := strconv.ParseFloat(paramValue, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue, sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamNumber(p, paramValue, sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } -func (v *paramValidator) resolveInteger(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string) (string, int64, []*errors.ValidationError) { +func (v *paramValidator) resolveInteger(sch *base.Schema, p *v3.Parameter, isLabel bool, isMatrix bool, paramValue string, pathValue string, renderedSchema string) (string, int64, []*errors.ValidationError) { if isLabel && p.Style == helpers.LabelStyle { paramValueParsed, err := strconv.ParseInt(paramValue[1:], 10, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue[1:], paramValueParsed, nil } @@ -401,13 +428,13 @@ func (v *paramValidator) resolveInteger(sch *base.Schema, p *v3.Parameter, isLab paramValue = strings.Replace(paramValue[1:], fmt.Sprintf("%s=", p.Name), "", 1) paramValueParsed, err := strconv.ParseInt(paramValue, 10, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue[1:], sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } paramValueParsed, err := strconv.ParseInt(paramValue, 10, 64) if err != nil { - return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue, sch)} + return "", 0, []*errors.ValidationError{errors.IncorrectPathParamInteger(p, paramValue, sch, pathValue, renderedSchema)} } return paramValue, paramValueParsed, nil } diff --git a/parameters/path_parameters_test.go b/parameters/path_parameters_test.go index a12a82b..02f2068 100644 --- a/parameters/path_parameters_test.go +++ b/parameters/path_parameters_test.go @@ -440,7 +440,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: minimum: got 1, want 10, Location: /minimum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: minimum: got 1, want 10", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_SimpleEncodedPath_MinimumInteger(t *testing.T) { @@ -495,7 +495,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: maximum: got 11, want 10, Location: /maximum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: maximum: got 11, want 10", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_SimpleEncodedPath_MaximumInteger(t *testing.T) { @@ -577,7 +577,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: minimum: got 1.3, want 10.2, Location: /minimum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: minimum: got 1.3, want 10.2", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_SimpleEncodedPath_MinimumNumber(t *testing.T) { @@ -632,7 +632,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: maximum: got 11.2, want 10.2, Location: /maximum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: maximum: got 11.2, want 10.2", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_SimpleEncodedPath_MaximumNumber(t *testing.T) { @@ -741,7 +741,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: minimum: got 3, want 10, Location: /minimum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: minimum: got 3, want 10", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_LabelEncodedPath_MaximumIntegerViolation(t *testing.T) { @@ -771,7 +771,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: maximum: got 32, want 10, Location: /maximum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: maximum: got 32, want 10", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_LabelEncodedPath_InvalidBoolean(t *testing.T) { @@ -1278,7 +1278,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: minimum: got 3, want 5, Location: /minimum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: minimum: got 3, want 5", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_MatrixEncodedPath_MaximumIntegerViolation(t *testing.T) { @@ -1308,7 +1308,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: maximum: got 30, want 5, Location: /maximum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: maximum: got 30, want 5", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_MatrixEncodedPath_InvalidNumber(t *testing.T) { @@ -1365,7 +1365,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: minimum: got 3, want 5, Location: /minimum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: minimum: got 3, want 5", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_MatrixEncodedPath_MaximumNumberViolation(t *testing.T) { @@ -1395,7 +1395,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: maximum: got 30, want 5, Location: /maximum", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: maximum: got 30, want 5", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_MatrixEncodedPath_ValidPrimitiveBoolean(t *testing.T) { @@ -1796,7 +1796,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: minLength: got 3, want 4, Location: /minLength", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: minLength: got 3, want 4", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_PathParamStringMaxLengthViolation(t *testing.T) { @@ -1825,7 +1825,7 @@ paths: assert.Len(t, errors, 1) assert.Equal(t, "Path parameter 'burgerId' failed to validate", errors[0].Message) assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "Reason: maxLength: got 3, want 1, Location: /maxLength", errors[0].SchemaValidationErrors[0].Error()) + assert.Equal(t, "Reason: maxLength: got 3, want 1", errors[0].SchemaValidationErrors[0].Error()) } func TestNewValidator_PathParamIntegerEnumValid(t *testing.T) { diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index 888cbc8..8de715c 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -70,6 +70,9 @@ func (v *paramValidator) ValidateQueryParamsWithPathItem(request *http.Request, } } + // Get operation from request method (lowercase for JSON Pointer) + operation := strings.ToLower(request.Method) + // look through the params for the query key doneLooking: for p := range params { @@ -101,6 +104,15 @@ doneLooking: break } } + + // Render schema once for ReferenceSchema field in errors + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + pType := sch.Type // for each param, check each type @@ -113,34 +125,34 @@ doneLooking: if !params[p].AllowReserved { if rxRxp.MatchString(ef) && params[p].IsExploded() { validationErrors = append(validationErrors, - errors.IncorrectReservedValues(params[p], ef, sch)) + errors.IncorrectReservedValues(params[p], ef, sch, pathValue, operation, renderedSchema)) } } for _, ty := range pType { switch ty { case helpers.String: - validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, ef, params[p])...) + validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, ef, params[p], pathValue, operation, renderedSchema)...) case helpers.Integer: efF, err := strconv.ParseInt(ef, 10, 64) if err != nil { validationErrors = append(validationErrors, - errors.InvalidQueryParamInteger(params[p], ef, sch)) + errors.InvalidQueryParamInteger(params[p], ef, sch, pathValue, operation, renderedSchema)) break } - validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p])...) + validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p], pathValue, operation, renderedSchema)...) case helpers.Number: efF, err := strconv.ParseFloat(ef, 64) if err != nil { validationErrors = append(validationErrors, - errors.InvalidQueryParamNumber(params[p], ef, sch)) + errors.InvalidQueryParamNumber(params[p], ef, sch, pathValue, operation, renderedSchema)) break } - validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p])...) + validationErrors = append(validationErrors, v.validateSimpleParam(sch, ef, efF, params[p], pathValue, operation, renderedSchema)...) case helpers.Boolean: if _, err := strconv.ParseBool(ef); err != nil { validationErrors = append(validationErrors, - errors.IncorrectQueryParamBool(params[p], ef, sch)) + errors.IncorrectQueryParamBool(params[p], ef, sch, pathValue, operation, renderedSchema)) } case helpers.Object: @@ -165,7 +177,7 @@ doneLooking: encodedObj = make(map[string]interface{}) if err := json.Unmarshal([]byte(ef), &encodedParams); err != nil { validationErrors = append(validationErrors, - errors.IncorrectParamEncodingJSON(params[p], ef, sch)) + errors.IncorrectParamEncodingJSON(params[p], ef, sch, pathValue, operation, renderedSchema)) break skipValues } encodedObj[params[p].Name] = encodedParams @@ -195,7 +207,7 @@ doneLooking: // only check if items is a schema, not a boolean if sch.Items != nil && sch.Items.IsA() { validationErrors = append(validationErrors, - ValidateQueryArray(sch, params[p], ef, contentWrapped, v.options)...) + ValidateQueryArray(sch, params[p], ef, contentWrapped, v.options, pathValue, operation, renderedSchema)...) } } } @@ -225,7 +237,23 @@ doneLooking: } // if there is no match, check if the param is required or not. if params[p].Required != nil && *params[p].Required { - validationErrors = append(validationErrors, errors.QueryParameterMissing(params[p])) + // Render schema for missing parameter + var sch *base.Schema + if params[p].Schema != nil { + sch = params[p].Schema.Schema() + } else { + for pair := orderedmap.First(params[p].Content); pair != nil; pair = pair.Next() { + sch = pair.Value().Schema.Schema() + break + } + } + var renderedSchema string + if sch != nil { + rendered, _ := sch.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedSchema = string(schemaBytes) + } + validationErrors = append(validationErrors, errors.QueryParameterMissing(params[p], pathValue, operation, renderedSchema)) } } } @@ -239,7 +267,7 @@ doneLooking: return true, nil } -func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, parsedParam any, parameter *v3.Parameter) (validationErrors []*errors.ValidationError) { +func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, parsedParam any, parameter *v3.Parameter, pathTemplate string, operation string, renderedSchema string) (validationErrors []*errors.ValidationError) { // check if the param is within an enum if sch.Enum != nil { matchFound := false @@ -250,7 +278,7 @@ func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, } } if !matchFound { - return []*errors.ValidationError{errors.IncorrectQueryParamEnum(parameter, rawParam, sch)} + return []*errors.ValidationError{errors.IncorrectQueryParamEnum(parameter, rawParam, sch, pathTemplate, operation, renderedSchema)} } } @@ -263,5 +291,7 @@ func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string, helpers.ParameterValidation, helpers.ParameterValidationQuery, v.options, + pathTemplate, + operation, ) } diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index 137969c..8165995 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -1729,7 +1729,7 @@ paths: assert.Equal(t, "The query parameter (which is an array) 'fishy' is defined as an object, "+ "however it failed to pass a schema validation", errors[0].Reason) assert.Equal(t, "missing properties 'vinegar', 'chips'", errors[0].SchemaValidationErrors[0].Reason) - assert.Equal(t, "/required", errors[0].SchemaValidationErrors[0].Location) + assert.Equal(t, "/required", errors[0].SchemaValidationErrors[0].KeywordLocation) } func TestNewValidator_QueryParamValidTypeObjectPropType_Invalid(t *testing.T) { diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index bdd8034..1f962f0 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -33,6 +33,8 @@ func ValidateSingleParameterSchema( validationType string, subValType string, o *config.ValidationOptions, + pathTemplate string, + operation string, ) (validationErrors []*errors.ValidationError) { // Get the JSON Schema for the parameter definition. jsonSchema, err := buildJsonRender(schema) @@ -50,7 +52,7 @@ func ValidateSingleParameterSchema( scErrs := jsch.Validate(rawObject) var werras *jsonschema.ValidationError if stdError.As(scErrs, &werras) { - validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType) + validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType, pathTemplate, operation) } return validationErrors } @@ -127,23 +129,17 @@ func ValidateParameterSchema( jsch, err := helpers.NewCompiledSchema(name, jsonSchema, validationOptions) if err != nil { // schema compilation failed, return validation error instead of panicking - violation := &errors.SchemaValidationFailure{ - Reason: fmt.Sprintf("failed to compile JSON schema: %s", err.Error()), - Location: "schema compilation", - ReferenceSchema: string(jsonSchema), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: validationType, ValidationSubType: subValType, Message: fmt.Sprintf("%s '%s' failed schema compilation", entity, name), Reason: fmt.Sprintf("%s '%s' schema compilation failed: %s", reasonEntity, name, err.Error()), - SpecLine: 1, - SpecCol: 0, - ParameterName: name, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "check the parameter schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: string(jsonSchema), + SpecLine: 1, + SpecCol: 0, + ParameterName: name, + HowToFix: "check the parameter schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: string(jsonSchema), }) return validationErrors } @@ -189,7 +185,7 @@ func ValidateParameterSchema( } var werras *jsonschema.ValidationError if stdError.As(scErrs, &werras) { - validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType) + validationErrors = formatJsonSchemaValidationError(schema, werras, entity, reasonEntity, name, validationType, subValType, "", "") } // if there are no validationErrors, check that the supplied value is even JSON @@ -213,7 +209,7 @@ func ValidateParameterSchema( return validationErrors } -func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.ValidationError, entity string, reasonEntity string, name string, validationType string, subValType string) (validationErrors []*errors.ValidationError) { +func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.ValidationError, entity string, reasonEntity string, name string, validationType string, subValType string, pathTemplate string, operation string) (validationErrors []*errors.ValidationError) { // flatten the validationErrors schFlatErrs := scErrs.BasicOutput().Errors var schemaValidationErrors []*errors.SchemaValidationFailure @@ -225,18 +221,27 @@ func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.Val continue // ignore this error, it's not useful } + // Construct full OpenAPI path for KeywordLocation if pathTemplate and operation are provided + keywordLocation := er.KeywordLocation + if pathTemplate != "" && operation != "" && validationType == helpers.ParameterValidation { + // er.KeywordLocation is relative to the schema (e.g., "/minLength" or "/enum") + keyword := strings.TrimPrefix(er.KeywordLocation, "/") + keywordLocation = helpers.ConstructParameterJSONPointer(pathTemplate, operation, name, keyword) + } + fail := &errors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.KeywordLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - OriginalError: scErrs, + Reason: errMsg, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: keywordLocation, + OriginalJsonSchemaError: scErrs, } if schema != nil { rendered, err := schema.RenderInline() if err == nil && rendered != nil { - fail.ReferenceSchema = string(rendered) + renderedBytes, _ := json.Marshal(rendered) + fail.ReferenceSchema = string(renderedBytes) } } schemaValidationErrors = append(schemaValidationErrors, fail) diff --git a/parameters/validate_parameter_test.go b/parameters/validate_parameter_test.go index ba31573..9c51395 100644 --- a/parameters/validate_parameter_test.go +++ b/parameters/validate_parameter_test.go @@ -2,6 +2,7 @@ package parameters import ( "net/http" + "strings" "sync" "testing" @@ -17,7 +18,7 @@ import ( func Test_ForceCompilerError(t *testing.T) { // Try to force a panic - result := ValidateSingleParameterSchema(nil, nil, "", "", "", "", "", nil) + result := ValidateSingleParameterSchema(nil, nil, "", "", "", "", "", nil, "", "") // Ideally this would result in an error response, current behavior swallows the error require.Empty(t, result) @@ -264,7 +265,7 @@ func TestUnifiedErrorFormatWithFormatValidation(t *testing.T) { // verify unified error format - SchemaValidationErrors should be populated assert.Len(t, valErrs[0].SchemaValidationErrors, 1) assert.Contains(t, valErrs[0].SchemaValidationErrors[0].Reason, "is not valid email") - assert.Equal(t, "/format", valErrs[0].SchemaValidationErrors[0].Location) + assert.Equal(t, "/paths/test/get/parameters/email_param/schema/format", valErrs[0].SchemaValidationErrors[0].KeywordLocation) assert.NotEmpty(t, valErrs[0].SchemaValidationErrors[0].ReferenceSchema) } @@ -319,8 +320,9 @@ func TestParameterNameFieldPopulation(t *testing.T) { assert.Equal(t, "integer_param", valErrs[0].ParameterName) assert.Equal(t, "Query parameter 'integer_param' is not a valid integer", valErrs[0].Message) - // basic type errors should NOT have SchemaValidationErrors (no JSONSchema validation occurred) - assert.Empty(t, valErrs[0].SchemaValidationErrors) + // basic type errors SHOULD have SchemaValidationErrors because we know the parameter schema + assert.Len(t, valErrs[0].SchemaValidationErrors, 1) + assert.Equal(t, "integer_param", valErrs[0].SchemaValidationErrors[0].FieldName) } func TestHeaderSchemaStringNoJSON(t *testing.T) { @@ -467,16 +469,13 @@ func TestComplexRegexSchemaCompilationError(t *testing.T) { found := false for _, err := range valErrs { if err.ParameterName == "complexParam" && - err.SchemaValidationErrors != nil && - len(err.SchemaValidationErrors) > 0 { - for _, schemaErr := range err.SchemaValidationErrors { - if schemaErr.Location == "schema compilation" && - schemaErr.Reason != "" { - found = true - assert.Contains(t, schemaErr.Reason, "failed to compile JSON schema") - assert.Contains(t, err.HowToFix, "complex regex patterns") - break - } + len(err.SchemaValidationErrors) == 0 { + // Schema compilation errors don't have SchemaValidationFailure objects + if strings.Contains(err.Reason, "failed to compile JSON schema") { + found = true + assert.Contains(t, err.Reason, "failed to compile JSON schema") + assert.Contains(t, err.HowToFix, "complex regex patterns") + break } } } @@ -555,15 +554,14 @@ func TestValidateParameterSchema_SchemaCompilationFailure(t *testing.T) { for _, validationError := range validationErrors { if validationError.ParameterName == "failParam" && validationError.ValidationSubType == helpers.ParameterValidationQuery && - validationError.SchemaValidationErrors != nil { - for _, schemaErr := range validationError.SchemaValidationErrors { - if schemaErr.Location == "schema compilation" { - assert.Contains(t, schemaErr.Reason, "failed to compile JSON schema") - assert.Contains(t, validationError.HowToFix, "complex regex patterns") - assert.Equal(t, "Query parameter 'failParam' failed schema compilation", validationError.Message) - found = true - break - } + len(validationError.SchemaValidationErrors) == 0 { + // Schema compilation errors don't have SchemaValidationFailure objects + if strings.Contains(validationError.Reason, "failed to compile JSON schema") { + assert.Contains(t, validationError.Reason, "failed to compile JSON schema") + assert.Contains(t, validationError.HowToFix, "complex regex patterns") + assert.Equal(t, "Query parameter 'failParam' failed schema compilation", validationError.Message) + found = true + break } } } diff --git a/parameters/validation_functions.go b/parameters/validation_functions.go index f1080a5..f931588 100644 --- a/parameters/validation_functions.go +++ b/parameters/validation_functions.go @@ -4,13 +4,14 @@ package parameters import ( + "encoding/json" "fmt" "slices" "strconv" "strings" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/pb33f/libopenapi/datamodel/high/v3" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" @@ -19,11 +20,18 @@ import ( // ValidateCookieArray will validate a cookie parameter that is an array func ValidateCookieArray( - sch *base.Schema, param *v3.Parameter, value string, + sch *base.Schema, param *v3.Parameter, value string, pathTemplate string, operation string, renderedSchema string, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() + var renderedItemsSchema string + if itemsSchema != nil { + rendered, _ := itemsSchema.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedItemsSchema = string(schemaBytes) + } + // header arrays can only be encoded as CSV items := helpers.ExplodeQueryValue(value, helpers.DefaultDelimited) @@ -35,18 +43,18 @@ func ValidateCookieArray( case helpers.Integer, helpers.Number: if _, err := strconv.ParseFloat(item, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectCookieParamArrayNumber(param, item, sch, itemsSchema)) + errors.IncorrectCookieParamArrayNumber(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.Boolean: if _, err := strconv.ParseBool(item); err != nil { validationErrors = append(validationErrors, - errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // check for edge-cases "0" and "1" which can also be parsed into valid booleans if item == "0" || item == "1" { validationErrors = append(validationErrors, - errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectCookieParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.String: // do nothing for now. @@ -59,11 +67,18 @@ func ValidateCookieArray( // ValidateHeaderArray will validate a header parameter that is an array func ValidateHeaderArray( - sch *base.Schema, param *v3.Parameter, value string, + sch *base.Schema, param *v3.Parameter, value string, pathTemplate string, operation string, renderedSchema string, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() + var renderedItemsSchema string + if itemsSchema != nil { + rendered, _ := itemsSchema.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedItemsSchema = string(schemaBytes) + } + // header arrays can only be encoded as CSV items := helpers.ExplodeQueryValue(value, helpers.DefaultDelimited) @@ -75,18 +90,18 @@ func ValidateHeaderArray( case helpers.Integer, helpers.Number: if _, err := strconv.ParseFloat(item, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamArrayNumber(param, item, sch, itemsSchema)) + errors.IncorrectHeaderParamArrayNumber(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.Boolean: if _, err := strconv.ParseBool(item); err != nil { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // check for edge-cases "0" and "1" which can also be parsed into valid booleans if item == "0" || item == "1" { validationErrors = append(validationErrors, - errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectHeaderParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.String: // do nothing for now. @@ -99,11 +114,18 @@ func ValidateHeaderArray( // ValidateQueryArray will validate a query parameter that is an array func ValidateQueryArray( - sch *base.Schema, param *v3.Parameter, ef string, contentWrapped bool, validationOptions *config.ValidationOptions, + sch *base.Schema, param *v3.Parameter, ef string, contentWrapped bool, validationOptions *config.ValidationOptions, pathTemplate string, operation string, renderedSchema string, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() + var renderedItemsSchema string + if itemsSchema != nil { + rendered, _ := itemsSchema.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + renderedItemsSchema = string(schemaBytes) + } + // check for an exploded bit on the schema. // if it's exploded, then we need to check each item in the array // if it's not exploded, then we need to check the whole array as a string @@ -141,7 +163,7 @@ func ValidateQueryArray( } if !matchFound { validationErrors = append(validationErrors, - errors.IncorrectQueryParamEnumArray(param, item, sch)) + errors.IncorrectQueryParamEnumArray(param, item, sch, pathTemplate, operation, renderedItemsSchema)) } } } @@ -165,7 +187,7 @@ func ValidateQueryArray( case helpers.Integer: if _, err := strconv.ParseInt(item, 10, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectQueryParamArrayInteger(param, item, sch, itemsSchema)) + errors.IncorrectQueryParamArrayInteger(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // will it blend? @@ -173,7 +195,7 @@ func ValidateQueryArray( case helpers.Number: if _, err := strconv.ParseFloat(item, 64); err != nil { validationErrors = append(validationErrors, - errors.IncorrectQueryParamArrayNumber(param, item, sch, itemsSchema)) + errors.IncorrectQueryParamArrayNumber(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) break } // will it blend? @@ -182,7 +204,7 @@ func ValidateQueryArray( case helpers.Boolean: if _, err := strconv.ParseBool(item); err != nil { validationErrors = append(validationErrors, - errors.IncorrectQueryParamArrayBoolean(param, item, sch, itemsSchema)) + errors.IncorrectQueryParamArrayBoolean(param, item, sch, itemsSchema, pathTemplate, operation, renderedItemsSchema)) } case helpers.Object: validationErrors = append(validationErrors, @@ -207,14 +229,14 @@ func ValidateQueryArray( if sch.MaxItems != nil { if len(items) > int(*sch.MaxItems) { validationErrors = append(validationErrors, - errors.IncorrectParamArrayMaxNumItems(param, sch, *sch.MaxItems, int64(len(items)))) + errors.IncorrectParamArrayMaxNumItems(param, sch, *sch.MaxItems, int64(len(items)), pathTemplate, operation, renderedSchema)) } } if sch.MinItems != nil { if len(items) < int(*sch.MinItems) { validationErrors = append(validationErrors, - errors.IncorrectParamArrayMinNumItems(param, sch, *sch.MinItems, int64(len(items)))) + errors.IncorrectParamArrayMinNumItems(param, sch, *sch.MinItems, int64(len(items)), pathTemplate, operation, renderedSchema)) } } @@ -222,7 +244,7 @@ func ValidateQueryArray( if sch.UniqueItems != nil { if *sch.UniqueItems && !uniqueItems { validationErrors = append(validationErrors, - errors.IncorrectParamArrayUniqueItems(param, sch, strings.Join(duplicates, ", "))) + errors.IncorrectParamArrayUniqueItems(param, sch, strings.Join(duplicates, ", "), pathTemplate, operation, renderedSchema)) } } return validationErrors diff --git a/requests/validate_body.go b/requests/validate_body.go index 6e9c13a..748bea7 100644 --- a/requests/validate_body.go +++ b/requests/validate_body.go @@ -99,7 +99,9 @@ func (v *requestBodyValidator) extractContentType(contentType string, operation return mediaType, true } ctMediaRange := strings.SplitN(ct, "/", 2) - for s, mediaTypeValue := range operation.RequestBody.Content.FromOldest() { + for contentPair := operation.RequestBody.Content.First(); contentPair != nil; contentPair = contentPair.Next() { + s := contentPair.Key() + mediaTypeValue := contentPair.Value() opMediaRange := strings.SplitN(s, "/", 2) if (opMediaRange[0] == "*" || opMediaRange[0] == ctMediaRange[0]) && (opMediaRange[1] == "*" || opMediaRange[1] == ctMediaRange[1]) { diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go index a74b6ae..d32489b 100644 --- a/requests/validate_body_test.go +++ b/requests/validate_body_test.go @@ -1309,8 +1309,8 @@ components: assert.False(t, valid) assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason) + assert.Nil(t, errors[0].SchemaValidationErrors) + assert.Contains(t, errors[0].Reason, "cannot be decoded") } func TestValidateBody_SchemaNoType_Issue75(t *testing.T) { diff --git a/requests/validate_request.go b/requests/validate_request.go index 03cc20e..460a1ea 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -88,22 +88,16 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V input.Version, ) if err != nil { - violation := &errors.SchemaValidationFailure{ - Reason: fmt.Sprintf("failed to compile JSON schema: %s", err.Error()), - Location: "schema compilation", - ReferenceSchema: referenceSchema, - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body for '%s' failed schema compilation", input.Request.Method, input.Request.URL.Path), - Reason: fmt.Sprintf("The request schema failed to compile: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "check the request schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: referenceSchema, + Reason: fmt.Sprintf("The request schema failed to compile: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: "check the request schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: input.Schema, }) return false, validationErrors } @@ -139,23 +133,16 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V err := json.Unmarshal(requestBody, &decodedObj) if err != nil { // cannot decode the request body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: err.Error(), - Location: "unavailable", - ReferenceSchema: referenceSchema, - ReferenceObject: string(requestBody), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body for '%s' failed to validate schema", request.Method, request.URL.Path), - Reason: fmt.Sprintf("The request body cannot be decoded: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Reason: fmt.Sprintf("The request body cannot be decoded: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: errors.HowToFixInvalidSchema, + Context: schema, }) return false, validationErrors } @@ -172,22 +159,16 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V } // cannot decode the request body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: "request body is empty, but there is a schema defined", - ReferenceSchema: referenceSchema, - ReferenceObject: string(requestBody), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.RequestBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s request body is empty for '%s'", request.Method, request.URL.Path), - Reason: "The request body is empty but there is a schema defined", - SpecLine: line, - SpecCol: col, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Reason: "The request body is empty but there is a schema defined", + SpecLine: line, + SpecCol: col, + HowToFix: errors.HowToFixInvalidSchema, + Context: schema, }) return false, validationErrors } @@ -237,14 +218,14 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) violation := &errors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.KeywordLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - ReferenceSchema: referenceSchema, - ReferenceObject: referenceObject, - OriginalError: jk, + Reason: errMsg, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + ReferenceSchema: referenceSchema, + ReferenceObject: referenceObject, + OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { @@ -285,7 +266,7 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V SpecCol: col, SchemaValidationErrors: schemaValidationErrors, HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Context: schema, }) } if len(validationErrors) > 0 { diff --git a/responses/validate_body.go b/responses/validate_body.go index fc760db..87bb53a 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -109,7 +109,7 @@ func (v *responseBodyValidator) ValidateResponseBodyWithPathItem(request *http.R if foundResponse != nil { // check for headers in the response if foundResponse.Headers != nil { - if ok, herrs := ValidateResponseHeaders(request, response, foundResponse.Headers); !ok { + if ok, herrs := ValidateResponseHeaders(request, response, foundResponse.Headers, pathFound, codeStr); !ok { validationErrors = append(validationErrors, herrs...) } } diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index 5096225..6097348 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -1262,7 +1262,8 @@ paths: assert.False(t, valid) assert.Len(t, errors, 1) assert.Equal(t, "POST response body for '/burgers/createBurger' failed to validate schema", errors[0].Message) - assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason) + assert.Nil(t, errors[0].SchemaValidationErrors) + assert.Contains(t, errors[0].Reason, "cannot be decoded") } func TestValidateBody_NoContentType_Valid(t *testing.T) { @@ -1487,16 +1488,13 @@ paths: found := false for _, err := range validationErrors { if err.ValidationSubType == helpers.Schema && - err.SchemaValidationErrors != nil && - len(err.SchemaValidationErrors) > 0 { - for _, schemaErr := range err.SchemaValidationErrors { - if schemaErr.Location == "schema compilation" && - schemaErr.Reason != "" { - found = true - assert.Contains(t, schemaErr.Reason, "failed to compile JSON schema") - assert.Contains(t, err.HowToFix, "complex regex patterns") - break - } + len(err.SchemaValidationErrors) == 0 { + // Schema compilation errors don't have SchemaValidationFailure objects + if strings.Contains(err.Reason, "failed to compile JSON schema") { + found = true + assert.Contains(t, err.Reason, "failed to compile JSON schema") + assert.Contains(t, err.HowToFix, "complex regex patterns") + break } } } diff --git a/responses/validate_headers.go b/responses/validate_headers.go index 5eb22df..f1a7cdc 100644 --- a/responses/validate_headers.go +++ b/responses/validate_headers.go @@ -24,6 +24,8 @@ func ValidateResponseHeaders( request *http.Request, response *http.Response, headers *orderedmap.Map[string, *v3.Header], + pathTemplate string, + statusCode string, opts ...config.Option, ) (bool, []*errors.ValidationError) { options := config.NewValidationOptions(opts...) @@ -39,7 +41,9 @@ func ValidateResponseHeaders( // iterate through the response headers for name, v := range response.Header { // check if the model is in the spec - for k, header := range headers.FromOldest() { + for pair := headers.First(); pair != nil; pair = pair.Next() { + k := pair.Key() + header := pair.Value() if strings.EqualFold(k, name) { locatedHeaders[strings.ToLower(name)] = headerPair{ name: k, @@ -51,9 +55,13 @@ func ValidateResponseHeaders( } // determine if any required headers are missing from the response - for name, header := range headers.FromOldest() { + for pair := headers.First(); pair != nil; pair = pair.Next() { + name := pair.Key() + header := pair.Value() if header.Required { if _, ok := locatedHeaders[strings.ToLower(name)]; !ok { + keywordLocation := helpers.ConstructResponseHeaderJSONPointer(pathTemplate, request.Method, statusCode, name, "required") + validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.ParameterValidationHeader, @@ -64,6 +72,12 @@ func ValidateResponseHeaders( HowToFix: errors.HowToFixMissingHeader, RequestPath: request.URL.Path, RequestMethod: request.Method, + SchemaValidationErrors: []*errors.SchemaValidationFailure{{ + Reason: fmt.Sprintf("Required header '%s' is missing", name), + FieldName: name, + InstancePath: []string{name}, + KeywordLocation: keywordLocation, + }}, }) } } diff --git a/responses/validate_headers_test.go b/responses/validate_headers_test.go index feb5600..ddcf521 100644 --- a/responses/validate_headers_test.go +++ b/responses/validate_headers_test.go @@ -54,7 +54,7 @@ paths: headers := m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate! - valid, errors := ValidateResponseHeaders(request, response, headers) + valid, errors := ValidateResponseHeaders(request, response, headers, "/health", "200") assert.False(t, valid) assert.Len(t, errors, 1) @@ -76,7 +76,7 @@ paths: headers = m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate! - valid, errors = ValidateResponseHeaders(request, response, headers) + valid, errors = ValidateResponseHeaders(request, response, headers, "/health", "200") assert.False(t, valid) assert.Len(t, errors, 1) @@ -125,7 +125,7 @@ paths: headers := m.Model.Paths.PathItems.GetOrZero("/health").Get.Responses.Codes.GetOrZero("200").Headers // validate! - valid, errors := ValidateResponseHeaders(request, response, headers) + valid, errors := ValidateResponseHeaders(request, response, headers, "/health", "200") assert.True(t, valid) assert.Len(t, errors, 0) diff --git a/responses/validate_response.go b/responses/validate_response.go index ab6c4af..f8f104b 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -91,11 +91,6 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors input.Version, ) if err != nil { - violation := &errors.SchemaValidationFailure{ - Reason: fmt.Sprintf("failed to compile JSON schema: %s", err.Error()), - Location: "schema compilation", - ReferenceSchema: referenceSchema, - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, @@ -103,11 +98,10 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors input.Response.StatusCode, input.Request.URL.Path), Reason: fmt.Sprintf("The response schema for status code '%d' failed to compile: %s", input.Response.StatusCode, err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "check the response schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: referenceSchema, + SpecLine: 1, + SpecCol: 0, + HowToFix: "check the response schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: input.Schema, }) return false, validationErrors } @@ -130,22 +124,16 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors if response == nil || response.Body == http.NoBody { // cannot decode the response body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: "response is empty", - Location: "unavailable", - ReferenceSchema: referenceSchema, - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: "response", ValidationSubType: "object", Message: fmt.Sprintf("%s response object is missing for '%s'", request.Method, request.URL.Path), - Reason: "The response object is completely missing", - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "ensure response object has been set", - Context: referenceSchema, // attach the rendered schema to the error + Reason: "The response object is completely missing", + SpecLine: 1, + SpecCol: 0, + HowToFix: "ensure response object has been set", + Context: schema, }) return false, validationErrors } @@ -153,23 +141,16 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors responseBody, ioErr := io.ReadAll(response.Body) if ioErr != nil { // cannot decode the response body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: ioErr.Error(), - Location: "unavailable", - ReferenceSchema: referenceSchema, - ReferenceObject: string(responseBody), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s response body for '%s' cannot be read, it's empty or malformed", request.Method, request.URL.Path), - Reason: fmt.Sprintf("The response body cannot be decoded: %s", ioErr.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: "ensure body is not empty", - Context: referenceSchema, // attach the rendered schema to the error + Reason: fmt.Sprintf("The response body cannot be decoded: %s", ioErr.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: "ensure body is not empty", + Context: schema, }) return false, validationErrors } @@ -184,23 +165,16 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors err := json.Unmarshal(responseBody, &decodedObj) if err != nil { // cannot decode the response body, so it's not valid - violation := &errors.SchemaValidationFailure{ - Reason: err.Error(), - Location: "unavailable", - ReferenceSchema: referenceSchema, - ReferenceObject: string(responseBody), - } validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.Schema, Message: fmt.Sprintf("%s response body for '%s' failed to validate schema", request.Method, request.URL.Path), - Reason: fmt.Sprintf("The response body cannot be decoded: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*errors.SchemaValidationFailure{violation}, - HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Reason: fmt.Sprintf("The response body cannot be decoded: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: errors.HowToFixInvalidSchema, + Context: schema, }) return false, validationErrors } @@ -252,14 +226,14 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors } violation := &errors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.KeywordLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - ReferenceSchema: referenceSchema, - ReferenceObject: referenceObject, - OriginalError: jk, + Reason: errMsg, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + ReferenceSchema: referenceSchema, + ReferenceObject: referenceObject, + OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { @@ -300,7 +274,7 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors SpecCol: col, SchemaValidationErrors: schemaValidationErrors, HowToFix: errors.HowToFixInvalidSchema, - Context: referenceSchema, // attach the rendered schema to the error + Context: schema, }) } if len(validationErrors) > 0 { diff --git a/schema_validation/property_locator.go b/schema_validation/property_locator.go index 14bfe56..dce4377 100644 --- a/schema_validation/property_locator.go +++ b/schema_validation/property_locator.go @@ -233,7 +233,6 @@ func applyPropertyNameFallback( &violation.Reason, &violation.FieldName, &violation.FieldPath, - &violation.Location, &violation.InstancePath, ) } @@ -254,7 +253,6 @@ func enrichSchemaValidationFailure( reason *string, fieldName *string, fieldPath *string, - location *string, instancePath *[]string, ) bool { if failure == nil { @@ -298,11 +296,9 @@ func enrichSchemaValidationFailure( // construct JSONPath from parent location segments if len(failure.ParentLocation) > 0 { *fieldPath = helpers.ExtractJSONPathFromStringLocation("/" + strings.Join(failure.ParentLocation, "/") + "/" + failure.PropertyName) - *location = "/" + strings.Join(failure.ParentLocation, "/") *instancePath = failure.ParentLocation } else { *fieldPath = helpers.ExtractJSONPathFromStringLocation("/" + failure.PropertyName) - *location = "/" *instancePath = []string{} } diff --git a/schema_validation/property_locator_test.go b/schema_validation/property_locator_test.go index f28546c..56b74ad 100644 --- a/schema_validation/property_locator_test.go +++ b/schema_validation/property_locator_test.go @@ -114,13 +114,13 @@ components: if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { sve := errors[0].SchemaValidationErrors[0] - if sve.OriginalError != nil { + if sve.OriginalJsonSchemaError != nil { // Test extractPatternFromCauses directly with the real error - pattern := extractPatternFromCauses(sve.OriginalError) + pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) assert.NotEmpty(t, pattern, "Should extract pattern from ValidationError") // Also test the info extraction - info := checkErrorForPropertyInfo(sve.OriginalError) + info := checkErrorForPropertyInfo(sve.OriginalJsonSchemaError) assert.NotNil(t, info) assert.Equal(t, "$with-pattern", info.PropertyName) assert.NotEmpty(t, info.Pattern, "Pattern should be extracted from causes") @@ -146,9 +146,9 @@ info: if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { for _, sve := range errors[0].SchemaValidationErrors { - if sve.OriginalError != nil { + if sve.OriginalJsonSchemaError != nil { // Call extractPatternFromCauses - may return empty string for errors without pattern - pattern := extractPatternFromCauses(sve.OriginalError) + pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) // Pattern might be empty for non-property-name errors (covering line 108) _ = pattern } @@ -241,9 +241,9 @@ components: if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { sve := errors[0].SchemaValidationErrors[0] - if sve.OriginalError != nil { + if sve.OriginalJsonSchemaError != nil { // Test pattern extraction - pattern := extractPatternFromCauses(sve.OriginalError) + pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) assert.NotEmpty(t, pattern, "Should extract pattern from error") assert.Equal(t, "^[a-zA-Z0-9._-]+$", pattern) } @@ -293,15 +293,15 @@ components: _, errors := ValidateOpenAPIDocument(doc) if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { sve := errors[0].SchemaValidationErrors[0] - if sve.OriginalError != nil { + if sve.OriginalJsonSchemaError != nil { // Test extraction from root error - info := extractPropertyNameFromError(sve.OriginalError) + info := extractPropertyNameFromError(sve.OriginalJsonSchemaError) assert.NotNil(t, info, "Should extract property name from root error") assert.Equal(t, "$direct-test", info.PropertyName) assert.NotEmpty(t, info.EnhancedReason) // Test extractPatternFromCauses on the root error - pattern := extractPatternFromCauses(sve.OriginalError) + pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) assert.NotEmpty(t, pattern, "Should extract pattern from error message") assert.Equal(t, "^[a-zA-Z0-9._-]+$", pattern) } @@ -343,8 +343,8 @@ paths: } else if len(errors) > 0 && len(errors[0].SchemaValidationErrors) > 0 { // If there are errors, test extraction (might not find property name info) sve := errors[0].SchemaValidationErrors[0] - if sve.OriginalError != nil { - info := extractPropertyNameFromError(sve.OriginalError) + if sve.OriginalJsonSchemaError != nil { + info := extractPropertyNameFromError(sve.OriginalJsonSchemaError) // Info might be nil for non-property-name errors _ = info } @@ -637,7 +637,7 @@ components: } var line, column int - var reason, fieldName, fieldPath, location string + var reason, fieldName, fieldPath string var instancePath []string enriched := enrichSchemaValidationFailure( @@ -648,7 +648,6 @@ components: &reason, &fieldName, &fieldPath, - &location, &instancePath, ) @@ -658,7 +657,6 @@ components: assert.Equal(t, "invalid propertyName '$defs-atmVolatility_type': does not match pattern '^[a-zA-Z0-9._-]+$'", reason) assert.Equal(t, "$defs-atmVolatility_type", fieldName) assert.Contains(t, fieldPath, "$defs-atmVolatility_type") - assert.Equal(t, "/components/schemas", location) assert.Equal(t, []string{"components", "schemas"}, instancePath) } @@ -672,7 +670,7 @@ test: value assert.NoError(t, err) var line, column int - var reason, fieldName, fieldPath, location string + var reason, fieldName, fieldPath string var instancePath []string enriched := enrichSchemaValidationFailure( @@ -683,7 +681,6 @@ test: value &reason, &fieldName, &fieldPath, - &location, &instancePath, ) @@ -711,7 +708,7 @@ components: } var line, column int - var reason, fieldName, fieldPath, location string + var reason, fieldName, fieldPath string var instancePath []string enriched := enrichSchemaValidationFailure( @@ -722,7 +719,6 @@ components: &reason, &fieldName, &fieldPath, - &location, &instancePath, ) @@ -748,7 +744,7 @@ $defs-test: } var line, column int - var reason, fieldName, fieldPath, location string + var reason, fieldName, fieldPath string var instancePath []string enriched := enrichSchemaValidationFailure( @@ -759,7 +755,6 @@ $defs-test: &reason, &fieldName, &fieldPath, - &location, &instancePath, ) @@ -767,7 +762,6 @@ $defs-test: assert.Greater(t, line, 0) assert.Equal(t, "test reason", reason) assert.Equal(t, "$defs-test", fieldName) - assert.Equal(t, "/", location) assert.Equal(t, []string{}, instancePath) } @@ -821,9 +815,9 @@ components: assert.Contains(t, sve.FieldPath, "$defs-atmVolatility_type", "FieldPath should include property name") // Original validation check that extractPropertyNameFromError works - assert.NotNil(t, sve.OriginalError, "OriginalError should be populated") + assert.NotNil(t, sve.OriginalJsonSchemaError, "OriginalError should be populated") - info := extractPropertyNameFromError(sve.OriginalError) + info := extractPropertyNameFromError(sve.OriginalJsonSchemaError) // This should successfully extract the property name assert.NotNil(t, info, "Should extract property name info from error") assert.Equal(t, "$defs-atmVolatility_type", info.PropertyName) @@ -833,15 +827,15 @@ components: // Explicitly test checkErrorForPropertyInfo with the root error and causes // to ensure coverage of different code paths - rootInfo := checkErrorForPropertyInfo(sve.OriginalError) - if rootInfo == nil && len(sve.OriginalError.Causes) > 0 { + rootInfo := checkErrorForPropertyInfo(sve.OriginalJsonSchemaError) + if rootInfo == nil && len(sve.OriginalJsonSchemaError.Causes) > 0 { // Check first cause - causeInfo := checkErrorForPropertyInfo(sve.OriginalError.Causes[0]) + causeInfo := checkErrorForPropertyInfo(sve.OriginalJsonSchemaError.Causes[0]) _ = causeInfo } // Explicitly test extractPatternFromCauses to exercise recursive pattern extraction - pattern := extractPatternFromCauses(sve.OriginalError) + pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) if pattern != "" { assert.Equal(t, "^[a-zA-Z0-9._-]+$", pattern) } @@ -903,8 +897,8 @@ components: noPatternCount := 0 for _, sve := range errors[0].SchemaValidationErrors { - if sve.OriginalError != nil { - info := extractPropertyNameFromError(sve.OriginalError) + if sve.OriginalJsonSchemaError != nil { + info := extractPropertyNameFromError(sve.OriginalJsonSchemaError) if info != nil { foundCount++ assert.NotEmpty(t, info.PropertyName) @@ -921,7 +915,7 @@ components: } // Test extractPatternFromCauses coverage - pattern := extractPatternFromCauses(sve.OriginalError) + pattern := extractPatternFromCauses(sve.OriginalJsonSchemaError) // Pattern may or may not be found depending on error structure _ = pattern } @@ -999,7 +993,7 @@ components: "FieldPath should include the property name") // Verify OriginalError is preserved for debugging - assert.NotNil(t, sve.OriginalError, "OriginalError should be populated for debugging") + assert.NotNil(t, sve.OriginalJsonSchemaError, "OriginalError should be populated for debugging") } // TestValidateOpenAPIDocument_Issue726_MultipleInvalidPropertyNames tests that the fix diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index 7eb9b64..03a299f 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -40,21 +40,15 @@ func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bo jsch, err := helpers.NewCompiledSchema("schema", []byte(loadedSchema), options) if err != nil { // schema compilation failed, return validation error instead of panicking - violation := &liberrors.SchemaValidationFailure{ - Reason: fmt.Sprintf("failed to compile OpenAPI schema: %s", err.Error()), - Location: "schema compilation", - ReferenceSchema: loadedSchema, - } validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: "schema", - ValidationSubType: "compilation", - Message: "OpenAPI document schema compilation failed", - Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: loadedSchema, + ValidationType: "schema", + ValidationSubType: "compilation", + Message: "OpenAPI document schema compilation failed", + Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: loadedSchema, }) return false, validationErrors } @@ -87,14 +81,12 @@ func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bo // locate the violated property in the schema located := LocateSchemaPropertyNodeByJSONPath(info.RootNode.Content[0], er.InstanceLocation) violation := &liberrors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.InstanceLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - DeepLocation: er.KeywordLocation, - AbsoluteLocation: er.AbsoluteKeywordLocation, - OriginalError: jk, + Reason: errMsg, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error diff --git a/schema_validation/validate_document_test.go b/schema_validation/validate_document_test.go index eff930f..90a4797 100644 --- a/schema_validation/validate_document_test.go +++ b/schema_validation/validate_document_test.go @@ -62,21 +62,16 @@ func validateOpenAPIDocumentWithMalformedSchema(loadedSchema string, decodedDocu _, err := helpers.NewCompiledSchema("schema", []byte(loadedSchema), options) if err != nil { // schema compilation failed, return validation error instead of panicking - violation := &liberrors.SchemaValidationFailure{ - Reason: fmt.Sprintf("failed to compile OpenAPI schema: %s", err.Error()), - Location: "schema compilation", - ReferenceSchema: loadedSchema, - } + // NO SchemaValidationFailure for pre-validation errors like compilation failures validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: "schema", - ValidationSubType: "compilation", - Message: "OpenAPI document schema compilation failed", - Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: loadedSchema, + ValidationType: "schema", + ValidationSubType: "compilation", + Message: "OpenAPI document schema compilation failed", + Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: loadedSchema, }) return false, validationErrors } @@ -115,12 +110,8 @@ func TestValidateDocument_SchemaCompilationFailure(t *testing.T) { assert.Equal(t, 1, validationError.SpecLine) assert.Equal(t, 0, validationError.SpecCol) - // Verify schema validation errors - assert.NotEmpty(t, validationError.SchemaValidationErrors) - schemaErr := validationError.SchemaValidationErrors[0] - assert.Equal(t, "schema compilation", schemaErr.Location) - assert.Contains(t, schemaErr.Reason, "failed to compile OpenAPI schema") - assert.Equal(t, malformedSchema, schemaErr.ReferenceSchema) + // Schema compilation errors don't have SchemaValidationFailure objects + assert.Empty(t, validationError.SchemaValidationErrors) } // TestValidateDocument_CompilationFailure tests the actual ValidateOpenAPIDocument function @@ -132,8 +123,8 @@ func TestValidateDocument_CompilationFailure(t *testing.T) { valid, errors := ValidateOpenAPIDocument(doc) assert.False(t, valid) assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 1) - assert.Contains(t, errors[0].SchemaValidationErrors[0].Reason, "failed to compile OpenAPI schema") + assert.Contains(t, errors[0].Reason, "The OpenAPI schema failed to compile") + assert.Nil(t, errors[0].SchemaValidationErrors, "Compilation errors should not have SchemaValidationErrors") } func TestValidateSchema_ValidateLicenseIdentifier(t *testing.T) { diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index 9f2b139..8db3176 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -133,22 +133,15 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload renderedSchema, e = schema.RenderInline() if e != nil { // schema cannot be rendered, so it's not valid! - violation := &liberrors.SchemaValidationFailure{ - Reason: e.Error(), - Location: "unavailable", - ReferenceSchema: string(renderedSchema), - ReferenceObject: string(payload), - } validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: helpers.RequestBodyValidation, - ValidationSubType: helpers.Schema, - Message: "schema does not pass validation", - Reason: fmt.Sprintf("The schema cannot be decoded: %s", e.Error()), - SpecLine: schema.GoLow().GetRootNode().Line, - SpecCol: schema.GoLow().GetRootNode().Column, - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: liberrors.HowToFixInvalidSchema, - Context: string(renderedSchema), + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: "schema does not pass validation", + Reason: fmt.Sprintf("The schema cannot be decoded: %s", e.Error()), + SpecLine: schema.GoLow().GetRootNode().Line, + SpecCol: schema.GoLow().GetRootNode().Column, + HowToFix: liberrors.HowToFixInvalidSchema, + Context: string(renderedSchema), }) s.lock.Unlock() return false, validationErrors @@ -163,12 +156,6 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload if err != nil { // cannot decode the request body, so it's not valid - violation := &liberrors.SchemaValidationFailure{ - Reason: err.Error(), - Location: "unavailable", - ReferenceSchema: string(renderedSchema), - ReferenceObject: string(payload), - } line := 1 col := 0 if schema.GoLow().Type.KeyNode != nil { @@ -176,15 +163,14 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload col = schema.GoLow().Type.KeyNode.Column } validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: helpers.RequestBodyValidation, - ValidationSubType: helpers.Schema, - Message: "schema does not pass validation", - Reason: fmt.Sprintf("The schema cannot be decoded: %s", err.Error()), - SpecLine: line, - SpecCol: col, - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: liberrors.HowToFixInvalidSchema, - Context: string(renderedSchema), + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: "schema does not pass validation", + Reason: fmt.Sprintf("The schema cannot be decoded: %s", err.Error()), + SpecLine: line, + SpecCol: col, + HowToFix: liberrors.HowToFixInvalidSchema, + Context: string(renderedSchema), }) return false, validationErrors } @@ -199,12 +185,6 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload var schemaValidationErrors []*liberrors.SchemaValidationFailure if err != nil { - violation := &liberrors.SchemaValidationFailure{ - Reason: err.Error(), - Location: "schema compilation", - ReferenceSchema: string(renderedSchema), - ReferenceObject: string(payload), - } line := 1 col := 0 if schema.GoLow().Type.KeyNode != nil { @@ -212,15 +192,14 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload col = schema.GoLow().Type.KeyNode.Column } validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: helpers.Schema, - ValidationSubType: helpers.Schema, - Message: "schema compilation failed", - Reason: fmt.Sprintf("Schema compilation failed: %s", err.Error()), - SpecLine: line, - SpecCol: col, - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: liberrors.HowToFixInvalidSchema, - Context: string(renderedSchema), + ValidationType: helpers.Schema, + ValidationSubType: helpers.Schema, + Message: "schema compilation failed", + Reason: fmt.Sprintf("Schema compilation failed: %s", err.Error()), + SpecLine: line, + SpecCol: col, + HowToFix: liberrors.HowToFixInvalidSchema, + Context: string(renderedSchema), }) return false, validationErrors } @@ -303,16 +282,14 @@ func extractBasicErrors(schFlatErrs []jsonschema.OutputUnit, } violation := &liberrors.SchemaValidationFailure{ - Reason: errMsg, - Location: er.InstanceLocation, - FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), - FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), - InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), - DeepLocation: er.KeywordLocation, - AbsoluteLocation: er.AbsoluteKeywordLocation, - ReferenceSchema: string(renderedSchema), - ReferenceObject: referenceObject, - OriginalError: jk, + Reason: errMsg, + FieldName: helpers.ExtractFieldNameFromStringLocation(er.InstanceLocation), + FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), + InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), + KeywordLocation: er.KeywordLocation, + ReferenceSchema: string(renderedSchema), + ReferenceObject: referenceObject, + OriginalJsonSchemaError: jk, } // if we have a location within the schema, add it to the error if located != nil { diff --git a/schema_validation/validate_schema_openapi_test.go b/schema_validation/validate_schema_openapi_test.go index fb3f14f..a193248 100644 --- a/schema_validation/validate_schema_openapi_test.go +++ b/schema_validation/validate_schema_openapi_test.go @@ -346,13 +346,13 @@ components: foundCompilationError := false for _, err := range errors { - if err.SchemaValidationErrors != nil { - for _, schErr := range err.SchemaValidationErrors { - if schErr.Location == "unavailable" && schErr.Reason == "schema render failure, circular reference: `#/components/schemas/b`" { - foundCompilationError = true - } - } + if err.Message == "schema does not pass validation" && + err.Reason != "" && + (err.Reason == "The schema cannot be decoded: schema render failure, circular reference: `#/components/schemas/b`" || + err.Reason == "The schema cannot be decoded: schema render failure, circular reference: `#/components/schemas/Node`") { + foundCompilationError = true } + assert.Nil(t, err.SchemaValidationErrors, "Rendering errors should not have SchemaValidationErrors") } assert.True(t, foundCompilationError, "Should have schema compilation error for circular references") }) diff --git a/schema_validation/validate_schema_test.go b/schema_validation/validate_schema_test.go index 7c7229f..7bf6303 100644 --- a/schema_validation/validate_schema_test.go +++ b/schema_validation/validate_schema_test.go @@ -524,8 +524,52 @@ paths: assert.False(t, valid) assert.Len(t, errors, 1) - assert.Equal(t, "schema does not pass validation", errors[0].Message) - assert.Equal(t, "invalid character '}' looking for beginning of object key string", errors[0].SchemaValidationErrors[0].Reason) + assert.Contains(t, errors[0].Reason, "invalid character '}' looking for beginning of object key string") + assert.Nil(t, errors[0].SchemaValidationErrors) +} + +func TestValidateSchema_CompilationFailure(t *testing.T) { + // Test that schema compilation failure doesn't create SchemaValidationErrors + // This uses an extremely complex regex that might fail compilation in some regex engines + spec := `openapi: 3.1.0 +paths: + /test: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + password: + type: string + pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}(?:(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,})*'` + + doc, _ := libopenapi.NewDocument([]byte(spec)) + m, _ := doc.BuildV3Model() + + sch := m.Model.Paths.PathItems.GetOrZero("/test").Post.RequestBody.Content.GetOrZero("application/json").Schema + + // create a schema validator with strict regex engine that might fail on complex patterns + v := NewSchemaValidator() + + // Try to validate - if compilation fails, we should get an error without SchemaValidationErrors + validData := `{"password": "ValidPass123!"}` + valid, errors := v.ValidateSchemaString(sch.Schema(), validData) + + // This test is environment-dependent - compilation might succeed or fail depending on regex engine + // If it fails, we want to ensure SchemaValidationErrors is nil + if !valid && len(errors) > 0 { + for _, err := range errors { + if err.Message == "schema compilation failed" { + // Compilation failure should NOT have SchemaValidationErrors + assert.Nil(t, err.SchemaValidationErrors, "Schema compilation errors should not have SchemaValidationErrors") + t.Logf("Schema compilation failed as expected: %s", err.Reason) + } + } + } else { + t.Skip("Regex engine handled the complex pattern - skipping compilation failure test") + } } //// https://github.com/pb33f/libopenapi-validator/issues/26 diff --git a/schema_validation/validate_xml.go b/schema_validation/validate_xml.go index ad30bd8..a2c82a7 100644 --- a/schema_validation/validate_xml.go +++ b/schema_validation/validate_xml.go @@ -28,19 +28,13 @@ func (x *xmlValidator) validateXMLWithVersion(schema *base.Schema, xmlString str // parse xml and transform to json structure matching schema transformedJSON, err := transformXMLToSchemaJSON(xmlString, schema) if err != nil { - violation := &liberrors.SchemaValidationFailure{ - Reason: err.Error(), - Location: "xml parsing", - ReferenceSchema: "", - ReferenceObject: xmlString, - } + // XML parsing is a pre-validation error - no SchemaValidationFailure validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: helpers.RequestBodyValidation, - ValidationSubType: helpers.Schema, - Message: "xml example is malformed", - Reason: fmt.Sprintf("failed to parse xml: %s", err.Error()), - SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation}, - HowToFix: "ensure xml is well-formed and matches schema structure", + ValidationType: helpers.RequestBodyValidation, + ValidationSubType: helpers.Schema, + Message: "xml example is malformed", + Reason: fmt.Sprintf("failed to parse xml: %s", err.Error()), + HowToFix: "ensure xml is well-formed and matches schema structure", }) return false, validationErrors } diff --git a/validator_test.go b/validator_test.go index 3244ed7..7ffcb8e 100644 --- a/validator_test.go +++ b/validator_test.go @@ -312,7 +312,7 @@ paths: assert.Equal(t, "POST request body for '/burgers/createBurger' failed to validate schema", errors[0].Message) require.Len(t, errors[0].SchemaValidationErrors, 1) require.NotNil(t, errors[0].SchemaValidationErrors[0]) - assert.Equal(t, "/properties/name/format", errors[0].SchemaValidationErrors[0].Location) + assert.Equal(t, "/properties/name/format", errors[0].SchemaValidationErrors[0].KeywordLocation) assert.Equal(t, "'big mac' is not valid capital: expected first latter to be uppercase", errors[0].SchemaValidationErrors[0].Reason) }