diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ceeafb4..574737d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -43,16 +43,16 @@ "problemMatcher": "$rustc", "group": "build" }, - // { - // "label": "Run Unit Tests", - // "type": "cargo", - // "command": "test", - // "args": [ - // "--lib" - // ], - // "problemMatcher": "$rustc", - // "group": "test" - // }, + { + "label": "Run Unit Tests", + "type": "cargo", + "command": "test", + "args": [ + "--lib" + ], + "problemMatcher": "$rustc", + "group": "test" + }, // { // "label": "Run Integration Tests", // "type": "cargo", @@ -64,16 +64,16 @@ // "problemMatcher": "$rustc", // "group": "test" // }, - // { - // "label": "Run All Tests", - // "type": "shell", - // "command": "echo All Tests successful!", - // "dependsOn": [ - // "Run Unit Tests", - // "Run Integration Tests" - // ], - // "dependsOrder": "sequence", - // "group": "test" - // } + { + "label": "Run All Tests", + "type": "shell", + "command": "echo All Tests successful!", + "dependsOn": [ + "Run Unit Tests", + "Run Integration Tests" + ], + "dependsOrder": "sequence", + "group": "test" + } ], } \ No newline at end of file diff --git a/src/sharry/api.rs b/src/sharry/api.rs deleted file mode 100644 index afba37f..0000000 --- a/src/sharry/api.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::{fmt, sync::LazyLock}; - -use log::{debug, trace}; -use regex::Regex; -use serde::{Deserialize, Serialize}; - -use crate::error; - -#[derive(Serialize, Deserialize, Debug)] -pub struct Uri(String); - -impl fmt::Display for Uri { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } -} - -impl AsRef<[u8]> for Uri { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl Uri { - pub fn new(protocol: impl fmt::Display, base_url: impl fmt::Display) -> Self { - Self(format!("{protocol}://{base_url}")) - } - - fn endpoint(&self, path: fmt::Arguments) -> String { - let uri = format!("{}/api/v2/{path}", self.0); - trace!("endpoint: {uri:?}"); - uri - } - - pub fn share_create(&self) -> String { - self.endpoint(format_args!("alias/upload/new")) - } - - pub fn share_notify(&self, share_id: &str) -> String { - self.endpoint(format_args!("alias/mail/notify/{share_id}")) - } - - pub fn file_create(&self, share_id: &str) -> String { - self.endpoint(format_args!("alias/upload/{share_id}/files/tus")) - } - - pub fn file_patch(&self, share_id: &str, file_id: &FileID) -> String { - self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}")) - } -} - -// 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 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::unknown(format!( - "Could not extract File ID from {value:?}" - ))) - } - } -} - -// TODO move into tests subdir - -// #[cfg(test)] -// mod tests { -// use super::*; - -// #[test] -// fn test_get_file_id() { -// let good = "https://example.com/api/v2/alias/upload/SID123/files/tus/FID456"; -// let good = Client::get_file_id(good); -// assert!(good.is_ok()); -// assert_eq!(good.unwrap(), "FID456"); - -// let bad = "https://example.com/api/v2/alias/upload//files/tus/FID456"; // missing SID -// assert!(Client::get_file_id(bad).is_err()); - -// let bad: &'static str = "https://example.com/api/v2/alias/upload/SID123/files/tus/"; // missing FID -// assert!(Client::get_file_id(bad).is_err()); -// } -// } - -#[derive(Serialize, Debug)] -#[allow(non_snake_case)] -pub struct NewShareRequest { - name: String, - validity: u32, - description: Option, - maxViews: u32, - password: Option, -} - -impl NewShareRequest { - pub fn new( - name: impl Into, - description: Option>, - max_views: u32, - ) -> Self { - Self { - name: name.into(), - validity: 0, - description: description.map(Into::into), - maxViews: max_views, - password: None, - } - } -} - -#[derive(Deserialize, Debug)] -pub struct NewShareResponse { - pub success: bool, - pub message: String, - pub id: String, -} - -#[derive(Deserialize, Debug)] -#[allow(dead_code)] -pub struct NotifyShareResponse { - pub success: bool, - pub message: String, -} diff --git a/src/sharry/api/id.rs b/src/sharry/api/id.rs new file mode 100644 index 0000000..71b7610 --- /dev/null +++ b/src/sharry/api/id.rs @@ -0,0 +1,83 @@ +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::unknown(format!( + "Could not extract File ID from {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()); + } +} diff --git a/src/sharry/api/json.rs b/src/sharry/api/json.rs new file mode 100644 index 0000000..2f6f680 --- /dev/null +++ b/src/sharry/api/json.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Debug)] +#[allow(non_snake_case)] +pub struct NewShareRequest { + name: String, + validity: u32, + description: Option, + maxViews: u32, + password: Option, +} + +impl NewShareRequest { + pub fn new( + name: impl Into, + description: Option>, + max_views: u32, + ) -> Self { + Self { + name: name.into(), + validity: 0, + description: description.map(Into::into), + maxViews: max_views, + password: None, + } + } +} + +#[derive(Deserialize, Debug)] +pub struct NewShareResponse { + pub success: bool, + pub message: String, + pub id: String, +} + +#[derive(Deserialize, Debug)] +#[allow(dead_code)] +pub struct NotifyShareResponse { + pub success: bool, + pub message: String, +} diff --git a/src/sharry/api/mod.rs b/src/sharry/api/mod.rs new file mode 100644 index 0000000..5e17abf --- /dev/null +++ b/src/sharry/api/mod.rs @@ -0,0 +1,7 @@ +mod id; +mod json; +mod uri; + +pub use id::FileID; +pub use json::{NewShareRequest, NewShareResponse, NotifyShareResponse}; +pub use uri::Uri; diff --git a/src/sharry/api/uri.rs b/src/sharry/api/uri.rs new file mode 100644 index 0000000..e3c0c5f --- /dev/null +++ b/src/sharry/api/uri.rs @@ -0,0 +1,47 @@ +use std::fmt; + +use log::trace; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Uri(String); + +impl fmt::Display for Uri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl AsRef<[u8]> for Uri { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl Uri { + pub fn new(protocol: impl fmt::Display, base_url: impl fmt::Display) -> Self { + Self(format!("{protocol}://{base_url}")) + } + + fn endpoint(&self, path: fmt::Arguments) -> String { + let uri = format!("{}/api/v2/{path}", self.0); + trace!("endpoint: {uri:?}"); + uri + } + + pub fn share_create(&self) -> String { + self.endpoint(format_args!("alias/upload/new")) + } + + pub fn share_notify(&self, share_id: &str) -> String { + self.endpoint(format_args!("alias/mail/notify/{share_id}")) + } + + pub fn file_create(&self, share_id: &str) -> String { + self.endpoint(format_args!("alias/upload/{share_id}/files/tus")) + } + + pub fn file_patch(&self, share_id: &str, file_id: &super::FileID) -> String { + self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}")) + } +}