Skip to main content

alux_http_openapi/
output.rs

1//! States what each output kind answers with, in the vocabulary a document reads.
2
3use crate::OpenApiHandlerImpl;
4use alux_http::{
5    BytesOutAlg, EmptyOutAlg, FileOutAlg, HeaderNameAlg, HeaderOutAlg, HtmlOutAlg, HttpErrorAlg, HttpStatus,
6    JsonOutAlg, OutputAlg, RedirectOutAlg, ResultOutAlg, StatusOutAlg, StreamOutAlg, TextOutAlg,
7};
8use alux_shape::ShapeOf;
9use alux_shape_jsonschema::{JsonSchema, JsonSchemaShape};
10use core::marker::PhantomData;
11use serde_json::Value;
12
13/// The media type a body of bytes is written as, whatever produced the bytes.
14const BYTES: &str = "application/octet-stream";
15
16/// The header a redirect carries, naming where the caller is sent.
17const LOCATION: &str = "location";
18
19/// One answer an endpoint states, as a document describes it.
20#[derive(Debug, Clone)]
21pub struct OpenApiAnswer {
22    /// The status this answer carries.
23    pub status: HttpStatus,
24    /// Every header this answer carries beside its body.
25    pub headers: Vec<&'static str>,
26    /// The media type this answer is written as, where it has a body.
27    pub content_type: Option<&'static str>,
28    /// The schema this answer's body carries, where it has one.
29    pub schema: Option<Value>,
30}
31
32impl OpenApiAnswer {
33    /// States an answer carrying a body of a stated media type.
34    pub fn content(status: HttpStatus, content_type: &'static str, schema: Value) -> Self {
35        Self { status, headers: Vec::new(), content_type: Some(content_type), schema: Some(schema) }
36    }
37
38    /// States an answer carrying no body.
39    pub fn bodiless(status: HttpStatus) -> Self {
40        Self { status, headers: Vec::new(), content_type: None, schema: None }
41    }
42}
43
44/// States how a document describes what one output kind answers with.
45///
46/// An interpretation that runs converts a value it was given. This one has no value, so what an
47/// endpoint answers with has to be read from the kind's type.
48pub trait OpenApiOutputAlg<From> {
49    /// Describes every answer this kind states, naming the shapes it mentions along the way.
50    fn answers(schema: &JsonSchemaShape) -> Vec<OpenApiAnswer>;
51}
52
53macro_rules! openapi_outputs {
54    ($($output:ident => $content_type:literal, $meaning:literal),+ $(,)?) => {
55        $(
56            #[doc = concat!("Describes ", $meaning, " answers in a document.")]
57            pub struct $output;
58
59            impl<From> OutputAlg<From> for $output {
60                type Output = From;
61
62                fn output(from: From) -> From {
63                    from
64                }
65            }
66
67            impl<From> OpenApiOutputAlg<From> for $output
68            where
69                From: ShapeOf<JsonSchemaShape, Shape = JsonSchema>,
70            {
71                fn answers(schema: &JsonSchemaShape) -> Vec<OpenApiAnswer> {
72                    vec![OpenApiAnswer::content(HttpStatus::OK, $content_type, From::shape_of(schema).into_value())]
73                }
74            }
75        )+
76    };
77}
78
79openapi_outputs! {
80    OpenApiJsonOutput => "application/json", "JSON",
81    OpenApiTextOutput => "text/plain", "plain-text",
82    OpenApiHtmlOutput => "text/html", "HTML",
83}
84
85macro_rules! openapi_bytes {
86    ($($output:ident => $meaning:literal),+ $(,)?) => {
87        $(
88            #[doc = concat!("Describes ", $meaning, " answers in a document.")]
89            pub struct $output;
90
91            impl<From> OutputAlg<From> for $output {
92                type Output = From;
93
94                fn output(from: From) -> From {
95                    from
96                }
97            }
98        )+
99    };
100}
101
102openapi_bytes! {
103    OpenApiBytesOutput  => "raw-byte",
104    OpenApiFileOutput   => "streamed-file",
105    OpenApiStreamOutput => "streamed",
106}
107
108/// Describes the bytes an endpoint answers with, whatever produced them.
109///
110/// What a caller receives is bytes, so the document says so: a shape would describe the value the
111/// handler answered with rather than the body it becomes, and a body produced over time has no
112/// shape to describe at all.
113impl<From> OpenApiOutputAlg<From> for OpenApiBytesOutput {
114    fn answers(_schema: &JsonSchemaShape) -> Vec<OpenApiAnswer> {
115        vec![OpenApiAnswer::content(HttpStatus::OK, BYTES, binary())]
116    }
117}
118
119impl<From> OpenApiOutputAlg<From> for OpenApiStreamOutput {
120    fn answers(_schema: &JsonSchemaShape) -> Vec<OpenApiAnswer> {
121        vec![OpenApiAnswer::content(HttpStatus::OK, BYTES, binary())]
122    }
123}
124
125/// Describes a download: the bytes it answers with, and what reading the file can fail as.
126///
127/// A file handler answers with the file it read and the name to offer it under, so the failure is
128/// stated by the file rather than by a `.result()` around the endpoint. Both halves are described
129/// here: the successful body is bytes, and the failure states its own statuses.
130impl<File, Error> OpenApiOutputAlg<(Result<File, Error>, String)> for OpenApiFileOutput
131where
132    Error: HttpErrorAlg,
133{
134    fn answers(_schema: &JsonSchemaShape) -> Vec<OpenApiAnswer> {
135        let failures = Error::HTTP_STATUSES.iter().map(|status| OpenApiAnswer::content(*status, "text/plain", text()));
136
137        core::iter::once(OpenApiAnswer::content(HttpStatus::OK, BYTES, binary())).chain(failures).collect()
138    }
139}
140
141/// Describes an answer with no body in a document.
142pub struct OpenApiEmptyOutput;
143
144impl<From> OutputAlg<From> for OpenApiEmptyOutput {
145    type Output = From;
146
147    fn output(from: From) -> From {
148        from
149    }
150}
151
152impl OpenApiOutputAlg<()> for OpenApiEmptyOutput {
153    fn answers(_schema: &JsonSchemaShape) -> Vec<OpenApiAnswer> {
154        vec![OpenApiAnswer::bodiless(HttpStatus::NO_CONTENT)]
155    }
156}
157
158/// Describes a redirect in a document.
159pub struct OpenApiRedirectOutput;
160
161impl<From> OutputAlg<From> for OpenApiRedirectOutput {
162    type Output = From;
163
164    fn output(from: From) -> From {
165        from
166    }
167}
168
169impl<From> OpenApiOutputAlg<From> for OpenApiRedirectOutput {
170    fn answers(_schema: &JsonSchemaShape) -> Vec<OpenApiAnswer> {
171        // Where a caller is sent is what a redirect answers, and it is carried by this header, so
172        // a document that omits it describes an answer no interpretation produces.
173        vec![OpenApiAnswer { headers: vec![LOCATION], ..OpenApiAnswer::bodiless(HttpStatus::SEE_OTHER) }]
174    }
175}
176
177/// Describes a header an answer carries, beside the body it states.
178pub struct OpenApiHeaderOutput<Inner, Name>(PhantomData<fn(Inner, Name)>);
179
180impl<Inner, Name, From> OutputAlg<From> for OpenApiHeaderOutput<Inner, Name> {
181    type Output = From;
182
183    fn output(from: From) -> From {
184        from
185    }
186}
187
188impl<Inner, Name, Value, Rest> OpenApiOutputAlg<(Value, Rest)> for OpenApiHeaderOutput<Inner, Name>
189where
190    Inner: OpenApiOutputAlg<Rest>,
191    Name: HeaderNameAlg,
192{
193    fn answers(schema: &JsonSchemaShape) -> Vec<OpenApiAnswer> {
194        Inner::answers(schema)
195            .into_iter()
196            .map(|answer| {
197                let mut carried = answer;
198                if carried.status.is_success() {
199                    carried.headers.push(Name::HEADER_NAME);
200                }
201
202                carried
203            })
204            .collect()
205    }
206}
207
208impl<Context> HeaderOutAlg for OpenApiHandlerImpl<Context> {
209    type Header<Inner, Name> = OpenApiHeaderOutput<Inner, Name>;
210}
211
212/// Describes a declared status around the answer a kind already states.
213pub struct OpenApiStatusOutput<Inner, const CODE: u16>(PhantomData<Inner>);
214
215impl<Inner, From, const CODE: u16> OutputAlg<From> for OpenApiStatusOutput<Inner, CODE> {
216    type Output = From;
217
218    fn output(from: From) -> From {
219        from
220    }
221}
222
223impl<Inner, From, const CODE: u16> OpenApiOutputAlg<From> for OpenApiStatusOutput<Inner, CODE>
224where
225    Inner: OpenApiOutputAlg<From>,
226{
227    fn answers(schema: &JsonSchemaShape) -> Vec<OpenApiAnswer> {
228        Inner::answers(schema)
229            .into_iter()
230            .map(|answer| OpenApiAnswer { status: HttpStatus::new(CODE), ..answer })
231            .collect()
232    }
233}
234
235/// Describes both what an endpoint answers with and what its failures answer with.
236///
237/// A failure states its statuses on its type, which is the only place a document can read them: it
238/// folds a program rather than running one, so it never holds a failure to ask.
239pub struct OpenApiResultOutput<Inner, Error>(PhantomData<fn(Inner, Error)>);
240
241impl<Inner, Error, From> OutputAlg<From> for OpenApiResultOutput<Inner, Error> {
242    type Output = From;
243
244    fn output(from: From) -> From {
245        from
246    }
247}
248
249impl<Inner, Error, Value> OpenApiOutputAlg<Result<Value, Error>> for OpenApiResultOutput<Inner, Error>
250where
251    Inner: OpenApiOutputAlg<Value>,
252    Error: HttpErrorAlg,
253{
254    fn answers(schema: &JsonSchemaShape) -> Vec<OpenApiAnswer> {
255        let failures = Error::HTTP_STATUSES.iter().map(|status| OpenApiAnswer::content(*status, "text/plain", text()));
256
257        Inner::answers(schema).into_iter().chain(failures).collect()
258    }
259}
260
261/// The schema a stated failure carries, which is the message it answers with.
262fn text() -> Value {
263    serde_json::json!({ "type": "string" })
264}
265
266/// The schema a body of bytes carries, which a document states rather than describes.
267fn binary() -> Value {
268    serde_json::json!({ "type": "string", "format": "binary" })
269}
270
271macro_rules! openapi_kinds {
272    ($($alg:ident => $selected:ident, $output:ty),+ $(,)?) => {
273        $(
274            impl<Context> $alg for OpenApiHandlerImpl<Context> {
275                type $selected<From> = $output;
276            }
277        )+
278    };
279}
280
281openapi_kinds! {
282    JsonOutAlg     => Json, OpenApiJsonOutput,
283    FileOutAlg     => File, OpenApiFileOutput,
284    TextOutAlg     => Text, OpenApiTextOutput,
285    HtmlOutAlg     => Html, OpenApiHtmlOutput,
286    BytesOutAlg    => Bytes, OpenApiBytesOutput,
287    EmptyOutAlg    => Empty, OpenApiEmptyOutput,
288    RedirectOutAlg => Redirect, OpenApiRedirectOutput,
289    StreamOutAlg   => Stream, OpenApiStreamOutput,
290}
291
292impl<Context> StatusOutAlg for OpenApiHandlerImpl<Context> {
293    type Status<Inner, const CODE: u16> = OpenApiStatusOutput<Inner, CODE>;
294}
295
296impl<Context> ResultOutAlg for OpenApiHandlerImpl<Context> {
297    type Result<Inner, Error> = OpenApiResultOutput<Inner, Error>;
298}