mod crate_path;
mod db;
#[allow(
dead_code,
reason = "0.217 stages the private declaration compiler before the atomic Patch 5 authority cut"
)]
mod endpoint;
use std::sync::Arc;
use crate::{
build::get_schema,
node::{Canister, Entity, Schema, Store},
};
use icydb_schema::encode_schema_fragment;
use proc_macro2::TokenStream;
use quote::quote;
use sha2::{Digest, Sha256};
#[must_use]
pub fn generate(canister_path: &str) -> String {
generate_with_crate_path(canister_path, None)
}
fn generate_with_crate_path(canister_path: &str, icydb_crate_path: Option<&str>) -> String {
let schema = get_schema().expect("schema must be valid before codegen");
let canister = schema
.cast_node::<Canister>(canister_path)
.expect("canister path must resolve to a canister node");
let fragment = schema
.schema_fragment_for_canister(canister_path)
.expect("sealed canister database closure must lower into a schema fragment");
let code = ActorBuilder::new(Arc::new(schema.clone()), canister.clone(), fragment);
drop(schema);
let tokens = crate_path::rewrite_icydb_path(code.generate(), icydb_crate_path);
tokens.to_string()
}
pub(crate) struct ActorBuilder {
pub(crate) schema: Arc<Schema>,
pub(crate) canister: Canister,
pub(crate) schema_fragment_bytes: Vec<u8>,
pub(crate) schema_submission_key: String,
}
impl ActorBuilder {
#[must_use]
pub fn new(
schema: Arc<Schema>,
canister: Canister,
fragment: icydb_schema::SchemaFragment,
) -> Self {
let schema_fragment_bytes =
encode_schema_fragment(&fragment).expect("sealed schema fragment must encode");
let digest = Sha256::digest(schema_fragment_bytes.as_slice());
let schema_submission_key = format!("generated/{}", hex_bytes(digest.as_slice()));
Self {
schema,
canister,
schema_fragment_bytes,
schema_submission_key,
}
}
#[must_use]
pub fn generate(self) -> TokenStream {
let mut tokens = quote!();
tokens.extend(db::generate(&self));
tokens.extend(generate_endpoint_runtime());
quote! {
#tokens
}
}
#[must_use]
pub fn get_stores(&self) -> Vec<(String, Store)> {
let canister_path = self.canister_path();
self.schema
.filter_nodes::<Store>(|node| node.canister() == canister_path)
.map(|(path, store)| (path.to_string(), store.clone()))
.collect()
}
#[must_use]
pub fn get_entities(&self) -> Vec<(String, Entity)> {
let canister_path = self.canister_path();
self.schema
.get_nodes::<Entity>()
.filter_map(|(path, entity)| {
let store = self.schema.cast_node::<Store>(entity.store()).ok()?;
if store.canister() == canister_path {
Some((path.to_string(), entity.clone()))
} else {
None
}
})
.collect()
}
fn canister_path(&self) -> String {
self.canister.def().path()
}
}
fn hex_bytes(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut encoded = String::with_capacity(bytes.len() * 2);
for byte in bytes {
encoded.push(char::from(HEX[usize::from(byte >> 4)]));
encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
encoded
}
fn generate_endpoint_runtime() -> TokenStream {
let authorization = endpoint::emit_endpoint_authorization_helpers();
quote! {
#authorization
#[allow(unused_imports)]
pub(crate) mod endpoint_handlers {
pub(crate) use super::__icydb_endpoint_handler_schema as schema;
::icydb::__icydb_with_sql_items! {
pub(crate) use super::__icydb_endpoint_handler_fixtures_load as fixtures_load;
pub(crate) use super::__icydb_endpoint_handler_fixtures_reset as fixtures_reset;
pub(crate) use super::__icydb_endpoint_handler_sql_ddl as sql_ddl;
pub(crate) use super::__icydb_endpoint_handler_sql_integrity as sql_integrity;
pub(crate) use super::__icydb_endpoint_handler_sql_query as sql_query;
pub(crate) use super::__icydb_endpoint_handler_sql_update_bounded as sql_update_bounded;
pub(crate) use super::__icydb_endpoint_handler_sql_update_primary_key as sql_update_primary_key;
}
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn metrics(
window_start_ms: Option<u64>,
) -> Result<::icydb::metrics::CompactMetricsReport, ::icydb::Error> {
Ok(::icydb::metrics::compact_metrics_report(window_start_ms))
}
::icydb::__icydb_with_metrics_extended_items! {
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn metrics_extended(
window_start_ms: Option<u64>,
) -> Result<::icydb::metrics::EventReport, ::icydb::Error> {
Ok(::icydb::metrics::metrics_report(window_start_ms))
}
}
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn metrics_reset() -> Result<(), ::icydb::Error> {
::icydb::metrics::metrics_reset_all();
Ok(())
}
pub(crate) fn snapshot() -> Result<::icydb::db::StorageReport, ::icydb::Error> {
::icydb::__macro::execute_generated_storage_report(&super::db()?)
}
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::node::{Canister, Def, Schema};
use proc_macro2::TokenStream;
use super::ActorBuilder;
fn compact_tokens(tokens: TokenStream) -> String {
tokens
.to_string()
.chars()
.filter(|character| !character.is_whitespace())
.collect()
}
fn actor_builder() -> ActorBuilder {
ActorBuilder::new(
Arc::new(Schema::new()),
Canister::new(Def::new("test", "Canister"), "test", 0, 1, 2, 3),
icydb_schema::SchemaFragment::try_new(Vec::new(), Vec::new())
.expect("empty test fragment should admit"),
)
}
#[test]
fn generated_endpoint_runtime_is_private_and_contains_no_export_attributes() {
let surface = compact_tokens(super::generate_endpoint_runtime());
for handler in [
"metrics",
"metrics_extended",
"metrics_reset",
"snapshot",
"schema",
"sql_query",
] {
assert!(surface.contains(handler));
}
for forbidden in [
"ic_cdk::query",
"ic_cdk::update",
"export_name",
"no_mangle",
] {
assert!(!surface.contains(forbidden));
}
}
#[test]
fn complete_generated_actor_cannot_create_a_public_endpoint() {
let surface = compact_tokens(actor_builder().generate());
assert!(surface.contains("pub(crate)modendpoint_handlers"));
assert!(surface.contains("pub(crate)modendpoint_authorization"));
for forbidden in [
"ic_cdk::query",
"ic_cdk::update",
"export_name",
"no_mangle",
] {
assert!(!surface.contains(forbidden));
}
}
}