Skip to content

Commit adce55b

Browse files
committed
discord: Add ContainerComponents.Unmarshal
This feature is similar to the one added a few commits prior.
1 parent af940e5 commit adce55b

File tree

4 files changed

+265
-9
lines changed

4 files changed

+265
-9
lines changed

discord/component.go

+148
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package discord
22

33
import (
44
"fmt"
5+
"reflect"
6+
"strings"
57

8+
"github.com/diamondburned/arikawa/v3/internal/rfutil"
69
"github.com/diamondburned/arikawa/v3/utils/json"
710
"github.com/diamondburned/arikawa/v3/utils/json/option"
811
"github.com/pkg/errors"
@@ -39,6 +42,141 @@ func (t ComponentType) String() string {
3942
// type for component lists.
4043
type ContainerComponents []ContainerComponent
4144

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+
42180
// UnmarshalJSON unmarshals JSON into the component. It does type-checking and
43181
// will only accept container components.
44182
func (c *ContainerComponents) UnmarshalJSON(b []byte) error {
@@ -197,6 +335,16 @@ func (a *ActionRowComponent) Type() ComponentType {
197335
func (a *ActionRowComponent) _cmp() {}
198336
func (a *ActionRowComponent) _ctn() {}
199337

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+
200348
// MarshalJSON marshals the action row in the format Discord expects.
201349
func (a *ActionRowComponent) MarshalJSON() ([]byte, error) {
202350
var actionRow struct {

discord/component_example_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package discord_test
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
8+
"github.com/diamondburned/arikawa/v3/discord"
9+
"github.com/diamondburned/arikawa/v3/utils/json/option"
10+
)
11+
12+
func ExampleContainerComponents_Unmarshal() {
13+
components := &discord.ContainerComponents{
14+
&discord.ActionRowComponent{
15+
&discord.TextInputComponent{
16+
CustomID: "text1",
17+
Value: option.NewNullableString("hello"),
18+
},
19+
},
20+
&discord.ActionRowComponent{
21+
&discord.TextInputComponent{
22+
CustomID: "text2",
23+
Value: option.NewNullableString("hello 2"),
24+
},
25+
&discord.TextInputComponent{
26+
CustomID: "text3",
27+
Value: option.NewNullableString("hello 3"),
28+
},
29+
},
30+
&discord.ActionRowComponent{
31+
&discord.SelectComponent{
32+
CustomID: "select1",
33+
Options: []discord.SelectOption{
34+
{Value: "option 1"},
35+
{Value: "option 2"},
36+
},
37+
},
38+
&discord.ButtonComponent{
39+
CustomID: "button1",
40+
},
41+
},
42+
&discord.ActionRowComponent{
43+
&discord.SelectComponent{
44+
CustomID: "select2",
45+
Options: []discord.SelectOption{
46+
{Value: "option 1"},
47+
},
48+
},
49+
},
50+
}
51+
52+
var data struct {
53+
Text1 string `discord:"text1"`
54+
Text2 string `discord:"text2?"`
55+
Text3 *string `discord:"text3"`
56+
Text4 string `discord:"text4?"`
57+
Text5 *string `discord:"text5"`
58+
Select1 []string `discord:"select1"`
59+
Select2 string `discord:"select2"`
60+
Button1 bool `discord:"button1"`
61+
}
62+
63+
if err := components.Unmarshal(&data); err != nil {
64+
log.Fatalln(err)
65+
}
66+
67+
b, _ := json.MarshalIndent(data, "", " ")
68+
fmt.Println(string(b))
69+
70+
// Output:
71+
// {
72+
// "Text1": "hello",
73+
// "Text2": "hello 2",
74+
// "Text3": "hello 3",
75+
// "Text4": "",
76+
// "Text5": null,
77+
// "Select1": [
78+
// "option 1",
79+
// "option 2"
80+
// ],
81+
// "Select2": "option 1",
82+
// "Button1": true
83+
// }
84+
}

discord/interaction.go

+7-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"reflect"
66
"strings"
77

8+
"github.com/diamondburned/arikawa/v3/internal/rfutil"
89
"github.com/diamondburned/arikawa/v3/utils/json"
910
"github.com/pkg/errors"
1011
)
@@ -448,20 +449,17 @@ var optionKindMap = map[reflect.Kind]CommandOptionType{
448449
//
449450
// Any types that are derived from any of the above built-in types are also
450451
// supported.
452+
//
453+
// Pointer types to any of the above types are also supported and will also
454+
// implicitly imply optionality.
451455
func (o CommandInteractionOptions) Unmarshal(v interface{}) error {
452456
return o.unmarshal(reflect.ValueOf(v))
453457
}
454458

455459
func (o CommandInteractionOptions) unmarshal(rv reflect.Value) error {
456-
rt := rv.Type()
457-
if rt.Kind() != reflect.Ptr {
458-
return errors.New("v is not a pointer")
459-
}
460-
461-
rv = rv.Elem()
462-
rt = rt.Elem()
463-
if rt.Kind() != reflect.Struct {
464-
return errors.New("v is not a pointer to a struct")
460+
rv, rt, err := rfutil.StructRValue(rv)
461+
if err != nil {
462+
return err
465463
}
466464

467465
numField := rt.NumField()

internal/rfutil/rfutil.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package rfutil
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
)
7+
8+
func StructValue(v interface{}) (reflect.Value, reflect.Type, error) {
9+
rv := reflect.ValueOf(v)
10+
return StructRValue(rv)
11+
}
12+
13+
func StructRValue(rv reflect.Value) (reflect.Value, reflect.Type, error) {
14+
rt := rv.Type()
15+
if rt.Kind() != reflect.Ptr {
16+
return reflect.Value{}, nil, errors.New("v is not a pointer")
17+
}
18+
19+
rv = rv.Elem()
20+
rt = rt.Elem()
21+
if rt.Kind() != reflect.Struct {
22+
return reflect.Value{}, nil, errors.New("v is not a pointer to a struct")
23+
}
24+
25+
return rv, rt, nil
26+
}

0 commit comments

Comments
 (0)