Skip to content

initial attempt to support rama in shuttle #1943

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 20, 2025
Merged
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -388,6 +388,7 @@ workflows:
- services/shuttle-actix-web
- services/shuttle-axum
- services/shuttle-poem
- services/shuttle-rama
- services/shuttle-rocket
- services/shuttle-salvo
- services/shuttle-serenity
@@ -558,6 +559,7 @@ workflows:
- services/shuttle-actix-web
- services/shuttle-axum
- services/shuttle-poem
- services/shuttle-rama
- services/shuttle-rocket
- services/shuttle-salvo
- services/shuttle-serenity
1 change: 1 addition & 0 deletions codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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) | [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) | [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) | [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) | [GitHub](https://github.com/shuttle-hq/shuttle-examples/tree/main/rama/hello-world) |
/// | `ShuttleRocket` | [shuttle-rocket](https://crates.io/crates/shuttle-rocket) | [rocket](https://docs.rs/rocket) | [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) | [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) and [poise](https://docs.rs/poise) | [GitHub](https://github.com/shuttle-hq/shuttle-examples/tree/main/serenity/hello-world) |
15 changes: 15 additions & 0 deletions services/shuttle-rama/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "shuttle-rama"
version = "0.54.0"
edition = "2024"
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", features = ["tcp", "http-full"] }
shuttle-runtime = { path = "../../runtime", version = "0.54.0", default-features = false }

[features]
default = []
90 changes: 90 additions & 0 deletions services/shuttle-rama/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
## Shuttle service integration for the Rama framework

Learn more about rama at <https://ramaproxy.org/> and see more [Rama v0.2] examples
at <https://github.com/plabayo/rama/tree/rama-0.2.0/examples>.

[Rama]: https://github.com/plabayo/rama

### Examples

#### Application Service

```rust,ignore
use rama::{
Context, Layer,
error::ErrorContext,
http::{
StatusCode,
layer::forwarded::GetForwardedHeaderLayer,
service::web::{Router, response::Result},
},
net::forwarded::Forwarded,
};

async fn hello_world(ctx: Context<()>) -> Result<String> {
Ok(match ctx.get::<Forwarded>() {
Some(forwarded) => format!(
"Hello cloud user @ {}!",
forwarded
.client_ip()
.context("missing IP information from user")
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?
),
None => "Hello local user! Are you developing?".to_owned(),
})
}

#[shuttle_runtime::main]
async fn main() -> Result<impl shuttle_rama::ShuttleService, shuttle_rama::ShuttleError> {
let router = Router::new().get("/", hello_world);

let app =
// Shuttle sits behind a load-balancer,
// so in case you want the real IP of the user,
// you need to ensure this headers is handled.
//
// Learn more at <https://docs.shuttle.dev/docs/deployment-environment#https-traffic>
GetForwardedHeaderLayer::x_forwarded_for().into_layer(router);

Ok(shuttle_rama::RamaService::application(app))
}
```

#### Transport Service

```rust,ignore
use rama::{net, service::service_fn};
use std::convert::Infallible;
use tokio::io::AsyncWriteExt;

async fn hello_world<S>(mut stream: S) -> Result<(), Infallible>
where
S: net::stream::Stream + Unpin,
{
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<impl shuttle_rama::ShuttleService, shuttle_rama::ShuttleError> {
Ok(shuttle_rama::RamaService::transport(service_fn(
hello_world,
)))
}
```
142 changes: 142 additions & 0 deletions services/shuttle-rama/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#![doc = include_str!("../README.md")]

use rama::{
Service,
error::OpaqueError,
http::{Request, server::HttpServer, service::web::response::IntoResponse},
tcp::server::TcpListener,
};
use shuttle_runtime::{CustomError, Error, tokio};
use std::{convert::Infallible, fmt, net::SocketAddr};

/// A wrapper type for [`Service`] so we can implement [`shuttle_runtime::Service`] for it.
pub struct RamaService<T, State> {
svc: T,
state: State,
}

impl<T: Clone, State: Clone> Clone for RamaService<T, State> {
fn clone(&self) -> Self {
Self {
svc: self.svc.clone(),
state: self.state.clone(),
}
}
}

impl<T: fmt::Debug, State: fmt::Debug> fmt::Debug for RamaService<T, State> {
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>(S);

/// Private type wrapper to indicate [`RamaService`]
/// is used by the user from the Application layer (http(s)).
pub struct Application<S>(S);

macro_rules! impl_wrapper_derive_traits {
($name:ident) => {
impl<S: Clone> Clone for $name<S> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}

impl<S: fmt::Debug> fmt::Debug for $name<S> {
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<S> RamaService<Transport<S>, ()> {
pub fn transport(svc: S) -> Self {
Self {
svc: Transport(svc),
state: (),
}
}
}

impl<S> RamaService<Application<S>, ()> {
pub fn application(svc: S) -> Self {
Self {
svc: Application(svc),
state: (),
}
}
}

impl<T> RamaService<T, ()> {
/// Attach state to this [`RamaService`], such that it will be passed
/// as part of each request's [`Context`].
///
/// [`Context`]: rama::Context
pub fn with_state<State>(self, state: State) -> RamaService<T, State>
where
State: Clone + Send + Sync + 'static,
{
RamaService {
svc: self.svc,
state,
}
}
}

#[shuttle_runtime::async_trait]
impl<S, State> shuttle_runtime::Service for RamaService<Transport<S>, State>
where
S: Service<State, tokio::net::TcpStream>,
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> {
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<S, State, Response> shuttle_runtime::Service for RamaService<Application<S>, State>
where
S: Service<State, Request, Response = Response, Error = Infallible>,
Response: 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> {
// shuttle only supports h1 between load balancer <=> web service,
// h2 is terminated by shuttle's load balancer
HttpServer::http1()
.listen_with_state(self.state, addr, self.svc.0)
.await
.map_err(|err| CustomError::new(OpaqueError::from_boxed(err)))?;
Ok(())
}
}

#[doc = include_str!("../README.md")]
pub type ShuttleRamaTransport<S, State = ()> = Result<RamaService<Transport<S>, State>, Error>;

#[doc = include_str!("../README.md")]
pub type ShuttleRamaApplication<S, State = ()> = Result<RamaService<Application<S>, State>, Error>;

pub use shuttle_runtime::{Error as ShuttleError, Service as ShuttleService};