use std::{fmt, sync::LazyLock}; use log::{debug, trace}; use regex::Regex; use serde::{Deserialize, Serialize}; use crate::error; // pub struct AliasID(String); // pub struct ShareID(String); #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FileID(String); impl fmt::Display for FileID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.0) } } impl AsRef for FileID { fn as_ref(&self) -> &str { &self.0 } } impl TryFrom for FileID { type Error = error::Error; fn try_from(value: String) -> error::Result { /// Pattern breakdown: /// - `^([^:/?#]+)://` – scheme (anything but `:/?#`) + `"://"` /// - `([^/?#]+)` – authority/host (anything but `/?#`) /// - `/api/v2/alias/upload/` – literal path segment /// - `([^/]+)` – capture SID (one or more non-slash chars) /// - `/files/tus/` – literal path segment /// - `(?P[^/]+)` – capture FID (one or more non-slash chars) /// - `$` – end of string static UPLOAD_URL_RE: LazyLock = LazyLock::new(|| { trace!("compiling UPLOAD_URL_RE"); Regex::new( r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P[^/]+)$", ) .expect("Regex compilation failed") }); trace!("TryFrom {value:?}"); if let Some(fid) = UPLOAD_URL_RE .captures(&value) .and_then(|caps| caps.name("fid").map(|m| m.as_str())) { let result = Self(fid.to_owned()); debug!("{result:?}"); Ok(result) } else { Err(error::Error::mismatch( ":///api/v2/alias/upload//files/tus/", value, )) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_fileid_tryfrom_string() { let good = "https://example.com/api/v2/alias/upload/SID123/files/tus/FID456".to_owned(); let good = FileID::try_from(good); assert!(good.is_ok()); assert_eq!(good.unwrap().as_ref(), "FID456"); let bad = "https://example.com/api/v2/alias/upload//files/tus/FID456".to_owned(); // missing SID assert!(FileID::try_from(bad).is_err()); let bad = "https://example.com/api/v2/alias/upload/SID123/files/tus/".to_owned(); // missing FID assert!(FileID::try_from(bad).is_err()); } }