Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
10 KiB
324 lines
10 KiB
// Copyright 2015 go-swagger maintainers |
|
// |
|
// Licensed under the Apache License, Version 2.0 (the "License"); |
|
// you may not use this file except in compliance with the License. |
|
// You may obtain a copy of the License at |
|
// |
|
// http://www.apache.org/licenses/LICENSE-2.0 |
|
// |
|
// Unless required by applicable law or agreed to in writing, software |
|
// distributed under the License is distributed on an "AS IS" BASIS, |
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
// See the License for the specific language governing permissions and |
|
// limitations under the License. |
|
|
|
package validate |
|
|
|
// TODO: define this as package validate/internal |
|
// This must be done while keeping CI intact with all tests and test coverage |
|
|
|
import ( |
|
"reflect" |
|
"strconv" |
|
"strings" |
|
|
|
"github.com/go-openapi/errors" |
|
"github.com/go-openapi/spec" |
|
) |
|
|
|
const ( |
|
swaggerBody = "body" |
|
swaggerExample = "example" |
|
swaggerExamples = "examples" |
|
) |
|
|
|
const ( |
|
objectType = "object" |
|
arrayType = "array" |
|
stringType = "string" |
|
integerType = "integer" |
|
numberType = "number" |
|
booleanType = "boolean" |
|
fileType = "file" |
|
nullType = "null" |
|
) |
|
|
|
const ( |
|
jsonProperties = "properties" |
|
jsonItems = "items" |
|
jsonType = "type" |
|
// jsonSchema = "schema" |
|
jsonDefault = "default" |
|
) |
|
|
|
const ( |
|
stringFormatDate = "date" |
|
stringFormatDateTime = "date-time" |
|
stringFormatPassword = "password" |
|
stringFormatByte = "byte" |
|
// stringFormatBinary = "binary" |
|
stringFormatCreditCard = "creditcard" |
|
stringFormatDuration = "duration" |
|
stringFormatEmail = "email" |
|
stringFormatHexColor = "hexcolor" |
|
stringFormatHostname = "hostname" |
|
stringFormatIPv4 = "ipv4" |
|
stringFormatIPv6 = "ipv6" |
|
stringFormatISBN = "isbn" |
|
stringFormatISBN10 = "isbn10" |
|
stringFormatISBN13 = "isbn13" |
|
stringFormatMAC = "mac" |
|
stringFormatBSONObjectID = "bsonobjectid" |
|
stringFormatRGBColor = "rgbcolor" |
|
stringFormatSSN = "ssn" |
|
stringFormatURI = "uri" |
|
stringFormatUUID = "uuid" |
|
stringFormatUUID3 = "uuid3" |
|
stringFormatUUID4 = "uuid4" |
|
stringFormatUUID5 = "uuid5" |
|
|
|
integerFormatInt32 = "int32" |
|
integerFormatInt64 = "int64" |
|
integerFormatUInt32 = "uint32" |
|
integerFormatUInt64 = "uint64" |
|
|
|
numberFormatFloat32 = "float32" |
|
numberFormatFloat64 = "float64" |
|
numberFormatFloat = "float" |
|
numberFormatDouble = "double" |
|
) |
|
|
|
// Helpers available at the package level |
|
var ( |
|
pathHelp *pathHelper |
|
valueHelp *valueHelper |
|
errorHelp *errorHelper |
|
paramHelp *paramHelper |
|
responseHelp *responseHelper |
|
) |
|
|
|
type errorHelper struct { |
|
// A collection of unexported helpers for error construction |
|
} |
|
|
|
func (h *errorHelper) sErr(err errors.Error) *Result { |
|
// Builds a Result from standard errors.Error |
|
return &Result{Errors: []error{err}} |
|
} |
|
|
|
func (h *errorHelper) addPointerError(res *Result, err error, ref string, fromPath string) *Result { |
|
// Provides more context on error messages |
|
// reported by the jsoinpointer package by altering the passed Result |
|
if err != nil { |
|
res.AddErrors(cannotResolveRefMsg(fromPath, ref, err)) |
|
} |
|
return res |
|
} |
|
|
|
type pathHelper struct { |
|
// A collection of unexported helpers for path validation |
|
} |
|
|
|
func (h *pathHelper) stripParametersInPath(path string) string { |
|
// Returns a path stripped from all path parameters, with multiple or trailing slashes removed. |
|
// |
|
// Stripping is performed on a slash-separated basis, e.g '/a{/b}' remains a{/b} and not /a. |
|
// - Trailing "/" make a difference, e.g. /a/ !~ /a (ex: canary/bitbucket.org/swagger.json) |
|
// - presence or absence of a parameter makes a difference, e.g. /a/{log} !~ /a/ (ex: canary/kubernetes/swagger.json) |
|
|
|
// Regexp to extract parameters from path, with surrounding {}. |
|
// NOTE: important non-greedy modifier |
|
rexParsePathParam := mustCompileRegexp(`{[^{}]+?}`) |
|
strippedSegments := []string{} |
|
|
|
for _, segment := range strings.Split(path, "/") { |
|
strippedSegments = append(strippedSegments, rexParsePathParam.ReplaceAllString(segment, "X")) |
|
} |
|
return strings.Join(strippedSegments, "/") |
|
} |
|
|
|
func (h *pathHelper) extractPathParams(path string) (params []string) { |
|
// Extracts all params from a path, with surrounding "{}" |
|
rexParsePathParam := mustCompileRegexp(`{[^{}]+?}`) |
|
|
|
for _, segment := range strings.Split(path, "/") { |
|
for _, v := range rexParsePathParam.FindAllStringSubmatch(segment, -1) { |
|
params = append(params, v...) |
|
} |
|
} |
|
return |
|
} |
|
|
|
type valueHelper struct { |
|
// A collection of unexported helpers for value validation |
|
} |
|
|
|
func (h *valueHelper) asInt64(val interface{}) int64 { |
|
// Number conversion function for int64, without error checking |
|
// (implements an implicit type upgrade). |
|
v := reflect.ValueOf(val) |
|
switch v.Kind() { |
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
return v.Int() |
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
|
return int64(v.Uint()) |
|
case reflect.Float32, reflect.Float64: |
|
return int64(v.Float()) |
|
default: |
|
// panic("Non numeric value in asInt64()") |
|
return 0 |
|
} |
|
} |
|
|
|
func (h *valueHelper) asUint64(val interface{}) uint64 { |
|
// Number conversion function for uint64, without error checking |
|
// (implements an implicit type upgrade). |
|
v := reflect.ValueOf(val) |
|
switch v.Kind() { |
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
return uint64(v.Int()) |
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
|
return v.Uint() |
|
case reflect.Float32, reflect.Float64: |
|
return uint64(v.Float()) |
|
default: |
|
// panic("Non numeric value in asUint64()") |
|
return 0 |
|
} |
|
} |
|
|
|
// Same for unsigned floats |
|
func (h *valueHelper) asFloat64(val interface{}) float64 { |
|
// Number conversion function for float64, without error checking |
|
// (implements an implicit type upgrade). |
|
v := reflect.ValueOf(val) |
|
switch v.Kind() { |
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
return float64(v.Int()) |
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
|
return float64(v.Uint()) |
|
case reflect.Float32, reflect.Float64: |
|
return v.Float() |
|
default: |
|
// panic("Non numeric value in asFloat64()") |
|
return 0 |
|
} |
|
} |
|
|
|
type paramHelper struct { |
|
// A collection of unexported helpers for parameters resolution |
|
} |
|
|
|
func (h *paramHelper) safeExpandedParamsFor(path, method, operationID string, res *Result, s *SpecValidator) (params []spec.Parameter) { |
|
operation, ok := s.analyzer.OperationFor(method, path) |
|
if ok { |
|
// expand parameters first if necessary |
|
resolvedParams := []spec.Parameter{} |
|
for _, ppr := range operation.Parameters { |
|
resolvedParam, red := h.resolveParam(path, method, operationID, &ppr, s) //#nosec |
|
res.Merge(red) |
|
if resolvedParam != nil { |
|
resolvedParams = append(resolvedParams, *resolvedParam) |
|
} |
|
} |
|
// remove params with invalid expansion from Slice |
|
operation.Parameters = resolvedParams |
|
|
|
for _, ppr := range s.analyzer.SafeParamsFor(method, path, |
|
func(p spec.Parameter, err error) bool { |
|
// since params have already been expanded, there are few causes for error |
|
res.AddErrors(someParametersBrokenMsg(path, method, operationID)) |
|
// original error from analyzer |
|
res.AddErrors(err) |
|
return true |
|
}) { |
|
params = append(params, ppr) |
|
} |
|
} |
|
return |
|
} |
|
|
|
func (h *paramHelper) resolveParam(path, method, operationID string, param *spec.Parameter, s *SpecValidator) (*spec.Parameter, *Result) { |
|
// Ensure parameter is expanded |
|
var err error |
|
res := new(Result) |
|
isRef := param.Ref.String() != "" |
|
if s.spec.SpecFilePath() == "" { |
|
err = spec.ExpandParameterWithRoot(param, s.spec.Spec(), nil) |
|
} else { |
|
err = spec.ExpandParameter(param, s.spec.SpecFilePath()) |
|
|
|
} |
|
if err != nil { // Safeguard |
|
// NOTE: we may enter enter here when the whole parameter is an unresolved $ref |
|
refPath := strings.Join([]string{"\"" + path + "\"", method}, ".") |
|
errorHelp.addPointerError(res, err, param.Ref.String(), refPath) |
|
return nil, res |
|
} |
|
res.Merge(h.checkExpandedParam(param, param.Name, param.In, operationID, isRef)) |
|
return param, res |
|
} |
|
|
|
func (h *paramHelper) checkExpandedParam(pr *spec.Parameter, path, in, operation string, isRef bool) *Result { |
|
// Secure parameter structure after $ref resolution |
|
res := new(Result) |
|
simpleZero := spec.SimpleSchema{} |
|
// Try to explain why... best guess |
|
switch { |
|
case pr.In == swaggerBody && (pr.SimpleSchema != simpleZero && pr.SimpleSchema.Type != objectType): |
|
if isRef { |
|
// Most likely, a $ref with a sibling is an unwanted situation: in itself this is a warning... |
|
// but we detect it because of the following error: |
|
// schema took over Parameter for an unexplained reason |
|
res.AddWarnings(refShouldNotHaveSiblingsMsg(path, operation)) |
|
} |
|
res.AddErrors(invalidParameterDefinitionMsg(path, in, operation)) |
|
case pr.In != swaggerBody && pr.Schema != nil: |
|
if isRef { |
|
res.AddWarnings(refShouldNotHaveSiblingsMsg(path, operation)) |
|
} |
|
res.AddErrors(invalidParameterDefinitionAsSchemaMsg(path, in, operation)) |
|
case (pr.In == swaggerBody && pr.Schema == nil) || (pr.In != swaggerBody && pr.SimpleSchema == simpleZero): |
|
// Other unexpected mishaps |
|
res.AddErrors(invalidParameterDefinitionMsg(path, in, operation)) |
|
} |
|
return res |
|
} |
|
|
|
type responseHelper struct { |
|
// A collection of unexported helpers for response resolution |
|
} |
|
|
|
func (r *responseHelper) expandResponseRef( |
|
response *spec.Response, |
|
path string, s *SpecValidator) (*spec.Response, *Result) { |
|
// Ensure response is expanded |
|
var err error |
|
res := new(Result) |
|
if s.spec.SpecFilePath() == "" { |
|
// there is no physical document to resolve $ref in response |
|
err = spec.ExpandResponseWithRoot(response, s.spec.Spec(), nil) |
|
} else { |
|
err = spec.ExpandResponse(response, s.spec.SpecFilePath()) |
|
} |
|
if err != nil { // Safeguard |
|
// NOTE: we may enter here when the whole response is an unresolved $ref. |
|
errorHelp.addPointerError(res, err, response.Ref.String(), path) |
|
return nil, res |
|
} |
|
return response, res |
|
} |
|
|
|
func (r *responseHelper) responseMsgVariants( |
|
responseType string, |
|
responseCode int) (responseName, responseCodeAsStr string) { |
|
// Path variants for messages |
|
if responseType == jsonDefault { |
|
responseCodeAsStr = jsonDefault |
|
responseName = "default response" |
|
} else { |
|
responseCodeAsStr = strconv.Itoa(responseCode) |
|
responseName = "response " + responseCodeAsStr |
|
} |
|
return |
|
}
|
|
|