use-go-identifier 0.0.1

ASCII-safe Go identifier primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

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

/// Error returned when an ASCII Go identifier is invalid.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GoIdentifierError {
    Empty,
    InvalidStart { character: char },
    InvalidContinue { index: usize, character: char },
    NotExported,
    NotUnexported,
}

impl fmt::Display for GoIdentifierError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("Go identifier cannot be empty"),
            Self::InvalidStart { character } => {
                write!(formatter, "invalid Go identifier start `{character}`")
            }
            Self::InvalidContinue { index, character } => write!(
                formatter,
                "invalid Go identifier continuation `{character}` at byte index {index}"
            ),
            Self::NotExported => formatter.write_str("Go identifier is not exported"),
            Self::NotUnexported => formatter.write_str("Go identifier is not unexported"),
        }
    }
}

impl Error for GoIdentifierError {}

/// Validated ASCII-safe Go identifier.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoIdentifier(String);

impl GoIdentifier {
    /// Creates an ASCII-safe Go identifier.
    ///
    /// # Errors
    ///
    /// Returns [`GoIdentifierError`] when `value` is empty or not ASCII identifier-shaped.
    pub fn new(value: impl Into<String>) -> Result<Self, GoIdentifierError> {
        let value = value.into();
        validate_ascii_go_identifier(&value)?;
        Ok(Self(value))
    }

    /// Returns the identifier as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

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

    /// Returns whether this identifier is exported by ASCII convention.
    #[must_use]
    pub fn is_exported(&self) -> bool {
        is_exported_go_identifier(self.as_str())
    }

    /// Returns whether this identifier is unexported by ASCII convention.
    #[must_use]
    pub fn is_unexported(&self) -> bool {
        is_unexported_go_identifier(self.as_str())
    }
}

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

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

impl FromStr for GoIdentifier {
    type Err = GoIdentifierError;

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

impl TryFrom<&str> for GoIdentifier {
    type Error = GoIdentifierError;

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

/// Validated exported ASCII-safe Go identifier.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoExportedIdentifier(GoIdentifier);

impl GoExportedIdentifier {
    /// Creates an exported Go identifier.
    ///
    /// # Errors
    ///
    /// Returns [`GoIdentifierError`] when `value` is not a valid ASCII Go identifier or is not exported.
    pub fn new(value: impl Into<String>) -> Result<Self, GoIdentifierError> {
        let identifier = GoIdentifier::new(value)?;
        if identifier.is_exported() {
            Ok(Self(identifier))
        } else {
            Err(GoIdentifierError::NotExported)
        }
    }

    /// Returns the identifier as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }

    /// Consumes the wrapper and returns the general identifier.
    #[must_use]
    pub fn into_identifier(self) -> GoIdentifier {
        self.0
    }
}

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

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

impl FromStr for GoExportedIdentifier {
    type Err = GoIdentifierError;

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

impl TryFrom<&str> for GoExportedIdentifier {
    type Error = GoIdentifierError;

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

/// Validated unexported ASCII-safe Go identifier.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoUnexportedIdentifier(GoIdentifier);

impl GoUnexportedIdentifier {
    /// Creates an unexported Go identifier.
    ///
    /// # Errors
    ///
    /// Returns [`GoIdentifierError`] when `value` is not a valid ASCII Go identifier or is not unexported.
    pub fn new(value: impl Into<String>) -> Result<Self, GoIdentifierError> {
        let identifier = GoIdentifier::new(value)?;
        if identifier.is_unexported() {
            Ok(Self(identifier))
        } else {
            Err(GoIdentifierError::NotUnexported)
        }
    }

    /// Returns the identifier as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }

    /// Consumes the wrapper and returns the general identifier.
    #[must_use]
    pub fn into_identifier(self) -> GoIdentifier {
        self.0
    }
}

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

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

impl FromStr for GoUnexportedIdentifier {
    type Err = GoIdentifierError;

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

impl TryFrom<&str> for GoUnexportedIdentifier {
    type Error = GoIdentifierError;

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

/// Returns whether `character` is accepted as an ASCII Go identifier start.
#[must_use]
pub const fn is_ascii_go_identifier_start(character: char) -> bool {
    character == '_' || character.is_ascii_alphabetic()
}

/// Returns whether `character` is accepted after the first identifier character.
#[must_use]
pub const fn is_ascii_go_identifier_continue(character: char) -> bool {
    is_ascii_go_identifier_start(character) || character.is_ascii_digit()
}

/// Returns whether `value` is an ASCII-safe Go identifier.
#[must_use]
pub fn is_valid_ascii_go_identifier(value: &str) -> bool {
    validate_ascii_go_identifier(value).is_ok()
}

/// Returns whether `value` is an exported ASCII-safe Go identifier.
#[must_use]
pub fn is_exported_go_identifier(value: &str) -> bool {
    first_identifier_char(value).is_some_and(|character| character.is_ascii_uppercase())
        && is_valid_ascii_go_identifier(value)
}

/// Returns whether `value` is an unexported ASCII-safe Go identifier.
#[must_use]
pub fn is_unexported_go_identifier(value: &str) -> bool {
    first_identifier_char(value)
        .is_some_and(|character| character == '_' || character.is_ascii_lowercase())
        && is_valid_ascii_go_identifier(value)
}

fn first_identifier_char(value: &str) -> Option<char> {
    value.chars().next()
}

fn validate_ascii_go_identifier(value: &str) -> Result<(), GoIdentifierError> {
    let mut characters = value.char_indices();
    let Some((_, first)) = characters.next() else {
        return Err(GoIdentifierError::Empty);
    };

    if !is_ascii_go_identifier_start(first) {
        return Err(GoIdentifierError::InvalidStart { character: first });
    }

    for (index, character) in characters {
        if !is_ascii_go_identifier_continue(character) {
            return Err(GoIdentifierError::InvalidContinue { index, character });
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{
        is_exported_go_identifier, is_unexported_go_identifier, is_valid_ascii_go_identifier,
        GoExportedIdentifier, GoIdentifier, GoIdentifierError, GoUnexportedIdentifier,
    };

    #[test]
    fn accepts_ascii_identifiers() -> Result<(), GoIdentifierError> {
        let identifier = GoIdentifier::new("ServeHTTP")?;
        assert_eq!(identifier.as_str(), "ServeHTTP");
        assert!(identifier.is_exported());
        assert!(is_valid_ascii_go_identifier("handler_1"));
        assert!(is_valid_ascii_go_identifier("_internal"));
        Ok(())
    }

    #[test]
    fn distinguishes_exported_and_unexported_identifiers() -> Result<(), GoIdentifierError> {
        let exported = GoExportedIdentifier::new("Client")?;
        let unexported = GoUnexportedIdentifier::new("_client")?;

        assert_eq!(exported.as_str(), "Client");
        assert_eq!(unexported.as_str(), "_client");
        assert!(is_exported_go_identifier("Client"));
        assert!(is_unexported_go_identifier("client"));
        assert_eq!(
            GoExportedIdentifier::new("client"),
            Err(GoIdentifierError::NotExported)
        );
        assert_eq!(
            GoUnexportedIdentifier::new("Client"),
            Err(GoIdentifierError::NotUnexported)
        );
        Ok(())
    }

    #[test]
    fn rejects_invalid_identifiers() {
        assert_eq!(GoIdentifier::new(""), Err(GoIdentifierError::Empty));
        assert_eq!(
            GoIdentifier::new("1value"),
            Err(GoIdentifierError::InvalidStart { character: '1' })
        );
        assert!(!is_valid_ascii_go_identifier("has-dash"));
        assert!(!is_valid_ascii_go_identifier("has space"));
        assert!(!is_valid_ascii_go_identifier("π"));
    }
}