split api.rs into modules

- `id` for multiple "ID" types
- `json` for types directly interacting with the Sharry API
- `uri` for the `Uri` type
- activate testing
This commit is contained in:
Jörn-Michael Miehe 2025-06-25 23:42:00 +00:00
parent f1c6eb5d75
commit c9c21aa128
6 changed files with 199 additions and 183 deletions

42
.vscode/tasks.json vendored
View file

@ -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"
}
],
}

View file

@ -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<String> for FileID {
type Error = error::Error;
fn try_from(value: String) -> error::Result<Self> {
/// 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<fid>[^/]+)` capture FID (one or more non-slash chars)
/// - `$` end of string
static UPLOAD_URL_RE: LazyLock<Regex> = LazyLock::new(|| {
trace!("compiling UPLOAD_URL_RE");
Regex::new(
r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P<fid>[^/]+)$",
)
.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<String>,
maxViews: u32,
password: Option<String>,
}
impl NewShareRequest {
pub fn new(
name: impl Into<String>,
description: Option<impl Into<String>>,
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,
}

83
src/sharry/api/id.rs Normal file
View file

@ -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<str> for FileID {
fn as_ref(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for FileID {
type Error = error::Error;
fn try_from(value: String) -> error::Result<Self> {
/// 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<fid>[^/]+)` capture FID (one or more non-slash chars)
/// - `$` end of string
static UPLOAD_URL_RE: LazyLock<Regex> = LazyLock::new(|| {
trace!("compiling UPLOAD_URL_RE");
Regex::new(
r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P<fid>[^/]+)$",
)
.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());
}
}

41
src/sharry/api/json.rs Normal file
View file

@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)]
#[allow(non_snake_case)]
pub struct NewShareRequest {
name: String,
validity: u32,
description: Option<String>,
maxViews: u32,
password: Option<String>,
}
impl NewShareRequest {
pub fn new(
name: impl Into<String>,
description: Option<impl Into<String>>,
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,
}

7
src/sharry/api/mod.rs Normal file
View file

@ -0,0 +1,7 @@
mod id;
mod json;
mod uri;
pub use id::FileID;
pub use json::{NewShareRequest, NewShareResponse, NotifyShareResponse};
pub use uri::Uri;

47
src/sharry/api/uri.rs Normal file
View file

@ -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}"))
}
}