Skip to main content

doido_controller/
respond.rs

1//! Format-based content negotiation (Rails `respond_to`).
2//!
3//! [`Context::negotiated_format`](crate::Context::negotiated_format) picks a
4//! [`Format`] from the request (a `.json`/`.html` path extension wins, else the
5//! `Accept` header), and [`Context::respond_to`](crate::Context::respond_to)
6//! returns a [`RespondTo`] builder that runs the branch matching that format.
7
8use crate::axum::body::Body;
9use crate::axum::response::Response;
10use http::StatusCode;
11
12/// A negotiated response format. `Any` matches the first branch offered.
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub enum Format {
15    Html,
16    Json,
17    Any,
18}
19
20/// Builder that evaluates only the branch matching the negotiated format.
21pub struct RespondTo {
22    format: Format,
23    response: Option<Response>,
24}
25
26impl RespondTo {
27    pub fn new(format: Format) -> Self {
28        Self {
29            format,
30            response: None,
31        }
32    }
33
34    /// Offer an HTML branch (runs for `Html` or `Any`).
35    pub fn html(mut self, f: impl FnOnce() -> Response) -> Self {
36        if self.response.is_none() && matches!(self.format, Format::Html | Format::Any) {
37            self.response = Some(f());
38        }
39        self
40    }
41
42    /// Offer a JSON branch (runs for `Json` or `Any`).
43    pub fn json(mut self, f: impl FnOnce() -> Response) -> Self {
44        if self.response.is_none() && matches!(self.format, Format::Json | Format::Any) {
45            self.response = Some(f());
46        }
47        self
48    }
49
50    /// Resolve to the matched branch, or `406 Not Acceptable` if none matched.
51    pub fn finish(self) -> Response {
52        self.response.unwrap_or_else(|| {
53            Response::builder()
54                .status(StatusCode::NOT_ACCEPTABLE)
55                .body(Body::empty())
56                .expect("valid 406 response")
57        })
58    }
59}