Skip to main content

armature_core/
http.rs

1// HTTP request and response types
2
3use crate::body::RequestBody;
4use crate::extensions::Extensions;
5use crate::headers::HeaderMap;
6use crate::query::{QueryPairs, QueryView, parse as parse_query};
7use crate::{ByteStr, Method};
8use bytes::Bytes;
9use serde::{Deserialize, Serialize};
10use smallvec::SmallVec;
11use std::collections::HashMap;
12use std::sync::{Arc, OnceLock};
13
14/// Route parameters captured from the request target.
15///
16/// Names are `&'static str` from the compiled route pattern (see
17/// [`crate::param_intern`]), so the name half of a match is free. Values are
18/// `Bytes` the matchers fill with `Bytes::copy_from_slice`, so a match costs one
19/// small copy per captured value. Four inline slots covers the overwhelming
20/// majority of routes.
21pub type RouteParams = SmallVec<[(&'static str, Bytes); 4]>;
22
23/// Read helpers for [`RouteParams`].
24///
25/// The type is a `SmallVec` of pairs rather than a map, so `get` on it means
26/// "index into the slice". This is the by-name lookup.
27pub trait RouteParamsExt {
28    /// The value captured for `name`, as UTF-8.
29    fn get_str(&self, name: &str) -> Option<&str>;
30
31    /// The value captured for `name`, raw.
32    fn get_bytes(&self, name: &str) -> Option<&Bytes>;
33}
34
35impl RouteParamsExt for RouteParams {
36    #[inline]
37    fn get_str(&self, name: &str) -> Option<&str> {
38        self.get_bytes(name)
39            .and_then(|v| std::str::from_utf8(v).ok())
40    }
41
42    #[inline]
43    fn get_bytes(&self, name: &str) -> Option<&Bytes> {
44        self.iter().find(|(k, _)| *k == name).map(|(_, v)| v)
45    }
46}
47
48/// The memoized query pairs.
49///
50/// `OnceLock` rather than `OnceCell` because `HttpRequest` must stay `Sync`:
51/// extractors hold `&HttpRequest` across an `await` inside a `Send` future, and
52/// `&T: Send` requires `T: Sync`.
53#[derive(Debug, Default)]
54pub struct QueryCache(OnceLock<QueryPairs>);
55
56impl Clone for QueryCache {
57    /// A clone starts cold.
58    ///
59    /// Carrying the parsed pairs across a clone would be wrong, not merely
60    /// wasteful: `path` is a public field, so a caller can clone a request and
61    /// then change its target. A cold cache cannot answer for the wrong path.
62    fn clone(&self) -> Self {
63        Self(OnceLock::new())
64    }
65}
66
67/// HTTP request wrapper
68///
69/// The path and body are `Bytes`-backed, so cloning a request is a handful of
70/// refcount bumps rather than a deep copy of the target and payload.
71#[derive(Debug, Clone)]
72pub struct HttpRequest {
73    /// The request method.
74    ///
75    /// Was a `String`. An unrecognized token is carried as `Method::Other`
76    /// rather than rejected here; routing answers it with 404
77    /// ([`crate::Error::RouteNotFound`]) since no route can match the token.
78    pub method: Method,
79    /// The raw request target, query string included.
80    ///
81    /// Was a `String`. A `ByteStr` so it can be a slice of the connection read
82    /// buffer once the serve path moves onto `armature-h1`; `Deref<Target = str>`
83    /// keeps `&req.path` working wherever a `&str` is wanted.
84    pub path: ByteStr,
85    /// Request headers stored in a SmallVec-backed `HeaderMap`.
86    ///
87    /// For typical requests (<12 headers) this is stored inline on the stack,
88    /// avoiding the per-request HashMap heap allocation on the read path.
89    /// The API is HashMap-compatible (`get`/`insert`/`iter`/`contains_key`/...),
90    /// with case-insensitive header name lookup.
91    pub headers: HeaderMap,
92    /// The request body.
93    ///
94    /// Was a `Vec<u8>` shadowed by an optional `Bytes` that could disagree with
95    /// it. One field, always authoritative.
96    pub body: Bytes,
97    pub path_params: RouteParams,
98    /// Type-safe extensions for storing application state.
99    ///
100    /// Use this to pass typed data to handlers without DI container lookups.
101    /// Access via the `State<T>` extractor for zero-cost state retrieval.
102    pub extensions: Extensions,
103    /// Parsed lazily by [`HttpRequest::query`].
104    query_cache: QueryCache,
105}
106
107impl HttpRequest {
108    /// Create a request.
109    ///
110    /// Generic in the method so every existing `HttpRequest::new("GET", …)`
111    /// call site compiles unchanged.
112    #[inline]
113    pub fn new(method: impl Into<Method>, path: impl Into<ByteStr>) -> Self {
114        Self {
115            method: method.into(),
116            path: path.into(),
117            headers: HeaderMap::new(),
118            body: Bytes::new(),
119            path_params: RouteParams::new(),
120            extensions: Extensions::new(),
121            query_cache: QueryCache::default(),
122        }
123    }
124
125    /// Create a new request with pre-allocated extensions capacity.
126    #[inline]
127    pub fn with_extensions_capacity(
128        method: impl Into<Method>,
129        path: impl Into<ByteStr>,
130        capacity: usize,
131    ) -> Self {
132        Self {
133            method: method.into(),
134            path: path.into(),
135            headers: HeaderMap::new(),
136            body: Bytes::new(),
137            path_params: RouteParams::new(),
138            extensions: Extensions::with_capacity(capacity),
139            query_cache: QueryCache::default(),
140        }
141    }
142
143    /// Create a new request with a Bytes body (zero-copy).
144    ///
145    /// This is the most efficient way to create a request from Hyper's body,
146    /// as it avoids copying the body data.
147    #[inline]
148    pub fn with_bytes_body(
149        method: impl Into<Method>,
150        path: impl Into<ByteStr>,
151        body: Bytes,
152    ) -> Self {
153        Self {
154            method: method.into(),
155            path: path.into(),
156            headers: HeaderMap::new(),
157            body,
158            path_params: RouteParams::new(),
159            extensions: Extensions::new(),
160            query_cache: QueryCache::default(),
161        }
162    }
163
164    /// Set the body (zero-copy).
165    #[inline]
166    pub fn set_body_bytes(&mut self, bytes: Bytes) {
167        self.body = bytes;
168    }
169
170    /// The body as `Bytes`. A refcount bump, not a copy.
171    #[inline]
172    pub fn body_bytes(&self) -> Bytes {
173        self.body.clone()
174    }
175
176    /// The body as a byte slice.
177    #[inline]
178    pub fn body_slice(&self) -> &[u8] {
179        &self.body
180    }
181
182    /// The body as a byte slice.
183    #[inline]
184    pub fn body_ref(&self) -> &[u8] {
185        &self.body
186    }
187
188    /// The request target as a string, query string included.
189    #[inline]
190    pub fn path_str(&self) -> &str {
191        self.path.as_str()
192    }
193
194    /// The request target with any query string removed.
195    ///
196    /// This is what routing matches on, and what most callers mean when they
197    /// say "the path" — `path`/`path_str` are the raw target, which is what the
198    /// query is parsed out of.
199    #[inline]
200    pub fn path_only(&self) -> &str {
201        self.path
202            .split_once('?')
203            .map_or(self.path.as_str(), |(p, _)| p)
204    }
205
206    /// Get the body as a RequestBody (zero-copy wrapper).
207    #[inline]
208    pub fn request_body(&self) -> RequestBody {
209        RequestBody::from_bytes(self.body_bytes())
210    }
211
212    /// Whether the body holds anything.
213    ///
214    /// Kept for call-site compatibility from when the body could live in either
215    /// of two fields; it is always `Bytes` now.
216    #[inline]
217    pub fn has_bytes_body(&self) -> bool {
218        !self.body.is_empty()
219    }
220
221    /// The method as a string, for logging and for code that compares tokens.
222    #[inline]
223    pub fn method_str(&self) -> &str {
224        self.method.as_str()
225    }
226
227    /// Set the body from a `Vec<u8>`, taking over its allocation.
228    #[inline]
229    pub fn set_body(&mut self, body: Vec<u8>) {
230        self.body = Bytes::from(body);
231    }
232
233    /// Create a request from all parts (for compatibility in tests).
234    #[inline]
235    pub fn from_parts(
236        method: impl Into<Method>,
237        path: impl Into<ByteStr>,
238        headers: HashMap<String, String>,
239        body: Vec<u8>,
240        path_params: HashMap<String, String>,
241        query_params: HashMap<String, String>,
242    ) -> Self {
243        // Names are interned rather than borrowed: `from_parts` is a
244        // compatibility shim taking an owned map, so there is no route pattern
245        // to borrow a `&'static str` from.
246        let path_params: RouteParams = path_params
247            .into_iter()
248            .map(|(k, v)| (crate::param_intern::intern(&k), Bytes::from(v)))
249            .collect();
250        // `query_params` is accepted for source compatibility and ignored: the
251        // query now comes from `path`, parsed on demand. Callers that need one
252        // honoured should put it in the path.
253        let _ = query_params;
254        Self {
255            method: method.into(),
256            path: path.into(),
257            headers: headers.into(),
258            body: Bytes::from(body),
259            path_params,
260            extensions: Extensions::new(),
261            query_cache: QueryCache::default(),
262        }
263    }
264
265    /// Insert a typed value into request extensions.
266    ///
267    /// Use this to pass application state to handlers.
268    ///
269    /// # Example
270    ///
271    /// ```rust,ignore
272    /// let mut request = HttpRequest::new("GET", "/");
273    /// request.insert_extension(app_state);
274    /// ```
275    #[inline]
276    pub fn insert_extension<T: Send + Sync + 'static>(&mut self, value: T) {
277        self.extensions.insert(value);
278    }
279
280    /// Insert an Arc-wrapped value into request extensions.
281    ///
282    /// This is more efficient when you already have an Arc.
283    #[inline]
284    pub fn insert_extension_arc<T: Send + Sync + 'static>(&mut self, value: Arc<T>) {
285        self.extensions.insert_arc(value);
286    }
287
288    /// Get a reference to a typed extension.
289    ///
290    /// Returns `None` if no value of this type exists.
291    #[inline]
292    pub fn extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
293        self.extensions.get::<T>()
294    }
295
296    /// Get an Arc reference to a typed extension.
297    #[inline]
298    pub fn extension_arc<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
299        self.extensions.get_arc::<T>()
300    }
301
302    /// Parse the request body as JSON.
303    ///
304    /// With the `simd-json` feature enabled, this uses SIMD-accelerated parsing
305    /// which can be 2-3x faster on modern x86_64 CPUs.
306    ///
307    /// # Example
308    ///
309    /// ```rust,ignore
310    /// let user: CreateUser = request.json()?;
311    /// ```
312    #[inline]
313    pub fn json<T: for<'de> Deserialize<'de>>(&self) -> Result<T, crate::Error> {
314        crate::json::from_slice(self.body_ref())
315            .map_err(|e| crate::Error::Deserialization(e.to_string()))
316    }
317
318    /// Parse URL-encoded form data
319    pub fn form<T: for<'de> Deserialize<'de>>(&self) -> Result<T, crate::Error> {
320        crate::form::parse_form(self.body_ref())
321    }
322
323    /// Parse URL-encoded form data into a HashMap
324    pub fn form_map(&self) -> Result<HashMap<String, String>, crate::Error> {
325        crate::form::parse_form_map(self.body_ref())
326    }
327
328    /// Parse multipart form data
329    pub fn multipart(&self) -> Result<Vec<crate::form::FormField>, crate::Error> {
330        // One lookup: header names intern case-insensitively, so the
331        // lowercased retry was always redundant.
332        let content_type = self
333            .headers
334            .get("Content-Type")
335            .ok_or_else(|| crate::Error::BadRequest("Missing Content-Type header".to_string()))?;
336
337        let parser = crate::form::MultipartParser::from_content_type(content_type)?;
338        parser.parse(self.body_ref())
339    }
340
341    /// A captured route parameter, as UTF-8.
342    #[inline]
343    pub fn param(&self, name: &str) -> Option<&str> {
344        self.param_bytes(name)
345            .and_then(|v| std::str::from_utf8(v).ok())
346    }
347
348    /// A captured route parameter, raw.
349    #[inline]
350    pub fn param_bytes(&self, name: &str) -> Option<&Bytes> {
351        self.path_params
352            .iter()
353            .find(|(k, _)| *k == name)
354            .map(|(_, v)| v)
355    }
356
357    /// Add one captured route parameter, interning its name.
358    ///
359    /// The router uses [`HttpRequest::set_params`] with names already interned
360    /// at registration; this is for callers assembling a request by hand. The
361    /// interner is hard-capped ([`crate::param_intern::MAX_INTERNED`]), so
362    /// feeding this a request-derived name cannot grow the process without
363    /// bound — past the cap the name resolves to
364    /// [`crate::param_intern::OVERFLOW_NAME`] and the parameter is no longer
365    /// retrievable by its own name.
366    pub fn push_param(&mut self, name: &str, value: impl Into<Bytes>) {
367        self.path_params
368            .push((crate::param_intern::intern(name), value.into()));
369    }
370
371    /// Replace the captured parameters. Called by the router.
372    #[inline]
373    pub fn set_params(&mut self, params: RouteParams) {
374        self.path_params = params;
375    }
376
377    /// The raw query string, without the `?`.
378    #[inline]
379    pub fn query_string(&self) -> Option<&str> {
380        self.path.as_str().split_once('?').map(|(_, q)| q)
381    }
382
383    /// A parsed view of the query string.
384    ///
385    /// Parses on the first call and memoizes; a handler that never calls this
386    /// pays nothing. Note the shape change: this used to take a name and return
387    /// one value — that accessor is now [`HttpRequest::query_param`].
388    #[inline]
389    pub fn query(&self) -> QueryView<'_> {
390        let pairs = self
391            .query_cache
392            .0
393            .get_or_init(|| match self.query_string() {
394                Some(q) => parse_query(q),
395                None => QueryPairs::new(),
396            });
397        QueryView::new(pairs)
398    }
399
400    /// Append a query parameter to the target, percent-encoding both sides.
401    ///
402    /// The query lives in `path` now, so this is how a caller adds one without
403    /// hand-assembling the target. Any memoized parse is discarded, since the
404    /// target it was parsed from no longer describes this request.
405    pub fn push_query_param(&mut self, name: impl AsRef<str>, value: impl AsRef<str>) {
406        let pair = [(name.as_ref(), value.as_ref())];
407        let Ok(encoded) = serde_urlencoded::to_string(pair) else {
408            return;
409        };
410        let separator = if self.path.contains('?') { '&' } else { '?' };
411        self.path = ByteStr::from(format!("{}{separator}{encoded}", self.path.as_str()));
412        self.query_cache = QueryCache::default();
413    }
414
415    /// The first query value for `name`.
416    #[inline]
417    pub fn query_param(&self, name: &str) -> Option<&str> {
418        self.query().get(name)
419    }
420}
421
422/// Lazy-initialized HashMap that doesn't allocate until first insert.
423///
424/// This provides the same API as HashMap but with zero allocation cost
425/// for empty maps.
426#[derive(Debug, Clone, Default)]
427pub struct LazyHeaders {
428    inner: Option<HashMap<String, String>>,
429}
430
431impl LazyHeaders {
432    /// Create a new empty LazyHeaders (no allocation).
433    #[inline(always)]
434    pub const fn new() -> Self {
435        Self { inner: None }
436    }
437
438    /// Create with pre-allocated capacity.
439    #[inline]
440    pub fn with_capacity(cap: usize) -> Self {
441        Self {
442            inner: Some(HashMap::with_capacity(cap)),
443        }
444    }
445
446    /// Insert a key-value pair.
447    #[inline]
448    pub fn insert(&mut self, key: String, value: String) -> Option<String> {
449        self.inner
450            .get_or_insert_with(HashMap::new)
451            .insert(key, value)
452    }
453
454    /// Get a value by key.
455    #[inline]
456    pub fn get(&self, key: &str) -> Option<&String> {
457        self.inner.as_ref()?.get(key)
458    }
459
460    /// Check if key exists.
461    #[inline]
462    pub fn contains_key(&self, key: &str) -> bool {
463        self.inner.as_ref().is_some_and(|m| m.contains_key(key))
464    }
465
466    /// Get number of headers.
467    #[inline]
468    pub fn len(&self) -> usize {
469        self.inner.as_ref().map_or(0, |m| m.len())
470    }
471
472    /// Check if empty.
473    #[inline]
474    pub fn is_empty(&self) -> bool {
475        self.inner.as_ref().is_none_or(|m| m.is_empty())
476    }
477
478    /// Iterate over headers.
479    #[inline]
480    pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
481        self.inner.iter().flat_map(|m| m.iter())
482    }
483
484    /// Convert to HashMap (for compatibility).
485    #[inline]
486    pub fn to_hashmap(&self) -> HashMap<String, String> {
487        self.inner.clone().unwrap_or_default()
488    }
489
490    /// Remove a header by key.
491    #[inline]
492    pub fn remove(&mut self, key: &str) -> Option<String> {
493        self.inner.as_mut()?.remove(key)
494    }
495
496    /// Get an entry for in-place manipulation.
497    #[inline]
498    pub fn entry(&mut self, key: String) -> std::collections::hash_map::Entry<'_, String, String> {
499        self.inner.get_or_insert_with(HashMap::new).entry(key)
500    }
501
502    /// Extend with headers from an iterator.
503    #[inline]
504    pub fn extend<I: IntoIterator<Item = (String, String)>>(&mut self, iter: I) {
505        let map = self.inner.get_or_insert_with(HashMap::new);
506        map.extend(iter);
507    }
508
509    /// Clear all headers.
510    #[inline]
511    pub fn clear(&mut self) {
512        if let Some(ref mut map) = self.inner {
513            map.clear();
514        }
515    }
516
517    /// Clone the inner HashMap if present.
518    #[inline]
519    pub fn clone_inner(&self) -> Option<HashMap<String, String>> {
520        self.inner.clone()
521    }
522}
523
524impl From<HashMap<String, String>> for LazyHeaders {
525    #[inline]
526    fn from(map: HashMap<String, String>) -> Self {
527        Self { inner: Some(map) }
528    }
529}
530
531impl From<LazyHeaders> for HashMap<String, String> {
532    #[inline]
533    fn from(lazy: LazyHeaders) -> Self {
534        lazy.inner.unwrap_or_default()
535    }
536}
537
538// Allow iteration
539impl<'a> IntoIterator for &'a LazyHeaders {
540    type Item = (&'a String, &'a String);
541    type IntoIter = std::iter::Flatten<std::option::Iter<'a, HashMap<String, String>>>;
542
543    fn into_iter(self) -> Self::IntoIter {
544        self.inner.iter().flatten()
545    }
546}
547
548/// HTTP response wrapper
549///
550/// The body is `Bytes`, so handing a response to the writer — or cloning one out
551/// of a cache — is a refcount bump rather than a copy. Build one from an
552/// existing buffer with `with_bytes_body()`.
553///
554/// ## Performance Note
555///
556/// Response creation is optimized for minimal allocation:
557/// - `headers` uses `LazyHeaders` which doesn't allocate until first insert
558/// - `body` is an empty `Bytes` until set, which doesn't allocate
559/// - Use `FastResponse` from `armature_core::fast_response` for even faster creation
560#[derive(Debug)]
561pub struct HttpResponse {
562    pub status: u16,
563    /// Response headers with lazy allocation.
564    pub headers: LazyHeaders,
565    /// Set-Cookie headers (supports multiple cookies per response).
566    pub cookies: Vec<String>,
567    /// The response body.
568    ///
569    /// Was a `Vec<u8>` shadowed by an optional `Bytes`. One field, always
570    /// authoritative.
571    pub body: Bytes,
572}
573
574/// Default pre-allocated response buffer size (512 bytes).
575pub const DEFAULT_RESPONSE_CAPACITY: usize = 512;
576
577impl HttpResponse {
578    /// Create a new response with the given status code.
579    ///
580    /// This is optimized for minimal allocation - headers use `LazyHeaders`
581    /// which doesn't allocate until first insert, and body uses `Vec::new()`
582    /// which is zero-cost.
583    #[inline(always)]
584    pub fn new(status: u16) -> Self {
585        Self {
586            status,
587            headers: LazyHeaders::new(),
588            cookies: Vec::new(),
589            body: Bytes::new(),
590        }
591    }
592
593    /// Create a new response with pre-allocated header capacity.
594    ///
595    /// The `capacity` argument is retained for source compatibility but no
596    /// longer reserves body space: `Bytes` is handed a finished buffer rather
597    /// than grown in place.
598    ///
599    /// # Example
600    ///
601    /// ```rust,ignore
602    /// let response = HttpResponse::with_capacity(200, 512);
603    /// ```
604    #[inline]
605    pub fn with_capacity(status: u16, _capacity: usize) -> Self {
606        Self {
607            status,
608            headers: LazyHeaders::with_capacity(8),
609            cookies: Vec::new(),
610            body: Bytes::new(),
611        }
612    }
613
614    /// Create a 200 OK response.
615    #[inline(always)]
616    pub fn ok() -> Self {
617        Self::new(200)
618    }
619
620    /// Create a 200 OK response with pre-allocated buffer (512 bytes default).
621    #[inline]
622    pub fn ok_preallocated() -> Self {
623        Self::with_capacity(200, DEFAULT_RESPONSE_CAPACITY)
624    }
625
626    /// Create a 201 Created response.
627    #[inline(always)]
628    pub fn created() -> Self {
629        Self::new(201)
630    }
631
632    /// Create a 204 No Content response.
633    #[inline(always)]
634    pub fn no_content() -> Self {
635        Self::new(204)
636    }
637
638    /// Create a 400 Bad Request response.
639    #[inline(always)]
640    pub fn bad_request() -> Self {
641        Self::new(400)
642    }
643
644    /// Create a 404 Not Found response.
645    #[inline(always)]
646    pub fn not_found() -> Self {
647        Self::new(404)
648    }
649
650    /// Create a 500 Internal Server Error response.
651    #[inline(always)]
652    pub fn internal_server_error() -> Self {
653        Self::new(500)
654    }
655
656    /// Set the body from a `Vec<u8>`, taking over its allocation.
657    pub fn with_body(mut self, body: Vec<u8>) -> Self {
658        self.body = Bytes::from(body);
659        self
660    }
661
662    /// Set the body using Bytes (zero-copy).
663    ///
664    /// This is the most efficient way to set response body data,
665    /// as it can be passed directly to Hyper without copying.
666    #[inline]
667    pub fn with_bytes_body(mut self, bytes: Bytes) -> Self {
668        self.body = bytes;
669        self
670    }
671
672    /// Set the body from a static byte slice (zero-copy).
673    #[inline]
674    pub fn with_static_body(mut self, body: &'static [u8]) -> Self {
675        self.body = Bytes::from_static(body);
676        self
677    }
678
679    /// The body as `Bytes`. A refcount bump, not a copy.
680    #[inline]
681    pub fn body_bytes(&self) -> Bytes {
682        self.body.clone()
683    }
684
685    /// Consume the response and return the body.
686    #[inline]
687    pub fn into_body_bytes(self) -> Bytes {
688        self.body
689    }
690
691    /// The body as a byte slice.
692    #[inline]
693    pub fn body_slice(&self) -> &[u8] {
694        &self.body
695    }
696
697    /// The body as a byte slice.
698    #[inline]
699    pub fn body_ref(&self) -> &[u8] {
700        &self.body
701    }
702
703    /// The body length in bytes.
704    #[inline]
705    pub fn body_len(&self) -> usize {
706        self.body.len()
707    }
708
709    /// Whether the body holds anything.
710    ///
711    /// Kept for call-site compatibility from when the body could live in either
712    /// of two fields; it is always `Bytes` now.
713    #[inline]
714    pub fn has_bytes_body(&self) -> bool {
715        !self.body.is_empty()
716    }
717
718    /// Serialize a value as JSON and set it as the response body.
719    ///
720    /// With the `simd-json` feature enabled, this uses SIMD-accelerated serialization
721    /// which can be 1.5-2x faster on modern x86_64 CPUs.
722    ///
723    /// The body is stored as `Bytes` for zero-copy passthrough to Hyper.
724    ///
725    /// # Example
726    ///
727    /// ```rust,ignore
728    /// HttpResponse::ok().with_json(&user)?
729    /// ```
730    #[inline]
731    pub fn with_json<T: Serialize>(mut self, value: &T) -> Result<Self, crate::Error> {
732        let vec =
733            crate::json::to_vec(value).map_err(|e| crate::Error::Serialization(e.to_string()))?;
734        self.body = Bytes::from(vec);
735        self.headers
736            .insert("Content-Type".to_string(), "application/json".to_string());
737        Ok(self)
738    }
739
740    pub fn with_header(mut self, key: String, value: String) -> Self {
741        self.headers.insert(key, value);
742        self
743    }
744
745    /// Set multiple headers from a HashMap.
746    #[inline]
747    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
748        self.headers = LazyHeaders::from(headers);
749        self
750    }
751
752    /// Create a response with status and headers (for CORS preflight, etc.).
753    #[inline]
754    pub fn with_status_and_headers(status: u16, headers: HashMap<String, String>) -> Self {
755        Self {
756            status,
757            headers: LazyHeaders::from(headers),
758            cookies: Vec::new(),
759            body: Bytes::new(),
760        }
761    }
762
763    /// Create a response with all components (for compatibility).
764    ///
765    /// This is useful when you need to construct a response with all parts at once.
766    #[inline]
767    pub fn from_parts(status: u16, headers: HashMap<String, String>, body: Vec<u8>) -> Self {
768        Self {
769            status,
770            headers: LazyHeaders::from(headers),
771            cookies: Vec::new(),
772            body: Bytes::from(body),
773        }
774    }
775
776    // ============================================================================
777    // Convenience Methods for Common Response Types
778    // ============================================================================
779
780    /// Create an accepted response (202).
781    ///
782    /// # Example
783    /// ```
784    /// use armature_core::HttpResponse;
785    /// let response = HttpResponse::accepted();
786    /// assert_eq!(response.status, 202);
787    /// ```
788    pub fn accepted() -> Self {
789        Self::new(202)
790    }
791
792    /// Create an unauthorized response (401).
793    ///
794    /// # Example
795    /// ```
796    /// use armature_core::HttpResponse;
797    /// let response = HttpResponse::unauthorized();
798    /// assert_eq!(response.status, 401);
799    /// ```
800    pub fn unauthorized() -> Self {
801        Self::new(401)
802    }
803
804    /// Create a forbidden response (403).
805    ///
806    /// # Example
807    /// ```
808    /// use armature_core::HttpResponse;
809    /// let response = HttpResponse::forbidden();
810    /// assert_eq!(response.status, 403);
811    /// ```
812    pub fn forbidden() -> Self {
813        Self::new(403)
814    }
815
816    /// Create a conflict response (409).
817    ///
818    /// # Example
819    /// ```
820    /// use armature_core::HttpResponse;
821    /// let response = HttpResponse::conflict();
822    /// assert_eq!(response.status, 409);
823    /// ```
824    pub fn conflict() -> Self {
825        Self::new(409)
826    }
827
828    /// Create a service unavailable response (503).
829    ///
830    /// # Example
831    /// ```
832    /// use armature_core::HttpResponse;
833    /// let response = HttpResponse::service_unavailable();
834    /// assert_eq!(response.status, 503);
835    /// ```
836    pub fn service_unavailable() -> Self {
837        Self::new(503)
838    }
839
840    /// Shorthand for creating a JSON response with 200 OK status.
841    ///
842    /// # Example
843    /// ```
844    /// use armature_core::HttpResponse;
845    /// use serde_json::json;
846    ///
847    /// let response = HttpResponse::json(&json!({"message": "Hello"})).unwrap();
848    /// assert_eq!(response.status, 200);
849    /// ```
850    pub fn json<T: Serialize>(value: &T) -> Result<Self, crate::Error> {
851        Self::ok().with_json(value)
852    }
853
854    /// Create an HTML response with 200 OK status.
855    ///
856    /// # Example
857    /// ```
858    /// use armature_core::HttpResponse;
859    /// let response = HttpResponse::html("<h1>Hello</h1>");
860    /// assert_eq!(response.status, 200);
861    /// assert_eq!(response.headers.get("Content-Type"), Some(&"text/html; charset=utf-8".to_string()));
862    /// ```
863    pub fn html(content: impl Into<String>) -> Self {
864        Self::ok()
865            .with_header(
866                "Content-Type".to_string(),
867                "text/html; charset=utf-8".to_string(),
868            )
869            .with_body(content.into().into_bytes())
870    }
871
872    /// Create a plain text response with 200 OK status.
873    ///
874    /// # Example
875    /// ```
876    /// use armature_core::HttpResponse;
877    /// let response = HttpResponse::text("Hello, World!");
878    /// assert_eq!(response.status, 200);
879    /// assert_eq!(response.headers.get("Content-Type"), Some(&"text/plain; charset=utf-8".to_string()));
880    /// ```
881    pub fn text(content: impl Into<String>) -> Self {
882        Self::ok()
883            .with_header(
884                "Content-Type".to_string(),
885                "text/plain; charset=utf-8".to_string(),
886            )
887            .with_body(content.into().into_bytes())
888    }
889
890    /// Create a redirect response (302 Found).
891    ///
892    /// # Example
893    /// ```
894    /// use armature_core::HttpResponse;
895    /// let response = HttpResponse::redirect("https://example.com");
896    /// assert_eq!(response.status, 302);
897    /// assert_eq!(response.headers.get("Location"), Some(&"https://example.com".to_string()));
898    /// ```
899    pub fn redirect(url: impl Into<String>) -> Self {
900        Self::new(302).with_header("Location".to_string(), url.into())
901    }
902
903    /// Create a permanent redirect response (301 Moved Permanently).
904    ///
905    /// # Example
906    /// ```
907    /// use armature_core::HttpResponse;
908    /// let response = HttpResponse::redirect_permanent("https://example.com");
909    /// assert_eq!(response.status, 301);
910    /// ```
911    pub fn redirect_permanent(url: impl Into<String>) -> Self {
912        Self::new(301).with_header("Location".to_string(), url.into())
913    }
914
915    /// Create a see other redirect response (303 See Other).
916    /// Useful after a POST request to redirect to a GET.
917    ///
918    /// # Example
919    /// ```
920    /// use armature_core::HttpResponse;
921    /// let response = HttpResponse::see_other("/success");
922    /// assert_eq!(response.status, 303);
923    /// ```
924    pub fn see_other(url: impl Into<String>) -> Self {
925        Self::new(303).with_header("Location".to_string(), url.into())
926    }
927
928    /// Alias for no_content() - returns 204 with empty body.
929    ///
930    /// # Example
931    /// ```
932    /// use armature_core::HttpResponse;
933    /// let response = HttpResponse::empty();
934    /// assert_eq!(response.status, 204);
935    /// ```
936    pub fn empty() -> Self {
937        Self::no_content()
938    }
939
940    /// Set the Content-Type header.
941    ///
942    /// # Example
943    /// ```
944    /// use armature_core::HttpResponse;
945    /// let response = HttpResponse::ok().content_type("application/xml");
946    /// assert_eq!(response.headers.get("Content-Type"), Some(&"application/xml".to_string()));
947    /// ```
948    pub fn content_type(self, content_type: impl Into<String>) -> Self {
949        self.with_header("Content-Type".to_string(), content_type.into())
950    }
951
952    /// Set the Cache-Control header.
953    ///
954    /// # Example
955    /// ```
956    /// use armature_core::HttpResponse;
957    /// let response = HttpResponse::ok().cache_control("max-age=3600");
958    /// ```
959    pub fn cache_control(self, directive: impl Into<String>) -> Self {
960        self.with_header("Cache-Control".to_string(), directive.into())
961    }
962
963    /// Mark the response as not cacheable.
964    ///
965    /// # Example
966    /// ```
967    /// use armature_core::HttpResponse;
968    /// let response = HttpResponse::ok().no_cache();
969    /// ```
970    pub fn no_cache(self) -> Self {
971        self.cache_control("no-store, no-cache, must-revalidate")
972    }
973
974    /// Set a cookie on the response. Can be called multiple times to set
975    /// multiple cookies — each produces a separate `Set-Cookie` header.
976    ///
977    /// # Example
978    /// ```
979    /// use armature_core::HttpResponse;
980    /// let response = HttpResponse::ok()
981    ///     .cookie("session", "abc123; HttpOnly; Secure")
982    ///     .cookie("theme", "dark; Path=/");
983    /// ```
984    pub fn cookie(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
985        self.cookies
986            .push(format!("{}={}", name.into(), value.into()));
987        self
988    }
989
990    /// Clear a cookie by setting it with an expired Max-Age.
991    ///
992    /// # Example
993    /// ```
994    /// use armature_core::HttpResponse;
995    /// let response = HttpResponse::ok().clear_cookie("session", "/");
996    /// ```
997    pub fn clear_cookie(mut self, name: impl Into<String>, path: impl Into<String>) -> Self {
998        self.cookies
999            .push(format!("{}=; Path={}; Max-Age=0", name.into(), path.into(),));
1000        self
1001    }
1002
1003    /// Get the response body as a string (lossy UTF-8 conversion).
1004    pub fn body_string(&self) -> String {
1005        String::from_utf8_lossy(self.body_ref()).to_string()
1006    }
1007
1008    /// Check if the response is successful (2xx status code).
1009    pub fn is_success(&self) -> bool {
1010        (200..300).contains(&self.status)
1011    }
1012
1013    /// Check if the response is a redirect (3xx status code).
1014    pub fn is_redirect(&self) -> bool {
1015        (300..400).contains(&self.status)
1016    }
1017
1018    /// Check if the response is a client error (4xx status code).
1019    pub fn is_client_error(&self) -> bool {
1020        (400..500).contains(&self.status)
1021    }
1022
1023    /// Check if the response is a server error (5xx status code).
1024    pub fn is_server_error(&self) -> bool {
1025        (500..600).contains(&self.status)
1026    }
1027}
1028
1029/// JSON response helper
1030#[derive(Debug)]
1031pub struct Json<T: Serialize>(pub T);
1032
1033impl<T: Serialize> Json<T> {
1034    pub fn into_response(self) -> Result<HttpResponse, crate::Error> {
1035        HttpResponse::ok().with_json(&self.0)
1036    }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041    use super::*;
1042
1043    #[test]
1044    fn new_accepts_str_and_string_and_method() {
1045        // All three forms must compile: existing call sites pass a String, and
1046        // new code should be able to pass a Method directly.
1047        let a = HttpRequest::new("GET", "/a".to_string());
1048        let b = HttpRequest::new("POST", "/b".to_string());
1049        let c = HttpRequest::new(Method::Put, "/c".to_string());
1050        assert_eq!(a.method, Method::Get);
1051        assert_eq!(b.method, Method::Post);
1052        assert_eq!(c.method, Method::Put);
1053    }
1054
1055    #[test]
1056    fn from_parts_ignores_query_params_entirely() {
1057        // The argument is kept for source compatibility only — the query comes
1058        // from the target now. Pinned so a caller still passing a map can't
1059        // silently depend on it being honoured.
1060        let mut query = HashMap::new();
1061        query.insert("page".to_string(), "2".to_string());
1062
1063        let req = HttpRequest::from_parts(
1064            "GET",
1065            "/items",
1066            HashMap::new(),
1067            Vec::new(),
1068            HashMap::new(),
1069            query,
1070        );
1071
1072        assert_eq!(req.query_string(), None);
1073        assert_eq!(req.query_param("page"), None);
1074        assert_eq!(req.query().len(), 0);
1075    }
1076
1077    #[test]
1078    fn with_capacity_ignores_its_capacity_argument() {
1079        // `Bytes` is handed a finished buffer rather than grown in place, so
1080        // there is no body capacity to reserve. Any two capacities must produce
1081        // indistinguishable responses.
1082        let small = HttpResponse::with_capacity(200, 0);
1083        let large = HttpResponse::with_capacity(200, 1 << 20);
1084
1085        assert_eq!(small.status, large.status);
1086        assert_eq!(small.body.len(), large.body.len());
1087        assert!(small.body.is_empty());
1088        assert_eq!(small.headers.len(), large.headers.len());
1089    }
1090
1091    #[test]
1092    fn params_read_back_as_str_and_bytes() {
1093        let mut req = HttpRequest::new("GET", "/users/42/posts/7");
1094        let mut params = RouteParams::new();
1095        params.push((
1096            crate::param_intern::intern("user_id"),
1097            Bytes::from_static(b"42"),
1098        ));
1099        params.push((
1100            crate::param_intern::intern("post_id"),
1101            Bytes::from_static(b"7"),
1102        ));
1103        req.set_params(params);
1104
1105        assert_eq!(req.param("user_id"), Some("42"));
1106        assert_eq!(req.param("post_id"), Some("7"));
1107        assert_eq!(req.param("nope"), None);
1108        assert_eq!(req.param_bytes("user_id").map(|b| b.len()), Some(2));
1109        assert_eq!(
1110            req.param("user_id").and_then(|v| v.parse::<u32>().ok()),
1111            Some(42)
1112        );
1113    }
1114
1115    #[test]
1116    fn four_params_stay_inline() {
1117        let mut params = RouteParams::new();
1118        for name in ["a", "b", "c", "d"] {
1119            params.push((crate::param_intern::intern(name), Bytes::from_static(b"x")));
1120        }
1121        assert!(!params.spilled(), "four params must not allocate");
1122    }
1123
1124    #[test]
1125    fn path_is_a_bytestr_and_still_compares_and_prints_as_a_str() {
1126        let req = HttpRequest::new("GET", "/users/42?a=1");
1127        assert_eq!(req.path_str(), "/users/42?a=1");
1128        assert!(req.path == "/users/42?a=1");
1129        assert_eq!(format!("{}", req.path), "/users/42?a=1");
1130        // Deref<Target = str> keeps the `&str` surface intact.
1131        assert!(req.path.starts_with("/users"));
1132    }
1133
1134    #[test]
1135    fn request_body_is_bytes_and_the_shadow_field_is_gone() {
1136        let mut req = HttpRequest::new("POST", "/x");
1137        req.set_body(b"hello".to_vec());
1138        assert_eq!(req.body_slice(), b"hello");
1139        // The old two-field arrangement could disagree with itself; one field
1140        // cannot.
1141        assert_eq!(req.body_bytes(), Bytes::from_static(b"hello"));
1142        assert!(req.has_bytes_body());
1143
1144        req.set_body_bytes(Bytes::from_static(b"world"));
1145        assert_eq!(req.body_slice(), b"world");
1146        assert_eq!(req.body_ref(), b"world");
1147    }
1148
1149    #[test]
1150    fn cloning_a_body_does_not_copy_it() {
1151        let big = Bytes::from(vec![7u8; 64 * 1024]);
1152        let mut req = HttpRequest::new("POST", "/x");
1153        req.set_body_bytes(big.clone());
1154        let copy = req.clone();
1155        // Same allocation, reached from two requests: the whole point of Bytes.
1156        assert_eq!(copy.body.as_ptr(), req.body.as_ptr());
1157    }
1158
1159    #[test]
1160    fn response_body_is_bytes() {
1161        let mut resp = HttpResponse::new(200);
1162        resp.body = Bytes::from_static(b"{}");
1163        assert_eq!(resp.body_slice(), b"{}");
1164        assert_eq!(resp.body_len(), 2);
1165    }
1166
1167    #[test]
1168    fn method_compares_against_str_and_reports_itself_as_str() {
1169        let req = HttpRequest::new("DELETE", "/x".to_string());
1170        assert!(req.method == "DELETE");
1171        assert!(req.method != "GET");
1172        assert_eq!(req.method_str(), "DELETE");
1173
1174        // An unknown token survives intact rather than being coerced.
1175        let odd = HttpRequest::new("PURGE", "/x".to_string());
1176        assert_eq!(odd.method_str(), "PURGE");
1177        assert!(odd.method == "PURGE");
1178    }
1179
1180    #[test]
1181    fn test_http_request_new() {
1182        let req = HttpRequest::new("GET", "/test".to_string());
1183        assert_eq!(req.method, "GET");
1184        assert_eq!(req.path, "/test");
1185        assert!(req.headers.is_empty());
1186        assert!(req.body.is_empty());
1187    }
1188
1189    #[test]
1190    fn test_http_request_with_body() {
1191        let mut req = HttpRequest::new("POST", "/api".to_string());
1192        req.body = Bytes::from(vec![1, 2, 3, 4]);
1193        assert_eq!(req.body.len(), 4);
1194    }
1195
1196    #[test]
1197    fn test_http_request_json_deserialization() {
1198        #[derive(Deserialize, Debug, PartialEq)]
1199        struct TestData {
1200            name: String,
1201            age: u32,
1202        }
1203
1204        let mut req = HttpRequest::new("POST", "/api".to_string());
1205        req.body = Bytes::from(
1206            serde_json::to_vec(&serde_json::json!({
1207                "name": "John",
1208                "age": 30
1209            }))
1210            .unwrap(),
1211        );
1212
1213        let data: TestData = req.json().unwrap();
1214        assert_eq!(data.name, "John");
1215        assert_eq!(data.age, 30);
1216    }
1217
1218    #[test]
1219    fn test_http_request_param() {
1220        let mut req = HttpRequest::new("GET", "/users/123".to_string());
1221        req.push_param("id", "123");
1222
1223        assert_eq!(req.param("id"), Some("123"));
1224        assert_eq!(req.param("name"), None);
1225    }
1226
1227    #[test]
1228    fn test_http_request_query() {
1229        let req = HttpRequest::new("GET", "/users?sort=asc");
1230
1231        assert_eq!(req.query_param("sort"), Some("asc"));
1232        assert_eq!(req.query_param("limit"), None);
1233    }
1234
1235    #[test]
1236    fn test_http_request_clone() {
1237        let req1 = HttpRequest::new("GET", "/test".to_string());
1238        let req2 = req1.clone();
1239
1240        assert_eq!(req1.method, req2.method);
1241        assert_eq!(req1.path, req2.path);
1242    }
1243
1244    #[test]
1245    fn test_http_response_ok() {
1246        let res = HttpResponse::ok();
1247        assert_eq!(res.status, 200);
1248    }
1249
1250    #[test]
1251    fn test_http_response_created() {
1252        let res = HttpResponse::created();
1253        assert_eq!(res.status, 201);
1254    }
1255
1256    #[test]
1257    fn test_http_response_no_content() {
1258        let res = HttpResponse::no_content();
1259        assert_eq!(res.status, 204);
1260    }
1261
1262    #[test]
1263    fn test_http_response_bad_request() {
1264        let res = HttpResponse::bad_request();
1265        assert_eq!(res.status, 400);
1266    }
1267
1268    #[test]
1269    fn test_http_response_not_found() {
1270        let res = HttpResponse::not_found();
1271        assert_eq!(res.status, 404);
1272    }
1273
1274    #[test]
1275    fn test_http_response_internal_server_error() {
1276        let res = HttpResponse::internal_server_error();
1277        assert_eq!(res.status, 500);
1278    }
1279
1280    #[test]
1281    fn test_http_response_with_body() {
1282        let body = b"Hello, World!".to_vec();
1283        let res = HttpResponse::ok().with_body(body.clone());
1284        assert_eq!(res.body, body);
1285    }
1286
1287    #[test]
1288    fn test_http_response_with_json() {
1289        #[derive(Serialize)]
1290        struct TestData {
1291            message: String,
1292        }
1293
1294        let data = TestData {
1295            message: "test".to_string(),
1296        };
1297
1298        let res = HttpResponse::ok().with_json(&data).unwrap();
1299        assert!(!res.body_ref().is_empty());
1300        assert_eq!(
1301            res.headers.get("Content-Type"),
1302            Some(&"application/json".to_string())
1303        );
1304    }
1305
1306    #[test]
1307    fn test_http_response_with_header() {
1308        let res = HttpResponse::ok().with_header("X-Custom".to_string(), "value".to_string());
1309
1310        assert_eq!(res.headers.get("X-Custom"), Some(&"value".to_string()));
1311    }
1312
1313    #[test]
1314    fn test_http_response_multiple_headers() {
1315        let res = HttpResponse::ok()
1316            .with_header("X-Header-1".to_string(), "value1".to_string())
1317            .with_header("X-Header-2".to_string(), "value2".to_string());
1318
1319        assert_eq!(res.headers.len(), 2);
1320    }
1321
1322    #[test]
1323    fn test_json_helper() {
1324        #[derive(Serialize)]
1325        struct Data {
1326            value: i32,
1327        }
1328
1329        let json = Json(Data { value: 42 });
1330        let response = json.into_response().unwrap();
1331
1332        assert_eq!(response.status, 200);
1333        assert!(!response.body_ref().is_empty());
1334    }
1335
1336    #[test]
1337    fn test_http_request_with_headers() {
1338        let mut req = HttpRequest::new("GET", "/api".to_string());
1339        req.headers
1340            .insert("Authorization", "Bearer token".to_string());
1341        req.headers
1342            .insert("Content-Type", "application/json".to_string());
1343
1344        assert_eq!(req.headers.len(), 2);
1345    }
1346
1347    #[test]
1348    fn test_http_request_from_parts_headermap_roundtrip() {
1349        // `from_parts` still takes a HashMap for backwards compatibility, but
1350        // now stores headers in a `HeaderMap`. Lookups must be case-insensitive.
1351        let mut headers = HashMap::new();
1352        headers.insert("Content-Type".to_string(), "application/json".to_string());
1353        headers.insert("X-Custom".to_string(), "abc".to_string());
1354
1355        let req = HttpRequest::from_parts(
1356            "GET",
1357            "/api".to_string(),
1358            headers,
1359            Vec::new(),
1360            HashMap::new(),
1361            HashMap::new(),
1362        );
1363
1364        assert_eq!(req.headers.len(), 2);
1365        // Case-insensitive lookup via HeaderMap.
1366        assert_eq!(req.headers.get("content-type"), Some("application/json"));
1367        assert_eq!(req.headers.get("Content-Type"), Some("application/json"));
1368        assert!(req.headers.contains_key("x-custom"));
1369    }
1370
1371    #[test]
1372    fn test_http_request_json_invalid() {
1373        #[derive(Deserialize)]
1374        #[allow(dead_code)]
1375        struct TestData {
1376            name: String,
1377        }
1378
1379        let mut req = HttpRequest::new("POST", "/api".to_string());
1380        req.body = Bytes::from_static(b"invalid json");
1381
1382        let result: Result<TestData, crate::Error> = req.json();
1383        assert!(result.is_err());
1384    }
1385
1386    #[test]
1387    fn test_http_response_new_custom_status() {
1388        let res = HttpResponse::new(418); // I'm a teapot
1389        assert_eq!(res.status, 418);
1390    }
1391
1392    #[test]
1393    fn test_http_response_with_json_complex() {
1394        #[derive(Serialize)]
1395        struct ComplexData {
1396            nested: Vec<HashMap<String, i32>>,
1397        }
1398
1399        let mut map = HashMap::new();
1400        map.insert("key".to_string(), 123);
1401
1402        let data = ComplexData { nested: vec![map] };
1403
1404        let res = HttpResponse::ok().with_json(&data);
1405        assert!(res.is_ok());
1406    }
1407}