mirror of
https://codeberg.org/readeck/readeck.git
synced 2026-06-18 11:04:36 +00:00
332 lines
8.6 KiB
Go
332 lines
8.6 KiB
Go
// SPDX-FileCopyrightText: © 2026 Olivier Meunier <olivier@neokraft.net>
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package forms
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/url"
|
|
"slices"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
_ Binder = (*Field[any, BaseValue[any]])(nil)
|
|
_ TypedBinder[any] = (*Field[any, BaseValue[any]])(nil)
|
|
_ ValidatorsProvider = (*Field[any, BaseValue[any]])(nil)
|
|
_ ContextHolder = (*Field[any, BaseValue[any]])(nil)
|
|
)
|
|
|
|
// ErrInvalidValue is the error for invalid value.
|
|
var ErrInvalidValue = errors.New("invalid value")
|
|
|
|
// FieldFlags is a field's flag list.
|
|
type FieldFlags byte
|
|
|
|
const (
|
|
// ValidatedField indicates a field has been validated.
|
|
ValidatedField FieldFlags = 1 << iota
|
|
)
|
|
|
|
type marshalledField[T any, V Valuer[T]] struct {
|
|
IsNil bool `json:"is_null"`
|
|
IsBound bool `json:"is_bound"`
|
|
Value V `json:"value"`
|
|
Errors Errors `json:"errors"`
|
|
}
|
|
|
|
// Binder describes a type that is "bound" to some data by the means of
|
|
// appropriate unmarshaling. It then provides informations about its state.
|
|
type Binder interface {
|
|
fmt.Stringer
|
|
IsBound() bool
|
|
IsEmpty() bool
|
|
IsNil() bool
|
|
Errors() Errors
|
|
V() any
|
|
}
|
|
|
|
// TypedBinder describes a [Binder] with its Value method.
|
|
type TypedBinder[T any] interface {
|
|
Binder
|
|
Value() T
|
|
}
|
|
|
|
// Field is a generic field that holds a value of the
|
|
// given type and implements [Binder].
|
|
// It's the common building block for a specialized field.
|
|
type Field[T any, V Valuer[T]] struct {
|
|
flags FieldFlags
|
|
name string
|
|
errors Errors
|
|
validators []Validator
|
|
ctx context.Context
|
|
value V
|
|
}
|
|
|
|
// Name returns the field's name.
|
|
func (f Field[T, V]) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
// SetName sets the field's name.
|
|
func (f *Field[T, V]) SetName(name string) {
|
|
f.name = name
|
|
}
|
|
|
|
// Value returns the field's [Valuer] value.
|
|
func (f Field[T, V]) Value() T {
|
|
return f.value.Value()
|
|
}
|
|
|
|
// V returns the field's value with "any" type.
|
|
func (f Field[T, V]) V() any {
|
|
return f.value.Value()
|
|
}
|
|
|
|
// String returns the field's [Valuer] string value.
|
|
func (f Field[T, V]) String() string {
|
|
return f.value.String()
|
|
}
|
|
|
|
// Set sets the [Valuer]'s value if it implements [Setter].
|
|
func (f *Field[T, V]) Set(v T) {
|
|
if t, ok := any(&f.value).(Setter[T]); ok {
|
|
t.Set(v)
|
|
}
|
|
}
|
|
|
|
// SetNil sets the [Valuer] to nil if it implements [Setter] and [FlagSetter].
|
|
// It only sets the nil flag and doesn't empty the value.
|
|
func (f *Field[T, V]) SetNil() {
|
|
if t, ok := any(&f.value).(interface {
|
|
Setter[T]
|
|
FlagSetter
|
|
}); ok {
|
|
t.SetFlags(IsEmpty | IsNil)
|
|
}
|
|
}
|
|
|
|
// Errors return the field's [Errors].
|
|
func (f Field[T, V]) Errors() Errors {
|
|
return slices.Collect(IterErrorsTr(f.ctx, f.errors))
|
|
}
|
|
|
|
func (f Field[T, V]) hasErrors() bool {
|
|
return len(f.errors) > 0
|
|
}
|
|
|
|
// AddErrors add errors to the field.
|
|
func (f *Field[T, V]) AddErrors(errs ...error) {
|
|
for _, err := range errs {
|
|
if err != nil {
|
|
f.errors = append(f.errors, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetContext sets the field's context. It implements [ContextHolder].
|
|
func (f *Field[T, V]) SetContext(ctx context.Context) {
|
|
f.ctx = ctx
|
|
}
|
|
|
|
// IsBound returns true if the field is bound.
|
|
func (f Field[T, V]) IsBound() bool {
|
|
return f.value.IsBound()
|
|
}
|
|
|
|
// IsNil returns true if the field's value is null.
|
|
func (f Field[T, V]) IsNil() bool {
|
|
return f.value.IsNil()
|
|
}
|
|
|
|
// IsEmpty returns true if the field's value is empty.
|
|
func (f Field[T, V]) IsEmpty() bool {
|
|
return f.value.IsEmpty()
|
|
}
|
|
|
|
// Validators implements [ValidatorsProvider] and returns
|
|
// the value's validators.
|
|
func (f Field[T, V]) Validators() []Validator {
|
|
return f.validators
|
|
}
|
|
|
|
// SetValidators implements [ValidatorsProvider]
|
|
// and sets the field's validators.
|
|
func (f *Field[T, V]) SetValidators(validators []Validator) {
|
|
f.validators = validators
|
|
}
|
|
|
|
// ApplyValidators applies the given validators to the field
|
|
// and add the resulting errors to the field's error list.
|
|
// It can run at any time, including during a form or field
|
|
// custom validation.
|
|
func (f *Field[T, V]) ApplyValidators(validators ...Validator) {
|
|
f.AddErrors(ApplyValidators[T](f, f.Value(), validators...)...)
|
|
}
|
|
|
|
// UnmarshalJSON implements [json.Unmarshaler]. It always returns nil
|
|
// and errors, if any, are added to the field's error list.
|
|
func (f *Field[T, V]) UnmarshalJSON(in []byte) error {
|
|
if v, ok := any(&f.value).(ValidatorsProvider); ok {
|
|
v.SetValidators(f.validators)
|
|
}
|
|
|
|
if err := json.Unmarshal(in, &f.value); err != nil {
|
|
f.AddErrors(ErrInvalidValue)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalValues implement [ValuesUnmarshaler]. It always returns nil
|
|
// and errors, if any, are added to the field's error list.
|
|
func (f *Field[T, V]) UnmarshalValues(values []string) error {
|
|
if v, ok := any(&f.value).(ValidatorsProvider); ok {
|
|
v.SetValidators(f.validators)
|
|
}
|
|
|
|
if err := UnmarshalValues(values, &f.value); err != nil {
|
|
f.AddErrors(ErrInvalidValue)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalFiles implements [FilesUnmarshaler]. It only works on
|
|
// [Valuer]s implementing [FilesUnmarshaler] themselves.
|
|
func (f *Field[T, V]) UnmarshalFiles(files []*multipart.FileHeader) error {
|
|
if v, ok := any(&f.value).(ValidatorsProvider); ok {
|
|
v.SetValidators(f.validators)
|
|
}
|
|
|
|
if v, ok := any(&f.value).(FilesUnmarshaler); ok {
|
|
if err := v.UnmarshalFiles(files); err != nil {
|
|
f.AddErrors(ErrInvalidValue)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON implements [json.Marshaler].
|
|
func (f Field[T, V]) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(marshalledField[T, V]{
|
|
IsNil: f.IsNil(),
|
|
IsBound: f.IsBound(),
|
|
Value: f.value,
|
|
Errors: slices.Collect(IterErrorsTr(f.ctx, f.errors)),
|
|
})
|
|
}
|
|
|
|
// IsValid applies the field's [FieldValidator] and [ValueValidator].
|
|
// Each returned error is added to the field's error list.
|
|
// It returns false when the error list is not empty.
|
|
func (f *Field[T, V]) IsValid() bool {
|
|
if f.flags&ValidatedField > 0 {
|
|
return !f.hasErrors()
|
|
}
|
|
|
|
defer func() {
|
|
f.flags |= ValidatedField
|
|
}()
|
|
|
|
if err := ApplyValidators[T](f, f.Value(), f.validators...); len(err) > 0 {
|
|
f.AddErrors(err...)
|
|
}
|
|
|
|
return !f.hasErrors()
|
|
}
|
|
|
|
// ChoicesField implements [ChoicesProvider]. It can be used to augment
|
|
// a [Field] for comparable types.
|
|
type ChoicesField[T comparable] struct {
|
|
choices ValueChoices[T]
|
|
}
|
|
|
|
// Choices returns the stored choices.
|
|
func (f *ChoicesField[T]) Choices() ValueChoices[T] {
|
|
return f.choices
|
|
}
|
|
|
|
// SetChoices sets the stored choices.
|
|
func (f *ChoicesField[T]) SetChoices(choices ValueChoices[T]) {
|
|
f.choices = choices
|
|
}
|
|
|
|
// TextField is a field that holds a [string] value.
|
|
type TextField struct {
|
|
Field[string, StringValue]
|
|
ChoicesField[string]
|
|
}
|
|
|
|
// BooleanField is a field that holds a [bool] value.
|
|
type BooleanField = Field[bool, BooleanValue]
|
|
|
|
// NumberField is a field that holds a given number type.
|
|
type NumberField[T numberType] struct {
|
|
Field[T, NumberValue[T]]
|
|
ChoicesField[T]
|
|
}
|
|
|
|
// IntegerField is a field that holds an [int] value.
|
|
type IntegerField = NumberField[int]
|
|
|
|
// DatetimeField is a field that holds a [time.Time] value.
|
|
type DatetimeField = Field[time.Time, DatetimeValue]
|
|
|
|
// URLField is a field that holds a [url.URL] value.
|
|
type URLField = Field[url.URL, URLValue]
|
|
|
|
/* List fields
|
|
--------------------------------------------------------------- */
|
|
|
|
// ListField is a list field of items T with a matching [Valuer].
|
|
// A ListField is not necessary for unmarshaling and you can simply
|
|
// use Field[[]T, ListValue[T, V]] for it to work. This type, however,
|
|
// applies the validators to each item.
|
|
type ListField[T any, V Valuer[[]T]] struct {
|
|
Field[[]T, V]
|
|
}
|
|
|
|
// IsValid first applies [Field.IsValid] and then perform the validation
|
|
// on each item.
|
|
func (f *ListField[T, V]) IsValid() bool {
|
|
if f.flags&ValidatedField > 0 {
|
|
return !f.hasErrors()
|
|
}
|
|
|
|
if !f.Field.IsValid() { // note: this will add the [ValidatedField] flag.
|
|
return false
|
|
}
|
|
|
|
for _, val := range f.value.Value() {
|
|
f.AddErrors(ApplyValidators[T](f, val, f.validators...)...)
|
|
}
|
|
|
|
return !f.hasErrors()
|
|
}
|
|
|
|
// TextListField is a field that holds a list of [string] values.
|
|
type TextListField struct {
|
|
ListField[string, ListValue[string, StringValue]]
|
|
ChoicesField[string]
|
|
}
|
|
|
|
// NumberListField is a field that holds a list of number values.
|
|
type NumberListField[T numberType] struct {
|
|
ListField[T, ListValue[T, NumberValue[T]]]
|
|
ChoicesField[T]
|
|
}
|
|
|
|
// IntegerListField is a field that holds a list of [int] values.
|
|
type IntegerListField = NumberListField[int]
|
|
|
|
// DatetimeListField is a field that holds a list of [time.Time] values.
|
|
type DatetimeListField = ListField[time.Time, ListValue[time.Time, DatetimeValue]]
|
|
|
|
// URLListField is a field that holds a list of [url.URL] values.
|
|
type URLListField = ListField[url.URL, PivotListValue[url.URL, URLValue, string, StringValue]]
|