1#[cfg(feature = "maud")]
2pub mod maud {
3 use crate::Message;
4
5 impl maud::Render for Message {
6 fn render_to(&self, buffer: &mut String) {
7 str::render_to(&self.0, buffer);
8 }
9 }
10}
11
12#[cfg(feature = "axum")]
13pub mod axum {
14 use std::{future::Future, sync::Arc};
15
16 use accept_language;
17 use axum_core::extract::FromRequestParts;
18 use axum_extra::extract::CookieJar;
19 use http::{header::ACCEPT_LANGUAGE, request::Parts, StatusCode};
20
21 use crate::MessageBundle;
22
23 pub struct RequestLanguage<T: MessageBundle>(pub T);
24
25 #[derive(Debug, Default)]
26 struct RequestLanguageConfigInner {
27 skip_language_header: bool,
28 language_cookie: Option<String>,
29 default_language: Option<String>,
30 }
31
32 #[derive(Debug, Clone, Default)]
33 pub struct RequestLanguageConfig {
34 inner: Arc<RequestLanguageConfigInner>,
35 }
36
37 impl RequestLanguageConfig {
38 pub fn builder() -> RequestLanguageConfigBuilder {
39 RequestLanguageConfigBuilder {
40 inner: RequestLanguageConfigInner::default(),
41 }
42 }
43
44 pub fn ignore_language_header(&self) -> bool {
45 self.inner.skip_language_header
46 }
47
48 pub fn language_cookie_name(&self) -> Option<&str> {
49 self.inner.language_cookie.as_deref()
50 }
51
52 pub fn fallback_language_id(&self) -> Option<&str> {
53 self.inner.default_language.as_deref()
54 }
55 }
56
57 pub struct RequestLanguageConfigBuilder {
58 inner: RequestLanguageConfigInner,
59 }
60
61 impl RequestLanguageConfigBuilder {
62 pub fn ignore_accept_language_header(mut self, value: bool) -> Self {
63 self.inner.skip_language_header = value;
64 self
65 }
66
67 pub fn language_cookie_name(mut self, value: &str) -> Self {
68 self.inner.language_cookie = Some(value.to_string());
69 self
70 }
71
72 pub fn fallback_language_id(mut self, value: &str) -> Self {
73 self.inner.default_language = Some(value.to_string());
74 self
75 }
76
77 pub fn build(self) -> RequestLanguageConfig {
78 RequestLanguageConfig {
79 inner: Arc::new(self.inner),
80 }
81 }
82 }
83
84 impl<T: MessageBundle, S> FromRequestParts<S> for RequestLanguage<T>
85 where
86 S: Send + Sync,
87 {
88 type Rejection = (StatusCode, &'static str);
89
90 fn from_request_parts(
91 parts: &mut Parts,
92 _state: &S,
93 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
94 async move {
95 let cfg = parts
96 .extensions
97 .get::<RequestLanguageConfig>()
98 .map(|cfg| cfg.clone())
99 .unwrap_or_default();
100
101 if let Some(cookie_name) = cfg.language_cookie_name().as_ref() {
102 if let Some(bundle) = CookieJar::from_request_parts(parts, _state)
103 .await
104 .ok()
105 .and_then(|jar| {
106 jar.get(cookie_name)
107 .map(|cookie| cookie.value_trimmed())
108 .and_then(T::get)
109 })
110 {
111 return Ok(Self(bundle));
112 };
113 };
114
115 let bundle = if !cfg.ignore_language_header() {
116 parts
117 .headers
118 .get(ACCEPT_LANGUAGE)
119 .and_then(|v| v.to_str().ok())
120 .and_then(|value| {
121 accept_language::intersection_ordered(
122 value,
123 T::supported_language_ids(),
124 )
125 .first()
126 .and_then(|lang| T::get(lang))
127 })
128 } else {
129 None
130 };
131
132 Ok(Self(
133 bundle
134 .or_else(|| cfg.fallback_language_id().and_then(T::get))
135 .unwrap_or_default(),
136 ))
137 }
138 }
139 }
140
141 #[cfg(test)]
142 mod tests {
143
144 use crate::LanguageAware;
145
146 use super::*;
147 use http::Request;
148
149 #[derive(Debug, Clone, PartialEq, Eq)]
150 struct LanguageSpec(String);
151
152 impl Default for LanguageSpec {
153 fn default() -> Self {
154 Self::get("en").unwrap()
155 }
156 }
157
158 impl LanguageAware for LanguageSpec {
159 fn language_id(&self) -> &str {
160 &self.0
161 }
162 }
163
164 impl MessageBundle for LanguageSpec {
165 fn get(language_id: &str) -> Option<Self>
166 where
167 Self: Sized,
168 {
169 match language_id {
170 "en" => Some(LanguageSpec("en".to_string())),
171 "de" => Some(LanguageSpec("de".to_string())),
172 "fr" => Some(LanguageSpec("fr".to_string())),
173 _ => None,
174 }
175 }
176
177 fn default_language_id() -> &'static str {
178 "en"
179 }
180
181 fn supported_language_ids() -> &'static [&'static str] {
182 &["de", "en", "fr"]
183 }
184 }
185
186 #[tokio::test]
187 async fn test_language_from_header() {
188 let req = Request::builder()
189 .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
190 .body(String::default())
191 .unwrap();
192
193 let parts = &mut req.into_parts().0;
194 parts.extensions.insert(RequestLanguageConfig::default());
195
196 assert_eq!(
197 RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
198 .await
199 .unwrap()
200 .0,
201 LanguageSpec("en".to_string())
202 );
203 }
204
205 #[tokio::test]
206 async fn test_language_from_cookie() {
207 let cookie_name = "lang";
208 let cookie_value = "de";
209
210 let mut req = Request::builder()
212 .header("Cookie", format!("{}={}", cookie_name, cookie_value))
213 .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
214 .body(String::default())
215 .unwrap();
216 req.extensions_mut().insert(
217 RequestLanguageConfig::builder()
218 .ignore_accept_language_header(true)
219 .language_cookie_name(cookie_name)
220 .build(),
221 );
222
223 let parts = &mut req.into_parts().0;
224 assert_eq!(
225 RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
226 .await
227 .unwrap()
228 .0,
229 LanguageSpec("de".to_string())
230 );
231 }
232
233 #[tokio::test]
234 async fn test_default_language() {
235 let mut req = Request::builder().body(String::default()).unwrap();
236
237 req.extensions_mut().insert(
238 RequestLanguageConfig::builder()
239 .fallback_language_id("fr")
240 .build(),
241 );
242
243 let parts = &mut req.into_parts().0;
244
245 assert_eq!(
246 RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
247 .await
248 .unwrap()
249 .0,
250 LanguageSpec("fr".to_string())
251 );
252 }
253
254 #[tokio::test]
255 async fn test_no_language_specified() {
256 let mut req = Request::builder().body(String::default()).unwrap();
257
258 req.extensions_mut()
259 .insert(RequestLanguageConfig::default());
260
261 let parts = &mut req.into_parts().0;
262
263 assert_eq!(
264 RequestLanguage::<LanguageSpec>::from_request_parts(parts, &())
265 .await
266 .unwrap()
267 .0,
268 LanguageSpec::default()
269 );
270 }
271 }
272}