shrupl/src/sharry/uri.rs
2025-07-03 11:07:32 +00:00

146 lines
4.2 KiB
Rust

use std::fmt;
use log::trace;
use serde::{Deserialize, Serialize};
/// ID of a file in a Sharry share
///
/// - impl `Clone` as this is just a String
/// - impl `serde` for cachefile handling
/// - impl `Display` for formatting compatibility
/// - impl `AsRef<[u8]>` for hashing with `blake2b_simd`
#[derive(Debug, Clone, Serialize, Deserialize)]
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 {
/// create a new Sharry URI
pub fn new(scheme: impl fmt::Display, host: impl fmt::Display) -> Self {
Self(format!("{scheme}://{host}"))
}
/// arbitrary endpoint in the Sharry API v2
fn endpoint(&self, path: fmt::Arguments) -> String {
let uri = format!("{}/api/v2/{path}", self.0);
trace!("endpoint: {uri:?}");
uri
}
/// Sharry API endpoint to create a new share
pub fn share_create(&self) -> String {
self.endpoint(format_args!("alias/upload/new"))
}
/// Sharry API endpoint to ping a share's notification hook
pub fn share_notify(&self, share_id: &super::ShareID) -> String {
self.endpoint(format_args!("alias/mail/notify/{share_id}"))
}
/// Sharry API endpoint to create a new file inside a share
pub fn file_create(&self, share_id: &super::ShareID) -> String {
self.endpoint(format_args!("alias/upload/{share_id}/files/tus"))
}
/// Sharry API endpoint to push data into a file inside a share
pub fn file_patch(&self, share_id: &super::ShareID, file_id: &super::FileID) -> String {
self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
check_trait,
sharry::{FileID, ShareID},
};
#[test]
fn basic_traits_working() {
let cases = vec![
// simple http host
"http://example.com",
// https host with port
"https://my-host:8080",
// custom scheme
"custom+scheme://host",
];
for uri_data in cases {
let uri = Uri(uri_data.to_owned());
check_trait(&uri_data, &uri.to_string(), "Display", "Uri");
check_trait(&uri_data.as_bytes(), &uri.as_ref(), "AsRef<[u8]>", "Uri");
}
}
#[test]
fn test_new() {
let cases = vec![
// simple http host
("http", "example.com", "http://example.com"),
// https host with port
("https", "my-host:8080", "https://my-host:8080"),
// custom scheme
("custom+scheme", "host", "custom+scheme://host"),
];
for (scheme, host, expected) in cases {
let uri = Uri::new(scheme, host);
assert_eq!(&expected, &uri.to_string());
}
}
#[test]
fn test_endpoint() {
let cases = vec![
// simple path
("path/to/something", "/api/v2/path/to/something"),
// underscores, hyphens, dots
("bob_smith-son.eve", "/api/v2/bob_smith-son.eve"),
// unicode
("漢字ユーザー", "/api/v2/漢字ユーザー"),
// empty path
("", "/api/v2/"),
// leading/trailing spaces
(" frank ", "/api/v2/ frank "),
// uppercase
("GUEST", "/api/v2/GUEST"),
// numeric
("12345", "/api/v2/12345"),
];
let uri = Uri("".to_owned());
for (path, expected) in cases {
assert_eq!(&expected, &uri.endpoint(format_args!("{path}")));
}
}
#[test]
fn test_pub_endpoints() {
let uri = Uri("".to_owned());
let share_id = ShareID("sid".to_owned());
let file_id = FileID("fid".to_owned());
assert_eq!("/api/v2/alias/upload/new", uri.share_create());
assert_eq!("/api/v2/alias/mail/notify/sid", uri.share_notify(&share_id));
assert_eq!(
"/api/v2/alias/upload/sid/files/tus",
uri.file_create(&share_id)
);
assert_eq!(
"/api/v2/alias/upload/sid/files/tus/fid",
uri.file_patch(&share_id, &file_id)
);
}
}