mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
1 Commits
2693
...
api-routes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
beea04a050 |
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user