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