diff --git a/.circleci/config.yml b/.circleci/config.yml index 0fbf817c5..3c433850a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -538,6 +538,7 @@ workflows: - services/shuttle-actix-web - services/shuttle-axum - services/shuttle-poem + - services/shuttle-rama - services/shuttle-rocket - services/shuttle-salvo - services/shuttle-serenity @@ -801,6 +802,7 @@ workflows: - services/shuttle-actix-web - services/shuttle-axum - services/shuttle-poem + - services/shuttle-rama - services/shuttle-rocket - services/shuttle-salvo - services/shuttle-serenity diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index e470f7a0e..9738b35f0 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -24,6 +24,7 @@ mod shuttle_main; /// | `ShuttleActixWeb` | [shuttle-actix-web](https://crates.io/crates/shuttle-actix-web)| [actix-web](https://docs.rs/actix-web/4.3) | 4.3 | [GitHub](https://github.com/shuttle-hq/shuttle-examples/tree/main/actix-web/hello-world)| /// | `ShuttleAxum` | [shuttle-axum](https://crates.io/crates/shuttle-axum) | [axum](https://docs.rs/axum/0.7) | 0.7 | [GitHub](https://github.com/shuttle-hq/shuttle-examples/tree/main/axum/hello-world) | /// | `ShuttlePoem` | [shuttle-poem](https://crates.io/crates/shuttle-poem) | [poem](https://docs.rs/poem/2.0) | 2.0 | [GitHub](https://github.com/shuttle-hq/shuttle-examples/tree/main/poem/hello-world) | +/// | `ShuttleRama` | [shuttle-rama](https://crates.io/crates/shuttle-rama) | [rama](https://docs.rs/rama/0.2.0-alpha.5) /// | `ShuttleRocket` | [shuttle-rocket](https://crates.io/crates/shuttle-rocket) | [rocket](https://docs.rs/rocket/0.5) | 0.5 | [GitHub](https://github.com/shuttle-hq/shuttle-examples/tree/main/rocket/hello-world) | /// | `ShuttleSalvo` | [shuttle-salvo](https://crates.io/crates/shuttle-salvo) | [salvo](https://docs.rs/salvo/0.63) | 0.63 | [GitHub](https://github.com/shuttle-hq/shuttle-examples/tree/main/salvo/hello-world) | /// | `ShuttleSerenity` | [shuttle-serenity](https://crates.io/crates/shuttle-serenity) | [serenity](https://docs.rs/serenity/0.12) and [poise](https://docs.rs/poise/0.6) | 0.12 | [GitHub](https://github.com/shuttle-hq/shuttle-examples/tree/main/serenity/hello-world) | diff --git a/services/shuttle-rama/Cargo.toml b/services/shuttle-rama/Cargo.toml new file mode 100644 index 000000000..a16aaea60 --- /dev/null +++ b/services/shuttle-rama/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "shuttle-rama" +version = "0.50.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a rama server on shuttle" +repository = "https://github.com/shuttle-hq/shuttle" +keywords = ["shuttle-service", "rama"] + +[dependencies] +rama = { version = "0.2.0-alpha.6", features = ["tcp", "http-full"] } +shuttle-runtime = { path = "../../runtime", version = "0.50.0", default-features = false } + +[features] +default = [] diff --git a/services/shuttle-rama/README.md b/services/shuttle-rama/README.md new file mode 100644 index 000000000..d26b392b8 --- /dev/null +++ b/services/shuttle-rama/README.md @@ -0,0 +1,71 @@ +## Shuttle service integration for the Rama framework + +Rama is still in early development and for now the latest +alpha release is used, `0.2.0-alpha.5`. + +### Examples + +#### Application Service + +```rust,ignore +use rama::service::service_fn; +use std::convert::Infallible; + +async fn hello_world() -> Result<&'static str, Infallible> { + Ok("Hello, world!") +} + +#[shuttle_runtime::main] +async fn main() -> Result { + Ok(shuttle_rama::RamaService::application( + service_fn(hello_world), + )) +} +``` + +#### Transport Service + +```rust,ignore +use rama::{net, service::service_fn}; +use std::convert::Infallible; +use tokio::io::AsyncWriteExt; + +async fn hello_world(mut stream: S) -> Result<(), Infallible> +where + S: net::stream::Socket + net::stream::Stream + Unpin, +{ + println!( + "Incoming connection from: {}", + stream + .peer_addr() + .map(|a| a.to_string()) + .unwrap_or_else(|_| "???".to_owned()) + ); + + const TEXT: &str = "Hello, Shuttle!"; + + let resp = [ + "HTTP/1.1 200 OK", + "Content-Type: text/plain", + format!("Content-Length: {}", TEXT.len()).as_str(), + "", + TEXT, + "", + ] + .join("\r\n"); + + stream + .write_all(resp.as_bytes()) + .await + .expect("write to stream"); + + Ok::<_, std::convert::Infallible>(()) +} + +#[shuttle_runtime::main] +async fn main() -> Result { + Ok(shuttle_rama::RamaService::transport(service_fn( + hello_world, + ))) +} +``` diff --git a/services/shuttle-rama/src/lib.rs b/services/shuttle-rama/src/lib.rs new file mode 100644 index 000000000..d76baa68f --- /dev/null +++ b/services/shuttle-rama/src/lib.rs @@ -0,0 +1,132 @@ +#![doc = include_str!("../README.md")] + +use shuttle_runtime::{tokio, CustomError, Error}; +use std::{convert::Infallible, fmt, net::SocketAddr}; + +/// A wrapper type for [rama::Service] so we can implement [shuttle_runtime::Service] for it. +pub struct RamaService { + svc: T, + state: State, +} + +impl Clone for RamaService { + fn clone(&self) -> Self { + Self { + svc: self.svc.clone(), + state: self.state.clone(), + } + } +} + +impl fmt::Debug for RamaService { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RamaService") + .field("svc", &self.svc) + .field("state", &self.state) + .finish() + } +} + +/// Private type wrapper to indicate [`RamaService`] +/// is used by the user from the Transport layer (tcp). +pub struct Transport(S); + +/// Private type wrapper to indicate [`RamaService`] +/// is used by the user from the Application layer (http(s)). +pub struct Application(S); + +macro_rules! impl_wrapper_derive_traits { + ($name:ident) => { + impl Clone for $name { + fn clone(&self) -> Self { + Self(self.0.clone()) + } + } + + impl fmt::Debug for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple(stringify!($name)).field(&self.0).finish() + } + } + }; +} + +impl_wrapper_derive_traits!(Transport); +impl_wrapper_derive_traits!(Application); + +impl RamaService, ()> { + pub fn transport(svc: S) -> Self { + Self { + svc: Transport(svc), + state: (), + } + } +} + +impl RamaService, ()> { + pub fn application(svc: S) -> Self { + Self { + svc: Application(svc), + state: (), + } + } +} + +impl RamaService { + /// Attach state to this [`RamaService`], such that it will be passed + /// as part of each request's [`rama::Context`]. + pub fn with_state(self, state: State) -> RamaService + where + State: Clone + Send + Sync + 'static, + { + RamaService { + svc: self.svc, + state, + } + } +} + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for RamaService, State> +where + S: rama::Service, + State: Clone + Send + Sync + 'static, +{ + /// Takes the service that is returned by the user in their [shuttle_runtime::main] function + /// and binds to an address passed in by shuttle. + async fn bind(self, addr: SocketAddr) -> Result<(), Error> { + rama::tcp::server::TcpListener::build_with_state(self.state) + .bind(addr) + .await + .map_err(|err| Error::BindPanic(err.to_string()))? + .serve(self.svc.0) + .await; + Ok(()) + } +} + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for RamaService, State> +where + S: rama::Service, + Response: rama::http::IntoResponse + Send + 'static, + State: Clone + Send + Sync + 'static, +{ + /// Takes the service that is returned by the user in their [shuttle_runtime::main] function + /// and binds to an address passed in by shuttle. + async fn bind(self, addr: SocketAddr) -> Result<(), Error> { + rama::http::server::HttpServer::auto(rama::rt::Executor::new()) + .listen_with_state(self.state, addr, self.svc.0) + .await + .map_err(|err| CustomError::new(rama::error::OpaqueError::from_boxed(err)))?; + Ok(()) + } +} + +#[doc = include_str!("../README.md")] +pub type ShuttleRamaTransport = Result, State>, Error>; + +#[doc = include_str!("../README.md")] +pub type ShuttleRamaApplication = Result, State>, Error>; + +pub use shuttle_runtime::{Error as ShuttleError, Service as ShuttleService};