#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[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 {}
#[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 {
#[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",
}
}
#[must_use]
pub const fn is_nil(&self) -> bool {
matches!(self, Self::Nil)
}
#[must_use]
pub const fn is_numeric(&self) -> bool {
matches!(self, Self::Int(_) | Self::Float(_) | Self::Complex { .. })
}
#[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(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum GoNumericValue {
Int(String),
Float(f64),
Complex { real: f64, imag: f64 },
}
impl GoNumericValue {
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Self::Int(_) => "int",
Self::Float(_) => "float",
Self::Complex { .. } => "complex",
}
}
#[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 },
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoStringLiteralKind {
Interpreted,
Raw,
}
impl GoStringLiteralKind {
#[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),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoRuneLiteral(char);
impl GoRuneLiteral {
#[must_use]
pub const fn new(value: char) -> Self {
Self(value)
}
#[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)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoBoolLiteral(bool);
impl GoBoolLiteral {
#[must_use]
pub const fn new(value: bool) -> Self {
Self(value)
}
#[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)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoNil;
impl GoNil {
#[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");
}
}