1use std::convert::Infallible;
8use std::marker::PhantomData;
9use std::sync::Arc;
10
11use axum::extract::{FromRef, FromRequest, FromRequestParts, Request};
12use bytes::Bytes;
13use http::request::Parts;
14use serde::de::DeserializeOwned;
15
16use jsonapi_core::{Document, JsonApiMediaType, PrimaryData, Query, ResourceObject, TypeRegistry};
17use jsonapi_http::{
18 ClientIdPolicy, check_client_id, check_content_type, check_id_matches, deserialize_body,
19 parse_query,
20};
21
22use crate::error::JsonApiError;
23
24#[derive(Debug, Clone)]
44pub struct NegotiatedMediaType(pub JsonApiMediaType);
45
46impl<S> FromRequestParts<S> for NegotiatedMediaType
47where
48 S: Send + Sync,
49{
50 type Rejection = Infallible;
51
52 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
53 let media = parts
54 .extensions
55 .get::<JsonApiMediaType>()
56 .cloned()
57 .unwrap_or_else(JsonApiMediaType::plain);
58 Ok(NegotiatedMediaType(media))
59 }
60}
61
62#[derive(Debug, Clone)]
74pub struct JsonApi<T>(pub Document<T>);
75
76impl<T, S> FromRequest<S> for JsonApi<T>
77where
78 T: ResourceObject + DeserializeOwned + 'static,
79 S: Send + Sync,
80{
81 type Rejection = JsonApiError;
82
83 async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
84 check_content_type(req.headers()).map_err(|err| JsonApiError::from_core(&err))?;
85
86 let bytes = Bytes::from_request(req, state).await.map_err(|rejection| {
87 JsonApiError::from_status(rejection.status(), Some(rejection.body_text()))
92 })?;
93
94 let document = deserialize_body::<T>(&bytes).map_err(JsonApiError::from)?;
95 Ok(JsonApi(document))
96 }
97}
98
99impl<T: ResourceObject> JsonApi<T> {
100 fn body_id(&self) -> Option<&str> {
102 match &self.0 {
103 Document::Data {
104 data: PrimaryData::Single(primary),
105 ..
106 } => primary.resource_id(),
107 _ => None,
108 }
109 }
110
111 pub fn require_id(&self, path_id: &str) -> Result<(), JsonApiError> {
126 check_id_matches(self.body_id(), path_id).map_err(JsonApiError::from)
127 }
128
129 pub fn check_client_id(&self, policy: ClientIdPolicy) -> Result<(), JsonApiError> {
137 check_client_id(policy, self.body_id()).map_err(JsonApiError::from)
138 }
139}
140
141#[derive(Debug, Clone)]
164pub struct BaseUrl(pub String);
165
166impl<S> FromRequestParts<S> for BaseUrl
167where
168 S: Send + Sync,
169 Self: FromRef<S>,
170{
171 type Rejection = Infallible;
172
173 async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
174 Ok(Self::from_ref(state))
175 }
176}
177
178#[derive(Debug, Clone)]
182pub struct JsonApiQuery(pub Query);
183
184impl<S> FromRequestParts<S> for JsonApiQuery
185where
186 S: Send + Sync,
187{
188 type Rejection = JsonApiError;
189
190 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
191 let query = parse_query(&parts.uri).map_err(|err| JsonApiError::from_core(&err))?;
192 Ok(JsonApiQuery(query))
193 }
194}
195
196#[derive(Debug, Clone)]
200pub struct JsonApiQueryValidated<T> {
201 pub query: Query,
203 _marker: PhantomData<T>,
204}
205
206impl<T, S> FromRequestParts<S> for JsonApiQueryValidated<T>
207where
208 T: ResourceObject + 'static,
209 S: Send + Sync,
210 Arc<TypeRegistry>: FromRef<S>,
211{
212 type Rejection = JsonApiError;
213
214 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
215 let query = parse_query(&parts.uri).map_err(|err| JsonApiError::from_core(&err))?;
216
217 let registry = Arc::<TypeRegistry>::from_ref(state);
218 let root = T::type_info().type_name;
219 let includes: Vec<&str> = query.include.iter().map(String::as_str).collect();
220 registry
221 .validate_include_paths(root, &includes)
222 .map_err(|err| JsonApiError::from_core(&err))?;
223
224 Ok(JsonApiQueryValidated {
225 query,
226 _marker: PhantomData,
227 })
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use axum::Router;
235 use axum::body::Body;
236 use axum::routing::{get, post};
237 use http::{Request, StatusCode, header};
238 use jsonapi_core::Relationship;
239 use serde_json::Value;
240 use tower::ServiceExt;
241
242 #[derive(Debug, Clone, jsonapi_core::JsonApi)]
243 #[jsonapi(type = "people")]
244 struct Person {
245 #[jsonapi(id)]
246 id: String,
247 #[allow(dead_code)]
248 name: String,
249 }
250
251 #[derive(Debug, Clone, jsonapi_core::JsonApi)]
252 #[jsonapi(type = "articles")]
253 struct Article {
254 #[jsonapi(id)]
255 id: String,
256 #[allow(dead_code)]
257 title: String,
258 #[jsonapi(relationship, type = "people")]
259 #[allow(dead_code)]
260 author: Relationship<Person>,
261 }
262
263 const VALID_ARTICLE: &str = r#"{"data":{"type":"articles","id":"1",
264 "attributes":{"title":"Hi"},
265 "relationships":{"author":{"data":{"type":"people","id":"9"}}}}}"#;
266
267 fn registry() -> Arc<TypeRegistry> {
268 let mut registry = TypeRegistry::new();
269 registry.register::<Article>().register::<Person>();
270 Arc::new(registry)
271 }
272
273 async fn status_of(response: axum::response::Response) -> (StatusCode, Value) {
274 let status = response.status();
275 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
276 .await
277 .unwrap();
278 let value = if bytes.is_empty() {
279 Value::Null
280 } else {
281 serde_json::from_slice(&bytes).unwrap_or(Value::Null)
282 };
283 (status, value)
284 }
285
286 fn body_router() -> Router {
289 async fn create(JsonApi(_doc): JsonApi<Article>) -> StatusCode {
290 StatusCode::CREATED
291 }
292 Router::new().route("/articles", post(create))
293 }
294
295 fn post_request(content_type: &str, body: &str) -> Request<Body> {
296 Request::builder()
297 .method("POST")
298 .uri("/articles")
299 .header(header::CONTENT_TYPE, content_type)
300 .body(Body::from(body.to_owned()))
301 .unwrap()
302 }
303
304 #[test]
305 fn body_extractor_accepts_valid_document() {
306 pollster::block_on(async {
307 let response = body_router()
308 .oneshot(post_request("application/vnd.api+json", VALID_ARTICLE))
309 .await
310 .unwrap();
311 assert_eq!(response.status(), StatusCode::CREATED);
312 });
313 }
314
315 #[test]
316 fn body_extractor_rejects_wrong_content_type_with_415() {
317 pollster::block_on(async {
318 let response = body_router()
319 .oneshot(post_request("application/json", VALID_ARTICLE))
320 .await
321 .unwrap();
322 let (status, _) = status_of(response).await;
323 assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
324 });
325 }
326
327 #[test]
328 fn body_extractor_maps_body_limit_to_413() {
329 pollster::block_on(async {
330 use axum::extract::DefaultBodyLimit;
334 let router = body_router().layer(DefaultBodyLimit::max(8));
335 let big_body = format!(
336 r#"{{"data":{{"type":"articles","id":"1","attributes":{{"title":"{}"}}}}}}"#,
337 "x".repeat(256)
338 );
339 let response = router
340 .oneshot(post_request("application/vnd.api+json", &big_body))
341 .await
342 .unwrap();
343 let (status, json) = status_of(response).await;
344 assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
345 assert_eq!(json["errors"][0]["status"], "413");
346 });
347 }
348
349 #[test]
350 fn body_extractor_maps_missing_attribute_to_422_with_pointer() {
351 pollster::block_on(async {
352 let body = r#"{"data":{"type":"articles","id":"1","attributes":{},
353 "relationships":{"author":{"data":{"type":"people","id":"9"}}}}}"#;
354 let response = body_router()
355 .oneshot(post_request("application/vnd.api+json", body))
356 .await
357 .unwrap();
358 let (status, json) = status_of(response).await;
359 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
360 assert_eq!(
361 json["errors"][0]["source"]["pointer"],
362 "/data/attributes/title"
363 );
364 });
365 }
366
367 #[test]
368 fn body_extractor_maps_type_mismatch_to_409_with_pointer() {
369 pollster::block_on(async {
370 let body = r#"{"data":{"type":"people","id":"1","attributes":{"title":"x"},
372 "relationships":{"author":{"data":{"type":"people","id":"9"}}}}}"#;
373 let response = body_router()
374 .oneshot(post_request("application/vnd.api+json", body))
375 .await
376 .unwrap();
377 let (status, json) = status_of(response).await;
378 assert_eq!(status, StatusCode::CONFLICT);
379 assert_eq!(json["errors"][0]["source"]["pointer"], "/data/type");
380 });
381 }
382
383 #[test]
386 fn query_extractor_parses_values_through_to_the_handler() {
387 pollster::block_on(async {
388 async fn list(JsonApiQuery(q): JsonApiQuery) -> String {
391 let field = &q.sort[0];
392 let page_size = q.page.get("size").map(String::as_str).unwrap_or("none");
393 format!(
394 "sort={}:{} page_size={page_size}",
395 field.field, field.descending
396 )
397 }
398 let router = Router::new().route("/articles", get(list));
399 let request = Request::builder()
400 .uri("/articles?sort=-created&page[size]=2")
401 .body(Body::empty())
402 .unwrap();
403 let (status, body) = {
404 let response = router.oneshot(request).await.unwrap();
405 let status = response.status();
406 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
407 .await
408 .unwrap();
409 (status, String::from_utf8(bytes.to_vec()).unwrap())
410 };
411 assert_eq!(status, StatusCode::OK);
412 assert_eq!(body, "sort=created:true page_size=2");
413 });
414 }
415
416 #[test]
417 fn query_extractor_rejects_malformed_param_with_400() {
418 pollster::block_on(async {
419 async fn list(JsonApiQuery(_q): JsonApiQuery) -> StatusCode {
420 StatusCode::OK
421 }
422 let router = Router::new().route("/articles", get(list));
423 let request = Request::builder()
425 .uri("/articles?fields[=title")
426 .body(Body::empty())
427 .unwrap();
428 let response = router.oneshot(request).await.unwrap();
429 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
430 });
431 }
432
433 fn validated_router() -> Router {
436 async fn list(_q: JsonApiQueryValidated<Article>) -> StatusCode {
437 StatusCode::OK
438 }
439 Router::new()
440 .route("/articles", get(list))
441 .with_state(registry())
442 }
443
444 #[test]
445 fn validated_query_accepts_known_include() {
446 pollster::block_on(async {
447 let request = Request::builder()
448 .uri("/articles?include=author")
449 .body(Body::empty())
450 .unwrap();
451 let response = validated_router().oneshot(request).await.unwrap();
452 assert_eq!(response.status(), StatusCode::OK);
453 });
454 }
455
456 #[test]
457 fn validated_query_rejects_unknown_include_with_400() {
458 pollster::block_on(async {
459 let request = Request::builder()
460 .uri("/articles?include=bogus")
461 .body(Body::empty())
462 .unwrap();
463 let response = validated_router().oneshot(request).await.unwrap();
464 let (status, json) = status_of(response).await;
465 assert_eq!(status, StatusCode::BAD_REQUEST);
466 assert_eq!(json["errors"][0]["source"]["parameter"], Value::Null);
467 assert!(
468 json["errors"][0]["detail"]
469 .as_str()
470 .unwrap()
471 .contains("bogus")
472 );
473 });
474 }
475
476 const TEST_PROFILE: &str = "https://example.com/p";
479
480 async fn body_text(response: axum::response::Response) -> String {
481 let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
482 .await
483 .unwrap();
484 String::from_utf8(bytes.to_vec()).unwrap()
485 }
486
487 #[test]
488 fn negotiated_media_type_reflects_accept_layer_profile() {
489 pollster::block_on(async {
490 async fn handler(NegotiatedMediaType(media): NegotiatedMediaType) -> String {
493 media.profile.join(",")
494 }
495 let router = Router::new()
496 .route("/x", get(handler))
497 .layer(crate::AcceptLayer::new().profile([TEST_PROFILE]));
498 let request = Request::builder()
499 .uri("/x")
500 .header(
501 header::ACCEPT,
502 format!("application/vnd.api+json; profile=\"{TEST_PROFILE}\""),
503 )
504 .body(Body::empty())
505 .unwrap();
506 let response = router.oneshot(request).await.unwrap();
507 assert_eq!(response.status(), StatusCode::OK);
508 assert_eq!(body_text(response).await, TEST_PROFILE);
509 });
510 }
511
512 #[test]
513 fn negotiated_media_type_without_layer_falls_back_to_plain() {
514 pollster::block_on(async {
515 async fn handler(NegotiatedMediaType(media): NegotiatedMediaType) -> String {
516 (media == JsonApiMediaType::plain()).to_string()
517 }
518 let router = Router::new().route("/x", get(handler));
520 let request = Request::builder().uri("/x").body(Body::empty()).unwrap();
521 let response = router.oneshot(request).await.unwrap();
522 assert_eq!(body_text(response).await, "true");
523 });
524 }
525
526 #[test]
527 fn negotiated_media_type_drives_response_content_type_end_to_end() {
528 pollster::block_on(async {
529 use crate::JsonApiResponse;
530 async fn handler(
531 NegotiatedMediaType(media): NegotiatedMediaType,
532 ) -> JsonApiResponse<jsonapi_core::Resource> {
533 let document: Document<jsonapi_core::Resource> =
534 serde_json::from_str(r#"{"data":{"type":"articles","id":"1"}}"#).unwrap();
535 JsonApiResponse::new(document).media_type(media)
536 }
537 let router = Router::new()
538 .route("/x", get(handler))
539 .layer(crate::AcceptLayer::new().profile([TEST_PROFILE]));
540 let request = Request::builder()
541 .uri("/x")
542 .header(
543 header::ACCEPT,
544 format!("application/vnd.api+json; profile=\"{TEST_PROFILE}\""),
545 )
546 .body(Body::empty())
547 .unwrap();
548 let response = router.oneshot(request).await.unwrap();
549 let content_type = response
550 .headers()
551 .get(header::CONTENT_TYPE)
552 .and_then(|v| v.to_str().ok())
553 .unwrap()
554 .to_string();
555 assert_eq!(
556 content_type,
557 format!("application/vnd.api+json; profile=\"{TEST_PROFILE}\"")
558 );
559 });
560 }
561}