Skip to content
This repository has been archived by the owner on Jan 18, 2020. It is now read-only.

Commit

Permalink
feat(codegen): add custom attribute to modify the signature of handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
ubnt-intrepid committed Jul 7, 2018
1 parent f3da082 commit ed92eb7
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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] (
Expand Down
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand All @@ -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"]
14 changes: 14 additions & 0 deletions examples/codegen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "example-codegen"
version = "0.0.0"
authors = ["Yusuke Sasaki <[email protected]>"]
publish = false

[[bin]]
name = "codegen"
path = "src/main.rs"
doc = false

[dependencies]
tsukuyomi = { path = "../..", features = ["codegen"] }
futures-await = "0.1.1"
38 changes: 38 additions & 0 deletions examples/codegen/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<Item = String, Error = Error> + Send + 'static {
input.body_mut().read_all().convert_to()
}

#[handler(await)]
fn await_handler() -> tsukuyomi::Result<String> {
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)
}
19 changes: 17 additions & 2 deletions src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down Expand Up @@ -179,7 +180,8 @@ impl Handle {
Handle::ready(Err(err.into()))
}

fn ready(result: Result<Output, Error>) -> Handle {
#[doc(hidden)]
pub fn ready(result: Result<Output, Error>) -> Handle {
Handle(HandleKind::Ready(Some(result)))
}

Expand All @@ -193,6 +195,19 @@ impl Handle {
})))
}

#[doc(hidden)]
pub fn async_responder<F>(mut future: F) -> Handle
where
F: Future + Send + 'static,
F::Item: Responder,
Error: From<F::Error>,
{
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<Output, Error> {
match self.0 {
HandleKind::Ready(ref mut res) => res.take().expect("this future has already polled").map(Async::Ready),
Expand Down
10 changes: 10 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
21 changes: 21 additions & 0 deletions tsukuyomi-codegen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "tsukuyomi-codegen"
version = "0.1.0"
authors = ["Yusuke Sasaki <[email protected]>"]
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"]
187 changes: 187 additions & 0 deletions tsukuyomi-codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Self::Err> {
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<HandlerMode, String> {
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<Item = String, Error = Error> + 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<syn::Stmt> = if mode == HandlerMode::AsyncAwait {
Some(parse_quote!(use futures::prelude::async;))
} else {
None
};

Box::new(parse_quote!({
#prelude
#inner
#body
}))
};

quote!(#new_item).into()
}
Loading

0 comments on commit ed92eb7

Please sign in to comment.