actix_chain/
link.rs

1use std::{rc::Rc, str::FromStr};
2
3use actix_service::{IntoServiceFactory, ServiceFactory, ServiceFactoryExt, boxed};
4use actix_web::{
5    Error, HttpResponse,
6    dev::{ServiceRequest, ServiceResponse},
7    guard::{Guard, GuardContext},
8    http::{StatusCode, Uri, header, uri::PathAndQuery},
9    mime,
10};
11
12use crate::{
13    next::{IsStatus, Next},
14    service::{HttpNewService, HttpService},
15};
16
17/// A single [`Link`] in the greater [`Chain`](crate::Chain)
18///
19/// Wraps an Actix-Web service factory with details on when the service should
20/// be evaluated in the chain and if processing should continue afterwards.
21///
22/// # Examples
23///
24/// ```
25/// use actix_web::{App, guard::Header, http::StatusCode, web};
26/// use actix_chain::{Chain, Link, next::IsStatus};
27///
28/// async fn index() -> &'static str {
29///     "Hello World!"
30/// }
31///
32/// Link::new(web::get().to(index))
33///     .prefix("/index")
34///     .guard(Header("Host", "example.com"))
35///     .next(IsStatus(StatusCode::NOT_FOUND));
36/// ```
37#[derive(Clone)]
38pub struct Link {
39    prefix: String,
40    guards: Vec<Rc<dyn Guard>>,
41    next: Vec<Rc<dyn Next>>,
42    service: Rc<HttpNewService>,
43}
44
45impl Link {
46    /// Create a new [`Link`] for your [`Chain`](crate::Chain).
47    ///
48    /// Any Actix-Web service can be passed such as [`actix_web::Route`].
49    pub fn new<F, U>(service: F) -> Self
50    where
51        F: IntoServiceFactory<U, ServiceRequest>,
52        U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
53            + 'static,
54    {
55        Self {
56            prefix: String::new(),
57            guards: Vec::new(),
58            next: Vec::new(),
59            service: Rc::new(boxed::factory(service.into_factory().map_init_err(|_| ()))),
60        }
61    }
62
63    /// Assign a `match-prefix` / `mount_path` to the link.
64    ///
65    /// The prefix is the root URL at which the service is used.
66    /// For example, /assets will serve files at example.com/assets/....
67    pub fn prefix<S: Into<String>>(mut self, prefix: S) -> Self {
68        self.prefix = prefix.into();
69        self
70    }
71
72    /// Adds a routing guard.
73    ///
74    /// Use this to allow multiple chained services that respond to strictly different
75    /// properties of a request.
76    ///
77    /// **IMPORTANT:** If a guard supplied here does not match a given request,
78    /// the request WILL be forwarded to the next [`Link`] in the chain, unlike
79    /// [`Chain::guard`](crate::Chain::guard)
80    ///
81    /// # Examples
82    /// ```
83    /// use actix_web::{guard::Header, App, web};
84    /// use actix_chain::{Chain, Link};
85    ///
86    /// async fn index() -> &'static str {
87    ///     "Hello world!"
88    /// }
89    ///
90    /// let svc = web::get().to(index);
91    /// Chain::default()
92    ///     .link(Link::new(svc)
93    ///         .guard(Header("Host", "example.com")));
94    /// ```
95    pub fn guard<G: Guard + 'static>(mut self, guards: G) -> Self {
96        self.guards.push(Rc::new(guards));
97        self
98    }
99
100    /// Configure when a [`Link`] should forward to the next chain
101    /// instead of returning its [`ServiceResponse`](actix_web::dev::ServiceResponse).
102    ///
103    /// Any responses that match the supplied criteria will instead be ignored,
104    /// assuming another link exists within the chain.
105    ///
106    /// The default [`Link`] behavior is to continue down the chain
107    /// on "404 Not Found" responses only.
108    ///
109    /// # Examples
110    /// ```
111    /// use actix_web::{http::StatusCode, web};
112    /// use actix_chain::{Link, next::IsStatus};
113    ///
114    /// async fn index() -> &'static str {
115    ///     "Hello world!"
116    /// }
117    ///
118    /// Link::new(web::get().to(index))
119    ///     .next(IsStatus(StatusCode::NOT_FOUND));
120    /// ```
121    pub fn next<N>(mut self, next: N) -> Self
122    where
123        N: Next + 'static,
124    {
125        self.next.push(Rc::new(next));
126        self
127    }
128
129    /// Convert public [`Link`] builder into [`LinkInner`]
130    pub(crate) async fn into_inner(&self) -> Result<LinkInner, ()> {
131        let guard = match self.guards.is_empty() {
132            true => None,
133            false => Some(AllGuard(self.guards.clone())),
134        };
135        let next: Vec<Rc<dyn Next>> = match self.next.is_empty() {
136            true => vec![Rc::new(IsStatus::new(StatusCode::NOT_FOUND))],
137            false => self.next.clone(),
138        };
139        Ok(LinkInner {
140            guard,
141            next,
142            prefix: self.prefix.clone(),
143            service: Rc::new(self.service.new_service(()).await?),
144        })
145    }
146}
147
148struct AllGuard(Vec<Rc<dyn Guard>>);
149
150impl Guard for AllGuard {
151    #[inline]
152    fn check(&self, ctx: &actix_web::guard::GuardContext<'_>) -> bool {
153        self.0.iter().all(|g| g.check(ctx))
154    }
155}
156
157/// Default 404 Response when service is unable to respond
158#[inline]
159pub(crate) fn default_response(req: ServiceRequest) -> ServiceResponse {
160    req.into_response(
161        HttpResponse::NotFound()
162            .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
163            .body("Not Found"),
164    )
165}
166
167pub(crate) struct LinkInner {
168    prefix: String,
169    guard: Option<AllGuard>,
170    pub(crate) service: Rc<HttpService>,
171    pub(crate) next: Vec<Rc<dyn Next>>,
172}
173
174impl LinkInner {
175    /// Generate new URI with prefix stripped if prefix is not empty
176    pub(crate) fn new_uri(&self, uri: &Uri) -> Option<Uri> {
177        if self.prefix.is_empty() {
178            return None;
179        }
180        let mut parts = uri.clone().into_parts();
181        parts.path_and_query = parts
182            .path_and_query
183            .and_then(|pq| PathAndQuery::from_str(pq.as_str().strip_prefix(&self.prefix)?).ok());
184        Uri::from_parts(parts).ok()
185    }
186
187    /// Check if request path matches prefix and any guards are met
188    #[inline]
189    pub(crate) fn matches(&self, path: &str, ctx: &GuardContext) -> bool {
190        path.starts_with(&self.prefix) && self.guard.as_ref().map(|g| !g.check(ctx)).unwrap_or(true)
191    }
192
193    /// Check if response is invalid, and next link should execute
194    #[inline]
195    pub(crate) fn go_next(&self, res: &HttpResponse) -> bool {
196        self.next.iter().any(|next| next.next(res))
197    }
198
199    /// Call inner service once and return [`actix_web::dev::ServiceResponse`]
200    /// no matter what.
201    #[inline]
202    pub(crate) async fn call_once(
203        &self,
204        mut req: ServiceRequest,
205    ) -> Result<ServiceResponse, Error> {
206        if !self.matches(req.uri().path(), &req.guard_ctx()) {
207            return Ok(default_response(req));
208        }
209        if let Some(uri) = self.new_uri(req.uri()) {
210            req.head_mut().uri = uri;
211        }
212        self.service.call(req).await
213    }
214}