mod url_path;
use std::collections::HashMap;
use std::ffi::OsString;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::{fmt, fs};
use thiserror::Error;
use chrono::Utc;
use crate::redirector::url_path::UrlPath;
#[derive(Debug, Error)]
pub enum RedirectorError {
#[error("Failed to create redirect file")]
FileCreationError(#[from] std::io::Error),
#[error("Short link not found")]
ShortLinkNotFound,
#[error("Invalid URL path: {0}")]
InvalidUrlPath(#[from] url_path::UrlPathError),
#[error("Failed to read redirect registry")]
FailedToReadRegistry(#[from] serde_json::Error),
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Redirector {
long_path: UrlPath,
short_file_name: OsString,
path: PathBuf,
}
impl Redirector {
pub fn new<S: ToString>(long_path: S) -> Result<Self, RedirectorError> {
let long_path = UrlPath::new(long_path.to_string())?;
let short_file_name = Redirector::generate_short_file_name(&long_path);
Ok(Redirector {
long_path,
short_file_name,
path: PathBuf::from("s"),
})
}
fn generate_short_file_name(long_path: &UrlPath) -> OsString {
let name = base62::encode(
Utc::now().timestamp_millis() as u64
+ long_path.encode_utf16().iter().sum::<u16>() as u64,
);
OsString::from(format!("{name}.html"))
}
pub fn short_file_name(&self) -> OsString {
self.short_file_name.clone()
}
pub fn set_path<P: Into<PathBuf>>(&mut self, path: P) {
self.path = path.into();
}
pub fn write_redirect(&self) -> Result<String, RedirectorError> {
if !Path::new(&self.path).exists() {
fs::create_dir_all(&self.path)?;
}
const REDIRECT_REGISTRY: &str = "registry.json";
let mut registry: HashMap<String, String> = HashMap::new();
if Path::new(&self.path).join(REDIRECT_REGISTRY).exists() {
registry = serde_json::from_reader::<_, HashMap<String, String>>(File::open(
self.path.join(REDIRECT_REGISTRY),
)?)?;
}
let file_path = self.path.join(&self.short_file_name);
if let Some(existing_path) = registry.get(&self.long_path.to_string()) {
Ok(existing_path.clone())
} else {
let mut file = File::create(&file_path)?;
file.write_all(self.to_string().as_bytes())?;
file.sync_all()?;
registry.insert(
self.long_path.to_string(),
file_path.to_string_lossy().to_string(),
);
serde_json::to_writer_pretty(
File::create(self.path.join(REDIRECT_REGISTRY))?,
®istry,
)?;
Ok(file_path.to_string_lossy().to_string())
}
}
}
impl fmt::Display for Redirector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let target = self.long_path.to_string();
write!(
f,
r#"
<!DOCTYPE HTML>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url={target}">
<script type="text/javascript">
window.location.href = "{target}";
</script>
<title>Page Redirection</title>
</head>
<body>
<!-- Note: don't tell people to `click` the link, just tell them that it is a link. -->
If you are not redirected automatically, follow this <a href='{target}'>link to page</a>.
</body>
</html>
"#
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::thread;
use std::time::Duration;
#[test]
fn test_new_redirector() {
let long_link = "/some/path";
let redirector = Redirector::new(long_link).unwrap();
assert_eq!(
redirector.long_path,
UrlPath::new(long_link.to_string()).unwrap()
);
assert!(!redirector.short_file_name.is_empty());
assert_eq!(redirector.path, PathBuf::from("s"));
}
#[test]
fn test_generate_short_link_unique() {
let redirector1 = Redirector::new("/some/path").unwrap();
thread::sleep(Duration::from_millis(1));
let redirector2 = Redirector::new("/some/path").unwrap();
assert_ne!(redirector1.short_file_name, redirector2.short_file_name);
}
#[test]
fn test_set_path() {
let mut redirector = Redirector::new("/some/path/").unwrap();
redirector.set_path("custom_path");
assert_eq!(redirector.path, PathBuf::from("custom_path"));
redirector.set_path("another/path".to_string());
assert_eq!(redirector.path, PathBuf::from("another/path"));
}
#[test]
fn test_display_renders_html() {
let redirector = Redirector::new("some/path").unwrap();
let output = format!("{redirector}");
assert!(output.contains("<!DOCTYPE HTML>"));
assert!(output.contains("/some/path/"));
assert!(output.contains("meta http-equiv=\"refresh\""));
assert!(output.contains("window.location.href"));
}
#[test]
fn test_display_with_complex_path() {
let redirector = Redirector::new("api/v2/users").unwrap();
let output = format!("{redirector}");
assert!(output.contains("<!DOCTYPE HTML>"));
assert!(output.contains("/api/v2/users/"));
assert!(output.contains("meta http-equiv=\"refresh\""));
assert!(output.contains("window.location.href"));
}
#[test]
fn test_write_redirect_with_valid_path() {
let test_dir = format!(
"test_write_redirect_with_valid_path_{}",
Utc::now().timestamp_nanos_opt().unwrap_or(0)
);
let mut redirector = Redirector::new("some/path").unwrap();
redirector.set_path(&test_dir);
let result = redirector.write_redirect();
assert!(result.is_ok());
fs::remove_dir_all(&test_dir).ok();
}
#[test]
fn test_write_redirect_success() {
let test_dir = format!(
"test_write_redirect_success_{}",
Utc::now().timestamp_nanos_opt().unwrap_or(0)
);
let mut redirector = Redirector::new("some/path").unwrap();
redirector.set_path(&test_dir);
let result = redirector.write_redirect();
assert!(result.is_ok());
let file_path = result.unwrap();
assert!(Path::new(&file_path).exists());
let content = fs::read_to_string(&file_path).unwrap();
assert!(content.contains("<!DOCTYPE HTML>"));
assert!(content.contains("meta http-equiv=\"refresh\""));
assert!(content.contains("window.location.href"));
assert!(content.contains("If you are not redirected automatically"));
assert!(content.contains("/some/path/"));
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_write_redirect_creates_directory() {
let test_dir = format!(
"test_write_redirect_creates_directory_{}",
Utc::now().timestamp_nanos_opt().unwrap_or(0)
);
let subdir_path = format!("{test_dir}/subdir");
let mut redirector = Redirector::new("some/path").unwrap();
redirector.set_path(&subdir_path);
assert!(!Path::new(&test_dir).exists());
let result = redirector.write_redirect();
assert!(result.is_ok());
assert!(Path::new(&subdir_path).exists());
let file_path = result.unwrap();
assert!(Path::new(&file_path).exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_redirector_clone() {
let mut redirector = Redirector::new("some/path").unwrap();
redirector.set_path("custom");
let cloned = redirector.clone();
assert_eq!(redirector, cloned);
assert_eq!(redirector.long_path, cloned.long_path);
assert_eq!(redirector.short_file_name, cloned.short_file_name);
assert_eq!(redirector.path, cloned.path);
}
#[test]
fn test_redirector_default() {
let redirector = Redirector::default();
assert_eq!(redirector.long_path, UrlPath::default());
assert_eq!(redirector.path, PathBuf::new());
assert!(redirector.short_file_name.is_empty());
}
#[test]
fn test_write_redirect_returns_correct_path() {
let test_dir = format!(
"test_write_redirect_returns_correct_path_{}",
Utc::now().timestamp_nanos_opt().unwrap_or(0)
);
let mut redirector = Redirector::new("some/path").unwrap();
redirector.set_path(&test_dir);
let result = redirector.write_redirect();
assert!(result.is_ok());
let returned_path = result.unwrap();
let expected_path = redirector.path.join(&redirector.short_file_name);
assert_eq!(returned_path, expected_path.to_string_lossy());
assert!(Path::new(&returned_path).exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_write_redirect_registry_functionality() {
let test_dir = format!(
"test_write_redirect_registry_functionality_{}",
Utc::now().timestamp_nanos_opt().unwrap_or(0)
);
let mut redirector1 = Redirector::new("some/path").unwrap();
redirector1.set_path(&test_dir);
let mut redirector2 = Redirector::new("some/path").unwrap();
redirector2.set_path(&test_dir);
let result1 = redirector1.write_redirect();
assert!(result1.is_ok());
let path1 = result1.unwrap();
let result2 = redirector2.write_redirect();
assert!(result2.is_ok());
let path2 = result2.unwrap();
assert_eq!(path1, path2);
let registry_path = PathBuf::from(&test_dir).join("registry.json");
assert!(registry_path.exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_write_redirect_different_paths_different_files() {
let test_dir = format!(
"test_write_redirect_different_paths_different_files_{}",
Utc::now().timestamp_nanos_opt().unwrap_or(0)
);
let mut redirector1 = Redirector::new("some/path").unwrap();
redirector1.set_path(&test_dir);
let mut redirector2 = Redirector::new("other/path").unwrap();
redirector2.set_path(&test_dir);
let result1 = redirector1.write_redirect();
assert!(result1.is_ok());
let path1 = result1.unwrap();
let result2 = redirector2.write_redirect();
assert!(result2.is_ok());
let path2 = result2.unwrap();
assert_ne!(path1, path2);
assert!(Path::new(&path1).exists());
assert!(Path::new(&path2).exists());
fs::remove_dir_all(&test_dir).unwrap();
}
#[test]
fn test_new_redirector_error_handling() {
let result = Redirector::new("api");
assert!(result.is_ok());
let result = Redirector::new("");
assert!(result.is_err());
let result = Redirector::new("api?param=value");
assert!(result.is_err());
}
#[test]
fn test_generate_short_link_different_paths() {
let redirector1 = Redirector::new("api/v1").unwrap();
let redirector2 = Redirector::new("api/v2").unwrap();
assert_ne!(redirector1.short_file_name, redirector2.short_file_name);
}
#[test]
fn test_short_file_name_format() {
let redirector = Redirector::new("some/path").unwrap();
let file_name = redirector.short_file_name.to_string_lossy();
assert!(file_name.ends_with(".html"));
assert!(!file_name.is_empty());
}
#[test]
fn test_debug_and_partialeq_traits() {
let redirector1 = Redirector::new("some/path").unwrap();
let redirector2 = redirector1.clone();
assert_eq!(redirector1, redirector2);
let debug_output = format!("{redirector1:?}");
assert!(debug_output.contains("Redirector"));
}
}