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