mirror of
https://github.com/diamondburned/arikawa.git
synced 2025-01-05 19:57:02 +00:00
discord: Add CommandInteractionOptions.Unmarshal
This commit adds an Unmarshal method into CommandInteractionOptions. It is probably the first commit to break the rule of keeping package discord simple. The goal of this rationale is that package discord should continue prioritizing convenience and ease of use by providing small, helpful additions without being oversized. This method, while it uses reflect, is actually fairly small in implementation. Its functionality should be kept to a bare minimum and as such will not cover every single use case (but should cover most).
This commit is contained in:
parent
6bf83a4747
commit
47c06557c2
|
@ -1,6 +1,8 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||
|
@ -399,6 +401,140 @@ type CommandInteractionOption struct {
|
|||
Options CommandInteractionOptions `json:"options"`
|
||||
}
|
||||
|
||||
var optionSupportedSnowflakeTypes = map[reflect.Type]struct{}{
|
||||
reflect.TypeOf(ChannelID(0)): {},
|
||||
reflect.TypeOf(UserID(0)): {},
|
||||
reflect.TypeOf(RoleID(0)): {},
|
||||
reflect.TypeOf(MessageID(0)): {},
|
||||
reflect.TypeOf(Snowflake(0)): {},
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals the options 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.
|
||||
//
|
||||
// Supported Types
|
||||
//
|
||||
// The following types are supported:
|
||||
//
|
||||
// - ChannelID
|
||||
// - UserID
|
||||
// - RoleID
|
||||
// - MessageID
|
||||
// - Snowflake
|
||||
// - string
|
||||
// - bool
|
||||
// - int* (int, int8, int16, int32, int64)
|
||||
// - float* (float32, float64)
|
||||
// - (any struct and struct pointer)
|
||||
//
|
||||
// Any types that are derived from any of the above built-in types are also
|
||||
// supported.
|
||||
func (o CommandInteractionOptions) Unmarshal(v interface{}) error {
|
||||
return o.unmarshal(reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
func (o CommandInteractionOptions) unmarshal(rv reflect.Value) error {
|
||||
rt := rv.Type()
|
||||
if rt.Kind() != reflect.Ptr {
|
||||
return errors.New("v is not a pointer")
|
||||
}
|
||||
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
||||
option := o.Find(strings.TrimSuffix(name, "?"))
|
||||
fieldv := rv.Field(i)
|
||||
fieldt := fieldStruct.Type
|
||||
|
||||
if strings.HasSuffix(name, "?") {
|
||||
name = strings.TrimSuffix(name, "?")
|
||||
if option.Type == 0 {
|
||||
// not found
|
||||
continue
|
||||
}
|
||||
} else if fieldStruct.Type.Kind() == reflect.Ptr {
|
||||
fieldt = fieldt.Elem()
|
||||
if option.Type == 0 {
|
||||
// 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 option.Type == 0 {
|
||||
// not found AND the field is not a pointer, so error out
|
||||
return fmt.Errorf("option %q is required but not found", name)
|
||||
}
|
||||
|
||||
if _, ok := optionSupportedSnowflakeTypes[fieldt]; ok {
|
||||
snowflake, err := option.SnowflakeValue()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "option %q is not a valid snowflake", name)
|
||||
}
|
||||
|
||||
fieldv.Set(reflect.ValueOf(snowflake).Convert(fieldt))
|
||||
continue
|
||||
}
|
||||
|
||||
switch fieldt.Kind() {
|
||||
case reflect.Struct:
|
||||
if err := option.Options.unmarshal(fieldv.Addr()); err != nil {
|
||||
return errors.Wrapf(err, "option %q has invalid suboptions", name)
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i64, err := option.IntValue()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "option %q is not a valid int64", name)
|
||||
}
|
||||
fieldv.Set(reflect.ValueOf(i64).Convert(fieldt))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
i64, err := option.IntValue()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "option %q is not a valid int", name)
|
||||
}
|
||||
fieldv.Set(reflect.ValueOf(i64).Convert(fieldt))
|
||||
case reflect.Bool:
|
||||
b, err := option.BoolValue()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "option %q is not a valid bool", name)
|
||||
}
|
||||
fieldv.Set(reflect.ValueOf(b).Convert(fieldt))
|
||||
case reflect.String:
|
||||
fieldv.Set(reflect.ValueOf(option.String()).Convert(fieldt))
|
||||
default:
|
||||
return fmt.Errorf("field %s (%q) has unknown type %s", fieldStruct.Name, name, fieldt)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find returns the named command option
|
||||
func (o CommandInteractionOptions) Find(name string) CommandInteractionOption {
|
||||
for _, opt := range o {
|
||||
|
|
89
discord/interaction_example_test.go
Normal file
89
discord/interaction_example_test.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package discord_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
internaljson "github.com/diamondburned/arikawa/v3/utils/json"
|
||||
)
|
||||
|
||||
func ExampleCommandInteractionOptions_Unmarshal() {
|
||||
options := discord.CommandInteractionOptions{
|
||||
opt(discord.ChannelOptionType, "channel_id", 1),
|
||||
opt(discord.StringOptionType, "string1", "hello"),
|
||||
opt(discord.StringOptionType, "string2", "hello"),
|
||||
opt(discord.StringOptionType, "string3", "hello"),
|
||||
opt(discord.SubcommandOptionType, "sub", discord.CommandInteractionOptions{
|
||||
{
|
||||
Type: discord.RoleOptionType,
|
||||
Name: "role_id",
|
||||
Value: mustJSON("2"),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
var quickCommand struct {
|
||||
ChannelID discord.ChannelID `discord:"channel_id"`
|
||||
String1 string
|
||||
OptionalString2 *string `discord:"string2"`
|
||||
OptionalString3 string `discord:"string3?"`
|
||||
OptionalString4 *string `discord:"string4"`
|
||||
OptionalString5 string `discord:"string5?"`
|
||||
Suboption struct {
|
||||
RoleID discord.RoleID `discord:"role_id"`
|
||||
} `discord:"sub"`
|
||||
OptionalSuboption2 *struct {
|
||||
RoleID discord.RoleID `discord:"role_id"`
|
||||
} `discord:"sub2"`
|
||||
OptionalSuboption3 struct {
|
||||
RoleID discord.RoleID `discord:"role_id"`
|
||||
} `discord:"sub3?"`
|
||||
}
|
||||
|
||||
if err := options.Unmarshal(&quickCommand); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
b, _ := json.MarshalIndent(quickCommand, "", " ")
|
||||
fmt.Println(string(b))
|
||||
|
||||
// Output:
|
||||
// {
|
||||
// "ChannelID": "1",
|
||||
// "String1": "hello",
|
||||
// "OptionalString2": "hello",
|
||||
// "OptionalString3": "hello",
|
||||
// "OptionalString4": null,
|
||||
// "OptionalString5": "",
|
||||
// "Suboption": {
|
||||
// "RoleID": "2"
|
||||
// },
|
||||
// "OptionalSuboption2": null,
|
||||
// "OptionalSuboption3": {
|
||||
// "RoleID": null
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
func opt(t discord.CommandOptionType, name string, v interface{}) discord.CommandInteractionOption {
|
||||
o := discord.CommandInteractionOption{
|
||||
Type: t,
|
||||
Name: name,
|
||||
}
|
||||
if opts, ok := v.(discord.CommandInteractionOptions); ok {
|
||||
o.Options = opts
|
||||
} else {
|
||||
o.Value = mustJSON(v)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func mustJSON(v interface{}) internaljson.Raw {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return internaljson.Raw(b)
|
||||
}
|
Loading…
Reference in a new issue