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
61pub const APP_MEMORY_ID_MIN: u8 = 100;
62pub const APP_MEMORY_ID_MAX: u8 = 254;
63const RESERVED_INTERNAL_MEMORY_ID: u8 = u8::MAX;
64
65#[derive(Debug, ThisError)]
73pub enum NodeError {
74 #[error("{0} is an incorrect node type")]
75 IncorrectNodeType(String),
76
77 #[error("path not found: {0}")]
78 PathNotFound(String),
79}
80
81pub(crate) trait MacroNode: Any {
94 fn as_any(&self) -> &dyn Any;
95}
96
97pub(crate) trait ValidateNode {
105 fn validate(&self) -> Result<(), ErrorTree> {
106 Ok(())
107 }
108}
109
110pub(crate) trait VisitableNode: ValidateNode {
118 fn route_key(&self) -> String {
120 String::new()
121 }
122
123 fn accept<V: Visitor>(&self, visitor: &mut V) {
125 visitor.push(&self.route_key());
126 visitor.visit(self, Event::Enter);
127 self.drive(visitor);
128 visitor.visit(self, Event::Exit);
129 visitor.pop();
130 }
131
132 fn drive<V: Visitor>(&self, _: &mut V) {}
134}
135
136pub(crate) fn validate_source_name<K>(
138 errs: &mut ErrorTree,
139 kind: &str,
140 name: &str,
141 constructor: impl FnOnce(String) -> Result<K, icydb_schema::SchemaContractError>,
142) {
143 if let Err(error) = constructor(name.to_string()) {
144 err!(errs, "invalid {kind} name '{name}': {error}");
145 }
146}
147
148pub(crate) fn validate_memory_id_in_range(
150 errs: &mut ErrorTree,
151 label: &str,
152 memory_id: u8,
153 min: u8,
154 max: u8,
155) {
156 if !memory_id_is_in_range(memory_id, min, max) {
157 err!(errs, "{label} {memory_id} outside of range {min}-{max}");
158 }
159}
160
161pub(crate) fn validate_memory_id_not_reserved(errs: &mut ErrorTree, label: &str, memory_id: u8) {
163 if memory_id_is_reserved(memory_id) {
164 err!(
165 errs,
166 "{label} {memory_id} is reserved for stable-structures internals",
167 );
168 }
169}
170
171pub(crate) fn validate_app_memory_id(errs: &mut ErrorTree, label: &str, memory_id: u8) {
173 if !app_memory_id_is_valid(memory_id) {
174 err!(
175 errs,
176 "{label} {memory_id} outside of application memory range {APP_MEMORY_ID_MIN}-{APP_MEMORY_ID_MAX}",
177 );
178 }
179}
180
181#[must_use]
182pub const fn memory_id_is_in_range(memory_id: u8, min: u8, max: u8) -> bool {
183 memory_id >= min && memory_id <= max
184}
185
186#[must_use]
187pub const fn memory_id_is_reserved(memory_id: u8) -> bool {
188 memory_id == RESERVED_INTERNAL_MEMORY_ID
189}
190
191#[must_use]
192pub const fn app_memory_id_is_valid(memory_id: u8) -> bool {
193 memory_id >= APP_MEMORY_ID_MIN && memory_id <= APP_MEMORY_ID_MAX
194}
195
196pub(crate) fn validate_stable_key_segment(errs: &mut ErrorTree, label: &str, value: &str) {
197 if !stable_key_segment_is_canonical(value) {
198 err!(
199 errs,
200 "{label} `{value}` must use lowercase ASCII letters, digits, and underscores",
201 );
202 }
203}
204
205pub(crate) fn validate_stable_key(errs: &mut ErrorTree, label: &str, value: &str) {
206 if !stable_key_is_canonical(value) {
207 err!(
208 errs,
209 "{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.",
210 );
211 }
212}
213
214#[must_use]
215pub fn stable_key_segment_is_canonical(value: &str) -> bool {
216 let mut bytes = value.bytes();
217 bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
218 && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
219}
220
221#[must_use]
222pub(crate) fn stable_key_is_canonical(value: &str) -> bool {
223 if value.len() > 128 || value.starts_with("canic.") {
224 return false;
225 }
226
227 let mut saw_segment = false;
228 let mut last_segment = "";
229 for segment in value.split('.') {
230 if !stable_key_segment_is_canonical(segment) {
231 return false;
232 }
233 saw_segment = true;
234 last_segment = segment;
235 }
236
237 saw_segment && last_segment == "v1"
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn app_memory_id_policy_accepts_only_application_range() {
246 for memory_id in APP_MEMORY_ID_MIN..=APP_MEMORY_ID_MAX {
247 let mut errors = ErrorTree::new();
248 validate_app_memory_id(&mut errors, "memory_id", memory_id);
249 validate_memory_id_not_reserved(&mut errors, "memory_id", memory_id);
250 assert!(
251 errors.is_empty(),
252 "schema should accept app memory id {memory_id}: {errors}",
253 );
254 }
255
256 for memory_id in [0, APP_MEMORY_ID_MIN - 1] {
257 let mut errors = ErrorTree::new();
258 validate_app_memory_id(&mut errors, "memory_id", memory_id);
259 assert!(
260 !errors.is_empty(),
261 "schema should reject below-range app memory id {memory_id}",
262 );
263 }
264
265 let mut errors = ErrorTree::new();
266 validate_app_memory_id(&mut errors, "memory_id", u8::MAX);
267 validate_memory_id_not_reserved(&mut errors, "memory_id", u8::MAX);
268 let rendered = errors.to_string();
269 assert!(
270 rendered.contains("outside of application memory range 100-254"),
271 "reserved id should also fail the app range check: {rendered}",
272 );
273 assert!(
274 rendered.contains("reserved for stable-structures internals"),
275 "reserved id should fail closed explicitly: {rendered}",
276 );
277 }
278
279 #[test]
280 fn stable_key_segment_policy_requires_a_lowercase_letter_prefix() {
281 for segment in ["db", "demo_rpg", "store_1", "v1"] {
282 assert!(stable_key_segment_is_canonical(segment));
283 }
284
285 for segment in [
286 "",
287 "1db",
288 "_db",
289 "Demo",
290 "demo-rpg",
291 "demo.rpg",
292 "canic.owned",
293 ] {
294 assert!(!stable_key_segment_is_canonical(segment));
295 }
296 }
297
298 #[test]
299 fn full_stable_key_policy_rejects_reserved_and_malformed_keys() {
300 assert!(stable_key_is_canonical("icydb.demo_rpg.characters.data.v1"));
301
302 for key in [
303 "canic.demo_rpg.characters.data.v1",
304 "icydb.demo_rpg.characters.data",
305 "icydb.demo-rpg.characters.data.v1",
306 "icydb.demo_rpg..data.v1",
307 "icydb.Demo.characters.data.v1",
308 "icydb.demo_rpg.characters.data.v2",
309 "icydb.1demo.characters.data.v1",
310 "icydb._demo.characters.data.v1",
311 ] {
312 assert!(!stable_key_is_canonical(key), "key should fail: {key}");
313 }
314
315 let maximum = format!("icydb.{}.data.v1", "a".repeat(114));
316 assert_eq!(maximum.len(), 128);
317 assert!(stable_key_is_canonical(&maximum));
318
319 let oversized = format!("icydb.{}.data.v1", "a".repeat(115));
320 assert_eq!(oversized.len(), 129);
321 assert!(!stable_key_is_canonical(&oversized));
322 }
323}