Skip to main content

user_agent_parser/
request_guards_axum.rs

1use std::{borrow::Cow, convert::Infallible, sync::Arc};
2
3use axum::{
4    extract::FromRequestParts,
5    http::{header::USER_AGENT, request::Parts},
6};
7
8use crate::{UserAgentParser, models::*};
9
10// axum extractors return owned values, so every model is produced as `T<'static>`.
11// The parser must be shared through an `Extension<Arc<UserAgentParser>>` layer.
12
13#[inline]
14fn user_agent_str(parts: &Parts) -> Option<&str> {
15    parts.headers.get(USER_AGENT).and_then(|value| value.to_str().ok())
16}
17
18#[inline]
19fn parser(parts: &Parts) -> &Arc<UserAgentParser> {
20    parts.extensions.get::<Arc<UserAgentParser>>().expect(
21        "a `UserAgentParser` is not shared with axum; add an `Extension<Arc<UserAgentParser>>` \
22         layer to the router",
23    )
24}
25
26impl<S: Send + Sync> FromRequestParts<S> for UserAgent<'static> {
27    type Rejection = Infallible;
28
29    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
30        let user_agent = user_agent_str(parts).map(|ua| Cow::from(ua.to_string()));
31
32        Ok(UserAgent {
33            user_agent,
34        })
35    }
36}
37
38macro_rules! impl_from_request_parts {
39    ($model:ident, $parse:ident) => {
40        impl<S: Send + Sync> FromRequestParts<S> for $model<'static> {
41            type Rejection = Infallible;
42
43            async fn from_request_parts(
44                parts: &mut Parts,
45                _state: &S,
46            ) -> Result<Self, Self::Rejection> {
47                // The parser is looked up first, so a missing layer is reported even by a request which carries no `User-Agent`.
48                let user_agent_parser = parser(parts);
49
50                // A request without a `User-Agent` header is normal, unlike a missing parser, so it just yields the default.
51                let result = match user_agent_str(parts) {
52                    Some(user_agent) => user_agent_parser.$parse(user_agent).into_owned(),
53                    None => $model::default(),
54                };
55
56                Ok(result)
57            }
58        }
59    };
60}
61
62impl_from_request_parts!(Product, parse_product);
63impl_from_request_parts!(OS, parse_os);
64impl_from_request_parts!(Device, parse_device);
65impl_from_request_parts!(CPU, parse_cpu);
66impl_from_request_parts!(Engine, parse_engine);