Skip to main content

reinhardt_http/
path_params.rs

1//! Ordered path parameter storage.
2//!
3//! `PathParams` preserves the order in which path parameters appear in the URL
4//! pattern, which is essential for correct tuple-based extraction such as
5//! `Path<(T1, T2)>`. Internally it is a `Vec<(String, String)>`, but it exposes
6//! a small subset of the `HashMap`-like API (`get`, `iter`, `len`, `is_empty`,
7//! `insert`, `values`) so existing callers can continue to look up parameters
8//! by name without any code changes.
9//!
10//! # Why a `Vec` and not a `HashMap`?
11//!
12//! `HashMap` iteration order is non-deterministic. URL routers (matchit in
13//! particular) yield parameters in URL declaration order, which is the order
14//! users expect when destructuring `Path<(T1, T2)>`. Storing parameters in a
15//! `Vec<(String, String)>` preserves that order all the way from the router to
16//! the extractor.
17//!
18//! See issue #4013 for details.
19
20use std::collections::HashMap;
21
22/// Ordered collection of path parameters extracted from a URL pattern.
23///
24/// Preserves insertion order so that tuple extractors like `Path<(T1, T2)>`
25/// can rely on URL pattern declaration order when populating tuple fields.
26///
27/// # Example
28///
29/// ```
30/// use reinhardt_http::PathParams;
31///
32/// let mut params = PathParams::new();
33/// params.insert("org", "myslug");
34/// params.insert("cluster_id", "5");
35///
36/// // Insertion order is preserved.
37/// let collected: Vec<_> = params.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
38/// assert_eq!(collected, vec![("org", "myslug"), ("cluster_id", "5")]);
39///
40/// // Named lookup still works.
41/// assert_eq!(params.get("org").map(String::as_str), Some("myslug"));
42/// ```
43#[derive(Debug, Clone, Default, PartialEq, Eq)]
44pub struct PathParams {
45	inner: Vec<(String, String)>,
46}
47
48impl PathParams {
49	/// Create a new, empty `PathParams`.
50	pub fn new() -> Self {
51		Self { inner: Vec::new() }
52	}
53
54	/// Number of stored parameters.
55	pub fn len(&self) -> usize {
56		self.inner.len()
57	}
58
59	/// `true` if no parameters are stored.
60	pub fn is_empty(&self) -> bool {
61		self.inner.is_empty()
62	}
63
64	/// Look up a parameter by name.
65	///
66	/// Returns the first match if multiple entries share the same name (which
67	/// should not happen in practice because URL patterns require unique names).
68	pub fn get(&self, key: &str) -> Option<&String> {
69		self.inner.iter().find(|(k, _)| k == key).map(|(_, v)| v)
70	}
71
72	/// Insert or update a parameter.
73	///
74	/// If `key` already exists, its value is replaced and its position is kept.
75	/// Otherwise the new entry is appended, preserving insertion order.
76	pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
77		let key = key.into();
78		let value = value.into();
79		if let Some(slot) = self.inner.iter_mut().find(|(k, _)| *k == key) {
80			slot.1 = value;
81		} else {
82			self.inner.push((key, value));
83		}
84	}
85
86	/// Iterate over `(key, value)` pairs in insertion order.
87	pub fn iter(&self) -> std::slice::Iter<'_, (String, String)> {
88		self.inner.iter()
89	}
90
91	/// Iterate over values in insertion order.
92	pub fn values(&self) -> impl Iterator<Item = &String> {
93		self.inner.iter().map(|(_, v)| v)
94	}
95
96	/// Borrow the underlying ordered slice of `(key, value)` pairs.
97	pub fn as_slice(&self) -> &[(String, String)] {
98		&self.inner
99	}
100
101	/// Consume the wrapper and return the inner ordered `Vec`.
102	pub fn into_vec(self) -> Vec<(String, String)> {
103		self.inner
104	}
105}
106
107impl<K, V> FromIterator<(K, V)> for PathParams
108where
109	K: Into<String>,
110	V: Into<String>,
111{
112	fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
113		let mut params = PathParams::new();
114		for (k, v) in iter {
115			params.insert(k, v);
116		}
117		params
118	}
119}
120
121impl IntoIterator for PathParams {
122	type Item = (String, String);
123	type IntoIter = std::vec::IntoIter<(String, String)>;
124
125	fn into_iter(self) -> Self::IntoIter {
126		self.inner.into_iter()
127	}
128}
129
130impl<'a> IntoIterator for &'a PathParams {
131	type Item = &'a (String, String);
132	type IntoIter = std::slice::Iter<'a, (String, String)>;
133
134	fn into_iter(self) -> Self::IntoIter {
135		self.inner.iter()
136	}
137}
138
139impl From<Vec<(String, String)>> for PathParams {
140	fn from(inner: Vec<(String, String)>) -> Self {
141		// Caller is responsible for the ordering of the supplied vector.
142		Self { inner }
143	}
144}
145
146impl From<HashMap<String, String>> for PathParams {
147	/// Convert from a `HashMap`. Iteration order is **not** preserved because
148	/// `HashMap` does not have a defined order. Prefer `From<Vec<_>>` when
149	/// order matters.
150	fn from(map: HashMap<String, String>) -> Self {
151		Self {
152			inner: map.into_iter().collect(),
153		}
154	}
155}
156
157#[cfg(test)]
158mod tests {
159	use super::*;
160	use rstest::rstest;
161
162	#[rstest]
163	fn insert_preserves_order() {
164		// Arrange
165		let mut params = PathParams::new();
166
167		// Act
168		params.insert("z", "first");
169		params.insert("a", "second");
170		params.insert("m", "third");
171
172		// Assert
173		let order: Vec<&str> = params.iter().map(|(k, _)| k.as_str()).collect();
174		assert_eq!(order, vec!["z", "a", "m"]);
175	}
176
177	#[rstest]
178	fn get_finds_by_name() {
179		// Arrange
180		let mut params = PathParams::new();
181		params.insert("org", "myslug");
182		params.insert("cluster_id", "5");
183
184		// Act
185		let org = params.get("org");
186		let cluster_id = params.get("cluster_id");
187		let missing = params.get("missing");
188
189		// Assert
190		assert_eq!(org.map(String::as_str), Some("myslug"));
191		assert_eq!(cluster_id.map(String::as_str), Some("5"));
192		assert_eq!(missing, None);
193	}
194
195	#[rstest]
196	fn insert_replaces_existing_in_place() {
197		// Arrange
198		let mut params = PathParams::new();
199		params.insert("a", "1");
200		params.insert("b", "2");
201
202		// Act
203		params.insert("a", "updated");
204
205		// Assert: order unchanged, value replaced
206		let collected: Vec<_> = params
207			.iter()
208			.map(|(k, v)| (k.as_str(), v.as_str()))
209			.collect();
210		assert_eq!(collected, vec![("a", "updated"), ("b", "2")]);
211	}
212
213	#[rstest]
214	fn from_vec_preserves_caller_order() {
215		// Arrange
216		let vec = vec![
217			("org".to_string(), "myslug".to_string()),
218			("cluster_id".to_string(), "5".to_string()),
219		];
220
221		// Act
222		let params = PathParams::from(vec);
223
224		// Assert
225		let order: Vec<&str> = params.iter().map(|(k, _)| k.as_str()).collect();
226		assert_eq!(order, vec!["org", "cluster_id"]);
227	}
228
229	#[rstest]
230	fn from_iter_collects_in_order() {
231		// Arrange
232		let pairs = vec![("z", "1"), ("a", "2")];
233
234		// Act
235		let params: PathParams = pairs.into_iter().collect();
236
237		// Assert
238		let order: Vec<&str> = params.iter().map(|(k, _)| k.as_str()).collect();
239		assert_eq!(order, vec!["z", "a"]);
240	}
241}