Compare commits

..

25 Commits

Author SHA1 Message Date
Greg Johnston
ca3806e6bc v0.6.0-rc1 2024-01-24 21:35:14 -05:00
Greg Johnston
936c2077c3 Merge pull request #2222 from leptos-rs/2221
fix: `.refetch()` should not include any tracked reads
2024-01-24 20:44:51 -05:00
Greg Johnston
b3b18875c6 chore: allow unknown lints 2024-01-24 19:43:28 -05:00
Greg Johnston
5cbab48713 chore: avoid possible false positive in cargo check 2024-01-24 19:38:16 -05:00
Greg Johnston
5a8880dd2e fix: correctly track in the effect that creates the resource 2024-01-24 16:18:45 -05:00
Greg Johnston
ea6c957f3d fix: .refetch() should not track any reads (closes #2221) 2024-01-23 11:08:50 -05:00
Greg Johnston
694e5f1cb3 fix: cast to correct type on Memo::try_with_untracked 2024-01-23 11:08:33 -05:00
Greg Johnston
fce2c727ab feat: add support for custom encodings to #[server] macro (#2216) (closes #2210) 2024-01-21 16:14:02 -05:00
Greg Johnston
7d1ce45a57 chore: minimize features activated with leptos_axum's default feature (#1846) (#2213)
- `leptos_axum` default feature:
  - remove `tokio/full`, `axum/macros`
  - add `tokio/fs`, `tokio/sync`
- example `leptos-tailwind-axum`:
  - enable `tokio`'s `rt-multi-thread` and `macros` features
- example `ssr_modes_axum`:
  - enable `tokio`'s `rt-multi-thread` and `macros` features

Co-authored-by: Paul Nettleton <paulnett7@hotmail.com>
2024-01-21 15:22:46 -05:00
Niklas Eicker
997b99081b change: for static routes, remove .static and provide additional context for static_params closures (#2207) 2024-01-21 13:33:05 -05:00
Chris
d33e57d4b7 feat: Default for LeptosOptions, ConfFile (#2208)
Co-authored-by: chrisp60 <gh@cperry.me>
2024-01-21 13:26:10 -05:00
Greg Johnston
b450f0fd10 fix: don't enable tracing feature on leptos by default (#2211) 2024-01-20 17:58:22 -05:00
Greg Johnston
c84c6ee8cd Merge pull request #2158 from leptos-rs/leptos_v0.6 2024-01-20 15:58:25 -05:00
Greg Johnston
567644df8f clarify docs here 2024-01-20 14:29:22 -05:00
Greg Johnston
39f5481b8c clean up in docs and rename Axum extract() to match Actix extract() 2024-01-20 14:29:08 -05:00
Greg Johnston
c88bfbe0a0 tweak sets of features for CI 2024-01-20 14:18:25 -05:00
Greg Johnston
40da1fe94e clippy 2024-01-20 14:16:13 -05:00
Greg Johnston
8df46fcdb9 examples: use old Axum version of hackernews_js_fetch until supported by axum-js-fetch 2024-01-20 12:39:16 -05:00
Greg Johnston
b4a1d90327 clean up for CI 2024-01-20 12:32:51 -05:00
Chris
d746f83387 docs: View::render_to_string panic (#2200)
Co-authored-by: chrisp60 <gh@cperry.me>
2024-01-19 17:07:39 -08:00
Greg Johnston
2092c40bc7 missing derives 2024-01-19 18:21:57 -05:00
Greg Johnston
70ec0c2d0a update sso example 2024-01-19 18:02:22 -05:00
Greg Johnston
eb45d05f3b clippy 2024-01-19 17:43:05 -05:00
Greg Johnston
f19def9541 clippy 2024-01-19 16:55:16 -05:00
Greg Johnston
ddda785045 fix multipart support 2024-01-19 16:52:41 -05:00
57 changed files with 671 additions and 1089 deletions

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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");

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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

View File

@@ -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",
"${@}",
]

View File

@@ -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
```

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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
}

View File

@@ -1,4 +0,0 @@
pub mod action;
pub mod check;
pub mod find;
pub mod world;

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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)
}

View File

@@ -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>
}
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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")] {
}
}
}
}

View File

@@ -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>
}
}

View File

@@ -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),
)),
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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("/");

View File

@@ -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,
}

View File

@@ -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]

View File

@@ -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"

View File

@@ -1,5 +1,3 @@
#[cfg(feature = "ssr")]
pub mod middleware;
pub mod todo;
#[cfg(feature = "hydrate")]

View File

@@ -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)
})
}
}

View File

@@ -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::*;

View File

@@ -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)
/// }
/// ```

View File

@@ -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"]

View File

@@ -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:?}")))
}

View File

@@ -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()
}

View File

@@ -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,)

View File

@@ -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;

View File

@@ -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()

View File

@@ -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

View File

@@ -321,7 +321,7 @@ where
/// Creates an [MultiAction] that can be used to call a server function.
///
/// ```rust
/// ```rust,ignore
/// # use leptos::*;
///
/// #[server(MyServerFn)]

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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 }
}

View File

@@ -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"]]

View File

@@ -1,6 +1,6 @@
[package]
name = "server_fn_macro_default"
version = "0.6.0"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -60,7 +60,7 @@ pub use stream::*;
///
/// For example, heres 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, heres 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, heres 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, heres the implementation for [`Json`].
///
/// ```rust
/// ```rust,ignore
/// impl<CustErr, T, Response> FromRes<Json, Response, CustErr> for T
/// where
/// Response: ClientRes<CustErr> + Send,

View File

@@ -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`

View File

@@ -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!()
}
}
}

View File

@@ -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>),

View File

@@ -1,6 +1,6 @@
[package]
name = "server_fn_macro"
version = "0.6.0"
version = { workspace = true }
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -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;