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::ResolvedCrateConfig;
14use alef_core::hash::{self, CommentStyle};
15use anyhow::Result;
16use heck::{ToPascalCase, ToSnakeCase};
17use std::collections::HashMap;
18use std::fmt::Write as FmtWrite;
19use std::path::PathBuf;
20
21use super::E2eCodegen;
22
23pub struct CCodegen;
25
26impl E2eCodegen for CCodegen {
27 fn generate(
28 &self,
29 groups: &[FixtureGroup],
30 e2e_config: &E2eConfig,
31 config: &ResolvedCrateConfig,
32 ) -> Result<Vec<GeneratedFile>> {
33 let lang = self.language_name();
34 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
35
36 let mut files = Vec::new();
37
38 let call = &e2e_config.call;
40 let overrides = call.overrides.get(lang);
41 let result_var = &call.result_var;
42 let prefix = overrides
43 .and_then(|o| o.prefix.as_ref())
44 .cloned()
45 .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
46 .unwrap_or_default();
47 let header = overrides
48 .and_then(|o| o.header.as_ref())
49 .cloned()
50 .unwrap_or_else(|| config.ffi_header_name());
51
52 let c_pkg = e2e_config.resolve_package("c");
54 let lib_name = c_pkg
55 .as_ref()
56 .and_then(|p| p.name.as_ref())
57 .cloned()
58 .unwrap_or_else(|| config.ffi_lib_name());
59
60 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
62 .iter()
63 .filter_map(|group| {
64 let active: Vec<&Fixture> = group
65 .fixtures
66 .iter()
67 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
68 .filter(|f| f.visitor.is_none())
69 .collect();
70 if active.is_empty() { None } else { Some((group, active)) }
71 })
72 .collect();
73
74 let ffi_crate_path = c_pkg
82 .as_ref()
83 .and_then(|p| p.path.as_ref())
84 .cloned()
85 .unwrap_or_else(|| config.ffi_crate_path());
86
87 let category_names: Vec<String> = active_groups
89 .iter()
90 .map(|(g, _)| sanitize_filename(&g.category))
91 .collect();
92 files.push(GeneratedFile {
93 path: output_base.join("Makefile"),
94 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
95 generated_header: true,
96 });
97
98 let github_repo = config.github_repo();
100 let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
101 let ffi_pkg_name = e2e_config
102 .registry
103 .packages
104 .get("c")
105 .and_then(|p| p.name.as_ref())
106 .cloned()
107 .unwrap_or_else(|| lib_name.clone());
108 files.push(GeneratedFile {
109 path: output_base.join("download_ffi.sh"),
110 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
111 generated_header: true,
112 });
113
114 files.push(GeneratedFile {
116 path: output_base.join("test_runner.h"),
117 content: render_test_runner_header(&active_groups),
118 generated_header: true,
119 });
120
121 files.push(GeneratedFile {
123 path: output_base.join("main.c"),
124 content: render_main_c(&active_groups),
125 generated_header: true,
126 });
127
128 let field_resolver = FieldResolver::new(
129 &e2e_config.fields,
130 &e2e_config.fields_optional,
131 &e2e_config.result_fields,
132 &e2e_config.fields_array,
133 &std::collections::HashSet::new(),
134 );
135
136 for (group, active) in &active_groups {
140 let filename = format!("test_{}.c", sanitize_filename(&group.category));
141 let content = render_test_file(
142 &group.category,
143 active,
144 &header,
145 &prefix,
146 result_var,
147 e2e_config,
148 lang,
149 &field_resolver,
150 );
151 files.push(GeneratedFile {
152 path: output_base.join(filename),
153 content,
154 generated_header: true,
155 });
156 }
157
158 Ok(files)
159 }
160
161 fn language_name(&self) -> &'static str {
162 "c"
163 }
164}
165
166struct ResolvedCallInfo {
168 function_name: String,
169 result_type_name: String,
170 options_type_name: String,
171 client_factory: Option<String>,
172 args: Vec<crate::config::ArgMapping>,
173 raw_c_result_type: Option<String>,
174 c_free_fn: Option<String>,
175 result_is_option: bool,
176}
177
178fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
179 let overrides = call.overrides.get(lang);
180 let function_name = overrides
181 .and_then(|o| o.function.as_ref())
182 .cloned()
183 .unwrap_or_else(|| call.function.clone());
184 let result_type_name = overrides
189 .and_then(|o| o.result_type.as_ref())
190 .cloned()
191 .unwrap_or_else(|| call.function.to_pascal_case());
192 let options_type_name = overrides
193 .and_then(|o| o.options_type.as_deref())
194 .unwrap_or("ConversionOptions")
195 .to_string();
196 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
197 let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
198 let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
199 let result_is_option = overrides
200 .and_then(|o| if o.result_is_option { Some(true) } else { None })
201 .unwrap_or(call.result_is_option);
202 ResolvedCallInfo {
203 function_name,
204 result_type_name,
205 options_type_name,
206 client_factory,
207 args: call.args.clone(),
208 raw_c_result_type,
209 c_free_fn,
210 result_is_option,
211 }
212}
213
214fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
220 let call = e2e_config.resolve_call(fixture.call.as_deref());
221 let mut info = resolve_call_info(call, lang);
222
223 if info.client_factory.is_none() {
226 let default_overrides = e2e_config.call.overrides.get(lang);
227 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
228 info.client_factory = Some(factory.clone());
229 }
230 }
231
232 info
233}
234
235fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
236 let mut out = String::new();
237 out.push_str(&hash::header(CommentStyle::Hash));
238 let _ = writeln!(out, "CC = gcc");
239 let _ = writeln!(out, "FFI_DIR = ffi");
240 let _ = writeln!(out);
241
242 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
244 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
245 let _ = writeln!(
246 out,
247 " LDFLAGS = -L$(FFI_DIR)/lib -l{lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
248 );
249 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
250 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
251 let _ = writeln!(
252 out,
253 " LDFLAGS = -L../../target/release -l{lib_name} -Wl,-rpath,../../target/release"
254 );
255 let _ = writeln!(out, "else");
256 let _ = writeln!(
257 out,
258 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
259 );
260 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
261 let _ = writeln!(out, "endif");
262 let _ = writeln!(out);
263
264 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
265 let srcs = src_files.join(" ");
266
267 let _ = writeln!(out, "SRCS = main.c {srcs}");
268 let _ = writeln!(out, "TARGET = run_tests");
269 let _ = writeln!(out);
270 let _ = writeln!(out, ".PHONY: all clean test");
271 let _ = writeln!(out);
272 let _ = writeln!(out, "all: $(TARGET)");
273 let _ = writeln!(out);
274 let _ = writeln!(out, "$(TARGET): $(SRCS)");
275 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
276 let _ = writeln!(out);
277 let _ = writeln!(out, "test: $(TARGET)");
278 let _ = writeln!(out, "\t./$(TARGET)");
279 let _ = writeln!(out);
280 let _ = writeln!(out, "clean:");
281 let _ = writeln!(out, "\trm -f $(TARGET)");
282 out
283}
284
285fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
286 let mut out = String::new();
287 let _ = writeln!(out, "#!/usr/bin/env bash");
288 out.push_str(&hash::header(CommentStyle::Hash));
289 let _ = writeln!(out, "set -euo pipefail");
290 let _ = writeln!(out);
291 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
292 let _ = writeln!(out, "VERSION=\"{version}\"");
293 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
294 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
295 let _ = writeln!(out);
296 let _ = writeln!(out, "# Detect OS and architecture.");
297 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
298 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
299 let _ = writeln!(out);
300 let _ = writeln!(out, "case \"$ARCH\" in");
301 let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
302 let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
303 let _ = writeln!(out, "*)");
304 let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
305 let _ = writeln!(out, " exit 1");
306 let _ = writeln!(out, " ;;");
307 let _ = writeln!(out, "esac");
308 let _ = writeln!(out);
309 let _ = writeln!(out, "case \"$OS\" in");
310 let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
311 let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
312 let _ = writeln!(out, "*)");
313 let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
314 let _ = writeln!(out, " exit 1");
315 let _ = writeln!(out, " ;;");
316 let _ = writeln!(out, "esac");
317 let _ = writeln!(out);
318 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
319 let _ = writeln!(
320 out,
321 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
322 );
323 let _ = writeln!(out);
324 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
325 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
326 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
327 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
328 out
329}
330
331fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
332 let mut out = String::new();
333 out.push_str(&hash::header(CommentStyle::Block));
334 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
335 let _ = writeln!(out, "#define TEST_RUNNER_H");
336 let _ = writeln!(out);
337 let _ = writeln!(out, "#include <string.h>");
338 let _ = writeln!(out, "#include <stdlib.h>");
339 let _ = writeln!(out);
340 let _ = writeln!(out, "/**");
342 let _ = writeln!(
343 out,
344 " * Compare a string against an expected value, trimming trailing whitespace."
345 );
346 let _ = writeln!(
347 out,
348 " * Returns 0 if the trimmed actual string equals the expected string."
349 );
350 let _ = writeln!(out, " */");
351 let _ = writeln!(
352 out,
353 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
354 );
355 let _ = writeln!(
356 out,
357 " if (actual == NULL || expected == NULL) return actual != expected;"
358 );
359 let _ = writeln!(out, " size_t alen = strlen(actual);");
360 let _ = writeln!(
361 out,
362 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
363 );
364 let _ = writeln!(out, " size_t elen = strlen(expected);");
365 let _ = writeln!(out, " if (alen != elen) return 1;");
366 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
367 let _ = writeln!(out, "}}");
368 let _ = writeln!(out);
369
370 let _ = writeln!(out, "/**");
371 let _ = writeln!(
372 out,
373 " * Extract a string value for a given key from a JSON object string."
374 );
375 let _ = writeln!(
376 out,
377 " * Returns a heap-allocated copy of the value, or NULL if not found."
378 );
379 let _ = writeln!(out, " * Caller must free() the returned string.");
380 let _ = writeln!(out, " */");
381 let _ = writeln!(
382 out,
383 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
384 );
385 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
386 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
387 let _ = writeln!(out, " size_t key_len = strlen(key);");
388 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
389 let _ = writeln!(out, " if (!pattern) return NULL;");
390 let _ = writeln!(out, " pattern[0] = '\"';");
391 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
392 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
393 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
394 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
395 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
396 let _ = writeln!(out, " free(pattern);");
397 let _ = writeln!(out, " if (!found) return NULL;");
398 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
399 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
400 let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
401 let _ = writeln!(out, " found++; /* skip opening quote */");
402 let _ = writeln!(out, " const char *end = found;");
403 let _ = writeln!(out, " while (*end && *end != '\"') {{");
404 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
405 let _ = writeln!(out, " else end++;");
406 let _ = writeln!(out, " }}");
407 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
408 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
409 let _ = writeln!(out, " if (!result_str) return NULL;");
410 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
411 let _ = writeln!(out, " result_str[val_len] = '\\0';");
412 let _ = writeln!(out, " return result_str;");
413 let _ = writeln!(out, "}}");
414 let _ = writeln!(out);
415 let _ = writeln!(out, "/**");
416 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
417 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
418 let _ = writeln!(out, " */");
419 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
420 let _ = writeln!(out, " if (json == NULL) return 0;");
421 let _ = writeln!(out, " /* Skip leading whitespace */");
422 let _ = writeln!(
423 out,
424 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
425 );
426 let _ = writeln!(out, " if (*json != '[') return 0;");
427 let _ = writeln!(out, " json++;");
428 let _ = writeln!(out, " /* Skip whitespace after '[' */");
429 let _ = writeln!(
430 out,
431 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
432 );
433 let _ = writeln!(out, " if (*json == ']') return 0;");
434 let _ = writeln!(out, " int count = 1;");
435 let _ = writeln!(out, " int depth = 0;");
436 let _ = writeln!(out, " int in_string = 0;");
437 let _ = writeln!(
438 out,
439 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
440 );
441 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
442 let _ = writeln!(
443 out,
444 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
445 );
446 let _ = writeln!(out, " if (in_string) continue;");
447 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
448 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
449 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
450 let _ = writeln!(out, " }}");
451 let _ = writeln!(out, " return count;");
452 let _ = writeln!(out, "}}");
453 let _ = writeln!(out);
454
455 for (group, fixtures) in active_groups {
456 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
457 for fixture in fixtures {
458 let fn_name = sanitize_ident(&fixture.id);
459 let _ = writeln!(out, "void test_{fn_name}(void);");
460 }
461 let _ = writeln!(out);
462 }
463
464 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
465 out
466}
467
468fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
469 let mut out = String::new();
470 out.push_str(&hash::header(CommentStyle::Block));
471 let _ = writeln!(out, "#include <stdio.h>");
472 let _ = writeln!(out, "#include \"test_runner.h\"");
473 let _ = writeln!(out);
474 let _ = writeln!(out, "int main(void) {{");
475 let _ = writeln!(out, " int passed = 0;");
476 let _ = writeln!(out);
477
478 for (group, fixtures) in active_groups {
479 let _ = writeln!(out, " /* Category: {} */", group.category);
480 for fixture in fixtures {
481 let fn_name = sanitize_ident(&fixture.id);
482 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
483 let _ = writeln!(out, " test_{fn_name}();");
484 let _ = writeln!(out, " printf(\" PASSED\\n\");");
485 let _ = writeln!(out, " passed++;");
486 }
487 let _ = writeln!(out);
488 }
489
490 let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
491 let _ = writeln!(out, " return 0;");
492 let _ = writeln!(out, "}}");
493 out
494}
495
496#[allow(clippy::too_many_arguments)]
497fn render_test_file(
498 category: &str,
499 fixtures: &[&Fixture],
500 header: &str,
501 prefix: &str,
502 result_var: &str,
503 e2e_config: &E2eConfig,
504 lang: &str,
505 field_resolver: &FieldResolver,
506) -> String {
507 let mut out = String::new();
508 out.push_str(&hash::header(CommentStyle::Block));
509 let _ = writeln!(out, "/* E2e tests for category: {category} */");
510 let _ = writeln!(out);
511 let _ = writeln!(out, "#include <assert.h>");
512 let _ = writeln!(out, "#include <string.h>");
513 let _ = writeln!(out, "#include <stdio.h>");
514 let _ = writeln!(out, "#include <stdlib.h>");
515 let _ = writeln!(out, "#include \"{header}\"");
516 let _ = writeln!(out, "#include \"test_runner.h\"");
517 let _ = writeln!(out);
518
519 for (i, fixture) in fixtures.iter().enumerate() {
520 if fixture.visitor.is_some() {
523 panic!(
524 "C e2e generator: visitor pattern not supported for fixture: {}",
525 fixture.id
526 );
527 }
528
529 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
530 render_test_function(
531 &mut out,
532 fixture,
533 prefix,
534 &call_info.function_name,
535 result_var,
536 &call_info.args,
537 field_resolver,
538 &e2e_config.fields_c_types,
539 &call_info.result_type_name,
540 &call_info.options_type_name,
541 call_info.client_factory.as_deref(),
542 call_info.raw_c_result_type.as_deref(),
543 call_info.c_free_fn.as_deref(),
544 call_info.result_is_option,
545 );
546 if i + 1 < fixtures.len() {
547 let _ = writeln!(out);
548 }
549 }
550
551 out
552}
553
554#[allow(clippy::too_many_arguments)]
555fn render_test_function(
556 out: &mut String,
557 fixture: &Fixture,
558 prefix: &str,
559 function_name: &str,
560 result_var: &str,
561 args: &[crate::config::ArgMapping],
562 field_resolver: &FieldResolver,
563 fields_c_types: &HashMap<String, String>,
564 result_type_name: &str,
565 options_type_name: &str,
566 client_factory: Option<&str>,
567 raw_c_result_type: Option<&str>,
568 c_free_fn: Option<&str>,
569 result_is_option: bool,
570) {
571 let fn_name = sanitize_ident(&fixture.id);
572 let description = &fixture.description;
573
574 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
575
576 let _ = writeln!(out, "void test_{fn_name}(void) {{");
577 let _ = writeln!(out, " /* {description} */");
578
579 let prefix_upper = prefix.to_uppercase();
580
581 if let Some(factory) = client_factory {
586 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); for arg in args {
589 if arg.arg_type == "json_object" {
590 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
595 options_type_name.to_string()
596 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
597 format!("{}Request", stripped)
598 } else {
599 format!("{result_type_name}Request")
600 };
601 let request_type_snake = request_type_pascal.to_snake_case();
602 let var_name = format!("{request_type_snake}_handle");
603
604 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
605 let json_val = if field.is_empty() || field == "input" {
606 Some(&fixture.input)
607 } else {
608 fixture.input.get(field)
609 };
610
611 if let Some(val) = json_val {
612 if !val.is_null() {
613 let normalized = super::normalize_json_keys_to_snake_case(val);
614 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
615 let escaped = escape_c(&json_str);
616 let _ = writeln!(
617 out,
618 " {prefix_upper}{request_type_pascal}* {var_name} = \
619 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
620 );
621 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
622 request_handle_vars.push((arg.name.clone(), var_name));
623 }
624 }
625 }
626 }
627
628 let _ = writeln!(
629 out,
630 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
631 );
632 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
633
634 let method_args = if request_handle_vars.is_empty() {
635 String::new()
636 } else {
637 let handles: Vec<&str> = request_handle_vars.iter().map(|(_, v)| v.as_str()).collect();
638 format!(", {}", handles.join(", "))
639 };
640
641 let call_fn = format!("{prefix}_default_client_{function_name}");
642
643 if expects_error {
644 let _ = writeln!(
645 out,
646 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
647 );
648 for (_, var_name) in &request_handle_vars {
649 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
650 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
651 }
652 let _ = writeln!(out, " {prefix}_default_client_free(client);");
653 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
654 let _ = writeln!(out, "}}");
655 return;
656 }
657
658 let _ = writeln!(
659 out,
660 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
661 );
662 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
663
664 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
665 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
666
667 for assertion in &fixture.assertions {
668 if let Some(f) = &assertion.field {
669 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
670 let resolved = field_resolver.resolve(f);
671 let local_var = f.replace(['.', '['], "_").replace(']', "");
672 let has_map_access = resolved.contains('[');
673 if resolved.contains('.') {
674 emit_nested_accessor(
675 out,
676 prefix,
677 resolved,
678 &local_var,
679 result_var,
680 fields_c_types,
681 &mut intermediate_handles,
682 result_type_name,
683 );
684 } else {
685 let result_type_snake = result_type_name.to_snake_case();
686 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
687 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
688 }
689 accessed_fields.push((f.clone(), local_var, has_map_access));
690 }
691 }
692 }
693
694 for assertion in &fixture.assertions {
695 render_assertion(out, assertion, result_var, prefix, field_resolver, &accessed_fields);
696 }
697
698 for (_f, local_var, from_json) in &accessed_fields {
699 if *from_json {
700 let _ = writeln!(out, " free({local_var});");
701 } else {
702 let _ = writeln!(out, " {prefix}_free_string({local_var});");
703 }
704 }
705 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
706 if snake_type == "free_string" {
707 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
708 } else {
709 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
710 }
711 }
712 let result_type_snake = result_type_name.to_snake_case();
713 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
714 for (_, var_name) in &request_handle_vars {
715 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
716 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
717 }
718 let _ = writeln!(out, " {prefix}_default_client_free(client);");
719 let _ = writeln!(out, "}}");
720 return;
721 }
722
723 if let Some(raw_type) = raw_c_result_type {
726 let args_str = if args.is_empty() {
728 String::new()
729 } else {
730 let parts: Vec<String> = args
731 .iter()
732 .filter_map(|arg| {
733 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
734 let val = fixture.input.get(field);
735 match val {
736 None if arg.optional => Some("NULL".to_string()),
737 None => None,
738 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
739 Some(v) => Some(json_to_c(v)),
740 }
741 })
742 .collect();
743 parts.join(", ")
744 };
745
746 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
748
749 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
751 if has_not_error {
752 match raw_type {
753 "char*" if !result_is_option => {
754 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
755 }
756 "int32_t" => {
757 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
758 }
759 "uintptr_t" => {
760 let _ = writeln!(
761 out,
762 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
763 );
764 }
765 _ => {}
766 }
767 }
768
769 for assertion in &fixture.assertions {
771 match assertion.assertion_type.as_str() {
772 "not_error" | "error" => {} "not_empty" => {
774 let _ = writeln!(
775 out,
776 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
777 );
778 }
779 "is_empty" => {
780 if result_is_option && raw_type == "char*" {
781 let _ = writeln!(
782 out,
783 " assert({result_var} == NULL && \"expected empty/null value\");"
784 );
785 } else {
786 let _ = writeln!(
787 out,
788 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
789 );
790 }
791 }
792 "count_min" => {
793 if let Some(val) = &assertion.value {
794 if let Some(n) = val.as_u64() {
795 match raw_type {
796 "char*" => {
797 let _ = writeln!(out, " {{");
798 let _ = writeln!(
799 out,
800 " assert({result_var} != NULL && \"expected non-null JSON array\");"
801 );
802 let _ =
803 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
804 let _ = writeln!(
805 out,
806 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
807 );
808 let _ = writeln!(out, " }}");
809 }
810 _ => {
811 let _ = writeln!(
812 out,
813 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
814 );
815 }
816 }
817 }
818 }
819 }
820 "greater_than_or_equal" => {
821 if let Some(val) = &assertion.value {
822 let c_val = json_to_c(val);
823 let _ = writeln!(
824 out,
825 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
826 );
827 }
828 }
829 "contains" => {
830 if let Some(val) = &assertion.value {
831 let c_val = json_to_c(val);
832 let _ = writeln!(
833 out,
834 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
835 );
836 }
837 }
838 "contains_all" => {
839 if let Some(values) = &assertion.values {
840 for val in values {
841 let c_val = json_to_c(val);
842 let _ = writeln!(
843 out,
844 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
845 );
846 }
847 }
848 }
849 "equals" => {
850 if let Some(val) = &assertion.value {
851 let c_val = json_to_c(val);
852 if val.is_string() {
853 let _ = writeln!(
854 out,
855 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
856 );
857 } else {
858 let _ = writeln!(
859 out,
860 " assert({result_var} == {c_val} && \"equals assertion failed\");"
861 );
862 }
863 }
864 }
865 "not_contains" => {
866 if let Some(val) = &assertion.value {
867 let c_val = json_to_c(val);
868 let _ = writeln!(
869 out,
870 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
871 );
872 }
873 }
874 "starts_with" => {
875 if let Some(val) = &assertion.value {
876 let c_val = json_to_c(val);
877 let _ = writeln!(
878 out,
879 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
880 );
881 }
882 }
883 "is_true" => {
884 let _ = writeln!(out, " assert({result_var});");
885 }
886 "is_false" => {
887 let _ = writeln!(out, " assert(!{result_var});");
888 }
889 other => {
890 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
891 }
892 }
893 }
894
895 if raw_type == "char*" {
897 let free_fn = c_free_fn
898 .map(|s| s.to_string())
899 .unwrap_or_else(|| format!("{prefix}_free_string"));
900 if result_is_option {
901 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
902 } else {
903 let _ = writeln!(out, " {free_fn}({result_var});");
904 }
905 }
906
907 let _ = writeln!(out, "}}");
908 return;
909 }
910
911 let prefixed_fn = function_name.to_string();
917
918 let mut has_options_handle = false;
920 for arg in args {
921 if arg.arg_type == "json_object" {
922 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
923 if let Some(val) = fixture.input.get(field) {
924 if !val.is_null() {
925 let normalized = super::normalize_json_keys_to_snake_case(val);
929 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
930 let escaped = escape_c(&json_str);
931 let upper = prefix.to_uppercase();
932 let options_type_pascal = options_type_name;
933 let options_type_snake = options_type_name.to_snake_case();
934 let _ = writeln!(
935 out,
936 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
937 );
938 has_options_handle = true;
939 }
940 }
941 }
942 }
943
944 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
945
946 if expects_error {
947 let _ = writeln!(
948 out,
949 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
950 );
951 if has_options_handle {
952 let options_type_snake = options_type_name.to_snake_case();
953 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
954 }
955 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
956 let _ = writeln!(out, "}}");
957 return;
958 }
959
960 let _ = writeln!(
962 out,
963 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
964 );
965 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
966
967 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
975 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
978
979 for assertion in &fixture.assertions {
980 if let Some(f) = &assertion.field {
981 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
982 let resolved = field_resolver.resolve(f);
983 let local_var = f.replace(['.', '['], "_").replace(']', "");
984 let has_map_access = resolved.contains('[');
985
986 if resolved.contains('.') {
987 emit_nested_accessor(
988 out,
989 prefix,
990 resolved,
991 &local_var,
992 result_var,
993 fields_c_types,
994 &mut intermediate_handles,
995 result_type_name,
996 );
997 } else {
998 let result_type_snake = result_type_name.to_snake_case();
999 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1000 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1001 }
1002 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1003 }
1004 }
1005 }
1006
1007 for assertion in &fixture.assertions {
1008 render_assertion(out, assertion, result_var, prefix, field_resolver, &accessed_fields);
1009 }
1010
1011 for (_f, local_var, from_json) in &accessed_fields {
1013 if *from_json {
1014 let _ = writeln!(out, " free({local_var});");
1015 } else {
1016 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1017 }
1018 }
1019 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1021 if snake_type == "free_string" {
1022 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1024 } else {
1025 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1026 }
1027 }
1028 if has_options_handle {
1029 let options_type_snake = options_type_name.to_snake_case();
1030 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1031 }
1032 let result_type_snake = result_type_name.to_snake_case();
1033 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1034 let _ = writeln!(out, "}}");
1035}
1036
1037#[allow(clippy::too_many_arguments)]
1051fn emit_nested_accessor(
1052 out: &mut String,
1053 prefix: &str,
1054 resolved: &str,
1055 local_var: &str,
1056 result_var: &str,
1057 fields_c_types: &HashMap<String, String>,
1058 intermediate_handles: &mut Vec<(String, String)>,
1059 result_type_name: &str,
1060) {
1061 let segments: Vec<&str> = resolved.split('.').collect();
1062 let prefix_upper = prefix.to_uppercase();
1063
1064 let mut current_snake_type = result_type_name.to_snake_case();
1066 let mut current_handle = result_var.to_string();
1067
1068 for (i, segment) in segments.iter().enumerate() {
1069 let is_leaf = i + 1 == segments.len();
1070
1071 if let Some(bracket_pos) = segment.find('[') {
1073 let field_name = &segment[..bracket_pos];
1074 let key = segment[bracket_pos + 1..].trim_end_matches(']');
1075 let field_snake = field_name.to_snake_case();
1076 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
1077
1078 let json_var = format!("{field_snake}_json");
1081 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
1082 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
1083 let _ = writeln!(out, " assert({json_var} != NULL);");
1084 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
1086 }
1087 let _ = writeln!(
1089 out,
1090 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
1091 );
1092 return; }
1094
1095 let seg_snake = segment.to_snake_case();
1096 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
1097
1098 if is_leaf {
1099 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
1101 } else {
1102 let lookup_key = format!("{current_snake_type}.{seg_snake}");
1104 let return_type_pascal = match fields_c_types.get(&lookup_key) {
1105 Some(t) => t.clone(),
1106 None => {
1107 segment.to_pascal_case()
1109 }
1110 };
1111 let return_snake = return_type_pascal.to_snake_case();
1112 let handle_var = format!("{seg_snake}_handle");
1113
1114 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
1117 let _ = writeln!(
1118 out,
1119 " {prefix_upper}{return_type_pascal}* {handle_var} = \
1120 {accessor_fn}({current_handle});"
1121 );
1122 let _ = writeln!(out, " assert({handle_var} != NULL);");
1123 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
1124 }
1125
1126 current_snake_type = return_snake;
1127 current_handle = handle_var;
1128 }
1129 }
1130}
1131
1132fn build_args_string_c(
1136 input: &serde_json::Value,
1137 args: &[crate::config::ArgMapping],
1138 has_options_handle: bool,
1139) -> String {
1140 if args.is_empty() {
1141 return json_to_c(input);
1142 }
1143
1144 let parts: Vec<String> = args
1145 .iter()
1146 .filter_map(|arg| {
1147 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1148 let val = input.get(field);
1149 match val {
1150 None if arg.optional => Some("NULL".to_string()),
1152 None => None,
1154 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1156 Some(v) => {
1157 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
1160 Some("options_handle".to_string())
1161 } else {
1162 Some(json_to_c(v))
1163 }
1164 }
1165 }
1166 })
1167 .collect();
1168
1169 parts.join(", ")
1170}
1171
1172fn render_assertion(
1173 out: &mut String,
1174 assertion: &Assertion,
1175 result_var: &str,
1176 ffi_prefix: &str,
1177 _field_resolver: &FieldResolver,
1178 accessed_fields: &[(String, String, bool)],
1179) {
1180 if let Some(f) = &assertion.field {
1182 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
1183 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1184 return;
1185 }
1186 }
1187
1188 let field_expr = match &assertion.field {
1189 Some(f) if !f.is_empty() => {
1190 accessed_fields
1192 .iter()
1193 .find(|(k, _, _)| k == f)
1194 .map(|(_, local, _)| local.clone())
1195 .unwrap_or_else(|| result_var.to_string())
1196 }
1197 _ => result_var.to_string(),
1198 };
1199
1200 match assertion.assertion_type.as_str() {
1201 "equals" => {
1202 if let Some(expected) = &assertion.value {
1203 let c_val = json_to_c(expected);
1204 if expected.is_string() {
1205 let _ = writeln!(
1207 out,
1208 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
1209 );
1210 } else {
1211 let _ = writeln!(
1212 out,
1213 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
1214 );
1215 }
1216 }
1217 }
1218 "contains" => {
1219 if let Some(expected) = &assertion.value {
1220 let c_val = json_to_c(expected);
1221 let _ = writeln!(
1222 out,
1223 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1224 );
1225 }
1226 }
1227 "contains_all" => {
1228 if let Some(values) = &assertion.values {
1229 for val in values {
1230 let c_val = json_to_c(val);
1231 let _ = writeln!(
1232 out,
1233 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1234 );
1235 }
1236 }
1237 }
1238 "not_contains" => {
1239 if let Some(expected) = &assertion.value {
1240 let c_val = json_to_c(expected);
1241 let _ = writeln!(
1242 out,
1243 " assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1244 );
1245 }
1246 }
1247 "not_empty" => {
1248 let _ = writeln!(
1249 out,
1250 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
1251 );
1252 }
1253 "is_empty" => {
1254 let _ = writeln!(
1255 out,
1256 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
1257 );
1258 }
1259 "contains_any" => {
1260 if let Some(values) = &assertion.values {
1261 let _ = writeln!(out, " {{");
1262 let _ = writeln!(out, " int found = 0;");
1263 for val in values {
1264 let c_val = json_to_c(val);
1265 let _ = writeln!(
1266 out,
1267 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
1268 );
1269 }
1270 let _ = writeln!(
1271 out,
1272 " assert(found && \"expected to contain at least one of the specified values\");"
1273 );
1274 let _ = writeln!(out, " }}");
1275 }
1276 }
1277 "greater_than" => {
1278 if let Some(val) = &assertion.value {
1279 let c_val = json_to_c(val);
1280 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
1281 }
1282 }
1283 "less_than" => {
1284 if let Some(val) = &assertion.value {
1285 let c_val = json_to_c(val);
1286 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
1287 }
1288 }
1289 "greater_than_or_equal" => {
1290 if let Some(val) = &assertion.value {
1291 let c_val = json_to_c(val);
1292 let _ = writeln!(
1293 out,
1294 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
1295 );
1296 }
1297 }
1298 "less_than_or_equal" => {
1299 if let Some(val) = &assertion.value {
1300 let c_val = json_to_c(val);
1301 let _ = writeln!(
1302 out,
1303 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
1304 );
1305 }
1306 }
1307 "starts_with" => {
1308 if let Some(expected) = &assertion.value {
1309 let c_val = json_to_c(expected);
1310 let _ = writeln!(
1311 out,
1312 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1313 );
1314 }
1315 }
1316 "ends_with" => {
1317 if let Some(expected) = &assertion.value {
1318 let c_val = json_to_c(expected);
1319 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
1320 let _ = writeln!(
1321 out,
1322 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
1323 );
1324 }
1325 }
1326 "min_length" => {
1327 if let Some(val) = &assertion.value {
1328 if let Some(n) = val.as_u64() {
1329 let _ = writeln!(
1330 out,
1331 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
1332 );
1333 }
1334 }
1335 }
1336 "max_length" => {
1337 if let Some(val) = &assertion.value {
1338 if let Some(n) = val.as_u64() {
1339 let _ = writeln!(
1340 out,
1341 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
1342 );
1343 }
1344 }
1345 }
1346 "count_min" => {
1347 if let Some(val) = &assertion.value {
1348 if let Some(n) = val.as_u64() {
1349 let _ = writeln!(out, " {{");
1350 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
1351 let _ = writeln!(
1352 out,
1353 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1354 );
1355 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1356 let _ = writeln!(
1357 out,
1358 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1359 );
1360 let _ = writeln!(out, " }}");
1361 }
1362 }
1363 }
1364 "count_equals" => {
1365 if let Some(val) = &assertion.value {
1366 if let Some(n) = val.as_u64() {
1367 let _ = writeln!(out, " {{");
1368 let _ = writeln!(out, " /* count_equals: count elements in array */");
1369 let _ = writeln!(
1370 out,
1371 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1372 );
1373 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1374 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
1375 let _ = writeln!(out, " }}");
1376 }
1377 }
1378 }
1379 "is_true" => {
1380 let _ = writeln!(out, " assert({field_expr});");
1381 }
1382 "is_false" => {
1383 let _ = writeln!(out, " assert(!{field_expr});");
1384 }
1385 "method_result" => {
1386 if let Some(method_name) = &assertion.method {
1387 render_method_result_assertion(
1388 out,
1389 result_var,
1390 ffi_prefix,
1391 method_name,
1392 assertion.args.as_ref(),
1393 assertion.return_type.as_deref(),
1394 assertion.check.as_deref().unwrap_or("is_true"),
1395 assertion.value.as_ref(),
1396 );
1397 } else {
1398 panic!("C e2e generator: method_result assertion missing 'method' field");
1399 }
1400 }
1401 "matches_regex" => {
1402 if let Some(expected) = &assertion.value {
1403 let c_val = json_to_c(expected);
1404 let _ = writeln!(out, " {{");
1405 let _ = writeln!(out, " regex_t _re;");
1406 let _ = writeln!(
1407 out,
1408 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
1409 );
1410 let _ = writeln!(
1411 out,
1412 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
1413 );
1414 let _ = writeln!(out, " regfree(&_re);");
1415 let _ = writeln!(out, " }}");
1416 }
1417 }
1418 "not_error" => {
1419 }
1421 "error" => {
1422 }
1424 other => {
1425 panic!("C e2e generator: unsupported assertion type: {other}");
1426 }
1427 }
1428}
1429
1430#[allow(clippy::too_many_arguments)]
1439fn render_method_result_assertion(
1440 out: &mut String,
1441 result_var: &str,
1442 ffi_prefix: &str,
1443 method_name: &str,
1444 args: Option<&serde_json::Value>,
1445 return_type: Option<&str>,
1446 check: &str,
1447 value: Option<&serde_json::Value>,
1448) {
1449 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
1450
1451 if return_type == Some("string") {
1452 let _ = writeln!(out, " {{");
1454 let _ = writeln!(out, " char* _method_result = {call_expr};");
1455 if check == "is_error" {
1456 let _ = writeln!(
1457 out,
1458 " assert(_method_result == NULL && \"expected method to return error\");"
1459 );
1460 let _ = writeln!(out, " }}");
1461 return;
1462 }
1463 let _ = writeln!(
1464 out,
1465 " assert(_method_result != NULL && \"method_result returned NULL\");"
1466 );
1467 match check {
1468 "contains" => {
1469 if let Some(val) = value {
1470 let c_val = json_to_c(val);
1471 let _ = writeln!(
1472 out,
1473 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1474 );
1475 }
1476 }
1477 "equals" => {
1478 if let Some(val) = value {
1479 let c_val = json_to_c(val);
1480 let _ = writeln!(
1481 out,
1482 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
1483 );
1484 }
1485 }
1486 "is_true" => {
1487 let _ = writeln!(
1488 out,
1489 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
1490 );
1491 }
1492 "count_min" => {
1493 if let Some(val) = value {
1494 let n = val.as_u64().unwrap_or(0);
1495 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1496 let _ = writeln!(
1497 out,
1498 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
1499 );
1500 }
1501 }
1502 other_check => {
1503 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
1504 }
1505 }
1506 let _ = writeln!(out, " free(_method_result);");
1507 let _ = writeln!(out, " }}");
1508 return;
1509 }
1510
1511 match check {
1513 "equals" => {
1514 if let Some(val) = value {
1515 let c_val = json_to_c(val);
1516 let _ = writeln!(
1517 out,
1518 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
1519 );
1520 }
1521 }
1522 "is_true" => {
1523 let _ = writeln!(
1524 out,
1525 " assert({call_expr} && \"method_result is_true assertion failed\");"
1526 );
1527 }
1528 "is_false" => {
1529 let _ = writeln!(
1530 out,
1531 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
1532 );
1533 }
1534 "greater_than_or_equal" => {
1535 if let Some(val) = value {
1536 let n = val.as_u64().unwrap_or(0);
1537 let _ = writeln!(
1538 out,
1539 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
1540 );
1541 }
1542 }
1543 "count_min" => {
1544 if let Some(val) = value {
1545 let n = val.as_u64().unwrap_or(0);
1546 let _ = writeln!(
1547 out,
1548 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
1549 );
1550 }
1551 }
1552 other_check => {
1553 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1554 }
1555 }
1556}
1557
1558fn build_c_method_call(
1565 result_var: &str,
1566 ffi_prefix: &str,
1567 method_name: &str,
1568 args: Option<&serde_json::Value>,
1569) -> String {
1570 let extra_args = if let Some(args_val) = args {
1571 args_val
1572 .as_object()
1573 .map(|obj| {
1574 obj.values()
1575 .map(|v| match v {
1576 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
1577 serde_json::Value::Bool(true) => "1".to_string(),
1578 serde_json::Value::Bool(false) => "0".to_string(),
1579 serde_json::Value::Number(n) => n.to_string(),
1580 serde_json::Value::Null => "NULL".to_string(),
1581 other => format!("\"{}\"", escape_c(&other.to_string())),
1582 })
1583 .collect::<Vec<_>>()
1584 .join(", ")
1585 })
1586 .unwrap_or_default()
1587 } else {
1588 String::new()
1589 };
1590
1591 if extra_args.is_empty() {
1592 format!("{ffi_prefix}_{method_name}({result_var})")
1593 } else {
1594 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
1595 }
1596}
1597
1598fn json_to_c(value: &serde_json::Value) -> String {
1600 match value {
1601 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
1602 serde_json::Value::Bool(true) => "1".to_string(),
1603 serde_json::Value::Bool(false) => "0".to_string(),
1604 serde_json::Value::Number(n) => n.to_string(),
1605 serde_json::Value::Null => "NULL".to_string(),
1606 other => format!("\"{}\"", escape_c(&other.to_string())),
1607 }
1608}