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