frm/types/types.go

287 lines
12 KiB
Go
Raw Permalink Normal View History

2024-11-05 16:18:04 +00:00
package types
import (
"encoding/json"
2025-01-10 18:52:12 +00:00
"errors"
2025-02-20 21:51:59 +00:00
"math/rand/v2"
"slices"
"sort"
2024-11-05 16:18:04 +00:00
"github.com/google/uuid"
)
2025-01-10 18:52:12 +00:00
// ErrRequiredNoValueProvided is a form validation error for required fields missing values
var ErrRequiredNoValueProvided = errors.New("This field is required")
2025-02-20 21:51:59 +00:00
var ErrUnknownOptionProvided = errors.New("This field is required, please choose a valid option")
2025-01-10 18:52:12 +00:00
// ValidationErrors is a mapping of form field IDs to the errors validating values submitted to those fields
type ValidationErrors map[string]error
func (v ValidationErrors) Any() bool {
return len(v) > 0
}
2024-11-05 16:18:04 +00:00
// FormFieldType enum enumerates all possible form field types
//
//go:generate enumer -type FormFieldType -trimprefix FormFieldType -transform=snake -json
type FormFieldType int
const (
FormFieldTypeTextSingle FormFieldType = iota // single line of text
FormFieldTypeTextMultiple // multiple lines of text
FormFieldTypeSingleSelect // single-select dropdown
FormFieldTypeMultiSelect // multi-select dropdown
FormFieldTypeSingleChoice // nicely styled radio buttons
FormFieldTypeSingleChoiceSpaced // nicely styled radio buttons, spaced out
2024-11-05 16:18:04 +00:00
)
2025-01-11 00:11:33 +00:00
// FormFieldDataType enum enumerates all possible data types for form fields
//
// This type informs how form field submissions may be used by 'frm' users.
//
2025-01-11 00:11:33 +00:00
//go:generate enumer -type FormFieldDataType -trimprefix FormFieldDataType -transform=snake -json
type FormFieldDataType int
const (
FormFieldDataTypeText FormFieldDataType = iota // textual data
FormFieldDataTypeNumeric // numeric data
FormFieldDataTypeRating // chosen values represent a 'rating'
2025-01-11 00:11:33 +00:00
)
func FormFieldDataTypes() []FormFieldDataType {
return []FormFieldDataType{FormFieldDataTypeText, FormFieldDataTypeNumeric, FormFieldDataTypeRating}
}
2024-11-05 16:18:04 +00:00
// FieldLogicComparator enum enumerates all possible form field logic comparators
//
//go:generate enumer -type FieldLogicComparator -trimprefix FieldLogicComparator -transform=snake -json -text
type FieldLogicComparator int
const (
FieldLogicComparatorEqual FieldLogicComparator = iota // target field value is equal to the subject value
FieldLogicComparatorContains // target field value contains the subject value
2025-02-11 15:31:12 +00:00
FieldLogicComparatorNot // target field value is "not" the subject value
2024-11-05 16:18:04 +00:00
)
2025-02-20 21:51:59 +00:00
// FormFieldOptionOrder enum enumerates all possible ways to order FieldOptions
//
//go:generate enumer -type FormFieldOptionOrder -trimprefix FormFieldOptionOrder -transform=snake -json -text
type FormFieldOptionOrder int
const (
OptionOrderNatural FormFieldOptionOrder = iota // FieldOptions are ordered naturally according to their order field
OptionOrderRandom // FieldOptions are ordered randomly
)
2024-11-05 16:18:04 +00:00
// FieldLogicTriggerAction enum enumerates all possible field logic trigger actions
//
//go:generate enumer -type FieldLogicTriggerAction -trimprefix FieldLogicTriggerAction -transform=snake -json -text
type FieldLogicTriggerAction int
const (
FieldLogicTriggerShow FieldLogicTriggerAction = iota // make the field visible to the user
2025-02-20 21:51:59 +00:00
FieldLogicTriggerRequire // require the user to enter a value
2024-11-05 16:18:04 +00:00
)
// FormFields is a collection of form fields associated with a Form
//
// The underlying type is a map, where keys are form field IDs and values are the corresponding form field
type FormFields map[string]FormField
2025-01-11 00:11:33 +00:00
// FormFieldValues is a collection of form fields submitted to a form
//
// The underlying type is a map, where keys are form field IDs and values are what was submited to the form representing that field
type FormFieldValues map[string]FormFieldSubmission
2024-11-05 16:18:04 +00:00
// FieldOptions are options for single or multi-selector fields
type FieldOptions []Option
// FormField is a field associated with a form
type FormField struct {
2025-02-20 21:51:59 +00:00
ID uuid.UUID `json:"id"` // field's unique id
Order int `json:"order"` // order in which the field appears on forms
Label string `json:"label"` // field's label (name)
Logic *FieldLogic `json:"logic"` // UI logic for this field
Options FieldOptions `json:"options"` // single/multi-select options
OptionLabels []string `json:"option_labels"` // option labels are shown below [types.FormFieldTypeSingleChoice] options
OptionOrder FormFieldOptionOrder `json:"option_order"` // the order in which options appear to viewers
Placeholder string `json:"placeholder"` // placeholder value
Required bool `json:"required"` // whether the field is required
Hidden bool `json:"hidden"` // whether the field is hidden
Type FormFieldType `json:"type"` // field type
DataType FormFieldDataType `json:"data_type"` // the data type form submissions to this field
2025-01-11 00:11:33 +00:00
}
// FormFieldSubmission is a form submission for a particular form field. Form submissions consists of one or more form field submission
type FormFieldSubmission struct {
ID uuid.UUID `json:"id"` // field submission's unique id
FormFieldID uuid.UUID `json:"form_field_id"`
Order int `json:"order"` // order in which the field appeared on the submitted form
Required bool `json:"required"` // whether the field was requird
Hidden bool `json:"hidden"` // whether the field was hidden
Type FormFieldType `json:"type"` // field type
DataType FormFieldDataType `json:"data_type"` // the data type of the Value
Value any `json:"value"` // the value that was submitted
2024-11-05 16:18:04 +00:00
}
// FieldLogic defines logic associated with a field
type FieldLogic struct {
TargetFieldID uuid.UUID `json:"target_field_id"` // ID of the field to monitor for logic evaluation
TriggerComparator FieldLogicComparator `json:"field_comparator"` // comparator to use evaluating target field's value with trigger values
TriggerValues []string `json:"trigger_values"` // values that target field's value is compared with
TriggerActions FieldLogicTriggerActions `json:"actions"` // actions to take when the field comparator evaluates true
}
// FieldLogicTriggerActions is a collection of field logic trigger actions
2024-11-05 16:18:04 +00:00
type FieldLogicTriggerActions []FieldLogicTriggerAction
// Contains determines whether FieldLogicTriggerActions contains some other trigger action
func (f FieldLogicTriggerActions) Contains(a FieldLogicTriggerAction) bool {
for _, ta := range f {
if ta == a {
return true
}
}
return false
}
// Option is a select option (single and multi)
type Option struct {
ID uuid.UUID `json:"id"`
Value string `json:"value"`
Label string `json:"label"`
Order int `json:"order"`
2024-11-05 16:18:04 +00:00
Selected bool `json:"-"`
Disabled bool `json:"-"`
}
// FormFieldSortByOrder implements sort.Interface for []FormField based on
// the Order field.
type FormFieldSortByOrder []FormField
func (f FormFieldSortByOrder) Len() int { return len(f) }
func (f FormFieldSortByOrder) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
func (f FormFieldSortByOrder) Less(i, j int) bool { return f[i].Order < f[j].Order }
2025-02-20 21:51:59 +00:00
// FormFieldOptionSortNatural implements sort.Interface for [[]Option], sorting options naturally by Order
type FormFieldOptionSortNatural []Option
func (f FormFieldOptionSortNatural) Len() int { return len(f) }
func (f FormFieldOptionSortNatural) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
func (f FormFieldOptionSortNatural) Less(i, j int) bool { return f[i].Order < f[j].Order }
// FormFieldOptionSortRand implements sort.Interface for [[]Option], sorting options randomly
type FormFieldOptionSortRand []Option
func (f FormFieldOptionSortRand) Len() int { return len(f) }
func (f FormFieldOptionSortRand) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
func (f FormFieldOptionSortRand) Less(i, j int) bool { return rand.Int64()%2 == 0 }
2025-01-10 18:52:12 +00:00
// Validate validates values submitted to a form field
func (f FormField) Validate(value []string) (err error) {
if f.Required {
if len(value) == 0 {
return ErrRequiredNoValueProvided
}
for _, ffv := range value {
if ffv == "" {
return ErrRequiredNoValueProvided
}
}
}
2025-01-30 19:04:48 +00:00
switch f.Type {
// ensure that the provided value is one of this field's available options
case FormFieldTypeSingleSelect, FormFieldTypeMultiSelect, FormFieldTypeSingleChoice, FormFieldTypeSingleChoiceSpaced:
// Choices-js causes fields to be submitted with am empty value, rather than excluding it. This is a bit hacky.
if !f.Required && len(value) == 1 && value[0] == "" {
return nil
}
2025-01-30 19:04:48 +00:00
if !allValid(f, value) {
2025-02-20 21:51:59 +00:00
return ErrUnknownOptionProvided
2025-01-30 19:04:48 +00:00
}
return nil
default:
return nil
}
2025-01-10 18:52:12 +00:00
}
2024-11-05 16:18:04 +00:00
// MarshalJSON implements the json.Marshaler interface for FormFieldType
func (f FormField) MarshalJSON() ([]byte, error) {
id := uuid.Nil
if f.ID != id {
id = f.ID
}
// only confiugre logic when logic is _completely_ configured
var logic *FieldLogic
if f.Logic != nil && f.Logic.TargetFieldID != uuid.Nil && len(f.Logic.TriggerValues) > 0 && f.Logic.TriggerValues[0] != "" {
logic = f.Logic
}
2024-11-05 16:18:04 +00:00
d := struct {
2025-02-20 21:51:59 +00:00
ID uuid.UUID `json:"id"` // field's unique id
Order int `json:"order"` // order in which the field appears on forms
Label string `json:"label"` // field's label (name)
Logic *FieldLogic `json:"logic"` // field's logic configuration
Options FieldOptions `json:"options"` // single/multi-select options
OptionLabels []string `json:"option_labels"` // labels for [FormFieldTypeSingleChoice] options
OptionOrder FormFieldOptionOrder `json:"option_order"` // the order in which options appear
Placeholder string `json:"placeholder"` // placeholder value
Required bool `json:"required"` // whether the field is required
Hidden bool `json:"hidden"` // whether the field is hidden
Type FormFieldType `json:"type"` // field type
DataType FormFieldDataType `json:"data_type"` // field's data type
2024-11-05 16:18:04 +00:00
}{
2025-01-29 15:19:35 +00:00
ID: id,
Order: f.Order,
Label: f.Label,
Options: f.Options,
OptionLabels: f.OptionLabels,
2025-02-20 21:51:59 +00:00
OptionOrder: f.OptionOrder,
2025-01-29 15:19:35 +00:00
Placeholder: f.Placeholder,
Required: f.Required,
Hidden: f.Hidden,
Type: f.Type,
Logic: logic,
DataType: f.DataType,
2024-11-05 16:18:04 +00:00
}
return json.Marshal(d)
}
2025-01-30 19:04:48 +00:00
2025-02-20 21:51:59 +00:00
// SortedOptions returns a field's options sorted according to its [OptionOrder]
func (f *FormField) SortedOptions() (sorted []Option) {
sorted = slices.Clone(f.Options)
switch f.OptionOrder {
case OptionOrderNatural:
sort.Sort(FormFieldOptionSortNatural(sorted))
return
case OptionOrderRandom:
sort.Sort(FormFieldOptionSortRand(sorted))
return
default:
sort.Sort(FormFieldOptionSortNatural(sorted))
return
}
}
2025-01-30 19:04:48 +00:00
// allValid checks if all field submission values are valid options
func allValid(field FormField, subset []string) bool {
set := make(map[string]bool)
for _, v := range field.Options {
set[v.Value] = true
}
// Check if all form responses are valid options;
for _, v := range subset {
if !set[v] {
return false
}
}
return true
}