├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ ├── docs.yml │ └── lint.yml ├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md ├── command_attr ├── Cargo.toml └── src │ ├── impl_check │ ├── mod.rs │ └── options.rs │ ├── impl_command │ ├── mod.rs │ └── options.rs │ ├── impl_hook.rs │ ├── lib.rs │ ├── paths.rs │ └── utils.rs ├── framework ├── Cargo.toml └── src │ ├── argument.rs │ ├── category.rs │ ├── check.rs │ ├── command.rs │ ├── configuration.rs │ ├── context.rs │ ├── error.rs │ ├── lib.rs │ ├── parse.rs │ ├── prelude.rs │ └── utils │ ├── id_map.rs │ ├── mod.rs │ └── segments.rs └── rustfmt.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [*.rs] 10 | charset = utf-8 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 9 | 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | name: 14 | - stable 15 | - beta 16 | - nightly 17 | - macOS 18 | - Windows 19 | 20 | include: 21 | - name: beta 22 | toolchain: beta 23 | - name: nightly 24 | toolchain: nightly 25 | - name: macOS 26 | os: macOS-latest 27 | - name: Windows 28 | os: windows-latest 29 | 30 | steps: 31 | - name: Checkout sources 32 | uses: actions/checkout@v2 33 | 34 | - name: Install toolchain 35 | id: tc 36 | uses: actions-rs/toolchain@v1 37 | with: 38 | toolchain: ${{ matrix.toolchain || 'stable' }} 39 | profile: minimal 40 | override: true 41 | 42 | - name: Setup cache 43 | if: runner.os != 'macOS' 44 | uses: actions/cache@v2 45 | with: 46 | path: | 47 | ~/.cargo/registry 48 | ~/.cargo/git 49 | target 50 | key: ${{ runner.os }}-test-${{ steps.tc.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.toml') }} 51 | 52 | - name: Build all features 53 | run: cargo build --all-features 54 | 55 | - name: Test all features 56 | run: cargo test --all-features 57 | 58 | doc: 59 | name: Build docs 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - name: Checkout sources 64 | uses: actions/checkout@v2 65 | 66 | - name: Install toolchain 67 | id: tc 68 | uses: actions-rs/toolchain@v1 69 | with: 70 | toolchain: nightly 71 | profile: minimal 72 | override: true 73 | 74 | - name: Setup cache 75 | uses: actions/cache@v2 76 | with: 77 | path: | 78 | ~/.cargo/registry 79 | ~/.cargo/git 80 | key: ${{ runner.os }}-docs-${{ steps.tc.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.toml') }} 81 | 82 | - name: Build docs 83 | env: 84 | RUSTDOCFLAGS: -D broken_intra_doc_links 85 | run: | 86 | cargo doc --no-deps -p serenity_framework 87 | cargo doc --no-deps -p command_attr 88 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | docs: 10 | name: Publish docs 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v2 16 | 17 | - name: Install toolchain 18 | id: tc 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: nightly 22 | profile: minimal 23 | override: true 24 | 25 | - name: Setup cache 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ~/.cargo/registry 30 | ~/.cargo/git 31 | target/debug 32 | key: ${{ runner.os }}-gh-pages-${{ steps.tc.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.toml') }} 33 | 34 | - name: Build docs 35 | env: 36 | RUSTDOCFLAGS: -D broken_intra_doc_links 37 | run: | 38 | cargo doc --no-deps -p serenity_framework 39 | cargo doc --no-deps -p command_attr 40 | 41 | - name: Prepare docs 42 | shell: bash -e -O extglob {0} 43 | run: | 44 | DIR=${GITHUB_REF/refs\/+(heads|tags)\//} 45 | mkdir -p ./docs/$DIR 46 | touch ./docs/.nojekyll 47 | echo '' > ./docs/$DIR/index.html 48 | mv ./target/doc/* ./docs/$DIR/ 49 | 50 | - name: Deploy docs 51 | uses: peaceiris/actions-gh-pages@v3 52 | with: 53 | github_token: ${{ secrets.GITHUB_TOKEN }} 54 | publish_branch: gh-pages 55 | publish_dir: ./docs 56 | allow_empty_commit: false 57 | keep_files: true 58 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # Copied from Twilight's Lint workflow. 2 | # 3 | # https://github.com/twilight-rs/twilight/blob/trunk/.github/workflows/lint.yml 4 | name: Lint 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | clippy: 10 | name: Clippy 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v2 16 | 17 | - name: Install stable toolchain 18 | id: toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: nightly 22 | components: clippy 23 | profile: minimal 24 | override: true 25 | 26 | - name: Setup cache 27 | uses: actions/cache@v2 28 | with: 29 | path: | 30 | ~/.cargo/registry 31 | ~/.cargo/git 32 | target 33 | key: ${{ runner.os }}-clippy-rustc-${{ steps.toolchain.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }} 34 | 35 | - name: Run clippy 36 | uses: actions-rs/clippy-check@v1 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | args: --workspace --tests 40 | 41 | rustfmt: 42 | name: Format 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - name: Checkout sources 47 | uses: actions/checkout@v2 48 | 49 | - name: Install stable toolchain 50 | uses: actions-rs/toolchain@v1 51 | with: 52 | toolchain: nightly 53 | components: rustfmt 54 | profile: minimal 55 | override: true 56 | 57 | - name: Run cargo fmt 58 | run: cargo fmt --all -- --check 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["framework", "command_attr"] 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2020, Serenity Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serenity Framework 2 | 3 | The official command framework for the [Serenity] Discord API wrapper. 4 | 5 | [Serenity]: https://github.com/serenity-rs/serenity 6 | -------------------------------------------------------------------------------- /command_attr/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "command_attr" 3 | version = "0.1.0" 4 | authors = ["Alex M. M. "] 5 | edition = "2018" 6 | description = "Hook macro extracted from the `command_attr` crate" 7 | license = "ISC" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | quote = "1.0" 14 | syn = { version = "1.0", features = ["full", "derive", "extra-traits"] } 15 | proc-macro2 = "1.0" 16 | -------------------------------------------------------------------------------- /command_attr/src/impl_check/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::parse2; 4 | use syn::{ItemFn, Result, Type}; 5 | 6 | use crate::paths; 7 | use crate::utils; 8 | 9 | mod options; 10 | 11 | use options::Options; 12 | 13 | pub fn impl_check(attr: TokenStream, input: TokenStream) -> Result { 14 | let mut fun = parse2::(input)?; 15 | 16 | let name = if attr.is_empty() { 17 | fun.sig.ident.to_string() 18 | } else { 19 | parse2::(attr)?.value() 20 | }; 21 | 22 | let (_, _, data, error) = utils::parse_generics(&fun.sig)?; 23 | let options = Options::parse(&mut fun.attrs)?; 24 | 25 | let builder_fn = builder_fn(&data, &error, &mut fun, &name, &options); 26 | 27 | let hook_macro = paths::hook_macro(); 28 | 29 | let result = quote! { 30 | #builder_fn 31 | 32 | #[#hook_macro] 33 | #[doc(hidden)] 34 | #fun 35 | }; 36 | 37 | Ok(result) 38 | } 39 | 40 | fn builder_fn( 41 | data: &Type, 42 | error: &Type, 43 | function: &mut ItemFn, 44 | name: &str, 45 | options: &Options, 46 | ) -> TokenStream { 47 | // Derive the name of the builder from the check function. 48 | // Prepend the check function's name with an underscore to avoid name 49 | // collisions. 50 | let builder_name = function.sig.ident.clone(); 51 | let function_name = format_ident!("_{}", builder_name); 52 | function.sig.ident = function_name.clone(); 53 | 54 | let check_builder = paths::check_builder_type(); 55 | let check = paths::check_type(data, error); 56 | 57 | let vis = &function.vis; 58 | let external = &function.attrs; 59 | 60 | quote! { 61 | #(#external)* 62 | #vis fn #builder_name() -> #check { 63 | #check_builder::new(#name) 64 | .function(#function_name) 65 | #options 66 | .build() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /command_attr/src/impl_check/options.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::{quote, ToTokens}; 5 | use syn::{Attribute, Result}; 6 | 7 | use crate::utils::parse_bool; 8 | 9 | #[derive(Default)] 10 | pub struct Options { 11 | check_in_help: Option, 12 | display_in_help: Option, 13 | } 14 | 15 | impl Options { 16 | pub fn parse(attrs: &mut Vec) -> Result { 17 | let mut options = Self::default(); 18 | 19 | let mut i = 0; 20 | 21 | while i < attrs.len() { 22 | let attr = &attrs[i]; 23 | let name = attr.path.get_ident().unwrap().to_string(); 24 | 25 | match name.as_str() { 26 | "check_in_help" => options.check_in_help = Some(parse_bool(&attr.try_into()?)?), 27 | "display_in_help" => options.display_in_help = Some(parse_bool(&attr.try_into()?)?), 28 | _ => { 29 | i += 1; 30 | 31 | continue; 32 | }, 33 | } 34 | 35 | attrs.remove(i); 36 | } 37 | 38 | Ok(options) 39 | } 40 | } 41 | 42 | impl ToTokens for Options { 43 | fn to_tokens(&self, tokens: &mut TokenStream) { 44 | let Options { 45 | check_in_help, 46 | display_in_help, 47 | } = self; 48 | 49 | if let Some(check) = check_in_help { 50 | tokens.extend(quote!(.check_in_help(#check))); 51 | } 52 | 53 | if let Some(display) = display_in_help { 54 | tokens.extend(quote!(.display_in_help(#display))); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /command_attr/src/impl_command/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, TokenStream}; 2 | use quote::{format_ident, quote, ToTokens}; 3 | use syn::spanned::Spanned; 4 | use syn::{parse2, Attribute, Error, FnArg, ItemFn, Path, Result, Type}; 5 | 6 | use crate::paths; 7 | use crate::utils::{self, AttributeArgs}; 8 | 9 | mod options; 10 | 11 | use options::Options; 12 | 13 | pub fn impl_command(attr: TokenStream, input: TokenStream) -> Result { 14 | let mut fun = parse2::(input)?; 15 | 16 | let names = if attr.is_empty() { 17 | vec![fun.sig.ident.to_string()] 18 | } else { 19 | parse2::(attr)?.0 20 | }; 21 | 22 | let (ctx_name, msg_name, data, error) = utils::parse_generics(&fun.sig)?; 23 | let options = Options::parse(&mut fun.attrs)?; 24 | 25 | parse_arguments(ctx_name, msg_name, &mut fun, &options)?; 26 | 27 | let builder_fn = builder_fn(&data, &error, &mut fun, names, &options); 28 | 29 | let hook_macro = paths::hook_macro(); 30 | 31 | let result = quote! { 32 | #builder_fn 33 | 34 | #[#hook_macro] 35 | #[doc(hidden)] 36 | #fun 37 | }; 38 | 39 | Ok(result) 40 | } 41 | 42 | fn builder_fn( 43 | data: &Type, 44 | error: &Type, 45 | function: &mut ItemFn, 46 | mut names: Vec, 47 | options: &Options, 48 | ) -> TokenStream { 49 | let name = names.remove(0); 50 | let aliases = names; 51 | 52 | // Derive the name of the builder from the command function. 53 | // Prepend the command function's name with an underscore to avoid name 54 | // collisions. 55 | let builder_name = function.sig.ident.clone(); 56 | let function_name = format_ident!("_{}", builder_name); 57 | function.sig.ident = function_name.clone(); 58 | 59 | let command_builder = paths::command_builder_type(); 60 | let command = paths::command_type(data, error); 61 | 62 | let vis = &function.vis; 63 | let external = &function.attrs; 64 | 65 | quote! { 66 | #(#external)* 67 | #vis fn #builder_name() -> #command { 68 | #command_builder::new(#name) 69 | #(.name(#aliases))* 70 | .function(#function_name) 71 | #options 72 | .build() 73 | } 74 | } 75 | } 76 | 77 | fn parse_arguments( 78 | ctx_name: Ident, 79 | msg_name: Ident, 80 | function: &mut ItemFn, 81 | options: &Options, 82 | ) -> Result<()> { 83 | let mut arguments = Vec::new(); 84 | 85 | while function.sig.inputs.len() > 2 { 86 | let argument = function.sig.inputs.pop().unwrap().into_value(); 87 | 88 | arguments.push(Argument::new(argument)?); 89 | } 90 | 91 | if !arguments.is_empty() { 92 | arguments.reverse(); 93 | 94 | check_arguments(&arguments)?; 95 | 96 | let delimiter = options.delimiter.as_ref().map_or(" ", String::as_str); 97 | let asegsty = paths::argument_segments_type(); 98 | 99 | let b = &function.block; 100 | 101 | let argument_names = arguments.iter().map(|arg| &arg.name).collect::>(); 102 | let argument_tys = arguments.iter().map(|arg| &arg.ty).collect::>(); 103 | let argument_parsers = arguments.iter().map(|arg| &arg.parser).collect::>(); 104 | 105 | function.block = parse2(quote! {{ 106 | let (#(#argument_names),*) = { 107 | // Place the segments into its scope to allow mutation of `Context::args` 108 | // afterwards, as `ArgumentSegments` holds a reference to the source string. 109 | let mut __args = #asegsty::new(&#ctx_name.args, #delimiter); 110 | 111 | #(let #argument_names: #argument_tys = #argument_parsers( 112 | &#ctx_name.serenity_ctx, 113 | &#msg_name, 114 | &mut __args 115 | ).await?;)* 116 | 117 | (#(#argument_names),*) 118 | }; 119 | 120 | #b 121 | }})?; 122 | } 123 | 124 | Ok(()) 125 | } 126 | 127 | /// Returns a result indicating whether the list of arguments is valid. 128 | /// 129 | /// Valid is defined as: 130 | /// - a list of arguments that have required arguments first, 131 | /// optional arguments second, and variadic arguments third; one or two of these 132 | /// types of arguments can be missing. 133 | /// - a list of arguments that only has one variadic argument parameter, if present. 134 | /// - a list of arguments that only has one rest argument parameter, if present. 135 | /// - a list of arguments that only has one variadic argument parameter or one rest 136 | /// argument parameter. 137 | fn check_arguments(args: &[Argument]) -> Result<()> { 138 | let mut last_arg: Option<&Argument> = None; 139 | 140 | for arg in args { 141 | if let Some(last_arg) = last_arg { 142 | match (last_arg.parser.type_, arg.parser.type_) { 143 | (ArgumentType::Optional, ArgumentType::Required) => { 144 | return Err(Error::new( 145 | last_arg.name.span(), 146 | "optional argument cannot precede a required argument", 147 | )); 148 | }, 149 | (ArgumentType::Variadic, ArgumentType::Required) => { 150 | return Err(Error::new( 151 | last_arg.name.span(), 152 | "variadic argument cannot precede a required argument", 153 | )); 154 | }, 155 | (ArgumentType::Variadic, ArgumentType::Optional) => { 156 | return Err(Error::new( 157 | last_arg.name.span(), 158 | "variadic argument cannot precede an optional argument", 159 | )); 160 | }, 161 | (ArgumentType::Rest, ArgumentType::Required) => { 162 | return Err(Error::new( 163 | last_arg.name.span(), 164 | "rest argument cannot precede a required argument", 165 | )); 166 | }, 167 | (ArgumentType::Rest, ArgumentType::Optional) => { 168 | return Err(Error::new( 169 | last_arg.name.span(), 170 | "rest argument cannot precede an optional argument", 171 | )); 172 | }, 173 | (ArgumentType::Rest, ArgumentType::Variadic) => { 174 | return Err(Error::new( 175 | last_arg.name.span(), 176 | "a rest argument cannot be used alongside a variadic argument", 177 | )); 178 | }, 179 | (ArgumentType::Variadic, ArgumentType::Rest) => { 180 | return Err(Error::new( 181 | last_arg.name.span(), 182 | "a variadic argument cannot be used alongside a rest argument", 183 | )); 184 | }, 185 | (ArgumentType::Variadic, ArgumentType::Variadic) => { 186 | return Err(Error::new( 187 | arg.name.span(), 188 | "a command cannot have two variadic argument parameters", 189 | )); 190 | }, 191 | (ArgumentType::Rest, ArgumentType::Rest) => { 192 | return Err(Error::new( 193 | arg.name.span(), 194 | "a command cannot have two rest argument parameters", 195 | )); 196 | }, 197 | (ArgumentType::Required, ArgumentType::Required) 198 | | (ArgumentType::Optional, ArgumentType::Optional) 199 | | (ArgumentType::Required, ArgumentType::Optional) 200 | | (ArgumentType::Required, ArgumentType::Variadic) 201 | | (ArgumentType::Optional, ArgumentType::Variadic) 202 | | (ArgumentType::Required, ArgumentType::Rest) 203 | | (ArgumentType::Optional, ArgumentType::Rest) => {}, 204 | }; 205 | } 206 | 207 | last_arg = Some(arg); 208 | } 209 | 210 | Ok(()) 211 | } 212 | 213 | struct Argument { 214 | name: Ident, 215 | ty: Box, 216 | parser: ArgumentParser, 217 | } 218 | 219 | impl Argument { 220 | fn new(arg: FnArg) -> Result { 221 | let binding = utils::get_pat_type(&arg)?; 222 | 223 | let name = utils::get_ident(&binding.pat)?; 224 | 225 | let ty = binding.ty.clone(); 226 | 227 | let path = utils::get_path(&ty)?; 228 | let parser = ArgumentParser::new(&binding.attrs, path)?; 229 | 230 | Ok(Self { 231 | name, 232 | ty, 233 | parser, 234 | }) 235 | } 236 | } 237 | 238 | #[derive(Clone, Copy)] 239 | enum ArgumentType { 240 | Required, 241 | Optional, 242 | Variadic, 243 | Rest, 244 | } 245 | 246 | #[derive(Clone, Copy)] 247 | struct ArgumentParser { 248 | type_: ArgumentType, 249 | use_parse_trait: bool, 250 | } 251 | 252 | impl ArgumentParser { 253 | fn new(attrs: &[Attribute], path: &Path) -> Result { 254 | let mut is_rest_argument = false; 255 | let mut use_parse_trait = false; 256 | for attr in attrs { 257 | let attr = utils::parse_attribute(attr)?; 258 | 259 | if attr.path.is_ident("rest") { 260 | is_rest_argument = true; 261 | 262 | if !attr.values.is_empty() { 263 | return Err(Error::new( 264 | attrs[0].span(), 265 | "the `rest` attribute does not accept any input", 266 | )); 267 | } 268 | } else if attr.path.is_ident("parse") { 269 | use_parse_trait = true; 270 | 271 | if !attr.values.is_empty() { 272 | return Err(Error::new( 273 | attrs[0].span(), 274 | "the `parse` attribute does not accept any input", 275 | )); 276 | } 277 | } else { 278 | return Err(Error::new( 279 | attrs[0].span(), 280 | "invalid attribute name, expected `rest` or `parse`", 281 | )); 282 | } 283 | } 284 | 285 | let type_ = if is_rest_argument { 286 | ArgumentType::Rest 287 | } else { 288 | match path.segments.last().unwrap().ident.to_string().as_str() { 289 | "Option" => ArgumentType::Optional, 290 | "Vec" => ArgumentType::Variadic, 291 | _ => ArgumentType::Required, 292 | } 293 | }; 294 | 295 | Ok(Self { 296 | type_, 297 | use_parse_trait, 298 | }) 299 | } 300 | } 301 | 302 | impl ToTokens for ArgumentParser { 303 | fn to_tokens(&self, tokens: &mut TokenStream) { 304 | let path = match (self.type_, self.use_parse_trait) { 305 | (ArgumentType::Required, false) => paths::required_argument_from_str_func(), 306 | (ArgumentType::Required, true) => paths::required_argument_parse_func(), 307 | (ArgumentType::Optional, false) => paths::optional_argument_from_str_func(), 308 | (ArgumentType::Optional, true) => paths::optional_argument_parse_func(), 309 | (ArgumentType::Variadic, false) => paths::variadic_arguments_from_str_func(), 310 | (ArgumentType::Variadic, true) => paths::variadic_arguments_parse_func(), 311 | (ArgumentType::Rest, false) => paths::rest_argument_from_str_func(), 312 | (ArgumentType::Rest, true) => paths::rest_argument_parse_func(), 313 | }; 314 | 315 | tokens.extend(quote!(#path)); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /command_attr/src/impl_command/options.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use proc_macro2::{Ident, TokenStream}; 4 | use quote::{quote, ToTokens}; 5 | use syn::{Attribute, Result}; 6 | 7 | use crate::utils::{parse_bool, parse_identifier, parse_identifiers, parse_string}; 8 | 9 | #[derive(Default)] 10 | pub struct Options { 11 | subcommands: Vec, 12 | description: Option, 13 | dynamic_description: Option, 14 | usage: Option, 15 | dynamic_usage: Option, 16 | examples: Vec, 17 | dynamic_examples: Option, 18 | help_available: Option, 19 | check: Option, 20 | pub delimiter: Option, 21 | } 22 | 23 | impl Options { 24 | pub fn parse(attrs: &mut Vec) -> Result { 25 | let mut options = Self::default(); 26 | 27 | let mut i = 0; 28 | 29 | while i < attrs.len() { 30 | let attr = &attrs[i]; 31 | let name = attr.path.get_ident().unwrap().to_string(); 32 | 33 | match name.as_str() { 34 | "doc" | "description" => { 35 | let desc = options.description.get_or_insert_with(String::new); 36 | 37 | if !desc.is_empty() { 38 | desc.push('\n'); 39 | } 40 | 41 | let mut s = parse_string(&attr.try_into()?)?; 42 | 43 | if s.starts_with(' ') { 44 | s.remove(0); 45 | } 46 | 47 | desc.push_str(&s); 48 | }, 49 | "subcommands" => options.subcommands = parse_identifiers(&attr.try_into()?)?, 50 | "dynamic_description" => { 51 | options.dynamic_description = Some(parse_identifier(&attr.try_into()?)?) 52 | }, 53 | "usage" => options.usage = Some(parse_string(&attr.try_into()?)?), 54 | "dynamic_usage" => { 55 | options.dynamic_usage = Some(parse_identifier(&attr.try_into()?)?) 56 | }, 57 | "example" => options.examples.push(parse_string(&attr.try_into()?)?), 58 | "dynamic_examples" => { 59 | options.dynamic_examples = Some(parse_identifier(&attr.try_into()?)?) 60 | }, 61 | "help_available" => options.help_available = Some(parse_bool(&attr.try_into()?)?), 62 | "check" => options.check = Some(parse_identifier(&attr.try_into()?)?), 63 | "delimiter" => options.delimiter = Some(parse_string(&attr.try_into()?)?), 64 | _ => { 65 | i += 1; 66 | 67 | continue; 68 | }, 69 | } 70 | 71 | attrs.remove(i); 72 | } 73 | 74 | Ok(options) 75 | } 76 | } 77 | 78 | impl ToTokens for Options { 79 | fn to_tokens(&self, tokens: &mut TokenStream) { 80 | let Options { 81 | subcommands, 82 | description, 83 | dynamic_description, 84 | usage, 85 | dynamic_usage, 86 | examples, 87 | dynamic_examples, 88 | help_available, 89 | check, 90 | .. 91 | } = self; 92 | 93 | tokens.extend(quote! { 94 | #(.subcommand(#subcommands))* 95 | }); 96 | 97 | if let Some(desc) = description { 98 | tokens.extend(quote!(.description(#desc))); 99 | } 100 | 101 | if let Some(dyn_desc) = dynamic_description { 102 | tokens.extend(quote!(.dynamic_description(#dyn_desc))); 103 | } 104 | 105 | if let Some(usage) = usage { 106 | tokens.extend(quote!(.usage(#usage))); 107 | } 108 | 109 | if let Some(dyn_usage) = dynamic_usage { 110 | tokens.extend(quote!(.dynamic_usage(#dyn_usage))); 111 | } 112 | 113 | tokens.extend(quote! { 114 | #(.example(#examples))* 115 | }); 116 | 117 | if let Some(dyn_examples) = dynamic_examples { 118 | tokens.extend(quote!(.dynamic_examples(#dyn_examples))); 119 | } 120 | 121 | if let Some(help_available) = help_available { 122 | tokens.extend(quote!(.help_available(#help_available))); 123 | } 124 | 125 | if let Some(check) = check { 126 | tokens.extend(quote!(.check(#check))); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /command_attr/src/impl_hook.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::parse2; 4 | use syn::punctuated::Punctuated; 5 | use syn::spanned::Spanned; 6 | use syn::{Error, FnArg, GenericParam, Generics, ItemFn, Lifetime}; 7 | use syn::{LifetimeDef, Result, ReturnType, Signature, Token, Type}; 8 | 9 | pub fn impl_hook(attr: TokenStream, input: TokenStream) -> Result { 10 | if !attr.is_empty() { 11 | return Err(Error::new(attr.span(), "parameters to the `#[hook]` macro are ignored")); 12 | } 13 | 14 | let fun = parse2::(input)?; 15 | 16 | let ItemFn { 17 | attrs, 18 | vis, 19 | sig, 20 | block, 21 | } = fun; 22 | 23 | let sig_span = sig.span(); 24 | let Signature { 25 | asyncness, 26 | ident, 27 | mut inputs, 28 | output, 29 | mut generics, 30 | .. 31 | } = sig; 32 | 33 | if asyncness.is_none() { 34 | return Err(Error::new(sig_span, "`async` keyword is missing")); 35 | } 36 | 37 | let output = match output { 38 | ReturnType::Default => quote!(()), 39 | ReturnType::Type(_, t) => quote!(#t), 40 | }; 41 | 42 | add_fut_lifetime(&mut generics); 43 | populate_lifetime(&mut inputs); 44 | 45 | let result = quote! { 46 | #(#attrs)* 47 | #vis fn #ident #generics (#inputs) -> std::pin::Pin + 'fut + Send>> { 48 | Box::pin(async move { 49 | // Nudge the compiler into providing us with a good error message 50 | // when the return type of the body does not match with the return 51 | // type of the function. 52 | let result: #output = #block; 53 | result 54 | }) 55 | } 56 | }; 57 | 58 | Ok(result) 59 | } 60 | 61 | fn add_fut_lifetime(generics: &mut Generics) { 62 | generics.params.insert( 63 | 0, 64 | GenericParam::Lifetime(LifetimeDef { 65 | attrs: Vec::default(), 66 | lifetime: Lifetime::new("'fut", Span::call_site()), 67 | colon_token: None, 68 | bounds: Punctuated::default(), 69 | }), 70 | ); 71 | } 72 | 73 | fn populate_lifetime(inputs: &mut Punctuated) { 74 | for input in inputs { 75 | if let FnArg::Typed(kind) = input { 76 | if let Type::Reference(ty) = &mut *kind.ty { 77 | ty.lifetime = Some(Lifetime::new("'fut", Span::call_site())); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /command_attr/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | mod paths; 4 | mod utils; 5 | 6 | mod impl_check; 7 | mod impl_command; 8 | mod impl_hook; 9 | 10 | use impl_check::impl_check; 11 | use impl_command::impl_command; 12 | use impl_hook::impl_hook; 13 | 14 | #[proc_macro_attribute] 15 | pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { 16 | match impl_command(attr.into(), input.into()) { 17 | Ok(stream) => stream.into(), 18 | Err(err) => err.to_compile_error().into(), 19 | } 20 | } 21 | 22 | #[proc_macro_attribute] 23 | pub fn check(attr: TokenStream, input: TokenStream) -> TokenStream { 24 | match impl_check(attr.into(), input.into()) { 25 | Ok(stream) => stream.into(), 26 | Err(err) => err.to_compile_error().into(), 27 | } 28 | } 29 | 30 | #[proc_macro_attribute] 31 | pub fn hook(attr: TokenStream, input: TokenStream) -> TokenStream { 32 | match impl_hook(attr.into(), input.into()) { 33 | Ok(stream) => stream.into(), 34 | Err(err) => err.to_compile_error().into(), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /command_attr/src/paths.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{parse2, Path, Type}; 4 | 5 | fn to_path(tokens: TokenStream) -> Path { 6 | parse2(tokens).unwrap() 7 | } 8 | 9 | fn to_type(tokens: TokenStream) -> Box { 10 | parse2(tokens).unwrap() 11 | } 12 | 13 | pub fn default_data_type() -> Box { 14 | to_type(quote! { 15 | serenity_framework::DefaultData 16 | }) 17 | } 18 | 19 | pub fn default_error_type() -> Box { 20 | to_type(quote! { 21 | serenity_framework::DefaultError 22 | }) 23 | } 24 | 25 | pub fn command_type(data: &Type, error: &Type) -> Path { 26 | to_path(quote! { 27 | serenity_framework::command::Command<#data, #error> 28 | }) 29 | } 30 | 31 | pub fn command_builder_type() -> Path { 32 | to_path(quote! { 33 | serenity_framework::command::CommandBuilder 34 | }) 35 | } 36 | 37 | pub fn hook_macro() -> Path { 38 | to_path(quote! { 39 | serenity_framework::prelude::hook 40 | }) 41 | } 42 | 43 | pub fn argument_segments_type() -> Path { 44 | to_path(quote! { 45 | serenity_framework::utils::ArgumentSegments 46 | }) 47 | } 48 | 49 | pub fn required_argument_from_str_func() -> Path { 50 | to_path(quote! { 51 | serenity_framework::argument::required_argument_from_str 52 | }) 53 | } 54 | 55 | pub fn required_argument_parse_func() -> Path { 56 | to_path(quote! { 57 | serenity_framework::argument::required_argument_parse 58 | }) 59 | } 60 | 61 | pub fn optional_argument_from_str_func() -> Path { 62 | to_path(quote! { 63 | serenity_framework::argument::optional_argument_from_str 64 | }) 65 | } 66 | 67 | pub fn optional_argument_parse_func() -> Path { 68 | to_path(quote! { 69 | serenity_framework::argument::optional_argument_parse 70 | }) 71 | } 72 | 73 | pub fn variadic_arguments_from_str_func() -> Path { 74 | to_path(quote! { 75 | serenity_framework::argument::variadic_arguments_from_str 76 | }) 77 | } 78 | 79 | pub fn variadic_arguments_parse_func() -> Path { 80 | to_path(quote! { 81 | serenity_framework::argument::variadic_arguments_parse 82 | }) 83 | } 84 | 85 | pub fn rest_argument_from_str_func() -> Path { 86 | to_path(quote! { 87 | serenity_framework::argument::rest_argument_from_str 88 | }) 89 | } 90 | 91 | pub fn rest_argument_parse_func() -> Path { 92 | to_path(quote! { 93 | serenity_framework::argument::rest_argument_parse 94 | }) 95 | } 96 | 97 | pub fn check_type(data: &Type, error: &Type) -> Path { 98 | to_path(quote! { 99 | serenity_framework::check::Check<#data, #error> 100 | }) 101 | } 102 | 103 | pub fn check_builder_type() -> Path { 104 | to_path(quote! { 105 | serenity_framework::check::CheckBuilder 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /command_attr/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use proc_macro2::{Ident, TokenStream}; 4 | use quote::{quote, ToTokens}; 5 | use syn::parse::{Parse, ParseStream}; 6 | use syn::spanned::Spanned; 7 | use syn::{Attribute, Error, FnArg, GenericArgument, Lit, LitStr, Meta}; 8 | use syn::{NestedMeta, Pat, PatType, Path, PathArguments, Result, Signature, Token, Type}; 9 | 10 | use crate::paths::{default_data_type, default_error_type}; 11 | 12 | pub struct AttributeArgs(pub Vec); 13 | 14 | impl Parse for AttributeArgs { 15 | fn parse(input: ParseStream) -> Result { 16 | let mut v = Vec::new(); 17 | 18 | loop { 19 | if input.is_empty() { 20 | break; 21 | } 22 | 23 | v.push(input.parse::()?.value()); 24 | 25 | if input.is_empty() { 26 | break; 27 | } 28 | 29 | input.parse::()?; 30 | } 31 | 32 | Ok(Self(v)) 33 | } 34 | } 35 | 36 | #[derive(Debug, Clone)] 37 | pub enum Value { 38 | Ident(Ident), 39 | Lit(Lit), 40 | } 41 | 42 | impl ToTokens for Value { 43 | fn to_tokens(&self, tokens: &mut TokenStream) { 44 | match self { 45 | Value::Ident(ident) => ident.to_tokens(tokens), 46 | Value::Lit(lit) => lit.to_tokens(tokens), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, Clone)] 52 | pub struct Attr { 53 | pub path: Path, 54 | pub values: Vec, 55 | } 56 | 57 | impl Attr { 58 | pub fn new(path: Path, values: Vec) -> Self { 59 | Self { 60 | path, 61 | values, 62 | } 63 | } 64 | } 65 | 66 | impl ToTokens for Attr { 67 | fn to_tokens(&self, tokens: &mut TokenStream) { 68 | let Attr { 69 | path, 70 | values, 71 | } = self; 72 | 73 | tokens.extend(if values.is_empty() { 74 | quote!(#[#path]) 75 | } else { 76 | quote!(#[#path(#(#values)*,)]) 77 | }); 78 | } 79 | } 80 | 81 | impl TryFrom<&Attribute> for Attr { 82 | type Error = Error; 83 | 84 | fn try_from(attr: &Attribute) -> Result { 85 | parse_attribute(attr) 86 | } 87 | } 88 | 89 | pub fn parse_attribute(attr: &Attribute) -> Result { 90 | let meta = attr.parse_meta()?; 91 | 92 | match meta { 93 | Meta::Path(p) => Ok(Attr::new(p, Vec::new())), 94 | Meta::List(l) => { 95 | let path = l.path; 96 | let values = l 97 | .nested 98 | .into_iter() 99 | .map(|m| match m { 100 | NestedMeta::Lit(lit) => Ok(Value::Lit(lit)), 101 | NestedMeta::Meta(m) => match m { 102 | Meta::Path(p) => Ok(Value::Ident(p.get_ident().unwrap().clone())), 103 | _ => Err(Error::new( 104 | m.span(), 105 | "nested lists or name values are not supported", 106 | )), 107 | }, 108 | }) 109 | .collect::>>()?; 110 | 111 | Ok(Attr::new(path, values)) 112 | }, 113 | Meta::NameValue(nv) => Ok(Attr::new(nv.path, vec![Value::Lit(nv.lit)])), 114 | } 115 | } 116 | 117 | pub fn parse_identifiers(attr: &Attr) -> Result> { 118 | attr.values 119 | .iter() 120 | .map(|v| match v { 121 | Value::Ident(ident) => Ok(ident.clone()), 122 | Value::Lit(lit) => Err(Error::new(lit.span(), "literals are forbidden")), 123 | }) 124 | .collect::>>() 125 | } 126 | 127 | pub fn parse_value(attr: &Attr, f: impl FnOnce(&Value) -> Result) -> Result { 128 | if attr.values.is_empty() { 129 | return Err(Error::new(attr.span(), "attribute input must not be empty")); 130 | } 131 | 132 | if attr.values.len() > 1 { 133 | return Err(Error::new( 134 | attr.span(), 135 | "attribute input must not exceed more than one argument", 136 | )); 137 | } 138 | 139 | f(&attr.values[0]) 140 | } 141 | 142 | pub fn parse_identifier(attr: &Attr) -> Result { 143 | parse_value(attr, |value| { 144 | Ok(match value { 145 | Value::Ident(ident) => ident.clone(), 146 | _ => return Err(Error::new(value.span(), "argument must be an identifier")), 147 | }) 148 | }) 149 | } 150 | 151 | pub fn parse_string(attr: &Attr) -> Result { 152 | parse_value(attr, |value| { 153 | Ok(match value { 154 | Value::Lit(Lit::Str(s)) => s.value(), 155 | _ => return Err(Error::new(value.span(), "argument must be a string")), 156 | }) 157 | }) 158 | } 159 | 160 | pub fn parse_bool(attr: &Attr) -> Result { 161 | parse_value(attr, |value| { 162 | Ok(match value { 163 | Value::Lit(Lit::Bool(b)) => b.value, 164 | _ => return Err(Error::new(value.span(), "argument must be a boolean")), 165 | }) 166 | }) 167 | } 168 | 169 | pub fn parse_generics(sig: &Signature) -> Result<(Ident, Ident, Box, Box)> { 170 | let (ctx, msg) = get_first_two_parameters(sig)?; 171 | 172 | let msg_indent = get_ident(&get_pat_type(msg)?.pat)?; 173 | 174 | let ctx_binding = get_pat_type(ctx)?; 175 | let ctx_ident = get_ident(&ctx_binding.pat)?; 176 | let path = get_path(&ctx_binding.ty)?; 177 | let mut arguments = get_generic_arguments(path)?; 178 | 179 | let default_data = default_data_type(); 180 | let default_error = default_error_type(); 181 | 182 | let data = match arguments.next() { 183 | Some(GenericArgument::Lifetime(_)) => match arguments.next() { 184 | Some(arg) => get_generic_type(arg)?, 185 | None => default_data, 186 | }, 187 | Some(arg) => get_generic_type(arg)?, 188 | None => default_data, 189 | }; 190 | 191 | let error = match arguments.next() { 192 | Some(arg) => get_generic_type(arg)?, 193 | None => default_error, 194 | }; 195 | 196 | Ok((ctx_ident, msg_indent, data, error)) 197 | } 198 | 199 | fn get_first_two_parameters(sig: &Signature) -> Result<(&FnArg, &FnArg)> { 200 | let mut parameters = sig.inputs.iter(); 201 | match (parameters.next(), parameters.next()) { 202 | (Some(first), Some(second)) => Ok((first, second)), 203 | _ => Err(Error::new( 204 | sig.inputs.span(), 205 | "the function must have a context and a message parameter", 206 | )), 207 | } 208 | } 209 | 210 | pub fn get_pat_type(arg: &FnArg) -> Result<&PatType> { 211 | match arg { 212 | FnArg::Typed(t) => Ok(t), 213 | _ => Err(Error::new(arg.span(), "`self` cannot be used as the context type")), 214 | } 215 | } 216 | 217 | pub fn get_ident(p: &Pat) -> Result { 218 | match p { 219 | Pat::Ident(pi) => Ok(pi.ident.clone()), 220 | _ => Err(Error::new(p.span(), "parameter must have an identifier")), 221 | } 222 | } 223 | 224 | pub fn get_path(t: &Type) -> Result<&Path> { 225 | match t { 226 | Type::Path(p) => Ok(&p.path), 227 | Type::Reference(r) => get_path(&r.elem), 228 | _ => Err(Error::new(t.span(), "parameter must be a path to a context type")), 229 | } 230 | } 231 | 232 | fn get_generic_arguments(path: &Path) -> Result + '_> { 233 | match &path.segments.last().unwrap().arguments { 234 | PathArguments::None => Ok(Vec::new().into_iter()), 235 | PathArguments::AngleBracketed(arguments) => { 236 | Ok(arguments.args.iter().collect::>().into_iter()) 237 | }, 238 | _ => Err(Error::new( 239 | path.span(), 240 | "context type cannot have generic parameters in parenthesis", 241 | )), 242 | } 243 | } 244 | 245 | fn get_generic_type(arg: &GenericArgument) -> Result> { 246 | match arg { 247 | GenericArgument::Type(t) => Ok(Box::new(t.clone())), 248 | _ => Err(Error::new(arg.span(), "generic parameter must be a type")), 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /framework/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serenity_framework" 3 | version = "0.1.0" 4 | authors = ["Alex M. M. "] 5 | edition = "2018" 6 | 7 | [dependencies.serenity] 8 | git = "https://github.com/serenity-rs/serenity" 9 | branch = "current" 10 | default_features = false 11 | features = ["client", "model", "gateway", "cache", "rustls_backend"] 12 | 13 | [dependencies.command_attr] 14 | path = "../command_attr" 15 | optional = true 16 | 17 | [features] 18 | default = ["macros"] 19 | macros = ["command_attr"] 20 | -------------------------------------------------------------------------------- /framework/src/argument.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for parsing command arguments. 2 | 3 | use std::error::Error as StdError; 4 | use std::fmt; 5 | 6 | use serenity::{async_trait, model::prelude::*, prelude::*, utils::Parse}; 7 | 8 | use crate::utils::ArgumentSegments; 9 | 10 | /// Error that might have occured when trying to parse an argument. 11 | #[derive(Debug)] 12 | pub enum ArgumentError { 13 | /// Required argument is missing. 14 | /// 15 | /// This is only returned by the [`required_argument_from_str`] and [`required_argument_parse`] 16 | /// functions. 17 | Missing, 18 | /// Parsing the argument failed. 19 | /// 20 | /// Contains the error from [`serenity::utils::Parse::Err`]. 21 | Argument(E), 22 | } 23 | 24 | impl fmt::Display for ArgumentError { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | match self { 27 | ArgumentError::Missing => f.write_str("missing required argument"), 28 | ArgumentError::Argument(err) => fmt::Display::fmt(err, f), 29 | } 30 | } 31 | } 32 | 33 | impl StdError for ArgumentError { 34 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 35 | match self { 36 | ArgumentError::Argument(err) => Some(err), 37 | _ => None, 38 | } 39 | } 40 | } 41 | 42 | /// Takes a single segment from a list of segments and parses an argument out of it using the 43 | /// [std::str::FromStr] trait. 44 | /// 45 | /// # Errors 46 | /// 47 | /// - If the list of segments is empty, [`ArgumentError::Missing`] is returned. 48 | /// - If the segment cannot be parsed into an argument, [`ArgumentError::Argument`] is 49 | /// returned. 50 | pub async fn required_argument_from_str( 51 | _ctx: &Context, 52 | _msg: &Message, 53 | segments: &mut ArgumentSegments<'_>, 54 | ) -> Result> 55 | where 56 | T: std::str::FromStr, 57 | { 58 | match segments.next() { 59 | Some(seg) => T::from_str(seg).map_err(ArgumentError::Argument), 60 | None => Err(ArgumentError::Missing), 61 | } 62 | } 63 | 64 | /// Takes a single segment from a list of segments and parses an argument out of it using the 65 | /// [serenity::utils::Parse] trait. 66 | /// 67 | /// # Errors 68 | /// 69 | /// - If the list of segments is empty, [`ArgumentError::Missing`] is returned. 70 | /// - If the segment cannot be parsed into an argument, [`ArgumentError::Argument`] is 71 | /// returned. 72 | pub async fn required_argument_parse( 73 | ctx: &Context, 74 | msg: &Message, 75 | segments: &mut ArgumentSegments<'_>, 76 | ) -> Result> 77 | where 78 | T: Parse, 79 | { 80 | match segments.next() { 81 | Some(seg) => T::parse(ctx, msg, seg).await.map_err(ArgumentError::Argument), 82 | None => Err(ArgumentError::Missing), 83 | } 84 | } 85 | 86 | /// Tries to take a single segment from a list of segments and parse 87 | /// an argument out of it using the [std::str::FromStr] trait. 88 | /// 89 | /// If the list of segments is empty, `Ok(None)` is returned. Otherwise, 90 | /// the first segment is taken and parsed into an argument. If parsing succeeds, 91 | /// `Ok(Some(...))` is returned, otherwise `Err(...)`. The error is wrapped in 92 | /// [`ArgumentError::Argument`]. 93 | pub async fn optional_argument_from_str( 94 | _ctx: &Context, 95 | _msg: &Message, 96 | segments: &mut ArgumentSegments<'_>, 97 | ) -> Result, ArgumentError> 98 | where 99 | T: std::str::FromStr, 100 | { 101 | match segments.next() { 102 | Some(seg) => T::from_str(seg).map(Some).map_err(ArgumentError::Argument), 103 | None => Ok(None), 104 | } 105 | } 106 | 107 | /// Tries to take a single segment from a list of segments and parse 108 | /// an argument out of it using the [serenity::utils::Parse] trait. 109 | /// 110 | /// If the list of segments is empty, `Ok(None)` is returned. Otherwise, 111 | /// the first segment is taken and parsed into an argument. If parsing succeeds, 112 | /// `Ok(Some(...))` is returned, otherwise `Err(...)`. The error is wrapped in 113 | /// [`ArgumentError::Argument`]. 114 | pub async fn optional_argument_parse( 115 | ctx: &Context, 116 | msg: &Message, 117 | segments: &mut ArgumentSegments<'_>, 118 | ) -> Result, ArgumentError> 119 | where 120 | T: Parse, 121 | { 122 | match segments.next() { 123 | Some(seg) => T::parse(ctx, msg, seg).await.map(Some).map_err(ArgumentError::Argument), 124 | None => Ok(None), 125 | } 126 | } 127 | 128 | /// Tries to parse many arguments from a list of segments using the [std::str::FromStr] trait. 129 | /// 130 | /// Each segment in the list is parsed into a vector of arguments. If parsing 131 | /// all segments succeeds, the vector is returned. Otherwise, the first error 132 | /// is returned. The error is wrapped in [`ArgumentError::Argument`]. 133 | pub async fn variadic_arguments_from_str( 134 | _ctx: &Context, 135 | _msg: &Message, 136 | segments: &mut ArgumentSegments<'_>, 137 | ) -> Result, ArgumentError> 138 | where 139 | T: std::str::FromStr, 140 | { 141 | segments.map(|seg| T::from_str(seg).map_err(ArgumentError::Argument)).collect() 142 | } 143 | 144 | /// Tries to parse many arguments from a list of segments using the [serenity::utils::Parse] trait. 145 | /// 146 | /// Each segment in the list is parsed into a vector of arguments. If parsing 147 | /// all segments succeeds, the vector is returned. Otherwise, the first error 148 | /// is returned. The error is wrapped in [`ArgumentError::Argument`]. 149 | pub async fn variadic_arguments_parse( 150 | ctx: &Context, 151 | msg: &Message, 152 | segments: &mut ArgumentSegments<'_>, 153 | ) -> Result, ArgumentError> 154 | where 155 | T: Parse, 156 | { 157 | serenity::futures::future::try_join_all(segments.map(|seg| T::parse(ctx, msg, seg))) 158 | .await 159 | .map_err(ArgumentError::Argument) 160 | } 161 | 162 | /// Parses the remainder of the list of segments into an argument using the [std::str::FromStr] 163 | /// trait. 164 | /// 165 | /// All segments (even if none) are concatenated to a single string 166 | /// and parsed to the specified argument type. If parsing success, 167 | /// `Ok(...)` is returned, otherwise `Err(...)`. The error is wrapped in 168 | /// [`ArgumentError::Argument`]. 169 | pub async fn rest_argument_from_str( 170 | _ctx: &Context, 171 | _msg: &Message, 172 | segments: &mut ArgumentSegments<'_>, 173 | ) -> Result> 174 | where 175 | T: std::str::FromStr, 176 | { 177 | T::from_str(segments.source()).map_err(ArgumentError::Argument) 178 | } 179 | 180 | /// Parses the remainder of the list of segments into an argument using the [serenity::utils::Parse] 181 | /// trait. 182 | /// 183 | /// All segments (even if none) are concatenated to a single string 184 | /// and parsed to the specified argument type. If parsing success, 185 | /// `Ok(...)` is returned, otherwise `Err(...)`. The error is wrapped in 186 | /// [`ArgumentError::Argument`]. 187 | pub async fn rest_argument_parse( 188 | ctx: &Context, 189 | msg: &Message, 190 | segments: &mut ArgumentSegments<'_>, 191 | ) -> Result> 192 | where 193 | T: Parse, 194 | { 195 | T::parse(ctx, msg, segments.source()).await.map_err(ArgumentError::Argument) 196 | } 197 | 198 | /// Denotes a type that can be either one of two different types. 199 | /// 200 | /// It derives the [`Parse`] trait and can be used to parse an argument as either of two types. 201 | /// It attempts to parse into the type that is indicated first. If parsing into the first type fails, 202 | /// an attempt to parse into the second type is made. If both attempts fail, the overall parsing 203 | /// fails and returns a [`ParseEitherError`]. 204 | /// 205 | /// This can also be used to handle larger combinations of types by chaining [`ParseEither`]s, 206 | /// for example, `ParseEither>`. 207 | #[derive(Debug)] 208 | pub enum ParseEither 209 | where 210 | T: Parse, 211 | U: Parse, 212 | { 213 | /// The first variant. 214 | VariantOne(T), 215 | /// The second variant. 216 | VariantTwo(U), 217 | } 218 | 219 | /// Error that is returned when [`ParseEither::parse`] fails. 220 | pub struct ParseEitherError 221 | where 222 | T: Parse, 223 | U: Parse, 224 | { 225 | /// The error returned from parsing the first variant. 226 | pub err_one: T::Err, 227 | /// The error returned from parsing the second variant. 228 | pub err_two: U::Err, 229 | } 230 | 231 | impl fmt::Debug for ParseEitherError 232 | where 233 | T: Parse, 234 | T::Err: fmt::Debug, 235 | U: Parse, 236 | U::Err: fmt::Debug, 237 | { 238 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 239 | f.debug_struct("ParseEitherError") 240 | .field("err_one", &self.err_one) 241 | .field("err_two", &self.err_two) 242 | .finish() 243 | } 244 | } 245 | 246 | impl std::error::Error for ParseEitherError 247 | where 248 | T: Parse, 249 | T::Err: fmt::Debug + fmt::Display, 250 | U: Parse, 251 | U::Err: fmt::Debug + fmt::Display, 252 | { 253 | } 254 | 255 | impl fmt::Display for ParseEitherError 256 | where 257 | T: Parse, 258 | T::Err: fmt::Display, 259 | U: Parse, 260 | U::Err: fmt::Display, 261 | { 262 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 263 | write!( 264 | f, 265 | "Parsing into type one failed: {}\nParsing into type two failed: {}", 266 | self.err_one, self.err_two 267 | ) 268 | } 269 | } 270 | 271 | #[async_trait] 272 | impl Parse for ParseEither 273 | where 274 | T: Parse, 275 | T::Err: Send, 276 | U: Parse, 277 | { 278 | type Err = ParseEitherError; 279 | 280 | async fn parse(ctx: &Context, msg: &Message, s: &str) -> Result { 281 | let err1 = match T::parse(ctx, msg, s).await { 282 | Ok(res) => return Ok(Self::VariantOne(res)), 283 | Err(err1) => err1, 284 | }; 285 | 286 | match U::parse(ctx, msg, s).await { 287 | Ok(res) => Ok(Self::VariantTwo(res)), 288 | Err(err2) => Err(Self::Err { 289 | err_one: err1, 290 | err_two: err2, 291 | }), 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /framework/src/category.rs: -------------------------------------------------------------------------------- 1 | //! A list of separate, but related commands. 2 | 3 | use crate::command::CommandId; 4 | 5 | /// Grouping of independent commands with a related theme. 6 | /// 7 | /// This grouping, or "categorization" is not special in any way and 8 | /// does not affect invocation of commands. The type serves to simplify 9 | /// [registration of commands][register] and displaying commands together in help messages. 10 | /// 11 | /// [register]: crate::configuration::Configuration::command 12 | #[derive(Debug, Default, Clone)] 13 | pub struct Category { 14 | /// Name of the category. 15 | pub name: String, 16 | /// [`Command`][cmd]s pertaining to this category. 17 | /// 18 | /// [cmd]: crate::command::Command 19 | pub commands: Vec, 20 | } 21 | -------------------------------------------------------------------------------- /framework/src/check.rs: -------------------------------------------------------------------------------- 1 | //! Functions and types relating to checks. 2 | //! 3 | //! A check is a function that can be plugged into a [command] to allow/deny 4 | //! a user's access. The check returns a [`Result`] that indicates whether 5 | //! it succeeded or failed. In the case of failure, additional information 6 | //! can be given, a reason, that describes the failure. 7 | //! 8 | //! [command]: crate::command 9 | 10 | use std::error::Error as StdError; 11 | use std::fmt::{self, Display}; 12 | 13 | use serenity::futures::future::BoxFuture; 14 | use serenity::model::channel::Message; 15 | 16 | use crate::context::CheckContext; 17 | 18 | /// The reason describing why a check failed. 19 | /// 20 | /// # Notes 21 | /// 22 | /// This information is not handled by the framework; it is only propagated 23 | /// to the consumer of the framework. 24 | #[derive(Debug, Clone)] 25 | #[non_exhaustive] 26 | pub enum Reason { 27 | /// There is no information. 28 | Unknown, 29 | /// Information for the user. 30 | User(String), 31 | /// Information for logging purposes. 32 | Log(String), 33 | /// Information both for the user and logging purposes. 34 | UserAndLog { 35 | /// Information for the user. 36 | user: String, 37 | /// Information for logging purposes. 38 | log: String, 39 | }, 40 | } 41 | 42 | impl Display for Reason { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | match self { 45 | Self::Unknown => f.write_str("Unknown"), 46 | Self::User(msg) => write!(f, "User: {}", msg), 47 | Self::Log(msg) => write!(f, "Log: {}", msg), 48 | Self::UserAndLog { 49 | user, 50 | log, 51 | } => write!(f, "User: {}; Log: {}", user, log), 52 | } 53 | } 54 | } 55 | 56 | impl StdError for Reason {} 57 | 58 | /// The result type of a [check function][fn] 59 | /// 60 | /// [fn]: CheckFn 61 | pub type CheckResult = std::result::Result; 62 | 63 | /// The definition of a check function. 64 | pub type CheckFn = 65 | for<'fut> fn(&'fut CheckContext<'_, D, E>, &'fut Message) -> BoxFuture<'fut, CheckResult<()>>; 66 | 67 | /// A constructor of the [`Check`] type provided by the consumer of the framework. 68 | pub type CheckConstructor = fn() -> Check; 69 | 70 | /// Data relating to a check. 71 | /// 72 | /// Refer to the [module-level documentation][docs] 73 | /// 74 | /// [docs]: crate::check 75 | #[non_exhaustive] 76 | pub struct Check { 77 | /// Name of the check. 78 | /// 79 | /// Used in help commands. 80 | pub name: String, 81 | /// The function of this check. 82 | pub function: CheckFn, 83 | /// A boolean indicating whether the check can apply in help commands. 84 | pub check_in_help: bool, 85 | /// A boolean indicating whether the check can be displayed in help commands. 86 | pub display_in_help: bool, 87 | } 88 | 89 | impl Clone for Check { 90 | fn clone(&self) -> Self { 91 | Self { 92 | name: self.name.clone(), 93 | function: self.function, 94 | check_in_help: self.check_in_help, 95 | display_in_help: self.display_in_help, 96 | } 97 | } 98 | } 99 | 100 | impl Default for Check { 101 | fn default() -> Self { 102 | Self { 103 | name: String::default(), 104 | function: |_, _| Box::pin(async move { Ok(()) }), 105 | check_in_help: true, 106 | display_in_help: true, 107 | } 108 | } 109 | } 110 | 111 | impl fmt::Debug for Check { 112 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 113 | f.debug_struct("Check") 114 | .field("name", &self.name) 115 | .field("function", &"") 116 | .field("check_in_help", &self.check_in_help) 117 | .field("display_in_help", &self.display_in_help) 118 | .finish() 119 | } 120 | } 121 | 122 | impl Check { 123 | /// Constructs a builder that will be used to create a check from scratch. 124 | /// 125 | /// Argument is the main name of the check. 126 | pub fn builder(name: I) -> CheckBuilder 127 | where 128 | I: Into, 129 | { 130 | CheckBuilder::new(name) 131 | } 132 | } 133 | 134 | /// A builder type for creating a [`Check`] from scratch. 135 | pub struct CheckBuilder { 136 | inner: Check, 137 | } 138 | 139 | impl CheckBuilder { 140 | /// Constructs a new instance of the builder. 141 | /// 142 | /// Argument is the main name of the check. 143 | pub fn new(name: I) -> Self 144 | where 145 | I: Into, 146 | { 147 | CheckBuilder { 148 | inner: Check { 149 | name: name.into(), 150 | ..Default::default() 151 | }, 152 | } 153 | } 154 | /// Assigns the function to this function. 155 | pub fn function(mut self, function: CheckFn) -> Self { 156 | self.inner.function = function; 157 | self 158 | } 159 | 160 | /// Assigns the indicator to this function. 161 | pub fn check_in_help(mut self, check_in_help: bool) -> Self { 162 | self.inner.check_in_help = check_in_help; 163 | self 164 | } 165 | 166 | /// Assigns the indicator to this function. 167 | pub fn display_in_help(mut self, display_in_help: bool) -> Self { 168 | self.inner.display_in_help = display_in_help; 169 | self 170 | } 171 | 172 | /// Complete building a check. 173 | pub fn build(self) -> Check { 174 | self.inner 175 | } 176 | } 177 | 178 | impl Clone for CheckBuilder { 179 | fn clone(&self) -> Self { 180 | Self { 181 | inner: self.inner.clone(), 182 | } 183 | } 184 | } 185 | 186 | impl Default for CheckBuilder { 187 | fn default() -> Self { 188 | Self { 189 | inner: Check::default(), 190 | } 191 | } 192 | } 193 | 194 | impl fmt::Debug for CheckBuilder { 195 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 196 | f.debug_struct("CheckBuilder").field("inner", &self.inner).finish() 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /framework/src/command.rs: -------------------------------------------------------------------------------- 1 | //! Functions and types relating to commands. 2 | //! 3 | //! A command is a function that performs work. It is invoked by a user on Discord. 4 | //! It may have many names by which it can be invoked, but will always have at least 5 | //! one name. It may possess subcommands to arrange functionality together. It may have 6 | //! information that relays to the user what it does, what it is for, and how it 7 | //! is used. It may have [`check`]s to allow/deny a user's access to the command. 8 | //! 9 | //! [`check`]: crate::check 10 | 11 | use std::collections::HashSet; 12 | use std::fmt; 13 | 14 | use serenity::futures::future::BoxFuture; 15 | use serenity::model::channel::Message; 16 | 17 | use crate::check::{Check, CheckConstructor}; 18 | use crate::context::Context; 19 | use crate::utils::IdMap; 20 | use crate::DefaultError; 21 | 22 | /// A function to dynamically create a string. 23 | /// 24 | /// Used for [`Command::dynamic_description`] and [`Command::dynamic_usage`]. 25 | pub type StringHook = 26 | for<'a> fn(ctx: &'a Context, msg: &'a Message) -> BoxFuture<'a, Option>; 27 | 28 | /// A function to dynamically create a list of strings. 29 | /// 30 | /// Used for [`Command::dynamic_examples`]. 31 | pub type StringsHook = 32 | for<'a> fn(ctx: &'a Context, msg: &'a Message) -> BoxFuture<'a, Vec>; 33 | 34 | /// [`IdMap`] for storing commands. 35 | /// 36 | /// [`IdMap`]: crate::utils::IdMap 37 | pub type CommandMap = IdMap>; 38 | 39 | /// The result type of a [command function][fn]. 40 | /// 41 | /// [fn]: CommandFn 42 | pub type CommandResult = std::result::Result; 43 | 44 | /// The definition of a command function. 45 | pub type CommandFn = 46 | for<'a> fn(Context, &'a Message) -> BoxFuture<'a, CommandResult<(), E>>; 47 | 48 | /// A constructor of the [`Command`] type provided by the consumer of the framework. 49 | pub type CommandConstructor = fn() -> Command; 50 | 51 | /// A unique identifier of a [`Command`] stored in the [`CommandMap`]. 52 | /// 53 | /// It is constructed from [`CommandConstructor`]. 54 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] 55 | pub struct CommandId(pub(crate) usize); 56 | 57 | impl CommandId { 58 | /// Converts the identifier to its internal representation. 59 | pub fn into_usize(self) -> usize { 60 | self.0 61 | } 62 | 63 | /// Converts the identifier to the constructor it points to. 64 | pub(crate) fn into_constructor(self) -> CommandConstructor { 65 | // SAFETY: CommandId in user code can only be constructed by its 66 | // `From>` impl. This makes the transmute safe. 67 | 68 | unsafe { std::mem::transmute(self.0 as *const ()) } 69 | } 70 | } 71 | 72 | impl From> for CommandId { 73 | fn from(f: CommandConstructor) -> Self { 74 | Self(f as usize) 75 | } 76 | } 77 | 78 | /// Data surrounding a command. 79 | /// 80 | /// Refer to the [module-level documentation][docs]. 81 | /// 82 | /// [docs]: index.html 83 | #[non_exhaustive] 84 | pub struct Command { 85 | /// The identifier of this command. 86 | pub id: CommandId, 87 | /// The function of this command. 88 | pub function: CommandFn, 89 | /// The names of this command by which it can be invoked. 90 | pub names: Vec, 91 | /// The subcommands belonging to this command. 92 | pub subcommands: HashSet, 93 | /// A string describing this command. 94 | pub description: Option, 95 | /// A function to dynamically describe this command. 96 | pub dynamic_description: Option>, 97 | /// A string to express usage of this command. 98 | pub usage: Option, 99 | /// A function to dynamically express usage of this command. 100 | pub dynamic_usage: Option>, 101 | /// A list of strings demonstrating usage of this command. 102 | pub examples: Vec, 103 | /// A function to dynamically demonstrate usage of this command. 104 | pub dynamic_examples: Option>, 105 | /// A boolean to indicate whether the command can be shown in help commands. 106 | pub help_available: bool, 107 | /// A function that allows/denies access to this command. 108 | pub check: Option>, 109 | } 110 | 111 | impl Clone for Command { 112 | fn clone(&self) -> Self { 113 | Self { 114 | id: self.id, 115 | function: self.function, 116 | names: self.names.clone(), 117 | subcommands: self.subcommands.clone(), 118 | description: self.description.clone(), 119 | dynamic_description: self.dynamic_description, 120 | usage: self.usage.clone(), 121 | dynamic_usage: self.dynamic_usage, 122 | examples: self.examples.clone(), 123 | dynamic_examples: self.dynamic_examples, 124 | help_available: self.help_available, 125 | check: self.check.clone(), 126 | } 127 | } 128 | } 129 | 130 | impl Default for Command { 131 | fn default() -> Self { 132 | Self { 133 | id: CommandId::from((|| Command::default()) as CommandConstructor), 134 | function: |_, _| Box::pin(async { Ok(()) }), 135 | names: Vec::default(), 136 | subcommands: HashSet::default(), 137 | description: None, 138 | dynamic_description: None, 139 | usage: None, 140 | dynamic_usage: None, 141 | examples: Vec::default(), 142 | dynamic_examples: None, 143 | help_available: true, 144 | check: None, 145 | } 146 | } 147 | } 148 | 149 | impl fmt::Debug for Command { 150 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 151 | f.debug_struct("Command") 152 | .field("id", &self.id) 153 | .field("function", &"") 154 | .field("names", &self.names) 155 | .field("subcommands", &self.subcommands) 156 | .field("description", &self.description) 157 | .field("dynamic_description", &"") 158 | .field("usage", &self.usage) 159 | .field("dynamic_usage", &"") 160 | .field("examples", &self.examples) 161 | .field("dynamic_examples", &"") 162 | .field("help_available", &self.help_available) 163 | .field("check", &self.check) 164 | .finish() 165 | } 166 | } 167 | 168 | impl Command { 169 | /// Constructs a builder that will be used to create a command from scratch. 170 | /// 171 | /// Argument is the main name of the command. 172 | pub fn builder(name: I) -> CommandBuilder 173 | where 174 | I: Into, 175 | { 176 | CommandBuilder::new(name) 177 | } 178 | } 179 | 180 | /// A builder type for creating a [`Command`] from scratch. 181 | pub struct CommandBuilder { 182 | inner: Command, 183 | } 184 | 185 | impl CommandBuilder { 186 | /// Constructs a new instance of the builder. 187 | /// 188 | /// Argument is the main name of the command. 189 | pub fn new(name: I) -> Self 190 | where 191 | I: Into, 192 | { 193 | Self::default().name(name) 194 | } 195 | 196 | /// Assigns a name to this command. 197 | /// 198 | /// The name is added to the [`names`] list. 199 | /// 200 | /// [`names`]: Command::names 201 | pub fn name(mut self, name: I) -> Self 202 | where 203 | I: Into, 204 | { 205 | self.inner.names.push(name.into()); 206 | self 207 | } 208 | 209 | /// Assigns the function to this command. 210 | pub fn function(mut self, f: CommandFn) -> Self { 211 | self.inner.function = f; 212 | self 213 | } 214 | 215 | /// Assigns a subcommand to this command. 216 | /// 217 | /// The subcommand is added to the [`subcommands`] list. 218 | /// 219 | /// [`subcommands`]: Command::subcommands 220 | pub fn subcommand(mut self, subcommand: CommandConstructor) -> Self { 221 | self.inner.subcommands.insert(CommandId::from(subcommand)); 222 | self 223 | } 224 | 225 | /// Assigns a static description to this command. 226 | pub fn description(mut self, description: I) -> Self 227 | where 228 | I: Into, 229 | { 230 | self.inner.description = Some(description.into()); 231 | 232 | self 233 | } 234 | 235 | /// Assigns a function to dynamically create a description to this command. 236 | pub fn dynamic_description(mut self, hook: StringHook) -> Self { 237 | self.inner.dynamic_description = Some(hook); 238 | self 239 | } 240 | 241 | /// Assigns a static usage to this command. 242 | pub fn usage(mut self, usage: I) -> Self 243 | where 244 | I: Into, 245 | { 246 | self.inner.usage = Some(usage.into()); 247 | self 248 | } 249 | 250 | /// Assigns a function to dynamically create a usage to this command. 251 | pub fn dynamic_usage(mut self, hook: StringHook) -> Self { 252 | self.inner.dynamic_usage = Some(hook); 253 | self 254 | } 255 | 256 | /// Assigns a static example of usage to this command. 257 | /// 258 | /// The example is added to the [`examples`] list. 259 | /// 260 | /// [`examples`]: Command::examples 261 | pub fn example(mut self, example: I) -> Self 262 | where 263 | I: Into, 264 | { 265 | self.inner.examples.push(example.into()); 266 | self 267 | } 268 | 269 | /// Assigns a function to dynamically create a list of examples to this command. 270 | pub fn dynamic_examples(mut self, hook: StringsHook) -> Self { 271 | self.inner.dynamic_examples = Some(hook); 272 | self 273 | } 274 | 275 | /// Assigns a [`check`] function to this command. 276 | /// 277 | /// [`check`]: crate::check 278 | pub fn check(mut self, check: CheckConstructor) -> Self { 279 | self.inner.check = Some(check()); 280 | self 281 | } 282 | 283 | /// Complete building a command. 284 | /// 285 | /// # Panics 286 | /// 287 | /// This function may panic if: 288 | /// 289 | /// - The command that is about to be built is missing names. 290 | pub fn build(self) -> Command { 291 | assert!(!self.inner.names.is_empty(), "a command must have at least one name"); 292 | 293 | self.inner 294 | } 295 | } 296 | 297 | impl Default for CommandBuilder { 298 | fn default() -> Self { 299 | Self { 300 | inner: Command::default(), 301 | } 302 | } 303 | } 304 | 305 | impl Clone for CommandBuilder { 306 | fn clone(&self) -> Self { 307 | Self { 308 | inner: self.inner.clone(), 309 | } 310 | } 311 | } 312 | 313 | impl fmt::Debug for CommandBuilder { 314 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 315 | f.debug_struct("CommandBuilder").field("inner", &self.inner).finish() 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /framework/src/configuration.rs: -------------------------------------------------------------------------------- 1 | //! Configuration of the framework. 2 | 3 | use std::collections::HashSet; 4 | use std::fmt; 5 | 6 | use serenity::futures::future::BoxFuture; 7 | use serenity::model::channel::Message; 8 | use serenity::model::id::UserId; 9 | 10 | use crate::category::Category; 11 | use crate::command::{CommandConstructor, CommandId, CommandMap}; 12 | use crate::context::PrefixContext; 13 | 14 | /// The definition of the dynamic prefix hook. 15 | pub type DynamicPrefix = 16 | for<'a> fn(ctx: PrefixContext<'_, D, E>, msg: &'a Message) -> BoxFuture<'a, Option>; 17 | 18 | /// The configuration of the framework. 19 | #[non_exhaustive] 20 | pub struct Configuration { 21 | /// A list of static prefixes. 22 | pub prefixes: Vec, 23 | /// A function to dynamically parse the prefix. 24 | pub dynamic_prefix: Option>, 25 | /// A boolean indicating whether casing of the letters in static prefixes, 26 | /// or command names does not matter. 27 | pub case_insensitive: bool, 28 | /// A boolean indicating whether the prefix is not necessary in direct messages. 29 | pub no_dm_prefix: bool, 30 | /// A user id of the bot that is used to compare mentions in prefix position. 31 | /// 32 | /// If filled, this allows for invoking commands by mentioning the bot. 33 | pub on_mention: Option, 34 | /// A list of [`Category`]s. 35 | /// 36 | /// [`Category`]: crate::category::Category 37 | pub categories: Vec, 38 | /// A set of commands that can only appear at the beginning of a command invocation. 39 | pub root_level_commands: HashSet, 40 | /// An [`IdMap`] containing all [`Command`]s. 41 | /// 42 | /// [`IdMap`]: crate::utils::IdMap 43 | /// [`Command`]: crate::command::Command 44 | pub commands: CommandMap, 45 | } 46 | 47 | impl Clone for Configuration { 48 | fn clone(&self) -> Self { 49 | Self { 50 | prefixes: self.prefixes.clone(), 51 | dynamic_prefix: self.dynamic_prefix, 52 | case_insensitive: self.case_insensitive, 53 | no_dm_prefix: self.no_dm_prefix, 54 | on_mention: self.on_mention.clone(), 55 | categories: self.categories.clone(), 56 | root_level_commands: self.root_level_commands.clone(), 57 | commands: self.commands.clone(), 58 | } 59 | } 60 | } 61 | 62 | impl Default for Configuration { 63 | fn default() -> Self { 64 | Self { 65 | prefixes: Vec::default(), 66 | dynamic_prefix: None, 67 | case_insensitive: false, 68 | no_dm_prefix: false, 69 | on_mention: None, 70 | categories: Vec::default(), 71 | root_level_commands: HashSet::default(), 72 | commands: CommandMap::default(), 73 | } 74 | } 75 | } 76 | 77 | impl Configuration { 78 | /// Creates a new instance of the framework configuration. 79 | pub fn new() -> Self { 80 | Self::default() 81 | } 82 | 83 | /// Assigns a prefix to this configuration. 84 | /// 85 | /// The prefix is added to the [`prefixes`] list. 86 | /// 87 | /// [`prefixes`]: Self::prefix 88 | pub fn prefix(&mut self, prefix: I) -> &mut Self 89 | where 90 | I: Into, 91 | { 92 | self.prefixes.push(prefix.into()); 93 | self 94 | } 95 | 96 | /// Assigns a function to dynamically parse the prefix. 97 | pub fn dynamic_prefix(&mut self, prefix: DynamicPrefix) -> &mut Self { 98 | self.dynamic_prefix = Some(prefix); 99 | self 100 | } 101 | 102 | /// Assigns a boolean indicating whether the casing of letters in static prefixes, 103 | /// or command names does not matter. 104 | pub fn case_insensitive(&mut self, b: bool) -> &mut Self { 105 | self.case_insensitive = b; 106 | 107 | self 108 | } 109 | 110 | /// Assigns a boolean indicating whether the prefix is not necessary in 111 | /// direct messages. 112 | pub fn no_dm_prefix(&mut self, b: bool) -> &mut Self { 113 | self.no_dm_prefix = b; 114 | self 115 | } 116 | 117 | /// Assigns a user id of the bot that will allow for mentions in prefix position. 118 | pub fn on_mention(&mut self, id: I) -> &mut Self 119 | where 120 | I: Into, 121 | { 122 | self.on_mention = Some(id.into().to_string()); 123 | self 124 | } 125 | 126 | /// Assigns a category to this configuration. 127 | /// 128 | /// The category is added to the [`categories`] list. Additionally, 129 | /// all of its commands [are added][cmd] to the [`commands`] map 130 | /// 131 | /// [`categories`]: Self::categories 132 | /// [`commands`]: Self::commands 133 | /// [cmd]: Self::command 134 | pub fn category(&mut self, name: I, cmds: &[CommandConstructor]) -> &mut Self 135 | where 136 | I: Into, 137 | { 138 | let mut commands = Vec::with_capacity(cmds.len()); 139 | 140 | for cmd in cmds { 141 | self.command(*cmd); 142 | commands.push(CommandId::from(*cmd)); 143 | } 144 | 145 | self.categories.push(Category { 146 | name: name.into(), 147 | commands, 148 | }); 149 | 150 | self 151 | } 152 | 153 | /// Assigns a command to this configuration. 154 | /// 155 | /// The command is added to the [`commands`] map, alongside its subcommands. 156 | /// It it also added into the [`root_level_commands`] set. 157 | /// 158 | /// [`commands`]: Self::commands 159 | /// [`root_level_commands`]: Self::root_level_commands 160 | pub fn command(&mut self, command: CommandConstructor) -> &mut Self { 161 | let id = CommandId::from(command); 162 | 163 | // Skip instantiating this root command if if already exists. 164 | if self.root_level_commands.contains(&id) { 165 | return self; 166 | } 167 | 168 | self.root_level_commands.insert(id); 169 | self._command(id, command); 170 | self 171 | } 172 | 173 | fn _command(&mut self, id: CommandId, command: CommandConstructor) { 174 | let mut command = command(); 175 | command.id = id; 176 | 177 | for name in &command.names { 178 | let name = if self.case_insensitive { name.to_lowercase() } else { name.clone() }; 179 | 180 | self.commands.insert_name(name, command.id); 181 | } 182 | 183 | for id in &command.subcommands { 184 | // Skip instantiating this subcommand if it already exists. 185 | if self.commands.contains_id(*id) { 186 | continue; 187 | } 188 | 189 | let ctor: CommandConstructor = id.into_constructor(); 190 | self._command(*id, ctor); 191 | } 192 | 193 | self.commands.insert(command.id, command); 194 | } 195 | } 196 | 197 | impl fmt::Debug for Configuration { 198 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 199 | f.debug_struct("Configuration") 200 | .field("prefixes", &self.prefixes) 201 | .field("dynamic_prefix", &"") 202 | .field("case_insensitive", &self.case_insensitive) 203 | .field("no_dm_prefix", &self.no_dm_prefix) 204 | .field("on_mention", &self.on_mention) 205 | .field("categories", &self.categories) 206 | .field("root_level_commands", &self.root_level_commands) 207 | .field("commands", &self.commands) 208 | .finish() 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /framework/src/context.rs: -------------------------------------------------------------------------------- 1 | //! Data provided in different *contexts*. 2 | //! 3 | //! A context type contains data that is only available in certain phases of command 4 | //! dispatch. Other than [the Discord message][msg], data is placed into the context 5 | //! types in order to arrange them together and to allow extending the types in future 6 | //! releases without breaking function definitions. 7 | //! 8 | //! [msg]: serenity::model::channel::Message 9 | 10 | use std::sync::Arc; 11 | 12 | use serenity::cache::Cache; 13 | use serenity::client::Context as SerenityContext; 14 | use serenity::http::{CacheHttp, Http}; 15 | use serenity::prelude::RwLock; 16 | 17 | use crate::command::CommandId; 18 | use crate::configuration::Configuration; 19 | use crate::{DefaultData, DefaultError}; 20 | 21 | /// The final context type. 22 | /// 23 | /// [Ownership of this context is given to the consumer of the framework][ctx], 24 | /// as it's created in the last phase of the dispatch process: Invoking 25 | /// the command function. Consequently, this context type contains 26 | /// every data that's relevant to the command. 27 | /// 28 | /// [ctx]: crate::command::CommandFn 29 | #[non_exhaustive] 30 | pub struct Context { 31 | /// User data. 32 | pub data: Arc, 33 | /// Framework configuration. 34 | pub conf: Arc>>, 35 | /// Serenity's context type. 36 | pub serenity_ctx: SerenityContext, 37 | /// The identifier of the command. 38 | pub command_id: CommandId, 39 | /// The [prefix] that was used to invoke this command. 40 | /// 41 | /// [prefix]: crate::parse::content 42 | pub prefix: String, 43 | /// The arguments of the command. 44 | /// 45 | /// This is the content of the message after the command. 46 | pub args: String, 47 | } 48 | 49 | impl Clone for Context { 50 | fn clone(&self) -> Self { 51 | Self { 52 | data: Arc::clone(&self.data), 53 | conf: Arc::clone(&self.conf), 54 | serenity_ctx: self.serenity_ctx.clone(), 55 | command_id: self.command_id, 56 | prefix: self.prefix.clone(), 57 | args: self.args.clone(), 58 | } 59 | } 60 | } 61 | 62 | impl AsRef for Context { 63 | fn as_ref(&self) -> &Http { 64 | &self.serenity_ctx.http 65 | } 66 | } 67 | 68 | impl AsRef for Context { 69 | fn as_ref(&self) -> &Cache { 70 | &self.serenity_ctx.cache 71 | } 72 | } 73 | 74 | impl CacheHttp for Context 75 | where 76 | D: Send + Sync, 77 | E: Send + Sync, 78 | { 79 | fn http(&self) -> &Http { 80 | &self.serenity_ctx.http 81 | } 82 | 83 | fn cache(&self) -> Option<&Arc> { 84 | Some(&self.serenity_ctx.cache) 85 | } 86 | } 87 | 88 | /// The prefix context. 89 | /// 90 | /// This is passed in the [dynamic prefix][dyn_prefix] hook. 91 | /// 92 | /// [dyn_prefix]: crate::configuration::DynamicPrefix 93 | #[non_exhaustive] 94 | pub struct PrefixContext<'a, D = DefaultData, E = DefaultError> { 95 | /// User data. 96 | pub data: &'a Arc, 97 | /// Framework configuration. 98 | pub conf: &'a Configuration, 99 | /// Serenity's context type. 100 | pub serenity_ctx: &'a SerenityContext, 101 | } 102 | 103 | impl<'a, D, E> Clone for PrefixContext<'a, D, E> { 104 | fn clone(&self) -> Self { 105 | Self { 106 | data: self.data, 107 | conf: self.conf, 108 | serenity_ctx: self.serenity_ctx, 109 | } 110 | } 111 | } 112 | 113 | impl AsRef for PrefixContext<'_, D, E> { 114 | fn as_ref(&self) -> &Http { 115 | &self.serenity_ctx.http 116 | } 117 | } 118 | 119 | impl AsRef for PrefixContext<'_, D, E> { 120 | fn as_ref(&self) -> &Cache { 121 | &self.serenity_ctx.cache 122 | } 123 | } 124 | 125 | impl CacheHttp for PrefixContext<'_, D, E> 126 | where 127 | D: Send + Sync, 128 | E: Send + Sync, 129 | { 130 | fn http(&self) -> &Http { 131 | &self.serenity_ctx.http 132 | } 133 | 134 | fn cache(&self) -> Option<&Arc> { 135 | Some(&self.serenity_ctx.cache) 136 | } 137 | } 138 | 139 | /// The check context. 140 | /// 141 | /// This is passed to the [check function][fn]. 142 | /// 143 | /// [fn]: crate::check::CheckFn 144 | #[non_exhaustive] 145 | pub struct CheckContext<'a, D = DefaultData, E = DefaultError> { 146 | /// User data. 147 | pub data: &'a Arc, 148 | /// Framework configuration. 149 | pub conf: &'a Configuration, 150 | /// Serenity's context type. 151 | pub serenity_ctx: &'a SerenityContext, 152 | /// The identifier of the command that is being checked upon. 153 | pub command_id: CommandId, 154 | } 155 | 156 | impl<'a, D, E> Clone for CheckContext<'a, D, E> { 157 | fn clone(&self) -> Self { 158 | Self { 159 | data: self.data, 160 | conf: self.conf, 161 | serenity_ctx: self.serenity_ctx, 162 | command_id: self.command_id, 163 | } 164 | } 165 | } 166 | 167 | impl AsRef for CheckContext<'_, D, E> { 168 | fn as_ref(&self) -> &Http { 169 | &self.serenity_ctx.http 170 | } 171 | } 172 | 173 | impl AsRef for CheckContext<'_, D, E> { 174 | fn as_ref(&self) -> &Cache { 175 | &self.serenity_ctx.cache 176 | } 177 | } 178 | 179 | impl CacheHttp for CheckContext<'_, D, E> 180 | where 181 | D: Send + Sync, 182 | E: Send + Sync, 183 | { 184 | fn http(&self) -> &Http { 185 | &self.serenity_ctx.http 186 | } 187 | 188 | fn cache(&self) -> Option<&Arc> { 189 | Some(&self.serenity_ctx.cache) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /framework/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Defines error types used by the framework. 2 | 3 | use std::error::Error as StdError; 4 | use std::fmt; 5 | 6 | use crate::check::Reason; 7 | 8 | /// An error describing why [`dispatch`]ing failed. 9 | /// 10 | /// [`dispatch`]: crate::Framework::dispatch 11 | #[derive(Debug, Clone)] 12 | pub enum DispatchError { 13 | /// The message does not contain a command invocation. 14 | NormalMessage, 15 | /// The message only contains a prefix. Contains the prefix. 16 | PrefixOnly(String), 17 | /// The message contains a name not belonging to any command. 18 | InvalidCommandName(String), 19 | /// A check failed. Contains its name and the reasoning why it failed. 20 | CheckFailed(String, Reason), 21 | } 22 | 23 | impl fmt::Display for DispatchError { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | match self { 26 | DispatchError::NormalMessage => { 27 | write!(f, "message is normal") 28 | }, 29 | DispatchError::PrefixOnly(prefix) => { 30 | write!(f, "only the prefix (`{}`) is present", prefix) 31 | }, 32 | DispatchError::InvalidCommandName(name) => { 33 | write!(f, "name \"{}\" does not refer to any command", name) 34 | }, 35 | DispatchError::CheckFailed(name, _) => write!(f, "\"{}\" check failed", name), 36 | } 37 | } 38 | } 39 | 40 | impl StdError for DispatchError {} 41 | 42 | /// Returned when the call of [`dispatch`] fails. 43 | /// 44 | /// [`dispatch`]: crate::Framework::dispatch 45 | #[derive(Debug, Clone)] 46 | pub enum Error { 47 | /// Failed to dispatch a command. 48 | Dispatch(DispatchError), 49 | /// A command returned an error. 50 | User(E), 51 | } 52 | 53 | impl From for Error { 54 | fn from(e: DispatchError) -> Self { 55 | Self::Dispatch(e) 56 | } 57 | } 58 | 59 | impl fmt::Display for Error { 60 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 61 | match self { 62 | Error::Dispatch(err) => fmt::Display::fmt(err, f), 63 | Error::User(err) => fmt::Display::fmt(err, f), 64 | } 65 | } 66 | } 67 | 68 | impl StdError for Error { 69 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 70 | match self { 71 | Error::Dispatch(err) => Some(err), 72 | Error::User(err) => Some(err), 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /framework/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The official command framework for [Serenity] bots. 2 | //! 3 | //! The framework provides an interface between functionality of the bot and 4 | //! a user on Discord through the concept of *commands*. They are functions 5 | //! that the user invokes in a guild channel or private channel. 6 | //! 7 | //! Command invocations start with a *prefix* at the beginning of the message. 8 | //! The prefix distinguishes normal messages and command invocations. If the prefix 9 | //! is unique, it also avoids collision with command invocations of other bots. 10 | //! The bot may have many prefixes, statically or dynamically defined. 11 | //! 12 | //! Assuming the prefix is `!` and a command with the name `ping` exists, a typical 13 | //! invocation might look like: 14 | //! 15 | //! ```text 16 | //! !ping 17 | //! ``` 18 | //! 19 | //! Commands can accept arguments. These are the content of the message after 20 | //! the command name. As an example: 21 | //! 22 | //! ```text 23 | //! !sort 4 2 8 -3 24 | //! ``` 25 | //! 26 | //! The arguments of the `sort` command is a `"4 2 8 -3"` string. Arguments are 27 | //! not processed by the framework, as it is the responsibility of each command 28 | //! to decide the correct format of its arguments, and how they should be parsed. 29 | //! 30 | //! Commands may be *categorized*. A category is a list of individual commands 31 | //! with a common theme, such as moderation. They do not participate in command 32 | //! invocation. They are used to register commands in bulk and display related 33 | //! commands in the help command. 34 | //! 35 | //! [Serenity]: https://github.com/serenity-rs/serenity 36 | 37 | #![warn(missing_docs)] 38 | 39 | use std::error::Error as StdError; 40 | use std::sync::Arc; 41 | 42 | use serenity::model::channel::Message; 43 | use serenity::prelude::{Context as SerenityContext, RwLock}; 44 | 45 | pub mod argument; 46 | pub mod category; 47 | pub mod check; 48 | pub mod command; 49 | pub mod configuration; 50 | pub mod context; 51 | pub mod error; 52 | pub mod parse; 53 | pub mod prelude; 54 | pub mod utils; 55 | 56 | use command::CommandFn; 57 | use configuration::Configuration; 58 | use context::Context; 59 | use error::{DispatchError, Error}; 60 | 61 | /// The default type for [user data][data] when it is unspecified. 62 | /// 63 | /// [data]: Framework::data 64 | pub type DefaultData = (); 65 | 66 | /// The default type for [command errors][errors] when it is unspecified. 67 | /// 68 | /// [errors]: crate::command::CommandResult 69 | pub type DefaultError = Box; 70 | 71 | /// The core of the framework. 72 | #[derive(Clone)] 73 | pub struct Framework { 74 | /// Configuration of the framework that dictates its behaviour. 75 | pub conf: Arc>>, 76 | /// User data that is accessable in every command and function hook. 77 | pub data: Arc, 78 | } 79 | 80 | impl Framework 81 | where 82 | D: Default, 83 | { 84 | /// Creates a new instanstiation of the framework using a given configuration. 85 | /// 86 | /// The [`data`] field is [`Default`] initialized. 87 | /// 88 | /// [`data`]: Self::data 89 | /// [`Default`]: std::default::Default 90 | #[inline] 91 | pub fn new(conf: Configuration) -> Self { 92 | Self::with_data(conf, D::default()) 93 | } 94 | } 95 | 96 | impl Framework { 97 | /// Creates new instanstiation of the framework using a given configuration and data. 98 | /// 99 | /// # Notes 100 | /// 101 | /// This consumes the data. 102 | /// 103 | /// If you need to retain ownership of the data, consider using [`with_arc_data`]. 104 | /// 105 | /// [`with_arc_data`]: Self::with_arc_data 106 | #[inline] 107 | pub fn with_data(conf: Configuration, data: D) -> Self { 108 | Self::with_arc_data(conf, Arc::new(data)) 109 | } 110 | 111 | /// Creates new instanstiation of the framework using a given configuration and data. 112 | #[inline] 113 | pub fn with_arc_data(conf: Configuration, data: Arc) -> Self { 114 | Self { 115 | conf: Arc::new(RwLock::new(conf)), 116 | data, 117 | } 118 | } 119 | 120 | /// Dispatches a command from a message if one is present. 121 | #[inline] 122 | pub async fn dispatch(&self, ctx: &SerenityContext, msg: &Message) -> Result<(), Error> { 123 | let (ctx, func) = self.parse(ctx, msg).await?; 124 | 125 | func(ctx, msg).await.map_err(Error::User) 126 | } 127 | 128 | /// Parses a command out of a message, if one is present. 129 | pub async fn parse( 130 | &self, 131 | ctx: &SerenityContext, 132 | msg: &Message, 133 | ) -> Result<(Context, CommandFn), DispatchError> { 134 | let (func, command_id, prefix, args) = { 135 | let conf = self.conf.read().await; 136 | 137 | let (prefix, content) = match parse::content(&self.data, &conf, &ctx, &msg).await { 138 | Some(pair) => pair, 139 | None => return Err(DispatchError::NormalMessage), 140 | }; 141 | 142 | let (command, args) = 143 | match parse::command(&self.data, &conf, &ctx, &msg, content).await? { 144 | Some(pair) => pair, 145 | None => return Err(DispatchError::PrefixOnly(prefix.to_string())), 146 | }; 147 | 148 | (command.function, command.id, prefix.to_string(), args) 149 | }; 150 | 151 | let ctx = Context { 152 | data: Arc::clone(&self.data), 153 | conf: Arc::clone(&self.conf), 154 | serenity_ctx: ctx.clone(), 155 | command_id, 156 | prefix, 157 | args, 158 | }; 159 | 160 | Ok((ctx, func)) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /framework/src/parse.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to parse the prefix and command out of a message. 2 | //! 3 | //! Refer to the [`content`] function for the definition of a prefix. 4 | 5 | use std::sync::Arc; 6 | 7 | use serenity::client::Context as SerenityContext; 8 | use serenity::model::channel::Message; 9 | 10 | use crate::command::Command; 11 | use crate::configuration::Configuration; 12 | use crate::context::{CheckContext, PrefixContext}; 13 | use crate::error::DispatchError; 14 | use crate::utils::Segments; 15 | 16 | /// Parses a mention from the message. 17 | /// 18 | /// A mention is defined as text starting with `<@`, which may be followed by `!`, 19 | /// proceeded by a user id, and ended by a `>`. 20 | /// 21 | /// This can be expressed in a regular expression as `<@!?\d+>`. 22 | /// 23 | /// As an example, these are valid mentions: 24 | /// - \<@110372470472613888> 25 | /// - <@!110372470472613888> 26 | /// 27 | /// Returns the mention and the rest of the message after the mention, with trimmed 28 | /// whitespace. 29 | pub fn mention<'a>(msg: &'a str, id: &str) -> Option<(&'a str, &'a str)> { 30 | if !msg.starts_with("<@") { 31 | return None; 32 | } 33 | 34 | let msg = msg[2..].trim_start_matches('!'); 35 | 36 | let index = msg.find('>').unwrap_or(0); 37 | let mention = &msg[..index]; 38 | 39 | if mention == id { 40 | // + 1 to remove the angle bracket 41 | let (mention, mut rest) = msg.split_at(index + 1); 42 | rest = rest.trim_start(); 43 | Some((mention, rest)) 44 | } else { 45 | None 46 | } 47 | } 48 | 49 | /// Parses a prefix from the message dynamically using the [`Configuration::dynamic_prefix`] 50 | /// hook. 51 | /// 52 | /// If the hook is not registered, or the hook returned `None`, `None` is returned. 53 | /// Otherwise, the prefix and the rest of the message after the prefix is returned. 54 | /// 55 | /// [`Configuration::dynamic_prefix`]: crate::configuration::Configuration::dynamic_prefix 56 | #[allow(clippy::needless_lifetimes)] 57 | pub async fn dynamic_prefix<'a, D, E>( 58 | ctx: PrefixContext<'_, D, E>, 59 | msg: &'a Message, 60 | ) -> Option<(&'a str, &'a str)> { 61 | if let Some(dynamic_prefix) = ctx.conf.dynamic_prefix { 62 | let index = dynamic_prefix(ctx, msg).await?; 63 | Some(msg.content.split_at(index)) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | /// Parses a prefix from the message statically from a list of prefixes. 70 | /// 71 | /// If none of the prefixes stored in the list are found in the message, `None` is returned. 72 | /// Otherwise, the prefix and the rest of the message after the prefix is returned. 73 | pub fn static_prefix<'a>(msg: &'a str, prefixes: &[String]) -> Option<(&'a str, &'a str)> { 74 | prefixes.iter().find(|p| msg.starts_with(p.as_str())).map(|p| msg.split_at(p.len())) 75 | } 76 | 77 | /// Returns the content of the message after parsing a prefix. 78 | /// 79 | /// The content is defined as the substring of the message after the prefix. 80 | /// If the [`Configuration::no_dm_prefix`] option is enabled, the content is 81 | /// the whole message. 82 | /// 83 | /// The prefix is defined as: 84 | /// 1. a [mention] 85 | /// 2. a [statically defined prefix from a list][prefixes] 86 | /// 3. or a [dynamically chosen prefix][dyn_prefix] 87 | /// 88 | /// It is parsed in that order. 89 | /// 90 | /// If [`Configuration::no_dm_prefix`] is `false` and no prefix is found, 91 | /// `None` is returned. Otherwise, the prefix and the content are returned. 92 | /// 93 | /// [`Configuration::no_dm_prefix`]: crate::configuration::Configuration::no_dm_prefix 94 | /// [prefixes]: static_prefix 95 | /// [dyn_prefix]: dynamic_prefix 96 | #[allow(clippy::needless_lifetimes)] 97 | pub async fn content<'a, D, E>( 98 | data: &Arc, 99 | conf: &Configuration, 100 | serenity_ctx: &SerenityContext, 101 | msg: &'a Message, 102 | ) -> Option<(&'a str, &'a str)> { 103 | if msg.is_private() && conf.no_dm_prefix { 104 | return Some(("", &msg.content)); 105 | } 106 | 107 | if let Some(on_mention) = &conf.on_mention { 108 | if let Some(pair) = mention(&msg.content, &on_mention) { 109 | return Some(pair); 110 | } 111 | } 112 | 113 | if let Some(pair) = static_prefix(&msg.content, &conf.prefixes) { 114 | return Some(pair); 115 | } 116 | 117 | let ctx = PrefixContext { 118 | data, 119 | conf, 120 | serenity_ctx, 121 | }; 122 | 123 | dynamic_prefix(ctx, msg).await 124 | } 125 | 126 | /// Command parsing iterator. 127 | /// 128 | /// This is returned by [`commands`]. 129 | /// 130 | /// Refer to its documentation for more information. 131 | /// 132 | /// [`commands`]: self::commands 133 | pub struct CommandIterator<'a, 'b, 'c, D, E> { 134 | conf: &'a Configuration, 135 | segments: &'b mut Segments<'c>, 136 | command: Option<&'a Command>, 137 | } 138 | 139 | impl<'a, 'b, 'c, D, E> Iterator for CommandIterator<'a, 'b, 'c, D, E> { 140 | type Item = Result<&'a Command, DispatchError>; 141 | 142 | fn next(&mut self) -> Option { 143 | let checkpoint = self.segments.source(); 144 | let name = self.segments.next()?; 145 | 146 | let cmd = match self.conf.commands.get_by_name(&*name) { 147 | Some(cmd) => cmd, 148 | None => { 149 | self.segments.set_source(checkpoint); 150 | 151 | // At least one valid command must be present in the message. 152 | // After the first command, we do not care if the "name" is invalid, 153 | // as it may be the argument to the command at that point. 154 | if self.command.is_none() { 155 | return Some(Err(DispatchError::InvalidCommandName(name.into_owned()))); 156 | } 157 | 158 | return None; 159 | }, 160 | }; 161 | 162 | if self.command.is_none() && !self.conf.root_level_commands.contains(&cmd.id) { 163 | self.segments.set_source(checkpoint); 164 | return None; 165 | } 166 | 167 | if let Some(command) = self.command { 168 | if !command.subcommands.contains(&cmd.id) { 169 | // We received a command, but it's not a subcommand of the previously 170 | // parsed command. Interpret it as an argument instead. 171 | // 172 | // This enables user-defined `help` commands. 173 | self.segments.set_source(checkpoint); 174 | return None; 175 | } 176 | } 177 | 178 | self.command = Some(cmd); 179 | 180 | Some(Ok(cmd)) 181 | } 182 | } 183 | 184 | /// Creates a command parsing iterator. 185 | /// 186 | /// The [returned iterator][iter] will iterate through the segments of the message, 187 | /// returning each valid command that it can find. 188 | /// 189 | /// ## Return type of the iterator 190 | /// 191 | /// The iterator will return items of the type `Result<&`[`Command`]`,`[`DispatchError`]`>`. 192 | /// 193 | /// The `Result` signifies whether a given name for the first command exists. 194 | /// If it is not the case, the [`InvalidCommandName`] error is returned. 195 | /// 196 | /// The `Option` returned from calling [`Iterator::next`] will signify whether the content had a 197 | /// command, did not have a command, or was empty. 198 | /// 199 | /// [iter]: self::CommandIterator 200 | /// [`Command`]: crate::command::Command 201 | /// [`DispatchError`]: crate::error::DispatchError 202 | /// [`InvalidCommandName`]: crate::error::DispatchError::InvalidCommandName 203 | pub fn commands<'a, 'b, 'c, D, E>( 204 | conf: &'a Configuration, 205 | segments: &'b mut Segments<'c>, 206 | ) -> CommandIterator<'a, 'b, 'c, D, E> { 207 | CommandIterator { 208 | conf, 209 | segments, 210 | command: None, 211 | } 212 | } 213 | 214 | /// Parses and checks all valid commands in a message after the prefix. 215 | /// 216 | /// This parses commands from `content` using [`commands`]. For each valid command, 217 | /// it calls its [`check`] function if it has one configured. Commands are 218 | /// parsed from space-delimited [`Segments`]. 219 | /// 220 | /// ## Return type 221 | /// 222 | /// This returns the last valid command and its arguments if parsing 223 | /// and checking commands went successfully. 224 | /// 225 | /// It may be `None` if no command was found in `content` (it is empty); or 226 | /// it may be `Err(...)` if either the first segment is an invalid command 227 | /// name or the check function returned an error. 228 | /// 229 | /// [`check`]: crate::command::Command::check 230 | /// [`Segments`]: crate::utils::Segments 231 | #[allow(clippy::needless_lifetimes)] 232 | pub async fn command<'a, D, E>( 233 | data: &Arc, 234 | conf: &'a Configuration, 235 | ctx: &SerenityContext, 236 | msg: &Message, 237 | content: &str, 238 | ) -> Result, String)>, DispatchError> { 239 | let mut segments = Segments::new(content, " ", conf.case_insensitive); 240 | 241 | let mut command = None; 242 | 243 | for cmd in commands(conf, &mut segments) { 244 | let cmd = cmd?; 245 | 246 | if let Some(check) = &cmd.check { 247 | let ctx = CheckContext { 248 | data, 249 | conf, 250 | serenity_ctx: &ctx, 251 | command_id: cmd.id, 252 | }; 253 | 254 | if let Err(reason) = (check.function)(&ctx, msg).await { 255 | return Err(DispatchError::CheckFailed(check.name.clone(), reason)); 256 | } 257 | } 258 | 259 | command = Some(cmd); 260 | } 261 | 262 | let args = segments.source(); 263 | 264 | Ok(command.map(|c| (c, args.to_string()))) 265 | } 266 | -------------------------------------------------------------------------------- /framework/src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! A series of re-exports to simplify usage of the framework. 2 | //! 3 | //! Some exports are renamed to avoid name conflicts as they are generic. 4 | //! These include: 5 | //! 6 | //! - `Context` -> `FrameworkContext` 7 | //! - `Error` -> `FrameworkError` 8 | 9 | #[cfg(feature = "macros")] 10 | pub use command_attr::{check, command, hook}; 11 | 12 | pub use crate::category::Category; 13 | pub use crate::check::{Check, CheckResult, Reason}; 14 | pub use crate::command::{Command, CommandResult}; 15 | pub use crate::configuration::Configuration; 16 | pub use crate::context::{CheckContext, Context as FrameworkContext}; 17 | pub use crate::error::{DispatchError, Error as FrameworkError}; 18 | pub use crate::Framework; 19 | -------------------------------------------------------------------------------- /framework/src/utils/id_map.rs: -------------------------------------------------------------------------------- 1 | //! An Identifier Map. An abstraction for structures who may have many names, but only 2 | //! once instance. 3 | //! 4 | //! The Identifier Map, or `IdMap` for short, handles the case when a structure is stored 5 | //! once, but may be retrieved using a variety of names or aliases. A naive approach would be 6 | //! a simple `HashMap`. However, this is inefficient. You would have to keep 7 | //! copies of the structure for each of its name. To avoid this, the `IdMap` assigns a unique 8 | //! *identifier* to the structure that is cheap to copy and small to store in memory. Consequently, 9 | //! instead of keeping copies of the structure for each of its name, we do this for the identifier. 10 | //! The structure can then be retrieved with the identifier. The `IdMap` employs two `HashMap`s to 11 | //! accomplish its job. For small structures, `IdMap` might be inefficient memory-wise and 12 | //! processor-wise. But it can pay off well for big/huge structures, which may be hundreds of bytes 13 | //! long. 14 | //! 15 | //! The `IdMap` is generic. You can use it with any type for the name of the structure, the identifier 16 | //! for the structure, and the structure itself. 17 | //! 18 | //! # Examples 19 | //! 20 | //! Using `IdMap` for your own purposes: 21 | //! 22 | //! ```rust 23 | //! use serenity_framework::utils::IdMap; 24 | //! 25 | //! #[derive(Debug, PartialEq)] 26 | //! struct Foo { 27 | //! bar: i32, 28 | //! baz: String, 29 | //! } 30 | //! 31 | //! #[derive(Clone, Copy, PartialEq, Eq, Hash)] 32 | //! struct FooId(u64); 33 | //! 34 | //! let mut map: IdMap = IdMap::new(); 35 | //! 36 | //! let foo1 = Foo { 37 | //! bar: 1, 38 | //! baz: "2".to_string(), 39 | //! }; 40 | //! let foo2 = Foo { 41 | //! bar: 3, 42 | //! baz: "4".to_string(), 43 | //! }; 44 | //! 45 | //! map.insert_name("fo".to_string(), FooId(1)); 46 | //! map.insert_name("foo".to_string(), FooId(1)); 47 | //! map.insert(FooId(1), foo1); 48 | //! 49 | //! map.insert_name("go".to_string(), FooId(2)); 50 | //! map.insert(FooId(2), foo2); 51 | //! 52 | //! assert_eq!( 53 | //! map.get(FooId(1)), 54 | //! Some(&Foo { 55 | //! bar: 1, 56 | //! baz: "2".to_string() 57 | //! }) 58 | //! ); 59 | //! // This will panic if a structure under that identifier does not exist. 60 | //! assert_eq!(&map[FooId(1)], &Foo { 61 | //! bar: 1, 62 | //! baz: "2".to_string() 63 | //! }); 64 | //! assert_eq!( 65 | //! map.get_by_name("fo"), 66 | //! Some(&Foo { 67 | //! bar: 1, 68 | //! baz: "2".to_string() 69 | //! }) 70 | //! ); 71 | //! assert_eq!( 72 | //! map.get_by_name("foo"), 73 | //! Some(&Foo { 74 | //! bar: 1, 75 | //! baz: "2".to_string() 76 | //! }) 77 | //! ); 78 | //! 79 | //! assert_eq!(&map[FooId(2)], &Foo { 80 | //! bar: 3, 81 | //! baz: "4".to_string() 82 | //! }); 83 | //! assert_eq!( 84 | //! map.get_by_name("go"), 85 | //! Some(&Foo { 86 | //! bar: 3, 87 | //! baz: "4".to_string() 88 | //! }) 89 | //! ); 90 | //! assert_eq!(map.get_by_name("goo"), None); 91 | //! ``` 92 | 93 | use std::borrow::Borrow; 94 | use std::collections::hash_map::{HashMap, IntoIter, Iter, IterMut, Keys, Values}; 95 | use std::hash::Hash; 96 | use std::ops::{Index, IndexMut}; 97 | 98 | /// An Identifier Map. An abstraction for structures who may have many names, but only 99 | /// once instance. 100 | /// 101 | /// Refer to the [module-level documentation][module] 102 | /// 103 | /// [module]: index.html 104 | #[derive(Debug, Clone)] 105 | pub struct IdMap { 106 | name_to_id: HashMap, 107 | structures: HashMap, 108 | } 109 | 110 | impl Default for IdMap { 111 | fn default() -> Self { 112 | Self { 113 | name_to_id: HashMap::default(), 114 | structures: HashMap::default(), 115 | } 116 | } 117 | } 118 | 119 | impl IdMap { 120 | /// Creates a new `IdMap` instance. 121 | pub fn new() -> Self { 122 | Self::default() 123 | } 124 | 125 | /// Returns the total number of names stored. 126 | pub fn len_names(&self) -> usize { 127 | self.name_to_id.len() 128 | } 129 | 130 | /// Returns the total number of structures stored. 131 | pub fn len(&self) -> usize { 132 | self.structures.len() 133 | } 134 | 135 | /// Returns a boolean indicating that the map is empty. 136 | /// 137 | /// The map is regarded as empty when it contains no structures. 138 | pub fn is_empty(&self) -> bool { 139 | self.len() == 0 140 | } 141 | 142 | /// Returns an iterator over all names stored in the map. 143 | pub fn iter_names(&self) -> Keys<'_, Name, Id> { 144 | self.name_to_id.keys() 145 | } 146 | 147 | /// Returns an iterator over all identifiers stored in the map. 148 | /// 149 | /// Duplicate identifiers may appear. 150 | pub fn iter_ids(&self) -> Values<'_, Name, Id> { 151 | self.name_to_id.values() 152 | } 153 | 154 | /// Returns an iterator over all structures and their assigned 155 | /// identifier. 156 | pub fn iter(&self) -> Iter<'_, Id, Struct> { 157 | self.structures.iter() 158 | } 159 | 160 | /// Returns a mutable iterator over all structures and their assigned 161 | /// identifier. 162 | /// 163 | /// Only the structures are mutable. 164 | pub fn iter_mut(&mut self) -> IterMut<'_, Id, Struct> { 165 | self.structures.iter_mut() 166 | } 167 | } 168 | 169 | impl IdMap 170 | where 171 | Name: Hash + Eq, 172 | Id: Hash + Eq + Copy, 173 | { 174 | /// Assigns a name to an identifier. 175 | /// 176 | /// Returns `None` if the name does not exist in the map. 177 | /// 178 | /// Returns `Some(old_id)` if the name exists in the map. The identifier 179 | /// is overwritten with the new identifier. 180 | pub fn insert_name(&mut self, name: Name, id: Id) -> Option { 181 | self.name_to_id.insert(name, id) 182 | } 183 | 184 | /// Retrieves an identifier based on a name. 185 | /// 186 | /// A copy of the identifier is returned. 187 | /// 188 | /// Returns `None` if an identifier does not belong to the name, 189 | /// otherwise `Some`. 190 | pub fn get_id(&self, name: &B) -> Option 191 | where 192 | Name: Borrow, 193 | B: Hash + Eq, 194 | { 195 | self.name_to_id.get(name).copied() 196 | } 197 | 198 | /// Retrieves a structure based on an identifier. 199 | /// 200 | /// An immutable reference to the structure is returned. 201 | /// 202 | /// Returns `None` if a structure does not belong to the name, 203 | /// otherwise `Some`. 204 | pub fn get_by_name(&self, name: &B) -> Option<&Struct> 205 | where 206 | Name: Borrow, 207 | B: Hash + Eq, 208 | { 209 | self.get_id(name).and_then(|id| self.structures.get(&id)) 210 | } 211 | 212 | /// Retrieves a structure based on an identifier. 213 | /// 214 | /// A mutable reference to the structure is returned. 215 | /// 216 | /// Returns `None` if a structure does not belong to the name, 217 | /// otherwise `Some`. 218 | pub fn get_by_name_mut(&mut self, name: &B) -> Option<&mut Struct> 219 | where 220 | Name: Borrow, 221 | B: Hash + Eq, 222 | { 223 | self.get_id(name).and_then(move |id| self.structures.get_mut(&id)) 224 | } 225 | 226 | /// Retrieves both an identifier and its structure based on a name. 227 | /// 228 | /// An identifier and an immutable reference to the structure is returned. 229 | /// 230 | /// Returns `None` if a identifier/structure does not belong to the name, 231 | /// otherwise `Some`. 232 | pub fn get_pair(&self, name: &B) -> Option<(Id, &Struct)> 233 | where 234 | Name: Borrow, 235 | B: Hash + Eq, 236 | { 237 | let id = self.get_id(name)?; 238 | self.structures.get(&id).map(|aggr| (id, aggr)) 239 | } 240 | 241 | /// Returns a boolean indicating that a structure exists under a name. 242 | pub fn contains(&self, name: &B) -> bool 243 | where 244 | Name: Borrow, 245 | B: Hash + Eq, 246 | { 247 | match self.get_id(name) { 248 | Some(id) => self.contains_id(id), 249 | None => false, 250 | } 251 | } 252 | 253 | /// Returns a boolean indicating that a structure exists under an identifier. 254 | pub fn contains_id(&self, id: Id) -> bool { 255 | self.structures.contains_key(&id) 256 | } 257 | } 258 | 259 | impl IdMap 260 | where 261 | Id: Hash + Eq, 262 | { 263 | /// Assigns a structure to an identifier. 264 | /// 265 | /// Returns `None` if the identifier does not exist in the map. 266 | /// 267 | /// Returns `Some(old_struct)` if the identifier exists in the map. 268 | /// The structure is overwritten with the new structure. 269 | pub fn insert(&mut self, id: Id, aggr: Struct) -> Option { 270 | self.structures.insert(id, aggr) 271 | } 272 | 273 | /// Retrieves a structure based on an identifier. 274 | /// 275 | /// An immutable reference is returned. 276 | /// 277 | /// Returns `None` if a structure does not belong to the identifier, 278 | /// otherwise `Some`. 279 | pub fn get(&self, id: Id) -> Option<&Struct> { 280 | self.structures.get(&id) 281 | } 282 | 283 | /// Retrieves a structure based on an identifier. 284 | /// 285 | /// An mutable reference is returned. 286 | /// 287 | /// Returns `None` if a structure does not belong to the identifier, 288 | /// otherwise `Some`. 289 | pub fn get_mut(&mut self, id: Id) -> Option<&mut Struct> { 290 | self.structures.get_mut(&id) 291 | } 292 | } 293 | 294 | impl Index for IdMap 295 | where 296 | Id: Hash + Eq, 297 | { 298 | type Output = Struct; 299 | 300 | fn index(&self, index: Id) -> &Self::Output { 301 | self.get(index).expect("ID with an associated structure") 302 | } 303 | } 304 | 305 | impl IndexMut for IdMap 306 | where 307 | Id: Hash + Eq, 308 | { 309 | fn index_mut(&mut self, index: Id) -> &mut Self::Output { 310 | self.get_mut(index).expect("ID with an associated structure") 311 | } 312 | } 313 | 314 | impl IntoIterator for IdMap { 315 | type IntoIter = IntoIter; 316 | type Item = (Id, Struct); 317 | 318 | fn into_iter(self) -> Self::IntoIter { 319 | self.structures.into_iter() 320 | } 321 | } 322 | 323 | impl<'a, Name, Id, Struct> IntoIterator for &'a IdMap { 324 | type IntoIter = Iter<'a, Id, Struct>; 325 | type Item = (&'a Id, &'a Struct); 326 | 327 | fn into_iter(self) -> Self::IntoIter { 328 | self.structures.iter() 329 | } 330 | } 331 | 332 | impl<'a, Name, Id, Struct> IntoIterator for &'a mut IdMap { 333 | type IntoIter = IterMut<'a, Id, Struct>; 334 | type Item = (&'a Id, &'a mut Struct); 335 | 336 | fn into_iter(self) -> Self::IntoIter { 337 | self.structures.iter_mut() 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /framework/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! A set of abstraction utilities used by the framework to simplify its code. 2 | //! 3 | //! Usable outside of the framework. 4 | 5 | pub mod id_map; 6 | pub mod segments; 7 | 8 | pub use id_map::*; 9 | pub use segments::*; 10 | -------------------------------------------------------------------------------- /framework/src/utils/segments.rs: -------------------------------------------------------------------------------- 1 | //! Functions and types for handling *segments*. 2 | //! 3 | //! A segment is a substring of a source string. The boundaries of the substring 4 | //! are determined by a delimiter, which is a &[`str`] value. 5 | 6 | use std::borrow::Cow; 7 | 8 | /// Returns the index to the end of a segment in the source. 9 | /// 10 | /// If the delimiter could not be found in the source, the length of the source 11 | /// is returned instead. 12 | /// 13 | /// # Examples 14 | /// 15 | /// ```rust 16 | /// use serenity_framework::utils::segment_index; 17 | /// 18 | /// assert_eq!(segment_index("hello world", " "), 5); 19 | /// assert_eq!(segment_index("world", " "), "world".len()); 20 | /// ``` 21 | pub fn segment_index(src: &str, delimiter: &str) -> usize { 22 | src.find(delimiter).unwrap_or_else(|| src.len()) 23 | } 24 | 25 | /// Returns a segment of the source. 26 | /// 27 | /// If the source is empty, `None` is returned. 28 | /// 29 | /// # Examples 30 | /// 31 | /// ```rust 32 | /// use serenity_framework::utils::segment; 33 | /// 34 | /// assert_eq!(segment("", " "), None); 35 | /// assert_eq!(segment("hello world", " "), Some("hello")); 36 | /// assert_eq!(segment("world", " "), Some("world")); 37 | /// ``` 38 | pub fn segment<'a>(src: &'a str, delimiter: &str) -> Option<&'a str> { 39 | if src.is_empty() { 40 | None 41 | } else { 42 | Some(&src[..segment_index(src, delimiter)]) 43 | } 44 | } 45 | 46 | /// Returns a segment and the rest of the source after the delimiter. 47 | /// 48 | /// If the delimiter appears many times after the segment, all instances of it 49 | /// are removed. 50 | /// 51 | /// If the source is empty, `None` is returned. 52 | /// 53 | /// # Examples 54 | /// 55 | /// ```rust 56 | /// use serenity_framework::utils::segment_split; 57 | /// 58 | /// assert_eq!(segment_split("hello world", " "), Some(("hello", "world"))); 59 | /// assert_eq!(segment_split("world", " "), Some(("world", ""))); 60 | /// assert_eq!(segment_split("", " "), None); 61 | /// ``` 62 | pub fn segment_split<'a>(src: &'a str, delimiter: &str) -> Option<(&'a str, &'a str)> { 63 | if src.is_empty() { 64 | None 65 | } else { 66 | let (segment, rest) = src.split_at(segment_index(src, delimiter)); 67 | Some((segment, rest.trim_start_matches(delimiter))) 68 | } 69 | } 70 | 71 | /// An iterator type that splits a string into segments using a delimiter. 72 | /// 73 | /// It returns [`Cow`] values to handle case sensitivity. 74 | /// 75 | /// [`Cow::Borrowed`] is returned if the [`case_insensitive`] field is `false`, 76 | /// as the segment is a slice to the string. 77 | /// 78 | /// [`Cow::Owned`] is returned if [`case_insensitive`] is `true`, as the segment 79 | /// is converted to lowercase using [`str::to_lowercase`]. 80 | /// 81 | /// # Examples 82 | /// 83 | /// ```rust 84 | /// use std::borrow::Cow; 85 | /// 86 | /// use serenity_framework::utils::Segments; 87 | /// 88 | /// let mut iter = Segments::new("hello world", " ", false); 89 | /// 90 | /// assert_eq!(iter.next(), Some(Cow::Borrowed("hello"))); 91 | /// assert_eq!(iter.next(), Some(Cow::Borrowed("world"))); 92 | /// assert_eq!(iter.next(), None); 93 | /// 94 | /// let mut iter = Segments::new("hElLo WOrLd", " ", true); 95 | /// 96 | /// assert_eq!(iter.next(), Some(Cow::Owned("hello".to_string()))); 97 | /// assert_eq!(iter.next(), Some(Cow::Owned("world".to_string()))); 98 | /// assert_eq!(iter.next(), None); 99 | /// ``` 100 | /// 101 | /// [`Cow`]: std::borrow::Cow 102 | /// [`case_insensitive`]: Segments::case_insensitive 103 | #[derive(Debug, Clone)] 104 | pub struct Segments<'a> { 105 | src: &'a str, 106 | delimiter: &'a str, 107 | case_insensitive: bool, 108 | } 109 | 110 | impl<'a> Segments<'a> { 111 | /// Creates a `Segments` instance. 112 | pub fn new(src: &'a str, delimiter: &'a str, case_insensitive: bool) -> Self { 113 | Self { 114 | src, 115 | delimiter, 116 | case_insensitive, 117 | } 118 | } 119 | 120 | /// Returns the source string from which segments are constructed. 121 | pub fn source(&self) -> &'a str { 122 | self.src 123 | } 124 | 125 | /// Sets the new source string from which segments are constructed. 126 | pub fn set_source(&mut self, src: &'a str) { 127 | self.src = src; 128 | } 129 | 130 | /// Returns the delimiter string that is used to determine the boundaries 131 | /// of a segment. 132 | pub fn delimiter(&self) -> &'a str { 133 | self.delimiter 134 | } 135 | 136 | /// Returns the boolean that determines whether to ignore casing of segments. 137 | pub fn case_insensitive(&self) -> bool { 138 | self.case_insensitive 139 | } 140 | 141 | /// Returns a boolean indicating that the source string is empty. 142 | pub fn is_empty(&self) -> bool { 143 | self.src.is_empty() 144 | } 145 | } 146 | 147 | impl<'a> Iterator for Segments<'a> { 148 | type Item = Cow<'a, str>; 149 | 150 | fn next(&mut self) -> Option { 151 | let (segment, rest) = segment_split(self.src, self.delimiter)?; 152 | 153 | self.src = rest; 154 | 155 | Some(if self.case_insensitive { 156 | Cow::Owned(segment.to_lowercase()) 157 | } else { 158 | Cow::Borrowed(segment) 159 | }) 160 | } 161 | } 162 | 163 | /// Returns a quoted segment and the rest of the source. 164 | /// 165 | /// A quoted segment is a part of the source that is encompassed by quotation marks. 166 | /// Or, if a leading quotation mark exists, but the trailing mark is missing, 167 | /// the quoted segment is the rest of the source excluding the leading mark. 168 | /// 169 | /// If the source is empty or the source does not start with a leading quotation mark, 170 | /// `None` is returned. 171 | /// 172 | /// # Examples 173 | /// 174 | /// ``` 175 | /// // Used example strings are from the YouTube video https://www.youtube.com/watch?v=1edPxKqiptw 176 | /// use serenity_framework::utils::quoted_segment_split; 177 | /// 178 | /// assert_eq!(quoted_segment_split(""), None); 179 | /// assert_eq!(quoted_segment_split("Doll and roll"), None); 180 | /// assert_eq!(quoted_segment_split("\"and some\" and home."), Some(("and some", " and home."))); 181 | /// assert_eq!( 182 | /// quoted_segment_split("\"Stranger does not rhyme with anger"), 183 | /// Some(("Stranger does not rhyme with anger", "")) 184 | /// ); 185 | /// ``` 186 | pub fn quoted_segment_split(src: &str) -> Option<(&str, &str)> { 187 | if src.is_empty() || !src.starts_with('"') { 188 | return None; 189 | } 190 | 191 | let src = &src[1..]; 192 | 193 | match src.find('"') { 194 | Some(index) => Some((&src[..index], &src[(index + 1)..])), 195 | None => Some((src, "")), 196 | } 197 | } 198 | 199 | /// Returns a quoted segment of the source. 200 | /// 201 | /// Refer to [`quoted_segment_split`] for the definition of a quoted segment. 202 | /// 203 | /// If the source is empty or the source does not start with a leading quotation mark, 204 | /// `None` is returned. 205 | /// 206 | /// # Examples 207 | /// 208 | /// ``` 209 | /// // Used example strings are from the YouTube video https://www.youtube.com/watch?v=1edPxKqiptw 210 | /// use serenity_framework::utils::quoted_segment; 211 | /// 212 | /// assert_eq!(quoted_segment(""), None); 213 | /// assert_eq!(quoted_segment("Neither does devour with clangour"), None); 214 | /// assert_eq!(quoted_segment("\"Souls but\" foul"), Some("Souls but")); 215 | /// assert_eq!(quoted_segment("\"haunt but aunt"), Some("haunt but aunt")); 216 | /// ``` 217 | pub fn quoted_segment(src: &str) -> Option<&str> { 218 | quoted_segment_split(src).map(|(seg, _)| seg) 219 | } 220 | 221 | /// Returns an argument segment and the rest of the source. 222 | /// 223 | /// An argument segment is either [a quoted segment][qseg] 224 | /// or [a normal segment][seg]. 225 | /// 226 | /// When the segment is quoted, the rest of the source is trimmed off of 227 | /// the specified `delimiter`. 228 | /// 229 | /// If the source is empty, `None` is returned. 230 | /// 231 | /// # Examples 232 | /// 233 | /// ``` 234 | /// // Used example strings are from the YouTube video https://www.youtube.com/watch?v=1edPxKqiptw 235 | /// use serenity_framework::utils::argument_segment_split; 236 | /// 237 | /// assert_eq!(argument_segment_split("", ", "), None); 238 | /// assert_eq!(argument_segment_split("Font, front, wont", ", "), Some(("Font", "front, wont"))); 239 | /// assert_eq!( 240 | /// argument_segment_split("\"want, grand\", and grant", ", "), 241 | /// Some(("want, grand", "and grant")) 242 | /// ); 243 | /// assert_eq!( 244 | /// argument_segment_split("\"Shoes, goes, does.", ", "), 245 | /// Some(("Shoes, goes, does.", "")) 246 | /// ); 247 | /// ``` 248 | /// 249 | /// [qseg]: quoted_segment_split 250 | /// [seg]: segment 251 | pub fn argument_segment_split<'a>(src: &'a str, delimiter: &str) -> Option<(&'a str, &'a str)> { 252 | match quoted_segment_split(src) { 253 | Some((segment, rest)) => Some((segment, rest.trim_start_matches(delimiter))), 254 | None => segment_split(src, delimiter), 255 | } 256 | } 257 | 258 | /// Returns an argument segment of the source. 259 | /// 260 | /// Refer to [`argument_segment_split`] for the definition of an argument segment. 261 | /// 262 | /// If the source is empty, `None` is returned. 263 | /// 264 | /// # Examples 265 | /// 266 | /// ``` 267 | /// // Used example strings are from the YouTube video https://www.youtube.com/watch?v=1edPxKqiptw 268 | /// use serenity_framework::utils::argument_segment; 269 | /// 270 | /// assert_eq!(argument_segment("", ", "), None); 271 | /// assert_eq!(argument_segment("Now first say finger, ", ", "), Some("Now first say finger")); 272 | /// assert_eq!( 273 | /// argument_segment("\"And then singer, ginger\", linger, ", ", "), 274 | /// Some("And then singer, ginger") 275 | /// ); 276 | /// assert_eq!(argument_segment("\"Real, zeal, mauve", ", "), Some("Real, zeal, mauve")); 277 | /// ``` 278 | 279 | pub fn argument_segment<'a>(src: &'a str, delimiter: &str) -> Option<&'a str> { 280 | argument_segment_split(src, delimiter).map(|(seg, _)| seg) 281 | } 282 | 283 | /// An iterator type that splits a string into [argument segments][aseg] using a delimiter and quotes. 284 | /// 285 | /// # Examples 286 | /// 287 | /// ```rust 288 | /// // Used example strings are from the YouTube video https://www.youtube.com/watch?v=1edPxKqiptw 289 | /// use serenity_framework::utils::ArgumentSegments; 290 | /// 291 | /// let mut iter = ArgumentSegments::new("Marriage, \"foliage, mirage\", \"and age.", ", "); 292 | /// 293 | /// assert_eq!(iter.next(), Some("Marriage")); 294 | /// assert_eq!(iter.next(), Some("foliage, mirage")); 295 | /// assert_eq!(iter.next(), Some("and age.")); 296 | /// assert_eq!(iter.next(), None); 297 | /// ``` 298 | /// 299 | /// [aseg]: argument_segment_split 300 | #[derive(Debug, Clone)] 301 | pub struct ArgumentSegments<'a> { 302 | src: &'a str, 303 | delimiter: &'a str, 304 | } 305 | 306 | impl<'a> ArgumentSegments<'a> { 307 | /// Creates a new `ArgumentSegments` instance. 308 | pub fn new(src: &'a str, delimiter: &'a str) -> Self { 309 | Self { 310 | src, 311 | delimiter, 312 | } 313 | } 314 | 315 | /// Returns the source string from which segments are constructed. 316 | pub fn source(&self) -> &'a str { 317 | self.src 318 | } 319 | 320 | /// Sets the new source string from which segments are constructed. 321 | pub fn set_source(&mut self, src: &'a str) { 322 | self.src = src; 323 | } 324 | 325 | /// Returns the delimiter string that is used to determine the boundaries 326 | /// of a segment. 327 | pub fn delimiter(&self) -> &'a str { 328 | self.delimiter 329 | } 330 | 331 | /// Returns a boolean indicating that the source string is empty. 332 | pub fn is_empty(&self) -> bool { 333 | self.src.is_empty() 334 | } 335 | } 336 | 337 | impl<'a> Iterator for ArgumentSegments<'a> { 338 | type Item = &'a str; 339 | 340 | fn next(&mut self) -> Option { 341 | let (segment, rest) = argument_segment_split(self.src, self.delimiter)?; 342 | 343 | self.src = rest; 344 | 345 | Some(segment) 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | unstable_features = true 2 | use_small_heuristics = "Max" 3 | # Turn on once the rustfmt supporting these becomes available on rustup 4 | # width_heuristics = "Max" 5 | # imports_granularity = "Module" 6 | group_imports = "StdExternalCrate" 7 | imports_layout = "HorizontalVertical" 8 | struct_lit_single_line = false 9 | match_arm_blocks = true 10 | match_block_trailing_comma = true 11 | overflow_delimited_expr = true 12 | newline_style = "Unix" 13 | use_field_init_shorthand = true 14 | use_try_shorthand = true 15 | normalize_comments = true 16 | format_code_in_doc_comments = true 17 | --------------------------------------------------------------------------------