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