mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-11-18 21:02:47 +00:00
discord: Add ContainerComponents.Unmarshal
This feature is similar to the one added a few commits prior.
This commit is contained in:
parent
af940e5a37
commit
adce55b02d
|
@ -2,7 +2,10 @@ package discord
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/v3/internal/rfutil"
|
||||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -39,6 +42,141 @@ func (t ComponentType) String() string {
|
||||||
// type for component lists.
|
// type for component lists.
|
||||||
type ContainerComponents []ContainerComponent
|
type ContainerComponents []ContainerComponent
|
||||||
|
|
||||||
|
// Find finds any component with the given custom ID.
|
||||||
|
func (c *ContainerComponents) Find(customID ComponentID) Component {
|
||||||
|
for _, component := range *c {
|
||||||
|
switch component := component.(type) {
|
||||||
|
case *ActionRowComponent:
|
||||||
|
if component := component.Find(customID); component != nil {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal unmarshals the components into the struct pointer v. Each struct
|
||||||
|
// field must be exported and is of a supported type.
|
||||||
|
//
|
||||||
|
// Fields that don't satisfy any of the above are ignored. The "discord" struct
|
||||||
|
// tag with a value "-" is ignored. Fields that aren't found in the list of
|
||||||
|
// options and have a "?" at the end of the "discord" struct tag are ignored.
|
||||||
|
//
|
||||||
|
// Each struct field will be used to search the tree of components for a
|
||||||
|
// matching custom ID. The struct must be a flat struct that lists all the
|
||||||
|
// components it needs using the custom ID.
|
||||||
|
//
|
||||||
|
// Supported Types
|
||||||
|
//
|
||||||
|
// The following types are supported:
|
||||||
|
//
|
||||||
|
// - string (SelectComponent if range = [n, 1], TextInputComponent)
|
||||||
|
// - bool (ButtonComponent or any component, true if present)
|
||||||
|
// - []string (SelectComponent)
|
||||||
|
//
|
||||||
|
// Any types that are derived from any of the above built-in types are also
|
||||||
|
// supported.
|
||||||
|
//
|
||||||
|
// Pointer types to any of the above types are also supported and will also
|
||||||
|
// implicitly imply optionality.
|
||||||
|
func (c *ContainerComponents) Unmarshal(v interface{}) error {
|
||||||
|
rv, rt, err := rfutil.StructValue(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
numField := rt.NumField()
|
||||||
|
for i := 0; i < numField; i++ {
|
||||||
|
fieldStruct := rt.Field(i)
|
||||||
|
if !fieldStruct.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := fieldStruct.Tag.Get("discord")
|
||||||
|
switch name {
|
||||||
|
case "-":
|
||||||
|
continue
|
||||||
|
case "?":
|
||||||
|
name = fieldStruct.Name + "?"
|
||||||
|
case "":
|
||||||
|
name = fieldStruct.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
component := c.Find(ComponentID(strings.TrimSuffix(name, "?")))
|
||||||
|
fieldv := rv.Field(i)
|
||||||
|
fieldt := fieldStruct.Type
|
||||||
|
|
||||||
|
if strings.HasSuffix(name, "?") {
|
||||||
|
name = strings.TrimSuffix(name, "?")
|
||||||
|
if component == nil {
|
||||||
|
// not found
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if fieldStruct.Type.Kind() == reflect.Ptr {
|
||||||
|
fieldt = fieldt.Elem()
|
||||||
|
if component == nil {
|
||||||
|
// not found
|
||||||
|
fieldv.Set(reflect.NewAt(fieldt, nil))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// found, so allocate new value and use that to set
|
||||||
|
newv := reflect.New(fieldt)
|
||||||
|
fieldv.Set(newv)
|
||||||
|
fieldv = newv.Elem()
|
||||||
|
} else if component == nil {
|
||||||
|
// not found AND the field is not a pointer, so error out
|
||||||
|
return fmt.Errorf("component %q is required but not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fieldt.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
// Intended for ButtonComponents.
|
||||||
|
fieldv.Set(reflect.ValueOf(true).Convert(fieldt))
|
||||||
|
case reflect.String:
|
||||||
|
var v string
|
||||||
|
|
||||||
|
switch component := component.(type) {
|
||||||
|
case *TextInputComponent:
|
||||||
|
v = component.Value.Val
|
||||||
|
case *SelectComponent:
|
||||||
|
switch len(component.Options) {
|
||||||
|
case 0:
|
||||||
|
// ok
|
||||||
|
case 1:
|
||||||
|
v = component.Options[0].Value
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("component %q selected more than one item (bug, check ValueRange)", name)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("component %q is of unsupported type %T", name, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldv.Set(reflect.ValueOf(v).Convert(fieldt))
|
||||||
|
case reflect.Slice:
|
||||||
|
elemt := fieldt.Elem()
|
||||||
|
|
||||||
|
switch elemt.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
switch component := component.(type) {
|
||||||
|
case *SelectComponent:
|
||||||
|
fieldv.Set(reflect.MakeSlice(fieldt, len(component.Options), len(component.Options)))
|
||||||
|
for i, option := range component.Options {
|
||||||
|
fieldv.Index(i).Set(reflect.ValueOf(option.Value).Convert(elemt))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("component %q is of unsupported type %T", name, component)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("field %s (%q) has unknown slice type %s", fieldStruct.Name, name, fieldt)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("field %s (%q) has unknown type %s", fieldStruct.Name, name, fieldt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalJSON unmarshals JSON into the component. It does type-checking and
|
// UnmarshalJSON unmarshals JSON into the component. It does type-checking and
|
||||||
// will only accept container components.
|
// will only accept container components.
|
||||||
func (c *ContainerComponents) UnmarshalJSON(b []byte) error {
|
func (c *ContainerComponents) UnmarshalJSON(b []byte) error {
|
||||||
|
@ -197,6 +335,16 @@ func (a *ActionRowComponent) Type() ComponentType {
|
||||||
func (a *ActionRowComponent) _cmp() {}
|
func (a *ActionRowComponent) _cmp() {}
|
||||||
func (a *ActionRowComponent) _ctn() {}
|
func (a *ActionRowComponent) _ctn() {}
|
||||||
|
|
||||||
|
// Find finds any component with the given custom ID.
|
||||||
|
func (a *ActionRowComponent) Find(customID ComponentID) Component {
|
||||||
|
for _, component := range *a {
|
||||||
|
if component.ID() == customID {
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalJSON marshals the action row in the format Discord expects.
|
// MarshalJSON marshals the action row in the format Discord expects.
|
||||||
func (a *ActionRowComponent) MarshalJSON() ([]byte, error) {
|
func (a *ActionRowComponent) MarshalJSON() ([]byte, error) {
|
||||||
var actionRow struct {
|
var actionRow struct {
|
||||||
|
|
84
discord/component_example_test.go
Normal file
84
discord/component_example_test.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package discord_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/v3/discord"
|
||||||
|
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleContainerComponents_Unmarshal() {
|
||||||
|
components := &discord.ContainerComponents{
|
||||||
|
&discord.ActionRowComponent{
|
||||||
|
&discord.TextInputComponent{
|
||||||
|
CustomID: "text1",
|
||||||
|
Value: option.NewNullableString("hello"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&discord.ActionRowComponent{
|
||||||
|
&discord.TextInputComponent{
|
||||||
|
CustomID: "text2",
|
||||||
|
Value: option.NewNullableString("hello 2"),
|
||||||
|
},
|
||||||
|
&discord.TextInputComponent{
|
||||||
|
CustomID: "text3",
|
||||||
|
Value: option.NewNullableString("hello 3"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&discord.ActionRowComponent{
|
||||||
|
&discord.SelectComponent{
|
||||||
|
CustomID: "select1",
|
||||||
|
Options: []discord.SelectOption{
|
||||||
|
{Value: "option 1"},
|
||||||
|
{Value: "option 2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&discord.ButtonComponent{
|
||||||
|
CustomID: "button1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&discord.ActionRowComponent{
|
||||||
|
&discord.SelectComponent{
|
||||||
|
CustomID: "select2",
|
||||||
|
Options: []discord.SelectOption{
|
||||||
|
{Value: "option 1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Text1 string `discord:"text1"`
|
||||||
|
Text2 string `discord:"text2?"`
|
||||||
|
Text3 *string `discord:"text3"`
|
||||||
|
Text4 string `discord:"text4?"`
|
||||||
|
Text5 *string `discord:"text5"`
|
||||||
|
Select1 []string `discord:"select1"`
|
||||||
|
Select2 string `discord:"select2"`
|
||||||
|
Button1 bool `discord:"button1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := components.Unmarshal(&data); err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := json.MarshalIndent(data, "", " ")
|
||||||
|
fmt.Println(string(b))
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// {
|
||||||
|
// "Text1": "hello",
|
||||||
|
// "Text2": "hello 2",
|
||||||
|
// "Text3": "hello 3",
|
||||||
|
// "Text4": "",
|
||||||
|
// "Text5": null,
|
||||||
|
// "Select1": [
|
||||||
|
// "option 1",
|
||||||
|
// "option 2"
|
||||||
|
// ],
|
||||||
|
// "Select2": "option 1",
|
||||||
|
// "Button1": true
|
||||||
|
// }
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/v3/internal/rfutil"
|
||||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -448,20 +449,17 @@ var optionKindMap = map[reflect.Kind]CommandOptionType{
|
||||||
//
|
//
|
||||||
// Any types that are derived from any of the above built-in types are also
|
// Any types that are derived from any of the above built-in types are also
|
||||||
// supported.
|
// supported.
|
||||||
|
//
|
||||||
|
// Pointer types to any of the above types are also supported and will also
|
||||||
|
// implicitly imply optionality.
|
||||||
func (o CommandInteractionOptions) Unmarshal(v interface{}) error {
|
func (o CommandInteractionOptions) Unmarshal(v interface{}) error {
|
||||||
return o.unmarshal(reflect.ValueOf(v))
|
return o.unmarshal(reflect.ValueOf(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o CommandInteractionOptions) unmarshal(rv reflect.Value) error {
|
func (o CommandInteractionOptions) unmarshal(rv reflect.Value) error {
|
||||||
rt := rv.Type()
|
rv, rt, err := rfutil.StructRValue(rv)
|
||||||
if rt.Kind() != reflect.Ptr {
|
if err != nil {
|
||||||
return errors.New("v is not a pointer")
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
rv = rv.Elem()
|
|
||||||
rt = rt.Elem()
|
|
||||||
if rt.Kind() != reflect.Struct {
|
|
||||||
return errors.New("v is not a pointer to a struct")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
numField := rt.NumField()
|
numField := rt.NumField()
|
||||||
|
|
26
internal/rfutil/rfutil.go
Normal file
26
internal/rfutil/rfutil.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package rfutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StructValue(v interface{}) (reflect.Value, reflect.Type, error) {
|
||||||
|
rv := reflect.ValueOf(v)
|
||||||
|
return StructRValue(rv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StructRValue(rv reflect.Value) (reflect.Value, reflect.Type, error) {
|
||||||
|
rt := rv.Type()
|
||||||
|
if rt.Kind() != reflect.Ptr {
|
||||||
|
return reflect.Value{}, nil, errors.New("v is not a pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
rv = rv.Elem()
|
||||||
|
rt = rt.Elem()
|
||||||
|
if rt.Kind() != reflect.Struct {
|
||||||
|
return reflect.Value{}, nil, errors.New("v is not a pointer to a struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv, rt, nil
|
||||||
|
}
|
Loading…
Reference in a new issue