eveng 0.1.1

An API Client for EVE-NG
Documentation
//! Types and clients for managing folders on the EVE-NG instance.

use crate::labs::{LabClient, LabsClient};
use crate::utils::validate_name;
use crate::{Client, Error, Result};

use serde::{Deserialize, Serialize};
use std::sync::Arc;

/// Type to describe a folder listing.
#[derive(Debug, Serialize, Deserialize)]
pub struct Folder {
    /// List of folders within the folder.
    pub folders: Vec<FolderEntry>,
    /// List of labs within the folder.
    pub labs: Vec<LabEntry>,
}

/// Type to describe a folder entry in a folder listing.
#[derive(Debug, Serialize, Deserialize)]
pub struct FolderEntry {
    /// Name of the folder.
    pub name: String,
    /// Absolute path of the folder.
    pub path: String,
}

/// Type to describe a lab entry in a folder listing.
#[derive(Debug, Serialize, Deserialize)]
pub struct LabEntry {
    /// Name of the lab file without the path (eg: `Test.unl`).
    #[serde(rename = "file")]
    pub filename: String,
    /// Modification time of the lab.
    pub mtime: String,
    /// Absolute path of the lab (eg: `/Test.unl`).
    pub path: String,
    /// Modification time of the lab, as a UNIX timestamp.
    pub umtime: u64,
}

/// Newtype to manage the path to a folder.
#[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(())
    }

    // Creating from segments rather than the full path
    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()))
    }

    // For cases where the path is already valid (eg: from an API response)
    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> {
        // Only leaf needs to be validated
        Self::validate_segment(name)?;
        Ok(Self::from_str(&Self::join(self.parent(), name)))
    }

    pub(crate) fn move_to(&self, path: &FolderPath) -> Self {
        // Both parts are already validated
        Self::from_str(&Self::join(path.as_str(), self.leaf()))
    }

    fn join(parent: &str, leaf: &str) -> String {
        format!("{}/{}", parent.trim_end_matches("/"), leaf)
    }
}

/// A client for managing a single folder.
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())?,
        })
    }

    /// Lists the contents of the folder.
    pub async fn list(&self) -> Result<Folder> {
        self.client
            .get(&format!("folders{}", self.path))
            .await?
            .into_data()
    }

    /// Add a folder in the current folder.
    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", &params)
            .await?;

        Ok(FolderClient {
            client: self.client.clone(),
            path,
        })
    }

    /// Renames the folder.
    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,
        })
    }

    /// Moves the folder to the specified 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), &params)
            .await?;

        Ok(())
    }

    /// Deletes the folder.
    pub async fn delete(self) -> Result<()> {
        self.client
            .delete::<()>(&format!("folders{}", self.path))
            .await?;
        Ok(())
    }

    /// Returns a client for managing labs.
    pub fn labs(&self) -> LabsClient {
        LabsClient::new(self.client.clone(), self.path.clone())
    }

    /// Returns a client for managing a single lab.
    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(())
    }
}