1
0
Fork 0
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:
diamondburned 2022-08-15 14:57:30 -07:00
parent af940e5a37
commit adce55b02d
No known key found for this signature in database
GPG key ID: D78C4471CE776659
4 changed files with 265 additions and 9 deletions

View file

@ -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 {

View 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
// }
}

View file

@ -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
View 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
}