diff --git a/.appveyor.yml b/.appveyor.yml index 4b257d388..5e9ed4121 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -35,6 +35,9 @@ test_script: #- if [%TOOLCHAIN%]==[nightly] ( # cargo test --features nightly # ) + - if [%TOOLCHAIN%]==[nightly] ( + cargo test -p tsukuyomi-codegen + ) # contrib - cargo test -p tsukuyomi-juniper - if [%TOOLCHAIN%]==[nightly] ( diff --git a/.travis.yml b/.travis.yml index 81f2c38fa..70a819fe1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -81,6 +81,7 @@ matrix: - cargo test - cargo test --no-default-features #- cargo test --features nightly + - cargo test -p tsukuyomi-codegen # contrib - cargo test -p tsukuyomi-juniper # doctest @@ -104,6 +105,7 @@ matrix: script: - cargo clean - cargo doc + #- cargo doc -p tsukuyomi-codegen - cargo doc -p tsukuyomi-juniper - rm -f target/doc/.lock deploy: diff --git a/Cargo.toml b/Cargo.toml index dedfddbd1..f73902e32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,10 +30,12 @@ maintenance = { status = "actively-developed" } [workspace] members = [ "doctest", + "tsukuyomi-codegen", "tsukuyomi-juniper", "examples/async-await", "examples/basic", + "examples/codegen", "examples/diesel", "examples/git-server", "examples/json", @@ -64,6 +66,8 @@ serde = { version = "1.0.66", features = ["derive"] } serde_json = "1.0.20" state = "0.4" +tsukuyomi-codegen = { version = "0.1.0", path = "tsukuyomi-codegen", optional = true } + [dev-dependencies] matches = "0.1.6" time = "0.1.40" @@ -75,4 +79,5 @@ tokio-uds = "0.2.0" default = [] secure = ["cookie/secure"] tls = ["rustls", "tokio-rustls"] -# nightly = [] +codegen = ["tsukuyomi-codegen"] +nightly = ["tsukuyomi-codegen/nightly"] diff --git a/examples/codegen/Cargo.toml b/examples/codegen/Cargo.toml new file mode 100644 index 000000000..0156f1ad0 --- /dev/null +++ b/examples/codegen/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "example-codegen" +version = "0.0.0" +authors = ["Yusuke Sasaki "] +publish = false + +[[bin]] +name = "codegen" +path = "src/main.rs" +doc = false + +[dependencies] +tsukuyomi = { path = "../..", features = ["codegen"] } +futures-await = "0.1.1" diff --git a/examples/codegen/src/main.rs b/examples/codegen/src/main.rs new file mode 100644 index 000000000..3e1ecf5c3 --- /dev/null +++ b/examples/codegen/src/main.rs @@ -0,0 +1,38 @@ +#![feature(proc_macro)] +#![feature(proc_macro_non_items, generators)] // for futures-await + +extern crate futures_await as futures; +extern crate tsukuyomi; + +use futures::prelude::{await, Future}; +use tsukuyomi::prelude::handler; +use tsukuyomi::{App, Error, Input, Handler}; + +#[handler] +fn ready_handler() -> &'static str { + "Hello, Tsukuyomi.\n" +} + +#[handler(async)] +fn async_handler(input: &mut Input) -> impl Future + Send + 'static { + input.body_mut().read_all().convert_to() +} + +#[handler(await)] +fn await_handler() -> tsukuyomi::Result { + let read_all = Input::with_current(|input| input.body_mut().read_all()); + let data: String = await!(read_all.convert_to())?; + Ok(format!("Received: {}", data)) +} + +fn main() -> tsukuyomi::AppResult<()> { + let app = App::builder() + .mount("/", |m| { + m.get("/ready").handle(Handler::new(ready_handler)); + m.post("/async").handle(Handler::new(async_handler)); + m.post("/await").handle(Handler::new(await_handler)); + }) + .finish()?; + + tsukuyomi::run(app) +} diff --git a/src/handler.rs b/src/handler.rs index 9776eeaff..86c6c4c63 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -18,7 +18,8 @@ impl fmt::Debug for Handler { } impl Handler { - fn new(handler: impl Fn(&mut Input) -> Handle + Send + Sync + 'static) -> Handler { + #[doc(hidden)] + pub fn new(handler: impl Fn(&mut Input) -> Handle + Send + Sync + 'static) -> Handler { Handler(Box::new(handler)) } @@ -179,7 +180,8 @@ impl Handle { Handle::ready(Err(err.into())) } - fn ready(result: Result) -> Handle { + #[doc(hidden)] + pub fn ready(result: Result) -> Handle { Handle(HandleKind::Ready(Some(result))) } @@ -193,6 +195,19 @@ impl Handle { }))) } + #[doc(hidden)] + pub fn async_responder(mut future: F) -> Handle + where + F: Future + Send + 'static, + F::Item: Responder, + Error: From, + { + Handle(HandleKind::Async(Box::new(move |input| { + let x = try_ready!(input.with_set_current(|| future.poll())); + x.respond_to(input).map(Async::Ready) + }))) + } + pub(crate) fn poll_ready(&mut self, input: &mut Input) -> Poll { match self.0 { HandleKind::Ready(ref mut res) => res.take().expect("this future has already polled").map(Async::Ready), diff --git a/src/lib.rs b/src/lib.rs index 3c0fd2ea2..2f35fd8bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ #![deny(unused_extern_crates)] #![deny(warnings)] #![deny(bare_trait_objects)] +#![cfg_attr(feature = "codegen", feature(use_extern_macros))] extern crate bytes; extern crate cookie; @@ -38,6 +39,9 @@ extern crate rustls; #[cfg(feature = "tls")] extern crate tokio_rustls; +#[cfg(feature = "codegen")] +extern crate tsukuyomi_codegen; + pub mod app; pub mod error; pub mod handler; @@ -72,3 +76,9 @@ pub fn run(app: App) -> AppResult<()> { server.serve(); Ok(()) } + +#[allow(missing_docs)] +pub mod prelude { + #[cfg(feature = "codegen")] + pub use tsukuyomi_codegen::handler; +} diff --git a/tsukuyomi-codegen/Cargo.toml b/tsukuyomi-codegen/Cargo.toml new file mode 100644 index 000000000..0a5634c73 --- /dev/null +++ b/tsukuyomi-codegen/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tsukuyomi-codegen" +version = "0.1.0" +authors = ["Yusuke Sasaki "] +publish = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "0.4.6" +syn = { version = "0.14.4", features = ["full", "extra-traits"] } +quote = "0.6.3" + +[dev-dependencies] +tsukuyomi = { version = "0.2.0", path = ".." } +futures = "0.1.21" +futures-await = "0.1.1" + +[features] +nightly = ["proc-macro2/nightly"] diff --git a/tsukuyomi-codegen/src/lib.rs b/tsukuyomi-codegen/src/lib.rs new file mode 100644 index 000000000..d0eb83a7b --- /dev/null +++ b/tsukuyomi-codegen/src/lib.rs @@ -0,0 +1,187 @@ +//! Code generation support for Tsukuyomi. + +#![feature(proc_macro, use_extern_macros)] + +extern crate proc_macro; +extern crate proc_macro2; +#[macro_use] +extern crate syn; +extern crate quote; + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::*; + +macro_rules! try_quote { + ($e:expr) => { + match $e { + Ok(v) => v, + Err(e) => { + use proc_macro2::Span; + use quote::*; + let msg = e.to_string(); + return Into::into(quote_spanned!(Span::call_site() => compile_error!(#msg))); + } + } + }; +} + +macro_rules! bail_quote { + ($e:expr) => {{ + use proc_macro2::Span; + use quote::*; + let msg = $e.to_string(); + return Into::into(quote_spanned!(Span::call_site() => compile_error!(#msg))); + }}; + ($e:expr, $($args:expr),*) => {{ + bail_quote!(format!($e, $($args),*)) + }} +} + +#[derive(Debug, Copy, Clone, PartialEq)] +enum HandlerMode { + Ready, + Async, + AsyncAwait, +} + +impl std::str::FromStr for HandlerMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.trim() { + "" | "ready" => Ok(HandlerMode::Ready), + "async" => Ok(HandlerMode::Async), + "await" => Ok(HandlerMode::AsyncAwait), + s => Err(format!("invalid mode: `{}'", s)), + } + } +} + +fn detect_mode(attr: &TokenStream, _item: &syn::ItemFn) -> Result { + attr.to_string().parse() +} + +/// Modifies the signature of a free-standing function to a suitable form for using the handler. +/// +/// # Examples +/// +/// ``` +/// # #![feature(proc_macro, use_extern_macros)] +/// # extern crate tsukuyomi; +/// # extern crate tsukuyomi_codegen; +/// # use tsukuyomi_codegen::handler; +/// #[handler] +/// fn handler() -> &'static str { +/// "Hello" +/// } +/// ``` +/// +/// ``` +/// # #![feature(proc_macro, use_extern_macros)] +/// # extern crate tsukuyomi; +/// # extern crate tsukuyomi_codegen; +/// # use tsukuyomi_codegen::handler; +/// # use tsukuyomi::Input; +/// #[handler] +/// fn handler(input: &mut Input) -> String { +/// format!("path = {:?}", input.uri().path()) +/// } +/// ``` +/// +/// ``` +/// # #![feature(proc_macro, use_extern_macros)] +/// # extern crate tsukuyomi; +/// # extern crate tsukuyomi_codegen; +/// # extern crate futures; +/// # use tsukuyomi_codegen::handler; +/// # use tsukuyomi::{Input, Error}; +/// # use futures::Future; +/// #[handler(async)] +/// fn handler(input: &mut Input) -> impl Future + Send + 'static { +/// input.body_mut().read_all().convert_to() +/// } +/// ``` +/// +/// ``` +/// # #![feature(proc_macro, use_extern_macros, proc_macro_non_items, generators)] +/// # extern crate tsukuyomi; +/// # extern crate tsukuyomi_codegen; +/// # extern crate futures_await as futures; +/// # use tsukuyomi_codegen::handler; +/// # use tsukuyomi::Error; +/// #[handler(await)] +/// fn handler() -> Result<&'static str, Error> { +/// Ok("Hello") +/// } +/// ``` +#[proc_macro_attribute] +pub fn handler(attr: TokenStream, item: TokenStream) -> TokenStream { + let item: syn::ItemFn = try_quote!(syn::parse(item)); + + // FIXME: detect the keyword `async` + let mode = try_quote!(detect_mode(&attr, &item)); + + let num_args = item.decl.inputs.iter().count(); + if num_args > 1 { + bail_quote!("Too many arguments"); + } + if mode == HandlerMode::AsyncAwait && num_args != 0 { + bail_quote!("The number of arguments in #[async] handler must be zero."); + } + + let mut inner = item.clone(); + inner.ident = syn::Ident::new("inner", inner.ident.span()); + if mode == HandlerMode::AsyncAwait { + inner.attrs.push(parse_quote!(#[async])); + } + + let mut new_item = item.clone(); + let input_ident: syn::Ident = if num_args == 0 && mode != HandlerMode::Ready { + syn::Ident::new("_input", Span::call_site()) + } else { + syn::Ident::new("input", Span::call_site()) + }; + new_item.decl.inputs = Some(syn::punctuated::Pair::End(parse_quote!( + #input_ident: &mut ::tsukuyomi::input::Input + ))).into_iter() + .collect(); + match new_item.decl.output { + syn::ReturnType::Default => bail_quote!("unimplemented"), + syn::ReturnType::Type(_, ref mut ty) => { + *ty = Box::new(parse_quote!(::tsukuyomi::handler::Handle)); + } + } + new_item.block = { + let call: syn::Expr = match num_args { + 0 => parse_quote!(inner()), + 1 => parse_quote!(inner(input)), + _ => unreachable!(), + }; + + let body: syn::Expr = match mode { + HandlerMode::Ready => parse_quote!({ + ::tsukuyomi::handler::Handle::ready( + ::tsukuyomi::output::Responder::respond_to(#call, input) + ) + }), + HandlerMode::Async | HandlerMode::AsyncAwait => parse_quote!({ + ::tsukuyomi::handler::Handle::async_responder(#call) + }), + }; + + let prelude: Option = if mode == HandlerMode::AsyncAwait { + Some(parse_quote!(use futures::prelude::async;)) + } else { + None + }; + + Box::new(parse_quote!({ + #prelude + #inner + #body + })) + }; + + quote!(#new_item).into() +} diff --git a/tsukuyomi-codegen/tests/smokes.rs b/tsukuyomi-codegen/tests/smokes.rs new file mode 100644 index 000000000..f2cc70473 --- /dev/null +++ b/tsukuyomi-codegen/tests/smokes.rs @@ -0,0 +1,56 @@ +#![feature(proc_macro, proc_macro_non_items, generators)] + +extern crate futures_await as futures; +extern crate tsukuyomi; +extern crate tsukuyomi_codegen; + +use futures::future; +use futures::prelude::{async, await, Future}; +use tsukuyomi::{Error, Handler, Input}; +use tsukuyomi_codegen::handler; + +#[allow(dead_code)] +fn ready_without_input() { + #[handler] + fn handler() -> &'static str { + "Hello" + } + let _ = Handler::new(handler); +} + +#[allow(dead_code)] +fn ready_with_input() { + #[handler] + fn handler(_: &mut Input) -> &'static str { + "Hello" + } + let _ = Handler::new(handler); +} + +#[allow(dead_code)] +fn async_without_input() { + #[handler(async)] + fn handler() -> impl Future + Send + 'static { + future::ok("Hello") + } + let _ = Handler::new(handler); +} + +#[allow(dead_code)] +fn async_with_input() { + #[handler(async)] + fn handler(_: &mut Input) -> impl Future + Send + 'static { + future::ok("Hello") + } + let _ = Handler::new(handler); +} + +#[allow(dead_code)] +fn async_with_await() { + #[handler(await)] + fn handler() -> Result<&'static str, Error> { + await!(future::ok::<(), Error>(()))?; + Ok("Hello") + } + let _ = Handler::new(handler); +}