1use std::ops::{Deref, DerefMut};
2
3use axum::{
4 Json,
5 extract::{FromRequest, FromRequestParts, Path, Query, Request},
6};
7use http::{StatusCode, request::Parts};
8use minco_contract::{ContractValidate, ContractValidationErrors};
9use serde::de::DeserializeOwned;
10
11use crate::{ApiFailure, request_id_from_headers};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct ValidatedJson<T>(pub T);
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct ValidatedQuery<T>(pub T);
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct ValidatedPath<T>(pub T);
24
25macro_rules! validated_accessors {
26 ($name:ident) => {
27 impl<T> $name<T> {
28 #[must_use]
29 pub fn into_inner(self) -> T {
30 self.0
31 }
32 }
33
34 impl<T> Deref for $name<T> {
35 type Target = T;
36
37 fn deref(&self) -> &Self::Target {
38 &self.0
39 }
40 }
41
42 impl<T> DerefMut for $name<T> {
43 fn deref_mut(&mut self) -> &mut Self::Target {
44 &mut self.0
45 }
46 }
47 };
48}
49
50validated_accessors!(ValidatedJson);
51validated_accessors!(ValidatedQuery);
52validated_accessors!(ValidatedPath);
53
54impl<T, S> FromRequest<S> for ValidatedJson<T>
55where
56 T: DeserializeOwned + ContractValidate + Send,
57 S: Send + Sync,
58{
59 type Rejection = ApiFailure;
60
61 async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
62 let request_id = request_id_from_headers(request.headers());
63 let Json(value) = Json::<T>::from_request(request, state)
64 .await
65 .map_err(|rejection| json_failure(rejection, request_id.clone()))?;
66 validate(value, request_id).map(Self)
67 }
68}
69
70impl<T, S> FromRequestParts<S> for ValidatedQuery<T>
71where
72 T: DeserializeOwned + ContractValidate + Send,
73 S: Send + Sync,
74{
75 type Rejection = ApiFailure;
76
77 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
78 let request_id = request_id_from_headers(&parts.headers);
79 let Query(value) = Query::<T>::from_request_parts(parts, state)
80 .await
81 .map_err(|_| {
82 ApiFailure::new(
83 StatusCode::BAD_REQUEST,
84 "invalid_query",
85 "Invalid query",
86 "Query parameters do not match the operation contract.",
87 request_id.clone(),
88 )
89 })?;
90 validate(value, request_id).map(Self)
91 }
92}
93
94impl<T, S> FromRequestParts<S> for ValidatedPath<T>
95where
96 T: DeserializeOwned + ContractValidate + Send,
97 S: Send + Sync,
98{
99 type Rejection = ApiFailure;
100
101 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
102 let request_id = request_id_from_headers(&parts.headers);
103 let Path(value) = Path::<T>::from_request_parts(parts, state)
104 .await
105 .map_err(|_| {
106 ApiFailure::new(
107 StatusCode::BAD_REQUEST,
108 "invalid_path",
109 "Invalid path",
110 "Path parameters do not match the operation contract.",
111 request_id.clone(),
112 )
113 })?;
114 validate(value, request_id).map(Self)
115 }
116}
117
118fn validate<T: ContractValidate>(value: T, request_id: String) -> Result<T, ApiFailure> {
119 let mut errors = ContractValidationErrors::new();
120 value.validate_contract(&mut errors);
121 if errors.is_empty() {
122 return Ok(value);
123 }
124 let mut failure =
125 ApiFailure::validation("Request fields violate the operation contract.", request_id);
126 failure.errors = errors.into_fields();
127 Err(failure)
128}
129
130fn json_failure(
131 rejection: axum::extract::rejection::JsonRejection,
132 request_id: String,
133) -> ApiFailure {
134 use axum::extract::rejection::JsonRejection;
135
136 match rejection {
137 JsonRejection::MissingJsonContentType(_) => ApiFailure::new(
138 StatusCode::UNSUPPORTED_MEDIA_TYPE,
139 "unsupported_media_type",
140 "Unsupported media type",
141 "A supported application/json Content-Type is required.",
142 request_id,
143 ),
144 JsonRejection::JsonSyntaxError(_) => ApiFailure::new(
145 StatusCode::BAD_REQUEST,
146 "invalid_json",
147 "Invalid JSON",
148 "Request body is not valid JSON.",
149 request_id,
150 ),
151 JsonRejection::BytesRejection(rejection)
152 if rejection.status() == StatusCode::PAYLOAD_TOO_LARGE =>
153 {
154 ApiFailure::new(
155 StatusCode::PAYLOAD_TOO_LARGE,
156 "payload_too_large",
157 "Payload too large",
158 "Request body exceeds the configured limit.",
159 request_id,
160 )
161 }
162 _ => ApiFailure::new(
163 StatusCode::BAD_REQUEST,
164 "invalid_request",
165 "Invalid request",
166 "Request body does not match the operation contract.",
167 request_id,
168 ),
169 }
170}