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/plain")]
39        TextPlain,
40        #[accept(mediatype = "application/json")]
41        ApplicationJson,
42        #[accept(mediatype = "application/ld+json")]
43        ApplicationLdJson,
44    }
45
46    #[tokio::test]
47    async fn test_accept_extractor_basic() -> Result<(), Box<dyn std::error::Error>> {
48        let req = Request::builder()
49            .header("accept", "application/json,text/plain")
50            .body(Body::from(""))?;
51        let state = ();
52        let media_type = Accept::from_request(req, &state)
53            .await
54            .expect("Expected no rejection");
55        let Accept::ApplicationJson = media_type else {
56            panic!("expected application/json")
57        };
58        Ok(())
59    }
60
61    #[tokio::test]
62    async fn test_accept_extractor_q() -> Result<(), Box<dyn std::error::Error>> {
63        let req = Request::builder()
64            .header("accept", "application/json;q=0.9,text/plain")
65            .body(Body::from(""))?;
66        let state = ();
67        let media_type = Accept::from_request(req, &state)
68            .await
69            .expect("Expected no rejection");
70        let Accept::TextPlain = media_type else {
71            panic!("expected text/plain")
72        };
73        Ok(())
74    }
75
76    #[tokio::test]
77    async fn test_accept_extractor_specifity() -> Result<(), Box<dyn std::error::Error>> {
78        let req = Request::builder()
79            .header("accept", "text/*,text/plain")
80            .body(Body::from(""))?;
81        let state = ();
82        let media_type = Accept::from_request(req, &state)
83            .await
84            .expect("Expected no rejection");
85        let Accept::TextPlain = media_type else {
86            panic!("expected text/plain")
87        };
88        Ok(())
89    }
90
91    #[tokio::test]
92    async fn test_accept_extractor_suffix() -> Result<(), Box<dyn std::error::Error>> {
93        let req = Request::builder()
94            .header("accept", "text/*,application/ld+json,text/plain")
95            .body(Body::from(""))?;
96        let state = ();
97        let media_type = Accept::from_request(req, &state)
98            .await
99            .expect("Expected no rejection");
100        let Accept::ApplicationLdJson = media_type else {
101            panic!("expected application/ldjson")
102        };
103        Ok(())
104    }
105
106    #[tokio::test]
107    async fn test_accept_extractor_no_match() -> Result<(), Box<dyn std::error::Error>> {
108        let req = Request::builder()
109            .header("accept", "text/csv")
110            .body(Body::from(""))?;
111        let state = ();
112        let media_type = Accept::from_request(req, &state).await;
113        let Err(AcceptRejection::NoSupportedMediaTypeFound) = media_type else {
114            panic!("expected no supported media type found")
115        };
116        Ok(())
117    }
118
119    #[tokio::test]
120    async fn test_accept_extractor_star() -> Result<(), Box<dyn std::error::Error>> {
121        let req = Request::builder()
122            .header("accept", "text/csv,text/*")
123            .body(Body::from(""))?;
124        let state = ();
125        let media_type = Accept::from_request(req, &state).await;
126        let Ok(Accept::TextPlain) = media_type else {
127            panic!("expected text/*, got {:?}", media_type)
128        };
129        Ok(())
130    }
131
132    #[tokio::test]
133    async fn test_accept_extractor_star_star() -> Result<(), Box<dyn std::error::Error>> {
134        let req = Request::builder()
135            .header("accept", "text/csv,*/*")
136            .body(Body::from(""))?;
137        let state = ();
138        let media_type = Accept::from_request(req, &state).await;
139        let Ok(Accept::TextPlain) = media_type else {
140            panic!("expected text/plain")
141        };
142        Ok(())
143    }
144
145    #[derive(Debug, AcceptExtractor, Default)]
146    enum AcceptWithDefault {
147        #[accept(mediatype = "application/json")]
148        ApplicationJson,
149        #[default]
150        #[accept(mediatype = "text/plain")]
151        TextPlain,
152    }
153
154    #[tokio::test]
155    async fn test_accept_extractor_default() -> Result<(), Box<dyn std::error::Error>> {
156        let req = Request::builder()
157            .header("accept", "text/csv")
158            .body(Body::from(""))?;
159        let state = ();
160        let media_type = AcceptWithDefault::from_request(req, &state).await;
161        let Err(AcceptRejection::NoSupportedMediaTypeFound) = media_type else {
162            panic!("expected no supported media type found")
163        };
164
165        let req = Request::builder().body(Body::from(""))?;
166        let state = ();
167        let media_type = AcceptWithDefault::from_request(req, &state).await;
168        let Ok(AcceptWithDefault::TextPlain) = media_type else {
169            panic!("expected text/plain (default)")
170        };
171        Ok(())
172    }
173
174    #[tokio::test]
175    async fn test_accept_extractor_star_star_default() -> Result<(), Box<dyn std::error::Error>> {
176        let req = Request::builder()
177            .header("accept", "text/csv,*/*")
178            .body(Body::from(""))?;
179        let state = ();
180        let media_type = AcceptWithDefault::from_request(req, &state).await;
181        let Ok(AcceptWithDefault::TextPlain) = media_type else {
182            panic!("expected text/plain (default)")
183        };
184        Ok(())
185    }
186}