actix-web 4.13.0

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust
Documentation
use super::{Guard, GuardContext};
use crate::http::header::Accept;

/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type.
///
/// An exception is that matching `*/*` must be explicitly enabled because most browsers send this
/// as part of their `Accept` header for almost every request.
///
/// # Examples
/// ```
/// use actix_web::{guard::Acceptable, web, HttpResponse};
///
/// web::resource("/images")
///     .guard(Acceptable::new(mime::IMAGE_STAR))
///     .default_service(web::to(|| async {
///         HttpResponse::Ok().body("only called when images responses are acceptable")
///     }));
/// ```
#[derive(Debug, Clone)]
pub struct Acceptable {
    mime: mime::Mime,

    /// Whether to match `*/*` mime type.
    ///
    /// Defaults to false because it's not very useful otherwise.
    match_star_star: bool,
}

impl Acceptable {
    /// Constructs new `Acceptable` guard with the given `mime` type/pattern.
    pub fn new(mime: mime::Mime) -> Self {
        Self {
            mime,
            match_star_star: false,
        }
    }

    /// Allows `*/*` in the `Accept` header to pass the guard check.
    pub fn match_star_star(mut self) -> Self {
        self.match_star_star = true;
        self
    }
}

impl Guard for Acceptable {
    fn check(&self, ctx: &GuardContext<'_>) -> bool {
        let accept = match ctx.header::<Accept>() {
            Some(hdr) => hdr,
            None => return false,
        };

        let target_type = self.mime.type_();
        let target_subtype = self.mime.subtype();

        for mime in accept.0.into_iter().map(|q| q.item) {
            return match (mime.type_(), mime.subtype()) {
                (typ, subtype) if typ == target_type && subtype == target_subtype => true,
                (typ, mime::STAR) if typ == target_type => true,
                (mime::STAR, mime::STAR) if self.match_star_star => true,
                _ => continue,
            };
        }

        false
    }

    #[cfg(feature = "experimental-introspection")]
    fn name(&self) -> String {
        if self.match_star_star {
            format!("Acceptable({}, match_star_star=true)", self.mime)
        } else {
            format!("Acceptable({})", self.mime)
        }
    }

    #[cfg(feature = "experimental-introspection")]
    fn details(&self) -> Option<Vec<super::GuardDetail>> {
        let mut details = Vec::new();
        details.push(super::GuardDetail::Generic(format!("mime={}", self.mime)));
        if self.match_star_star {
            details.push(super::GuardDetail::Generic(
                "match_star_star=true".to_string(),
            ));
        }
        Some(details)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{http::header, test::TestRequest};

    #[test]
    fn test_acceptable() {
        let req = TestRequest::default().to_srv_request();
        assert!(!Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));

        let req = TestRequest::default()
            .insert_header((header::ACCEPT, "application/json"))
            .to_srv_request();
        assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));

        let req = TestRequest::default()
            .insert_header((header::ACCEPT, "text/html, application/json"))
            .to_srv_request();
        assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx()));
    }

    #[test]
    fn test_acceptable_star() {
        let req = TestRequest::default()
            .insert_header((header::ACCEPT, "text/html, */*;q=0.8"))
            .to_srv_request();

        assert!(Acceptable::new(mime::APPLICATION_JSON)
            .match_star_star()
            .check(&req.guard_ctx()));
    }

    #[cfg(feature = "experimental-introspection")]
    #[test]
    fn acceptable_guard_details_include_mime() {
        let guard = Acceptable::new(mime::APPLICATION_JSON).match_star_star();
        let details = guard.details().expect("missing guard details");

        assert!(details.iter().any(|detail| match detail {
            crate::guard::GuardDetail::Generic(value) => value == "match_star_star=true",
            _ => false,
        }));
        let expected = format!("mime={}", mime::APPLICATION_JSON);
        assert!(details.iter().any(|detail| match detail {
            crate::guard::GuardDetail::Generic(value) => value == &expected,
            _ => false,
        }));
        assert_eq!(
            guard.name(),
            format!(
                "Acceptable({}, match_star_star=true)",
                mime::APPLICATION_JSON
            )
        );
    }
}