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