github.com/df-mc/dragonfly@v0.9.13/server/player/form/form.go (about)

     1  package form
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"reflect"
     8  	"strings"
     9  	"unicode/utf8"
    10  )
    11  
    12  // Form represents a form that may be sent to a Submitter. The three types of forms, custom forms, menu forms
    13  // and modal forms implement this interface.
    14  type Form interface {
    15  	json.Marshaler
    16  	SubmitJSON(b []byte, submitter Submitter) error
    17  }
    18  
    19  // Custom represents a form that may be sent to a player and has fields that should be filled out by the
    20  // player that the form is sent to.
    21  type Custom struct {
    22  	title       string
    23  	submittable Submittable
    24  }
    25  
    26  // MarshalJSON ...
    27  func (f Custom) MarshalJSON() ([]byte, error) {
    28  	return json.Marshal(map[string]any{
    29  		"type":    "custom_form",
    30  		"title":   f.title,
    31  		"content": f.Elements(),
    32  	})
    33  }
    34  
    35  // New creates a new (custom) form with the title passed and returns it. The title is formatted according to
    36  // the rules of fmt.Sprintln.
    37  // The submittable passed is used to create the structure of the form. The values of the Submittable's form
    38  // fields are used to set text, defaults and placeholders. If the Submittable passed is not a struct, New
    39  // panics. New also panics if one of the exported field types of the Submittable is not one that implements
    40  // the Element interface.
    41  func New(submittable Submittable, title ...any) Custom {
    42  	t := reflect.TypeOf(submittable)
    43  	if t.Kind() != reflect.Struct {
    44  		panic("submittable must be struct")
    45  	}
    46  	f := Custom{title: format(title), submittable: submittable}
    47  	f.verify()
    48  	return f
    49  }
    50  
    51  // Title returns the formatted title passed when the form was created using New().
    52  func (f Custom) Title() string {
    53  	return f.title
    54  }
    55  
    56  // Elements returns a list of all elements as set in the Submittable passed to form.New().
    57  func (f Custom) Elements() []Element {
    58  	v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
    59  	v.Set(reflect.ValueOf(f.submittable))
    60  	n := v.NumField()
    61  
    62  	elements := make([]Element, 0, n)
    63  	for i := 0; i < n; i++ {
    64  		field := v.Field(i)
    65  		if !field.CanSet() {
    66  			continue
    67  		}
    68  		// Each exported field is guaranteed to implement the Element interface.
    69  		elements = append(elements, field.Interface().(Element))
    70  	}
    71  	return elements
    72  }
    73  
    74  // SubmitJSON submits a JSON data slice to the form. The form will check all values in the JSON array passed,
    75  // making sure their values are valid for the form's elements.
    76  // If the values are valid and can be parsed properly, the Submittable.Submit() method of the form's Submittable is
    77  // called and the fields of the Submittable will be filled out.
    78  func (f Custom) SubmitJSON(b []byte, submitter Submitter) error {
    79  	if b == nil {
    80  		if closer, ok := f.submittable.(Closer); ok {
    81  			closer.Close(submitter)
    82  		}
    83  		return nil
    84  	}
    85  
    86  	dec := json.NewDecoder(bytes.NewBuffer(b))
    87  	dec.UseNumber()
    88  
    89  	var data []any
    90  	if err := dec.Decode(&data); err != nil {
    91  		return fmt.Errorf("error decoding JSON data to slice: %w", err)
    92  	}
    93  
    94  	v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
    95  	v.Set(reflect.ValueOf(f.submittable))
    96  
    97  	for i := 0; i < v.NumField(); i++ {
    98  		fieldV := v.Field(i)
    99  		if !fieldV.CanSet() {
   100  			continue
   101  		}
   102  		if len(data) == 0 {
   103  			return fmt.Errorf("form JSON data array does not have enough values")
   104  		}
   105  		elem, err := f.parseValue(fieldV.Interface().(Element), data[0])
   106  		if err != nil {
   107  			return fmt.Errorf("error parsing form response value: %w", err)
   108  		}
   109  		fieldV.Set(elem)
   110  		data = data[1:]
   111  	}
   112  
   113  	v.Interface().(Submittable).Submit(submitter)
   114  
   115  	return nil
   116  }
   117  
   118  // parseValue parses a value into the Element passed and returns it as a reflection Value. If the value is not
   119  // valid for the element, an error is returned.
   120  func (f Custom) parseValue(elem Element, s any) (reflect.Value, error) {
   121  	var ok bool
   122  	var value reflect.Value
   123  
   124  	switch element := elem.(type) {
   125  	case Label:
   126  		value = reflect.ValueOf(element)
   127  	case Input:
   128  		element.value, ok = s.(string)
   129  		if !ok {
   130  			return value, fmt.Errorf("value %v is not allowed for input element", s)
   131  		}
   132  		if !utf8.ValidString(element.value) {
   133  			return value, fmt.Errorf("value %v is not valid UTF8", s)
   134  		}
   135  		value = reflect.ValueOf(element)
   136  	case Toggle:
   137  		element.value, ok = s.(bool)
   138  		if !ok {
   139  			return value, fmt.Errorf("value %v is not allowed for toggle element", s)
   140  		}
   141  		value = reflect.ValueOf(element)
   142  	case Slider:
   143  		v, ok := s.(json.Number)
   144  		f, err := v.Float64()
   145  		if !ok || err != nil {
   146  			return value, fmt.Errorf("value %v is not allowed for slider element", s)
   147  		}
   148  		if f > element.Max || f < element.Min {
   149  			return value, fmt.Errorf("slider value %v is out of range %v-%v", f, element.Min, element.Max)
   150  		}
   151  		element.value = f
   152  		value = reflect.ValueOf(element)
   153  	case Dropdown:
   154  		v, ok := s.(json.Number)
   155  		f, err := v.Int64()
   156  		if !ok || err != nil {
   157  			return value, fmt.Errorf("value %v is not allowed for dropdown element", s)
   158  		}
   159  		if f < 0 || int(f) >= len(element.Options) {
   160  			return value, fmt.Errorf("dropdown value %v is out of range %v-%v", f, 0, len(element.Options)-1)
   161  		}
   162  		element.value = int(f)
   163  		value = reflect.ValueOf(element)
   164  	case StepSlider:
   165  		v, ok := s.(json.Number)
   166  		f, err := v.Int64()
   167  		if !ok || err != nil {
   168  			return value, fmt.Errorf("value %v is not allowed for dropdown element", s)
   169  		}
   170  		if f < 0 || int(f) >= len(element.Options) {
   171  			return value, fmt.Errorf("dropdown value %v is out of range %v-%v", f, 0, len(element.Options)-1)
   172  		}
   173  		element.value = int(f)
   174  		value = reflect.ValueOf(element)
   175  	}
   176  	return value, nil
   177  }
   178  
   179  // verify verifies if the form is valid, checking if the fields all implement the Element interface. It panics
   180  // if the form is not valid.
   181  func (f Custom) verify() {
   182  	el := reflect.TypeOf((*Element)(nil)).Elem()
   183  
   184  	v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
   185  	v.Set(reflect.ValueOf(f.submittable))
   186  
   187  	t := reflect.TypeOf(f.submittable)
   188  	for i := 0; i < v.NumField(); i++ {
   189  		if !v.Field(i).CanSet() {
   190  			continue
   191  		}
   192  		if !t.Field(i).Type.Implements(el) {
   193  			panic("all exported fields must implement form.Element interface")
   194  		}
   195  	}
   196  }
   197  
   198  // format is a utility function to format a list of values to have spaces between them, but no newline at the
   199  // end.
   200  func format(a []any) string {
   201  	return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n")
   202  }