1use crate::config::{CallConfig, E2eConfig};
9use crate::escape::{escape_c, sanitize_filename, sanitize_ident};
10use crate::field_access::FieldResolver;
11use crate::fixture::{Assertion, Fixture, FixtureGroup};
12use alef_core::backend::GeneratedFile;
13use alef_core::config::AlefConfig;
14use anyhow::Result;
15use heck::{ToPascalCase, ToSnakeCase};
16use std::collections::HashMap;
17use std::fmt::Write as FmtWrite;
18use std::path::PathBuf;
19
20use super::E2eCodegen;
21
22pub struct CCodegen;
24
25impl E2eCodegen for CCodegen {
26 fn generate(
27 &self,
28 groups: &[FixtureGroup],
29 e2e_config: &E2eConfig,
30 alef_config: &AlefConfig,
31 ) -> Result<Vec<GeneratedFile>> {
32 let lang = self.language_name();
33 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
34
35 let mut files = Vec::new();
36
37 let call = &e2e_config.call;
39 let overrides = call.overrides.get(lang);
40 let result_var = &call.result_var;
41 let prefix = overrides
42 .and_then(|o| o.prefix.as_ref())
43 .cloned()
44 .or_else(|| alef_config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
45 .unwrap_or_default();
46 let header = overrides
47 .and_then(|o| o.header.as_ref())
48 .cloned()
49 .unwrap_or_else(|| format!("{}.h", call.module));
50
51 let c_pkg = e2e_config.resolve_package("c");
53 let lib_name = c_pkg
54 .as_ref()
55 .and_then(|p| p.name.as_ref())
56 .cloned()
57 .unwrap_or_else(|| call.module.clone());
58
59 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
61 .iter()
62 .filter_map(|group| {
63 let active: Vec<&Fixture> = group
64 .fixtures
65 .iter()
66 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
67 .collect();
68 if active.is_empty() { None } else { Some((group, active)) }
69 })
70 .collect();
71
72 let ffi_crate_path = c_pkg
77 .as_ref()
78 .and_then(|p| p.path.as_ref())
79 .cloned()
80 .unwrap_or_else(|| format!("../../crates/{}-ffi", alef_config.crate_config.name));
81
82 let category_names: Vec<String> = active_groups
84 .iter()
85 .map(|(g, _)| sanitize_filename(&g.category))
86 .collect();
87 files.push(GeneratedFile {
88 path: output_base.join("Makefile"),
89 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
90 generated_header: true,
91 });
92
93 let github_repo = alef_config.github_repo();
95 let version = alef_config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
96 let ffi_pkg_name = e2e_config
97 .registry
98 .packages
99 .get("c")
100 .and_then(|p| p.name.as_ref())
101 .cloned()
102 .unwrap_or_else(|| lib_name.clone());
103 files.push(GeneratedFile {
104 path: output_base.join("download_ffi.sh"),
105 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
106 generated_header: true,
107 });
108
109 files.push(GeneratedFile {
111 path: output_base.join("test_runner.h"),
112 content: render_test_runner_header(&active_groups),
113 generated_header: true,
114 });
115
116 files.push(GeneratedFile {
118 path: output_base.join("main.c"),
119 content: render_main_c(&active_groups),
120 generated_header: true,
121 });
122
123 let field_resolver = FieldResolver::new(
124 &e2e_config.fields,
125 &e2e_config.fields_optional,
126 &e2e_config.result_fields,
127 &e2e_config.fields_array,
128 );
129
130 for (group, active) in &active_groups {
134 let filename = format!("test_{}.c", sanitize_filename(&group.category));
135 let content = render_test_file(
136 &group.category,
137 active,
138 &header,
139 &prefix,
140 result_var,
141 e2e_config,
142 lang,
143 &field_resolver,
144 );
145 files.push(GeneratedFile {
146 path: output_base.join(filename),
147 content,
148 generated_header: true,
149 });
150 }
151
152 Ok(files)
153 }
154
155 fn language_name(&self) -> &'static str {
156 "c"
157 }
158}
159
160struct ResolvedCallInfo {
162 function_name: String,
163 result_type_name: String,
164 client_factory: Option<String>,
165 args: Vec<crate::config::ArgMapping>,
166}
167
168fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
169 let overrides = call.overrides.get(lang);
170 let function_name = overrides
171 .and_then(|o| o.function.as_ref())
172 .cloned()
173 .unwrap_or_else(|| call.function.clone());
174 let result_type_name = overrides
175 .and_then(|o| o.result_type.as_ref())
176 .cloned()
177 .unwrap_or_else(|| function_name.to_pascal_case());
178 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
179 ResolvedCallInfo {
180 function_name,
181 result_type_name,
182 client_factory,
183 args: call.args.clone(),
184 }
185}
186
187fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
193 let call = e2e_config.resolve_call(fixture.call.as_deref());
194 let mut info = resolve_call_info(call, lang);
195
196 if info.client_factory.is_none() {
199 let default_overrides = e2e_config.call.overrides.get(lang);
200 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
201 info.client_factory = Some(factory.clone());
202 }
203 }
204
205 info
206}
207
208fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
209 let mut out = String::new();
210 let _ = writeln!(out, "# This file is auto-generated by alef. DO NOT EDIT.");
211 let _ = writeln!(out, "CC = gcc");
212 let _ = writeln!(out, "FFI_DIR = ffi");
213 let _ = writeln!(out);
214
215 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
217 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
218 let _ = writeln!(
219 out,
220 " LDFLAGS = -L$(FFI_DIR)/lib -l{lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
221 );
222 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
223 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
224 let _ = writeln!(
225 out,
226 " LDFLAGS = -L../../target/release -l{lib_name} -Wl,-rpath,../../target/release"
227 );
228 let _ = writeln!(out, "else");
229 let _ = writeln!(
230 out,
231 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
232 );
233 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
234 let _ = writeln!(out, "endif");
235 let _ = writeln!(out);
236
237 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
238 let srcs = src_files.join(" ");
239
240 let _ = writeln!(out, "SRCS = main.c {srcs}");
241 let _ = writeln!(out, "TARGET = run_tests");
242 let _ = writeln!(out);
243 let _ = writeln!(out, ".PHONY: all clean test");
244 let _ = writeln!(out);
245 let _ = writeln!(out, "all: $(TARGET)");
246 let _ = writeln!(out);
247 let _ = writeln!(out, "$(TARGET): $(SRCS)");
248 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
249 let _ = writeln!(out);
250 let _ = writeln!(out, "test: $(TARGET)");
251 let _ = writeln!(out, "\t./$(TARGET)");
252 let _ = writeln!(out);
253 let _ = writeln!(out, "clean:");
254 let _ = writeln!(out, "\trm -f $(TARGET)");
255 out
256}
257
258fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
259 let mut out = String::new();
260 let _ = writeln!(out, "#!/usr/bin/env bash");
261 let _ = writeln!(out, "# This file is auto-generated by alef. DO NOT EDIT.");
262 let _ = writeln!(out, "set -euo pipefail");
263 let _ = writeln!(out);
264 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
265 let _ = writeln!(out, "VERSION=\"{version}\"");
266 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
267 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
268 let _ = writeln!(out);
269 let _ = writeln!(out, "# Detect OS and architecture.");
270 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
271 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
272 let _ = writeln!(out);
273 let _ = writeln!(out, "case \"$ARCH\" in");
274 let _ = writeln!(out, " x86_64|amd64) ARCH=\"x86_64\" ;;");
275 let _ = writeln!(out, " arm64|aarch64) ARCH=\"aarch64\" ;;");
276 let _ = writeln!(out, " *) echo \"Unsupported architecture: $ARCH\" >&2; exit 1 ;;");
277 let _ = writeln!(out, "esac");
278 let _ = writeln!(out);
279 let _ = writeln!(out, "case \"$OS\" in");
280 let _ = writeln!(out, " linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
281 let _ = writeln!(out, " darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
282 let _ = writeln!(out, " *) echo \"Unsupported OS: $OS\" >&2; exit 1 ;;");
283 let _ = writeln!(out, "esac");
284 let _ = writeln!(out);
285 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
286 let _ = writeln!(
287 out,
288 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
289 );
290 let _ = writeln!(out);
291 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
292 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
293 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
294 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
295 out
296}
297
298fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
299 let mut out = String::new();
300 let _ = writeln!(out, "/* This file is auto-generated by alef. DO NOT EDIT. */");
301 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
302 let _ = writeln!(out, "#define TEST_RUNNER_H");
303 let _ = writeln!(out);
304 let _ = writeln!(out, "#include <string.h>");
305 let _ = writeln!(out, "#include <stdlib.h>");
306 let _ = writeln!(out);
307 let _ = writeln!(out, "/**");
309 let _ = writeln!(
310 out,
311 " * Compare a string against an expected value, trimming trailing whitespace."
312 );
313 let _ = writeln!(
314 out,
315 " * Returns 0 if the trimmed actual string equals the expected string."
316 );
317 let _ = writeln!(out, " */");
318 let _ = writeln!(
319 out,
320 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
321 );
322 let _ = writeln!(
323 out,
324 " if (actual == NULL || expected == NULL) return actual != expected;"
325 );
326 let _ = writeln!(out, " size_t alen = strlen(actual);");
327 let _ = writeln!(
328 out,
329 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
330 );
331 let _ = writeln!(out, " size_t elen = strlen(expected);");
332 let _ = writeln!(out, " if (alen != elen) return 1;");
333 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
334 let _ = writeln!(out, "}}");
335 let _ = writeln!(out);
336
337 let _ = writeln!(out, "/**");
338 let _ = writeln!(
339 out,
340 " * Extract a string value for a given key from a JSON object string."
341 );
342 let _ = writeln!(
343 out,
344 " * Returns a heap-allocated copy of the value, or NULL if not found."
345 );
346 let _ = writeln!(out, " * Caller must free() the returned string.");
347 let _ = writeln!(out, " */");
348 let _ = writeln!(
349 out,
350 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
351 );
352 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
353 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
354 let _ = writeln!(out, " size_t key_len = strlen(key);");
355 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
356 let _ = writeln!(out, " if (!pattern) return NULL;");
357 let _ = writeln!(out, " pattern[0] = '\"';");
358 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
359 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
360 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
361 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
362 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
363 let _ = writeln!(out, " free(pattern);");
364 let _ = writeln!(out, " if (!found) return NULL;");
365 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
366 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
367 let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
368 let _ = writeln!(out, " found++; /* skip opening quote */");
369 let _ = writeln!(out, " const char *end = found;");
370 let _ = writeln!(out, " while (*end && *end != '\"') {{");
371 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
372 let _ = writeln!(out, " else end++;");
373 let _ = writeln!(out, " }}");
374 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
375 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
376 let _ = writeln!(out, " if (!result_str) return NULL;");
377 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
378 let _ = writeln!(out, " result_str[val_len] = '\\0';");
379 let _ = writeln!(out, " return result_str;");
380 let _ = writeln!(out, "}}");
381 let _ = writeln!(out);
382 let _ = writeln!(out, "/**");
383 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
384 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
385 let _ = writeln!(out, " */");
386 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
387 let _ = writeln!(out, " if (json == NULL) return 0;");
388 let _ = writeln!(out, " /* Skip leading whitespace */");
389 let _ = writeln!(
390 out,
391 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
392 );
393 let _ = writeln!(out, " if (*json != '[') return 0;");
394 let _ = writeln!(out, " json++;");
395 let _ = writeln!(out, " /* Skip whitespace after '[' */");
396 let _ = writeln!(
397 out,
398 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
399 );
400 let _ = writeln!(out, " if (*json == ']') return 0;");
401 let _ = writeln!(out, " int count = 1;");
402 let _ = writeln!(out, " int depth = 0;");
403 let _ = writeln!(out, " int in_string = 0;");
404 let _ = writeln!(
405 out,
406 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
407 );
408 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
409 let _ = writeln!(
410 out,
411 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
412 );
413 let _ = writeln!(out, " if (in_string) continue;");
414 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
415 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
416 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
417 let _ = writeln!(out, " }}");
418 let _ = writeln!(out, " return count;");
419 let _ = writeln!(out, "}}");
420 let _ = writeln!(out);
421
422 for (group, fixtures) in active_groups {
423 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
424 for fixture in fixtures {
425 let fn_name = sanitize_ident(&fixture.id);
426 let _ = writeln!(out, "void test_{fn_name}(void);");
427 }
428 let _ = writeln!(out);
429 }
430
431 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
432 out
433}
434
435fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
436 let mut out = String::new();
437 let _ = writeln!(out, "/* This file is auto-generated by alef. DO NOT EDIT. */");
438 let _ = writeln!(out, "#include <stdio.h>");
439 let _ = writeln!(out, "#include \"test_runner.h\"");
440 let _ = writeln!(out);
441 let _ = writeln!(out, "int main(void) {{");
442 let _ = writeln!(out, " int passed = 0;");
443 let _ = writeln!(out, " int failed = 0;");
444 let _ = writeln!(out);
445
446 for (group, fixtures) in active_groups {
447 let _ = writeln!(out, " /* Category: {} */", group.category);
448 for fixture in fixtures {
449 let fn_name = sanitize_ident(&fixture.id);
450 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
451 let _ = writeln!(out, " test_{fn_name}();");
452 let _ = writeln!(out, " printf(\" PASSED\\n\");");
453 let _ = writeln!(out, " passed++;");
454 }
455 let _ = writeln!(out);
456 }
457
458 let _ = writeln!(
459 out,
460 " printf(\"\\nResults: %d passed, %d failed\\n\", passed, failed);"
461 );
462 let _ = writeln!(out, " return failed > 0 ? 1 : 0;");
463 let _ = writeln!(out, "}}");
464 out
465}
466
467#[allow(clippy::too_many_arguments)]
468fn render_test_file(
469 category: &str,
470 fixtures: &[&Fixture],
471 header: &str,
472 prefix: &str,
473 result_var: &str,
474 e2e_config: &E2eConfig,
475 lang: &str,
476 field_resolver: &FieldResolver,
477) -> String {
478 let mut out = String::new();
479 let _ = writeln!(out, "/* This file is auto-generated by alef. DO NOT EDIT. */");
480 let _ = writeln!(out, "/* E2e tests for category: {category} */");
481 let _ = writeln!(out);
482 let _ = writeln!(out, "#include <assert.h>");
483 let _ = writeln!(out, "#include <string.h>");
484 let _ = writeln!(out, "#include <stdio.h>");
485 let _ = writeln!(out, "#include <stdlib.h>");
486 let _ = writeln!(out, "#include \"{header}\"");
487 let _ = writeln!(out, "#include \"test_runner.h\"");
488 let _ = writeln!(out);
489
490 for (i, fixture) in fixtures.iter().enumerate() {
491 if fixture.visitor.is_some() {
493 let _ = writeln!(
494 out,
495 "/* TODO: {} - visitor pattern not supported in C yet */",
496 fixture.id
497 );
498 continue;
499 }
500
501 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
502 render_test_function(
503 &mut out,
504 fixture,
505 prefix,
506 &call_info.function_name,
507 result_var,
508 &call_info.args,
509 field_resolver,
510 &e2e_config.fields_c_types,
511 &call_info.result_type_name,
512 call_info.client_factory.as_deref(),
513 );
514 if i + 1 < fixtures.len() {
515 let _ = writeln!(out);
516 }
517 }
518
519 out
520}
521
522#[allow(clippy::too_many_arguments)]
523fn render_test_function(
524 out: &mut String,
525 fixture: &Fixture,
526 prefix: &str,
527 function_name: &str,
528 result_var: &str,
529 args: &[crate::config::ArgMapping],
530 field_resolver: &FieldResolver,
531 fields_c_types: &HashMap<String, String>,
532 result_type_name: &str,
533 client_factory: Option<&str>,
534) {
535 let fn_name = sanitize_ident(&fixture.id);
536 let description = &fixture.description;
537
538 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
539
540 let _ = writeln!(out, "void test_{fn_name}(void) {{");
541 let _ = writeln!(out, " /* {description} */");
542
543 let prefix_upper = prefix.to_uppercase();
544
545 if let Some(factory) = client_factory {
550 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); for arg in args {
553 if arg.arg_type == "json_object" {
554 let request_type_pascal = if let Some(stripped) = result_type_name.strip_suffix("Response") {
557 format!("{}Request", stripped)
558 } else {
559 format!("{result_type_name}Request")
560 };
561 let request_type_snake = request_type_pascal.to_snake_case();
562 let var_name = format!("{request_type_snake}_handle");
563
564 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
565 let json_val = if field.is_empty() || field == "input" {
566 Some(&fixture.input)
567 } else {
568 fixture.input.get(field)
569 };
570
571 if let Some(val) = json_val {
572 if !val.is_null() {
573 let normalized = super::normalize_json_keys_to_snake_case(val);
574 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
575 let escaped = escape_c(&json_str);
576 let _ = writeln!(
577 out,
578 " {prefix_upper}{request_type_pascal}* {var_name} = \
579 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
580 );
581 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
582 request_handle_vars.push((arg.name.clone(), var_name));
583 }
584 }
585 }
586 }
587
588 let _ = writeln!(
589 out,
590 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
591 );
592 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
593
594 let method_args = if request_handle_vars.is_empty() {
595 String::new()
596 } else {
597 let handles: Vec<&str> = request_handle_vars.iter().map(|(_, v)| v.as_str()).collect();
598 format!(", {}", handles.join(", "))
599 };
600
601 let call_fn = format!("{prefix}_default_client_{function_name}");
602
603 if expects_error {
604 let _ = writeln!(
605 out,
606 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
607 );
608 for (_, var_name) in &request_handle_vars {
609 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
610 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
611 }
612 let _ = writeln!(out, " {prefix}_default_client_free(client);");
613 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
614 let _ = writeln!(out, "}}");
615 return;
616 }
617
618 let _ = writeln!(
619 out,
620 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
621 );
622 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
623
624 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
625 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
626
627 for assertion in &fixture.assertions {
628 if let Some(f) = &assertion.field {
629 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
630 let resolved = field_resolver.resolve(f);
631 let local_var = f.replace(['.', '['], "_").replace(']', "");
632 let has_map_access = resolved.contains('[');
633 if resolved.contains('.') {
634 emit_nested_accessor(
635 out,
636 prefix,
637 resolved,
638 &local_var,
639 result_var,
640 fields_c_types,
641 &mut intermediate_handles,
642 result_type_name,
643 );
644 } else {
645 let result_type_snake = result_type_name.to_snake_case();
646 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
647 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
648 }
649 accessed_fields.push((f.clone(), local_var, has_map_access));
650 }
651 }
652 }
653
654 for assertion in &fixture.assertions {
655 render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
656 }
657
658 for (_f, local_var, from_json) in &accessed_fields {
659 if *from_json {
660 let _ = writeln!(out, " free({local_var});");
661 } else {
662 let _ = writeln!(out, " {prefix}_free_string({local_var});");
663 }
664 }
665 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
666 if snake_type == "free_string" {
667 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
668 } else {
669 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
670 }
671 }
672 let result_type_snake = result_type_name.to_snake_case();
673 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
674 for (_, var_name) in &request_handle_vars {
675 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
676 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
677 }
678 let _ = writeln!(out, " {prefix}_default_client_free(client);");
679 let _ = writeln!(out, "}}");
680 return;
681 }
682
683 let prefixed_fn = function_name.to_string();
689
690 let mut has_options_handle = false;
692 for arg in args {
693 if arg.arg_type == "json_object" {
694 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
695 if let Some(val) = fixture.input.get(field) {
696 if !val.is_null() {
697 let normalized = super::normalize_json_keys_to_snake_case(val);
701 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
702 let escaped = escape_c(&json_str);
703 let upper = prefix.to_uppercase();
704 let _ = writeln!(
705 out,
706 " {upper}ConversionOptions* options_handle = {prefix}_conversion_options_from_json(\"{escaped}\");"
707 );
708 has_options_handle = true;
709 }
710 }
711 }
712 }
713
714 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
715
716 if expects_error {
717 let _ = writeln!(
718 out,
719 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
720 );
721 if has_options_handle {
722 let _ = writeln!(out, " {prefix}_conversion_options_free(options_handle);");
723 }
724 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
725 let _ = writeln!(out, "}}");
726 return;
727 }
728
729 let _ = writeln!(
731 out,
732 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
733 );
734 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
735
736 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
744 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
747
748 for assertion in &fixture.assertions {
749 if let Some(f) = &assertion.field {
750 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
751 let resolved = field_resolver.resolve(f);
752 let local_var = f.replace(['.', '['], "_").replace(']', "");
753 let has_map_access = resolved.contains('[');
754
755 if resolved.contains('.') {
756 emit_nested_accessor(
757 out,
758 prefix,
759 resolved,
760 &local_var,
761 result_var,
762 fields_c_types,
763 &mut intermediate_handles,
764 result_type_name,
765 );
766 } else {
767 let result_type_snake = result_type_name.to_snake_case();
768 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
769 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
770 }
771 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
772 }
773 }
774 }
775
776 for assertion in &fixture.assertions {
777 render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
778 }
779
780 for (_f, local_var, from_json) in &accessed_fields {
782 if *from_json {
783 let _ = writeln!(out, " free({local_var});");
784 } else {
785 let _ = writeln!(out, " {prefix}_free_string({local_var});");
786 }
787 }
788 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
790 if snake_type == "free_string" {
791 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
793 } else {
794 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
795 }
796 }
797 if has_options_handle {
798 let _ = writeln!(out, " {prefix}_conversion_options_free(options_handle);");
799 }
800 let result_type_snake = result_type_name.to_snake_case();
801 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
802 let _ = writeln!(out, "}}");
803}
804
805#[allow(clippy::too_many_arguments)]
819fn emit_nested_accessor(
820 out: &mut String,
821 prefix: &str,
822 resolved: &str,
823 local_var: &str,
824 result_var: &str,
825 fields_c_types: &HashMap<String, String>,
826 intermediate_handles: &mut Vec<(String, String)>,
827 result_type_name: &str,
828) {
829 let segments: Vec<&str> = resolved.split('.').collect();
830 let prefix_upper = prefix.to_uppercase();
831
832 let mut current_snake_type = result_type_name.to_snake_case();
834 let mut current_handle = result_var.to_string();
835
836 for (i, segment) in segments.iter().enumerate() {
837 let is_leaf = i + 1 == segments.len();
838
839 if let Some(bracket_pos) = segment.find('[') {
841 let field_name = &segment[..bracket_pos];
842 let key = segment[bracket_pos + 1..].trim_end_matches(']');
843 let field_snake = field_name.to_snake_case();
844 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
845
846 let json_var = format!("{field_snake}_json");
849 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
850 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
851 let _ = writeln!(out, " assert({json_var} != NULL);");
852 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
854 }
855 let _ = writeln!(
857 out,
858 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
859 );
860 return; }
862
863 let seg_snake = segment.to_snake_case();
864 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
865
866 if is_leaf {
867 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
869 } else {
870 let lookup_key = format!("{current_snake_type}.{seg_snake}");
872 let return_type_pascal = match fields_c_types.get(&lookup_key) {
873 Some(t) => t.clone(),
874 None => {
875 segment.to_pascal_case()
877 }
878 };
879 let return_snake = return_type_pascal.to_snake_case();
880 let handle_var = format!("{seg_snake}_handle");
881
882 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
885 let _ = writeln!(
886 out,
887 " {prefix_upper}{return_type_pascal}* {handle_var} = \
888 {accessor_fn}({current_handle});"
889 );
890 let _ = writeln!(out, " assert({handle_var} != NULL);");
891 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
892 }
893
894 current_snake_type = return_snake;
895 current_handle = handle_var;
896 }
897 }
898}
899
900fn build_args_string_c(
904 input: &serde_json::Value,
905 args: &[crate::config::ArgMapping],
906 has_options_handle: bool,
907) -> String {
908 if args.is_empty() {
909 return json_to_c(input);
910 }
911
912 let parts: Vec<String> = args
913 .iter()
914 .filter_map(|arg| {
915 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
916 let val = input.get(field);
917 match val {
918 None if arg.optional => Some("NULL".to_string()),
920 None => None,
922 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
924 Some(v) => {
925 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
928 Some("options_handle".to_string())
929 } else {
930 Some(json_to_c(v))
931 }
932 }
933 }
934 })
935 .collect();
936
937 parts.join(", ")
938}
939
940fn render_assertion(
941 out: &mut String,
942 assertion: &Assertion,
943 result_var: &str,
944 _field_resolver: &FieldResolver,
945 accessed_fields: &[(String, String, bool)],
946) {
947 if let Some(f) = &assertion.field {
949 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
950 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
951 return;
952 }
953 }
954
955 let field_expr = match &assertion.field {
956 Some(f) if !f.is_empty() => {
957 accessed_fields
959 .iter()
960 .find(|(k, _, _)| k == f)
961 .map(|(_, local, _)| local.clone())
962 .unwrap_or_else(|| result_var.to_string())
963 }
964 _ => result_var.to_string(),
965 };
966
967 match assertion.assertion_type.as_str() {
968 "equals" => {
969 if let Some(expected) = &assertion.value {
970 let c_val = json_to_c(expected);
971 if expected.is_string() {
972 let _ = writeln!(
974 out,
975 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
976 );
977 } else {
978 let _ = writeln!(
979 out,
980 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
981 );
982 }
983 }
984 }
985 "contains" => {
986 if let Some(expected) = &assertion.value {
987 let c_val = json_to_c(expected);
988 let _ = writeln!(
989 out,
990 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
991 );
992 }
993 }
994 "contains_all" => {
995 if let Some(values) = &assertion.values {
996 for val in values {
997 let c_val = json_to_c(val);
998 let _ = writeln!(
999 out,
1000 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1001 );
1002 }
1003 }
1004 }
1005 "not_contains" => {
1006 if let Some(expected) = &assertion.value {
1007 let c_val = json_to_c(expected);
1008 let _ = writeln!(
1009 out,
1010 " assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1011 );
1012 }
1013 }
1014 "not_empty" => {
1015 let _ = writeln!(
1016 out,
1017 " assert(strlen({field_expr}) > 0 && \"expected non-empty value\");"
1018 );
1019 }
1020 "is_empty" => {
1021 let _ = writeln!(
1022 out,
1023 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
1024 );
1025 }
1026 "contains_any" => {
1027 if let Some(values) = &assertion.values {
1028 let _ = writeln!(out, " {{");
1029 let _ = writeln!(out, " int found = 0;");
1030 for val in values {
1031 let c_val = json_to_c(val);
1032 let _ = writeln!(
1033 out,
1034 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
1035 );
1036 }
1037 let _ = writeln!(
1038 out,
1039 " assert(found && \"expected to contain at least one of the specified values\");"
1040 );
1041 let _ = writeln!(out, " }}");
1042 }
1043 }
1044 "greater_than" => {
1045 if let Some(val) = &assertion.value {
1046 let c_val = json_to_c(val);
1047 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
1048 }
1049 }
1050 "less_than" => {
1051 if let Some(val) = &assertion.value {
1052 let c_val = json_to_c(val);
1053 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
1054 }
1055 }
1056 "greater_than_or_equal" => {
1057 if let Some(val) = &assertion.value {
1058 let c_val = json_to_c(val);
1059 let _ = writeln!(
1060 out,
1061 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
1062 );
1063 }
1064 }
1065 "less_than_or_equal" => {
1066 if let Some(val) = &assertion.value {
1067 let c_val = json_to_c(val);
1068 let _ = writeln!(
1069 out,
1070 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
1071 );
1072 }
1073 }
1074 "starts_with" => {
1075 if let Some(expected) = &assertion.value {
1076 let c_val = json_to_c(expected);
1077 let _ = writeln!(
1078 out,
1079 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1080 );
1081 }
1082 }
1083 "ends_with" => {
1084 if let Some(expected) = &assertion.value {
1085 let c_val = json_to_c(expected);
1086 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
1087 let _ = writeln!(
1088 out,
1089 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
1090 );
1091 }
1092 }
1093 "min_length" => {
1094 if let Some(val) = &assertion.value {
1095 if let Some(n) = val.as_u64() {
1096 let _ = writeln!(
1097 out,
1098 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
1099 );
1100 }
1101 }
1102 }
1103 "max_length" => {
1104 if let Some(val) = &assertion.value {
1105 if let Some(n) = val.as_u64() {
1106 let _ = writeln!(
1107 out,
1108 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
1109 );
1110 }
1111 }
1112 }
1113 "count_min" => {
1114 if let Some(val) = &assertion.value {
1115 if let Some(n) = val.as_u64() {
1116 let _ = writeln!(out, " {{");
1117 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
1118 let _ = writeln!(
1119 out,
1120 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1121 );
1122 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1123 let _ = writeln!(
1124 out,
1125 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1126 );
1127 let _ = writeln!(out, " }}");
1128 }
1129 }
1130 }
1131 "count_equals" => {
1132 if let Some(val) = &assertion.value {
1133 if let Some(n) = val.as_u64() {
1134 let _ = writeln!(out, " {{");
1135 let _ = writeln!(out, " /* count_equals: count elements in array */");
1136 let _ = writeln!(
1137 out,
1138 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1139 );
1140 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1141 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
1142 let _ = writeln!(out, " }}");
1143 }
1144 }
1145 }
1146 "is_true" => {
1147 let _ = writeln!(out, " assert({field_expr});");
1148 }
1149 "not_error" => {
1150 }
1152 "error" => {
1153 }
1155 other => {
1156 let _ = writeln!(out, " /* TODO: unsupported assertion type: {other} */");
1157 }
1158 }
1159}
1160
1161fn json_to_c(value: &serde_json::Value) -> String {
1163 match value {
1164 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
1165 serde_json::Value::Bool(true) => "1".to_string(),
1166 serde_json::Value::Bool(false) => "0".to_string(),
1167 serde_json::Value::Number(n) => n.to_string(),
1168 serde_json::Value::Null => "NULL".to_string(),
1169 other => format!("\"{}\"", escape_c(&other.to_string())),
1170 }
1171}