Skip to content

Commit

Permalink
add responder
Browse files Browse the repository at this point in the history
  • Loading branch information
robjtede committed Feb 11, 2021
1 parent 6c31ca3 commit e083521
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 100 deletions.
172 changes: 76 additions & 96 deletions src/redirect.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
//! See [`Redirect`] for service documentation.
//! See [`Redirect`] for service/responder documentation.

use std::{borrow::Cow, ops::Deref};

use actix_service::fn_service;
use futures_util::future::ready;

use crate::{
dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest},
http::{header, StatusCode},
HttpResponse,
HttpRequest, HttpResponse, Responder,
};

/// Determines how redirects are routed.
#[derive(Debug, Clone)]
enum RedirectType {
/// An absolute path or full URL used as-is when redirecting.
Absolute(String),

/// A path relative to matched path.
Relative(String),
}

/// An HTTP service for redirecting one path to another path or URL.
///
/// Redirects are either [relative](Redirect::to_relative) or [absolute](Redirect::to_absolute).
/// Redirects are either [relative](Redirect::to) or [absolute](Redirect::to).
///
/// By default, the "308 Temporary Redirect" status is used when responding.
/// See [this MDN article](mdn-redirects) on why 308 is preferred over 301.
Expand All @@ -32,58 +24,67 @@ enum RedirectType {
///
/// App::new()
/// // redirect "/duck" to DuckDuckGo
/// .service(Redirect::from("/duck").to_absolute("https://duckduckgo.com/"))
/// .service(Redirect::new("/duck", "https://duckduckgo.com/"))
/// .service(
/// // redirect "/api/old" to "/api/new" using `web::redirect` helper
/// web::scope("/api").service(web::redirect("/old").to_relative("/new"))
/// web::scope("/api").service(web::redirect("/old", "/new"))
/// );
/// ```
///
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
#[derive(Debug, Clone)]
pub struct Redirect {
from: String,
to: RedirectType,
from: Cow<'static, str>,
to: Cow<'static, str>,
status_code: StatusCode,
}

impl Redirect {
/// Create a new `Redirect` service, first providing the path that should be redirected.
///
/// The default "to" location is the root path (`/`). It is expected that you should call either
/// [`to_relative`](Redirect::to_relative) or [`to_absolute`](Redirect::to_absolute) afterwards.
pub fn from(from: impl Into<String>) -> Self {
/// [`to`](Redirect::to) or [`to`](Redirect::to) afterwards.
///
/// Note this function has no effect when used as a Responder
///
/// Redirect to an address or path.
///
/// Whatever argument is provided shall be used as-is when setting the redirect location.
/// You can also use relative paths to navigate relative to the matched path.
///
/// # Examples
/// ```
/// # use actix_web::Redirect;
/// // redirects "/oh/hi/mark" to "/oh/bye/mark"
/// Redirect::new("/oh/hi/mark", "../../bye/mark");
/// ```
pub fn new(
from: impl Into<Cow<'static, str>>,
to: impl Into<Cow<'static, str>>,
) -> Self {
Self {
from: from.into(),
to: RedirectType::Absolute("/".to_owned()),
to: to.into(),
status_code: StatusCode::PERMANENT_REDIRECT,
}
}

/// Redirect to an absolute address or path.
/// Shortcut for creating a redirect to use as a Responder.
///
/// Whatever argument is provided shall be used as-is when setting the redirect location.
#[allow(dead_code, clippy::wrong_self_convention)]
pub fn to_absolute(mut self, to: impl Into<String>) -> Self {
self.to = RedirectType::Absolute(to.into());
self
}

/// Redirect to a relative path.
///
/// The provided argument will replace
#[allow(clippy::wrong_self_convention)]
pub fn to_relative(mut self, to: impl Into<String>) -> Self {
self.to = RedirectType::Relative(to.into());
self
/// Only receives a `to` argument since responders do not need to do route matching.
pub fn to(to: impl Into<Cow<'static, str>>) -> Self {
Self {
from: "/".into(),
to: to.into(),
status_code: StatusCode::PERMANENT_REDIRECT,
}
}

/// Use the "307 Temporary Redirect" status when responding.
///
/// See [this MDN article](mdn-redirects) on why 307 is preferred over 302.
///
/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections
#[allow(dead_code)]
pub fn temporary(self) -> Self {
self.using_status_code(StatusCode::TEMPORARY_REDIRECT)
}
Expand All @@ -97,16 +98,13 @@ impl Redirect {
/// ```
/// # use actix_web::{http::StatusCode, Redirect};
/// // redirects would use "301 Moved Permanently" status code
/// Redirect::from("/old")
/// .to_relative("/new")
/// Redirect::new("/old", "/new")
/// .using_status_code(StatusCode::MOVED_PERMANENTLY);
///
/// // redirects would use "302 Found" status code
/// Redirect::from("/old")
/// .to_relative("/new")
/// Redirect::new("/old", "/new")
/// .using_status_code(StatusCode::FOUND);
/// ```
#[allow(dead_code)]
pub fn using_status_code(mut self, status: StatusCode) -> Self {
self.status_code = status;
self
Expand All @@ -121,25 +119,11 @@ impl HttpServiceFactory for Redirect {
status_code,
} = self;

let rdef = ResourceDef::new(from.clone());
let rdef = ResourceDef::new(from.into_owned());
let redirect_factory = fn_service(move |req: ServiceRequest| {
let uri = req.uri().to_string();

let redirect_to = match &to {
RedirectType::Absolute(to) => to.clone(),
RedirectType::Relative(to) => {
// if service matched then suffix can always be stripped
let uri = uri.strip_suffix(&from).unwrap();

let mut redirect_to = uri.to_owned();
redirect_to.push_str(&to.clone());
redirect_to
}
};

ready(Ok(req.into_response(
HttpResponse::build(status_code)
.insert_header((header::LOCATION, redirect_to))
.insert_header((header::LOCATION, to.deref()))
.finish(),
)))
});
Expand All @@ -148,35 +132,31 @@ impl HttpServiceFactory for Redirect {
}
}

impl Responder for Redirect {
fn respond_to(self, _req: &HttpRequest) -> HttpResponse {
let Redirect {
to, status_code, ..
} = self;

HttpResponse::build(status_code)
.insert_header((header::LOCATION, to.into_owned()))
.finish()
}
}

#[cfg(test)]
mod tests {
use super::*;

use crate::{
dev::Service,
http::StatusCode,
test::{self, TestRequest},
web, App,
};
use crate::{dev::Service, http::StatusCode, test, App};

#[actix_rt::test]
async fn absolute_redirects() {
let redirector = Redirect::from("/one").to_absolute("/two");
let redirector = Redirect::new("/one", "/two");

let mut svc = test::init_service(
App::new()
.service(web::scope("/scoped").service(redirector.clone()))
.service(redirector),
)
.await;

let req = TestRequest::default().uri("/one").to_request();
let res = svc.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
let hdr = res.headers().get(&header::LOCATION).unwrap();
assert_eq!(hdr.to_str().unwrap(), "/two");
let svc = test::init_service(App::new().service(redirector)).await;

let req = TestRequest::default().uri("/scoped/one").to_request();
let req = test::TestRequest::default().uri("/one").to_request();
let res = svc.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
let hdr = res.headers().get(&header::LOCATION).unwrap();
Expand All @@ -185,38 +165,38 @@ mod tests {

#[actix_rt::test]
async fn relative_redirects() {
let redirector = Redirect::from("/one").to_relative("/two");
let redirector = Redirect::new("/one", "two");

let mut svc = test::init_service(
App::new()
.service(web::scope("/scoped").service(redirector.clone()))
.service(redirector),
)
.await;
let svc = test::init_service(App::new().service(redirector)).await;

let req = TestRequest::default().uri("/one").to_request();
let req = test::TestRequest::default().uri("/one").to_request();
let res = svc.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
let hdr = res.headers().get(&header::LOCATION).unwrap();
assert_eq!(hdr.to_str().unwrap(), "/two");
assert_eq!(hdr.to_str().unwrap(), "two");
}

#[actix_rt::test]
async fn temporary_redirects() {
let external_service =
Redirect::new("/external", "https://duck.com").temporary();

let svc = test::init_service(App::new().service(external_service)).await;

let req = TestRequest::default().uri("/scoped/one").to_request();
let req = test::TestRequest::default().uri("/external").to_request();
let res = svc.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::from_u16(308).unwrap());
assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
let hdr = res.headers().get(&header::LOCATION).unwrap();
assert_eq!(hdr.to_str().unwrap(), "/scoped/two");
assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
}

#[actix_rt::test]
async fn temporary_redirects() {
let external_service = Redirect::from("/external")
.to_absolute("https://duck.com")
.temporary();
async fn as_responder() {
let responder = Redirect::to("https://duck.com").temporary();

let mut svc = test::init_service(App::new().service(external_service)).await;
let req = test::TestRequest::default().to_http_request();
let res = responder.respond_to(&req);

let req = TestRequest::default().uri("/external").to_request();
let res = svc.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::from_u16(307).unwrap());
let hdr = res.headers().get(&header::LOCATION).unwrap();
assert_eq!(hdr.to_str().unwrap(), "https://duck.com");
Expand Down
11 changes: 7 additions & 4 deletions src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use actix_http::http::Method;
use actix_router::IntoPattern;
use std::future::Future;
use std::{borrow::Cow, future::Future};

pub use actix_http::Response as HttpResponse;
pub use bytes::{Buf, BufMut, Bytes, BytesMut};
Expand Down Expand Up @@ -239,11 +239,14 @@ pub fn method(method: Method) -> Route {
/// use actix_web::{web, App};
///
/// let app = App::new().service(
/// web::redirect("/one").to_relative("/two")
/// web::redirect("/one", "/two")
/// );
/// ```
pub fn redirect(path: impl Into<String>) -> Redirect {
Redirect::from(path)
pub fn redirect(
from: impl Into<Cow<'static, str>>,
to: impl Into<Cow<'static, str>>,
) -> Redirect {
Redirect::new(from, to)
}

/// Create a new route and add handler.
Expand Down

0 comments on commit e083521

Please sign in to comment.