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}