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. Usage varies by feature profile,
16// so these Wasm allowances must not require dead code to be present.
17#[cfg_attr(
18    target_arch = "wasm32",
19    allow(
20        dead_code,
21        reason = "Wasm retains declaration nodes while host code owns whole-graph traversal"
22    )
23)]
24pub mod node;
25pub mod normalize;
26#[cfg_attr(
27    target_arch = "wasm32",
28    expect(dead_code, reason = "whole-graph schema validation is host-owned")
29)]
30mod schema_validate;
31mod typed_adapter;
32pub mod types;
33pub mod validate;
34#[cfg_attr(
35    target_arch = "wasm32",
36    allow(
37        dead_code,
38        reason = "Wasm retains declarations while host code owns graph visitation"
39    )
40)]
41mod visit;
42pub mod visitor;
43
44// Maximum length for entity schema identifiers.
45pub const MAX_ENTITY_NAME_LEN: usize = 64;
46
47// Maximum length for field schema identifiers.
48pub const MAX_FIELD_NAME_LEN: usize = 64;
49
50// Maximum number of fields allowed in a derived index.
51pub const MAX_INDEX_FIELDS: usize = 4;
52
53// Maximum length for derived index identifiers.
54pub const MAX_INDEX_NAME_LEN: usize =
55    MAX_ENTITY_NAME_LEN + (MAX_INDEX_FIELDS * (1 + MAX_FIELD_NAME_LEN));
56
57use crate::{build::BuildError, node::NodeError};
58use thiserror::Error as ThisError;
59
60/// Shared schema-building prelude used by validators, macros, and tests.
61pub mod prelude {
62    pub(crate) use crate::build::schema_read;
63    pub use crate::{
64        EntitySource as _, Inner as _, NormalizeAndValidate as _, Path as _, base, canister,
65        entity, enum_, err,
66        error::ErrorTree,
67        list, map, newtype,
68        node::*,
69        normalizer, record,
70        schema::*,
71        set, store, tuple,
72        types::{Cardinality, Primitive},
73        validator,
74        visitor::{
75            Issue, Normalize as _, NormalizeAuto, NormalizeCustom, Normalizer as _, Validate as _,
76            ValidateAuto, ValidateCustom, Validator as _, Visitable as _, VisitorContext,
77        },
78    };
79    pub(crate) use crate::{
80        node::{MacroNode, ValidateNode, VisitableNode},
81        visit::Visitor,
82    };
83
84    pub use serde::{Deserialize, Serialize};
85}
86
87pub use application::NormalizeAndValidate;
88pub use icydb_model_macros::{
89    Add, AddAssign, Deref, DerefMut, Display, Div, DivAssign, Inner, Mul, MulAssign, Neg, Product,
90    Rem, RemAssign, Sub, SubAssign, Sum, canister, entity, enum_, list, map, newtype, normalizer,
91    record, set, store, tuple, validator,
92};
93pub use normalize::normalize;
94#[doc(hidden)]
95pub use typed_adapter::{
96    TypedAdapterContext, TypedEnumDescriptor, TypedEnumSelection, TypedInputValue, TypedNamedType,
97    TypedOutputValue, TypedScalarValue, TypedValueError,
98};
99pub use validate::validate;
100
101/// Fully-qualified path identity for generated application declarations.
102pub trait Path {
103    /// Stable Rust declaration path.
104    const PATH: &'static str;
105}
106
107/// Schema-authored entity source identity emitted for runtime-enabled models.
108///
109/// This is application vocabulary only. It cannot establish accepted-schema
110/// presence, freshness, or authority; runtime APIs resolve the source against
111/// their pinned accepted snapshot.
112pub trait EntitySource {
113    /// Exact authored entity source name.
114    const ENTITY: &'static str;
115}
116
117/// Borrowed and consuming access to one-field application wrappers.
118pub trait Inner<T> {
119    /// Borrow the wrapped value.
120    fn inner(&self) -> &T;
121
122    /// Consume the wrapper and return its value.
123    fn into_inner(self) -> T;
124}
125
126/// Exact public proposal vocabulary consumed by application-model lowering.
127pub mod schema {
128    pub use icydb_schema::*;
129}
130
131/// Dependencies intentionally exposed to generated model code.
132#[doc(hidden)]
133pub mod __reexports {
134
135    #[cfg(not(target_arch = "wasm32"))]
136    pub use ctor;
137    pub use icydb_model_macros;
138    pub use remain;
139    pub use serde;
140}
141
142///
143/// Error
144///
145/// Top-level schema error boundary spanning build-time validation and node
146/// lookup/type errors.
147///
148#[derive(Debug, ThisError)]
149pub enum Error {
150    #[error(transparent)]
151    BuildError(#[from] BuildError),
152
153    #[error(transparent)]
154    NodeError(#[from] NodeError),
155}
156
157//
158// TESTS
159//
160
161#[cfg(test)]
162mod tests {
163    use super::{Error, build::BuildError, error::ErrorTree, node::NodeError};
164
165    #[test]
166    fn build_errors_remain_in_build_boundary() {
167        let schema_error = Error::from(BuildError::Validation(ErrorTree::from(
168            "missing schema relation target",
169        )));
170
171        match schema_error {
172            Error::BuildError(BuildError::Validation(tree)) => {
173                assert!(
174                    tree.messages()
175                        .iter()
176                        .any(|message| message == "missing schema relation target"),
177                    "build validation errors must remain wrapped as build-boundary failures",
178                );
179            }
180            Error::BuildError(BuildError::Graph(error)) => {
181                panic!("unexpected graph error: {error}");
182            }
183            Error::NodeError(_) => {
184                panic!("build validation failures must not be remapped into node-boundary errors");
185            }
186        }
187    }
188
189    #[test]
190    fn node_errors_remain_in_node_boundary() {
191        let schema_error = Error::from(NodeError::PathNotFound("entity.user_id".to_string()));
192
193        match schema_error {
194            Error::NodeError(NodeError::PathNotFound(path)) => {
195                assert_eq!(path, "entity.user_id");
196            }
197            Error::NodeError(NodeError::IncorrectNodeType(path)) => {
198                panic!("unexpected node error kind after conversion for path {path}");
199            }
200            Error::BuildError(_) => {
201                panic!("node errors must not be remapped into build-boundary failures");
202            }
203        }
204    }
205}