Compare commits

...

1 Commits

Author SHA1 Message Date
Greg Johnston
beea04a050 preliminary work 2023-05-04 17:11:01 -04:00
5 changed files with 134 additions and 35 deletions

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6.2", optional = true }
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
actix-web = { version = "4.2.1", features = ["macros"] }
anyhow = "1.0.68"
broadcaster = "1.0.0"
console_log = "1.0.0"
@@ -19,7 +19,7 @@ cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_actix = { path = "../../integrations/actix" }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
@@ -36,10 +36,8 @@ default = ["ssr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:sqlx",
"leptos/ssr",
"leptos_actix",
"leptos_meta/ssr",
"leptos_router/ssr",
]

View File

@@ -107,6 +107,9 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
cx,
<Todos/>
}/>
<Api path="bananas" route=web::get().to(|req: HttpRequest| async move {
req.path()
})
</Routes>
</main>
</Router>

View File

@@ -16,7 +16,7 @@ use actix_web::{
use futures::{Stream, StreamExt};
use http::StatusCode;
use leptos::{
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
leptos_dom::{Transparent, ssr::render_to_stream_with_prefix_undisposed_with_context},
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
*,
@@ -922,7 +922,7 @@ pub fn generate_route_list_with_exclusions<IV>(
where
IV: IntoView + 'static,
{
let mut routes = leptos_router::generate_route_list_inner(app_fn);
let (mut routes, mut api_routes) = leptos_router::generate_route_list_inner(app_fn);
// Empty strings screw with Actix pathing, they need to be "/"
routes = routes
@@ -1113,6 +1113,14 @@ where
}
}
/// Defines an API route, which mounts the given route handler at this path.
#[component(transparent)]
pub fn Api<P>(cx: leptos::Scope, path: P, route: actix_web::Route) -> impl IntoView
where P: Into<String>
{
Transparent::new(ApiRouteListing::new(path.into(), route))
}
/// A helper to make it easier to use Axum extractors in server functions. This takes
/// a handler function as its argument. The handler follows similar rules to an Actix
/// [Handler](actix_web::Handler): it is an async function that receives arguments that

View File

@@ -5,6 +5,7 @@ use crate::{
RouteDefinition, RouteMatch,
},
use_is_back_navigation, RouteContext, RouterContext,
ApiRouteListing
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{
@@ -43,7 +44,7 @@ pub fn Routes(
#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
Branches::with(&base, |branches| {
*context.0.borrow_mut() = branches.to_vec()
*context.ui.borrow_mut() = branches.to_vec();
});
}
@@ -118,7 +119,7 @@ pub fn AnimatedRoutes(
#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
Branches::with(&base, |branches| {
*context.0.borrow_mut() = branches.to_vec()
*context.ui.borrow_mut() = branches.to_vec()
});
}
@@ -224,8 +225,12 @@ pub fn AnimatedRoutes(
pub(crate) struct Branches;
type AppRoutes = (Vec<Branch>, Vec<ApiRouteListing>);
thread_local! {
static BRANCHES: RefCell<HashMap<String, Vec<Branch>>> = RefCell::new(HashMap::new());
// map is indexed by base
// this allows multiple apps per server
static BRANCHES: RefCell<HashMap<String, AppRoutes>> = Default::default();
}
impl Branches {
@@ -233,29 +238,30 @@ impl Branches {
BRANCHES.with(|branches| {
let mut current = branches.borrow_mut();
if !current.contains_key(base) {
let mut branches = Vec::new();
let children = children
let mut branches = (Vec::new(), Vec::new());
let mut route_defs = Vec::new();
let mut api_routes = Vec::new();
for child in children
.as_children()
.iter()
.filter_map(|child| {
let def = child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>());
if def.is_none() {
.iter() {
let transparent = child.as_transparent();
if let Some(def) = transparent.and_then(|t| t.downcast_ref::<RouteDefinition>()) {
route_defs.push(def.clone());
} else if let Some(def) = transparent.and_then(|t| t.downcast_ref::<ApiRouteListing>()) {
api_routes.push(def.clone());
} else {
warn!(
"[NOTE] The <Routes/> component should \
include *only* <Route/>or <ProtectedRoute/> \
include *only* <Route/> or <ProtectedRoute/> or <ApiRoute/> \
components, or some \
#[component(transparent)] that returns a \
RouteDefinition."
);
}
def
})
.cloned()
.collect::<Vec<_>>();
}
create_branches(
&children,
&route_defs,
base,
&mut Vec::new(),
&mut branches,
@@ -272,7 +278,18 @@ impl Branches {
"Branches::initialize() should be called before \
Branches::with()",
);
cb(branches)
cb(&branches.0)
})
}
pub fn with_api_routes<T>(base: &str, cb: impl FnOnce(&[ApiRouteListing]) -> T) -> T {
BRANCHES.with(|branches| {
let branches = branches.borrow();
let branches = branches.get(base).expect(
"Branches::initialize() should be called before \
Branches::with()",
);
cb(&branches.1)
})
}
}
@@ -481,7 +498,7 @@ fn create_branches(
route_defs: &[RouteDefinition],
base: &str,
stack: &mut Vec<RouteData>,
branches: &mut Vec<Branch>,
branches: &mut (Vec<Branch>, Vec<ApiRouteListing>),
) {
for def in route_defs {
let routes = create_routes(def, base);
@@ -489,8 +506,8 @@ fn create_branches(
stack.push(route.clone());
if def.children.is_empty() {
let branch = create_branch(stack, branches.len());
branches.push(branch);
let branch = create_branch(stack, branches.0.len());
branches.0.push(branch);
} else {
create_branches(&def.children, &route.pattern, stack, branches);
}
@@ -500,7 +517,7 @@ fn create_branches(
}
if stack.is_empty() {
branches.sort_by_key(|branch| Reverse(branch.score));
branches.0.sort_by_key(|branch| Reverse(branch.score));
}
}

View File

@@ -2,14 +2,24 @@ use crate::{
Branch, Method, RouterIntegrationContext, ServerIntegration, SsrMode,
};
use leptos::*;
use std::{cell::RefCell, collections::HashSet, rc::Rc};
use std::{any::Any, cell::RefCell, collections::HashSet, rc::Rc, sync::Arc};
/// Context to contain all possible routes.
#[derive(Clone, Default, Debug)]
pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
pub struct PossibleBranchContext {
pub(crate) ui: Rc<RefCell<Vec<Branch>>>,
pub(crate) api: Rc<RefCell<Vec<ApiRouteListing>>>
}
#[derive(Clone, Debug)]
/// A route that this application can serve.
pub enum PossibleRouteListing {
View(RouteListing),
Api(ApiRouteListing)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
/// A route that this application can serve.
/// Route listing for a component-based view.
pub struct RouteListing {
path: String,
mode: SsrMode,
@@ -46,12 +56,69 @@ impl RouteListing {
}
}
#[derive(Clone)]
/// Route listing for an API route.
pub struct ApiRouteListing {
path: String,
methods: Option<HashSet<Method>>,
// this will be downcast by the implementation
handler: Arc<dyn Any>
}
impl std::fmt::Debug for ApiRouteListing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiRouteListing").field("path", &self.path).field("methods", &self.methods).finish()
}
}
impl ApiRouteListing {
/// Create an API route listing from its parts.
pub fn new<T: 'static>(
path: impl ToString,
handler: T
) -> Self {
Self {
path: path.to_string(),
methods: None,
handler: Arc::new(handler)
}
}
/// Create an API route listing from its parts.
pub fn new_with_methods<T: 'static>(
path: impl ToString,
methods: impl IntoIterator<Item = Method>,
handler: T
) -> Self {
Self {
path: path.to_string(),
methods: Some(methods.into_iter().collect()),
handler: Arc::new(handler)
}
}
/// The path this route handles.
pub fn path(&self) -> &str {
&self.path
}
/// The HTTP request methods this path can handle.
pub fn methods(&self) -> impl Iterator<Item = Method> + '_ {
self.methods.iter().flatten().copied()
}
/// The handler for a request at this route
pub fn handler<T: 'static>(&self) -> Option<&T> {
self.handler.downcast_ref::<T>()
}
}
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
/// format. Odds are you want `generate_route_list()` from either the actix, axum, or viz integrations if you want
/// to work with their router
pub fn generate_route_list_inner<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<RouteListing>
) -> (Vec<RouteListing>, Vec<ApiRouteListing>)
where
IV: IntoView + 'static,
{
@@ -69,8 +136,8 @@ where
_ = app_fn(cx).into_view(cx);
leptos::suppress_resource_load(false);
let branches = branches.0.borrow();
branches
let ui_branches = branches.ui.borrow();
let ui = ui_branches
.iter()
.flat_map(|branch| {
let mode = branch
@@ -93,6 +160,12 @@ where
methods: methods.clone(),
})
})
.collect()
.collect();
let api_branches = branches.api.borrow();
let api = api_branches
.iter()
.cloned()
.collect();
(ui, api)
})
}