Skip to content

Instantly share code, notes, and snippets.

@ShabbirHasan1
Created January 23, 2025 12:51
Show Gist options
  • Save ShabbirHasan1/3f10b4a42a067d096e87b3e9abb0768a to your computer and use it in GitHub Desktop.
Save ShabbirHasan1/3f10b4a42a067d096e87b3e9abb0768a to your computer and use it in GitHub Desktop.
//! ## Task Description
//!
//! The goal is to develop a backend service for shortening URLs using CQRS
//! (Command Query Responsibility Segregation) and ES (Event Sourcing)
//! approaches. The service should support the following features:
//!
//! ## Functional Requirements
//!
//! ### Creating a short link with a random slug
//!
//! The user sends a long URL, and the service returns a shortened URL with a
//! random slug.
//!
//! ### Creating a short link with a predefined slug
//!
//! The user sends a long URL along with a predefined slug, and the service
//! checks if the slug is unique. If it is unique, the service creates the short
//! link.
//!
//! ### Counting the number of redirects for the link
//!
//! - Every time a user accesses the short link, the click count should
//! increment.
//! - The click count can be retrieved via an API.
//!
//! ### CQRS+ES Architecture
//!
//! CQRS: Commands (creating links, updating click count) are separated from
//! queries (retrieving link information).
//!
//! Event Sourcing: All state changes (link creation, click count update) must be
//! recorded as events, which can be replayed to reconstruct the system's state.
//!
//! ### Technical Requirements
//!
//! - The service must be built using CQRS and Event Sourcing approaches.
//! - The service must be possible to run in Rust Playground (so no database like
//! Postgres is allowed)
//! - Public API already written for this task must not be changed (any change to
//! the public API items must be considered as breaking change).
//! - Event Sourcing should be actively utilized for implementing logic, rather
//! than existing without a clear purpose.
#![allow(unused_variables, dead_code)]
use std::{
collections::HashMap,
sync::{
mpsc::{sync_channel, Receiver, SyncSender},
Arc, RwLock,
},
thread,
time::Instant,
};
use helpers::UrlShortenerHelper;
use log::{SlugActionType, StatLog};
static EMPTY_STRING: &str = "";
static HASH_LENGTH: usize = 10;
pub(crate) mod helpers {
use super::{ShortenerError, Slug, Url, EMPTY_STRING, HASH_LENGTH};
use rand::distributions::{Alphanumeric, DistString};
use regex::Regex;
pub struct UrlShortenerHelper;
impl UrlShortenerHelper {
pub fn generate_slug() -> Slug {
Slug(Alphanumeric.sample_string(&mut rand::thread_rng(), HASH_LENGTH))
}
pub fn validate_url(url: &Url) -> Result<(), ShortenerError> {
UrlShortenerRegexHelper::get_re_validator()
.find(&url.0)
.map(|_| ())
.ok_or(ShortenerError::InvalidUrl)
}
pub fn get_url_path(url: &Url) -> Url {
let result =
UrlShortenerRegexHelper::get_re_prefix_with_domain().replace(&url.0, EMPTY_STRING);
Url(result.into_owned())
}
}
pub struct UrlShortenerRegexHelper;
impl UrlShortenerRegexHelper {
pub fn get_re_validator() -> Regex {
Regex::new(r"^(?i)((ftp|http|https):\/\/)?(www.)?[a-zA-Z0-9-]+.[a-zA-Z0-9-]{2,3}((\/)?(\w+)?)*((\?)(\w+=\w+))?$").unwrap()
}
pub fn get_re_prefix_with_domain() -> Regex {
Regex::new(r"^(?i)((ht|f)tps?:\/\/)?(www.)?([0-9a-z-]+)(.)([a-z]{2,3})").unwrap()
}
}
}
/// All possible errors of the [`UrlShortenerService`].
#[derive(Debug, PartialEq)]
pub enum ShortenerError {
/// This error occurs when an invalid [`Url`] is provided for shortening.
InvalidUrl,
/// This error occurs when an attempt is made to use a slug (custom alias)
/// that already exists.
SlugAlreadyInUse,
/// This error occurs when the provided [`Slug`] does not map to any existing
/// short link.
SlugNotFound,
}
/// A unique string (or alias) that represents the shortened version of the
/// URL.
#[derive(Clone, Debug, PartialEq)]
pub struct Slug(pub String);
/// The original URL that the short link points to.
#[derive(Clone, Debug, PartialEq)]
pub struct Url(pub String);
/// Shortened URL representation.
#[derive(Debug, Clone, PartialEq)]
pub struct ShortLink {
/// A unique string (or alias) that represents the shortened version of the
/// URL.
pub slug: Slug,
/// The original URL that the short link points to.
pub url: Url,
}
/// Statistics of the [`ShortLink`].
#[derive(Debug, Clone, PartialEq)]
pub struct Stats {
/// [`ShortLink`] to which this [`Stats`] are related.
pub link: ShortLink,
/// Count of redirects of the [`ShortLink`].
pub redirects: u64,
}
pub mod log {
use super::{Slug, Url};
use std::time::Instant;
#[derive(Debug, Clone, PartialEq)]
pub enum SlugActionType {
CREATE,
REDIRECT,
DELETE,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatLog {
pub slug: Slug,
pub url: Url,
pub action_type: SlugActionType,
pub created: Instant,
}
}
/// Commands for CQRS.
pub mod commands {
use super::{ShortLink, ShortenerError, Slug, Url};
/// Trait for command handlers.
pub trait CommandHandler {
/// Creates a new short link. It accepts the original url and an
/// optional [`Slug`]. If a [`Slug`] is not provided, the service will generate
/// one. Returns the newly created [`ShortLink`].
///
/// ## Errors
///
/// See [`ShortenerError`].
fn handle_create_short_link(
&mut self,
url: Url,
slug: Option<Slug>,
) -> Result<ShortLink, ShortenerError>;
/// Processes a redirection by [`Slug`], returning the associated
/// [`ShortLink`] or a [`ShortenerError`].
fn handle_redirect(&mut self, slug: Slug) -> Result<ShortLink, ShortenerError>;
}
}
/// Queries for CQRS
pub mod queries {
use super::{ShortenerError, Slug, Stats};
/// Trait for query handlers.
pub trait QueryHandler {
/// Returns the [`Stats`] for a specific [`ShortLink`], such as the
/// number of redirects (clicks).
///
/// [`ShortLink`]: super::ShortLink
fn get_stats(&self, slug: Slug) -> Result<Stats, ShortenerError>;
}
}
/// CQRS and Event Sourcing-based service implementation
pub struct UrlShortenerService {
slug_url_map: Arc<RwLock<HashMap<String, Url>>>,
slug_counter_map: Arc<RwLock<HashMap<String, u64>>>,
url_to_slug_map: HashMap<String, Slug>,
log_tx: SyncSender<StatLog>,
}
impl UrlShortenerService {
/// Creates a new instance of the service
pub fn new() -> Self {
let (tx, rx): (SyncSender<StatLog>, Receiver<StatLog>) = sync_channel(2);
let mut s = Self {
slug_url_map: Arc::new(RwLock::new(HashMap::new())),
slug_counter_map: Arc::new(RwLock::new(HashMap::new())),
url_to_slug_map: HashMap::new(),
log_tx: tx,
};
s.receive_action_for_slug(rx);
s
}
}
impl UrlShortenerService {
fn apply_slug_for_url(&mut self, url: &Url, slug: &Slug) -> Result<ShortLink, ShortenerError> {
let read_guard = self.slug_url_map.read().unwrap();
let is_slug_occupies: bool = read_guard.get(&slug.0).is_some();
drop(read_guard);
if is_slug_occupies {
return Err(ShortenerError::SlugAlreadyInUse);
}
let mut guard = self.slug_url_map.write().unwrap();
guard.insert(slug.0.clone(), url.clone());
drop(guard);
Some(url.clone())
.map(|inserted_url: Url| {
self.url_to_slug_map
.insert(inserted_url.0.clone(), slug.clone())
.map(|prev_slug| {
self.send_action_for_slug(&prev_slug, &url, SlugActionType::DELETE)
});
(inserted_url, slug.clone())
})
.map(|(inserted_url, inserted_slug): (Url, Slug)| ShortLink {
slug: inserted_slug.clone(),
url: inserted_url,
})
.ok_or(ShortenerError::SlugAlreadyInUse)
}
fn create_short_link(&mut self, url: &Url) -> Result<ShortLink, ShortenerError> {
self.url_to_slug_map
.get(&url.0)
.map(|slug_ref| ShortLink {
slug: slug_ref.to_owned(),
url: url.clone(),
})
.or_else(|| {
let mut generated_slug = UrlShortenerHelper::generate_slug();
while self.apply_slug_for_url(&url, &generated_slug).is_err() {
generated_slug = UrlShortenerHelper::generate_slug();
}
Some(ShortLink {
slug: generated_slug,
url: url.clone(),
})
})
.ok_or(ShortenerError::InvalidUrl)
}
fn get_short_link_for_slug(&mut self, slug: &Slug) -> Result<ShortLink, ShortenerError> {
let short_link_opt: Option<ShortLink> =
self.slug_url_map
.read()
.unwrap()
.get(&slug.0)
.map(|url_ref| ShortLink {
slug: slug.clone(),
url: url_ref.clone(),
});
short_link_opt
.map(|short_link: ShortLink| {
self.send_action_for_slug(
&short_link.slug,
&short_link.url,
SlugActionType::REDIRECT,
);
short_link
})
.ok_or(ShortenerError::SlugNotFound)
}
fn get_stats_for_slug(&self, slug: &Slug) -> Result<Stats, ShortenerError> {
self.slug_counter_map
.read()
.unwrap()
.get(&slug.0)
.map(|v| v.clone())
.ok_or(ShortenerError::SlugNotFound)
.and_then(|redirects| {
self.slug_url_map
.read()
.unwrap()
.get(&slug.0)
.map(|url: &Url| url.clone())
.ok_or(ShortenerError::SlugNotFound)
.map(|url| Stats {
link: ShortLink {
slug: slug.clone(),
url,
},
redirects,
})
})
}
fn send_action_for_slug(&mut self, slug: &Slug, url: &Url, action_type: SlugActionType) {
self.log_tx
.send(StatLog {
slug: slug.clone(),
url: url.clone(),
action_type,
created: Instant::now(),
})
.expect("Could not send log on channel");
}
fn receive_action_for_slug(&mut self, rx: Receiver<StatLog>) {
let slug_counter_map = self.slug_counter_map.clone();
let slug_url_map = self.slug_url_map.clone();
// thread::scope(|s| {
thread::spawn(move || {
for stat_log in rx.iter() {
match stat_log.action_type {
SlugActionType::CREATE => {
UrlShortenerService::store_stat_log(&stat_log, "created");
}
SlugActionType::REDIRECT => {
UrlShortenerService::store_stat_log(&stat_log, "redireted");
// 1. increment counter at slug_counter_map
let mut guard = slug_counter_map.write().unwrap();
*guard.entry(stat_log.slug.0.to_owned()).or_default() += 1;
}
SlugActionType::DELETE => {
UrlShortenerService::store_stat_log(&stat_log, "deleted");
// !!!!!!
// I'm not sure if I need to remove old slugs (with stats counter??) when a new slug is provided for a Url
// !!!!!!
let mut guard = slug_url_map.write().unwrap();
guard.remove(&stat_log.slug.0);
drop(guard);
let mut guard = slug_counter_map.write().unwrap();
guard.remove(&stat_log.slug.0);
drop(guard);
}
}
}
});
// })
}
fn store_stat_log(stat_log: &StatLog, action: &str) {
println!(
"slug '{:?} for url '{:?}' {} at {:?};",
stat_log.slug, stat_log.url, action, stat_log
)
}
}
impl commands::CommandHandler for UrlShortenerService {
fn handle_create_short_link(
&mut self,
url: Url,
slug: Option<Slug>,
) -> Result<ShortLink, ShortenerError> {
// todo!("Implement the logic for creating a short link");
println!("requested url {:?} , slug {:?}", &url, slug);
UrlShortenerHelper::validate_url(&url)?;
match slug {
Some(s) => self.apply_slug_for_url(&url, &s),
None => self.create_short_link(&url),
}
}
fn handle_redirect(&mut self, slug: Slug) -> Result<ShortLink, ShortenerError> {
// todo!("Implement the logic for redirection and incrementing the click counter")
self.get_short_link_for_slug(&slug)
}
}
impl queries::QueryHandler for UrlShortenerService {
fn get_stats(&self, slug: Slug) -> Result<Stats, ShortenerError> {
// todo!("Implement the logic for retrieving link statistics")
self.get_stats_for_slug(&slug)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use commands::CommandHandler;
use queries::QueryHandler;
use super::*;
#[test]
fn should_pass_test_init_test() {
// before
println!("1");
let url_str = String::from("http://google.com/folder/subfolder/123?page=2");
let mut url_short_service = UrlShortenerService::new();
println!("2");
// when
let short_link_res = url_short_service.handle_create_short_link(Url(url_str.clone()), None);
println!("3");
// then
assert!(short_link_res.is_ok());
let short_link: ShortLink = short_link_res.unwrap();
assert!(short_link.slug.0.len() == 10);
assert!(short_link.url.0 == url_str);
let stored_short_link: ShortLink =
url_short_service.handle_redirect(short_link.slug).unwrap();
assert!(stored_short_link.url == Url(url_str.clone()))
}
#[test]
fn should_pass_test_same_url_should_receive_same_slug() {
// before
let url_1 = "http://google.com/folder/subfolder/123?page=1";
let mut url_short_service = UrlShortenerService::new();
// when
let short_link_res_1 =
url_short_service.handle_create_short_link(Url(String::from(url_1)), None);
let short_link_res_2 =
url_short_service.handle_create_short_link(Url(String::from(url_1)), None);
// then
assert_eq!(short_link_res_1, short_link_res_2);
}
#[test]
fn should_pass_test_different_url_paths_should_have_different_slugs() {
// before
let url_1 = "http://google.com/folder/subfolder/123?page=1";
let url_2 = "http://google.com/folder/subfolder/123?page=2";
// when
let mut url_short_service = UrlShortenerService::new();
let short_link_res_1 =
url_short_service.handle_create_short_link(Url(String::from(url_1)), None);
let short_link_res_2 =
url_short_service.handle_create_short_link(Url(String::from(url_2)), None);
// then
assert!(short_link_res_1 != short_link_res_2);
}
#[test]
fn should_pass_test_different_url_domains_should_have_different_slugs() {
// before
let url_1 = "http://google.com/folder/subfolder/123?page=1";
let url_2 = "http://yandex.com/folder/subfolder/123?page=1";
// when
let mut url_short_service = UrlShortenerService::new();
let short_link_res_1 =
url_short_service.handle_create_short_link(Url(String::from(url_1)), None);
let short_link_res_2 =
url_short_service.handle_create_short_link(Url(String::from(url_2)), None);
// then
assert!(short_link_res_1 != short_link_res_2);
}
#[test]
fn should_pass_test_change_slug_and_retrieve_new_one() {
// before
let url_1 = "http://google.com/folder/subfolder/123?page=1";
let url_2 = "http://yandex.com/folder/subfolder/123?page=2";
let new_slug = String::from("1234567890");
let mut url_short_service = UrlShortenerService::new();
// when
let short_link_res_1 =
url_short_service.handle_create_short_link(Url(String::from(url_1)), None);
let _ = url_short_service
.handle_create_short_link(Url(String::from(url_1)), Some(Slug(new_slug.clone())));
let short_link_2: ShortLink = url_short_service
.handle_create_short_link(Url(String::from(url_1)), None)
.unwrap();
// then
assert_eq!(short_link_2.slug.0, new_slug);
}
#[test]
fn should_fail_test_slug_doesnt_exists() {
// before
let absent_slug = String::from("1234567890");
let mut url_short_service = UrlShortenerService::new();
// when
// then
assert!(
url_short_service
.handle_redirect(Slug(absent_slug.clone()))
.unwrap_err()
== ShortenerError::SlugNotFound
);
// when
// then
assert!(
url_short_service.get_stats(Slug(absent_slug)).unwrap_err()
== ShortenerError::SlugNotFound
);
}
#[test]
fn should_pass_on_valid_url() {
// before
let mut url_short_service = UrlShortenerService::new();
let url_1 = "http://google.com";
let url_2 = "HTTPS://GOOGLE.COM/";
let url_3 = "HTTPS://WWW.YA-YA.RU/folder/subfolder/123?page=1";
let url_4 = "FTP://GOO-gle.com/folder/subfolder/123?page=1";
let url_5 = "http://www.goo-123-123-gle.com/folder/subfolder/123?page=1";
// when
// then
assert!(url_short_service
.handle_create_short_link(Url(String::from(url_1)), None)
.is_ok());
// when
// then
assert!(url_short_service
.handle_create_short_link(Url(String::from(url_2)), None)
.is_ok());
// when
// then
assert!(url_short_service
.handle_create_short_link(Url(String::from(url_3)), None)
.is_ok());
// when
// then
assert!(url_short_service
.handle_create_short_link(Url(String::from(url_4)), None)
.is_ok());
// when
// then
assert!(url_short_service
.handle_create_short_link(Url(String::from(url_5)), None)
.is_ok());
}
#[test]
fn should_fail_on_invalid_url() {
// before
let mut url_short_service = UrlShortenerService::new();
let url_1 = "ht-tp://google.com/folder/subfolder/123?page=1";
let url_2 = "assdasd://google.com/folder/subfolder/123?page=1";
let url_3 = "wwwwwww.google.com/folder/subfolder/123?page=1";
let url_4 = "http://goo_gle.com/folder/subfolder/123?page=1";
let url_5 = "ht-tp://google.comcomcom/folder/subfolder/123?page=1";
// when
// then
assert!(
url_short_service
.handle_create_short_link(Url(String::from(url_1)), None)
.unwrap_err()
== ShortenerError::InvalidUrl
);
// when
// then
assert!(
url_short_service
.handle_create_short_link(Url(String::from(url_2)), None)
.unwrap_err()
== ShortenerError::InvalidUrl
);
// when
// then
assert!(
url_short_service
.handle_create_short_link(Url(String::from(url_3)), None)
.unwrap_err()
== ShortenerError::InvalidUrl
);
// when
// then
assert!(
url_short_service
.handle_create_short_link(Url(String::from(url_4)), None)
.unwrap_err()
== ShortenerError::InvalidUrl
);
// when
// then
assert!(
url_short_service
.handle_create_short_link(Url(String::from(url_5)), None)
.unwrap_err()
== ShortenerError::InvalidUrl
);
}
#[test]
fn should_pass_test_count_redirects_for_valid_slug() {
// before
let url_1 = "http://google.com/folder/subfolder/123?page=1";
let mut url_short_service = UrlShortenerService::new();
// when
let short_link: ShortLink = url_short_service
.handle_create_short_link(Url(String::from(url_1)), None)
.unwrap();
let _ = url_short_service.handle_redirect(short_link.slug.clone());
let _ = url_short_service.handle_redirect(short_link.slug.clone());
let _ = url_short_service.handle_redirect(short_link.slug.clone());
thread::sleep(Duration::from_millis(100));
// then
let stats: Stats = url_short_service.get_stats(short_link.slug).unwrap();
assert_eq!(stats.link.url.0, url_1);
assert_eq!(stats.redirects, 3);
}
#[test]
fn should_fail_test_slug_already_in_use() {
// before
let url_1 = "http://google.com/folder/subfolder/123?page=1";
let url_2 = "http://yandex.com/folder/subfolder/123?page=2";
// when
let mut url_short_service = UrlShortenerService::new();
let short_link: ShortLink = url_short_service
.handle_create_short_link(Url(String::from(url_1)), None)
.unwrap();
let result = url_short_service
.handle_create_short_link(Url(String::from(url_2)), Some(short_link.slug.clone()));
// then
assert!(result.unwrap_err() == ShortenerError::SlugAlreadyInUse);
}
#[test]
fn should_fail_test_slug_already_in_use_for_same_url() {
// before
let url_1 = "http://google.com/folder/subfolder/123?page=1";
let mut url_short_service = UrlShortenerService::new();
// whoen
let short_link: ShortLink = url_short_service
.handle_create_short_link(Url(String::from(url_1)), None)
.unwrap();
let result = url_short_service
.handle_create_short_link(Url(String::from(url_1)), Some(short_link.slug.clone()));
// then
assert!(result.unwrap_err() == ShortenerError::SlugAlreadyInUse);
}
#[test]
fn should_fail_test_receive_stats_for_outdated_slug() {
// before
let url_1 = "http://google.com/folder/subfolder/123?page=1";
let mut url_short_service = UrlShortenerService::new();
// when
let short_link: ShortLink = url_short_service
.handle_create_short_link(Url(String::from(url_1)), None)
.unwrap();
let _ = url_short_service.handle_redirect(short_link.slug.clone());
let new_slug = Slug(String::from("123"));
let updated_short_url = url_short_service
.handle_create_short_link(Url(String::from(url_1)), Some(new_slug.clone()))
.unwrap();
let _ = url_short_service.handle_redirect(updated_short_url.slug.clone());
thread::sleep(Duration::from_millis(100));
// then
assert!(
url_short_service
.handle_redirect(short_link.slug.clone())
.unwrap_err()
== ShortenerError::SlugNotFound
);
assert!(
url_short_service.get_stats(short_link.slug).unwrap_err()
== ShortenerError::SlugNotFound
);
assert!(url_short_service
.handle_redirect(updated_short_url.slug.clone())
.is_ok());
assert!(url_short_service.get_stats(updated_short_url.slug).is_ok());
}
}
//! ## Task Description
//!
//! The goal is to develop a backend service for shortening URLs using CQRS
//! (Command Query Responsibility Segregation) and ES (Event Sourcing)
//! approaches. The service should support the following features:
//!
//! ## Functional Requirements
//!
//! ### Creating a short link with a random slug
//!
//! The user sends a long URL, and the service returns a shortened URL with a
//! random slug.
//!
//! ### Creating a short link with a predefined slug
//!
//! The user sends a long URL along with a predefined slug, and the service
//! checks if the slug is unique. If it is unique, the service creates the short
//! link.
//!
//! ### Counting the number of redirects for the link
//!
//! - Every time a user accesses the short link, the click count should
//! increment.
//! - The click count can be retrieved via an API.
//!
//! ### CQRS+ES Architecture
//!
//! CQRS: Commands (creating links, updating click count) are separated from
//! queries (retrieving link information).
//!
//! Event Sourcing: All state changes (link creation, click count update) must be
//! recorded as events, which can be replayed to reconstruct the system's state.
//!
//! ### Technical Requirements
//!
//! - The service must be built using CQRS and Event Sourcing approaches.
//! - The service must be possible to run in Rust Playground (so no database like
//! Postgres is allowed)
//! - Public API already written for this task must not be changed (any change to
//! the public API items must be considered as breaking change).
//! - Event Sourcing should be actively utilized for implementing logic, rather
//! than existing without a clear purpose.
#![allow(unused_variables, dead_code)]
/// All possible errors of the [`UrlShortenerService`].
#[derive(Debug, PartialEq)]
pub enum ShortenerError {
/// This error occurs when an invalid [`Url`] is provided for shortening.
InvalidUrl,
/// This error occurs when an attempt is made to use a slug (custom alias)
/// that already exists.
SlugAlreadyInUse,
/// This error occurs when the provided [`Slug`] does not map to any existing
/// short link.
SlugNotFound,
}
/// A unique string (or alias) that represents the shortened version of the
/// URL.
#[derive(Clone, Debug, PartialEq)]
pub struct Slug(pub String);
/// The original URL that the short link points to.
#[derive(Clone, Debug, PartialEq)]
pub struct Url(pub String);
/// Shortened URL representation.
#[derive(Debug, Clone, PartialEq)]
pub struct ShortLink {
/// A unique string (or alias) that represents the shortened version of the
/// URL.
pub slug: Slug,
/// The original URL that the short link points to.
pub url: Url,
}
/// Statistics of the [`ShortLink`].
#[derive(Debug, Clone, PartialEq)]
pub struct Stats {
/// [`ShortLink`] to which this [`Stats`] are related.
pub link: ShortLink,
/// Count of redirects of the [`ShortLink`].
pub redirects: u64,
}
/// Commands for CQRS.
pub mod commands {
use super::{ShortLink, ShortenerError, Slug, Url};
/// Trait for command handlers.
pub trait CommandHandler {
/// Creates a new short link. It accepts the original url and an
/// optional [`Slug`]. If a [`Slug`] is not provided, the service will generate
/// one. Returns the newly created [`ShortLink`].
///
/// ## Errors
///
/// See [`ShortenerError`].
fn handle_create_short_link(
&mut self,
url: Url,
slug: Option<Slug>,
) -> Result<ShortLink, ShortenerError>;
/// Processes a redirection by [`Slug`], returning the associated
/// [`ShortLink`] or a [`ShortenerError`].
fn handle_redirect(
&mut self,
slug: Slug,
) -> Result<ShortLink, ShortenerError>;
}
}
/// Queries for CQRS
pub mod queries {
use super::{ShortenerError, Slug, Stats};
/// Trait for query handlers.
pub trait QueryHandler {
/// Returns the [`Stats`] for a specific [`ShortLink`], such as the
/// number of redirects (clicks).
///
/// [`ShortLink`]: super::ShortLink
fn get_stats(&self, slug: Slug) -> Result<Stats, ShortenerError>;
}
}
/// CQRS and Event Sourcing-based service implementation
pub struct UrlShortenerService {
// TODO: add needed fields
}
impl UrlShortenerService {
/// Creates a new instance of the service
pub fn new() -> Self {
Self {}
}
}
impl commands::CommandHandler for UrlShortenerService {
fn handle_create_short_link(
&mut self,
url: Url,
slug: Option<Slug>,
) -> Result<ShortLink, ShortenerError> {
todo!("Implement the logic for creating a short link")
}
fn handle_redirect(
&mut self,
slug: Slug,
) -> Result<ShortLink, ShortenerError> {
todo!("Implement the logic for redirection and incrementing the click counter")
}
}
impl queries::QueryHandler for UrlShortenerService {
fn get_stats(&self, slug: Slug) -> Result<Stats, ShortenerError> {
todo!("Implement the logic for retrieving link statistics")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment