use crate::config::{CallConfig, E2eConfig};
use crate::escape::{escape_c, sanitize_filename, sanitize_ident};
use crate::field_access::FieldResolver;
use crate::fixture::{Assertion, Fixture, FixtureGroup};
use alef_core::backend::GeneratedFile;
use alef_core::config::ResolvedCrateConfig;
use alef_core::hash::{self, CommentStyle};
use anyhow::Result;
use heck::{ToPascalCase, ToSnakeCase};
use std::collections::HashMap;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;
use super::E2eCodegen;
pub struct CCodegen;
impl E2eCodegen for CCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
) -> Result<Vec<GeneratedFile>> {
let lang = self.language_name();
let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
let mut files = Vec::new();
let call = &e2e_config.call;
let overrides = call.overrides.get(lang);
let result_var = &call.result_var;
let prefix = overrides
.and_then(|o| o.prefix.as_ref())
.cloned()
.or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
.unwrap_or_default();
let header = overrides
.and_then(|o| o.header.as_ref())
.cloned()
.unwrap_or_else(|| config.ffi_header_name());
let c_pkg = e2e_config.resolve_package("c");
let lib_name = c_pkg
.as_ref()
.and_then(|p| p.name.as_ref())
.cloned()
.unwrap_or_else(|| config.ffi_lib_name());
let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
.iter()
.filter_map(|group| {
let active: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|f| super::should_include_fixture(f, lang, e2e_config))
.filter(|f| f.visitor.is_none())
.collect();
if active.is_empty() { None } else { Some((group, active)) }
})
.collect();
let ffi_crate_path = c_pkg
.as_ref()
.and_then(|p| p.path.as_ref())
.cloned()
.unwrap_or_else(|| config.ffi_crate_path());
let category_names: Vec<String> = active_groups
.iter()
.map(|(g, _)| sanitize_filename(&g.category))
.collect();
files.push(GeneratedFile {
path: output_base.join("Makefile"),
content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
generated_header: true,
});
let github_repo = config.github_repo();
let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
let ffi_pkg_name = e2e_config
.registry
.packages
.get("c")
.and_then(|p| p.name.as_ref())
.cloned()
.unwrap_or_else(|| lib_name.clone());
files.push(GeneratedFile {
path: output_base.join("download_ffi.sh"),
content: render_download_script(&github_repo, &version, &ffi_pkg_name),
generated_header: true,
});
files.push(GeneratedFile {
path: output_base.join("test_runner.h"),
content: render_test_runner_header(&active_groups),
generated_header: true,
});
files.push(GeneratedFile {
path: output_base.join("main.c"),
content: render_main_c(&active_groups),
generated_header: true,
});
let field_resolver = FieldResolver::new(
&e2e_config.fields,
&e2e_config.fields_optional,
&e2e_config.result_fields,
&e2e_config.fields_array,
&std::collections::HashSet::new(),
);
for (group, active) in &active_groups {
let filename = format!("test_{}.c", sanitize_filename(&group.category));
let content = render_test_file(
&group.category,
active,
&header,
&prefix,
result_var,
e2e_config,
lang,
&field_resolver,
);
files.push(GeneratedFile {
path: output_base.join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"c"
}
}
struct ResolvedCallInfo {
function_name: String,
result_type_name: String,
options_type_name: String,
client_factory: Option<String>,
args: Vec<crate::config::ArgMapping>,
raw_c_result_type: Option<String>,
c_free_fn: Option<String>,
result_is_option: bool,
}
fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
let overrides = call.overrides.get(lang);
let function_name = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call.function.clone());
let result_type_name = overrides
.and_then(|o| o.result_type.as_ref())
.cloned()
.unwrap_or_else(|| call.function.to_pascal_case());
let options_type_name = overrides
.and_then(|o| o.options_type.as_deref())
.unwrap_or("ConversionOptions")
.to_string();
let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
let result_is_option = overrides
.and_then(|o| if o.result_is_option { Some(true) } else { None })
.unwrap_or(call.result_is_option);
ResolvedCallInfo {
function_name,
result_type_name,
options_type_name,
client_factory,
args: call.args.clone(),
raw_c_result_type,
c_free_fn,
result_is_option,
}
}
fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
let call = e2e_config.resolve_call(fixture.call.as_deref());
let mut info = resolve_call_info(call, lang);
if info.client_factory.is_none() {
let default_overrides = e2e_config.call.overrides.get(lang);
if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
info.client_factory = Some(factory.clone());
}
}
info
}
fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
let mut out = String::new();
out.push_str(&hash::header(CommentStyle::Hash));
let _ = writeln!(out, "CC = gcc");
let _ = writeln!(out, "FFI_DIR = ffi");
let _ = writeln!(out);
let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
let _ = writeln!(
out,
" LDFLAGS = -L$(FFI_DIR)/lib -l{lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
);
let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
let _ = writeln!(
out,
" LDFLAGS = -L../../target/release -l{lib_name} -Wl,-rpath,../../target/release"
);
let _ = writeln!(out, "else");
let _ = writeln!(
out,
" CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
);
let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
let _ = writeln!(out, "endif");
let _ = writeln!(out);
let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
let srcs = src_files.join(" ");
let _ = writeln!(out, "SRCS = main.c {srcs}");
let _ = writeln!(out, "TARGET = run_tests");
let _ = writeln!(out);
let _ = writeln!(out, ".PHONY: all clean test");
let _ = writeln!(out);
let _ = writeln!(out, "all: $(TARGET)");
let _ = writeln!(out);
let _ = writeln!(out, "$(TARGET): $(SRCS)");
let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
let _ = writeln!(out);
let _ = writeln!(out, "test: $(TARGET)");
let _ = writeln!(out, "\t./$(TARGET)");
let _ = writeln!(out);
let _ = writeln!(out, "clean:");
let _ = writeln!(out, "\trm -f $(TARGET)");
out
}
fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
let mut out = String::new();
let _ = writeln!(out, "#!/usr/bin/env bash");
out.push_str(&hash::header(CommentStyle::Hash));
let _ = writeln!(out, "set -euo pipefail");
let _ = writeln!(out);
let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
let _ = writeln!(out, "VERSION=\"{version}\"");
let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
let _ = writeln!(out, "FFI_DIR=\"ffi\"");
let _ = writeln!(out);
let _ = writeln!(out, "# Detect OS and architecture.");
let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
let _ = writeln!(out);
let _ = writeln!(out, "case \"$ARCH\" in");
let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
let _ = writeln!(out, "*)");
let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
let _ = writeln!(out, " exit 1");
let _ = writeln!(out, " ;;");
let _ = writeln!(out, "esac");
let _ = writeln!(out);
let _ = writeln!(out, "case \"$OS\" in");
let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
let _ = writeln!(out, "*)");
let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
let _ = writeln!(out, " exit 1");
let _ = writeln!(out, " ;;");
let _ = writeln!(out, "esac");
let _ = writeln!(out);
let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
let _ = writeln!(
out,
"URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
);
let _ = writeln!(out);
let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
out
}
fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
let mut out = String::new();
out.push_str(&hash::header(CommentStyle::Block));
let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
let _ = writeln!(out, "#define TEST_RUNNER_H");
let _ = writeln!(out);
let _ = writeln!(out, "#include <string.h>");
let _ = writeln!(out, "#include <stdlib.h>");
let _ = writeln!(out);
let _ = writeln!(out, "/**");
let _ = writeln!(
out,
" * Compare a string against an expected value, trimming trailing whitespace."
);
let _ = writeln!(
out,
" * Returns 0 if the trimmed actual string equals the expected string."
);
let _ = writeln!(out, " */");
let _ = writeln!(
out,
"static inline int str_trim_eq(const char *actual, const char *expected) {{"
);
let _ = writeln!(
out,
" if (actual == NULL || expected == NULL) return actual != expected;"
);
let _ = writeln!(out, " size_t alen = strlen(actual);");
let _ = writeln!(
out,
" while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
);
let _ = writeln!(out, " size_t elen = strlen(expected);");
let _ = writeln!(out, " if (alen != elen) return 1;");
let _ = writeln!(out, " return memcmp(actual, expected, elen);");
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(out, "/**");
let _ = writeln!(
out,
" * Extract a string value for a given key from a JSON object string."
);
let _ = writeln!(
out,
" * Returns a heap-allocated copy of the value, or NULL if not found."
);
let _ = writeln!(out, " * Caller must free() the returned string.");
let _ = writeln!(out, " */");
let _ = writeln!(
out,
"static inline char *alef_json_get_string(const char *json, const char *key) {{"
);
let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
let _ = writeln!(out, " /* Build search pattern: \"key\": */");
let _ = writeln!(out, " size_t key_len = strlen(key);");
let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
let _ = writeln!(out, " if (!pattern) return NULL;");
let _ = writeln!(out, " pattern[0] = '\"';");
let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
let _ = writeln!(out, " pattern[key_len + 2] = ':';");
let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
let _ = writeln!(out, " const char *found = strstr(json, pattern);");
let _ = writeln!(out, " free(pattern);");
let _ = writeln!(out, " if (!found) return NULL;");
let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
let _ = writeln!(out, " found++; /* skip opening quote */");
let _ = writeln!(out, " const char *end = found;");
let _ = writeln!(out, " while (*end && *end != '\"') {{");
let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
let _ = writeln!(out, " else end++;");
let _ = writeln!(out, " }}");
let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
let _ = writeln!(out, " if (!result_str) return NULL;");
let _ = writeln!(out, " memcpy(result_str, found, val_len);");
let _ = writeln!(out, " result_str[val_len] = '\\0';");
let _ = writeln!(out, " return result_str;");
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(out, "/**");
let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
let _ = writeln!(out, " */");
let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
let _ = writeln!(out, " if (json == NULL) return 0;");
let _ = writeln!(out, " /* Skip leading whitespace */");
let _ = writeln!(
out,
" while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
);
let _ = writeln!(out, " if (*json != '[') return 0;");
let _ = writeln!(out, " json++;");
let _ = writeln!(out, " /* Skip whitespace after '[' */");
let _ = writeln!(
out,
" while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
);
let _ = writeln!(out, " if (*json == ']') return 0;");
let _ = writeln!(out, " int count = 1;");
let _ = writeln!(out, " int depth = 0;");
let _ = writeln!(out, " int in_string = 0;");
let _ = writeln!(
out,
" for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
);
let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
let _ = writeln!(
out,
" if (*json == '\"') {{ in_string = !in_string; continue; }}"
);
let _ = writeln!(out, " if (in_string) continue;");
let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
let _ = writeln!(out, " }}");
let _ = writeln!(out, " return count;");
let _ = writeln!(out, "}}");
let _ = writeln!(out);
for (group, fixtures) in active_groups {
let _ = writeln!(out, "/* Tests for category: {} */", group.category);
for fixture in fixtures {
let fn_name = sanitize_ident(&fixture.id);
let _ = writeln!(out, "void test_{fn_name}(void);");
}
let _ = writeln!(out);
}
let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
out
}
fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
let mut out = String::new();
out.push_str(&hash::header(CommentStyle::Block));
let _ = writeln!(out, "#include <stdio.h>");
let _ = writeln!(out, "#include \"test_runner.h\"");
let _ = writeln!(out);
let _ = writeln!(out, "int main(void) {{");
let _ = writeln!(out, " int passed = 0;");
let _ = writeln!(out);
for (group, fixtures) in active_groups {
let _ = writeln!(out, " /* Category: {} */", group.category);
for fixture in fixtures {
let fn_name = sanitize_ident(&fixture.id);
let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
let _ = writeln!(out, " test_{fn_name}();");
let _ = writeln!(out, " printf(\" PASSED\\n\");");
let _ = writeln!(out, " passed++;");
}
let _ = writeln!(out);
}
let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
let _ = writeln!(out, " return 0;");
let _ = writeln!(out, "}}");
out
}
#[allow(clippy::too_many_arguments)]
fn render_test_file(
category: &str,
fixtures: &[&Fixture],
header: &str,
prefix: &str,
result_var: &str,
e2e_config: &E2eConfig,
lang: &str,
field_resolver: &FieldResolver,
) -> String {
let mut out = String::new();
out.push_str(&hash::header(CommentStyle::Block));
let _ = writeln!(out, "/* E2e tests for category: {category} */");
let _ = writeln!(out);
let _ = writeln!(out, "#include <assert.h>");
let _ = writeln!(out, "#include <string.h>");
let _ = writeln!(out, "#include <stdio.h>");
let _ = writeln!(out, "#include <stdlib.h>");
let _ = writeln!(out, "#include \"{header}\"");
let _ = writeln!(out, "#include \"test_runner.h\"");
let _ = writeln!(out);
for (i, fixture) in fixtures.iter().enumerate() {
if fixture.visitor.is_some() {
panic!(
"C e2e generator: visitor pattern not supported for fixture: {}",
fixture.id
);
}
let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
render_test_function(
&mut out,
fixture,
prefix,
&call_info.function_name,
result_var,
&call_info.args,
field_resolver,
&e2e_config.fields_c_types,
&call_info.result_type_name,
&call_info.options_type_name,
call_info.client_factory.as_deref(),
call_info.raw_c_result_type.as_deref(),
call_info.c_free_fn.as_deref(),
call_info.result_is_option,
);
if i + 1 < fixtures.len() {
let _ = writeln!(out);
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn render_test_function(
out: &mut String,
fixture: &Fixture,
prefix: &str,
function_name: &str,
result_var: &str,
args: &[crate::config::ArgMapping],
field_resolver: &FieldResolver,
fields_c_types: &HashMap<String, String>,
result_type_name: &str,
options_type_name: &str,
client_factory: Option<&str>,
raw_c_result_type: Option<&str>,
c_free_fn: Option<&str>,
result_is_option: bool,
) {
let fn_name = sanitize_ident(&fixture.id);
let description = &fixture.description;
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let _ = writeln!(out, "void test_{fn_name}(void) {{");
let _ = writeln!(out, " /* {description} */");
let prefix_upper = prefix.to_uppercase();
if let Some(factory) = client_factory {
let mut request_handle_vars: Vec<(String, String)> = Vec::new();
for arg in args {
if arg.arg_type == "json_object" {
let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
options_type_name.to_string()
} else if let Some(stripped) = result_type_name.strip_suffix("Response") {
format!("{}Request", stripped)
} else {
format!("{result_type_name}Request")
};
let request_type_snake = request_type_pascal.to_snake_case();
let var_name = format!("{request_type_snake}_handle");
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let json_val = if field.is_empty() || field == "input" {
Some(&fixture.input)
} else {
fixture.input.get(field)
};
if let Some(val) = json_val {
if !val.is_null() {
let normalized = super::normalize_json_keys_to_snake_case(val);
let json_str = serde_json::to_string(&normalized).unwrap_or_default();
let escaped = escape_c(&json_str);
let _ = writeln!(
out,
" {prefix_upper}{request_type_pascal}* {var_name} = \
{prefix}_{request_type_snake}_from_json(\"{escaped}\");"
);
let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
request_handle_vars.push((arg.name.clone(), var_name));
}
}
}
}
let _ = writeln!(
out,
" {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
);
let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
let method_args = if request_handle_vars.is_empty() {
String::new()
} else {
let handles: Vec<&str> = request_handle_vars.iter().map(|(_, v)| v.as_str()).collect();
format!(", {}", handles.join(", "))
};
let call_fn = format!("{prefix}_default_client_{function_name}");
if expects_error {
let _ = writeln!(
out,
" {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
);
for (_, var_name) in &request_handle_vars {
let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
}
let _ = writeln!(out, " {prefix}_default_client_free(client);");
let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
let _ = writeln!(out, "}}");
return;
}
let _ = writeln!(
out,
" {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
);
let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
let mut intermediate_handles: Vec<(String, String)> = Vec::new();
let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
for assertion in &fixture.assertions {
if let Some(f) = &assertion.field {
if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
let resolved = field_resolver.resolve(f);
let local_var = f.replace(['.', '['], "_").replace(']', "");
let has_map_access = resolved.contains('[');
if resolved.contains('.') {
emit_nested_accessor(
out,
prefix,
resolved,
&local_var,
result_var,
fields_c_types,
&mut intermediate_handles,
result_type_name,
);
} else {
let result_type_snake = result_type_name.to_snake_case();
let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
}
accessed_fields.push((f.clone(), local_var, has_map_access));
}
}
}
for assertion in &fixture.assertions {
render_assertion(out, assertion, result_var, prefix, field_resolver, &accessed_fields);
}
for (_f, local_var, from_json) in &accessed_fields {
if *from_json {
let _ = writeln!(out, " free({local_var});");
} else {
let _ = writeln!(out, " {prefix}_free_string({local_var});");
}
}
for (handle_var, snake_type) in intermediate_handles.iter().rev() {
if snake_type == "free_string" {
let _ = writeln!(out, " {prefix}_free_string({handle_var});");
} else {
let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
}
}
let result_type_snake = result_type_name.to_snake_case();
let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
for (_, var_name) in &request_handle_vars {
let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
}
let _ = writeln!(out, " {prefix}_default_client_free(client);");
let _ = writeln!(out, "}}");
return;
}
if let Some(raw_type) = raw_c_result_type {
let args_str = if args.is_empty() {
String::new()
} else {
let parts: Vec<String> = args
.iter()
.filter_map(|arg| {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = fixture.input.get(field);
match val {
None if arg.optional => Some("NULL".to_string()),
None => None,
Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
Some(v) => Some(json_to_c(v)),
}
})
.collect();
parts.join(", ")
};
let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
if has_not_error {
match raw_type {
"char*" if !result_is_option => {
let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
}
"int32_t" => {
let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
}
"uintptr_t" => {
let _ = writeln!(
out,
" assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
);
}
_ => {}
}
}
for assertion in &fixture.assertions {
match assertion.assertion_type.as_str() {
"not_error" | "error" => {} "not_empty" => {
let _ = writeln!(
out,
" assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
);
}
"is_empty" => {
if result_is_option && raw_type == "char*" {
let _ = writeln!(
out,
" assert({result_var} == NULL && \"expected empty/null value\");"
);
} else {
let _ = writeln!(
out,
" assert(strlen({result_var}) == 0 && \"expected empty value\");"
);
}
}
"count_min" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
match raw_type {
"char*" => {
let _ = writeln!(out, " {{");
let _ = writeln!(
out,
" assert({result_var} != NULL && \"expected non-null JSON array\");"
);
let _ =
writeln!(out, " int elem_count = alef_json_array_count({result_var});");
let _ = writeln!(
out,
" assert(elem_count >= {n} && \"expected at least {n} elements\");"
);
let _ = writeln!(out, " }}");
}
_ => {
let _ = writeln!(
out,
" assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
);
}
}
}
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert({result_var} >= {c_val} && \"expected greater than or equal\");"
);
}
}
"contains" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
"contains_all" => {
if let Some(values) = &assertion.values {
for val in values {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
}
"equals" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if val.is_string() {
let _ = writeln!(
out,
" assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
);
} else {
let _ = writeln!(
out,
" assert({result_var} == {c_val} && \"equals assertion failed\");"
);
}
}
}
"not_contains" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
);
}
}
"starts_with" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
);
}
}
"is_true" => {
let _ = writeln!(out, " assert({result_var});");
}
"is_false" => {
let _ = writeln!(out, " assert(!{result_var});");
}
other => {
panic!("C e2e raw-result generator: unsupported assertion type: {other}");
}
}
}
if raw_type == "char*" {
let free_fn = c_free_fn
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{prefix}_free_string"));
if result_is_option {
let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
} else {
let _ = writeln!(out, " {free_fn}({result_var});");
}
}
let _ = writeln!(out, "}}");
return;
}
let prefixed_fn = function_name.to_string();
let mut has_options_handle = false;
for arg in args {
if arg.arg_type == "json_object" {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
if let Some(val) = fixture.input.get(field) {
if !val.is_null() {
let normalized = super::normalize_json_keys_to_snake_case(val);
let json_str = serde_json::to_string(&normalized).unwrap_or_default();
let escaped = escape_c(&json_str);
let upper = prefix.to_uppercase();
let options_type_pascal = options_type_name;
let options_type_snake = options_type_name.to_snake_case();
let _ = writeln!(
out,
" {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
);
has_options_handle = true;
}
}
}
}
let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
if expects_error {
let _ = writeln!(
out,
" {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
);
if has_options_handle {
let options_type_snake = options_type_name.to_snake_case();
let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
}
let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
let _ = writeln!(out, "}}");
return;
}
let _ = writeln!(
out,
" {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
);
let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
let mut intermediate_handles: Vec<(String, String)> = Vec::new();
for assertion in &fixture.assertions {
if let Some(f) = &assertion.field {
if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
let resolved = field_resolver.resolve(f);
let local_var = f.replace(['.', '['], "_").replace(']', "");
let has_map_access = resolved.contains('[');
if resolved.contains('.') {
emit_nested_accessor(
out,
prefix,
resolved,
&local_var,
result_var,
fields_c_types,
&mut intermediate_handles,
result_type_name,
);
} else {
let result_type_snake = result_type_name.to_snake_case();
let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
}
accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
}
}
}
for assertion in &fixture.assertions {
render_assertion(out, assertion, result_var, prefix, field_resolver, &accessed_fields);
}
for (_f, local_var, from_json) in &accessed_fields {
if *from_json {
let _ = writeln!(out, " free({local_var});");
} else {
let _ = writeln!(out, " {prefix}_free_string({local_var});");
}
}
for (handle_var, snake_type) in intermediate_handles.iter().rev() {
if snake_type == "free_string" {
let _ = writeln!(out, " {prefix}_free_string({handle_var});");
} else {
let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
}
}
if has_options_handle {
let options_type_snake = options_type_name.to_snake_case();
let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
}
let result_type_snake = result_type_name.to_snake_case();
let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
let _ = writeln!(out, "}}");
}
#[allow(clippy::too_many_arguments)]
fn emit_nested_accessor(
out: &mut String,
prefix: &str,
resolved: &str,
local_var: &str,
result_var: &str,
fields_c_types: &HashMap<String, String>,
intermediate_handles: &mut Vec<(String, String)>,
result_type_name: &str,
) {
let segments: Vec<&str> = resolved.split('.').collect();
let prefix_upper = prefix.to_uppercase();
let mut current_snake_type = result_type_name.to_snake_case();
let mut current_handle = result_var.to_string();
for (i, segment) in segments.iter().enumerate() {
let is_leaf = i + 1 == segments.len();
if let Some(bracket_pos) = segment.find('[') {
let field_name = &segment[..bracket_pos];
let key = segment[bracket_pos + 1..].trim_end_matches(']');
let field_snake = field_name.to_snake_case();
let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
let json_var = format!("{field_snake}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
let _ = writeln!(out, " assert({json_var} != NULL);");
intermediate_handles.push((json_var.clone(), "free_string".to_string()));
}
let _ = writeln!(
out,
" char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
);
return; }
let seg_snake = segment.to_snake_case();
let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
if is_leaf {
let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
} else {
let lookup_key = format!("{current_snake_type}.{seg_snake}");
let return_type_pascal = match fields_c_types.get(&lookup_key) {
Some(t) => t.clone(),
None => {
segment.to_pascal_case()
}
};
let return_snake = return_type_pascal.to_snake_case();
let handle_var = format!("{seg_snake}_handle");
if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
let _ = writeln!(
out,
" {prefix_upper}{return_type_pascal}* {handle_var} = \
{accessor_fn}({current_handle});"
);
let _ = writeln!(out, " assert({handle_var} != NULL);");
intermediate_handles.push((handle_var.clone(), return_snake.clone()));
}
current_snake_type = return_snake;
current_handle = handle_var;
}
}
}
fn build_args_string_c(
input: &serde_json::Value,
args: &[crate::config::ArgMapping],
has_options_handle: bool,
) -> String {
if args.is_empty() {
return json_to_c(input);
}
let parts: Vec<String> = args
.iter()
.filter_map(|arg| {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = input.get(field);
match val {
None if arg.optional => Some("NULL".to_string()),
None => None,
Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
Some(v) => {
if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
Some("options_handle".to_string())
} else {
Some(json_to_c(v))
}
}
}
})
.collect();
parts.join(", ")
}
fn render_assertion(
out: &mut String,
assertion: &Assertion,
result_var: &str,
ffi_prefix: &str,
_field_resolver: &FieldResolver,
accessed_fields: &[(String, String, bool)],
) {
if let Some(f) = &assertion.field {
if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
return;
}
}
let field_expr = match &assertion.field {
Some(f) if !f.is_empty() => {
accessed_fields
.iter()
.find(|(k, _, _)| k == f)
.map(|(_, local, _)| local.clone())
.unwrap_or_else(|| result_var.to_string())
}
_ => result_var.to_string(),
};
match assertion.assertion_type.as_str() {
"equals" => {
if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
if expected.is_string() {
let _ = writeln!(
out,
" assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
);
} else {
let _ = writeln!(
out,
" assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
);
}
}
}
"contains" => {
if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(
out,
" assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
"contains_all" => {
if let Some(values) = &assertion.values {
for val in values {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
}
"not_contains" => {
if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(
out,
" assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
);
}
}
"not_empty" => {
let _ = writeln!(
out,
" assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
);
}
"is_empty" => {
let _ = writeln!(
out,
" assert(strlen({field_expr}) == 0 && \"expected empty value\");"
);
}
"contains_any" => {
if let Some(values) = &assertion.values {
let _ = writeln!(out, " {{");
let _ = writeln!(out, " int found = 0;");
for val in values {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
);
}
let _ = writeln!(
out,
" assert(found && \"expected to contain at least one of the specified values\");"
);
let _ = writeln!(out, " }}");
}
}
"greater_than" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
}
}
"less_than" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
);
}
}
"less_than_or_equal" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert({field_expr} <= {c_val} && \"expected less than or equal\");"
);
}
}
"starts_with" => {
if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(
out,
" assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
);
}
}
"ends_with" => {
if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
let _ = writeln!(
out,
" strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
);
}
}
"min_length" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(
out,
" assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
);
}
}
}
"max_length" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(
out,
" assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
);
}
}
}
"count_min" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(out, " {{");
let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
let _ = writeln!(
out,
" assert({field_expr} != NULL && \"expected non-null collection JSON\");"
);
let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
let _ = writeln!(
out,
" assert(elem_count >= {n} && \"expected at least {n} elements\");"
);
let _ = writeln!(out, " }}");
}
}
}
"count_equals" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(out, " {{");
let _ = writeln!(out, " /* count_equals: count elements in array */");
let _ = writeln!(
out,
" assert({field_expr} != NULL && \"expected non-null collection JSON\");"
);
let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
let _ = writeln!(out, " }}");
}
}
}
"is_true" => {
let _ = writeln!(out, " assert({field_expr});");
}
"is_false" => {
let _ = writeln!(out, " assert(!{field_expr});");
}
"method_result" => {
if let Some(method_name) = &assertion.method {
render_method_result_assertion(
out,
result_var,
ffi_prefix,
method_name,
assertion.args.as_ref(),
assertion.return_type.as_deref(),
assertion.check.as_deref().unwrap_or("is_true"),
assertion.value.as_ref(),
);
} else {
panic!("C e2e generator: method_result assertion missing 'method' field");
}
}
"matches_regex" => {
if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(out, " {{");
let _ = writeln!(out, " regex_t _re;");
let _ = writeln!(
out,
" assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
);
let _ = writeln!(
out,
" assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
);
let _ = writeln!(out, " regfree(&_re);");
let _ = writeln!(out, " }}");
}
}
"not_error" => {
}
"error" => {
}
other => {
panic!("C e2e generator: unsupported assertion type: {other}");
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_method_result_assertion(
out: &mut String,
result_var: &str,
ffi_prefix: &str,
method_name: &str,
args: Option<&serde_json::Value>,
return_type: Option<&str>,
check: &str,
value: Option<&serde_json::Value>,
) {
let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
if return_type == Some("string") {
let _ = writeln!(out, " {{");
let _ = writeln!(out, " char* _method_result = {call_expr};");
if check == "is_error" {
let _ = writeln!(
out,
" assert(_method_result == NULL && \"expected method to return error\");"
);
let _ = writeln!(out, " }}");
return;
}
let _ = writeln!(
out,
" assert(_method_result != NULL && \"method_result returned NULL\");"
);
match check {
"contains" => {
if let Some(val) = value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
);
}
}
"equals" => {
if let Some(val) = value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
);
}
}
"is_true" => {
let _ = writeln!(
out,
" assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
);
}
"count_min" => {
if let Some(val) = value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
let _ = writeln!(
out,
" assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
);
}
}
other_check => {
panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
}
}
let _ = writeln!(out, " free(_method_result);");
let _ = writeln!(out, " }}");
return;
}
match check {
"equals" => {
if let Some(val) = value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
);
}
}
"is_true" => {
let _ = writeln!(
out,
" assert({call_expr} && \"method_result is_true assertion failed\");"
);
}
"is_false" => {
let _ = writeln!(
out,
" assert(!{call_expr} && \"method_result is_false assertion failed\");"
);
}
"greater_than_or_equal" => {
if let Some(val) = value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(
out,
" assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
);
}
}
"count_min" => {
if let Some(val) = value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(
out,
" assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
);
}
}
other_check => {
panic!("C e2e generator: unsupported method_result check type: {other_check}");
}
}
}
fn build_c_method_call(
result_var: &str,
ffi_prefix: &str,
method_name: &str,
args: Option<&serde_json::Value>,
) -> String {
let extra_args = if let Some(args_val) = args {
args_val
.as_object()
.map(|obj| {
obj.values()
.map(|v| match v {
serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
serde_json::Value::Bool(true) => "1".to_string(),
serde_json::Value::Bool(false) => "0".to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Null => "NULL".to_string(),
other => format!("\"{}\"", escape_c(&other.to_string())),
})
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default()
} else {
String::new()
};
if extra_args.is_empty() {
format!("{ffi_prefix}_{method_name}({result_var})")
} else {
format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
}
}
fn json_to_c(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
serde_json::Value::Bool(true) => "1".to_string(),
serde_json::Value::Bool(false) => "0".to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Null => "NULL".to_string(),
other => format!("\"{}\"", escape_c(&other.to_string())),
}
}