diff --git a/Cargo.toml b/Cargo.toml index 15f8e1777..c379010e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,9 @@ members = [ "leptos_reactive", "leptos_server", + # integrations + "integrations/actix", + # libraries "meta", "router", diff --git a/examples/counter-isomorphic/Cargo.toml b/examples/counter-isomorphic/Cargo.toml index fb043cd53..4f04a8144 100644 --- a/examples/counter-isomorphic/Cargo.toml +++ b/examples/counter-isomorphic/Cargo.toml @@ -19,12 +19,12 @@ lazy_static = "1" leptos = { path = "../../../leptos/leptos", default-features = false, features = [ "serde", ] } +leptos_actix = { path = "../../../leptos/integrations/actix", optional = true } leptos_meta = { path = "../../../leptos/meta", default-features = false } leptos_router = { path = "../../../leptos/router", default-features = false } log = "0.4" simple_logger = "2" gloo = { git = "https://github.com/rustwasm/gloo" } -#counter = { path = "../counter", default-features = false} [features] default = ["csr"] @@ -34,10 +34,11 @@ ssr = [ "dep:actix-files", "dep:actix-web", "leptos/ssr", + "leptos_actix", "leptos_meta/ssr", "leptos_router/ssr", ] [package.metadata.cargo-all-features] -denylist = ["actix-files", "actix-web"] +denylist = ["actix-files", "actix-web", "leptos_actix"] skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]] diff --git a/examples/counter-isomorphic/src/main.rs b/examples/counter-isomorphic/src/main.rs index 449c8dd4d..111f63538 100644 --- a/examples/counter-isomorphic/src/main.rs +++ b/examples/counter-isomorphic/src/main.rs @@ -41,45 +41,6 @@ cfg_if! { )) } - #[post("/api/{tail:.*}")] - async fn handle_server_fns( - req: HttpRequest, - params: web::Path, - body: web::Bytes, - ) -> impl Responder { - 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 (cx, disposer) = raw_scope_and_disposer(); - match server_fn(cx, &body).await { - Ok(serialized) => { - // if this is Accept: application/json then send a serialized JSON response - if let Some("application/json") = accept_header { - HttpResponse::Ok().body(serialized) - } - // otherwise, it's probably a
submit or something: redirect back to the referrer - else { - HttpResponse::SeeOther() - .insert_header(("Location", "/")) - .content_type("application/json") - .body(serialized) - } - } - Err(e) => { - eprintln!("server function error: {e:#?}"); - HttpResponse::InternalServerError().body(e.to_string()) - } - } - } else { - HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) - } - } - #[get("/api/events")] async fn counter_events() -> impl Responder { use futures::StreamExt; @@ -105,7 +66,7 @@ cfg_if! { App::new() .service(Files::new("/pkg", "./pkg")) .service(counter_events) - .service(handle_server_fns) + .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) .service(render) //.wrap(middleware::Compress::default()) }) diff --git a/examples/todo-app-sqlite/Cargo.toml b/examples/todo-app-sqlite/Cargo.toml index 202e9be20..82955b4dd 100644 --- a/examples/todo-app-sqlite/Cargo.toml +++ b/examples/todo-app-sqlite/Cargo.toml @@ -19,6 +19,7 @@ cfg-if = "1" leptos = { path = "../../../leptos/leptos", default-features = false, features = [ "serde", ] } +leptos_actix = { path = "../../../leptos/integrations/actix", optional = true } leptos_meta = { path = "../../../leptos/meta", default-features = false } leptos_router = { path = "../../../leptos/router", default-features = false } log = "0.4" @@ -37,10 +38,11 @@ ssr = [ "dep:actix-web", "dep:sqlx", "leptos/ssr", + "leptos_actix", "leptos_meta/ssr", "leptos_router/ssr", ] [package.metadata.cargo-all-features] -denylist = ["actix-files", "actix-web", "sqlx"] +denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"] skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]] diff --git a/examples/todo-app-sqlite/src/main.rs b/examples/todo-app-sqlite/src/main.rs index aa2825c58..bb27c1be3 100644 --- a/examples/todo-app-sqlite/src/main.rs +++ b/examples/todo-app-sqlite/src/main.rs @@ -54,46 +54,6 @@ cfg_if! { ) } - #[post("/api/{tail:.*}")] - async fn handle_server_fns( - req: HttpRequest, - params: web::Path, - body: web::Bytes, - ) -> impl Responder { - 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 (cx, disposer) = raw_scope_and_disposer(); - provide_context(cx, req.clone()); - match server_fn(cx, &body).await { - Ok(serialized) => { - // if this is Accept: application/json then send a serialized JSON response - if let Some("application/json") = accept_header { - HttpResponse::Ok().body(serialized) - } - // otherwise, it's probably a submit or something: redirect back to the referrer - else { - HttpResponse::SeeOther() - .insert_header(("Location", "/")) - .content_type("application/json") - .body(serialized) - } - } - Err(e) => { - eprintln!("server function error: {e:#?}"); - HttpResponse::InternalServerError().body(e.to_string()) - } - } - } else { - HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) - } - } - #[actix_web::main] async fn main() -> std::io::Result<()> { let mut conn = db().await.expect("couldn't connect to DB"); @@ -107,7 +67,7 @@ cfg_if! { HttpServer::new(|| { App::new() .service(Files::new("/pkg", "./pkg")) - .service(handle_server_fns) + .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) .service(render) //.wrap(middleware::Compress::default()) }) diff --git a/integrations/actix/Cargo.toml b/integrations/actix/Cargo.toml new file mode 100644 index 000000000..dc07eb590 --- /dev/null +++ b/integrations/actix/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "leptos_actix" +version = "0.1.0" +edition = "2021" + +[dependencies] +actix-web = "4" +leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [ + "ssr", +] } diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs new file mode 100644 index 000000000..9b00c510c --- /dev/null +++ b/integrations/actix/src/lib.rs @@ -0,0 +1,86 @@ +use actix_web::*; +use leptos::*; + +/// An Actix [Route](actix_web::Route) that listens for a `POST` request with +/// Leptos server function arguments in the body, runs the server function if found, +/// and returns the resulting [HttpResponse]. +/// +/// The provides the [HttpRequest] to the server [Scope](leptos_reactive::Scope). +/// +/// This can then be set up at an appropriate route in your application: +/// +/// ``` +/// use actix_web::*; +/// +/// fn register_server_functions() { +/// // call ServerFn::register() for each of the server functions you've defined +/// } +/// +/// # if false { // don't actually try to run a server in a doctest... +/// #[actix_web::main] +/// async fn main() -> std::io::Result<()> { +/// // make sure you actually register your server functions +/// register_server_functions(); +/// +/// HttpServer::new(|| { +/// App::new() +/// // "/api" should match the prefix, if any, declared when defining server functions +/// // {tail:.*} passes the remainder of the URL as the server function name +/// .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) +/// }) +/// .bind(("127.0.0.1", 8080))? +/// .run() +/// .await +/// } +/// # } +/// ``` +pub fn handle_server_fns() -> Route { + web::post().to(handle_server_fn) +} + +async fn handle_server_fn( + req: HttpRequest, + params: web::Path, + body: web::Bytes, +) -> impl Responder { + 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 (cx, disposer) = raw_scope_and_disposer(); + + // provide HttpRequest as context in server scope + provide_context(cx, req.clone()); + + match server_fn(cx, body).await { + Ok(serialized) => { + // clean up the scope, which we only needed to run the server fn + disposer.dispose(); + + // if this is Accept: application/json then send a serialized JSON response + if let Some("application/json") = accept_header { + HttpResponse::Ok().body(serialized) + } + // otherwise, it's probably a submit or something: redirect back to the referrer + else { + let referer = req + .headers() + .get("Referer") + .and_then(|value| value.to_str().ok()) + .unwrap_or("/"); + HttpResponse::SeeOther() + .insert_header(("Location", referer)) + .content_type("application/json") + .body(serialized) + } + } + Err(e) => HttpResponse::InternalServerError().body(e.to_string()), + } + } else { + HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) + } +}