commit f2418cfb9fa221460780fd7dae9301bde494f756
parent c8ac67bd8351bd41e50c6a32b70d9e4859d8d15c
Author: triesap <triesap@radroots.dev>
Date: Mon, 19 Jan 2026 07:56:42 +0000
app-lib: add query encoding helpers
- add encode_query_params helper with percent encoding
- add encode_route helper for query routes
- add unit tests for query encoding
- re-export query helpers from app lib
Diffstat:
4 files changed, 77 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1582,6 +1582,7 @@ dependencies = [
"regex",
"serde",
"serde-wasm-bindgen",
+ "url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
diff --git a/crates/app-lib/Cargo.toml b/crates/app-lib/Cargo.toml
@@ -15,6 +15,7 @@ serde-wasm-bindgen = { workspace = true }
js-sys = { workspace = true }
wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
+url = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
web-sys = { workspace = true }
diff --git a/crates/app-lib/src/lib.rs b/crates/app-lib/src/lib.rs
@@ -4,6 +4,7 @@ pub mod browser;
pub mod fetch;
pub mod geo;
pub mod path;
+pub mod query;
pub mod sleep;
pub mod storage;
pub mod symbols;
@@ -12,6 +13,7 @@ pub use browser::{browser_platform, BrowserPlatformInfo};
pub use fetch::{fetch_json, FetchJsonError, FetchJsonErrorKind, FetchJsonResult};
pub use geo::{geop_init, geop_is_valid, AppGeolocationPoint};
pub use path::{normalize_path, sanitize_path, trim_slashes};
+pub use query::{encode_query_params, encode_route};
pub use sleep::sleep;
pub use storage::{build_storage_key, build_storage_key_with_prefix, fmt_id, fmt_id_from_path};
pub use symbols::{
diff --git a/crates/app-lib/src/query.rs b/crates/app-lib/src/query.rs
@@ -0,0 +1,73 @@
+#![forbid(unsafe_code)]
+
+pub fn encode_query_params<K: AsRef<str>, V: AsRef<str>>(params: &[(K, V)]) -> String {
+ let mut output = String::new();
+ for (key, value) in params {
+ let key = key.as_ref().trim();
+ let value = value.as_ref().trim();
+ if key.is_empty() || value.is_empty() {
+ continue;
+ }
+ if !output.is_empty() {
+ output.push('&');
+ }
+ output.push_str(key);
+ output.push('=');
+ for part in url::form_urlencoded::byte_serialize(value.as_bytes()) {
+ for ch in part.chars() {
+ if ch == '+' {
+ output.push_str("%20");
+ } else {
+ output.push(ch);
+ }
+ }
+ }
+ }
+ if output.is_empty() {
+ String::new()
+ } else {
+ format!("?{output}")
+ }
+}
+
+pub fn encode_route<K: AsRef<str>, V: AsRef<str>>(route: &str, params: &[(K, V)]) -> String {
+ let query = encode_query_params(params);
+ if query.is_empty() {
+ return route.to_string();
+ }
+ let base = if route == "/" {
+ route.to_string()
+ } else {
+ let trimmed = route.trim_end_matches('/');
+ if trimmed.is_empty() {
+ "/".to_string()
+ } else {
+ trimmed.to_string()
+ }
+ };
+ format!("{base}{query}")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{encode_query_params, encode_route};
+
+ #[test]
+ fn encode_query_params_skips_empty_entries() {
+ let params = [("a", "b c"), ("", "skip"), ("c", "")];
+ assert_eq!(encode_query_params(¶ms), "?a=b%20c");
+ }
+
+ #[test]
+ fn encode_route_appends_query() {
+ let params = [("q", "1")];
+ assert_eq!(encode_route("/path/", ¶ms), "/path?q=1");
+ assert_eq!(encode_route("/", ¶ms), "/?q=1");
+ }
+
+ #[test]
+ fn encode_route_preserves_route_without_params() {
+ let params: [(&str, &str); 0] = [];
+ assert_eq!(encode_route("/path/", ¶ms), "/path/");
+ }
+}