add rama support to datastar rust sdk (#847)
* add rama support to datastar rust sdk Closes #845 * bump rama alpha version
This commit is contained in:
parent
6b2d488ae6
commit
10b29846ae
|
@ -152,6 +152,7 @@ func writeOutConsts(version string) error {
|
|||
"examples/ruby/hello-world/hello-world.html": helloWorldExample,
|
||||
"examples/rust/axum/hello-world/hello-world.html": helloWorldExample,
|
||||
"examples/rust/rocket/hello-world/hello-world.html": helloWorldExample,
|
||||
"examples/rust/rama/hello-world/hello-world.html": helloWorldExample,
|
||||
}
|
||||
|
||||
for path, tmplFn := range templates {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
name = "hello_world"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.85.0"
|
||||
|
||||
[dependencies]
|
||||
async-stream = "0.3.6"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
target
|
||||
Cargo.lock
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
edition = "2024"
|
||||
name = "hello_world"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.85.0"
|
||||
|
||||
[dependencies]
|
||||
async-stream = "0.3.6"
|
||||
rama = { version = "0.2.0-alpha.11", features = ["http-full"] }
|
||||
datastar = { path = "../../../../sdk/rust", features = ["rama"] }
|
||||
futures = "0.3.31"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
|
@ -0,0 +1,35 @@
|
|||
<!-- This is auto-generated by Datastar. DO NOT EDIT. -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Datastar SDK Demo</title>
|
||||
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"></script>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900 text-lg max-w-xl mx-auto my-16">
|
||||
<div data-signals-delay="400" class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded-lg px-6 py-8 ring shadow-xl ring-gray-900/5 space-y-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-gray-900 dark:text-white text-3xl font-semibold">
|
||||
Datastar SDK Demo
|
||||
</h1>
|
||||
<img src="https://data-star.dev/static/images/rocket.png" alt="Rocket" width="64" height="64"/>
|
||||
</div>
|
||||
<p class="mt-2">
|
||||
SSE events will be streamed from the backend to the frontend.
|
||||
</p>
|
||||
<div class="space-x-2">
|
||||
<label for="delay">
|
||||
Delay in milliseconds
|
||||
</label>
|
||||
<input data-bind-delay id="delay" type="number" step="100" min="0" class="w-36 rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-sky-500 focus:outline focus:outline-sky-500 dark:disabled:border-gray-700 dark:disabled:bg-gray-800/20" />
|
||||
</div>
|
||||
<button data-on-click="@get('/hello-world')" class="rounded-md bg-sky-500 px-5 py-2.5 leading-5 font-semibold text-white hover:bg-sky-700 hover:text-gray-100 cursor-pointer">
|
||||
Start
|
||||
</button>
|
||||
</div>
|
||||
<div class="my-16 text-8xl font-bold text-transparent" style="background: linear-gradient(to right in oklch, red, orange, yellow, green, blue, blue, violet); background-clip: text">
|
||||
<div id="message">Hello, world!</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,57 @@
|
|||
use {
|
||||
async_stream::stream,
|
||||
core::time::Duration,
|
||||
datastar::{
|
||||
Sse,
|
||||
prelude::{MergeFragments, ReadSignals},
|
||||
},
|
||||
rama::{
|
||||
error::BoxError,
|
||||
http::{IntoResponse, response::Html, server::HttpServer, service::web::Router},
|
||||
rt::Executor,
|
||||
},
|
||||
serde::Deserialize,
|
||||
tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt},
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), BoxError> {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
let app = Router::new()
|
||||
.get("/", index)
|
||||
.get("/hello-world", hello_world);
|
||||
|
||||
tracing::debug!("listening on 127.0.0.1:3000");
|
||||
HttpServer::auto(Executor::default())
|
||||
.listen("127.0.0.1:3000", app)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn index() -> Html<&'static str> {
|
||||
Html(include_str!("../hello-world.html"))
|
||||
}
|
||||
|
||||
const MESSAGE: &str = "Hello, world!";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Signals {
|
||||
pub delay: u64,
|
||||
}
|
||||
|
||||
async fn hello_world(ReadSignals(signals): ReadSignals<Signals>) -> impl IntoResponse {
|
||||
Sse(stream! {
|
||||
for i in 0..MESSAGE.len() {
|
||||
yield MergeFragments::new(format!("<div id='message'>{}</div>", &MESSAGE[0..i + 1])).into();
|
||||
tokio::time::sleep(Duration::from_millis(signals.delay)).await;
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
name = "hello_world"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.85.0"
|
||||
|
||||
[dependencies]
|
||||
datastar = { path = "../../../../sdk/rust", features = ["rocket"] }
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
[package]
|
||||
authors = ["Johnathan Stevers <jmstevers@gmail.com>"]
|
||||
authors = [
|
||||
"Johnathan Stevers <jmstevers@gmail.com>",
|
||||
"Glen Henri J. De Cauwsemaecker <glen@plabayo.tech>",
|
||||
]
|
||||
categories = ["web-programming"]
|
||||
description = "Datastar is the Rust implementation of the [Datastar](https://data-star.dev) SDK."
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
homepage = "https://data-star.dev"
|
||||
keywords = ["datastar", "web", "backend"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
@ -10,34 +13,39 @@ name = "datastar"
|
|||
readme = "README.md"
|
||||
repository = "https://github.com/starfederation/datastar-rs"
|
||||
version = "0.1.0"
|
||||
rust-version = "1.85.0"
|
||||
|
||||
[dev-dependencies]
|
||||
async-stream = { version = "0.3.6", default-features = false }
|
||||
serde = { version = "1.0.217", default-features = false, features = ["derive"] }
|
||||
serde_json = { version = "1.0.138", default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||
serde_json = { version = "1", default-features = false, features = ["std"] }
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
axum = { version = "0.8.1" }
|
||||
rocket = { version = "0.5.1", features = ["json"] }
|
||||
rama = { version = "0.2.0-alpha.10", features = ["http-full"] }
|
||||
|
||||
|
||||
[dependencies]
|
||||
matchit = "0.8.4"
|
||||
axum = { version = "0.8.1", default-features = false, optional = true, features = [
|
||||
"query",
|
||||
"tokio",
|
||||
] }
|
||||
futures-util = { version = "0.3.31", default-features = false }
|
||||
http-body = { version = "1.0.1", default-features = false, optional = true }
|
||||
pin-project-lite = { version = "0.2.16", default-features = false, optional = true }
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
http-body = { version = "1.0", default-features = false, optional = true }
|
||||
pin-project-lite = { version = "0.2", default-features = false, optional = true }
|
||||
rocket = { version = "0.5.1", default-features = false, optional = true }
|
||||
serde = { version = "1.0.217", default-features = false, optional = true, features = [
|
||||
rama = { version = "0.2.0-alpha.11", default-features = false, optional = true, features = [
|
||||
"http",
|
||||
] }
|
||||
serde = { version = "1", default-features = false, optional = true, features = [
|
||||
"derive",
|
||||
] }
|
||||
serde_json = { version = "1.0.138", default-features = false, optional = true, features = [
|
||||
serde_json = { version = "1", default-features = false, optional = true, features = [
|
||||
"std",
|
||||
] }
|
||||
sync_wrapper = { version = "1.0.2", default-features = false, optional = true }
|
||||
sync_wrapper = { version = "1", default-features = false, optional = true }
|
||||
bytes = { version = "1", default-features = false, optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
|
@ -51,6 +59,14 @@ axum = [
|
|||
]
|
||||
http2 = []
|
||||
rocket = ["dep:rocket"]
|
||||
rama = [
|
||||
"dep:rama",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
"dep:pin-project-lite",
|
||||
"dep:bytes",
|
||||
"dep:sync_wrapper",
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Datastar Rust SDK
|
||||
|
||||
An implementation of the Datastar SDK in Rust with framework integration for Axum and Rocket.
|
||||
An implementation of the Datastar SDK in Rust with framework integration for Axum, Rocket and Rama.
|
||||
|
||||
# Usage
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum;
|
||||
#[cfg(feature = "rama")]
|
||||
pub mod rama;
|
||||
#[cfg(feature = "rocket")]
|
||||
pub mod rocket;
|
||||
|
||||
|
@ -23,6 +25,8 @@ mod consts;
|
|||
pub mod prelude {
|
||||
#[cfg(feature = "axum")]
|
||||
pub use crate::axum::ReadSignals;
|
||||
#[cfg(all(feature = "rama", not(feature = "axum")))]
|
||||
pub use crate::rama::ReadSignals;
|
||||
pub use crate::{
|
||||
consts::FragmentMergeMode, execute_script::ExecuteScript, merge_fragments::MergeFragments,
|
||||
merge_signals::MergeSignals, remove_fragments::RemoveFragments,
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
//! Rama integration for Datastar.
|
||||
//!
|
||||
//! Learn more about rama at
|
||||
//! <https://github.com/plabayo/rama>.
|
||||
|
||||
use {
|
||||
crate::{Sse, TrySse, prelude::DatastarEvent},
|
||||
bytes::Bytes,
|
||||
futures_util::{Stream, StreamExt},
|
||||
pin_project_lite::pin_project,
|
||||
rama::http::{
|
||||
Body, BodyExtractExt, IntoResponse, Method, Request, Response, StatusCode,
|
||||
dep::http_body::{Body as HttpBody, Frame},
|
||||
header,
|
||||
service::web::extract::{FromRequest, Query},
|
||||
},
|
||||
serde::{Deserialize, de::DeserializeOwned},
|
||||
std::{
|
||||
convert::Infallible,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
},
|
||||
sync_wrapper::SyncWrapper,
|
||||
};
|
||||
|
||||
pin_project! {
|
||||
struct SseBody<S> {
|
||||
#[pin]
|
||||
stream: SyncWrapper<S>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> IntoResponse for Sse<S>
|
||||
where
|
||||
S: Stream<Item = DatastarEvent> + Send + 'static,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "text/event-stream"),
|
||||
(header::CACHE_CONTROL, "no-cache"),
|
||||
(header::CONNECTION, "keep-alive"),
|
||||
],
|
||||
Body::new(SseBody {
|
||||
stream: SyncWrapper::new(self.0.map(Ok::<_, Infallible>)),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> IntoResponse for TrySse<S>
|
||||
where
|
||||
S: Stream<Item = Result<DatastarEvent, E>> + Send + 'static,
|
||||
E: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
[
|
||||
(header::CONTENT_TYPE, "text/event-stream"),
|
||||
(header::CACHE_CONTROL, "no-cache"),
|
||||
(header::CONNECTION, "keep-alive"),
|
||||
],
|
||||
Body::new(SseBody {
|
||||
stream: SyncWrapper::new(self.0),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> HttpBody for SseBody<S>
|
||||
where
|
||||
S: Stream<Item = Result<DatastarEvent, E>>,
|
||||
{
|
||||
type Data = Bytes;
|
||||
type Error = E;
|
||||
|
||||
fn poll_frame(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
let this = self.project();
|
||||
|
||||
match this.stream.get_pin_mut().poll_next(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Some(Err(error))) => Poll::Ready(Some(Err(error))),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Ready(Some(Ok(event))) => {
|
||||
Poll::Ready(Some(Ok(Frame::data(event.to_string().into()))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DatastarParam {
|
||||
datastar: serde_json::Value,
|
||||
}
|
||||
|
||||
/// [`ReadSignals`] is a request extractor that reads datastar signals from the request.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use datastar::rama::ReadSignals;
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct Signals {
|
||||
/// foo: String,
|
||||
/// bar: i32,
|
||||
/// }
|
||||
///
|
||||
/// async fn handler(ReadSignals(signals): ReadSignals<Signals>) {
|
||||
/// println!("foo: {}", signals.foo);
|
||||
/// println!("bar: {}", signals.bar);
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct ReadSignals<T: DeserializeOwned>(pub T);
|
||||
|
||||
impl<T> FromRequest for ReadSignals<T>
|
||||
where
|
||||
T: DeserializeOwned + Send + Sync + 'static,
|
||||
{
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request(req: Request) -> Result<Self, Self::Rejection> {
|
||||
let json = match *req.method() {
|
||||
Method::GET => {
|
||||
let query =
|
||||
Query::<DatastarParam>::parse_query_str(req.uri().query().unwrap_or(""))
|
||||
.map_err(IntoResponse::into_response)?;
|
||||
|
||||
let signals = query.0.datastar.as_str().ok_or_else(|| {
|
||||
(StatusCode::BAD_REQUEST, "Failed to parse JSON").into_response()
|
||||
})?;
|
||||
|
||||
serde_json::from_str(signals)
|
||||
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()).into_response())?
|
||||
}
|
||||
_ => req
|
||||
.into_body()
|
||||
.try_into_json()
|
||||
.await
|
||||
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()).into_response())?,
|
||||
};
|
||||
|
||||
Ok(Self(json))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {
|
||||
super::Sse,
|
||||
crate::{
|
||||
prelude::ReadSignals,
|
||||
testing::{self, Signals},
|
||||
},
|
||||
rama::{
|
||||
error::BoxError,
|
||||
http::{IntoResponse, server::HttpServer, service::web::Router},
|
||||
rt::Executor,
|
||||
},
|
||||
};
|
||||
|
||||
async fn test(ReadSignals(signals): ReadSignals<Signals>) -> impl IntoResponse {
|
||||
Sse(testing::test(signals.events))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sdk_test() -> Result<(), BoxError> {
|
||||
HttpServer::auto(Executor::default())
|
||||
.listen(
|
||||
"127.0.0.1:3000",
|
||||
Router::new().get("/test", test).post("/test", test),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue