Files
TarnaWijaya 26ed99fda6
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
up3
2026-07-09 08:33:57 +08:00

218 lines
6.3 KiB
Go

package form
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"unicode/utf8"
"github.com/df-mc/dragonfly/server/world"
)
// Form represents a form that may be sent to a Submitter. The three types of forms, custom forms, menu forms
// and modal forms implement this interface.
type Form interface {
json.Marshaler
SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error
}
// Custom represents a form that may be sent to a player and has fields that should be filled out by the
// player that the form is sent to.
type Custom struct {
title string
submittable Submittable
}
// MarshalJSON ...
func (f Custom) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"type": "custom_form",
"title": f.title,
"content": f.Elements(),
})
}
// New creates a new (custom) form with the title passed and returns it. The title is formatted according to
// the rules of fmt.Sprintln.
// The submittable passed is used to create the structure of the form. The values of the Submittable's form
// fields are used to set text, defaults and placeholders. If the Submittable passed is not a struct, New
// panics. New also panics if one of the exported field types of the Submittable is not one that implements
// the Element interface.
func New(submittable Submittable, title ...any) Custom {
t := reflect.TypeOf(submittable)
if t.Kind() != reflect.Struct {
panic("submittable must be struct")
}
f := Custom{title: format(title), submittable: submittable}
f.verify()
return f
}
// Title returns the formatted title passed when the form was created using New().
func (f Custom) Title() string {
return f.title
}
// Elements returns a list of all elements as set in the Submittable passed to form.New().
func (f Custom) Elements() []Element {
v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
v.Set(reflect.ValueOf(f.submittable))
n := v.NumField()
elements := make([]Element, 0, n)
for i := 0; i < n; i++ {
field := v.Field(i)
if !field.CanSet() {
continue
}
// Each exported field is guaranteed to implement the Element interface.
elements = append(elements, field.Interface().(Element))
}
return elements
}
// SubmitJSON submits a JSON data slice to the form. The form will check all values in the JSON array passed,
// making sure their values are valid for the form's elements.
// If the values are valid and can be parsed properly, the Submittable.Submit() method of the form's Submittable is
// called and the fields of the Submittable will be filled out.
func (f Custom) SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error {
if b == nil {
if closer, ok := f.submittable.(Closer); ok {
closer.Close(submitter, tx)
}
return nil
}
dec := json.NewDecoder(bytes.NewBuffer(b))
dec.UseNumber()
var data []any
if err := dec.Decode(&data); err != nil {
return fmt.Errorf("error decoding JSON data to slice: %w", err)
}
v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
v.Set(reflect.ValueOf(f.submittable))
for i := 0; i < v.NumField(); i++ {
fieldV := v.Field(i)
if !fieldV.CanSet() {
continue
}
e := fieldV.Interface().(Element)
if len(data) == 0 {
return fmt.Errorf("form JSON data array does not have enough values")
}
if elementReadonly(e) {
data = data[1:]
continue
}
elem, err := f.parseValue(e, data[0])
if err != nil {
return fmt.Errorf("error parsing form response value: %w", err)
}
fieldV.Set(elem)
data = data[1:]
}
v.Interface().(Submittable).Submit(submitter, tx)
return nil
}
// elementReadonly returns true if the element is read only.
func elementReadonly(e Element) bool {
switch e.(type) {
case Divider, Header, Label:
return true
default:
return false
}
}
// parseValue parses a value into the Element passed and returns it as a reflection Value. If the value is not
// valid for the element, an error is returned.
func (f Custom) parseValue(elem Element, s any) (reflect.Value, error) {
var ok bool
var value reflect.Value
switch element := elem.(type) {
case Input:
element.value, ok = s.(string)
if !ok {
return value, fmt.Errorf("value %v is not allowed for input element", s)
}
if !utf8.ValidString(element.value) {
return value, fmt.Errorf("value %v is not valid UTF8", s)
}
value = reflect.ValueOf(element)
case Toggle:
element.value, ok = s.(bool)
if !ok {
return value, fmt.Errorf("value %v is not allowed for toggle element", s)
}
value = reflect.ValueOf(element)
case Slider:
v, ok := s.(json.Number)
f, err := v.Float64()
if !ok || err != nil {
return value, fmt.Errorf("value %v is not allowed for slider element", s)
}
if f > element.Max || f < element.Min {
return value, fmt.Errorf("slider value %v is out of range %v-%v", f, element.Min, element.Max)
}
element.value = f
value = reflect.ValueOf(element)
case Dropdown:
v, ok := s.(json.Number)
f, err := v.Int64()
if !ok || err != nil {
return value, fmt.Errorf("value %v is not allowed for dropdown element", s)
}
if f < 0 || int(f) >= len(element.Options) {
return value, fmt.Errorf("dropdown value %v is out of range %v-%v", f, 0, len(element.Options)-1)
}
element.value = int(f)
value = reflect.ValueOf(element)
case StepSlider:
v, ok := s.(json.Number)
f, err := v.Int64()
if !ok || err != nil {
return value, fmt.Errorf("value %v is not allowed for dropdown element", s)
}
if f < 0 || int(f) >= len(element.Options) {
return value, fmt.Errorf("dropdown value %v is out of range %v-%v", f, 0, len(element.Options)-1)
}
element.value = int(f)
value = reflect.ValueOf(element)
}
return value, nil
}
// verify verifies if the form is valid, checking if the fields all implement the Element interface. It panics
// if the form is not valid.
func (f Custom) verify() {
el := reflect.TypeOf((*Element)(nil)).Elem()
v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
v.Set(reflect.ValueOf(f.submittable))
t := reflect.TypeOf(f.submittable)
for i := 0; i < v.NumField(); i++ {
if !v.Field(i).CanSet() {
continue
}
if !t.Field(i).Type.Implements(el) {
panic("all exported fields must implement form.Element interface")
}
}
}
// format is a utility function to format a list of values to have spaces between them, but no newline at the
// end.
func format(a []any) string {
return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n")
}