ruma-bot

git clone git://archive.git.mtrnord.blog/MTRNord/ruma-bot.git
Log | Files | Refs

commit 77bbd2f7ae2f5eda963d5fff76d8eb049ffaae44
Author: Jonas Platte <jplatte+git@posteo.de>
Date:   Sun, 21 Jul 2019 04:02:26 +0200

Initial commit

Diffstat:
A.gitignore | 3+++
ACargo.toml | 15+++++++++++++++
Amacro/Cargo.toml | 17+++++++++++++++++
Amacro/src/args.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amacro/src/lib.rs | 36++++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/echo.rs | 11+++++++++++
7 files changed, 221 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ruma-bot" +version = "0.1.0" +authors = ["Jonas Platte <jplatte+git@posteo.de>"] +edition = "2018" + +[dependencies.ruma-bot-macro] +path = "macro" + +[dev-dependencies.tokio] +git = "https://github.com/tokio-rs/tokio" + +[workspace] +members = ["macro"] +\ No newline at end of file diff --git a/macro/Cargo.toml b/macro/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ruma-bot-macro" +version = "0.1.0" +authors = ["Jonas Platte <jplatte+git@posteo.de>"] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "0.4.30" +quote = "0.6.13" + +[dependencies.syn] +version = "0.15.40" +features = ["full", "extra-traits"] +\ No newline at end of file diff --git a/macro/src/args.rs b/macro/src/args.rs @@ -0,0 +1,81 @@ +use std::collections::HashSet; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::{Pair, Punctuated}, + spanned::Spanned, + Expr, ExprArray, ExprLit, Ident, Lit, LitStr, Token, +}; + +pub enum Arg { + Command(LitStr), + Commands(HashSet<LitStr>), + //DmOnly +} + +impl Parse for Arg { + fn parse(input: ParseStream) -> syn::Result<Self> { + let ident = input.call(Ident::parse)?; + let arg_name = ident.to_string(); + match &arg_name[..] { + "command" => { + if input.peek(Token![=]) { + let _: Token![=] = input.parse()?; + Ok(Self::Command(input.parse()?)) + } else { + Err(input.error("expected `=`")) + } + } + "commands" => { + if input.peek(Token![=]) { + let _: Token![=] = input.parse()?; + let array: ExprArray = input.parse()?; + + if !array.attrs.is_empty() { + return Err(syn::Error::new( + array.attrs[0].tts.span(), + "attributes are not allowed here", + )); + } + + let commands = array + .elems + .into_pairs() + .map(Pair::into_value) + .map(|expr| { + if let Expr::Lit(ExprLit { attrs, lit }) = expr { + if !attrs.is_empty() { + return Err(syn::Error::new( + attrs[0].tts.span(), + "attributes are not allowed here", + )); + } + + if let Lit::Str(s) = lit { + Ok(s) + } else { + Err(syn::Error::new(lit.span(), "expected string literal")) + } + } else { + Err(syn::Error::new(expr.span(), "expected string literal")) + } + }) + .collect::<Result<_, _>>()?; + + Ok(Self::Commands(commands)) + } else { + Err(input.error("expected `=`")) + } + } + _ => Err(syn::Error::new(ident.span(), "unknown ruma_bot option")), + } + } +} + +pub struct Args(Vec<Arg>); + +impl Parse for Args { + fn parse(input: ParseStream) -> syn::Result<Self> { + let args = Punctuated::<Arg, Token![,]>::parse_terminated(input)?; + Ok(Self(args.into_iter().collect())) + } +} diff --git a/macro/src/lib.rs b/macro/src/lib.rs @@ -0,0 +1,36 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; + +use quote::quote; +use syn::{parse_macro_input, ItemFn}; + +use args::Args; + +mod args; + +#[proc_macro_attribute] +pub fn command_handler(args: TokenStream, input: TokenStream) -> TokenStream { + let handler_fn = parse_macro_input!(input as ItemFn); + let macro_args = parse_macro_input!(args as Args); + + assert!( + handler_fn.unsafety.is_none(), + "ruma_bot handler functions are not allowed to be `unsafe`" + ); + + let ident = handler_fn.ident; + // TODO + let commands = vec![ident.to_string()]; + + TokenStream::from(quote! { + #[allow(non_camel_case_types)] + struct #ident; + + impl ruma_bot::CommandHandler for #ident { + fn commands(&self) -> &'static [&'static str] { + &[#(#commands),*] + } + } + }) +} diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1,58 @@ +//! `ruma_api_bot` is a crate that aims to simplify creating bots for [matrix][] servers. +//! +//! It currently requires nightly, but should work on stable once the `async_await` feature is +//! stabilized. +//! +//! [matrix]: https://matrix.org/ + +#![feature(async_await)] +#![warn(missing_debug_implementations, missing_docs)] + +use std::{collections::HashMap, sync::Arc}; + +pub use ruma_bot_macro::command_handler; + +/// An `async fn` annotated with `#[ruma_bot::command_handler] +pub trait CommandHandler { + /// The command(s) this function handles + fn commands(&self) -> &'static [&'static str]; +} + +#[derive(Clone)] +pub struct BotBuilder { + handlers: HashMap<&'static str, &'static dyn CommandHandler>, +} + +impl BotBuilder { + pub fn new() -> Self { + Self { + handlers: HashMap::new(), + } + } + + /// Register a command handler + pub fn register(mut self, handler: &'static dyn CommandHandler) -> Self { + for command in handler.commands() { + let _old_value = self.handlers.insert(command, handler); + // TODO: Log a warning if _old_value is Some + } + + self + } + + pub fn build(self) -> Bot { + Bot { + handlers: Arc::new(self.handlers), + } + } +} + +pub struct Bot { + handlers: Arc<HashMap<&'static str, &'static dyn CommandHandler>>, +} + +impl Bot { + pub async fn run(&self) -> Result<(), ()> { + Ok(()) + } +} diff --git a/tests/echo.rs b/tests/echo.rs @@ -0,0 +1,11 @@ +#![feature(async_await)] + +use ruma_bot::{command_handler, BotBuilder}; + +#[command_handler(command = "help")] +async fn help() {} + +#[tokio::main] +async fn main() { + let bot = BotBuilder::new().register(&help).build(); +}