mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 07:52:34 -05:00
Compare commits
25 Commits
actix_midd
...
0.6.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca3806e6bc | ||
|
|
936c2077c3 | ||
|
|
b3b18875c6 | ||
|
|
5cbab48713 | ||
|
|
5a8880dd2e | ||
|
|
ea6c957f3d | ||
|
|
694e5f1cb3 | ||
|
|
fce2c727ab | ||
|
|
7d1ce45a57 | ||
|
|
997b99081b | ||
|
|
d33e57d4b7 | ||
|
|
b450f0fd10 | ||
|
|
c84c6ee8cd | ||
|
|
567644df8f | ||
|
|
39f5481b8c | ||
|
|
c88bfbe0a0 | ||
|
|
40da1fe94e | ||
|
|
8df46fcdb9 | ||
|
|
b4a1d90327 | ||
|
|
d746f83387 | ||
|
|
2092c40bc7 | ||
|
|
70ec0c2d0a | ||
|
|
eb45d05f3b | ||
|
|
f19def9541 | ||
|
|
ddda785045 |
26
Cargo.toml
26
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.0-beta"
|
||||
version = "0.6.0-rc1"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.6.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.0-beta" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.0-beta" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.0-beta" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.0-beta" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.0-beta" }
|
||||
leptos = { path = "./leptos", version = "0.6.0-rc1" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.0-rc1" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.0-rc1" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.0-rc1" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.0-rc1" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.0-rc1" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.0-rc1" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.0-rc1" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.6.0-beta" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.0-beta" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.0-beta" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.0-rc1" }
|
||||
leptos_router = { path = "./router", version = "0.6.0-rc1" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.0-rc1" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.0-rc1" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -118,9 +118,9 @@ pub fn Counters() -> impl IntoView {
|
||||
// This is the typical pattern for a CRUD app
|
||||
#[component]
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let dec = create_action(|_| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
let dec = create_action(|_: &()| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(|_: &()| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(|_: &()| clear_server_count());
|
||||
let counter = create_resource(
|
||||
move || {
|
||||
(
|
||||
@@ -222,9 +222,10 @@ pub fn FormCounter() -> impl IntoView {
|
||||
#[component]
|
||||
pub fn MultiuserCounter() -> impl IntoView {
|
||||
let dec =
|
||||
create_action(|_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(|_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(|_| clear_server_count());
|
||||
create_action(|_: &()| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc =
|
||||
create_action(|_: &()| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(|_: &()| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
|
||||
@@ -21,7 +21,7 @@ async fn main() {
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
leptos::logging::log!("listening on {}", addr);
|
||||
println!("listening on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
|
||||
@@ -11,21 +11,22 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { version = "0.5", features = ["nightly"] }
|
||||
leptos_axum = { version = "0.5", default-features = false, optional = true }
|
||||
leptos_meta = { version = "0.5", features = ["nightly"] }
|
||||
leptos_router = { version = "0.5", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.4", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
axum = { version = "0.7", default-features = false, optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
http = { version = "1.0", optional = true }
|
||||
gloo-net = { version = "0.4.0", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6", default-features = false, optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
http = { version = "0.2.11", optional = true }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"AbortController",
|
||||
"AbortSignal",
|
||||
@@ -33,10 +34,10 @@ web-sys = { version = "0.3", features = [
|
||||
"Response",
|
||||
] }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = { version = "0.4", features = [
|
||||
wasm-bindgen-futures = { version = "0.4.37", features = [
|
||||
"futures-core-03-stream",
|
||||
], optional = true }
|
||||
axum-js-fetch = { version = "0.2", optional = true }
|
||||
axum-js-fetch = { version = "0.2.1", optional = true }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
This example uses the basic Hacker News example as its basis, but shows how to run the server side as WASM running in a JS environment. In this example, Deno is used as the runtime.
|
||||
|
||||
**NOTE**: This example is slightly out of date pending an update to [`axum-js-fetch`](https://github.com/seanaye/axum-js-fetch/), which was waiting on a version of `gloo-net` that uses `http` 1.0. It still works with Leptos 0.5 and Axum 0.6, but not with the versions of Leptos (0.6 and later) that support Axum 1.0.
|
||||
|
||||
## Server Side Rendering with Deno
|
||||
|
||||
To run the Deno version, run
|
||||
|
||||
@@ -40,7 +40,7 @@ pub fn hydrate() {
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
use crate::App;
|
||||
use axum::Router;
|
||||
use axum::{routing::post, Router};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use log::{info, Level};
|
||||
@@ -52,7 +52,7 @@ mod ssr_imports {
|
||||
#[wasm_bindgen]
|
||||
impl Handler {
|
||||
pub async fn new() -> Self {
|
||||
console_log::init_with_level(Level::Debug);
|
||||
_ = console_log::init_with_level(Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let leptos_options = LeptosOptions::builder()
|
||||
@@ -62,8 +62,9 @@ mod ssr_imports {
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
// build our application with a route
|
||||
let app: axum::Router = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
let app: axum::Router<(), axum::body::Body> = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, || view! { <App/> })
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.with_state(leptos_options);
|
||||
|
||||
info!("creating handler instance");
|
||||
|
||||
@@ -45,7 +45,6 @@ ssr = [
|
||||
"dep:leptos_axum",
|
||||
"dep:notify"
|
||||
]
|
||||
notify = ["dep:notify"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "leptos_axum"]
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
CLIENT_PROCESS_NAME = "todo_app_sqlite_axum"
|
||||
|
||||
[tasks.test-ui]
|
||||
cwd = "./e2e"
|
||||
command = "cargo"
|
||||
args = ["make", "test-ui", "${@}"]
|
||||
CLIENT_PROCESS_NAME = "server_fns_axum"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "todo_app_sqlite_axum_e2e"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.72"
|
||||
async-trait = "0.1.72"
|
||||
cucumber = "0.19.1"
|
||||
fantoccini = "0.19.3"
|
||||
pretty_assertions = "1.4.0"
|
||||
serde_json = "1.0.104"
|
||||
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] }
|
||||
url = "2.4.0"
|
||||
|
||||
[[test]]
|
||||
name = "app_suite"
|
||||
harness = false # Allow Cucumber to print output instead of libtest
|
||||
@@ -1,20 +0,0 @@
|
||||
extend = { path = "../../cargo-make/main.toml" }
|
||||
|
||||
[tasks.test]
|
||||
env = { RUN_AUTOMATICALLY = false }
|
||||
condition = { env_true = ["RUN_AUTOMATICALLY"] }
|
||||
|
||||
[tasks.ci]
|
||||
|
||||
[tasks.test-ui]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--test",
|
||||
"app_suite",
|
||||
"--",
|
||||
"--retry",
|
||||
"2",
|
||||
"--fail-fast",
|
||||
"${@}",
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
# E2E Testing
|
||||
|
||||
This example demonstrates e2e testing with Rust using executable requirements.
|
||||
|
||||
## Testing Stack
|
||||
|
||||
| | Role | Description |
|
||||
|---|---|---|
|
||||
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
|
||||
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
|
||||
| [Cargo Leptos ](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
|
||||
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome
|
||||
|
||||
## Testing Organization
|
||||
|
||||
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
|
||||
|
||||
Here is a brief overview of how things fit together.
|
||||
|
||||
```bash
|
||||
features
|
||||
└── {action}_{object}.feature # Specify test scenarios
|
||||
tests
|
||||
├── fixtures
|
||||
│ ├── action.rs # Perform a user action (click, type, etc.)
|
||||
│ ├── check.rs # Assert what a user can see/not see
|
||||
│ ├── find.rs # Query page elements
|
||||
│ ├── mod.rs
|
||||
│ └── world
|
||||
│ ├── action_steps.rs # Map Gherkin steps to user actions
|
||||
│ ├── check_steps.rs # Map Gherkin steps to user expectations
|
||||
│ └── mod.rs
|
||||
└── app_suite.rs # Test main
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
@add_todo
|
||||
Feature: Add Todo
|
||||
|
||||
Background:
|
||||
Given I see the app
|
||||
|
||||
@add_todo-see
|
||||
Scenario: Should see the todo
|
||||
Given I set the todo as Buy Bread
|
||||
When I click the Add button
|
||||
Then I see the todo named Buy Bread
|
||||
|
||||
@add_todo-style
|
||||
Scenario: Should see the pending todo
|
||||
When I add a todo as Buy Oranges
|
||||
Then I see the pending todo
|
||||
@@ -1,18 +0,0 @@
|
||||
@delete_todo
|
||||
Feature: Delete Todo
|
||||
|
||||
Background:
|
||||
Given I see the app
|
||||
|
||||
@serial
|
||||
@delete_todo-remove
|
||||
Scenario: Should not see the deleted todo
|
||||
Given I add a todo as Buy Yogurt
|
||||
When I delete the todo named Buy Yogurt
|
||||
Then I do not see the todo named Buy Yogurt
|
||||
|
||||
@serial
|
||||
@delete_todo-message
|
||||
Scenario: Should see the empty list message
|
||||
When I empty the todo list
|
||||
Then I see the empty list message is No tasks were found.
|
||||
@@ -1,12 +0,0 @@
|
||||
@open_app
|
||||
Feature: Open App
|
||||
|
||||
@open_app-title
|
||||
Scenario: Should see the home page title
|
||||
When I open the app
|
||||
Then I see the page title is My Tasks
|
||||
|
||||
@open_app-label
|
||||
Scenario: Should see the input label
|
||||
When I open the app
|
||||
Then I see the label of the input is Add a Todo
|
||||
@@ -1,14 +0,0 @@
|
||||
mod fixtures;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fixtures::world::AppWorld;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
AppWorld::cucumber()
|
||||
.fail_on_skipped()
|
||||
.run_and_exit("./features")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
use super::{find, world::HOST};
|
||||
use anyhow::Result;
|
||||
use fantoccini::Client;
|
||||
use std::result::Result::Ok;
|
||||
use tokio::{self, time};
|
||||
|
||||
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
|
||||
let url = format!("{}{}", HOST, path);
|
||||
client.goto(&url).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_todo(client: &Client, text: &str) -> Result<()> {
|
||||
fill_todo(client, text).await?;
|
||||
click_add_button(client).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fill_todo(client: &Client, text: &str) -> Result<()> {
|
||||
let textbox = find::todo_input(client).await;
|
||||
textbox.send_keys(text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn click_add_button(client: &Client) -> Result<()> {
|
||||
let add_button = find::add_button(client).await;
|
||||
add_button.click().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn empty_todo_list(client: &Client) -> Result<()> {
|
||||
let todos = find::todos(client).await;
|
||||
|
||||
for _todo in todos {
|
||||
let _ = delete_first_todo(client).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_first_todo(client: &Client) -> Result<()> {
|
||||
if let Some(element) = find::first_delete_button(client).await {
|
||||
element.click().await.expect("Failed to delete todo");
|
||||
time::sleep(time::Duration::from_millis(250)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_todo(client: &Client, text: &str) -> Result<()> {
|
||||
if let Some(element) = find::delete_button(client, text).await {
|
||||
element.click().await?;
|
||||
time::sleep(time::Duration::from_millis(250)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
use super::find;
|
||||
use anyhow::{Ok, Result};
|
||||
use fantoccini::{Client, Locator};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
pub async fn text_on_element(
|
||||
client: &Client,
|
||||
selector: &str,
|
||||
expected_text: &str,
|
||||
) -> Result<()> {
|
||||
let element = client
|
||||
.wait()
|
||||
.for_element(Locator::Css(selector))
|
||||
.await
|
||||
.expect(
|
||||
format!("Element not found by Css selector `{}`", selector)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let actual = element.text().await?;
|
||||
assert_eq!(&actual, expected_text);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn todo_present(
|
||||
client: &Client,
|
||||
text: &str,
|
||||
expected: bool,
|
||||
) -> Result<()> {
|
||||
let todo_present = is_todo_present(client, text).await;
|
||||
|
||||
assert_eq!(todo_present, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_todo_present(client: &Client, text: &str) -> bool {
|
||||
let todos = find::todos(client).await;
|
||||
|
||||
for todo in todos {
|
||||
let todo_title = todo.text().await.expect("Todo title not found");
|
||||
if todo_title == text {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn todo_is_pending(client: &Client) -> Result<()> {
|
||||
if let None = find::pending_todo(client).await {
|
||||
assert!(false, "Pending todo not found");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
use fantoccini::{elements::Element, Client, Locator};
|
||||
|
||||
pub async fn todo_input(client: &Client) -> Element {
|
||||
let textbox = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("input[name='title"))
|
||||
.await
|
||||
.expect("Todo textbox not found");
|
||||
|
||||
textbox
|
||||
}
|
||||
|
||||
pub async fn add_button(client: &Client) -> Element {
|
||||
let button = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("input[value='Add']"))
|
||||
.await
|
||||
.expect("");
|
||||
|
||||
button
|
||||
}
|
||||
|
||||
pub async fn first_delete_button(client: &Client) -> Option<Element> {
|
||||
if let Ok(element) = client
|
||||
.wait()
|
||||
.for_element(Locator::Css("li:first-child input[value='X']"))
|
||||
.await
|
||||
{
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn delete_button(client: &Client, text: &str) -> Option<Element> {
|
||||
let selector = format!("//*[text()='{text}']//input[@value='X']");
|
||||
if let Ok(element) =
|
||||
client.wait().for_element(Locator::XPath(&selector)).await
|
||||
{
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn pending_todo(client: &Client) -> Option<Element> {
|
||||
if let Ok(element) =
|
||||
client.wait().for_element(Locator::Css(".pending")).await
|
||||
{
|
||||
return Some(element);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn todos(client: &Client) -> Vec<Element> {
|
||||
let todos = client
|
||||
.find_all(Locator::Css("li"))
|
||||
.await
|
||||
.expect("Todo List not found");
|
||||
|
||||
todos
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod action;
|
||||
pub mod check;
|
||||
pub mod find;
|
||||
pub mod world;
|
||||
@@ -1,57 +0,0 @@
|
||||
use crate::fixtures::{action, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::{given, when};
|
||||
|
||||
#[given("I see the app")]
|
||||
#[when("I open the app")]
|
||||
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::goto_path(client, "").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I add a todo as (.*)$")]
|
||||
#[when(regex = "^I add a todo as (.*)$")]
|
||||
async fn i_add_a_todo_titled(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::add_todo(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given(regex = "^I set the todo as (.*)$")]
|
||||
async fn i_set_the_todo_as(world: &mut AppWorld, text: String) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::fill_todo(client, &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(regex = "I click the Add button$")]
|
||||
async fn i_click_the_button(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::click_add_button(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[when(regex = "^I delete the todo named (.*)$")]
|
||||
async fn i_delete_the_todo_named(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::delete_todo(client, text.as_str()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[given("the todo list is empty")]
|
||||
#[when("I empty the todo list")]
|
||||
async fn i_empty_the_todo_list(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
action::empty_todo_list(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use crate::fixtures::{check, world::AppWorld};
|
||||
use anyhow::{Ok, Result};
|
||||
use cucumber::then;
|
||||
|
||||
#[then(regex = "^I see the page title is (.*)$")]
|
||||
async fn i_see_the_page_title_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "h1", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the label of the input is (.*)$")]
|
||||
async fn i_see_the_label_of_the_input_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "label", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the todo named (.*)$")]
|
||||
async fn i_see_the_todo_is_present(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::todo_present(client, text.as_str(), true).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then("I see the pending todo")]
|
||||
async fn i_see_the_pending_todo(world: &mut AppWorld) -> Result<()> {
|
||||
let client = &world.client;
|
||||
|
||||
check::todo_is_pending(client).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I see the empty list message is (.*)$")]
|
||||
async fn i_see_the_empty_list_message_is(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::text_on_element(client, "ul p", &text).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[then(regex = "^I do not see the todo named (.*)$")]
|
||||
async fn i_do_not_see_the_todo_is_present(
|
||||
world: &mut AppWorld,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let client = &world.client;
|
||||
check::todo_present(client, text.as_str(), false).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
pub mod action_steps;
|
||||
pub mod check_steps;
|
||||
|
||||
use anyhow::Result;
|
||||
use cucumber::World;
|
||||
use fantoccini::{
|
||||
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
|
||||
};
|
||||
|
||||
pub const HOST: &str = "http://127.0.0.1:3000";
|
||||
|
||||
#[derive(Debug, World)]
|
||||
#[world(init = Self::new)]
|
||||
pub struct AppWorld {
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl AppWorld {
|
||||
async fn new() -> Result<Self, anyhow::Error> {
|
||||
let webdriver_client = build_client().await?;
|
||||
|
||||
Ok(Self {
|
||||
client: webdriver_client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_client() -> Result<Client, NewSessionError> {
|
||||
let mut cap = Capabilities::new();
|
||||
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
|
||||
cap.insert("goog:chromeOptions".to_string(), arg);
|
||||
|
||||
let client = ClientBuilder::native()
|
||||
.capabilities(cap)
|
||||
.connect("http://localhost:4444")
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
use futures::StreamExt;
|
||||
use http::Method;
|
||||
use leptos::{html::Input, *};
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{ActionForm, Route, Router, Routes};
|
||||
use server_fn::codec::{
|
||||
GetUrl, MultipartData, MultipartFormData, Rkyv, SerdeLite, StreamingText,
|
||||
TextStream,
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use server_fn::{
|
||||
codec::{
|
||||
Encoding, FromReq, FromRes, GetUrl, IntoReq, IntoRes, MultipartData,
|
||||
MultipartFormData, Rkyv, SerdeLite, StreamingText, TextStream,
|
||||
},
|
||||
request::{ClientReq, Req},
|
||||
response::{ClientRes, Res},
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::{
|
||||
@@ -50,6 +56,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
<RkyvExample/>
|
||||
<FileUpload/>
|
||||
<FileWatcher/>
|
||||
<CustomEncoding/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,15 +175,12 @@ pub fn WithAnAction() -> impl IntoView {
|
||||
<button
|
||||
on:click=move |_| {
|
||||
let text = input_ref.get().unwrap().value();
|
||||
action.dispatch(text);
|
||||
action.dispatch(text.into());
|
||||
// note: technically, this `action` takes `AddRow` (the server fn type) as its
|
||||
// argument
|
||||
//
|
||||
// however, `.dispatch()` takes `impl Into<I>`, and for any one-argument server
|
||||
// functions, `From<_>` is implemented between the server function type and the
|
||||
// type of this single argument
|
||||
//
|
||||
// so `action.dispatch(text)` means `action.dispatch(AddRow { text })`
|
||||
// however, for any one-argument server functions, `From<_>` is implemented between
|
||||
// the server function type and the type of this single argument
|
||||
}
|
||||
>
|
||||
Submit
|
||||
@@ -506,3 +510,125 @@ pub fn CustomErrorTypes() -> impl IntoView {
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
/// Server function encodings are just types that implement a few traits.
|
||||
/// This means that you can implement your own encodings, by implementing those traits!
|
||||
///
|
||||
/// Here, we'll create a custom encoding that serializes and deserializes the server fn
|
||||
/// using TOML. Why would you ever want to do this? I don't know, but you can!
|
||||
pub struct Toml;
|
||||
|
||||
/// A newtype wrapper around server fn data that will be TOML-encoded.
|
||||
///
|
||||
/// This is needed because of Rust rules around implementing foreign traits for foreign types.
|
||||
/// It will be fed into the `custom = ` argument to the server fn below.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TomlEncoded<T>(T);
|
||||
|
||||
impl Encoding for Toml {
|
||||
const CONTENT_TYPE: &'static str = "application/toml";
|
||||
const METHOD: Method = Method::POST;
|
||||
}
|
||||
|
||||
impl<T, Request, Err> IntoReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: ClientReq<Err>,
|
||||
T: Serialize,
|
||||
{
|
||||
fn into_req(
|
||||
self,
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
) -> Result<Request, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Request::try_new_post(path, Toml::CONTENT_TYPE, accepts, data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Request, Err> FromReq<Toml, Request, Err> for TomlEncoded<T>
|
||||
where
|
||||
Request: Req<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
|
||||
let string_data = req.try_into_string().await?;
|
||||
toml::from_str::<T>(&string_data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnError::Args(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Response, Err> IntoRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: Res<Err>,
|
||||
T: Serialize + Send,
|
||||
{
|
||||
async fn into_res(self) -> Result<Response, ServerFnError<Err>> {
|
||||
let data = toml::to_string(&self.0)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
Response::try_from_string(Toml::CONTENT_TYPE, data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Response, Err> FromRes<Toml, Response, Err> for TomlEncoded<T>
|
||||
where
|
||||
Response: ClientRes<Err> + Send,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
|
||||
let data = res.try_into_string().await?;
|
||||
toml::from_str(&data)
|
||||
.map(TomlEncoded)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WhyNotResult {
|
||||
original: String,
|
||||
modified: String,
|
||||
}
|
||||
|
||||
#[server(
|
||||
input = Toml,
|
||||
output = Toml,
|
||||
custom = TomlEncoded
|
||||
)]
|
||||
pub async fn why_not(
|
||||
original: String,
|
||||
addition: String,
|
||||
) -> Result<TomlEncoded<WhyNotResult>, ServerFnError> {
|
||||
// insert a simulated wait
|
||||
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
||||
Ok(TomlEncoded(WhyNotResult {
|
||||
modified: format!("{original}{addition}"),
|
||||
original,
|
||||
}))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn CustomEncoding() -> impl IntoView {
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
let (result, set_result) = create_signal("foo".to_string());
|
||||
|
||||
view! {
|
||||
<h3>Custom encodings</h3>
|
||||
<p>
|
||||
"This example creates a custom encoding that sends server fn data using TOML. Why? Well... why not?"
|
||||
</p>
|
||||
<input node_ref=input_ref placeholder="Type something here."/>
|
||||
<button
|
||||
on:click=move |_| {
|
||||
let value = input_ref.get().unwrap().value();
|
||||
spawn_local(async move {
|
||||
let new_value = why_not(value, ", but in TOML!!!".to_string()).await.unwrap();
|
||||
set_result(new_value.0.modified);
|
||||
});
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<p>{result}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,40 +7,39 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
oauth2 = {version="4.4.2",optional=true}
|
||||
oauth2 = { version = "4.4.2", optional = true }
|
||||
anyhow = "1.0.66"
|
||||
console_log = "1.0.0"
|
||||
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos"}
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router"}
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = {version="1.0.108", 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 }
|
||||
serde_json = { version = "1.0.108", optional = true }
|
||||
axum = { version = "0.7", optional = true, features = ["macros"] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
http = { version = "1" }
|
||||
sqlx = { version = "0.7", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_session_auth = { version = "0.9", features = [
|
||||
axum_session_auth = { version = "0.12", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.9", features = [
|
||||
axum_session = { version = "0.12", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
reqwest= {version="0.11",optional=true, features=["json"]}
|
||||
reqwest = { version = "0.11", optional = true, features = ["json"] }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
@@ -63,7 +62,9 @@ ssr = [
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/cargo-leptos.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
[env]
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
CLIENT_PROCESS_NAME = "sso_auth_axum"
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
use cfg_if::cfg_if;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::SqlitePool;
|
||||
use axum_session_auth::{SessionSqlitePool, Authentication, HasPermission};
|
||||
pub type AuthSession = axum_session_auth::AuthSession<User, i64, SessionSqlitePool, SqlitePool>;
|
||||
}}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
@@ -28,36 +20,56 @@ impl Default for User {
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr_imports {
|
||||
use super::User;
|
||||
pub use axum_session_auth::{
|
||||
Authentication, HasPermission, SessionSqlitePool,
|
||||
};
|
||||
pub use sqlx::SqlitePool;
|
||||
use std::collections::HashSet;
|
||||
pub type AuthSession = axum_session_auth::AuthSession<
|
||||
User,
|
||||
i64,
|
||||
SessionSqlitePool,
|
||||
SqlitePool,
|
||||
>;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
impl User {
|
||||
pub async fn get(id: i64, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
"SELECT token FROM user_permissions WHERE user_id = ?;",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
.bind(id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(sqluser.into_user(Some(sql_user_perms)))
|
||||
}
|
||||
|
||||
pub async fn get_from_email(email: &str, pool: &SqlitePool) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>("SELECT * FROM users WHERE email = ?")
|
||||
.bind(email)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
pub async fn get_from_email(
|
||||
email: &str,
|
||||
pool: &SqlitePool,
|
||||
) -> Option<Self> {
|
||||
let sqluser = sqlx::query_as::<_, SqlUser>(
|
||||
"SELECT * FROM users WHERE email = ?",
|
||||
)
|
||||
.bind(email)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
//lets just get all the tokens the user can use, we will only use the full permissions if modifing them.
|
||||
let sql_user_perms = sqlx::query_as::<_, SqlPermissionTokens>(
|
||||
@@ -84,7 +96,10 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
#[async_trait]
|
||||
impl Authentication<User, i64, SqlitePool> for User {
|
||||
async fn load_user(userid: i64, pool: Option<&SqlitePool>) -> Result<User, anyhow::Error> {
|
||||
async fn load_user(
|
||||
userid: i64,
|
||||
pool: Option<&SqlitePool>,
|
||||
) -> Result<User, anyhow::Error> {
|
||||
let pool = pool.unwrap();
|
||||
|
||||
User::get(userid, pool)
|
||||
@@ -123,9 +138,11 @@ if #[cfg(feature = "ssr")] {
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
|
||||
impl SqlUser {
|
||||
pub fn into_user(self, sql_user_perms: Option<Vec<SqlPermissionTokens>>) -> User {
|
||||
pub fn into_user(
|
||||
self,
|
||||
sql_user_perms: Option<Vec<SqlPermissionTokens>>,
|
||||
) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
email: self.email,
|
||||
@@ -141,4 +158,3 @@ if #[cfg(feature = "ssr")] {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ pub fn error_template(errors: RwSignal<Errors>) -> View {
|
||||
children= move | (_, error)| {
|
||||
let error_string = error.to_string();
|
||||
view! {
|
||||
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
use cfg_if::cfg_if;
|
||||
use crate::error_template::error_template;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::*;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::*;
|
||||
use crate::error_template::error_template;
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
leptos::logging::log!("{:?}:{}",res.status(),uri);
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
|| error_template(create_rw_signal(leptos::Errors::default())
|
||||
)
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
}
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
leptos::logging::log!("{:?}:{}", res.status(), uri);
|
||||
let handler =
|
||||
leptos_axum::render_app_to_stream(options.to_owned(), || {
|
||||
error_template(create_rw_signal(leptos::Errors::default()))
|
||||
});
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
pub mod auth;
|
||||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
pub mod sign_in_sign_up;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod state;
|
||||
use leptos::{leptos_dom::helpers::TimeoutHandle, *};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use sign_in_sign_up::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use crate::{
|
||||
state::AppState,
|
||||
auth::{AuthSession,User,SqlRefreshToken}
|
||||
};
|
||||
use oauth2::{
|
||||
reqwest::async_http_client,
|
||||
TokenResponse
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use crate::auth::ssr_imports::{AuthSession, SqlRefreshToken};
|
||||
pub use leptos::{use_context, ServerFnError};
|
||||
pub use oauth2::{reqwest::async_http_client, TokenResponse};
|
||||
pub use sqlx::SqlitePool;
|
||||
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
pub fn pool() -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>()
|
||||
.ok_or_else(|| ServerFnError::new("Pool missing."))
|
||||
}
|
||||
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
|
||||
}
|
||||
pub fn auth() -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>()
|
||||
.ok_or_else(|| ServerFnError::new("Auth session missing."))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +34,14 @@ pub struct Email(RwSignal<Option<String>>);
|
||||
pub struct ExpiresIn(RwSignal<u64>);
|
||||
#[server]
|
||||
pub async fn refresh_token(email: String) -> Result<u64, ServerFnError> {
|
||||
use crate::{auth::User, state::AppState};
|
||||
use ssr_imports::*;
|
||||
|
||||
let pool = pool()?;
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let user = User::get_from_email(&email, &pool)
|
||||
.await
|
||||
.ok_or(ServerFnError::ServerError("User not found".to_string()))?;
|
||||
.ok_or(ServerFnError::new("User not found"))?;
|
||||
|
||||
let refresh_secret = sqlx::query_as::<_, SqlRefreshToken>(
|
||||
"SELECT secret FROM google_refresh_tokens WHERE user_id = ?",
|
||||
@@ -77,6 +74,7 @@ pub async fn refresh_token(email: String) -> Result<u64, ServerFnError> {
|
||||
.await?;
|
||||
Ok(expires_in)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
@@ -143,20 +141,11 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use leptos::view;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
||||
@@ -1,136 +1,154 @@
|
||||
use cfg_if::cfg_if;
|
||||
use crate::ssr_imports::*;
|
||||
use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
http::Request,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_session::{Key, SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
|
||||
use leptos::{get_configuration, logging::log, provide_context, view};
|
||||
use leptos_axum::{
|
||||
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
|
||||
};
|
||||
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
|
||||
use sso_auth_axum::{
|
||||
auth::*, fallback::file_and_error_handler, state::AppState,
|
||||
};
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{IntoResponse},
|
||||
routing::get,
|
||||
extract::{Path, State, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
};
|
||||
use sso_auth_axum::auth::*;
|
||||
use sso_auth_axum::state::AppState;
|
||||
use sso_auth_axum::fallback::file_and_error_handler;
|
||||
use leptos_axum::{generate_route_list, handle_server_fns_with_context, LeptosRoutes};
|
||||
use leptos::{logging::log, view, provide_context, get_configuration};
|
||||
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
|
||||
use axum_session::{SessionConfig, SessionLayer, SessionStore,Key, SecurityMode};
|
||||
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
async fn server_fn_handler(
|
||||
State(app_state): State<AppState>,
|
||||
auth_session: AuthSession,
|
||||
path: Path<String>,
|
||||
request: Request<AxumBody>,
|
||||
) -> impl IntoResponse {
|
||||
log!("{:?}", path);
|
||||
|
||||
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 || {
|
||||
handle_server_fns_with_context(
|
||||
move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
}, request).await
|
||||
}
|
||||
},
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
axum::extract::State(option): axum::extract::State<leptos::LeptosOptions>,
|
||||
request: Request<AxumBody>,
|
||||
) -> axum::response::Response {
|
||||
let handler = leptos_axum::render_app_async_with_context(
|
||||
option.clone(),
|
||||
move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
move || view! { <sso_auth_axum::App/> },
|
||||
);
|
||||
pub async fn leptos_routes_handler(
|
||||
auth_session: AuthSession,
|
||||
State(app_state): State<AppState>,
|
||||
axum::extract::State(option): axum::extract::State<leptos::LeptosOptions>,
|
||||
request: Request<AxumBody>,
|
||||
) -> axum::response::Response {
|
||||
let handler = leptos_axum::render_app_async_with_context(
|
||||
option.clone(),
|
||||
move || {
|
||||
provide_context(app_state.clone());
|
||||
provide_context(auth_session.clone());
|
||||
provide_context(app_state.pool.clone());
|
||||
},
|
||||
move || view! { <sso_auth_axum::App/> },
|
||||
);
|
||||
|
||||
handler(request).await.into_response()
|
||||
}
|
||||
handler(request).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Info).expect("couldn't initialize logging");
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:sso.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect("sqlite:sso.db")
|
||||
.await
|
||||
.expect("Could not make pool.");
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default()
|
||||
.with_table_name("sessions_table")
|
||||
.with_key(Key::generate())
|
||||
.with_database_key(Key::generate());
|
||||
// .with_security_mode(SecurityMode::PerSession); // FIXME did this disappear?
|
||||
|
||||
// Auth section
|
||||
let session_config = SessionConfig::default()
|
||||
.with_table_name("sessions_table")
|
||||
.with_key(Key::generate())
|
||||
.with_database_key(Key::generate())
|
||||
.with_security_mode(SecurityMode::PerSession);
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(
|
||||
Some(pool.clone().into()),
|
||||
session_config,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let auth_config = AuthConfig::<i64>::default();
|
||||
let session_store = SessionStore::<SessionSqlitePool>::new(Some(pool.clone().into()), session_config).await.unwrap();
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
// 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 = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(sso_auth_axum::App);
|
||||
|
||||
|
||||
|
||||
// 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 = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(sso_auth_axum::App);
|
||||
|
||||
// We create our client using provided environment variables.
|
||||
// We create our client using provided environment variables.
|
||||
let client = oauth2::basic::BasicClient::new(
|
||||
oauth2::ClientId::new(std::env::var("G_AUTH_CLIENT_ID").expect("G_AUTH_CLIENT Env var to be set.")),
|
||||
Some(oauth2::ClientSecret::new(std::env::var("G_AUTH_SECRET").expect("G_AUTH_SECRET Env var to be set"))),
|
||||
oauth2::ClientId::new(
|
||||
std::env::var("G_AUTH_CLIENT_ID")
|
||||
.expect("G_AUTH_CLIENT Env var to be set."),
|
||||
),
|
||||
Some(oauth2::ClientSecret::new(
|
||||
std::env::var("G_AUTH_SECRET")
|
||||
.expect("G_AUTH_SECRET Env var to be set"),
|
||||
)),
|
||||
oauth2::AuthUrl::new(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth".to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
Some(
|
||||
oauth2::TokenUrl::new("https://oauth2.googleapis.com/token".to_string())
|
||||
.unwrap(),
|
||||
oauth2::TokenUrl::new(
|
||||
"https://oauth2.googleapis.com/token".to_string(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.set_redirect_uri(oauth2::RedirectUrl::new(std::env::var("REDIRECT_URL").expect("REDIRECT_URL Env var to be set")).unwrap());
|
||||
.set_redirect_uri(
|
||||
oauth2::RedirectUrl::new(
|
||||
std::env::var("REDIRECT_URL")
|
||||
.expect("REDIRECT_URL Env var to be set"),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let app_state = AppState {
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
client,
|
||||
};
|
||||
|
||||
let app_state = AppState{
|
||||
leptos_options,
|
||||
pool: pool.clone(),
|
||||
client,
|
||||
};
|
||||
// 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),
|
||||
)
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.with_state(app_state);
|
||||
|
||||
// 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))
|
||||
.layer(SessionLayer::new(session_store))
|
||||
.with_state(app_state);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// Only the server may directly connect to the database.
|
||||
}
|
||||
}
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use super::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
use oauth2::{
|
||||
AuthorizationCode,
|
||||
TokenResponse,
|
||||
reqwest::async_http_client,
|
||||
CsrfToken,
|
||||
Scope,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use crate::{
|
||||
auth::{User,SqlCsrfToken},
|
||||
state::AppState
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr_imports {
|
||||
pub use crate::{
|
||||
auth::{ssr_imports::SqlCsrfToken, User},
|
||||
state::AppState,
|
||||
};
|
||||
pub use oauth2::{
|
||||
reqwest::async_http_client, AuthorizationCode, CsrfToken, Scope,
|
||||
TokenResponse,
|
||||
};
|
||||
pub use serde_json::Value;
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn google_sso() -> Result<String, ServerFnError> {
|
||||
use crate::ssr_imports::*;
|
||||
use ssr_imports::*;
|
||||
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let pool = pool()?;
|
||||
|
||||
@@ -80,6 +80,9 @@ pub async fn handle_g_auth_redirect(
|
||||
provided_csrf: String,
|
||||
code: String,
|
||||
) -> Result<(String, u64), ServerFnError> {
|
||||
use crate::ssr_imports::*;
|
||||
use ssr_imports::*;
|
||||
|
||||
let oauth_client = expect_context::<AppState>().client;
|
||||
let pool = pool()?;
|
||||
let auth_session = auth()?;
|
||||
@@ -90,9 +93,7 @@ pub async fn handle_g_auth_redirect(
|
||||
.bind(provided_csrf)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
ServerFnError::ServerError(format!("CSRF_TOKEN error : {err:?}"))
|
||||
})?;
|
||||
.map_err(|err| ServerFnError::new(format!("CSRF_TOKEN error : {err:?}")))?;
|
||||
|
||||
let token_response = oauth_client
|
||||
.exchange_code(AuthorizationCode::new(code.clone()))
|
||||
@@ -118,7 +119,7 @@ pub async fn handle_g_auth_redirect(
|
||||
.expect("email to parse to string")
|
||||
.to_string()
|
||||
} else {
|
||||
return Err(ServerFnError::ServerError(format!(
|
||||
return Err(ServerFnError::new(format!(
|
||||
"Response from google has status of {}",
|
||||
response.status()
|
||||
)));
|
||||
@@ -193,6 +194,8 @@ pub fn HandleGAuth() -> impl IntoView {
|
||||
|
||||
#[server]
|
||||
pub async fn logout() -> Result<(), ServerFnError> {
|
||||
use crate::ssr_imports::*;
|
||||
|
||||
let auth = auth()?;
|
||||
auth.logout_user();
|
||||
leptos_axum::redirect("/");
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::extract::FromRef;
|
||||
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 struct AppState {
|
||||
pub leptos_options: LeptosOptions,
|
||||
pub pool: SqlitePool,
|
||||
pub client:oauth2::basic::BasicClient,
|
||||
}
|
||||
}
|
||||
pub client: oauth2::basic::BasicClient,
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ thiserror = "1"
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["time"], optional = true }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -16,7 +16,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4"
|
||||
tokio = { version = "1", optional = true }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"], optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod middleware;
|
||||
pub mod todo;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
use actix_web::{
|
||||
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||
Error,
|
||||
};
|
||||
use std::{
|
||||
future::{ready, Future, Ready},
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
pub struct LoggingLayer;
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> for LoggingLayer
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type InitError = ();
|
||||
type Transform = LoggingService<S>;
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(LoggingService { service }))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoggingService<S> {
|
||||
service: S,
|
||||
}
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for LoggingService<S>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future =
|
||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||
|
||||
forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
println!("1. Middleware running before server fn.");
|
||||
|
||||
let fut = self.service.call(req);
|
||||
|
||||
Box::pin(async move {
|
||||
let res = fut.await?;
|
||||
println!("3. Middleware running after server fn.");
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ pub async fn get_todos() -> Result<Vec<Todo>, ServerFnError> {
|
||||
}
|
||||
|
||||
#[server]
|
||||
#[middleware(crate::middleware::LoggingLayer)]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
use self::ssr::*;
|
||||
|
||||
|
||||
@@ -1369,8 +1369,7 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions, with a
|
||||
/// simpler API than [`extract()`].
|
||||
/// A helper to make it easier to use Axum extractors in server functions.
|
||||
///
|
||||
/// It is generic over some type `T` that implements [`FromRequest`] and can
|
||||
/// therefore be used in an extractor. The compiler can often infer this type.
|
||||
@@ -1383,7 +1382,11 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
|
||||
/// use actix_web::web::Query;
|
||||
/// use leptos_actix::*;
|
||||
///
|
||||
/// let Query(data) = extract().await?;
|
||||
///
|
||||
/// // do something with the data
|
||||
///
|
||||
/// Ok(data)
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@@ -34,5 +34,5 @@ tokio = { version = "1", features = ["net"] }
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
wasm = []
|
||||
default = ["tokio/full", "axum/macros"]
|
||||
default = ["tokio/fs", "tokio/sync"]
|
||||
experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! To run in this environment, you need to disable the default feature set and enable
|
||||
//! the `wasm` feature on `leptos_axum` in your `Cargo.toml`.
|
||||
//! ```toml
|
||||
//! leptos_axum = { version = "0.6.0-beta", default-features = false, features = ["wasm"] }
|
||||
//! leptos_axum = { version = "0.6.0-rc1", default-features = false, features = ["wasm"] }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Features
|
||||
@@ -164,7 +164,7 @@ pub fn generate_request_and_parts(
|
||||
}
|
||||
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
/// run the server function if found, and return the resulting [`Response`].
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
@@ -224,7 +224,7 @@ macro_rules! spawn_task {
|
||||
}
|
||||
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
/// run the server function if found, and return the resulting [`Response`].
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
@@ -234,15 +234,16 @@ macro_rules! spawn_task {
|
||||
/// of one that should work much like this one.
|
||||
///
|
||||
/// **NOTE**: If your server functions expect a context, make sure to provide it both in
|
||||
/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever
|
||||
/// [`handle_server_fns_with_context`] **and** in
|
||||
/// [`leptos_routes_with_context`](LeptosRoutes::leptos_routes_with_context) (or whatever
|
||||
/// rendering method you are using). During SSR, server functions are called by the rendering
|
||||
/// method, while subsequent calls from the client are handled by the server function handler.
|
||||
/// The same context needs to be provided to both handlers.
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub async fn handle_server_fns_with_context(
|
||||
additional_context: impl Fn() + 'static + Clone + Send,
|
||||
@@ -383,10 +384,10 @@ pub type PinnedHtmlStream =
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [RequestParts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -474,10 +475,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_in_order<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -515,10 +516,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -626,10 +627,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -787,10 +788,10 @@ async fn forward_stream(
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -917,10 +918,10 @@ fn provide_contexts(
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_async<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -959,10 +960,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_async_stream_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -1088,10 +1089,10 @@ where
|
||||
///
|
||||
/// ## Provided Context Types
|
||||
/// This function always provides context values including the following types:
|
||||
/// - [Parts]
|
||||
/// - [ResponseOptions]
|
||||
/// - [MetaContext](leptos_meta::MetaContext)
|
||||
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
|
||||
/// - [`Parts`]
|
||||
/// - [`ResponseOptions`]
|
||||
/// - [`MetaContext`](leptos_meta::MetaContext)
|
||||
/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext)
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn render_app_async_with_context<IV>(
|
||||
options: LeptosOptions,
|
||||
@@ -1766,18 +1767,18 @@ fn get_leptos_pool() -> LocalPoolHandle {
|
||||
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
|
||||
/// use axum::{extract::Query, http::Method};
|
||||
/// use leptos_axum::*;
|
||||
/// let Query(query) = extractor().await?;
|
||||
/// let Query(query) = extract().await?;
|
||||
///
|
||||
/// Ok(query)
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extractor<T, CustErr>() -> Result<T, ServerFnError>
|
||||
pub async fn extract<T, CustErr>() -> Result<T, ServerFnError>
|
||||
where
|
||||
T: Sized + FromRequestParts<()>,
|
||||
T::Rejection: Debug,
|
||||
CustErr: Error + 'static,
|
||||
{
|
||||
extractor_with_state::<T, (), CustErr>(&()).await
|
||||
extract_with_state::<T, (), CustErr>(&()).await
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This
|
||||
@@ -1794,12 +1795,12 @@ where
|
||||
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
|
||||
/// use axum::{extract::Query, http::Method};
|
||||
/// use leptos_axum::*;
|
||||
/// let Query(query) = extractor().await?;
|
||||
/// let Query(query) = extract().await?;
|
||||
///
|
||||
/// Ok(query)
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extractor_with_state<T, S, CustErr>(
|
||||
pub async fn extract_with_state<T, S, CustErr>(
|
||||
state: &S,
|
||||
) -> Result<T, ServerFnError>
|
||||
where
|
||||
@@ -1813,7 +1814,7 @@ where
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
T::from_request_parts(&mut parts, &state)
|
||||
T::from_request_parts(&mut parts, state)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(format!("{e:?}")))
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
/// occur with LeptosOptions
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[derive(Clone, Debug, serde::Deserialize, Default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ConfFile {
|
||||
pub leptos_options: LeptosOptions,
|
||||
@@ -27,7 +27,7 @@ pub struct ConfFile {
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LeptosOptions {
|
||||
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
|
||||
#[builder(setter(into))]
|
||||
#[builder(setter(into), default=default_output_name())]
|
||||
pub output_name: String,
|
||||
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
|
||||
/// tools.
|
||||
@@ -112,6 +112,16 @@ impl LeptosOptions {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LeptosOptions {
|
||||
fn default() -> Self {
|
||||
LeptosOptions::builder().build()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_name() -> String {
|
||||
env!("CARGO_CRATE_NAME").replace('-', "_")
|
||||
}
|
||||
|
||||
fn default_site_root() -> String {
|
||||
".".to_string()
|
||||
}
|
||||
|
||||
@@ -363,6 +363,13 @@ fn fragments_to_chunks(
|
||||
|
||||
impl View {
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
///
|
||||
/// This is __NOT__ the same as [`render_to_string`]. This
|
||||
/// functions differs in that it assumes a runtime is in scope.
|
||||
/// [`render_to_string`] creates, and disposes of a runtime for you.
|
||||
///
|
||||
/// # Panics
|
||||
/// When called in a scope without a runtime. Use [`render_to_string`] instead.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#![forbid(unsafe_code)]
|
||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||
#![allow(stable_features)]
|
||||
// FIXME? every use of quote! {} is warning here -- false positive?
|
||||
#![allow(unknown_lints)]
|
||||
#![allow(private_macro_use)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error;
|
||||
|
||||
@@ -357,7 +357,12 @@ impl<T> SignalWithUntracked for Memo<T> {
|
||||
#[inline]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
|
||||
with_runtime(|runtime| {
|
||||
self.id.try_with_no_subscription(runtime, |v: &T| f(v)).ok()
|
||||
self.id
|
||||
.try_with_no_subscription(runtime, |v: &Option<T>| {
|
||||
v.as_ref().map(f)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
|
||||
@@ -8,8 +8,8 @@ use crate::{
|
||||
signal_prelude::format_signal_warning, spawn::spawn_local,
|
||||
suspense::LocalStatus, use_context, GlobalSuspenseContext, Memo,
|
||||
ReadSignal, ScopeProperty, Signal, SignalDispose, SignalGet,
|
||||
SignalGetUntracked, SignalSet, SignalUpdate, SignalWith, SuspenseContext,
|
||||
WriteSignal,
|
||||
SignalGetUntracked, SignalSet, SignalUpdate, SignalWith,
|
||||
SignalWithUntracked, SuspenseContext, WriteSignal,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -244,6 +244,7 @@ where
|
||||
create_isomorphic_effect({
|
||||
let r = Rc::clone(&r);
|
||||
move |_| {
|
||||
source.track();
|
||||
load_resource(id, r.clone());
|
||||
}
|
||||
});
|
||||
@@ -1358,7 +1359,7 @@ where
|
||||
self.version.set(version);
|
||||
self.scheduled.set(false);
|
||||
|
||||
_ = self.source.try_with(|source| {
|
||||
_ = self.source.try_with_untracked(|source| {
|
||||
let fut = (self.fetcher)(source.clone());
|
||||
|
||||
// `scheduled` is true for the rest of this code only
|
||||
|
||||
@@ -321,7 +321,7 @@ where
|
||||
|
||||
/// Creates an [MultiAction] that can be used to call a server function.
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// #[server(MyServerFn)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.6.0-beta"
|
||||
version = "0.6.0-rc1"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -20,10 +20,10 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
csr = ["leptos/csr", "leptos/tracing"]
|
||||
hydrate = ["leptos/hydrate", "leptos/tracing"]
|
||||
ssr = ["leptos/ssr", "leptos/tracing"]
|
||||
nightly = ["leptos/nightly", "leptos/tracing"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
nightly = ["leptos/nightly"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.6.0-beta"
|
||||
version = "0.6.0-rc1"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::{RouteListing, RouterIntegrationContext, ServerIntegration};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::{provide_context, IntoView, LeptosOptions};
|
||||
use leptos::{create_runtime, provide_context, IntoView, LeptosOptions};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_meta::MetaContext;
|
||||
use linear_map::LinearMap;
|
||||
@@ -204,9 +204,8 @@ impl ResolvedStaticPath {
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let html = self.build(options, app_fn, additional_context).await;
|
||||
let path = Path::new(&options.site_root)
|
||||
.join(format!("{}.static.html", self.0.trim_start_matches('/')));
|
||||
|
||||
let file_path = static_file_path(options, &self.0);
|
||||
let path = Path::new(&file_path);
|
||||
if let Some(path) = path.parent() {
|
||||
std::fs::create_dir_all(path)?
|
||||
}
|
||||
@@ -247,12 +246,15 @@ where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut static_data: HashMap<&str, StaticParamsMap> = HashMap::new();
|
||||
let runtime = create_runtime();
|
||||
additional_context();
|
||||
for (key, value) in static_data_map {
|
||||
match value {
|
||||
Some(value) => static_data.insert(key, value.as_ref()().await),
|
||||
None => static_data.insert(key, StaticParamsMap::default()),
|
||||
};
|
||||
}
|
||||
runtime.dispose();
|
||||
let static_routes = routes
|
||||
.iter()
|
||||
.filter(|route| route.static_mode().is_some())
|
||||
@@ -277,33 +279,6 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn purge_dir_of_static_files(path: PathBuf) -> Result<(), std::io::Error> {
|
||||
for entry in path.read_dir()? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
purge_dir_of_static_files(path)?;
|
||||
} else if path.is_file() {
|
||||
if let Some(name) = path.file_name().and_then(|i| i.to_str()) {
|
||||
if name.ends_with(".static.html") {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Purge all statically generated route files
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn purge_all_static_routes<IV>(
|
||||
options: &LeptosOptions,
|
||||
) -> Result<(), std::io::Error> {
|
||||
purge_dir_of_static_files(Path::new(&options.site_root).to_path_buf())
|
||||
}
|
||||
|
||||
pub type StaticData = Arc<StaticDataFn>;
|
||||
|
||||
pub type StaticDataFn = dyn Fn() -> Pin<Box<dyn Future<Output = StaticParamsMap> + Send + Sync>>
|
||||
@@ -350,17 +325,20 @@ pub enum StaticResponse {
|
||||
#[inline(always)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn static_file_path(options: &LeptosOptions, path: &str) -> String {
|
||||
format!("{}{}.static.html", options.site_root, path)
|
||||
let trimmed_path = path.trim_start_matches('/');
|
||||
let path = if trimmed_path.is_empty() {
|
||||
"index"
|
||||
} else {
|
||||
trimmed_path
|
||||
};
|
||||
format!("{}/{}.html", options.site_root, path)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[inline(always)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn not_found_path(options: &LeptosOptions) -> String {
|
||||
format!(
|
||||
"{}{}.static.html",
|
||||
options.site_root, options.not_found_path
|
||||
)
|
||||
format!("{}{}.html", options.site_root, options.not_found_path)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -443,7 +421,6 @@ where
|
||||
let body = ResolvedStaticPath(path.into())
|
||||
.build(options, app_fn, additional_context)
|
||||
.await;
|
||||
let path = Path::new(&options.site_root)
|
||||
.join(format!("{}.static.html", path.trim_start_matches('/')));
|
||||
let path = Path::new(&static_file_path(options, path)).into();
|
||||
StaticResponse::WriteFile { body, path }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ description = "RPC for any web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
server_fn_macro_default = { workspace = true}
|
||||
server_fn_macro_default = "0.6.0-rc1"
|
||||
# used for hashing paths in #[server] macro
|
||||
const_format = "0.2"
|
||||
xxhash-rust = { version = "0.8", features = ["const_xxh64"] }
|
||||
@@ -94,7 +94,7 @@ browser = [
|
||||
]
|
||||
json = []
|
||||
serde-lite = ["dep:serde-lite"]
|
||||
multipart = ["dep:multer"]
|
||||
multipart = ["browser", "dep:multer"]
|
||||
url = ["dep:serde_qs"]
|
||||
cbor = ["dep:ciborium"]
|
||||
rkyv = ["dep:rkyv"]
|
||||
@@ -105,3 +105,8 @@ ssr = ["inventory"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
# disables some feature combos for testing in CI
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["rustls", "default-tls", "form-redirects"]
|
||||
skip_feature_sets = [["actix", "axum"], ["browser", "actix"], ["browser", "axum"], ["browser", "reqwest"]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "server_fn_macro_default"
|
||||
version = "0.6.0"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -60,7 +60,7 @@ pub use stream::*;
|
||||
///
|
||||
/// For example, here’s the implementation for [`Json`].
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// impl<CustErr, T, Request> IntoReq<Json, Request, CustErr> for T
|
||||
/// where
|
||||
/// Request: ClientReq<CustErr>,
|
||||
@@ -98,7 +98,7 @@ pub trait IntoReq<Encoding, Request, CustErr> {
|
||||
///
|
||||
/// For example, here’s the implementation for [`Json`].
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// impl<CustErr, T, Request> FromReq<Json, Request, CustErr> for T
|
||||
/// where
|
||||
/// // require the Request implement `Req`
|
||||
@@ -137,7 +137,7 @@ where
|
||||
///
|
||||
/// For example, here’s the implementation for [`Json`].
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// impl<CustErr, T, Response> IntoRes<Json, Response, CustErr> for T
|
||||
/// where
|
||||
/// Response: Res<CustErr>,
|
||||
@@ -170,7 +170,7 @@ pub trait IntoRes<Encoding, Response, CustErr> {
|
||||
///
|
||||
/// For example, here’s the implementation for [`Json`].
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T
|
||||
/// where
|
||||
/// Response: ClientRes<CustErr> + Send,
|
||||
|
||||
@@ -61,7 +61,18 @@ impl From<ServerFnError> for Error {
|
||||
|
||||
/// An empty value indicating that there is no custom error type associated
|
||||
/// with this server function.
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Deserialize,
|
||||
Serialize,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Clone,
|
||||
Copy,
|
||||
)]
|
||||
pub struct NoCustomError;
|
||||
|
||||
// Implement `Display` for `NoCustomError`
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{future::Future, pin::Pin};
|
||||
|
||||
/// An abstraction over a middleware layer, which can be used to add additional
|
||||
/// middleware layer to a [`Service`].
|
||||
pub trait Layer<Req, Res>: 'static {
|
||||
pub trait Layer<Req, Res>: Send + Sync + 'static {
|
||||
/// Adds this layer to the inner service.
|
||||
fn layer(&self, inner: BoxedService<Req, Res>) -> BoxedService<Req, Res>;
|
||||
}
|
||||
@@ -104,53 +104,18 @@ mod axum {
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
mod actix {
|
||||
use super::BoxedService;
|
||||
use crate::{
|
||||
request::actix::ActixRequest,
|
||||
response::{actix::ActixResponse, Res},
|
||||
ServerFnError,
|
||||
};
|
||||
use actix_web::{
|
||||
dev::{Service, Transform},
|
||||
HttpRequest, HttpResponse,
|
||||
};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
impl<T> super::Layer<HttpRequest, HttpResponse> for T
|
||||
where
|
||||
T: Transform<BoxedService<HttpRequest, HttpResponse>, HttpRequest>
|
||||
+ 'static, /* + Send
|
||||
+ Sync
|
||||
+ 'static,*/
|
||||
{
|
||||
fn layer(
|
||||
&self,
|
||||
inner: BoxedService<HttpRequest, HttpResponse>,
|
||||
) -> BoxedService<HttpRequest, HttpResponse> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> super::Layer<ActixRequest, ActixResponse> for T
|
||||
where
|
||||
T: Transform<BoxedService<ActixRequest, ActixResponse>, HttpRequest>
|
||||
//+ Send
|
||||
//+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
fn layer(
|
||||
&self,
|
||||
inner: BoxedService<ActixRequest, ActixResponse>,
|
||||
) -> BoxedService<ActixRequest, ActixResponse> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> super::Service<HttpRequest, HttpResponse> for S
|
||||
where
|
||||
S: actix_web::dev::Service<HttpRequest, Response = HttpResponse>,
|
||||
@@ -192,46 +157,4 @@ mod actix {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req> Service<Req> for BoxedService<HttpRequest, HttpResponse> {
|
||||
type Response = HttpResponse;
|
||||
type Error = ServerFnError;
|
||||
type Future = Pin<
|
||||
Box<
|
||||
dyn Future<Output = Result<Self::Response, Self::Error>> + Send,
|
||||
>,
|
||||
>;
|
||||
|
||||
fn poll_ready(
|
||||
&self,
|
||||
ctx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn call(&self, req: Req) -> Self::Future {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req> Service<Req> for BoxedService<ActixRequest, ActixResponse> {
|
||||
type Response = HttpResponse;
|
||||
type Error = ServerFnError;
|
||||
type Future = Pin<
|
||||
Box<
|
||||
dyn Future<Output = Result<Self::Response, Self::Error>> + Send,
|
||||
>,
|
||||
>;
|
||||
|
||||
fn poll_ready(
|
||||
&self,
|
||||
ctx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Self::Error>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn call(&self, req: Req) -> Self::Future {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
use super::ClientReq;
|
||||
use crate::{
|
||||
client::get_server_url,
|
||||
error::{ServerFnError, ServerFnErrorErr},
|
||||
};
|
||||
use crate::{client::get_server_url, error::ServerFnError};
|
||||
use bytes::Bytes;
|
||||
use futures::{Stream, StreamExt};
|
||||
use futures::Stream;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{
|
||||
header::{ACCEPT, CONTENT_TYPE},
|
||||
Body,
|
||||
};
|
||||
use reqwest::header::{ACCEPT, CONTENT_TYPE};
|
||||
pub use reqwest::{multipart::Form, Client, Method, Request, Url};
|
||||
|
||||
pub(crate) static CLIENT: Lazy<Client> = Lazy::new(Client::new);
|
||||
@@ -97,12 +91,17 @@ impl<CustErr> ClientReq<CustErr> for Request {
|
||||
}
|
||||
|
||||
fn try_new_streaming(
|
||||
path: &str,
|
||||
accepts: &str,
|
||||
content_type: &str,
|
||||
body: impl Stream<Item = Bytes> + 'static,
|
||||
_path: &str,
|
||||
_accepts: &str,
|
||||
_content_type: &str,
|
||||
_body: impl Stream<Item = Bytes> + 'static,
|
||||
) -> Result<Self, ServerFnError<CustErr>> {
|
||||
todo!("Streaming requests are not yet implemented for reqwest.")
|
||||
// We run into a fundamental issue here.
|
||||
// To be a reqwest body, the type must be Sync
|
||||
// That means the streaming types need to be wrappers over Sync streams
|
||||
// However, Axum BodyDataStream is !Sync, so we can't use the same wrapper type there
|
||||
|
||||
/* let url = format!("{}{}", get_server_url(), path);
|
||||
let body = Body::wrap_stream(
|
||||
body.map(|chunk| Ok(chunk) as Result<Bytes, ServerFnErrorErr>),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "server_fn_macro"
|
||||
version = "0.6.0"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -72,7 +72,6 @@ pub fn server_macro_impl(
|
||||
client,
|
||||
custom_wrapper,
|
||||
} = args;
|
||||
_ = custom_wrapper; // TODO: this should be used to enable custom encodings
|
||||
let prefix = prefix.unwrap_or_else(|| Literal::string(default_path));
|
||||
let fn_path = fn_path.unwrap_or_else(|| Literal::string(""));
|
||||
let input_ident = match &input {
|
||||
@@ -117,6 +116,19 @@ pub fn server_macro_impl(
|
||||
Ident::new(&upper_camel_case_name, body.ident.span())
|
||||
});
|
||||
|
||||
// struct name, wrapped in any custom-encoding newtype wrapper
|
||||
let wrapped_struct_name = if let Some(wrapper) = custom_wrapper.as_ref() {
|
||||
quote! { #wrapper<#struct_name> }
|
||||
} else {
|
||||
quote! { #struct_name }
|
||||
};
|
||||
let wrapped_struct_name_turbofish =
|
||||
if let Some(wrapper) = custom_wrapper.as_ref() {
|
||||
quote! { #wrapper::<#struct_name> }
|
||||
} else {
|
||||
quote! { #struct_name }
|
||||
};
|
||||
|
||||
// build struct for type
|
||||
let mut body = body;
|
||||
let fn_name = &body.ident;
|
||||
@@ -268,12 +280,12 @@ pub fn server_macro_impl(
|
||||
#server_fn_path::inventory::submit! {{
|
||||
use #server_fn_path::{ServerFn, codec::Encoding};
|
||||
#server_fn_path::ServerFnTraitObj::new(
|
||||
#struct_name::PATH,
|
||||
<#struct_name as ServerFn>::InputEncoding::METHOD,
|
||||
#wrapped_struct_name_turbofish::PATH,
|
||||
<#wrapped_struct_name as ServerFn>::InputEncoding::METHOD,
|
||||
|req| {
|
||||
Box::pin(#struct_name::run_on_server(req))
|
||||
Box::pin(#wrapped_struct_name_turbofish::run_on_server(req))
|
||||
},
|
||||
#struct_name::middlewares
|
||||
#wrapped_struct_name_turbofish::middlewares
|
||||
)
|
||||
}}
|
||||
}
|
||||
@@ -283,6 +295,16 @@ pub fn server_macro_impl(
|
||||
|
||||
// run_body in the trait implementation
|
||||
let run_body = if cfg!(feature = "ssr") {
|
||||
let destructure = if let Some(wrapper) = custom_wrapper.as_ref() {
|
||||
quote! {
|
||||
let #wrapper(#struct_name { #(#field_names),* }) = self;
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
}
|
||||
};
|
||||
|
||||
// using the impl Future syntax here is thanks to Actix
|
||||
//
|
||||
// if we use Actix types inside the function, here, it becomes !Send
|
||||
@@ -292,7 +314,7 @@ pub fn server_macro_impl(
|
||||
//
|
||||
// however, SendWrapper<Future<Output = T>> impls Future<Output = T>
|
||||
let body = quote! {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
#destructure
|
||||
#dummy_name(#(#field_names),*).await
|
||||
};
|
||||
let body = if cfg!(feature = "actix") {
|
||||
@@ -333,13 +355,23 @@ pub fn server_macro_impl(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let restructure = if let Some(custom_wrapper) = custom_wrapper.as_ref()
|
||||
{
|
||||
quote! {
|
||||
let data = #custom_wrapper(#struct_name { #(#field_names),* });
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
let data = #struct_name { #(#field_names),* };
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#docs
|
||||
#(#attrs)*
|
||||
#[allow(unused_variables)]
|
||||
#vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty {
|
||||
use #server_fn_path::ServerFn;
|
||||
let data = #struct_name { #(#field_names),* };
|
||||
#restructure
|
||||
data.run_on_client().await
|
||||
}
|
||||
}
|
||||
@@ -416,12 +448,12 @@ pub fn server_macro_impl(
|
||||
} else if let Some(req_ty) = preset_req {
|
||||
req_ty.to_token_stream()
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
"If the `ssr` feature is enabled, either the `actix` or `axum` \
|
||||
features should also be enabled, or the `req = ` argument should \
|
||||
be provided to specify the request type.",
|
||||
));
|
||||
// fall back to the browser version, to avoid erroring out
|
||||
// in things like doctests
|
||||
// in reality, one of the above needs to be set
|
||||
quote! {
|
||||
#server_fn_path::request::BrowserMockReq
|
||||
}
|
||||
};
|
||||
let res = if !cfg!(feature = "ssr") {
|
||||
quote! {
|
||||
@@ -440,12 +472,12 @@ pub fn server_macro_impl(
|
||||
} else if let Some(res_ty) = preset_res {
|
||||
res_ty.to_token_stream()
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site(),
|
||||
"If the `ssr` feature is enabled, either the `actix` or `axum` \
|
||||
features should also be enabled, or the `res = ` argument should \
|
||||
be provided to specify the response type.",
|
||||
));
|
||||
// fall back to the browser version, to avoid erroring out
|
||||
// in things like doctests
|
||||
// in reality, one of the above needs to be set
|
||||
quote! {
|
||||
#server_fn_path::response::BrowserMockRes
|
||||
}
|
||||
};
|
||||
|
||||
// Remove any leading slashes, even if they exist (we'll add them below)
|
||||
@@ -509,7 +541,7 @@ pub fn server_macro_impl(
|
||||
|
||||
#from_impl
|
||||
|
||||
impl #server_fn_path::ServerFn for #struct_name {
|
||||
impl #server_fn_path::ServerFn for #wrapped_struct_name {
|
||||
const PATH: &'static str = #path;
|
||||
|
||||
type Client = #client;
|
||||
@@ -641,7 +673,7 @@ struct ServerFnArgs {
|
||||
req_ty: Option<Type>,
|
||||
res_ty: Option<Type>,
|
||||
client: Option<Type>,
|
||||
custom_wrapper: Option<Type>,
|
||||
custom_wrapper: Option<Path>,
|
||||
builtin_encoding: bool,
|
||||
}
|
||||
|
||||
@@ -659,7 +691,7 @@ impl Parse for ServerFnArgs {
|
||||
let mut req_ty: Option<Type> = None;
|
||||
let mut res_ty: Option<Type> = None;
|
||||
let mut client: Option<Type> = None;
|
||||
let mut custom_wrapper: Option<Type> = None;
|
||||
let mut custom_wrapper: Option<Path> = None;
|
||||
|
||||
let mut use_key_and_value = false;
|
||||
let mut arg_pos = 0;
|
||||
|
||||
Reference in New Issue
Block a user