@@ -2,7 +2,10 @@ package discord
2
2
3
3
import (
4
4
"fmt"
5
+ "reflect"
6
+ "strings"
5
7
8
+ "github.com/diamondburned/arikawa/v3/internal/rfutil"
6
9
"github.com/diamondburned/arikawa/v3/utils/json"
7
10
"github.com/diamondburned/arikawa/v3/utils/json/option"
8
11
"github.com/pkg/errors"
@@ -39,6 +42,141 @@ func (t ComponentType) String() string {
39
42
// type for component lists.
40
43
type ContainerComponents []ContainerComponent
41
44
45
+ // Find finds any component with the given custom ID.
46
+ func (c * ContainerComponents ) Find (customID ComponentID ) Component {
47
+ for _ , component := range * c {
48
+ switch component := component .(type ) {
49
+ case * ActionRowComponent :
50
+ if component := component .Find (customID ); component != nil {
51
+ return component
52
+ }
53
+ }
54
+ }
55
+ return nil
56
+ }
57
+
58
+ // Unmarshal unmarshals the components into the struct pointer v. Each struct
59
+ // field must be exported and is of a supported type.
60
+ //
61
+ // Fields that don't satisfy any of the above are ignored. The "discord" struct
62
+ // tag with a value "-" is ignored. Fields that aren't found in the list of
63
+ // options and have a "?" at the end of the "discord" struct tag are ignored.
64
+ //
65
+ // Each struct field will be used to search the tree of components for a
66
+ // matching custom ID. The struct must be a flat struct that lists all the
67
+ // components it needs using the custom ID.
68
+ //
69
+ // Supported Types
70
+ //
71
+ // The following types are supported:
72
+ //
73
+ // - string (SelectComponent if range = [n, 1], TextInputComponent)
74
+ // - bool (ButtonComponent or any component, true if present)
75
+ // - []string (SelectComponent)
76
+ //
77
+ // Any types that are derived from any of the above built-in types are also
78
+ // supported.
79
+ //
80
+ // Pointer types to any of the above types are also supported and will also
81
+ // implicitly imply optionality.
82
+ func (c * ContainerComponents ) Unmarshal (v interface {}) error {
83
+ rv , rt , err := rfutil .StructValue (v )
84
+ if err != nil {
85
+ return err
86
+ }
87
+
88
+ numField := rt .NumField ()
89
+ for i := 0 ; i < numField ; i ++ {
90
+ fieldStruct := rt .Field (i )
91
+ if ! fieldStruct .IsExported () {
92
+ continue
93
+ }
94
+
95
+ name := fieldStruct .Tag .Get ("discord" )
96
+ switch name {
97
+ case "-" :
98
+ continue
99
+ case "?" :
100
+ name = fieldStruct .Name + "?"
101
+ case "" :
102
+ name = fieldStruct .Name
103
+ }
104
+
105
+ component := c .Find (ComponentID (strings .TrimSuffix (name , "?" )))
106
+ fieldv := rv .Field (i )
107
+ fieldt := fieldStruct .Type
108
+
109
+ if strings .HasSuffix (name , "?" ) {
110
+ name = strings .TrimSuffix (name , "?" )
111
+ if component == nil {
112
+ // not found
113
+ continue
114
+ }
115
+ } else if fieldStruct .Type .Kind () == reflect .Ptr {
116
+ fieldt = fieldt .Elem ()
117
+ if component == nil {
118
+ // not found
119
+ fieldv .Set (reflect .NewAt (fieldt , nil ))
120
+ continue
121
+ }
122
+ // found, so allocate new value and use that to set
123
+ newv := reflect .New (fieldt )
124
+ fieldv .Set (newv )
125
+ fieldv = newv .Elem ()
126
+ } else if component == nil {
127
+ // not found AND the field is not a pointer, so error out
128
+ return fmt .Errorf ("component %q is required but not found" , name )
129
+ }
130
+
131
+ switch fieldt .Kind () {
132
+ case reflect .Bool :
133
+ // Intended for ButtonComponents.
134
+ fieldv .Set (reflect .ValueOf (true ).Convert (fieldt ))
135
+ case reflect .String :
136
+ var v string
137
+
138
+ switch component := component .(type ) {
139
+ case * TextInputComponent :
140
+ v = component .Value .Val
141
+ case * SelectComponent :
142
+ switch len (component .Options ) {
143
+ case 0 :
144
+ // ok
145
+ case 1 :
146
+ v = component .Options [0 ].Value
147
+ default :
148
+ return fmt .Errorf ("component %q selected more than one item (bug, check ValueRange)" , name )
149
+ }
150
+ default :
151
+ return fmt .Errorf ("component %q is of unsupported type %T" , name , component )
152
+ }
153
+
154
+ fieldv .Set (reflect .ValueOf (v ).Convert (fieldt ))
155
+ case reflect .Slice :
156
+ elemt := fieldt .Elem ()
157
+
158
+ switch elemt .Kind () {
159
+ case reflect .String :
160
+ switch component := component .(type ) {
161
+ case * SelectComponent :
162
+ fieldv .Set (reflect .MakeSlice (fieldt , len (component .Options ), len (component .Options )))
163
+ for i , option := range component .Options {
164
+ fieldv .Index (i ).Set (reflect .ValueOf (option .Value ).Convert (elemt ))
165
+ }
166
+ default :
167
+ return fmt .Errorf ("component %q is of unsupported type %T" , name , component )
168
+ }
169
+ default :
170
+ return fmt .Errorf ("field %s (%q) has unknown slice type %s" , fieldStruct .Name , name , fieldt )
171
+ }
172
+ default :
173
+ return fmt .Errorf ("field %s (%q) has unknown type %s" , fieldStruct .Name , name , fieldt )
174
+ }
175
+ }
176
+
177
+ return nil
178
+ }
179
+
42
180
// UnmarshalJSON unmarshals JSON into the component. It does type-checking and
43
181
// will only accept container components.
44
182
func (c * ContainerComponents ) UnmarshalJSON (b []byte ) error {
@@ -197,6 +335,16 @@ func (a *ActionRowComponent) Type() ComponentType {
197
335
func (a * ActionRowComponent ) _cmp () {}
198
336
func (a * ActionRowComponent ) _ctn () {}
199
337
338
+ // Find finds any component with the given custom ID.
339
+ func (a * ActionRowComponent ) Find (customID ComponentID ) Component {
340
+ for _ , component := range * a {
341
+ if component .ID () == customID {
342
+ return component
343
+ }
344
+ }
345
+ return nil
346
+ }
347
+
200
348
// MarshalJSON marshals the action row in the format Discord expects.
201
349
func (a * ActionRowComponent ) MarshalJSON () ([]byte , error ) {
202
350
var actionRow struct {
0 commit comments