1use crate::body::Body;
4use crate::error::ApiError;
5use crate::extract::{FromRequest, Request, invalid, validated};
6use bytes::Bytes;
7use http::StatusCode;
8use serde::de::DeserializeOwned;
9use siderite_openapi::{Operation, Schema, SchemaObject, SchemaRegistry};
10use siderite_validation::{Validate, ValidationContext, text_pairs_to_value};
11
12pub const DEFAULT_MULTIPART_LIMIT: usize = 10 * 1024 * 1024;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub struct Form<T>(pub T);
22
23impl<T> FromRequest for Form<T>
24where
25 T: DeserializeOwned + Send + Schema + Validate + 'static,
26{
27 async fn from_request(req: Request) -> Result<Self, ApiError> {
28 use axum::extract::FromRequest as _;
29 use axum::extract::rejection::FormRejection;
30 match axum::Form::<Vec<(String, String)>>::from_request(req.map(Body::into_inner), &())
31 .await
32 {
33 Ok(axum::Form(pairs)) => validated(
34 text_pairs_to_value(pairs),
35 ValidationContext::for_text().at_root("body"),
36 )
37 .map(Form),
38 Err(rejection) => {
39 let message = rejection.body_text();
40 Err(match rejection {
41 FormRejection::FailedToDeserializeForm(_)
42 | FormRejection::FailedToDeserializeFormBody(_) => {
43 invalid("body", "form_invalid", message)
44 }
45 FormRejection::InvalidFormContentType(_) => {
46 ApiError::new(StatusCode::UNSUPPORTED_MEDIA_TYPE, message)
47 }
48 FormRejection::BytesRejection(rejection) => {
49 ApiError::new(rejection.status(), message)
50 }
51 other => ApiError::new(other.status(), message),
52 })
53 }
54 }
55 }
56
57 fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
58 let schema = registry.subschema::<T>();
59 op.set_request_body("application/x-www-form-urlencoded", schema, true);
60 }
61}
62
63#[derive(Debug)]
68pub struct Multipart {
69 inner: axum::extract::Multipart,
70}
71
72impl FromRequest for Multipart {
73 async fn from_request(mut req: Request) -> Result<Self, ApiError> {
74 let content_type = req
75 .headers()
76 .get(http::header::CONTENT_TYPE)
77 .and_then(|v| v.to_str().ok())
78 .unwrap_or("");
79 if !content_type
80 .to_ascii_lowercase()
81 .starts_with("multipart/form-data")
82 {
83 return Err(ApiError::new(
84 StatusCode::UNSUPPORTED_MEDIA_TYPE,
85 "multipart requests must have Content-Type: multipart/form-data",
86 ));
87 }
88 axum::extract::DefaultBodyLimit::max(DEFAULT_MULTIPART_LIMIT).apply(&mut req);
89 use axum::extract::FromRequest as _;
90 match axum::extract::Multipart::from_request(req.map(Body::into_inner), &()).await {
91 Ok(inner) => Ok(Self { inner }),
92 Err(rejection) => Err(ApiError::bad_request(rejection.body_text())),
93 }
94 }
95
96 fn describe(op: &mut Operation, _registry: &mut SchemaRegistry) {
97 op.set_request_body("multipart/form-data", SchemaObject::of_type("object"), true);
98 }
99}
100
101impl Multipart {
102 pub async fn next_field(&mut self) -> Result<Option<MultipartField<'_>>, ApiError> {
108 match self.inner.next_field().await {
109 Ok(Some(field)) => Ok(Some(MultipartField { inner: field })),
110 Ok(None) => Ok(None),
111 Err(err) => Err(multipart_error(err)),
112 }
113 }
114}
115
116#[derive(Debug)]
118pub struct MultipartField<'a> {
119 inner: axum::extract::multipart::Field<'a>,
120}
121
122impl MultipartField<'_> {
123 #[must_use]
125 pub fn name(&self) -> Option<&str> {
126 self.inner.name()
127 }
128
129 #[must_use]
131 pub fn file_name(&self) -> Option<&str> {
132 self.inner.file_name()
133 }
134
135 #[must_use]
137 pub fn content_type(&self) -> Option<&str> {
138 self.inner.content_type()
139 }
140
141 pub async fn bytes(self) -> Result<Bytes, ApiError> {
147 self.inner.bytes().await.map_err(multipart_error)
148 }
149
150 pub async fn text(self) -> Result<String, ApiError> {
156 self.inner.text().await.map_err(multipart_error)
157 }
158}
159
160fn multipart_error(err: axum::extract::multipart::MultipartError) -> ApiError {
161 ApiError::new(err.status(), err.body_text())
162}