Skip to main content

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}