1mod arg;
7mod canister;
8mod constraint;
9mod def;
10mod entity;
11mod r#enum;
12mod field;
13mod index;
14mod item;
15mod list;
16mod map;
17mod newtype;
18mod normalizer;
19mod primary_key;
20mod record;
21mod relation;
22mod schema;
23mod set;
24mod store;
25mod tuple;
26mod r#type;
27mod validator;
28mod value;
29
30use crate::{
31 prelude::*,
32 visit::{Event, Visitor},
33};
34use std::any::Any;
35use thiserror::Error as ThisError;
36
37pub use arg::*;
38pub use canister::*;
39pub use constraint::*;
40pub use def::*;
41pub use entity::*;
42pub use r#enum::*;
43pub use field::*;
44pub use index::*;
45pub use item::*;
46pub use list::*;
47pub use map::*;
48pub use newtype::*;
49pub use normalizer::*;
50pub use primary_key::*;
51pub use record::*;
52pub use relation::*;
53pub use schema::*;
54pub use set::*;
55pub use store::*;
56pub use tuple::*;
57pub use r#type::*;
58pub use validator::*;
59pub use value::*;
60
61#[derive(Debug, ThisError)]
69pub enum NodeError {
70 #[error("{0} is an incorrect node type")]
71 IncorrectNodeType(String),
72
73 #[error("path not found: {0}")]
74 PathNotFound(String),
75}
76
77pub(crate) trait MacroNode: Any {
90 fn as_any(&self) -> &dyn Any;
91}
92
93pub(crate) trait ValidateNode {
101 fn validate(&self) -> Result<(), ErrorTree> {
102 Ok(())
103 }
104}
105
106pub(crate) trait VisitableNode: ValidateNode {
114 fn route_key(&self) -> String {
116 String::new()
117 }
118
119 fn accept<V: Visitor>(&self, visitor: &mut V) {
121 visitor.push(&self.route_key());
122 visitor.visit(self, Event::Enter);
123 self.drive(visitor);
124 visitor.visit(self, Event::Exit);
125 visitor.pop();
126 }
127
128 fn drive<V: Visitor>(&self, _: &mut V) {}
130}
131
132pub(crate) fn validate_source_name<K>(
134 errs: &mut ErrorTree,
135 kind: &str,
136 name: &str,
137 constructor: impl FnOnce(String) -> Result<K, icydb_schema::SchemaContractError>,
138) {
139 if let Err(error) = constructor(name.to_string()) {
140 err!(errs, "invalid {kind} name '{name}': {error}");
141 }
142}
143
144pub(crate) fn validate_stable_key_segment(errs: &mut ErrorTree, label: &str, value: &str) {
145 if !stable_key_segment_is_canonical(value) {
146 err!(
147 errs,
148 "{label} `{value}` must use lowercase ASCII letters, digits, and underscores",
149 );
150 }
151}
152
153pub(crate) fn validate_stable_key(errs: &mut ErrorTree, label: &str, value: &str) {
154 if !stable_key_is_canonical(value) {
155 err!(
156 errs,
157 "{label} `{value}` must be at most 128 bytes, must use lowercase ASCII segments beginning with a letter, must use dots as separators, must use underscores instead of hyphens, must end in .v1, and must not start with canic.",
158 );
159 }
160}
161
162#[must_use]
163pub fn stable_key_segment_is_canonical(value: &str) -> bool {
164 let mut bytes = value.bytes();
165 bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
166 && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
167}
168
169#[must_use]
170pub(crate) fn stable_key_is_canonical(value: &str) -> bool {
171 if value.len() > 128 || value.starts_with("canic.") {
172 return false;
173 }
174
175 let mut saw_segment = false;
176 let mut last_segment = "";
177 for segment in value.split('.') {
178 if !stable_key_segment_is_canonical(segment) {
179 return false;
180 }
181 saw_segment = true;
182 last_segment = segment;
183 }
184
185 saw_segment && last_segment == "v1"
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn stable_key_segment_policy_requires_a_lowercase_letter_prefix() {
194 for segment in ["db", "demo_rpg", "store_1", "v1"] {
195 assert!(stable_key_segment_is_canonical(segment));
196 }
197
198 for segment in [
199 "",
200 "1db",
201 "_db",
202 "Demo",
203 "demo-rpg",
204 "demo.rpg",
205 "canic.owned",
206 ] {
207 assert!(!stable_key_segment_is_canonical(segment));
208 }
209 }
210
211 #[test]
212 fn full_stable_key_policy_rejects_reserved_and_malformed_keys() {
213 assert!(stable_key_is_canonical("icydb.demo_rpg.characters.data.v1"));
214
215 for key in [
216 "canic.demo_rpg.characters.data.v1",
217 "icydb.demo_rpg.characters.data",
218 "icydb.demo-rpg.characters.data.v1",
219 "icydb.demo_rpg..data.v1",
220 "icydb.Demo.characters.data.v1",
221 "icydb.demo_rpg.characters.data.v2",
222 "icydb.1demo.characters.data.v1",
223 "icydb._demo.characters.data.v1",
224 ] {
225 assert!(!stable_key_is_canonical(key), "key should fail: {key}");
226 }
227
228 let maximum = format!("icydb.{}.data.v1", "a".repeat(114));
229 assert_eq!(maximum.len(), 128);
230 assert!(stable_key_is_canonical(&maximum));
231
232 let oversized = format!("icydb.{}.data.v1", "a".repeat(115));
233 assert_eq!(oversized.len(), 129);
234 assert!(!stable_key_is_canonical(&oversized));
235 }
236}