Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Any map list validation #110

Merged
merged 4 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 140 additions & 12 deletions schema/any.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,8 @@ func (a *AnySchema) Unserialize(data any) (any, error) {
return a.checkAndConvert(data)
}

//nolint:funlen
func (a *AnySchema) ValidateCompatibility(typeOrData any) error {
// Check if it's a schema.Type. If it is, verify it. If not, verify it as data.
schemaType, ok := typeOrData.(Type)
if !ok {
_, err := a.Unserialize(typeOrData)
return err
}

switch schemaType.ReflectedType().Kind() {
func (a *AnySchema) validateSchemaCompatibility(schema Type) error {
switch schema.ReflectedType().Kind() {
case reflect.Int:
fallthrough
case reflect.Uint:
Expand Down Expand Up @@ -69,16 +61,152 @@ func (a *AnySchema) ValidateCompatibility(typeOrData any) error {
default:
// Schema is not a primitive, slice, or map type, so check the complex types
// Explicitly allow object schemas since their reflected type can be a struct if they are struct mapped.
switch typeOrData.(type) {
switch schema.(type) {
case *AnySchema, *OneOfSchema[int64], *OneOfSchema[string], *ObjectSchema:
// These are the allowed values.
default:
// It's not an any schema or a type compatible with an any schema, so error
return &ConstraintError{
Message: fmt.Sprintf("schema type `%T` cannot be used as an input for an 'any' type", typeOrData),
Message: fmt.Sprintf("schema type `%T` cannot be used as an input for an 'any' type", schema),
}
}
return nil
}
}

func (a *AnySchema) validateAnyMap(data map[any]any) error {
// Test individual values
var firstReflectKind reflect.Kind
for key, value := range data {
// Validate key
reflectKind := reflect.ValueOf(key).Kind()
switch reflectKind {
// While it is possible to add more types of ints, it's likely better to keep it consistent with i64
case reflect.Int64:
fallthrough
case reflect.String:
// Valid type
default:
return &ConstraintError{
Message: fmt.Sprintf("invalid key type for map passed into 'any' type (%s); should be string or i64", reflectKind),
}
}
if firstReflectKind == reflect.Invalid { // First item
firstReflectKind = reflectKind
} else if firstReflectKind != reflectKind {
return &ConstraintError{
Message: fmt.Sprintf(
"mismatched key types in map passed into 'any' type: %s != %s",
firstReflectKind, reflectKind),
}
}

// Validate value
err := a.ValidateCompatibility(value)
if err != nil {
return &ConstraintError{
Message: fmt.Sprintf("validation error while validating any-keyed map item item %q of map for 'any' type (%s)", key, err.Error()),
}
}
}
return nil
}

//nolint:nestif
func (a *AnySchema) validateAnyList(data []any) error {
if len(data) == 0 {
return nil // No items to check, and following code assumes non-empty list
}
// Test list items
for _, item := range data {
err := a.ValidateCompatibility(item)
if err != nil {
return &ConstraintError{
Message: fmt.Sprintf("validation error while validating list item of type `%T` in any type (%s)", item, err.Error()),
}
}
}
// validate that all list items are compatible with the first to make the list homogeneous.
firstItem := data[0]
firstItemType, firstValIsSchema := firstItem.(Type)
if firstValIsSchema {
for i := 1; i < len(data); i++ {
valToTest := data[i]
err := firstItemType.ValidateCompatibility(valToTest)
if err != nil {
return &ConstraintError{
Message: fmt.Sprintf(
"validation error while validating for homogeneous list item of type `%T` in any type (%s)",
valToTest, err.Error()),
}
}
}
} else {
// Loop through all items. Ensure they have the same type.
firstItemType := reflect.ValueOf(firstItem).Kind()
for i := 1; i < len(data); i++ {
valToTest := data[i]
typeToTest := reflect.ValueOf(valToTest).Kind()
if firstItemType != typeToTest {
// Not compatible or is a schema
schemaType, valIsSchema := valToTest.(Type)
if !valIsSchema {
return &ConstraintError{
Message: fmt.Sprintf(
"types do not match between list items passed for any type %T != %T; "+
"lists should have homogeneous types",
firstItem, valToTest),
}
} else {
err := schemaType.ValidateCompatibility(valToTest)
if err != nil {
return &ConstraintError{
Message: fmt.Sprintf(
"types do not match between list items passed for any type %s; "+
"lists should have homogeneous types",
err),
}
}
}
}
}
}
return nil
}

func (a *AnySchema) ValidateCompatibility(typeOrData any) error {
switch typeOrData := typeOrData.(type) {
case Type:
return a.validateSchemaCompatibility(typeOrData)
case map[string]any:
// Test individual values
for key, value := range typeOrData {
err := a.ValidateCompatibility(value)
if err != nil {
return &ConstraintError{
Message: fmt.Sprintf("validation error while validating string-keyed map item item %q of map for 'any' type (%s)", key, err.Error()),
}
}
}
return nil
case map[int64]any:
// Test individual values
for key, value := range typeOrData {
err := a.ValidateCompatibility(value)
if err != nil {
return &ConstraintError{
Message: fmt.Sprintf("validation error while validating int-keyed map item item %q of map for 'any' type (%s)", key, err.Error()),
}
}
}
return nil
case map[any]any:
return a.validateAnyMap(typeOrData)
case []interface{}:
return a.validateAnyList(typeOrData)
default:
_, err := a.Unserialize(typeOrData)
return err
}
}

Expand Down
136 changes: 113 additions & 23 deletions schema/any_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,40 +152,38 @@ func TestAnyTypeReflectedType(t *testing.T) {
assert.NotNil(t, a.ReflectedType())
}

func TestAnyValidateCompatibility(t *testing.T) {
s1 := schema.NewAnySchema()
properties := map[string]*schema.PropertySchema{
"field1": schema.NewPropertySchema(
schema.NewIntSchema(nil, nil, nil),
nil,
true,
nil,
nil,
nil,
nil,
nil,
),
}
type someStruct struct {
//nolint:unused // This is just for test purposes.
field1 int
}
objectSchema := schema.NewObjectSchema("some-id", properties)
structMappedObjectSchema := schema.NewStructMappedObjectSchema[someStruct]("some-id", properties)
var properties = map[string]*schema.PropertySchema{
"field1": schema.NewPropertySchema(
schema.NewIntSchema(nil, nil, nil),
nil,
true,
nil,
nil,
nil,
nil,
nil,
),
}

type someStruct struct {
field1 int
}

var objectSchema = schema.NewObjectSchema("some-id", properties)
var structMappedObjectSchema = schema.NewStructMappedObjectSchema[someStruct]("some-id", properties)

func TestAnyValidateCompatibilitySimple(t *testing.T) {
s1 := schema.NewAnySchema()
assert.NoError(t, s1.ValidateCompatibility(schema.NewAnySchema()))
assert.NoError(t, s1.ValidateCompatibility(schema.NewStringSchema(nil, nil, nil)))
assert.NoError(t, s1.ValidateCompatibility(schema.NewIntSchema(nil, nil, nil)))
assert.NoError(t, s1.ValidateCompatibility(schema.NewBoolSchema()))
assert.NoError(t, s1.ValidateCompatibility(schema.NewListSchema(schema.NewBoolSchema(), nil, nil)))
assert.NoError(t, s1.ValidateCompatibility(schema.NewFloatSchema(nil, nil, nil)))
assert.Error(t, s1.ValidateCompatibility(schema.NewDisplayValue(nil, nil, nil)))
assert.NoError(t, s1.ValidateCompatibility("test"))
assert.NoError(t, s1.ValidateCompatibility(1))
assert.NoError(t, s1.ValidateCompatibility(1.5))
assert.NoError(t, s1.ValidateCompatibility(true))
assert.NoError(t, s1.ValidateCompatibility([]string{}))
assert.NoError(t, s1.ValidateCompatibility(map[string]any{}))
assert.NoError(t, s1.ValidateCompatibility(schema.NewStringEnumSchema(map[string]*schema.DisplayValue{})))
assert.NoError(t, s1.ValidateCompatibility(schema.NewIntEnumSchema(map[int64]*schema.DisplayValue{}, nil)))
assert.NoError(t, s1.ValidateCompatibility(objectSchema))
Expand All @@ -197,4 +195,96 @@ func TestAnyValidateCompatibility(t *testing.T) {
assert.NoError(t, s1.ValidateCompatibility(
schema.NewOneOfIntSchema[int64](map[int64]schema.Object{}, "id", false),
))

}

func TestAnyValidateCompatibilityLists(t *testing.T) {
s1 := schema.NewAnySchema()
assert.NoError(t, s1.ValidateCompatibility(schema.NewListSchema(schema.NewBoolSchema(), nil, nil)))
assert.NoError(t, s1.ValidateCompatibility([]string{}))

// Test non-homogeneous list
err := s1.ValidateCompatibility([]any{
int64(5),
"5",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "homogeneous")
// Test a list of object schemas
assert.NoError(t, s1.ValidateCompatibility([]any{
structMappedObjectSchema,
}))
}

//nolint:funlen
func TestAnyValidateCompatibilityMaps(t *testing.T) {
// Test custom maps with schemas and data
s1 := schema.NewAnySchema()
assert.NoError(t, s1.ValidateCompatibility(map[string]any{}))
// Include invalid item within an any map
err := s1.ValidateCompatibility(map[any]any{
"b": someStruct{field1: 1},
})
assert.Error(t, err)
// Include invalid item within a string map
err = s1.ValidateCompatibility(map[string]any{
"b": someStruct{field1: 1},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), `"b"`) // Identifies the problematic key
assert.Contains(t, err.Error(), "someStruct") // Identifies the problematic type
// String key type
assert.NoError(t, s1.ValidateCompatibility(map[string]any{
"a": true,
"b": "test",
"c": []any{
structMappedObjectSchema,
},
"d": structMappedObjectSchema,
"e": schema.NewStringSchema(nil, nil, nil),
}))
// int key type
assert.NoError(t, s1.ValidateCompatibility(map[int64]any{
1: true,
2: "test",
3: []any{
structMappedObjectSchema,
},
4: structMappedObjectSchema,
5: schema.NewStringSchema(nil, nil, nil),
}))
// any key type with string key values
assert.NoError(t, s1.ValidateCompatibility(map[any]any{
"a": true,
"b": "test",
"c": []any{
structMappedObjectSchema,
},
"d": structMappedObjectSchema,
"e": schema.NewStringSchema(nil, nil, nil),
}))
// any key type with integer key values
assert.NoError(t, s1.ValidateCompatibility(map[any]any{
int64(1): true,
int64(2): "test",
int64(3): []any{
structMappedObjectSchema,
},
int64(4): structMappedObjectSchema,
int64(5): schema.NewStringSchema(nil, nil, nil),
}))
// any key type with mixed key values
err = s1.ValidateCompatibility(map[any]any{
"a": true,
int64(2): "test",
int64(3): []any{
structMappedObjectSchema,
},
int64(4): structMappedObjectSchema,
int64(5): schema.NewStringSchema(nil, nil, nil),
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "mismatched")
assert.Contains(t, err.Error(), "string")
assert.Contains(t, err.Error(), "int64")
}
Loading