Skip to main content

icydb_schema/
types.rs

1use crate::prelude::*;
2use candid::CandidType;
3use darling::FromMeta;
4use derive_more::{Display, FromStr};
5use proc_macro2::TokenStream;
6use quote::{ToTokens, format_ident, quote};
7
8///
9/// Cardinality
10///
11
12#[derive(
13    CandidType, Clone, Copy, Default, Debug, Deserialize, Display, Eq, FromStr, PartialEq, Serialize,
14)]
15pub enum Cardinality {
16    #[default]
17    One,
18    Opt,
19    Many,
20}
21
22impl FromMeta for Cardinality {
23    fn from_string(s: &str) -> Result<Self, darling::Error> {
24        s.parse::<Self>()
25            .map_err(|_| darling::Error::unknown_value(s))
26    }
27}
28
29impl ToTokens for Cardinality {
30    fn to_tokens(&self, tokens: &mut TokenStream) {
31        let ident = format_ident!("{self}");
32
33        tokens.extend(quote!(::icydb::schema::types::Cardinality::#ident));
34    }
35}
36
37///
38/// Primitive
39///
40
41#[derive(
42    CandidType, Clone, Copy, Debug, Deserialize, Display, Eq, PartialEq, FromStr, Serialize,
43)]
44#[remain::sorted]
45pub enum Primitive {
46    Account,
47    Blob,
48    Bool,
49    Date,
50    Decimal,
51    Duration,
52    E8s,
53    E18s,
54    Float32,
55    Float64,
56    Int,
57    Int8,
58    Int16,
59    Int32,
60    Int64,
61    Int128,
62    Nat,
63    Nat8,
64    Nat16,
65    Nat32,
66    Nat64,
67    Nat128,
68    Principal,
69    Subaccount,
70    Text,
71    Timestamp,
72    Ulid,
73    Unit,
74}
75
76// Local helper to map scalar registry variants to schema primitive variants.
77macro_rules! primitive_matches_scalar {
78    ( $primitive:expr, Int ) => {
79        matches!(
80            $primitive,
81            Primitive::Int8 | Primitive::Int16 | Primitive::Int32 | Primitive::Int64
82        )
83    };
84    ( $primitive:expr, Uint ) => {
85        matches!(
86            $primitive,
87            Primitive::Nat8 | Primitive::Nat16 | Primitive::Nat32 | Primitive::Nat64
88        )
89    };
90    ( $primitive:expr, Enum ) => {
91        false
92    };
93    ( $primitive:expr, IntBig ) => {
94        matches!($primitive, Primitive::Int)
95    };
96    ( $primitive:expr, UintBig ) => {
97        matches!($primitive, Primitive::Nat)
98    };
99    ( $primitive:expr, Uint128 ) => {
100        matches!($primitive, Primitive::Nat128)
101    };
102    ( $primitive:expr, $scalar:ident ) => {
103        matches!($primitive, Primitive::$scalar)
104    };
105}
106
107macro_rules! primitive_supports_arithmetic_from_registry {
108    ( @args $primitive:expr; @entries $( ($scalar:ident, $family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
109        false $( || (primitive_matches_scalar!($primitive, $scalar) && $supports_arithmetic) )*
110    };
111}
112
113impl Primitive {
114    #[must_use]
115    pub const fn supports_arithmetic(self) -> bool {
116        icydb_core::scalar_registry!(primitive_supports_arithmetic_from_registry, self)
117    }
118
119    #[must_use]
120    pub const fn supports_remainder(self) -> bool {
121        matches!(
122            self,
123            Self::Decimal
124                | Self::Int8
125                | Self::Int16
126                | Self::Int32
127                | Self::Int64
128                | Self::Int128
129                | Self::Nat8
130                | Self::Nat16
131                | Self::Nat32
132                | Self::Nat64
133                | Self::Nat128
134        )
135    }
136
137    #[must_use]
138    pub const fn supports_copy(self) -> bool {
139        !matches!(self, Self::Blob | Self::Int | Self::Nat | Self::Text)
140    }
141
142    #[must_use]
143    pub const fn supports_hash(self) -> bool {
144        !matches!(self, Self::Blob | Self::Unit)
145    }
146
147    // Int and Nat are unbounded integers so have no native representation
148    #[must_use]
149    pub const fn supports_num_cast(self) -> bool {
150        matches!(
151            self,
152            Self::Date
153                | Self::Decimal
154                | Self::Duration
155                | Self::E8s
156                | Self::E18s
157                | Self::Int8
158                | Self::Int16
159                | Self::Int32
160                | Self::Int64
161                | Self::Float32
162                | Self::Float64
163                | Self::Nat8
164                | Self::Nat16
165                | Self::Nat32
166                | Self::Nat64
167                | Self::Timestamp
168        )
169    }
170
171    // both Ord and PartialOrd
172    #[must_use]
173    pub const fn supports_ord(self) -> bool {
174        !matches!(self, Self::Blob | Self::Unit)
175    }
176
177    //
178    // grouped helpers
179    //
180
181    #[must_use]
182    pub const fn is_decimal(self) -> bool {
183        matches!(self, Self::Decimal)
184    }
185
186    // is_numeric
187    // Includes ints, floats, fixed‑point (E8s/E18s), and Decimal.
188    #[must_use]
189    pub const fn is_numeric(self) -> bool {
190        self.is_int() || self.is_float() || self.is_fixed_point() || self.is_decimal()
191    }
192
193    #[must_use]
194    pub const fn is_float(self) -> bool {
195        matches!(self, Self::Float32 | Self::Float64)
196    }
197
198    #[must_use]
199    pub const fn is_signed_int(self) -> bool {
200        matches!(
201            self,
202            Self::Int | Self::Int8 | Self::Int16 | Self::Int32 | Self::Int64 | Self::Int128
203        )
204    }
205
206    #[must_use]
207    pub const fn is_unsigned_int(self) -> bool {
208        matches!(
209            self,
210            Self::Nat | Self::Nat8 | Self::Nat16 | Self::Nat32 | Self::Nat64 | Self::Nat128
211        )
212    }
213
214    #[must_use]
215    pub const fn is_int(self) -> bool {
216        self.is_signed_int() || self.is_unsigned_int()
217    }
218
219    #[must_use]
220    pub const fn is_fixed_point(self) -> bool {
221        matches!(self, Self::E8s | Self::E18s)
222    }
223
224    #[must_use]
225    pub fn as_type(self) -> TokenStream {
226        let ident = format_ident!("{self}");
227
228        quote!(::icydb::types::#ident)
229    }
230
231    ///
232    /// Returns the numeric cast function suffix for supported primitives.
233    /// Emits a structured error for non-numeric primitives.
234    ///
235    pub fn num_cast_fn(self) -> Result<&'static str, darling::Error> {
236        match self {
237            Self::E18s => Ok("u128"),
238            Self::Float32 => Ok("f32"),
239            Self::Float64 | Self::Decimal => Ok("f64"),
240            Self::Int8 => Ok("i8"),
241            Self::Int16 => Ok("i16"),
242            Self::Int32 | Self::Date => Ok("i32"),
243            Self::Int64 => Ok("i64"),
244            Self::Nat8 => Ok("u8"),
245            Self::Nat16 => Ok("u16"),
246            Self::Nat32 => Ok("u32"),
247            Self::Nat64 | Self::Duration | Self::E8s | Self::Timestamp => Ok("u64"),
248            _ => Err(darling::Error::custom(format!(
249                "numeric cast is unsupported for primitive {self}"
250            ))),
251        }
252    }
253}
254
255impl FromMeta for Primitive {
256    fn from_string(s: &str) -> Result<Self, darling::Error> {
257        s.parse::<Self>()
258            .map_err(|_| darling::Error::unknown_value(s))
259    }
260}
261
262impl ToTokens for Primitive {
263    fn to_tokens(&self, tokens: &mut TokenStream) {
264        let ident = format_ident!("{self}");
265
266        tokens.extend(quote!(::icydb::schema::types::Primitive::#ident));
267    }
268}