Skip to main content

siderite_core/
response.rs

1//! Responses.
2//!
3//! siderite owns [`IntoResponse`] so response types document themselves for
4//! OpenAPI through [`IntoResponse::describe`] (default: nothing). Handlers may
5//! return `impl IntoResponse`.
6
7use crate::body::Body;
8use crate::error::ApiError;
9use http::{HeaderValue, StatusCode, header};
10use siderite_openapi::{Operation, Schema, SchemaRegistry};
11use siderite_validation::{Dump, DumpOptions};
12
13/// An HTTP response with a siderite [`Body`].
14pub type Response = http::Response<Body>;
15
16/// Status code under which handler return types document success.
17pub const SUCCESS: &str = "200";
18
19/// Convert a value into an HTTP response.
20pub trait IntoResponse {
21    /// Build the response.
22    fn into_response(self) -> Response;
23
24    /// Document the responses this type can produce.
25    fn describe(_op: &mut Operation, _registry: &mut SchemaRegistry) {}
26}
27
28/// JSON body (response) or JSON request extractor.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub struct Json<T>(pub T);
31
32/// HTML response body.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Html<T>(pub T);
35
36/// `text/plain` response body.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct PlainText<T>(pub T);
39
40/// `204 No Content` response.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct NoContent;
43
44/// Overrides the status code of an inner response.
45///
46/// The status is only known at runtime, so the OpenAPI document still lists
47/// the inner response under its own code (usually `200`). To document a
48/// fixed non-200 success status, set it on the route with
49/// [`MethodRouter::status`](crate::routing::MethodRouter::status) instead
50/// and return the plain body from the handler.
51#[derive(Debug, Clone, Copy)]
52pub struct WithStatus<R>(pub StatusCode, pub R);
53
54/// Build a response with `body` and a `Content-Type`.
55pub(crate) fn with_content_type(content_type: &'static str, body: impl Into<Body>) -> Response {
56    let mut response = Response::new(body.into());
57    response
58        .headers_mut()
59        .insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
60    response
61}
62
63/// Build a bodiless response with `status`.
64pub(crate) fn status_only(status: StatusCode) -> Response {
65    let mut response = Response::new(Body::empty());
66    *response.status_mut() = status;
67    response
68}
69
70impl IntoResponse for Response {
71    fn into_response(self) -> Response {
72        self
73    }
74}
75
76/// Serialize `value` with `opts` into a JSON response.
77fn dump_response<T: Dump + ?Sized>(value: &T, opts: &DumpOptions) -> Response {
78    let value = siderite_validation::dump::DumpSerialize(value, opts);
79    match serde_json::to_vec(&value) {
80        Ok(bytes) => with_content_type("application/json", bytes),
81        Err(err) => ApiError::internal(err).into_response(),
82    }
83}
84
85/// JSON response serialized with explicit [`DumpOptions`]
86/// (FastAPI `response_model_exclude_none` and friends).
87#[derive(Debug, Clone)]
88pub struct JsonDump<T>(pub T, pub DumpOptions);
89
90impl<T: Dump + Schema + 'static> IntoResponse for JsonDump<T> {
91    fn into_response(self) -> Response {
92        dump_response(&self.0, &self.1)
93    }
94
95    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
96        <Json<T> as IntoResponse>::describe(op, registry);
97    }
98}
99
100impl<T: Dump + Schema + 'static> IntoResponse for Json<T> {
101    /// Serialized through [`Dump`], so computed fields and serializers apply.
102    fn into_response(self) -> Response {
103        dump_response(&self.0, &DumpOptions::default())
104    }
105
106    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
107        let schema = registry.subschema::<T>();
108        op.add_response(
109            SUCCESS,
110            "Successful Response",
111            Some(("application/json", schema)),
112        );
113    }
114}
115
116macro_rules! text_like {
117    ($($ty:ident => $mime:literal),*) => {$(
118        impl<T: Into<String>> IntoResponse for $ty<T> {
119            fn into_response(self) -> Response {
120                with_content_type($mime, self.0.into())
121            }
122
123            fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
124                let schema = registry.subschema::<String>();
125                op.add_response(SUCCESS, "Successful Response", Some(($mime, schema)));
126            }
127        }
128    )*};
129}
130text_like!(Html => "text/html; charset=utf-8", PlainText => "text/plain; charset=utf-8");
131
132impl IntoResponse for &'static str {
133    fn into_response(self) -> Response {
134        PlainText(self).into_response()
135    }
136
137    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
138        <PlainText<String> as IntoResponse>::describe(op, registry);
139    }
140}
141
142impl IntoResponse for String {
143    fn into_response(self) -> Response {
144        PlainText(self).into_response()
145    }
146
147    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
148        <PlainText<String> as IntoResponse>::describe(op, registry);
149    }
150}
151
152impl IntoResponse for () {
153    fn into_response(self) -> Response {
154        status_only(StatusCode::OK)
155    }
156
157    fn describe(op: &mut Operation, _: &mut SchemaRegistry) {
158        op.add_response(SUCCESS, "Successful Response", None);
159    }
160}
161
162impl IntoResponse for StatusCode {
163    fn into_response(self) -> Response {
164        status_only(self)
165    }
166}
167
168impl IntoResponse for NoContent {
169    fn into_response(self) -> Response {
170        status_only(StatusCode::NO_CONTENT)
171    }
172
173    fn describe(op: &mut Operation, _: &mut SchemaRegistry) {
174        op.add_response("204", "No Content", None);
175    }
176}
177
178impl<R: IntoResponse> IntoResponse for WithStatus<R> {
179    fn into_response(self) -> Response {
180        let mut response = self.1.into_response();
181        *response.status_mut() = self.0;
182        response
183    }
184
185    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
186        R::describe(op, registry);
187    }
188}
189
190/// Same as [`WithStatus`]: the runtime status is not documented. Use
191/// [`MethodRouter::status`](crate::routing::MethodRouter::status) for a
192/// documented non-200 success status.
193impl<R: IntoResponse> IntoResponse for (StatusCode, R) {
194    fn into_response(self) -> Response {
195        WithStatus(self.0, self.1).into_response()
196    }
197
198    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
199        R::describe(op, registry);
200    }
201}
202
203impl<T: IntoResponse, E: IntoResponse> IntoResponse for Result<T, E> {
204    fn into_response(self) -> Response {
205        match self {
206            Ok(v) => v.into_response(),
207            Err(e) => e.into_response(),
208        }
209    }
210
211    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
212        T::describe(op, registry);
213        E::describe(op, registry);
214    }
215}