app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 1+
Mcrates/app-lib/Cargo.toml | 1+
Mcrates/app-lib/src/lib.rs | 2++
Acrates/app-lib/src/query.rs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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(&params), "?a=b%20c"); + } + + #[test] + fn encode_route_appends_query() { + let params = [("q", "1")]; + assert_eq!(encode_route("/path/", &params), "/path?q=1"); + assert_eq!(encode_route("/", &params), "/?q=1"); + } + + #[test] + fn encode_route_preserves_route_without_params() { + let params: [(&str, &str); 0] = []; + assert_eq!(encode_route("/path/", &params), "/path/"); + } +}