-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcomponentstate.go
More file actions
199 lines (175 loc) · 5.98 KB
/
Copy pathcomponentstate.go
File metadata and controls
199 lines (175 loc) · 5.98 KB
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
// Component state travels to Discord and back inside a custom_id, which is
// the only place strut's data leaves the process. The encoding is derived
// from the state type's name and fields, so a button keeps working across a
// restart.
package strut
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
"github.com/disgoorg/snowflake/v2"
)
// Discord caps a component's custom_id at 100 characters.
const maxCustomID = 100
// stateCodec encodes a component's state into a custom_id and reads it back.
//
// The route is derived from the state type's name, so a button still works
// after a restart, and the pattern is a disgo route with one variable per
// field: /c/PageBtn/{p}/{u}
type stateCodec struct {
typ reflect.Type
route string // the pattern registered with the router
prefix string // the literal segment before the values
names []string // variable names, in field order
fields [][]int
setters []func(dst reflect.Value, raw string) error
}
// newStateCodec compiles a state struct.
func newStateCodec(t reflect.Type) (*stateCodec, error) {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("strut: component state %s must be a struct", t)
}
c := &stateCodec{typ: t, prefix: "/c/" + t.Name()}
seen := map[string]bool{}
spec, err := analyze(t, modeComponent)
if err != nil {
return nil, err
}
for _, f := range spec.Fields {
if seen[f.Tag.Name] {
return nil, fmt.Errorf("strut: component state %s: duplicate field %q", t, f.Tag.Name)
}
seen[f.Tag.Name] = true
set, err := stateSetter(f)
if err != nil {
return nil, fmt.Errorf("strut: component state %s: %w", t, err)
}
c.names = append(c.names, f.Tag.Name)
c.fields = append(c.fields, f.Index)
c.setters = append(c.setters, set)
}
c.route = c.prefix
for _, n := range c.names {
c.route += "/{" + n + "}"
}
// The literal part is fixed, so an unusable prefix is caught at
// registration rather than the first click.
if len(c.prefix) >= maxCustomID {
return nil, fmt.Errorf("strut: component state %s: name is too long for a custom_id", t)
}
return c, nil
}
// encode renders state into a custom_id.
func (c *stateCodec) encode(v reflect.Value) (string, error) {
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
var b strings.Builder
b.WriteString(c.prefix)
for i, path := range c.fields {
s, err := stateString(v.FieldByIndex(path))
if err != nil {
return "", fmt.Errorf("strut: component state %s: field %q: %w", c.typ, c.names[i], err)
}
if strings.ContainsAny(s, "/") {
return "", fmt.Errorf("strut: component state %s: field %q contains a slash, which separates fields",
c.typ, c.names[i])
}
b.WriteByte('/')
b.WriteString(s)
}
id := b.String()
if len(id) > maxCustomID {
return "", fmt.Errorf("strut: component state %s: custom_id is %d characters, max %d",
c.typ, len(id), maxCustomID)
}
return id, nil
}
// decode reads state back out of the router's path variables. A variable the
// pattern did not capture stays zero, so adding a field does not break
// components created before it existed.
func (c *stateCodec) decode(vars map[string]string) (reflect.Value, error) {
out := reflect.New(c.typ)
for i, name := range c.names {
raw, ok := vars[name]
if !ok || raw == "" {
continue
}
if err := c.setters[i](out.Elem().FieldByIndex(c.fields[i]), raw); err != nil {
return reflect.Value{}, fmt.Errorf("strut: component state %s: field %q: %w", c.typ, name, err)
}
}
return out.Elem(), nil
}
// stateString renders one field value.
func stateString(v reflect.Value) (string, error) {
switch v.Type() {
case snowflakeType:
return v.Interface().(snowflake.ID).String(), nil
case durationType:
return strconv.FormatInt(int64(v.Interface().(time.Duration)), 10), nil
}
switch v.Kind() {
case reflect.String:
return v.String(), nil
case reflect.Bool:
if v.Bool() {
return "1", nil
}
return "0", nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(v.Uint(), 10), nil
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(v.Float(), 'g', -1, 64), nil
}
return "", fmt.Errorf("%s cannot be stored in a custom_id", v.Type())
}
func stateSetter(f fieldSpec) (func(reflect.Value, string) error, error) {
target := f.Type
switch target {
case snowflakeType:
return convertingSetter(target, func(s string) (any, error) { return snowflake.Parse(s) }), nil
case durationType:
return convertingSetter(target, func(s string) (any, error) {
n, err := strconv.ParseInt(s, 10, 64)
return time.Duration(n), err
}), nil
}
switch target.Kind() {
case reflect.String:
return convertingSetter(target, func(s string) (any, error) { return s, nil }), nil
case reflect.Bool:
return convertingSetter(target, func(s string) (any, error) { return s == "1" || s == "true", nil }), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return convertingSetter(target, func(s string) (any, error) { return strconv.ParseInt(s, 10, 64) }), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return convertingSetter(target, func(s string) (any, error) { return strconv.ParseUint(s, 10, 64) }), nil
case reflect.Float32, reflect.Float64:
return convertingSetter(target, func(s string) (any, error) { return strconv.ParseFloat(s, 64) }), nil
}
return nil, fmt.Errorf("%s cannot be stored in a custom_id", target)
}
func convertingSetter(target reflect.Type, parse func(string) (any, error)) func(reflect.Value, string) error {
return func(dst reflect.Value, raw string) error {
v, err := parse(raw)
if err != nil {
return err
}
rv := reflect.ValueOf(v)
if rv.Type() != target {
if !rv.CanConvert(target) {
return fmt.Errorf("cannot convert %s to %s", rv.Type(), target)
}
rv = rv.Convert(target)
}
dst.Set(rv)
return nil
}
}