Skip to main content

icydb_model/
lib.rs

1//! IcyDB application-model authoring and code generation.
2//!
3//! This package owns application declarations, the host-only authoring graph,
4//! explicit application validation and normalization, and lowering into the
5//! public [`icydb_schema`] proposal contract. It is not database authority.
6
7extern crate self as icydb_model;
8
9pub mod base;
10pub mod build;
11pub mod error;
12pub mod fragment;
13// Declarations remain available on Wasm, while their whole-graph validation
14// helpers are deliberately host-only work.
15#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
16pub mod node;
17pub mod normalize;
18#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
19mod schema_validate;
20mod typed_adapter;
21pub mod types;
22pub mod validate;
23#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
24mod visit;
25pub mod visitor;
26
27// Maximum length for entity schema identifiers.
28pub const MAX_ENTITY_NAME_LEN: usize = 64;
29
30// Maximum length for field schema identifiers.
31pub const MAX_FIELD_NAME_LEN: usize = 64;
32
33// Maximum number of fields allowed in a derived index.
34pub const MAX_INDEX_FIELDS: usize = 4;
35
36// Maximum length for derived index identifiers.
37pub const MAX_INDEX_NAME_LEN: usize =
38    MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (1 + MAX_FIELD_NAME_LEN));
39
40use crate::{build::BuildError, node::NodeError};
41use thiserror::Error as ThisError;
42
43/// Shared schema-building prelude used by validators, macros, and tests.
44pub mod prelude {
45    pub(crate) use crate::build::schema_read;
46    pub use crate::{
47        Collection as _, Inner as _, MapCollection as _, Path as _, base, canister, entity, enum_,
48        err,
49        error::ErrorTree,
50        list, map, newtype,
51        node::*,
52        normalizer, record,
53        schema::*,
54        set, store, tuple,
55        types::{Cardinality, Primitive},
56        validator,
57        visitor::{
58            Issue, Normalize as _, NormalizeAuto, NormalizeCustom, Normalizer as _, Validate as _,
59            ValidateAuto, ValidateCustom, Validator as _, Visitable as _, VisitorContext,
60        },
61    };
62    pub(crate) use crate::{
63        node::{MacroNode, ValidateNode, VisitableNode},
64        visit::Visitor,
65    };
66    pub use candid::CandidType;
67    pub use serde::{Deserialize, Serialize};
68}
69
70pub use icydb_model_macros::{
71    Add, AddAssign, Deref, DerefMut, Display, Div, DivAssign, Inner, Mul, MulAssign, Rem, Sub,
72    SubAssign, Sum, canister, entity, enum_, list, map, newtype, normalizer, record, set, store,
73    tuple, validator,
74};
75pub use normalize::normalize;
76#[doc(hidden)]
77pub use typed_adapter::{
78    TypedAdapterContext, TypedEnumOutput, TypedInputValue, TypedNamedType, TypedOutputValue,
79    TypedScalarValue, TypedValueError,
80};
81pub use validate::validate;
82
83/// Fully-qualified path identity for generated application declarations.
84pub trait Path {
85    /// Stable Rust declaration path.
86    const PATH: &'static str;
87}
88
89/// Borrowed and consuming access to one-field application wrappers.
90pub trait Inner<T> {
91    /// Borrow the wrapped value.
92    fn inner(&self) -> &T;
93
94    /// Consume the wrapper and return its value.
95    fn into_inner(self) -> T;
96}
97
98/// Iteration contract for generated list and set wrappers.
99pub trait Collection {
100    /// Element type.
101    type Item;
102
103    /// Borrowed iterator type.
104    type Iter<'a>: Iterator<Item = &'a Self::Item> + 'a
105    where
106        Self: 'a;
107
108    /// Iterate over elements.
109    fn iter(&self) -> Self::Iter<'_>;
110
111    /// Return the number of elements.
112    fn len(&self) -> usize;
113
114    /// Return whether this collection is empty.
115    fn is_empty(&self) -> bool {
116        self.len() == 0
117    }
118}
119
120/// Iteration contract for generated map wrappers.
121pub trait MapCollection {
122    /// Key type.
123    type Key;
124
125    /// Value type.
126    type Value;
127
128    /// Borrowed iterator type.
129    type Iter<'a>: Iterator<Item = (&'a Self::Key, &'a Self::Value)> + 'a
130    where
131        Self: 'a;
132
133    /// Iterate over entries.
134    fn iter(&self) -> Self::Iter<'_>;
135
136    /// Return the number of entries.
137    fn len(&self) -> usize;
138
139    /// Return whether this map is empty.
140    fn is_empty(&self) -> bool {
141        self.len() == 0
142    }
143}
144
145/// Exact public proposal vocabulary consumed by application-model lowering.
146pub mod schema {
147    pub use icydb_schema::*;
148}
149
150/// Dependencies intentionally exposed to generated model code.
151#[doc(hidden)]
152pub mod __reexports {
153    pub use candid;
154    #[cfg(not(target_arch = "wasm32"))]
155    pub use ctor;
156    pub use icydb_model_macros;
157    pub use remain;
158    pub use serde;
159}
160
161///
162/// Error
163///
164/// Top-level schema error boundary spanning build-time validation and node
165/// lookup/type errors.
166///
167#[derive(Debug, ThisError)]
168pub enum Error {
169    #[error(transparent)]
170    BuildError(#[from] BuildError),
171
172    #[error(transparent)]
173    NodeError(#[from] NodeError),
174}
175
176//
177// TESTS
178//
179
180#[cfg(test)]
181mod tests {
182    use super::{Error, build::BuildError, error::ErrorTree, node::NodeError};
183
184    #[test]
185    fn build_errors_remain_in_build_boundary() {
186        let schema_error = Error::from(BuildError::Validation(ErrorTree::from(
187            "missing schema relation target",
188        )));
189
190        match schema_error {
191            Error::BuildError(BuildError::Validation(tree)) => {
192                assert!(
193                    tree.messages()
194                        .iter()
195                        .any(|message| message == "missing schema relation target"),
196                    "build validation errors must remain wrapped as build-boundary failures",
197                );
198            }
199            Error::BuildError(BuildError::Graph(error)) => {
200                panic!("unexpected graph error: {error}");
201            }
202            Error::NodeError(_) => {
203                panic!("build validation failures must not be remapped into node-boundary errors");
204            }
205        }
206    }
207
208    #[test]
209    fn node_errors_remain_in_node_boundary() {
210        let schema_error = Error::from(NodeError::PathNotFound("entity.user_id".to_string()));
211
212        match schema_error {
213            Error::NodeError(NodeError::PathNotFound(path)) => {
214                assert_eq!(path, "entity.user_id");
215            }
216            Error::NodeError(NodeError::IncorrectNodeType(path)) => {
217                panic!("unexpected node error kind after conversion for path {path}");
218            }
219            Error::BuildError(_) => {
220                panic!("node errors must not be remapped into build-boundary failures");
221            }
222        }
223    }
224}