#![forbid(unsafe_code)]
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
http::header,
web::Bytes,
*,
};
use futures::{Future, Stream, StreamExt};
use http::StatusCode;
use leptos::{
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
leptos_server::{server_fn_by_path, Payload},
*,
};
use leptos_integration_utils::{build_async_response, html_parts};
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct ResponseParts {
pub headers: header::HeaderMap,
pub status: Option<StatusCode>,
}
impl ResponseParts {
pub fn insert_header(
&mut self,
key: header::HeaderName,
value: header::HeaderValue,
) {
self.headers.insert(key, value);
}
pub fn append_header(
&mut self,
key: header::HeaderName,
value: header::HeaderValue,
) {
self.headers.append(key, value);
}
}
#[derive(Debug, Clone, Default)]
pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
pub fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write();
*writable = parts
}
pub fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
pub fn insert_header(
&self,
key: header::HeaderName,
value: header::HeaderValue,
) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
pub fn append_header(
&self,
key: header::HeaderName,
value: header::HeaderValue,
) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
}
pub fn redirect(cx: leptos::Scope, path: &str) {
if let Some(response_options) = use_context::<ResponseOptions>(cx) {
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
}
}
pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|_cx| {})
}
pub fn handle_server_fns_with_context(
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> Route {
web::post().to(
move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| {
let additional_context = additional_context.clone();
async move {
let additional_context = additional_context.clone();
let path = params.into_inner();
let accept_header = req
.headers()
.get("Accept")
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body: &[u8] = &body;
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
additional_context(cx);
let res_options = ResponseOptions::default();
provide_context(cx, req.clone());
provide_context(cx, res_options.clone());
match server_fn(cx, body).await {
Ok(serialized) => {
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
disposer.dispose();
runtime.dispose();
let mut res: HttpResponseBuilder;
let mut res_parts = res_options.0.write();
if accept_header == Some("application/json")
|| accept_header
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = HttpResponse::Ok();
}
else {
let referer = req
.headers()
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
res = HttpResponse::SeeOther();
res.insert_header(("Location", referer))
.content_type("application/json");
};
if let Some(status) = res_parts.status {
res.status(status);
}
let _count = res_parts
.headers
.drain()
.map(|(k, v)| {
if let Some(k) = k {
res.append_header((k, v));
}
})
.count();
match serialized {
Payload::Binary(data) => {
res.content_type("application/cbor");
res.body(Bytes::from(data))
}
Payload::Url(data) => {
res.content_type(
"application/x-www-form-urlencoded",
);
res.body(data)
}
Payload::Json(data) => {
res.content_type("application/json");
res.body(data)
}
}
}
Err(e) => HttpResponse::InternalServerError()
.body(e.to_string()),
}
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
\n\nIt's likely that you need to call \
ServerFn::register() on the server function type, \
somewhere in your `main` function.",
req.path()
))
}
}
},
)
}
pub fn render_app_to_stream<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_with_context(options, |_cx| {}, app_fn)
}
pub fn render_app_to_stream_in_order<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_in_order_with_context(options, |_cx| {}, app_fn)
}
pub fn render_app_async<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
render_app_async_with_context(options, |_cx| {}, app_fn)
}
pub fn render_app_to_stream_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let res_options = ResponseOptions::default();
async move {
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx).into_view(cx)
}
};
stream_app(&options, app, res_options, additional_context).await
}
})
}
pub fn render_app_to_stream_in_order_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let res_options = ResponseOptions::default();
async move {
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx).into_view(cx)
}
};
stream_app_in_order(&options, app, res_options, additional_context)
.await
}
})
}
pub fn render_app_async_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let res_options = ResponseOptions::default();
async move {
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx).into_view(cx)
}
};
render_app_async_helper(
&options,
app,
res_options,
additional_context,
)
.await
}
})
}
#[deprecated = "You can now use `render_app_async` with `create_resource` and \
`<Suspense/>` to achieve async rendering without manually \
preloading data."]
pub fn render_preloaded_data_app<Data, Fut, IV>(
options: LeptosOptions,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Route
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let data_fn = data_fn.clone();
let res_options = ResponseOptions::default();
async move {
let data = match data_fn(req.clone()).await {
Err(e) => return HttpResponse::from_error(e),
Ok(DataResponse::Response(r)) => return r.into(),
Ok(DataResponse::Data(d)) => d,
};
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx, data).into_view(cx)
}
};
stream_app(&options, app, res_options, |_cx| {}).await
}
})
}
fn provide_contexts(
cx: leptos::Scope,
req: &HttpRequest,
res_options: ResponseOptions,
) {
let path = leptos_corrected_path(req);
let integration = ServerIntegration { path };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, MetaContext::new());
provide_context(cx, res_options);
provide_context(cx, req.clone());
provide_server_redirect(cx, move |path| redirect(cx, path));
}
fn leptos_corrected_path(req: &HttpRequest) -> String {
let path = req.path();
let query = req.query_string();
if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
}
}
async fn stream_app(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| generate_head_metadata(cx).into(),
additional_context,
);
build_stream_response(options, res_options, stream, runtime, scope).await
}
async fn stream_app_in_order(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
move |cx| {
generate_head_metadata(cx).into()
},
additional_context,
);
build_stream_response(options, res_options, stream, runtime, scope).await
}
async fn build_stream_response(
options: &LeptosOptions,
res_options: ResponseOptions,
stream: impl Stream<Item = String> + 'static,
runtime: RuntimeId,
scope: ScopeId,
) -> HttpResponse {
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) =
html_parts(options, use_context::<MetaContext>(cx).as_ref());
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
.chain(stream)
.chain(futures::stream::once(async move {
runtime.dispose();
tail.to_string()
}))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
);
let first_chunk = stream.next().await;
let second_chunk = stream.next().await;
let res_options = res_options.0.read();
let (status, mut headers) =
(res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
let complete_stream =
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
.chain(stream);
let mut res = HttpResponse::Ok()
.content_type("text/html")
.streaming(complete_stream);
for (key, value) in headers.drain() {
if let Some(key) = key {
res.headers_mut().append(key, value);
}
}
let res_status = res.status_mut();
*res_status = status;
res
}
async fn render_app_async_helper(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
move |_| "".into(),
additional_context,
);
let html = build_async_response(stream, options, runtime, scope).await;
let res_options = res_options.0.read();
let (status, mut headers) =
(res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
let mut res = HttpResponse::Ok().content_type("text/html").body(html);
for (key, value) in headers.drain() {
if let Some(key) = key {
res.headers_mut().append(key, value);
}
}
let res_status = res.status_mut();
*res_status = status;
res
}
pub fn generate_route_list<IV>(
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
) -> Vec<(String, SsrMode)>
where
IV: IntoView + 'static,
{
let mut routes = leptos_router::generate_route_list_inner(app_fn);
routes = routes
.into_iter()
.map(|(s, mode)| {
if s.is_empty() {
return ("/".to_string(), mode);
}
(s, mode)
})
.collect();
let wildcard_re = Regex::new(r"\*.*").unwrap();
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
let routes: Vec<(String, SsrMode)> = routes
.into_iter()
.map(|(s, m)| (wildcard_re.replace_all(&s, "{tail:.*}").to_string(), m))
.map(|(s, m)| (capture_re.replace_all(&s, "{$1}").to_string(), m))
.collect();
if routes.is_empty() {
vec![("/".to_string(), Default::default())]
} else {
routes
}
}
pub enum DataResponse<T> {
Data(T),
Response(actix_web::dev::Response<BoxBody>),
}
pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
#[deprecated = "You can now use `leptos_routes` and a `<Route \
mode=SsrMode::Async/>`
to achieve async rendering without manually preloading \
data."]
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Self
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static;
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
}
impl<T> LeptosRoutes for actix_web::App<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Error = Error,
InitError = (),
>,
{
fn leptos_routes<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
self.leptos_routes_with_context(options, paths, |_| {}, app_fn)
}
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Self
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
#[allow(deprecated)]
render_preloaded_data_app(
options.clone(),
data_fn.clone(),
app_fn.clone(),
),
);
}
router
}
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<(String, SsrMode)>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for (path, mode) in paths.iter() {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)
}
SsrMode::Async => render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
},
);
}
router
}
}