From 5bcb78ab1482819928f614f6dc50ae70f92ef0bf Mon Sep 17 00:00:00 2001 From: Xavientois <34867186+Xavientois@users.noreply.github.com> Date: Tue, 26 Jul 2022 02:57:19 -0400 Subject: [PATCH 1/2] refactor: main macro to own module (#279) * feat: add lambda support * chore: move tests to main subdirectory * chore: remove lambda for refactor --- codegen/src/lib.rs | 392 +----------------- codegen/src/main/mod.rs | 390 +++++++++++++++++ .../tests/ui/{ => main}/missing-attribute.rs | 0 .../ui/{ => main}/missing-attribute.stderr | 8 +- codegen/tests/ui/{ => main}/missing-return.rs | 0 .../tests/ui/{ => main}/missing-return.stderr | 6 +- codegen/tests/ui/{ => main}/return-tuple.rs | 0 .../tests/ui/{ => main}/return-tuple.stderr | 6 +- 8 files changed, 405 insertions(+), 397 deletions(-) create mode 100644 codegen/src/main/mod.rs rename codegen/tests/ui/{ => main}/missing-attribute.rs (100%) rename codegen/tests/ui/{ => main}/missing-attribute.stderr (81%) rename codegen/tests/ui/{ => main}/missing-return.rs (100%) rename codegen/tests/ui/{ => main}/missing-return.stderr (78%) rename codegen/tests/ui/{ => main}/return-tuple.rs (100%) rename codegen/tests/ui/{ => main}/return-tuple.stderr (80%) diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index e877f8ac0..3541c3c41 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -1,392 +1,10 @@ +mod main; + use proc_macro::TokenStream; -use proc_macro_error::{emit_error, proc_macro_error}; -use quote::{quote, ToTokens}; -use syn::{ - parse_macro_input, parse_quote, spanned::Spanned, Attribute, FnArg, Ident, ItemFn, Pat, Path, - ReturnType, Signature, Stmt, Type, -}; +use proc_macro_error::proc_macro_error; #[proc_macro_error] #[proc_macro_attribute] -pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream { - let mut fn_decl = parse_macro_input!(item as ItemFn); - - let wrapper = Wrapper::from_item_fn(&mut fn_decl); - - let expanded = quote! { - #wrapper - - fn __binder( - service: Box, - addr: std::net::SocketAddr, - runtime: &shuttle_service::Runtime, - ) -> shuttle_service::ServeHandle { - runtime.spawn(async move { service.bind(addr).await }) - } - - #fn_decl - - #[no_mangle] - pub extern "C" fn _create_service() -> *mut shuttle_service::Bootstrapper { - let builder: shuttle_service::StateBuilder> = - |factory, runtime, logger| Box::pin(__shuttle_wrapper(factory, runtime, logger)); - - let bootstrapper = shuttle_service::Bootstrapper::new( - builder, - __binder, - shuttle_service::Runtime::new().unwrap(), - ); - - let boxed = Box::new(bootstrapper); - Box::into_raw(boxed) - } - }; - - expanded.into() -} - -struct Wrapper { - fn_ident: Ident, - fn_inputs: Vec, -} - -#[derive(Debug, PartialEq)] -struct Input { - /// The identifier for a resource input - ident: Ident, - - /// The shuttle_service path to the builder for this resource - builder: Path, -} - -impl Wrapper { - fn from_item_fn(item_fn: &mut ItemFn) -> Self { - let inputs: Vec<_> = item_fn - .sig - .inputs - .iter_mut() - .filter_map(|input| match input { - FnArg::Receiver(_) => None, - FnArg::Typed(typed) => Some(typed), - }) - .filter_map(|typed| match typed.pat.as_ref() { - Pat::Ident(ident) => Some((ident, typed.attrs.drain(..).collect())), - _ => None, - }) - .filter_map(|(pat_ident, attrs)| { - match attribute_to_path(attrs) { - Ok(builder) => Some(Input { - ident: pat_ident.ident.clone(), - builder, - }), - Err(err) => { - emit_error!(pat_ident, err; hint = pat_ident.span() => "Try adding a config like `#[shared::Postgres]`"); - None - } - } - }) - .collect(); - - check_return_type(&item_fn.sig); - - Self { - fn_ident: item_fn.sig.ident.clone(), - fn_inputs: inputs, - } - } -} - -fn check_return_type(signature: &Signature) { - match &signature.output { - ReturnType::Default => emit_error!( - signature, - "shuttle_service::main functions need to return a service"; - hint = "See the docs for services with first class support"; - doc = "https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services" - ), - ReturnType::Type(_, r#type) => match r#type.as_ref() { - Type::Path(_) => {} - _ => emit_error!( - r#type, - "shuttle_service::main functions need to return a first class service or 'Result"; - hint = "See the docs for services with first class support"; - doc = "https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services" - ), - }, - } -} - -fn attribute_to_path(attrs: Vec) -> Result { - if attrs.is_empty() { - return Err("resource needs an attribute configuration".to_string()); - } - - let builder = attrs[0].path.clone(); - - Ok(builder) -} - -impl ToTokens for Wrapper { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let fn_ident = &self.fn_ident; - let fn_inputs: Vec<_> = self.fn_inputs.iter().map(|i| i.ident.clone()).collect(); - let fn_inputs_builder: Vec<_> = self.fn_inputs.iter().map(|i| i.builder.clone()).collect(); - - let factory_ident: Ident = if self.fn_inputs.is_empty() { - parse_quote!(_factory) - } else { - parse_quote!(factory) - }; - - let extra_imports: Option = if self.fn_inputs.is_empty() { - None - } else { - Some(parse_quote!( - use shuttle_service::ResourceBuilder; - )) - }; - - let wrapper = quote! { - async fn __shuttle_wrapper( - #factory_ident: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: Box, - ) -> Result, shuttle_service::Error> { - #extra_imports - - runtime.spawn_blocking(move || { - shuttle_service::log::set_boxed_logger(logger) - .map(|()| shuttle_service::log::set_max_level(shuttle_service::log::LevelFilter::Info)) - .expect("logger set should succeed"); - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) - } - })?; - - - #(let #fn_inputs = shuttle_service::#fn_inputs_builder::new().build(#factory_ident, runtime).await?;)* - - runtime.spawn(async { - #fn_ident(#(#fn_inputs),*) - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) - } - })? - } - }; - - wrapper.to_tokens(tokens); - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - use quote::quote; - use syn::{parse_quote, Ident}; - - use crate::{Input, Wrapper}; - - #[test] - fn from_with_return() { - let mut input = parse_quote!( - async fn complex() -> ShuttleAxum {} - ); - - let actual = Wrapper::from_item_fn(&mut input); - let expected_ident: Ident = parse_quote!(complex); - - assert_eq!(actual.fn_ident, expected_ident); - assert_eq!(actual.fn_inputs, Vec::::new()); - } - - #[test] - fn output_with_return() { - let input = Wrapper { - fn_ident: parse_quote!(complex), - fn_inputs: Vec::new(), - }; - - let actual = quote!(#input); - let expected = quote! { - async fn __shuttle_wrapper( - _factory: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: Box, - ) -> Result, shuttle_service::Error> { - runtime.spawn_blocking(move || { - shuttle_service::log::set_boxed_logger(logger) - .map(|()| shuttle_service::log::set_max_level(shuttle_service::log::LevelFilter::Info)) - .expect("logger set should succeed"); - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) - } - })?; - - runtime.spawn(async { - complex() - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) - } - })? - } - }; - - assert_eq!(actual.to_string(), expected.to_string()); - } - - #[test] - fn from_with_inputs() { - let mut input = parse_quote!( - async fn complex(#[shared::Postgres] pool: PgPool) -> ShuttleTide {} - ); - - let actual = Wrapper::from_item_fn(&mut input); - let expected_ident: Ident = parse_quote!(complex); - let expected_inputs: Vec = vec![Input { - ident: parse_quote!(pool), - builder: parse_quote!(shared::Postgres), - }]; - - assert_eq!(actual.fn_ident, expected_ident); - assert_eq!(actual.fn_inputs, expected_inputs); - - // Make sure attributes was removed from input - if let syn::FnArg::Typed(param) = input.sig.inputs.first().unwrap() { - assert!( - param.attrs.is_empty(), - "some attributes were not removed: {:?}", - param.attrs - ); - } else { - panic!("expected first input to be typed") - } - } - - #[test] - fn output_with_inputs() { - let input = Wrapper { - fn_ident: parse_quote!(complex), - fn_inputs: vec![ - Input { - ident: parse_quote!(pool), - builder: parse_quote!(shared::Postgres), - }, - Input { - ident: parse_quote!(redis), - builder: parse_quote!(shared::Redis), - }, - ], - }; - - let actual = quote!(#input); - let expected = quote! { - async fn __shuttle_wrapper( - factory: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: Box, - ) -> Result, shuttle_service::Error> { - use shuttle_service::ResourceBuilder; - - runtime.spawn_blocking(move || { - shuttle_service::log::set_boxed_logger(logger) - .map(|()| shuttle_service::log::set_max_level(shuttle_service::log::LevelFilter::Info)) - .expect("logger set should succeed"); - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) - } - })?; - - let pool = shuttle_service::shared::Postgres::new().build(factory, runtime).await?; - let redis = shuttle_service::shared::Redis::new().build(factory, runtime).await?; - - runtime.spawn(async { - complex(pool, redis) - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) - } - })? - } - }; - - assert_eq!(actual.to_string(), expected.to_string()); - } - - #[test] - fn ui() { - let t = trybuild::TestCases::new(); - t.compile_fail("tests/ui/*.rs"); - } +pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream { + main::r#impl(attr, item) } diff --git a/codegen/src/main/mod.rs b/codegen/src/main/mod.rs new file mode 100644 index 000000000..11bd57fe5 --- /dev/null +++ b/codegen/src/main/mod.rs @@ -0,0 +1,390 @@ +use proc_macro::TokenStream; +use proc_macro_error::emit_error; +use quote::{quote, ToTokens}; +use syn::{ + parse_macro_input, parse_quote, spanned::Spanned, Attribute, FnArg, Ident, ItemFn, Pat, Path, + ReturnType, Signature, Stmt, Type, +}; + +pub(crate) fn r#impl(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut fn_decl = parse_macro_input!(item as ItemFn); + + let wrapper = Wrapper::from_item_fn(&mut fn_decl); + + let expanded = quote! { + #wrapper + + fn __binder( + service: Box, + addr: std::net::SocketAddr, + runtime: &shuttle_service::Runtime, + ) -> shuttle_service::ServeHandle { + runtime.spawn(async move { service.bind(addr).await }) + } + + #fn_decl + + #[no_mangle] + pub extern "C" fn _create_service() -> *mut shuttle_service::Bootstrapper { + let builder: shuttle_service::StateBuilder> = + |factory, runtime, logger| Box::pin(__shuttle_wrapper(factory, runtime, logger)); + + let bootstrapper = shuttle_service::Bootstrapper::new( + builder, + __binder, + shuttle_service::Runtime::new().unwrap(), + ); + + let boxed = Box::new(bootstrapper); + Box::into_raw(boxed) + } + }; + + expanded.into() +} + +struct Wrapper { + fn_ident: Ident, + fn_inputs: Vec, +} + +#[derive(Debug, PartialEq)] +struct Input { + /// The identifier for a resource input + ident: Ident, + + /// The shuttle_service path to the builder for this resource + builder: Path, +} + +impl Wrapper { + pub(crate) fn from_item_fn(item_fn: &mut ItemFn) -> Self { + let inputs: Vec<_> = item_fn + .sig + .inputs + .iter_mut() + .filter_map(|input| match input { + FnArg::Receiver(_) => None, + FnArg::Typed(typed) => Some(typed), + }) + .filter_map(|typed| match typed.pat.as_ref() { + Pat::Ident(ident) => Some((ident, typed.attrs.drain(..).collect())), + _ => None, + }) + .filter_map(|(pat_ident, attrs)| { + match attribute_to_path(attrs) { + Ok(builder) => Some(Input { + ident: pat_ident.ident.clone(), + builder, + }), + Err(err) => { + emit_error!(pat_ident, err; hint = pat_ident.span() => "Try adding a config like `#[shared::Postgres]`"); + None + } + } + }) + .collect(); + + check_return_type(&item_fn.sig); + + Self { + fn_ident: item_fn.sig.ident.clone(), + fn_inputs: inputs, + } + } +} + +fn check_return_type(signature: &Signature) { + match &signature.output { + ReturnType::Default => emit_error!( + signature, + "shuttle_service::main functions need to return a service"; + hint = "See the docs for services with first class support"; + doc = "https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services" + ), + ReturnType::Type(_, r#type) => match r#type.as_ref() { + Type::Path(_) => {} + _ => emit_error!( + r#type, + "shuttle_service::main functions need to return a first class service or 'Result"; + hint = "See the docs for services with first class support"; + doc = "https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services" + ), + }, + } +} + +fn attribute_to_path(attrs: Vec) -> Result { + if attrs.is_empty() { + return Err("resource needs an attribute configuration".to_string()); + } + + let builder = attrs[0].path.clone(); + + Ok(builder) +} + +impl ToTokens for Wrapper { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let fn_ident = &self.fn_ident; + let fn_inputs: Vec<_> = self.fn_inputs.iter().map(|i| i.ident.clone()).collect(); + let fn_inputs_builder: Vec<_> = self.fn_inputs.iter().map(|i| i.builder.clone()).collect(); + + let factory_ident: Ident = if self.fn_inputs.is_empty() { + parse_quote!(_factory) + } else { + parse_quote!(factory) + }; + + let extra_imports: Option = if self.fn_inputs.is_empty() { + None + } else { + Some(parse_quote!( + use shuttle_service::ResourceBuilder; + )) + }; + + let wrapper = quote! { + async fn __shuttle_wrapper( + #factory_ident: &mut dyn shuttle_service::Factory, + runtime: &shuttle_service::Runtime, + logger: Box, + ) -> Result, shuttle_service::Error> { + #extra_imports + + runtime.spawn_blocking(move || { + shuttle_service::log::set_boxed_logger(logger) + .map(|()| shuttle_service::log::set_max_level(shuttle_service::log::LevelFilter::Info)) + .expect("logger set should succeed"); + }) + .await + .map_err(|e| { + if e.is_panic() { + let mes = e + .into_panic() + .downcast_ref::<&str>() + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_string()); + + shuttle_service::Error::BuildPanic(mes) + } else { + shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) + } + })?; + + + #(let #fn_inputs = shuttle_service::#fn_inputs_builder::new().build(#factory_ident, runtime).await?;)* + + runtime.spawn(async { + #fn_ident(#(#fn_inputs),*) + .await + .map(|ok| Box::new(ok) as Box) + }) + .await + .map_err(|e| { + if e.is_panic() { + let mes = e + .into_panic() + .downcast_ref::<&str>() + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_string()); + + shuttle_service::Error::BuildPanic(mes) + } else { + shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) + } + })? + } + }; + + wrapper.to_tokens(tokens); + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use quote::quote; + use syn::{parse_quote, Ident}; + + use super::{Input, Wrapper}; + + #[test] + fn from_with_return() { + let mut input = parse_quote!( + async fn complex() -> ShuttleAxum {} + ); + + let actual = Wrapper::from_item_fn(&mut input); + let expected_ident: Ident = parse_quote!(complex); + + assert_eq!(actual.fn_ident, expected_ident); + assert_eq!(actual.fn_inputs, Vec::::new()); + } + + #[test] + fn output_with_return() { + let input = Wrapper { + fn_ident: parse_quote!(complex), + fn_inputs: Vec::new(), + }; + + let actual = quote!(#input); + let expected = quote! { + async fn __shuttle_wrapper( + _factory: &mut dyn shuttle_service::Factory, + runtime: &shuttle_service::Runtime, + logger: Box, + ) -> Result, shuttle_service::Error> { + runtime.spawn_blocking(move || { + shuttle_service::log::set_boxed_logger(logger) + .map(|()| shuttle_service::log::set_max_level(shuttle_service::log::LevelFilter::Info)) + .expect("logger set should succeed"); + }) + .await + .map_err(|e| { + if e.is_panic() { + let mes = e + .into_panic() + .downcast_ref::<&str>() + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_string()); + + shuttle_service::Error::BuildPanic(mes) + } else { + shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) + } + })?; + + runtime.spawn(async { + complex() + .await + .map(|ok| Box::new(ok) as Box) + }) + .await + .map_err(|e| { + if e.is_panic() { + let mes = e + .into_panic() + .downcast_ref::<&str>() + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_string()); + + shuttle_service::Error::BuildPanic(mes) + } else { + shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) + } + })? + } + }; + + assert_eq!(actual.to_string(), expected.to_string()); + } + + #[test] + fn from_with_inputs() { + let mut input = parse_quote!( + async fn complex(#[shared::Postgres] pool: PgPool) -> ShuttleTide {} + ); + + let actual = Wrapper::from_item_fn(&mut input); + let expected_ident: Ident = parse_quote!(complex); + let expected_inputs: Vec = vec![Input { + ident: parse_quote!(pool), + builder: parse_quote!(shared::Postgres), + }]; + + assert_eq!(actual.fn_ident, expected_ident); + assert_eq!(actual.fn_inputs, expected_inputs); + + // Make sure attributes was removed from input + if let syn::FnArg::Typed(param) = input.sig.inputs.first().unwrap() { + assert!( + param.attrs.is_empty(), + "some attributes were not removed: {:?}", + param.attrs + ); + } else { + panic!("expected first input to be typed") + } + } + + #[test] + fn output_with_inputs() { + let input = Wrapper { + fn_ident: parse_quote!(complex), + fn_inputs: vec![ + Input { + ident: parse_quote!(pool), + builder: parse_quote!(shared::Postgres), + }, + Input { + ident: parse_quote!(redis), + builder: parse_quote!(shared::Redis), + }, + ], + }; + + let actual = quote!(#input); + let expected = quote! { + async fn __shuttle_wrapper( + factory: &mut dyn shuttle_service::Factory, + runtime: &shuttle_service::Runtime, + logger: Box, + ) -> Result, shuttle_service::Error> { + use shuttle_service::ResourceBuilder; + + runtime.spawn_blocking(move || { + shuttle_service::log::set_boxed_logger(logger) + .map(|()| shuttle_service::log::set_max_level(shuttle_service::log::LevelFilter::Info)) + .expect("logger set should succeed"); + }) + .await + .map_err(|e| { + if e.is_panic() { + let mes = e + .into_panic() + .downcast_ref::<&str>() + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_string()); + + shuttle_service::Error::BuildPanic(mes) + } else { + shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) + } + })?; + + let pool = shuttle_service::shared::Postgres::new().build(factory, runtime).await?; + let redis = shuttle_service::shared::Redis::new().build(factory, runtime).await?; + + runtime.spawn(async { + complex(pool, redis) + .await + .map(|ok| Box::new(ok) as Box) + }) + .await + .map_err(|e| { + if e.is_panic() { + let mes = e + .into_panic() + .downcast_ref::<&str>() + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_string()); + + shuttle_service::Error::BuildPanic(mes) + } else { + shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e)) + } + })? + } + }; + + assert_eq!(actual.to_string(), expected.to_string()); + } + + #[test] + fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/main/*.rs"); + } +} diff --git a/codegen/tests/ui/missing-attribute.rs b/codegen/tests/ui/main/missing-attribute.rs similarity index 100% rename from codegen/tests/ui/missing-attribute.rs rename to codegen/tests/ui/main/missing-attribute.rs diff --git a/codegen/tests/ui/missing-attribute.stderr b/codegen/tests/ui/main/missing-attribute.stderr similarity index 81% rename from codegen/tests/ui/missing-attribute.stderr rename to codegen/tests/ui/main/missing-attribute.stderr index 7501308c5..d4d0c3af0 100644 --- a/codegen/tests/ui/missing-attribute.stderr +++ b/codegen/tests/ui/main/missing-attribute.stderr @@ -2,7 +2,7 @@ error: resource needs an attribute configuration = help: Try adding a config like `#[shared::Postgres]` - --> tests/ui/missing-attribute.rs:2:27 + --> tests/ui/main/missing-attribute.rs:2:27 | 2 | async fn missing_attriute(pool: PgPool, cache: Redis) -> ShuttleRocket {} | ^^^^ @@ -11,13 +11,13 @@ error: resource needs an attribute configuration = help: Try adding a config like `#[shared::Postgres]` - --> tests/ui/missing-attribute.rs:2:41 + --> tests/ui/main/missing-attribute.rs:2:41 | 2 | async fn missing_attriute(pool: PgPool, cache: Redis) -> ShuttleRocket {} | ^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/missing-attribute.rs:2:74 + --> tests/ui/main/missing-attribute.rs:2:74 | 2 | async fn missing_attriute(pool: PgPool, cache: Redis) -> ShuttleRocket {} - | ^ consider adding a `main` function to `$DIR/tests/ui/missing-attribute.rs` + | ^ consider adding a `main` function to `$DIR/tests/ui/main/missing-attribute.rs` diff --git a/codegen/tests/ui/missing-return.rs b/codegen/tests/ui/main/missing-return.rs similarity index 100% rename from codegen/tests/ui/missing-return.rs rename to codegen/tests/ui/main/missing-return.rs diff --git a/codegen/tests/ui/missing-return.stderr b/codegen/tests/ui/main/missing-return.stderr similarity index 78% rename from codegen/tests/ui/missing-return.stderr rename to codegen/tests/ui/main/missing-return.stderr index bd28227b6..e84ed9923 100644 --- a/codegen/tests/ui/missing-return.stderr +++ b/codegen/tests/ui/main/missing-return.stderr @@ -3,13 +3,13 @@ error: shuttle_service::main functions need to return a service = help: See the docs for services with first class support = note: https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services - --> tests/ui/missing-return.rs:2:1 + --> tests/ui/main/missing-return.rs:2:1 | 2 | async fn missing_return() {} | ^^^^^^^^^^^^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/missing-return.rs:2:29 + --> tests/ui/main/missing-return.rs:2:29 | 2 | async fn missing_return() {} - | ^ consider adding a `main` function to `$DIR/tests/ui/missing-return.rs` + | ^ consider adding a `main` function to `$DIR/tests/ui/main/missing-return.rs` diff --git a/codegen/tests/ui/return-tuple.rs b/codegen/tests/ui/main/return-tuple.rs similarity index 100% rename from codegen/tests/ui/return-tuple.rs rename to codegen/tests/ui/main/return-tuple.rs diff --git a/codegen/tests/ui/return-tuple.stderr b/codegen/tests/ui/main/return-tuple.stderr similarity index 80% rename from codegen/tests/ui/return-tuple.stderr rename to codegen/tests/ui/main/return-tuple.stderr index dd22b1a1c..b9fed7820 100644 --- a/codegen/tests/ui/return-tuple.stderr +++ b/codegen/tests/ui/main/return-tuple.stderr @@ -3,13 +3,13 @@ error: shuttle_service::main functions need to return a first class service or ' = help: See the docs for services with first class support = note: https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services - --> tests/ui/return-tuple.rs:2:28 + --> tests/ui/main/return-tuple.rs:2:28 | 2 | async fn return_tuple() -> (String, bool) {} | ^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` - --> tests/ui/return-tuple.rs:2:45 + --> tests/ui/main/return-tuple.rs:2:45 | 2 | async fn return_tuple() -> (String, bool) {} - | ^ consider adding a `main` function to `$DIR/tests/ui/return-tuple.rs` + | ^ consider adding a `main` function to `$DIR/tests/ui/main/return-tuple.rs` From 50e7cc2e29cd5f85498b89b3c555d24327cb3376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Tue, 26 Jul 2022 08:59:50 +0200 Subject: [PATCH 2/2] feat: poem-web integration (#275) * feat: poem-web integration implement Service for poem-web apps, add a hello-world example and an e2e integration test * feat: support cargo shuttle init --poem * feat: add postgres example * test: add integration tests in cargo-shuttle * ci: update CI to include poem feature * fix: rebase nits simplify bounds on impl Service for poem, bump poem docs version * refactor: sort Cargo.toml Co-authored-by: chesedo --- .github/workflows/fmt.yml | 2 +- .github/workflows/test.yml | 2 +- api/users.toml | 2 + cargo-shuttle/src/args.rs | 11 ++-- cargo-shuttle/src/init.rs | 83 ++++++++++++++++++++++++- cargo-shuttle/tests/integration/init.rs | 2 + cargo-shuttle/tests/integration/run.rs | 47 ++++++++++++++ e2e/tests/integration/main.rs | 1 + e2e/tests/integration/poem.rs | 57 +++++++++++++++++ examples/poem/hello-world/Cargo.toml | 10 +++ examples/poem/hello-world/Shuttle.toml | 1 + examples/poem/hello-world/src/lib.rs | 13 ++++ examples/poem/postgres/Cargo.toml | 12 ++++ examples/poem/postgres/Secrets.toml | 1 + examples/poem/postgres/Shuttle.toml | 1 + examples/poem/postgres/schema.sql | 6 ++ examples/poem/postgres/src/lib.rs | 68 ++++++++++++++++++++ service/Cargo.toml | 2 + service/src/lib.rs | 20 ++++++ 19 files changed, 334 insertions(+), 7 deletions(-) create mode 100644 e2e/tests/integration/poem.rs create mode 100644 examples/poem/hello-world/Cargo.toml create mode 100644 examples/poem/hello-world/Shuttle.toml create mode 100644 examples/poem/hello-world/src/lib.rs create mode 100644 examples/poem/postgres/Cargo.toml create mode 100644 examples/poem/postgres/Secrets.toml create mode 100644 examples/poem/postgres/Shuttle.toml create mode 100644 examples/poem/postgres/schema.sql create mode 100644 examples/poem/postgres/src/lib.rs diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index dfdf8108b..4b0177ed0 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -33,7 +33,7 @@ jobs: strategy: fail-fast: true matrix: - features: ["web-axum", "web-rocket", "web-tide", "web-tower"] + features: ["web-axum", "web-rocket", "web-tide", "web-tower", web-poem] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 835c4d989..0abeb1fc2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - features: ["web-axum", "web-rocket", "web-tide", "web-tower"] + features: ["web-axum", "web-rocket", "web-tide", "web-tower", "web-poem"] steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 diff --git a/api/users.toml b/api/users.toml index bdd29575e..dc2a69a26 100644 --- a/api/users.toml +++ b/api/users.toml @@ -9,4 +9,6 @@ projects = [ 'hello-world-tide-app', 'hello-world-tower-app', 'postgres-tide-app', + 'hello-world-poem-app', + 'postgres-poem-app' ] diff --git a/cargo-shuttle/src/args.rs b/cargo-shuttle/src/args.rs index 3109c2955..8d55ed759 100644 --- a/cargo-shuttle/src/args.rs +++ b/cargo-shuttle/src/args.rs @@ -98,17 +98,20 @@ pub struct RunArgs { #[derive(Parser, Debug)] pub struct InitArgs { /// Initialize with axum framework - #[clap(long, conflicts_with_all = &["rocket", "tide", "tower"])] + #[clap(long, conflicts_with_all = &["rocket", "tide", "tower", "poem"])] pub axum: bool, /// Initialize with actix-web framework - #[clap(long, conflicts_with_all = &["axum", "tide", "tower"])] + #[clap(long, conflicts_with_all = &["axum", "tide", "tower", "poem"])] pub rocket: bool, /// Initialize with tide framework - #[clap(long, conflicts_with_all = &["axum", "rocket", "tower"])] + #[clap(long, conflicts_with_all = &["axum", "rocket", "tower", "poem"])] pub tide: bool, /// Initialize with tower framework - #[clap(long, conflicts_with_all = &["axum", "rocket", "tide"])] + #[clap(long, conflicts_with_all = &["axum", "rocket", "tide", "poem"])] pub tower: bool, + /// Initialize with poem framework + #[clap(long, conflicts_with_all = &["axum", "rocket", "tide", "tower"])] + pub poem: bool, /// Path to initialize a new shuttle project #[clap( parse(try_from_os_str = parse_init_path), diff --git a/cargo-shuttle/src/init.rs b/cargo-shuttle/src/init.rs index ada99a893..a9dc98c5e 100644 --- a/cargo-shuttle/src/init.rs +++ b/cargo-shuttle/src/init.rs @@ -155,6 +155,49 @@ impl ShuttleInit for ShuttleInitTide { } } +pub struct ShuttleInitPoem; + +impl ShuttleInit for ShuttleInitPoem { + fn set_cargo_dependencies( + &self, + dependencies: &mut Table, + manifest_path: &Path, + url: &Url, + get_dependency_version_fn: GetDependencyVersionFn, + ) { + set_inline_table_dependency_features( + "shuttle-service", + dependencies, + vec!["web-poem".to_string()], + ); + + set_key_value_dependency_version( + "poem", + dependencies, + manifest_path, + url, + get_dependency_version_fn, + ); + } + + fn get_boilerplate_code_for_framework(&self) -> &'static str { + indoc! {r#" + use poem::{get, handler, Route}; + + #[handler] + fn hello_world() -> &'static str { + "Hello, world!" + } + + #[shuttle_service::main] + async fn poem() -> shuttle_service::ShuttlePoem { + let app = Route::new().at("/hello", get(hello_world)); + + Ok(app) + }"#} + } +} + pub struct ShuttleInitTower; impl ShuttleInit for ShuttleInitTower { @@ -267,6 +310,10 @@ pub fn get_framework(init_args: &InitArgs) -> Box { return Box::new(ShuttleInitTower); } + if init_args.poem { + return Box::new(ShuttleInitPoem); + } + Box::new(ShuttleInitNoOp) } @@ -405,6 +452,7 @@ mod shuttle_init_tests { rocket: false, tide: false, tower: false, + poem: false, path: PathBuf::new(), }; @@ -413,6 +461,7 @@ mod shuttle_init_tests { "rocket" => init_args.rocket = true, "tide" => init_args.tide = true, "tower" => init_args.tower = true, + "poem" => init_args.poem = true, _ => unreachable!(), } @@ -437,12 +486,13 @@ mod shuttle_init_tests { #[test] fn test_get_framework_via_get_boilerplate_code() { - let frameworks = vec!["axum", "rocket", "tide", "tower"]; + let frameworks = vec!["axum", "rocket", "tide", "tower", "poem"]; let framework_inits: Vec> = vec![ Box::new(ShuttleInitAxum), Box::new(ShuttleInitRocket), Box::new(ShuttleInitTide), Box::new(ShuttleInitTower), + Box::new(ShuttleInitPoem), ]; for (framework, expected_framework_init) in frameworks.into_iter().zip(framework_inits) { @@ -644,4 +694,35 @@ mod shuttle_init_tests { assert_eq!(cargo_toml.to_string(), expected); } + + #[test] + fn test_set_cargo_dependencies_poem() { + let mut cargo_toml = cargo_toml_factory(); + let dependencies = cargo_toml["dependencies"].as_table_mut().unwrap(); + let manifest_path = PathBuf::new(); + let url = Url::parse("https://shuttle.rs").unwrap(); + + set_inline_table_dependency_version( + "shuttle-service", + dependencies, + &manifest_path, + &url, + mock_get_latest_dependency_version, + ); + + ShuttleInitPoem.set_cargo_dependencies( + dependencies, + &manifest_path, + &url, + mock_get_latest_dependency_version, + ); + + let expected = indoc! {r#" + [dependencies] + shuttle-service = { version = "1.0", features = ["web-poem"] } + poem = "1.0" + "#}; + + assert_eq!(cargo_toml.to_string(), expected); + } } diff --git a/cargo-shuttle/tests/integration/init.rs b/cargo-shuttle/tests/integration/init.rs index 4ef5925a6..c53dd31b2 100644 --- a/cargo-shuttle/tests/integration/init.rs +++ b/cargo-shuttle/tests/integration/init.rs @@ -23,6 +23,7 @@ async fn cargo_shuttle_init(path: PathBuf) -> anyhow::Result { rocket: false, tide: false, tower: false, + poem: false, path, }), }) @@ -45,6 +46,7 @@ async fn cargo_shuttle_init_framework(path: PathBuf) -> anyhow::Result &'static str { + "Hello, world!" +} + +#[shuttle_service::main] +async fn main() -> shuttle_service::ShuttlePoem { + let app = Route::new().at("/hello", get(hello_world)); + + Ok(app) +} diff --git a/examples/poem/postgres/Cargo.toml b/examples/poem/postgres/Cargo.toml new file mode 100644 index 000000000..33210f794 --- /dev/null +++ b/examples/poem/postgres/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "postgres" +version = "0.1.0" +edition = "2021" + +[lib] + +[dependencies] +poem = "1.3.35" +serde = "1.0" +sqlx = { version = "0.5", features = ["runtime-tokio-native-tls", "postgres"] } +shuttle-service = { version = "0.4.0", features = ["sqlx-postgres", "secrets", "web-poem"] } diff --git a/examples/poem/postgres/Secrets.toml b/examples/poem/postgres/Secrets.toml new file mode 100644 index 000000000..ceedf199e --- /dev/null +++ b/examples/poem/postgres/Secrets.toml @@ -0,0 +1 @@ +MY_API_KEY = 'the contents of my API key' diff --git a/examples/poem/postgres/Shuttle.toml b/examples/poem/postgres/Shuttle.toml new file mode 100644 index 000000000..33ccd0784 --- /dev/null +++ b/examples/poem/postgres/Shuttle.toml @@ -0,0 +1 @@ +name = "postgres-poem-app" diff --git a/examples/poem/postgres/schema.sql b/examples/poem/postgres/schema.sql new file mode 100644 index 000000000..460e7c23d --- /dev/null +++ b/examples/poem/postgres/schema.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS todos; + +CREATE TABLE todos ( + id serial PRIMARY KEY, + note TEXT NOT NULL +); diff --git a/examples/poem/postgres/src/lib.rs b/examples/poem/postgres/src/lib.rs new file mode 100644 index 000000000..5cb35c00f --- /dev/null +++ b/examples/poem/postgres/src/lib.rs @@ -0,0 +1,68 @@ +use poem::{ + error::BadRequest, + get, handler, + middleware::AddData, + post, + web::{Data, Json, Path}, + EndpointExt, Result, Route, +}; +use serde::{Deserialize, Serialize}; +use shuttle_service::error::CustomError; +use shuttle_service::SecretStore; +use sqlx::{Executor, FromRow, PgPool}; + +#[handler] +async fn retrieve(Path(id): Path, state: Data<&PgPool>) -> Result> { + let todo = sqlx::query_as("SELECT * FROM todos WHERE id = $1") + .bind(id) + .fetch_one(state.0) + .await + .map_err(BadRequest)?; + + Ok(Json(todo)) +} + +#[handler] +async fn add(data: Json, state: Data<&PgPool>) -> Result> { + let todo = sqlx::query_as("INSERT INTO todos(note) VALUES ($1) RETURNING id, note") + .bind(&data.note) + .fetch_one(state.0) + .await + .map_err(BadRequest)?; + + Ok(Json(todo)) +} + +#[handler] +async fn secret(state: Data<&PgPool>) -> Result { + // get secret defined in `Secrets.toml` file. + state.0.get_secret("MY_API_KEY").await.map_err(BadRequest) +} + +#[shuttle_service::main] +async fn main( + #[shared::Postgres] pool: PgPool, +) -> shuttle_service::ShuttlePoem { + pool.execute(include_str!("../schema.sql")) + .await + .map_err(CustomError::new)?; + + let app = Route::new() + .at("/secret", get(secret)) + .at("/todo", post(add)) + .at("/todo/:id", get(retrieve)) + .with(AddData::new(pool)); + + Ok(app) +} + +#[derive(Deserialize)] +struct TodoNew { + pub note: String, +} + +#[derive(Serialize, FromRow)] +struct Todo { + pub id: i32, + pub note: String, +} diff --git a/service/Cargo.toml b/service/Cargo.toml index 62c217f8f..c59159a31 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -20,6 +20,7 @@ lazy_static = "1.4.0" libloading = { version = "0.7.3", optional = true } log = "0.4.17" paste = "1.0.7" +poem = { version = "1.3.35", optional = true } regex = "1.5.6" rocket = { version = "0.5.0-rc.2", optional = true } sqlx = { version = "0.5.13", optional = true } @@ -65,3 +66,4 @@ web-axum = ["axum", "sync_wrapper"] web-rocket = ["rocket"] web-tide = ["tide"] web-tower = ["tower", "hyper"] +web-poem = ["poem"] diff --git a/service/src/lib.rs b/service/src/lib.rs index 626d3075a..3ede8d8cc 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -263,6 +263,7 @@ extern crate shuttle_codegen; /// | `ShuttleRocket` | web-rocket | [rocket](https://docs.rs/rocket/0.5.0-rc.2) | 0.5.0-rc.2 | [GitHub](https://github.com/getsynth/shuttle/tree/main/examples/rocket/hello-world) | /// | `ShuttleAxum` | web-axum | [axum](https://docs.rs/axum/0.5) | 0.5 | [GitHub](https://github.com/getsynth/shuttle/tree/main/examples/axum/hello-world) | /// | `ShuttleTide` | web-tide | [tide](https://docs.rs/tide/0.16.0) | 0.16.0 | [GitHub](https://github.com/getsynth/shuttle/tree/main/examples/tide/hello-world) | +/// | `ShuttlePoem` | web-poem | [poem](https://docs.rs/poem/1.3.35) | 1.3.35 | [GitHub](https://github.com/getsynth/shuttle/tree/main/examples/poem/hello-world) | /// | `Result` | web-tower | [tower](https://docs.rs/tower/0.4.12) | 0.14.12 | [GitHub](https://github.com/getsynth/shuttle/tree/main/examples/tower/hello-world) | /// /// # Getting shuttle managed services @@ -437,6 +438,25 @@ impl Service for rocket::Rocket { #[cfg(feature = "web-rocket")] pub type ShuttleRocket = Result, Error>; +#[cfg(feature = "web-poem")] +#[async_trait] +impl Service for T +where + T: poem::Endpoint + Sync + Send + 'static, +{ + async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { + poem::Server::new(poem::listener::TcpListener::bind(addr)) + .run(self) + .await + .map_err(error::CustomError::new)?; + + Ok(()) + } +} + +#[cfg(feature = "web-poem")] +pub type ShuttlePoem = Result; + #[cfg(feature = "web-axum")] #[async_trait] impl Service for sync_wrapper::SyncWrapper {