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 is_skipped_c_field(fields_c_types: &HashMap<String, String>, parent_snake: &str, field_snake: &str) -> bool {
55 let key = format!("{parent_snake}.{field_snake}");
56 fields_c_types.get(&key).is_some_and(|t| t == "skip")
57}
58
59fn infer_opaque_handle_type(
77 fields_c_types: &HashMap<String, String>,
78 parent_snake_type: &str,
79 field_snake: &str,
80) -> Option<String> {
81 let lookup_key = format!("{parent_snake_type}.{field_snake}");
82 if let Some(t) = fields_c_types.get(&lookup_key) {
83 if !is_primitive_c_type(t) && t != "char*" {
84 return Some(t.clone());
85 }
86 return None;
88 }
89 let nested_prefix = format!("{field_snake}.");
91 if fields_c_types.keys().any(|k| k.starts_with(&nested_prefix)) {
92 return Some(field_snake.to_pascal_case());
93 }
94 None
95}
96
97#[allow(clippy::too_many_arguments)]
114fn try_emit_enum_accessor(
115 out: &mut String,
116 prefix: &str,
117 prefix_upper: &str,
118 raw_field: &str,
119 resolved_field: &str,
120 parent_snake_type: &str,
121 accessor_fn: &str,
122 parent_handle: &str,
123 local_var: &str,
124 fields_c_types: &HashMap<String, String>,
125 fields_enum: &HashSet<String>,
126 intermediate_handles: &mut Vec<(String, String)>,
127) -> bool {
128 if !(fields_enum.contains(raw_field) || fields_enum.contains(resolved_field)) {
129 return false;
130 }
131 let lookup_key = format!("{parent_snake_type}.{resolved_field}");
132 let Some(enum_pascal) = fields_c_types.get(&lookup_key) else {
133 return false;
134 };
135 if is_primitive_c_type(enum_pascal) || enum_pascal == "char*" {
136 return false;
137 }
138 let enum_snake = enum_pascal.to_snake_case();
139 let handle_var = format!("{local_var}_handle");
140 let _ = writeln!(
141 out,
142 " {prefix_upper}{enum_pascal}* {handle_var} = {accessor_fn}({parent_handle});"
143 );
144 let _ = writeln!(out, " assert({handle_var} != NULL);");
145 let _ = writeln!(
146 out,
147 " char* {local_var} = {prefix}_{enum_snake}_to_string({handle_var});"
148 );
149 intermediate_handles.push((handle_var, enum_snake));
150 true
151}
152
153impl E2eCodegen for CCodegen {
154 fn generate(
155 &self,
156 groups: &[FixtureGroup],
157 e2e_config: &E2eConfig,
158 config: &ResolvedCrateConfig,
159 _type_defs: &[alef_core::ir::TypeDef],
160 _enums: &[alef_core::ir::EnumDef],
161 ) -> Result<Vec<GeneratedFile>> {
162 let lang = self.language_name();
163 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
164
165 let mut files = Vec::new();
166
167 let call = &e2e_config.call;
169 let overrides = call.overrides.get(lang);
170 let result_var = &call.result_var;
171 let prefix = overrides
172 .and_then(|o| o.prefix.as_ref())
173 .cloned()
174 .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
175 .unwrap_or_default();
176 let header = overrides
177 .and_then(|o| o.header.as_ref())
178 .cloned()
179 .unwrap_or_else(|| config.ffi_header_name());
180
181 let c_pkg = e2e_config.resolve_package("c");
183 let lib_name = c_pkg
184 .as_ref()
185 .and_then(|p| p.name.as_ref())
186 .cloned()
187 .unwrap_or_else(|| config.ffi_lib_name());
188
189 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
191 .iter()
192 .filter_map(|group| {
193 let active: Vec<&Fixture> = group
194 .fixtures
195 .iter()
196 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
197 .filter(|f| f.visitor.is_none())
198 .collect();
199 if active.is_empty() { None } else { Some((group, active)) }
200 })
201 .collect();
202
203 let ffi_crate_path = c_pkg
211 .as_ref()
212 .and_then(|p| p.path.as_ref())
213 .cloned()
214 .unwrap_or_else(|| config.ffi_crate_path());
215
216 let category_names: Vec<String> = active_groups
218 .iter()
219 .map(|(g, _)| sanitize_filename(&g.category))
220 .collect();
221 let needs_mock_server = active_groups
222 .iter()
223 .flat_map(|(_, fixtures)| fixtures.iter())
224 .any(|f| f.needs_mock_server());
225 files.push(GeneratedFile {
226 path: output_base.join("Makefile"),
227 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name, needs_mock_server),
228 generated_header: true,
229 });
230
231 let github_repo = config.github_repo();
233 let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
234 let ffi_pkg_name = e2e_config
235 .registry
236 .packages
237 .get("c")
238 .and_then(|p| p.name.as_ref())
239 .cloned()
240 .unwrap_or_else(|| lib_name.clone());
241 files.push(GeneratedFile {
242 path: output_base.join("download_ffi.sh"),
243 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
244 generated_header: true,
245 });
246
247 files.push(GeneratedFile {
249 path: output_base.join("test_runner.h"),
250 content: render_test_runner_header(&active_groups),
251 generated_header: true,
252 });
253
254 files.push(GeneratedFile {
256 path: output_base.join("main.c"),
257 content: render_main_c(&active_groups),
258 generated_header: true,
259 });
260
261 let field_resolver = FieldResolver::new(
262 &e2e_config.fields,
263 &e2e_config.fields_optional,
264 &e2e_config.result_fields,
265 &e2e_config.fields_array,
266 &std::collections::HashSet::new(),
267 );
268
269 for (group, active) in &active_groups {
273 let filename = format!("test_{}.c", sanitize_filename(&group.category));
274 let content = render_test_file(
275 &group.category,
276 active,
277 &header,
278 &prefix,
279 result_var,
280 e2e_config,
281 lang,
282 &field_resolver,
283 );
284 files.push(GeneratedFile {
285 path: output_base.join(filename),
286 content,
287 generated_header: true,
288 });
289 }
290
291 Ok(files)
292 }
293
294 fn language_name(&self) -> &'static str {
295 "c"
296 }
297}
298
299struct ResolvedCallInfo {
301 function_name: String,
302 result_type_name: String,
303 options_type_name: String,
304 client_factory: Option<String>,
305 args: Vec<crate::config::ArgMapping>,
306 raw_c_result_type: Option<String>,
307 c_free_fn: Option<String>,
308 c_engine_factory: Option<String>,
309 result_is_option: bool,
310 result_is_bytes: bool,
316 extra_args: Vec<String>,
320}
321
322fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
323 let overrides = call.overrides.get(lang);
324 let function_name = overrides
325 .and_then(|o| o.function.as_ref())
326 .cloned()
327 .unwrap_or_else(|| call.function.clone());
328 let result_type_name = overrides
333 .and_then(|o| o.result_type.as_ref())
334 .cloned()
335 .unwrap_or_else(|| call.function.to_pascal_case());
336 let options_type_name = overrides
337 .and_then(|o| o.options_type.as_deref())
338 .unwrap_or("ConversionOptions")
339 .to_string();
340 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
341 let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
342 let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
343 let c_engine_factory = overrides.and_then(|o| o.c_engine_factory.clone());
344 let result_is_option = overrides
345 .and_then(|o| if o.result_is_option { Some(true) } else { None })
346 .unwrap_or(call.result_is_option);
347 let result_is_bytes = call.result_is_bytes || overrides.is_some_and(|o| o.result_is_bytes);
352 let extra_args = overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
353 ResolvedCallInfo {
354 function_name,
355 result_type_name,
356 options_type_name,
357 client_factory,
358 args: call.args.clone(),
359 raw_c_result_type,
360 c_free_fn,
361 c_engine_factory,
362 result_is_option,
363 result_is_bytes,
364 extra_args,
365 }
366}
367
368fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
374 let call = e2e_config.resolve_call_for_fixture(
375 fixture.call.as_deref(),
376 &fixture.id,
377 &fixture.resolved_category(),
378 &fixture.tags,
379 &fixture.input,
380 );
381 let mut info = resolve_call_info(call, lang);
382
383 let default_overrides = e2e_config.call.overrides.get(lang);
384
385 if info.client_factory.is_none() {
388 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
389 info.client_factory = Some(factory.clone());
390 }
391 }
392
393 if info.c_engine_factory.is_none() {
396 if let Some(factory) = default_overrides.and_then(|o| o.c_engine_factory.as_ref()) {
397 info.c_engine_factory = Some(factory.clone());
398 }
399 }
400
401 info
402}
403
404fn render_makefile(
405 categories: &[String],
406 header_name: &str,
407 ffi_crate_path: &str,
408 lib_name: &str,
409 needs_mock_server: bool,
410) -> String {
411 let mut out = String::new();
412 out.push_str(&hash::header(CommentStyle::Hash));
413 let _ = writeln!(out, "CC = gcc");
414 let _ = writeln!(out, "FFI_DIR = ffi");
415 let _ = writeln!(out);
416
417 let link_lib_name = lib_name.replace('-', "_");
422
423 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
425 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
426 let _ = writeln!(
427 out,
428 " LDFLAGS = -L$(FFI_DIR)/lib -l{link_lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
429 );
430 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
431 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
432 let _ = writeln!(
433 out,
434 " LDFLAGS = -L../../target/release -l{link_lib_name} -Wl,-rpath,../../target/release"
435 );
436 let _ = writeln!(out, "else");
437 let _ = writeln!(
438 out,
439 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
440 );
441 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
442 let _ = writeln!(out, "endif");
443 let _ = writeln!(out);
444
445 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
446 let srcs = src_files.join(" ");
447
448 let _ = writeln!(out, "SRCS = main.c {srcs}");
449 let _ = writeln!(out, "TARGET = run_tests");
450 let _ = writeln!(out);
451 let _ = writeln!(out, ".PHONY: all clean test");
452 let _ = writeln!(out);
453 let _ = writeln!(out, "all: $(TARGET)");
454 let _ = writeln!(out);
455 let _ = writeln!(out, "$(TARGET): $(SRCS)");
456 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
457 let _ = writeln!(out);
458
459 if !needs_mock_server {
460 let _ = writeln!(out, "test: $(TARGET)");
462 let _ = writeln!(out, "\t./$(TARGET)");
463 let _ = writeln!(out);
464 let _ = writeln!(out, "clean:");
465 let _ = writeln!(out, "\trm -f $(TARGET)");
466 return out;
467 }
468
469 let _ = writeln!(out, "MOCK_SERVER_BIN ?= ../rust/target/release/mock-server");
479 let _ = writeln!(out, "FIXTURES_DIR ?= ../../fixtures");
480 let _ = writeln!(out);
481 let _ = writeln!(out, "test: $(TARGET)");
482 let _ = writeln!(out, "\t@if [ -n \"$$MOCK_SERVER_URL\" ]; then \\");
483 let _ = writeln!(out, "\t\tif [ -n \"$$MOCK_SERVERS\" ]; then \\");
486 let _ = writeln!(
487 out,
488 "\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()))\"); \\"
489 );
490 let _ = writeln!(out, "\t\tfi; \\");
491 let _ = writeln!(out, "\t\t./$(TARGET); \\");
492 let _ = writeln!(out, "\telse \\");
493 let _ = writeln!(out, "\t\tif [ ! -x \"$(MOCK_SERVER_BIN)\" ]; then \\");
494 let _ = writeln!(
495 out,
496 "\t\t\techo \"mock-server binary not found at $(MOCK_SERVER_BIN); run: cargo build -p mock-server --release\" >&2; \\"
497 );
498 let _ = writeln!(out, "\t\t\texit 1; \\");
499 let _ = writeln!(out, "\t\tfi; \\");
500 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
501 let _ = writeln!(out, "\t\tmkfifo mock_server.stdin; \\");
502 let _ = writeln!(
503 out,
504 "\t\t\"$(MOCK_SERVER_BIN)\" \"$(FIXTURES_DIR)\" <mock_server.stdin >mock_server.stdout 2>&1 & \\"
505 );
506 let _ = writeln!(out, "\t\tMOCK_PID=$$!; \\");
507 let _ = writeln!(out, "\t\texec 9>mock_server.stdin; \\");
508 let _ = writeln!(out, "\t\tMOCK_URL=\"\"; MOCK_SERVERS_JSON=\"\"; \\");
509 let _ = writeln!(out, "\t\tfor _ in $$(seq 1 100); do \\");
511 let _ = writeln!(out, "\t\t\tif [ -s mock_server.stdout ]; then \\");
512 let _ = writeln!(
513 out,
514 "\t\t\t\tMOCK_URL=$$(grep -o 'MOCK_SERVER_URL=[^ ]*' mock_server.stdout | head -1 | cut -d= -f2); \\"
515 );
516 let _ = writeln!(out, "\t\t\t\tif [ -n \"$$MOCK_URL\" ]; then break; fi; \\");
517 let _ = writeln!(out, "\t\t\tfi; \\");
518 let _ = writeln!(out, "\t\t\tsleep 0.05; \\");
519 let _ = writeln!(out, "\t\tdone; \\");
520 let _ = writeln!(
522 out,
523 "\t\tMOCK_SERVERS_JSON=$$(grep -o 'MOCK_SERVERS={{.*}}' mock_server.stdout | head -1 | cut -d= -f2-); \\"
524 );
525 let _ = writeln!(
526 out,
527 "\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; \\"
528 );
529 let _ = writeln!(
531 out,
532 "\t\tif [ -n \"$$MOCK_SERVERS_JSON\" ] && command -v python3 >/dev/null 2>&1; then \\"
533 );
534 let _ = writeln!(
535 out,
536 "\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\"); \\"
537 );
538 let _ = writeln!(out, "\t\tfi; \\");
539 let _ = writeln!(out, "\t\tMOCK_SERVER_URL=\"$$MOCK_URL\" ./$(TARGET); STATUS=$$?; \\");
540 let _ = writeln!(out, "\t\texec 9>&-; \\");
541 let _ = writeln!(out, "\t\tkill $$MOCK_PID 2>/dev/null || true; \\");
542 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
543 let _ = writeln!(out, "\t\texit $$STATUS; \\");
544 let _ = writeln!(out, "\tfi");
545 let _ = writeln!(out);
546 let _ = writeln!(out, "clean:");
547 let _ = writeln!(out, "\trm -f $(TARGET) mock_server.stdout mock_server.stdin");
548 out
549}
550
551fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
552 let mut out = String::new();
553 let _ = writeln!(out, "#!/usr/bin/env bash");
554 out.push_str(&hash::header(CommentStyle::Hash));
555 let _ = writeln!(out, "set -euo pipefail");
556 let _ = writeln!(out);
557 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
558 let _ = writeln!(out, "VERSION=\"{version}\"");
559 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
560 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
561 let _ = writeln!(out);
562 let _ = writeln!(out, "# Detect OS and architecture.");
563 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
564 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
565 let _ = writeln!(out);
566 let _ = writeln!(out, "case \"$ARCH\" in");
567 let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
568 let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
569 let _ = writeln!(out, "*)");
570 let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
571 let _ = writeln!(out, " exit 1");
572 let _ = writeln!(out, " ;;");
573 let _ = writeln!(out, "esac");
574 let _ = writeln!(out);
575 let _ = writeln!(out, "case \"$OS\" in");
576 let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
577 let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
578 let _ = writeln!(out, "*)");
579 let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
580 let _ = writeln!(out, " exit 1");
581 let _ = writeln!(out, " ;;");
582 let _ = writeln!(out, "esac");
583 let _ = writeln!(out);
584 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
585 let _ = writeln!(
586 out,
587 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
588 );
589 let _ = writeln!(out);
590 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
591 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
592 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
593 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
594 out
595}
596
597fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
598 let mut out = String::new();
599 out.push_str(&hash::header(CommentStyle::Block));
600 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
601 let _ = writeln!(out, "#define TEST_RUNNER_H");
602 let _ = writeln!(out);
603 let _ = writeln!(out, "#include <string.h>");
604 let _ = writeln!(out, "#include <stdlib.h>");
605 let _ = writeln!(out);
606 let _ = writeln!(out, "/**");
608 let _ = writeln!(
609 out,
610 " * Compare a string against an expected value, trimming trailing whitespace."
611 );
612 let _ = writeln!(
613 out,
614 " * Returns 0 if the trimmed actual string equals the expected string."
615 );
616 let _ = writeln!(out, " */");
617 let _ = writeln!(
618 out,
619 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
620 );
621 let _ = writeln!(
622 out,
623 " if (actual == NULL || expected == NULL) return actual != expected;"
624 );
625 let _ = writeln!(out, " size_t alen = strlen(actual);");
626 let _ = writeln!(
627 out,
628 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
629 );
630 let _ = writeln!(out, " size_t elen = strlen(expected);");
631 let _ = writeln!(out, " if (alen != elen) return 1;");
632 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
633 let _ = writeln!(out, "}}");
634 let _ = writeln!(out);
635
636 let _ = writeln!(
639 out,
640 "static inline char *alef_json_get_object(const char *json, const char *key);"
641 );
642 let _ = writeln!(out);
643 let _ = writeln!(out, "/**");
644 let _ = writeln!(
645 out,
646 " * Extract a string value for a given key from a JSON object string."
647 );
648 let _ = writeln!(
649 out,
650 " * Returns a heap-allocated copy of the value, or NULL if not found."
651 );
652 let _ = writeln!(out, " * Caller must free() the returned string.");
653 let _ = writeln!(out, " */");
654 let _ = writeln!(
655 out,
656 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
657 );
658 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
659 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
660 let _ = writeln!(out, " size_t key_len = strlen(key);");
661 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
662 let _ = writeln!(out, " if (!pattern) return NULL;");
663 let _ = writeln!(out, " pattern[0] = '\"';");
664 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
665 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
666 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
667 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
668 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
669 let _ = writeln!(out, " free(pattern);");
670 let _ = writeln!(out, " if (!found) return NULL;");
671 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
672 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
673 let _ = writeln!(
674 out,
675 " /* Non-string values (arrays/objects) — fall through to alef_json_get_object so"
676 );
677 let _ = writeln!(
678 out,
679 " leaf accessors over collection-typed fields (Vec<T>, Option<Vec<T>>) work for"
680 );
681 let _ = writeln!(
682 out,
683 " not_empty / count_equals assertions without needing per-field type metadata. */"
684 );
685 let _ = writeln!(out, " if (*found == '{{' || *found == '[') {{");
686 let _ = writeln!(out, " return alef_json_get_object(json, key);");
687 let _ = writeln!(out, " }}");
688 let _ = writeln!(
689 out,
690 " /* Primitive non-string value: extract its raw token (numeric / true / false / null)"
691 );
692 let _ = writeln!(
693 out,
694 " so callers asserting on numeric fields can `atoll`/`atof` the result. */"
695 );
696 let _ = writeln!(out, " if (*found != '\"') {{");
697 let _ = writeln!(out, " const char *p = found;");
698 let _ = writeln!(
699 out,
700 " while (*p && *p != ',' && *p != '}}' && *p != ']' && *p != ' ' && *p != '\\t' && *p != '\\n' && *p != '\\r') p++;"
701 );
702 let _ = writeln!(out, " size_t plen = (size_t)(p - found);");
703 let _ = writeln!(out, " if (plen == 0) return NULL;");
704 let _ = writeln!(out, " char *prim = (char *)malloc(plen + 1);");
705 let _ = writeln!(out, " if (!prim) return NULL;");
706 let _ = writeln!(out, " memcpy(prim, found, plen);");
707 let _ = writeln!(out, " prim[plen] = '\\0';");
708 let _ = writeln!(out, " return prim;");
709 let _ = writeln!(out, " }}");
710 let _ = writeln!(out, " found++; /* skip opening quote */");
711 let _ = writeln!(out, " const char *end = found;");
712 let _ = writeln!(out, " while (*end && *end != '\"') {{");
713 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
714 let _ = writeln!(out, " else end++;");
715 let _ = writeln!(out, " }}");
716 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
717 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
718 let _ = writeln!(out, " if (!result_str) return NULL;");
719 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
720 let _ = writeln!(out, " result_str[val_len] = '\\0';");
721 let _ = writeln!(out, " return result_str;");
722 let _ = writeln!(out, "}}");
723 let _ = writeln!(out);
724 let _ = writeln!(out, "/**");
725 let _ = writeln!(
726 out,
727 " * Extract a JSON object/array value `{{...}}` or `[...]` for a given key from"
728 );
729 let _ = writeln!(
730 out,
731 " * a JSON object string. Returns a heap-allocated copy of the value INCLUDING"
732 );
733 let _ = writeln!(
734 out,
735 " * its surrounding braces, or NULL if the key is missing or its value is a"
736 );
737 let _ = writeln!(out, " * primitive. Caller must free() the returned string.");
738 let _ = writeln!(out, " *");
739 let _ = writeln!(
740 out,
741 " * Used by chained-accessor codegen for intermediate object extraction:"
742 );
743 let _ = writeln!(
744 out,
745 " * `choices[0].message.content` first peels off `message` (an object), then"
746 );
747 let _ = writeln!(out, " * looks up `content` (a string) within the extracted substring.");
748 let _ = writeln!(out, " */");
749 let _ = writeln!(
750 out,
751 "static inline char *alef_json_get_object(const char *json, const char *key) {{"
752 );
753 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
754 let _ = writeln!(out, " size_t key_len = strlen(key);");
755 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 4);");
756 let _ = writeln!(out, " if (!pattern) return NULL;");
757 let _ = writeln!(out, " pattern[0] = '\"';");
758 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
759 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
760 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
761 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
762 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
763 let _ = writeln!(out, " free(pattern);");
764 let _ = writeln!(out, " if (!found) return NULL;");
765 let _ = writeln!(out, " found += key_len + 3;");
766 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
767 let _ = writeln!(out, " char open_ch = *found;");
768 let _ = writeln!(out, " char close_ch;");
769 let _ = writeln!(out, " if (open_ch == '{{') close_ch = '}}';");
770 let _ = writeln!(out, " else if (open_ch == '[') close_ch = ']';");
771 let _ = writeln!(
772 out,
773 " else return NULL; /* primitive — caller should use alef_json_get_string */"
774 );
775 let _ = writeln!(out, " int depth = 0;");
776 let _ = writeln!(out, " int in_string = 0;");
777 let _ = writeln!(out, " const char *end = found;");
778 let _ = writeln!(out, " for (; *end; end++) {{");
779 let _ = writeln!(out, " if (in_string) {{");
780 let _ = writeln!(
781 out,
782 " if (*end == '\\\\' && *(end + 1)) {{ end++; continue; }}"
783 );
784 let _ = writeln!(out, " if (*end == '\"') in_string = 0;");
785 let _ = writeln!(out, " continue;");
786 let _ = writeln!(out, " }}");
787 let _ = writeln!(out, " if (*end == '\"') {{ in_string = 1; continue; }}");
788 let _ = writeln!(out, " if (*end == open_ch) depth++;");
789 let _ = writeln!(out, " else if (*end == close_ch) {{");
790 let _ = writeln!(out, " depth--;");
791 let _ = writeln!(out, " if (depth == 0) {{ end++; break; }}");
792 let _ = writeln!(out, " }}");
793 let _ = writeln!(out, " }}");
794 let _ = writeln!(out, " if (depth != 0) return NULL;");
795 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
796 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
797 let _ = writeln!(out, " if (!result_str) return NULL;");
798 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
799 let _ = writeln!(out, " result_str[val_len] = '\\0';");
800 let _ = writeln!(out, " return result_str;");
801 let _ = writeln!(out, "}}");
802 let _ = writeln!(out);
803 let _ = writeln!(out, "/**");
804 let _ = writeln!(
805 out,
806 " * Extract the Nth top-level element of a JSON array as a heap string."
807 );
808 let _ = writeln!(
809 out,
810 " * Returns NULL if the input is not an array, the index is out of bounds, or"
811 );
812 let _ = writeln!(out, " * allocation fails. Caller must free() the returned string.");
813 let _ = writeln!(out, " */");
814 let _ = writeln!(
815 out,
816 "static inline char *alef_json_array_get_index(const char *json, int index) {{"
817 );
818 let _ = writeln!(out, " if (json == NULL || index < 0) return NULL;");
819 let _ = writeln!(
820 out,
821 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
822 );
823 let _ = writeln!(out, " if (*json != '[') return NULL;");
824 let _ = writeln!(out, " json++;");
825 let _ = writeln!(out, " int current = 0;");
826 let _ = writeln!(out, " while (*json) {{");
827 let _ = writeln!(
828 out,
829 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
830 );
831 let _ = writeln!(out, " if (*json == ']') return NULL;");
832 let _ = writeln!(out, " const char *elem_start = json;");
833 let _ = writeln!(out, " int depth = 0;");
834 let _ = writeln!(out, " int in_string = 0;");
835 let _ = writeln!(out, " for (; *json; json++) {{");
836 let _ = writeln!(out, " if (in_string) {{");
837 let _ = writeln!(
838 out,
839 " if (*json == '\\\\' && *(json + 1)) {{ json++; continue; }}"
840 );
841 let _ = writeln!(out, " if (*json == '\"') in_string = 0;");
842 let _ = writeln!(out, " continue;");
843 let _ = writeln!(out, " }}");
844 let _ = writeln!(out, " if (*json == '\"') {{ in_string = 1; continue; }}");
845 let _ = writeln!(out, " if (*json == '{{' || *json == '[') depth++;");
846 let _ = writeln!(out, " else if (*json == '}}' || *json == ']') {{");
847 let _ = writeln!(out, " if (depth == 0) break;");
848 let _ = writeln!(out, " depth--;");
849 let _ = writeln!(out, " }}");
850 let _ = writeln!(out, " else if (*json == ',' && depth == 0) break;");
851 let _ = writeln!(out, " }}");
852 let _ = writeln!(out, " if (current == index) {{");
853 let _ = writeln!(out, " const char *elem_end = json;");
854 let _ = writeln!(
855 out,
856 " while (elem_end > elem_start && (*(elem_end - 1) == ' ' || *(elem_end - 1) == '\\t' || *(elem_end - 1) == '\\n')) elem_end--;"
857 );
858 let _ = writeln!(out, " size_t elem_len = (size_t)(elem_end - elem_start);");
859 let _ = writeln!(out, " char *out_buf = (char *)malloc(elem_len + 1);");
860 let _ = writeln!(out, " if (!out_buf) return NULL;");
861 let _ = writeln!(out, " memcpy(out_buf, elem_start, elem_len);");
862 let _ = writeln!(out, " out_buf[elem_len] = '\\0';");
863 let _ = writeln!(out, " return out_buf;");
864 let _ = writeln!(out, " }}");
865 let _ = writeln!(out, " current++;");
866 let _ = writeln!(out, " if (*json == ']') return NULL;");
867 let _ = writeln!(out, " if (*json == ',') json++;");
868 let _ = writeln!(out, " }}");
869 let _ = writeln!(out, " return NULL;");
870 let _ = writeln!(out, "}}");
871 let _ = writeln!(out);
872 let _ = writeln!(out, "/**");
873 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
874 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
875 let _ = writeln!(out, " */");
876 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
877 let _ = writeln!(out, " if (json == NULL) return 0;");
878 let _ = writeln!(out, " /* Skip leading whitespace */");
879 let _ = writeln!(
880 out,
881 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
882 );
883 let _ = writeln!(out, " if (*json != '[') return 0;");
884 let _ = writeln!(out, " json++;");
885 let _ = writeln!(out, " /* Skip whitespace after '[' */");
886 let _ = writeln!(
887 out,
888 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
889 );
890 let _ = writeln!(out, " if (*json == ']') return 0;");
891 let _ = writeln!(out, " int count = 1;");
892 let _ = writeln!(out, " int depth = 0;");
893 let _ = writeln!(out, " int in_string = 0;");
894 let _ = writeln!(
895 out,
896 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
897 );
898 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
899 let _ = writeln!(
900 out,
901 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
902 );
903 let _ = writeln!(out, " if (in_string) continue;");
904 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
905 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
906 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
907 let _ = writeln!(out, " }}");
908 let _ = writeln!(out, " return count;");
909 let _ = writeln!(out, "}}");
910 let _ = writeln!(out);
911
912 for (group, fixtures) in active_groups {
913 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
914 for fixture in fixtures {
915 let fn_name = sanitize_ident(&fixture.id);
916 let _ = writeln!(out, "void test_{fn_name}(void);");
917 }
918 let _ = writeln!(out);
919 }
920
921 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
922 out
923}
924
925fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
926 let mut out = String::new();
927 out.push_str(&hash::header(CommentStyle::Block));
928 let _ = writeln!(out, "#include <stdio.h>");
929 let _ = writeln!(out, "#include \"test_runner.h\"");
930 let _ = writeln!(out);
931 let _ = writeln!(out, "int main(void) {{");
932 let _ = writeln!(out, " int passed = 0;");
933 let _ = writeln!(out);
934
935 for (group, fixtures) in active_groups {
936 let _ = writeln!(out, " /* Category: {} */", group.category);
937 for fixture in fixtures {
938 let fn_name = sanitize_ident(&fixture.id);
939 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
940 let _ = writeln!(out, " test_{fn_name}();");
941 let _ = writeln!(out, " printf(\" PASSED\\n\");");
942 let _ = writeln!(out, " passed++;");
943 }
944 let _ = writeln!(out);
945 }
946
947 let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
948 let _ = writeln!(out, " return 0;");
949 let _ = writeln!(out, "}}");
950 out
951}
952
953#[allow(clippy::too_many_arguments)]
954fn render_test_file(
955 category: &str,
956 fixtures: &[&Fixture],
957 header: &str,
958 prefix: &str,
959 result_var: &str,
960 e2e_config: &E2eConfig,
961 lang: &str,
962 field_resolver: &FieldResolver,
963) -> String {
964 let mut out = String::new();
965 out.push_str(&hash::header(CommentStyle::Block));
966 let _ = writeln!(out, "/* E2e tests for category: {category} */");
967 let _ = writeln!(out);
968 let _ = writeln!(out, "#include <assert.h>");
969 let _ = writeln!(out, "#include <stdint.h>");
970 let _ = writeln!(out, "#include <string.h>");
971 let _ = writeln!(out, "#include <stdio.h>");
972 let _ = writeln!(out, "#include <stdlib.h>");
973 let _ = writeln!(out, "#include \"{header}\"");
974 let _ = writeln!(out, "#include \"test_runner.h\"");
975 let _ = writeln!(out);
976
977 for (i, fixture) in fixtures.iter().enumerate() {
978 if fixture.visitor.is_some() {
981 panic!(
982 "C e2e generator: visitor pattern not supported for fixture: {}",
983 fixture.id
984 );
985 }
986
987 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
988
989 let mut effective_fields_enum = e2e_config.fields_enum.clone();
995 let fixture_call = e2e_config.resolve_call_for_fixture(
996 fixture.call.as_deref(),
997 &fixture.id,
998 &fixture.resolved_category(),
999 &fixture.tags,
1000 &fixture.input,
1001 );
1002 if let Some(co) = fixture_call.overrides.get(lang) {
1003 for k in co.enum_fields.keys() {
1004 effective_fields_enum.insert(k.clone());
1005 }
1006 }
1007
1008 let per_call_field_resolver = FieldResolver::new(
1014 e2e_config.effective_fields(fixture_call),
1015 e2e_config.effective_fields_optional(fixture_call),
1016 e2e_config.effective_result_fields(fixture_call),
1017 e2e_config.effective_fields_array(fixture_call),
1018 &std::collections::HashSet::new(),
1019 );
1020 let _ = field_resolver; let field_resolver = &per_call_field_resolver;
1022
1023 render_test_function(
1024 &mut out,
1025 fixture,
1026 prefix,
1027 &call_info.function_name,
1028 result_var,
1029 &call_info.args,
1030 field_resolver,
1031 &e2e_config.fields_c_types,
1032 &effective_fields_enum,
1033 &call_info.result_type_name,
1034 &call_info.options_type_name,
1035 call_info.client_factory.as_deref(),
1036 call_info.raw_c_result_type.as_deref(),
1037 call_info.c_free_fn.as_deref(),
1038 call_info.c_engine_factory.as_deref(),
1039 call_info.result_is_option,
1040 call_info.result_is_bytes,
1041 &call_info.extra_args,
1042 );
1043 if i + 1 < fixtures.len() {
1044 let _ = writeln!(out);
1045 }
1046 }
1047
1048 out
1049}
1050
1051#[allow(clippy::too_many_arguments)]
1052fn render_test_function(
1053 out: &mut String,
1054 fixture: &Fixture,
1055 prefix: &str,
1056 function_name: &str,
1057 result_var: &str,
1058 args: &[crate::config::ArgMapping],
1059 field_resolver: &FieldResolver,
1060 fields_c_types: &HashMap<String, String>,
1061 fields_enum: &HashSet<String>,
1062 result_type_name: &str,
1063 options_type_name: &str,
1064 client_factory: Option<&str>,
1065 raw_c_result_type: Option<&str>,
1066 c_free_fn: Option<&str>,
1067 c_engine_factory: Option<&str>,
1068 result_is_option: bool,
1069 result_is_bytes: bool,
1070 extra_args: &[String],
1071) {
1072 let fn_name = sanitize_ident(&fixture.id);
1073 let description = &fixture.description;
1074
1075 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1076
1077 let _ = writeln!(out, "void test_{fn_name}(void) {{");
1078 let _ = writeln!(out, " /* {description} */");
1079
1080 let has_mock = fixture.needs_mock_server();
1089 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1090 if let Some(env) = &fixture.env {
1091 if let Some(var) = &env.api_key_var {
1092 let fixture_id = &fixture.id;
1093 if has_mock {
1094 let _ = writeln!(out, " const char* api_key = getenv(\"{var}\");");
1095 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1096 let _ = writeln!(out, " char base_url_buf[512];");
1097 let _ = writeln!(out, " int use_mock = !(api_key && api_key[0] != '\\0');");
1098 let _ = writeln!(out, " if (!use_mock) {{");
1099 let _ = writeln!(
1100 out,
1101 " fprintf(stderr, \"{fixture_id}: using real API ({var} is set)\\n\");"
1102 );
1103 let _ = writeln!(out, " }} else {{");
1104 let _ = writeln!(
1105 out,
1106 " fprintf(stderr, \"{fixture_id}: using mock server ({var} not set)\\n\");"
1107 );
1108 let _ = writeln!(
1109 out,
1110 " snprintf(base_url_buf, sizeof(base_url_buf), \"%s/fixtures/{fixture_id}\", mock_base ? mock_base : \"\");"
1111 );
1112 let _ = writeln!(out, " api_key = \"test-key\";");
1113 let _ = writeln!(out, " }}");
1114 } else {
1115 let _ = writeln!(out, " if (getenv(\"{var}\") == NULL) {{ return; }}");
1116 }
1117 }
1118 }
1119
1120 let prefix_upper = prefix.to_uppercase();
1121
1122 if let Some(config_type) = c_engine_factory {
1126 render_engine_factory_test_function(
1127 out,
1128 fixture,
1129 prefix,
1130 function_name,
1131 result_var,
1132 field_resolver,
1133 fields_c_types,
1134 fields_enum,
1135 result_type_name,
1136 config_type,
1137 expects_error,
1138 raw_c_result_type,
1139 );
1140 return;
1141 }
1142
1143 if client_factory.is_some() && function_name == "chat_stream" {
1149 render_chat_stream_test_function(
1150 out,
1151 fixture,
1152 prefix,
1153 result_var,
1154 args,
1155 options_type_name,
1156 expects_error,
1157 api_key_var,
1158 );
1159 return;
1160 }
1161
1162 if let Some(factory) = client_factory {
1170 if result_is_bytes {
1171 render_bytes_test_function(
1172 out,
1173 fixture,
1174 prefix,
1175 function_name,
1176 result_var,
1177 args,
1178 options_type_name,
1179 result_type_name,
1180 factory,
1181 expects_error,
1182 );
1183 return;
1184 }
1185 }
1186
1187 if let Some(factory) = client_factory {
1192 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); let mut inline_method_args: Vec<String> = Vec::new();
1197
1198 for arg in args {
1199 if arg.arg_type == "json_object" {
1200 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1205 options_type_name.to_string()
1206 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1207 format!("{}Request", stripped)
1208 } else {
1209 format!("{result_type_name}Request")
1210 };
1211 let request_type_snake = request_type_pascal.to_snake_case();
1212 let var_name = format!("{request_type_snake}_handle");
1213
1214 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1215 let json_val = if field.is_empty() || field == "input" {
1216 Some(&fixture.input)
1217 } else {
1218 fixture.input.get(field)
1219 };
1220
1221 if let Some(val) = json_val {
1222 if !val.is_null() {
1223 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1224 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1225 let escaped = escape_c(&json_str);
1226 let _ = writeln!(
1227 out,
1228 " {prefix_upper}{request_type_pascal}* {var_name} = \
1229 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1230 );
1231 if expects_error {
1232 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
1240 } else {
1241 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1242 }
1243 request_handle_vars.push((arg.name.clone(), var_name));
1244 }
1245 }
1246 } else if arg.arg_type == "string" {
1247 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1249 let val = fixture.input.get(field);
1250 match val {
1251 Some(v) if v.is_string() => {
1252 let s = v.as_str().unwrap_or_default();
1253 let escaped = escape_c(s);
1254 inline_method_args.push(format!("\"{escaped}\""));
1255 }
1256 Some(serde_json::Value::Null) | None if arg.optional => {
1257 inline_method_args.push("NULL".to_string());
1258 }
1259 None => {
1260 inline_method_args.push("\"\"".to_string());
1261 }
1262 Some(other) => {
1263 let s = serde_json::to_string(other).unwrap_or_default();
1264 let escaped = escape_c(&s);
1265 inline_method_args.push(format!("\"{escaped}\""));
1266 }
1267 }
1268 } else if arg.optional {
1269 inline_method_args.push("NULL".to_string());
1271 }
1272 }
1273
1274 let fixture_id = &fixture.id;
1275 if has_mock && api_key_var.is_some() {
1280 let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
1284 let _ = writeln!(
1285 out,
1286 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
1287 );
1288 } else if has_mock {
1289 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1290 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1291 let _ = writeln!(out, " char base_url[1024];");
1292 let _ = writeln!(
1293 out,
1294 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1295 );
1296 let _ = writeln!(
1297 out,
1298 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1299 );
1300 } else {
1301 let _ = writeln!(
1302 out,
1303 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1304 );
1305 }
1306 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1307
1308 let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
1309 String::new()
1310 } else {
1311 let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
1312 let parts: Vec<String> = handles
1313 .into_iter()
1314 .chain(inline_method_args.iter().cloned())
1315 .chain(extra_args.iter().cloned())
1316 .collect();
1317 format!(", {}", parts.join(", "))
1318 };
1319
1320 let call_fn = format!("{prefix}_default_client_{function_name}");
1321
1322 if expects_error {
1323 let _ = writeln!(
1324 out,
1325 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1326 );
1327 for (_, var_name) in &request_handle_vars {
1328 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1329 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1330 }
1331 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1332 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1333 let _ = writeln!(out, "}}");
1334 return;
1335 }
1336
1337 let _ = writeln!(
1338 out,
1339 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1340 );
1341 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1342
1343 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1344 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1345 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1348 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1351
1352 for assertion in &fixture.assertions {
1353 if let Some(f) = &assertion.field {
1354 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1355 let resolved_raw = field_resolver.resolve(f);
1356 let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
1359 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
1360 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
1361 if field_resolver.is_valid_for_result(stripped_first) {
1362 stripped
1363 } else {
1364 resolved_raw
1365 }
1366 } else {
1367 resolved_raw
1368 };
1369 let local_var = f.replace(['.', '['], "_").replace(']', "");
1370 let has_map_access = resolved.contains('[');
1371 if resolved.contains('.') {
1372 let leaf_primitive = emit_nested_accessor(
1373 out,
1374 prefix,
1375 resolved,
1376 &local_var,
1377 result_var,
1378 fields_c_types,
1379 fields_enum,
1380 &mut intermediate_handles,
1381 result_type_name,
1382 f,
1383 );
1384 if let Some(prim) = leaf_primitive {
1385 primitive_locals.insert(local_var.clone(), prim);
1386 }
1387 } else {
1388 let result_type_snake = result_type_name.to_snake_case();
1389 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1390 let lookup_key = format!("{result_type_snake}.{resolved}");
1391 if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
1392 primitive_locals.insert(local_var.clone(), "__skip__".to_string());
1394 } else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1395 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1396 primitive_locals.insert(local_var.clone(), t.clone());
1397 } else if try_emit_enum_accessor(
1398 out,
1399 prefix,
1400 &prefix_upper,
1401 f,
1402 resolved,
1403 &result_type_snake,
1404 &accessor_fn,
1405 result_var,
1406 &local_var,
1407 fields_c_types,
1408 fields_enum,
1409 &mut intermediate_handles,
1410 ) {
1411 } else if let Some(handle_pascal) =
1413 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1414 {
1415 let _ = writeln!(
1417 out,
1418 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1419 );
1420 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1421 } else {
1422 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1423 }
1424 }
1425 accessed_fields.push((f.clone(), local_var, has_map_access));
1426 }
1427 }
1428 }
1429
1430 for assertion in &fixture.assertions {
1431 render_assertion(
1432 out,
1433 assertion,
1434 result_var,
1435 prefix,
1436 field_resolver,
1437 &accessed_fields,
1438 &primitive_locals,
1439 &opaque_handle_locals,
1440 );
1441 }
1442
1443 for (_f, local_var, from_json) in &accessed_fields {
1444 if primitive_locals.contains_key(local_var) {
1445 continue;
1446 }
1447 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1448 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
1449 continue;
1450 }
1451 if *from_json {
1452 let _ = writeln!(out, " free({local_var});");
1453 } else {
1454 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1455 }
1456 }
1457 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1458 if snake_type == "free_string" {
1459 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1460 } else if snake_type == "free" {
1461 let _ = writeln!(out, " free({handle_var});");
1464 } else {
1465 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1466 }
1467 }
1468 let result_type_snake = result_type_name.to_snake_case();
1469 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1470 for (_, var_name) in &request_handle_vars {
1471 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1472 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1473 }
1474 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1475 let _ = writeln!(out, "}}");
1476 return;
1477 }
1478
1479 if let Some(raw_type) = raw_c_result_type {
1482 let args_str = if args.is_empty() {
1484 String::new()
1485 } else {
1486 let parts: Vec<String> = args
1487 .iter()
1488 .filter_map(|arg| {
1489 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1490 let val = fixture.input.get(field);
1491 match val {
1492 None if arg.optional => Some("NULL".to_string()),
1493 None => None,
1494 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1495 Some(v) => Some(json_to_c(v)),
1496 }
1497 })
1498 .collect();
1499 parts.join(", ")
1500 };
1501
1502 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
1504
1505 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
1507 if has_not_error {
1508 match raw_type {
1509 "char*" if !result_is_option => {
1510 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1511 }
1512 "int32_t" => {
1513 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
1514 }
1515 "uintptr_t" => {
1516 let _ = writeln!(
1517 out,
1518 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
1519 );
1520 }
1521 _ => {}
1522 }
1523 }
1524
1525 for assertion in &fixture.assertions {
1527 match assertion.assertion_type.as_str() {
1528 "not_error" | "error" => {} "not_empty" => {
1530 let _ = writeln!(
1531 out,
1532 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
1533 );
1534 }
1535 "is_empty" => {
1536 if result_is_option && raw_type == "char*" {
1537 let _ = writeln!(
1538 out,
1539 " assert({result_var} == NULL && \"expected empty/null value\");"
1540 );
1541 } else {
1542 let _ = writeln!(
1543 out,
1544 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
1545 );
1546 }
1547 }
1548 "count_min" => {
1549 if let Some(val) = &assertion.value {
1550 if let Some(n) = val.as_u64() {
1551 match raw_type {
1552 "char*" => {
1553 let _ = writeln!(out, " {{");
1554 let _ = writeln!(
1555 out,
1556 " assert({result_var} != NULL && \"expected non-null JSON array\");"
1557 );
1558 let _ =
1559 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
1560 let _ = writeln!(
1561 out,
1562 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1563 );
1564 let _ = writeln!(out, " }}");
1565 }
1566 _ => {
1567 let _ = writeln!(
1568 out,
1569 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1570 );
1571 }
1572 }
1573 }
1574 }
1575 }
1576 "greater_than_or_equal" => {
1577 if let Some(val) = &assertion.value {
1578 let c_val = json_to_c(val);
1579 let _ = writeln!(
1580 out,
1581 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1582 );
1583 }
1584 }
1585 "contains" => {
1586 if let Some(val) = &assertion.value {
1587 let c_val = json_to_c(val);
1588 let _ = writeln!(
1589 out,
1590 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1591 );
1592 }
1593 }
1594 "contains_all" => {
1595 if let Some(values) = &assertion.values {
1596 for val in values {
1597 let c_val = json_to_c(val);
1598 let _ = writeln!(
1599 out,
1600 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1601 );
1602 }
1603 }
1604 }
1605 "equals" => {
1606 if let Some(val) = &assertion.value {
1607 let c_val = json_to_c(val);
1608 if val.is_string() {
1609 let _ = writeln!(
1610 out,
1611 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1612 );
1613 } else {
1614 let _ = writeln!(
1615 out,
1616 " assert({result_var} == {c_val} && \"equals assertion failed\");"
1617 );
1618 }
1619 }
1620 }
1621 "not_contains" => {
1622 if let Some(val) = &assertion.value {
1623 let c_val = json_to_c(val);
1624 let _ = writeln!(
1625 out,
1626 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1627 );
1628 }
1629 }
1630 "starts_with" => {
1631 if let Some(val) = &assertion.value {
1632 let c_val = json_to_c(val);
1633 let _ = writeln!(
1634 out,
1635 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1636 );
1637 }
1638 }
1639 "is_true" => {
1640 let _ = writeln!(out, " assert({result_var});");
1641 }
1642 "is_false" => {
1643 let _ = writeln!(out, " assert(!{result_var});");
1644 }
1645 other => {
1646 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1647 }
1648 }
1649 }
1650
1651 if raw_type == "char*" {
1653 let free_fn = c_free_fn
1654 .map(|s| s.to_string())
1655 .unwrap_or_else(|| format!("{prefix}_free_string"));
1656 if result_is_option {
1657 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1658 } else {
1659 let _ = writeln!(out, " {free_fn}({result_var});");
1660 }
1661 }
1662
1663 let _ = writeln!(out, "}}");
1664 return;
1665 }
1666
1667 let prefixed_fn = function_name.to_string();
1673
1674 let mut has_options_handle = false;
1676 for arg in args {
1677 if arg.arg_type == "json_object" {
1678 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1679 if let Some(val) = fixture.input.get(field) {
1680 if !val.is_null() {
1681 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1685 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1686 let escaped = escape_c(&json_str);
1687 let upper = prefix.to_uppercase();
1688 let options_type_pascal = options_type_name;
1689 let options_type_snake = options_type_name.to_snake_case();
1690 let _ = writeln!(
1691 out,
1692 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1693 );
1694 has_options_handle = true;
1695 }
1696 }
1697 }
1698 }
1699
1700 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1701
1702 if expects_error {
1703 let _ = writeln!(
1704 out,
1705 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1706 );
1707 if has_options_handle {
1708 let options_type_snake = options_type_name.to_snake_case();
1709 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1710 }
1711 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1712 let _ = writeln!(out, "}}");
1713 return;
1714 }
1715
1716 let _ = writeln!(
1718 out,
1719 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1720 );
1721 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1722
1723 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1731 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1734 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1736 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1738
1739 for assertion in &fixture.assertions {
1740 if let Some(f) = &assertion.field {
1741 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1742 let resolved_raw = field_resolver.resolve(f);
1743 let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
1746 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
1747 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
1748 if field_resolver.is_valid_for_result(stripped_first) {
1749 stripped
1750 } else {
1751 resolved_raw
1752 }
1753 } else {
1754 resolved_raw
1755 };
1756 let local_var = f.replace(['.', '['], "_").replace(']', "");
1757 let has_map_access = resolved.contains('[');
1758
1759 if resolved.contains('.') {
1760 let leaf_primitive = emit_nested_accessor(
1761 out,
1762 prefix,
1763 resolved,
1764 &local_var,
1765 result_var,
1766 fields_c_types,
1767 fields_enum,
1768 &mut intermediate_handles,
1769 result_type_name,
1770 f,
1771 );
1772 if let Some(prim) = leaf_primitive {
1773 primitive_locals.insert(local_var.clone(), prim);
1774 }
1775 } else {
1776 let result_type_snake = result_type_name.to_snake_case();
1777 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1778 let lookup_key = format!("{result_type_snake}.{resolved}");
1779 if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
1780 primitive_locals.insert(local_var.clone(), "__skip__".to_string());
1782 } else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1783 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1784 primitive_locals.insert(local_var.clone(), t.clone());
1785 } else if try_emit_enum_accessor(
1786 out,
1787 prefix,
1788 &prefix_upper,
1789 f,
1790 resolved,
1791 &result_type_snake,
1792 &accessor_fn,
1793 result_var,
1794 &local_var,
1795 fields_c_types,
1796 fields_enum,
1797 &mut intermediate_handles,
1798 ) {
1799 } else if let Some(handle_pascal) =
1801 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1802 {
1803 let _ = writeln!(
1804 out,
1805 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1806 );
1807 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1808 } else {
1809 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1810 }
1811 }
1812 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1813 }
1814 }
1815 }
1816
1817 for assertion in &fixture.assertions {
1818 render_assertion(
1819 out,
1820 assertion,
1821 result_var,
1822 prefix,
1823 field_resolver,
1824 &accessed_fields,
1825 &primitive_locals,
1826 &opaque_handle_locals,
1827 );
1828 }
1829
1830 for (_f, local_var, from_json) in &accessed_fields {
1832 if primitive_locals.contains_key(local_var) {
1833 continue;
1834 }
1835 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1836 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
1837 continue;
1838 }
1839 if *from_json {
1840 let _ = writeln!(out, " free({local_var});");
1841 } else {
1842 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1843 }
1844 }
1845 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1847 if snake_type == "free_string" {
1848 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1850 } else if snake_type == "free" {
1851 let _ = writeln!(out, " free({handle_var});");
1853 } else {
1854 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1855 }
1856 }
1857 if has_options_handle {
1858 let options_type_snake = options_type_name.to_snake_case();
1859 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1860 }
1861 let result_type_snake = result_type_name.to_snake_case();
1862 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1863 let _ = writeln!(out, "}}");
1864}
1865
1866#[allow(clippy::too_many_arguments)]
1875fn render_engine_factory_test_function(
1876 out: &mut String,
1877 fixture: &Fixture,
1878 prefix: &str,
1879 function_name: &str,
1880 result_var: &str,
1881 field_resolver: &FieldResolver,
1882 fields_c_types: &HashMap<String, String>,
1883 fields_enum: &HashSet<String>,
1884 result_type_name: &str,
1885 config_type: &str,
1886 expects_error: bool,
1887 raw_c_result_type: Option<&str>,
1888) {
1889 let prefix_upper = prefix.to_uppercase();
1890 let config_snake = config_type.to_snake_case();
1891
1892 let config_val = fixture.input.get("config");
1894 let config_json = match config_val {
1895 Some(v) if !v.is_null() => {
1896 let normalized = super::transform_json_keys_for_language(v, "snake_case");
1897 serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1898 }
1899 _ => "{}".to_string(),
1900 };
1901 let config_escaped = escape_c(&config_json);
1902 let fixture_id = &fixture.id;
1903
1904 let has_active_assertions = fixture.assertions.iter().any(|a| {
1908 if let Some(f) = &a.field {
1909 !f.is_empty() && field_resolver.is_valid_for_result(f)
1910 } else {
1911 false
1912 }
1913 });
1914
1915 let _ = writeln!(
1917 out,
1918 " {prefix_upper}{config_type}* config_handle = \
1919 {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1920 );
1921 if expects_error {
1922 let _ = writeln!(out, " if (config_handle == NULL) {{ return; }}");
1925 } else {
1926 let _ = writeln!(out, " assert(config_handle != NULL && \"failed to parse config\");");
1927 }
1928 let _ = writeln!(
1929 out,
1930 " {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1931 );
1932 let _ = writeln!(out, " {prefix}_{config_snake}_free(config_handle);");
1933 if expects_error {
1934 let _ = writeln!(out, " if (engine == NULL) {{ return; }}");
1937 } else {
1938 let _ = writeln!(out, " assert(engine != NULL && \"failed to create engine\");");
1939 }
1940
1941 let fixture_env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1945 let _ = writeln!(out, " const char* mock_per_fixture = getenv(\"{fixture_env_key}\");");
1946 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1947 let _ = writeln!(out, " char url[2048];");
1948 let _ = writeln!(out, " if (mock_per_fixture && mock_per_fixture[0] != '\\0') {{");
1949 let _ = writeln!(out, " snprintf(url, sizeof(url), \"%s\", mock_per_fixture);");
1950 let _ = writeln!(out, " }} else {{");
1951 let _ = writeln!(
1952 out,
1953 " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");"
1954 );
1955 let _ = writeln!(
1956 out,
1957 " snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
1958 );
1959 let _ = writeln!(out, " }}");
1960
1961 let actions_arg = fixture.input.get("actions").and_then(|v| {
1967 if v.is_null() {
1968 None
1969 } else {
1970 let normalized = super::transform_json_keys_for_language(v, "snake_case");
1971 let json = serde_json::to_string(&normalized).ok()?;
1972 let escaped = escape_c(&json);
1973 Some(escaped)
1974 }
1975 });
1976 if let Some(ref escaped_actions) = actions_arg {
1977 let _ = writeln!(out, " const char* actions_json = \"{escaped_actions}\";");
1978 }
1979
1980 let extra_call_args = if actions_arg.is_some() {
1983 ", actions_json".to_string()
1984 } else {
1985 String::new()
1986 };
1987
1988 if let Some(raw_type) = raw_c_result_type {
1997 if raw_type == "char*" {
1998 let _ = writeln!(
1999 out,
2000 " char* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
2001 );
2002 let _ = writeln!(out, " if ({result_var} != NULL) {prefix}_free_string({result_var});");
2003 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2004 let _ = writeln!(out, "}}");
2005 return;
2006 } else {
2007 let raw_snake = raw_type.to_snake_case();
2010 let _ = writeln!(
2011 out,
2012 " {prefix_upper}{raw_type}* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
2013 );
2014 let _ = writeln!(
2015 out,
2016 " if ({result_var} != NULL) {prefix}_{raw_snake}_free({result_var});"
2017 );
2018 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2019 let _ = writeln!(out, "}}");
2020 return;
2021 }
2022 }
2023
2024 let _ = writeln!(
2025 out,
2026 " {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
2027 );
2028
2029 if !has_active_assertions {
2032 let result_type_snake = result_type_name.to_snake_case();
2033 let _ = writeln!(
2034 out,
2035 " if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
2036 );
2037 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2038 let _ = writeln!(out, "}}");
2039 return;
2040 }
2041
2042 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
2043
2044 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
2046 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
2047 let mut primitive_locals: HashMap<String, String> = HashMap::new();
2048 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
2049
2050 for assertion in &fixture.assertions {
2051 if let Some(f) = &assertion.field {
2052 if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
2053 {
2054 let resolved_raw = field_resolver.resolve(f);
2055 let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
2058 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
2059 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
2060 if field_resolver.is_valid_for_result(stripped_first) {
2061 stripped
2062 } else {
2063 resolved_raw
2064 }
2065 } else {
2066 resolved_raw
2067 };
2068 let local_var = f.replace(['.', '['], "_").replace(']', "");
2069 let has_map_access = resolved.contains('[');
2070 if resolved.contains('.') {
2071 let leaf_primitive = emit_nested_accessor(
2072 out,
2073 prefix,
2074 resolved,
2075 &local_var,
2076 result_var,
2077 fields_c_types,
2078 fields_enum,
2079 &mut intermediate_handles,
2080 result_type_name,
2081 f,
2082 );
2083 if let Some(prim) = leaf_primitive {
2084 primitive_locals.insert(local_var.clone(), prim);
2085 }
2086 } else {
2087 let result_type_snake = result_type_name.to_snake_case();
2088 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
2089 let lookup_key = format!("{result_type_snake}.{resolved}");
2090 if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
2091 primitive_locals.insert(local_var.clone(), "__skip__".to_string());
2093 } else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2094 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
2095 primitive_locals.insert(local_var.clone(), t.clone());
2096 } else if try_emit_enum_accessor(
2097 out,
2098 prefix,
2099 &prefix_upper,
2100 f,
2101 resolved,
2102 &result_type_snake,
2103 &accessor_fn,
2104 result_var,
2105 &local_var,
2106 fields_c_types,
2107 fields_enum,
2108 &mut intermediate_handles,
2109 ) {
2110 } else if let Some(handle_pascal) =
2112 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
2113 {
2114 let _ = writeln!(
2115 out,
2116 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
2117 );
2118 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
2119 } else {
2120 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
2121 }
2122 }
2123 accessed_fields.push((f.clone(), local_var, has_map_access));
2124 }
2125 }
2126 }
2127
2128 for assertion in &fixture.assertions {
2129 render_assertion(
2130 out,
2131 assertion,
2132 result_var,
2133 prefix,
2134 field_resolver,
2135 &accessed_fields,
2136 &primitive_locals,
2137 &opaque_handle_locals,
2138 );
2139 }
2140
2141 for (_f, local_var, from_json) in &accessed_fields {
2143 if primitive_locals.contains_key(local_var) {
2144 continue;
2145 }
2146 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
2147 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
2148 continue;
2149 }
2150 if *from_json {
2151 let _ = writeln!(out, " free({local_var});");
2152 } else {
2153 let _ = writeln!(out, " {prefix}_free_string({local_var});");
2154 }
2155 }
2156 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
2157 if snake_type == "free_string" {
2158 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
2159 } else if snake_type == "free" {
2160 let _ = writeln!(out, " free({handle_var});");
2162 } else {
2163 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
2164 }
2165 }
2166
2167 let result_type_snake = result_type_name.to_snake_case();
2168 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
2169 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2170 let _ = writeln!(out, "}}");
2171}
2172
2173#[allow(clippy::too_many_arguments)]
2195fn render_bytes_test_function(
2196 out: &mut String,
2197 fixture: &Fixture,
2198 prefix: &str,
2199 function_name: &str,
2200 _result_var: &str,
2201 args: &[crate::config::ArgMapping],
2202 options_type_name: &str,
2203 result_type_name: &str,
2204 factory: &str,
2205 expects_error: bool,
2206) {
2207 let prefix_upper = prefix.to_uppercase();
2208 let mut request_handle_vars: Vec<(String, String)> = Vec::new();
2209 let mut string_arg_exprs: Vec<String> = Vec::new();
2210
2211 for arg in args {
2212 match arg.arg_type.as_str() {
2213 "json_object" => {
2214 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2215 options_type_name.to_string()
2216 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
2217 format!("{}Request", stripped)
2218 } else {
2219 format!("{result_type_name}Request")
2220 };
2221 let request_type_snake = request_type_pascal.to_snake_case();
2222 let var_name = format!("{request_type_snake}_handle");
2223
2224 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2225 let json_val = if field.is_empty() || field == "input" {
2226 Some(&fixture.input)
2227 } else {
2228 fixture.input.get(field)
2229 };
2230
2231 if let Some(val) = json_val {
2232 if !val.is_null() {
2233 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2234 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2235 let escaped = escape_c(&json_str);
2236 let _ = writeln!(
2237 out,
2238 " {prefix_upper}{request_type_pascal}* {var_name} = \
2239 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2240 );
2241 if expects_error {
2242 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
2250 } else {
2251 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2252 }
2253 request_handle_vars.push((arg.name.clone(), var_name));
2254 }
2255 }
2256 }
2257 "string" => {
2258 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2261 let val = fixture.input.get(field);
2262 let expr = match val {
2263 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
2264 Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
2265 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
2266 None => "NULL".to_string(),
2267 };
2268 string_arg_exprs.push(expr);
2269 }
2270 _ => {
2271 string_arg_exprs.push("NULL".to_string());
2274 }
2275 }
2276 }
2277
2278 let fixture_id = &fixture.id;
2279 if fixture.needs_mock_server() {
2280 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2281 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2282 let _ = writeln!(out, " char base_url[1024];");
2283 let _ = writeln!(
2284 out,
2285 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2286 );
2287 let _ = writeln!(
2292 out,
2293 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2294 );
2295 } else {
2296 let _ = writeln!(
2297 out,
2298 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2299 );
2300 }
2301 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2302
2303 let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
2305 let _ = writeln!(out, " uintptr_t out_len = 0;");
2306 let _ = writeln!(out, " uintptr_t out_cap = 0;");
2307
2308 let mut method_args: Vec<String> = Vec::new();
2310 for (_, v) in &request_handle_vars {
2311 method_args.push(v.clone());
2312 }
2313 method_args.extend(string_arg_exprs.iter().cloned());
2314 let extra_args = if method_args.is_empty() {
2315 String::new()
2316 } else {
2317 format!(", {}", method_args.join(", "))
2318 };
2319
2320 let call_fn = format!("{prefix}_default_client_{function_name}");
2321 let _ = writeln!(
2322 out,
2323 " int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
2324 );
2325
2326 if expects_error {
2327 for (_, var_name) in &request_handle_vars {
2328 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2329 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2330 }
2331 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2332 let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
2333 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2336 let _ = writeln!(out, "}}");
2337 return;
2338 }
2339
2340 let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
2341
2342 let mut emitted_len_check = false;
2347 for assertion in &fixture.assertions {
2348 match assertion.assertion_type.as_str() {
2349 "not_error" => {
2350 }
2352 "not_empty" | "not_null" => {
2353 if !emitted_len_check {
2354 let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
2355 emitted_len_check = true;
2356 }
2357 }
2358 _ => {
2359 let _ = writeln!(
2363 out,
2364 " /* skipped: assertion '{}' not meaningful on raw byte buffer */",
2365 assertion.assertion_type
2366 );
2367 }
2368 }
2369 }
2370
2371 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2372 for (_, var_name) in &request_handle_vars {
2373 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2374 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2375 }
2376 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2377 let _ = writeln!(out, "}}");
2378}
2379
2380#[allow(clippy::too_many_arguments)]
2391fn render_chat_stream_test_function(
2392 out: &mut String,
2393 fixture: &Fixture,
2394 prefix: &str,
2395 result_var: &str,
2396 args: &[crate::config::ArgMapping],
2397 options_type_name: &str,
2398 expects_error: bool,
2399 api_key_var: Option<&str>,
2400) {
2401 let prefix_upper = prefix.to_uppercase();
2402
2403 let mut request_var: Option<String> = None;
2404 for arg in args {
2405 if arg.arg_type == "json_object" {
2406 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2407 options_type_name.to_string()
2408 } else {
2409 "ChatCompletionRequest".to_string()
2410 };
2411 let request_type_snake = request_type_pascal.to_snake_case();
2412 let var_name = format!("{request_type_snake}_handle");
2413
2414 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2415 let json_val = if field.is_empty() || field == "input" {
2416 Some(&fixture.input)
2417 } else {
2418 fixture.input.get(field)
2419 };
2420
2421 if let Some(val) = json_val {
2422 if !val.is_null() {
2423 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2424 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2425 let escaped = escape_c(&json_str);
2426 let _ = writeln!(
2427 out,
2428 " {prefix_upper}{request_type_pascal}* {var_name} = \
2429 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2430 );
2431 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2432 request_var = Some(var_name);
2433 break;
2434 }
2435 }
2436 }
2437 }
2438
2439 let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
2440 let req_snake = request_var
2441 .as_ref()
2442 .and_then(|v| v.strip_suffix("_handle"))
2443 .unwrap_or("chat_completion_request")
2444 .to_string();
2445
2446 let fixture_id = &fixture.id;
2447 let has_mock = fixture.needs_mock_server();
2448 if has_mock && api_key_var.is_some() {
2449 let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
2455 let _ = writeln!(
2456 out,
2457 " {prefix_upper}DefaultClient* client = {prefix}_create_client(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
2458 );
2459 } else if has_mock {
2460 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2461 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2462 let _ = writeln!(out, " char base_url[1024];");
2463 let _ = writeln!(
2464 out,
2465 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2466 );
2467 let _ = writeln!(
2472 out,
2473 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2474 );
2475 } else {
2476 let _ = writeln!(
2477 out,
2478 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2479 );
2480 }
2481 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2482
2483 let _ = writeln!(
2484 out,
2485 " {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
2486 {prefix}_default_client_chat_stream_start(client, {req_handle});"
2487 );
2488
2489 if expects_error {
2490 let _ = writeln!(
2491 out,
2492 " assert(stream_handle == NULL && \"expected stream-start to fail\");"
2493 );
2494 if request_var.is_some() {
2495 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2496 }
2497 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2498 let _ = writeln!(out, "}}");
2499 return;
2500 }
2501
2502 let _ = writeln!(
2503 out,
2504 " assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2505 );
2506
2507 let _ = writeln!(out, " size_t chunks_count = 0;");
2508 let _ = writeln!(out, " char* stream_content = (char*)malloc(1);");
2509 let _ = writeln!(out, " assert(stream_content != NULL);");
2510 let _ = writeln!(out, " stream_content[0] = '\\0';");
2511 let _ = writeln!(out, " size_t stream_content_len = 0;");
2512 let _ = writeln!(out, " int stream_complete = 0;");
2513 let _ = writeln!(out, " int no_chunks_after_done = 1;");
2514 let _ = writeln!(out, " char* last_choices_json = NULL;");
2515 let _ = writeln!(out, " uint64_t total_tokens = 0;");
2516 let _ = writeln!(out);
2517
2518 let _ = writeln!(out, " while (1) {{");
2519 let _ = writeln!(
2520 out,
2521 " {prefix_upper}ChatCompletionChunk* {result_var} = \
2522 {prefix}_default_client_chat_stream_next(stream_handle);"
2523 );
2524 let _ = writeln!(out, " if ({result_var} == NULL) {{");
2525 let _ = writeln!(
2526 out,
2527 " if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2528 );
2529 let _ = writeln!(out, " break;");
2530 let _ = writeln!(out, " }}");
2531 let _ = writeln!(out, " chunks_count++;");
2532 let _ = writeln!(
2533 out,
2534 " char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2535 );
2536 let _ = writeln!(out, " if (choices_json != NULL) {{");
2537 let _ = writeln!(
2538 out,
2539 " const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2540 );
2541 let _ = writeln!(out, " if (d != NULL) {{");
2542 let _ = writeln!(out, " d += 10;");
2543 let _ = writeln!(out, " while (*d == ' ' || *d == '\\t') d++;");
2544 let _ = writeln!(out, " if (*d == '\"') {{");
2545 let _ = writeln!(out, " d++;");
2546 let _ = writeln!(out, " const char* e = d;");
2547 let _ = writeln!(out, " while (*e && *e != '\"') {{");
2548 let _ = writeln!(
2549 out,
2550 " if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2551 );
2552 let _ = writeln!(out, " }}");
2553 let _ = writeln!(out, " size_t add = (size_t)(e - d);");
2554 let _ = writeln!(out, " if (add > 0) {{");
2555 let _ = writeln!(
2556 out,
2557 " char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2558 );
2559 let _ = writeln!(out, " if (nc != NULL) {{");
2560 let _ = writeln!(out, " stream_content = nc;");
2561 let _ = writeln!(
2562 out,
2563 " memcpy(stream_content + stream_content_len, d, add);"
2564 );
2565 let _ = writeln!(out, " stream_content_len += add;");
2566 let _ = writeln!(
2567 out,
2568 " stream_content[stream_content_len] = '\\0';"
2569 );
2570 let _ = writeln!(out, " }}");
2571 let _ = writeln!(out, " }}");
2572 let _ = writeln!(out, " }}");
2573 let _ = writeln!(out, " }}");
2574 let _ = writeln!(
2575 out,
2576 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2577 );
2578 let _ = writeln!(out, " last_choices_json = choices_json;");
2579 let _ = writeln!(out, " }}");
2580 let _ = writeln!(
2581 out,
2582 " {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2583 );
2584 let _ = writeln!(out, " if (usage_handle != NULL) {{");
2585 let _ = writeln!(
2586 out,
2587 " total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2588 );
2589 let _ = writeln!(out, " {prefix}_usage_free(usage_handle);");
2590 let _ = writeln!(out, " }}");
2591 let _ = writeln!(out, " {prefix}_chat_completion_chunk_free({result_var});");
2592 let _ = writeln!(out, " }}");
2593 let _ = writeln!(out, " {prefix}_default_client_chat_stream_free(stream_handle);");
2594 let _ = writeln!(out);
2595
2596 let _ = writeln!(out, " char* finish_reason = NULL;");
2597 let _ = writeln!(out, " char* tool_calls_json = NULL;");
2598 let _ = writeln!(out, " char* tool_calls_0_function_name = NULL;");
2599 let _ = writeln!(out, " if (last_choices_json != NULL) {{");
2600 let _ = writeln!(
2601 out,
2602 " finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2603 );
2604 let _ = writeln!(
2605 out,
2606 " const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2607 );
2608 let _ = writeln!(out, " if (tc != NULL) {{");
2609 let _ = writeln!(out, " tc += 13;");
2610 let _ = writeln!(out, " while (*tc == ' ' || *tc == '\\t') tc++;");
2611 let _ = writeln!(out, " if (*tc == '[') {{");
2612 let _ = writeln!(out, " int depth = 0;");
2613 let _ = writeln!(out, " const char* end = tc;");
2614 let _ = writeln!(out, " int in_str = 0;");
2615 let _ = writeln!(out, " for (; *end; end++) {{");
2616 let _ = writeln!(
2617 out,
2618 " if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2619 );
2620 let _ = writeln!(
2621 out,
2622 " if (*end == '\"') {{ in_str = !in_str; continue; }}"
2623 );
2624 let _ = writeln!(out, " if (in_str) continue;");
2625 let _ = writeln!(out, " if (*end == '[' || *end == '{{') depth++;");
2626 let _ = writeln!(
2627 out,
2628 " else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2629 );
2630 let _ = writeln!(out, " }}");
2631 let _ = writeln!(out, " size_t tlen = (size_t)(end - tc);");
2632 let _ = writeln!(out, " tool_calls_json = (char*)malloc(tlen + 1);");
2633 let _ = writeln!(out, " if (tool_calls_json != NULL) {{");
2634 let _ = writeln!(out, " memcpy(tool_calls_json, tc, tlen);");
2635 let _ = writeln!(out, " tool_calls_json[tlen] = '\\0';");
2636 let _ = writeln!(
2637 out,
2638 " const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2639 );
2640 let _ = writeln!(out, " if (fn != NULL) {{");
2641 let _ = writeln!(
2642 out,
2643 " const char* np = strstr(fn, \"\\\"name\\\":\");"
2644 );
2645 let _ = writeln!(out, " if (np != NULL) {{");
2646 let _ = writeln!(out, " np += 7;");
2647 let _ = writeln!(
2648 out,
2649 " while (*np == ' ' || *np == '\\t') np++;"
2650 );
2651 let _ = writeln!(out, " if (*np == '\"') {{");
2652 let _ = writeln!(out, " np++;");
2653 let _ = writeln!(out, " const char* ne = np;");
2654 let _ = writeln!(
2655 out,
2656 " while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2657 );
2658 let _ = writeln!(out, " size_t nlen = (size_t)(ne - np);");
2659 let _ = writeln!(
2660 out,
2661 " tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2662 );
2663 let _ = writeln!(
2664 out,
2665 " if (tool_calls_0_function_name != NULL) {{"
2666 );
2667 let _ = writeln!(
2668 out,
2669 " memcpy(tool_calls_0_function_name, np, nlen);"
2670 );
2671 let _ = writeln!(
2672 out,
2673 " tool_calls_0_function_name[nlen] = '\\0';"
2674 );
2675 let _ = writeln!(out, " }}");
2676 let _ = writeln!(out, " }}");
2677 let _ = writeln!(out, " }}");
2678 let _ = writeln!(out, " }}");
2679 let _ = writeln!(out, " }}");
2680 let _ = writeln!(out, " }}");
2681 let _ = writeln!(out, " }}");
2682 let _ = writeln!(out, " }}");
2683 let _ = writeln!(out);
2684
2685 for assertion in &fixture.assertions {
2686 emit_chat_stream_assertion(out, assertion);
2687 }
2688
2689 let _ = writeln!(out, " free(stream_content);");
2690 let _ = writeln!(
2691 out,
2692 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2693 );
2694 let _ = writeln!(out, " if (finish_reason != NULL) free(finish_reason);");
2695 let _ = writeln!(out, " if (tool_calls_json != NULL) free(tool_calls_json);");
2696 let _ = writeln!(
2697 out,
2698 " if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2699 );
2700 if request_var.is_some() {
2701 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2702 }
2703 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2704 let _ = writeln!(
2705 out,
2706 " /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2707 (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2708 );
2709 let _ = writeln!(out, "}}");
2710}
2711
2712fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2716 let field = assertion.field.as_deref().unwrap_or("");
2717
2718 enum Kind {
2719 IntCount,
2720 Bool,
2721 Str,
2722 IntTokens,
2723 Unsupported,
2724 }
2725
2726 let (expr, kind) = match field {
2727 "chunks" => ("chunks_count", Kind::IntCount),
2728 "stream_content" => ("stream_content", Kind::Str),
2729 "stream_complete" => ("stream_complete", Kind::Bool),
2730 "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2731 "finish_reason" => ("finish_reason", Kind::Str),
2732 "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2741 "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2742 _ => ("", Kind::Unsupported),
2743 };
2744
2745 let atype = assertion.assertion_type.as_str();
2746 if atype == "not_error" || atype == "error" {
2747 return;
2748 }
2749
2750 if matches!(kind, Kind::Unsupported) {
2751 let _ = writeln!(
2752 out,
2753 " /* skipped: streaming assertion on unsupported field '{field}' */"
2754 );
2755 return;
2756 }
2757
2758 match (atype, &kind) {
2759 ("count_min", Kind::IntCount) => {
2760 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2761 let _ = writeln!(out, " assert({expr} >= {n} && \"expected at least {n} chunks\");");
2762 }
2763 }
2764 ("equals", Kind::Str) => {
2765 if let Some(val) = &assertion.value {
2766 let c_val = json_to_c(val);
2767 let _ = writeln!(
2768 out,
2769 " assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2770 );
2771 }
2772 }
2773 ("contains", Kind::Str) => {
2774 if let Some(val) = &assertion.value {
2775 let c_val = json_to_c(val);
2776 let _ = writeln!(
2777 out,
2778 " assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2779 );
2780 }
2781 }
2782 ("not_empty", Kind::Str) => {
2783 let _ = writeln!(
2784 out,
2785 " assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2786 );
2787 }
2788 ("is_true", Kind::Bool) => {
2789 let _ = writeln!(out, " assert({expr} && \"expected {field} to be true\");");
2790 }
2791 ("is_false", Kind::Bool) => {
2792 let _ = writeln!(out, " assert(!{expr} && \"expected {field} to be false\");");
2793 }
2794 ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2795 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2796 let _ = writeln!(out, " assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2797 }
2798 }
2799 ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2800 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2801 let _ = writeln!(out, " assert({expr} == {n} && \"equals assertion failed\");");
2802 }
2803 }
2804 _ => {
2805 let _ = writeln!(
2806 out,
2807 " /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2808 );
2809 }
2810 }
2811}
2812
2813#[allow(clippy::too_many_arguments)]
2827fn emit_nested_accessor(
2828 out: &mut String,
2829 prefix: &str,
2830 resolved: &str,
2831 local_var: &str,
2832 result_var: &str,
2833 fields_c_types: &HashMap<String, String>,
2834 fields_enum: &HashSet<String>,
2835 intermediate_handles: &mut Vec<(String, String)>,
2836 result_type_name: &str,
2837 raw_field: &str,
2838) -> Option<String> {
2839 let segments: Vec<&str> = resolved.split('.').collect();
2840 let prefix_upper = prefix.to_uppercase();
2841
2842 let mut current_snake_type = result_type_name.to_snake_case();
2844 let mut current_handle = result_var.to_string();
2845 let mut json_extract_mode = false;
2848
2849 for (i, segment) in segments.iter().enumerate() {
2850 let is_leaf = i + 1 == segments.len();
2851
2852 if json_extract_mode {
2856 let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2861 Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2862 None => (segment, None),
2863 };
2864 let seg_snake = bare_segment.to_snake_case();
2865 if is_leaf {
2866 let _ = writeln!(
2867 out,
2868 " char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2869 );
2870 return None; }
2872 let json_var = format!("{seg_snake}_json");
2877 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2878 let _ = writeln!(
2879 out,
2880 " char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2881 );
2882 intermediate_handles.push((json_var.clone(), "free".to_string()));
2883 }
2884 if let Some(key) = bracket_key {
2888 if let Ok(idx) = key.parse::<usize>() {
2889 let elem_var = format!("{seg_snake}_{idx}_json");
2890 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2891 let _ = writeln!(
2892 out,
2893 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2894 );
2895 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2896 }
2897 current_handle = elem_var;
2898 continue;
2899 }
2900 }
2901 current_handle = json_var;
2902 continue;
2903 }
2904
2905 if let Some(bracket_pos) = segment.find('[') {
2907 let field_name = &segment[..bracket_pos];
2908 let key = segment[bracket_pos + 1..].trim_end_matches(']');
2909 let field_snake = field_name.to_snake_case();
2910 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2911
2912 let json_var = format!("{field_snake}_json");
2914 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2915 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2916 let _ = writeln!(out, " assert({json_var} != NULL);");
2917 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2919 }
2920
2921 if key.is_empty() {
2927 if !is_leaf {
2928 current_handle = json_var;
2929 json_extract_mode = true;
2930 continue;
2931 }
2932 return None;
2933 }
2934 if let Ok(idx) = key.parse::<usize>() {
2935 let elem_var = format!("{field_snake}_{idx}_json");
2936 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2937 let _ = writeln!(
2938 out,
2939 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2940 );
2941 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2942 }
2943 if !is_leaf {
2944 current_handle = elem_var;
2945 json_extract_mode = true;
2946 continue;
2947 }
2948 return None;
2950 }
2951
2952 let _ = writeln!(
2954 out,
2955 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
2956 );
2957 return None; }
2959
2960 let seg_snake = segment.to_snake_case();
2961 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
2962
2963 if is_skipped_c_field(fields_c_types, ¤t_snake_type, &seg_snake) {
2965 return Some("__skip__".to_string()); }
2967
2968 if is_leaf {
2969 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2972 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2973 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
2974 return Some(t.clone());
2975 }
2976 if let Some(opaque_type) = fields_c_types.get(&lookup_key).filter(|t| {
2983 *t != "char*"
2984 && *t != "skip"
2985 && !is_primitive_c_type(t)
2986 && t.chars().next().is_some_and(|c| c.is_uppercase())
2987 }) {
2988 let handle_var = format!("{seg_snake}_handle");
2989 let opaque_snake = opaque_type.to_snake_case();
2990 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
2991 let _ = writeln!(
2992 out,
2993 " {prefix_upper}{opaque_type}* {handle_var} = {accessor_fn}({current_handle});"
2994 );
2995 intermediate_handles.push((handle_var.clone(), opaque_snake));
2996 }
2997 if local_var != handle_var {
3000 let _ = writeln!(out, " {prefix_upper}{opaque_type}* {local_var} = {handle_var};");
3001 }
3002 return None; }
3004 if try_emit_enum_accessor(
3006 out,
3007 prefix,
3008 &prefix_upper,
3009 raw_field,
3010 &seg_snake,
3011 ¤t_snake_type,
3012 &accessor_fn,
3013 ¤t_handle,
3014 local_var,
3015 fields_c_types,
3016 fields_enum,
3017 intermediate_handles,
3018 ) {
3019 return None;
3020 }
3021 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
3022 } else {
3023 let lookup_key = format!("{current_snake_type}.{seg_snake}");
3025 let return_type_pascal = match fields_c_types.get(&lookup_key) {
3026 Some(t) => t.clone(),
3027 None => {
3028 segment.to_pascal_case()
3030 }
3031 };
3032
3033 if return_type_pascal == "char*" {
3036 let json_var = format!("{seg_snake}_json");
3037 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
3038 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
3039 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
3040 }
3041 if i + 2 == segments.len() && segments[i + 1] == "length" {
3043 let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
3044 return Some("int".to_string());
3045 }
3046 current_snake_type = seg_snake.clone();
3047 current_handle = json_var;
3048 continue;
3049 }
3050
3051 let return_snake = return_type_pascal.to_snake_case();
3052 let handle_var = format!("{seg_snake}_handle");
3053
3054 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
3057 let _ = writeln!(
3058 out,
3059 " {prefix_upper}{return_type_pascal}* {handle_var} = \
3060 {accessor_fn}({current_handle});"
3061 );
3062 let _ = writeln!(out, " assert({handle_var} != NULL);");
3063 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
3064 }
3065
3066 current_snake_type = return_snake;
3067 current_handle = handle_var;
3068 }
3069 }
3070 None
3071}
3072
3073fn build_args_string_c(
3077 input: &serde_json::Value,
3078 args: &[crate::config::ArgMapping],
3079 has_options_handle: bool,
3080) -> String {
3081 if args.is_empty() {
3082 return json_to_c(input);
3083 }
3084
3085 let parts: Vec<String> = args
3086 .iter()
3087 .filter_map(|arg| {
3088 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
3089 let val = input.get(field);
3090 match val {
3091 None if arg.optional => Some("NULL".to_string()),
3093 None => None,
3095 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
3097 Some(v) => {
3098 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
3101 Some("options_handle".to_string())
3102 } else {
3103 Some(json_to_c(v))
3104 }
3105 }
3106 }
3107 })
3108 .collect();
3109
3110 parts.join(", ")
3111}
3112
3113#[allow(clippy::too_many_arguments)]
3114fn render_assertion(
3115 out: &mut String,
3116 assertion: &Assertion,
3117 result_var: &str,
3118 ffi_prefix: &str,
3119 _field_resolver: &FieldResolver,
3120 accessed_fields: &[(String, String, bool)],
3121 primitive_locals: &HashMap<String, String>,
3122 opaque_handle_locals: &HashMap<String, String>,
3123) {
3124 if let Some(f) = &assertion.field {
3126 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
3127 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
3128 return;
3129 }
3130 }
3131
3132 let field_expr = match &assertion.field {
3133 Some(f) if !f.is_empty() => {
3134 accessed_fields
3136 .iter()
3137 .find(|(k, _, _)| k == f)
3138 .map(|(_, local, _)| local.clone())
3139 .unwrap_or_else(|| result_var.to_string())
3140 }
3141 _ => result_var.to_string(),
3142 };
3143
3144 if primitive_locals.get(&field_expr).is_some_and(|t| t == "__skip__") {
3147 let _ = writeln!(out, " // skipped: field '{field_expr}' not available in C FFI");
3148 return;
3149 }
3150
3151 let field_is_primitive = primitive_locals.contains_key(&field_expr);
3152 let field_primitive_type = primitive_locals.get(&field_expr).cloned();
3153 let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
3158 let field_is_map_access = if let Some(f) = &assertion.field {
3162 accessed_fields.iter().any(|(k, _, m)| k == f && *m)
3163 } else {
3164 false
3165 };
3166
3167 let assertion_field_is_optional = assertion
3171 .field
3172 .as_deref()
3173 .map(|f| {
3174 if f.is_empty() {
3175 return false;
3176 }
3177 if _field_resolver.is_optional(f) {
3178 return true;
3179 }
3180 let resolved = _field_resolver.resolve(f);
3182 _field_resolver.is_optional(resolved)
3183 })
3184 .unwrap_or(false);
3185
3186 match assertion.assertion_type.as_str() {
3187 "equals" => {
3188 if let Some(expected) = &assertion.value {
3189 let c_val = json_to_c(expected);
3190 if field_is_primitive {
3191 let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
3192 match expected.as_bool() {
3193 Some(true) => "1".to_string(),
3194 Some(false) => "0".to_string(),
3195 None => c_val,
3196 }
3197 } else {
3198 c_val
3199 };
3200 let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
3203 if assertion_field_is_optional && is_numeric {
3204 let _ = writeln!(
3205 out,
3206 " assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
3207 );
3208 } else {
3209 let _ = writeln!(
3210 out,
3211 " assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
3212 );
3213 }
3214 } else if expected.is_string() {
3215 let _ = writeln!(
3216 out,
3217 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3218 );
3219 } else if field_is_map_access && expected.is_boolean() {
3220 let lit = match expected.as_bool() {
3221 Some(true) => "\"true\"",
3222 _ => "\"false\"",
3223 };
3224 let _ = writeln!(
3225 out,
3226 " assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
3227 );
3228 } else if field_is_map_access && expected.is_number() {
3229 if expected.is_f64() {
3230 let _ = writeln!(
3231 out,
3232 " assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
3233 );
3234 } else {
3235 let _ = writeln!(
3236 out,
3237 " assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
3238 );
3239 }
3240 } else {
3241 let _ = writeln!(
3242 out,
3243 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3244 );
3245 }
3246 }
3247 }
3248 "contains" => {
3249 if let Some(expected) = &assertion.value {
3250 let c_val = json_to_c(expected);
3251 let _ = writeln!(
3252 out,
3253 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3254 );
3255 }
3256 }
3257 "contains_all" => {
3258 if let Some(values) = &assertion.values {
3259 for val in values {
3260 let c_val = json_to_c(val);
3261 let _ = writeln!(
3262 out,
3263 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3264 );
3265 }
3266 }
3267 }
3268 "not_contains" => {
3269 if let Some(expected) = &assertion.value {
3270 let c_val = json_to_c(expected);
3271 let _ = writeln!(
3272 out,
3273 " assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3274 );
3275 }
3276 }
3277 "not_empty" => {
3278 if field_is_opaque_handle {
3279 let _ = writeln!(out, " assert({field_expr} != NULL && \"expected non-null handle\");");
3283 } else {
3284 let _ = writeln!(
3285 out,
3286 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
3287 );
3288 }
3289 }
3290 "is_empty" => {
3291 if field_is_opaque_handle {
3292 let _ = writeln!(out, " assert({field_expr} == NULL && \"expected null handle\");");
3293 } else if assertion_field_is_optional || !field_is_primitive {
3294 let _ = writeln!(
3296 out,
3297 " assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
3298 );
3299 } else {
3300 let _ = writeln!(
3301 out,
3302 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
3303 );
3304 }
3305 }
3306 "contains_any" => {
3307 if let Some(values) = &assertion.values {
3308 let _ = writeln!(out, " {{");
3309 let _ = writeln!(out, " int found = 0;");
3310 for val in values {
3311 let c_val = json_to_c(val);
3312 let _ = writeln!(
3313 out,
3314 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
3315 );
3316 }
3317 let _ = writeln!(
3318 out,
3319 " assert(found && \"expected to contain at least one of the specified values\");"
3320 );
3321 let _ = writeln!(out, " }}");
3322 }
3323 }
3324 "greater_than" => {
3325 if let Some(val) = &assertion.value {
3326 let c_val = json_to_c(val);
3327 if field_is_map_access && val.is_number() && !field_is_primitive {
3328 let _ = writeln!(
3329 out,
3330 " assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
3331 );
3332 } else {
3333 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
3334 }
3335 }
3336 }
3337 "less_than" => {
3338 if let Some(val) = &assertion.value {
3339 let c_val = json_to_c(val);
3340 if field_is_map_access && val.is_number() && !field_is_primitive {
3341 let _ = writeln!(
3342 out,
3343 " assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
3344 );
3345 } else {
3346 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
3347 }
3348 }
3349 }
3350 "greater_than_or_equal" => {
3351 if let Some(val) = &assertion.value {
3352 let c_val = json_to_c(val);
3353 if field_is_map_access && val.is_number() && !field_is_primitive {
3354 let _ = writeln!(
3355 out,
3356 " assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
3357 );
3358 } else {
3359 let _ = writeln!(
3360 out,
3361 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
3362 );
3363 }
3364 }
3365 }
3366 "less_than_or_equal" => {
3367 if let Some(val) = &assertion.value {
3368 let c_val = json_to_c(val);
3369 if field_is_map_access && val.is_number() && !field_is_primitive {
3370 let _ = writeln!(
3371 out,
3372 " assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
3373 );
3374 } else {
3375 let _ = writeln!(
3376 out,
3377 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
3378 );
3379 }
3380 }
3381 }
3382 "starts_with" => {
3383 if let Some(expected) = &assertion.value {
3384 let c_val = json_to_c(expected);
3385 let _ = writeln!(
3386 out,
3387 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
3388 );
3389 }
3390 }
3391 "ends_with" => {
3392 if let Some(expected) = &assertion.value {
3393 let c_val = json_to_c(expected);
3394 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
3395 let _ = writeln!(
3396 out,
3397 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
3398 );
3399 }
3400 }
3401 "min_length" => {
3402 if let Some(val) = &assertion.value {
3403 if let Some(n) = val.as_u64() {
3404 let _ = writeln!(
3405 out,
3406 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
3407 );
3408 }
3409 }
3410 }
3411 "max_length" => {
3412 if let Some(val) = &assertion.value {
3413 if let Some(n) = val.as_u64() {
3414 let _ = writeln!(
3415 out,
3416 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
3417 );
3418 }
3419 }
3420 }
3421 "count_min" => {
3422 if let Some(val) = &assertion.value {
3423 if let Some(n) = val.as_u64() {
3424 let _ = writeln!(out, " {{");
3425 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
3426 let _ = writeln!(
3427 out,
3428 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3429 );
3430 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3431 let _ = writeln!(
3432 out,
3433 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
3434 );
3435 let _ = writeln!(out, " }}");
3436 }
3437 }
3438 }
3439 "count_equals" => {
3440 if let Some(val) = &assertion.value {
3441 if let Some(n) = val.as_u64() {
3442 let _ = writeln!(out, " {{");
3443 let _ = writeln!(out, " /* count_equals: count elements in array */");
3444 let _ = writeln!(
3445 out,
3446 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3447 );
3448 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3449 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
3450 let _ = writeln!(out, " }}");
3451 }
3452 }
3453 }
3454 "is_true" => {
3455 let _ = writeln!(out, " assert({field_expr});");
3456 }
3457 "is_false" => {
3458 let _ = writeln!(out, " assert(!{field_expr});");
3459 }
3460 "method_result" => {
3461 if let Some(method_name) = &assertion.method {
3462 render_method_result_assertion(
3463 out,
3464 result_var,
3465 ffi_prefix,
3466 method_name,
3467 assertion.args.as_ref(),
3468 assertion.return_type.as_deref(),
3469 assertion.check.as_deref().unwrap_or("is_true"),
3470 assertion.value.as_ref(),
3471 );
3472 } else {
3473 panic!("C e2e generator: method_result assertion missing 'method' field");
3474 }
3475 }
3476 "matches_regex" => {
3477 if let Some(expected) = &assertion.value {
3478 let c_val = json_to_c(expected);
3479 let _ = writeln!(out, " {{");
3480 let _ = writeln!(out, " regex_t _re;");
3481 let _ = writeln!(
3482 out,
3483 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3484 );
3485 let _ = writeln!(
3486 out,
3487 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3488 );
3489 let _ = writeln!(out, " regfree(&_re);");
3490 let _ = writeln!(out, " }}");
3491 }
3492 }
3493 "not_error" => {
3494 }
3496 "error" => {
3497 }
3499 other => {
3500 panic!("C e2e generator: unsupported assertion type: {other}");
3501 }
3502 }
3503}
3504
3505#[allow(clippy::too_many_arguments)]
3514fn render_method_result_assertion(
3515 out: &mut String,
3516 result_var: &str,
3517 ffi_prefix: &str,
3518 method_name: &str,
3519 args: Option<&serde_json::Value>,
3520 return_type: Option<&str>,
3521 check: &str,
3522 value: Option<&serde_json::Value>,
3523) {
3524 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3525
3526 if return_type == Some("string") {
3527 let _ = writeln!(out, " {{");
3529 let _ = writeln!(out, " char* _method_result = {call_expr};");
3530 if check == "is_error" {
3531 let _ = writeln!(
3532 out,
3533 " assert(_method_result == NULL && \"expected method to return error\");"
3534 );
3535 let _ = writeln!(out, " }}");
3536 return;
3537 }
3538 let _ = writeln!(
3539 out,
3540 " assert(_method_result != NULL && \"method_result returned NULL\");"
3541 );
3542 match check {
3543 "contains" => {
3544 if let Some(val) = value {
3545 let c_val = json_to_c(val);
3546 let _ = writeln!(
3547 out,
3548 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3549 );
3550 }
3551 }
3552 "equals" => {
3553 if let Some(val) = value {
3554 let c_val = json_to_c(val);
3555 let _ = writeln!(
3556 out,
3557 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3558 );
3559 }
3560 }
3561 "is_true" => {
3562 let _ = writeln!(
3563 out,
3564 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3565 );
3566 }
3567 "count_min" => {
3568 if let Some(val) = value {
3569 let n = val.as_u64().unwrap_or(0);
3570 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
3571 let _ = writeln!(
3572 out,
3573 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3574 );
3575 }
3576 }
3577 other_check => {
3578 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3579 }
3580 }
3581 let _ = writeln!(out, " free(_method_result);");
3582 let _ = writeln!(out, " }}");
3583 return;
3584 }
3585
3586 match check {
3588 "equals" => {
3589 if let Some(val) = value {
3590 let c_val = json_to_c(val);
3591 let _ = writeln!(
3592 out,
3593 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3594 );
3595 }
3596 }
3597 "is_true" => {
3598 let _ = writeln!(
3599 out,
3600 " assert({call_expr} && \"method_result is_true assertion failed\");"
3601 );
3602 }
3603 "is_false" => {
3604 let _ = writeln!(
3605 out,
3606 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
3607 );
3608 }
3609 "greater_than_or_equal" => {
3610 if let Some(val) = value {
3611 let n = val.as_u64().unwrap_or(0);
3612 let _ = writeln!(
3613 out,
3614 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3615 );
3616 }
3617 }
3618 "count_min" => {
3619 if let Some(val) = value {
3620 let n = val.as_u64().unwrap_or(0);
3621 let _ = writeln!(
3622 out,
3623 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3624 );
3625 }
3626 }
3627 other_check => {
3628 panic!("C e2e generator: unsupported method_result check type: {other_check}");
3629 }
3630 }
3631}
3632
3633fn build_c_method_call(
3640 result_var: &str,
3641 ffi_prefix: &str,
3642 method_name: &str,
3643 args: Option<&serde_json::Value>,
3644) -> String {
3645 let extra_args = if let Some(args_val) = args {
3646 args_val
3647 .as_object()
3648 .map(|obj| {
3649 obj.values()
3650 .map(|v| match v {
3651 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3652 serde_json::Value::Bool(true) => "1".to_string(),
3653 serde_json::Value::Bool(false) => "0".to_string(),
3654 serde_json::Value::Number(n) => n.to_string(),
3655 serde_json::Value::Null => "NULL".to_string(),
3656 other => format!("\"{}\"", escape_c(&other.to_string())),
3657 })
3658 .collect::<Vec<_>>()
3659 .join(", ")
3660 })
3661 .unwrap_or_default()
3662 } else {
3663 String::new()
3664 };
3665
3666 if extra_args.is_empty() {
3667 format!("{ffi_prefix}_{method_name}({result_var})")
3668 } else {
3669 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3670 }
3671}
3672
3673fn json_to_c(value: &serde_json::Value) -> String {
3675 match value {
3676 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3677 serde_json::Value::Bool(true) => "1".to_string(),
3678 serde_json::Value::Bool(false) => "0".to_string(),
3679 serde_json::Value::Number(n) => n.to_string(),
3680 serde_json::Value::Null => "NULL".to_string(),
3681 other => format!("\"{}\"", escape_c(&other.to_string())),
3682 }
3683}