rama_net/input_ext.rs
1//! Small, single-concern accessor traits for reading routing/transport
2//! properties off a service input (an http request, a connect target, …).
3//!
4//! Each concern (URI, path, authority, protocol, http version, transport) is
5//! its own small trait, so a caller reads exactly the piece it needs instead of
6//! building a combined context up front.
7//!
8//! Design (matching [`ClientIp`](crate::ClientIp)): each *resolution* trait has
9//! no [`ExtensionsRef`] bound and is never
10//! blanket-derived from another trait — every input type opts in with the
11//! resolution that fits it (the http `Request`/`Parts` impls in `rama-http-types`
12//! walk the uri → TLS SNI → `Forwarded` → `Host` fallback chain;
13//! a transport target resolves its authority directly). The only blanket impls
14//! are the trivial reference-forwarding ones and the composed
15//! [`ConnectorTargetInputExt`], whose method is purely derived.
16//!
17//! The return type *is* the fallibility contract: an `Option` may be absent (a
18//! caller that requires it does `.ok_or_else(|| …)?` with its own error);
19//! [`transport_protocol`](TransportProtocolInputExt::transport_protocol) is
20//! always known, so it returns a bare value.
21//!
22//! Each resolution trait also carries **default methods** built on its one
23//! required accessor (e.g. [`AuthorityInputExt::host_as_domain`]), so callers
24//! get ergonomic projections without re-writing the same closure chains.
25
26use crate::Protocol;
27
28#[cfg(feature = "std")]
29use crate::address::HostWithPort;
30use crate::address::{Domain, Host, HostWithOptPort};
31
32#[cfg(feature = "std")]
33use crate::client::ConnectorTarget;
34
35#[cfg(feature = "http")]
36use crate::http::Version;
37use crate::transport::TransportProtocol;
38use crate::uri::{PathRef, Uri};
39
40#[cfg(feature = "std")]
41use rama_core::extensions::ExtensionsRef;
42
43/// Read the [`Uri`] of a service input that carries one.
44///
45/// Unlike the other `*InputExt` traits (which return an `Option` because they
46/// *resolve* a property that may be absent), this is a structural **capability**:
47/// a type implements it only when it always has a URI, so `uri()` returns a
48/// `&Uri` directly. Plenty of inputs besides http requests carry a URI (a bare
49/// [`Uri`], a redirect target, a url-keyed config, …), which is why this lives
50/// next to the http request impls rather than being tied to them.
51///
52/// Note: a `UriInputExt` is **not** automatically an [`AuthorityInputExt`] /
53/// [`ProtocolInputExt`] — an http request, for instance, resolves its authority
54/// from more than just its URI (proxy target, TLS SNI, `Forwarded`, `Host`), so
55/// those impls are deliberately per-type rather than blanket-derived from here.
56pub trait UriInputExt {
57 /// The [`Uri`] this input carries.
58 fn uri(&self) -> &Uri;
59}
60
61impl<T: UriInputExt + ?Sized> UriInputExt for &T {
62 fn uri(&self) -> &Uri {
63 (**self).uri()
64 }
65}
66
67impl UriInputExt for Uri {
68 fn uri(&self) -> &Uri {
69 self
70 }
71}
72
73/// Read the URI path of a service input.
74///
75/// Implementations should return a typed [`PathRef`] and use
76/// [`Uri::path_ref_or_root`] when the path comes from a [`Uri`], so an empty URI
77/// path is observed as `/` and callers never need to fall back to raw strings.
78pub trait PathInputExt {
79 /// The path to route against.
80 fn path_ref(&self) -> PathRef<'_>;
81}
82
83impl<T: PathInputExt + ?Sized> PathInputExt for &T {
84 fn path_ref(&self) -> PathRef<'_> {
85 (**self).path_ref()
86 }
87}
88
89impl PathInputExt for Uri {
90 fn path_ref(&self) -> PathRef<'_> {
91 self.path_ref_or_root()
92 }
93}
94
95/// Read the routing **authority** (`host[:port]`) of a service input.
96///
97/// This is the HTTP routing authority — the `:authority` pseudo-header / `Host`
98/// header target — so it is a [`HostWithOptPort`], **not** the RFC-3986
99/// [`Authority`](crate::address::Authority) type (userinfo is never used for
100/// routing). Returns `None` when no authority can be resolved.
101pub trait AuthorityInputExt {
102 /// The routing authority (`host[:port]`), or `None` if none is resolvable.
103 fn authority(&self) -> Option<HostWithOptPort>;
104
105 /// The authority [`Host`], dropping any port.
106 fn host(&self) -> Option<Host> {
107 self.authority().map(|a| a.host)
108 }
109
110 /// The authority host as a [`Domain`], or `None` if absent or not a domain
111 /// (e.g. an IP literal).
112 fn host_as_domain(&self) -> Option<Domain> {
113 self.authority().and_then(|a| a.host.try_into_domain().ok())
114 }
115
116 /// The authority port, if one is set explicitly.
117 fn port(&self) -> Option<u16> {
118 self.authority().and_then(|a| a.port_u16())
119 }
120}
121
122impl<T: AuthorityInputExt + ?Sized> AuthorityInputExt for &T {
123 fn authority(&self) -> Option<HostWithOptPort> {
124 (**self).authority()
125 }
126}
127
128/// Read the application-layer [`Protocol`] (scheme) of a service input.
129pub trait ProtocolInputExt {
130 /// The application protocol, or `None` if it can't be determined.
131 fn protocol(&self) -> Option<&Protocol>;
132
133 /// The default port of the resolved [`Protocol`] (e.g. 443 for HTTPS), or
134 /// `None` if the protocol is unknown or portless.
135 fn protocol_default_port(&self) -> Option<u16> {
136 self.protocol().and_then(|p| p.default_port())
137 }
138}
139
140impl<T: ProtocolInputExt + ?Sized> ProtocolInputExt for &T {
141 fn protocol(&self) -> Option<&Protocol> {
142 (**self).protocol()
143 }
144}
145
146/// Read the contextual HTTP [`Version`] of a service input.
147///
148/// For an HTTP request this may resolve the original client version from
149/// forwarded context, so it is not necessarily the egress version a connector
150/// should establish. Connector code should use [`TargetHttpVersionInputExt`].
151/// It is `None` for non-HTTP inputs (e.g. a raw transport target).
152#[cfg(feature = "http")]
153pub trait HttpVersionInputExt {
154 /// The HTTP version, or `None` for non-HTTP inputs.
155 fn http_version(&self) -> Option<Version>;
156}
157
158#[cfg(feature = "http")]
159impl<T: HttpVersionInputExt + ?Sized> HttpVersionInputExt for &T {
160 fn http_version(&self) -> Option<Version> {
161 (**self).http_version()
162 }
163}
164
165/// Read the target HTTP [`Version`] a connector should establish.
166///
167/// This is deliberately separate from [`HttpVersionInputExt`], which may
168/// resolve the original client version from forwarded request context. A
169/// connector needs the selected egress version instead.
170#[cfg(feature = "http")]
171pub trait TargetHttpVersionInputExt {
172 /// The selected egress HTTP version, or `None` if none is available.
173 fn target_http_version(&self) -> Option<Version>;
174
175 /// Resolve the selected egress HTTP version with a post-negotiation fallback.
176 ///
177 /// Implementations that can distinguish an explicit target from an
178 /// implicit input version should override this method so `fallback` is
179 /// considered between those two sources.
180 fn target_http_version_with_fallback(&self, fallback: Option<Version>) -> Option<Version> {
181 self.target_http_version().or(fallback)
182 }
183}
184
185#[cfg(feature = "http")]
186impl<T: TargetHttpVersionInputExt + ?Sized> TargetHttpVersionInputExt for &T {
187 fn target_http_version(&self) -> Option<Version> {
188 (**self).target_http_version()
189 }
190
191 fn target_http_version_with_fallback(&self, fallback: Option<Version>) -> Option<Version> {
192 (**self).target_http_version_with_fallback(fallback)
193 }
194}
195
196/// Read the transport-layer [`TransportProtocol`] (TCP/UDP) of a service input.
197///
198/// Always known, so this is infallible.
199pub trait TransportProtocolInputExt {
200 /// The transport protocol (TCP or UDP).
201 fn transport_protocol(&self) -> Option<TransportProtocol>;
202}
203
204impl<T: TransportProtocolInputExt + ?Sized> TransportProtocolInputExt for &T {
205 fn transport_protocol(&self) -> Option<TransportProtocol> {
206 (**self).transport_protocol()
207 }
208}
209
210#[cfg(feature = "std")]
211mod private {
212 use super::{AuthorityInputExt, ProtocolInputExt};
213
214 /// Seals [`ConnectorTargetInputExt`](super::ConnectorTargetInputExt): it is
215 /// purely derived from [`AuthorityInputExt`] + [`ProtocolInputExt`], so it must
216 /// never be implemented by hand.
217 pub trait Sealed {}
218 impl<T: AuthorityInputExt + ProtocolInputExt + ?Sized> Sealed for T {}
219}
220
221/// Resolve the **transport address** (`host:port`) to connect to: the routing
222/// [`authority`](AuthorityInputExt::authority) with the application
223/// [`protocol`](ProtocolInputExt::protocol)'s default port as the port fallback.
224///
225/// Auto-implemented (and sealed) for every input that is both an
226/// [`AuthorityInputExt`] and a [`ProtocolInputExt`]; it yields the typed
227/// `host:port` a connector needs, and is never implemented by hand.
228#[cfg(feature = "std")]
229pub trait ConnectorTargetInputExt:
230 AuthorityInputExt + ProtocolInputExt + ExtensionsRef + private::Sealed
231{
232 /// The `host:port` to connect to: the authority's port if set, else the
233 /// protocol's default port. `None` when no host (or no port) resolves.
234 ///
235 /// NOTE that this method respects the extension [`ConnectorTarget`]
236 /// as overwrite, used for proxy connections and similar bypasses.
237 fn connector_target(&self) -> Option<HostWithPort> {
238 if let Some(ConnectorTarget(target)) = self.extensions().get_ref() {
239 return Some(target.clone());
240 }
241
242 self.authority()
243 .and_then(|a| a.into_host_with_port(self.protocol_default_port()))
244 }
245
246 /// Like [`connector_target`](Self::connector_target) but with `default_port` as
247 /// the ultimate fallback (ConnectorTarget → authority port → protocol default → `default_port`),
248 /// so it yields `Some` whenever an authority resolves at all.
249 ///
250 /// NOTE that this method respects the extension [`ConnectorTarget`]
251 /// as overwrite, used for proxy connections and similar bypasses.
252 fn connector_target_with_default_port(&self, default_port: u16) -> Option<HostWithPort> {
253 if let Some(ConnectorTarget(target)) = self.extensions().get_ref() {
254 return Some(target.clone());
255 }
256
257 self.authority()
258 .map(|a| a.into_host_with_port_or(self.protocol_default_port().unwrap_or(default_port)))
259 }
260}
261
262#[cfg(feature = "std")]
263impl<T: AuthorityInputExt + ProtocolInputExt + ExtensionsRef + ?Sized> ConnectorTargetInputExt
264 for T
265{
266}
267
268#[cfg(test)]
269mod tests {
270 use super::{PathInputExt, UriInputExt};
271 use crate::uri::{PathPattern, Uri};
272
273 #[test]
274 fn uri_input_ref_forwards_to_inner_uri() {
275 let uri: Uri = "https://example.com/a%2Fb?q=1".parse().unwrap();
276 let uri_ref = &uri;
277 let forwarded = <&Uri as UriInputExt>::uri(&uri_ref);
278
279 assert_eq!(forwarded.path_ref_or_root(), "/a%2Fb");
280 assert_ne!(forwarded.path_ref_or_root(), "/a/b");
281 }
282
283 #[test]
284 fn path_input_ref_forwards_to_inner_path() {
285 let uri: Uri = "https://example.com/a%2Fb?q=1".parse().unwrap();
286 let uri_ref = &uri;
287 let forwarded = <&Uri as PathInputExt>::path_ref(&uri_ref);
288
289 assert_eq!(uri.path_ref(), "/a%2Fb");
290 assert_eq!(forwarded, "/a%2Fb");
291 assert_ne!(forwarded, "/a/b");
292 }
293
294 #[test]
295 fn path_input_for_uri_uses_root_fallback() {
296 let uri: Uri = "https://example.com".parse().unwrap();
297
298 assert_eq!(uri.path_ref(), "/");
299 }
300
301 #[test]
302 fn uri_pattern_helpers_route_through_typed_path() {
303 let uri: Uri = "https://example.com/api/acme/widgets".parse().unwrap();
304 let pattern = PathPattern::new("/api/{tenant}/widgets");
305 let miss = PathPattern::new("/api/{tenant}/orders");
306
307 assert!(uri.is_pattern_match(&pattern));
308 assert!(!uri.is_pattern_match(&miss));
309
310 let captures = uri.pattern_captures(&pattern).unwrap();
311 assert_eq!(captures.get("tenant"), Some("acme"));
312 assert!(uri.pattern_captures(&miss).is_none());
313 }
314}