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