mirror of
https://codeberg.org/readeck/readeck.git
synced 2026-06-18 11:04:36 +00:00
273 lines
4.8 KiB
Go
273 lines
4.8 KiB
Go
// SPDX-FileCopyrightText: © 2024 Olivier Meunier <olivier@neokraft.net>
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package forms
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/araddon/dateparse"
|
|
)
|
|
|
|
// nilText is a text null value.
|
|
// In an URL of form value, it would be a field with %EF%BC%80 value.
|
|
// On an HTML field, it's simply ＀.
|
|
const nilText = "\uff00"
|
|
|
|
// ValueFlags holds the flags a value can get.
|
|
type ValueFlags int8
|
|
|
|
const (
|
|
// IsOk must be true when a field was successfully decoded.
|
|
IsOk ValueFlags = 1 << iota
|
|
// IsBound is the flag for a bound value.
|
|
IsBound
|
|
// IsNil is the flag for a nil value.
|
|
IsNil
|
|
// IsEmpty is the flag for an empty value.
|
|
IsEmpty
|
|
)
|
|
|
|
// IsOk returns true when [IsOk] is present in the flags.
|
|
func (f ValueFlags) IsOk() bool {
|
|
return f&IsOk > 0
|
|
}
|
|
|
|
// IsBound returns true when [IsBound] is present in the flags.
|
|
func (f ValueFlags) IsBound() bool {
|
|
return f&IsBound > 0
|
|
}
|
|
|
|
// IsNil returns true when [IsNil] is present in the flags.
|
|
func (f ValueFlags) IsNil() bool {
|
|
return f&IsNil > 0
|
|
}
|
|
|
|
// IsEmpty returns true when [IsEmpty] is present in the flags.
|
|
func (f ValueFlags) IsEmpty() bool {
|
|
return f&IsEmpty > 0
|
|
}
|
|
|
|
// Value is the value holder.
|
|
type Value[T any] struct {
|
|
F ValueFlags
|
|
V T
|
|
}
|
|
|
|
// NewValue returns a new, empty [Value].
|
|
func NewValue[T any]() (value Value[T]) {
|
|
value.F = IsEmpty
|
|
return
|
|
}
|
|
|
|
func (v *Value[T]) set(value T) {
|
|
v.F = IsOk
|
|
v.V = value
|
|
}
|
|
|
|
// Decoder describes a typed value decoder.
|
|
type Decoder[T any] interface {
|
|
DecodeAny(data any) Value[T]
|
|
DecodeText(text string) Value[T]
|
|
}
|
|
|
|
type valueStringer[T any] interface {
|
|
ValueString(v T) string
|
|
}
|
|
|
|
type valueDecoder[T any] struct {
|
|
decodeAny func(data any) Value[T]
|
|
decodeText func(text string) Value[T]
|
|
stringer func(T) string
|
|
}
|
|
|
|
func (d valueDecoder[T]) DecodeAny(data any) Value[T] {
|
|
return d.decodeAny(data)
|
|
}
|
|
|
|
func (d valueDecoder[T]) DecodeText(text string) Value[T] {
|
|
return d.decodeText(text)
|
|
}
|
|
|
|
func (d valueDecoder[T]) ValueString(v T) string {
|
|
if d.stringer != nil {
|
|
return d.stringer(v)
|
|
}
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
|
|
// NewValueDecoder creates a [Decoder] with a decoder functions pair.
|
|
func NewValueDecoder[T any](
|
|
decodeAny func(data any) Value[T],
|
|
decodeText func(text string) Value[T],
|
|
stringer func(T) string,
|
|
) Decoder[T] {
|
|
return valueDecoder[T]{
|
|
decodeAny: decodeAny,
|
|
decodeText: decodeText,
|
|
stringer: stringer,
|
|
}
|
|
}
|
|
|
|
// DecodeString is a [Value] decoder for strings.
|
|
var DecodeString = NewValueDecoder(
|
|
func(data any) Value[string] {
|
|
value := NewValue[string]()
|
|
|
|
if data == nil {
|
|
value.F |= IsNil
|
|
return value
|
|
}
|
|
|
|
if V, ok := data.(string); ok {
|
|
value.F |= IsOk
|
|
if V != "" {
|
|
value.set(V)
|
|
}
|
|
}
|
|
|
|
return value
|
|
},
|
|
func(text string) Value[string] {
|
|
value := NewValue[string]()
|
|
if text != "" {
|
|
value.set(text)
|
|
} else {
|
|
value.F |= IsOk
|
|
}
|
|
return value
|
|
},
|
|
func(v string) string {
|
|
return v
|
|
},
|
|
)
|
|
|
|
// DecodeBoolean is a [Value] decoder for booleans.
|
|
var DecodeBoolean = NewValueDecoder(
|
|
func(data any) Value[bool] {
|
|
value := NewValue[bool]()
|
|
|
|
if data == nil {
|
|
value.F |= IsNil
|
|
return value
|
|
}
|
|
|
|
if V, ok := data.(bool); ok {
|
|
value.set(V)
|
|
}
|
|
|
|
return value
|
|
},
|
|
func(text string) Value[bool] {
|
|
value := NewValue[bool]()
|
|
|
|
switch text {
|
|
case "":
|
|
value.F |= IsNil
|
|
return value
|
|
case "on":
|
|
value.set(true)
|
|
default:
|
|
if v, err := strconv.ParseBool(text); err == nil {
|
|
value.set(v)
|
|
}
|
|
}
|
|
|
|
return value
|
|
},
|
|
strconv.FormatBool,
|
|
)
|
|
|
|
// DecodeInt is a [Value] decoder for integers.
|
|
var DecodeInt = NewValueDecoder(
|
|
func(data any) Value[int] {
|
|
value := NewValue[int]()
|
|
|
|
if data == nil {
|
|
value.F |= IsNil
|
|
return value
|
|
}
|
|
|
|
switch V := data.(type) {
|
|
case int:
|
|
value.set(V)
|
|
case float64:
|
|
if V == float64(int(V)) {
|
|
value.set(int(V))
|
|
}
|
|
}
|
|
|
|
return value
|
|
},
|
|
func(text string) Value[int] {
|
|
value := NewValue[int]()
|
|
|
|
if len(text) == 0 {
|
|
value.F |= IsNil
|
|
}
|
|
v, err := strconv.ParseInt(text, 10, 0)
|
|
if err != nil {
|
|
return value
|
|
}
|
|
value.set(int(v))
|
|
|
|
return value
|
|
},
|
|
strconv.Itoa,
|
|
)
|
|
|
|
// DecodeTime is a [Value] decoder for [time.Time] values.
|
|
var DecodeTime = NewValueDecoder(
|
|
func(data any) Value[time.Time] {
|
|
value := NewValue[time.Time]()
|
|
|
|
if data == nil {
|
|
value.F |= IsNil
|
|
return value
|
|
}
|
|
|
|
switch V := data.(type) {
|
|
case string:
|
|
if len(V) == 0 {
|
|
value.F |= IsNil
|
|
return value
|
|
}
|
|
if v, err := dateparse.ParseAny(V); err == nil {
|
|
value.set(v)
|
|
}
|
|
case time.Time:
|
|
value.set(V)
|
|
}
|
|
|
|
if value.V.IsZero() {
|
|
value.F |= IsEmpty
|
|
}
|
|
|
|
return value
|
|
},
|
|
func(text string) Value[time.Time] {
|
|
value := NewValue[time.Time]()
|
|
|
|
if len(text) == 0 {
|
|
value.F |= IsNil
|
|
return value
|
|
}
|
|
|
|
if v, err := dateparse.ParseAny(text); err == nil {
|
|
value.set(v)
|
|
}
|
|
|
|
if value.V.IsZero() {
|
|
value.F |= IsEmpty
|
|
}
|
|
|
|
return value
|
|
},
|
|
func(v time.Time) string {
|
|
return v.Format("2006-01-02")
|
|
},
|
|
)
|