databricks-cli/libs/jsonschema/instance.go

163 lines
4.2 KiB
Go

package jsonschema
import (
"encoding/json"
"fmt"
"os"
"slices"
)
// Load a JSON document and validate it against the JSON schema. Instance here
// refers to a JSON document. see: https://json-schema.org/draft/2020-12/json-schema-core.html#name-instance
func (s *Schema) LoadInstance(path string) (map[string]any, error) {
instance := make(map[string]any)
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
err = json.Unmarshal(b, &instance)
if err != nil {
return nil, err
}
// The default JSON unmarshaler parses untyped number values as float64.
// We convert integer properties from float64 to int64 here.
for name, v := range instance {
propertySchema, ok := s.Properties[name]
if !ok || propertySchema.Type != IntegerType {
continue
}
integerValue, err := toInteger(v)
if err != nil {
return nil, fmt.Errorf("failed to parse property %s: %w", name, err)
}
instance[name] = integerValue
}
return instance, s.ValidateInstance(instance)
}
// Validate an instance against the schema
func (s *Schema) ValidateInstance(instance map[string]any) error {
validations := []func(map[string]any) error{
s.validateAdditionalProperties,
s.validateEnum,
s.validateRequired,
s.validateTypes,
s.validatePattern,
s.validateConst,
s.validateAnyOf,
}
for _, fn := range validations {
err := fn(instance)
if err != nil {
return err
}
}
return nil
}
// If additional properties is set to false, this function validates instance only
// contains properties defined in the schema.
func (s *Schema) validateAdditionalProperties(instance map[string]any) error {
// Note: AdditionalProperties has the type any.
if s.AdditionalProperties != false {
return nil
}
for k := range instance {
_, ok := s.Properties[k]
if !ok {
return fmt.Errorf("property %s is not defined in the schema", k)
}
}
return nil
}
// This function validates that all require properties in the schema have values
// in the instance.
func (s *Schema) validateRequired(instance map[string]any) error {
for _, name := range s.Required {
if _, ok := instance[name]; !ok {
return fmt.Errorf("no value provided for required property %s", name)
}
}
return nil
}
// Validates the types of all input properties values match their types defined in the schema
func (s *Schema) validateTypes(instance map[string]any) error {
for k, v := range instance {
fieldInfo, ok := s.Properties[k]
if !ok {
continue
}
err := validateType(v, fieldInfo.Type)
if err != nil {
return fmt.Errorf("incorrect type for property %s: %w", k, err)
}
}
return nil
}
func (s *Schema) validateEnum(instance map[string]any) error {
for k, v := range instance {
fieldInfo, ok := s.Properties[k]
if !ok || fieldInfo.Enum == nil {
continue
}
if !slices.Contains(fieldInfo.Enum, v) {
return fmt.Errorf("expected value of property %s to be one of %v. Found: %v", k, fieldInfo.Enum, v)
}
}
return nil
}
func (s *Schema) validatePattern(instance map[string]any) error {
for k, v := range instance {
fieldInfo, ok := s.Properties[k]
if !ok {
continue
}
err := validatePatternMatch(k, v, fieldInfo)
if err != nil {
return err
}
}
return nil
}
func (s *Schema) validateConst(instance map[string]any) error {
for k, v := range instance {
fieldInfo, ok := s.Properties[k]
if !ok || fieldInfo.Const == nil {
continue
}
if v != fieldInfo.Const {
return fmt.Errorf("expected value of property %s to be %v. Found: %v", k, fieldInfo.Const, v)
}
}
return nil
}
// Validates that the instance matches at least one of the schemas in anyOf
// For more information, see https://json-schema.org/understanding-json-schema/reference/combining#anyof.
func (s *Schema) validateAnyOf(instance map[string]any) error {
if s.AnyOf == nil {
return nil
}
// According to the JSON schema RFC, anyOf must contain at least one schema.
// https://json-schema.org/draft/2020-12/json-schema-core
if len(s.AnyOf) == 0 {
return fmt.Errorf("anyOf must contain at least one schema")
}
for _, anyOf := range s.AnyOf {
err := anyOf.ValidateInstance(instance)
if err == nil {
return nil
}
}
return fmt.Errorf("instance does not match any of the schemas in anyOf")
}