commit 77bbd2f7ae2f5eda963d5fff76d8eb049ffaae44
Author: Jonas Platte <jplatte+git@posteo.de>
Date: Sun, 21 Jul 2019 04:02:26 +0200
Initial commit
Diffstat:
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();
+}