feat: use Axum SubStates to enable .with_state in Axum router (#1085)

This commit is contained in:
Ben Wishovich 2023-05-24 05:34:17 -07:00 committed by GitHub
parent 27f2a672ba
commit 06d28f7d67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 74 additions and 88 deletions

View file

@ -10,12 +10,10 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, view};
use crate::landing::App;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View file

@ -14,18 +14,17 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use errors_axum::*;
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
}}
//Define a handler to test extractor with state
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
State(options): State<Arc<LeptosOptions>>,
State(options): State<LeptosOptions>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
(*options).clone(),
options.clone(),
move |cx| {
provide_context(cx, id.clone());
},
@ -44,7 +43,7 @@ async fn main() {
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
@ -53,7 +52,7 @@ async fn main() {
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(
leptos_options.clone(),
&leptos_options,
routes,
|cx| view! { cx, <App/> },
)

View file

@ -11,12 +11,10 @@ if #[cfg(feature = "ssr")] {
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View file

@ -9,7 +9,6 @@ if #[cfg(feature = "ssr")] {
routing::get,
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
use hackernews_axum::fallback::file_and_error_handler;
#[tokio::main]
@ -17,7 +16,7 @@ if #[cfg(feature = "ssr")] {
use hackernews_axum::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
@ -26,7 +25,7 @@ if #[cfg(feature = "ssr")] {
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
.leptos_routes(&leptos_options, routes, |cx| view! { cx, <App/> } )
.fallback(file_and_error_handler)
.with_state(leptos_options);

View file

@ -22,7 +22,7 @@ leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
axum = { version = "0.6.1", optional = true }
axum = { version = "0.6.1", optional = true, features=["macros"] }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }

View file

@ -11,13 +11,11 @@ if #[cfg(feature = "ssr")] {
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View file

@ -4,6 +4,7 @@ pub mod auth;
pub mod error_template;
pub mod errors;
pub mod fallback;
pub mod state;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.

View file

@ -6,38 +6,38 @@ if #[cfg(feature = "ssr")] {
use axum::{
response::{Response, IntoResponse},
routing::get,
extract::{Path, State, Extension, RawQuery},
extract::{Path, State, RawQuery},
http::{Request, header::HeaderMap},
body::Body as AxumBody,
Router,
};
use session_auth_axum::todo::*;
use session_auth_axum::auth::*;
use session_auth_axum::state::AppState;
use session_auth_axum::*;
use session_auth_axum::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
use leptos::{log, view, provide_context, LeptosOptions, get_configuration};
use std::sync::Arc;
use leptos::{log, view, provide_context, get_configuration};
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
use axum_sessions_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
async fn server_fn_handler(State(app_state): State<AppState>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
request: Request<AxumBody>) -> impl IntoResponse {
log!("{:?}", path);
handle_server_fns_with_context(path, headers, raw_query, move |cx| {
provide_context(cx, auth_session.clone());
provide_context(cx, pool.clone());
provide_context(cx, app_state.pool.clone());
}, request).await
}
async fn leptos_routes_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
async fn leptos_routes_handler(auth_session: AuthSession, State(app_state): State<AppState>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context(app_state.leptos_options.clone(),
move |cx| {
provide_context(cx, auth_session.clone());
provide_context(cx, pool.clone());
provide_context(cx, app_state.pool.clone());
},
|cx| view! { cx, <TodoApp/> }
);
@ -68,20 +68,24 @@ if #[cfg(feature = "ssr")] {
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
let app_state = AppState{
leptos_options,
pool: pool.clone(),
};
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
.fallback(file_and_error_handler)
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
.with_config(auth_config))
.with_config(auth_config))
.layer(SessionLayer::new(session_store))
.layer(Extension(pool))
.with_state(leptos_options);
.with_state(app_state);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`

View file

@ -0,0 +1,17 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos::LeptosOptions;
use sqlx::SqlitePool;
use axum::extract::FromRef;
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
/// item in Axum's State. Leptos requires you to have leptosOptions in your State struct for the leptos route handlers
#[derive(FromRef, Debug, Clone)]
pub struct AppState{
pub leptos_options: LeptosOptions,
pub pool: SqlitePool
}
}
}

View file

@ -14,8 +14,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
use leptos::{LeptosOptions, view};
use crate::app::App;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View file

@ -5,11 +5,10 @@ async fn main() {
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
use std::sync::Arc;
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
@ -19,7 +18,7 @@ async fn main() {
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes(
leptos_options.clone(),
&leptos_options,
routes,
|cx| view! { cx, <App/> },
)

View file

@ -11,13 +11,11 @@ if #[cfg(feature = "ssr")] {
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::ErrorTemplate;
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, State(options): State<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();

View file

@ -15,11 +15,10 @@ cfg_if! {
use todo_app_sqlite_axum::*;
use crate::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
//Define a handler to test extractor with state
async fn custom_handler(Path(id): Path<String>, State(options): State<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
async fn custom_handler(Path(id): Path<String>, State(options): State<LeptosOptions>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context(options,
move |cx| {
provide_context(cx, id.clone());
},
@ -42,7 +41,7 @@ cfg_if! {
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = Arc::new(conf.leptos_options);
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
@ -50,7 +49,7 @@ cfg_if! {
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
.leptos_routes(&leptos_options, routes, |cx| view! { cx, <TodoApp/> } )
.fallback(file_and_error_handler)
.with_state(leptos_options);

View file

@ -7,8 +7,8 @@ if #[cfg(feature = "ssr")] {
errors::TodoAppError,
};
use http::Uri;
use leptos::{view, Errors, LeptosOptions};
use std::sync::Arc;
use leptos::{view, Errors, LeptosOptions};
use viz::{
handlers::serve, header::HeaderMap, types::RouteInfo, Body, Error, Handler,
Request, RequestExt, Response, ResponseExt, Result,
@ -18,7 +18,7 @@ if #[cfg(feature = "ssr")] {
let uri = req.uri().clone();
let headers = req.headers().clone();
let route_info = req.route_info().clone();
let options = &*req.state::<Arc<LeptosOptions>>().ok_or(
let options = req.state::<LeptosOptions>().ok_or(
Error::Responder(Response::text("missing state type LeptosOptions")),
)?;
let root = &options.site_root;

View file

@ -7,7 +7,6 @@ cfg_if! {
use crate::fallback::file_and_error_handler;
use crate::todo::*;
use leptos_viz::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
use todo_app_sqlite_viz::*;
use viz::{
types::{State, StateError},
@ -17,8 +16,8 @@ cfg_if! {
//Define a handler to test extractor with state
async fn custom_handler(req: Request) -> Result<Response> {
let id = req.params::<String>()?;
let options = &*req
.state::<Arc<LeptosOptions>>()
let options = req
.state::<LeptosOptions>()
.ok_or(StateError::new::<LeptosOptions>())?;
let handler = leptos_viz::render_app_to_stream_with_context(
options.clone(),
@ -59,7 +58,7 @@ cfg_if! {
|cx| view! { cx, <TodoApp/> },
)
.get("/*", file_and_error_handler)
.with(State(Arc::new(leptos_options)));
.with(State(leptos_options));
// run our app with hyper
// `viz::Server` is a re-export of `hyper::Server`

View file

@ -7,7 +7,7 @@
use axum::{
body::{Body, Bytes, Full, StreamBody},
extract::{Path, RawQuery},
extract::{FromRef, Path, RawQuery},
http::{
header::{HeaderName, HeaderValue},
HeaderMap, Request, StatusCode,
@ -1126,39 +1126,16 @@ where
}
}
/// This trait allows one to use your custom struct in Axum's router, provided it can provide the
/// `LeptosOptions` to use for the `LeptosRoutes` trait functions.
pub trait LeptosOptionProvider {
fn options(&self) -> LeptosOptions;
}
/// Implement `LeptosOptionProvider` trait for `LeptosOptions` itself.
impl LeptosOptionProvider for LeptosOptions {
fn options(&self) -> LeptosOptions {
self.clone()
}
}
/// Implement `LeptosOptionProvider` trait for any type wrapped in an Arc, if that type implements
/// `LeptosOptionProvider` as states in axum are often provided wrapped in an Arc.
impl<T> LeptosOptionProvider for Arc<T>
where
T: LeptosOptionProvider,
{
fn options(&self) -> LeptosOptions {
(**self).options()
}
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes<OP>
pub trait LeptosRoutes<S>
where
OP: LeptosOptionProvider,
LeptosOptions: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
fn leptos_routes<IV>(
self,
options: OP,
options: &S,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@ -1167,7 +1144,7 @@ where
fn leptos_routes_with_context<IV>(
self,
options: OP,
options: &S,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@ -1181,20 +1158,21 @@ where
handler: H,
) -> Self
where
H: axum::handler::Handler<T, OP, axum::body::Body>,
H: axum::handler::Handler<T, S, axum::body::Body>,
T: 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl<OP> LeptosRoutes<OP> for axum::Router<OP>
impl<S> LeptosRoutes<S> for axum::Router<S>
where
OP: LeptosOptionProvider + Clone + Send + Sync + 'static,
LeptosOptions: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
#[tracing::instrument(level = "info", fields(error), skip_all)]
fn leptos_routes<IV>(
self,
options: OP,
options: &S,
paths: Vec<RouteListing>,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
@ -1207,7 +1185,7 @@ where
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_context<IV>(
self,
options: OP,
options: &S,
paths: Vec<RouteListing>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@ -1225,7 +1203,7 @@ where
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.options(),
LeptosOptions::from_ref(options),
additional_context.clone(),
app_fn.clone(),
);
@ -1239,7 +1217,7 @@ where
}
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
options.options(),
LeptosOptions::from_ref(options),
additional_context.clone(),
app_fn.clone(),
true
@ -1254,7 +1232,7 @@ where
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.options(),
LeptosOptions::from_ref(options),
additional_context.clone(),
app_fn.clone(),
);
@ -1268,7 +1246,7 @@ where
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.options(),
LeptosOptions::from_ref(&options),
additional_context.clone(),
app_fn.clone(),
);
@ -1294,7 +1272,7 @@ where
handler: H,
) -> Self
where
H: axum::handler::Handler<T, OP, axum::body::Body>,
H: axum::handler::Handler<T, S, axum::body::Body>,
T: 'static,
{
let mut router = self;