diff --git a/crates/tuono/src/app.rs b/crates/tuono/src/app.rs index f3e58087..b9e42128 100644 --- a/crates/tuono/src/app.rs +++ b/crates/tuono/src/app.rs @@ -362,7 +362,7 @@ mod tests { ("/about", "/about"), ("/posts/index", "/posts"), ("/posts/any-post", "/posts/any-post"), - ("/posts/[post]", "/posts/:post"), + ("/posts/[post]", "/posts/{post}"), ]; results.into_iter().for_each(|(path, expected_path)| { diff --git a/crates/tuono/src/route.rs b/crates/tuono/src/route.rs index 40dcd176..241f701b 100644 --- a/crates/tuono/src/route.rs +++ b/crates/tuono/src/route.rs @@ -46,6 +46,20 @@ impl AxumInfo { } if route.is_dynamic { + let dyn_re = Regex::new(r"\[(.*?)\]").expect("Failed to create dyn regex"); + let catch_all_re = + Regex::new(r"\{\.\.\.(.*?)\}").expect("Failed to create catch all regex"); + + let dyn_result = dyn_re.replace_all(&axum_route, |caps: ®ex::Captures| { + format!("{{{}}}", &caps[1]) + }); + + let axum_route = catch_all_re + .replace_all(&dyn_result, |caps: ®ex::Captures| { + format!("{{*{}}}", &caps[1]) + }) + .to_string(); + return AxumInfo { module_import: module .as_str() @@ -55,10 +69,7 @@ impl AxumInfo { .replace('[', "dyn_") .replace("...", "catch_all_") .replace(']', ""), - axum_route: axum_route - .replace("[...", "*") - .replace('[', ":") - .replace(']', ""), + axum_route, }; } @@ -296,7 +307,7 @@ mod tests { let dyn_info = AxumInfo::new(&Route::new("/[posts]".to_string())); - assert_eq!(dyn_info.axum_route, "/:posts"); + assert_eq!(dyn_info.axum_route, "/{posts}"); assert_eq!(dyn_info.module_import, "dyn_posts"); } diff --git a/crates/tuono/tests/cli_build.rs b/crates/tuono/tests/cli_build.rs index f87a7001..52c6a5e4 100644 --- a/crates/tuono/tests/cli_build.rs +++ b/crates/tuono/tests/cli_build.rs @@ -127,19 +127,19 @@ fn it_successfully_create_catch_all_routes() { assert!(temp_main_rs_content.contains("mod dyn_catch_all_all_routes;")); assert!(temp_main_rs_content.contains( - r#".route("/api/*all_apis", post(api_dyn_catch_all_all_apis::post_tuono_internal_api))"# + r#".route("/api/{*all_apis}", post(api_dyn_catch_all_all_apis::post_tuono_internal_api))"# )); assert!(temp_main_rs_content.contains( - r#".route("/*all_routes", get(dyn_catch_all_all_routes::tuono_internal_route))"# + r#".route("/{*all_routes}", get(dyn_catch_all_all_routes::tuono_internal_route))"# )); assert!(temp_main_rs_content.contains( - r#".route("/*all_routes", get(dyn_catch_all_all_routes::tuono_internal_route))"# + r#".route("/{*all_routes}", get(dyn_catch_all_all_routes::tuono_internal_route))"# )); assert!(temp_main_rs_content.contains( - r#".route("/__tuono/data/*all_routes", get(dyn_catch_all_all_routes::tuono_internal_api))"# + r#".route("/__tuono/data/{*all_routes}", get(dyn_catch_all_all_routes::tuono_internal_api))"# )); } diff --git a/crates/tuono_lib/Cargo.toml b/crates/tuono_lib/Cargo.toml index 8041f8f3..d87587ac 100644 --- a/crates/tuono_lib/Cargo.toml +++ b/crates/tuono_lib/Cargo.toml @@ -18,8 +18,8 @@ include = [ [dependencies] ssr_rs = "0.8.1" -axum = {version = "0.7.5", features = ["json", "ws"]} -axum-extra = {version = "0.9.6", features = ["cookie"]} +axum = {version = "0.8.1", features = ["json", "ws"]} +axum-extra = {version = "0.10.0", features = ["cookie"]} tokio = { version = "1.37.0", features = ["full"] } serde = { version = "1.0.202", features = ["derive"] } erased-serde = "0.4.5" @@ -35,9 +35,9 @@ colored = "2.1.0" tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.17.8"} tuono_internal = {path = "../tuono_internal", version = "0.17.8"} # Match the same version used by axum -tokio-tungstenite = "0.24.0" +tokio-tungstenite = "0.26.0" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } -tungstenite = "0.24.0" +tungstenite = "0.26.0" http = "1.1.0" pin-project = "1.1.7" tower = "0.5.1" diff --git a/crates/tuono_lib/src/server.rs b/crates/tuono_lib/src/server.rs index 662ede82..8a7cf222 100644 --- a/crates/tuono_lib/src/server.rs +++ b/crates/tuono_lib/src/server.rs @@ -64,7 +64,7 @@ impl Server { .to_owned() .layer(LoggerLayer::new()) .route("/vite-server/", get(vite_websocket_proxy)) - .route("/vite-server/*path", get(vite_reverse_proxy)) + .route("/vite-server/{*path}", get(vite_reverse_proxy)) .fallback_service( ServeDir::new(DEV_PUBLIC_DIR) .fallback(get(catch_all).layer(LoggerLayer::new())), diff --git a/crates/tuono_lib/src/vite_websocket_proxy.rs b/crates/tuono_lib/src/vite_websocket_proxy.rs index cf61cbfc..863ffce6 100644 --- a/crates/tuono_lib/src/vite_websocket_proxy.rs +++ b/crates/tuono_lib/src/vite_websocket_proxy.rs @@ -1,5 +1,5 @@ use crate::config::GLOBAL_CONFIG; -use axum::extract::ws::{self, WebSocket, WebSocketUpgrade}; +use axum::extract::ws::{self, Utf8Bytes as AxumUtf8Bytes, WebSocket, WebSocketUpgrade}; use axum::response::IntoResponse; use futures_util::{SinkExt, StreamExt}; use tokio_tungstenite::connect_async; @@ -64,7 +64,7 @@ async fn handle_socket(mut tuono_socket: WebSocket) { while let Some(msg) = tuono_receiver.next().await { if let Ok(msg) = msg { let msg_to_vite = match msg.clone() { - ws::Message::Text(str) => Message::Text(str), + ws::Message::Text(str) => Message::Text(str.to_string().into()), ws::Message::Pong(payload) => Message::Pong(payload), ws::Message::Ping(payload) => Message::Ping(payload), ws::Message::Binary(payload) => Message::Binary(payload), @@ -91,7 +91,7 @@ async fn handle_socket(mut tuono_socket: WebSocket) { tokio::spawn(async move { while let Some(Ok(msg)) = vite_receiver.next().await { let msg_to_browser = match msg { - Message::Text(str) => ws::Message::Text(str), + Message::Text(str) => ws::Message::Text(AxumUtf8Bytes::from(str.to_string())), Message::Ping(payload) => ws::Message::Ping(payload), Message::Pong(payload) => ws::Message::Pong(payload), Message::Binary(payload) => ws::Message::Binary(payload), @@ -100,7 +100,7 @@ async fn handle_socket(mut tuono_socket: WebSocket) { Message::Close(_) => ws::Message::Close(None), _ => { eprintln!("Unexpected message from the vite WebSocket to the browser: {msg:?}"); - ws::Message::Text("Unhandled".to_string()) + ws::Message::Text("Unhandled".to_string().into()) } }; diff --git a/crates/tuono_lib/tests/server_test.rs b/crates/tuono_lib/tests/server_test.rs index 4ccba5d4..93b8c247 100644 --- a/crates/tuono_lib/tests/server_test.rs +++ b/crates/tuono_lib/tests/server_test.rs @@ -103,3 +103,47 @@ async fn api_route_route() { "{\"data\":\"{}\",\"info\":{\"redirect_destination\":null}}" ); } + +#[tokio::test] +#[serial] +async fn it_reads_the_catch_all_path_parameter() { + let app = MockTuonoServer::spawn().await; + + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + + let server_url = format!("http://{}:{}", &app.address, &app.port); + + let response = client + .get(format!("{server_url}/catch_all/url_parameter")) + .send() + .await + .expect("Failed to execute request."); + + assert!(response.status().is_success()); + assert_eq!(response.text().await.unwrap(), "url_parameter"); +} + +#[tokio::test] +#[serial] +async fn it_reads_the_path_parameter() { + let app = MockTuonoServer::spawn().await; + + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + + let server_url = format!("http://{}:{}", &app.address, &app.port); + + let response = client + .get(format!("{server_url}/dynamic/url_parameter")) + .send() + .await + .expect("Failed to execute request."); + + assert!(response.status().is_success()); + assert_eq!(response.text().await.unwrap(), "url_parameter"); +} diff --git a/crates/tuono_lib/tests/utils/catch_all.rs b/crates/tuono_lib/tests/utils/catch_all.rs new file mode 100644 index 00000000..b49edda1 --- /dev/null +++ b/crates/tuono_lib/tests/utils/catch_all.rs @@ -0,0 +1,11 @@ +use tuono_lib::Request; + +#[tuono_lib::api(GET)] +async fn read_catch_all_parameter(req: Request) -> String { + let param = req + .params + .get("catch_all") + .expect("Failed to get the catch_all param"); + + param.to_string() +} diff --git a/crates/tuono_lib/tests/utils/dynamic_parameter.rs b/crates/tuono_lib/tests/utils/dynamic_parameter.rs new file mode 100644 index 00000000..c7d3f269 --- /dev/null +++ b/crates/tuono_lib/tests/utils/dynamic_parameter.rs @@ -0,0 +1,11 @@ +use tuono_lib::Request; + +#[tuono_lib::api(GET)] +async fn read_dynamic_parameter(req: Request) -> String { + let param = req + .params + .get("parameter") + .expect("Failed to get the catch_all param"); + + param.to_string() +} diff --git a/crates/tuono_lib/tests/utils/mock_server.rs b/crates/tuono_lib/tests/utils/mock_server.rs index ee904363..4f490847 100644 --- a/crates/tuono_lib/tests/utils/mock_server.rs +++ b/crates/tuono_lib/tests/utils/mock_server.rs @@ -7,6 +7,8 @@ use tempfile::{tempdir, TempDir}; use tuono_lib::axum::routing::get; use tuono_lib::{axum::Router, tuono_internal_init_v8_platform, Mode, Server}; +use crate::utils::catch_all::get_tuono_internal_api as catch_all; +use crate::utils::dynamic_parameter::get_tuono_internal_api as dynamic_parameter; use crate::utils::health_check::get_tuono_internal_api as health_check; use crate::utils::route as html_route; use crate::utils::route::tuono_internal_api as route_api; @@ -76,7 +78,9 @@ impl MockTuonoServer { .route("/", get(html_route::tuono_internal_route)) .route("/tuono/data", get(html_route::tuono_internal_api)) .route("/health_check", get(health_check)) - .route("/route-api", get(route_api)); + .route("/route-api", get(route_api)) + .route("/catch_all/{*catch_all}", get(catch_all)) + .route("/dynamic/{parameter}", get(dynamic_parameter)); let server = Server::init(router, Mode::Prod).await; diff --git a/crates/tuono_lib/tests/utils/mod.rs b/crates/tuono_lib/tests/utils/mod.rs index 93dc2e9d..ed23aaf4 100644 --- a/crates/tuono_lib/tests/utils/mod.rs +++ b/crates/tuono_lib/tests/utils/mod.rs @@ -1,3 +1,5 @@ +pub mod catch_all; +pub mod dynamic_parameter; pub mod health_check; pub mod mock_server; pub mod route;