use-go-import 0.0.1

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

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

use use_go_identifier::is_valid_ascii_go_identifier;

/// Error returned by Go import metadata constructors.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GoImportError {
    EmptyPath,
    InvalidPath,
    EmptyAlias,
    InvalidAlias,
    UnknownLabel,
}

impl fmt::Display for GoImportError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyPath => formatter.write_str("Go import path cannot be empty"),
            Self::InvalidPath => formatter.write_str("invalid Go import path"),
            Self::EmptyAlias => formatter.write_str("Go import alias cannot be empty"),
            Self::InvalidAlias => formatter.write_str("invalid Go import alias"),
            Self::UnknownLabel => formatter.write_str("unknown Go import metadata label"),
        }
    }
}

impl Error for GoImportError {}

/// Validated Go import path metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoImportPath(String);

impl GoImportPath {
    /// Creates a Go import path from non-empty text.
    ///
    /// # Errors
    ///
    /// Returns [`GoImportError`] when the path is empty or contains obvious whitespace.
    pub fn new(value: impl AsRef<str>) -> Result<Self, GoImportError> {
        let trimmed = value.as_ref().trim();
        if trimmed.is_empty() {
            return Err(GoImportError::EmptyPath);
        }
        if trimmed.chars().any(char::is_whitespace) || trimmed.split('/').any(str::is_empty) {
            return Err(GoImportError::InvalidPath);
        }
        Ok(Self(trimmed.to_string()))
    }

    /// Returns the import path.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Returns whether this import path is relative.
    #[must_use]
    pub fn is_relative(&self) -> bool {
        self.0.starts_with("./") || self.0.starts_with("../")
    }

    /// Consumes the path and returns the owned text.
    #[must_use]
    pub fn into_string(self) -> String {
        self.0
    }
}

impl AsRef<str> for GoImportPath {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

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

impl FromStr for GoImportPath {
    type Err = GoImportError;

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

impl TryFrom<&str> for GoImportPath {
    type Error = GoImportError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

/// Validated Go import alias metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoImportAlias(String);

impl GoImportAlias {
    /// Creates an import alias from `_`, `.`, or an ASCII Go identifier.
    ///
    /// # Errors
    ///
    /// Returns [`GoImportError`] when the alias is empty or invalid.
    pub fn new(value: impl AsRef<str>) -> Result<Self, GoImportError> {
        let trimmed = value.as_ref().trim();
        if trimmed.is_empty() {
            return Err(GoImportError::EmptyAlias);
        }
        if trimmed != "_" && trimmed != "." && !is_valid_ascii_go_identifier(trimmed) {
            return Err(GoImportError::InvalidAlias);
        }
        Ok(Self(trimmed.to_string()))
    }

    /// Returns the alias text.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Returns whether this is the blank import alias.
    #[must_use]
    pub fn is_blank(&self) -> bool {
        self.0 == "_"
    }

    /// Returns whether this is the dot import alias.
    #[must_use]
    pub fn is_dot(&self) -> bool {
        self.0 == "."
    }
}

impl AsRef<str> for GoImportAlias {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

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

impl FromStr for GoImportAlias {
    type Err = GoImportError;

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

impl TryFrom<&str> for GoImportAlias {
    type Error = GoImportError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

/// Go import kind metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoImportKind {
    StandardLibrary,
    ThirdParty,
    Internal,
    Relative,
    Blank,
    Dot,
    Aliased,
}

impl GoImportKind {
    /// Returns the import kind label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::StandardLibrary => "standard-library",
            Self::ThirdParty => "third-party",
            Self::Internal => "internal",
            Self::Relative => "relative",
            Self::Blank => "blank",
            Self::Dot => "dot",
            Self::Aliased => "aliased",
        }
    }
}

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

impl FromStr for GoImportKind {
    type Err = GoImportError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match normalized_label(value)?.as_str() {
            "standard-library" | "standard_library" | "standard library" => {
                Ok(Self::StandardLibrary)
            }
            "third-party" | "third_party" | "third party" => Ok(Self::ThirdParty),
            "internal" => Ok(Self::Internal),
            "relative" => Ok(Self::Relative),
            "blank" => Ok(Self::Blank),
            "dot" => Ok(Self::Dot),
            "aliased" => Ok(Self::Aliased),
            _ => Err(GoImportError::UnknownLabel),
        }
    }
}

/// Go import group metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoImportGroup {
    StandardLibrary,
    External,
    Internal,
    Local,
}

impl GoImportGroup {
    /// Returns the import group label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::StandardLibrary => "standard-library",
            Self::External => "external",
            Self::Internal => "internal",
            Self::Local => "local",
        }
    }
}

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

impl FromStr for GoImportGroup {
    type Err = GoImportError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match normalized_label(value)?.as_str() {
            "standard-library" | "standard_library" | "standard library" => {
                Ok(Self::StandardLibrary)
            }
            "external" => Ok(Self::External),
            "internal" => Ok(Self::Internal),
            "local" => Ok(Self::Local),
            _ => Err(GoImportError::UnknownLabel),
        }
    }
}

/// Go import specification metadata.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GoImportSpec {
    path: GoImportPath,
    alias: Option<GoImportAlias>,
    kind: GoImportKind,
}

impl GoImportSpec {
    /// Creates import specification metadata.
    #[must_use]
    pub const fn new(path: GoImportPath, kind: GoImportKind) -> Self {
        Self {
            path,
            alias: None,
            kind,
        }
    }

    /// Adds an import alias.
    #[must_use]
    pub fn with_alias(mut self, alias: GoImportAlias) -> Self {
        self.alias = Some(alias);
        self
    }

    /// Returns the import path.
    #[must_use]
    pub const fn path(&self) -> &GoImportPath {
        &self.path
    }

    /// Returns the import alias.
    #[must_use]
    pub const fn alias(&self) -> Option<&GoImportAlias> {
        self.alias.as_ref()
    }

    /// Returns the import kind.
    #[must_use]
    pub const fn kind(&self) -> GoImportKind {
        self.kind
    }
}

fn normalized_label(value: &str) -> Result<String, GoImportError> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        Err(GoImportError::UnknownLabel)
    } else {
        Ok(trimmed.to_ascii_lowercase())
    }
}

#[cfg(test)]
mod tests {
    use super::{
        GoImportAlias, GoImportError, GoImportGroup, GoImportKind, GoImportPath, GoImportSpec,
    };

    #[test]
    fn validates_import_paths() -> Result<(), GoImportError> {
        let path = GoImportPath::new("net/http")?;
        assert_eq!(path.as_str(), "net/http");
        assert!(!path.is_relative());
        assert!(GoImportPath::new("../internal").is_ok_and(|value| value.is_relative()));
        assert_eq!(GoImportPath::new(""), Err(GoImportError::EmptyPath));
        assert_eq!(
            GoImportPath::new("net//http"),
            Err(GoImportError::InvalidPath)
        );
        assert_eq!(
            GoImportPath::new("net/http client"),
            Err(GoImportError::InvalidPath)
        );
        Ok(())
    }

    #[test]
    fn validates_import_aliases() -> Result<(), GoImportError> {
        let blank = GoImportAlias::new("_")?;
        let dot = GoImportAlias::new(".")?;
        let named = GoImportAlias::new("httpx")?;

        assert!(blank.is_blank());
        assert!(dot.is_dot());
        assert_eq!(named.as_str(), "httpx");
        assert_eq!(GoImportAlias::new(""), Err(GoImportError::EmptyAlias));
        assert_eq!(
            GoImportAlias::new("bad-alias"),
            Err(GoImportError::InvalidAlias)
        );
        Ok(())
    }

    #[test]
    fn parses_import_enums() -> Result<(), GoImportError> {
        assert_eq!(
            "third party".parse::<GoImportKind>()?,
            GoImportKind::ThirdParty
        );
        assert_eq!(
            "standard_library".parse::<GoImportGroup>()?,
            GoImportGroup::StandardLibrary
        );
        assert_eq!(GoImportKind::Aliased.to_string(), "aliased");
        Ok(())
    }

    #[test]
    fn models_import_specs() -> Result<(), GoImportError> {
        let spec = GoImportSpec::new(GoImportPath::new("net/http")?, GoImportKind::Aliased)
            .with_alias(GoImportAlias::new("httpx")?);

        assert_eq!(spec.path().as_str(), "net/http");
        assert_eq!(spec.kind(), GoImportKind::Aliased);
        assert_eq!(spec.alias().map(GoImportAlias::as_str), Some("httpx"));
        Ok(())
    }
}