1mod crate_path;
7mod db;
8
9use std::sync::Arc;
10
11use crate::{
12 build::get_schema,
13 node::{Canister, Entity, Schema, Store},
14};
15use icydb_schema::encode_schema_fragment;
16use proc_macro2::TokenStream;
17use quote::quote;
18use sha2::{Digest, Sha256};
19
20#[must_use]
28pub fn generate_with_options(canister_path: &str, options: BuildOptions) -> String {
29 let schema = get_schema().expect("schema must be valid before codegen");
31 let canister = schema
32 .cast_node::<Canister>(canister_path)
33 .expect("canister path must resolve to a canister node");
34 let fragment = schema
35 .schema_fragment_for_canister(canister_path)
36 .expect("sealed canister database closure must lower into a schema fragment");
37
38 let code = ActorBuilder::new(
40 Arc::new(schema.clone()),
41 canister.clone(),
42 options,
43 fragment,
44 );
45 drop(schema);
46 let tokens = crate_path::rewrite_icydb_path(code.generate(), options.icydb_crate_path());
47
48 tokens.to_string()
49}
50
51#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
59pub struct BuildOptions {
60 sql: BuildSqlOptions,
61 metrics: BuildMetricsOptions,
62 snapshot_enabled: bool,
63 schema_enabled: bool,
64 icydb_crate_path: Option<&'static str>,
65}
66
67impl BuildOptions {
68 #[must_use]
70 pub const fn with_sql_readonly_enabled(mut self, enabled: bool) -> Self {
71 self.sql.surfaces = self.sql.surfaces.with_readonly_enabled(enabled);
72
73 self
74 }
75
76 #[must_use]
78 pub const fn with_sql_ddl_enabled(mut self, enabled: bool) -> Self {
79 self.sql.surfaces = self.sql.surfaces.with_ddl_enabled(enabled);
80
81 self
82 }
83
84 #[must_use]
86 pub const fn with_sql_fixtures_enabled(mut self, enabled: bool) -> Self {
87 self.sql.surfaces = self.sql.surfaces.with_fixtures_enabled(enabled);
88
89 self
90 }
91
92 #[must_use]
94 pub const fn with_sql_integrity_enabled(mut self, enabled: bool) -> Self {
95 self.sql.surfaces = self.sql.surfaces.with_integrity_enabled(enabled);
96
97 self
98 }
99
100 #[must_use]
102 pub const fn with_sql_introspection_enabled(mut self, enabled: bool) -> Self {
103 self.sql.surfaces = self.sql.surfaces.with_introspection_enabled(enabled);
104
105 self
106 }
107
108 #[must_use]
110 pub const fn with_sql_update_policy(mut self, policy: Option<BuildSqlUpdatePolicy>) -> Self {
111 self.sql.update_policy = policy;
112
113 self
114 }
115
116 #[must_use]
118 pub const fn with_metrics_enabled(mut self, enabled: bool) -> Self {
119 self.metrics.enabled = enabled;
120
121 self
122 }
123
124 #[must_use]
126 pub const fn with_metrics_extended_enabled(mut self, enabled: bool) -> Self {
127 self.metrics.extended_enabled = enabled;
128
129 self
130 }
131
132 #[must_use]
134 pub const fn with_snapshot_enabled(mut self, enabled: bool) -> Self {
135 self.snapshot_enabled = enabled;
136
137 self
138 }
139
140 #[must_use]
142 pub const fn with_schema_enabled(mut self, enabled: bool) -> Self {
143 self.schema_enabled = enabled;
144
145 self
146 }
147
148 #[must_use]
154 pub const fn with_icydb_crate_path(mut self, path: &'static str) -> Self {
155 self.icydb_crate_path = Some(path);
156
157 self
158 }
159
160 #[must_use]
162 pub const fn sql_readonly_enabled(self) -> bool {
163 self.sql.surfaces.readonly_enabled()
164 }
165
166 #[must_use]
168 pub const fn sql_ddl_enabled(self) -> bool {
169 self.sql.surfaces.ddl_enabled()
170 }
171
172 #[must_use]
174 pub const fn sql_fixtures_enabled(self) -> bool {
175 self.sql.surfaces.fixtures_enabled()
176 }
177
178 #[must_use]
180 pub const fn sql_integrity_enabled(self) -> bool {
181 self.sql.surfaces.integrity_enabled()
182 }
183
184 #[must_use]
186 pub const fn sql_introspection_enabled(self) -> bool {
187 self.sql.surfaces.introspection_enabled()
188 }
189
190 #[must_use]
191 pub(crate) const fn sql_surface_flags(self) -> BuildSqlSurfaceFlags {
192 self.sql.surfaces
193 }
194
195 #[must_use]
196 const fn icydb_crate_path(self) -> Option<&'static str> {
197 self.icydb_crate_path
198 }
199
200 #[must_use]
202 pub const fn sql_update_policy(self) -> Option<BuildSqlUpdatePolicy> {
203 self.sql.update_policy
204 }
205
206 #[must_use]
208 pub const fn sql_update_enabled(self) -> bool {
209 self.sql_update_policy().is_some()
210 }
211
212 #[must_use]
214 pub const fn metrics_enabled(self) -> bool {
215 self.metrics.enabled
216 }
217
218 #[must_use]
220 pub const fn metrics_extended_enabled(self) -> bool {
221 self.metrics.enabled && self.metrics.extended_enabled
222 }
223
224 #[must_use]
226 pub const fn snapshot_enabled(self) -> bool {
227 self.snapshot_enabled
228 }
229
230 #[must_use]
232 pub const fn schema_enabled(self) -> bool {
233 self.schema_enabled
234 }
235
236 #[must_use]
238 pub const fn sql_enabled(self) -> bool {
239 self.sql_readonly_enabled()
240 || self.sql_ddl_enabled()
241 || self.sql_fixtures_enabled()
242 || self.sql_integrity_enabled()
243 || self.sql_update_enabled()
244 }
245}
246
247#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
248struct BuildSqlOptions {
249 surfaces: BuildSqlSurfaceFlags,
250 update_policy: Option<BuildSqlUpdatePolicy>,
251}
252
253#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
254pub(crate) struct BuildSqlSurfaceFlags(u8);
255
256impl BuildSqlSurfaceFlags {
257 const DDL: u8 = 1 << 1;
258 const FIXTURES: u8 = 1 << 2;
259 const INTROSPECTION: u8 = 1 << 3;
260 const INTEGRITY: u8 = 1 << 4;
261 const READONLY: u8 = 1;
262
263 #[must_use]
264 pub(crate) const fn with_readonly_enabled(self, enabled: bool) -> Self {
265 self.with_flag(Self::READONLY, enabled)
266 }
267
268 #[must_use]
269 pub(crate) const fn with_ddl_enabled(self, enabled: bool) -> Self {
270 self.with_flag(Self::DDL, enabled)
271 }
272
273 #[must_use]
274 pub(crate) const fn with_fixtures_enabled(self, enabled: bool) -> Self {
275 self.with_flag(Self::FIXTURES, enabled)
276 }
277
278 #[must_use]
279 pub(crate) const fn with_integrity_enabled(self, enabled: bool) -> Self {
280 self.with_flag(Self::INTEGRITY, enabled)
281 }
282
283 #[must_use]
284 pub(crate) const fn with_introspection_enabled(self, enabled: bool) -> Self {
285 self.with_flag(Self::INTROSPECTION, enabled)
286 }
287
288 #[must_use]
289 pub(crate) const fn readonly_enabled(self) -> bool {
290 self.contains(Self::READONLY)
291 }
292
293 #[must_use]
294 pub(crate) const fn ddl_enabled(self) -> bool {
295 self.contains(Self::DDL)
296 }
297
298 #[must_use]
299 pub(crate) const fn fixtures_enabled(self) -> bool {
300 self.contains(Self::FIXTURES)
301 }
302
303 #[must_use]
304 pub(crate) const fn integrity_enabled(self) -> bool {
305 self.contains(Self::INTEGRITY)
306 }
307
308 #[must_use]
309 pub(crate) const fn introspection_enabled(self) -> bool {
310 self.contains(Self::INTROSPECTION)
311 }
312
313 #[must_use]
314 const fn contains(self, flag: u8) -> bool {
315 self.0 & flag == flag
316 }
317
318 #[must_use]
319 const fn with_flag(self, flag: u8, enabled: bool) -> Self {
320 if enabled {
321 Self(self.0 | flag)
322 } else {
323 Self(self.0 & !flag)
324 }
325 }
326}
327
328#[derive(Clone, Copy, Debug, Eq, PartialEq)]
329struct BuildMetricsOptions {
330 enabled: bool,
331 extended_enabled: bool,
332}
333
334impl Default for BuildMetricsOptions {
335 fn default() -> Self {
336 Self {
337 enabled: true,
338 extended_enabled: false,
339 }
340 }
341}
342
343#[derive(Clone, Copy, Debug, Eq, PartialEq)]
346pub enum BuildSqlUpdatePolicy {
347 PublicPrimaryKeyOnly,
349 PublicBoundedDeterministic,
351}
352
353#[macro_export]
362macro_rules! build_with_options {
363 ($actor:expr, $options:expr) => {
364 use std::{env::var, fs::File, io::Write, path::PathBuf};
365
366 println!("cargo:rerun-if-changed=build.rs");
369 println!("cargo:rustc-check-cfg=cfg(icydb)");
370 println!("cargo:rustc-check-cfg=cfg(feature, values(\"sql\"))");
371 println!("cargo:rustc-cfg=icydb");
372
373 let out_dir = var("OUT_DIR").expect("OUT_DIR not set");
375 let output = $crate::build::generate_with_options($actor, $options);
376 let actor_file = PathBuf::from(out_dir.clone()).join("actor.rs");
377 let mut file = File::create(actor_file)?;
378 file.write_all(output.as_bytes())?;
379 };
380}
381
382pub(crate) struct ActorBuilder {
390 pub(crate) schema: Arc<Schema>,
391 pub(crate) canister: Canister,
392 pub(crate) options: BuildOptions,
393 pub(crate) schema_fragment_bytes: Vec<u8>,
394 pub(crate) schema_submission_key: String,
395}
396
397impl ActorBuilder {
398 #[must_use]
400 pub fn new(
401 schema: Arc<Schema>,
402 canister: Canister,
403 options: BuildOptions,
404 fragment: icydb_schema::SchemaFragment,
405 ) -> Self {
406 let schema_fragment_bytes =
407 encode_schema_fragment(&fragment).expect("sealed schema fragment must encode");
408 let digest = Sha256::digest(schema_fragment_bytes.as_slice());
409 let schema_submission_key = format!("generated/{}", hex_bytes(digest.as_slice()));
410
411 Self {
412 schema,
413 canister,
414 options,
415 schema_fragment_bytes,
416 schema_submission_key,
417 }
418 }
419
420 #[must_use]
422 pub fn generate(self) -> TokenStream {
423 let mut tokens = quote!();
424
425 tokens.extend(db::generate(&self));
427 tokens.extend(generate_snapshot(&self));
428 tokens.extend(generate_metrics(&self));
429
430 quote! {
431 #tokens
432 }
433 }
434
435 #[must_use]
437 pub fn get_stores(&self) -> Vec<(String, Store)> {
438 let canister_path = self.canister_path();
439
440 self.schema
441 .filter_nodes::<Store>(|node| node.canister() == canister_path)
442 .map(|(path, store)| (path.to_string(), store.clone()))
443 .collect()
444 }
445
446 #[must_use]
448 pub fn get_entities(&self) -> Vec<(String, Entity)> {
449 let canister_path = self.canister_path();
450
451 self.schema
452 .get_nodes::<Entity>()
453 .filter_map(|(path, entity)| {
454 let store = self.schema.cast_node::<Store>(entity.store()).ok()?;
455 if store.canister() == canister_path {
456 Some((path.to_string(), entity.clone()))
457 } else {
458 None
459 }
460 })
461 .collect()
462 }
463
464 fn canister_path(&self) -> String {
465 self.canister.def().path()
466 }
467}
468
469fn hex_bytes(bytes: &[u8]) -> String {
470 const HEX: &[u8; 16] = b"0123456789abcdef";
471 let mut encoded = String::with_capacity(bytes.len() * 2);
472 for byte in bytes {
473 encoded.push(char::from(HEX[usize::from(byte >> 4)]));
474 encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
475 }
476 encoded
477}
478
479#[must_use]
481fn generate_snapshot(builder: &ActorBuilder) -> TokenStream {
482 if builder.options.snapshot_enabled() {
483 quote! {
484 #[::icydb::__reexports::ic_cdk::query(name = "icydb_snapshot")]
485 pub fn __icydb_snapshot() -> Result<::icydb::db::StorageReport, ::icydb::Error> {
486 ::icydb::__macro::execute_generated_storage_report(&db()?)
487 }
488 }
489 } else {
490 TokenStream::new()
491 }
492}
493
494#[must_use]
496fn generate_metrics(builder: &ActorBuilder) -> TokenStream {
497 let metrics_endpoint = builder.options.metrics_enabled().then(|| {
498 quote! {
499 #[::icydb::__reexports::ic_cdk::query(name = "icydb_metrics")]
500 pub fn __icydb_metrics(window_start_ms: Option<u64>) -> Result<::icydb::metrics::CompactMetricsReport, ::icydb::Error> {
501 Ok(::icydb::metrics::compact_metrics_report(window_start_ms))
502 }
503 }
504 });
505
506 let metrics_extended_endpoint = builder.options.metrics_extended_enabled().then(|| {
507 quote! {
508 #[::icydb::__reexports::ic_cdk::query(name = "icydb_metrics_extended")]
509 pub fn __icydb_metrics_extended(window_start_ms: Option<u64>) -> Result<::icydb::metrics::EventReport, ::icydb::Error> {
510 Ok(::icydb::metrics::metrics_report(window_start_ms))
511 }
512 }
513 });
514
515 let metrics_reset_endpoint = builder.options.metrics_enabled().then(|| {
516 quote! {
517 #[::icydb::__reexports::ic_cdk::update(name = "icydb_metrics_reset")]
518 pub fn __icydb_metrics_reset() -> Result<(), ::icydb::Error> {
519 ::icydb::metrics::metrics_reset_all();
520
521 Ok(())
522 }
523 }
524 });
525
526 quote! {
527 #metrics_endpoint
528 #metrics_extended_endpoint
529 #metrics_reset_endpoint
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use std::sync::Arc;
536
537 use crate::node::{Canister, Def, Schema};
538 use proc_macro2::TokenStream;
539
540 use super::{ActorBuilder, BuildOptions};
541
542 fn compact_tokens(tokens: TokenStream) -> String {
543 tokens
544 .to_string()
545 .chars()
546 .filter(|character| !character.is_whitespace())
547 .collect()
548 }
549
550 fn actor_builder_with_options(options: BuildOptions) -> ActorBuilder {
551 ActorBuilder::new(
552 Arc::new(Schema::new()),
553 Canister::new(Def::new("test", "Canister"), "test", 0, 1, 2, 3),
554 options,
555 icydb_schema::SchemaFragment::try_new(Vec::new(), Vec::new())
556 .expect("empty test fragment should admit"),
557 )
558 }
559
560 #[test]
561 fn default_build_options_enable_minimal_metrics_only() {
562 let options = BuildOptions::default();
563
564 assert!(!options.sql_readonly_enabled());
565 assert!(!options.sql_ddl_enabled());
566 assert!(!options.sql_fixtures_enabled());
567 assert!(!options.sql_integrity_enabled());
568 assert!(!options.sql_introspection_enabled());
569 assert!(!options.sql_update_enabled());
570 assert_eq!(options.sql_update_policy(), None);
571 assert!(options.metrics_enabled());
572 assert!(!options.metrics_extended_enabled());
573 assert!(!options.snapshot_enabled());
574 assert!(!options.schema_enabled());
575 }
576
577 #[test]
578 fn extended_metrics_requires_metrics_surface() {
579 let options = BuildOptions::default()
580 .with_metrics_enabled(false)
581 .with_metrics_extended_enabled(true);
582
583 assert!(!options.metrics_enabled());
584 assert!(!options.metrics_extended_enabled());
585
586 let options = options.with_metrics_enabled(true);
587
588 assert!(options.metrics_enabled());
589 assert!(options.metrics_extended_enabled());
590 }
591
592 #[test]
593 fn generated_metrics_surface_uses_public_icydb_endpoint_names() {
594 let builder =
595 actor_builder_with_options(BuildOptions::default().with_metrics_extended_enabled(true));
596 let surface = compact_tokens(super::generate_metrics(&builder));
597
598 assert!(surface.contains("name=\"icydb_metrics\""));
599 assert!(surface.contains("name=\"icydb_metrics_extended\""));
600 assert!(surface.contains("name=\"icydb_metrics_reset\""));
601 assert!(surface.contains("pubfn__icydb_metrics("));
602 assert!(surface.contains("pubfn__icydb_metrics_extended("));
603 assert!(surface.contains("pubfn__icydb_metrics_reset("));
604 }
605
606 #[test]
607 fn generated_snapshot_surface_uses_public_icydb_endpoint_name() {
608 let builder =
609 actor_builder_with_options(BuildOptions::default().with_snapshot_enabled(true));
610 let surface = compact_tokens(super::generate_snapshot(&builder));
611
612 assert!(surface.contains("name=\"icydb_snapshot\""));
613 assert!(surface.contains("pubfn__icydb_snapshot("));
614 }
615}