├── .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