2023-08-01 14:09:27 +00:00
package jsonschema
2023-08-15 14:28:04 +00:00
import (
"encoding/json"
"fmt"
"os"
2023-09-25 09:53:38 +00:00
"regexp"
2023-09-08 12:07:22 +00:00
"slices"
2024-04-25 11:23:50 +00:00
"strings"
2023-10-19 14:01:48 +00:00
"github.com/databricks/cli/internal/build"
"golang.org/x/mod/semver"
2023-08-15 14:28:04 +00:00
)
2023-08-01 14:09:27 +00:00
// defines schema for a json object
2024-08-20 13:34:49 +00:00
// TODO: Remove pointers from properties and AnyOf.
// TODO: Can / should we emulate dyn.V here in having a readonly model for the data
// structure? Makes it easier to reason about.
//
// Any performance issues can be addressed by storing the schema
//
// as an embedded file.
2023-08-01 14:09:27 +00:00
type Schema struct {
2024-08-20 16:40:30 +00:00
// TODO: Comments for this field
Definitions any ` json:"$defs,omitempty" `
2023-08-01 14:09:27 +00:00
// Type of the object
Type Type ` json:"type,omitempty" `
// Description of the object. This is rendered as inline documentation in the
// IDE. This is manually injected here using schema.Docs
Description string ` json:"description,omitempty" `
2023-11-30 16:07:45 +00:00
// Expected value for the JSON object. The object value must be equal to this
// field if it's specified in the schema.
2024-08-20 13:34:49 +00:00
// TODO: Generics here? OR maybe a type from the reflection package.
2023-11-30 16:07:45 +00:00
Const any ` json:"const,omitempty" `
2023-08-01 14:09:27 +00:00
// Schemas for the fields of an struct. The keys are the first json tag.
// The values are the schema for the type of the field
Properties map [ string ] * Schema ` json:"properties,omitempty" `
// The schema for all values of an array
Items * Schema ` json:"items,omitempty" `
// The schema for any properties not mentioned in the Schema.Properties field.
// this validates maps[string]any in bundle configuration
// OR
// A boolean type with value false. Setting false here validates that all
// properties in the config have been defined in the json schema as properties
//
2024-08-20 13:34:49 +00:00
// Its type during runtime will either be Schema or bool
// TODO: Generics to represent either a Schema{} or a bool.
2023-08-01 14:09:27 +00:00
AdditionalProperties any ` json:"additionalProperties,omitempty" `
// Required properties for the object. Any fields missing the "omitempty"
// json tag will be included
Required [ ] string ` json:"required,omitempty" `
// URI to a json schema
2024-08-20 16:40:30 +00:00
// TODO: Would be nice to make items as well as this a non-pointer.
2023-08-01 14:09:27 +00:00
Reference * string ` json:"$ref,omitempty" `
// Default value for the property / object
Default any ` json:"default,omitempty" `
2023-09-05 11:08:25 +00:00
2023-09-08 12:07:22 +00:00
// List of valid values for a JSON instance for this schema.
Enum [ ] any ` json:"enum,omitempty" `
2023-09-25 09:53:38 +00:00
// A pattern is a regular expression the object will be validated against.
// Can only be used with type "string". The regex syntax supported is available
// here: https://github.com/google/re2/wiki/Syntax
Pattern string ` json:"pattern,omitempty" `
2023-09-05 11:08:25 +00:00
// Extension embeds our custom JSON schema extensions.
Extension
2024-01-25 10:09:42 +00:00
// Schema that must match any of the schemas in the array
2024-08-20 13:34:49 +00:00
AnyOf [ ] Schema ` json:"anyOf,omitempty" `
2023-08-01 14:09:27 +00:00
}
2023-11-06 15:05:17 +00:00
// Default value defined in a JSON Schema, represented as a string.
func ( s * Schema ) DefaultString ( ) ( string , error ) {
return toString ( s . Default , s . Type )
}
// Allowed enum values defined in a JSON Schema, represented as a slice of strings.
func ( s * Schema ) EnumStringSlice ( ) ( [ ] string , error ) {
return toStringSlice ( s . Enum , s . Type )
}
// Parses a string as a Go primitive value. The type of the value is determined
// by the type defined in the JSON Schema.
func ( s * Schema ) ParseString ( v string ) ( any , error ) {
return fromString ( v , s . Type )
}
2024-04-25 11:23:50 +00:00
func ( s * Schema ) getByPath ( path string ) ( * Schema , error ) {
p := strings . Split ( path , "." )
res := s
for _ , node := range p {
if node == "*" {
res = res . AdditionalProperties . ( * Schema )
continue
}
var ok bool
res , ok = res . Properties [ node ]
if ! ok {
return nil , fmt . Errorf ( "property %q not found in schema. Query path: %s" , node , path )
}
}
return res , nil
}
func ( s * Schema ) GetByPath ( path string ) ( Schema , error ) {
v , err := s . getByPath ( path )
if err != nil {
return Schema { } , err
}
return * v , nil
}
func ( s * Schema ) SetByPath ( path string , v Schema ) error {
dst , err := s . getByPath ( path )
if err != nil {
return err
}
* dst = v
return nil
}
2023-08-01 14:09:27 +00:00
type Type string
const (
2024-08-20 13:34:49 +00:00
// Default zero value of a schema. This does not correspond to a type in the
// JSON schema spec and is an internal type defined for convenience.
2023-08-01 14:09:27 +00:00
InvalidType Type = "invalid"
2024-08-20 13:34:49 +00:00
NullType Type = "null"
2023-08-01 14:09:27 +00:00
BooleanType Type = "boolean"
StringType Type = "string"
NumberType Type = "number"
ObjectType Type = "object"
ArrayType Type = "array"
IntegerType Type = "integer"
)
2023-08-15 14:28:04 +00:00
2023-10-19 14:01:48 +00:00
// Validate property types are all valid JSON schema types.
func ( schema * Schema ) validateSchemaPropertyTypes ( ) error {
2023-08-15 14:28:04 +00:00
for _ , v := range schema . Properties {
switch v . Type {
2024-04-25 11:23:50 +00:00
case NumberType , BooleanType , StringType , IntegerType , ObjectType , ArrayType :
2023-08-15 14:28:04 +00:00
continue
case "int" , "int32" , "int64" :
return fmt . Errorf ( "type %s is not a recognized json schema type. Please use \"integer\" instead" , v . Type )
case "float" , "float32" , "float64" :
return fmt . Errorf ( "type %s is not a recognized json schema type. Please use \"number\" instead" , v . Type )
case "bool" :
return fmt . Errorf ( "type %s is not a recognized json schema type. Please use \"boolean\" instead" , v . Type )
default :
return fmt . Errorf ( "type %s is not a recognized json schema type" , v . Type )
}
}
2023-10-19 14:01:48 +00:00
return nil
}
2023-09-07 14:36:06 +00:00
2023-10-19 14:01:48 +00:00
// Validate default property values are consistent with types.
func ( schema * Schema ) validateSchemaDefaultValueTypes ( ) error {
2023-09-07 14:36:06 +00:00
for name , property := range schema . Properties {
if property . Default == nil {
continue
}
if err := validateType ( property . Default , property . Type ) ; err != nil {
return fmt . Errorf ( "type validation for default value of property %s failed: %w" , name , err )
}
}
2023-10-19 14:01:48 +00:00
return nil
}
2023-09-07 14:36:06 +00:00
2023-11-30 16:07:45 +00:00
func ( schema * Schema ) validateConstValueTypes ( ) error {
for name , property := range schema . Properties {
if property . Const == nil {
continue
}
if err := validateType ( property . Const , property . Type ) ; err != nil {
return fmt . Errorf ( "type validation for const value of property %s failed: %w" , name , err )
}
}
return nil
}
2023-10-19 14:01:48 +00:00
// Validate enum field values for properties are consistent with types.
func ( schema * Schema ) validateSchemaEnumValueTypes ( ) error {
2023-09-08 12:07:22 +00:00
for name , property := range schema . Properties {
if property . Enum == nil {
continue
}
for i , enum := range property . Enum {
err := validateType ( enum , property . Type )
if err != nil {
return fmt . Errorf ( "type validation for enum at index %v failed for property %s: %w" , i , name , err )
}
}
}
2023-10-19 14:01:48 +00:00
return nil
}
2023-09-08 12:07:22 +00:00
2023-10-19 14:01:48 +00:00
// Validate default value is contained in the list of enums if both are defined.
func ( schema * Schema ) validateSchemaDefaultValueIsInEnums ( ) error {
2023-09-08 12:07:22 +00:00
for name , property := range schema . Properties {
if property . Default == nil || property . Enum == nil {
continue
}
// We expect the default value to be consistent with the list of enum
// values.
if ! slices . Contains ( property . Enum , property . Default ) {
return fmt . Errorf ( "list of enum values for property %s does not contain default value %v: %v" , name , property . Default , property . Enum )
}
}
2023-10-19 14:01:48 +00:00
return nil
}
2023-09-25 09:53:38 +00:00
2023-10-19 14:01:48 +00:00
// Validate usage of "pattern" is consistent.
func ( schema * Schema ) validateSchemaPattern ( ) error {
2023-09-25 09:53:38 +00:00
for name , property := range schema . Properties {
pattern := property . Pattern
if pattern == "" {
continue
}
// validate property type is string
if property . Type != StringType {
return fmt . Errorf ( "property %q has a non-empty regex pattern %q specified. Patterns are only supported for string properties" , name , pattern )
}
// validate regex pattern syntax
r , err := regexp . Compile ( pattern )
if err != nil {
return fmt . Errorf ( "invalid regex pattern %q provided for property %q: %w" , pattern , name , err )
}
// validate enum values against the pattern
for i , enum := range property . Enum {
if ! r . MatchString ( enum . ( string ) ) {
return fmt . Errorf ( "enum value %q at index %v for property %q does not match specified regex pattern: %q" , enum , i , name , pattern )
}
}
}
2023-08-15 14:28:04 +00:00
return nil
}
2023-10-19 14:01:48 +00:00
func ( schema * Schema ) validateSchemaMinimumCliVersion ( currentVersion string ) func ( ) error {
return func ( ) error {
if schema . MinDatabricksCliVersion == "" {
return nil
}
// Ignore this validation rule for local builds.
if semver . Compare ( "v" + build . DefaultSemver , currentVersion ) == 0 {
return nil
}
// Confirm that MinDatabricksCliVersion is a valid semver.
if ! semver . IsValid ( schema . MinDatabricksCliVersion ) {
return fmt . Errorf ( "invalid minimum CLI version %q specified. Please specify the version in the format v0.0.0" , schema . MinDatabricksCliVersion )
}
// Confirm that MinDatabricksCliVersion is less than or equal to the current version.
if semver . Compare ( schema . MinDatabricksCliVersion , currentVersion ) > 0 {
return fmt . Errorf ( "minimum CLI version %q is greater than current CLI version %q. Please upgrade your current Databricks CLI" , schema . MinDatabricksCliVersion , currentVersion )
}
return nil
}
}
2023-11-30 16:07:45 +00:00
func ( schema * Schema ) validateSchemaSkippedPropertiesHaveDefaults ( ) error {
for name , property := range schema . Properties {
if property . SkipPromptIf != nil && property . Default == nil {
return fmt . Errorf ( "property %q has a skip_prompt_if clause but no default value" , name )
}
}
return nil
}
2023-10-19 14:01:48 +00:00
func ( schema * Schema ) validate ( ) error {
for _ , fn := range [ ] func ( ) error {
schema . validateSchemaPropertyTypes ,
schema . validateSchemaDefaultValueTypes ,
schema . validateSchemaEnumValueTypes ,
2023-11-30 16:07:45 +00:00
schema . validateConstValueTypes ,
2023-10-19 14:01:48 +00:00
schema . validateSchemaDefaultValueIsInEnums ,
schema . validateSchemaPattern ,
schema . validateSchemaMinimumCliVersion ( "v" + build . GetInfo ( ) . Version ) ,
2023-11-30 16:07:45 +00:00
schema . validateSchemaSkippedPropertiesHaveDefaults ,
2023-10-19 14:01:48 +00:00
} {
err := fn ( )
if err != nil {
return err
}
}
return nil
}
2023-08-15 14:28:04 +00:00
func Load ( path string ) ( * Schema , error ) {
b , err := os . ReadFile ( path )
if err != nil {
return nil , err
}
schema := & Schema { }
err = json . Unmarshal ( b , schema )
if err != nil {
return nil , err
}
2023-09-07 14:36:06 +00:00
// Convert the default values of top-level properties to integers.
// This is required because the default JSON unmarshaler parses numbers
// as floats when the Golang field it's being loaded to is untyped.
//
// NOTE: properties can be recursively defined in a schema, but the current
// use-cases only uses the first layer of properties so we skip converting
// any recursive properties.
for name , property := range schema . Properties {
if property . Type != IntegerType {
continue
}
if property . Default != nil {
property . Default , err = toInteger ( property . Default )
if err != nil {
return nil , fmt . Errorf ( "failed to parse default value for property %s: %w" , name , err )
}
}
2023-11-30 16:07:45 +00:00
if property . Const != nil {
property . Const , err = toInteger ( property . Const )
if err != nil {
return nil , fmt . Errorf ( "failed to parse const value for property %s: %w" , name , err )
}
}
2023-09-08 12:07:22 +00:00
for i , enum := range property . Enum {
property . Enum [ i ] , err = toInteger ( enum )
if err != nil {
return nil , fmt . Errorf ( "failed to parse enum value %v at index %v for property %s: %w" , enum , i , name , err )
}
}
2023-09-07 14:36:06 +00:00
}
2023-08-15 14:28:04 +00:00
return schema , schema . validate ( )
}