See the comment in tools.go, I cannot fully explain what's happening here. Somehow, Go 1.14 wants to use the vendored version of oapi-codegen but without this file, oapi-codegen isn't vendored so the generation fails. Signed-off-by: Ondřej Budai <ondrej@budai.cz>
1235 lines
29 KiB
Go
1235 lines
29 KiB
Go
package openapi3
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"regexp"
|
|
"strconv"
|
|
"unicode/utf16"
|
|
|
|
"github.com/getkin/kin-openapi/jsoninfo"
|
|
)
|
|
|
|
var (
|
|
// SchemaErrorDetailsDisabled disables printing of details about schema errors.
|
|
SchemaErrorDetailsDisabled = false
|
|
|
|
//SchemaFormatValidationDisabled disables validation of schema type formats.
|
|
SchemaFormatValidationDisabled = false
|
|
|
|
errSchema = errors.New("Input does not match the schema")
|
|
|
|
ErrSchemaInputNaN = errors.New("NaN is not allowed")
|
|
ErrSchemaInputInf = errors.New("Inf is not allowed")
|
|
)
|
|
|
|
// Float64Ptr is a helper for defining OpenAPI schemas.
|
|
func Float64Ptr(value float64) *float64 {
|
|
return &value
|
|
}
|
|
|
|
// BoolPtr is a helper for defining OpenAPI schemas.
|
|
func BoolPtr(value bool) *bool {
|
|
return &value
|
|
}
|
|
|
|
// Int64Ptr is a helper for defining OpenAPI schemas.
|
|
func Int64Ptr(value int64) *int64 {
|
|
return &value
|
|
}
|
|
|
|
// Uint64Ptr is a helper for defining OpenAPI schemas.
|
|
func Uint64Ptr(value uint64) *uint64 {
|
|
return &value
|
|
}
|
|
|
|
// Schema is specified by OpenAPI/Swagger 3.0 standard.
|
|
type Schema struct {
|
|
ExtensionProps
|
|
|
|
OneOf []*SchemaRef `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
|
|
AnyOf []*SchemaRef `json:"anyOf,omitempty" yaml:"anyOf,omitempty"`
|
|
AllOf []*SchemaRef `json:"allOf,omitempty" yaml:"allOf,omitempty"`
|
|
Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"`
|
|
Type string `json:"type,omitempty" yaml:"type,omitempty"`
|
|
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
|
Format string `json:"format,omitempty" yaml:"format,omitempty"`
|
|
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
|
Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"`
|
|
Default interface{} `json:"default,omitempty" yaml:"default,omitempty"`
|
|
Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
|
|
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
|
|
|
|
// Object-related, here for struct compactness
|
|
AdditionalPropertiesAllowed *bool `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"`
|
|
// Array-related, here for struct compactness
|
|
UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
|
|
// Number-related, here for struct compactness
|
|
ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"`
|
|
ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"`
|
|
// Properties
|
|
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
|
|
ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"`
|
|
WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"`
|
|
XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"`
|
|
|
|
// Number
|
|
Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
|
|
Max *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"`
|
|
MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"`
|
|
|
|
// String
|
|
MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"`
|
|
MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"`
|
|
Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"`
|
|
compiledPattern *compiledPattern
|
|
|
|
// Array
|
|
MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"`
|
|
MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"`
|
|
Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"`
|
|
|
|
// Object
|
|
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
|
|
Properties map[string]*SchemaRef `json:"properties,omitempty" yaml:"properties,omitempty"`
|
|
MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
|
|
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
|
|
AdditionalProperties *SchemaRef `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"`
|
|
Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
|
|
}
|
|
|
|
func NewSchema() *Schema {
|
|
return &Schema{}
|
|
}
|
|
|
|
func (schema *Schema) MarshalJSON() ([]byte, error) {
|
|
return jsoninfo.MarshalStrictStruct(schema)
|
|
}
|
|
|
|
func (schema *Schema) UnmarshalJSON(data []byte) error {
|
|
return jsoninfo.UnmarshalStrictStruct(data, schema)
|
|
}
|
|
|
|
func (schema *Schema) NewRef() *SchemaRef {
|
|
return &SchemaRef{
|
|
Value: schema,
|
|
}
|
|
}
|
|
|
|
func NewOneOfSchema(schemas ...*Schema) *Schema {
|
|
refs := make([]*SchemaRef, 0, len(schemas))
|
|
for _, schema := range schemas {
|
|
refs = append(refs, &SchemaRef{Value: schema})
|
|
}
|
|
return &Schema{
|
|
OneOf: refs,
|
|
}
|
|
}
|
|
|
|
func NewAnyOfSchema(schemas ...*Schema) *Schema {
|
|
refs := make([]*SchemaRef, 0, len(schemas))
|
|
for _, schema := range schemas {
|
|
refs = append(refs, &SchemaRef{Value: schema})
|
|
}
|
|
return &Schema{
|
|
AnyOf: refs,
|
|
}
|
|
}
|
|
|
|
func NewAllOfSchema(schemas ...*Schema) *Schema {
|
|
refs := make([]*SchemaRef, 0, len(schemas))
|
|
for _, schema := range schemas {
|
|
refs = append(refs, &SchemaRef{Value: schema})
|
|
}
|
|
return &Schema{
|
|
AllOf: refs,
|
|
}
|
|
}
|
|
|
|
func NewBoolSchema() *Schema {
|
|
return &Schema{
|
|
Type: "boolean",
|
|
}
|
|
}
|
|
|
|
func NewFloat64Schema() *Schema {
|
|
return &Schema{
|
|
Type: "number",
|
|
}
|
|
}
|
|
|
|
func NewIntegerSchema() *Schema {
|
|
return &Schema{
|
|
Type: "integer",
|
|
}
|
|
}
|
|
|
|
func NewInt32Schema() *Schema {
|
|
return &Schema{
|
|
Type: "integer",
|
|
Format: "int32",
|
|
}
|
|
}
|
|
|
|
func NewInt64Schema() *Schema {
|
|
return &Schema{
|
|
Type: "integer",
|
|
Format: "int64",
|
|
}
|
|
}
|
|
|
|
func NewStringSchema() *Schema {
|
|
return &Schema{
|
|
Type: "string",
|
|
}
|
|
}
|
|
|
|
func NewDateTimeSchema() *Schema {
|
|
return &Schema{
|
|
Type: "string",
|
|
Format: "date-time",
|
|
}
|
|
}
|
|
|
|
func NewUUIDSchema() *Schema {
|
|
return &Schema{
|
|
Type: "string",
|
|
Format: "uuid",
|
|
}
|
|
}
|
|
|
|
func NewBytesSchema() *Schema {
|
|
return &Schema{
|
|
Type: "string",
|
|
Format: "byte",
|
|
}
|
|
}
|
|
|
|
func NewArraySchema() *Schema {
|
|
return &Schema{
|
|
Type: "array",
|
|
}
|
|
}
|
|
|
|
func NewObjectSchema() *Schema {
|
|
return &Schema{
|
|
Type: "object",
|
|
Properties: make(map[string]*SchemaRef),
|
|
}
|
|
}
|
|
|
|
type compiledPattern struct {
|
|
Regexp *regexp.Regexp
|
|
ErrReason string
|
|
}
|
|
|
|
func (schema *Schema) WithNullable() *Schema {
|
|
schema.Nullable = true
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMin(value float64) *Schema {
|
|
schema.Min = &value
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMax(value float64) *Schema {
|
|
schema.Max = &value
|
|
return schema
|
|
}
|
|
func (schema *Schema) WithExclusiveMin(value bool) *Schema {
|
|
schema.ExclusiveMin = value
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithExclusiveMax(value bool) *Schema {
|
|
schema.ExclusiveMax = value
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithEnum(values ...interface{}) *Schema {
|
|
schema.Enum = values
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithDefault(defaultValue interface{}) *Schema {
|
|
schema.Default = defaultValue
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithFormat(value string) *Schema {
|
|
schema.Format = value
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithLength(i int64) *Schema {
|
|
n := uint64(i)
|
|
schema.MinLength = n
|
|
schema.MaxLength = &n
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMinLength(i int64) *Schema {
|
|
n := uint64(i)
|
|
schema.MinLength = n
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMaxLength(i int64) *Schema {
|
|
n := uint64(i)
|
|
schema.MaxLength = &n
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithLengthDecodedBase64(i int64) *Schema {
|
|
n := uint64(i)
|
|
v := (n*8 + 5) / 6
|
|
schema.MinLength = v
|
|
schema.MaxLength = &v
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMinLengthDecodedBase64(i int64) *Schema {
|
|
n := uint64(i)
|
|
schema.MinLength = (n*8 + 5) / 6
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMaxLengthDecodedBase64(i int64) *Schema {
|
|
n := uint64(i)
|
|
schema.MinLength = (n*8 + 5) / 6
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithPattern(pattern string) *Schema {
|
|
schema.Pattern = pattern
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithItems(value *Schema) *Schema {
|
|
schema.Items = &SchemaRef{
|
|
Value: value,
|
|
}
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMinItems(i int64) *Schema {
|
|
n := uint64(i)
|
|
schema.MinItems = n
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMaxItems(i int64) *Schema {
|
|
n := uint64(i)
|
|
schema.MaxItems = &n
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithUniqueItems(unique bool) *Schema {
|
|
schema.UniqueItems = unique
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithProperty(name string, propertySchema *Schema) *Schema {
|
|
return schema.WithPropertyRef(name, &SchemaRef{
|
|
Value: propertySchema,
|
|
})
|
|
}
|
|
|
|
func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema {
|
|
properties := schema.Properties
|
|
if properties == nil {
|
|
properties = make(map[string]*SchemaRef)
|
|
schema.Properties = properties
|
|
}
|
|
properties[name] = ref
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithProperties(properties map[string]*Schema) *Schema {
|
|
result := make(map[string]*SchemaRef, len(properties))
|
|
for k, v := range properties {
|
|
result[k] = &SchemaRef{
|
|
Value: v,
|
|
}
|
|
}
|
|
schema.Properties = result
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMinProperties(i int64) *Schema {
|
|
n := uint64(i)
|
|
schema.MinProps = n
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithMaxProperties(i int64) *Schema {
|
|
n := uint64(i)
|
|
schema.MaxProps = &n
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithAnyAdditionalProperties() *Schema {
|
|
schema.AdditionalProperties = nil
|
|
t := true
|
|
schema.AdditionalPropertiesAllowed = &t
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema {
|
|
if v == nil {
|
|
schema.AdditionalProperties = nil
|
|
} else {
|
|
schema.AdditionalProperties = &SchemaRef{
|
|
Value: v,
|
|
}
|
|
}
|
|
return schema
|
|
}
|
|
|
|
func (schema *Schema) IsEmpty() bool {
|
|
if schema.Type != "" || schema.Format != "" || len(schema.Enum) != 0 ||
|
|
schema.UniqueItems || schema.ExclusiveMin || schema.ExclusiveMax ||
|
|
!schema.Nullable ||
|
|
schema.Min != nil || schema.Max != nil || schema.MultipleOf != nil ||
|
|
schema.MinLength != 0 || schema.MaxLength != nil || schema.Pattern != "" ||
|
|
schema.MinItems != 0 || schema.MaxItems != nil ||
|
|
len(schema.Required) != 0 ||
|
|
schema.MinProps != 0 || schema.MaxProps != nil {
|
|
return false
|
|
}
|
|
if n := schema.Not; n != nil && !n.Value.IsEmpty() {
|
|
return false
|
|
}
|
|
if ap := schema.AdditionalProperties; ap != nil && !ap.Value.IsEmpty() {
|
|
return false
|
|
}
|
|
if apa := schema.AdditionalPropertiesAllowed; apa != nil && !*apa {
|
|
return false
|
|
}
|
|
if items := schema.Items; items != nil && !items.Value.IsEmpty() {
|
|
return false
|
|
}
|
|
for _, s := range schema.Properties {
|
|
if !s.Value.IsEmpty() {
|
|
return false
|
|
}
|
|
}
|
|
for _, s := range schema.OneOf {
|
|
if !s.Value.IsEmpty() {
|
|
return false
|
|
}
|
|
}
|
|
for _, s := range schema.AnyOf {
|
|
if !s.Value.IsEmpty() {
|
|
return false
|
|
}
|
|
}
|
|
for _, s := range schema.AllOf {
|
|
if !s.Value.IsEmpty() {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (schema *Schema) Validate(c context.Context) error {
|
|
return schema.validate(c, []*Schema{})
|
|
}
|
|
|
|
func (schema *Schema) validate(c context.Context, stack []*Schema) (err error) {
|
|
for _, existing := range stack {
|
|
if existing == schema {
|
|
return
|
|
}
|
|
}
|
|
stack = append(stack, schema)
|
|
|
|
for _, item := range schema.OneOf {
|
|
v := item.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(item.Ref)
|
|
}
|
|
if err = v.validate(c, stack); err == nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, item := range schema.AnyOf {
|
|
v := item.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(item.Ref)
|
|
}
|
|
if err = v.validate(c, stack); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, item := range schema.AllOf {
|
|
v := item.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(item.Ref)
|
|
}
|
|
if err = v.validate(c, stack); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if ref := schema.Not; ref != nil {
|
|
v := ref.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(ref.Ref)
|
|
}
|
|
if err = v.validate(c, stack); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
schemaType := schema.Type
|
|
switch schemaType {
|
|
case "":
|
|
case "boolean":
|
|
case "number":
|
|
if format := schema.Format; len(format) > 0 {
|
|
switch format {
|
|
case "float", "double":
|
|
default:
|
|
if !SchemaFormatValidationDisabled {
|
|
return unsupportedFormat(format)
|
|
}
|
|
}
|
|
}
|
|
case "integer":
|
|
if format := schema.Format; len(format) > 0 {
|
|
switch format {
|
|
case "int32", "int64":
|
|
default:
|
|
if !SchemaFormatValidationDisabled {
|
|
return unsupportedFormat(format)
|
|
}
|
|
}
|
|
}
|
|
case "string":
|
|
if format := schema.Format; len(format) > 0 {
|
|
switch format {
|
|
// Supported by OpenAPIv3.0.1:
|
|
case "byte", "binary", "date", "date-time", "password":
|
|
// In JSON Draft-07 (not validated yet though):
|
|
case "regex":
|
|
case "time", "email", "idn-email":
|
|
case "hostname", "idn-hostname", "ipv4", "ipv6":
|
|
case "uri", "uri-reference", "iri", "iri-reference", "uri-template":
|
|
case "json-pointer", "relative-json-pointer":
|
|
default:
|
|
// Try to check for custom defined formats
|
|
if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled {
|
|
return unsupportedFormat(format)
|
|
}
|
|
}
|
|
}
|
|
case "array":
|
|
if schema.Items == nil {
|
|
return errors.New("When schema type is 'array', schema 'items' must be non-null")
|
|
}
|
|
case "object":
|
|
default:
|
|
return fmt.Errorf("Unsupported 'type' value '%s'", schemaType)
|
|
}
|
|
|
|
if ref := schema.Items; ref != nil {
|
|
v := ref.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(ref.Ref)
|
|
}
|
|
if err = v.validate(c, stack); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, ref := range schema.Properties {
|
|
v := ref.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(ref.Ref)
|
|
}
|
|
if err = v.validate(c, stack); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if ref := schema.AdditionalProperties; ref != nil {
|
|
v := ref.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(ref.Ref)
|
|
}
|
|
if err = v.validate(c, stack); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (schema *Schema) IsMatching(value interface{}) bool {
|
|
return schema.visitJSON(value, true) == nil
|
|
}
|
|
|
|
func (schema *Schema) IsMatchingJSONBoolean(value bool) bool {
|
|
return schema.visitJSON(value, true) == nil
|
|
}
|
|
|
|
func (schema *Schema) IsMatchingJSONNumber(value float64) bool {
|
|
return schema.visitJSON(value, true) == nil
|
|
}
|
|
|
|
func (schema *Schema) IsMatchingJSONString(value string) bool {
|
|
return schema.visitJSON(value, true) == nil
|
|
}
|
|
|
|
func (schema *Schema) IsMatchingJSONArray(value []interface{}) bool {
|
|
return schema.visitJSON(value, true) == nil
|
|
}
|
|
|
|
func (schema *Schema) IsMatchingJSONObject(value map[string]interface{}) bool {
|
|
return schema.visitJSON(value, true) == nil
|
|
}
|
|
|
|
func (schema *Schema) VisitJSON(value interface{}) error {
|
|
return schema.visitJSON(value, false)
|
|
}
|
|
|
|
func (schema *Schema) visitJSON(value interface{}, fast bool) (err error) {
|
|
switch value := value.(type) {
|
|
case nil:
|
|
return schema.visitJSONNull(fast)
|
|
case float64:
|
|
if math.IsNaN(value) {
|
|
return ErrSchemaInputNaN
|
|
}
|
|
if math.IsInf(value, 0) {
|
|
return ErrSchemaInputInf
|
|
}
|
|
}
|
|
|
|
if schema.IsEmpty() {
|
|
return
|
|
}
|
|
if err = schema.visitSetOperations(value, fast); err != nil {
|
|
return
|
|
}
|
|
|
|
switch value := value.(type) {
|
|
case nil:
|
|
return schema.visitJSONNull(fast)
|
|
case bool:
|
|
return schema.visitJSONBoolean(value, fast)
|
|
case float64:
|
|
return schema.visitJSONNumber(value, fast)
|
|
case string:
|
|
return schema.visitJSONString(value, fast)
|
|
case []interface{}:
|
|
return schema.visitJSONArray(value, fast)
|
|
case map[string]interface{}:
|
|
return schema.visitJSONObject(value, fast)
|
|
default:
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "type",
|
|
Reason: fmt.Sprintf("Not a JSON value: %T", value),
|
|
}
|
|
}
|
|
}
|
|
|
|
func (schema *Schema) visitSetOperations(value interface{}, fast bool) (err error) {
|
|
if enum := schema.Enum; len(enum) != 0 {
|
|
for _, v := range enum {
|
|
if value == v {
|
|
return
|
|
}
|
|
}
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "enum",
|
|
Reason: "JSON value is not one of the allowed values",
|
|
}
|
|
}
|
|
|
|
if ref := schema.Not; ref != nil {
|
|
v := ref.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(ref.Ref)
|
|
}
|
|
if err := v.visitJSON(value, true); err == nil {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "not",
|
|
}
|
|
}
|
|
}
|
|
|
|
if v := schema.OneOf; len(v) > 0 {
|
|
ok := 0
|
|
for _, item := range v {
|
|
v := item.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(item.Ref)
|
|
}
|
|
if err := v.visitJSON(value, true); err == nil {
|
|
ok++
|
|
}
|
|
}
|
|
if ok != 1 {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "oneOf",
|
|
}
|
|
}
|
|
}
|
|
|
|
if v := schema.AnyOf; len(v) > 0 {
|
|
ok := false
|
|
for _, item := range v {
|
|
v := item.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(item.Ref)
|
|
}
|
|
if err := v.visitJSON(value, true); err == nil {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "anyOf",
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, item := range schema.AllOf {
|
|
v := item.Value
|
|
if v == nil {
|
|
return foundUnresolvedRef(item.Ref)
|
|
}
|
|
if err := v.visitJSON(value, false); err != nil {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "allOf",
|
|
Origin: err,
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (schema *Schema) visitJSONNull(fast bool) (err error) {
|
|
if schema.Nullable {
|
|
return
|
|
}
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: nil,
|
|
Schema: schema,
|
|
SchemaField: "nullable",
|
|
Reason: "Value is not nullable",
|
|
}
|
|
}
|
|
|
|
func (schema *Schema) VisitJSONBoolean(value bool) error {
|
|
return schema.visitJSONBoolean(value, false)
|
|
}
|
|
|
|
func (schema *Schema) visitJSONBoolean(value bool, fast bool) (err error) {
|
|
if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" {
|
|
return schema.expectedType("boolean", fast)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (schema *Schema) VisitJSONNumber(value float64) error {
|
|
return schema.visitJSONNumber(value, false)
|
|
}
|
|
|
|
func (schema *Schema) visitJSONNumber(value float64, fast bool) (err error) {
|
|
schemaType := schema.Type
|
|
if schemaType == "integer" {
|
|
if bigFloat := big.NewFloat(value); !bigFloat.IsInt() {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "type",
|
|
Reason: "Value must be an integer",
|
|
}
|
|
}
|
|
} else if schemaType != "" && schemaType != "number" {
|
|
return schema.expectedType("number, integer", fast)
|
|
}
|
|
|
|
// "exclusiveMinimum"
|
|
if v := schema.ExclusiveMin; v && !(*schema.Min < value) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "exclusiveMinimum",
|
|
Reason: fmt.Sprintf("Number must be more than %g", *schema.Min),
|
|
}
|
|
}
|
|
|
|
// "exclusiveMaximum"
|
|
if v := schema.ExclusiveMax; v && !(*schema.Max > value) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "exclusiveMaximum",
|
|
Reason: fmt.Sprintf("Number must be less than %g", *schema.Max),
|
|
}
|
|
}
|
|
|
|
// "minimum"
|
|
if v := schema.Min; v != nil && !(*v <= value) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "minimum",
|
|
Reason: fmt.Sprintf("Number must be at least %g", *v),
|
|
}
|
|
}
|
|
|
|
// "maximum"
|
|
if v := schema.Max; v != nil && !(*v >= value) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "maximum",
|
|
Reason: fmt.Sprintf("Number must be most %g", *v),
|
|
}
|
|
}
|
|
|
|
// "multipleOf"
|
|
if v := schema.MultipleOf; v != nil {
|
|
// "A numeric instance is valid only if division by this keyword's
|
|
// value results in an integer."
|
|
if bigFloat := big.NewFloat(value / *v); !bigFloat.IsInt() {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "multipleOf",
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (schema *Schema) VisitJSONString(value string) error {
|
|
return schema.visitJSONString(value, false)
|
|
}
|
|
|
|
func (schema *Schema) visitJSONString(value string, fast bool) (err error) {
|
|
if schemaType := schema.Type; schemaType != "" && schemaType != "string" {
|
|
return schema.expectedType("string", fast)
|
|
}
|
|
|
|
// "minLength" and "maxLength"
|
|
minLength := schema.MinLength
|
|
maxLength := schema.MaxLength
|
|
if minLength != 0 || maxLength != nil {
|
|
// JSON schema string lengths are UTF-16, not UTF-8!
|
|
length := int64(0)
|
|
for _, r := range value {
|
|
if utf16.IsSurrogate(r) {
|
|
length += 2
|
|
} else {
|
|
length++
|
|
}
|
|
}
|
|
if minLength != 0 && length < int64(minLength) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "minLength",
|
|
Reason: fmt.Sprintf("Minimum string length is %d", minLength),
|
|
}
|
|
}
|
|
if maxLength != nil && length > int64(*maxLength) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "maxLength",
|
|
Reason: fmt.Sprintf("Maximum string length is %d", *maxLength),
|
|
}
|
|
}
|
|
}
|
|
|
|
// "format" and "pattern"
|
|
cp := schema.compiledPattern
|
|
if cp == nil {
|
|
pattern := schema.Pattern
|
|
if v := schema.Pattern; len(v) > 0 {
|
|
// Pattern
|
|
re, err := regexp.Compile(v)
|
|
if err != nil {
|
|
return fmt.Errorf("Error while compiling regular expression '%s': %v", pattern, err)
|
|
}
|
|
cp = &compiledPattern{
|
|
Regexp: re,
|
|
ErrReason: "JSON string doesn't match the regular expression '" + v + "'",
|
|
}
|
|
schema.compiledPattern = cp
|
|
} else if v := schema.Format; len(v) > 0 {
|
|
// No pattern, but does have a format
|
|
re := SchemaStringFormats[v]
|
|
if re != nil {
|
|
cp = &compiledPattern{
|
|
Regexp: re,
|
|
ErrReason: "JSON string doesn't match the format '" + v + " (regular expression `" + re.String() + "`)'",
|
|
}
|
|
schema.compiledPattern = cp
|
|
}
|
|
}
|
|
}
|
|
if cp != nil {
|
|
if !cp.Regexp.MatchString(value) {
|
|
field := "format"
|
|
if schema.Pattern != "" {
|
|
field = "pattern"
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: field,
|
|
Reason: cp.ErrReason,
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (schema *Schema) VisitJSONArray(value []interface{}) error {
|
|
return schema.visitJSONArray(value, false)
|
|
}
|
|
|
|
func (schema *Schema) visitJSONArray(value []interface{}, fast bool) (err error) {
|
|
if schemaType := schema.Type; schemaType != "" && schemaType != "array" {
|
|
return schema.expectedType("array", fast)
|
|
}
|
|
|
|
lenValue := int64(len(value))
|
|
|
|
// "minItems"
|
|
if v := schema.MinItems; v != 0 && lenValue < int64(v) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "minItems",
|
|
Reason: fmt.Sprintf("Minimum number of items is %d", v),
|
|
}
|
|
}
|
|
|
|
// "maxItems"
|
|
if v := schema.MaxItems; v != nil && lenValue > int64(*v) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "maxItems",
|
|
Reason: fmt.Sprintf("Maximum number of items is %d", *v),
|
|
}
|
|
}
|
|
|
|
// "uniqueItems"
|
|
if v := schema.UniqueItems; v && !sliceUniqueItemsChecker(value) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "uniqueItems",
|
|
Reason: fmt.Sprintf("Duplicate items found"),
|
|
}
|
|
}
|
|
|
|
// "items"
|
|
if itemSchemaRef := schema.Items; itemSchemaRef != nil {
|
|
itemSchema := itemSchemaRef.Value
|
|
if itemSchema == nil {
|
|
return foundUnresolvedRef(itemSchemaRef.Ref)
|
|
}
|
|
for i, item := range value {
|
|
if err := itemSchema.VisitJSON(item); err != nil {
|
|
return markSchemaErrorIndex(err, i)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (schema *Schema) VisitJSONObject(value map[string]interface{}) error {
|
|
return schema.visitJSONObject(value, false)
|
|
}
|
|
|
|
func (schema *Schema) visitJSONObject(value map[string]interface{}, fast bool) (err error) {
|
|
if schemaType := schema.Type; schemaType != "" && schemaType != "object" {
|
|
return schema.expectedType("object", fast)
|
|
}
|
|
|
|
// "properties"
|
|
properties := schema.Properties
|
|
lenValue := int64(len(value))
|
|
|
|
// "minProperties"
|
|
if v := schema.MinProps; v != 0 && lenValue < int64(v) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "minProperties",
|
|
Reason: fmt.Sprintf("There must be at least %d properties", v),
|
|
}
|
|
}
|
|
|
|
// "maxProperties"
|
|
if v := schema.MaxProps; v != nil && lenValue > int64(*v) {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "maxProperties",
|
|
Reason: fmt.Sprintf("There must be at most %d properties", *v),
|
|
}
|
|
}
|
|
|
|
// "additionalProperties"
|
|
var additionalProperties *Schema
|
|
if ref := schema.AdditionalProperties; ref != nil {
|
|
additionalProperties = ref.Value
|
|
}
|
|
for k, v := range value {
|
|
if properties != nil {
|
|
propertyRef := properties[k]
|
|
if propertyRef != nil {
|
|
p := propertyRef.Value
|
|
if p == nil {
|
|
return foundUnresolvedRef(propertyRef.Ref)
|
|
}
|
|
if err := p.VisitJSON(v); err != nil {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return markSchemaErrorKey(err, k)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
allowed := schema.AdditionalPropertiesAllowed
|
|
if additionalProperties != nil || allowed == nil || (allowed != nil && *allowed) {
|
|
if additionalProperties != nil {
|
|
if err := additionalProperties.VisitJSON(v); err != nil {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return markSchemaErrorKey(err, k)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "properties",
|
|
Reason: fmt.Sprintf("Property '%s' is unsupported", k),
|
|
}
|
|
}
|
|
for _, k := range schema.Required {
|
|
if _, ok := value[k]; !ok {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return markSchemaErrorKey(&SchemaError{
|
|
Value: value,
|
|
Schema: schema,
|
|
SchemaField: "required",
|
|
Reason: fmt.Sprintf("Property '%s' is missing", k),
|
|
}, k)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (schema *Schema) expectedType(typ string, fast bool) error {
|
|
if fast {
|
|
return errSchema
|
|
}
|
|
return &SchemaError{
|
|
Value: typ,
|
|
Schema: schema,
|
|
SchemaField: "type",
|
|
Reason: "Field must be set to " + schema.Type + " or not be present",
|
|
}
|
|
}
|
|
|
|
type SchemaError struct {
|
|
Value interface{}
|
|
reversePath []string
|
|
Schema *Schema
|
|
SchemaField string
|
|
Reason string
|
|
Origin error
|
|
}
|
|
|
|
func markSchemaErrorKey(err error, key string) error {
|
|
if v, ok := err.(*SchemaError); ok {
|
|
v.reversePath = append(v.reversePath, key)
|
|
return v
|
|
}
|
|
return err
|
|
}
|
|
|
|
func markSchemaErrorIndex(err error, index int) error {
|
|
if v, ok := err.(*SchemaError); ok {
|
|
v.reversePath = append(v.reversePath, strconv.FormatInt(int64(index), 10))
|
|
return v
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (err *SchemaError) JSONPointer() []string {
|
|
reversePath := err.reversePath
|
|
path := append([]string(nil), reversePath...)
|
|
for left, right := 0, len(path)-1; left < right; left, right = left+1, right-1 {
|
|
path[left], path[right] = path[right], path[left]
|
|
}
|
|
return path
|
|
}
|
|
|
|
func (err *SchemaError) Error() string {
|
|
if err.Origin != nil {
|
|
return err.Origin.Error()
|
|
}
|
|
|
|
buf := bytes.NewBuffer(make([]byte, 0, 256))
|
|
if len(err.reversePath) > 0 {
|
|
buf.WriteString(`Error at "`)
|
|
reversePath := err.reversePath
|
|
for i := len(reversePath) - 1; i >= 0; i-- {
|
|
buf.WriteByte('/')
|
|
buf.WriteString(reversePath[i])
|
|
}
|
|
buf.WriteString(`":`)
|
|
}
|
|
reason := err.Reason
|
|
if reason == "" {
|
|
buf.WriteString(`Doesn't match schema "`)
|
|
buf.WriteString(err.SchemaField)
|
|
buf.WriteString(`"`)
|
|
} else {
|
|
buf.WriteString(reason)
|
|
}
|
|
if !SchemaErrorDetailsDisabled {
|
|
buf.WriteString("\nSchema:\n ")
|
|
encoder := json.NewEncoder(buf)
|
|
encoder.SetIndent(" ", " ")
|
|
if err := encoder.Encode(err.Schema); err != nil {
|
|
panic(err)
|
|
}
|
|
buf.WriteString("\nValue:\n ")
|
|
if err := encoder.Encode(err.Value); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func isSliceOfUniqueItems(xs []interface{}) bool {
|
|
s := len(xs)
|
|
m := make(map[string]struct{}, s)
|
|
for _, x := range xs {
|
|
// The input slice is coverted from a JSON string, there shall
|
|
// have no error when covert it back.
|
|
key, _ := json.Marshal(&x)
|
|
m[string(key)] = struct{}{}
|
|
}
|
|
return s == len(m)
|
|
}
|
|
|
|
// SliceUniqueItemsChecker is an function used to check if an given slice
|
|
// have unique items.
|
|
type SliceUniqueItemsChecker func(items []interface{}) bool
|
|
|
|
// By default using predefined func isSliceOfUniqueItems which make use of
|
|
// json.Marshal to generate a key for map used to check if a given slice
|
|
// have unique items.
|
|
var sliceUniqueItemsChecker SliceUniqueItemsChecker = isSliceOfUniqueItems
|
|
|
|
// RegisterArrayUniqueItemsChecker is used to register a customized function
|
|
// used to check if JSON array have unique items.
|
|
func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) {
|
|
sliceUniqueItemsChecker = fn
|
|
}
|
|
|
|
func unsupportedFormat(format string) error {
|
|
return fmt.Errorf("Unsupported 'format' value '%s'", format)
|
|
}
|