shorty_http/
lib.rs

1// Copyright 2019 Federico Fissore
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[macro_use]
16extern crate serde_derive;
17
18use actix_web::http::StatusCode;
19use actix_web::{HttpRequest, HttpResponse, Json, Path};
20use redis::Client;
21
22use shorty::redis_facade::RedisFacade;
23use shorty::Shortener;
24
25pub struct AppState {
26    shortener: Shortener,
27    api_key_mandatory: bool,
28}
29
30impl AppState {
31    pub fn new(
32        redis_host: &str,
33        redis_port: &str,
34        id_length: usize,
35        id_generation_max_attempts: u8,
36        rate_limit_period: usize,
37        rate_limit: i64,
38        api_key_mandatory: bool,
39    ) -> AppState {
40        let redis = Client::open(format!("redis://{}:{}/", redis_host, redis_port).as_str())
41            .unwrap()
42            .get_connection()
43            .unwrap();
44
45        let alphabet = vec![
46            (b'a'..=b'z').map(char::from).collect::<Vec<_>>(),
47            (b'A'..=b'Z').map(char::from).collect::<Vec<_>>(),
48            (b'0'..=b'9').map(char::from).collect::<Vec<_>>(),
49        ]
50        .into_iter()
51        .flatten()
52        .collect::<Vec<char>>();
53
54        AppState {
55            shortener: Shortener::new(
56                id_length,
57                alphabet,
58                id_generation_max_attempts,
59                RedisFacade::new(redis),
60                rate_limit_period,
61                rate_limit,
62            ),
63            api_key_mandatory,
64        }
65    }
66}
67
68pub fn goto((req, id): (HttpRequest<AppState>, Path<String>)) -> HttpResponse {
69    let app_state: &AppState = &req.state();
70
71    match app_state.shortener.lookup(&id) {
72        Some(url) => HttpResponse::Found().header("Location", url).finish(),
73        None => HttpResponse::NotFound().finish(),
74    }
75}
76
77#[derive(Deserialize)]
78pub struct ShortenRequest {
79    api_key: Option<String>,
80    url: String,
81}
82
83#[derive(Serialize)]
84struct ErrorResponse {
85    err: String,
86}
87
88pub fn shorten((req, payload): (HttpRequest<AppState>, Json<ShortenRequest>)) -> HttpResponse {
89    let app_state: &AppState = &req.state();
90
91    if payload.api_key.is_none() && app_state.api_key_mandatory {
92        return HttpResponse::Ok()
93            .status(StatusCode::FORBIDDEN)
94            .json(ErrorResponse {
95                err: String::from("Missing API key"),
96            });
97    }
98
99    let api_key = payload.api_key.as_ref().map(String::as_str);
100
101    let host_domain = &req
102        .connection_info()
103        .host()
104        .split(":")
105        .take(1)
106        .collect::<String>();
107
108    match app_state
109        .shortener
110        .shorten(&api_key, Some(host_domain), &payload.url)
111    {
112        Ok(shorten_result) => HttpResponse::Ok().json(shorten_result),
113        Err(err) => HttpResponse::InternalServerError().json(ErrorResponse {
114            err: err.to_string(),
115        }),
116    }
117}