-
Notifications
You must be signed in to change notification settings - Fork 143
/
form.go
205 lines (183 loc) · 6.08 KB
/
form.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package form
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"unicode/utf8"
)
// 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) 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]interface{}{
"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 ...interface{}) 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) error {
if b == nil {
if closer, ok := f.submittable.(Closer); ok {
closer.Close(submitter)
}
return nil
}
dec := json.NewDecoder(bytes.NewBuffer(b))
dec.UseNumber()
var data []interface{}
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
}
if len(data) == 0 {
return fmt.Errorf("form JSON data array does not have enough values")
}
elem, err := f.parseValue(fieldV.Interface().(Element), 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)
return nil
}
// 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 interface{}) (reflect.Value, error) {
var ok bool
var value reflect.Value
switch element := elem.(type) {
case Label:
value = reflect.ValueOf(element)
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 []interface{}) string {
return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n")
}
func (f Custom) __() {}