Skip to content
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

initial attempt to support rama in shuttle #1943

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Missing version and example columns that other services have in the table. Add version '0.2.0-alpha.5' and GitHub example link to maintain consistency.

/// | `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.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 = []
71 changes: 71 additions & 0 deletions services/shuttle-rama/README.md
Original file line number Diff line number Diff line change
@@ -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<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_fn};
use std::convert::Infallible;
use tokio::io::AsyncWriteExt;

async fn hello_world<S>(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");
Comment on lines +45 to +55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: consider extracting HTTP response construction into a separate function for better reusability and maintainability

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an example 🤷


stream
.write_all(resp.as_bytes())
.await
.expect("write to stream");

jonaro00 marked this conversation as resolved.
Show resolved Hide resolved
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;
GlenDC marked this conversation as resolved.
Show resolved Hide resolved
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>,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: constraining Error to Infallible may be too restrictive for real-world HTTP services that need error handling

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the Transport layer version if you want to have errors on transport layer, but an http service throwing errors makes no sense as only thing you can do with it is log + abort, which you can do yourself in your root http service. What you really want to do better however is turn it into an http error.

You can lookup in the rama docs, as for handlers, similar to Axum, you can also have Result types, but that's for your endpoint handlers not the root endpoint, but even there the Error type of the Result needs to be convertable into a Response.

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>;
Comment on lines +126 to +130
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: including full README.md in type aliases via doc attribute seems excessive - consider more focused documentation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is copy pasted from other integrations found in services/


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