apimock_routing/util/http.rs
1//! HTTP-path utilities used by the matcher.
2//!
3//! # Why only `normalize_url_path` lives here
4//!
5//! The full HTTP utility set (content-type inspection, response-delay
6//! sleep) was originally grouped into a single `util::http` module when
7//! the whole codebase was one crate. In the 5.0 split, the only helper
8//! the *matcher* needs is URL-path normalization — content-type and
9//! delay are server-side concerns, kept in `apimock-server::http_util`.
10
11/// Normalize a URL path to one-leading-slash, no-trailing-slash form.
12///
13/// # Why we canonicalise here instead of at each call site
14///
15/// Rule-set authors write paths inconsistently — `/api/v1`, `api/v1`,
16/// `/api/v1/` — and client requests arrive with similar variation.
17/// Choosing one canonical form at the boundary means every matcher
18/// downstream compares already-normalized strings, eliminating a class
19/// of "why isn't my rule matching?" bugs.
20pub fn normalize_url_path(url_path: &str, url_path_prefix: Option<&str>) -> String {
21 let url_path_prefix = match url_path_prefix {
22 Some(prefix) if !prefix.is_empty() => prefix.strip_suffix('/').unwrap_or(prefix),
23 _ => "",
24 };
25
26 let url_path = url_path.strip_prefix('/').unwrap_or(url_path);
27
28 let merged = format!("{}/{}", url_path_prefix, url_path);
29
30 // Apply the two strips in the same order as the pre-5.0 implementation.
31 // Using intermediate `&str` bindings (rather than chaining) keeps the
32 // behaviour identical: each `strip_*` is independent of whether the
33 // previous one matched.
34 let trimmed = merged.strip_suffix('/').unwrap_or(merged.as_str());
35 let trimmed = trimmed.strip_prefix('/').unwrap_or(trimmed);
36 format!("/{}", trimmed)
37}