#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Debug, PartialEq)]
pub enum PythonPrimitiveValue {
None,
Bool(bool),
Int(String),
Float(f64),
Complex { real: f64, imag: f64 },
String(String),
Bytes(Vec<u8>),
Ellipsis,
}
impl PythonPrimitiveValue {
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Self::None => "NoneType",
Self::Bool(_) => "bool",
Self::Int(_) => "int",
Self::Float(_) => "float",
Self::Complex { .. } => "complex",
Self::String(_) => "str",
Self::Bytes(_) => "bytes",
Self::Ellipsis => "ellipsis",
}
}
#[must_use]
pub const fn is_none(&self) -> bool {
matches!(self, Self::None)
}
#[must_use]
pub fn is_truthy_like(&self) -> bool {
match self {
Self::None => false,
Self::Bool(value) => *value,
Self::Int(value) => !matches!(normalized_int_text(value).as_str(), "" | "0"),
Self::Float(value) => *value != 0.0 && !value.is_nan(),
Self::Complex { real, imag } => {
(*real != 0.0 || *imag != 0.0) && !real.is_nan() && !imag.is_nan()
}
Self::String(value) => !value.is_empty(),
Self::Bytes(value) => !value.is_empty(),
Self::Ellipsis => true,
}
}
#[must_use]
pub const fn is_numeric(&self) -> bool {
matches!(
self,
Self::Bool(_) | Self::Int(_) | Self::Float(_) | Self::Complex { .. }
)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PythonNumberValue {
Bool(bool),
Int(String),
Float(f64),
Complex { real: f64, imag: f64 },
}
impl PythonNumberValue {
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Self::Bool(_) => "bool",
Self::Int(_) => "int",
Self::Float(_) => "float",
Self::Complex { .. } => "complex",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PythonStringKind {
String,
RawString,
FormatString,
TemplateString,
}
impl PythonStringKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::String => "string",
Self::RawString => "raw-string",
Self::FormatString => "format-string",
Self::TemplateString => "template-string",
}
}
}
impl fmt::Display for PythonStringKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PythonStringKind {
type Err = PythonValueParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"string" | "str" => Ok(Self::String),
"rawstring" | "raw" | "r" => Ok(Self::RawString),
"formatstring" | "fstring" | "f" => Ok(Self::FormatString),
"templatestring" | "tstring" | "t" => Ok(Self::TemplateString),
_ => Err(PythonValueParseError::UnknownLabel),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PythonBytesValue(Vec<u8>);
impl PythonBytesValue {
#[must_use]
pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
Self(bytes.into())
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PythonNone;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PythonEllipsis;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PythonValueParseError {
Empty,
UnknownLabel,
}
impl fmt::Display for PythonValueParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Python value metadata label cannot be empty"),
Self::UnknownLabel => formatter.write_str("unknown Python value metadata label"),
}
}
}
impl Error for PythonValueParseError {}
fn normalized_int_text(input: &str) -> String {
let trimmed = input.trim();
let unsigned = trimmed.strip_prefix(['+', '-']).unwrap_or(trimmed);
unsigned.trim_start_matches('0').to_string()
}
fn normalized_label(input: &str) -> Result<String, PythonValueParseError> {
let trimmed = input.trim();
if trimmed.is_empty() {
Err(PythonValueParseError::Empty)
} else {
Ok(trimmed.to_ascii_lowercase().replace(['-', '_', ' '], ""))
}
}
#[cfg(test)]
mod tests {
use super::{PythonBytesValue, PythonPrimitiveValue, PythonStringKind, PythonValueParseError};
#[test]
fn reports_type_names() {
assert_eq!(PythonPrimitiveValue::None.type_name(), "NoneType");
assert_eq!(
PythonPrimitiveValue::Int(String::from("10")).type_name(),
"int"
);
assert_eq!(PythonStringKind::RawString.as_str(), "raw-string");
}
#[test]
fn parses_and_displays_string_kinds() -> Result<(), PythonValueParseError> {
assert_eq!(
"f-string".parse::<PythonStringKind>()?,
PythonStringKind::FormatString
);
assert_eq!(
PythonStringKind::TemplateString.to_string(),
"template-string"
);
Ok(())
}
#[test]
fn checks_truthy_and_numeric_values() {
assert!(!PythonPrimitiveValue::None.is_truthy_like());
assert!(!PythonPrimitiveValue::Int(String::from("000")).is_truthy_like());
assert!(PythonPrimitiveValue::String(String::from("x")).is_truthy_like());
assert!(PythonPrimitiveValue::Bool(false).is_numeric());
}
#[test]
fn stores_bytes_metadata() {
let bytes = PythonBytesValue::new([1_u8, 2, 3]);
assert_eq!(bytes.as_bytes(), &[1, 2, 3]);
}
}