diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 2100ba0..6667b76 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -1,3 +1,6 @@ +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley +// SPDX-License-Identifier: MIT + package errors import ( @@ -13,6 +16,7 @@ import ( ) func IncorrectFormEncoding(param *v3.Parameter, qp *helpers.QueryParam, i int) *ValidationError { + specLine, specCol := paramExplodeLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -20,8 +24,8 @@ func IncorrectFormEncoding(param *v3.Parameter, qp *helpers.QueryParam, i int) * Reason: fmt.Sprintf("The query parameter '%s' has a default or 'form' encoding defined, "+ "however the value '%s' is encoded as an object or an array using commas. The contract defines "+ "the explode value to set to 'true'", param.Name, qp.Values[i]), - SpecLine: param.GoLow().Explode.ValueNode.Line, - SpecCol: param.GoLow().Explode.ValueNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: param, HowToFix: fmt.Sprintf(HowToFixParamInvalidFormEncode, @@ -30,6 +34,7 @@ func IncorrectFormEncoding(param *v3.Parameter, qp *helpers.QueryParam, i int) * } func IncorrectSpaceDelimiting(param *v3.Parameter, qp *helpers.QueryParam) *ValidationError { + specLine, specCol := paramStyleLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -37,8 +42,8 @@ func IncorrectSpaceDelimiting(param *v3.Parameter, qp *helpers.QueryParam) *Vali Reason: fmt.Sprintf("The query parameter '%s' has 'spaceDelimited' style defined, "+ "and explode is defined as false. There are multiple values (%d) supplied, instead of a single"+ " space delimited value", param.Name, len(qp.Values)), - SpecLine: param.GoLow().Style.ValueNode.Line, - SpecCol: param.GoLow().Style.ValueNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: param, HowToFix: fmt.Sprintf(HowToFixParamInvalidSpaceDelimitedObjectExplode, @@ -47,6 +52,7 @@ func IncorrectSpaceDelimiting(param *v3.Parameter, qp *helpers.QueryParam) *Vali } func IncorrectPipeDelimiting(param *v3.Parameter, qp *helpers.QueryParam) *ValidationError { + specLine, specCol := paramStyleLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -54,8 +60,8 @@ func IncorrectPipeDelimiting(param *v3.Parameter, qp *helpers.QueryParam) *Valid Reason: fmt.Sprintf("The query parameter '%s' has 'pipeDelimited' style defined, "+ "and explode is defined as false. There are multiple values (%d) supplied, instead of a single"+ " space delimited value", param.Name, len(qp.Values)), - SpecLine: param.GoLow().Style.ValueNode.Line, - SpecCol: param.GoLow().Style.ValueNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: param, HowToFix: fmt.Sprintf(HowToFixParamInvalidPipeDelimitedObjectExplode, @@ -64,6 +70,7 @@ func IncorrectPipeDelimiting(param *v3.Parameter, qp *helpers.QueryParam) *Valid } func InvalidDeepObject(param *v3.Parameter, qp *helpers.QueryParam) *ValidationError { + specLine, specCol := paramStyleLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, @@ -71,8 +78,8 @@ func InvalidDeepObject(param *v3.Parameter, qp *helpers.QueryParam) *ValidationE Reason: fmt.Sprintf("The query parameter '%s' has the 'deepObject' style defined, "+ "There are multiple values (%d) supplied, instead of a single "+ "value", param.Name, len(qp.Values)), - SpecLine: param.GoLow().Style.ValueNode.Line, - SpecCol: param.GoLow().Style.ValueNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: param, HowToFix: fmt.Sprintf(HowToFixParamInvalidDeepObjectMultipleValues, @@ -82,6 +89,7 @@ func InvalidDeepObject(param *v3.Parameter, qp *helpers.QueryParam) *ValidationE func QueryParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "required") + specLine, specCol := paramRequiredLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -89,8 +97,8 @@ func QueryParameterMissing(param *v3.Parameter, pathTemplate string, operation s Message: fmt.Sprintf("Query parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being required, "+ "however it's missing from the requests", param.Name), - SpecLine: param.GoLow().Required.KeyNode.Line, - SpecCol: param.GoLow().Required.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixMissingValue, SchemaValidationErrors: []*SchemaValidationFailure{{ @@ -109,6 +117,7 @@ func HeaderParameterMissing(param *v3.Parameter, pathTemplate string, operation 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) + specLine, specCol := paramRequiredLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -116,8 +125,8 @@ func HeaderParameterMissing(param *v3.Parameter, pathTemplate string, operation Message: fmt.Sprintf("Header parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The header parameter '%s' is defined as being required, "+ "however it's missing from the requests", param.Name), - SpecLine: param.GoLow().Required.KeyNode.Line, - SpecCol: param.GoLow().Required.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixMissingValue, SchemaValidationErrors: []*SchemaValidationFailure{{ @@ -133,6 +142,7 @@ func HeaderParameterMissing(param *v3.Parameter, pathTemplate string, operation func CookieParameterMissing(param *v3.Parameter, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "required") + specLine, specCol := paramRequiredLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -140,8 +150,8 @@ func CookieParameterMissing(param *v3.Parameter, pathTemplate string, operation Message: fmt.Sprintf("Cookie parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' is defined as being required, "+ "however it's missing from the request", param.Name), - SpecLine: param.GoLow().Required.KeyNode.Line, - SpecCol: param.GoLow().Required.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixMissingValue, SchemaValidationErrors: []*SchemaValidationFailure{{ @@ -157,6 +167,7 @@ func CookieParameterMissing(param *v3.Parameter, pathTemplate string, operation func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string, pathTemplate string, operation string, renderedSchema string) *ValidationError { keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + specLine, specCol := paramSchemaTypeLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -164,8 +175,8 @@ func HeaderParameterCannotBeDecoded(param *v3.Parameter, val string, pathTemplat Message: fmt.Sprintf("Header parameter '%s' cannot be decoded", param.Name), Reason: fmt.Sprintf("The header parameter '%s' cannot be "+ "extracted into an object, '%s' is malformed", param.Name, val), - SpecLine: param.GoLow().Schema.Value.Schema().Type.KeyNode.Line, - SpecCol: param.GoLow().Schema.Value.Schema().Type.KeyNode.Line, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixInvalidEncoding, SchemaValidationErrors: []*SchemaValidationFailure{{ @@ -186,6 +197,7 @@ func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema, validEnums := strings.Join(enums, ", ") keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") + specLine, specCol := paramSchemaEnumLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -193,8 +205,8 @@ func IncorrectHeaderParamEnum(param *v3.Parameter, ef string, sch *base.Schema, Message: fmt.Sprintf("Header parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The header parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), - SpecLine: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Line, - SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), @@ -212,6 +224,7 @@ func IncorrectQueryParamArrayBoolean( 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -219,8 +232,8 @@ func IncorrectQueryParamArrayBoolean( Message: fmt.Sprintf("Query array parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid true/false value", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), @@ -236,6 +249,7 @@ func IncorrectQueryParamArrayBoolean( 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -243,8 +257,8 @@ func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expec Message: fmt.Sprintf("Query array parameter '%s' has too many items", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' has a maximum item length of %d, "+ "however the request provided %d items", param.Name, expected, actual), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixInvalidMaxItems, expected), @@ -260,6 +274,7 @@ func IncorrectParamArrayMaxNumItems(param *v3.Parameter, sch *base.Schema, expec 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -267,8 +282,8 @@ func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expec Message: fmt.Sprintf("Query array parameter '%s' does not have enough items", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' has a minimum items length of %d, "+ "however the request provided %d items", param.Name, expected, actual), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixInvalidMinItems, expected), @@ -284,14 +299,15 @@ func IncorrectParamArrayMinNumItems(param *v3.Parameter, sch *base.Schema, expec 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query array parameter '%s' contains non-unique items", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' contains the following duplicates: '%s'", param.Name, duplicates), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: "Ensure the array values are all unique", @@ -309,6 +325,7 @@ func IncorrectCookieParamArrayBoolean( 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -316,8 +333,8 @@ func IncorrectCookieParamArrayBoolean( Message: fmt.Sprintf("Cookie array parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The cookie parameter (which is an array) '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid true/false value", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), @@ -335,6 +352,7 @@ func IncorrectQueryParamArrayInteger( 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -342,8 +360,8 @@ func IncorrectQueryParamArrayInteger( Message: fmt.Sprintf("Query array parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, item), @@ -361,6 +379,7 @@ func IncorrectQueryParamArrayNumber( 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -368,8 +387,8 @@ func IncorrectQueryParamArrayNumber( Message: fmt.Sprintf("Query array parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The query parameter (which is an array) '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), @@ -387,6 +406,7 @@ func IncorrectCookieParamArrayNumber( 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -394,8 +414,8 @@ func IncorrectCookieParamArrayNumber( Message: fmt.Sprintf("Cookie array parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The cookie parameter (which is an array) '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), @@ -414,6 +434,7 @@ func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema 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) + specLine, specCol := paramContentLineCol(param, helpers.JSONContentType) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -421,8 +442,8 @@ func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema Message: fmt.Sprintf("Query parameter '%s' is not valid JSON", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being a JSON object, "+ "however the value '%s' is not valid JSON", param.Name, ef), - SpecLine: param.GoLow().FindContent(helpers.JSONContentType).ValueNode.Line, - SpecCol: param.GoLow().FindContent(helpers.JSONContentType).ValueNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: HowToFixInvalidJSON, @@ -438,6 +459,7 @@ func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema 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") + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -445,8 +467,8 @@ func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema, p Message: fmt.Sprintf("Query parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), @@ -462,6 +484,7 @@ func IncorrectQueryParamBool(param *v3.Parameter, ef string, sch *base.Schema, p 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") + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -469,8 +492,8 @@ func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema, Message: fmt.Sprintf("Query parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), @@ -486,6 +509,7 @@ func InvalidQueryParamInteger(param *v3.Parameter, ef string, sch *base.Schema, 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") + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -493,8 +517,8 @@ func InvalidQueryParamNumber(param *v3.Parameter, ef string, sch *base.Schema, p Message: fmt.Sprintf("Query parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The query parameter '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), @@ -516,6 +540,7 @@ func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema, p validEnums := strings.Join(enums, ", ") keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") + specLine, specCol := paramSchemaEnumLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -523,8 +548,8 @@ func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema, p Message: fmt.Sprintf("Query parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The query parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), - SpecLine: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Line, - SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), @@ -540,23 +565,45 @@ func IncorrectQueryParamEnum(param *v3.Parameter, ef string, sch *base.Schema, p 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 { - enums = append(enums, - fmt.Sprint(param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.Value[i].Value.Value)) + if low := param.GoLow(); low != nil { + if sv := low.Schema.Value; sv != nil { + if s := sv.Schema(); s != nil { + if iv := s.Items.Value; iv != nil { + if as := iv.A.Schema(); as != nil { + for i := range as.Enum.Value { + enums = append(enums, fmt.Sprint(as.Enum.Value[i].Value.Value)) + } + } + } + } + } } validEnums := strings.Join(enums, ", ") keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "items/enum") + specLine, specCol := 1, 0 + if low := param.GoLow(); low != nil { + if sv := low.Schema.Value; sv != nil { + if s := sv.Schema(); s != nil { + if iv := s.Items.Value; iv != nil { + if as := iv.A.Schema(); as != nil && as.Enum.KeyNode != nil { + specLine = as.Enum.KeyNode.Line + specCol = as.Enum.KeyNode.Column + } + } + } + } + } + return &ValidationError{ ValidationType: helpers.ParameterValidation, ValidationSubType: helpers.ParameterValidationQuery, Message: fmt.Sprintf("Query array parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The query array parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), - SpecLine: param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.KeyNode.Line, - SpecCol: param.GoLow().Schema.Value.Schema().Items.Value.A.Schema().Enum.KeyNode.Line, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), @@ -575,6 +622,7 @@ func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema, p 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) + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -582,8 +630,8 @@ func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema, p Message: fmt.Sprintf("Query parameter '%s' value contains reserved values", param.Name), Reason: fmt.Sprintf("The query parameter '%s' has 'allowReserved' set to false, "+ "however the value '%s' contains one of the following characters: :/?#[]@!$&'()*+,;=", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixReservedValues, url.QueryEscape(ef)), @@ -599,6 +647,7 @@ func IncorrectReservedValues(param *v3.Parameter, ef string, sch *base.Schema, p 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") + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -606,8 +655,8 @@ func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema, Message: fmt.Sprintf("Header parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The header parameter '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), @@ -623,6 +672,7 @@ func InvalidHeaderParamInteger(param *v3.Parameter, ef string, sch *base.Schema, 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") + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -630,8 +680,8 @@ func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema, Message: fmt.Sprintf("Header parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The header parameter '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), @@ -647,6 +697,7 @@ func InvalidHeaderParamNumber(param *v3.Parameter, ef string, sch *base.Schema, 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") + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -654,8 +705,8 @@ func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema, Message: fmt.Sprintf("Cookie parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, ef), @@ -671,6 +722,7 @@ func InvalidCookieParamInteger(param *v3.Parameter, ef string, sch *base.Schema, 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") + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -678,8 +730,8 @@ func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema, Message: fmt.Sprintf("Cookie parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, ef), @@ -695,6 +747,7 @@ func InvalidCookieParamNumber(param *v3.Parameter, ef string, sch *base.Schema, 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") + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -702,8 +755,8 @@ func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema, Message: fmt.Sprintf("Header parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The header parameter '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), @@ -719,6 +772,7 @@ func IncorrectHeaderParamBool(param *v3.Parameter, ef string, sch *base.Schema, 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") + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -726,8 +780,8 @@ func IncorrectCookieParamBool(param *v3.Parameter, ef string, sch *base.Schema, Message: fmt.Sprintf("Cookie parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, ef), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, ef), @@ -749,6 +803,7 @@ func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema, validEnums := strings.Join(enums, ", ") keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "enum") + specLine, specCol := paramSchemaEnumLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -756,8 +811,8 @@ func IncorrectCookieParamEnum(param *v3.Parameter, ef string, sch *base.Schema, Message: fmt.Sprintf("Cookie parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The cookie parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), - SpecLine: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Line, - SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), @@ -775,6 +830,7 @@ func IncorrectHeaderParamArrayBoolean( 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -782,8 +838,8 @@ func IncorrectHeaderParamArrayBoolean( Message: fmt.Sprintf("Header array parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The header parameter (which is an array) '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid true/false value", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), @@ -801,6 +857,7 @@ func IncorrectHeaderParamArrayNumber( 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") + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -808,8 +865,8 @@ func IncorrectHeaderParamArrayNumber( Message: fmt.Sprintf("Header array parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The header parameter (which is an array) '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), @@ -827,6 +884,7 @@ func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema, escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -834,8 +892,8 @@ func IncorrectPathParamBool(param *v3.Parameter, item string, sch *base.Schema, Message: fmt.Sprintf("Path parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The path parameter '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, item), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), @@ -859,6 +917,7 @@ func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pa enums = append(enums, fmt.Sprint(sch.Enum[i].Value)) } validEnums := strings.Join(enums, ", ") + specLine, specCol := paramSchemaEnumLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -867,8 +926,8 @@ func IncorrectPathParamEnum(param *v3.Parameter, ef string, sch *base.Schema, pa Message: fmt.Sprintf("Path parameter '%s' does not match allowed values", param.Name), Reason: fmt.Sprintf("The path parameter '%s' has pre-defined "+ "values set via an enum. The value '%s' is not one of those values.", param.Name, ef), - SpecLine: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Line, - SpecCol: param.GoLow().Schema.Value.Schema().Enum.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidEnum, ef, validEnums), SchemaValidationErrors: []*SchemaValidationFailure{{ @@ -885,6 +944,7 @@ func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schem escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -892,8 +952,8 @@ func IncorrectPathParamInteger(param *v3.Parameter, item string, sch *base.Schem Message: fmt.Sprintf("Path parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The path parameter '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, item), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidInteger, item), @@ -911,6 +971,7 @@ func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/type", escapedPath, param.Name) + specLine, specCol := paramSchemaKeyLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -918,8 +979,8 @@ func IncorrectPathParamNumber(param *v3.Parameter, item string, sch *base.Schema Message: fmt.Sprintf("Path parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The path parameter '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), - SpecLine: param.GoLow().Schema.KeyNode.Line, - SpecCol: param.GoLow().Schema.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: sch, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), @@ -939,6 +1000,7 @@ func IncorrectPathParamArrayNumber( escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -946,8 +1008,8 @@ func IncorrectPathParamArrayNumber( Message: fmt.Sprintf("Path array parameter '%s' is not a valid number", param.Name), Reason: fmt.Sprintf("The path parameter (which is an array) '%s' is defined as being a number, "+ "however the value '%s' is not a valid number", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), @@ -967,6 +1029,7 @@ func IncorrectPathParamArrayInteger( escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -974,8 +1037,8 @@ func IncorrectPathParamArrayInteger( Message: fmt.Sprintf("Path array parameter '%s' is not a valid integer", param.Name), Reason: fmt.Sprintf("The path parameter (which is an array) '%s' is defined as being an integer, "+ "however the value '%s' is not a valid integer", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidNumber, item), @@ -995,6 +1058,7 @@ func IncorrectPathParamArrayBoolean( escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") keywordLocation := fmt.Sprintf("/paths/%s/parameters/%s/schema/items/type", escapedPath, param.Name) + specLine, specCol := schemaItemsTypeLineCol(sch) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -1002,8 +1066,8 @@ func IncorrectPathParamArrayBoolean( Message: fmt.Sprintf("Path array parameter '%s' is not a valid boolean", param.Name), Reason: fmt.Sprintf("The path parameter (which is an array) '%s' is defined as being a boolean, "+ "however the value '%s' is not a valid boolean", param.Name, item), - SpecLine: sch.Items.A.GoLow().Schema().Type.KeyNode.Line, - SpecCol: sch.Items.A.GoLow().Schema().Type.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, Context: itemsSchema, HowToFix: fmt.Sprintf(HowToFixParamInvalidBoolean, item), @@ -1024,6 +1088,7 @@ func PathParameterMissing(param *v3.Parameter, pathTemplate string, actualPath s encodedPath = strings.ReplaceAll(encodedPath, "/", "~1") encodedPath = strings.TrimPrefix(encodedPath, "~1") keywordLoc := fmt.Sprintf("/paths/%s/parameters/%s/required", encodedPath, param.Name) + specLine, specCol := paramRequiredLineCol(param) return &ValidationError{ ValidationType: helpers.ParameterValidation, @@ -1031,8 +1096,8 @@ func PathParameterMissing(param *v3.Parameter, pathTemplate string, actualPath s Message: fmt.Sprintf("Path parameter '%s' is missing", param.Name), Reason: fmt.Sprintf("The path parameter '%s' is defined as being required, "+ "however it's missing from the requests", param.Name), - SpecLine: param.GoLow().Required.KeyNode.Line, - SpecCol: param.GoLow().Required.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, ParameterName: param.Name, HowToFix: HowToFixMissingValue, SchemaValidationErrors: []*SchemaValidationFailure{{ diff --git a/errors/parameter_errors_test.go b/errors/parameter_errors_test.go index 6ba81a5..4050271 100644 --- a/errors/parameter_errors_test.go +++ b/errors/parameter_errors_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package errors @@ -1242,3 +1242,316 @@ items: require.Contains(t, err.Reason, "The query parameter (which is an array) 'testQueryParam' contains the following duplicates: 'fish, cake'") require.Contains(t, err.HowToFix, "Ensure the array values are all unique") } + +// createMinimalParameter creates a parameter with nil GoLow nodes to test nil safety. +func createMinimalParameter() *v3.Parameter { + param := &lowv3.Parameter{ + Name: low.NodeReference[string]{Value: "minParam"}, + // All node references intentionally left with nil KeyNode/ValueNode + } + return v3.NewParameter(param) +} + +func TestParameterErrors_NilGoLowNodes(t *testing.T) { + // Tests that all parameter error constructors handle nil GoLow nodes + // without panicking. This covers the crash scenario from wiretap #134. + param := createMinimalParameter() + qp := &helpers.QueryParam{ + Key: "minParam", + Values: []string{"value"}, + } + sch := &base.Schema{} + + t.Run("IncorrectFormEncoding", func(t *testing.T) { + err := IncorrectFormEncoding(param, qp, 0) + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectSpaceDelimiting", func(t *testing.T) { + err := IncorrectSpaceDelimiting(param, qp) + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectPipeDelimiting", func(t *testing.T) { + err := IncorrectPipeDelimiting(param, qp) + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("InvalidDeepObject", func(t *testing.T) { + err := InvalidDeepObject(param, qp) + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("QueryParameterMissing", func(t *testing.T) { + err := QueryParameterMissing(param, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("HeaderParameterMissing", func(t *testing.T) { + err := HeaderParameterMissing(param, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("CookieParameterMissing", func(t *testing.T) { + err := CookieParameterMissing(param, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("HeaderParameterCannotBeDecoded", func(t *testing.T) { + err := HeaderParameterCannotBeDecoded(param, "bad", "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectHeaderParamEnum", func(t *testing.T) { + err := IncorrectHeaderParamEnum(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectQueryParamEnum", func(t *testing.T) { + err := IncorrectQueryParamEnum(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectCookieParamEnum", func(t *testing.T) { + err := IncorrectCookieParamEnum(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectPathParamEnum", func(t *testing.T) { + err := IncorrectPathParamEnum(param, "bad", sch, "/test", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectQueryParamBool", func(t *testing.T) { + err := IncorrectQueryParamBool(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("InvalidQueryParamInteger", func(t *testing.T) { + err := InvalidQueryParamInteger(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("InvalidQueryParamNumber", func(t *testing.T) { + err := InvalidQueryParamNumber(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectReservedValues", func(t *testing.T) { + err := IncorrectReservedValues(param, "a:b", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("InvalidHeaderParamInteger", func(t *testing.T) { + err := InvalidHeaderParamInteger(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("InvalidHeaderParamNumber", func(t *testing.T) { + err := InvalidHeaderParamNumber(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("InvalidCookieParamInteger", func(t *testing.T) { + err := InvalidCookieParamInteger(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("InvalidCookieParamNumber", func(t *testing.T) { + err := InvalidCookieParamNumber(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectHeaderParamBool", func(t *testing.T) { + err := IncorrectHeaderParamBool(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectCookieParamBool", func(t *testing.T) { + err := IncorrectCookieParamBool(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectPathParamBool", func(t *testing.T) { + err := IncorrectPathParamBool(param, "bad", sch, "/test", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectPathParamInteger", func(t *testing.T) { + err := IncorrectPathParamInteger(param, "bad", sch, "/test", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectPathParamNumber", func(t *testing.T) { + err := IncorrectPathParamNumber(param, "bad", sch, "/test", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectParamEncodingJSON", func(t *testing.T) { + err := IncorrectParamEncodingJSON(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectQueryParamEnumArray", func(t *testing.T) { + err := IncorrectQueryParamEnumArray(param, "bad", sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("PathParameterMissing", func(t *testing.T) { + err := PathParameterMissing(param, "/test/{id}", "/test/123") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) +} + +func TestParameterErrors_NilSchemaItems(t *testing.T) { + // Tests array parameter error constructors with nil Items in schema. + param := createMinimalParameter() + sch := &base.Schema{} // no Items set + + t.Run("IncorrectQueryParamArrayBoolean", func(t *testing.T) { + err := IncorrectQueryParamArrayBoolean(param, "bad", sch, sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectParamArrayMaxNumItems", func(t *testing.T) { + err := IncorrectParamArrayMaxNumItems(param, sch, 5, 10, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectParamArrayMinNumItems", func(t *testing.T) { + err := IncorrectParamArrayMinNumItems(param, sch, 5, 2, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectParamArrayUniqueItems", func(t *testing.T) { + err := IncorrectParamArrayUniqueItems(param, sch, "dup", "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectCookieParamArrayBoolean", func(t *testing.T) { + err := IncorrectCookieParamArrayBoolean(param, "bad", sch, sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectQueryParamArrayInteger", func(t *testing.T) { + err := IncorrectQueryParamArrayInteger(param, "bad", sch, sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectQueryParamArrayNumber", func(t *testing.T) { + err := IncorrectQueryParamArrayNumber(param, "bad", sch, sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectCookieParamArrayNumber", func(t *testing.T) { + err := IncorrectCookieParamArrayNumber(param, "bad", sch, sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectHeaderParamArrayBoolean", func(t *testing.T) { + err := IncorrectHeaderParamArrayBoolean(param, "bad", sch, sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectHeaderParamArrayNumber", func(t *testing.T) { + err := IncorrectHeaderParamArrayNumber(param, "bad", sch, sch, "/test", "get", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectPathParamArrayNumber", func(t *testing.T) { + err := IncorrectPathParamArrayNumber(param, "bad", sch, sch, "/test", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectPathParamArrayInteger", func(t *testing.T) { + err := IncorrectPathParamArrayInteger(param, "bad", sch, sch, "/test", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) + + t.Run("IncorrectPathParamArrayBoolean", func(t *testing.T) { + err := IncorrectPathParamArrayBoolean(param, "bad", sch, sch, "/test", "{}") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) + }) +} diff --git a/errors/request_errors.go b/errors/request_errors.go index f53c2b8..01bf3f4 100644 --- a/errors/request_errors.go +++ b/errors/request_errors.go @@ -1,4 +1,4 @@ -// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package errors @@ -18,8 +18,17 @@ import ( func RequestContentTypeNotFound(op *v3.Operation, request *http.Request, specPath string) *ValidationError { ct := request.Header.Get(helpers.ContentTypeHeader) var ctypes []string - for pair := orderedmap.First(op.RequestBody.Content); pair != nil; pair = pair.Next() { - ctypes = append(ctypes, pair.Key()) + var contentMap *orderedmap.Map[string, *v3.MediaType] + specLine, specCol := 1, 0 + if op.RequestBody != nil { + contentMap = op.RequestBody.Content + for pair := orderedmap.First(op.RequestBody.Content); pair != nil; pair = pair.Next() { + ctypes = append(ctypes, pair.Key()) + } + if low := op.RequestBody.GoLow(); low != nil && low.Content.KeyNode != nil { + specLine = low.Content.KeyNode.Line + specCol = low.Content.KeyNode.Column + } } return &ValidationError{ ValidationType: helpers.RequestBodyValidation, @@ -28,10 +37,10 @@ func RequestContentTypeNotFound(op *v3.Operation, request *http.Request, specPat request.Method, ct), Reason: fmt.Sprintf("The content type '%s' of the %s request submitted has not "+ "been defined, it's an unknown type", ct, request.Method), - SpecLine: op.RequestBody.GoLow().Content.KeyNode.Line, - SpecCol: op.RequestBody.GoLow().Content.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, Context: op, - HowToFix: fmt.Sprintf(HowToFixInvalidContentType, orderedmap.Len(op.RequestBody.Content), strings.Join(ctypes, ", ")), + HowToFix: fmt.Sprintf(HowToFixInvalidContentType, orderedmap.Len(contentMap), strings.Join(ctypes, ", ")), RequestPath: request.URL.Path, RequestMethod: request.Method, SpecPath: specPath, @@ -39,14 +48,19 @@ func RequestContentTypeNotFound(op *v3.Operation, request *http.Request, specPat } func OperationNotFound(pathItem *v3.PathItem, request *http.Request, method string, specPath string) *ValidationError { + specLine, specCol := 1, 0 + if low := pathItem.GoLow(); low != nil && low.KeyNode != nil { + specLine = low.KeyNode.Line + specCol = low.KeyNode.Column + } return &ValidationError{ ValidationType: helpers.RequestValidation, ValidationSubType: helpers.ValidationMissingOperation, Message: fmt.Sprintf("%s operation request content type '%s' does not exist", request.Method, method), Reason: fmt.Sprintf("The path was found, but there was no '%s' method found in the spec", request.Method), - SpecLine: pathItem.GoLow().KeyNode.Line, - SpecCol: pathItem.GoLow().KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, Context: pathItem, HowToFix: HowToFixPathMethod, RequestPath: request.URL.Path, diff --git a/errors/request_errors_test.go b/errors/request_errors_test.go index 47025a7..033f2d5 100644 --- a/errors/request_errors_test.go +++ b/errors/request_errors_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package errors @@ -98,3 +98,30 @@ func TestOperationNotFound(t *testing.T) { require.Equal(t, 25, err.SpecCol) require.Equal(t, HowToFixPathMethod, err.HowToFix) } + +func TestRequestContentTypeNotFound_NilContentKeyNode(t *testing.T) { + // RequestBody exists but has no content KeyNode — should not panic + reqBody := &lowv3.RequestBody{ + Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ + Value: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]](), + // KeyNode intentionally nil + }, + } + op := &lowv3.Operation{ + RequestBody: low.NodeReference[*lowv3.RequestBody]{ + Value: reqBody, + KeyNode: &yaml.Node{}, + ValueNode: &yaml.Node{}, + }, + } + highOp := v3.NewOperation(op) + + request, _ := http.NewRequest(http.MethodPost, "/test", nil) + request.Header.Set(helpers.ContentTypeHeader, "application/xml") + + // Should not panic + err := RequestContentTypeNotFound(highOp, request, "/test") + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) +} diff --git a/errors/response_errors.go b/errors/response_errors.go index a519e8d..06a7264 100644 --- a/errors/response_errors.go +++ b/errors/response_errors.go @@ -1,4 +1,4 @@ -// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package errors @@ -24,24 +24,33 @@ func ResponseContentTypeNotFound(op *v3.Operation, ct := response.Header.Get(helpers.ContentTypeHeader) mediaTypeString, _, _ := helpers.ExtractContentType(ct) var ctypes []string - var specLine, specCol int + specLine, specCol := 1, 0 var contentMap *orderedmap.Map[string, *v3.MediaType] // check for a default type (applies to all codes without a match) if !isDefault { - for pair := orderedmap.First(op.Responses.Codes.GetOrZero(code).Content); pair != nil; pair = pair.Next() { - ctypes = append(ctypes, pair.Key()) + resp := op.Responses.Codes.GetOrZero(code) + if resp != nil { + for pair := orderedmap.First(resp.Content); pair != nil; pair = pair.Next() { + ctypes = append(ctypes, pair.Key()) + } + contentMap = resp.Content + if low := resp.GoLow(); low != nil && low.Content.KeyNode != nil { + specLine = low.Content.KeyNode.Line + specCol = low.Content.KeyNode.Column + } } - specLine = op.Responses.Codes.GetOrZero(code).GoLow().Content.KeyNode.Line - specCol = op.Responses.Codes.GetOrZero(code).GoLow().Content.KeyNode.Column - contentMap = op.Responses.Codes.GetOrZero(code).Content } else { - for pair := orderedmap.First(op.Responses.Default.Content); pair != nil; pair = pair.Next() { - ctypes = append(ctypes, pair.Key()) + if op.Responses.Default != nil { + for pair := orderedmap.First(op.Responses.Default.Content); pair != nil; pair = pair.Next() { + ctypes = append(ctypes, pair.Key()) + } + contentMap = op.Responses.Default.Content + if low := op.Responses.Default.GoLow(); low != nil && low.Content.KeyNode != nil { + specLine = low.Content.KeyNode.Line + specCol = low.Content.KeyNode.Column + } } - specLine = op.Responses.Default.GoLow().Content.KeyNode.Line - specCol = op.Responses.Default.GoLow().Content.KeyNode.Column - contentMap = op.Responses.Default.Content } return &ValidationError{ ValidationType: helpers.ResponseBodyValidation, @@ -59,6 +68,11 @@ func ResponseContentTypeNotFound(op *v3.Operation, } func ResponseCodeNotFound(op *v3.Operation, request *http.Request, code int) *ValidationError { + specLine, specCol := 1, 0 + if low := op.GoLow(); low != nil && low.Responses.KeyNode != nil { + specLine = low.Responses.KeyNode.Line + specCol = low.Responses.KeyNode.Column + } return &ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.ResponseBodyResponseCode, @@ -66,8 +80,8 @@ func ResponseCodeNotFound(op *v3.Operation, request *http.Request, code int) *Va request.Method, code), Reason: fmt.Sprintf("The response code '%d' of the %s request submitted has not "+ "been defined, it's an unknown type", code, request.Method), - SpecLine: op.GoLow().Responses.KeyNode.Line, - SpecCol: op.GoLow().Responses.KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, Context: op, HowToFix: HowToFixInvalidResponseCode, } diff --git a/errors/response_errors_test.go b/errors/response_errors_test.go index 7e65c0d..69323a0 100644 --- a/errors/response_errors_test.go +++ b/errors/response_errors_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package errors @@ -159,3 +159,105 @@ func TestResponseCodeNotFound(t *testing.T) { require.Equal(t, 56, err.SpecCol) require.Equal(t, HowToFixInvalidResponseCode, err.HowToFix) } + +func TestResponseContentTypeNotFound_NilContentKeyNode(t *testing.T) { + // Response code exists but has no content KeyNode — should not panic + r := &lowv3.Response{ + Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ + Value: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]](), + // KeyNode intentionally nil + }, + } + resp := v3.NewResponse(r) + + responses := &lowv3.Responses{ + Codes: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.Response]](), + KeyNode: &yaml.Node{}, + } + op := &lowv3.Operation{ + Responses: low.NodeReference[*lowv3.Responses]{ + Value: responses, + KeyNode: &yaml.Node{}, + ValueNode: &yaml.Node{}, + }, + } + highOp := v3.NewOperation(op) + highOp.Responses.Codes.Set("200", resp) + + request, _ := http.NewRequest(http.MethodGet, "/test", nil) + response := &http.Response{ + Header: http.Header{ + helpers.ContentTypeHeader: {"application/xml"}, + }, + } + + // Should not panic + err := ResponseContentTypeNotFound(highOp, request, response, "200", false) + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) +} + +func TestResponseContentTypeNotFound_NilDefaultContentKeyNode(t *testing.T) { + // Default response exists but has no content KeyNode — should not panic + r := &lowv3.Response{ + Content: low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]]]{ + Value: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.MediaType]](), + // KeyNode intentionally nil + }, + } + + responses := &lowv3.Responses{ + Default: low.NodeReference[*lowv3.Response]{ + Value: r, + KeyNode: &yaml.Node{}, + ValueNode: &yaml.Node{}, + }, + Codes: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.Response]](), + KeyNode: &yaml.Node{}, + } + op := &lowv3.Operation{ + Responses: low.NodeReference[*lowv3.Responses]{ + Value: responses, + KeyNode: &yaml.Node{}, + ValueNode: &yaml.Node{}, + }, + } + highOp := v3.NewOperation(op) + + request, _ := http.NewRequest(http.MethodGet, "/test", nil) + response := &http.Response{ + Header: http.Header{ + helpers.ContentTypeHeader: {"application/xml"}, + }, + } + + // Should not panic + err := ResponseContentTypeNotFound(highOp, request, response, "200", true) + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) +} + +func TestResponseCodeNotFound_NilResponsesKeyNode(t *testing.T) { + // Operation with nil Responses KeyNode — should not panic + responses := &lowv3.Responses{ + Codes: orderedmap.New[low.KeyReference[string], low.ValueReference[*lowv3.Response]](), + // KeyNode intentionally nil + } + op := &lowv3.Operation{ + Responses: low.NodeReference[*lowv3.Responses]{ + Value: responses, + ValueNode: &yaml.Node{}, + }, + } + highOp := v3.NewOperation(op) + + request, _ := http.NewRequest(http.MethodGet, "/test", nil) + + // Should not panic + err := ResponseCodeNotFound(highOp, request, 404) + require.NotNil(t, err) + require.Equal(t, 1, err.SpecLine) + require.Equal(t, 0, err.SpecCol) +} diff --git a/errors/spec_line_col.go b/errors/spec_line_col.go new file mode 100644 index 0000000..0c679ee --- /dev/null +++ b/errors/spec_line_col.go @@ -0,0 +1,103 @@ +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley +// SPDX-License-Identifier: MIT + +package errors + +import ( + "go.yaml.in/yaml/v4" + + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + + "github.com/pb33f/libopenapi/datamodel/high/base" +) + +// SafeNodeLineCol safely extracts line and column from a yaml.Node pointer. +// Returns (1, 0) if the node is nil. +func SafeNodeLineCol(node *yaml.Node) (int, int) { + if node == nil { + return 1, 0 + } + return node.Line, node.Column +} + +// paramExplodeLineCol safely extracts SpecLine/SpecCol from param.GoLow().Explode.ValueNode. +func paramExplodeLineCol(param *v3.Parameter) (int, int) { + if low := param.GoLow(); low != nil && low.Explode.ValueNode != nil { + return low.Explode.ValueNode.Line, low.Explode.ValueNode.Column + } + return 1, 0 +} + +// paramStyleLineCol safely extracts SpecLine/SpecCol from param.GoLow().Style.ValueNode. +func paramStyleLineCol(param *v3.Parameter) (int, int) { + if low := param.GoLow(); low != nil && low.Style.ValueNode != nil { + return low.Style.ValueNode.Line, low.Style.ValueNode.Column + } + return 1, 0 +} + +// paramRequiredLineCol safely extracts SpecLine/SpecCol from param.GoLow().Required.KeyNode. +func paramRequiredLineCol(param *v3.Parameter) (int, int) { + if low := param.GoLow(); low != nil && low.Required.KeyNode != nil { + return low.Required.KeyNode.Line, low.Required.KeyNode.Column + } + return 1, 0 +} + +// paramSchemaKeyLineCol safely extracts SpecLine/SpecCol from param.GoLow().Schema.KeyNode. +func paramSchemaKeyLineCol(param *v3.Parameter) (int, int) { + if low := param.GoLow(); low != nil && low.Schema.KeyNode != nil { + return low.Schema.KeyNode.Line, low.Schema.KeyNode.Column + } + return 1, 0 +} + +// paramSchemaTypeLineCol safely extracts SpecLine/SpecCol from +// param.GoLow().Schema.Value.Schema().Type.KeyNode. +func paramSchemaTypeLineCol(param *v3.Parameter) (int, int) { + if low := param.GoLow(); low != nil { + if sv := low.Schema.Value; sv != nil { + if s := sv.Schema(); s != nil && s.Type.KeyNode != nil { + return s.Type.KeyNode.Line, s.Type.KeyNode.Column + } + } + } + return 1, 0 +} + +// paramSchemaEnumLineCol safely extracts SpecLine/SpecCol from +// param.GoLow().Schema.Value.Schema().Enum.KeyNode. +func paramSchemaEnumLineCol(param *v3.Parameter) (int, int) { + if low := param.GoLow(); low != nil { + if sv := low.Schema.Value; sv != nil { + if s := sv.Schema(); s != nil && s.Enum.KeyNode != nil { + return s.Enum.KeyNode.Line, s.Enum.KeyNode.Column + } + } + } + return 1, 0 +} + +// paramContentLineCol safely extracts SpecLine/SpecCol from +// param.GoLow().FindContent(ct).ValueNode. +func paramContentLineCol(param *v3.Parameter, contentType string) (int, int) { + if low := param.GoLow(); low != nil { + if found := low.FindContent(contentType); found != nil && found.ValueNode != nil { + return found.ValueNode.Line, found.ValueNode.Column + } + } + return 1, 0 +} + +// schemaItemsTypeLineCol safely extracts SpecLine/SpecCol from +// sch.Items.A.GoLow().Schema().Type.KeyNode. +func schemaItemsTypeLineCol(sch *base.Schema) (int, int) { + if sch.Items != nil && sch.Items.A != nil { + if low := sch.Items.A.GoLow(); low != nil { + if s := low.Schema(); s != nil && s.Type.KeyNode != nil { + return s.Type.KeyNode.Line, s.Type.KeyNode.Column + } + } + } + return 1, 0 +} diff --git a/paths/paths.go b/paths/paths.go index 635cfdb..ccddf91 100644 --- a/paths/paths.go +++ b/paths/paths.go @@ -174,7 +174,7 @@ func checkPathAgainstBase(docPath, urlPath string, basePaths []string) bool { return true } for _, basePath := range basePaths { - if basePath[len(basePath)-1] == '/' { + if len(basePath) > 1 && basePath[len(basePath)-1] == '/' { basePath = basePath[:len(basePath)-1] } merged := fmt.Sprintf("%s%s", basePath, urlPath) diff --git a/paths/paths_test.go b/paths/paths_test.go index ac5d7b8..3913140 100644 --- a/paths/paths_test.go +++ b/paths/paths_test.go @@ -1480,6 +1480,11 @@ func TestCheckPathAgainstBase_MergedPath(t *testing.T) { // No match result = checkPathAgainstBase("/other/path", "/users", basePaths) assert.False(t, result) + + // Issue #27: basePath is just "/" — should not produce empty string + basePaths = []string{"/"} + result = checkPathAgainstBase("/users", "/users", basePaths) + assert.True(t, result) } func TestFindPath_RegexFallback_MethodMismatch(t *testing.T) { diff --git a/requests/validate_request.go b/requests/validate_request.go index a5fda87..caaf5aa 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -1,4 +1,4 @@ -// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package requests @@ -183,11 +183,19 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V return true, nil } - line := schema.ParentProxy.GetSchemaKeyNode().Line - col := schema.ParentProxy.GetSchemaKeyNode().Line + line := 1 + col := 0 + if schema.ParentProxy != nil { + if keyNode := schema.ParentProxy.GetSchemaKeyNode(); keyNode != nil { + line = keyNode.Line + col = keyNode.Column + } + } if schema.Type != nil { - line = schema.GoLow().Type.KeyNode.Line - col = schema.GoLow().Type.KeyNode.Column + if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { + line = low.Type.KeyNode.Line + col = low.Type.KeyNode.Column + } } validationErrors = append(validationErrors, &errors.ValidationError{ @@ -231,7 +239,10 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V if er.Error != nil { // locate the violated property in the schema - located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation) + var located *yaml.Node + if len(renderedNode.Content) > 0 { + located = schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation) + } // extract the element specified by the instance val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation) @@ -283,9 +294,9 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V line := 1 col := 0 - if schema.GoLow().Type.KeyNode != nil { - line = schema.GoLow().Type.KeyNode.Line - col = schema.GoLow().Type.KeyNode.Column + if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { + line = low.Type.KeyNode.Line + col = low.Type.KeyNode.Column } // add the error to the list diff --git a/requests/validate_request_test.go b/requests/validate_request_test.go index 7e37f5a..fd4ea8d 100644 --- a/requests/validate_request_test.go +++ b/requests/validate_request_test.go @@ -318,3 +318,21 @@ components: assert.Contains(t, errors[0].Message, "failed schema rendering") assert.Contains(t, errors[0].Reason, "circular reference") } + +func TestValidateRequestSchema_NilParentProxy(t *testing.T) { + // Schema with nil ParentProxy and empty body — should not panic (fix for wiretap #134) + schema := &base.Schema{ + Type: []string{"object"}, + } + + valid, errs := ValidateRequestSchema(&ValidateRequestSchemaInput{ + Request: postRequestWithBody(""), + Schema: schema, + Version: 3.1, + BodyRequired: true, + }) + + // Should return error about nil schema low-level info, not panic + assert.False(t, valid) + assert.NotEmpty(t, errs) +} diff --git a/responses/validate_body.go b/responses/validate_body.go index 3fc9891..82d7796 100644 --- a/responses/validate_body.go +++ b/responses/validate_body.go @@ -1,4 +1,4 @@ -// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package responses @@ -61,6 +61,11 @@ func (v *responseBodyValidator) ValidateResponseBodyWithPathItem(request *http.R // extract the media type from the content type header. mediaTypeSting, _, _ := helpers.ExtractContentType(contentType) + // check if operation has responses defined + if operation.Responses == nil || operation.Responses.Codes == nil { + return true, nil + } + // check if the response code is in the contract foundResponse := operation.Responses.Codes.GetOrZero(codeStr) if foundResponse == nil { diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go index 8170d69..aa56496 100644 --- a/responses/validate_body_test.go +++ b/responses/validate_body_test.go @@ -1905,6 +1905,37 @@ paths: assert.Equal(t, "xml example is malformed", errors[0].Message) } +func TestValidateResponseBodyWithPathItem_NilResponses(t *testing.T) { + // Operation with nil Responses — should not panic (fix for wiretap #134) + spec := []byte(`openapi: 3.1.0 +paths: + /test: + get: + responses: + '200': + description: ok +`) + doc, err := libopenapi.NewDocument(spec) + require.NoError(t, err) + m, errs := doc.BuildV3Model() + require.Empty(t, errs) + + v := NewResponseBodyValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "/test", nil) + response := &http.Response{ + StatusCode: 200, + Header: http.Header{}, + Body: io.NopCloser(bytes.NewBufferString("")), + } + + // Should not panic even with no content in the response definition + valid, validationErrors := v.ValidateResponseBodyWithPathItem(request, response, + m.Model.Paths.PathItems.GetOrZero("/test"), "/test") + assert.True(t, valid) + assert.Empty(t, validationErrors) +} + type errorReader struct{} func (er *errorReader) Read(p []byte) (n int, err error) { diff --git a/responses/validate_headers.go b/responses/validate_headers.go index 0cdd85b..67a263d 100644 --- a/responses/validate_headers.go +++ b/responses/validate_headers.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package responses @@ -63,13 +63,19 @@ func ValidateResponseHeaders( if _, ok := locatedHeaders[strings.ToLower(name)]; !ok { keywordLocation := helpers.ConstructResponseHeaderJSONPointer(pathTemplate, request.Method, statusCode, name, "required") + specLine, specCol := 1, 0 + if low := header.GoLow(); low != nil && low.KeyNode != nil { + specLine = low.KeyNode.Line + specCol = low.KeyNode.Column + } + validationErrors = append(validationErrors, &errors.ValidationError{ ValidationType: helpers.ResponseBodyValidation, ValidationSubType: helpers.ParameterValidationHeader, Message: "Missing required header", Reason: fmt.Sprintf("Required header '%s' was not found in response", name), - SpecLine: header.GoLow().KeyNode.Line, - SpecCol: header.GoLow().KeyNode.Column, + SpecLine: specLine, + SpecCol: specCol, HowToFix: errors.HowToFixMissingHeader, RequestPath: request.URL.Path, RequestMethod: request.Method, diff --git a/responses/validate_response.go b/responses/validate_response.go index ae7cd6e..e0d4604 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -1,4 +1,4 @@ -// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package responses @@ -263,7 +263,10 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors } if er.Error != nil { // locate the violated property in the schema - located := schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation) + var located *yaml.Node + if len(renderedNode.Content) > 0 { + located = schema_validation.LocateSchemaPropertyNodeByJSONPath(renderedNode.Content[0], er.KeywordLocation) + } // extract the element specified by the instance val := instanceLocationRegex.FindStringSubmatch(er.InstanceLocation) @@ -313,9 +316,9 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors line := 1 col := 0 - if schema.GoLow().Type.KeyNode != nil { - line = schema.GoLow().Type.KeyNode.Line - col = schema.GoLow().Type.KeyNode.Column + if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil { + line = low.Type.KeyNode.Line + col = low.Type.KeyNode.Column } // add the error to the list