databricks-cli/libs/jsonschema/instance.go

165 lines
4.2 KiB
Go
Raw Normal View History

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 {
continue
}
if 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,
2024-01-20 00:51:34 +00:00
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 {
continue
}
if 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
}
2024-01-20 00:51:34 +00:00
func (s *Schema) validateConst(instance map[string]any) error {
for name, property := range s.Properties {
if property.Const == nil {
continue
}
v, ok := instance[name]
2024-01-22 06:52:31 +00:00
if ok && v != property.Const {
2024-01-20 00:51:34 +00:00
return fmt.Errorf("expected value of property %s to be %v. Found: %v", name, property.Const, v)
}
}
return nil
}
2024-01-22 06:52:31 +00:00
// Validates that the instance matches at least one of the schemas in anyOf
// but will also succeed if the property values are omitted.
// For more information, see https://json-schema.org/understanding-json-schema/reference/combining#anyof.
2024-01-20 00:51:34 +00:00
func (s *Schema) validateAnyOf(instance map[string]any) error {
if s.AnyOf == nil {
return nil
}
2024-01-22 06:52:31 +00:00
// Currently, we only validate const for anyOf schemas since anyOf is
// only used by skip_prompt_if, which only supports const.
2024-01-20 00:51:34 +00:00
for _, anyOf := range s.AnyOf {
err := anyOf.validateConst(instance)
if err == nil {
return nil
}
}
return fmt.Errorf("instance does not match any of the schemas in anyOf")
}