1use 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
13const BYTES: &str = "application/octet-stream";
15
16const LOCATION: &str = "location";
18
19#[derive(Debug, Clone)]
21pub struct OpenApiAnswer {
22 pub status: HttpStatus,
24 pub headers: Vec<&'static str>,
26 pub content_type: Option<&'static str>,
28 pub schema: Option<Value>,
30}
31
32impl OpenApiAnswer {
33 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 pub fn bodiless(status: HttpStatus) -> Self {
40 Self { status, headers: Vec::new(), content_type: None, schema: None }
41 }
42}
43
44pub trait OpenApiOutputAlg<From> {
49 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
108impl<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
125impl<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
141pub 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
158pub 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 vec![OpenApiAnswer { headers: vec![LOCATION], ..OpenApiAnswer::bodiless(HttpStatus::SEE_OTHER) }]
174 }
175}
176
177pub 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
212pub 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
235pub 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
261fn text() -> Value {
263 serde_json::json!({ "type": "string" })
264}
265
266fn 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}