axum_accept/
lib.rs

1//! Typed accept negotiation for axum, following RFC7231.
2//!
3//! # Example
4//!
5//! ```rust
6//! use axum_accept::AcceptExtractor;
7//!
8//! #[derive(AcceptExtractor)]
9//! enum Accept {
10//!     #[accept(mediatype="text/plain")]
11//!     TextPlain,
12//! }
13//! ```
14#![deny(warnings)]
15#![deny(clippy::pedantic, clippy::unwrap_used)]
16#![deny(missing_docs)]
17pub use axum_accept_macros::AcceptExtractor;
18pub use axum_accept_shared::AcceptRejection;
19
20#[cfg(doctest)]
21#[doc = include_str!("../../README.md")]
22pub struct ReadmeDoctests;
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27    use axum::{
28        body::Body,
29        extract::{FromRequest, Request},
30    };
31
32    #[derive(Debug, AcceptExtractor)]
33    enum Accept {
34        #[accept(mediatype = "text/*")]
35        Text,
36        #[accept(mediatype = "text/plain")]
37        TextPlain,
38        #[accept(mediatype = "application/json")]
39        ApplicationJson,
40        #[accept(mediatype = "application/ld+json")]
41        ApplicationLdJson,
42    }
43
44    #[tokio::test]
45    async fn test_accept_extractor_basic() -> Result<(), Box<dyn std::error::Error>> {
46        let req = Request::builder()
47            .header("accept", "application/json,text/plain")
48            .body(Body::from(""))?;
49        let state = ();
50        let media_type = Accept::from_request(req, &state)
51            .await
52            .expect("Expected no rejection");
53        let Accept::ApplicationJson = media_type else {
54            panic!("expected application/json")
55        };
56        Ok(())
57    }
58
59    #[tokio::test]
60    async fn test_accept_extractor_q() -> Result<(), Box<dyn std::error::Error>> {
61        let req = Request::builder()
62            .header("accept", "application/json;q=0.9,text/plain")
63            .body(Body::from(""))?;
64        let state = ();
65        let media_type = Accept::from_request(req, &state)
66            .await
67            .expect("Expected no rejection");
68        let Accept::TextPlain = media_type else {
69            panic!("expected text/plain")
70        };
71        Ok(())
72    }
73
74    #[tokio::test]
75    async fn test_accept_extractor_specifity() -> Result<(), Box<dyn std::error::Error>> {
76        let req = Request::builder()
77            .header("accept", "text/*,text/plain")
78            .body(Body::from(""))?;
79        let state = ();
80        let media_type = Accept::from_request(req, &state)
81            .await
82            .expect("Expected no rejection");
83        let Accept::TextPlain = media_type else {
84            panic!("expected text/plain")
85        };
86        Ok(())
87    }
88
89    #[tokio::test]
90    async fn test_accept_extractor_suffix() -> Result<(), Box<dyn std::error::Error>> {
91        let req = Request::builder()
92            .header("accept", "text/*,application/ld+json,text/plain")
93            .body(Body::from(""))?;
94        let state = ();
95        let media_type = Accept::from_request(req, &state)
96            .await
97            .expect("Expected no rejection");
98        let Accept::ApplicationLdJson = media_type else {
99            panic!("expected application/ldjson")
100        };
101        Ok(())
102    }
103
104    #[tokio::test]
105    async fn test_accept_extractor_no_match() -> Result<(), Box<dyn std::error::Error>> {
106        let req = Request::builder()
107            .header("accept", "text/csv")
108            .body(Body::from(""))?;
109        let state = ();
110        let media_type = Accept::from_request(req, &state).await;
111        let Err(AcceptRejection::NoSupportedMediaTypeFound) = media_type else {
112            panic!("expected no supported media type found")
113        };
114        Ok(())
115    }
116}