use std::{fmt::Write as _, fs, path::Path};
use toml::Value as TomlValue;
#[must_use]
pub fn read_config_source_or_default(
config_path: &Path,
explicit_config: bool,
default_role: Option<&str>,
) -> (String, bool) {
match fs::read_to_string(config_path) {
Ok(source) => (source, false),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
let role = default_role
.unwrap_or_else(|| panic!("Missing Canic config at {}", config_path.display()));
assert!(
!explicit_config,
"Missing explicit Canic config at {}",
config_path.display()
);
(standalone_config_source(role), true)
}
Err(err) => panic!("Failed to read {}: {err}", config_path.display()),
}
}
#[must_use]
pub fn declared_package_role(manifest_dir: &Path) -> Option<String> {
let manifest = fs::read_to_string(manifest_dir.join("Cargo.toml")).ok()?;
toml::from_str::<TomlValue>(&manifest)
.ok()?
.get("package")?
.get("metadata")?
.get("canic")?
.get("role")?
.as_str()
.map(str::to_string)
}
#[must_use]
pub fn standalone_config_source(role: &str) -> String {
assert!(
!role.is_empty() && role != "root",
"standalone Canic config requires a non-root role"
);
let role_key = toml_basic_string(role);
format!(
r#"controllers = []
app_index = []
[app]
init_mode = "enabled"
[app.whitelist]
[subnets.prime]
[subnets.prime.canisters.root]
kind = "root"
[subnets.prime.canisters.{role_key}]
kind = "singleton"
"#
)
}
fn toml_basic_string(value: &str) -> String {
let mut rendered = String::with_capacity(value.len() + 2);
rendered.push('"');
for ch in value.chars() {
match ch {
'"' => rendered.push_str("\\\""),
'\\' => rendered.push_str("\\\\"),
'\u{08}' => rendered.push_str("\\b"),
'\t' => rendered.push_str("\\t"),
'\n' => rendered.push_str("\\n"),
'\u{0c}' => rendered.push_str("\\f"),
'\r' => rendered.push_str("\\r"),
ch if ch.is_control() => {
let _ = write!(rendered, "\\u{:04X}", ch as u32);
}
ch => rendered.push(ch),
}
}
rendered.push('"');
rendered
}
#[cfg(test)]
mod tests {
use super::*;
use canic_core::bootstrap::parse_config_model;
#[test]
fn standalone_config_source_parses_for_plain_role() {
let source = standalone_config_source("sandbox_minimal");
let cfg = parse_config_model(&source).expect("generated standalone config parses");
let prime = cfg.subnets.get("prime").expect("prime subnet exists");
assert!(prime.canisters.contains_key("root"));
assert!(prime.canisters.contains_key("sandbox_minimal"));
}
#[test]
fn standalone_config_source_quotes_role_keys() {
let source = standalone_config_source("demo.role");
let cfg = parse_config_model(&source).expect("generated standalone config parses");
let prime = cfg.subnets.get("prime").expect("prime subnet exists");
assert!(prime.canisters.contains_key("demo.role"));
}
#[test]
#[should_panic(expected = "standalone Canic config requires a non-root role")]
fn standalone_config_source_rejects_root_role() {
let _ = standalone_config_source("root");
}
#[test]
fn read_config_source_or_default_generates_when_implicit_file_is_missing() {
let missing_path =
std::env::temp_dir().join(format!("canic-missing-default-{}.toml", std::process::id()));
let (source, generated) =
read_config_source_or_default(missing_path.as_path(), false, Some("test"));
assert!(generated);
assert!(source.contains("[subnets.prime.canisters.\"test\"]"));
}
#[test]
fn declared_package_role_reads_canic_metadata() {
let dir = std::env::temp_dir().join(format!("canic-role-metadata-{}", std::process::id()));
fs::create_dir_all(&dir).expect("create temp manifest dir");
fs::write(
dir.join("Cargo.toml"),
r#"[package]
name = "canister_scale"
version = "0.1.0"
edition = "2024"
[package.metadata.canic]
role = "scale_replica"
"#,
)
.expect("write manifest");
assert_eq!(
declared_package_role(&dir).as_deref(),
Some("scale_replica")
);
fs::remove_dir_all(&dir).expect("remove temp manifest dir");
}
}