schema/fields: validate for slices builder (#3566)

This PR changes the way slice types are built and adds the possibility to add a custom validation function to json slice types.
This commit is contained in:
Jannik Clausen
2023-05-30 10:02:27 +02:00
committed by GitHub
parent a8851db571
commit 9f917c7263
15 changed files with 894 additions and 37 deletions

View File

@@ -96,18 +96,18 @@ func JSON(name string, typ any) *jsonBuilder {
}
// Strings returns a new JSON Field with type []string.
func Strings(name string) *jsonBuilder {
return JSON(name, []string{})
func Strings(name string) *sliceBuilder[string] {
return sb[string](name)
}
// Ints returns a new JSON Field with type []int.
func Ints(name string) *jsonBuilder {
return JSON(name, []int{})
func Ints(name string) *sliceBuilder[int] {
return sb[int](name)
}
// Floats returns a new JSON Field with type []float.
func Floats(name string) *jsonBuilder {
return JSON(name, []float64{})
func Floats(name string) *sliceBuilder[float64] {
return sb[float64](name)
}
// Any returns a new JSON Field with type any. Although this field type can be
@@ -809,6 +809,120 @@ func (b *jsonBuilder) Descriptor() *Descriptor {
return b.desc
}
type (
sliceType interface {
int | string | float64
}
// sliceBuilder is the builder for string slice fields.
sliceBuilder[T sliceType] struct {
*jsonBuilder
}
)
// Validate adds a validator for this field. Operation fails if the validation fails.
func (b *sliceBuilder[T]) Validate(fn func([]T) error) *sliceBuilder[T] {
b.desc.Validators = append(b.desc.Validators, fn)
return b
}
// StorageKey sets the storage key of the field.
// In SQL dialects is the column name and Gremlin is the property.
func (b *sliceBuilder[T]) StorageKey(key string) *sliceBuilder[T] {
b.desc.StorageKey = key
return b
}
// Optional indicates that this field is optional on create.
// Unlike edges, fields are required by default.
func (b *sliceBuilder[T]) Optional() *sliceBuilder[T] {
b.desc.Optional = true
return b
}
// Immutable indicates that this field cannot be updated.
func (b *sliceBuilder[T]) Immutable() *sliceBuilder[T] {
b.desc.Immutable = true
return b
}
// Comment sets the comment of the field.
func (b *sliceBuilder[T]) Comment(c string) *sliceBuilder[T] {
b.desc.Comment = c
return b
}
// Sensitive fields not printable and not serializable.
func (b *sliceBuilder[T]) Sensitive() *sliceBuilder[T] {
b.desc.Sensitive = true
return b
}
// StructTag sets the struct tag of the field.
func (b *sliceBuilder[T]) StructTag(s string) *sliceBuilder[T] {
b.desc.Tag = s
return b
}
// SchemaType overrides the default database type with a custom
// schema type (per dialect) for json.
//
// field.Strings("strings").
// SchemaType(map[string]string{
// dialect.MySQL: "json",
// dialect.Postgres: "jsonb",
// })
func (b *sliceBuilder[T]) SchemaType(types map[string]string) *sliceBuilder[T] {
b.desc.SchemaType = types
return b
}
// Annotations adds a list of annotations to the field object to be used by
// codegen extensions.
func (b *sliceBuilder[T]) Annotations(annotations ...schema.Annotation) *sliceBuilder[T] {
b.desc.Annotations = append(b.desc.Annotations, annotations...)
return b
}
// Default sets the default value of the field. For example:
//
// field.Strings("names").
// Default([]string{"a8m", "masseelch"})
func (b *sliceBuilder[T]) Default(v []T) *sliceBuilder[T] {
b.desc.Default = v
return b
}
// Descriptor implements the ent.Field interface by returning its descriptor.
func (b *sliceBuilder[T]) Descriptor() *Descriptor {
return b.desc
}
// sb is a generic helper method to share code between Strings, Ints and Floats builder.
func sb[T sliceType](name string) *sliceBuilder[T] {
var typ []T
b := &jsonBuilder{&Descriptor{
Name: name,
Info: &TypeInfo{
Type: TypeJSON,
},
}}
t := reflect.TypeOf(typ)
if t == nil {
b.desc.Err = errors.New("expect a Go value as JSON type but got nil")
return &sliceBuilder[T]{b}
}
b.desc.Info.Ident = t.String()
b.desc.Info.PkgPath = t.PkgPath()
b.desc.goType(typ)
b.desc.checkGoType(t)
switch t.Kind() {
case reflect.Slice, reflect.Array, reflect.Ptr, reflect.Map:
b.desc.Info.Nillable = true
b.desc.Info.PkgPath = pkgPath(t)
}
return &sliceBuilder[T]{b}
}
// enumBuilder is the builder for enum fields.
type enumBuilder struct {
desc *Descriptor

View File

@@ -346,6 +346,50 @@ func TestString_ValueScanner(t *testing.T) {
require.True(t, ok)
}
func TestSlices(t *testing.T) {
fd := field.Strings("strings").
Default([]string{}).
Comment("comment").
Validate(func(xs []string) error {
return nil
}).
Descriptor()
assert.Equal(t, "strings", fd.Name)
assert.Equal(t, field.TypeJSON, fd.Info.Type)
assert.NotNil(t, fd.Default)
assert.Equal(t, []string{}, fd.Default)
assert.Equal(t, "comment", fd.Comment)
assert.Len(t, fd.Validators, 1)
fd = field.Ints("ints").
Default([]int{}).
Comment("comment").
Validate(func(xs []int) error {
return nil
}).
Descriptor()
assert.Equal(t, "ints", fd.Name)
assert.Equal(t, field.TypeJSON, fd.Info.Type)
assert.NotNil(t, fd.Default)
assert.Equal(t, []int{}, fd.Default)
assert.Equal(t, "comment", fd.Comment)
assert.Len(t, fd.Validators, 1)
fd = field.Floats("floats").
Default([]float64{}).
Comment("comment").
Validate(func(xs []float64) error {
return nil
}).
Descriptor()
assert.Equal(t, "floats", fd.Name)
assert.Equal(t, field.TypeJSON, fd.Info.Type)
assert.NotNil(t, fd.Default)
assert.Equal(t, []float64{}, fd.Default)
assert.Equal(t, "comment", fd.Comment)
assert.Len(t, fd.Validators, 1)
}
type VString string
func (s *VString) Scan(any) error {