Skip to main content

rama_net/http/uri/match_replace/
mod.rs

1//! Uri match and replace rules.
2
3use std::{borrow::Cow, sync::Arc};
4
5use crate::uri::Uri;
6
7use rama_core::error::BoxError;
8use rama_utils::collections::smallvec::SmallVec;
9use rama_utils::thirdparty::wildcard::Wildcard;
10
11mod fmt;
12
13mod rule;
14pub use rule::UriMatchReplaceRule;
15
16mod rule_set;
17pub use rule_set::UriMatchReplaceRuleset;
18
19mod scheme;
20pub use scheme::UriMatchReplaceScheme;
21
22mod domain;
23pub use domain::UriMatchReplaceDomain;
24
25mod slice;
26mod tuple;
27
28mod fallthrough;
29pub use fallthrough::UriMatchReplaceFallthrough;
30
31/// A trait for types that can match and optionally replace (rewrite)
32/// [`Uri`] values.
33///
34/// # Blanket implementations
35///
36/// This trait is implemented for common iterable types:
37///
38/// - `[R; N]` for any array of [`UriMatchReplace`]s of usize `N`;
39/// - `&[R]` for any slice of [`UriMatchReplace`]s;
40/// - `Vec<R>` for any [`Vec`]] of [`UriMatchReplace`]s;
41///
42/// It is also implemeneted for tuples size 1 to 12,
43/// allowing you to combine multiple [`UriMatchReplace`] types.
44///
45/// In case you wish a fallthrough behaviour for any supported
46/// slice type or tuple you can also wrap it with [`UriMatchReplaceFallthrough`]
47/// which will ensure that Uri's go through all rules,
48/// preserving the last found match (if any).
49///
50/// For [`UriMatchReplaceRule`] it is best to use [`UriMatchReplaceRuleset`]
51/// in case you wish to use multiple rules, as it is more optimal
52/// than the blanket slice implementation.
53///
54/// For match-replace rules specifically for a scheme or domain-like
55/// condition it is better to use [`UriMatchReplaceScheme`] and
56/// [`UriMatchReplaceDomain`] respectively over [`UriMatchReplaceRule`]
57/// as it also is more optimal and in some ways more powerful as well.
58///
59/// # Edge cases
60///
61/// - A rule that matches but produces an invalid URI will be skipped.
62/// - When multiple rules could match, **only the first** one in iteration order
63///   is applied.
64/// - Query parameters are part of the match only when the rule’s pattern or
65///   formatter includes `?` (escaped with `\\`) or ends on `*`
66///
67/// # Contract
68///
69/// A [`UriMatchReplace`] is expected to preserve the input [`Uri`] as-is
70/// when returning [`UriMatchError::NoMatch`]. This error variant
71/// is also to be returned for errors unless the original [`Uri`]
72/// is no longer present in which case a [`UriMatchError::Unexpected`]
73/// error can be returned.
74///
75/// # Examples
76///
77/// Apply a single rule:
78///
79/// ```rust
80/// # use core::str::FromStr;
81/// # use std::borrow::Cow;
82/// # use rama_net::uri::Uri;
83/// # use rama_net::http::uri::{UriMatchReplace, UriMatchReplaceRule};
84/// let rule = UriMatchReplaceRule::try_new("http://*", "https://$1").unwrap();
85///
86/// let uri = Uri::from_static("http://example.com/x");
87/// let out = rule.match_replace_uri(Cow::Owned(uri)).unwrap();
88/// assert_eq!(out.to_string(), "https://example.com/x");
89/// ```
90///
91/// Apply several rules in order, multiple rules even if applicable:
92///
93/// ```rust
94/// # use core::str::FromStr;
95/// # use std::borrow::Cow;
96/// # use rama_net::uri::Uri;
97/// # use rama_net::http::uri::{UriMatchReplace, UriMatchReplaceRule, UriMatchReplaceScheme, UriMatchReplaceFallthrough};
98/// let rules = UriMatchReplaceFallthrough((
99///     UriMatchReplaceScheme::http_to_https(),
100///     UriMatchReplaceRule::try_new("https://*/docs/*", "https://$1/knowledge/$2").unwrap(),
101/// ));
102///
103/// let uri = Uri::from_static("http://example.com/foo/docs/bar");
104/// let out = rules.match_replace_uri(Cow::Owned(uri)).unwrap();
105/// assert_eq!(out.to_string(), "https://example.com/foo/knowledge/bar");
106/// ```
107pub trait UriMatchReplace {
108    /// Tries to match `uri` against the rule's pattern and, on success,
109    /// returns the same Uri or a _new_ **reformatted** `Uri`.
110    ///
111    /// When the input does not match, or the resulting bytes do not parse as a
112    /// valid `Uri`, `None` is returned.
113    fn match_replace_uri<'a>(&self, uri: Cow<'a, Uri>) -> Result<Cow<'a, Uri>, UriMatchError<'a>>;
114}
115
116#[derive(Debug)]
117/// Error (including [`NoMatch`]) returned by a [`UriMatchReplace::match_replace_uri`] call.
118///
119/// [`NoMatch`]: UriMatchError::NoMatch
120pub enum UriMatchError<'a> {
121    /// No error occurred, but no match was found either.
122    ///
123    /// The uri should be returned as-is, as it is useful
124    /// in case of some kind of set or other chaining position
125    /// to give the next matcher a shot at it.
126    NoMatch(Cow<'a, Uri>),
127    /// An unexpected error occurred and the input
128    /// [`Uri`] has been lost in progress.
129    Unexpected(BoxError),
130}
131
132impl core::fmt::Display for UriMatchError<'_> {
133    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
134        match self {
135            UriMatchError::NoMatch(cow) => write!(f, "uri match error: no match: uri = {cow}"),
136            UriMatchError::Unexpected(err) => write!(f, "uri match error: unexpected: {err}"),
137        }
138    }
139}
140
141impl core::error::Error for UriMatchError<'_> {
142    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
143        match self {
144            UriMatchError::NoMatch(_) => None,
145            UriMatchError::Unexpected(err) => err.source(),
146        }
147    }
148}
149
150#[derive(Debug, Clone, Copy, Default)]
151#[non_exhaustive]
152/// A [`UriMatchReplace`] which never matches.
153pub struct UriMatchReplaceNever;
154
155impl UriMatchReplaceNever {
156    #[must_use]
157    pub fn new() -> Self {
158        Self
159    }
160}
161
162// placeholder
163impl UriMatchReplace for UriMatchReplaceNever {
164    #[inline]
165    fn match_replace_uri<'a>(&self, uri: Cow<'a, Uri>) -> Result<Cow<'a, Uri>, UriMatchError<'a>> {
166        Err(UriMatchError::NoMatch(uri))
167    }
168}
169
170impl<R: UriMatchReplace> UriMatchReplace for &R {
171    #[inline(always)]
172    fn match_replace_uri<'a>(&self, uri: Cow<'a, Uri>) -> Result<Cow<'a, Uri>, UriMatchError<'a>> {
173        (*self).match_replace_uri(uri)
174    }
175}
176
177impl<R: UriMatchReplace> UriMatchReplace for Arc<R> {
178    #[inline(always)]
179    fn match_replace_uri<'a>(&self, uri: Cow<'a, Uri>) -> Result<Cow<'a, Uri>, UriMatchError<'a>> {
180        (**self).match_replace_uri(uri)
181    }
182}
183
184impl UriMatchReplace for Arc<dyn UriMatchReplace> {
185    #[inline(always)]
186    fn match_replace_uri<'a>(&self, uri: Cow<'a, Uri>) -> Result<Cow<'a, Uri>, UriMatchError<'a>> {
187        (**self).match_replace_uri(uri)
188    }
189}
190
191impl UriMatchReplace for Box<dyn UriMatchReplace> {
192    #[inline(always)]
193    fn match_replace_uri<'a>(&self, uri: Cow<'a, Uri>) -> Result<Cow<'a, Uri>, UriMatchError<'a>> {
194        (**self).match_replace_uri(uri)
195    }
196}
197
198impl<R: UriMatchReplace> UriMatchReplace for Option<R> {
199    #[inline]
200    fn match_replace_uri<'a>(&self, uri: Cow<'a, Uri>) -> Result<Cow<'a, Uri>, UriMatchError<'a>> {
201        match self {
202            Some(r) => r.match_replace_uri(uri),
203            None => Err(UriMatchError::NoMatch(uri)),
204        }
205    }
206}
207
208macro_rules! impl_uri_match_replace_either {
209    ($id:ident, $($param:ident),+ $(,)?) => {
210        impl<$($param),+> UriMatchReplace for rama_core::combinators::$id<$($param),+>
211        where
212            $($param: UriMatchReplace),+,
213        {
214            fn match_replace_uri<'a>(&self, uri: Cow<'a, Uri>,
215            ) -> Result<Cow<'a, Uri>, UriMatchError<'a>>  {
216                match self {
217                    $(
218                        rama_core::combinators::$id::$param(r) => r.match_replace_uri(uri),
219                    )+
220                }
221            }
222        }
223    };
224}
225
226rama_core::combinators::impl_either!(impl_uri_match_replace_either);
227
228/// Private trait used by this module to easily create patterns from
229/// owned or static byte-like objects.
230#[expect(private_bounds)]
231pub trait TryIntoPattern: private_ptn::TryIntoPatternPriv {}
232
233impl TryIntoPattern for &'static str {}
234impl TryIntoPattern for String {}
235impl TryIntoPattern for &'static [u8] {}
236impl TryIntoPattern for Vec<u8> {}
237
238#[derive(Debug)]
239/// result of [`TryIntoPattern`].
240struct Pattern {
241    wildcard: Wildcard<'static>,
242    include_query: bool,
243}
244
245/// Private trait used by this module to easily create Uri formatters
246/// from owned or static byte-like objects.
247#[expect(private_bounds)]
248pub trait TryIntoUriFmt: private_fmt::TryIntoUriFmtPriv {}
249
250impl TryIntoUriFmt for &'static str {}
251impl TryIntoUriFmt for String {}
252impl TryIntoUriFmt for &'static [u8] {}
253impl TryIntoUriFmt for Vec<u8> {}
254
255mod private_ptn {
256    use super::*;
257
258    use rama_core::error::{BoxError, ErrorContext as _};
259    use rama_utils::{str::submatch_ignore_ascii_case, thirdparty::wildcard::WildcardBuilder};
260
261    pub(super) trait TryIntoPatternPriv {
262        fn try_into_wildcard(self) -> Result<Pattern, BoxError>;
263    }
264
265    impl TryIntoPatternPriv for &'static str {
266        #[inline(always)]
267        fn try_into_wildcard(self) -> Result<Pattern, BoxError> {
268            self.as_bytes().try_into_wildcard()
269        }
270    }
271
272    impl TryIntoPatternPriv for &'static [u8] {
273        fn try_into_wildcard(self) -> Result<Pattern, BoxError> {
274            let wildcard = WildcardBuilder::new(self)
275                .case_insensitive(true)
276                .build()
277                .context("build pattern from static slice")?;
278            let include_query = submatch_ignore_ascii_case(self, b"\\?");
279            Ok(Pattern {
280                wildcard,
281                include_query,
282            })
283        }
284    }
285
286    impl TryIntoPatternPriv for String {
287        #[inline]
288        fn try_into_wildcard(self) -> Result<Pattern, BoxError> {
289            self.into_bytes().try_into_wildcard()
290        }
291    }
292
293    impl TryIntoPatternPriv for Vec<u8> {
294        #[inline]
295        fn try_into_wildcard(self) -> Result<Pattern, BoxError> {
296            let wildcard = WildcardBuilder::from_owned(self)
297                .case_insensitive(true)
298                .build()
299                .context("build pattern from heap slice")?;
300            let include_query = submatch_ignore_ascii_case(wildcard.pattern(), b"\\?");
301            Ok(Pattern {
302                wildcard,
303                include_query,
304            })
305        }
306    }
307}
308
309mod private_fmt {
310    use super::*;
311
312    use rama_core::error::BoxError;
313
314    pub(super) trait TryIntoUriFmtPriv {
315        fn try_into_fmt(self) -> Result<fmt::UriFormatter, BoxError>;
316    }
317
318    impl TryIntoUriFmtPriv for &'static str {
319        #[inline(always)]
320        fn try_into_fmt(self) -> Result<fmt::UriFormatter, BoxError> {
321            self.as_bytes().try_into_fmt()
322        }
323    }
324
325    impl TryIntoUriFmtPriv for &'static [u8] {
326        #[inline(always)]
327        fn try_into_fmt(self) -> Result<fmt::UriFormatter, BoxError> {
328            fmt::UriFormatter::try_new(self.into())
329        }
330    }
331
332    impl TryIntoUriFmtPriv for String {
333        #[inline(always)]
334        fn try_into_fmt(self) -> Result<fmt::UriFormatter, BoxError> {
335            self.into_bytes().try_into_fmt()
336        }
337    }
338
339    impl TryIntoUriFmtPriv for Vec<u8> {
340        #[inline(always)]
341        fn try_into_fmt(self) -> Result<fmt::UriFormatter, BoxError> {
342            fmt::UriFormatter::try_new(self.into())
343        }
344    }
345}
346
347type SmallUriStr = SmallVec<[u8; 128]>;