use-deno 0.0.1

Deno runtime metadata primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

use core::{fmt, str::FromStr};
use std::error::Error;

/// Deno version metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DenoVersion {
    major: u16,
    minor: Option<u16>,
    patch: Option<u16>,
}

impl DenoVersion {
    /// Creates Deno version metadata.
    ///
    /// # Errors
    ///
    /// Returns [`DenoVersionParseError::InvalidVersion`] when the major version is zero.
    pub const fn new(
        major: u16,
        minor: Option<u16>,
        patch: Option<u16>,
    ) -> Result<Self, DenoVersionParseError> {
        if major == 0 || (minor.is_none() && patch.is_some()) {
            Err(DenoVersionParseError::InvalidVersion)
        } else {
            Ok(Self {
                major,
                minor,
                patch,
            })
        }
    }

    /// Returns the major version.
    #[must_use]
    pub const fn major(self) -> u16 {
        self.major
    }
}

impl FromStr for DenoVersion {
    type Err = DenoVersionParseError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        parse_dotted_version(input)
    }
}

/// Error returned while parsing a Deno version.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DenoVersionParseError {
    Empty,
    InvalidVersion,
}

impl fmt::Display for DenoVersionParseError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("Deno version cannot be empty"),
            Self::InvalidVersion => formatter.write_str("invalid Deno version"),
        }
    }
}

impl Error for DenoVersionParseError {}

/// Deno permission labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DenoPermission {
    Read,
    Write,
    Net,
    Env,
    Run,
    Ffi,
    Sys,
}

impl DenoPermission {
    /// Returns the permission label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Read => "read",
            Self::Write => "write",
            Self::Net => "net",
            Self::Env => "env",
            Self::Run => "run",
            Self::Ffi => "ffi",
            Self::Sys => "sys",
        }
    }
}

impl fmt::Display for DenoPermission {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for DenoPermission {
    type Err = DenoPermissionParseError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        let trimmed = input.trim();
        if trimmed.is_empty() {
            return Err(DenoPermissionParseError::Empty);
        }
        match trimmed.to_ascii_lowercase().as_str() {
            "read" => Ok(Self::Read),
            "write" => Ok(Self::Write),
            "net" => Ok(Self::Net),
            "env" => Ok(Self::Env),
            "run" => Ok(Self::Run),
            "ffi" => Ok(Self::Ffi),
            "sys" => Ok(Self::Sys),
            _ => Err(DenoPermissionParseError::Unknown),
        }
    }
}

/// Error returned while parsing Deno permissions.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DenoPermissionParseError {
    Empty,
    Unknown,
}

impl fmt::Display for DenoPermissionParseError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("Deno permission cannot be empty"),
            Self::Unknown => formatter.write_str("unknown Deno permission"),
        }
    }
}

impl Error for DenoPermissionParseError {}

/// Common Deno config file labels.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum DenoConfigFile {
    DenoJson,
    DenoJsonc,
}

impl DenoConfigFile {
    /// Returns the file name label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::DenoJson => "deno.json",
            Self::DenoJsonc => "deno.jsonc",
        }
    }
}

fn parse_dotted_version(input: &str) -> Result<DenoVersion, DenoVersionParseError> {
    let trimmed = input.trim().trim_start_matches('v');
    if trimmed.is_empty() {
        return Err(DenoVersionParseError::Empty);
    }
    let parts = trimmed.split('.').collect::<Vec<_>>();
    if parts.len() > 3 || parts.iter().any(|part| part.is_empty()) {
        return Err(DenoVersionParseError::InvalidVersion);
    }
    let major = parse_part(parts[0])?;
    let minor = parts.get(1).copied().map(parse_part).transpose()?;
    let patch = parts.get(2).copied().map(parse_part).transpose()?;
    DenoVersion::new(major, minor, patch)
}

fn parse_part(input: &str) -> Result<u16, DenoVersionParseError> {
    input
        .parse::<u16>()
        .map_err(|_error| DenoVersionParseError::InvalidVersion)
}

#[cfg(test)]
mod tests {
    use super::{DenoConfigFile, DenoPermission, DenoVersion};

    #[test]
    fn parses_deno_metadata() -> Result<(), Box<dyn std::error::Error>> {
        let version: DenoVersion = "1.42.0".parse()?;
        assert_eq!(version.major(), 1);
        assert_eq!("net".parse::<DenoPermission>()?, DenoPermission::Net);
        assert_eq!(DenoConfigFile::DenoJsonc.as_str(), "deno.jsonc");
        Ok(())
    }
}