From e76fa5a5c23dcb2e1ec82b198270970a43f8d9de Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Sun, 14 Jun 2026 22:17:12 +0300 Subject: [PATCH] refactor(portal): move path and query helpers into module --- adk-rust/crates/detmir-portal/src/main.rs | 55 ++----------------- .../crates/detmir-portal/src/path_query.rs | 54 ++++++++++++++++++ 2 files changed, 59 insertions(+), 50 deletions(-) create mode 100644 adk-rust/crates/detmir-portal/src/path_query.rs diff --git a/adk-rust/crates/detmir-portal/src/main.rs b/adk-rust/crates/detmir-portal/src/main.rs index eb5dfd0..b8b033d 100644 --- a/adk-rust/crates/detmir-portal/src/main.rs +++ b/adk-rust/crates/detmir-portal/src/main.rs @@ -25,6 +25,7 @@ use tiny_http::{Header, Method, Request, Response, Server, StatusCode}; mod command_runner; mod executive_actions; +mod path_query; mod portal_roles; mod production; mod risk_narrative; @@ -34,6 +35,10 @@ use command_runner::run_in_dir; use executive_actions::{ actions_from_center, build_action_center_from_report, filter_actions_for_role, }; +use path_query::{ + normalize_path, parse_case_path, parse_case_status_path, parse_investigation_pack_path, + query_flag, query_param, +}; use portal_roles::PortalRole; use production::{ build_healthz, build_readyz, build_version, http_request_metadata, is_limited_api_route, @@ -1655,16 +1660,6 @@ fn handle_evidence_only_request(request: Request, args: &Cli) -> Result<()> { ) } -fn normalize_path(url: &str) -> String { - let path = url.split('?').next().unwrap_or("/"); - let path = path.strip_prefix("/portal").unwrap_or(path); - if path.is_empty() { - "/".to_string() - } else { - path.to_string() - } -} - fn api_contract_summary() -> Value { json!({ "ok": true, @@ -1812,24 +1807,6 @@ fn read_json_file(path: &Path) -> Result { serde_json::from_str(&text).with_context(|| format!("parse {}", path.display())) } -fn query_flag(url: &str, key: &str) -> bool { - let Some(query) = url.split_once('?').map(|(_, query)| query) else { - return false; - }; - query.split('&').any(|pair| { - let (name, value) = pair.split_once('=').unwrap_or((pair, "1")); - name == key && matches!(value, "1" | "true" | "yes" | "on") - }) -} - -fn query_param(url: &str, key: &str) -> Option { - let query = url.split_once('?').map(|(_, query)| query)?; - query.split('&').find_map(|pair| { - let (name, value) = pair.split_once('=').unwrap_or((pair, "")); - (name == key && !value.is_empty()).then(|| value.to_string()) - }) -} - fn portal_role_from_request(request: &Request, url: &str) -> PortalRole { query_param(url, "role") .as_deref() @@ -1869,28 +1846,6 @@ fn respond_forbidden(request: Request, role: PortalRole, scope: &str) -> Result< ) } -fn parse_investigation_pack_path(path: &str) -> Option { - path.strip_prefix("/api/investigation-pack/") - .map(str::trim) - .filter(|value| !value.is_empty() && !value.contains('/')) - .map(ToString::to_string) -} - -fn parse_case_path(path: &str) -> Option { - path.strip_prefix("/api/cases/") - .map(str::trim) - .filter(|value| !value.is_empty() && !value.contains('/')) - .map(ToString::to_string) -} - -fn parse_case_status_path(path: &str) -> Option { - path.strip_prefix("/api/cases/") - .and_then(|value| value.strip_suffix("/status")) - .map(str::trim) - .filter(|value| !value.is_empty() && !value.contains('/')) - .map(ToString::to_string) -} - fn cached_snapshot(args: &Cli, cache: &SnapshotCache) -> Snapshot { let mut guard = cache.lock().expect("snapshot cache mutex poisoned"); if let Some(cached) = guard.as_ref() { diff --git a/adk-rust/crates/detmir-portal/src/path_query.rs b/adk-rust/crates/detmir-portal/src/path_query.rs new file mode 100644 index 0000000..fe32db8 --- /dev/null +++ b/adk-rust/crates/detmir-portal/src/path_query.rs @@ -0,0 +1,54 @@ +//! URL path and query parsing helpers for the portal. +//! +//! CONTRACT: these helpers are routing glue. Keep accepted URL shapes stable +//! because API handlers and the HTML portal depend on them. + +pub(crate) fn normalize_path(url: &str) -> String { + let path = url.split('?').next().unwrap_or("/"); + let path = path.strip_prefix("/portal").unwrap_or(path); + if path.is_empty() { + "/".to_string() + } else { + path.to_string() + } +} + +pub(crate) fn query_flag(url: &str, key: &str) -> bool { + let Some(query) = url.split_once('?').map(|(_, query)| query) else { + return false; + }; + query.split('&').any(|pair| { + let (name, value) = pair.split_once('=').unwrap_or((pair, "1")); + name == key && matches!(value, "1" | "true" | "yes" | "on") + }) +} + +pub(crate) fn query_param(url: &str, key: &str) -> Option { + let query = url.split_once('?').map(|(_, query)| query)?; + query.split('&').find_map(|pair| { + let (name, value) = pair.split_once('=').unwrap_or((pair, "")); + (name == key && !value.is_empty()).then(|| value.to_string()) + }) +} + +pub(crate) fn parse_investigation_pack_path(path: &str) -> Option { + path.strip_prefix("/api/investigation-pack/") + .map(str::trim) + .filter(|value| !value.is_empty() && !value.contains('/')) + .map(ToString::to_string) +} + +pub(crate) fn parse_case_path(path: &str) -> Option { + path.strip_prefix("/api/cases/") + .map(str::trim) + .filter(|value| !value.is_empty() && !value.contains('/')) + .map(ToString::to_string) +} + +pub(crate) fn parse_case_status_path(path: &str) -> Option { + path.strip_prefix("/api/cases/") + .and_then(|value| value.strip_suffix("/status")) + .map(str::trim) + .filter(|value| !value.is_empty() && !value.contains('/')) + .map(ToString::to_string) +}