1#[macro_use]
21extern crate serde_derive;
22
23use core::fmt;
24use std::error::Error;
25use std::fmt::{Display, Formatter};
26
27use redis::{ErrorKind, RedisError};
28use url::Url;
29
30#[cfg(test)]
31use tests::StubRedisFacade as RedisFacade;
32
33#[cfg(not(test))]
34use crate::redis_facade::RedisFacade;
35
36#[cfg(not(test))]
37pub mod redis_facade;
38
39#[derive(Debug)]
40pub struct ShortenerError {
41 message: &'static str,
42 cause: Option<Box<dyn Error>>,
43}
44
45impl ShortenerError {
46 fn new(message: &'static str) -> ShortenerError {
47 ShortenerError {
48 message,
49 cause: None,
50 }
51 }
52 fn new_with_cause(message: &'static str, error: Box<dyn Error>) -> ShortenerError {
53 ShortenerError {
54 message,
55 cause: Some(error),
56 }
57 }
58}
59
60impl Display for ShortenerError {
61 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
62 f.write_str(&self.message)?;
63
64 if let Some(err) = &self.cause {
65 return f.write_str(&format!(" - {}", &err));
66 }
67
68 Ok(())
69 }
70}
71
72impl Error for ShortenerError {}
73
74pub struct Shortener {
86 id_length: usize,
87 id_alphabet: Vec<char>,
88 id_generation_max_attempts: u8,
89 redis: RedisFacade,
90 rate_limit_period: usize,
91 rate_limit: i64,
92}
93
94#[derive(Serialize)]
97pub struct ShortenerResult {
98 id: String,
99 url: String,
100}
101
102impl Shortener {
103 pub fn new(
119 id_length: usize,
120 id_alphabet: Vec<char>,
121 id_generation_max_attempts: u8,
122 redis: RedisFacade,
123 rate_limit_period: usize,
124 rate_limit: i64,
125 ) -> Shortener {
126 Shortener {
127 id_length,
128 id_alphabet,
129 id_generation_max_attempts,
130 redis,
131 rate_limit_period,
132 rate_limit,
133 }
134 }
135
136 pub fn lookup(&self, id: &str) -> Option<String> {
139 match self.redis.get_string(id) {
140 Ok(url) => Some(url),
141 Err(_) => None,
142 }
143 }
144
145 fn verify_api_key(&self, api_key: &str) -> Result<(), ShortenerError> {
146 let api_key = format!("API_KEY_{}", api_key);
147 log::trace!("verifying api key '{}'", api_key);
148
149 let verify_and_increment = self.redis.get_bool(&api_key).and_then(|valid| {
150 if !valid {
151 return Err(RedisError::from((
152 ErrorKind::ExtensionError,
153 "API key expired",
154 )));
155 }
156
157 if self.rate_limit <= 0 {
158 return Ok(-1);
159 }
160
161 let rate_key = format!("RATE_{}", api_key);
162 log::trace!("verifying rate key '{}'", rate_key);
163
164 self.redis.exists(&rate_key).and_then(|exists| {
165 log::trace!("rate key exists {}", exists);
166
167 self.redis.increment(&rate_key).and_then(|number_of_calls| {
168 log::trace!("rate key {} number of calls {}", rate_key, number_of_calls);
169
170 if !exists {
171 self.redis
172 .expire(&rate_key, self.rate_limit_period)
173 .unwrap();
174 }
175
176 Ok(number_of_calls)
177 })
178 })
179 });
180
181 match verify_and_increment {
182 Ok(call_rate) if self.rate_limit > 0 && call_rate > self.rate_limit => {
183 Err(ShortenerError::new("Rate limit exceeded"))
184 }
185 Ok(_) => Ok(()),
186 Err(err) => Err(ShortenerError::new_with_cause(
187 "Invalid API key",
188 Box::new(err),
189 )),
190 }
191 }
192
193 fn generate_id(&self) -> Result<String, ShortenerError> {
194 for _ in 1..=self.id_generation_max_attempts {
195 let id = nanoid::custom(self.id_length, &self.id_alphabet);
196
197 let exists = self.redis.exists(&id).unwrap_or(false);
198
199 if !exists {
200 return Ok(id);
201 }
202 }
203
204 Err(ShortenerError::new(
205 "Failed to generate an ID: too many attempts. Consider using a longer ID",
206 ))
207 }
208
209 pub fn shorten(
219 &self,
220 api_key: &Option<&str>,
221 host: Option<&str>,
222 url: &str,
223 ) -> Result<ShortenerResult, ShortenerError> {
224 let verify_result = api_key
225 .as_ref()
226 .map(|api_key| self.verify_api_key(api_key))
227 .unwrap_or(Ok(()));
228
229 verify_result
230 .and_then(|_| self.generate_id())
231 .and_then(|id| {
232 let mut url = url.to_owned();
233 if !url.to_lowercase().starts_with("http") {
234 url = format!("http://{}", url);
235 }
236 Url::parse(&url)
237 .and_then(|parsed_url| Ok((id, url, parsed_url)))
238 .map_err(|parse_err| {
239 ShortenerError::new_with_cause("Unable to parse url", Box::new(parse_err))
240 })
241 })
242 .and_then(|(id, url, parsed_url)| {
243 if host.is_none() {
244 return Ok((id, url));
245 }
246
247 if parsed_url.host_str().unwrap().eq(host.unwrap()) {
248 return Err(ShortenerError::new("Link loop is not allowed"));
249 }
250
251 Ok((id, url))
252 })
253 .and_then(|(id, url)| {
254 self.redis
255 .set(&id, url.as_str())
256 .map(|_| ShortenerResult { id, url })
257 .map_err(|err| ShortenerError::new_with_cause("Redis error", Box::new(err)))
258 })
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use std::cell::RefCell;
265
266 use redis::RedisResult;
267
268 use super::*;
269
270 pub struct StubRedisFacade {
271 get_string_answers: RefCell<Vec<RedisResult<String>>>,
272 get_bool_answers: RefCell<Vec<RedisResult<bool>>>,
273 exists_answers: RefCell<Vec<RedisResult<bool>>>,
274 set_answers: RefCell<Vec<RedisResult<()>>>,
275 incr_answers: RefCell<Vec<RedisResult<i64>>>,
276 expire_answers: RefCell<Vec<RedisResult<()>>>,
277 }
278
279 impl StubRedisFacade {
280 fn new() -> Self {
281 StubRedisFacade {
282 get_string_answers: RefCell::new(vec![]),
283 get_bool_answers: RefCell::new(vec![]),
284 exists_answers: RefCell::new(vec![]),
285 set_answers: RefCell::new(vec![]),
286 incr_answers: RefCell::new(vec![]),
287 expire_answers: RefCell::new(vec![]),
288 }
289 }
290
291 pub fn get_string(&self, _key: &str) -> RedisResult<String> {
292 if self.get_string_answers.borrow().len() > 0 {
293 return self.get_string_answers.borrow_mut().remove(0);
294 }
295 panic!("unexpected get_string call");
296 }
297
298 pub fn get_bool(&self, _key: &str) -> RedisResult<bool> {
299 if self.get_bool_answers.borrow().len() > 0 {
300 return self.get_bool_answers.borrow_mut().remove(0);
301 }
302 panic!("unexpected get_bool call");
303 }
304
305 pub fn exists(&self, _key: &str) -> RedisResult<bool> {
306 if self.exists_answers.borrow().len() > 0 {
307 return self.exists_answers.borrow_mut().remove(0);
308 }
309 panic!("unexpected exists call");
310 }
311
312 pub fn set(&self, _key: &str, _value: &str) -> RedisResult<()> {
313 if self.set_answers.borrow().len() > 0 {
314 return self.set_answers.borrow_mut().remove(0);
315 }
316 panic!("unexpected set call");
317 }
318
319 pub fn increment(&self, _key: &str) -> RedisResult<i64> {
320 if self.incr_answers.borrow().len() > 0 {
321 return self.incr_answers.borrow_mut().remove(0);
322 }
323 panic!("unexpected increment call");
324 }
325
326 pub fn expire(&self, _key: &str, _seconds: usize) -> RedisResult<()> {
327 if self.expire_answers.borrow().len() > 0 {
328 return self.expire_answers.borrow_mut().remove(0);
329 }
330 panic!("unexpected expire call");
331 }
332 }
333
334 #[test]
335 fn test_lookup() {
336 let redis = StubRedisFacade::new();
337 &redis
338 .get_string_answers
339 .borrow_mut()
340 .push(Ok(String::from("test url")));
341
342 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, 10);
343 assert_eq!(shortener.lookup("id").unwrap(), "test url");
344 }
345
346 #[test]
347 fn test_shorten_happy_path_first_call() {
348 let redis = StubRedisFacade::new();
349 &redis.get_bool_answers.borrow_mut().push(Ok(true));
351 &redis.exists_answers.borrow_mut().push(Ok(false));
352 &redis.incr_answers.borrow_mut().push(Ok(1));
353 &redis.expire_answers.borrow_mut().push(Ok(()));
354
355 &redis.exists_answers.borrow_mut().push(Ok(false));
357
358 &redis.set_answers.borrow_mut().push(Ok(()));
360
361 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, 10);
362 let shorten_result = shortener
363 .shorten(&Some("api key"), Some("with.lv"), "example.com")
364 .unwrap();
365 assert_eq!(10, shorten_result.id.len());
366 assert_eq!("http://example.com", shorten_result.url);
367 }
368
369 #[test]
370 fn test_shorten_happy_path_no_rate_limit() {
371 let redis = StubRedisFacade::new();
372 &redis.get_bool_answers.borrow_mut().push(Ok(true));
374
375 &redis.exists_answers.borrow_mut().push(Ok(false));
377
378 &redis.set_answers.borrow_mut().push(Ok(()));
380
381 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, -1);
382 let shorten_result = shortener
383 .shorten(&Some("api key"), Some("with.lv"), "example.com")
384 .unwrap();
385 assert_eq!(10, shorten_result.id.len());
386 assert_eq!("http://example.com", shorten_result.url);
387 }
388
389 #[test]
390 fn test_shorten_happy_path_second_call() {
391 let redis = StubRedisFacade::new();
392 &redis.get_bool_answers.borrow_mut().push(Ok(true));
394 &redis.exists_answers.borrow_mut().push(Ok(true));
395 &redis.incr_answers.borrow_mut().push(Ok(2));
396
397 &redis.exists_answers.borrow_mut().push(Ok(false));
399
400 &redis.set_answers.borrow_mut().push(Ok(()));
402
403 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, 10);
404 let shorten_result = shortener
405 .shorten(&Some("api key"), Some("with.lv"), "example.com")
406 .unwrap();
407 assert_eq!(10, shorten_result.id.len());
408 assert_eq!("http://example.com", shorten_result.url);
409 }
410
411 #[test]
412 fn test_shorten_happy_path_no_api_key() {
413 let redis = StubRedisFacade::new();
414 &redis.exists_answers.borrow_mut().push(Ok(false));
416
417 &redis.set_answers.borrow_mut().push(Ok(()));
419
420 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, 10);
421 let shorten_result = shortener
422 .shorten(&None, Some("with.lv"), "example.com")
423 .unwrap();
424 assert_eq!(10, shorten_result.id.len());
425 assert_eq!("http://example.com", shorten_result.url);
426 }
427
428 #[test]
429 fn test_shorten_unhappy_path_rate_limit_exceeded() {
430 let rate_limit = 10;
431 let redis = StubRedisFacade::new();
432 &redis.get_bool_answers.borrow_mut().push(Ok(true));
434 &redis.exists_answers.borrow_mut().push(Ok(true));
435 &redis.incr_answers.borrow_mut().push(Ok(rate_limit + 1));
436
437 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, rate_limit);
438 let shorten_result_err = shortener
439 .shorten(&Some("api key"), Some("with.lv"), "example.com")
440 .err()
441 .unwrap();
442 assert_eq!("Rate limit exceeded", shorten_result_err.message);
443 }
444
445 #[test]
446 fn test_shorten_unhappy_path_bad_url() {
447 let redis = StubRedisFacade::new();
448 &redis.exists_answers.borrow_mut().push(Ok(false));
450
451 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, -1);
452 let shorten_result_err = shortener
453 .shorten(&None, Some("with.lv"), "wrong domain.com")
454 .err()
455 .unwrap();
456 assert_eq!("Unable to parse url", shorten_result_err.message);
457 }
458
459 #[test]
460 fn test_shorten_unhappy_path_same_domain() {
461 let redis = StubRedisFacade::new();
462 &redis.exists_answers.borrow_mut().push(Ok(false));
464
465 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, -1);
466 let shorten_result_err = shortener
467 .shorten(&None, Some("example.com"), "example.com")
468 .err()
469 .unwrap();
470 assert_eq!("Link loop is not allowed", shorten_result_err.message);
471 }
472
473 #[test]
474 fn test_shorten_happy_path_rate_limit_expired() {
475 let redis = StubRedisFacade::new();
476
477 &redis.get_bool_answers.borrow_mut().push(Ok(true));
479 &redis.exists_answers.borrow_mut().push(Ok(true));
480 &redis.incr_answers.borrow_mut().push(Ok(1));
481
482 &redis.exists_answers.borrow_mut().push(Ok(false));
484
485 &redis.set_answers.borrow_mut().push(Ok(()));
487
488 &redis.get_bool_answers.borrow_mut().push(Ok(true));
490 &redis.exists_answers.borrow_mut().push(Ok(false));
491 &redis.incr_answers.borrow_mut().push(Ok(1));
492 &redis.expire_answers.borrow_mut().push(Ok(()));
493
494 &redis.exists_answers.borrow_mut().push(Ok(false));
496
497 &redis.set_answers.borrow_mut().push(Ok(()));
499
500 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, 10);
501
502 let shorten_result = shortener
503 .shorten(&Some("api key"), Some("with.lv"), "example.com")
504 .unwrap();
505 assert_eq!(10, shorten_result.id.len());
506 assert_eq!("http://example.com", shorten_result.url);
507
508 let shorten_result = shortener
509 .shorten(&Some("api key"), Some("with.lv"), "www.wikipedia.org")
510 .unwrap();
511 assert_eq!(10, shorten_result.id.len());
512 assert_eq!("http://www.wikipedia.org", shorten_result.url);
513 }
514
515 #[test]
516 fn test_shorten_unhappy_path_invalid_api_key() {
517 let redis = StubRedisFacade::new();
518
519 &redis.get_bool_answers.borrow_mut().push(Ok(false));
521
522 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 10, redis, 600, 10);
523 let shorten_result_err = shortener
524 .shorten(&Some("api key"), Some("with.lv"), "example.com")
525 .err()
526 .unwrap();
527 assert_eq!("Invalid API key", shorten_result_err.message);
528 }
529
530 #[test]
531 fn test_shorten_unhappy_path_too_many_attempts_generating_id() {
532 let redis = StubRedisFacade::new();
533
534 &redis.exists_answers.borrow_mut().push(Ok(true));
536 &redis.exists_answers.borrow_mut().push(Ok(true));
537
538 let shortener = Shortener::new(10, vec!['a', 'b', 'c'], 2, redis, 600, 10);
539 let shorten_result_err = shortener
540 .shorten(&None, Some("with.lv"), "example.com")
541 .err()
542 .unwrap();
543 assert_eq!(
544 "Failed to generate an ID: too many attempts. Consider using a longer ID",
545 shorten_result_err.message
546 );
547 }
548}