#![forbid(unsafe_code)]
#![deny(missing_docs)]
use actix_files::NamedFile;
use actix_http::header::{HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER};
use actix_web::{
dev::{ServiceFactory, ServiceRequest},
http::header,
test,
web::{Data, Payload, ServiceConfig},
*,
};
use dashmap::DashMap;
use futures::{stream::once, Stream, StreamExt};
use http::StatusCode;
use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
hydration::IslandsRouterNavigation,
prelude::expect_context,
reactive::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_integration_utils::{
BoxedFnOnce, ExtendResponse, PinnedFuture, PinnedStream,
};
use leptos_meta::ServerMetaContext;
use leptos_router::{
components::provide_server_redirect,
location::RequestUrl,
static_routes::{RegenerationFn, ResolvedStaticPath},
ExpandOptionals, Method, PathSegment, RouteList, RouteListing, SsrMode,
};
use parking_lot::RwLock;
use send_wrapper::SendWrapper;
use server_fn::{
error::ServerFnErrorErr, redirect::REDIRECT_HEADER,
request::actix::ActixRequest,
};
use std::{
collections::HashSet,
fmt::{Debug, Display},
future::Future,
ops::{Deref, DerefMut},
path::Path,
sync::{Arc, LazyLock},
};
#[derive(Debug, Clone, Default)]
pub struct ResponseParts {
pub status: Option<StatusCode>,
pub headers: header::HeaderMap,
}
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)]
pub struct Request(SendWrapper<HttpRequest>);
impl Request {
pub fn new(req: &HttpRequest) -> Self {
Self(SendWrapper::new(req.clone()))
}
pub fn into_inner(self) -> HttpRequest {
self.0.take()
}
}
impl Deref for Request {
type Target = HttpRequest;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Request {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[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);
}
}
struct ActixResponse(HttpResponse);
impl ExtendResponse for ActixResponse {
type ResponseOptions = ResponseOptions;
fn from_stream(
stream: impl Stream<Item = String> + Send + 'static,
) -> Self {
ActixResponse(
HttpResponse::Ok()
.content_type("text/html")
.streaming(stream.map(|chunk| {
Ok(web::Bytes::from(chunk)) as Result<web::Bytes>
})),
)
}
fn extend_response(&mut self, res_options: &Self::ResponseOptions) {
let mut res_options = res_options.0.write();
let headers = self.0.headers_mut();
for (key, value) in std::mem::take(&mut res_options.headers) {
headers.append(key, value);
}
if let Some(status) = res_options.status {
*self.0.status_mut() = status;
}
}
fn set_default_content_type(&mut self, content_type: &str) {
let headers = self.0.headers_mut();
if !headers.contains_key(header::CONTENT_TYPE) {
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str(content_type).unwrap(),
);
}
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn redirect(path: &str) {
if let (Some(req), Some(res)) =
(use_context::<Request>(), use_context::<ResponseOptions>())
{
res.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path)
.expect("Failed to create HeaderValue"),
);
let accepts_html = req
.headers()
.get(ACCEPT)
.and_then(|v| v.to_str().ok())
.map(|v| v.contains("text/html"))
.unwrap_or(false);
if accepts_html {
res.set_status(StatusCode::FOUND);
} else {
res.insert_header(
HeaderName::from_static(REDIRECT_HEADER),
HeaderValue::from_str("").unwrap(),
);
}
} else {
let msg = "Couldn't retrieve either Parts or ResponseOptions while \
trying to redirect().";
#[cfg(feature = "tracing")]
tracing::warn!("{}", &msg);
#[cfg(not(feature = "tracing"))]
eprintln!("{}", &msg);
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|| {})
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn handle_server_fns_with_context(
additional_context: impl Fn() + 'static + Clone + Send,
) -> Route {
web::to(move |req: HttpRequest, payload: Payload| {
let additional_context = additional_context.clone();
async move {
let additional_context = additional_context.clone();
let path = req.path();
let method = req.method();
if let Some(mut service) =
server_fn::actix::get_server_fn_service(path, method)
{
let owner = Owner::new();
owner
.with(|| {
ScopedFuture::new(async move {
provide_context(Request::new(&req));
let res_options = ResponseOptions::default();
provide_context(res_options.clone());
additional_context();
let accepts_html = req
.headers()
.get(ACCEPT)
.and_then(|v| v.to_str().ok())
.map(|v| v.contains("text/html"))
.unwrap_or(false);
let referrer = req.headers().get(REFERER).cloned();
let mut res = ActixResponse(
service
.run(ActixRequest::from((req, payload)))
.await
.take(),
);
if accepts_html {
if let Some(referrer) = referrer {
let has_location =
res.0.headers().get(LOCATION).is_some();
if !has_location {
*res.0.status_mut() = StatusCode::FOUND;
res.0
.headers_mut()
.insert(LOCATION, referrer);
}
}
}
{
let mut res_options = res_options.0.write();
let headers = res.0.headers_mut();
for location in
res_options.headers.remove(header::LOCATION)
{
headers.insert(header::LOCATION, location);
}
}
res.extend_response(&res_options);
res.0
})
})
.await
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
\n\nIt's likely that either
1. The API prefix you specify in the `#[server]` \
macro doesn't match the prefix at which your server \
function handler is mounted, or \n2. You are on a \
platform that doesn't support automatic server function \
registration and you need to call \
ServerFn::register_explicit() on the server function \
type, somewhere in your `main` function.",
req.path()
))
}
}
})
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
render_app_to_stream_with_context(|| {}, app_fn, method)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
render_app_to_stream_in_order_with_context(|| {}, app_fn, method)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async<IV>(
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
render_app_async_with_context(|| {}, app_fn, method)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
render_app_to_stream_with_context_and_replace_blocks(
additional_context,
app_fn,
method,
false,
)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
replace_blocks: bool,
) -> Route
where
IV: IntoView + 'static,
{
_ = replace_blocks; handle_response(
method,
additional_context,
app_fn,
|app, chunks, supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
if supports_ooo {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_in_order_branching()
}
} else if supports_ooo {
app.to_html_stream_out_of_order()
} else {
app.to_html_stream_in_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
},
)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_to_stream_in_order_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
handle_response(
method,
additional_context,
app_fn,
|app, chunks, _supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
Box::pin(app.chain(chunks())) as PinnedStream<String>
})
},
)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
method: Method,
) -> Route
where
IV: IntoView + 'static,
{
handle_response(method, additional_context, app_fn, async_stream_builder)
}
fn async_stream_builder<IV>(
app: IV,
chunks: BoxedFnOnce<PinnedStream<String>>,
_supports_ooo: bool,
) -> PinnedFuture<PinnedStream<String>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks)) as PinnedStream<String>
})
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn provide_contexts(
req: Request,
meta_context: &ServerMetaContext,
res_options: &ResponseOptions,
) {
let path = leptos_corrected_path(&req);
provide_context(RequestUrl::new(&path));
provide_context(meta_context.clone());
provide_context(res_options.clone());
provide_context(req);
provide_server_redirect(redirect);
leptos::nonce::provide_nonce();
}
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
}
}
#[allow(clippy::type_complexity)]
fn handle_response<IV>(
method: Method,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
stream_builder: fn(
IV,
BoxedFnOnce<PinnedStream<String>>,
bool,
) -> PinnedFuture<PinnedStream<String>>,
) -> Route
where
IV: IntoView + 'static,
{
let handler = move |req: HttpRequest| {
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
async move {
let is_island_router_navigation = cfg!(feature = "islands-router")
&& req.headers().get("Islands-Router").is_some();
let res_options = ResponseOptions::default();
let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = {
let meta_context = meta_context.clone();
let res_options = res_options.clone();
let req = Request::new(&req);
move || {
provide_contexts(req, &meta_context, &res_options);
add_context();
if is_island_router_navigation {
provide_context(IslandsRouterNavigation);
}
}
};
let res = ActixResponse::from_app(
app_fn,
meta_output,
additional_context,
res_options,
stream_builder,
!is_island_router_navigation,
)
.await;
res.0
}
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Send + Clone,
) -> Vec<ActixRouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None).0
}
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Send + Clone,
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None)
}
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
) -> Vec<ActixRouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg_and_context(
app_fn,
excluded_routes,
|| {},
)
}
trait ActixPath {
fn to_actix_path(&self) -> String;
}
impl ActixPath for Vec<PathSegment> {
fn to_actix_path(&self) -> String {
let mut path = String::new();
for segment in self.iter() {
let raw = segment.as_raw_str();
if !raw.is_empty() && !raw.starts_with('/') {
path.push('/');
}
match segment {
PathSegment::Static(s) => path.push_str(s),
PathSegment::Param(s) => {
path.push('{');
path.push_str(s);
path.push('}');
}
PathSegment::Splat(s) => {
path.push('{');
path.push_str(s);
path.push_str(":.*}");
}
PathSegment::Unit => {}
PathSegment::OptionalParam(_) => {
#[cfg(feature = "tracing")]
tracing::error!(
"to_axum_path should only be called on expanded \
paths, which do not have OptionalParam any longer"
);
Default::default()
}
}
}
path
}
}
#[derive(Clone, Debug, Default)]
pub struct ActixRouteListing {
path: String,
mode: SsrMode,
methods: Vec<leptos_router::Method>,
regenerate: Vec<RegenerationFn>,
exclude: bool,
}
trait IntoRouteListing: Sized {
fn into_route_listing(self) -> Vec<ActixRouteListing>;
}
impl IntoRouteListing for RouteListing {
fn into_route_listing(self) -> Vec<ActixRouteListing> {
self.path()
.to_vec()
.expand_optionals()
.into_iter()
.map(|path| {
let path = path.to_actix_path();
let path = if path.is_empty() {
"/".to_string()
} else {
path
};
let mode = self.mode();
let methods = self.methods().collect();
let regenerate = self.regenerate().into();
ActixRouteListing {
path,
mode: mode.clone(),
methods,
regenerate,
exclude: false,
}
})
.collect()
}
}
impl ActixRouteListing {
pub fn new(
path: String,
mode: SsrMode,
methods: impl IntoIterator<Item = leptos_router::Method>,
regenerate: impl Into<Vec<RegenerationFn>>,
) -> Self {
Self {
path,
mode,
methods: methods.into_iter().collect(),
regenerate: regenerate.into(),
exclude: false,
}
}
pub fn path(&self) -> &str {
&self.path
}
pub fn mode(&self) -> SsrMode {
self.mode.clone()
}
pub fn methods(&self) -> impl Iterator<Item = leptos_router::Method> + '_ {
self.methods.iter().copied()
}
}
pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
app_fn: impl Fn() -> IV + 'static + Send + Clone,
excluded_routes: Option<Vec<String>>,
additional_context: impl Fn() + 'static + Send + Clone,
) -> (Vec<ActixRouteListing>, StaticRouteGenerator)
where
IV: IntoView + 'static,
{
let _ = any_spawner::Executor::init_tokio();
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
let (mock_meta, _) = ServerMetaContext::new();
let routes = owner
.with(|| {
provide_context(RequestUrl::new(""));
provide_context(ResponseOptions::default());
provide_context(mock_meta);
additional_context();
RouteList::generate(&app_fn)
})
.unwrap_or_default();
let generator = StaticRouteGenerator::new(
&routes,
app_fn.clone(),
additional_context.clone(),
);
let mut routes = routes
.into_inner()
.into_iter()
.flat_map(IntoRouteListing::into_route_listing)
.collect::<Vec<_>>();
let routes = if routes.is_empty() {
vec![ActixRouteListing::new(
"/".to_string(),
Default::default(),
[leptos_router::Method::Get],
vec![],
)]
} else {
if let Some(excluded_routes) = &excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
};
let excluded =
excluded_routes
.into_iter()
.flatten()
.map(|path| ActixRouteListing {
path,
mode: Default::default(),
methods: Vec::new(),
regenerate: Vec::new(),
exclude: true,
});
(routes.into_iter().chain(excluded).collect(), generator)
}
#[allow(clippy::type_complexity)]
pub struct StaticRouteGenerator(
#[allow(dead_code)] Owner,
Box<dyn FnOnce(&LeptosOptions) -> PinnedFuture<()> + Send>,
);
impl StaticRouteGenerator {
fn render_route<IV: IntoView + 'static>(
path: String,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> impl Future<Output = (Owner, String)> {
let (meta_context, meta_output) = ServerMetaContext::new();
let additional_context = {
let add_context = additional_context.clone();
move || {
let mock_req = test::TestRequest::with_uri(&path)
.insert_header(("Accept", "text/html"))
.to_http_request();
let res_options = ResponseOptions::default();
provide_contexts(
Request::new(&mock_req),
&meta_context,
&res_options,
);
add_context();
}
};
let (owner, stream) = leptos_integration_utils::build_response(
app_fn.clone(),
additional_context,
async_stream_builder,
false,
);
let sc = owner.shared_context().unwrap();
async move {
let stream = stream.await;
while let Some(pending) = sc.await_deferred() {
pending.await;
}
let html = meta_output
.inject_meta_context(stream)
.await
.collect::<String>()
.await;
(owner, html)
}
}
pub fn new<IV>(
routes: &RouteList,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let owner = Owner::new();
Self(owner.clone(), {
let routes = routes.clone();
Box::new(move |options| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
owner.with(|| {
additional_context();
Box::pin(ScopedFuture::new(routes.generate_static_files(
move |path: &ResolvedStaticPath| {
Self::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
)))
})
})
})
}
pub async fn generate(self, options: &LeptosOptions) {
(self.1)(options).await
}
}
static STATIC_HEADERS: LazyLock<DashMap<String, ResponseOptions>> =
LazyLock::new(DashMap::new);
fn was_404(owner: &Owner) -> bool {
let resp = owner.with(|| expect_context::<ResponseOptions>());
let status = resp.0.read().status;
if let Some(status) = status {
return status == StatusCode::NOT_FOUND;
}
false
}
fn static_path(options: &LeptosOptions, path: &str) -> String {
use leptos_integration_utils::static_file_path;
if path != "/" && path.ends_with("/") {
static_file_path(options, &format!("{path}index"))
} else {
static_file_path(options, path)
}
}
async fn write_static_route(
options: &LeptosOptions,
response_options: Option<ResponseOptions>,
path: &str,
html: &str,
) -> Result<(), std::io::Error> {
if let Some(options) = response_options {
STATIC_HEADERS.insert(path.to_string(), options);
}
let path = static_path(options, path);
let path = Path::new(&path);
if let Some(path) = path.parent() {
tokio::fs::create_dir_all(path).await?;
}
tokio::fs::write(path, &html).await?;
Ok(())
}
fn handle_static_route<IV>(
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
regenerate: Vec<RegenerationFn>,
) -> Route
where
IV: IntoView + 'static,
{
let handler = move |req: HttpRequest, data: Data<LeptosOptions>| {
Box::pin({
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let regenerate = regenerate.clone();
async move {
let options = data.into_inner();
let orig_path = req.uri().path();
let path = static_path(&options, orig_path);
let path = Path::new(&path);
let exists = tokio::fs::try_exists(path).await.unwrap_or(false);
let (response_options, html) = if !exists {
let path = ResolvedStaticPath::new(orig_path);
let (owner, html) = path
.build(
move |path: &ResolvedStaticPath| {
StaticRouteGenerator::render_route(
path.to_string(),
app_fn.clone(),
additional_context.clone(),
)
},
move |path: &ResolvedStaticPath,
owner: &Owner,
html: String| {
let options = options.clone();
let path = path.to_owned();
let response_options = owner.with(use_context);
async move {
write_static_route(
&options,
response_options,
path.as_ref(),
&html,
)
.await
}
},
was_404,
regenerate,
)
.await;
(owner.with(use_context::<ResponseOptions>), html)
} else {
let headers =
STATIC_HEADERS.get(orig_path).map(|v| v.clone());
(headers, None)
};
let mut res = ActixResponse(match html {
Some(html) => {
HttpResponse::Ok().content_type("text/html").body(html)
}
None => match NamedFile::open(path) {
Ok(res) => res.into_response(&req),
Err(err) => HttpResponse::InternalServerError()
.body(err.to_string()),
},
});
if let Some(options) = response_options {
res.extend_response(&options);
}
res.0
}
})
};
web::get().to(handler)
}
pub trait LeptosRoutes {
fn leptos_routes<IV>(
self,
paths: Vec<ActixRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
fn leptos_routes_with_context<IV>(
self,
paths: Vec<ActixRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> 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 = (),
>,
{
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes<IV>(
self,
paths: Vec<ActixRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
self.leptos_routes_with_context(paths, || {}, app_fn)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes_with_context<IV>(
self,
paths: Vec<ActixRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
let excluded = paths
.iter()
.filter(|&p| p.exclude)
.map(|p| p.path.as_str())
.collect::<HashSet<_>>();
for (path, _) in server_fn::actix::server_fn_paths() {
if !excluded.contains(path) {
let additional_context = additional_context.clone();
let handler =
handle_server_fns_with_context(additional_context);
router = router.route(path, handler);
}
}
for listing in paths.iter().filter(|p| !p.exclude) {
let path = listing.path();
let mode = listing.mode();
for method in listing.methods() {
let additional_context = additional_context.clone();
let additional_context_and_method = move || {
provide_context(method);
additional_context();
};
router = if matches!(listing.mode(), SsrMode::Static(_)) {
router.route(
path,
handle_static_route(
additional_context_and_method.clone(),
app_fn.clone(),
listing.regenerate.clone(),
),
)
} else {
router
.route(path, web::head().to(HttpResponse::Ok))
.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context_and_method.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context_and_method.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
)
};
}
}
router
}
}
impl LeptosRoutes for &mut ServiceConfig {
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes<IV>(
self,
paths: Vec<ActixRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
self.leptos_routes_with_context(paths, || {}, app_fn)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
fn leptos_routes_with_context<IV>(
self,
paths: Vec<ActixRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
let excluded = paths
.iter()
.filter(|&p| p.exclude)
.map(|p| p.path.as_str())
.collect::<HashSet<_>>();
for (path, _) in server_fn::actix::server_fn_paths() {
if !excluded.contains(path) {
let additional_context = additional_context.clone();
let handler =
handle_server_fns_with_context(additional_context);
router = router.route(path, handler);
}
}
for listing in paths.iter().filter(|p| !p.exclude) {
let path = listing.path();
let mode = listing.mode();
for method in listing.methods() {
if matches!(listing.mode(), SsrMode::Static(_)) {
router = router.route(
path,
handle_static_route(
additional_context.clone(),
app_fn.clone(),
listing.regenerate.clone(),
),
)
} else {
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
render_app_to_stream_with_context(
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
additional_context.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
additional_context.clone(),
app_fn.clone(),
method,
)
}
SsrMode::Async => render_app_async_with_context(
additional_context.clone(),
app_fn.clone(),
method,
),
_ => unreachable!()
},
);
}
}
}
router
}
}
pub async fn extract<T>() -> Result<T, ServerFnErrorErr>
where
T: actix_web::FromRequest,
<T as FromRequest>::Error: Display,
{
let req = use_context::<Request>().ok_or_else(|| {
ServerFnErrorErr::ServerError(
"HttpRequest should have been provided via context".to_string(),
)
})?;
SendWrapper::new(async move {
T::extract(&req)
.await
.map_err(|e| ServerFnErrorErr::ServerError(e.to_string()))
})
.await
}