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