Skip to content

Commit 8e055e1

Browse files
authored
feat(services): shuttle-rama service integration (#1943)
1 parent 454dc1e commit 8e055e1

File tree

5 files changed

+250
-0
lines changed

5 files changed

+250
-0
lines changed

.circleci/config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ workflows:
388388
- services/shuttle-actix-web
389389
- services/shuttle-axum
390390
- services/shuttle-poem
391+
- services/shuttle-rama
391392
- services/shuttle-rocket
392393
- services/shuttle-salvo
393394
- services/shuttle-serenity
@@ -558,6 +559,7 @@ workflows:
558559
- services/shuttle-actix-web
559560
- services/shuttle-axum
560561
- services/shuttle-poem
562+
- services/shuttle-rama
561563
- services/shuttle-rocket
562564
- services/shuttle-salvo
563565
- services/shuttle-serenity

codegen/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod shuttle_main;
2424
/// | `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)|
2525
/// | `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) |
2626
/// | `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) |
27+
/// | `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) |
2728
/// | `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) |
2829
/// | `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) |
2930
/// | `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) |

services/shuttle-rama/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "shuttle-rama"
3+
version = "0.54.0"
4+
edition = "2024"
5+
license = "Apache-2.0"
6+
description = "Service implementation to run a rama server on shuttle"
7+
repository = "https://github.com/shuttle-hq/shuttle"
8+
keywords = ["shuttle-service", "rama"]
9+
10+
[dependencies]
11+
rama = { version = "0.2", features = ["tcp", "http-full"] }
12+
shuttle-runtime = { path = "../../runtime", version = "0.54.0", default-features = false }
13+
14+
[features]
15+
default = []

services/shuttle-rama/README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
## Shuttle service integration for the Rama framework
2+
3+
Learn more about rama at <https://ramaproxy.org/> and see more [Rama v0.2] examples
4+
at <https://github.com/plabayo/rama/tree/rama-0.2.0/examples>.
5+
6+
[Rama]: https://github.com/plabayo/rama
7+
8+
### Examples
9+
10+
#### Application Service
11+
12+
```rust,ignore
13+
use rama::{
14+
Context, Layer,
15+
error::ErrorContext,
16+
http::{
17+
StatusCode,
18+
layer::forwarded::GetForwardedHeaderLayer,
19+
service::web::{Router, response::Result},
20+
},
21+
net::forwarded::Forwarded,
22+
};
23+
24+
async fn hello_world(ctx: Context<()>) -> Result<String> {
25+
Ok(match ctx.get::<Forwarded>() {
26+
Some(forwarded) => format!(
27+
"Hello cloud user @ {}!",
28+
forwarded
29+
.client_ip()
30+
.context("missing IP information from user")
31+
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?
32+
),
33+
None => "Hello local user! Are you developing?".to_owned(),
34+
})
35+
}
36+
37+
#[shuttle_runtime::main]
38+
async fn main() -> Result<impl shuttle_rama::ShuttleService, shuttle_rama::ShuttleError> {
39+
let router = Router::new().get("/", hello_world);
40+
41+
let app =
42+
// Shuttle sits behind a load-balancer,
43+
// so in case you want the real IP of the user,
44+
// you need to ensure this headers is handled.
45+
//
46+
// Learn more at <https://docs.shuttle.dev/docs/deployment-environment#https-traffic>
47+
GetForwardedHeaderLayer::x_forwarded_for().into_layer(router);
48+
49+
Ok(shuttle_rama::RamaService::application(app))
50+
}
51+
```
52+
53+
#### Transport Service
54+
55+
```rust,ignore
56+
use rama::{net, service::service_fn};
57+
use std::convert::Infallible;
58+
use tokio::io::AsyncWriteExt;
59+
60+
async fn hello_world<S>(mut stream: S) -> Result<(), Infallible>
61+
where
62+
S: net::stream::Stream + Unpin,
63+
{
64+
const TEXT: &str = "Hello, Shuttle!";
65+
66+
let resp = [
67+
"HTTP/1.1 200 OK",
68+
"Content-Type: text/plain",
69+
format!("Content-Length: {}", TEXT.len()).as_str(),
70+
"",
71+
TEXT,
72+
"",
73+
]
74+
.join("\r\n");
75+
76+
stream
77+
.write_all(resp.as_bytes())
78+
.await
79+
.expect("write to stream");
80+
81+
Ok::<_, std::convert::Infallible>(())
82+
}
83+
84+
#[shuttle_runtime::main]
85+
async fn main() -> Result<impl shuttle_rama::ShuttleService, shuttle_rama::ShuttleError> {
86+
Ok(shuttle_rama::RamaService::transport(service_fn(
87+
hello_world,
88+
)))
89+
}
90+
```

services/shuttle-rama/src/lib.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#![doc = include_str!("../README.md")]
2+
3+
use rama::{
4+
Service,
5+
error::OpaqueError,
6+
http::{Request, server::HttpServer, service::web::response::IntoResponse},
7+
tcp::server::TcpListener,
8+
};
9+
use shuttle_runtime::{CustomError, Error, tokio};
10+
use std::{convert::Infallible, fmt, net::SocketAddr};
11+
12+
/// A wrapper type for [`Service`] so we can implement [`shuttle_runtime::Service`] for it.
13+
pub struct RamaService<T, State> {
14+
svc: T,
15+
state: State,
16+
}
17+
18+
impl<T: Clone, State: Clone> Clone for RamaService<T, State> {
19+
fn clone(&self) -> Self {
20+
Self {
21+
svc: self.svc.clone(),
22+
state: self.state.clone(),
23+
}
24+
}
25+
}
26+
27+
impl<T: fmt::Debug, State: fmt::Debug> fmt::Debug for RamaService<T, State> {
28+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29+
f.debug_struct("RamaService")
30+
.field("svc", &self.svc)
31+
.field("state", &self.state)
32+
.finish()
33+
}
34+
}
35+
36+
/// Private type wrapper to indicate [`RamaService`]
37+
/// is used by the user from the Transport layer (tcp).
38+
pub struct Transport<S>(S);
39+
40+
/// Private type wrapper to indicate [`RamaService`]
41+
/// is used by the user from the Application layer (http(s)).
42+
pub struct Application<S>(S);
43+
44+
macro_rules! impl_wrapper_derive_traits {
45+
($name:ident) => {
46+
impl<S: Clone> Clone for $name<S> {
47+
fn clone(&self) -> Self {
48+
Self(self.0.clone())
49+
}
50+
}
51+
52+
impl<S: fmt::Debug> fmt::Debug for $name<S> {
53+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54+
f.debug_tuple(stringify!($name)).field(&self.0).finish()
55+
}
56+
}
57+
};
58+
}
59+
60+
impl_wrapper_derive_traits!(Transport);
61+
impl_wrapper_derive_traits!(Application);
62+
63+
impl<S> RamaService<Transport<S>, ()> {
64+
pub fn transport(svc: S) -> Self {
65+
Self {
66+
svc: Transport(svc),
67+
state: (),
68+
}
69+
}
70+
}
71+
72+
impl<S> RamaService<Application<S>, ()> {
73+
pub fn application(svc: S) -> Self {
74+
Self {
75+
svc: Application(svc),
76+
state: (),
77+
}
78+
}
79+
}
80+
81+
impl<T> RamaService<T, ()> {
82+
/// Attach state to this [`RamaService`], such that it will be passed
83+
/// as part of each request's [`Context`].
84+
///
85+
/// [`Context`]: rama::Context
86+
pub fn with_state<State>(self, state: State) -> RamaService<T, State>
87+
where
88+
State: Clone + Send + Sync + 'static,
89+
{
90+
RamaService {
91+
svc: self.svc,
92+
state,
93+
}
94+
}
95+
}
96+
97+
#[shuttle_runtime::async_trait]
98+
impl<S, State> shuttle_runtime::Service for RamaService<Transport<S>, State>
99+
where
100+
S: Service<State, tokio::net::TcpStream>,
101+
State: Clone + Send + Sync + 'static,
102+
{
103+
/// Takes the service that is returned by the user in their [shuttle_runtime::main] function
104+
/// and binds to an address passed in by shuttle.
105+
async fn bind(self, addr: SocketAddr) -> Result<(), Error> {
106+
TcpListener::build_with_state(self.state)
107+
.bind(addr)
108+
.await
109+
.map_err(|err| Error::BindPanic(err.to_string()))?
110+
.serve(self.svc.0)
111+
.await;
112+
Ok(())
113+
}
114+
}
115+
116+
#[shuttle_runtime::async_trait]
117+
impl<S, State, Response> shuttle_runtime::Service for RamaService<Application<S>, State>
118+
where
119+
S: Service<State, Request, Response = Response, Error = Infallible>,
120+
Response: IntoResponse + Send + 'static,
121+
State: Clone + Send + Sync + 'static,
122+
{
123+
/// Takes the service that is returned by the user in their [shuttle_runtime::main] function
124+
/// and binds to an address passed in by shuttle.
125+
async fn bind(self, addr: SocketAddr) -> Result<(), Error> {
126+
// shuttle only supports h1 between load balancer <=> web service,
127+
// h2 is terminated by shuttle's load balancer
128+
HttpServer::http1()
129+
.listen_with_state(self.state, addr, self.svc.0)
130+
.await
131+
.map_err(|err| CustomError::new(OpaqueError::from_boxed(err)))?;
132+
Ok(())
133+
}
134+
}
135+
136+
#[doc = include_str!("../README.md")]
137+
pub type ShuttleRamaTransport<S, State = ()> = Result<RamaService<Transport<S>, State>, Error>;
138+
139+
#[doc = include_str!("../README.md")]
140+
pub type ShuttleRamaApplication<S, State = ()> = Result<RamaService<Application<S>, State>, Error>;
141+
142+
pub use shuttle_runtime::{Error as ShuttleError, Service as ShuttleService};

0 commit comments

Comments
 (0)