use-go-value 0.0.1

Go-like primitive value metadata for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

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

/// Error returned when parsing Go value vocabulary fails.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GoValueParseError {
    Empty,
    Unknown,
}

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

impl Error for GoValueParseError {}

/// Primitive Go-like value metadata.
#[derive(Clone, Debug, PartialEq)]
pub enum GoPrimitiveValue {
    Nil,
    Bool(bool),
    Int(String),
    Float(f64),
    Complex { real: f64, imag: f64 },
    Rune(char),
    String(String),
}

impl GoPrimitiveValue {
    /// Returns a Go-like primitive type label.
    #[must_use]
    pub const fn type_name(&self) -> &'static str {
        match self {
            Self::Nil => "nil",
            Self::Bool(_) => "bool",
            Self::Int(_) => "int",
            Self::Float(_) => "float",
            Self::Complex { .. } => "complex",
            Self::Rune(_) => "rune",
            Self::String(_) => "string",
        }
    }

    /// Returns whether this value is nil.
    #[must_use]
    pub const fn is_nil(&self) -> bool {
        matches!(self, Self::Nil)
    }

    /// Returns whether this value is numeric metadata.
    #[must_use]
    pub const fn is_numeric(&self) -> bool {
        matches!(self, Self::Int(_) | Self::Float(_) | Self::Complex { .. })
    }

    /// Returns whether this value is zero-like for metadata purposes.
    #[must_use]
    pub fn is_zero_like(&self) -> bool {
        match self {
            Self::Nil => true,
            Self::Bool(value) => !value,
            Self::Int(value) => is_zero_integer_text(value),
            Self::Float(value) => *value == 0.0,
            Self::Complex { real, imag } => *real == 0.0 && *imag == 0.0,
            Self::Rune(value) => *value == '\0',
            Self::String(value) => value.is_empty(),
        }
    }
}

/// Primitive numeric Go-like value metadata.
#[derive(Clone, Debug, PartialEq)]
pub enum GoNumericValue {
    Int(String),
    Float(f64),
    Complex { real: f64, imag: f64 },
}

impl GoNumericValue {
    /// Returns a Go-like numeric type label.
    #[must_use]
    pub const fn type_name(&self) -> &'static str {
        match self {
            Self::Int(_) => "int",
            Self::Float(_) => "float",
            Self::Complex { .. } => "complex",
        }
    }

    /// Converts the numeric metadata into a primitive value.
    #[must_use]
    pub fn into_primitive(self) -> GoPrimitiveValue {
        match self {
            Self::Int(value) => GoPrimitiveValue::Int(value),
            Self::Float(value) => GoPrimitiveValue::Float(value),
            Self::Complex { real, imag } => GoPrimitiveValue::Complex { real, imag },
        }
    }
}

/// Go string literal kind metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoStringLiteralKind {
    Interpreted,
    Raw,
}

impl GoStringLiteralKind {
    /// Returns the literal kind label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Interpreted => "interpreted",
            Self::Raw => "raw",
        }
    }
}

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

impl FromStr for GoStringLiteralKind {
    type Err = GoValueParseError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "interpreted" | "quoted" => Ok(Self::Interpreted),
            "raw" | "backtick" => Ok(Self::Raw),
            _ => Err(GoValueParseError::Unknown),
        }
    }
}

/// Go rune literal metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoRuneLiteral(char);

impl GoRuneLiteral {
    /// Creates rune literal metadata from a character.
    #[must_use]
    pub const fn new(value: char) -> Self {
        Self(value)
    }

    /// Returns the stored character.
    #[must_use]
    pub const fn as_char(self) -> char {
        self.0
    }
}

impl fmt::Display for GoRuneLiteral {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.0)
    }
}

/// Go bool literal metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoBoolLiteral(bool);

impl GoBoolLiteral {
    /// Creates bool literal metadata.
    #[must_use]
    pub const fn new(value: bool) -> Self {
        Self(value)
    }

    /// Returns the stored bool.
    #[must_use]
    pub const fn value(self) -> bool {
        self.0
    }
}

impl fmt::Display for GoBoolLiteral {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.0)
    }
}

/// Go nil metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoNil;

impl GoNil {
    /// Returns the nil label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        "nil"
    }
}

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

fn is_zero_integer_text(value: &str) -> bool {
    let trimmed = value.trim();
    let digits = trimmed
        .strip_prefix(['+', '-'])
        .unwrap_or(trimmed)
        .trim_start_matches('0');
    !trimmed.is_empty() && digits.is_empty()
}

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

#[cfg(test)]
mod tests {
    use super::{
        GoBoolLiteral, GoNil, GoNumericValue, GoPrimitiveValue, GoRuneLiteral, GoStringLiteralKind,
        GoValueParseError,
    };

    #[test]
    fn reports_type_names() {
        assert_eq!(GoPrimitiveValue::Nil.type_name(), "nil");
        assert_eq!(GoPrimitiveValue::Bool(true).type_name(), "bool");
        assert_eq!(GoPrimitiveValue::Int(String::from("42")).type_name(), "int");
        assert_eq!(
            GoPrimitiveValue::String(String::from("go")).type_name(),
            "string"
        );
    }

    #[test]
    fn checks_zero_like_values() {
        assert!(GoPrimitiveValue::Nil.is_zero_like());
        assert!(GoPrimitiveValue::Bool(false).is_zero_like());
        assert!(GoPrimitiveValue::Int(String::from("-0")).is_zero_like());
        assert!(GoPrimitiveValue::Float(0.0).is_zero_like());
        assert!(
            GoPrimitiveValue::Complex {
                real: 0.0,
                imag: 0.0
            }
            .is_zero_like()
        );
        assert!(GoPrimitiveValue::Rune('\0').is_zero_like());
        assert!(GoPrimitiveValue::String(String::new()).is_zero_like());
        assert!(!GoPrimitiveValue::String(String::from("go")).is_zero_like());
    }

    #[test]
    fn models_numeric_values() {
        let numeric = GoNumericValue::Complex {
            real: 1.0,
            imag: 2.0,
        };
        assert_eq!(numeric.type_name(), "complex");
        assert!(numeric.into_primitive().is_numeric());
    }

    #[test]
    fn parses_string_literal_kinds() -> Result<(), GoValueParseError> {
        assert_eq!(
            "raw".parse::<GoStringLiteralKind>()?,
            GoStringLiteralKind::Raw
        );
        assert_eq!(GoStringLiteralKind::Interpreted.to_string(), "interpreted");
        assert_eq!(
            "".parse::<GoStringLiteralKind>(),
            Err(GoValueParseError::Empty)
        );
        Ok(())
    }

    #[test]
    fn models_literal_wrappers() {
        assert_eq!(GoRuneLiteral::new('g').as_char(), 'g');
        assert!(GoBoolLiteral::new(true).value());
        assert_eq!(GoNil.as_str(), "nil");
    }
}