use crate::labs::{LabClient, LabsClient};
use crate::utils::validate_name;
use crate::{Client, Error, Result};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Serialize, Deserialize)]
pub struct Folder {
pub folders: Vec<FolderEntry>,
pub labs: Vec<LabEntry>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FolderEntry {
pub name: String,
pub path: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LabEntry {
#[serde(rename = "file")]
pub filename: String,
pub mtime: String,
pub path: String,
pub umtime: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct FolderPath(Arc<str>);
impl std::fmt::Display for FolderPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl FolderPath {
pub(crate) fn new(path: impl AsRef<str>) -> Result<Self> {
let path = path.as_ref();
Self::validate(path)?;
Ok(Self(Arc::from(path)))
}
fn validate(path: &str) -> Result<()> {
if path.is_empty() {
return Err(Error::Client("Path cannot be empty".to_string()));
}
if !path.starts_with("/") {
return Err(Error::Client("Path must be an absolute path".to_string()));
}
for segment in path.split("/") {
Self::validate_segment(segment)?;
}
Ok(())
}
fn validate_segment(name: &str) -> Result<()> {
if !validate_name(name, &['-', '_', ' ']) {
return Err(Error::Client(format!(
"Invalid folder segment '{}', must only contain letters, digits, spaces and '-'/'_'",
name
)));
}
Ok(())
}
pub(crate) fn from_parts(parent: impl AsRef<str>, leaf: impl AsRef<str>) -> Result<Self> {
Self::new(Self::join(parent.as_ref(), leaf.as_ref()))
}
pub(crate) fn from_str(path: &str) -> Self {
Self(Arc::from(path))
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn parent(&self) -> &str {
self.0
.rsplit_once("/")
.map(|(p, _)| p)
.filter(|p| !p.is_empty())
.unwrap_or("/")
}
pub(crate) fn leaf(&self) -> &str {
self.0.rsplit("/").next().unwrap()
}
pub(crate) fn rename(&self, name: &str) -> Result<Self> {
Self::validate_segment(name)?;
Ok(Self::from_str(&Self::join(self.parent(), name)))
}
pub(crate) fn move_to(&self, path: &FolderPath) -> Self {
Self::from_str(&Self::join(path.as_str(), self.leaf()))
}
fn join(parent: &str, leaf: &str) -> String {
format!("{}/{}", parent.trim_end_matches("/"), leaf)
}
}
pub struct FolderClient {
client: Client,
pub(crate) path: FolderPath,
}
impl FolderClient {
pub(crate) fn new(client: Client, path: impl AsRef<str>) -> Result<Self> {
Ok(Self {
client,
path: FolderPath::new(path.as_ref())?,
})
}
pub async fn list(&self) -> Result<Folder> {
self.client
.get(&format!("folders{}", self.path))
.await?
.into_data()
}
pub async fn add(&self, name: impl Into<String>) -> Result<FolderClient> {
let name = name.into();
let path = FolderPath::from_parts(self.path.as_str(), &name)?;
let params = FolderEntry {
name,
path: self.path.as_str().into(),
};
self.client
.post::<(), FolderEntry>("folders", ¶ms)
.await?;
Ok(FolderClient {
client: self.client.clone(),
path,
})
}
pub async fn rename(self, name: impl AsRef<str>) -> Result<FolderClient> {
let name = name.as_ref();
let new_path = self.path.rename(name)?;
self.edit(new_path.as_str()).await?;
Ok(FolderClient {
client: self.client.clone(),
path: new_path,
})
}
pub async fn move_to(self, folder: &FolderClient) -> Result<FolderClient> {
let new_path = self.path.move_to(&folder.path);
self.edit(new_path.as_str()).await?;
Ok(FolderClient {
client: self.client.clone(),
path: new_path,
})
}
async fn edit(&self, path: &str) -> Result<()> {
let params = serde_json::json!({"path": path});
self.client
.put::<(), serde_json::Value>(&format!("folders{}", self.path), ¶ms)
.await?;
Ok(())
}
pub async fn delete(self) -> Result<()> {
self.client
.delete::<()>(&format!("folders{}", self.path))
.await?;
Ok(())
}
pub fn labs(&self) -> LabsClient {
LabsClient::new(self.client.clone(), self.path.clone())
}
pub fn lab(&self, name: impl AsRef<str>) -> Result<LabClient> {
LabClient::new(self.client.clone(), self.path.clone(), name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_folder_path() -> Result<()> {
let path = FolderPath::new("/New Folder")?;
assert_eq!(path.parent(), "/");
assert_eq!(path.leaf(), "New Folder");
Ok(())
}
#[test]
fn invalid_folder_path() -> Result<()> {
let path = FolderPath::new("New Folder");
assert!(path.is_err());
let path = FolderPath::new("/New+Folder");
assert!(path.is_err());
Ok(())
}
#[test]
fn folder_rename() -> Result<()> {
let path = FolderPath::new("/New Folder")?;
let new_path = path.rename("Test Folder")?;
assert_eq!(new_path.parent(), "/");
assert_eq!(new_path.leaf(), "Test Folder");
Ok(())
}
#[test]
fn folder_move() -> Result<()> {
let path = FolderPath::new("/New Folder")?;
let new_folder = FolderPath::new("/Test Folder")?;
let new_path = path.move_to(&new_folder);
assert_eq!(new_path.as_str(), "/Test Folder/New Folder");
assert_eq!(new_path.parent(), "/Test Folder");
assert_eq!(new_path.leaf(), "New Folder");
Ok(())
}
}