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
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
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.49.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.5", features = ["tcp", "http-full"] }
shuttle-runtime = { path = "../../runtime", version = "0.49.0", default-features = false }

[features]
default = []
67 changes: 67 additions & 0 deletions services/shuttle-rama/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
## 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::{http, Service, service::service_fn};

async fn hello_world() -> &'static str {
"Hello, world!"
}

#[shuttle_runtime::main]
async fn main() -> Result<impl shuttle_rama::ShuttleService, shuttle_rama::ShuttleError> {
Ok(shuttle_rama::RamaService::application(
service_fn(hello_world),
))
}
```

#### Transport Service

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

async fn hello_world(mut stream: impl net::stream::Socket + net::Stream + Unpin) -> Result<(), Infallible> {
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<impl shuttle_rama::ShuttleService, shuttle_rama::ShuttleError> {
Ok(shuttle_rama::RamaService::transport(
service_fn(hello_world),
))
}
```
132 changes: 132 additions & 0 deletions services/shuttle-rama/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<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 [`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: rama::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> {
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<S, State, Response> shuttle_runtime::Service for RamaService<Application<S>, State>
where
S: rama::Service<State, rama::http::Request, Response = Response, Error = Infallible>,
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<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};