use crate::codegen::component::{
ComponentContractIr, ComponentMethodIr, ResolvedComponent, ResolvedProvidedContract, WireType, entry_point_symbol,
};
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use anyhow::{Result, bail};
use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
use sha2::{Digest as _, Sha256};
use std::fmt::Write as _;
use std::path::PathBuf;
pub fn generate_component_producers(api: &ApiSurface, config: &ResolvedCrateConfig) -> Result<Vec<GeneratedFile>> {
let components = crate::codegen::component::resolve_components(api, config)?;
let mut files = Vec::with_capacity(components.len() * 3);
for component in components {
for provided in &component.provides {
validate_producer_types(&provided.contract)?;
}
let crate_dir = component_crate_dir(config, &component.component_name);
files.push(GeneratedFile {
path: crate_dir.join("Cargo.toml"),
content: producer_manifest(api, config, &component),
generated_header: true,
});
files.push(GeneratedFile {
path: crate_dir.join("src/lib.rs"),
content: producer_source(api, &component)?,
generated_header: true,
});
for provided in &component.provides {
files.push(GeneratedFile {
path: crate_dir.join(format!("include/{}.h", provided.contract.name.to_snake_case())),
content: provided.contract.c_header(),
generated_header: true,
});
}
}
Ok(files)
}
fn component_crate_dir(config: &ResolvedCrateConfig, component: &str) -> PathBuf {
PathBuf::from(format!(
"crates/{}-{}-component",
config.core_crate_dir(),
component.replace('_', "-")
))
}
fn producer_manifest(api: &ApiSurface, config: &ResolvedCrateConfig, component: &ResolvedComponent) -> String {
let package = format!(
"{}-{}-component",
config.core_crate_dir(),
component.component_name.replace('_', "-")
);
let core_package = &config.name;
let core_path = format!("../{}", config.core_crate_dir());
let features = component
.features
.iter()
.map(|feature| format!("\"{feature}\""))
.collect::<Vec<_>>()
.join(", ");
format!(
r#"[package]
name = "{package}"
version = "{version}"
edition = "2024"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
alef-component-abi = "{alef_version}"
futures = "0.3"
{core_package} = {{ path = "{core_path}", version = "{version}", default-features = {default_features}, features = [{features}] }}
"#,
version = api.version,
alef_version = env!("CARGO_PKG_VERSION"),
default_features = component.default_features,
)
}
fn producer_source(api: &ApiSurface, component: &ResolvedComponent) -> Result<String> {
let has_async = component
.provides
.iter()
.any(|provided| provided.contract.methods.iter().any(|method| method.is_async));
let mut out = String::new();
writeln!(out, "// This file is auto-generated by alef. DO NOT EDIT.")?;
out.push_str("#![allow(clippy::missing_safety_doc)]\n\n");
out.push_str("use alef_component_abi::{AlefComponentV1, AlefHostApiV1, AlefOwnedBuffer, AlefStatus, AlefStr");
if has_async {
out.push_str(", AlefTaskV1");
}
out.push_str("};\n");
out.push_str("use std::ffi::{c_char, c_void};\n");
out.push_str("use std::panic::{AssertUnwindSafe, catch_unwind};\n");
out.push_str("use std::sync::Arc;\n");
if has_async {
out.push_str("use std::sync::atomic::{AtomicBool, Ordering};\n");
}
out.push('\n');
writeln!(out, "const COMPONENT_ID: &[u8] = b\"{}\";", component.component_name)?;
writeln!(out, "const COMPONENT_VERSION: &[u8] = b\"{}\";", api.version)?;
out.push('\n');
out.push_str(PRODUCER_SUPPORT);
if has_async {
out.push_str(ASYNC_PRODUCER_SUPPORT);
}
let feature_hash = byte_array(&feature_set_hash(&component.features, component.default_features));
for provided in &component.provides {
emit_contract_block(&mut out, provided, &feature_hash)?;
}
Ok(out)
}
struct ContractNames {
prefix: String,
screaming_prefix: String,
implementation_alias: String,
contract_name: String,
api_name: String,
}
impl ContractNames {
fn new(contract: &ComponentContractIr) -> Self {
let contract_name = contract.name.to_upper_camel_case();
Self {
prefix: contract.name.to_snake_case(),
screaming_prefix: contract.name.to_shouty_snake_case(),
implementation_alias: format!("{contract_name}ComponentImplementation"),
api_name: format!("{contract_name}ApiV{}", contract.interface_version),
contract_name,
}
}
}
fn emit_contract_block(out: &mut String, provided: &ResolvedProvidedContract, feature_hash: &str) -> Result<()> {
let names = ContractNames::new(&provided.contract);
emit_lifecycle_functions(out, &names, &provided.implementation)?;
emit_contract_table_and_entry(out, &names, provided, feature_hash)
}
fn emit_lifecycle_functions(out: &mut String, names: &ContractNames, implementation: &str) -> Result<()> {
let ContractNames {
prefix,
implementation_alias,
..
} = names;
writeln!(out, "type {implementation_alias} = {implementation};\n")?;
writeln!(
out,
"unsafe fn {prefix}_component_ref<'a>(instance: *mut c_void) -> Option<&'a Arc<{implementation_alias}>> {{"
)?;
out.push_str(" // SAFETY: the host supplies the handle returned by create_component.\n");
writeln!(
out,
" unsafe {{ instance.cast::<Arc<{implementation_alias}>>().as_ref() }}"
)?;
out.push_str("}\n\n");
writeln!(
out,
"unsafe extern \"C\" fn {prefix}_create_component(_host: *const AlefHostApiV1, out_instance: *mut *mut c_void, out_error: *mut AlefOwnedBuffer) -> AlefStatus {{"
)?;
out.push_str(" if out_instance.is_null() { return AlefStatus::INVALID_ARGUMENT; }\n");
writeln!(
out,
" match catch_unwind(AssertUnwindSafe(|| Arc::new({implementation_alias}::default()))) {{"
)?;
out.push_str(" Ok(instance) => {\n");
out.push_str(" // SAFETY: out_instance is non-null.\n");
out.push_str(" unsafe { *out_instance = Box::into_raw(Box::new(instance)).cast() };\n");
out.push_str(" AlefStatus::OK\n }\n");
out.push_str(" Err(_) => {\n");
out.push_str(" if !out_error.is_null() {\n");
out.push_str(" // SAFETY: out_error is non-null.\n");
out.push_str(" unsafe { *out_error = error_buffer(\"component constructor panicked\") };\n");
out.push_str(" }\n AlefStatus::INTERNAL_ERROR\n }\n }\n}\n\n");
writeln!(
out,
"unsafe extern \"C\" fn {prefix}_destroy_component(instance: *mut c_void) {{"
)?;
out.push_str(" if !instance.is_null() {\n let _ = catch_unwind(AssertUnwindSafe(|| {\n");
out.push_str(" // SAFETY: the handle was allocated by create_component.\n");
writeln!(
out,
" drop(unsafe {{ Box::from_raw(instance.cast::<Arc<{implementation_alias}>>()) }});"
)?;
out.push_str(" }));\n }\n}\n\n");
Ok(())
}
fn emit_contract_table_and_entry(
out: &mut String,
names: &ContractNames,
provided: &ResolvedProvidedContract,
feature_hash: &str,
) -> Result<()> {
let ContractNames {
prefix,
contract_name,
api_name,
..
} = names;
let contract = &provided.contract;
for method in contract.methods.iter().filter(|method| method.is_async) {
emit_async_callback_alias(out, contract_name, method)?;
}
writeln!(out, "#[repr(C)]\nstruct {api_name} {{")?;
out.push_str(" struct_size: usize,\n");
for method in &contract.methods {
writeln!(
out,
" {}: Option<{}>,",
method.name.to_snake_case(),
rust_function_type(contract_name, method)
)?;
}
out.push_str("}\n\n");
for method in &contract.methods {
emit_method_wrapper(out, &contract.trait_path, prefix, contract_name, method)?;
}
writeln!(
out,
"static mut {screaming_prefix}_CONTRACT_API: {api_name} = {api_name} {{",
screaming_prefix = names.screaming_prefix,
)?;
writeln!(out, " struct_size: std::mem::size_of::<{api_name}>(),")?;
for method in &contract.methods {
let method_name = method.name.to_snake_case();
writeln!(out, " {method_name}: Some({prefix}_component_{method_name}),")?;
}
out.push_str("};\n\n");
emit_descriptor_and_entrypoint(out, names, provided, feature_hash)
}
fn emit_descriptor_and_entrypoint(
out: &mut String,
names: &ContractNames,
provided: &ResolvedProvidedContract,
feature_hash: &str,
) -> Result<()> {
let ContractNames {
prefix,
screaming_prefix,
api_name,
..
} = names;
let contract_hash = byte_array(&provided.contract_hash);
writeln!(
out,
"static mut {screaming_prefix}_COMPONENT: AlefComponentV1 = AlefComponentV1 {{"
)?;
out.push_str(" struct_size: std::mem::size_of::<AlefComponentV1>(),\n");
out.push_str(" abi_major: 1,\n abi_minor: 0,\n");
out.push_str(
" component_id: AlefStr { ptr: COMPONENT_ID.as_ptr().cast::<c_char>(), len: COMPONENT_ID.len() },\n",
);
out.push_str(
" component_version: AlefStr { ptr: COMPONENT_VERSION.as_ptr().cast::<c_char>(), len: COMPONENT_VERSION.len() },\n",
);
writeln!(out, " contract_hash: {contract_hash},")?;
writeln!(out, " feature_set_hash: {feature_hash},")?;
writeln!(
out,
" contract: (&raw const {screaming_prefix}_CONTRACT_API).cast::<c_void>(),"
)?;
writeln!(out, " contract_size: std::mem::size_of::<{api_name}>(),")?;
writeln!(
out,
" create: Some({prefix}_create_component),\n destroy: Some({prefix}_destroy_component),\n}};\n"
)?;
writeln!(out, "#[unsafe(no_mangle)]")?;
writeln!(
out,
"pub unsafe extern \"C\" fn {}(requested_abi_major: u32, _host: *const AlefHostApiV1, out_component: *mut *const AlefComponentV1) -> AlefStatus {{",
entry_point_symbol(&provided.contract.name)
)?;
out.push_str(
" if requested_abi_major != 1 || out_component.is_null() { return AlefStatus::INCOMPATIBLE_ABI; }\n",
);
writeln!(
out,
" // SAFETY: out_component is non-null and {screaming_prefix}_COMPONENT has process lifetime."
)?;
writeln!(
out,
" unsafe {{ *out_component = &raw const {screaming_prefix}_COMPONENT }};"
)?;
out.push_str(" AlefStatus::OK\n}\n\n");
Ok(())
}
const PRODUCER_SUPPORT: &str = r#"unsafe extern "C" fn free_buffer(
_context: *mut c_void,
ptr: *mut u8,
len: usize,
capacity: usize,
) {
if !ptr.is_null() {
// SAFETY: this buffer originated from owned_buffer.
drop(unsafe { Vec::from_raw_parts(ptr, len, capacity) });
}
}
fn owned_buffer(mut bytes: Vec<u8>) -> AlefOwnedBuffer {
let buffer = AlefOwnedBuffer {
ptr: bytes.as_mut_ptr(),
len: bytes.len(),
capacity: bytes.capacity(),
context: std::ptr::null_mut(),
free: Some(free_buffer),
};
std::mem::forget(bytes);
buffer
}
fn error_buffer(message: impl ToString) -> AlefOwnedBuffer {
owned_buffer(message.to_string().into_bytes())
}
"#;
const ASYNC_PRODUCER_SUPPORT: &str = r#"
unsafe extern "C" fn cancel_task(context: *mut c_void) -> AlefStatus {
if context.is_null() {
return AlefStatus::INVALID_ARGUMENT;
}
// SAFETY: async wrappers store Box<Arc<AtomicBool>> as task context.
unsafe { &*context.cast::<Arc<AtomicBool>>() }.store(true, Ordering::Release);
AlefStatus::OK
}
unsafe extern "C" fn free_task(context: *mut c_void) {
if !context.is_null() {
// SAFETY: context was created with Box::into_raw.
drop(unsafe { Box::from_raw(context.cast::<Arc<AtomicBool>>()) });
}
}
"#;
fn validate_producer_types(contract: &ComponentContractIr) -> Result<()> {
fn supported(ty: &WireType) -> bool {
matches!(
ty,
WireType::Unit
| WireType::Bool
| WireType::U8
| WireType::U16
| WireType::U32
| WireType::U64
| WireType::I8
| WireType::I16
| WireType::I32
| WireType::I64
| WireType::F32
| WireType::F64
| WireType::Utf8
| WireType::Bytes
)
}
for method in &contract.methods {
for param in &method.params {
if !supported(¶m.ty) {
bail!(
"component producer does not yet support {:?} in parameter {}::{}",
param.ty,
method.name,
param.name
);
}
}
if !supported(&method.result) {
bail!(
"component producer does not yet support {:?} as result of {}",
method.result,
method.name
);
}
}
Ok(())
}
fn rust_abi_type(ty: &WireType, output: bool) -> String {
match ty {
WireType::Unit => "()".into(),
WireType::Bool | WireType::U8 => "u8".into(),
WireType::U16 => "u16".into(),
WireType::U32 => "u32".into(),
WireType::U64 => "u64".into(),
WireType::I8 => "i8".into(),
WireType::I16 => "i16".into(),
WireType::I32 | WireType::Enum(_) => "i32".into(),
WireType::I64 => "i64".into(),
WireType::F32 => "f32".into(),
WireType::F64 => "f64".into(),
WireType::Utf8 | WireType::Char | WireType::Path | WireType::Bytes if output => "AlefOwnedBuffer".into(),
WireType::Utf8 | WireType::Char | WireType::Path => "AlefStr".into(),
WireType::Bytes => "alef_component_abi::AlefSlice".into(),
other => panic!("unsupported producer wire type: {other:?}"),
}
}
fn rust_function_type(contract: &str, method: &ComponentMethodIr) -> String {
let mut params = vec!["*mut c_void".to_string()];
params.extend(method.params.iter().map(|param| rust_abi_type(¶m.ty, false)));
if method.is_async {
params.push(format!("{contract}{}Completion", method.name.to_upper_camel_case()));
params.push("*mut c_void".into());
params.push("*mut AlefTaskV1".into());
} else {
if method.result != WireType::Unit {
params.push(format!("*mut {}", rust_abi_type(&method.result, true)));
}
params.push("*mut AlefOwnedBuffer".into());
}
format!("unsafe extern \"C\" fn({}) -> AlefStatus", params.join(", "))
}
fn emit_async_callback_alias(out: &mut String, contract: &str, method: &ComponentMethodIr) -> Result<()> {
let result = if method.result == WireType::Unit {
String::new()
} else {
format!(", {}", rust_abi_type(&method.result, true))
};
writeln!(
out,
"type {contract}{}Completion = unsafe extern \"C\" fn(*mut c_void, AlefStatus{result}, AlefOwnedBuffer);",
method.name.to_upper_camel_case()
)?;
Ok(())
}
fn emit_method_wrapper(
out: &mut String,
trait_path: &str,
prefix: &str,
contract: &str,
method: &ComponentMethodIr,
) -> Result<()> {
let method_name = method.name.to_snake_case();
write!(
out,
"unsafe extern \"C\" fn {prefix}_component_{method_name}(instance: *mut c_void"
)?;
for param in &method.params {
write!(
out,
", {}: {}",
param.name.to_snake_case(),
rust_abi_type(¶m.ty, false)
)?;
}
if method.is_async {
write!(
out,
", completion: {contract}{}Completion, completion_context: *mut c_void, out_task: *mut AlefTaskV1",
method.name.to_upper_camel_case()
)?;
} else {
if method.result != WireType::Unit {
write!(out, ", out_result: *mut {}", rust_abi_type(&method.result, true))?;
}
out.push_str(", out_error: *mut AlefOwnedBuffer");
}
out.push_str(") -> AlefStatus {\n");
writeln!(
out,
" let Some(instance) = (unsafe {{ {prefix}_component_ref(instance) }}) else {{ return AlefStatus::INVALID_ARGUMENT; }};"
)?;
if !method.is_async && method.result != WireType::Unit {
out.push_str(" if out_result.is_null() { return AlefStatus::INVALID_ARGUMENT; }\n");
}
for param in &method.params {
emit_input_conversion(out, param)?;
}
let args = method
.params
.iter()
.map(|param| format!("{}_value", param.name.to_snake_case()))
.collect::<Vec<_>>()
.join(", ");
if method.is_async {
emit_async_body(out, trait_path, &method_name, &args, method)?;
} else {
writeln!(
out,
" let call = catch_unwind(AssertUnwindSafe(|| {trait_path}::{method_name}(&**instance, {args})));"
)?;
emit_sync_match(out, method)?;
out.push_str("}\n\n");
}
Ok(())
}
fn emit_async_body(
out: &mut String,
trait_path: &str,
method_name: &str,
args: &str,
method: &ComponentMethodIr,
) -> Result<()> {
out.push_str(" if out_task.is_null() { return AlefStatus::INVALID_ARGUMENT; }\n");
out.push_str(" let instance = Arc::clone(instance);\n");
out.push_str(" let cancelled = Arc::new(AtomicBool::new(false));\n");
out.push_str(" let task_context = Box::into_raw(Box::new(Arc::clone(&cancelled))).cast::<c_void>();\n");
out.push_str(" // SAFETY: out_task is non-null.\n");
out.push_str(" unsafe { *out_task = AlefTaskV1 { struct_size: std::mem::size_of::<AlefTaskV1>(), context: task_context, start: None, cancel: Some(cancel_task), drop: Some(free_task) } };\n");
out.push_str(" std::thread::spawn(move || {\n");
out.push_str(" if cancelled.load(Ordering::Acquire) {\n");
emit_completion(out, method, "AlefStatus::CANCELLED", None, 12)?;
out.push_str(" return;\n }\n");
writeln!(
out,
" let call = catch_unwind(AssertUnwindSafe(|| futures::executor::block_on({trait_path}::{method_name}(&*instance, {args}))));"
)?;
emit_async_match(out, method)?;
out.push_str(" });\n AlefStatus::PENDING\n}\n\n");
Ok(())
}
fn emit_input_conversion(out: &mut String, param: &crate::codegen::component::ComponentParamIr) -> Result<()> {
let name = param.name.to_snake_case();
match ¶m.ty {
WireType::Utf8 => {
writeln!(
out,
" if {name}.ptr.is_null() && {name}.len != 0 {{ return AlefStatus::INVALID_ARGUMENT; }}"
)?;
writeln!(
out,
" let {name}_bytes: &[u8] = if {name}.len == 0 {{ &[] }} else {{ unsafe {{ std::slice::from_raw_parts({name}.ptr.cast::<u8>(), {name}.len) }} }};"
)?;
writeln!(
out,
" let Ok({name}_value) = std::str::from_utf8({name}_bytes) else {{ return AlefStatus::INVALID_ARGUMENT; }};"
)?;
if !param.borrowed {
writeln!(out, " let {name}_value = {name}_value.to_owned();")?;
}
}
WireType::Char | WireType::Path => unreachable!("validated as unsupported by producer"),
WireType::Bytes => {
writeln!(
out,
" if {name}.ptr.is_null() && {name}.len != 0 {{ return AlefStatus::INVALID_ARGUMENT; }}"
)?;
writeln!(
out,
" let {name}_value: &[u8] = if {name}.len == 0 {{ &[] }} else {{ unsafe {{ std::slice::from_raw_parts({name}.ptr, {name}.len) }} }};"
)?;
if !param.borrowed {
writeln!(out, " let {name}_value = {name}_value.to_vec();")?;
}
}
WireType::Bool => writeln!(out, " let {name}_value = {name} != 0;")?,
WireType::Unit => writeln!(out, " let {name}_value = ();")?,
_ => writeln!(out, " let {name}_value = {name};")?,
}
Ok(())
}
fn emit_sync_match(out: &mut String, method: &ComponentMethodIr) -> Result<()> {
out.push_str(" match call {\n");
if method.fallible {
out.push_str(" Ok(Ok(value)) => {\n");
emit_sync_success(out, method)?;
out.push_str(" }\n Ok(Err(error)) => {\n");
out.push_str(" if !out_error.is_null() { unsafe { *out_error = error_buffer(error) }; }\n");
out.push_str(" AlefStatus::INTERNAL_ERROR\n }\n");
} else {
out.push_str(" Ok(value) => {\n");
emit_sync_success(out, method)?;
out.push_str(" }\n");
}
out.push_str(" Err(_) => {\n");
out.push_str(" if !out_error.is_null() { unsafe { *out_error = error_buffer(\"component method panicked\") }; }\n");
out.push_str(" AlefStatus::INTERNAL_ERROR\n }\n }\n");
Ok(())
}
fn emit_sync_success(out: &mut String, method: &ComponentMethodIr) -> Result<()> {
if method.result != WireType::Unit {
writeln!(
out,
" unsafe {{ *out_result = {} }};",
output_conversion(&method.result, "value")
)?;
}
out.push_str(" AlefStatus::OK\n");
Ok(())
}
fn emit_async_match(out: &mut String, method: &ComponentMethodIr) -> Result<()> {
out.push_str(" match call {\n");
if method.fallible {
out.push_str(" Ok(Ok(value)) => {\n");
emit_completion(out, method, "AlefStatus::OK", None, 16)?;
out.push_str(" }\n Ok(Err(error)) => {\n");
emit_completion(out, method, "AlefStatus::INTERNAL_ERROR", Some("error"), 16)?;
out.push_str(" }\n");
} else {
out.push_str(" Ok(value) => {\n");
emit_completion(out, method, "AlefStatus::OK", None, 16)?;
out.push_str(" }\n");
}
out.push_str(" Err(_) => {\n");
emit_completion(
out,
method,
"AlefStatus::INTERNAL_ERROR",
Some("\"component method panicked\""),
16,
)?;
out.push_str(" }\n }\n");
Ok(())
}
fn emit_completion(
out: &mut String,
method: &ComponentMethodIr,
status: &str,
error: Option<&str>,
indent: usize,
) -> Result<()> {
let pad = " ".repeat(indent);
let result = if method.result == WireType::Unit {
String::new()
} else if status == "AlefStatus::OK" {
format!(", {}", output_conversion(&method.result, "value"))
} else {
format!(", {}", zero_value(&method.result))
};
let error = error.map_or_else(
|| "AlefOwnedBuffer::EMPTY".to_string(),
|value| format!("error_buffer({value})"),
);
writeln!(
out,
"{pad}unsafe {{ completion(completion_context, {status}{result}, {error}) }};"
)?;
Ok(())
}
fn output_conversion(ty: &WireType, value: &str) -> String {
match ty {
WireType::Utf8 => format!("owned_buffer({value}.into_bytes())"),
WireType::Bytes => format!("owned_buffer({value})"),
WireType::Bool => format!("u8::from({value})"),
WireType::Unit => "()".into(),
_ => value.into(),
}
}
fn zero_value(ty: &WireType) -> String {
match ty {
WireType::Utf8 | WireType::Char | WireType::Path | WireType::Bytes => "AlefOwnedBuffer::EMPTY".into(),
WireType::F32 | WireType::F64 => "0.0".into(),
WireType::Unit => "()".into(),
_ => "0".into(),
}
}
fn feature_set_hash(features: &[String], default_features: bool) -> [u8; 32] {
let mut normalized = features.to_vec();
normalized.sort();
normalized.dedup();
let input = serde_json::json!({
"default_features": default_features,
"features": normalized,
});
let mut hasher = Sha256::new();
hasher.update(serde_json::to_vec(&input).expect("feature hash input is serializable"));
hasher.finalize().into()
}
fn byte_array(bytes: &[u8; 32]) -> String {
format!("[{}]", bytes.iter().map(u8::to_string).collect::<Vec<_>>().join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::{ComponentConfig, ComponentContractConfig, ComponentProvidesConfig};
use crate::core::ir::{MethodDef, ParamDef, ReceiverKind, TypeDef, TypeRef};
fn extractor_type() -> TypeDef {
TypeDef {
name: "Extractor".into(),
rust_path: "demo::Extractor".into(),
is_trait: true,
is_opaque: true,
methods: vec![
MethodDef {
name: "extract".into(),
params: vec![ParamDef {
name: "input".into(),
ty: TypeRef::Bytes,
is_ref: true,
..ParamDef::default()
}],
return_type: TypeRef::Bytes,
receiver: Some(ReceiverKind::Ref),
error_type: Some("Error".into()),
..MethodDef::default()
},
MethodDef {
name: "warm".into(),
is_async: true,
receiver: Some(ReceiverKind::Ref),
..MethodDef::default()
},
],
..TypeDef::default()
}
}
fn fixture() -> (ApiSurface, ResolvedCrateConfig) {
let api = ApiSurface {
crate_name: "demo".into(),
version: "1.2.3".into(),
types: vec![extractor_type()],
..ApiSurface::default()
};
let config = ResolvedCrateConfig {
name: "demo".into(),
component_contracts: vec![ComponentContractConfig {
name: "extractor".into(),
trait_path: "demo::Extractor".into(),
interface_version: 1,
}],
components: vec![ComponentConfig {
name: "pdf".into(),
provides: vec![ComponentProvidesConfig {
contract: "extractor".into(),
implementation: "demo::PdfExtractor".into(),
}],
features: vec!["pdf".into()],
default_features: false,
targets: Some(vec!["x86_64-unknown-linux-gnu".into()]),
bundled_on: Vec::new(),
}],
..ResolvedCrateConfig::default()
};
(api, config)
}
#[test]
fn emits_profile_crate_header_and_async_task() {
let (api, config) = fixture();
let files = generate_component_producers(&api, &config).unwrap();
assert_eq!(files.len(), 3);
let source = files.iter().find(|file| file.path.ends_with("src/lib.rs")).unwrap();
assert!(source.content.contains("alef_component_entry_v1_extractor"));
assert!(source.content.contains("extractor_component_extract"));
assert!(source.content.contains("std::thread::spawn"));
syn::parse_file(&source.content).expect("generated component producer must be valid Rust syntax");
let manifest = files.iter().find(|file| file.path.ends_with("Cargo.toml")).unwrap();
assert!(manifest.content.contains("features = [\"pdf\"]"));
assert!(manifest.content.contains("crate-type = [\"cdylib\"]"));
}
#[test]
fn a_component_providing_two_contracts_emits_two_entry_points() {
let mut api = ApiSurface {
crate_name: "demo".into(),
version: "1.2.3".into(),
types: vec![extractor_type()],
..ApiSurface::default()
};
api.types.push(TypeDef {
name: "Coder".into(),
rust_path: "demo::Coder".into(),
is_trait: true,
is_opaque: true,
methods: vec![MethodDef {
name: "encode".into(),
return_type: TypeRef::Bytes,
receiver: Some(ReceiverKind::Ref),
..MethodDef::default()
}],
..TypeDef::default()
});
let config = ResolvedCrateConfig {
name: "demo".into(),
component_contracts: vec![
ComponentContractConfig {
name: "extractor".into(),
trait_path: "demo::Extractor".into(),
interface_version: 1,
},
ComponentContractConfig {
name: "coder".into(),
trait_path: "demo::Coder".into(),
interface_version: 1,
},
],
components: vec![ComponentConfig {
name: "bundle".into(),
provides: vec![
ComponentProvidesConfig {
contract: "extractor".into(),
implementation: "demo::BundleExtractor".into(),
},
ComponentProvidesConfig {
contract: "coder".into(),
implementation: "demo::BundleCoder".into(),
},
],
features: vec!["bundle".into()],
default_features: false,
targets: Some(vec!["x86_64-unknown-linux-gnu".into()]),
bundled_on: Vec::new(),
}],
..ResolvedCrateConfig::default()
};
let files = generate_component_producers(&api, &config).unwrap();
assert_eq!(files.len(), 4);
let source = files.iter().find(|file| file.path.ends_with("src/lib.rs")).unwrap();
assert!(source.content.contains("alef_component_entry_v1_extractor"));
assert!(source.content.contains("alef_component_entry_v1_coder"));
assert!(source.content.contains("extractor_component_extract"));
assert!(source.content.contains("coder_component_encode"));
syn::parse_file(&source.content).expect("multi-contract component producer must be valid Rust syntax");
}
}