Skip to main content

proxima_primitives/pipe/
method.rs

1use bytes::Bytes;
2
3/// HTTP request method (RFC 9110 §9). Standard methods are unit variants — an
4/// integer-discriminant compare, no allocation. A non-standard method carries
5/// its wire bytes in [`Method::Other`], so inbound parsing of an unknown method
6/// never copies beyond the wire buffer it already holds (use [`Method::from_wire`]).
7///
8/// Construction is via `From<&str>` / `From<&[u8]>` / `From<Bytes>`, so builder
9/// sites that wrote `.method("GET")` keep working — a standard method parses to
10/// a unit variant with zero heap, replacing the prior `Bytes::copy_from_slice`.
11#[derive(Clone, Debug, PartialEq, Eq, Default)]
12pub enum Method {
13    #[default]
14    Get,
15    Post,
16    Put,
17    Patch,
18    Delete,
19    Head,
20    Options,
21    Connect,
22    Trace,
23    /// A non-standard method, holding its wire bytes.
24    Other(Bytes),
25}
26
27impl Method {
28    /// Parse from raw bytes. Methods are case-sensitive uppercase tokens
29    /// (RFC 9110 §9.1), so the match is exact. Standard methods return a unit
30    /// variant without allocating; an unknown method copies into `Other`.
31    #[must_use]
32    pub fn from_bytes(bytes: &[u8]) -> Self {
33        match bytes {
34            b"GET" => Self::Get,
35            b"POST" => Self::Post,
36            b"PUT" => Self::Put,
37            b"PATCH" => Self::Patch,
38            b"DELETE" => Self::Delete,
39            b"HEAD" => Self::Head,
40            b"OPTIONS" => Self::Options,
41            b"CONNECT" => Self::Connect,
42            b"TRACE" => Self::Trace,
43            other => Self::Other(Bytes::copy_from_slice(other)),
44        }
45    }
46
47    /// Take ownership of wire `Bytes`: a standard method drops the bytes and
48    /// returns a unit variant; an unknown method keeps the bytes **zero-copy**
49    /// in `Other`. This is the inbound-decode boundary — no allocation beyond
50    /// the wire `Bytes` the caller already owns.
51    #[must_use]
52    pub fn from_wire(bytes: Bytes) -> Self {
53        match bytes.as_ref() {
54            b"GET" => Self::Get,
55            b"POST" => Self::Post,
56            b"PUT" => Self::Put,
57            b"PATCH" => Self::Patch,
58            b"DELETE" => Self::Delete,
59            b"HEAD" => Self::Head,
60            b"OPTIONS" => Self::Options,
61            b"CONNECT" => Self::Connect,
62            b"TRACE" => Self::Trace,
63            _ => Self::Other(bytes),
64        }
65    }
66
67    /// The method's wire bytes. Standard methods return a `'static` slice; an
68    /// `Other` returns its carried bytes. No allocation.
69    #[must_use]
70    pub fn as_bytes(&self) -> &[u8] {
71        match self {
72            Self::Get => b"GET",
73            Self::Post => b"POST",
74            Self::Put => b"PUT",
75            Self::Patch => b"PATCH",
76            Self::Delete => b"DELETE",
77            Self::Head => b"HEAD",
78            Self::Options => b"OPTIONS",
79            Self::Connect => b"CONNECT",
80            Self::Trace => b"TRACE",
81            Self::Other(bytes) => bytes.as_ref(),
82        }
83    }
84
85    /// The method as `Bytes` for wire serialization. Standard methods are
86    /// `Bytes::from_static` (zero allocation); `Other` clones its `Bytes`
87    /// (a refcount bump, no copy).
88    #[must_use]
89    pub fn to_bytes(&self) -> Bytes {
90        match self {
91            Self::Get => Bytes::from_static(b"GET"),
92            Self::Post => Bytes::from_static(b"POST"),
93            Self::Put => Bytes::from_static(b"PUT"),
94            Self::Patch => Bytes::from_static(b"PATCH"),
95            Self::Delete => Bytes::from_static(b"DELETE"),
96            Self::Head => Bytes::from_static(b"HEAD"),
97            Self::Options => Bytes::from_static(b"OPTIONS"),
98            Self::Connect => Bytes::from_static(b"CONNECT"),
99            Self::Trace => Bytes::from_static(b"TRACE"),
100            Self::Other(bytes) => Bytes::clone(bytes),
101        }
102    }
103
104    /// The method as `&str` when its bytes are valid UTF-8 (always true for
105    /// standard methods).
106    #[must_use]
107    pub fn as_str(&self) -> Option<&str> {
108        core::str::from_utf8(self.as_bytes()).ok()
109    }
110
111    /// Whether this is a safe + idempotent read method (GET/HEAD).
112    #[must_use]
113    pub fn is_read(&self) -> bool {
114        matches!(self, Self::Get | Self::Head)
115    }
116}
117
118impl From<&[u8]> for Method {
119    fn from(bytes: &[u8]) -> Self {
120        Self::from_bytes(bytes)
121    }
122}
123
124impl From<&str> for Method {
125    fn from(value: &str) -> Self {
126        Self::from_bytes(value.as_bytes())
127    }
128}
129
130impl From<Bytes> for Method {
131    fn from(bytes: Bytes) -> Self {
132        Self::from_wire(bytes)
133    }
134}
135
136impl From<&Bytes> for Method {
137    fn from(bytes: &Bytes) -> Self {
138        Self::from_bytes(bytes.as_ref())
139    }
140}
141
142impl PartialEq<[u8]> for Method {
143    fn eq(&self, other: &[u8]) -> bool {
144        self.as_bytes() == other
145    }
146}
147
148impl PartialEq<&[u8]> for Method {
149    fn eq(&self, other: &&[u8]) -> bool {
150        self.as_bytes() == *other
151    }
152}
153
154impl PartialEq<str> for Method {
155    fn eq(&self, other: &str) -> bool {
156        self.as_bytes() == other.as_bytes()
157    }
158}
159
160impl PartialEq<&str> for Method {
161    fn eq(&self, other: &&str) -> bool {
162        self.as_bytes() == other.as_bytes()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn standard_methods_parse_to_unit_variants() {
172        assert_eq!(Method::from_bytes(b"GET"), Method::Get);
173        assert_eq!(Method::from(&b"POST"[..]), Method::Post);
174        assert_eq!(Method::from("DELETE"), Method::Delete);
175    }
176
177    #[test]
178    fn from_wire_keeps_unknown_method_zero_copy() {
179        let wire = Bytes::from_static(b"PURGE");
180        let method = Method::from_wire(Bytes::clone(&wire));
181        // the Other arm holds the SAME allocation (ptr-equal), not a copy.
182        match method {
183            Method::Other(bytes) => assert_eq!(bytes.as_ptr(), wire.as_ptr()),
184            other => panic!("expected Other, got {other:?}"),
185        }
186    }
187
188    #[test]
189    fn from_wire_standard_drops_bytes_to_unit() {
190        assert_eq!(Method::from_wire(Bytes::from_static(b"PUT")), Method::Put);
191    }
192
193    #[test]
194    fn round_trips_through_wire_bytes() {
195        for raw in [
196            &b"GET"[..],
197            b"POST",
198            b"PUT",
199            b"PATCH",
200            b"DELETE",
201            b"HEAD",
202            b"OPTIONS",
203            b"CONNECT",
204            b"TRACE",
205            b"PURGE",
206        ] {
207            let method = Method::from_bytes(raw);
208            assert_eq!(method.as_bytes(), raw);
209            assert_eq!(method.to_bytes().as_ref(), raw);
210        }
211    }
212
213    #[test]
214    fn standard_to_bytes_is_static_no_alloc() {
215        // from_static carries no heap pointer the allocator owns; sanity that
216        // the standard path never touches Other.
217        assert!(matches!(Method::Get.to_bytes(), bytes if bytes.as_ref() == b"GET"));
218    }
219}