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