feat(schema)!: updates based on postgres testing (#20858)

This commit is contained in:
Aaron Craelius 2024-07-04 21:46:42 +02:00 committed by GitHub
parent 54586b2f4d
commit 20f96db224
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 217 additions and 117 deletions

View File

@ -5,6 +5,9 @@ import "fmt"
// EnumDefinition represents the definition of an enum type.
type EnumDefinition 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
// definition is identical each time
Name string
// Values is a list of distinct, non-empty values that are part of the enum type.
@ -44,3 +47,31 @@ func (e EnumDefinition) ValidateValue(value string) error {
}
return fmt.Errorf("value %q is not a valid enum value for %s", value, e.Name)
}
// checkEnumCompatibility checks that the enum values are consistent across object types and fields.
func checkEnumCompatibility(enumValueMap map[string]map[string]bool, field Field) error {
if field.Kind != EnumKind {
return nil
}
enum := field.EnumDefinition
if existing, ok := enumValueMap[enum.Name]; ok {
if len(existing) != len(enum.Values) {
return fmt.Errorf("enum %q has different number of values in different object types", enum.Name)
}
for _, value := range enum.Values {
if !existing[value] {
return fmt.Errorf("enum %q has different values in different object types", enum.Name)
}
}
} else {
valueMap := map[string]bool{}
for _, value := range enum.Values {
valueMap[value] = true
}
enumValueMap[enum.Name] = valueMap
}
return nil
}

View File

@ -10,10 +10,11 @@ type Field struct {
// Kind is the basic type of the field.
Kind Kind
// Nullable indicates whether null values are accepted for the field.
// Nullable indicates whether null values are accepted for the field. Key fields CANNOT be nullable.
Nullable bool
// AddressPrefix is the address prefix of the field's kind, currently only used for Bech32AddressKind.
// TODO: in a future update, stricter criteria and validation for address prefixes should be added
AddressPrefix string
// EnumDefinition is the definition of the enum type and is only valid when Kind is EnumKind.

View File

@ -2,15 +2,15 @@ package schema
import "fmt"
// ValidateForKeyFields validates that the value conforms to the set of fields as a Key in an ObjectUpdate.
// ValidateObjectKey validates that the value conforms to the set of fields as a Key in an ObjectUpdate.
// See ObjectUpdate.Key for documentation on the requirements of such keys.
func ValidateForKeyFields(keyFields []Field, value interface{}) error {
func ValidateObjectKey(keyFields []Field, value interface{}) error {
return validateFieldsValue(keyFields, value)
}
// ValidateForValueFields validates that the value conforms to the set of fields as a Value in an ObjectUpdate.
// ValidateObjectValue validates that the value conforms to the set of fields as a Value in an ObjectUpdate.
// See ObjectUpdate.Value for documentation on the requirements of such values.
func ValidateForValueFields(valueFields []Field, value interface{}) error {
func ValidateObjectValue(valueFields []Field, value interface{}) error {
valueUpdates, ok := value.(ValueUpdates)
if !ok {
return validateFieldsValue(valueFields, value)

View File

@ -56,7 +56,7 @@ func TestValidateForKeyFields(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateForKeyFields(tt.keyFields, tt.key)
err := ValidateObjectKey(tt.keyFields, tt.key)
if tt.errContains == "" {
if err != nil {
t.Fatalf("unexpected error: %v", err)
@ -128,7 +128,7 @@ func TestValidateForValueFields(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateForValueFields(tt.valueFields, tt.value)
err := ValidateObjectValue(tt.valueFields, tt.value)
if tt.errContains == "" {
if err != nil {
t.Fatalf("unexpected error: %v", err)

View File

@ -5,6 +5,7 @@ import (
"fmt"
"regexp"
"time"
"unicode/utf8"
)
// Kind represents the basic type of a field in an object.
@ -16,7 +17,8 @@ const (
// InvalidKind indicates that an invalid type.
InvalidKind Kind = iota
// StringKind is a string type and values of this type must be of the go type string.
// StringKind is a string type and values of this type must be of the go type string
// containing valid UTF-8 and cannot contain null characters.
StringKind
// BytesKind is a bytes type and values of this type must be of the go type []byte.
@ -46,14 +48,14 @@ const (
// Uint64Kind is a uint64 type and values of this type must be of the go type uint64.
Uint64Kind
// IntegerKind represents an arbitrary precision integer number. Values of this type must
// IntegerStringKind represents an arbitrary precision integer number. Values of this type must
// be of the go type string and formatted as base10 integers, specifically matching to
// the IntegerFormat regex.
IntegerKind
IntegerStringKind
// DecimalKind represents an arbitrary precision decimal or integer number. Values of this type
// DecimalStringKind represents an arbitrary precision decimal or integer number. Values of this type
// must be of the go type string and match the DecimalFormat regex.
DecimalKind
DecimalStringKind
// BoolKind is a boolean type and values of this type must be of the go type bool.
BoolKind
@ -134,9 +136,9 @@ func (t Kind) String() string {
return "int64"
case Uint64Kind:
return "uint64"
case DecimalKind:
case DecimalStringKind:
return "decimal"
case IntegerKind:
case IntegerStringKind:
return "integer"
case BoolKind:
return "bool"
@ -216,13 +218,13 @@ func (t Kind) ValidateValueType(value interface{}) error {
if !ok {
return fmt.Errorf("expected uint64, got %T", value)
}
case IntegerKind:
case IntegerStringKind:
_, ok := value.(string)
if !ok {
return fmt.Errorf("expected string, got %T", value)
}
case DecimalKind:
case DecimalStringKind:
_, ok := value.(string)
if !ok {
return fmt.Errorf("expected string, got %T", value)
@ -283,11 +285,23 @@ func (t Kind) ValidateValue(value interface{}) error {
}
switch t {
case IntegerKind:
case StringKind:
str := value.(string)
if !utf8.ValidString(str) {
return fmt.Errorf("expected valid utf-8 string, got %s", value)
}
// check for null characters
for _, r := range str {
if r == 0 {
return fmt.Errorf("expected string without null characters, got %s", value)
}
}
case IntegerStringKind:
if !integerRegex.Match([]byte(value.(string))) {
return fmt.Errorf("expected base10 integer, got %s", value)
}
case DecimalKind:
case DecimalStringKind:
if !decimalRegex.Match([]byte(value.(string))) {
return fmt.Errorf("expected decimal number, got %s", value)
}
@ -307,7 +321,7 @@ var (
)
// KindForGoValue finds the simplest kind that can represent the given go value. It will not, however,
// return kinds such as IntegerKind, DecimalKind, Bech32AddressKind, or EnumKind which all can be
// return kinds such as IntegerStringKind, DecimalStringKind, Bech32AddressKind, or EnumKind which all can be
// represented as strings.
func KindForGoValue(value interface{}) Kind {
switch value.(type) {

View File

@ -53,12 +53,12 @@ func TestKind_ValidateValueType(t *testing.T) {
{kind: Int64Kind, value: int32(1), valid: false},
{kind: Uint64Kind, value: uint64(1), valid: true},
{kind: Uint64Kind, value: uint32(1), valid: false},
{kind: IntegerKind, value: "1", valid: true},
{kind: IntegerKind, value: int32(1), valid: false},
{kind: DecimalKind, value: "1.0", valid: true},
{kind: DecimalKind, value: "1", valid: true},
{kind: DecimalKind, value: "1.1e4", valid: true},
{kind: DecimalKind, value: int32(1), valid: false},
{kind: IntegerStringKind, value: "1", valid: true},
{kind: IntegerStringKind, value: int32(1), valid: false},
{kind: DecimalStringKind, value: "1.0", valid: true},
{kind: DecimalStringKind, value: "1", valid: true},
{kind: DecimalStringKind, value: "1.1e4", valid: true},
{kind: DecimalStringKind, value: int32(1), valid: false},
{kind: Bech32AddressKind, value: []byte("hello"), valid: true},
{kind: Bech32AddressKind, value: 1, valid: false},
{kind: BoolKind, value: true, valid: true},
@ -110,55 +110,59 @@ func TestKind_ValidateValue(t *testing.T) {
{Int64Kind, int64(1), true},
{Int32Kind, "abc", false},
{BytesKind, nil, false},
// string must be valid UTF-8
{StringKind, string([]byte{0xff, 0xfe, 0xfd}), false},
// strings with null characters are invalid
{StringKind, string([]byte{1, 2, 0, 3}), false},
// check integer, decimal and json more thoroughly
{IntegerKind, "1", true},
{IntegerKind, "0", true},
{IntegerKind, "10", true},
{IntegerKind, "-100", true},
{IntegerKind, "1.0", false},
{IntegerKind, "00", true}, // leading zeros are allowed
{IntegerKind, "001", true},
{IntegerKind, "-01", true},
{IntegerStringKind, "1", true},
{IntegerStringKind, "0", true},
{IntegerStringKind, "10", true},
{IntegerStringKind, "-100", true},
{IntegerStringKind, "1.0", false},
{IntegerStringKind, "00", true}, // leading zeros are allowed
{IntegerStringKind, "001", true},
{IntegerStringKind, "-01", true},
// 100 digits
{IntegerKind, "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", true},
{IntegerStringKind, "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", true},
// more than 100 digits
{IntegerKind, "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", false},
{IntegerKind, "", false},
{IntegerKind, "abc", false},
{IntegerKind, "abc100", false},
{DecimalKind, "1.0", true},
{DecimalKind, "0.0", true},
{DecimalKind, "-100.075", true},
{DecimalKind, "1002346.000", true},
{DecimalKind, "0", true},
{DecimalKind, "10", true},
{DecimalKind, "-100", true},
{DecimalKind, "1", true},
{DecimalKind, "1.0e4", true},
{DecimalKind, "1.0e-4", true},
{DecimalKind, "1.0e+4", true},
{DecimalKind, "1.0e", false},
{DecimalKind, "1.0e4.0", false},
{DecimalKind, "1.0e-4.0", false},
{DecimalKind, "1.0e+4.0", false},
{DecimalKind, "-1.0e-4", true},
{DecimalKind, "-1.0e+4", true},
{DecimalKind, "-1.0E4", true},
{DecimalKind, "1E-9", true},
{DecimalKind, "1E-99", true},
{DecimalKind, "1E+9", true},
{DecimalKind, "1E+99", true},
{IntegerStringKind, "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", false},
{IntegerStringKind, "", false},
{IntegerStringKind, "abc", false},
{IntegerStringKind, "abc100", false},
{DecimalStringKind, "1.0", true},
{DecimalStringKind, "0.0", true},
{DecimalStringKind, "-100.075", true},
{DecimalStringKind, "1002346.000", true},
{DecimalStringKind, "0", true},
{DecimalStringKind, "10", true},
{DecimalStringKind, "-100", true},
{DecimalStringKind, "1", true},
{DecimalStringKind, "1.0e4", true},
{DecimalStringKind, "1.0e-4", true},
{DecimalStringKind, "1.0e+4", true},
{DecimalStringKind, "1.0e", false},
{DecimalStringKind, "1.0e4.0", false},
{DecimalStringKind, "1.0e-4.0", false},
{DecimalStringKind, "1.0e+4.0", false},
{DecimalStringKind, "-1.0e-4", true},
{DecimalStringKind, "-1.0e+4", true},
{DecimalStringKind, "-1.0E4", true},
{DecimalStringKind, "1E-9", true},
{DecimalStringKind, "1E-99", true},
{DecimalStringKind, "1E+9", true},
{DecimalStringKind, "1E+99", true},
// 50 digits before and after the decimal point
{DecimalKind, "10000000000000000000000000000000000000000000000000.10000000000000000000000000000000000000000000000001", true},
{DecimalStringKind, "10000000000000000000000000000000000000000000000000.10000000000000000000000000000000000000000000000001", true},
// too many digits before the decimal point
{DecimalKind, "10000000000000000000000000000000000000000000000000000000000000000000000000", false},
{DecimalStringKind, "10000000000000000000000000000000000000000000000000000000000000000000000000", false},
// too many digits after the decimal point
{DecimalKind, "1.0000000000000000000000000000000000000000000000000000000000000000000000001", false},
{DecimalStringKind, "1.0000000000000000000000000000000000000000000000000000000000000000000000001", false},
// exponent too big
{DecimalKind, "1E-999", false},
{DecimalKind, "", false},
{DecimalKind, "abc", false},
{DecimalKind, "abc", false},
{DecimalStringKind, "1E-999", false},
{DecimalStringKind, "", false},
{DecimalStringKind, "abc", false},
{DecimalStringKind, "abc", false},
{JSONKind, json.RawMessage(`{"a":10}`), true},
{JSONKind, json.RawMessage("10"), true},
{JSONKind, json.RawMessage("10.0"), true},
@ -200,8 +204,8 @@ func TestKind_String(t *testing.T) {
{Uint32Kind, "uint32"},
{Int64Kind, "int64"},
{Uint64Kind, "uint64"},
{IntegerKind, "integer"},
{DecimalKind, "decimal"},
{IntegerStringKind, "integer"},
{DecimalStringKind, "decimal"},
{BoolKind, "bool"},
{TimeKind, "time"},
{DurationKind, "duration"},

View File

@ -10,56 +10,13 @@ type ModuleSchema struct {
// Validate validates the module schema.
func (s ModuleSchema) Validate() error {
enumValueMap := map[string]map[string]bool{}
for _, objType := range s.ObjectTypes {
if err := objType.Validate(); err != nil {
if err := objType.validate(enumValueMap); err != nil {
return err
}
}
// validate that shared enum types are consistent across object types
enumValueMap := map[string]map[string]bool{}
for _, objType := range s.ObjectTypes {
for _, field := range objType.KeyFields {
err := checkEnum(enumValueMap, field)
if err != nil {
return err
}
}
for _, field := range objType.ValueFields {
err := checkEnum(enumValueMap, field)
if err != nil {
return err
}
}
}
return nil
}
func checkEnum(enumValueMap map[string]map[string]bool, field Field) error {
if field.Kind != EnumKind {
return nil
}
enum := field.EnumDefinition
if existing, ok := enumValueMap[enum.Name]; ok {
if len(existing) != len(enum.Values) {
return fmt.Errorf("enum %q has different number of values in different object types", enum.Name)
}
for _, value := range enum.Values {
if !existing[value] {
return fmt.Errorf("enum %q has different values in different object types", enum.Name)
}
}
} else {
valueMap := map[string]bool{}
for _, value := range enum.Values {
valueMap[value] = true
}
enumValueMap[enum.Name] = valueMap
}
return nil
}

View File

@ -110,6 +110,39 @@ func TestModuleSchema_Validate(t *testing.T) {
},
errContains: "different values",
},
{
name: "same enum",
moduleSchema: ModuleSchema{
ObjectTypes: []ObjectType{
{
Name: "object1",
KeyFields: []Field{
{
Name: "k",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
Name: "enum1",
Values: []string{"a", "b"},
},
},
},
},
{
Name: "object2",
KeyFields: []Field{
{
Name: "k",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
Name: "enum1",
Values: []string{"a", "b"},
},
},
},
},
},
},
},
}
for _, tt := range tests {

View File

@ -11,7 +11,7 @@ type ObjectType struct {
// KeyFields is a list of fields that make up the primary key of the object.
// It can be empty in which case indexers should assume that this object is
// a singleton and only has one value. Field names must be unique within the
// object between both key and value fields.
// object between both key and value fields. Key fields CANNOT be nullable.
KeyFields []Field
// ValueFields is a list of fields that are not part of the primary key of the object.
@ -29,20 +29,35 @@ type ObjectType struct {
// Validate validates the object type.
func (o ObjectType) Validate() error {
return o.validate(map[string]map[string]bool{})
}
// validate validates the object type with an enumValueMap that can be
// shared across a whole module schema.
func (o ObjectType) validate(enumValueMap map[string]map[string]bool) error {
if !ValidateName(o.Name) {
return fmt.Errorf("invalid object type name %q", o.Name)
}
fieldNames := map[string]bool{}
for _, field := range o.KeyFields {
if err := field.Validate(); err != nil {
return fmt.Errorf("invalid key field %q: %w", field.Name, err)
}
if field.Nullable {
return fmt.Errorf("key field %q cannot be nullable", field.Name)
}
if fieldNames[field.Name] {
return fmt.Errorf("duplicate field name %q", field.Name)
}
fieldNames[field.Name] = true
if err := checkEnumCompatibility(enumValueMap, field); err != nil {
return err
}
}
for _, field := range o.ValueFields {
@ -54,6 +69,10 @@ func (o ObjectType) Validate() error {
return fmt.Errorf("duplicate field name %q", field.Name)
}
fieldNames[field.Name] = true
if err := checkEnumCompatibility(enumValueMap, field); err != nil {
return err
}
}
if len(o.KeyFields) == 0 && len(o.ValueFields) == 0 {
@ -69,7 +88,7 @@ func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error {
return fmt.Errorf("object type name %q does not match update type name %q", o.Name, update.TypeName)
}
if err := ValidateForKeyFields(o.KeyFields, update.Key); err != nil {
if err := ValidateObjectKey(o.KeyFields, update.Key); err != nil {
return fmt.Errorf("invalid key for object type %q: %w", update.TypeName, err)
}
@ -77,5 +96,5 @@ func (o ObjectType) ValidateObjectUpdate(update ObjectUpdate) error {
return nil
}
return ValidateForValueFields(o.ValueFields, update.Value)
return ValidateObjectValue(o.ValueFields, update.Value)
}

View File

@ -148,6 +148,47 @@ func TestObjectType_Validate(t *testing.T) {
},
errContains: "duplicate field name",
},
{
name: "nullable key field",
objectType: ObjectType{
Name: "objectNullKey",
KeyFields: []Field{
{
Name: "field1",
Kind: StringKind,
Nullable: true,
},
},
},
errContains: "key field \"field1\" cannot be nullable",
},
{
name: "duplicate incompatible enum",
objectType: ObjectType{
Name: "objectWithEnums",
KeyFields: []Field{
{
Name: "key",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
Name: "enum1",
Values: []string{"a", "b"},
},
},
},
ValueFields: []Field{
{
Name: "value",
Kind: EnumKind,
EnumDefinition: EnumDefinition{
Name: "enum1",
Values: []string{"c", "b"},
},
},
},
},
errContains: "enum \"enum1\" has different values",
},
}
for _, tt := range tests {