-
Notifications
You must be signed in to change notification settings - Fork 258
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
base: main
Are you sure you want to change the base?
Changes from all commits
0261a57
3db0354
bbf9ce5
2347c5f
af673a3
719e6ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 = [] |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
))) | ||
} | ||
``` |
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>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is copy pasted from other integrations found in |
||
|
||
pub use shuttle_runtime::{Error as ShuttleError, Service as ShuttleService}; |
There was a problem hiding this comment.
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.