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 link_lib_name = lib_name.replace('-', "_");
247
248 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
250 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
251 let _ = writeln!(
252 out,
253 " LDFLAGS = -L$(FFI_DIR)/lib -l{link_lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
254 );
255 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
256 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
257 let _ = writeln!(
258 out,
259 " LDFLAGS = -L../../target/release -l{link_lib_name} -Wl,-rpath,../../target/release"
260 );
261 let _ = writeln!(out, "else");
262 let _ = writeln!(
263 out,
264 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
265 );
266 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
267 let _ = writeln!(out, "endif");
268 let _ = writeln!(out);
269
270 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
271 let srcs = src_files.join(" ");
272
273 let _ = writeln!(out, "SRCS = main.c {srcs}");
274 let _ = writeln!(out, "TARGET = run_tests");
275 let _ = writeln!(out);
276 let _ = writeln!(out, ".PHONY: all clean test");
277 let _ = writeln!(out);
278 let _ = writeln!(out, "all: $(TARGET)");
279 let _ = writeln!(out);
280 let _ = writeln!(out, "$(TARGET): $(SRCS)");
281 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
282 let _ = writeln!(out);
283 let _ = writeln!(out, "test: $(TARGET)");
284 let _ = writeln!(out, "\t./$(TARGET)");
285 let _ = writeln!(out);
286 let _ = writeln!(out, "clean:");
287 let _ = writeln!(out, "\trm -f $(TARGET)");
288 out
289}
290
291fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
292 let mut out = String::new();
293 let _ = writeln!(out, "#!/usr/bin/env bash");
294 out.push_str(&hash::header(CommentStyle::Hash));
295 let _ = writeln!(out, "set -euo pipefail");
296 let _ = writeln!(out);
297 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
298 let _ = writeln!(out, "VERSION=\"{version}\"");
299 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
300 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
301 let _ = writeln!(out);
302 let _ = writeln!(out, "# Detect OS and architecture.");
303 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
304 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
305 let _ = writeln!(out);
306 let _ = writeln!(out, "case \"$ARCH\" in");
307 let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
308 let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
309 let _ = writeln!(out, "*)");
310 let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
311 let _ = writeln!(out, " exit 1");
312 let _ = writeln!(out, " ;;");
313 let _ = writeln!(out, "esac");
314 let _ = writeln!(out);
315 let _ = writeln!(out, "case \"$OS\" in");
316 let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
317 let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
318 let _ = writeln!(out, "*)");
319 let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
320 let _ = writeln!(out, " exit 1");
321 let _ = writeln!(out, " ;;");
322 let _ = writeln!(out, "esac");
323 let _ = writeln!(out);
324 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
325 let _ = writeln!(
326 out,
327 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
328 );
329 let _ = writeln!(out);
330 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
331 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
332 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
333 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
334 out
335}
336
337fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
338 let mut out = String::new();
339 out.push_str(&hash::header(CommentStyle::Block));
340 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
341 let _ = writeln!(out, "#define TEST_RUNNER_H");
342 let _ = writeln!(out);
343 let _ = writeln!(out, "#include <string.h>");
344 let _ = writeln!(out, "#include <stdlib.h>");
345 let _ = writeln!(out);
346 let _ = writeln!(out, "/**");
348 let _ = writeln!(
349 out,
350 " * Compare a string against an expected value, trimming trailing whitespace."
351 );
352 let _ = writeln!(
353 out,
354 " * Returns 0 if the trimmed actual string equals the expected string."
355 );
356 let _ = writeln!(out, " */");
357 let _ = writeln!(
358 out,
359 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
360 );
361 let _ = writeln!(
362 out,
363 " if (actual == NULL || expected == NULL) return actual != expected;"
364 );
365 let _ = writeln!(out, " size_t alen = strlen(actual);");
366 let _ = writeln!(
367 out,
368 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
369 );
370 let _ = writeln!(out, " size_t elen = strlen(expected);");
371 let _ = writeln!(out, " if (alen != elen) return 1;");
372 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
373 let _ = writeln!(out, "}}");
374 let _ = writeln!(out);
375
376 let _ = writeln!(out, "/**");
377 let _ = writeln!(
378 out,
379 " * Extract a string value for a given key from a JSON object string."
380 );
381 let _ = writeln!(
382 out,
383 " * Returns a heap-allocated copy of the value, or NULL if not found."
384 );
385 let _ = writeln!(out, " * Caller must free() the returned string.");
386 let _ = writeln!(out, " */");
387 let _ = writeln!(
388 out,
389 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
390 );
391 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
392 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
393 let _ = writeln!(out, " size_t key_len = strlen(key);");
394 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
395 let _ = writeln!(out, " if (!pattern) return NULL;");
396 let _ = writeln!(out, " pattern[0] = '\"';");
397 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
398 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
399 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
400 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
401 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
402 let _ = writeln!(out, " free(pattern);");
403 let _ = writeln!(out, " if (!found) return NULL;");
404 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
405 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
406 let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
407 let _ = writeln!(out, " found++; /* skip opening quote */");
408 let _ = writeln!(out, " const char *end = found;");
409 let _ = writeln!(out, " while (*end && *end != '\"') {{");
410 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
411 let _ = writeln!(out, " else end++;");
412 let _ = writeln!(out, " }}");
413 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
414 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
415 let _ = writeln!(out, " if (!result_str) return NULL;");
416 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
417 let _ = writeln!(out, " result_str[val_len] = '\\0';");
418 let _ = writeln!(out, " return result_str;");
419 let _ = writeln!(out, "}}");
420 let _ = writeln!(out);
421 let _ = writeln!(out, "/**");
422 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
423 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
424 let _ = writeln!(out, " */");
425 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
426 let _ = writeln!(out, " if (json == NULL) return 0;");
427 let _ = writeln!(out, " /* Skip leading whitespace */");
428 let _ = writeln!(
429 out,
430 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
431 );
432 let _ = writeln!(out, " if (*json != '[') return 0;");
433 let _ = writeln!(out, " json++;");
434 let _ = writeln!(out, " /* Skip whitespace after '[' */");
435 let _ = writeln!(
436 out,
437 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
438 );
439 let _ = writeln!(out, " if (*json == ']') return 0;");
440 let _ = writeln!(out, " int count = 1;");
441 let _ = writeln!(out, " int depth = 0;");
442 let _ = writeln!(out, " int in_string = 0;");
443 let _ = writeln!(
444 out,
445 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
446 );
447 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
448 let _ = writeln!(
449 out,
450 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
451 );
452 let _ = writeln!(out, " if (in_string) continue;");
453 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
454 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
455 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
456 let _ = writeln!(out, " }}");
457 let _ = writeln!(out, " return count;");
458 let _ = writeln!(out, "}}");
459 let _ = writeln!(out);
460
461 for (group, fixtures) in active_groups {
462 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
463 for fixture in fixtures {
464 let fn_name = sanitize_ident(&fixture.id);
465 let _ = writeln!(out, "void test_{fn_name}(void);");
466 }
467 let _ = writeln!(out);
468 }
469
470 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
471 out
472}
473
474fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
475 let mut out = String::new();
476 out.push_str(&hash::header(CommentStyle::Block));
477 let _ = writeln!(out, "#include <stdio.h>");
478 let _ = writeln!(out, "#include \"test_runner.h\"");
479 let _ = writeln!(out);
480 let _ = writeln!(out, "int main(void) {{");
481 let _ = writeln!(out, " int passed = 0;");
482 let _ = writeln!(out);
483
484 for (group, fixtures) in active_groups {
485 let _ = writeln!(out, " /* Category: {} */", group.category);
486 for fixture in fixtures {
487 let fn_name = sanitize_ident(&fixture.id);
488 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
489 let _ = writeln!(out, " test_{fn_name}();");
490 let _ = writeln!(out, " printf(\" PASSED\\n\");");
491 let _ = writeln!(out, " passed++;");
492 }
493 let _ = writeln!(out);
494 }
495
496 let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
497 let _ = writeln!(out, " return 0;");
498 let _ = writeln!(out, "}}");
499 out
500}
501
502#[allow(clippy::too_many_arguments)]
503fn render_test_file(
504 category: &str,
505 fixtures: &[&Fixture],
506 header: &str,
507 prefix: &str,
508 result_var: &str,
509 e2e_config: &E2eConfig,
510 lang: &str,
511 field_resolver: &FieldResolver,
512) -> String {
513 let mut out = String::new();
514 out.push_str(&hash::header(CommentStyle::Block));
515 let _ = writeln!(out, "/* E2e tests for category: {category} */");
516 let _ = writeln!(out);
517 let _ = writeln!(out, "#include <assert.h>");
518 let _ = writeln!(out, "#include <string.h>");
519 let _ = writeln!(out, "#include <stdio.h>");
520 let _ = writeln!(out, "#include <stdlib.h>");
521 let _ = writeln!(out, "#include \"{header}\"");
522 let _ = writeln!(out, "#include \"test_runner.h\"");
523 let _ = writeln!(out);
524
525 for (i, fixture) in fixtures.iter().enumerate() {
526 if fixture.visitor.is_some() {
529 panic!(
530 "C e2e generator: visitor pattern not supported for fixture: {}",
531 fixture.id
532 );
533 }
534
535 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
536 render_test_function(
537 &mut out,
538 fixture,
539 prefix,
540 &call_info.function_name,
541 result_var,
542 &call_info.args,
543 field_resolver,
544 &e2e_config.fields_c_types,
545 &call_info.result_type_name,
546 &call_info.options_type_name,
547 call_info.client_factory.as_deref(),
548 call_info.raw_c_result_type.as_deref(),
549 call_info.c_free_fn.as_deref(),
550 call_info.result_is_option,
551 );
552 if i + 1 < fixtures.len() {
553 let _ = writeln!(out);
554 }
555 }
556
557 out
558}
559
560#[allow(clippy::too_many_arguments)]
561fn render_test_function(
562 out: &mut String,
563 fixture: &Fixture,
564 prefix: &str,
565 function_name: &str,
566 result_var: &str,
567 args: &[crate::config::ArgMapping],
568 field_resolver: &FieldResolver,
569 fields_c_types: &HashMap<String, String>,
570 result_type_name: &str,
571 options_type_name: &str,
572 client_factory: Option<&str>,
573 raw_c_result_type: Option<&str>,
574 c_free_fn: Option<&str>,
575 result_is_option: bool,
576) {
577 let fn_name = sanitize_ident(&fixture.id);
578 let description = &fixture.description;
579
580 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
581
582 let _ = writeln!(out, "void test_{fn_name}(void) {{");
583 let _ = writeln!(out, " /* {description} */");
584
585 let prefix_upper = prefix.to_uppercase();
586
587 if let Some(factory) = client_factory {
592 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); for arg in args {
595 if arg.arg_type == "json_object" {
596 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
601 options_type_name.to_string()
602 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
603 format!("{}Request", stripped)
604 } else {
605 format!("{result_type_name}Request")
606 };
607 let request_type_snake = request_type_pascal.to_snake_case();
608 let var_name = format!("{request_type_snake}_handle");
609
610 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
611 let json_val = if field.is_empty() || field == "input" {
612 Some(&fixture.input)
613 } else {
614 fixture.input.get(field)
615 };
616
617 if let Some(val) = json_val {
618 if !val.is_null() {
619 let normalized = super::normalize_json_keys_to_snake_case(val);
620 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
621 let escaped = escape_c(&json_str);
622 let _ = writeln!(
623 out,
624 " {prefix_upper}{request_type_pascal}* {var_name} = \
625 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
626 );
627 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
628 request_handle_vars.push((arg.name.clone(), var_name));
629 }
630 }
631 }
632 }
633
634 let _ = writeln!(
635 out,
636 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
637 );
638 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
639
640 let method_args = if request_handle_vars.is_empty() {
641 String::new()
642 } else {
643 let handles: Vec<&str> = request_handle_vars.iter().map(|(_, v)| v.as_str()).collect();
644 format!(", {}", handles.join(", "))
645 };
646
647 let call_fn = format!("{prefix}_default_client_{function_name}");
648
649 if expects_error {
650 let _ = writeln!(
651 out,
652 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
653 );
654 for (_, var_name) in &request_handle_vars {
655 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
656 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
657 }
658 let _ = writeln!(out, " {prefix}_default_client_free(client);");
659 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
660 let _ = writeln!(out, "}}");
661 return;
662 }
663
664 let _ = writeln!(
665 out,
666 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
667 );
668 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
669
670 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
671 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
672
673 for assertion in &fixture.assertions {
674 if let Some(f) = &assertion.field {
675 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
676 let resolved = field_resolver.resolve(f);
677 let local_var = f.replace(['.', '['], "_").replace(']', "");
678 let has_map_access = resolved.contains('[');
679 if resolved.contains('.') {
680 emit_nested_accessor(
681 out,
682 prefix,
683 resolved,
684 &local_var,
685 result_var,
686 fields_c_types,
687 &mut intermediate_handles,
688 result_type_name,
689 );
690 } else {
691 let result_type_snake = result_type_name.to_snake_case();
692 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
693 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
694 }
695 accessed_fields.push((f.clone(), local_var, has_map_access));
696 }
697 }
698 }
699
700 for assertion in &fixture.assertions {
701 render_assertion(out, assertion, result_var, prefix, field_resolver, &accessed_fields);
702 }
703
704 for (_f, local_var, from_json) in &accessed_fields {
705 if *from_json {
706 let _ = writeln!(out, " free({local_var});");
707 } else {
708 let _ = writeln!(out, " {prefix}_free_string({local_var});");
709 }
710 }
711 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
712 if snake_type == "free_string" {
713 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
714 } else {
715 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
716 }
717 }
718 let result_type_snake = result_type_name.to_snake_case();
719 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
720 for (_, var_name) in &request_handle_vars {
721 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
722 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
723 }
724 let _ = writeln!(out, " {prefix}_default_client_free(client);");
725 let _ = writeln!(out, "}}");
726 return;
727 }
728
729 if let Some(raw_type) = raw_c_result_type {
732 let args_str = if args.is_empty() {
734 String::new()
735 } else {
736 let parts: Vec<String> = args
737 .iter()
738 .filter_map(|arg| {
739 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
740 let val = fixture.input.get(field);
741 match val {
742 None if arg.optional => Some("NULL".to_string()),
743 None => None,
744 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
745 Some(v) => Some(json_to_c(v)),
746 }
747 })
748 .collect();
749 parts.join(", ")
750 };
751
752 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
754
755 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
757 if has_not_error {
758 match raw_type {
759 "char*" if !result_is_option => {
760 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
761 }
762 "int32_t" => {
763 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
764 }
765 "uintptr_t" => {
766 let _ = writeln!(
767 out,
768 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
769 );
770 }
771 _ => {}
772 }
773 }
774
775 for assertion in &fixture.assertions {
777 match assertion.assertion_type.as_str() {
778 "not_error" | "error" => {} "not_empty" => {
780 let _ = writeln!(
781 out,
782 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
783 );
784 }
785 "is_empty" => {
786 if result_is_option && raw_type == "char*" {
787 let _ = writeln!(
788 out,
789 " assert({result_var} == NULL && \"expected empty/null value\");"
790 );
791 } else {
792 let _ = writeln!(
793 out,
794 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
795 );
796 }
797 }
798 "count_min" => {
799 if let Some(val) = &assertion.value {
800 if let Some(n) = val.as_u64() {
801 match raw_type {
802 "char*" => {
803 let _ = writeln!(out, " {{");
804 let _ = writeln!(
805 out,
806 " assert({result_var} != NULL && \"expected non-null JSON array\");"
807 );
808 let _ =
809 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
810 let _ = writeln!(
811 out,
812 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
813 );
814 let _ = writeln!(out, " }}");
815 }
816 _ => {
817 let _ = writeln!(
818 out,
819 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
820 );
821 }
822 }
823 }
824 }
825 }
826 "greater_than_or_equal" => {
827 if let Some(val) = &assertion.value {
828 let c_val = json_to_c(val);
829 let _ = writeln!(
830 out,
831 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
832 );
833 }
834 }
835 "contains" => {
836 if let Some(val) = &assertion.value {
837 let c_val = json_to_c(val);
838 let _ = writeln!(
839 out,
840 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
841 );
842 }
843 }
844 "contains_all" => {
845 if let Some(values) = &assertion.values {
846 for val in values {
847 let c_val = json_to_c(val);
848 let _ = writeln!(
849 out,
850 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
851 );
852 }
853 }
854 }
855 "equals" => {
856 if let Some(val) = &assertion.value {
857 let c_val = json_to_c(val);
858 if val.is_string() {
859 let _ = writeln!(
860 out,
861 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
862 );
863 } else {
864 let _ = writeln!(
865 out,
866 " assert({result_var} == {c_val} && \"equals assertion failed\");"
867 );
868 }
869 }
870 }
871 "not_contains" => {
872 if let Some(val) = &assertion.value {
873 let c_val = json_to_c(val);
874 let _ = writeln!(
875 out,
876 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
877 );
878 }
879 }
880 "starts_with" => {
881 if let Some(val) = &assertion.value {
882 let c_val = json_to_c(val);
883 let _ = writeln!(
884 out,
885 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
886 );
887 }
888 }
889 "is_true" => {
890 let _ = writeln!(out, " assert({result_var});");
891 }
892 "is_false" => {
893 let _ = writeln!(out, " assert(!{result_var});");
894 }
895 other => {
896 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
897 }
898 }
899 }
900
901 if raw_type == "char*" {
903 let free_fn = c_free_fn
904 .map(|s| s.to_string())
905 .unwrap_or_else(|| format!("{prefix}_free_string"));
906 if result_is_option {
907 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
908 } else {
909 let _ = writeln!(out, " {free_fn}({result_var});");
910 }
911 }
912
913 let _ = writeln!(out, "}}");
914 return;
915 }
916
917 let prefixed_fn = function_name.to_string();
923
924 let mut has_options_handle = false;
926 for arg in args {
927 if arg.arg_type == "json_object" {
928 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
929 if let Some(val) = fixture.input.get(field) {
930 if !val.is_null() {
931 let normalized = super::normalize_json_keys_to_snake_case(val);
935 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
936 let escaped = escape_c(&json_str);
937 let upper = prefix.to_uppercase();
938 let options_type_pascal = options_type_name;
939 let options_type_snake = options_type_name.to_snake_case();
940 let _ = writeln!(
941 out,
942 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
943 );
944 has_options_handle = true;
945 }
946 }
947 }
948 }
949
950 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
951
952 if expects_error {
953 let _ = writeln!(
954 out,
955 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
956 );
957 if has_options_handle {
958 let options_type_snake = options_type_name.to_snake_case();
959 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
960 }
961 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
962 let _ = writeln!(out, "}}");
963 return;
964 }
965
966 let _ = writeln!(
968 out,
969 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
970 );
971 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
972
973 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
981 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
984
985 for assertion in &fixture.assertions {
986 if let Some(f) = &assertion.field {
987 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
988 let resolved = field_resolver.resolve(f);
989 let local_var = f.replace(['.', '['], "_").replace(']', "");
990 let has_map_access = resolved.contains('[');
991
992 if resolved.contains('.') {
993 emit_nested_accessor(
994 out,
995 prefix,
996 resolved,
997 &local_var,
998 result_var,
999 fields_c_types,
1000 &mut intermediate_handles,
1001 result_type_name,
1002 );
1003 } else {
1004 let result_type_snake = result_type_name.to_snake_case();
1005 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1006 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1007 }
1008 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1009 }
1010 }
1011 }
1012
1013 for assertion in &fixture.assertions {
1014 render_assertion(out, assertion, result_var, prefix, field_resolver, &accessed_fields);
1015 }
1016
1017 for (_f, local_var, from_json) in &accessed_fields {
1019 if *from_json {
1020 let _ = writeln!(out, " free({local_var});");
1021 } else {
1022 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1023 }
1024 }
1025 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1027 if snake_type == "free_string" {
1028 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1030 } else {
1031 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1032 }
1033 }
1034 if has_options_handle {
1035 let options_type_snake = options_type_name.to_snake_case();
1036 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1037 }
1038 let result_type_snake = result_type_name.to_snake_case();
1039 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1040 let _ = writeln!(out, "}}");
1041}
1042
1043#[allow(clippy::too_many_arguments)]
1057fn emit_nested_accessor(
1058 out: &mut String,
1059 prefix: &str,
1060 resolved: &str,
1061 local_var: &str,
1062 result_var: &str,
1063 fields_c_types: &HashMap<String, String>,
1064 intermediate_handles: &mut Vec<(String, String)>,
1065 result_type_name: &str,
1066) {
1067 let segments: Vec<&str> = resolved.split('.').collect();
1068 let prefix_upper = prefix.to_uppercase();
1069
1070 let mut current_snake_type = result_type_name.to_snake_case();
1072 let mut current_handle = result_var.to_string();
1073
1074 for (i, segment) in segments.iter().enumerate() {
1075 let is_leaf = i + 1 == segments.len();
1076
1077 if let Some(bracket_pos) = segment.find('[') {
1079 let field_name = &segment[..bracket_pos];
1080 let key = segment[bracket_pos + 1..].trim_end_matches(']');
1081 let field_snake = field_name.to_snake_case();
1082 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
1083
1084 let json_var = format!("{field_snake}_json");
1087 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
1088 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
1089 let _ = writeln!(out, " assert({json_var} != NULL);");
1090 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
1092 }
1093 let _ = writeln!(
1095 out,
1096 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
1097 );
1098 return; }
1100
1101 let seg_snake = segment.to_snake_case();
1102 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
1103
1104 if is_leaf {
1105 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
1107 } else {
1108 let lookup_key = format!("{current_snake_type}.{seg_snake}");
1110 let return_type_pascal = match fields_c_types.get(&lookup_key) {
1111 Some(t) => t.clone(),
1112 None => {
1113 segment.to_pascal_case()
1115 }
1116 };
1117 let return_snake = return_type_pascal.to_snake_case();
1118 let handle_var = format!("{seg_snake}_handle");
1119
1120 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
1123 let _ = writeln!(
1124 out,
1125 " {prefix_upper}{return_type_pascal}* {handle_var} = \
1126 {accessor_fn}({current_handle});"
1127 );
1128 let _ = writeln!(out, " assert({handle_var} != NULL);");
1129 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
1130 }
1131
1132 current_snake_type = return_snake;
1133 current_handle = handle_var;
1134 }
1135 }
1136}
1137
1138fn build_args_string_c(
1142 input: &serde_json::Value,
1143 args: &[crate::config::ArgMapping],
1144 has_options_handle: bool,
1145) -> String {
1146 if args.is_empty() {
1147 return json_to_c(input);
1148 }
1149
1150 let parts: Vec<String> = args
1151 .iter()
1152 .filter_map(|arg| {
1153 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1154 let val = input.get(field);
1155 match val {
1156 None if arg.optional => Some("NULL".to_string()),
1158 None => None,
1160 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1162 Some(v) => {
1163 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
1166 Some("options_handle".to_string())
1167 } else {
1168 Some(json_to_c(v))
1169 }
1170 }
1171 }
1172 })
1173 .collect();
1174
1175 parts.join(", ")
1176}
1177
1178fn render_assertion(
1179 out: &mut String,
1180 assertion: &Assertion,
1181 result_var: &str,
1182 ffi_prefix: &str,
1183 _field_resolver: &FieldResolver,
1184 accessed_fields: &[(String, String, bool)],
1185) {
1186 if let Some(f) = &assertion.field {
1188 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
1189 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1190 return;
1191 }
1192 }
1193
1194 let field_expr = match &assertion.field {
1195 Some(f) if !f.is_empty() => {
1196 accessed_fields
1198 .iter()
1199 .find(|(k, _, _)| k == f)
1200 .map(|(_, local, _)| local.clone())
1201 .unwrap_or_else(|| result_var.to_string())
1202 }
1203 _ => result_var.to_string(),
1204 };
1205
1206 match assertion.assertion_type.as_str() {
1207 "equals" => {
1208 if let Some(expected) = &assertion.value {
1209 let c_val = json_to_c(expected);
1210 if expected.is_string() {
1211 let _ = writeln!(
1213 out,
1214 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
1215 );
1216 } else {
1217 let _ = writeln!(
1218 out,
1219 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
1220 );
1221 }
1222 }
1223 }
1224 "contains" => {
1225 if let Some(expected) = &assertion.value {
1226 let c_val = json_to_c(expected);
1227 let _ = writeln!(
1228 out,
1229 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1230 );
1231 }
1232 }
1233 "contains_all" => {
1234 if let Some(values) = &assertion.values {
1235 for val in values {
1236 let c_val = json_to_c(val);
1237 let _ = writeln!(
1238 out,
1239 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1240 );
1241 }
1242 }
1243 }
1244 "not_contains" => {
1245 if let Some(expected) = &assertion.value {
1246 let c_val = json_to_c(expected);
1247 let _ = writeln!(
1248 out,
1249 " assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1250 );
1251 }
1252 }
1253 "not_empty" => {
1254 let _ = writeln!(
1255 out,
1256 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
1257 );
1258 }
1259 "is_empty" => {
1260 let _ = writeln!(
1261 out,
1262 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
1263 );
1264 }
1265 "contains_any" => {
1266 if let Some(values) = &assertion.values {
1267 let _ = writeln!(out, " {{");
1268 let _ = writeln!(out, " int found = 0;");
1269 for val in values {
1270 let c_val = json_to_c(val);
1271 let _ = writeln!(
1272 out,
1273 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
1274 );
1275 }
1276 let _ = writeln!(
1277 out,
1278 " assert(found && \"expected to contain at least one of the specified values\");"
1279 );
1280 let _ = writeln!(out, " }}");
1281 }
1282 }
1283 "greater_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 greater than\");");
1287 }
1288 }
1289 "less_than" => {
1290 if let Some(val) = &assertion.value {
1291 let c_val = json_to_c(val);
1292 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
1293 }
1294 }
1295 "greater_than_or_equal" => {
1296 if let Some(val) = &assertion.value {
1297 let c_val = json_to_c(val);
1298 let _ = writeln!(
1299 out,
1300 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
1301 );
1302 }
1303 }
1304 "less_than_or_equal" => {
1305 if let Some(val) = &assertion.value {
1306 let c_val = json_to_c(val);
1307 let _ = writeln!(
1308 out,
1309 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
1310 );
1311 }
1312 }
1313 "starts_with" => {
1314 if let Some(expected) = &assertion.value {
1315 let c_val = json_to_c(expected);
1316 let _ = writeln!(
1317 out,
1318 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1319 );
1320 }
1321 }
1322 "ends_with" => {
1323 if let Some(expected) = &assertion.value {
1324 let c_val = json_to_c(expected);
1325 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
1326 let _ = writeln!(
1327 out,
1328 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
1329 );
1330 }
1331 }
1332 "min_length" => {
1333 if let Some(val) = &assertion.value {
1334 if let Some(n) = val.as_u64() {
1335 let _ = writeln!(
1336 out,
1337 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
1338 );
1339 }
1340 }
1341 }
1342 "max_length" => {
1343 if let Some(val) = &assertion.value {
1344 if let Some(n) = val.as_u64() {
1345 let _ = writeln!(
1346 out,
1347 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
1348 );
1349 }
1350 }
1351 }
1352 "count_min" => {
1353 if let Some(val) = &assertion.value {
1354 if let Some(n) = val.as_u64() {
1355 let _ = writeln!(out, " {{");
1356 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
1357 let _ = writeln!(
1358 out,
1359 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1360 );
1361 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1362 let _ = writeln!(
1363 out,
1364 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1365 );
1366 let _ = writeln!(out, " }}");
1367 }
1368 }
1369 }
1370 "count_equals" => {
1371 if let Some(val) = &assertion.value {
1372 if let Some(n) = val.as_u64() {
1373 let _ = writeln!(out, " {{");
1374 let _ = writeln!(out, " /* count_equals: count elements in array */");
1375 let _ = writeln!(
1376 out,
1377 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1378 );
1379 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1380 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
1381 let _ = writeln!(out, " }}");
1382 }
1383 }
1384 }
1385 "is_true" => {
1386 let _ = writeln!(out, " assert({field_expr});");
1387 }
1388 "is_false" => {
1389 let _ = writeln!(out, " assert(!{field_expr});");
1390 }
1391 "method_result" => {
1392 if let Some(method_name) = &assertion.method {
1393 render_method_result_assertion(
1394 out,
1395 result_var,
1396 ffi_prefix,
1397 method_name,
1398 assertion.args.as_ref(),
1399 assertion.return_type.as_deref(),
1400 assertion.check.as_deref().unwrap_or("is_true"),
1401 assertion.value.as_ref(),
1402 );
1403 } else {
1404 panic!("C e2e generator: method_result assertion missing 'method' field");
1405 }
1406 }
1407 "matches_regex" => {
1408 if let Some(expected) = &assertion.value {
1409 let c_val = json_to_c(expected);
1410 let _ = writeln!(out, " {{");
1411 let _ = writeln!(out, " regex_t _re;");
1412 let _ = writeln!(
1413 out,
1414 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
1415 );
1416 let _ = writeln!(
1417 out,
1418 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
1419 );
1420 let _ = writeln!(out, " regfree(&_re);");
1421 let _ = writeln!(out, " }}");
1422 }
1423 }
1424 "not_error" => {
1425 }
1427 "error" => {
1428 }
1430 other => {
1431 panic!("C e2e generator: unsupported assertion type: {other}");
1432 }
1433 }
1434}
1435
1436#[allow(clippy::too_many_arguments)]
1445fn render_method_result_assertion(
1446 out: &mut String,
1447 result_var: &str,
1448 ffi_prefix: &str,
1449 method_name: &str,
1450 args: Option<&serde_json::Value>,
1451 return_type: Option<&str>,
1452 check: &str,
1453 value: Option<&serde_json::Value>,
1454) {
1455 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
1456
1457 if return_type == Some("string") {
1458 let _ = writeln!(out, " {{");
1460 let _ = writeln!(out, " char* _method_result = {call_expr};");
1461 if check == "is_error" {
1462 let _ = writeln!(
1463 out,
1464 " assert(_method_result == NULL && \"expected method to return error\");"
1465 );
1466 let _ = writeln!(out, " }}");
1467 return;
1468 }
1469 let _ = writeln!(
1470 out,
1471 " assert(_method_result != NULL && \"method_result returned NULL\");"
1472 );
1473 match check {
1474 "contains" => {
1475 if let Some(val) = value {
1476 let c_val = json_to_c(val);
1477 let _ = writeln!(
1478 out,
1479 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1480 );
1481 }
1482 }
1483 "equals" => {
1484 if let Some(val) = value {
1485 let c_val = json_to_c(val);
1486 let _ = writeln!(
1487 out,
1488 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
1489 );
1490 }
1491 }
1492 "is_true" => {
1493 let _ = writeln!(
1494 out,
1495 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
1496 );
1497 }
1498 "count_min" => {
1499 if let Some(val) = value {
1500 let n = val.as_u64().unwrap_or(0);
1501 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1502 let _ = writeln!(
1503 out,
1504 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
1505 );
1506 }
1507 }
1508 other_check => {
1509 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
1510 }
1511 }
1512 let _ = writeln!(out, " free(_method_result);");
1513 let _ = writeln!(out, " }}");
1514 return;
1515 }
1516
1517 match check {
1519 "equals" => {
1520 if let Some(val) = value {
1521 let c_val = json_to_c(val);
1522 let _ = writeln!(
1523 out,
1524 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
1525 );
1526 }
1527 }
1528 "is_true" => {
1529 let _ = writeln!(
1530 out,
1531 " assert({call_expr} && \"method_result is_true assertion failed\");"
1532 );
1533 }
1534 "is_false" => {
1535 let _ = writeln!(
1536 out,
1537 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
1538 );
1539 }
1540 "greater_than_or_equal" => {
1541 if let Some(val) = value {
1542 let n = val.as_u64().unwrap_or(0);
1543 let _ = writeln!(
1544 out,
1545 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
1546 );
1547 }
1548 }
1549 "count_min" => {
1550 if let Some(val) = value {
1551 let n = val.as_u64().unwrap_or(0);
1552 let _ = writeln!(
1553 out,
1554 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
1555 );
1556 }
1557 }
1558 other_check => {
1559 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1560 }
1561 }
1562}
1563
1564fn build_c_method_call(
1571 result_var: &str,
1572 ffi_prefix: &str,
1573 method_name: &str,
1574 args: Option<&serde_json::Value>,
1575) -> String {
1576 let extra_args = if let Some(args_val) = args {
1577 args_val
1578 .as_object()
1579 .map(|obj| {
1580 obj.values()
1581 .map(|v| match v {
1582 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
1583 serde_json::Value::Bool(true) => "1".to_string(),
1584 serde_json::Value::Bool(false) => "0".to_string(),
1585 serde_json::Value::Number(n) => n.to_string(),
1586 serde_json::Value::Null => "NULL".to_string(),
1587 other => format!("\"{}\"", escape_c(&other.to_string())),
1588 })
1589 .collect::<Vec<_>>()
1590 .join(", ")
1591 })
1592 .unwrap_or_default()
1593 } else {
1594 String::new()
1595 };
1596
1597 if extra_args.is_empty() {
1598 format!("{ffi_prefix}_{method_name}({result_var})")
1599 } else {
1600 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
1601 }
1602}
1603
1604fn json_to_c(value: &serde_json::Value) -> String {
1606 match value {
1607 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
1608 serde_json::Value::Bool(true) => "1".to_string(),
1609 serde_json::Value::Bool(false) => "0".to_string(),
1610 serde_json::Value::Number(n) => n.to_string(),
1611 serde_json::Value::Null => "NULL".to_string(),
1612 other => format!("\"{}\"", escape_c(&other.to_string())),
1613 }
1614}