paft_core/
lib.rs

1//! Core infrastructure utilities for the paft ecosystem.
2//!
3//! This crate provides:
4//! - Workspace-wide error type [`PaftError`]
5//! - Macros for canonical string enums (open/closed) and `Display` via `code()`
6//! - Reusable serde helpers for common timestamp encodings
7//! - Optional re-exports for lightweight dataframe traits
8//!
9//! # Quickstart
10//! ```rust
11//! use paft_core::{PaftError, string_enum_closed_with_code, impl_display_via_code};
12//! # use std::str::FromStr;
13//!
14//! // Define a closed string enum with canonical codes and parsing
15//! #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16//! enum Side { Buy, Sell }
17//! paft_core::string_enum_closed_with_code!(
18//!     Side, "Side",
19//!     { "BUY" => Side::Buy, "SELL" => Side::Sell }
20//! );
21//! paft_core::impl_display_via_code!(Side);
22//!
23//! assert_eq!(Side::Buy.code(), "BUY");
24//! assert_eq!("sell".parse::<Side>().unwrap(), Side::Sell);
25//! assert!(matches!("".parse::<Side>(), Err(PaftError::InvalidEnumValue { .. })));
26//! ```
27//!
28//! # Serde helpers
29//! See [`serde_helpers`] for helpers like `ts_seconds_vec` and `ts_seconds_option`.
30#![forbid(unsafe_code)]
31#![warn(missing_docs)]
32
33/// Error definitions shared across crates.
34pub mod error;
35/// Private serde helper modules for custom serialization patterns.
36pub mod serde_helpers;
37
38/// Internal macro exports for string-backed enums used across the paft workspace.
39/// These remain public for crate interoperability but are not covered by semver guarantees.
40#[doc(hidden)]
41#[macro_export]
42macro_rules! __string_enum_base {
43    (
44        $Type:ident, $enum_name:literal, error, {
45            $( $alias:literal => $variant:path ),+ $(,)?
46        }
47    ) => {
48        impl paft_utils::StringCode for $Type {
49            fn code(&self) -> &str { $Type::code(self) }
50        }
51
52        impl ::serde::Serialize for $Type {
53            fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
54            where
55                S: ::serde::Serializer,
56            {
57                serializer.serialize_str(self.code())
58            }
59        }
60
61        impl<'de> ::serde::Deserialize<'de> for $Type {
62            fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
63            where
64                D: ::serde::Deserializer<'de>,
65            {
66                let raw = <String as ::serde::Deserialize>::deserialize(deserializer)?;
67                Self::from_str(&raw).map_err(::serde::de::Error::custom)
68            }
69        }
70
71        impl ::std::str::FromStr for $Type {
72            type Err = $crate::error::PaftError;
73
74            fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
75                let trimmed = input.trim();
76                if trimmed.is_empty() {
77                    return Err($crate::error::PaftError::InvalidEnumValue {
78                        enum_name: $enum_name,
79                        value: input.to_string(),
80                    });
81                }
82                let token = paft_utils::canonicalize(trimmed);
83                let parsed = match token.as_ref() {
84                    $( $alias => $variant, )*
85                    _ => {
86                        return Err($crate::error::PaftError::InvalidEnumValue {
87                            enum_name: $enum_name,
88                            value: input.to_string(),
89                        });
90                    }
91                };
92                Ok(parsed)
93            }
94        }
95
96        impl ::std::convert::TryFrom<String> for $Type {
97            type Error = $crate::error::PaftError;
98
99            fn try_from(value: String) -> ::std::result::Result<Self, Self::Error> {
100                Self::from_str(&value)
101            }
102        }
103
104        impl ::std::convert::From<$Type> for String {
105            fn from(v: $Type) -> Self { v.code().to_string() }
106        }
107
108        impl $Type {
109            #[doc(hidden)]
110            pub const __ALIASES: &'static [(&'static str, $Type)] = &[
111                $( ($alias, $variant) ),*
112            ];
113        }
114    };
115
116    (
117        $Type:ident, $enum_name:literal, other($OtherVariant:path), {
118            $( $alias:literal => $variant:path ),+ $(,)?
119        }
120    ) => {
121        impl paft_utils::StringCode for $Type {
122            fn code(&self) -> &str { $Type::code(self) }
123            fn is_canonical(&self) -> bool { $Type::is_canonical(self) }
124        }
125
126        impl $Type {
127            /// Returns true when this value represents a canonical variant.
128            #[must_use]
129            pub const fn is_canonical(&self) -> bool { !matches!(self, $OtherVariant(_)) }
130        }
131
132        impl ::serde::Serialize for $Type {
133            fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
134            where
135                S: ::serde::Serializer,
136            {
137                serializer.serialize_str(self.code())
138            }
139        }
140
141        impl<'de> ::serde::Deserialize<'de> for $Type {
142            fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
143            where
144                D: ::serde::Deserializer<'de>,
145            {
146                let raw = <String as ::serde::Deserialize>::deserialize(deserializer)?;
147                Self::from_str(&raw).map_err(::serde::de::Error::custom)
148            }
149        }
150
151        impl ::std::str::FromStr for $Type {
152            type Err = $crate::error::PaftError;
153
154            fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
155                let trimmed = input.trim();
156                if trimmed.is_empty() {
157                    return Err($crate::error::PaftError::InvalidEnumValue {
158                        enum_name: $enum_name,
159                        value: input.to_string(),
160                    });
161                }
162                let token = paft_utils::canonicalize(trimmed);
163                let parsed = match token.as_ref() {
164                    $( $alias => $variant, )*
165                    _ => {
166                        let canon = paft_utils::Canonical::try_new(trimmed)
167                            .map_err(|_| $crate::error::PaftError::InvalidEnumValue {
168                                enum_name: $enum_name,
169                                value: input.to_string(),
170                            })?;
171                        return Ok($OtherVariant(canon));
172                    }
173                };
174                Ok(parsed)
175            }
176        }
177
178        impl ::std::convert::TryFrom<String> for $Type {
179            type Error = $crate::error::PaftError;
180
181            fn try_from(value: String) -> ::std::result::Result<Self, Self::Error> {
182                Self::from_str(&value)
183            }
184        }
185
186        impl ::std::convert::From<$Type> for String {
187            fn from(v: $Type) -> Self { v.code().to_string() }
188        }
189
190        impl $Type {
191            #[doc(hidden)]
192            pub const __ALIASES: &'static [(&'static str, $Type)] = &[
193                $( ($alias, $variant) ),*
194            ];
195        }
196    };
197}
198
199/// Helper to implement Display using the type's `code()` method.
200#[doc(hidden)]
201#[macro_export]
202macro_rules! impl_display_via_code {
203    ( $Type:ident ) => {
204        impl ::std::fmt::Display for $Type {
205            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
206                f.write_str(self.code())
207            }
208        }
209    };
210}
211
212/// Open (extensible) string enum with macro-provided `code()` and open parsing.
213#[doc(hidden)]
214#[macro_export]
215macro_rules! string_enum_with_code {
216    (
217        $Type:ident, $Other:ident, $enum_name:literal,
218        { $( $canon_token:literal => $canon_variant:path ),+ $(,)? },
219        { $( $alias:literal => $variant:path ),* $(,)? }
220    ) => {
221        impl $Type {
222            /// Returns the canonical string code for this value.
223            #[must_use]
224            pub fn code(&self) -> &str {
225                match self {
226                    $( $canon_variant => $canon_token, )+
227                    $Type::$Other(s) => s.as_ref(),
228                }
229            }
230        }
231
232        $crate::__string_enum_base! {
233            $Type, $enum_name, other($Type::$Other),
234            { $( $canon_token => $canon_variant ),+ $(, $alias => $variant )* }
235        }
236    };
237
238    (
239        $Type:ident, $Other:ident, $enum_name:literal,
240        { $( $canon_token:literal => $canon_variant:path ),+ $(,)? }
241    ) => {
242        $crate::string_enum_with_code!(
243            $Type, $Other, $enum_name,
244            { $( $canon_token => $canon_variant ),+ },
245            {}
246        );
247    };
248}
249
250/// Closed string enum with macro-provided `code()` and closed parsing.
251#[doc(hidden)]
252#[macro_export]
253macro_rules! string_enum_closed_with_code {
254    (
255        $Type:ident, $enum_name:literal,
256        { $( $canon_token:literal => $canon_variant:path ),+ $(,)? },
257        { $( $alias:literal => $variant:path ),* $(,)? }
258    ) => {
259        impl $Type {
260            /// Returns the canonical string code for this value.
261            #[must_use]
262            pub const fn code(&self) -> &str {
263                match self {
264                    $( $canon_variant => $canon_token, )+
265                }
266            }
267        }
268
269        $crate::__string_enum_base! {
270            $Type, $enum_name, error,
271            { $( $canon_token => $canon_variant ),+ $(, $alias => $variant )* }
272        }
273    };
274
275    (
276        $Type:ident, $enum_name:literal,
277        { $( $canon_token:literal => $canon_variant:path ),+ $(,)? }
278    ) => {
279        $crate::string_enum_closed_with_code!(
280            $Type, $enum_name,
281            { $( $canon_token => $canon_variant ),+ },
282            {}
283        );
284    };
285}
286
287pub use error::PaftError;