use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct HostCapsuleTypeConfig {
pub host_type: String,
#[serde(default)]
pub package: String,
#[serde(default)]
pub package_version: String,
#[serde(default)]
pub construct_expr: String,
}
impl HostCapsuleTypeConfig {
pub fn construct(&self, ptr_expr: &str, default_expr: &str) -> String {
let template = if self.construct_expr.is_empty() {
default_expr
} else {
self.construct_expr.as_str()
};
template.replace("{ptr}", ptr_expr)
}
pub fn construct_required(&self, ptr_expr: &str, type_name: &str, backend: &str) -> Result<String, anyhow::Error> {
if self.construct_expr.is_empty() {
anyhow::bail!(
"capsule type `{type_name}` in backend `{backend}`: \
`construct_expr` is required but not set in alef.toml — \
add `construct_expr = \"<expr using {{ptr}}>\"` under \
`[crates.{backend}.capsule_types.{type_name}]`"
);
}
Ok(self.construct_expr.replace("{ptr}", ptr_expr))
}
pub fn required_host_type(&self, type_name: &str, backend: &str) -> Result<&str, anyhow::Error> {
if self.host_type.is_empty() {
anyhow::bail!(
"capsule type `{type_name}` in backend `{backend}`: \
`host_type` is required but not set in alef.toml — \
add `host_type = \"<language type>\"` under \
`[crates.{backend}.capsule_types.{type_name}]`"
);
}
Ok(&self.host_type)
}
}
pub fn zig_capsule_import_name(host_type: &str) -> Option<&str> {
let qualified = host_type.split_whitespace().find(|token| token.contains('.'))?;
let qualified = qualified.trim_start_matches(['?', '*']);
if qualified.is_empty() || !qualified.contains('.') {
return None;
}
qualified.split('.').next()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_cfg(host_type: &str, construct_expr: &str) -> HostCapsuleTypeConfig {
HostCapsuleTypeConfig {
host_type: host_type.to_string(),
package: String::new(),
package_version: String::new(),
construct_expr: construct_expr.to_string(),
}
}
#[test]
fn construct_required_substitutes_ptr_placeholder() {
let cfg = make_cfg("*my_pkg.Language", "my_pkg.NewLanguage(unsafe.Pointer({ptr}))");
assert_eq!(
cfg.construct_required("ptr", "Language", "go").unwrap(),
"my_pkg.NewLanguage(unsafe.Pointer(ptr))"
);
}
#[test]
fn construct_required_errors_when_construct_expr_empty() {
let cfg = make_cfg("*my_pkg.Language", "");
let err = cfg.construct_required("ptr", "Language", "go").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("construct_expr"), "error must mention the field: {msg}");
assert!(msg.contains("Language"), "error must name the type: {msg}");
assert!(msg.contains("go"), "error must name the backend: {msg}");
}
#[test]
fn required_host_type_returns_value_when_set() {
let cfg = make_cfg("my_pkg.Language", "my_pkg.NewLanguage({ptr})");
assert_eq!(cfg.required_host_type("Language", "go").unwrap(), "my_pkg.Language");
}
#[test]
fn required_host_type_errors_when_empty() {
let cfg = make_cfg("", "my_pkg.NewLanguage({ptr})");
let err = cfg.required_host_type("Language", "swift").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("host_type"), "error must mention the field: {msg}");
assert!(msg.contains("Language"), "error must name the type: {msg}");
assert!(msg.contains("swift"), "error must name the backend: {msg}");
}
#[test]
fn zig_capsule_import_name_extracts_module_from_qualified_type() {
assert_eq!(zig_capsule_import_name("?*const my_module.Language"), Some("my_module"));
}
#[test]
fn zig_capsule_import_name_returns_none_for_unqualified_type() {
assert_eq!(zig_capsule_import_name("Language"), None);
}
}