Skip to main content

use_python_value/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Primitive Python-like values for metadata and validation helpers.
8#[derive(Clone, Debug, PartialEq)]
9pub enum PythonPrimitiveValue {
10    None,
11    Bool(bool),
12    Int(String),
13    Float(f64),
14    Complex { real: f64, imag: f64 },
15    String(String),
16    Bytes(Vec<u8>),
17    Ellipsis,
18}
19
20impl PythonPrimitiveValue {
21    /// Returns a Python-style primitive type label.
22    #[must_use]
23    pub const fn type_name(&self) -> &'static str {
24        match self {
25            Self::None => "NoneType",
26            Self::Bool(_) => "bool",
27            Self::Int(_) => "int",
28            Self::Float(_) => "float",
29            Self::Complex { .. } => "complex",
30            Self::String(_) => "str",
31            Self::Bytes(_) => "bytes",
32            Self::Ellipsis => "ellipsis",
33        }
34    }
35
36    /// Returns whether the value is Python `None`.
37    #[must_use]
38    pub const fn is_none(&self) -> bool {
39        matches!(self, Self::None)
40    }
41
42    /// Returns whether the value is approximately truthy by common Python primitive rules.
43    #[must_use]
44    pub fn is_truthy_like(&self) -> bool {
45        match self {
46            Self::None => false,
47            Self::Bool(value) => *value,
48            Self::Int(value) => !matches!(normalized_int_text(value).as_str(), "" | "0"),
49            Self::Float(value) => *value != 0.0 && !value.is_nan(),
50            Self::Complex { real, imag } => {
51                (*real != 0.0 || *imag != 0.0) && !real.is_nan() && !imag.is_nan()
52            }
53            Self::String(value) => !value.is_empty(),
54            Self::Bytes(value) => !value.is_empty(),
55            Self::Ellipsis => true,
56        }
57    }
58
59    /// Returns whether the value is numeric-like.
60    #[must_use]
61    pub const fn is_numeric(&self) -> bool {
62        matches!(
63            self,
64            Self::Bool(_) | Self::Int(_) | Self::Float(_) | Self::Complex { .. }
65        )
66    }
67}
68
69/// Primitive Python number value metadata.
70#[derive(Clone, Debug, PartialEq)]
71pub enum PythonNumberValue {
72    Bool(bool),
73    Int(String),
74    Float(f64),
75    Complex { real: f64, imag: f64 },
76}
77
78impl PythonNumberValue {
79    /// Returns a Python-style numeric type label.
80    #[must_use]
81    pub const fn type_name(&self) -> &'static str {
82        match self {
83            Self::Bool(_) => "bool",
84            Self::Int(_) => "int",
85            Self::Float(_) => "float",
86            Self::Complex { .. } => "complex",
87        }
88    }
89}
90
91/// Python string literal kind metadata.
92#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
93pub enum PythonStringKind {
94    String,
95    RawString,
96    FormatString,
97    TemplateString,
98}
99
100impl PythonStringKind {
101    /// Returns the string kind label.
102    #[must_use]
103    pub const fn as_str(self) -> &'static str {
104        match self {
105            Self::String => "string",
106            Self::RawString => "raw-string",
107            Self::FormatString => "format-string",
108            Self::TemplateString => "template-string",
109        }
110    }
111}
112
113impl fmt::Display for PythonStringKind {
114    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115        formatter.write_str(self.as_str())
116    }
117}
118
119impl FromStr for PythonStringKind {
120    type Err = PythonValueParseError;
121
122    fn from_str(input: &str) -> Result<Self, Self::Err> {
123        match normalized_label(input)?.as_str() {
124            "string" | "str" => Ok(Self::String),
125            "rawstring" | "raw" | "r" => Ok(Self::RawString),
126            "formatstring" | "fstring" | "f" => Ok(Self::FormatString),
127            "templatestring" | "tstring" | "t" => Ok(Self::TemplateString),
128            _ => Err(PythonValueParseError::UnknownLabel),
129        }
130    }
131}
132
133/// Python bytes value metadata.
134#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
135pub struct PythonBytesValue(Vec<u8>);
136
137impl PythonBytesValue {
138    /// Creates Python bytes metadata from raw bytes.
139    #[must_use]
140    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
141        Self(bytes.into())
142    }
143
144    /// Returns the stored bytes.
145    #[must_use]
146    pub fn as_bytes(&self) -> &[u8] {
147        &self.0
148    }
149}
150
151/// Python `None` metadata marker.
152#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
153pub struct PythonNone;
154
155/// Python ellipsis metadata marker.
156#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
157pub struct PythonEllipsis;
158
159/// Error returned when Python value metadata labels are invalid.
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
161pub enum PythonValueParseError {
162    Empty,
163    UnknownLabel,
164}
165
166impl fmt::Display for PythonValueParseError {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Self::Empty => formatter.write_str("Python value metadata label cannot be empty"),
170            Self::UnknownLabel => formatter.write_str("unknown Python value metadata label"),
171        }
172    }
173}
174
175impl Error for PythonValueParseError {}
176
177fn normalized_int_text(input: &str) -> String {
178    let trimmed = input.trim();
179    let unsigned = trimmed.strip_prefix(['+', '-']).unwrap_or(trimmed);
180    unsigned.trim_start_matches('0').to_string()
181}
182
183fn normalized_label(input: &str) -> Result<String, PythonValueParseError> {
184    let trimmed = input.trim();
185    if trimmed.is_empty() {
186        Err(PythonValueParseError::Empty)
187    } else {
188        Ok(trimmed.to_ascii_lowercase().replace(['-', '_', ' '], ""))
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::{PythonBytesValue, PythonPrimitiveValue, PythonStringKind, PythonValueParseError};
195
196    #[test]
197    fn reports_type_names() {
198        assert_eq!(PythonPrimitiveValue::None.type_name(), "NoneType");
199        assert_eq!(
200            PythonPrimitiveValue::Int(String::from("10")).type_name(),
201            "int"
202        );
203        assert_eq!(PythonStringKind::RawString.as_str(), "raw-string");
204    }
205
206    #[test]
207    fn parses_and_displays_string_kinds() -> Result<(), PythonValueParseError> {
208        assert_eq!(
209            "f-string".parse::<PythonStringKind>()?,
210            PythonStringKind::FormatString
211        );
212        assert_eq!(
213            PythonStringKind::TemplateString.to_string(),
214            "template-string"
215        );
216        Ok(())
217    }
218
219    #[test]
220    fn checks_truthy_and_numeric_values() {
221        assert!(!PythonPrimitiveValue::None.is_truthy_like());
222        assert!(!PythonPrimitiveValue::Int(String::from("000")).is_truthy_like());
223        assert!(PythonPrimitiveValue::String(String::from("x")).is_truthy_like());
224        assert!(PythonPrimitiveValue::Bool(false).is_numeric());
225    }
226
227    #[test]
228    fn stores_bytes_metadata() {
229        let bytes = PythonBytesValue::new([1_u8, 2, 3]);
230
231        assert_eq!(bytes.as_bytes(), &[1, 2, 3]);
232    }
233}