mod service;
mod trait_bridge;
pub use service::{
service_entrypoint_symbol, service_free_symbol, service_method_symbol, service_new_symbol, service_register_symbol,
};
pub use trait_bridge::{trait_clear_symbol, trait_register_symbol, trait_unregister_symbol};
use crate::codegen::naming::pascal_to_snake;
use crate::core::config::{ResolvedCrateConfig, resolve_output_dir};
use heck::ToShoutySnakeCase;
use std::path::PathBuf;
pub struct CConsumerContext<'a> {
pub config: &'a ResolvedCrateConfig,
pub header: String,
pub lib_name: String,
pub prefix: String,
}
impl<'a> CConsumerContext<'a> {
pub fn from_config(config: &'a ResolvedCrateConfig) -> Self {
Self {
config,
header: config.ffi_header_name(),
lib_name: config.ffi_lib_name(),
prefix: config.ffi_prefix(),
}
}
}
pub fn export_type_prefix(prefix: &str) -> String {
prefix.to_shouty_snake_case()
}
pub fn handle_type(prefix: &str) -> String {
format!("{}AlefHandle", export_type_prefix(prefix))
}
pub fn free_function_symbol(prefix: &str, function_name: &str) -> String {
format!("{prefix}_{}", pascal_to_snake(function_name))
}
pub fn method_symbol(prefix: &str, type_name: &str, method_name: &str) -> String {
format!("{prefix}_{}_{method_name}", pascal_to_snake(type_name))
}
pub fn result_presence_symbol(fn_symbol: &str) -> String {
format!("{fn_symbol}_has_result")
}
pub fn stream_adapter_symbol(prefix: &str, owner_type: &str, adapter_name: &str, operation: &str) -> String {
format!("{}_{operation}", method_symbol(prefix, owner_type, adapter_name))
}
pub fn free_string_symbol(prefix: &str) -> String {
format!("{prefix}_free_string")
}
pub fn last_error_code_symbol(prefix: &str) -> String {
format!("{prefix}_last_error_code")
}
pub fn last_error_context_symbol(prefix: &str) -> String {
format!("{prefix}_last_error_context")
}
pub fn default_output_dir(config: &ResolvedCrateConfig, default: &str) -> PathBuf {
let resolved = resolve_output_dir(None, &config.name, default);
PathBuf::from(resolved)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::NewAlefConfig;
fn make_config() -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
"#,
)
.unwrap();
cfg.resolve().unwrap().remove(0)
}
#[test]
fn free_function_symbol_produces_expected_format() {
assert_eq!(free_function_symbol("htm", "parse_document"), "htm_parse_document");
}
#[test]
fn method_symbol_folds_the_owning_type_into_the_name() {
assert_eq!(
method_symbol("literllm", "DefaultClient", "chat"),
"literllm_default_client_chat"
);
}
#[test]
fn method_symbol_is_not_the_free_function_symbol() {
assert_ne!(
method_symbol("htm", "Converter", "convert"),
free_function_symbol("htm", "convert"),
);
}
#[test]
fn result_presence_symbol_suffixes_has_result() {
assert_eq!(
result_presence_symbol(&free_function_symbol("cfg", "port")),
"cfg_port_has_result"
);
assert_eq!(
result_presence_symbol(&method_symbol("cfg", "Settings", "timeout")),
"cfg_settings_timeout_has_result"
);
}
#[test]
fn stream_adapter_symbol_suffixes_the_operation() {
assert_eq!(
stream_adapter_symbol("literllm", "DefaultClient", "chat_stream", "start"),
"literllm_default_client_chat_stream_start"
);
assert_eq!(
stream_adapter_symbol("literllm", "DefaultClient", "chat_stream", "next"),
"literllm_default_client_chat_stream_next"
);
assert_eq!(
stream_adapter_symbol("literllm", "DefaultClient", "chat_stream", "free"),
"literllm_default_client_chat_stream_free"
);
}
#[test]
fn type_component_agrees_with_the_streaming_backends_heck_conversion() {
use heck::ToSnakeCase;
for type_name in [
"DefaultClient",
"Converter",
"HTMLParser",
"XMLHttpRequest",
"IOError",
"JSONLD",
"UTF8Decoder",
"Base64Encoder",
"already_snake",
] {
assert_eq!(
method_symbol("p", type_name, "m"),
format!("p_{}_m", type_name.to_snake_case()),
"`{type_name}` snake-cases differently in the two backends"
);
}
}
#[test]
fn export_type_prefix_shouty_snakes_rather_than_uppercasing() {
for (prefix, expected) in [
("demoapi", "DEMOAPI"),
("demo_api", "DEMO_API"),
("ts_pack", "TS_PACK"),
("SampleCore", "SAMPLE_CORE"),
("sampleCore", "SAMPLE_CORE"),
] {
assert_eq!(export_type_prefix(prefix), expected, "`{prefix}` mis-derived");
}
assert_ne!(
export_type_prefix("SampleCore"),
"SampleCore".to_uppercase(),
"the discriminating row stopped discriminating"
);
}
#[test]
fn handle_type_is_the_prefixed_scalar_handle() {
assert_eq!(handle_type("htm"), "HTMAlefHandle");
assert_eq!(handle_type("SampleCore"), "SAMPLE_COREAlefHandle");
assert!(!handle_type("htm").contains('*'));
}
#[test]
fn free_string_symbol_produces_expected_format() {
assert_eq!(free_string_symbol("htm"), "htm_free_string");
}
#[test]
fn last_error_code_symbol_produces_expected_format() {
assert_eq!(last_error_code_symbol("krz"), "krz_last_error_code");
}
#[test]
fn last_error_context_symbol_produces_expected_format() {
assert_eq!(last_error_context_symbol("krz"), "krz_last_error_context");
}
#[test]
fn from_config_reads_ffi_fields() {
let config = make_config();
let ctx = CConsumerContext::from_config(&config);
assert!(!ctx.header.is_empty());
assert!(!ctx.lib_name.is_empty());
assert!(!ctx.prefix.is_empty());
}
#[test]
fn default_output_dir_uses_provided_default() {
let config = make_config();
let dir = default_output_dir(&config, "packages/go/");
assert!(dir.to_string_lossy().contains("go"));
}
}