#slash-command #discord-bot #discord #statically-typed #request #type #checking

discord_typed_interactions

suppose you're working with discord slash commands and you want statically typed request types

1 unstable release

0.1.0 Sep 16, 2021

#15 in #slash-command

MIT/Apache

29KB
624 lines

discord-typed-interactions

I was writing a discord bot and all the dynamic command data checking was really painful so I was inspired to not do that ever again. Thus, this.

A few points to note:

  • Input paths are relative to Cargo.toml; include_str! is a compiler built-in and we don't have any easy way to replicate that behavior.
  • We do not re-export serde, so you will need to depend on serde and serde_json for the generated code to compile.

proc macro

use discord_typed_interactions::typify;
use serde_json::json;

typify!("./schema/ctf.json");

fn main() {
    let play = json!({
    "id":"868983602015252520",
    "name":"ctf",
    "options":[
       {
          "name":"play",
          "options":[
             {
                "name":"name",
                "value":"howdy"
             }
          ]
       }
    ]
    });
    serde_json::from_value::<ctf::Ctf>(play).unwrap();
    println!("{:#?}", play)
}
Ctf {
    id: "868983602015252520",
    name: "ctf",
    options: Play(
        Options {
            name: "howdy",
        },
    ),
    resolved: None,
}

build.rs

use discord_typed_interactions::Configuration;

fn main() {
    Configuration::new("schema/ctf.json")
        // .src("schema/other.json") // should you have more commands you can use Configuration::src multiple times
        .dest("src/command.rs")
        .generate();
}

generated code

schema

{
  "name": "ctf",
  "description": "placeholder",
  "options": [
    {
      "type": 1,
      "name": "add",
      "description": "placeholder",
      "options": [
        {
          "type": 3,
          "name": "name",
          "description": "placeholder",
          "required": true
        }
      ]
    },
    {
      "type": 1,
      "name": "archive",
      "description": "placeholder",
      "options": [
        {
          "type": 7,
          "name": "channel",
          "description": "placeholder"
        }
      ]
    },
    {
      "type": 2,
      "name": "players",
      "description": "placeholder",
      "options": [
        {
          "type": 1,
          "name": "add",
          "description": "placeholder",
          "options": [
            {
              "type": 9,
              "name": "name",
              "description": "placeholder",
              "required": true
            }
          ]
        },
        {
          "type": 1,
          "name": "remove",
          "description": "placeholder",
          "options": [
            {
              "type": 9,
              "name": "name",
              "description": "placeholder",
              "required": true
            }
          ]
        }
      ]
    }
  ]
}

```
That schema generates the following code.
generated code

pub mod ctf {
    pub mod add {
        use serde::{
            de::{SeqAccess, Visitor},
            Deserializer,
        };
        use std::fmt;
        #[derive(serde :: Serialize, Debug, Default)]
        pub struct Options {
            pub name: String,
        }
        impl<'de> serde::Deserialize<'de> for Options {
            fn deserialize>(deserializer: D) -> Result {
                struct PropertyParser;
                impl<'de> Visitor<'de> for PropertyParser {
                    type Value = Options;
                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                        formatter.write_str("aaa")
                    }
                    fn visit_seq>(
                        self,
                        mut seq: A,
                    ) -> Result {
                        #[allow(non_camel_case_types)]
                        #[derive(serde :: Deserialize, Debug)]
                        #[serde(tag = "name", content = "value")]
                        enum Property {
                            name(String),
                        }
                        let mut prop = Options::default();
                        while let Some(tmp) = seq.next_element::()? {
                            match tmp {
                                Property::name(v) => prop.name = v,
                            }
                        }
                        Ok(prop)
                    }
                }
                deserializer.deserialize_seq(PropertyParser)
            }
        }
    }
    pub mod archive {
        use serde::{
            de::{SeqAccess, Visitor},
            Deserializer,
        };
        use std::fmt;
        #[derive(serde :: Serialize, Debug, Default)]
        pub struct Options {
            pub channel: String,
        }
        impl<'de> serde::Deserialize<'de> for Options {
            fn deserialize>(deserializer: D) -> Result {
                struct PropertyParser;
                impl<'de> Visitor<'de> for PropertyParser {
                    type Value = Options;
                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                        formatter.write_str("aaa")
                    }
                    fn visit_seq>(
                        self,
                        mut seq: A,
                    ) -> Result {
                        #[allow(non_camel_case_types)]
                        #[derive(serde :: Deserialize, Debug)]
                        #[serde(tag = "name", content = "value")]
                        enum Property {
                            channel(String),
                        }
                        let mut prop = Options::default();
                        while let Some(tmp) = seq.next_element::()? {
                            match tmp {
                                Property::channel(v) => prop.channel = v,
                            }
                        }
                        Ok(prop)
                    }
                }
                deserializer.deserialize_seq(PropertyParser)
            }
        }
    }
    #[derive(serde :: Serialize, serde :: Deserialize, Debug)]
    #[serde(tag = "name", rename_all = "snake_case")]
    pub struct Ctf {
        pub id: String,
        #[serde(deserialize_with = "parse_single")]
        pub options: Options,
        pub resolved: Option,
    }
    #[derive(serde :: Serialize, serde :: Deserialize, Debug)]
    #[serde(tag = "name", content = "options", rename_all = "snake_case")]
    pub enum Options {
        Add(add::Options),
        Archive(archive::Options),
        #[serde(deserialize_with = "parse_single")]
        Players(players::Players),
    }
    use serde::{
        de::{Error, SeqAccess, Visitor},
        Deserializer,
    };
    use std::fmt;
    use std::marker::PhantomData;
    fn parse_single<'de, D: Deserializer<'de>, T: serde::Deserialize<'de>>(
        deserializer: D,
    ) -> Result {
        struct PropertyParser(PhantomData);
        impl<'de, T: serde::Deserialize<'de>> Visitor<'de> for PropertyParser {
            type Value = T;
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                write!(
                    formatter,
                    "a nonempty list of {}",
                    std::any::type_name::()
                )
            }
            fn visit_seq>(self, mut seq: A) -> Result {
                seq.next_element::()?
                    .ok_or_else(|| A::Error::custom("empty array"))
            }
        }
        deserializer.deserialize_seq(PropertyParser(PhantomData))
    }
    pub mod players {
        pub mod add {
            use serde::{
                de::{SeqAccess, Visitor},
                Deserializer,
            };
            use std::fmt;
            #[derive(serde :: Serialize, Debug, Default)]
            pub struct Options {
                pub name: String,
            }
            impl<'de> serde::Deserialize<'de> for Options {
                fn deserialize>(deserializer: D) -> Result {
                    struct PropertyParser;
                    impl<'de> Visitor<'de> for PropertyParser {
                        type Value = Options;
                        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                            formatter.write_str("aaa")
                        }
                        fn visit_seq>(
                            self,
                            mut seq: A,
                        ) -> Result {
                            #[allow(non_camel_case_types)]
                            #[derive(serde :: Deserialize, Debug)]
                            #[serde(tag = "name", content = "value")]
                            enum Property {
                                name(String),
                            }
                            let mut prop = Options::default();
                            while let Some(tmp) = seq.next_element::()? {
                                match tmp {
                                    Property::name(v) => prop.name = v,
                                }
                            }
                            Ok(prop)
                        }
                    }
                    deserializer.deserialize_seq(PropertyParser)
                }
            }
        }
        pub mod remove {
            use serde::{
                de::{SeqAccess, Visitor},
                Deserializer,
            };
            use std::fmt;
            #[derive(serde :: Serialize, Debug, Default)]
            pub struct Options {
                pub name: String,
            }
            impl<'de> serde::Deserialize<'de> for Options {
                fn deserialize>(deserializer: D) -> Result {
                    struct PropertyParser;
                    impl<'de> Visitor<'de> for PropertyParser {
                        type Value = Options;
                        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                            formatter.write_str("aaa")
                        }
                        fn visit_seq>(
                            self,
                            mut seq: A,
                        ) -> Result {
                            #[allow(non_camel_case_types)]
                            #[derive(serde :: Deserialize, Debug)]
                            #[serde(tag = "name", content = "value")]
                            enum Property {
                                name(String),
                            }
                            let mut prop = Options::default();
                            while let Some(tmp) = seq.next_element::()? {
                                match tmp {
                                    Property::name(v) => prop.name = v,
                                }
                            }
                            Ok(prop)
                        }
                    }
                    deserializer.deserialize_seq(PropertyParser)
                }
            }
        }
        #[derive(serde :: Serialize, serde :: Deserialize, Debug)]
        #[serde(tag = "name", content = "options")]
        #[serde(rename_all = "snake_case")]
        pub enum Players {
            Add(add::Options),
            Remove(remove::Options),
        }
    }
}
#[derive(serde :: Serialize, Debug)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum Interaction {
    Ping(Ping),
    ApplicationCommand(ApplicationCommand),
}
use serde::de::Error;
impl<'de> serde::Deserialize<'de> for Interaction {
    fn deserialize>(deserializer: D) -> Result {
        let value = serde_json::Value::deserialize(deserializer)?;
        Ok(
            match value
                .get("type")
                .and_then(serde_json::Value::as_u64)
                .ok_or_else(|| D::Error::custom("type field is either missing or not u64"))?
            {
                1 => Interaction::Ping(
                    Ping::deserialize(value).map_err(|x| D::Error::custom(x.to_string()))?,
                ),
                2 => Interaction::ApplicationCommand(
                    ApplicationCommand::deserialize(value)
                        .map_err(|x| D::Error::custom(x.to_string()))?,
                ),
                _ => panic!("type isn't valid"),
            },
        )
    }
}
#[derive(serde :: Serialize, serde :: Deserialize, Debug)]
pub struct Ping {
    pub application_id: String,
    pub id: String,
    pub r#type: u64,
    pub token: String,
}
#[derive(serde :: Serialize, serde :: Deserialize, Debug)]
pub struct ApplicationCommand {
    pub application_id: String,
    pub channel_id: String,
    pub data: Command,
    pub guild_id: Option,
    pub id: String,
    pub member: Option,
    pub user: Option,
    pub token: String,
    pub r#type: u64,
    pub version: u64,
}
#[derive(serde :: Serialize, serde :: Deserialize, Debug)]
#[serde(untagged)]
pub enum Command {
    Ctf(ctf::Ctf),
    Other { id: String, name: String },
}
#[derive(serde :: Serialize, serde :: Deserialize, Debug)]
pub struct User {
    pub id: String,
    pub username: String,
    pub discriminator: String,
    pub avatar: String,
    pub bot: Option,
    pub system: Option,
    pub mfa_enabled: Option,
    pub locale: Option,
    pub verified: Option,
    pub email: Option,
    pub flags: Option,
    pub premium_type: Option,
    pub public_flags: Option,
}
#[derive(serde :: Serialize, serde :: Deserialize, Debug)]
pub struct PartialMember {
    pub user: Option,
    pub nick: Option,
    pub roles: Vec,
    pub joined_at: String,
    pub premium_since: Option,
    pub deaf: Option,
    pub mute: Option,
    pub pending: Option,
    pub permissions: Option,
}
use std::collections::HashMap;
#[derive(serde :: Serialize, serde :: Deserialize, Debug)]
pub struct Resolved {
    #[serde(default)]
    pub users: HashMap,
    #[serde(default)]
    pub members: HashMap,
    #[serde(default)]
    pub roles: HashMap,
    #[serde(default)]
    pub channels: HashMap,
}
#[derive(serde :: Serialize, serde :: Deserialize, Debug)]
pub struct Role {
    pub id: String,
    pub name: String,
    pub color: u64,
    pub hoist: bool,
    pub position: u64,
    pub permissions: String,
    pub managed: bool,
    pub mentionable: bool,
    pub tags: Option,
}
#[derive(serde :: Serialize, serde :: Deserialize, Debug)]
pub struct RoleTags {
    pub bot_id: Option,
    pub integration_id: Option,
    pub premium_subscriber: Option,
}
#[derive(serde :: Serialize, serde :: Deserialize, Debug)]
pub struct PartialChannel {
    pub id: String,
    pub r#type: u64,
    pub name: String,
    pub permissions: String,
}

Dependencies

~0.7–1.6MB
~35K SLoC