Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion cot/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,22 @@ pub fn split_view_name(view_name: &str) -> (Option<&str>, &str) {
}
}

// used in the reverse! macro; not part of public API
#[doc(hidden)]
#[must_use]
pub fn reverse_with_query(url: String, query_params: &[(&str, String)]) -> String {
if query_params.is_empty() {
return url;
}

let mut serializer = form_urlencoded::Serializer::new(String::new());
for (key, value) in query_params {
serializer.append_pair(key, value);
}

format!("{url}?{}", serializer.finish())
}

/// A route that can be used to route requests to their respective views.
///
/// Non-empty route paths may omit the leading slash. Cot normalizes them by
Expand Down Expand Up @@ -807,6 +823,14 @@ enum RouteInner {
/// Returns a [`cot::Result<String>`] that contains the URL for the view. You
/// will typically want to append `?` to the macro call to get the URL.
///
/// # Query parameters
///
/// Path parameters are passed as `key = value` pairs right after the view name.
/// Query parameters can be added after a semicolon, using the same syntax. They
/// are appended to the generated URL as a percent-encoded query string, so
/// `reverse!(request, "home"; page = 2)` returns `/?page=2`. The values only
/// need to implement [`ToString`], just like path parameters do.
///
/// # Examples
///
/// ```
Expand All @@ -821,6 +845,9 @@ enum RouteInner {
/// let url = reverse!(request, "home")?;
/// let url = reverse!(request, "my_custom_app:home")?;
///
/// // with query parameters, this returns `/?page=2&search=cot`:
/// let url = reverse!(request, "home"; page = 2, search = "cot")?;
///
/// Ok(Html::new(format!(
/// "Hello! The URL for this view is: {}",
/// url
Expand Down Expand Up @@ -851,7 +878,10 @@ enum RouteInner {
/// ```
#[macro_export]
macro_rules! reverse {
($request:expr, $view_name:literal $(, $($key:ident = $value:expr),*)?) => {{
($request:expr, $view_name:literal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to have this for reverse_redirect! as well

$(, $($key:ident = $value:expr),* )?
$(; $($query_key:ident = $query_value:expr),* )?
) => {{
#[allow(
clippy::allow_attributes,
unused_imports,
Expand All @@ -863,6 +893,10 @@ macro_rules! reverse {
$request
.router()
.reverse(app_name, view_name, &$crate::reverse_param_map!($( $($key = $value),* )?))
.map(|url| $crate::router::reverse_with_query(
url,
&[$( $( (stringify!($query_key), ::std::string::ToString::to_string(&$query_value)) ),* )?],
))
}};
}

Expand Down Expand Up @@ -1296,6 +1330,44 @@ mod tests {
assert_eq!(url, "/test/123");
}

#[test]
fn test_reverse_macro_query_params() {
let route = Route::with_handler_and_name("/", MockHandler, "home");
let router = Router::with_urls(vec![route]);

let request = TestRequestBuilder::get("/").router(router).build();
let url = reverse!(request, "home"; page = 2, search = "cot").unwrap();

assert_eq!(url, "/?page=2&search=cot");
}

#[test]
fn test_reverse_macro_path_and_query_params() {
let route = Route::with_handler_and_name("/test/{id}", MockHandler, "test");
let router = Router::with_urls(vec![route]);

let request = TestRequestBuilder::get("/").router(router).build();
let url = reverse!(request, "test", id = 123; page = 2).unwrap();

assert_eq!(url, "/test/123?page=2");
}

#[test]
fn test_reverse_macro_query_params_are_encoded() {
let route = Route::with_handler_and_name("/", MockHandler, "home");
let router = Router::with_urls(vec![route]);

let request = TestRequestBuilder::get("/").router(router).build();
let url = reverse!(request, "home"; search = "hello world & cot").unwrap();

assert_eq!(url, "/?search=hello+world+%26+cot");
}

#[test]
fn reverse_with_query_leaves_url_unchanged_when_empty() {
assert_eq!(reverse_with_query("/test".to_string(), &[]), "/test");
}

#[test]
fn test_reverse_redirect_macro() {
let route = Route::with_handler_and_name("/test/{id}", MockHandler, "test");
Expand Down
Loading