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, HashSet};
18use std::fmt::Write as FmtWrite;
19use std::path::PathBuf;
20
21use super::E2eCodegen;
22
23pub struct CCodegen;
25
26fn is_primitive_c_type(t: &str) -> bool {
30 matches!(
31 t,
32 "uint8_t"
33 | "uint16_t"
34 | "uint32_t"
35 | "uint64_t"
36 | "int8_t"
37 | "int16_t"
38 | "int32_t"
39 | "int64_t"
40 | "uintptr_t"
41 | "intptr_t"
42 | "size_t"
43 | "ssize_t"
44 | "double"
45 | "float"
46 | "bool"
47 | "int"
48 )
49}
50
51fn infer_opaque_handle_type(
69 fields_c_types: &HashMap<String, String>,
70 parent_snake_type: &str,
71 field_snake: &str,
72) -> Option<String> {
73 let lookup_key = format!("{parent_snake_type}.{field_snake}");
74 if let Some(t) = fields_c_types.get(&lookup_key) {
75 if !is_primitive_c_type(t) && t != "char*" {
76 return Some(t.clone());
77 }
78 return None;
80 }
81 let nested_prefix = format!("{field_snake}.");
83 if fields_c_types.keys().any(|k| k.starts_with(&nested_prefix)) {
84 return Some(field_snake.to_pascal_case());
85 }
86 None
87}
88
89#[allow(clippy::too_many_arguments)]
106fn try_emit_enum_accessor(
107 out: &mut String,
108 prefix: &str,
109 prefix_upper: &str,
110 raw_field: &str,
111 resolved_field: &str,
112 parent_snake_type: &str,
113 accessor_fn: &str,
114 parent_handle: &str,
115 local_var: &str,
116 fields_c_types: &HashMap<String, String>,
117 fields_enum: &HashSet<String>,
118 intermediate_handles: &mut Vec<(String, String)>,
119) -> bool {
120 if !(fields_enum.contains(raw_field) || fields_enum.contains(resolved_field)) {
121 return false;
122 }
123 let lookup_key = format!("{parent_snake_type}.{resolved_field}");
124 let Some(enum_pascal) = fields_c_types.get(&lookup_key) else {
125 return false;
126 };
127 if is_primitive_c_type(enum_pascal) || enum_pascal == "char*" {
128 return false;
129 }
130 let enum_snake = enum_pascal.to_snake_case();
131 let handle_var = format!("{local_var}_handle");
132 let _ = writeln!(
133 out,
134 " {prefix_upper}{enum_pascal}* {handle_var} = {accessor_fn}({parent_handle});"
135 );
136 let _ = writeln!(out, " assert({handle_var} != NULL);");
137 let _ = writeln!(
138 out,
139 " char* {local_var} = {prefix}_{enum_snake}_to_string({handle_var});"
140 );
141 intermediate_handles.push((handle_var, enum_snake));
142 true
143}
144
145impl E2eCodegen for CCodegen {
146 fn generate(
147 &self,
148 groups: &[FixtureGroup],
149 e2e_config: &E2eConfig,
150 config: &ResolvedCrateConfig,
151 _type_defs: &[alef_core::ir::TypeDef],
152 ) -> Result<Vec<GeneratedFile>> {
153 let lang = self.language_name();
154 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
155
156 let mut files = Vec::new();
157
158 let call = &e2e_config.call;
160 let overrides = call.overrides.get(lang);
161 let result_var = &call.result_var;
162 let prefix = overrides
163 .and_then(|o| o.prefix.as_ref())
164 .cloned()
165 .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
166 .unwrap_or_default();
167 let header = overrides
168 .and_then(|o| o.header.as_ref())
169 .cloned()
170 .unwrap_or_else(|| config.ffi_header_name());
171
172 let c_pkg = e2e_config.resolve_package("c");
174 let lib_name = c_pkg
175 .as_ref()
176 .and_then(|p| p.name.as_ref())
177 .cloned()
178 .unwrap_or_else(|| config.ffi_lib_name());
179
180 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
182 .iter()
183 .filter_map(|group| {
184 let active: Vec<&Fixture> = group
185 .fixtures
186 .iter()
187 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
188 .filter(|f| f.visitor.is_none())
189 .collect();
190 if active.is_empty() { None } else { Some((group, active)) }
191 })
192 .collect();
193
194 let ffi_crate_path = c_pkg
202 .as_ref()
203 .and_then(|p| p.path.as_ref())
204 .cloned()
205 .unwrap_or_else(|| config.ffi_crate_path());
206
207 let category_names: Vec<String> = active_groups
209 .iter()
210 .map(|(g, _)| sanitize_filename(&g.category))
211 .collect();
212 let needs_mock_server = active_groups
213 .iter()
214 .flat_map(|(_, fixtures)| fixtures.iter())
215 .any(|f| f.needs_mock_server());
216 files.push(GeneratedFile {
217 path: output_base.join("Makefile"),
218 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name, needs_mock_server),
219 generated_header: true,
220 });
221
222 let github_repo = config.github_repo();
224 let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
225 let ffi_pkg_name = e2e_config
226 .registry
227 .packages
228 .get("c")
229 .and_then(|p| p.name.as_ref())
230 .cloned()
231 .unwrap_or_else(|| lib_name.clone());
232 files.push(GeneratedFile {
233 path: output_base.join("download_ffi.sh"),
234 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
235 generated_header: true,
236 });
237
238 files.push(GeneratedFile {
240 path: output_base.join("test_runner.h"),
241 content: render_test_runner_header(&active_groups),
242 generated_header: true,
243 });
244
245 files.push(GeneratedFile {
247 path: output_base.join("main.c"),
248 content: render_main_c(&active_groups),
249 generated_header: true,
250 });
251
252 let field_resolver = FieldResolver::new(
253 &e2e_config.fields,
254 &e2e_config.fields_optional,
255 &e2e_config.result_fields,
256 &e2e_config.fields_array,
257 &std::collections::HashSet::new(),
258 );
259
260 for (group, active) in &active_groups {
264 let filename = format!("test_{}.c", sanitize_filename(&group.category));
265 let content = render_test_file(
266 &group.category,
267 active,
268 &header,
269 &prefix,
270 result_var,
271 e2e_config,
272 lang,
273 &field_resolver,
274 );
275 files.push(GeneratedFile {
276 path: output_base.join(filename),
277 content,
278 generated_header: true,
279 });
280 }
281
282 Ok(files)
283 }
284
285 fn language_name(&self) -> &'static str {
286 "c"
287 }
288}
289
290struct ResolvedCallInfo {
292 function_name: String,
293 result_type_name: String,
294 options_type_name: String,
295 client_factory: Option<String>,
296 args: Vec<crate::config::ArgMapping>,
297 raw_c_result_type: Option<String>,
298 c_free_fn: Option<String>,
299 c_engine_factory: Option<String>,
300 result_is_option: bool,
301 result_is_bytes: bool,
307 extra_args: Vec<String>,
311}
312
313fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
314 let overrides = call.overrides.get(lang);
315 let function_name = overrides
316 .and_then(|o| o.function.as_ref())
317 .cloned()
318 .unwrap_or_else(|| call.function.clone());
319 let result_type_name = overrides
324 .and_then(|o| o.result_type.as_ref())
325 .cloned()
326 .unwrap_or_else(|| call.function.to_pascal_case());
327 let options_type_name = overrides
328 .and_then(|o| o.options_type.as_deref())
329 .unwrap_or("ConversionOptions")
330 .to_string();
331 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
332 let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
333 let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
334 let c_engine_factory = overrides.and_then(|o| o.c_engine_factory.clone());
335 let result_is_option = overrides
336 .and_then(|o| if o.result_is_option { Some(true) } else { None })
337 .unwrap_or(call.result_is_option);
338 let result_is_bytes = call.result_is_bytes || overrides.is_some_and(|o| o.result_is_bytes);
343 let extra_args = overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
344 ResolvedCallInfo {
345 function_name,
346 result_type_name,
347 options_type_name,
348 client_factory,
349 args: call.args.clone(),
350 raw_c_result_type,
351 c_free_fn,
352 c_engine_factory,
353 result_is_option,
354 result_is_bytes,
355 extra_args,
356 }
357}
358
359fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
365 let call = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
366 let mut info = resolve_call_info(call, lang);
367
368 let default_overrides = e2e_config.call.overrides.get(lang);
369
370 if info.client_factory.is_none() {
373 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
374 info.client_factory = Some(factory.clone());
375 }
376 }
377
378 if info.c_engine_factory.is_none() {
381 if let Some(factory) = default_overrides.and_then(|o| o.c_engine_factory.as_ref()) {
382 info.c_engine_factory = Some(factory.clone());
383 }
384 }
385
386 info
387}
388
389fn render_makefile(
390 categories: &[String],
391 header_name: &str,
392 ffi_crate_path: &str,
393 lib_name: &str,
394 needs_mock_server: bool,
395) -> String {
396 let mut out = String::new();
397 out.push_str(&hash::header(CommentStyle::Hash));
398 let _ = writeln!(out, "CC = gcc");
399 let _ = writeln!(out, "FFI_DIR = ffi");
400 let _ = writeln!(out);
401
402 let link_lib_name = lib_name.replace('-', "_");
407
408 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
410 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
411 let _ = writeln!(
412 out,
413 " LDFLAGS = -L$(FFI_DIR)/lib -l{link_lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
414 );
415 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
416 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
417 let _ = writeln!(
418 out,
419 " LDFLAGS = -L../../target/release -l{link_lib_name} -Wl,-rpath,../../target/release"
420 );
421 let _ = writeln!(out, "else");
422 let _ = writeln!(
423 out,
424 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
425 );
426 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
427 let _ = writeln!(out, "endif");
428 let _ = writeln!(out);
429
430 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
431 let srcs = src_files.join(" ");
432
433 let _ = writeln!(out, "SRCS = main.c {srcs}");
434 let _ = writeln!(out, "TARGET = run_tests");
435 let _ = writeln!(out);
436 let _ = writeln!(out, ".PHONY: all clean test");
437 let _ = writeln!(out);
438 let _ = writeln!(out, "all: $(TARGET)");
439 let _ = writeln!(out);
440 let _ = writeln!(out, "$(TARGET): $(SRCS)");
441 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
442 let _ = writeln!(out);
443
444 if !needs_mock_server {
445 let _ = writeln!(out, "test: $(TARGET)");
447 let _ = writeln!(out, "\t./$(TARGET)");
448 let _ = writeln!(out);
449 let _ = writeln!(out, "clean:");
450 let _ = writeln!(out, "\trm -f $(TARGET)");
451 return out;
452 }
453
454 let _ = writeln!(out, "MOCK_SERVER_BIN ?= ../rust/target/release/mock-server");
464 let _ = writeln!(out, "FIXTURES_DIR ?= ../../fixtures");
465 let _ = writeln!(out);
466 let _ = writeln!(out, "test: $(TARGET)");
467 let _ = writeln!(out, "\t@if [ -n \"$$MOCK_SERVER_URL\" ]; then \\");
468 let _ = writeln!(out, "\t\tif [ -n \"$$MOCK_SERVERS\" ]; then \\");
471 let _ = writeln!(
472 out,
473 "\t\t\teval $$(python3 -c \"import json,os; d=json.loads(os.environ.get('MOCK_SERVERS','{{}}')); print(' '.join('export MOCK_SERVER_'+k.upper()+'='+v for k,v in d.items()))\"); \\"
474 );
475 let _ = writeln!(out, "\t\tfi; \\");
476 let _ = writeln!(out, "\t\t./$(TARGET); \\");
477 let _ = writeln!(out, "\telse \\");
478 let _ = writeln!(out, "\t\tif [ ! -x \"$(MOCK_SERVER_BIN)\" ]; then \\");
479 let _ = writeln!(
480 out,
481 "\t\t\techo \"mock-server binary not found at $(MOCK_SERVER_BIN); run: cargo build -p mock-server --release\" >&2; \\"
482 );
483 let _ = writeln!(out, "\t\t\texit 1; \\");
484 let _ = writeln!(out, "\t\tfi; \\");
485 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
486 let _ = writeln!(out, "\t\tmkfifo mock_server.stdin; \\");
487 let _ = writeln!(
488 out,
489 "\t\t\"$(MOCK_SERVER_BIN)\" \"$(FIXTURES_DIR)\" <mock_server.stdin >mock_server.stdout 2>&1 & \\"
490 );
491 let _ = writeln!(out, "\t\tMOCK_PID=$$!; \\");
492 let _ = writeln!(out, "\t\texec 9>mock_server.stdin; \\");
493 let _ = writeln!(out, "\t\tMOCK_URL=\"\"; MOCK_SERVERS_JSON=\"\"; \\");
494 let _ = writeln!(out, "\t\tfor _ in $$(seq 1 100); do \\");
496 let _ = writeln!(out, "\t\t\tif [ -s mock_server.stdout ]; then \\");
497 let _ = writeln!(
498 out,
499 "\t\t\t\tMOCK_URL=$$(grep -o 'MOCK_SERVER_URL=[^ ]*' mock_server.stdout | head -1 | cut -d= -f2); \\"
500 );
501 let _ = writeln!(out, "\t\t\t\tif [ -n \"$$MOCK_URL\" ]; then break; fi; \\");
502 let _ = writeln!(out, "\t\t\tfi; \\");
503 let _ = writeln!(out, "\t\t\tsleep 0.05; \\");
504 let _ = writeln!(out, "\t\tdone; \\");
505 let _ = writeln!(
507 out,
508 "\t\tMOCK_SERVERS_JSON=$$(grep -o 'MOCK_SERVERS={{.*}}' mock_server.stdout | head -1 | cut -d= -f2-); \\"
509 );
510 let _ = writeln!(
511 out,
512 "\t\tif [ -z \"$$MOCK_URL\" ]; then echo 'failed to start mock-server' >&2; cat mock_server.stdout >&2; kill $$MOCK_PID 2>/dev/null || true; exit 1; fi; \\"
513 );
514 let _ = writeln!(
516 out,
517 "\t\tif [ -n \"$$MOCK_SERVERS_JSON\" ] && command -v python3 >/dev/null 2>&1; then \\"
518 );
519 let _ = writeln!(
520 out,
521 "\t\t\teval $$(python3 -c \"import json,sys; d=json.loads(sys.argv[1]); print(' '.join('export MOCK_SERVER_{{}}={{}}'.format(k.upper(),v) for k,v in d.items()))\" \"$$MOCK_SERVERS_JSON\"); \\"
522 );
523 let _ = writeln!(out, "\t\tfi; \\");
524 let _ = writeln!(out, "\t\tMOCK_SERVER_URL=\"$$MOCK_URL\" ./$(TARGET); STATUS=$$?; \\");
525 let _ = writeln!(out, "\t\texec 9>&-; \\");
526 let _ = writeln!(out, "\t\tkill $$MOCK_PID 2>/dev/null || true; \\");
527 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
528 let _ = writeln!(out, "\t\texit $$STATUS; \\");
529 let _ = writeln!(out, "\tfi");
530 let _ = writeln!(out);
531 let _ = writeln!(out, "clean:");
532 let _ = writeln!(out, "\trm -f $(TARGET) mock_server.stdout mock_server.stdin");
533 out
534}
535
536fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
537 let mut out = String::new();
538 let _ = writeln!(out, "#!/usr/bin/env bash");
539 out.push_str(&hash::header(CommentStyle::Hash));
540 let _ = writeln!(out, "set -euo pipefail");
541 let _ = writeln!(out);
542 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
543 let _ = writeln!(out, "VERSION=\"{version}\"");
544 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
545 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
546 let _ = writeln!(out);
547 let _ = writeln!(out, "# Detect OS and architecture.");
548 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
549 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
550 let _ = writeln!(out);
551 let _ = writeln!(out, "case \"$ARCH\" in");
552 let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
553 let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
554 let _ = writeln!(out, "*)");
555 let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
556 let _ = writeln!(out, " exit 1");
557 let _ = writeln!(out, " ;;");
558 let _ = writeln!(out, "esac");
559 let _ = writeln!(out);
560 let _ = writeln!(out, "case \"$OS\" in");
561 let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
562 let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
563 let _ = writeln!(out, "*)");
564 let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
565 let _ = writeln!(out, " exit 1");
566 let _ = writeln!(out, " ;;");
567 let _ = writeln!(out, "esac");
568 let _ = writeln!(out);
569 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
570 let _ = writeln!(
571 out,
572 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
573 );
574 let _ = writeln!(out);
575 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
576 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
577 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
578 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
579 out
580}
581
582fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
583 let mut out = String::new();
584 out.push_str(&hash::header(CommentStyle::Block));
585 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
586 let _ = writeln!(out, "#define TEST_RUNNER_H");
587 let _ = writeln!(out);
588 let _ = writeln!(out, "#include <string.h>");
589 let _ = writeln!(out, "#include <stdlib.h>");
590 let _ = writeln!(out);
591 let _ = writeln!(out, "/**");
593 let _ = writeln!(
594 out,
595 " * Compare a string against an expected value, trimming trailing whitespace."
596 );
597 let _ = writeln!(
598 out,
599 " * Returns 0 if the trimmed actual string equals the expected string."
600 );
601 let _ = writeln!(out, " */");
602 let _ = writeln!(
603 out,
604 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
605 );
606 let _ = writeln!(
607 out,
608 " if (actual == NULL || expected == NULL) return actual != expected;"
609 );
610 let _ = writeln!(out, " size_t alen = strlen(actual);");
611 let _ = writeln!(
612 out,
613 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
614 );
615 let _ = writeln!(out, " size_t elen = strlen(expected);");
616 let _ = writeln!(out, " if (alen != elen) return 1;");
617 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
618 let _ = writeln!(out, "}}");
619 let _ = writeln!(out);
620
621 let _ = writeln!(
624 out,
625 "static inline char *alef_json_get_object(const char *json, const char *key);"
626 );
627 let _ = writeln!(out);
628 let _ = writeln!(out, "/**");
629 let _ = writeln!(
630 out,
631 " * Extract a string value for a given key from a JSON object string."
632 );
633 let _ = writeln!(
634 out,
635 " * Returns a heap-allocated copy of the value, or NULL if not found."
636 );
637 let _ = writeln!(out, " * Caller must free() the returned string.");
638 let _ = writeln!(out, " */");
639 let _ = writeln!(
640 out,
641 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
642 );
643 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
644 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
645 let _ = writeln!(out, " size_t key_len = strlen(key);");
646 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
647 let _ = writeln!(out, " if (!pattern) return NULL;");
648 let _ = writeln!(out, " pattern[0] = '\"';");
649 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
650 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
651 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
652 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
653 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
654 let _ = writeln!(out, " free(pattern);");
655 let _ = writeln!(out, " if (!found) return NULL;");
656 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
657 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
658 let _ = writeln!(
659 out,
660 " /* Non-string values (arrays/objects) — fall through to alef_json_get_object so"
661 );
662 let _ = writeln!(
663 out,
664 " leaf accessors over collection-typed fields (Vec<T>, Option<Vec<T>>) work for"
665 );
666 let _ = writeln!(
667 out,
668 " not_empty / count_equals assertions without needing per-field type metadata. */"
669 );
670 let _ = writeln!(out, " if (*found == '{{' || *found == '[') {{");
671 let _ = writeln!(out, " return alef_json_get_object(json, key);");
672 let _ = writeln!(out, " }}");
673 let _ = writeln!(
674 out,
675 " /* Primitive non-string value: extract its raw token (numeric / true / false / null)"
676 );
677 let _ = writeln!(
678 out,
679 " so callers asserting on numeric fields can `atoll`/`atof` the result. */"
680 );
681 let _ = writeln!(out, " if (*found != '\"') {{");
682 let _ = writeln!(out, " const char *p = found;");
683 let _ = writeln!(
684 out,
685 " while (*p && *p != ',' && *p != '}}' && *p != ']' && *p != ' ' && *p != '\\t' && *p != '\\n' && *p != '\\r') p++;"
686 );
687 let _ = writeln!(out, " size_t plen = (size_t)(p - found);");
688 let _ = writeln!(out, " if (plen == 0) return NULL;");
689 let _ = writeln!(out, " char *prim = (char *)malloc(plen + 1);");
690 let _ = writeln!(out, " if (!prim) return NULL;");
691 let _ = writeln!(out, " memcpy(prim, found, plen);");
692 let _ = writeln!(out, " prim[plen] = '\\0';");
693 let _ = writeln!(out, " return prim;");
694 let _ = writeln!(out, " }}");
695 let _ = writeln!(out, " found++; /* skip opening quote */");
696 let _ = writeln!(out, " const char *end = found;");
697 let _ = writeln!(out, " while (*end && *end != '\"') {{");
698 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
699 let _ = writeln!(out, " else end++;");
700 let _ = writeln!(out, " }}");
701 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
702 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
703 let _ = writeln!(out, " if (!result_str) return NULL;");
704 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
705 let _ = writeln!(out, " result_str[val_len] = '\\0';");
706 let _ = writeln!(out, " return result_str;");
707 let _ = writeln!(out, "}}");
708 let _ = writeln!(out);
709 let _ = writeln!(out, "/**");
710 let _ = writeln!(
711 out,
712 " * Extract a JSON object/array value `{{...}}` or `[...]` for a given key from"
713 );
714 let _ = writeln!(
715 out,
716 " * a JSON object string. Returns a heap-allocated copy of the value INCLUDING"
717 );
718 let _ = writeln!(
719 out,
720 " * its surrounding braces, or NULL if the key is missing or its value is a"
721 );
722 let _ = writeln!(out, " * primitive. Caller must free() the returned string.");
723 let _ = writeln!(out, " *");
724 let _ = writeln!(
725 out,
726 " * Used by chained-accessor codegen for intermediate object extraction:"
727 );
728 let _ = writeln!(
729 out,
730 " * `choices[0].message.content` first peels off `message` (an object), then"
731 );
732 let _ = writeln!(out, " * looks up `content` (a string) within the extracted substring.");
733 let _ = writeln!(out, " */");
734 let _ = writeln!(
735 out,
736 "static inline char *alef_json_get_object(const char *json, const char *key) {{"
737 );
738 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
739 let _ = writeln!(out, " size_t key_len = strlen(key);");
740 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 4);");
741 let _ = writeln!(out, " if (!pattern) return NULL;");
742 let _ = writeln!(out, " pattern[0] = '\"';");
743 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
744 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
745 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
746 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
747 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
748 let _ = writeln!(out, " free(pattern);");
749 let _ = writeln!(out, " if (!found) return NULL;");
750 let _ = writeln!(out, " found += key_len + 3;");
751 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
752 let _ = writeln!(out, " char open_ch = *found;");
753 let _ = writeln!(out, " char close_ch;");
754 let _ = writeln!(out, " if (open_ch == '{{') close_ch = '}}';");
755 let _ = writeln!(out, " else if (open_ch == '[') close_ch = ']';");
756 let _ = writeln!(
757 out,
758 " else return NULL; /* primitive — caller should use alef_json_get_string */"
759 );
760 let _ = writeln!(out, " int depth = 0;");
761 let _ = writeln!(out, " int in_string = 0;");
762 let _ = writeln!(out, " const char *end = found;");
763 let _ = writeln!(out, " for (; *end; end++) {{");
764 let _ = writeln!(out, " if (in_string) {{");
765 let _ = writeln!(
766 out,
767 " if (*end == '\\\\' && *(end + 1)) {{ end++; continue; }}"
768 );
769 let _ = writeln!(out, " if (*end == '\"') in_string = 0;");
770 let _ = writeln!(out, " continue;");
771 let _ = writeln!(out, " }}");
772 let _ = writeln!(out, " if (*end == '\"') {{ in_string = 1; continue; }}");
773 let _ = writeln!(out, " if (*end == open_ch) depth++;");
774 let _ = writeln!(out, " else if (*end == close_ch) {{");
775 let _ = writeln!(out, " depth--;");
776 let _ = writeln!(out, " if (depth == 0) {{ end++; break; }}");
777 let _ = writeln!(out, " }}");
778 let _ = writeln!(out, " }}");
779 let _ = writeln!(out, " if (depth != 0) return NULL;");
780 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
781 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
782 let _ = writeln!(out, " if (!result_str) return NULL;");
783 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
784 let _ = writeln!(out, " result_str[val_len] = '\\0';");
785 let _ = writeln!(out, " return result_str;");
786 let _ = writeln!(out, "}}");
787 let _ = writeln!(out);
788 let _ = writeln!(out, "/**");
789 let _ = writeln!(
790 out,
791 " * Extract the Nth top-level element of a JSON array as a heap string."
792 );
793 let _ = writeln!(
794 out,
795 " * Returns NULL if the input is not an array, the index is out of bounds, or"
796 );
797 let _ = writeln!(out, " * allocation fails. Caller must free() the returned string.");
798 let _ = writeln!(out, " */");
799 let _ = writeln!(
800 out,
801 "static inline char *alef_json_array_get_index(const char *json, int index) {{"
802 );
803 let _ = writeln!(out, " if (json == NULL || index < 0) return NULL;");
804 let _ = writeln!(
805 out,
806 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
807 );
808 let _ = writeln!(out, " if (*json != '[') return NULL;");
809 let _ = writeln!(out, " json++;");
810 let _ = writeln!(out, " int current = 0;");
811 let _ = writeln!(out, " while (*json) {{");
812 let _ = writeln!(
813 out,
814 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
815 );
816 let _ = writeln!(out, " if (*json == ']') return NULL;");
817 let _ = writeln!(out, " const char *elem_start = json;");
818 let _ = writeln!(out, " int depth = 0;");
819 let _ = writeln!(out, " int in_string = 0;");
820 let _ = writeln!(out, " for (; *json; json++) {{");
821 let _ = writeln!(out, " if (in_string) {{");
822 let _ = writeln!(
823 out,
824 " if (*json == '\\\\' && *(json + 1)) {{ json++; continue; }}"
825 );
826 let _ = writeln!(out, " if (*json == '\"') in_string = 0;");
827 let _ = writeln!(out, " continue;");
828 let _ = writeln!(out, " }}");
829 let _ = writeln!(out, " if (*json == '\"') {{ in_string = 1; continue; }}");
830 let _ = writeln!(out, " if (*json == '{{' || *json == '[') depth++;");
831 let _ = writeln!(out, " else if (*json == '}}' || *json == ']') {{");
832 let _ = writeln!(out, " if (depth == 0) break;");
833 let _ = writeln!(out, " depth--;");
834 let _ = writeln!(out, " }}");
835 let _ = writeln!(out, " else if (*json == ',' && depth == 0) break;");
836 let _ = writeln!(out, " }}");
837 let _ = writeln!(out, " if (current == index) {{");
838 let _ = writeln!(out, " const char *elem_end = json;");
839 let _ = writeln!(
840 out,
841 " while (elem_end > elem_start && (*(elem_end - 1) == ' ' || *(elem_end - 1) == '\\t' || *(elem_end - 1) == '\\n')) elem_end--;"
842 );
843 let _ = writeln!(out, " size_t elem_len = (size_t)(elem_end - elem_start);");
844 let _ = writeln!(out, " char *out_buf = (char *)malloc(elem_len + 1);");
845 let _ = writeln!(out, " if (!out_buf) return NULL;");
846 let _ = writeln!(out, " memcpy(out_buf, elem_start, elem_len);");
847 let _ = writeln!(out, " out_buf[elem_len] = '\\0';");
848 let _ = writeln!(out, " return out_buf;");
849 let _ = writeln!(out, " }}");
850 let _ = writeln!(out, " current++;");
851 let _ = writeln!(out, " if (*json == ']') return NULL;");
852 let _ = writeln!(out, " if (*json == ',') json++;");
853 let _ = writeln!(out, " }}");
854 let _ = writeln!(out, " return NULL;");
855 let _ = writeln!(out, "}}");
856 let _ = writeln!(out);
857 let _ = writeln!(out, "/**");
858 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
859 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
860 let _ = writeln!(out, " */");
861 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
862 let _ = writeln!(out, " if (json == NULL) return 0;");
863 let _ = writeln!(out, " /* Skip leading whitespace */");
864 let _ = writeln!(
865 out,
866 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
867 );
868 let _ = writeln!(out, " if (*json != '[') return 0;");
869 let _ = writeln!(out, " json++;");
870 let _ = writeln!(out, " /* Skip whitespace after '[' */");
871 let _ = writeln!(
872 out,
873 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
874 );
875 let _ = writeln!(out, " if (*json == ']') return 0;");
876 let _ = writeln!(out, " int count = 1;");
877 let _ = writeln!(out, " int depth = 0;");
878 let _ = writeln!(out, " int in_string = 0;");
879 let _ = writeln!(
880 out,
881 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
882 );
883 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
884 let _ = writeln!(
885 out,
886 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
887 );
888 let _ = writeln!(out, " if (in_string) continue;");
889 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
890 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
891 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
892 let _ = writeln!(out, " }}");
893 let _ = writeln!(out, " return count;");
894 let _ = writeln!(out, "}}");
895 let _ = writeln!(out);
896
897 for (group, fixtures) in active_groups {
898 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
899 for fixture in fixtures {
900 let fn_name = sanitize_ident(&fixture.id);
901 let _ = writeln!(out, "void test_{fn_name}(void);");
902 }
903 let _ = writeln!(out);
904 }
905
906 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
907 out
908}
909
910fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
911 let mut out = String::new();
912 out.push_str(&hash::header(CommentStyle::Block));
913 let _ = writeln!(out, "#include <stdio.h>");
914 let _ = writeln!(out, "#include \"test_runner.h\"");
915 let _ = writeln!(out);
916 let _ = writeln!(out, "int main(void) {{");
917 let _ = writeln!(out, " int passed = 0;");
918 let _ = writeln!(out);
919
920 for (group, fixtures) in active_groups {
921 let _ = writeln!(out, " /* Category: {} */", group.category);
922 for fixture in fixtures {
923 let fn_name = sanitize_ident(&fixture.id);
924 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
925 let _ = writeln!(out, " test_{fn_name}();");
926 let _ = writeln!(out, " printf(\" PASSED\\n\");");
927 let _ = writeln!(out, " passed++;");
928 }
929 let _ = writeln!(out);
930 }
931
932 let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
933 let _ = writeln!(out, " return 0;");
934 let _ = writeln!(out, "}}");
935 out
936}
937
938#[allow(clippy::too_many_arguments)]
939fn render_test_file(
940 category: &str,
941 fixtures: &[&Fixture],
942 header: &str,
943 prefix: &str,
944 result_var: &str,
945 e2e_config: &E2eConfig,
946 lang: &str,
947 field_resolver: &FieldResolver,
948) -> String {
949 let mut out = String::new();
950 out.push_str(&hash::header(CommentStyle::Block));
951 let _ = writeln!(out, "/* E2e tests for category: {category} */");
952 let _ = writeln!(out);
953 let _ = writeln!(out, "#include <assert.h>");
954 let _ = writeln!(out, "#include <stdint.h>");
955 let _ = writeln!(out, "#include <string.h>");
956 let _ = writeln!(out, "#include <stdio.h>");
957 let _ = writeln!(out, "#include <stdlib.h>");
958 let _ = writeln!(out, "#include \"{header}\"");
959 let _ = writeln!(out, "#include \"test_runner.h\"");
960 let _ = writeln!(out);
961
962 for (i, fixture) in fixtures.iter().enumerate() {
963 if fixture.visitor.is_some() {
966 panic!(
967 "C e2e generator: visitor pattern not supported for fixture: {}",
968 fixture.id
969 );
970 }
971
972 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
973
974 let mut effective_fields_enum = e2e_config.fields_enum.clone();
980 let fixture_call = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
981 if let Some(co) = fixture_call.overrides.get(lang) {
982 for k in co.enum_fields.keys() {
983 effective_fields_enum.insert(k.clone());
984 }
985 }
986
987 render_test_function(
988 &mut out,
989 fixture,
990 prefix,
991 &call_info.function_name,
992 result_var,
993 &call_info.args,
994 field_resolver,
995 &e2e_config.fields_c_types,
996 &effective_fields_enum,
997 &call_info.result_type_name,
998 &call_info.options_type_name,
999 call_info.client_factory.as_deref(),
1000 call_info.raw_c_result_type.as_deref(),
1001 call_info.c_free_fn.as_deref(),
1002 call_info.c_engine_factory.as_deref(),
1003 call_info.result_is_option,
1004 call_info.result_is_bytes,
1005 &call_info.extra_args,
1006 );
1007 if i + 1 < fixtures.len() {
1008 let _ = writeln!(out);
1009 }
1010 }
1011
1012 out
1013}
1014
1015#[allow(clippy::too_many_arguments)]
1016fn render_test_function(
1017 out: &mut String,
1018 fixture: &Fixture,
1019 prefix: &str,
1020 function_name: &str,
1021 result_var: &str,
1022 args: &[crate::config::ArgMapping],
1023 field_resolver: &FieldResolver,
1024 fields_c_types: &HashMap<String, String>,
1025 fields_enum: &HashSet<String>,
1026 result_type_name: &str,
1027 options_type_name: &str,
1028 client_factory: Option<&str>,
1029 raw_c_result_type: Option<&str>,
1030 c_free_fn: Option<&str>,
1031 c_engine_factory: Option<&str>,
1032 result_is_option: bool,
1033 result_is_bytes: bool,
1034 extra_args: &[String],
1035) {
1036 let fn_name = sanitize_ident(&fixture.id);
1037 let description = &fixture.description;
1038
1039 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1040
1041 let _ = writeln!(out, "void test_{fn_name}(void) {{");
1042 let _ = writeln!(out, " /* {description} */");
1043
1044 let has_mock = fixture.needs_mock_server();
1053 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1054 if let Some(env) = &fixture.env {
1055 if let Some(var) = &env.api_key_var {
1056 let fixture_id = &fixture.id;
1057 if has_mock {
1058 let _ = writeln!(out, " const char* api_key = getenv(\"{var}\");");
1059 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1060 let _ = writeln!(out, " char base_url_buf[512];");
1061 let _ = writeln!(out, " int use_mock = !(api_key && api_key[0] != '\\0');");
1062 let _ = writeln!(out, " if (!use_mock) {{");
1063 let _ = writeln!(
1064 out,
1065 " fprintf(stderr, \"{fixture_id}: using real API ({var} is set)\\n\");"
1066 );
1067 let _ = writeln!(out, " }} else {{");
1068 let _ = writeln!(
1069 out,
1070 " fprintf(stderr, \"{fixture_id}: using mock server ({var} not set)\\n\");"
1071 );
1072 let _ = writeln!(
1073 out,
1074 " snprintf(base_url_buf, sizeof(base_url_buf), \"%s/fixtures/{fixture_id}\", mock_base ? mock_base : \"\");"
1075 );
1076 let _ = writeln!(out, " api_key = \"test-key\";");
1077 let _ = writeln!(out, " }}");
1078 } else {
1079 let _ = writeln!(out, " if (getenv(\"{var}\") == NULL) {{ return; }}");
1080 }
1081 }
1082 }
1083
1084 let prefix_upper = prefix.to_uppercase();
1085
1086 if let Some(config_type) = c_engine_factory {
1090 render_engine_factory_test_function(
1091 out,
1092 fixture,
1093 prefix,
1094 function_name,
1095 result_var,
1096 field_resolver,
1097 fields_c_types,
1098 fields_enum,
1099 result_type_name,
1100 config_type,
1101 expects_error,
1102 raw_c_result_type,
1103 );
1104 return;
1105 }
1106
1107 if client_factory.is_some() && function_name == "chat_stream" {
1113 render_chat_stream_test_function(
1114 out,
1115 fixture,
1116 prefix,
1117 result_var,
1118 args,
1119 options_type_name,
1120 expects_error,
1121 api_key_var,
1122 );
1123 return;
1124 }
1125
1126 if let Some(factory) = client_factory {
1134 if result_is_bytes {
1135 render_bytes_test_function(
1136 out,
1137 fixture,
1138 prefix,
1139 function_name,
1140 result_var,
1141 args,
1142 options_type_name,
1143 result_type_name,
1144 factory,
1145 expects_error,
1146 );
1147 return;
1148 }
1149 }
1150
1151 if let Some(factory) = client_factory {
1156 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); let mut inline_method_args: Vec<String> = Vec::new();
1161
1162 for arg in args {
1163 if arg.arg_type == "json_object" {
1164 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1169 options_type_name.to_string()
1170 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1171 format!("{}Request", stripped)
1172 } else {
1173 format!("{result_type_name}Request")
1174 };
1175 let request_type_snake = request_type_pascal.to_snake_case();
1176 let var_name = format!("{request_type_snake}_handle");
1177
1178 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1179 let json_val = if field.is_empty() || field == "input" {
1180 Some(&fixture.input)
1181 } else {
1182 fixture.input.get(field)
1183 };
1184
1185 if let Some(val) = json_val {
1186 if !val.is_null() {
1187 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1188 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1189 let escaped = escape_c(&json_str);
1190 let _ = writeln!(
1191 out,
1192 " {prefix_upper}{request_type_pascal}* {var_name} = \
1193 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1194 );
1195 if expects_error {
1196 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
1204 } else {
1205 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1206 }
1207 request_handle_vars.push((arg.name.clone(), var_name));
1208 }
1209 }
1210 } else if arg.arg_type == "string" {
1211 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1213 let val = fixture.input.get(field);
1214 match val {
1215 Some(v) if v.is_string() => {
1216 let s = v.as_str().unwrap_or_default();
1217 let escaped = escape_c(s);
1218 inline_method_args.push(format!("\"{escaped}\""));
1219 }
1220 Some(serde_json::Value::Null) | None if arg.optional => {
1221 inline_method_args.push("NULL".to_string());
1222 }
1223 None => {
1224 inline_method_args.push("\"\"".to_string());
1225 }
1226 Some(other) => {
1227 let s = serde_json::to_string(other).unwrap_or_default();
1228 let escaped = escape_c(&s);
1229 inline_method_args.push(format!("\"{escaped}\""));
1230 }
1231 }
1232 } else if arg.optional {
1233 inline_method_args.push("NULL".to_string());
1235 }
1236 }
1237
1238 let fixture_id = &fixture.id;
1239 if has_mock && api_key_var.is_some() {
1244 let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
1248 let _ = writeln!(
1249 out,
1250 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
1251 );
1252 } else if has_mock {
1253 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1254 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1255 let _ = writeln!(out, " char base_url[1024];");
1256 let _ = writeln!(
1257 out,
1258 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1259 );
1260 let _ = writeln!(
1261 out,
1262 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1263 );
1264 } else {
1265 let _ = writeln!(
1266 out,
1267 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1268 );
1269 }
1270 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1271
1272 let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
1273 String::new()
1274 } else {
1275 let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
1276 let parts: Vec<String> = handles
1277 .into_iter()
1278 .chain(inline_method_args.iter().cloned())
1279 .chain(extra_args.iter().cloned())
1280 .collect();
1281 format!(", {}", parts.join(", "))
1282 };
1283
1284 let call_fn = format!("{prefix}_default_client_{function_name}");
1285
1286 if expects_error {
1287 let _ = writeln!(
1288 out,
1289 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1290 );
1291 for (_, var_name) in &request_handle_vars {
1292 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1293 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1294 }
1295 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1296 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1297 let _ = writeln!(out, "}}");
1298 return;
1299 }
1300
1301 let _ = writeln!(
1302 out,
1303 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1304 );
1305 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1306
1307 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1308 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1309 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1312 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1315
1316 for assertion in &fixture.assertions {
1317 if let Some(f) = &assertion.field {
1318 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1319 let resolved = field_resolver.resolve(f);
1320 let local_var = f.replace(['.', '['], "_").replace(']', "");
1321 let has_map_access = resolved.contains('[');
1322 if resolved.contains('.') {
1323 let leaf_primitive = emit_nested_accessor(
1324 out,
1325 prefix,
1326 resolved,
1327 &local_var,
1328 result_var,
1329 fields_c_types,
1330 fields_enum,
1331 &mut intermediate_handles,
1332 result_type_name,
1333 f,
1334 );
1335 if let Some(prim) = leaf_primitive {
1336 primitive_locals.insert(local_var.clone(), prim);
1337 }
1338 } else {
1339 let result_type_snake = result_type_name.to_snake_case();
1340 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1341 let lookup_key = format!("{result_type_snake}.{resolved}");
1342 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1343 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1344 primitive_locals.insert(local_var.clone(), t.clone());
1345 } else if try_emit_enum_accessor(
1346 out,
1347 prefix,
1348 &prefix_upper,
1349 f,
1350 resolved,
1351 &result_type_snake,
1352 &accessor_fn,
1353 result_var,
1354 &local_var,
1355 fields_c_types,
1356 fields_enum,
1357 &mut intermediate_handles,
1358 ) {
1359 } else if let Some(handle_pascal) =
1361 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1362 {
1363 let _ = writeln!(
1365 out,
1366 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1367 );
1368 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1369 } else {
1370 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1371 }
1372 }
1373 accessed_fields.push((f.clone(), local_var, has_map_access));
1374 }
1375 }
1376 }
1377
1378 for assertion in &fixture.assertions {
1379 render_assertion(
1380 out,
1381 assertion,
1382 result_var,
1383 prefix,
1384 field_resolver,
1385 &accessed_fields,
1386 &primitive_locals,
1387 &opaque_handle_locals,
1388 );
1389 }
1390
1391 for (_f, local_var, from_json) in &accessed_fields {
1392 if primitive_locals.contains_key(local_var) {
1393 continue;
1394 }
1395 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1396 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
1397 continue;
1398 }
1399 if *from_json {
1400 let _ = writeln!(out, " free({local_var});");
1401 } else {
1402 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1403 }
1404 }
1405 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1406 if snake_type == "free_string" {
1407 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1408 } else if snake_type == "free" {
1409 let _ = writeln!(out, " free({handle_var});");
1412 } else {
1413 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1414 }
1415 }
1416 let result_type_snake = result_type_name.to_snake_case();
1417 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1418 for (_, var_name) in &request_handle_vars {
1419 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1420 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1421 }
1422 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1423 let _ = writeln!(out, "}}");
1424 return;
1425 }
1426
1427 if let Some(raw_type) = raw_c_result_type {
1430 let args_str = if args.is_empty() {
1432 String::new()
1433 } else {
1434 let parts: Vec<String> = args
1435 .iter()
1436 .filter_map(|arg| {
1437 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1438 let val = fixture.input.get(field);
1439 match val {
1440 None if arg.optional => Some("NULL".to_string()),
1441 None => None,
1442 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1443 Some(v) => Some(json_to_c(v)),
1444 }
1445 })
1446 .collect();
1447 parts.join(", ")
1448 };
1449
1450 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
1452
1453 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
1455 if has_not_error {
1456 match raw_type {
1457 "char*" if !result_is_option => {
1458 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1459 }
1460 "int32_t" => {
1461 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
1462 }
1463 "uintptr_t" => {
1464 let _ = writeln!(
1465 out,
1466 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
1467 );
1468 }
1469 _ => {}
1470 }
1471 }
1472
1473 for assertion in &fixture.assertions {
1475 match assertion.assertion_type.as_str() {
1476 "not_error" | "error" => {} "not_empty" => {
1478 let _ = writeln!(
1479 out,
1480 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
1481 );
1482 }
1483 "is_empty" => {
1484 if result_is_option && raw_type == "char*" {
1485 let _ = writeln!(
1486 out,
1487 " assert({result_var} == NULL && \"expected empty/null value\");"
1488 );
1489 } else {
1490 let _ = writeln!(
1491 out,
1492 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
1493 );
1494 }
1495 }
1496 "count_min" => {
1497 if let Some(val) = &assertion.value {
1498 if let Some(n) = val.as_u64() {
1499 match raw_type {
1500 "char*" => {
1501 let _ = writeln!(out, " {{");
1502 let _ = writeln!(
1503 out,
1504 " assert({result_var} != NULL && \"expected non-null JSON array\");"
1505 );
1506 let _ =
1507 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
1508 let _ = writeln!(
1509 out,
1510 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1511 );
1512 let _ = writeln!(out, " }}");
1513 }
1514 _ => {
1515 let _ = writeln!(
1516 out,
1517 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1518 );
1519 }
1520 }
1521 }
1522 }
1523 }
1524 "greater_than_or_equal" => {
1525 if let Some(val) = &assertion.value {
1526 let c_val = json_to_c(val);
1527 let _ = writeln!(
1528 out,
1529 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1530 );
1531 }
1532 }
1533 "contains" => {
1534 if let Some(val) = &assertion.value {
1535 let c_val = json_to_c(val);
1536 let _ = writeln!(
1537 out,
1538 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1539 );
1540 }
1541 }
1542 "contains_all" => {
1543 if let Some(values) = &assertion.values {
1544 for val in values {
1545 let c_val = json_to_c(val);
1546 let _ = writeln!(
1547 out,
1548 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1549 );
1550 }
1551 }
1552 }
1553 "equals" => {
1554 if let Some(val) = &assertion.value {
1555 let c_val = json_to_c(val);
1556 if val.is_string() {
1557 let _ = writeln!(
1558 out,
1559 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1560 );
1561 } else {
1562 let _ = writeln!(
1563 out,
1564 " assert({result_var} == {c_val} && \"equals assertion failed\");"
1565 );
1566 }
1567 }
1568 }
1569 "not_contains" => {
1570 if let Some(val) = &assertion.value {
1571 let c_val = json_to_c(val);
1572 let _ = writeln!(
1573 out,
1574 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1575 );
1576 }
1577 }
1578 "starts_with" => {
1579 if let Some(val) = &assertion.value {
1580 let c_val = json_to_c(val);
1581 let _ = writeln!(
1582 out,
1583 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1584 );
1585 }
1586 }
1587 "is_true" => {
1588 let _ = writeln!(out, " assert({result_var});");
1589 }
1590 "is_false" => {
1591 let _ = writeln!(out, " assert(!{result_var});");
1592 }
1593 other => {
1594 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1595 }
1596 }
1597 }
1598
1599 if raw_type == "char*" {
1601 let free_fn = c_free_fn
1602 .map(|s| s.to_string())
1603 .unwrap_or_else(|| format!("{prefix}_free_string"));
1604 if result_is_option {
1605 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1606 } else {
1607 let _ = writeln!(out, " {free_fn}({result_var});");
1608 }
1609 }
1610
1611 let _ = writeln!(out, "}}");
1612 return;
1613 }
1614
1615 let prefixed_fn = function_name.to_string();
1621
1622 let mut has_options_handle = false;
1624 for arg in args {
1625 if arg.arg_type == "json_object" {
1626 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1627 if let Some(val) = fixture.input.get(field) {
1628 if !val.is_null() {
1629 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1633 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1634 let escaped = escape_c(&json_str);
1635 let upper = prefix.to_uppercase();
1636 let options_type_pascal = options_type_name;
1637 let options_type_snake = options_type_name.to_snake_case();
1638 let _ = writeln!(
1639 out,
1640 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1641 );
1642 has_options_handle = true;
1643 }
1644 }
1645 }
1646 }
1647
1648 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1649
1650 if expects_error {
1651 let _ = writeln!(
1652 out,
1653 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1654 );
1655 if has_options_handle {
1656 let options_type_snake = options_type_name.to_snake_case();
1657 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1658 }
1659 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1660 let _ = writeln!(out, "}}");
1661 return;
1662 }
1663
1664 let _ = writeln!(
1666 out,
1667 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1668 );
1669 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1670
1671 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1679 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1682 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1684 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1686
1687 for assertion in &fixture.assertions {
1688 if let Some(f) = &assertion.field {
1689 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1690 let resolved = field_resolver.resolve(f);
1691 let local_var = f.replace(['.', '['], "_").replace(']', "");
1692 let has_map_access = resolved.contains('[');
1693
1694 if resolved.contains('.') {
1695 let leaf_primitive = emit_nested_accessor(
1696 out,
1697 prefix,
1698 resolved,
1699 &local_var,
1700 result_var,
1701 fields_c_types,
1702 fields_enum,
1703 &mut intermediate_handles,
1704 result_type_name,
1705 f,
1706 );
1707 if let Some(prim) = leaf_primitive {
1708 primitive_locals.insert(local_var.clone(), prim);
1709 }
1710 } else {
1711 let result_type_snake = result_type_name.to_snake_case();
1712 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1713 let lookup_key = format!("{result_type_snake}.{resolved}");
1714 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1715 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1716 primitive_locals.insert(local_var.clone(), t.clone());
1717 } else if try_emit_enum_accessor(
1718 out,
1719 prefix,
1720 &prefix_upper,
1721 f,
1722 resolved,
1723 &result_type_snake,
1724 &accessor_fn,
1725 result_var,
1726 &local_var,
1727 fields_c_types,
1728 fields_enum,
1729 &mut intermediate_handles,
1730 ) {
1731 } else if let Some(handle_pascal) =
1733 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1734 {
1735 let _ = writeln!(
1736 out,
1737 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1738 );
1739 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1740 } else {
1741 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1742 }
1743 }
1744 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1745 }
1746 }
1747 }
1748
1749 for assertion in &fixture.assertions {
1750 render_assertion(
1751 out,
1752 assertion,
1753 result_var,
1754 prefix,
1755 field_resolver,
1756 &accessed_fields,
1757 &primitive_locals,
1758 &opaque_handle_locals,
1759 );
1760 }
1761
1762 for (_f, local_var, from_json) in &accessed_fields {
1764 if primitive_locals.contains_key(local_var) {
1765 continue;
1766 }
1767 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1768 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
1769 continue;
1770 }
1771 if *from_json {
1772 let _ = writeln!(out, " free({local_var});");
1773 } else {
1774 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1775 }
1776 }
1777 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1779 if snake_type == "free_string" {
1780 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1782 } else {
1783 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1784 }
1785 }
1786 if has_options_handle {
1787 let options_type_snake = options_type_name.to_snake_case();
1788 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1789 }
1790 let result_type_snake = result_type_name.to_snake_case();
1791 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1792 let _ = writeln!(out, "}}");
1793}
1794
1795#[allow(clippy::too_many_arguments)]
1804fn render_engine_factory_test_function(
1805 out: &mut String,
1806 fixture: &Fixture,
1807 prefix: &str,
1808 function_name: &str,
1809 result_var: &str,
1810 field_resolver: &FieldResolver,
1811 fields_c_types: &HashMap<String, String>,
1812 fields_enum: &HashSet<String>,
1813 result_type_name: &str,
1814 config_type: &str,
1815 expects_error: bool,
1816 raw_c_result_type: Option<&str>,
1817) {
1818 let prefix_upper = prefix.to_uppercase();
1819 let config_snake = config_type.to_snake_case();
1820
1821 let config_val = fixture.input.get("config");
1823 let config_json = match config_val {
1824 Some(v) if !v.is_null() => {
1825 let normalized = super::transform_json_keys_for_language(v, "snake_case");
1826 serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1827 }
1828 _ => "{}".to_string(),
1829 };
1830 let config_escaped = escape_c(&config_json);
1831 let fixture_id = &fixture.id;
1832
1833 let has_active_assertions = fixture.assertions.iter().any(|a| {
1837 if let Some(f) = &a.field {
1838 !f.is_empty() && field_resolver.is_valid_for_result(f)
1839 } else {
1840 false
1841 }
1842 });
1843
1844 let _ = writeln!(
1846 out,
1847 " {prefix_upper}{config_type}* config_handle = \
1848 {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1849 );
1850 if expects_error {
1851 let _ = writeln!(out, " if (config_handle == NULL) {{ return; }}");
1854 } else {
1855 let _ = writeln!(out, " assert(config_handle != NULL && \"failed to parse config\");");
1856 }
1857 let _ = writeln!(
1858 out,
1859 " {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1860 );
1861 let _ = writeln!(out, " {prefix}_{config_snake}_free(config_handle);");
1862 if expects_error {
1863 let _ = writeln!(out, " if (engine == NULL) {{ return; }}");
1866 } else {
1867 let _ = writeln!(out, " assert(engine != NULL && \"failed to create engine\");");
1868 }
1869
1870 let fixture_env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1874 let _ = writeln!(out, " const char* mock_per_fixture = getenv(\"{fixture_env_key}\");");
1875 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1876 let _ = writeln!(out, " char url[2048];");
1877 let _ = writeln!(out, " if (mock_per_fixture && mock_per_fixture[0] != '\\0') {{");
1878 let _ = writeln!(out, " snprintf(url, sizeof(url), \"%s\", mock_per_fixture);");
1879 let _ = writeln!(out, " }} else {{");
1880 let _ = writeln!(
1881 out,
1882 " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");"
1883 );
1884 let _ = writeln!(
1885 out,
1886 " snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
1887 );
1888 let _ = writeln!(out, " }}");
1889
1890 if raw_c_result_type == Some("char*") {
1895 let _ = writeln!(out, " char* {result_var} = {prefix}_{function_name}(engine, url);");
1896 let _ = writeln!(out, " if ({result_var} != NULL) {prefix}_free_string({result_var});");
1899 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1900 let _ = writeln!(out, "}}");
1901 return;
1902 }
1903
1904 let _ = writeln!(
1905 out,
1906 " {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url);"
1907 );
1908
1909 if !has_active_assertions {
1912 let result_type_snake = result_type_name.to_snake_case();
1913 let _ = writeln!(
1914 out,
1915 " if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
1916 );
1917 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1918 let _ = writeln!(out, "}}");
1919 return;
1920 }
1921
1922 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1923
1924 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1926 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1927 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1928 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1929
1930 for assertion in &fixture.assertions {
1931 if let Some(f) = &assertion.field {
1932 if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
1933 {
1934 let resolved = field_resolver.resolve(f);
1935 let local_var = f.replace(['.', '['], "_").replace(']', "");
1936 let has_map_access = resolved.contains('[');
1937 if resolved.contains('.') {
1938 let leaf_primitive = emit_nested_accessor(
1939 out,
1940 prefix,
1941 resolved,
1942 &local_var,
1943 result_var,
1944 fields_c_types,
1945 fields_enum,
1946 &mut intermediate_handles,
1947 result_type_name,
1948 f,
1949 );
1950 if let Some(prim) = leaf_primitive {
1951 primitive_locals.insert(local_var.clone(), prim);
1952 }
1953 } else {
1954 let result_type_snake = result_type_name.to_snake_case();
1955 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1956 let lookup_key = format!("{result_type_snake}.{resolved}");
1957 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1958 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1959 primitive_locals.insert(local_var.clone(), t.clone());
1960 } else if try_emit_enum_accessor(
1961 out,
1962 prefix,
1963 &prefix_upper,
1964 f,
1965 resolved,
1966 &result_type_snake,
1967 &accessor_fn,
1968 result_var,
1969 &local_var,
1970 fields_c_types,
1971 fields_enum,
1972 &mut intermediate_handles,
1973 ) {
1974 } else if let Some(handle_pascal) =
1976 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1977 {
1978 let _ = writeln!(
1979 out,
1980 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1981 );
1982 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1983 } else {
1984 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1985 }
1986 }
1987 accessed_fields.push((f.clone(), local_var, has_map_access));
1988 }
1989 }
1990 }
1991
1992 for assertion in &fixture.assertions {
1993 render_assertion(
1994 out,
1995 assertion,
1996 result_var,
1997 prefix,
1998 field_resolver,
1999 &accessed_fields,
2000 &primitive_locals,
2001 &opaque_handle_locals,
2002 );
2003 }
2004
2005 for (_f, local_var, from_json) in &accessed_fields {
2007 if primitive_locals.contains_key(local_var) {
2008 continue;
2009 }
2010 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
2011 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
2012 continue;
2013 }
2014 if *from_json {
2015 let _ = writeln!(out, " free({local_var});");
2016 } else {
2017 let _ = writeln!(out, " {prefix}_free_string({local_var});");
2018 }
2019 }
2020 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
2021 if snake_type == "free_string" {
2022 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
2023 } else {
2024 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
2025 }
2026 }
2027
2028 let result_type_snake = result_type_name.to_snake_case();
2029 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
2030 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2031 let _ = writeln!(out, "}}");
2032}
2033
2034#[allow(clippy::too_many_arguments)]
2056fn render_bytes_test_function(
2057 out: &mut String,
2058 fixture: &Fixture,
2059 prefix: &str,
2060 function_name: &str,
2061 _result_var: &str,
2062 args: &[crate::config::ArgMapping],
2063 options_type_name: &str,
2064 result_type_name: &str,
2065 factory: &str,
2066 expects_error: bool,
2067) {
2068 let prefix_upper = prefix.to_uppercase();
2069 let mut request_handle_vars: Vec<(String, String)> = Vec::new();
2070 let mut string_arg_exprs: Vec<String> = Vec::new();
2071
2072 for arg in args {
2073 match arg.arg_type.as_str() {
2074 "json_object" => {
2075 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2076 options_type_name.to_string()
2077 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
2078 format!("{}Request", stripped)
2079 } else {
2080 format!("{result_type_name}Request")
2081 };
2082 let request_type_snake = request_type_pascal.to_snake_case();
2083 let var_name = format!("{request_type_snake}_handle");
2084
2085 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2086 let json_val = if field.is_empty() || field == "input" {
2087 Some(&fixture.input)
2088 } else {
2089 fixture.input.get(field)
2090 };
2091
2092 if let Some(val) = json_val {
2093 if !val.is_null() {
2094 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2095 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2096 let escaped = escape_c(&json_str);
2097 let _ = writeln!(
2098 out,
2099 " {prefix_upper}{request_type_pascal}* {var_name} = \
2100 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2101 );
2102 if expects_error {
2103 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
2111 } else {
2112 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2113 }
2114 request_handle_vars.push((arg.name.clone(), var_name));
2115 }
2116 }
2117 }
2118 "string" => {
2119 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2122 let val = fixture.input.get(field);
2123 let expr = match val {
2124 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
2125 Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
2126 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
2127 None => "NULL".to_string(),
2128 };
2129 string_arg_exprs.push(expr);
2130 }
2131 _ => {
2132 string_arg_exprs.push("NULL".to_string());
2135 }
2136 }
2137 }
2138
2139 let fixture_id = &fixture.id;
2140 if fixture.needs_mock_server() {
2141 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2142 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2143 let _ = writeln!(out, " char base_url[1024];");
2144 let _ = writeln!(
2145 out,
2146 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2147 );
2148 let _ = writeln!(
2153 out,
2154 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2155 );
2156 } else {
2157 let _ = writeln!(
2158 out,
2159 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2160 );
2161 }
2162 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2163
2164 let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
2166 let _ = writeln!(out, " uintptr_t out_len = 0;");
2167 let _ = writeln!(out, " uintptr_t out_cap = 0;");
2168
2169 let mut method_args: Vec<String> = Vec::new();
2171 for (_, v) in &request_handle_vars {
2172 method_args.push(v.clone());
2173 }
2174 method_args.extend(string_arg_exprs.iter().cloned());
2175 let extra_args = if method_args.is_empty() {
2176 String::new()
2177 } else {
2178 format!(", {}", method_args.join(", "))
2179 };
2180
2181 let call_fn = format!("{prefix}_default_client_{function_name}");
2182 let _ = writeln!(
2183 out,
2184 " int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
2185 );
2186
2187 if expects_error {
2188 for (_, var_name) in &request_handle_vars {
2189 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2190 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2191 }
2192 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2193 let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
2194 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2197 let _ = writeln!(out, "}}");
2198 return;
2199 }
2200
2201 let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
2202
2203 let mut emitted_len_check = false;
2208 for assertion in &fixture.assertions {
2209 match assertion.assertion_type.as_str() {
2210 "not_error" => {
2211 }
2213 "not_empty" | "not_null" => {
2214 if !emitted_len_check {
2215 let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
2216 emitted_len_check = true;
2217 }
2218 }
2219 _ => {
2220 let _ = writeln!(
2224 out,
2225 " /* skipped: assertion '{}' not meaningful on raw byte buffer */",
2226 assertion.assertion_type
2227 );
2228 }
2229 }
2230 }
2231
2232 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2233 for (_, var_name) in &request_handle_vars {
2234 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2235 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2236 }
2237 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2238 let _ = writeln!(out, "}}");
2239}
2240
2241#[allow(clippy::too_many_arguments)]
2252fn render_chat_stream_test_function(
2253 out: &mut String,
2254 fixture: &Fixture,
2255 prefix: &str,
2256 result_var: &str,
2257 args: &[crate::config::ArgMapping],
2258 options_type_name: &str,
2259 expects_error: bool,
2260 api_key_var: Option<&str>,
2261) {
2262 let prefix_upper = prefix.to_uppercase();
2263
2264 let mut request_var: Option<String> = None;
2265 for arg in args {
2266 if arg.arg_type == "json_object" {
2267 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2268 options_type_name.to_string()
2269 } else {
2270 "ChatCompletionRequest".to_string()
2271 };
2272 let request_type_snake = request_type_pascal.to_snake_case();
2273 let var_name = format!("{request_type_snake}_handle");
2274
2275 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2276 let json_val = if field.is_empty() || field == "input" {
2277 Some(&fixture.input)
2278 } else {
2279 fixture.input.get(field)
2280 };
2281
2282 if let Some(val) = json_val {
2283 if !val.is_null() {
2284 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2285 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2286 let escaped = escape_c(&json_str);
2287 let _ = writeln!(
2288 out,
2289 " {prefix_upper}{request_type_pascal}* {var_name} = \
2290 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2291 );
2292 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2293 request_var = Some(var_name);
2294 break;
2295 }
2296 }
2297 }
2298 }
2299
2300 let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
2301 let req_snake = request_var
2302 .as_ref()
2303 .and_then(|v| v.strip_suffix("_handle"))
2304 .unwrap_or("chat_completion_request")
2305 .to_string();
2306
2307 let fixture_id = &fixture.id;
2308 let has_mock = fixture.needs_mock_server();
2309 if has_mock && api_key_var.is_some() {
2310 let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
2316 let _ = writeln!(
2317 out,
2318 " {prefix_upper}DefaultClient* client = {prefix}_create_client(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
2319 );
2320 } else if has_mock {
2321 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2322 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2323 let _ = writeln!(out, " char base_url[1024];");
2324 let _ = writeln!(
2325 out,
2326 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2327 );
2328 let _ = writeln!(
2333 out,
2334 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2335 );
2336 } else {
2337 let _ = writeln!(
2338 out,
2339 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2340 );
2341 }
2342 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2343
2344 let _ = writeln!(
2345 out,
2346 " {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
2347 {prefix}_default_client_chat_stream_start(client, {req_handle});"
2348 );
2349
2350 if expects_error {
2351 let _ = writeln!(
2352 out,
2353 " assert(stream_handle == NULL && \"expected stream-start to fail\");"
2354 );
2355 if request_var.is_some() {
2356 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2357 }
2358 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2359 let _ = writeln!(out, "}}");
2360 return;
2361 }
2362
2363 let _ = writeln!(
2364 out,
2365 " assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2366 );
2367
2368 let _ = writeln!(out, " size_t chunks_count = 0;");
2369 let _ = writeln!(out, " char* stream_content = (char*)malloc(1);");
2370 let _ = writeln!(out, " assert(stream_content != NULL);");
2371 let _ = writeln!(out, " stream_content[0] = '\\0';");
2372 let _ = writeln!(out, " size_t stream_content_len = 0;");
2373 let _ = writeln!(out, " int stream_complete = 0;");
2374 let _ = writeln!(out, " int no_chunks_after_done = 1;");
2375 let _ = writeln!(out, " char* last_choices_json = NULL;");
2376 let _ = writeln!(out, " uint64_t total_tokens = 0;");
2377 let _ = writeln!(out);
2378
2379 let _ = writeln!(out, " while (1) {{");
2380 let _ = writeln!(
2381 out,
2382 " {prefix_upper}ChatCompletionChunk* {result_var} = \
2383 {prefix}_default_client_chat_stream_next(stream_handle);"
2384 );
2385 let _ = writeln!(out, " if ({result_var} == NULL) {{");
2386 let _ = writeln!(
2387 out,
2388 " if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2389 );
2390 let _ = writeln!(out, " break;");
2391 let _ = writeln!(out, " }}");
2392 let _ = writeln!(out, " chunks_count++;");
2393 let _ = writeln!(
2394 out,
2395 " char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2396 );
2397 let _ = writeln!(out, " if (choices_json != NULL) {{");
2398 let _ = writeln!(
2399 out,
2400 " const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2401 );
2402 let _ = writeln!(out, " if (d != NULL) {{");
2403 let _ = writeln!(out, " d += 10;");
2404 let _ = writeln!(out, " while (*d == ' ' || *d == '\\t') d++;");
2405 let _ = writeln!(out, " if (*d == '\"') {{");
2406 let _ = writeln!(out, " d++;");
2407 let _ = writeln!(out, " const char* e = d;");
2408 let _ = writeln!(out, " while (*e && *e != '\"') {{");
2409 let _ = writeln!(
2410 out,
2411 " if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2412 );
2413 let _ = writeln!(out, " }}");
2414 let _ = writeln!(out, " size_t add = (size_t)(e - d);");
2415 let _ = writeln!(out, " if (add > 0) {{");
2416 let _ = writeln!(
2417 out,
2418 " char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2419 );
2420 let _ = writeln!(out, " if (nc != NULL) {{");
2421 let _ = writeln!(out, " stream_content = nc;");
2422 let _ = writeln!(
2423 out,
2424 " memcpy(stream_content + stream_content_len, d, add);"
2425 );
2426 let _ = writeln!(out, " stream_content_len += add;");
2427 let _ = writeln!(
2428 out,
2429 " stream_content[stream_content_len] = '\\0';"
2430 );
2431 let _ = writeln!(out, " }}");
2432 let _ = writeln!(out, " }}");
2433 let _ = writeln!(out, " }}");
2434 let _ = writeln!(out, " }}");
2435 let _ = writeln!(
2436 out,
2437 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2438 );
2439 let _ = writeln!(out, " last_choices_json = choices_json;");
2440 let _ = writeln!(out, " }}");
2441 let _ = writeln!(
2442 out,
2443 " {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2444 );
2445 let _ = writeln!(out, " if (usage_handle != NULL) {{");
2446 let _ = writeln!(
2447 out,
2448 " total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2449 );
2450 let _ = writeln!(out, " {prefix}_usage_free(usage_handle);");
2451 let _ = writeln!(out, " }}");
2452 let _ = writeln!(out, " {prefix}_chat_completion_chunk_free({result_var});");
2453 let _ = writeln!(out, " }}");
2454 let _ = writeln!(out, " {prefix}_default_client_chat_stream_free(stream_handle);");
2455 let _ = writeln!(out);
2456
2457 let _ = writeln!(out, " char* finish_reason = NULL;");
2458 let _ = writeln!(out, " char* tool_calls_json = NULL;");
2459 let _ = writeln!(out, " char* tool_calls_0_function_name = NULL;");
2460 let _ = writeln!(out, " if (last_choices_json != NULL) {{");
2461 let _ = writeln!(
2462 out,
2463 " finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2464 );
2465 let _ = writeln!(
2466 out,
2467 " const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2468 );
2469 let _ = writeln!(out, " if (tc != NULL) {{");
2470 let _ = writeln!(out, " tc += 13;");
2471 let _ = writeln!(out, " while (*tc == ' ' || *tc == '\\t') tc++;");
2472 let _ = writeln!(out, " if (*tc == '[') {{");
2473 let _ = writeln!(out, " int depth = 0;");
2474 let _ = writeln!(out, " const char* end = tc;");
2475 let _ = writeln!(out, " int in_str = 0;");
2476 let _ = writeln!(out, " for (; *end; end++) {{");
2477 let _ = writeln!(
2478 out,
2479 " if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2480 );
2481 let _ = writeln!(
2482 out,
2483 " if (*end == '\"') {{ in_str = !in_str; continue; }}"
2484 );
2485 let _ = writeln!(out, " if (in_str) continue;");
2486 let _ = writeln!(out, " if (*end == '[' || *end == '{{') depth++;");
2487 let _ = writeln!(
2488 out,
2489 " else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2490 );
2491 let _ = writeln!(out, " }}");
2492 let _ = writeln!(out, " size_t tlen = (size_t)(end - tc);");
2493 let _ = writeln!(out, " tool_calls_json = (char*)malloc(tlen + 1);");
2494 let _ = writeln!(out, " if (tool_calls_json != NULL) {{");
2495 let _ = writeln!(out, " memcpy(tool_calls_json, tc, tlen);");
2496 let _ = writeln!(out, " tool_calls_json[tlen] = '\\0';");
2497 let _ = writeln!(
2498 out,
2499 " const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2500 );
2501 let _ = writeln!(out, " if (fn != NULL) {{");
2502 let _ = writeln!(
2503 out,
2504 " const char* np = strstr(fn, \"\\\"name\\\":\");"
2505 );
2506 let _ = writeln!(out, " if (np != NULL) {{");
2507 let _ = writeln!(out, " np += 7;");
2508 let _ = writeln!(
2509 out,
2510 " while (*np == ' ' || *np == '\\t') np++;"
2511 );
2512 let _ = writeln!(out, " if (*np == '\"') {{");
2513 let _ = writeln!(out, " np++;");
2514 let _ = writeln!(out, " const char* ne = np;");
2515 let _ = writeln!(
2516 out,
2517 " while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2518 );
2519 let _ = writeln!(out, " size_t nlen = (size_t)(ne - np);");
2520 let _ = writeln!(
2521 out,
2522 " tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2523 );
2524 let _ = writeln!(
2525 out,
2526 " if (tool_calls_0_function_name != NULL) {{"
2527 );
2528 let _ = writeln!(
2529 out,
2530 " memcpy(tool_calls_0_function_name, np, nlen);"
2531 );
2532 let _ = writeln!(
2533 out,
2534 " tool_calls_0_function_name[nlen] = '\\0';"
2535 );
2536 let _ = writeln!(out, " }}");
2537 let _ = writeln!(out, " }}");
2538 let _ = writeln!(out, " }}");
2539 let _ = writeln!(out, " }}");
2540 let _ = writeln!(out, " }}");
2541 let _ = writeln!(out, " }}");
2542 let _ = writeln!(out, " }}");
2543 let _ = writeln!(out, " }}");
2544 let _ = writeln!(out);
2545
2546 for assertion in &fixture.assertions {
2547 emit_chat_stream_assertion(out, assertion);
2548 }
2549
2550 let _ = writeln!(out, " free(stream_content);");
2551 let _ = writeln!(
2552 out,
2553 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2554 );
2555 let _ = writeln!(out, " if (finish_reason != NULL) free(finish_reason);");
2556 let _ = writeln!(out, " if (tool_calls_json != NULL) free(tool_calls_json);");
2557 let _ = writeln!(
2558 out,
2559 " if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2560 );
2561 if request_var.is_some() {
2562 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2563 }
2564 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2565 let _ = writeln!(
2566 out,
2567 " /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2568 (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2569 );
2570 let _ = writeln!(out, "}}");
2571}
2572
2573fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2577 let field = assertion.field.as_deref().unwrap_or("");
2578
2579 enum Kind {
2580 IntCount,
2581 Bool,
2582 Str,
2583 IntTokens,
2584 Unsupported,
2585 }
2586
2587 let (expr, kind) = match field {
2588 "chunks" => ("chunks_count", Kind::IntCount),
2589 "stream_content" => ("stream_content", Kind::Str),
2590 "stream_complete" => ("stream_complete", Kind::Bool),
2591 "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2592 "finish_reason" => ("finish_reason", Kind::Str),
2593 "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2602 "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2603 _ => ("", Kind::Unsupported),
2604 };
2605
2606 let atype = assertion.assertion_type.as_str();
2607 if atype == "not_error" || atype == "error" {
2608 return;
2609 }
2610
2611 if matches!(kind, Kind::Unsupported) {
2612 let _ = writeln!(
2613 out,
2614 " /* skipped: streaming assertion on unsupported field '{field}' */"
2615 );
2616 return;
2617 }
2618
2619 match (atype, &kind) {
2620 ("count_min", Kind::IntCount) => {
2621 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2622 let _ = writeln!(out, " assert({expr} >= {n} && \"expected at least {n} chunks\");");
2623 }
2624 }
2625 ("equals", Kind::Str) => {
2626 if let Some(val) = &assertion.value {
2627 let c_val = json_to_c(val);
2628 let _ = writeln!(
2629 out,
2630 " assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2631 );
2632 }
2633 }
2634 ("contains", Kind::Str) => {
2635 if let Some(val) = &assertion.value {
2636 let c_val = json_to_c(val);
2637 let _ = writeln!(
2638 out,
2639 " assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2640 );
2641 }
2642 }
2643 ("not_empty", Kind::Str) => {
2644 let _ = writeln!(
2645 out,
2646 " assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2647 );
2648 }
2649 ("is_true", Kind::Bool) => {
2650 let _ = writeln!(out, " assert({expr} && \"expected {field} to be true\");");
2651 }
2652 ("is_false", Kind::Bool) => {
2653 let _ = writeln!(out, " assert(!{expr} && \"expected {field} to be false\");");
2654 }
2655 ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2656 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2657 let _ = writeln!(out, " assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2658 }
2659 }
2660 ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2661 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2662 let _ = writeln!(out, " assert({expr} == {n} && \"equals assertion failed\");");
2663 }
2664 }
2665 _ => {
2666 let _ = writeln!(
2667 out,
2668 " /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2669 );
2670 }
2671 }
2672}
2673
2674#[allow(clippy::too_many_arguments)]
2688fn emit_nested_accessor(
2689 out: &mut String,
2690 prefix: &str,
2691 resolved: &str,
2692 local_var: &str,
2693 result_var: &str,
2694 fields_c_types: &HashMap<String, String>,
2695 fields_enum: &HashSet<String>,
2696 intermediate_handles: &mut Vec<(String, String)>,
2697 result_type_name: &str,
2698 raw_field: &str,
2699) -> Option<String> {
2700 let segments: Vec<&str> = resolved.split('.').collect();
2701 let prefix_upper = prefix.to_uppercase();
2702
2703 let mut current_snake_type = result_type_name.to_snake_case();
2705 let mut current_handle = result_var.to_string();
2706 let mut json_extract_mode = false;
2709
2710 for (i, segment) in segments.iter().enumerate() {
2711 let is_leaf = i + 1 == segments.len();
2712
2713 if json_extract_mode {
2717 let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2722 Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2723 None => (segment, None),
2724 };
2725 let seg_snake = bare_segment.to_snake_case();
2726 if is_leaf {
2727 let _ = writeln!(
2728 out,
2729 " char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2730 );
2731 return None; }
2733 let json_var = format!("{seg_snake}_json");
2738 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2739 let _ = writeln!(
2740 out,
2741 " char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2742 );
2743 intermediate_handles.push((json_var.clone(), "free".to_string()));
2744 }
2745 if let Some(key) = bracket_key {
2749 if let Ok(idx) = key.parse::<usize>() {
2750 let elem_var = format!("{seg_snake}_{idx}_json");
2751 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2752 let _ = writeln!(
2753 out,
2754 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2755 );
2756 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2757 }
2758 current_handle = elem_var;
2759 continue;
2760 }
2761 }
2762 current_handle = json_var;
2763 continue;
2764 }
2765
2766 if let Some(bracket_pos) = segment.find('[') {
2768 let field_name = &segment[..bracket_pos];
2769 let key = segment[bracket_pos + 1..].trim_end_matches(']');
2770 let field_snake = field_name.to_snake_case();
2771 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2772
2773 let json_var = format!("{field_snake}_json");
2775 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2776 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2777 let _ = writeln!(out, " assert({json_var} != NULL);");
2778 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2780 }
2781
2782 if key.is_empty() {
2788 if !is_leaf {
2789 current_handle = json_var;
2790 json_extract_mode = true;
2791 continue;
2792 }
2793 return None;
2794 }
2795 if let Ok(idx) = key.parse::<usize>() {
2796 let elem_var = format!("{field_snake}_{idx}_json");
2797 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2798 let _ = writeln!(
2799 out,
2800 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2801 );
2802 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2803 }
2804 if !is_leaf {
2805 current_handle = elem_var;
2806 json_extract_mode = true;
2807 continue;
2808 }
2809 return None;
2811 }
2812
2813 let _ = writeln!(
2815 out,
2816 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
2817 );
2818 return None; }
2820
2821 let seg_snake = segment.to_snake_case();
2822 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
2823
2824 if is_leaf {
2825 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2828 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2829 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
2830 return Some(t.clone());
2831 }
2832 if try_emit_enum_accessor(
2834 out,
2835 prefix,
2836 &prefix_upper,
2837 raw_field,
2838 &seg_snake,
2839 ¤t_snake_type,
2840 &accessor_fn,
2841 ¤t_handle,
2842 local_var,
2843 fields_c_types,
2844 fields_enum,
2845 intermediate_handles,
2846 ) {
2847 return None;
2848 }
2849 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
2850 } else {
2851 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2853 let return_type_pascal = match fields_c_types.get(&lookup_key) {
2854 Some(t) => t.clone(),
2855 None => {
2856 segment.to_pascal_case()
2858 }
2859 };
2860
2861 if return_type_pascal == "char*" {
2864 let json_var = format!("{seg_snake}_json");
2865 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2866 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2867 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2868 }
2869 if i + 2 == segments.len() && segments[i + 1] == "length" {
2871 let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
2872 return Some("int".to_string());
2873 }
2874 current_snake_type = seg_snake.clone();
2875 current_handle = json_var;
2876 continue;
2877 }
2878
2879 let return_snake = return_type_pascal.to_snake_case();
2880 let handle_var = format!("{seg_snake}_handle");
2881
2882 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
2885 let _ = writeln!(
2886 out,
2887 " {prefix_upper}{return_type_pascal}* {handle_var} = \
2888 {accessor_fn}({current_handle});"
2889 );
2890 let _ = writeln!(out, " assert({handle_var} != NULL);");
2891 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
2892 }
2893
2894 current_snake_type = return_snake;
2895 current_handle = handle_var;
2896 }
2897 }
2898 None
2899}
2900
2901fn build_args_string_c(
2905 input: &serde_json::Value,
2906 args: &[crate::config::ArgMapping],
2907 has_options_handle: bool,
2908) -> String {
2909 if args.is_empty() {
2910 return json_to_c(input);
2911 }
2912
2913 let parts: Vec<String> = args
2914 .iter()
2915 .filter_map(|arg| {
2916 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2917 let val = input.get(field);
2918 match val {
2919 None if arg.optional => Some("NULL".to_string()),
2921 None => None,
2923 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
2925 Some(v) => {
2926 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
2929 Some("options_handle".to_string())
2930 } else {
2931 Some(json_to_c(v))
2932 }
2933 }
2934 }
2935 })
2936 .collect();
2937
2938 parts.join(", ")
2939}
2940
2941#[allow(clippy::too_many_arguments)]
2942fn render_assertion(
2943 out: &mut String,
2944 assertion: &Assertion,
2945 result_var: &str,
2946 ffi_prefix: &str,
2947 _field_resolver: &FieldResolver,
2948 accessed_fields: &[(String, String, bool)],
2949 primitive_locals: &HashMap<String, String>,
2950 opaque_handle_locals: &HashMap<String, String>,
2951) {
2952 if let Some(f) = &assertion.field {
2954 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
2955 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
2956 return;
2957 }
2958 }
2959
2960 let field_expr = match &assertion.field {
2961 Some(f) if !f.is_empty() => {
2962 accessed_fields
2964 .iter()
2965 .find(|(k, _, _)| k == f)
2966 .map(|(_, local, _)| local.clone())
2967 .unwrap_or_else(|| result_var.to_string())
2968 }
2969 _ => result_var.to_string(),
2970 };
2971
2972 let field_is_primitive = primitive_locals.contains_key(&field_expr);
2973 let field_primitive_type = primitive_locals.get(&field_expr).cloned();
2974 let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
2979 let field_is_map_access = if let Some(f) = &assertion.field {
2983 accessed_fields.iter().any(|(k, _, m)| k == f && *m)
2984 } else {
2985 false
2986 };
2987
2988 let assertion_field_is_optional = assertion
2992 .field
2993 .as_deref()
2994 .map(|f| {
2995 if f.is_empty() {
2996 return false;
2997 }
2998 if _field_resolver.is_optional(f) {
2999 return true;
3000 }
3001 let resolved = _field_resolver.resolve(f);
3003 _field_resolver.is_optional(resolved)
3004 })
3005 .unwrap_or(false);
3006
3007 match assertion.assertion_type.as_str() {
3008 "equals" => {
3009 if let Some(expected) = &assertion.value {
3010 let c_val = json_to_c(expected);
3011 if field_is_primitive {
3012 let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
3013 match expected.as_bool() {
3014 Some(true) => "1".to_string(),
3015 Some(false) => "0".to_string(),
3016 None => c_val,
3017 }
3018 } else {
3019 c_val
3020 };
3021 let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
3024 if assertion_field_is_optional && is_numeric {
3025 let _ = writeln!(
3026 out,
3027 " assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
3028 );
3029 } else {
3030 let _ = writeln!(
3031 out,
3032 " assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
3033 );
3034 }
3035 } else if expected.is_string() {
3036 let _ = writeln!(
3037 out,
3038 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3039 );
3040 } else if field_is_map_access && expected.is_boolean() {
3041 let lit = match expected.as_bool() {
3042 Some(true) => "\"true\"",
3043 _ => "\"false\"",
3044 };
3045 let _ = writeln!(
3046 out,
3047 " assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
3048 );
3049 } else if field_is_map_access && expected.is_number() {
3050 if expected.is_f64() {
3051 let _ = writeln!(
3052 out,
3053 " assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
3054 );
3055 } else {
3056 let _ = writeln!(
3057 out,
3058 " assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
3059 );
3060 }
3061 } else {
3062 let _ = writeln!(
3063 out,
3064 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3065 );
3066 }
3067 }
3068 }
3069 "contains" => {
3070 if let Some(expected) = &assertion.value {
3071 let c_val = json_to_c(expected);
3072 let _ = writeln!(
3073 out,
3074 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3075 );
3076 }
3077 }
3078 "contains_all" => {
3079 if let Some(values) = &assertion.values {
3080 for val in values {
3081 let c_val = json_to_c(val);
3082 let _ = writeln!(
3083 out,
3084 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3085 );
3086 }
3087 }
3088 }
3089 "not_contains" => {
3090 if let Some(expected) = &assertion.value {
3091 let c_val = json_to_c(expected);
3092 let _ = writeln!(
3093 out,
3094 " assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3095 );
3096 }
3097 }
3098 "not_empty" => {
3099 if field_is_opaque_handle {
3100 let _ = writeln!(out, " assert({field_expr} != NULL && \"expected non-null handle\");");
3104 } else {
3105 let _ = writeln!(
3106 out,
3107 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
3108 );
3109 }
3110 }
3111 "is_empty" => {
3112 if field_is_opaque_handle {
3113 let _ = writeln!(out, " assert({field_expr} == NULL && \"expected null handle\");");
3114 } else if assertion_field_is_optional || !field_is_primitive {
3115 let _ = writeln!(
3117 out,
3118 " assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
3119 );
3120 } else {
3121 let _ = writeln!(
3122 out,
3123 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
3124 );
3125 }
3126 }
3127 "contains_any" => {
3128 if let Some(values) = &assertion.values {
3129 let _ = writeln!(out, " {{");
3130 let _ = writeln!(out, " int found = 0;");
3131 for val in values {
3132 let c_val = json_to_c(val);
3133 let _ = writeln!(
3134 out,
3135 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
3136 );
3137 }
3138 let _ = writeln!(
3139 out,
3140 " assert(found && \"expected to contain at least one of the specified values\");"
3141 );
3142 let _ = writeln!(out, " }}");
3143 }
3144 }
3145 "greater_than" => {
3146 if let Some(val) = &assertion.value {
3147 let c_val = json_to_c(val);
3148 if field_is_map_access && val.is_number() && !field_is_primitive {
3149 let _ = writeln!(
3150 out,
3151 " assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
3152 );
3153 } else {
3154 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
3155 }
3156 }
3157 }
3158 "less_than" => {
3159 if let Some(val) = &assertion.value {
3160 let c_val = json_to_c(val);
3161 if field_is_map_access && val.is_number() && !field_is_primitive {
3162 let _ = writeln!(
3163 out,
3164 " assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
3165 );
3166 } else {
3167 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
3168 }
3169 }
3170 }
3171 "greater_than_or_equal" => {
3172 if let Some(val) = &assertion.value {
3173 let c_val = json_to_c(val);
3174 if field_is_map_access && val.is_number() && !field_is_primitive {
3175 let _ = writeln!(
3176 out,
3177 " assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
3178 );
3179 } else {
3180 let _ = writeln!(
3181 out,
3182 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
3183 );
3184 }
3185 }
3186 }
3187 "less_than_or_equal" => {
3188 if let Some(val) = &assertion.value {
3189 let c_val = json_to_c(val);
3190 if field_is_map_access && val.is_number() && !field_is_primitive {
3191 let _ = writeln!(
3192 out,
3193 " assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
3194 );
3195 } else {
3196 let _ = writeln!(
3197 out,
3198 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
3199 );
3200 }
3201 }
3202 }
3203 "starts_with" => {
3204 if let Some(expected) = &assertion.value {
3205 let c_val = json_to_c(expected);
3206 let _ = writeln!(
3207 out,
3208 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
3209 );
3210 }
3211 }
3212 "ends_with" => {
3213 if let Some(expected) = &assertion.value {
3214 let c_val = json_to_c(expected);
3215 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
3216 let _ = writeln!(
3217 out,
3218 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
3219 );
3220 }
3221 }
3222 "min_length" => {
3223 if let Some(val) = &assertion.value {
3224 if let Some(n) = val.as_u64() {
3225 let _ = writeln!(
3226 out,
3227 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
3228 );
3229 }
3230 }
3231 }
3232 "max_length" => {
3233 if let Some(val) = &assertion.value {
3234 if let Some(n) = val.as_u64() {
3235 let _ = writeln!(
3236 out,
3237 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
3238 );
3239 }
3240 }
3241 }
3242 "count_min" => {
3243 if let Some(val) = &assertion.value {
3244 if let Some(n) = val.as_u64() {
3245 let _ = writeln!(out, " {{");
3246 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
3247 let _ = writeln!(
3248 out,
3249 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3250 );
3251 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3252 let _ = writeln!(
3253 out,
3254 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
3255 );
3256 let _ = writeln!(out, " }}");
3257 }
3258 }
3259 }
3260 "count_equals" => {
3261 if let Some(val) = &assertion.value {
3262 if let Some(n) = val.as_u64() {
3263 let _ = writeln!(out, " {{");
3264 let _ = writeln!(out, " /* count_equals: count elements in array */");
3265 let _ = writeln!(
3266 out,
3267 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3268 );
3269 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3270 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
3271 let _ = writeln!(out, " }}");
3272 }
3273 }
3274 }
3275 "is_true" => {
3276 let _ = writeln!(out, " assert({field_expr});");
3277 }
3278 "is_false" => {
3279 let _ = writeln!(out, " assert(!{field_expr});");
3280 }
3281 "method_result" => {
3282 if let Some(method_name) = &assertion.method {
3283 render_method_result_assertion(
3284 out,
3285 result_var,
3286 ffi_prefix,
3287 method_name,
3288 assertion.args.as_ref(),
3289 assertion.return_type.as_deref(),
3290 assertion.check.as_deref().unwrap_or("is_true"),
3291 assertion.value.as_ref(),
3292 );
3293 } else {
3294 panic!("C e2e generator: method_result assertion missing 'method' field");
3295 }
3296 }
3297 "matches_regex" => {
3298 if let Some(expected) = &assertion.value {
3299 let c_val = json_to_c(expected);
3300 let _ = writeln!(out, " {{");
3301 let _ = writeln!(out, " regex_t _re;");
3302 let _ = writeln!(
3303 out,
3304 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3305 );
3306 let _ = writeln!(
3307 out,
3308 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3309 );
3310 let _ = writeln!(out, " regfree(&_re);");
3311 let _ = writeln!(out, " }}");
3312 }
3313 }
3314 "not_error" => {
3315 }
3317 "error" => {
3318 }
3320 other => {
3321 panic!("C e2e generator: unsupported assertion type: {other}");
3322 }
3323 }
3324}
3325
3326#[allow(clippy::too_many_arguments)]
3335fn render_method_result_assertion(
3336 out: &mut String,
3337 result_var: &str,
3338 ffi_prefix: &str,
3339 method_name: &str,
3340 args: Option<&serde_json::Value>,
3341 return_type: Option<&str>,
3342 check: &str,
3343 value: Option<&serde_json::Value>,
3344) {
3345 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3346
3347 if return_type == Some("string") {
3348 let _ = writeln!(out, " {{");
3350 let _ = writeln!(out, " char* _method_result = {call_expr};");
3351 if check == "is_error" {
3352 let _ = writeln!(
3353 out,
3354 " assert(_method_result == NULL && \"expected method to return error\");"
3355 );
3356 let _ = writeln!(out, " }}");
3357 return;
3358 }
3359 let _ = writeln!(
3360 out,
3361 " assert(_method_result != NULL && \"method_result returned NULL\");"
3362 );
3363 match check {
3364 "contains" => {
3365 if let Some(val) = value {
3366 let c_val = json_to_c(val);
3367 let _ = writeln!(
3368 out,
3369 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3370 );
3371 }
3372 }
3373 "equals" => {
3374 if let Some(val) = value {
3375 let c_val = json_to_c(val);
3376 let _ = writeln!(
3377 out,
3378 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3379 );
3380 }
3381 }
3382 "is_true" => {
3383 let _ = writeln!(
3384 out,
3385 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3386 );
3387 }
3388 "count_min" => {
3389 if let Some(val) = value {
3390 let n = val.as_u64().unwrap_or(0);
3391 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
3392 let _ = writeln!(
3393 out,
3394 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3395 );
3396 }
3397 }
3398 other_check => {
3399 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3400 }
3401 }
3402 let _ = writeln!(out, " free(_method_result);");
3403 let _ = writeln!(out, " }}");
3404 return;
3405 }
3406
3407 match check {
3409 "equals" => {
3410 if let Some(val) = value {
3411 let c_val = json_to_c(val);
3412 let _ = writeln!(
3413 out,
3414 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3415 );
3416 }
3417 }
3418 "is_true" => {
3419 let _ = writeln!(
3420 out,
3421 " assert({call_expr} && \"method_result is_true assertion failed\");"
3422 );
3423 }
3424 "is_false" => {
3425 let _ = writeln!(
3426 out,
3427 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
3428 );
3429 }
3430 "greater_than_or_equal" => {
3431 if let Some(val) = value {
3432 let n = val.as_u64().unwrap_or(0);
3433 let _ = writeln!(
3434 out,
3435 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3436 );
3437 }
3438 }
3439 "count_min" => {
3440 if let Some(val) = value {
3441 let n = val.as_u64().unwrap_or(0);
3442 let _ = writeln!(
3443 out,
3444 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3445 );
3446 }
3447 }
3448 other_check => {
3449 panic!("C e2e generator: unsupported method_result check type: {other_check}");
3450 }
3451 }
3452}
3453
3454fn build_c_method_call(
3461 result_var: &str,
3462 ffi_prefix: &str,
3463 method_name: &str,
3464 args: Option<&serde_json::Value>,
3465) -> String {
3466 let extra_args = if let Some(args_val) = args {
3467 args_val
3468 .as_object()
3469 .map(|obj| {
3470 obj.values()
3471 .map(|v| match v {
3472 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3473 serde_json::Value::Bool(true) => "1".to_string(),
3474 serde_json::Value::Bool(false) => "0".to_string(),
3475 serde_json::Value::Number(n) => n.to_string(),
3476 serde_json::Value::Null => "NULL".to_string(),
3477 other => format!("\"{}\"", escape_c(&other.to_string())),
3478 })
3479 .collect::<Vec<_>>()
3480 .join(", ")
3481 })
3482 .unwrap_or_default()
3483 } else {
3484 String::new()
3485 };
3486
3487 if extra_args.is_empty() {
3488 format!("{ffi_prefix}_{method_name}({result_var})")
3489 } else {
3490 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3491 }
3492}
3493
3494fn json_to_c(value: &serde_json::Value) -> String {
3496 match value {
3497 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3498 serde_json::Value::Bool(true) => "1".to_string(),
3499 serde_json::Value::Bool(false) => "0".to_string(),
3500 serde_json::Value::Number(n) => n.to_string(),
3501 serde_json::Value::Null => "NULL".to_string(),
3502 other => format!("\"{}\"", escape_c(&other.to_string())),
3503 }
3504}