mirror of
1
Fork 0
gotosocial/vendor/github.com/go-openapi/validate/object_validator.go

432 lines
12 KiB
Go

// 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
import (
"fmt"
"reflect"
"strings"
"github.com/go-openapi/errors"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
)
type objectValidator struct {
Path string
In string
MaxProperties *int64
MinProperties *int64
Required []string
Properties map[string]spec.Schema
AdditionalProperties *spec.SchemaOrBool
PatternProperties map[string]spec.Schema
Root interface{}
KnownFormats strfmt.Registry
Options *SchemaValidatorOptions
splitPath []string
}
func newObjectValidator(path, in string,
maxProperties, minProperties *int64, required []string, properties spec.SchemaProperties,
additionalProperties *spec.SchemaOrBool, patternProperties spec.SchemaProperties,
root interface{}, formats strfmt.Registry, opts *SchemaValidatorOptions) *objectValidator {
if opts == nil {
opts = new(SchemaValidatorOptions)
}
var v *objectValidator
if opts.recycleValidators {
v = pools.poolOfObjectValidators.BorrowValidator()
} else {
v = new(objectValidator)
}
v.Path = path
v.In = in
v.MaxProperties = maxProperties
v.MinProperties = minProperties
v.Required = required
v.Properties = properties
v.AdditionalProperties = additionalProperties
v.PatternProperties = patternProperties
v.Root = root
v.KnownFormats = formats
v.Options = opts
v.splitPath = strings.Split(v.Path, ".")
return v
}
func (o *objectValidator) SetPath(path string) {
o.Path = path
o.splitPath = strings.Split(path, ".")
}
func (o *objectValidator) Applies(source interface{}, kind reflect.Kind) bool {
// TODO: this should also work for structs
// there is a problem in the type validator where it will be unhappy about null values
// so that requires more testing
_, isSchema := source.(*spec.Schema)
return isSchema && (kind == reflect.Map || kind == reflect.Struct)
}
func (o *objectValidator) isProperties() bool {
p := o.splitPath
return len(p) > 1 && p[len(p)-1] == jsonProperties && p[len(p)-2] != jsonProperties
}
func (o *objectValidator) isDefault() bool {
p := o.splitPath
return len(p) > 1 && p[len(p)-1] == jsonDefault && p[len(p)-2] != jsonDefault
}
func (o *objectValidator) isExample() bool {
p := o.splitPath
return len(p) > 1 && (p[len(p)-1] == swaggerExample || p[len(p)-1] == swaggerExamples) && p[len(p)-2] != swaggerExample
}
func (o *objectValidator) checkArrayMustHaveItems(res *Result, val map[string]interface{}) {
// for swagger 2.0 schemas, there is an additional constraint to have array items defined explicitly.
// with pure jsonschema draft 4, one may have arrays with undefined items (i.e. any type).
if val == nil {
return
}
t, typeFound := val[jsonType]
if !typeFound {
return
}
tpe, isString := t.(string)
if !isString || tpe != arrayType {
return
}
item, itemsKeyFound := val[jsonItems]
if itemsKeyFound {
return
}
res.AddErrors(errors.Required(jsonItems, o.Path, item))
}
func (o *objectValidator) checkItemsMustBeTypeArray(res *Result, val map[string]interface{}) {
if val == nil {
return
}
if o.isProperties() || o.isDefault() || o.isExample() {
return
}
_, itemsKeyFound := val[jsonItems]
if !itemsKeyFound {
return
}
t, typeFound := val[jsonType]
if !typeFound {
// there is no type
res.AddErrors(errors.Required(jsonType, o.Path, t))
}
if tpe, isString := t.(string); !isString || tpe != arrayType {
res.AddErrors(errors.InvalidType(o.Path, o.In, arrayType, nil))
}
}
func (o *objectValidator) precheck(res *Result, val map[string]interface{}) {
if o.Options.EnableArrayMustHaveItemsCheck {
o.checkArrayMustHaveItems(res, val)
}
if o.Options.EnableObjectArrayTypeCheck {
o.checkItemsMustBeTypeArray(res, val)
}
}
func (o *objectValidator) Validate(data interface{}) *Result {
if o.Options.recycleValidators {
defer func() {
o.redeem()
}()
}
var val map[string]interface{}
if data != nil {
var ok bool
val, ok = data.(map[string]interface{})
if !ok {
return errorHelp.sErr(invalidObjectMsg(o.Path, o.In), o.Options.recycleResult)
}
}
numKeys := int64(len(val))
if o.MinProperties != nil && numKeys < *o.MinProperties {
return errorHelp.sErr(errors.TooFewProperties(o.Path, o.In, *o.MinProperties), o.Options.recycleResult)
}
if o.MaxProperties != nil && numKeys > *o.MaxProperties {
return errorHelp.sErr(errors.TooManyProperties(o.Path, o.In, *o.MaxProperties), o.Options.recycleResult)
}
var res *Result
if o.Options.recycleResult {
res = pools.poolOfResults.BorrowResult()
} else {
res = new(Result)
}
o.precheck(res, val)
// check validity of field names
if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows {
// Case: additionalProperties: false
o.validateNoAdditionalProperties(val, res)
} else {
// Cases: empty additionalProperties (implying: true), or additionalProperties: true, or additionalProperties: { <<schema>> }
o.validateAdditionalProperties(val, res)
}
o.validatePropertiesSchema(val, res)
// Check patternProperties
// TODO: it looks like we have done that twice in many cases
for key, value := range val {
_, regularProperty := o.Properties[key]
matched, _, patterns := o.validatePatternProperty(key, value, res) // applies to regular properties as well
if regularProperty || !matched {
continue
}
for _, pName := range patterns {
if v, ok := o.PatternProperties[pName]; ok {
r := newSchemaValidator(&v, o.Root, o.Path+"."+key, o.KnownFormats, o.Options).Validate(value)
res.mergeForField(data.(map[string]interface{}), key, r)
}
}
}
return res
}
func (o *objectValidator) validateNoAdditionalProperties(val map[string]interface{}, res *Result) {
for k := range val {
if k == "$schema" || k == "id" {
// special properties "$schema" and "id" are ignored
continue
}
_, regularProperty := o.Properties[k]
if regularProperty {
continue
}
matched := false
for pk := range o.PatternProperties {
re, err := compileRegexp(pk)
if err != nil {
continue
}
if matches := re.MatchString(k); matches {
matched = true
break
}
}
if matched {
continue
}
res.AddErrors(errors.PropertyNotAllowed(o.Path, o.In, k))
// BUG(fredbi): This section should move to a part dedicated to spec validation as
// it will conflict with regular schemas where a property "headers" is defined.
//
// Croaks a more explicit message on top of the standard one
// on some recognized cases.
//
// NOTE: edge cases with invalid type assertion are simply ignored here.
// NOTE: prefix your messages here by "IMPORTANT!" so there are not filtered
// by higher level callers (the IMPORTANT! tag will be eventually
// removed).
if k != "headers" || val[k] == nil {
continue
}
// $ref is forbidden in header
headers, mapOk := val[k].(map[string]interface{})
if !mapOk {
continue
}
for headerKey, headerBody := range headers {
if headerBody == nil {
continue
}
headerSchema, mapOfMapOk := headerBody.(map[string]interface{})
if !mapOfMapOk {
continue
}
_, found := headerSchema["$ref"]
if !found {
continue
}
refString, stringOk := headerSchema["$ref"].(string)
if !stringOk {
continue
}
msg := strings.Join([]string{", one may not use $ref=\":", refString, "\""}, "")
res.AddErrors(refNotAllowedInHeaderMsg(o.Path, headerKey, msg))
/*
case "$ref":
if val[k] != nil {
// TODO: check context of that ref: warn about siblings, check against invalid context
}
*/
}
}
}
func (o *objectValidator) validateAdditionalProperties(val map[string]interface{}, res *Result) {
for key, value := range val {
_, regularProperty := o.Properties[key]
if regularProperty {
continue
}
// Validates property against "patternProperties" if applicable
// BUG(fredbi): succeededOnce is always false
// NOTE: how about regular properties which do not match patternProperties?
matched, succeededOnce, _ := o.validatePatternProperty(key, value, res)
if matched || succeededOnce {
continue
}
if o.AdditionalProperties == nil || o.AdditionalProperties.Schema == nil {
continue
}
// Cases: properties which are not regular properties and have not been matched by the PatternProperties validator
// AdditionalProperties as Schema
r := newSchemaValidator(o.AdditionalProperties.Schema, o.Root, o.Path+"."+key, o.KnownFormats, o.Options).Validate(value)
res.mergeForField(val, key, r)
}
// Valid cases: additionalProperties: true or undefined
}
func (o *objectValidator) validatePropertiesSchema(val map[string]interface{}, res *Result) {
createdFromDefaults := map[string]struct{}{}
// Property types:
// - regular Property
pSchema := pools.poolOfSchemas.BorrowSchema() // recycle a spec.Schema object which lifespan extends only to the validation of properties
defer func() {
pools.poolOfSchemas.RedeemSchema(pSchema)
}()
for pName := range o.Properties {
*pSchema = o.Properties[pName]
var rName string
if o.Path == "" {
rName = pName
} else {
rName = o.Path + "." + pName
}
// Recursively validates each property against its schema
v, ok := val[pName]
if ok {
r := newSchemaValidator(pSchema, o.Root, rName, o.KnownFormats, o.Options).Validate(v)
res.mergeForField(val, pName, r)
continue
}
if pSchema.Default != nil {
// if a default value is defined, creates the property from defaults
// NOTE: JSON schema does not enforce default values to be valid against schema. Swagger does.
createdFromDefaults[pName] = struct{}{}
if !o.Options.skipSchemataResult {
res.addPropertySchemata(val, pName, pSchema) // this shallow-clones the content of the pSchema pointer
}
}
}
if len(o.Required) == 0 {
return
}
// Check required properties
for _, k := range o.Required {
v, ok := val[k]
if ok {
continue
}
_, isCreatedFromDefaults := createdFromDefaults[k]
if isCreatedFromDefaults {
continue
}
res.AddErrors(errors.Required(fmt.Sprintf("%s.%s", o.Path, k), o.In, v))
}
}
// TODO: succeededOnce is not used anywhere
func (o *objectValidator) validatePatternProperty(key string, value interface{}, result *Result) (bool, bool, []string) {
if len(o.PatternProperties) == 0 {
return false, false, nil
}
matched := false
succeededOnce := false
patterns := make([]string, 0, len(o.PatternProperties))
schema := pools.poolOfSchemas.BorrowSchema()
defer func() {
pools.poolOfSchemas.RedeemSchema(schema)
}()
for k := range o.PatternProperties {
re, err := compileRegexp(k)
if err != nil {
continue
}
match := re.MatchString(key)
if !match {
continue
}
*schema = o.PatternProperties[k]
patterns = append(patterns, k)
matched = true
validator := newSchemaValidator(schema, o.Root, fmt.Sprintf("%s.%s", o.Path, key), o.KnownFormats, o.Options)
res := validator.Validate(value)
result.Merge(res)
}
return matched, succeededOnce, patterns
}
func (o *objectValidator) redeem() {
pools.poolOfObjectValidators.RedeemValidator(o)
}