1use 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
15const BYTES: &str = "application/octet-stream";
17
18const LOCATION: &str = "location";
20
21#[derive(Debug, Clone)]
23pub struct OpenApiAnswer {
24 pub status: HttpStatus,
26 pub headers: Vec<String>,
28 pub content_type: Option<&'static str>,
30 pub schema: Option<Value>,
32}
33
34impl OpenApiAnswer {
35 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 pub fn bodiless(status: HttpStatus) -> Self {
42 Self { status, headers: Vec::new(), content_type: None, schema: None }
43 }
44}
45
46pub trait OpenApiOutputAlg<From> {
51 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
110impl<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
127impl<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
143pub 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
160pub 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 vec![OpenApiAnswer { headers: vec![LOCATION.to_owned()], ..OpenApiAnswer::bodiless(HttpStatus::SEE_OTHER) }]
176 }
177}
178
179pub 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
215pub 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
255pub 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
278pub 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
304fn text() -> Value {
306 serde_json::json!({ "type": "string" })
307}
308
309fn 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}