Skip to main content

icydb_schema/
types.rs

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