1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[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 #[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 #[must_use]
38 pub const fn is_none(&self) -> bool {
39 matches!(self, Self::None)
40 }
41
42 #[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 #[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#[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 #[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#[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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
135pub struct PythonBytesValue(Vec<u8>);
136
137impl PythonBytesValue {
138 #[must_use]
140 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
141 Self(bytes.into())
142 }
143
144 #[must_use]
146 pub fn as_bytes(&self) -> &[u8] {
147 &self.0
148 }
149}
150
151#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
153pub struct PythonNone;
154
155#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
157pub struct PythonEllipsis;
158
159#[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}