lenso_capability_http_endpoint/
extract.rs1use futures::future::LocalBoxFuture;
4use lenso_kernel::InvocationContext;
5use serde::de::DeserializeOwned;
6
7use crate::{EndpointHandleInvocationError, HandleRequest, HandleResponse, response};
8
9#[derive(Debug)]
11pub enum ExtractorRejection {
12 Response(HandleResponse),
14 Invocation(EndpointHandleInvocationError),
16}
17
18impl From<HandleResponse> for ExtractorRejection {
19 fn from(response: HandleResponse) -> Self {
20 Self::Response(response)
21 }
22}
23
24impl From<EndpointHandleInvocationError> for ExtractorRejection {
25 fn from(error: EndpointHandleInvocationError) -> Self {
26 Self::Invocation(error)
27 }
28}
29
30impl From<response::ResponseBuildError> for ExtractorRejection {
31 fn from(error: response::ResponseBuildError) -> Self {
32 Self::Invocation(error.into())
33 }
34}
35
36pub type ExtractorFuture<'a, T> = LocalBoxFuture<'a, Result<T, ExtractorRejection>>;
38
39pub trait FromRequest<P: ?Sized>: Sized {
46 fn from_request<'a>(
48 provider: &'a P,
49 context: &'a mut InvocationContext,
50 request: &'a HandleRequest,
51 ) -> ExtractorFuture<'a, Self>;
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct Json<T>(pub T);
57
58impl<P, T> FromRequest<P> for Json<T>
59where
60 P: ?Sized,
61 T: DeserializeOwned + 'static,
62{
63 fn from_request<'a>(
64 _provider: &'a P,
65 _context: &'a mut InvocationContext,
66 request: &'a HandleRequest,
67 ) -> ExtractorFuture<'a, Self> {
68 Box::pin(futures::future::ready(extract_json(request)))
69 }
70}
71
72#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct Path<T>(pub T);
75
76impl<P, T> FromRequest<P> for Path<T>
77where
78 P: ?Sized,
79 T: DeserializeOwned + 'static,
80{
81 fn from_request<'a>(
82 _provider: &'a P,
83 _context: &'a mut InvocationContext,
84 request: &'a HandleRequest,
85 ) -> ExtractorFuture<'a, Self> {
86 Box::pin(futures::future::ready(extract_path(request)))
87 }
88}
89
90#[derive(Clone, Debug, Eq, PartialEq)]
92pub struct Query<T>(pub T);
93
94impl<P, T> FromRequest<P> for Query<T>
95where
96 P: ?Sized,
97 T: DeserializeOwned + 'static,
98{
99 fn from_request<'a>(
100 _provider: &'a P,
101 _context: &'a mut InvocationContext,
102 request: &'a HandleRequest,
103 ) -> ExtractorFuture<'a, Self> {
104 Box::pin(futures::future::ready(extract_query(request)))
105 }
106}
107
108#[derive(Clone, Debug, Eq, PartialEq)]
110pub struct RequestId(pub String);
111
112impl<P> FromRequest<P> for RequestId
113where
114 P: ?Sized,
115{
116 fn from_request<'a>(
117 _provider: &'a P,
118 _context: &'a mut InvocationContext,
119 request: &'a HandleRequest,
120 ) -> ExtractorFuture<'a, Self> {
121 Box::pin(futures::future::ready(Ok(Self(request.request_id.clone()))))
122 }
123}
124
125fn extract_json<T>(request: &HandleRequest) -> Result<Json<T>, ExtractorRejection>
126where
127 T: DeserializeOwned,
128{
129 if !has_json_content_type(request) {
130 return Err(response::problem(
131 response::StatusCode::UNSUPPORTED_MEDIA_TYPE,
132 "json_content_type_required",
133 "The request content type must be application/json.",
134 )
135 .into());
136 }
137 serde_json::from_slice(request.body.as_ref())
138 .map(Json)
139 .map_err(|_| {
140 response::problem(
141 response::StatusCode::BAD_REQUEST,
142 "invalid_json_body",
143 "The request body is not valid JSON for this endpoint.",
144 )
145 .into()
146 })
147}
148
149fn extract_path<T>(request: &HandleRequest) -> Result<Path<T>, ExtractorRejection>
150where
151 T: DeserializeOwned,
152{
153 let parameters = request
154 .path_parameters
155 .iter()
156 .map(|parameter| (parameter.name.as_str(), parameter.value.as_str()))
157 .collect::<Vec<_>>();
158 let encoded = serde_urlencoded::to_string(parameters).map_err(|_| invalid_path())?;
159 serde_urlencoded::from_str(&encoded)
160 .map(Path)
161 .map_err(|_| invalid_path())
162}
163
164fn extract_query<T>(request: &HandleRequest) -> Result<Query<T>, ExtractorRejection>
165where
166 T: DeserializeOwned,
167{
168 serde_urlencoded::from_str(request.query.as_deref().unwrap_or_default())
169 .map(Query)
170 .map_err(|_| {
171 response::problem(
172 response::StatusCode::BAD_REQUEST,
173 "invalid_query",
174 "The query string is not valid for this endpoint.",
175 )
176 .into()
177 })
178}
179
180fn has_json_content_type(request: &HandleRequest) -> bool {
181 request.headers.iter().any(|header| {
182 header.name.eq_ignore_ascii_case("content-type")
183 && header
184 .value
185 .split(';')
186 .next()
187 .is_some_and(|value| value.trim().eq_ignore_ascii_case("application/json"))
188 })
189}
190
191fn invalid_path() -> ExtractorRejection {
192 response::problem(
193 response::StatusCode::BAD_REQUEST,
194 "invalid_path_parameters",
195 "The route path parameters are not valid for this endpoint.",
196 )
197 .into()
198}