feat(schema)!: add more range methods, rename EnumDefinition -> EnumType (#21043)

This commit is contained in:
Aaron Craelius 2024-07-24 15:42:47 +02:00 committed by GitHub
parent 1f0ce2f5b5
commit 339e26ea8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 163 additions and 70 deletions

View File

@ -5,8 +5,8 @@ import (
"fmt"
)
// EnumDefinition represents the definition of an enum type.
type EnumDefinition struct {
// EnumType represents the definition of an enum type.
type EnumType struct {
// Name is the name of the enum type. It must conform to the NameFormat regular expression.
// Its name must be unique between all enum types and object types in the module.
// The same enum, however, can be used in multiple object types and fields as long as the
@ -18,10 +18,15 @@ type EnumDefinition struct {
Values []string
}
func (EnumDefinition) isType() {}
// TypeName implements the Type interface.
func (e EnumType) TypeName() string {
return e.Name
}
func (EnumType) isType() {}
// Validate validates the enum definition.
func (e EnumDefinition) Validate() error {
func (e EnumType) Validate() error {
if !ValidateName(e.Name) {
return fmt.Errorf("invalid enum definition name %q", e.Name)
}
@ -44,7 +49,7 @@ func (e EnumDefinition) Validate() error {
}
// ValidateValue validates that the value is a valid enum value.
func (e EnumDefinition) ValidateValue(value string) error {
func (e EnumType) ValidateValue(value string) error {
for _, v := range e.Values {
if v == value {
return nil

View File

@ -8,12 +8,12 @@ import (
func TestEnumDefinition_Validate(t *testing.T) {
tests := []struct {
name string
enum EnumDefinition
enum EnumType
errContains string
}{
{
name: "valid enum",
enum: EnumDefinition{
enum: EnumType{
Name: "test",
Values: []string{"a", "b", "c"},
},
@ -21,7 +21,7 @@ func TestEnumDefinition_Validate(t *testing.T) {
},
{
name: "empty name",
enum: EnumDefinition{
enum: EnumType{
Name: "",
Values: []string{"a", "b", "c"},
},
@ -29,7 +29,7 @@ func TestEnumDefinition_Validate(t *testing.T) {
},
{
name: "empty values",
enum: EnumDefinition{
enum: EnumType{
Name: "test",
Values: []string{},
},
@ -37,7 +37,7 @@ func TestEnumDefinition_Validate(t *testing.T) {
},
{
name: "empty value",
enum: EnumDefinition{
enum: EnumType{
Name: "test",
Values: []string{"a", "", "c"},
},
@ -45,7 +45,7 @@ func TestEnumDefinition_Validate(t *testing.T) {
},
{
name: "duplicate value",
enum: EnumDefinition{
enum: EnumType{
Name: "test",
Values: []string{"a", "b", "a"},
},
@ -72,7 +72,7 @@ func TestEnumDefinition_Validate(t *testing.T) {
}
func TestEnumDefinition_ValidateValue(t *testing.T) {
enum := EnumDefinition{
enum := EnumType{
Name: "test",
Values: []string{"a", "b", "c"},
}

View File

@ -13,11 +13,11 @@ type Field struct {
// Nullable indicates whether null values are accepted for the field. Key fields CANNOT be nullable.
Nullable bool
// EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind.
// EnumType is the definition of the enum type and is only valid when Kind is EnumKind.
// The same enum types can be reused in the same module schema, but they always must contain
// the same values for the same enum name. This possibly introduces some duplication of
// definitions but makes it easier to reason about correctness and validation in isolation.
EnumDefinition EnumDefinition
EnumType EnumType
}
// Validate validates the field.
@ -34,10 +34,10 @@ func (c Field) Validate() error {
// enum definition only valid with EnumKind
if c.Kind == EnumKind {
if err := c.EnumDefinition.Validate(); err != nil {
if err := c.EnumType.Validate(); err != nil {
return fmt.Errorf("invalid enum definition for field %q: %v", c.Name, err) //nolint:errorlint // false positive due to using go1.12
}
} else if c.Kind != EnumKind && (c.EnumDefinition.Name != "" || c.EnumDefinition.Values != nil) {
} else if c.Kind != EnumKind && (c.EnumType.Name != "" || c.EnumType.Values != nil) {
return fmt.Errorf("enum definition is only valid for field %q with type EnumKind", c.Name)
}
@ -45,7 +45,7 @@ func (c Field) Validate() error {
}
// ValidateValue validates that the value conforms to the field's kind and nullability.
// Unlike Kind.ValidateValue, it also checks that the value conforms to the EnumDefinition
// Unlike Kind.ValidateValue, it also checks that the value conforms to the EnumType
// if the field is an EnumKind.
func (c Field) ValidateValue(value interface{}) error {
if value == nil {
@ -60,7 +60,7 @@ func (c Field) ValidateValue(value interface{}) error {
}
if c.Kind == EnumKind {
return c.EnumDefinition.ValidateValue(value.(string))
return c.EnumType.ValidateValue(value.(string))
}
return nil

View File

@ -46,18 +46,18 @@ func TestField_Validate(t *testing.T) {
{
name: "enum definition with non-EnumKind",
field: Field{
Name: "field1",
Kind: StringKind,
EnumDefinition: EnumDefinition{Name: "enum"},
Name: "field1",
Kind: StringKind,
EnumType: EnumType{Name: "enum"},
},
errContains: "enum definition is only valid for field \"field1\" with type EnumKind",
},
{
name: "valid enum",
field: Field{
Name: "field1",
Kind: EnumKind,
EnumDefinition: EnumDefinition{Name: "enum", Values: []string{"a", "b"}},
Name: "field1",
Kind: EnumKind,
EnumType: EnumType{Name: "enum", Values: []string{"a", "b"}},
},
},
}
@ -128,9 +128,9 @@ func TestField_ValidateValue(t *testing.T) {
{
name: "valid enum",
field: Field{
Name: "field1",
Kind: EnumKind,
EnumDefinition: EnumDefinition{Name: "enum", Values: []string{"a", "b"}},
Name: "field1",
Kind: EnumKind,
EnumType: EnumType{Name: "enum", Values: []string{"a", "b"}},
},
value: "a",
errContains: "",
@ -138,9 +138,9 @@ func TestField_ValidateValue(t *testing.T) {
{
name: "invalid enum",
field: Field{
Name: "field1",
Kind: EnumKind,
EnumDefinition: EnumDefinition{Name: "enum", Values: []string{"a", "b"}},
Name: "field1",
Kind: EnumKind,
EnumType: EnumType{Name: "enum", Values: []string{"a", "b"}},
},
value: "c",
errContains: "not a valid enum value",

View File

@ -78,7 +78,7 @@ const (
AddressKind
// EnumKind is an enum type and values of this type must be of the go type string.
// Fields of this type are expected to set the EnumDefinition field in the field definition to the enum
// Fields of this type are expected to set the EnumType field in the field definition to the enum
// definition.
EnumKind

View File

@ -31,7 +31,7 @@ func NewModuleSchema(objectTypes []ObjectType) (ModuleSchema, error) {
}
func addEnumType(types map[string]Type, field Field) error {
enumDef := field.EnumDefinition
enumDef := field.EnumType
if enumDef.Name == "" {
return nil
}
@ -42,7 +42,7 @@ func addEnumType(types map[string]Type, field Field) error {
return nil
}
existingEnum, ok := existing.(EnumDefinition)
existingEnum, ok := existing.(EnumType)
if !ok {
return fmt.Errorf("enum %q already exists as a different non-enum type", enumDef.Name)
}
@ -119,3 +119,25 @@ func (s ModuleSchema) Types(f func(Type) bool) {
}
}
}
// ObjectTypes iterators over all the object types in the schema in alphabetical order.
func (s ModuleSchema) ObjectTypes(f func(ObjectType) bool) {
s.Types(func(t Type) bool {
objTyp, ok := t.(ObjectType)
if ok {
return f(objTyp)
}
return true
})
}
// EnumTypes iterators over all the enum types in the schema in alphabetical order.
func (s ModuleSchema) EnumTypes(f func(EnumType) bool) {
s.Types(func(t Type) bool {
enumType, ok := t.(EnumType)
if ok {
return f(enumType)
}
return true
})
}

View File

@ -51,7 +51,7 @@ func TestModuleSchema_Validate(t *testing.T) {
{
Name: "k",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "enum1",
Values: []string{"a", "b"},
},
@ -61,7 +61,7 @@ func TestModuleSchema_Validate(t *testing.T) {
{
Name: "v",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "enum1",
Values: []string{"a", "b", "c"},
},
@ -80,7 +80,7 @@ func TestModuleSchema_Validate(t *testing.T) {
{
Name: "k",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "enum1",
Values: []string{"a", "b"},
},
@ -93,7 +93,7 @@ func TestModuleSchema_Validate(t *testing.T) {
{
Name: "k",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "enum1",
Values: []string{"a", "c"},
},
@ -111,7 +111,7 @@ func TestModuleSchema_Validate(t *testing.T) {
{
Name: "k",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "enum1",
Values: []string{"a", "b"},
},
@ -124,7 +124,7 @@ func TestModuleSchema_Validate(t *testing.T) {
{
Name: "k",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "enum1",
Values: []string{"a", "b"},
},
@ -141,7 +141,7 @@ func TestModuleSchema_Validate(t *testing.T) {
{
Name: "field1",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "type1",
Values: []string{"a", "b"},
},
@ -179,7 +179,7 @@ func TestModuleSchema_ValidateObjectUpdate(t *testing.T) {
}{
{
name: "valid object update",
moduleSchema: RequireNewModuleSchema(t, []ObjectType{
moduleSchema: requireModuleSchema(t, []ObjectType{
{
Name: "object1",
KeyFields: []Field{
@ -199,7 +199,7 @@ func TestModuleSchema_ValidateObjectUpdate(t *testing.T) {
},
{
name: "object type not found",
moduleSchema: RequireNewModuleSchema(t, []ObjectType{
moduleSchema: requireModuleSchema(t, []ObjectType{
{
Name: "object1",
KeyFields: []Field{
@ -219,14 +219,14 @@ func TestModuleSchema_ValidateObjectUpdate(t *testing.T) {
},
{
name: "type name refers to an enum",
moduleSchema: RequireNewModuleSchema(t, []ObjectType{
moduleSchema: requireModuleSchema(t, []ObjectType{
{
Name: "obj1",
KeyFields: []Field{
{
Name: "field1",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "enum1",
Values: []string{"a", "b"},
},
@ -258,7 +258,7 @@ func TestModuleSchema_ValidateObjectUpdate(t *testing.T) {
}
}
func RequireNewModuleSchema(t *testing.T, objectTypes []ObjectType) ModuleSchema {
func requireModuleSchema(t *testing.T, objectTypes []ObjectType) ModuleSchema {
t.Helper()
moduleSchema, err := NewModuleSchema(objectTypes)
if err != nil {
@ -268,7 +268,7 @@ func RequireNewModuleSchema(t *testing.T, objectTypes []ObjectType) ModuleSchema
}
func TestModuleSchema_LookupType(t *testing.T) {
moduleSchema := RequireNewModuleSchema(t, []ObjectType{
moduleSchema := requireModuleSchema(t, []ObjectType{
{
Name: "object1",
KeyFields: []Field{
@ -295,14 +295,18 @@ func TestModuleSchema_LookupType(t *testing.T) {
}
}
func TestModuleSchema_ScanTypes(t *testing.T) {
moduleSchema := RequireNewModuleSchema(t, []ObjectType{
func exampleSchema(t *testing.T) ModuleSchema {
return requireModuleSchema(t, []ObjectType{
{
Name: "object1",
KeyFields: []Field{
{
Name: "field1",
Kind: StringKind,
Kind: EnumKind,
EnumType: EnumType{
Name: "enum2",
Values: []string{"d", "e", "f"},
},
},
},
},
@ -311,40 +315,94 @@ func TestModuleSchema_ScanTypes(t *testing.T) {
KeyFields: []Field{
{
Name: "field1",
Kind: StringKind,
Kind: EnumKind,
EnumType: EnumType{
Name: "enum1",
Values: []string{"a", "b", "c"},
},
},
},
},
})
}
var objectTypeNames []string
func TestModuleSchema_Types(t *testing.T) {
moduleSchema := exampleSchema(t)
var typeNames []string
moduleSchema.Types(func(typ Type) bool {
objectType, ok := typ.(ObjectType)
if !ok {
t.Fatalf("expected object type, got %T", typ)
}
objectTypeNames = append(objectTypeNames, objectType.Name)
typeNames = append(typeNames, typ.TypeName())
return true
})
expected := []string{"enum1", "enum2", "object1", "object2"}
if !reflect.DeepEqual(typeNames, expected) {
t.Fatalf("expected %v, got %v", expected, typeNames)
}
typeNames = nil
// scan just the first type and return false
moduleSchema.Types(func(typ Type) bool {
typeNames = append(typeNames, typ.TypeName())
return false
})
expected = []string{"enum1"}
if !reflect.DeepEqual(typeNames, expected) {
t.Fatalf("expected %v, got %v", expected, typeNames)
}
}
func TestModuleSchema_ObjectTypes(t *testing.T) {
moduleSchema := exampleSchema(t)
var typeNames []string
moduleSchema.ObjectTypes(func(typ ObjectType) bool {
typeNames = append(typeNames, typ.Name)
return true
})
expected := []string{"object1", "object2"}
if !reflect.DeepEqual(objectTypeNames, expected) {
t.Fatalf("expected object type names %v, got %v", expected, objectTypeNames)
if !reflect.DeepEqual(typeNames, expected) {
t.Fatalf("expected %v, got %v", expected, typeNames)
}
objectTypeNames = nil
typeNames = nil
// scan just the first type and return false
moduleSchema.Types(func(typ Type) bool {
objectType, ok := typ.(ObjectType)
if !ok {
t.Fatalf("expected object type, got %T", typ)
}
objectTypeNames = append(objectTypeNames, objectType.Name)
moduleSchema.ObjectTypes(func(typ ObjectType) bool {
typeNames = append(typeNames, typ.Name)
return false
})
expected = []string{"object1"}
if !reflect.DeepEqual(objectTypeNames, expected) {
t.Fatalf("expected object type names %v, got %v", expected, objectTypeNames)
if !reflect.DeepEqual(typeNames, expected) {
t.Fatalf("expected %v, got %v", expected, typeNames)
}
}
func TestModuleSchema_EnumTypes(t *testing.T) {
moduleSchema := exampleSchema(t)
var typeNames []string
moduleSchema.EnumTypes(func(typ EnumType) bool {
typeNames = append(typeNames, typ.Name)
return true
})
expected := []string{"enum1", "enum2"}
if !reflect.DeepEqual(typeNames, expected) {
t.Fatalf("expected %v, got %v", expected, typeNames)
}
typeNames = nil
// scan just the first type and return false
moduleSchema.EnumTypes(func(typ EnumType) bool {
typeNames = append(typeNames, typ.Name)
return false
})
expected = []string{"enum1"}
if !reflect.DeepEqual(typeNames, expected) {
t.Fatalf("expected %v, got %v", expected, typeNames)
}
}

View File

@ -27,6 +27,11 @@ type ObjectType struct {
RetainDeletions bool
}
// TypeName implements the Type interface.
func (o ObjectType) TypeName() string {
return o.Name
}
func (ObjectType) isType() {}
// Validate validates the object type.

View File

@ -170,7 +170,7 @@ func TestObjectType_Validate(t *testing.T) {
{
Name: "key",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "enum1",
Values: []string{"a", "b"},
},
@ -180,7 +180,7 @@ func TestObjectType_Validate(t *testing.T) {
{
Name: "value",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
EnumType: EnumType{
Name: "enum1",
Values: []string{"c", "b"},
},

View File

@ -1,7 +1,10 @@
package schema
// Type is an interface that all types in the schema implement.
// Currently these are ObjectType and EnumDefinition.
// Currently these are ObjectType and EnumType.
type Type interface {
// TypeName returns the type's name.
TypeName() string
isType()
}