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
51#[allow(clippy::too_many_arguments)]
68fn try_emit_enum_accessor(
69 out: &mut String,
70 prefix: &str,
71 prefix_upper: &str,
72 raw_field: &str,
73 resolved_field: &str,
74 parent_snake_type: &str,
75 accessor_fn: &str,
76 parent_handle: &str,
77 local_var: &str,
78 fields_c_types: &HashMap<String, String>,
79 fields_enum: &HashSet<String>,
80 intermediate_handles: &mut Vec<(String, String)>,
81) -> bool {
82 if !(fields_enum.contains(raw_field) || fields_enum.contains(resolved_field)) {
83 return false;
84 }
85 let lookup_key = format!("{parent_snake_type}.{resolved_field}");
86 let Some(enum_pascal) = fields_c_types.get(&lookup_key) else {
87 return false;
88 };
89 if is_primitive_c_type(enum_pascal) || enum_pascal == "char*" {
90 return false;
91 }
92 let enum_snake = enum_pascal.to_snake_case();
93 let handle_var = format!("{local_var}_handle");
94 let _ = writeln!(
95 out,
96 " {prefix_upper}{enum_pascal}* {handle_var} = {accessor_fn}({parent_handle});"
97 );
98 let _ = writeln!(out, " assert({handle_var} != NULL);");
99 let _ = writeln!(
100 out,
101 " char* {local_var} = {prefix}_{enum_snake}_to_string({handle_var});"
102 );
103 intermediate_handles.push((handle_var, enum_snake));
104 true
105}
106
107impl E2eCodegen for CCodegen {
108 fn generate(
109 &self,
110 groups: &[FixtureGroup],
111 e2e_config: &E2eConfig,
112 config: &ResolvedCrateConfig,
113 _type_defs: &[alef_core::ir::TypeDef],
114 ) -> Result<Vec<GeneratedFile>> {
115 let lang = self.language_name();
116 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
117
118 let mut files = Vec::new();
119
120 let call = &e2e_config.call;
122 let overrides = call.overrides.get(lang);
123 let result_var = &call.result_var;
124 let prefix = overrides
125 .and_then(|o| o.prefix.as_ref())
126 .cloned()
127 .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
128 .unwrap_or_default();
129 let header = overrides
130 .and_then(|o| o.header.as_ref())
131 .cloned()
132 .unwrap_or_else(|| config.ffi_header_name());
133
134 let c_pkg = e2e_config.resolve_package("c");
136 let lib_name = c_pkg
137 .as_ref()
138 .and_then(|p| p.name.as_ref())
139 .cloned()
140 .unwrap_or_else(|| config.ffi_lib_name());
141
142 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
144 .iter()
145 .filter_map(|group| {
146 let active: Vec<&Fixture> = group
147 .fixtures
148 .iter()
149 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
150 .filter(|f| f.visitor.is_none())
151 .collect();
152 if active.is_empty() { None } else { Some((group, active)) }
153 })
154 .collect();
155
156 let ffi_crate_path = c_pkg
164 .as_ref()
165 .and_then(|p| p.path.as_ref())
166 .cloned()
167 .unwrap_or_else(|| config.ffi_crate_path());
168
169 let category_names: Vec<String> = active_groups
171 .iter()
172 .map(|(g, _)| sanitize_filename(&g.category))
173 .collect();
174 files.push(GeneratedFile {
175 path: output_base.join("Makefile"),
176 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
177 generated_header: true,
178 });
179
180 let github_repo = config.github_repo();
182 let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
183 let ffi_pkg_name = e2e_config
184 .registry
185 .packages
186 .get("c")
187 .and_then(|p| p.name.as_ref())
188 .cloned()
189 .unwrap_or_else(|| lib_name.clone());
190 files.push(GeneratedFile {
191 path: output_base.join("download_ffi.sh"),
192 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
193 generated_header: true,
194 });
195
196 files.push(GeneratedFile {
198 path: output_base.join("test_runner.h"),
199 content: render_test_runner_header(&active_groups),
200 generated_header: true,
201 });
202
203 files.push(GeneratedFile {
205 path: output_base.join("main.c"),
206 content: render_main_c(&active_groups),
207 generated_header: true,
208 });
209
210 let field_resolver = FieldResolver::new(
211 &e2e_config.fields,
212 &e2e_config.fields_optional,
213 &e2e_config.result_fields,
214 &e2e_config.fields_array,
215 &std::collections::HashSet::new(),
216 );
217
218 for (group, active) in &active_groups {
222 let filename = format!("test_{}.c", sanitize_filename(&group.category));
223 let content = render_test_file(
224 &group.category,
225 active,
226 &header,
227 &prefix,
228 result_var,
229 e2e_config,
230 lang,
231 &field_resolver,
232 );
233 files.push(GeneratedFile {
234 path: output_base.join(filename),
235 content,
236 generated_header: true,
237 });
238 }
239
240 Ok(files)
241 }
242
243 fn language_name(&self) -> &'static str {
244 "c"
245 }
246}
247
248struct ResolvedCallInfo {
250 function_name: String,
251 result_type_name: String,
252 options_type_name: String,
253 client_factory: Option<String>,
254 args: Vec<crate::config::ArgMapping>,
255 raw_c_result_type: Option<String>,
256 c_free_fn: Option<String>,
257 c_engine_factory: Option<String>,
258 result_is_option: bool,
259 result_is_bytes: bool,
265 extra_args: Vec<String>,
269}
270
271fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
272 let overrides = call.overrides.get(lang);
273 let function_name = overrides
274 .and_then(|o| o.function.as_ref())
275 .cloned()
276 .unwrap_or_else(|| call.function.clone());
277 let result_type_name = overrides
282 .and_then(|o| o.result_type.as_ref())
283 .cloned()
284 .unwrap_or_else(|| call.function.to_pascal_case());
285 let options_type_name = overrides
286 .and_then(|o| o.options_type.as_deref())
287 .unwrap_or("ConversionOptions")
288 .to_string();
289 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
290 let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
291 let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
292 let c_engine_factory = overrides.and_then(|o| o.c_engine_factory.clone());
293 let result_is_option = overrides
294 .and_then(|o| if o.result_is_option { Some(true) } else { None })
295 .unwrap_or(call.result_is_option);
296 let result_is_bytes = call.result_is_bytes || overrides.is_some_and(|o| o.result_is_bytes);
301 let extra_args = overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
302 ResolvedCallInfo {
303 function_name,
304 result_type_name,
305 options_type_name,
306 client_factory,
307 args: call.args.clone(),
308 raw_c_result_type,
309 c_free_fn,
310 c_engine_factory,
311 result_is_option,
312 result_is_bytes,
313 extra_args,
314 }
315}
316
317fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
323 let call = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
324 let mut info = resolve_call_info(call, lang);
325
326 let default_overrides = e2e_config.call.overrides.get(lang);
327
328 if info.client_factory.is_none() {
331 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
332 info.client_factory = Some(factory.clone());
333 }
334 }
335
336 if info.c_engine_factory.is_none() {
339 if let Some(factory) = default_overrides.and_then(|o| o.c_engine_factory.as_ref()) {
340 info.c_engine_factory = Some(factory.clone());
341 }
342 }
343
344 info
345}
346
347fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
348 let mut out = String::new();
349 out.push_str(&hash::header(CommentStyle::Hash));
350 let _ = writeln!(out, "CC = gcc");
351 let _ = writeln!(out, "FFI_DIR = ffi");
352 let _ = writeln!(out);
353
354 let link_lib_name = lib_name.replace('-', "_");
359
360 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
362 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
363 let _ = writeln!(
364 out,
365 " LDFLAGS = -L$(FFI_DIR)/lib -l{link_lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
366 );
367 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
368 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
369 let _ = writeln!(
370 out,
371 " LDFLAGS = -L../../target/release -l{link_lib_name} -Wl,-rpath,../../target/release"
372 );
373 let _ = writeln!(out, "else");
374 let _ = writeln!(
375 out,
376 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
377 );
378 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
379 let _ = writeln!(out, "endif");
380 let _ = writeln!(out);
381
382 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
383 let srcs = src_files.join(" ");
384
385 let _ = writeln!(out, "SRCS = main.c {srcs}");
386 let _ = writeln!(out, "TARGET = run_tests");
387 let _ = writeln!(out);
388 let _ = writeln!(out, ".PHONY: all clean test");
389 let _ = writeln!(out);
390 let _ = writeln!(out, "all: $(TARGET)");
391 let _ = writeln!(out);
392 let _ = writeln!(out, "$(TARGET): $(SRCS)");
393 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
394 let _ = writeln!(out);
395 let _ = writeln!(out, "MOCK_SERVER_BIN ?= ../rust/target/release/mock-server");
400 let _ = writeln!(out, "FIXTURES_DIR ?= ../../fixtures");
401 let _ = writeln!(out);
402 let _ = writeln!(out, "test: $(TARGET)");
403 let _ = writeln!(out, "\t@if [ -n \"$$MOCK_SERVER_URL\" ]; then \\");
404 let _ = writeln!(out, "\t\t./$(TARGET); \\");
405 let _ = writeln!(out, "\telse \\");
406 let _ = writeln!(out, "\t\tif [ ! -x \"$(MOCK_SERVER_BIN)\" ]; then \\");
407 let _ = writeln!(
408 out,
409 "\t\t\techo \"mock-server binary not found at $(MOCK_SERVER_BIN); run: cargo build -p mock-server --release\" >&2; \\"
410 );
411 let _ = writeln!(out, "\t\t\texit 1; \\");
412 let _ = writeln!(out, "\t\tfi; \\");
413 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
414 let _ = writeln!(out, "\t\tmkfifo mock_server.stdin; \\");
415 let _ = writeln!(
416 out,
417 "\t\t\"$(MOCK_SERVER_BIN)\" \"$(FIXTURES_DIR)\" <mock_server.stdin >mock_server.stdout 2>&1 & \\"
418 );
419 let _ = writeln!(out, "\t\tMOCK_PID=$$!; \\");
420 let _ = writeln!(out, "\t\texec 9>mock_server.stdin; \\");
421 let _ = writeln!(out, "\t\tMOCK_URL=\"\"; \\");
422 let _ = writeln!(out, "\t\tfor _ in $$(seq 1 50); do \\");
423 let _ = writeln!(out, "\t\t\tif [ -s mock_server.stdout ]; then \\");
424 let _ = writeln!(
425 out,
426 "\t\t\t\tMOCK_URL=$$(grep -o 'MOCK_SERVER_URL=[^ ]*' mock_server.stdout | head -1 | cut -d= -f2); \\"
427 );
428 let _ = writeln!(out, "\t\t\t\tif [ -n \"$$MOCK_URL\" ]; then break; fi; \\");
429 let _ = writeln!(out, "\t\t\tfi; \\");
430 let _ = writeln!(out, "\t\t\tsleep 0.1; \\");
431 let _ = writeln!(out, "\t\tdone; \\");
432 let _ = writeln!(
433 out,
434 "\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; \\"
435 );
436 let _ = writeln!(out, "\t\tMOCK_SERVER_URL=\"$$MOCK_URL\" ./$(TARGET); STATUS=$$?; \\");
437 let _ = writeln!(out, "\t\texec 9>&-; \\");
438 let _ = writeln!(out, "\t\tkill $$MOCK_PID 2>/dev/null || true; \\");
439 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
440 let _ = writeln!(out, "\t\texit $$STATUS; \\");
441 let _ = writeln!(out, "\tfi");
442 let _ = writeln!(out);
443 let _ = writeln!(out, "clean:");
444 let _ = writeln!(out, "\trm -f $(TARGET) mock_server.stdout mock_server.stdin");
445 out
446}
447
448fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
449 let mut out = String::new();
450 let _ = writeln!(out, "#!/usr/bin/env bash");
451 out.push_str(&hash::header(CommentStyle::Hash));
452 let _ = writeln!(out, "set -euo pipefail");
453 let _ = writeln!(out);
454 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
455 let _ = writeln!(out, "VERSION=\"{version}\"");
456 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
457 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
458 let _ = writeln!(out);
459 let _ = writeln!(out, "# Detect OS and architecture.");
460 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
461 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
462 let _ = writeln!(out);
463 let _ = writeln!(out, "case \"$ARCH\" in");
464 let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
465 let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
466 let _ = writeln!(out, "*)");
467 let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
468 let _ = writeln!(out, " exit 1");
469 let _ = writeln!(out, " ;;");
470 let _ = writeln!(out, "esac");
471 let _ = writeln!(out);
472 let _ = writeln!(out, "case \"$OS\" in");
473 let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
474 let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
475 let _ = writeln!(out, "*)");
476 let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
477 let _ = writeln!(out, " exit 1");
478 let _ = writeln!(out, " ;;");
479 let _ = writeln!(out, "esac");
480 let _ = writeln!(out);
481 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
482 let _ = writeln!(
483 out,
484 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
485 );
486 let _ = writeln!(out);
487 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
488 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
489 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
490 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
491 out
492}
493
494fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
495 let mut out = String::new();
496 out.push_str(&hash::header(CommentStyle::Block));
497 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
498 let _ = writeln!(out, "#define TEST_RUNNER_H");
499 let _ = writeln!(out);
500 let _ = writeln!(out, "#include <string.h>");
501 let _ = writeln!(out, "#include <stdlib.h>");
502 let _ = writeln!(out);
503 let _ = writeln!(out, "/**");
505 let _ = writeln!(
506 out,
507 " * Compare a string against an expected value, trimming trailing whitespace."
508 );
509 let _ = writeln!(
510 out,
511 " * Returns 0 if the trimmed actual string equals the expected string."
512 );
513 let _ = writeln!(out, " */");
514 let _ = writeln!(
515 out,
516 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
517 );
518 let _ = writeln!(
519 out,
520 " if (actual == NULL || expected == NULL) return actual != expected;"
521 );
522 let _ = writeln!(out, " size_t alen = strlen(actual);");
523 let _ = writeln!(
524 out,
525 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
526 );
527 let _ = writeln!(out, " size_t elen = strlen(expected);");
528 let _ = writeln!(out, " if (alen != elen) return 1;");
529 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
530 let _ = writeln!(out, "}}");
531 let _ = writeln!(out);
532
533 let _ = writeln!(
536 out,
537 "static inline char *alef_json_get_object(const char *json, const char *key);"
538 );
539 let _ = writeln!(out);
540 let _ = writeln!(out, "/**");
541 let _ = writeln!(
542 out,
543 " * Extract a string value for a given key from a JSON object string."
544 );
545 let _ = writeln!(
546 out,
547 " * Returns a heap-allocated copy of the value, or NULL if not found."
548 );
549 let _ = writeln!(out, " * Caller must free() the returned string.");
550 let _ = writeln!(out, " */");
551 let _ = writeln!(
552 out,
553 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
554 );
555 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
556 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
557 let _ = writeln!(out, " size_t key_len = strlen(key);");
558 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
559 let _ = writeln!(out, " if (!pattern) return NULL;");
560 let _ = writeln!(out, " pattern[0] = '\"';");
561 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
562 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
563 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
564 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
565 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
566 let _ = writeln!(out, " free(pattern);");
567 let _ = writeln!(out, " if (!found) return NULL;");
568 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
569 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
570 let _ = writeln!(
571 out,
572 " /* Non-string values (arrays/objects) — fall through to alef_json_get_object so"
573 );
574 let _ = writeln!(
575 out,
576 " leaf accessors over collection-typed fields (Vec<T>, Option<Vec<T>>) work for"
577 );
578 let _ = writeln!(
579 out,
580 " not_empty / count_equals assertions without needing per-field type metadata. */"
581 );
582 let _ = writeln!(out, " if (*found == '{{' || *found == '[') {{");
583 let _ = writeln!(out, " return alef_json_get_object(json, key);");
584 let _ = writeln!(out, " }}");
585 let _ = writeln!(
586 out,
587 " /* Primitive non-string value: extract its raw token (numeric / true / false / null)"
588 );
589 let _ = writeln!(
590 out,
591 " so callers asserting on numeric fields can `atoll`/`atof` the result. */"
592 );
593 let _ = writeln!(out, " if (*found != '\"') {{");
594 let _ = writeln!(out, " const char *p = found;");
595 let _ = writeln!(
596 out,
597 " while (*p && *p != ',' && *p != '}}' && *p != ']' && *p != ' ' && *p != '\\t' && *p != '\\n' && *p != '\\r') p++;"
598 );
599 let _ = writeln!(out, " size_t plen = (size_t)(p - found);");
600 let _ = writeln!(out, " if (plen == 0) return NULL;");
601 let _ = writeln!(out, " char *prim = (char *)malloc(plen + 1);");
602 let _ = writeln!(out, " if (!prim) return NULL;");
603 let _ = writeln!(out, " memcpy(prim, found, plen);");
604 let _ = writeln!(out, " prim[plen] = '\\0';");
605 let _ = writeln!(out, " return prim;");
606 let _ = writeln!(out, " }}");
607 let _ = writeln!(out, " found++; /* skip opening quote */");
608 let _ = writeln!(out, " const char *end = found;");
609 let _ = writeln!(out, " while (*end && *end != '\"') {{");
610 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
611 let _ = writeln!(out, " else end++;");
612 let _ = writeln!(out, " }}");
613 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
614 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
615 let _ = writeln!(out, " if (!result_str) return NULL;");
616 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
617 let _ = writeln!(out, " result_str[val_len] = '\\0';");
618 let _ = writeln!(out, " return result_str;");
619 let _ = writeln!(out, "}}");
620 let _ = writeln!(out);
621 let _ = writeln!(out, "/**");
622 let _ = writeln!(
623 out,
624 " * Extract a JSON object/array value `{{...}}` or `[...]` for a given key from"
625 );
626 let _ = writeln!(
627 out,
628 " * a JSON object string. Returns a heap-allocated copy of the value INCLUDING"
629 );
630 let _ = writeln!(
631 out,
632 " * its surrounding braces, or NULL if the key is missing or its value is a"
633 );
634 let _ = writeln!(out, " * primitive. Caller must free() the returned string.");
635 let _ = writeln!(out, " *");
636 let _ = writeln!(
637 out,
638 " * Used by chained-accessor codegen for intermediate object extraction:"
639 );
640 let _ = writeln!(
641 out,
642 " * `choices[0].message.content` first peels off `message` (an object), then"
643 );
644 let _ = writeln!(out, " * looks up `content` (a string) within the extracted substring.");
645 let _ = writeln!(out, " */");
646 let _ = writeln!(
647 out,
648 "static inline char *alef_json_get_object(const char *json, const char *key) {{"
649 );
650 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
651 let _ = writeln!(out, " size_t key_len = strlen(key);");
652 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 4);");
653 let _ = writeln!(out, " if (!pattern) return NULL;");
654 let _ = writeln!(out, " pattern[0] = '\"';");
655 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
656 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
657 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
658 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
659 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
660 let _ = writeln!(out, " free(pattern);");
661 let _ = writeln!(out, " if (!found) return NULL;");
662 let _ = writeln!(out, " found += key_len + 3;");
663 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
664 let _ = writeln!(out, " char open_ch = *found;");
665 let _ = writeln!(out, " char close_ch;");
666 let _ = writeln!(out, " if (open_ch == '{{') close_ch = '}}';");
667 let _ = writeln!(out, " else if (open_ch == '[') close_ch = ']';");
668 let _ = writeln!(
669 out,
670 " else return NULL; /* primitive — caller should use alef_json_get_string */"
671 );
672 let _ = writeln!(out, " int depth = 0;");
673 let _ = writeln!(out, " int in_string = 0;");
674 let _ = writeln!(out, " const char *end = found;");
675 let _ = writeln!(out, " for (; *end; end++) {{");
676 let _ = writeln!(out, " if (in_string) {{");
677 let _ = writeln!(
678 out,
679 " if (*end == '\\\\' && *(end + 1)) {{ end++; continue; }}"
680 );
681 let _ = writeln!(out, " if (*end == '\"') in_string = 0;");
682 let _ = writeln!(out, " continue;");
683 let _ = writeln!(out, " }}");
684 let _ = writeln!(out, " if (*end == '\"') {{ in_string = 1; continue; }}");
685 let _ = writeln!(out, " if (*end == open_ch) depth++;");
686 let _ = writeln!(out, " else if (*end == close_ch) {{");
687 let _ = writeln!(out, " depth--;");
688 let _ = writeln!(out, " if (depth == 0) {{ end++; break; }}");
689 let _ = writeln!(out, " }}");
690 let _ = writeln!(out, " }}");
691 let _ = writeln!(out, " if (depth != 0) return NULL;");
692 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
693 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
694 let _ = writeln!(out, " if (!result_str) return NULL;");
695 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
696 let _ = writeln!(out, " result_str[val_len] = '\\0';");
697 let _ = writeln!(out, " return result_str;");
698 let _ = writeln!(out, "}}");
699 let _ = writeln!(out);
700 let _ = writeln!(out, "/**");
701 let _ = writeln!(
702 out,
703 " * Extract the Nth top-level element of a JSON array as a heap string."
704 );
705 let _ = writeln!(
706 out,
707 " * Returns NULL if the input is not an array, the index is out of bounds, or"
708 );
709 let _ = writeln!(out, " * allocation fails. Caller must free() the returned string.");
710 let _ = writeln!(out, " */");
711 let _ = writeln!(
712 out,
713 "static inline char *alef_json_array_get_index(const char *json, int index) {{"
714 );
715 let _ = writeln!(out, " if (json == NULL || index < 0) return NULL;");
716 let _ = writeln!(
717 out,
718 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
719 );
720 let _ = writeln!(out, " if (*json != '[') return NULL;");
721 let _ = writeln!(out, " json++;");
722 let _ = writeln!(out, " int current = 0;");
723 let _ = writeln!(out, " while (*json) {{");
724 let _ = writeln!(
725 out,
726 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
727 );
728 let _ = writeln!(out, " if (*json == ']') return NULL;");
729 let _ = writeln!(out, " const char *elem_start = json;");
730 let _ = writeln!(out, " int depth = 0;");
731 let _ = writeln!(out, " int in_string = 0;");
732 let _ = writeln!(out, " for (; *json; json++) {{");
733 let _ = writeln!(out, " if (in_string) {{");
734 let _ = writeln!(
735 out,
736 " if (*json == '\\\\' && *(json + 1)) {{ json++; continue; }}"
737 );
738 let _ = writeln!(out, " if (*json == '\"') in_string = 0;");
739 let _ = writeln!(out, " continue;");
740 let _ = writeln!(out, " }}");
741 let _ = writeln!(out, " if (*json == '\"') {{ in_string = 1; continue; }}");
742 let _ = writeln!(out, " if (*json == '{{' || *json == '[') depth++;");
743 let _ = writeln!(out, " else if (*json == '}}' || *json == ']') {{");
744 let _ = writeln!(out, " if (depth == 0) break;");
745 let _ = writeln!(out, " depth--;");
746 let _ = writeln!(out, " }}");
747 let _ = writeln!(out, " else if (*json == ',' && depth == 0) break;");
748 let _ = writeln!(out, " }}");
749 let _ = writeln!(out, " if (current == index) {{");
750 let _ = writeln!(out, " const char *elem_end = json;");
751 let _ = writeln!(
752 out,
753 " while (elem_end > elem_start && (*(elem_end - 1) == ' ' || *(elem_end - 1) == '\\t' || *(elem_end - 1) == '\\n')) elem_end--;"
754 );
755 let _ = writeln!(out, " size_t elem_len = (size_t)(elem_end - elem_start);");
756 let _ = writeln!(out, " char *out_buf = (char *)malloc(elem_len + 1);");
757 let _ = writeln!(out, " if (!out_buf) return NULL;");
758 let _ = writeln!(out, " memcpy(out_buf, elem_start, elem_len);");
759 let _ = writeln!(out, " out_buf[elem_len] = '\\0';");
760 let _ = writeln!(out, " return out_buf;");
761 let _ = writeln!(out, " }}");
762 let _ = writeln!(out, " current++;");
763 let _ = writeln!(out, " if (*json == ']') return NULL;");
764 let _ = writeln!(out, " if (*json == ',') json++;");
765 let _ = writeln!(out, " }}");
766 let _ = writeln!(out, " return NULL;");
767 let _ = writeln!(out, "}}");
768 let _ = writeln!(out);
769 let _ = writeln!(out, "/**");
770 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
771 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
772 let _ = writeln!(out, " */");
773 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
774 let _ = writeln!(out, " if (json == NULL) return 0;");
775 let _ = writeln!(out, " /* Skip leading whitespace */");
776 let _ = writeln!(
777 out,
778 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
779 );
780 let _ = writeln!(out, " if (*json != '[') return 0;");
781 let _ = writeln!(out, " json++;");
782 let _ = writeln!(out, " /* Skip whitespace after '[' */");
783 let _ = writeln!(
784 out,
785 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
786 );
787 let _ = writeln!(out, " if (*json == ']') return 0;");
788 let _ = writeln!(out, " int count = 1;");
789 let _ = writeln!(out, " int depth = 0;");
790 let _ = writeln!(out, " int in_string = 0;");
791 let _ = writeln!(
792 out,
793 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
794 );
795 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
796 let _ = writeln!(
797 out,
798 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
799 );
800 let _ = writeln!(out, " if (in_string) continue;");
801 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
802 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
803 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
804 let _ = writeln!(out, " }}");
805 let _ = writeln!(out, " return count;");
806 let _ = writeln!(out, "}}");
807 let _ = writeln!(out);
808
809 for (group, fixtures) in active_groups {
810 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
811 for fixture in fixtures {
812 let fn_name = sanitize_ident(&fixture.id);
813 let _ = writeln!(out, "void test_{fn_name}(void);");
814 }
815 let _ = writeln!(out);
816 }
817
818 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
819 out
820}
821
822fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
823 let mut out = String::new();
824 out.push_str(&hash::header(CommentStyle::Block));
825 let _ = writeln!(out, "#include <stdio.h>");
826 let _ = writeln!(out, "#include \"test_runner.h\"");
827 let _ = writeln!(out);
828 let _ = writeln!(out, "int main(void) {{");
829 let _ = writeln!(out, " int passed = 0;");
830 let _ = writeln!(out);
831
832 for (group, fixtures) in active_groups {
833 let _ = writeln!(out, " /* Category: {} */", group.category);
834 for fixture in fixtures {
835 let fn_name = sanitize_ident(&fixture.id);
836 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
837 let _ = writeln!(out, " test_{fn_name}();");
838 let _ = writeln!(out, " printf(\" PASSED\\n\");");
839 let _ = writeln!(out, " passed++;");
840 }
841 let _ = writeln!(out);
842 }
843
844 let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
845 let _ = writeln!(out, " return 0;");
846 let _ = writeln!(out, "}}");
847 out
848}
849
850#[allow(clippy::too_many_arguments)]
851fn render_test_file(
852 category: &str,
853 fixtures: &[&Fixture],
854 header: &str,
855 prefix: &str,
856 result_var: &str,
857 e2e_config: &E2eConfig,
858 lang: &str,
859 field_resolver: &FieldResolver,
860) -> String {
861 let mut out = String::new();
862 out.push_str(&hash::header(CommentStyle::Block));
863 let _ = writeln!(out, "/* E2e tests for category: {category} */");
864 let _ = writeln!(out);
865 let _ = writeln!(out, "#include <assert.h>");
866 let _ = writeln!(out, "#include <stdint.h>");
867 let _ = writeln!(out, "#include <string.h>");
868 let _ = writeln!(out, "#include <stdio.h>");
869 let _ = writeln!(out, "#include <stdlib.h>");
870 let _ = writeln!(out, "#include \"{header}\"");
871 let _ = writeln!(out, "#include \"test_runner.h\"");
872 let _ = writeln!(out);
873
874 for (i, fixture) in fixtures.iter().enumerate() {
875 if fixture.visitor.is_some() {
878 panic!(
879 "C e2e generator: visitor pattern not supported for fixture: {}",
880 fixture.id
881 );
882 }
883
884 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
885
886 let mut effective_fields_enum = e2e_config.fields_enum.clone();
892 let fixture_call = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
893 if let Some(co) = fixture_call.overrides.get(lang) {
894 for k in co.enum_fields.keys() {
895 effective_fields_enum.insert(k.clone());
896 }
897 }
898
899 render_test_function(
900 &mut out,
901 fixture,
902 prefix,
903 &call_info.function_name,
904 result_var,
905 &call_info.args,
906 field_resolver,
907 &e2e_config.fields_c_types,
908 &effective_fields_enum,
909 &call_info.result_type_name,
910 &call_info.options_type_name,
911 call_info.client_factory.as_deref(),
912 call_info.raw_c_result_type.as_deref(),
913 call_info.c_free_fn.as_deref(),
914 call_info.c_engine_factory.as_deref(),
915 call_info.result_is_option,
916 call_info.result_is_bytes,
917 &call_info.extra_args,
918 );
919 if i + 1 < fixtures.len() {
920 let _ = writeln!(out);
921 }
922 }
923
924 out
925}
926
927#[allow(clippy::too_many_arguments)]
928fn render_test_function(
929 out: &mut String,
930 fixture: &Fixture,
931 prefix: &str,
932 function_name: &str,
933 result_var: &str,
934 args: &[crate::config::ArgMapping],
935 field_resolver: &FieldResolver,
936 fields_c_types: &HashMap<String, String>,
937 fields_enum: &HashSet<String>,
938 result_type_name: &str,
939 options_type_name: &str,
940 client_factory: Option<&str>,
941 raw_c_result_type: Option<&str>,
942 c_free_fn: Option<&str>,
943 c_engine_factory: Option<&str>,
944 result_is_option: bool,
945 result_is_bytes: bool,
946 extra_args: &[String],
947) {
948 let fn_name = sanitize_ident(&fixture.id);
949 let description = &fixture.description;
950
951 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
952
953 let _ = writeln!(out, "void test_{fn_name}(void) {{");
954 let _ = writeln!(out, " /* {description} */");
955
956 if let Some(env) = &fixture.env {
961 if let Some(var) = &env.api_key_var {
962 let _ = writeln!(out, " if (getenv(\"{var}\") == NULL) {{ return; }}");
963 }
964 }
965
966 let prefix_upper = prefix.to_uppercase();
967
968 if let Some(config_type) = c_engine_factory {
972 render_engine_factory_test_function(
973 out,
974 fixture,
975 prefix,
976 function_name,
977 result_var,
978 field_resolver,
979 fields_c_types,
980 fields_enum,
981 result_type_name,
982 config_type,
983 expects_error,
984 );
985 return;
986 }
987
988 if client_factory.is_some() && function_name == "chat_stream" {
994 render_chat_stream_test_function(out, fixture, prefix, result_var, args, options_type_name, expects_error);
995 return;
996 }
997
998 if let Some(factory) = client_factory {
1006 if result_is_bytes {
1007 render_bytes_test_function(
1008 out,
1009 fixture,
1010 prefix,
1011 function_name,
1012 result_var,
1013 args,
1014 options_type_name,
1015 result_type_name,
1016 factory,
1017 expects_error,
1018 );
1019 return;
1020 }
1021 }
1022
1023 if let Some(factory) = client_factory {
1028 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); let mut inline_method_args: Vec<String> = Vec::new();
1033
1034 for arg in args {
1035 if arg.arg_type == "json_object" {
1036 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1041 options_type_name.to_string()
1042 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1043 format!("{}Request", stripped)
1044 } else {
1045 format!("{result_type_name}Request")
1046 };
1047 let request_type_snake = request_type_pascal.to_snake_case();
1048 let var_name = format!("{request_type_snake}_handle");
1049
1050 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1051 let json_val = if field.is_empty() || field == "input" {
1052 Some(&fixture.input)
1053 } else {
1054 fixture.input.get(field)
1055 };
1056
1057 if let Some(val) = json_val {
1058 if !val.is_null() {
1059 let normalized = super::normalize_json_keys_to_snake_case(val);
1060 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1061 let escaped = escape_c(&json_str);
1062 let _ = writeln!(
1063 out,
1064 " {prefix_upper}{request_type_pascal}* {var_name} = \
1065 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1066 );
1067 if expects_error {
1068 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
1076 } else {
1077 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1078 }
1079 request_handle_vars.push((arg.name.clone(), var_name));
1080 }
1081 }
1082 } else if arg.arg_type == "string" {
1083 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1085 let val = fixture.input.get(field);
1086 match val {
1087 Some(v) if v.is_string() => {
1088 let s = v.as_str().unwrap_or_default();
1089 let escaped = escape_c(s);
1090 inline_method_args.push(format!("\"{escaped}\""));
1091 }
1092 Some(serde_json::Value::Null) | None if arg.optional => {
1093 inline_method_args.push("NULL".to_string());
1094 }
1095 None => {
1096 inline_method_args.push("\"\"".to_string());
1097 }
1098 Some(other) => {
1099 let s = serde_json::to_string(other).unwrap_or_default();
1100 let escaped = escape_c(&s);
1101 inline_method_args.push(format!("\"{escaped}\""));
1102 }
1103 }
1104 } else if arg.optional {
1105 inline_method_args.push("NULL".to_string());
1107 }
1108 }
1109
1110 let fixture_id = &fixture.id;
1111 if fixture.needs_mock_server() {
1112 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1113 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1114 let _ = writeln!(out, " char base_url[1024];");
1115 let _ = writeln!(
1116 out,
1117 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1118 );
1119 let _ = writeln!(
1124 out,
1125 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1126 );
1127 } else {
1128 let _ = writeln!(
1129 out,
1130 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1131 );
1132 }
1133 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1134
1135 let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
1136 String::new()
1137 } else {
1138 let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
1139 let parts: Vec<String> = handles
1140 .into_iter()
1141 .chain(inline_method_args.iter().cloned())
1142 .chain(extra_args.iter().cloned())
1143 .collect();
1144 format!(", {}", parts.join(", "))
1145 };
1146
1147 let call_fn = format!("{prefix}_default_client_{function_name}");
1148
1149 if expects_error {
1150 let _ = writeln!(
1151 out,
1152 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1153 );
1154 for (_, var_name) in &request_handle_vars {
1155 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1156 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1157 }
1158 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1159 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1160 let _ = writeln!(out, "}}");
1161 return;
1162 }
1163
1164 let _ = writeln!(
1165 out,
1166 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1167 );
1168 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1169
1170 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1171 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1172 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1175
1176 for assertion in &fixture.assertions {
1177 if let Some(f) = &assertion.field {
1178 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1179 let resolved = field_resolver.resolve(f);
1180 let local_var = f.replace(['.', '['], "_").replace(']', "");
1181 let has_map_access = resolved.contains('[');
1182 if resolved.contains('.') {
1183 let leaf_primitive = emit_nested_accessor(
1184 out,
1185 prefix,
1186 resolved,
1187 &local_var,
1188 result_var,
1189 fields_c_types,
1190 fields_enum,
1191 &mut intermediate_handles,
1192 result_type_name,
1193 f,
1194 );
1195 if let Some(prim) = leaf_primitive {
1196 primitive_locals.insert(local_var.clone(), prim);
1197 }
1198 } else {
1199 let result_type_snake = result_type_name.to_snake_case();
1200 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1201 let lookup_key = format!("{result_type_snake}.{resolved}");
1202 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1203 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1204 primitive_locals.insert(local_var.clone(), t.clone());
1205 } else if try_emit_enum_accessor(
1206 out,
1207 prefix,
1208 &prefix_upper,
1209 f,
1210 resolved,
1211 &result_type_snake,
1212 &accessor_fn,
1213 result_var,
1214 &local_var,
1215 fields_c_types,
1216 fields_enum,
1217 &mut intermediate_handles,
1218 ) {
1219 } else {
1221 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1222 }
1223 }
1224 accessed_fields.push((f.clone(), local_var, has_map_access));
1225 }
1226 }
1227 }
1228
1229 for assertion in &fixture.assertions {
1230 render_assertion(
1231 out,
1232 assertion,
1233 result_var,
1234 prefix,
1235 field_resolver,
1236 &accessed_fields,
1237 &primitive_locals,
1238 );
1239 }
1240
1241 for (_f, local_var, from_json) in &accessed_fields {
1242 if primitive_locals.contains_key(local_var) {
1243 continue;
1244 }
1245 if *from_json {
1246 let _ = writeln!(out, " free({local_var});");
1247 } else {
1248 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1249 }
1250 }
1251 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1252 if snake_type == "free_string" {
1253 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1254 } else if snake_type == "free" {
1255 let _ = writeln!(out, " free({handle_var});");
1258 } else {
1259 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1260 }
1261 }
1262 let result_type_snake = result_type_name.to_snake_case();
1263 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1264 for (_, var_name) in &request_handle_vars {
1265 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1266 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1267 }
1268 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1269 let _ = writeln!(out, "}}");
1270 return;
1271 }
1272
1273 if let Some(raw_type) = raw_c_result_type {
1276 let args_str = if args.is_empty() {
1278 String::new()
1279 } else {
1280 let parts: Vec<String> = args
1281 .iter()
1282 .filter_map(|arg| {
1283 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1284 let val = fixture.input.get(field);
1285 match val {
1286 None if arg.optional => Some("NULL".to_string()),
1287 None => None,
1288 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1289 Some(v) => Some(json_to_c(v)),
1290 }
1291 })
1292 .collect();
1293 parts.join(", ")
1294 };
1295
1296 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
1298
1299 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
1301 if has_not_error {
1302 match raw_type {
1303 "char*" if !result_is_option => {
1304 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1305 }
1306 "int32_t" => {
1307 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
1308 }
1309 "uintptr_t" => {
1310 let _ = writeln!(
1311 out,
1312 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
1313 );
1314 }
1315 _ => {}
1316 }
1317 }
1318
1319 for assertion in &fixture.assertions {
1321 match assertion.assertion_type.as_str() {
1322 "not_error" | "error" => {} "not_empty" => {
1324 let _ = writeln!(
1325 out,
1326 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
1327 );
1328 }
1329 "is_empty" => {
1330 if result_is_option && raw_type == "char*" {
1331 let _ = writeln!(
1332 out,
1333 " assert({result_var} == NULL && \"expected empty/null value\");"
1334 );
1335 } else {
1336 let _ = writeln!(
1337 out,
1338 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
1339 );
1340 }
1341 }
1342 "count_min" => {
1343 if let Some(val) = &assertion.value {
1344 if let Some(n) = val.as_u64() {
1345 match raw_type {
1346 "char*" => {
1347 let _ = writeln!(out, " {{");
1348 let _ = writeln!(
1349 out,
1350 " assert({result_var} != NULL && \"expected non-null JSON array\");"
1351 );
1352 let _ =
1353 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
1354 let _ = writeln!(
1355 out,
1356 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1357 );
1358 let _ = writeln!(out, " }}");
1359 }
1360 _ => {
1361 let _ = writeln!(
1362 out,
1363 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1364 );
1365 }
1366 }
1367 }
1368 }
1369 }
1370 "greater_than_or_equal" => {
1371 if let Some(val) = &assertion.value {
1372 let c_val = json_to_c(val);
1373 let _ = writeln!(
1374 out,
1375 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1376 );
1377 }
1378 }
1379 "contains" => {
1380 if let Some(val) = &assertion.value {
1381 let c_val = json_to_c(val);
1382 let _ = writeln!(
1383 out,
1384 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1385 );
1386 }
1387 }
1388 "contains_all" => {
1389 if let Some(values) = &assertion.values {
1390 for val in values {
1391 let c_val = json_to_c(val);
1392 let _ = writeln!(
1393 out,
1394 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1395 );
1396 }
1397 }
1398 }
1399 "equals" => {
1400 if let Some(val) = &assertion.value {
1401 let c_val = json_to_c(val);
1402 if val.is_string() {
1403 let _ = writeln!(
1404 out,
1405 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1406 );
1407 } else {
1408 let _ = writeln!(
1409 out,
1410 " assert({result_var} == {c_val} && \"equals assertion failed\");"
1411 );
1412 }
1413 }
1414 }
1415 "not_contains" => {
1416 if let Some(val) = &assertion.value {
1417 let c_val = json_to_c(val);
1418 let _ = writeln!(
1419 out,
1420 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1421 );
1422 }
1423 }
1424 "starts_with" => {
1425 if let Some(val) = &assertion.value {
1426 let c_val = json_to_c(val);
1427 let _ = writeln!(
1428 out,
1429 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1430 );
1431 }
1432 }
1433 "is_true" => {
1434 let _ = writeln!(out, " assert({result_var});");
1435 }
1436 "is_false" => {
1437 let _ = writeln!(out, " assert(!{result_var});");
1438 }
1439 other => {
1440 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1441 }
1442 }
1443 }
1444
1445 if raw_type == "char*" {
1447 let free_fn = c_free_fn
1448 .map(|s| s.to_string())
1449 .unwrap_or_else(|| format!("{prefix}_free_string"));
1450 if result_is_option {
1451 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1452 } else {
1453 let _ = writeln!(out, " {free_fn}({result_var});");
1454 }
1455 }
1456
1457 let _ = writeln!(out, "}}");
1458 return;
1459 }
1460
1461 let prefixed_fn = function_name.to_string();
1467
1468 let mut has_options_handle = false;
1470 for arg in args {
1471 if arg.arg_type == "json_object" {
1472 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1473 if let Some(val) = fixture.input.get(field) {
1474 if !val.is_null() {
1475 let normalized = super::normalize_json_keys_to_snake_case(val);
1479 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1480 let escaped = escape_c(&json_str);
1481 let upper = prefix.to_uppercase();
1482 let options_type_pascal = options_type_name;
1483 let options_type_snake = options_type_name.to_snake_case();
1484 let _ = writeln!(
1485 out,
1486 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1487 );
1488 has_options_handle = true;
1489 }
1490 }
1491 }
1492 }
1493
1494 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1495
1496 if expects_error {
1497 let _ = writeln!(
1498 out,
1499 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1500 );
1501 if has_options_handle {
1502 let options_type_snake = options_type_name.to_snake_case();
1503 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1504 }
1505 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1506 let _ = writeln!(out, "}}");
1507 return;
1508 }
1509
1510 let _ = writeln!(
1512 out,
1513 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1514 );
1515 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1516
1517 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1525 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1528 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1530
1531 for assertion in &fixture.assertions {
1532 if let Some(f) = &assertion.field {
1533 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1534 let resolved = field_resolver.resolve(f);
1535 let local_var = f.replace(['.', '['], "_").replace(']', "");
1536 let has_map_access = resolved.contains('[');
1537
1538 if resolved.contains('.') {
1539 let leaf_primitive = emit_nested_accessor(
1540 out,
1541 prefix,
1542 resolved,
1543 &local_var,
1544 result_var,
1545 fields_c_types,
1546 fields_enum,
1547 &mut intermediate_handles,
1548 result_type_name,
1549 f,
1550 );
1551 if let Some(prim) = leaf_primitive {
1552 primitive_locals.insert(local_var.clone(), prim);
1553 }
1554 } else {
1555 let result_type_snake = result_type_name.to_snake_case();
1556 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1557 let lookup_key = format!("{result_type_snake}.{resolved}");
1558 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1559 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1560 primitive_locals.insert(local_var.clone(), t.clone());
1561 } else if try_emit_enum_accessor(
1562 out,
1563 prefix,
1564 &prefix_upper,
1565 f,
1566 resolved,
1567 &result_type_snake,
1568 &accessor_fn,
1569 result_var,
1570 &local_var,
1571 fields_c_types,
1572 fields_enum,
1573 &mut intermediate_handles,
1574 ) {
1575 } else {
1577 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1578 }
1579 }
1580 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1581 }
1582 }
1583 }
1584
1585 for assertion in &fixture.assertions {
1586 render_assertion(
1587 out,
1588 assertion,
1589 result_var,
1590 prefix,
1591 field_resolver,
1592 &accessed_fields,
1593 &primitive_locals,
1594 );
1595 }
1596
1597 for (_f, local_var, from_json) in &accessed_fields {
1599 if primitive_locals.contains_key(local_var) {
1600 continue;
1601 }
1602 if *from_json {
1603 let _ = writeln!(out, " free({local_var});");
1604 } else {
1605 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1606 }
1607 }
1608 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1610 if snake_type == "free_string" {
1611 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1613 } else {
1614 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1615 }
1616 }
1617 if has_options_handle {
1618 let options_type_snake = options_type_name.to_snake_case();
1619 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1620 }
1621 let result_type_snake = result_type_name.to_snake_case();
1622 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1623 let _ = writeln!(out, "}}");
1624}
1625
1626#[allow(clippy::too_many_arguments)]
1635fn render_engine_factory_test_function(
1636 out: &mut String,
1637 fixture: &Fixture,
1638 prefix: &str,
1639 function_name: &str,
1640 result_var: &str,
1641 field_resolver: &FieldResolver,
1642 fields_c_types: &HashMap<String, String>,
1643 fields_enum: &HashSet<String>,
1644 result_type_name: &str,
1645 config_type: &str,
1646 _expects_error: bool,
1647) {
1648 let prefix_upper = prefix.to_uppercase();
1649 let config_snake = config_type.to_snake_case();
1650
1651 let config_val = fixture.input.get("config");
1653 let config_json = match config_val {
1654 Some(v) if !v.is_null() => {
1655 let normalized = super::normalize_json_keys_to_snake_case(v);
1656 serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1657 }
1658 _ => "{}".to_string(),
1659 };
1660 let config_escaped = escape_c(&config_json);
1661 let fixture_id = &fixture.id;
1662
1663 let has_active_assertions = fixture.assertions.iter().any(|a| {
1667 if let Some(f) = &a.field {
1668 !f.is_empty() && field_resolver.is_valid_for_result(f)
1669 } else {
1670 false
1671 }
1672 });
1673
1674 let _ = writeln!(
1676 out,
1677 " {prefix_upper}{config_type}* config_handle = \
1678 {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1679 );
1680 let _ = writeln!(out, " assert(config_handle != NULL && \"failed to parse config\");");
1681 let _ = writeln!(
1682 out,
1683 " {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1684 );
1685 let _ = writeln!(out, " {prefix}_{config_snake}_free(config_handle);");
1686 let _ = writeln!(out, " assert(engine != NULL && \"failed to create engine\");");
1687
1688 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1690 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1691 let _ = writeln!(out, " char url[2048];");
1692 let _ = writeln!(
1693 out,
1694 " snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
1695 );
1696
1697 let _ = writeln!(
1699 out,
1700 " {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url);"
1701 );
1702
1703 if !has_active_assertions {
1706 let result_type_snake = result_type_name.to_snake_case();
1707 let _ = writeln!(
1708 out,
1709 " if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
1710 );
1711 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1712 let _ = writeln!(out, "}}");
1713 return;
1714 }
1715
1716 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1717
1718 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1720 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1721 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1722
1723 for assertion in &fixture.assertions {
1724 if let Some(f) = &assertion.field {
1725 if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
1726 {
1727 let resolved = field_resolver.resolve(f);
1728 let local_var = f.replace(['.', '['], "_").replace(']', "");
1729 let has_map_access = resolved.contains('[');
1730 if resolved.contains('.') {
1731 let leaf_primitive = emit_nested_accessor(
1732 out,
1733 prefix,
1734 resolved,
1735 &local_var,
1736 result_var,
1737 fields_c_types,
1738 fields_enum,
1739 &mut intermediate_handles,
1740 result_type_name,
1741 f,
1742 );
1743 if let Some(prim) = leaf_primitive {
1744 primitive_locals.insert(local_var.clone(), prim);
1745 }
1746 } else {
1747 let result_type_snake = result_type_name.to_snake_case();
1748 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1749 let lookup_key = format!("{result_type_snake}.{resolved}");
1750 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1751 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1752 primitive_locals.insert(local_var.clone(), t.clone());
1753 } else if try_emit_enum_accessor(
1754 out,
1755 prefix,
1756 &prefix_upper,
1757 f,
1758 resolved,
1759 &result_type_snake,
1760 &accessor_fn,
1761 result_var,
1762 &local_var,
1763 fields_c_types,
1764 fields_enum,
1765 &mut intermediate_handles,
1766 ) {
1767 } else {
1769 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1770 }
1771 }
1772 accessed_fields.push((f.clone(), local_var, has_map_access));
1773 }
1774 }
1775 }
1776
1777 for assertion in &fixture.assertions {
1778 render_assertion(
1779 out,
1780 assertion,
1781 result_var,
1782 prefix,
1783 field_resolver,
1784 &accessed_fields,
1785 &primitive_locals,
1786 );
1787 }
1788
1789 for (_f, local_var, from_json) in &accessed_fields {
1791 if primitive_locals.contains_key(local_var) {
1792 continue;
1793 }
1794 if *from_json {
1795 let _ = writeln!(out, " free({local_var});");
1796 } else {
1797 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1798 }
1799 }
1800 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1801 if snake_type == "free_string" {
1802 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1803 } else {
1804 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1805 }
1806 }
1807
1808 let result_type_snake = result_type_name.to_snake_case();
1809 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1810 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1811 let _ = writeln!(out, "}}");
1812}
1813
1814#[allow(clippy::too_many_arguments)]
1836fn render_bytes_test_function(
1837 out: &mut String,
1838 fixture: &Fixture,
1839 prefix: &str,
1840 function_name: &str,
1841 _result_var: &str,
1842 args: &[crate::config::ArgMapping],
1843 options_type_name: &str,
1844 result_type_name: &str,
1845 factory: &str,
1846 expects_error: bool,
1847) {
1848 let prefix_upper = prefix.to_uppercase();
1849 let mut request_handle_vars: Vec<(String, String)> = Vec::new();
1850 let mut string_arg_exprs: Vec<String> = Vec::new();
1851
1852 for arg in args {
1853 match arg.arg_type.as_str() {
1854 "json_object" => {
1855 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1856 options_type_name.to_string()
1857 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1858 format!("{}Request", stripped)
1859 } else {
1860 format!("{result_type_name}Request")
1861 };
1862 let request_type_snake = request_type_pascal.to_snake_case();
1863 let var_name = format!("{request_type_snake}_handle");
1864
1865 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1866 let json_val = if field.is_empty() || field == "input" {
1867 Some(&fixture.input)
1868 } else {
1869 fixture.input.get(field)
1870 };
1871
1872 if let Some(val) = json_val {
1873 if !val.is_null() {
1874 let normalized = super::normalize_json_keys_to_snake_case(val);
1875 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1876 let escaped = escape_c(&json_str);
1877 let _ = writeln!(
1878 out,
1879 " {prefix_upper}{request_type_pascal}* {var_name} = \
1880 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1881 );
1882 if expects_error {
1883 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
1891 } else {
1892 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1893 }
1894 request_handle_vars.push((arg.name.clone(), var_name));
1895 }
1896 }
1897 }
1898 "string" => {
1899 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1902 let val = fixture.input.get(field);
1903 let expr = match val {
1904 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
1905 Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
1906 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
1907 None => "NULL".to_string(),
1908 };
1909 string_arg_exprs.push(expr);
1910 }
1911 _ => {
1912 string_arg_exprs.push("NULL".to_string());
1915 }
1916 }
1917 }
1918
1919 let fixture_id = &fixture.id;
1920 if fixture.needs_mock_server() {
1921 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1922 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1923 let _ = writeln!(out, " char base_url[1024];");
1924 let _ = writeln!(
1925 out,
1926 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1927 );
1928 let _ = writeln!(
1933 out,
1934 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1935 );
1936 } else {
1937 let _ = writeln!(
1938 out,
1939 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1940 );
1941 }
1942 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1943
1944 let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
1946 let _ = writeln!(out, " uintptr_t out_len = 0;");
1947 let _ = writeln!(out, " uintptr_t out_cap = 0;");
1948
1949 let mut method_args: Vec<String> = Vec::new();
1951 for (_, v) in &request_handle_vars {
1952 method_args.push(v.clone());
1953 }
1954 method_args.extend(string_arg_exprs.iter().cloned());
1955 let extra_args = if method_args.is_empty() {
1956 String::new()
1957 } else {
1958 format!(", {}", method_args.join(", "))
1959 };
1960
1961 let call_fn = format!("{prefix}_default_client_{function_name}");
1962 let _ = writeln!(
1963 out,
1964 " int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
1965 );
1966
1967 if expects_error {
1968 for (_, var_name) in &request_handle_vars {
1969 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1970 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1971 }
1972 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1973 let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
1974 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
1977 let _ = writeln!(out, "}}");
1978 return;
1979 }
1980
1981 let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
1982
1983 let mut emitted_len_check = false;
1988 for assertion in &fixture.assertions {
1989 match assertion.assertion_type.as_str() {
1990 "not_error" => {
1991 }
1993 "not_empty" | "not_null" => {
1994 if !emitted_len_check {
1995 let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
1996 emitted_len_check = true;
1997 }
1998 }
1999 _ => {
2000 let _ = writeln!(
2004 out,
2005 " /* skipped: assertion '{}' not meaningful on raw byte buffer */",
2006 assertion.assertion_type
2007 );
2008 }
2009 }
2010 }
2011
2012 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2013 for (_, var_name) in &request_handle_vars {
2014 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2015 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2016 }
2017 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2018 let _ = writeln!(out, "}}");
2019}
2020
2021fn render_chat_stream_test_function(
2032 out: &mut String,
2033 fixture: &Fixture,
2034 prefix: &str,
2035 result_var: &str,
2036 args: &[crate::config::ArgMapping],
2037 options_type_name: &str,
2038 expects_error: bool,
2039) {
2040 let prefix_upper = prefix.to_uppercase();
2041
2042 let mut request_var: Option<String> = None;
2043 for arg in args {
2044 if arg.arg_type == "json_object" {
2045 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2046 options_type_name.to_string()
2047 } else {
2048 "ChatCompletionRequest".to_string()
2049 };
2050 let request_type_snake = request_type_pascal.to_snake_case();
2051 let var_name = format!("{request_type_snake}_handle");
2052
2053 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2054 let json_val = if field.is_empty() || field == "input" {
2055 Some(&fixture.input)
2056 } else {
2057 fixture.input.get(field)
2058 };
2059
2060 if let Some(val) = json_val {
2061 if !val.is_null() {
2062 let normalized = super::normalize_json_keys_to_snake_case(val);
2063 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2064 let escaped = escape_c(&json_str);
2065 let _ = writeln!(
2066 out,
2067 " {prefix_upper}{request_type_pascal}* {var_name} = \
2068 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2069 );
2070 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2071 request_var = Some(var_name);
2072 break;
2073 }
2074 }
2075 }
2076 }
2077
2078 let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
2079 let req_snake = request_var
2080 .as_ref()
2081 .and_then(|v| v.strip_suffix("_handle"))
2082 .unwrap_or("chat_completion_request")
2083 .to_string();
2084
2085 let fixture_id = &fixture.id;
2086 if fixture.needs_mock_server() {
2087 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2088 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2089 let _ = writeln!(out, " char base_url[1024];");
2090 let _ = writeln!(
2091 out,
2092 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2093 );
2094 let _ = writeln!(
2099 out,
2100 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2101 );
2102 } else {
2103 let _ = writeln!(
2104 out,
2105 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2106 );
2107 }
2108 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2109
2110 let _ = writeln!(
2111 out,
2112 " {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
2113 {prefix}_default_client_chat_stream_start(client, {req_handle});"
2114 );
2115
2116 if expects_error {
2117 let _ = writeln!(
2118 out,
2119 " assert(stream_handle == NULL && \"expected stream-start to fail\");"
2120 );
2121 if request_var.is_some() {
2122 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2123 }
2124 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2125 let _ = writeln!(out, "}}");
2126 return;
2127 }
2128
2129 let _ = writeln!(
2130 out,
2131 " assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2132 );
2133
2134 let _ = writeln!(out, " size_t chunks_count = 0;");
2135 let _ = writeln!(out, " char* stream_content = (char*)malloc(1);");
2136 let _ = writeln!(out, " assert(stream_content != NULL);");
2137 let _ = writeln!(out, " stream_content[0] = '\\0';");
2138 let _ = writeln!(out, " size_t stream_content_len = 0;");
2139 let _ = writeln!(out, " int stream_complete = 0;");
2140 let _ = writeln!(out, " int no_chunks_after_done = 1;");
2141 let _ = writeln!(out, " char* last_choices_json = NULL;");
2142 let _ = writeln!(out, " uint64_t total_tokens = 0;");
2143 let _ = writeln!(out);
2144
2145 let _ = writeln!(out, " while (1) {{");
2146 let _ = writeln!(
2147 out,
2148 " {prefix_upper}ChatCompletionChunk* {result_var} = \
2149 {prefix}_default_client_chat_stream_next(stream_handle);"
2150 );
2151 let _ = writeln!(out, " if ({result_var} == NULL) {{");
2152 let _ = writeln!(
2153 out,
2154 " if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2155 );
2156 let _ = writeln!(out, " break;");
2157 let _ = writeln!(out, " }}");
2158 let _ = writeln!(out, " chunks_count++;");
2159 let _ = writeln!(
2160 out,
2161 " char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2162 );
2163 let _ = writeln!(out, " if (choices_json != NULL) {{");
2164 let _ = writeln!(
2165 out,
2166 " const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2167 );
2168 let _ = writeln!(out, " if (d != NULL) {{");
2169 let _ = writeln!(out, " d += 10;");
2170 let _ = writeln!(out, " while (*d == ' ' || *d == '\\t') d++;");
2171 let _ = writeln!(out, " if (*d == '\"') {{");
2172 let _ = writeln!(out, " d++;");
2173 let _ = writeln!(out, " const char* e = d;");
2174 let _ = writeln!(out, " while (*e && *e != '\"') {{");
2175 let _ = writeln!(
2176 out,
2177 " if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2178 );
2179 let _ = writeln!(out, " }}");
2180 let _ = writeln!(out, " size_t add = (size_t)(e - d);");
2181 let _ = writeln!(out, " if (add > 0) {{");
2182 let _ = writeln!(
2183 out,
2184 " char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2185 );
2186 let _ = writeln!(out, " if (nc != NULL) {{");
2187 let _ = writeln!(out, " stream_content = nc;");
2188 let _ = writeln!(
2189 out,
2190 " memcpy(stream_content + stream_content_len, d, add);"
2191 );
2192 let _ = writeln!(out, " stream_content_len += add;");
2193 let _ = writeln!(
2194 out,
2195 " stream_content[stream_content_len] = '\\0';"
2196 );
2197 let _ = writeln!(out, " }}");
2198 let _ = writeln!(out, " }}");
2199 let _ = writeln!(out, " }}");
2200 let _ = writeln!(out, " }}");
2201 let _ = writeln!(
2202 out,
2203 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2204 );
2205 let _ = writeln!(out, " last_choices_json = choices_json;");
2206 let _ = writeln!(out, " }}");
2207 let _ = writeln!(
2208 out,
2209 " {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2210 );
2211 let _ = writeln!(out, " if (usage_handle != NULL) {{");
2212 let _ = writeln!(
2213 out,
2214 " total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2215 );
2216 let _ = writeln!(out, " {prefix}_usage_free(usage_handle);");
2217 let _ = writeln!(out, " }}");
2218 let _ = writeln!(out, " {prefix}_chat_completion_chunk_free({result_var});");
2219 let _ = writeln!(out, " }}");
2220 let _ = writeln!(out, " {prefix}_default_client_chat_stream_free(stream_handle);");
2221 let _ = writeln!(out);
2222
2223 let _ = writeln!(out, " char* finish_reason = NULL;");
2224 let _ = writeln!(out, " char* tool_calls_json = NULL;");
2225 let _ = writeln!(out, " char* tool_calls_0_function_name = NULL;");
2226 let _ = writeln!(out, " if (last_choices_json != NULL) {{");
2227 let _ = writeln!(
2228 out,
2229 " finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2230 );
2231 let _ = writeln!(
2232 out,
2233 " const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2234 );
2235 let _ = writeln!(out, " if (tc != NULL) {{");
2236 let _ = writeln!(out, " tc += 13;");
2237 let _ = writeln!(out, " while (*tc == ' ' || *tc == '\\t') tc++;");
2238 let _ = writeln!(out, " if (*tc == '[') {{");
2239 let _ = writeln!(out, " int depth = 0;");
2240 let _ = writeln!(out, " const char* end = tc;");
2241 let _ = writeln!(out, " int in_str = 0;");
2242 let _ = writeln!(out, " for (; *end; end++) {{");
2243 let _ = writeln!(
2244 out,
2245 " if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2246 );
2247 let _ = writeln!(
2248 out,
2249 " if (*end == '\"') {{ in_str = !in_str; continue; }}"
2250 );
2251 let _ = writeln!(out, " if (in_str) continue;");
2252 let _ = writeln!(out, " if (*end == '[' || *end == '{{') depth++;");
2253 let _ = writeln!(
2254 out,
2255 " else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2256 );
2257 let _ = writeln!(out, " }}");
2258 let _ = writeln!(out, " size_t tlen = (size_t)(end - tc);");
2259 let _ = writeln!(out, " tool_calls_json = (char*)malloc(tlen + 1);");
2260 let _ = writeln!(out, " if (tool_calls_json != NULL) {{");
2261 let _ = writeln!(out, " memcpy(tool_calls_json, tc, tlen);");
2262 let _ = writeln!(out, " tool_calls_json[tlen] = '\\0';");
2263 let _ = writeln!(
2264 out,
2265 " const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2266 );
2267 let _ = writeln!(out, " if (fn != NULL) {{");
2268 let _ = writeln!(
2269 out,
2270 " const char* np = strstr(fn, \"\\\"name\\\":\");"
2271 );
2272 let _ = writeln!(out, " if (np != NULL) {{");
2273 let _ = writeln!(out, " np += 7;");
2274 let _ = writeln!(
2275 out,
2276 " while (*np == ' ' || *np == '\\t') np++;"
2277 );
2278 let _ = writeln!(out, " if (*np == '\"') {{");
2279 let _ = writeln!(out, " np++;");
2280 let _ = writeln!(out, " const char* ne = np;");
2281 let _ = writeln!(
2282 out,
2283 " while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2284 );
2285 let _ = writeln!(out, " size_t nlen = (size_t)(ne - np);");
2286 let _ = writeln!(
2287 out,
2288 " tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2289 );
2290 let _ = writeln!(
2291 out,
2292 " if (tool_calls_0_function_name != NULL) {{"
2293 );
2294 let _ = writeln!(
2295 out,
2296 " memcpy(tool_calls_0_function_name, np, nlen);"
2297 );
2298 let _ = writeln!(
2299 out,
2300 " tool_calls_0_function_name[nlen] = '\\0';"
2301 );
2302 let _ = writeln!(out, " }}");
2303 let _ = writeln!(out, " }}");
2304 let _ = writeln!(out, " }}");
2305 let _ = writeln!(out, " }}");
2306 let _ = writeln!(out, " }}");
2307 let _ = writeln!(out, " }}");
2308 let _ = writeln!(out, " }}");
2309 let _ = writeln!(out, " }}");
2310 let _ = writeln!(out);
2311
2312 for assertion in &fixture.assertions {
2313 emit_chat_stream_assertion(out, assertion);
2314 }
2315
2316 let _ = writeln!(out, " free(stream_content);");
2317 let _ = writeln!(
2318 out,
2319 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2320 );
2321 let _ = writeln!(out, " if (finish_reason != NULL) free(finish_reason);");
2322 let _ = writeln!(out, " if (tool_calls_json != NULL) free(tool_calls_json);");
2323 let _ = writeln!(
2324 out,
2325 " if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2326 );
2327 if request_var.is_some() {
2328 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2329 }
2330 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2331 let _ = writeln!(
2332 out,
2333 " /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2334 (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2335 );
2336 let _ = writeln!(out, "}}");
2337}
2338
2339fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2343 let field = assertion.field.as_deref().unwrap_or("");
2344
2345 enum Kind {
2346 IntCount,
2347 Bool,
2348 Str,
2349 IntTokens,
2350 Unsupported,
2351 }
2352
2353 let (expr, kind) = match field {
2354 "chunks" => ("chunks_count", Kind::IntCount),
2355 "stream_content" => ("stream_content", Kind::Str),
2356 "stream_complete" => ("stream_complete", Kind::Bool),
2357 "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2358 "finish_reason" => ("finish_reason", Kind::Str),
2359 "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2368 "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2369 _ => ("", Kind::Unsupported),
2370 };
2371
2372 let atype = assertion.assertion_type.as_str();
2373 if atype == "not_error" || atype == "error" {
2374 return;
2375 }
2376
2377 if matches!(kind, Kind::Unsupported) {
2378 let _ = writeln!(
2379 out,
2380 " /* skipped: streaming assertion on unsupported field '{field}' */"
2381 );
2382 return;
2383 }
2384
2385 match (atype, &kind) {
2386 ("count_min", Kind::IntCount) => {
2387 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2388 let _ = writeln!(out, " assert({expr} >= {n} && \"expected at least {n} chunks\");");
2389 }
2390 }
2391 ("equals", Kind::Str) => {
2392 if let Some(val) = &assertion.value {
2393 let c_val = json_to_c(val);
2394 let _ = writeln!(
2395 out,
2396 " assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2397 );
2398 }
2399 }
2400 ("contains", Kind::Str) => {
2401 if let Some(val) = &assertion.value {
2402 let c_val = json_to_c(val);
2403 let _ = writeln!(
2404 out,
2405 " assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2406 );
2407 }
2408 }
2409 ("not_empty", Kind::Str) => {
2410 let _ = writeln!(
2411 out,
2412 " assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2413 );
2414 }
2415 ("is_true", Kind::Bool) => {
2416 let _ = writeln!(out, " assert({expr} && \"expected {field} to be true\");");
2417 }
2418 ("is_false", Kind::Bool) => {
2419 let _ = writeln!(out, " assert(!{expr} && \"expected {field} to be false\");");
2420 }
2421 ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2422 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2423 let _ = writeln!(out, " assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2424 }
2425 }
2426 ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2427 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2428 let _ = writeln!(out, " assert({expr} == {n} && \"equals assertion failed\");");
2429 }
2430 }
2431 _ => {
2432 let _ = writeln!(
2433 out,
2434 " /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2435 );
2436 }
2437 }
2438}
2439
2440#[allow(clippy::too_many_arguments)]
2454fn emit_nested_accessor(
2455 out: &mut String,
2456 prefix: &str,
2457 resolved: &str,
2458 local_var: &str,
2459 result_var: &str,
2460 fields_c_types: &HashMap<String, String>,
2461 fields_enum: &HashSet<String>,
2462 intermediate_handles: &mut Vec<(String, String)>,
2463 result_type_name: &str,
2464 raw_field: &str,
2465) -> Option<String> {
2466 let segments: Vec<&str> = resolved.split('.').collect();
2467 let prefix_upper = prefix.to_uppercase();
2468
2469 let mut current_snake_type = result_type_name.to_snake_case();
2471 let mut current_handle = result_var.to_string();
2472 let mut json_extract_mode = false;
2475
2476 for (i, segment) in segments.iter().enumerate() {
2477 let is_leaf = i + 1 == segments.len();
2478
2479 if json_extract_mode {
2483 let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2488 Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2489 None => (segment, None),
2490 };
2491 let seg_snake = bare_segment.to_snake_case();
2492 if is_leaf {
2493 let _ = writeln!(
2494 out,
2495 " char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2496 );
2497 return None; }
2499 let json_var = format!("{seg_snake}_json");
2504 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2505 let _ = writeln!(
2506 out,
2507 " char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2508 );
2509 intermediate_handles.push((json_var.clone(), "free".to_string()));
2510 }
2511 if let Some(key) = bracket_key {
2515 if let Ok(idx) = key.parse::<usize>() {
2516 let elem_var = format!("{seg_snake}_{idx}_json");
2517 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2518 let _ = writeln!(
2519 out,
2520 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2521 );
2522 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2523 }
2524 current_handle = elem_var;
2525 continue;
2526 }
2527 }
2528 current_handle = json_var;
2529 continue;
2530 }
2531
2532 if let Some(bracket_pos) = segment.find('[') {
2534 let field_name = &segment[..bracket_pos];
2535 let key = segment[bracket_pos + 1..].trim_end_matches(']');
2536 let field_snake = field_name.to_snake_case();
2537 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2538
2539 let json_var = format!("{field_snake}_json");
2541 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2542 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2543 let _ = writeln!(out, " assert({json_var} != NULL);");
2544 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2546 }
2547
2548 if key.is_empty() {
2554 if !is_leaf {
2555 current_handle = json_var;
2556 json_extract_mode = true;
2557 continue;
2558 }
2559 return None;
2560 }
2561 if let Ok(idx) = key.parse::<usize>() {
2562 let elem_var = format!("{field_snake}_{idx}_json");
2563 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2564 let _ = writeln!(
2565 out,
2566 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2567 );
2568 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2569 }
2570 if !is_leaf {
2571 current_handle = elem_var;
2572 json_extract_mode = true;
2573 continue;
2574 }
2575 return None;
2577 }
2578
2579 let _ = writeln!(
2581 out,
2582 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
2583 );
2584 return None; }
2586
2587 let seg_snake = segment.to_snake_case();
2588 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
2589
2590 if is_leaf {
2591 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2594 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2595 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
2596 return Some(t.clone());
2597 }
2598 if try_emit_enum_accessor(
2600 out,
2601 prefix,
2602 &prefix_upper,
2603 raw_field,
2604 &seg_snake,
2605 ¤t_snake_type,
2606 &accessor_fn,
2607 ¤t_handle,
2608 local_var,
2609 fields_c_types,
2610 fields_enum,
2611 intermediate_handles,
2612 ) {
2613 return None;
2614 }
2615 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
2616 } else {
2617 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2619 let return_type_pascal = match fields_c_types.get(&lookup_key) {
2620 Some(t) => t.clone(),
2621 None => {
2622 segment.to_pascal_case()
2624 }
2625 };
2626
2627 if return_type_pascal == "char*" {
2630 let json_var = format!("{seg_snake}_json");
2631 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2632 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2633 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2634 }
2635 if i + 2 == segments.len() && segments[i + 1] == "length" {
2637 let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
2638 return Some("int".to_string());
2639 }
2640 current_snake_type = seg_snake.clone();
2641 current_handle = json_var;
2642 continue;
2643 }
2644
2645 let return_snake = return_type_pascal.to_snake_case();
2646 let handle_var = format!("{seg_snake}_handle");
2647
2648 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
2651 let _ = writeln!(
2652 out,
2653 " {prefix_upper}{return_type_pascal}* {handle_var} = \
2654 {accessor_fn}({current_handle});"
2655 );
2656 let _ = writeln!(out, " assert({handle_var} != NULL);");
2657 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
2658 }
2659
2660 current_snake_type = return_snake;
2661 current_handle = handle_var;
2662 }
2663 }
2664 None
2665}
2666
2667fn build_args_string_c(
2671 input: &serde_json::Value,
2672 args: &[crate::config::ArgMapping],
2673 has_options_handle: bool,
2674) -> String {
2675 if args.is_empty() {
2676 return json_to_c(input);
2677 }
2678
2679 let parts: Vec<String> = args
2680 .iter()
2681 .filter_map(|arg| {
2682 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2683 let val = input.get(field);
2684 match val {
2685 None if arg.optional => Some("NULL".to_string()),
2687 None => None,
2689 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
2691 Some(v) => {
2692 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
2695 Some("options_handle".to_string())
2696 } else {
2697 Some(json_to_c(v))
2698 }
2699 }
2700 }
2701 })
2702 .collect();
2703
2704 parts.join(", ")
2705}
2706
2707fn render_assertion(
2708 out: &mut String,
2709 assertion: &Assertion,
2710 result_var: &str,
2711 ffi_prefix: &str,
2712 _field_resolver: &FieldResolver,
2713 accessed_fields: &[(String, String, bool)],
2714 primitive_locals: &HashMap<String, String>,
2715) {
2716 if let Some(f) = &assertion.field {
2718 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
2719 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
2720 return;
2721 }
2722 }
2723
2724 let field_expr = match &assertion.field {
2725 Some(f) if !f.is_empty() => {
2726 accessed_fields
2728 .iter()
2729 .find(|(k, _, _)| k == f)
2730 .map(|(_, local, _)| local.clone())
2731 .unwrap_or_else(|| result_var.to_string())
2732 }
2733 _ => result_var.to_string(),
2734 };
2735
2736 let field_is_primitive = primitive_locals.contains_key(&field_expr);
2737 let field_primitive_type = primitive_locals.get(&field_expr).cloned();
2738 let field_is_map_access = if let Some(f) = &assertion.field {
2742 accessed_fields.iter().any(|(k, _, m)| k == f && *m)
2743 } else {
2744 false
2745 };
2746
2747 let assertion_field_is_optional = assertion
2751 .field
2752 .as_deref()
2753 .map(|f| {
2754 if f.is_empty() {
2755 return false;
2756 }
2757 if _field_resolver.is_optional(f) {
2758 return true;
2759 }
2760 let resolved = _field_resolver.resolve(f);
2762 _field_resolver.is_optional(resolved)
2763 })
2764 .unwrap_or(false);
2765
2766 match assertion.assertion_type.as_str() {
2767 "equals" => {
2768 if let Some(expected) = &assertion.value {
2769 let c_val = json_to_c(expected);
2770 if field_is_primitive {
2771 let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
2772 match expected.as_bool() {
2773 Some(true) => "1".to_string(),
2774 Some(false) => "0".to_string(),
2775 None => c_val,
2776 }
2777 } else {
2778 c_val
2779 };
2780 let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
2783 if assertion_field_is_optional && is_numeric {
2784 let _ = writeln!(
2785 out,
2786 " assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
2787 );
2788 } else {
2789 let _ = writeln!(
2790 out,
2791 " assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
2792 );
2793 }
2794 } else if expected.is_string() {
2795 let _ = writeln!(
2796 out,
2797 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2798 );
2799 } else if field_is_map_access && expected.is_boolean() {
2800 let lit = match expected.as_bool() {
2801 Some(true) => "\"true\"",
2802 _ => "\"false\"",
2803 };
2804 let _ = writeln!(
2805 out,
2806 " assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
2807 );
2808 } else if field_is_map_access && expected.is_number() {
2809 if expected.is_f64() {
2810 let _ = writeln!(
2811 out,
2812 " assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
2813 );
2814 } else {
2815 let _ = writeln!(
2816 out,
2817 " assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
2818 );
2819 }
2820 } else {
2821 let _ = writeln!(
2822 out,
2823 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2824 );
2825 }
2826 }
2827 }
2828 "contains" => {
2829 if let Some(expected) = &assertion.value {
2830 let c_val = json_to_c(expected);
2831 let _ = writeln!(
2832 out,
2833 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2834 );
2835 }
2836 }
2837 "contains_all" => {
2838 if let Some(values) = &assertion.values {
2839 for val in values {
2840 let c_val = json_to_c(val);
2841 let _ = writeln!(
2842 out,
2843 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2844 );
2845 }
2846 }
2847 }
2848 "not_contains" => {
2849 if let Some(expected) = &assertion.value {
2850 let c_val = json_to_c(expected);
2851 let _ = writeln!(
2852 out,
2853 " assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
2854 );
2855 }
2856 }
2857 "not_empty" => {
2858 let _ = writeln!(
2859 out,
2860 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
2861 );
2862 }
2863 "is_empty" => {
2864 if assertion_field_is_optional || !field_is_primitive {
2865 let _ = writeln!(
2867 out,
2868 " assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
2869 );
2870 } else {
2871 let _ = writeln!(
2872 out,
2873 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
2874 );
2875 }
2876 }
2877 "contains_any" => {
2878 if let Some(values) = &assertion.values {
2879 let _ = writeln!(out, " {{");
2880 let _ = writeln!(out, " int found = 0;");
2881 for val in values {
2882 let c_val = json_to_c(val);
2883 let _ = writeln!(
2884 out,
2885 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
2886 );
2887 }
2888 let _ = writeln!(
2889 out,
2890 " assert(found && \"expected to contain at least one of the specified values\");"
2891 );
2892 let _ = writeln!(out, " }}");
2893 }
2894 }
2895 "greater_than" => {
2896 if let Some(val) = &assertion.value {
2897 let c_val = json_to_c(val);
2898 if field_is_map_access && val.is_number() && !field_is_primitive {
2899 let _ = writeln!(
2900 out,
2901 " assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
2902 );
2903 } else {
2904 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
2905 }
2906 }
2907 }
2908 "less_than" => {
2909 if let Some(val) = &assertion.value {
2910 let c_val = json_to_c(val);
2911 if field_is_map_access && val.is_number() && !field_is_primitive {
2912 let _ = writeln!(
2913 out,
2914 " assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
2915 );
2916 } else {
2917 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
2918 }
2919 }
2920 }
2921 "greater_than_or_equal" => {
2922 if let Some(val) = &assertion.value {
2923 let c_val = json_to_c(val);
2924 if field_is_map_access && val.is_number() && !field_is_primitive {
2925 let _ = writeln!(
2926 out,
2927 " assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
2928 );
2929 } else {
2930 let _ = writeln!(
2931 out,
2932 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
2933 );
2934 }
2935 }
2936 }
2937 "less_than_or_equal" => {
2938 if let Some(val) = &assertion.value {
2939 let c_val = json_to_c(val);
2940 if field_is_map_access && val.is_number() && !field_is_primitive {
2941 let _ = writeln!(
2942 out,
2943 " assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
2944 );
2945 } else {
2946 let _ = writeln!(
2947 out,
2948 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
2949 );
2950 }
2951 }
2952 }
2953 "starts_with" => {
2954 if let Some(expected) = &assertion.value {
2955 let c_val = json_to_c(expected);
2956 let _ = writeln!(
2957 out,
2958 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
2959 );
2960 }
2961 }
2962 "ends_with" => {
2963 if let Some(expected) = &assertion.value {
2964 let c_val = json_to_c(expected);
2965 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
2966 let _ = writeln!(
2967 out,
2968 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
2969 );
2970 }
2971 }
2972 "min_length" => {
2973 if let Some(val) = &assertion.value {
2974 if let Some(n) = val.as_u64() {
2975 let _ = writeln!(
2976 out,
2977 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
2978 );
2979 }
2980 }
2981 }
2982 "max_length" => {
2983 if let Some(val) = &assertion.value {
2984 if let Some(n) = val.as_u64() {
2985 let _ = writeln!(
2986 out,
2987 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
2988 );
2989 }
2990 }
2991 }
2992 "count_min" => {
2993 if let Some(val) = &assertion.value {
2994 if let Some(n) = val.as_u64() {
2995 let _ = writeln!(out, " {{");
2996 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
2997 let _ = writeln!(
2998 out,
2999 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3000 );
3001 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3002 let _ = writeln!(
3003 out,
3004 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
3005 );
3006 let _ = writeln!(out, " }}");
3007 }
3008 }
3009 }
3010 "count_equals" => {
3011 if let Some(val) = &assertion.value {
3012 if let Some(n) = val.as_u64() {
3013 let _ = writeln!(out, " {{");
3014 let _ = writeln!(out, " /* count_equals: count elements in array */");
3015 let _ = writeln!(
3016 out,
3017 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3018 );
3019 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3020 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
3021 let _ = writeln!(out, " }}");
3022 }
3023 }
3024 }
3025 "is_true" => {
3026 let _ = writeln!(out, " assert({field_expr});");
3027 }
3028 "is_false" => {
3029 let _ = writeln!(out, " assert(!{field_expr});");
3030 }
3031 "method_result" => {
3032 if let Some(method_name) = &assertion.method {
3033 render_method_result_assertion(
3034 out,
3035 result_var,
3036 ffi_prefix,
3037 method_name,
3038 assertion.args.as_ref(),
3039 assertion.return_type.as_deref(),
3040 assertion.check.as_deref().unwrap_or("is_true"),
3041 assertion.value.as_ref(),
3042 );
3043 } else {
3044 panic!("C e2e generator: method_result assertion missing 'method' field");
3045 }
3046 }
3047 "matches_regex" => {
3048 if let Some(expected) = &assertion.value {
3049 let c_val = json_to_c(expected);
3050 let _ = writeln!(out, " {{");
3051 let _ = writeln!(out, " regex_t _re;");
3052 let _ = writeln!(
3053 out,
3054 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3055 );
3056 let _ = writeln!(
3057 out,
3058 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3059 );
3060 let _ = writeln!(out, " regfree(&_re);");
3061 let _ = writeln!(out, " }}");
3062 }
3063 }
3064 "not_error" => {
3065 }
3067 "error" => {
3068 }
3070 other => {
3071 panic!("C e2e generator: unsupported assertion type: {other}");
3072 }
3073 }
3074}
3075
3076#[allow(clippy::too_many_arguments)]
3085fn render_method_result_assertion(
3086 out: &mut String,
3087 result_var: &str,
3088 ffi_prefix: &str,
3089 method_name: &str,
3090 args: Option<&serde_json::Value>,
3091 return_type: Option<&str>,
3092 check: &str,
3093 value: Option<&serde_json::Value>,
3094) {
3095 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3096
3097 if return_type == Some("string") {
3098 let _ = writeln!(out, " {{");
3100 let _ = writeln!(out, " char* _method_result = {call_expr};");
3101 if check == "is_error" {
3102 let _ = writeln!(
3103 out,
3104 " assert(_method_result == NULL && \"expected method to return error\");"
3105 );
3106 let _ = writeln!(out, " }}");
3107 return;
3108 }
3109 let _ = writeln!(
3110 out,
3111 " assert(_method_result != NULL && \"method_result returned NULL\");"
3112 );
3113 match check {
3114 "contains" => {
3115 if let Some(val) = value {
3116 let c_val = json_to_c(val);
3117 let _ = writeln!(
3118 out,
3119 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3120 );
3121 }
3122 }
3123 "equals" => {
3124 if let Some(val) = value {
3125 let c_val = json_to_c(val);
3126 let _ = writeln!(
3127 out,
3128 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3129 );
3130 }
3131 }
3132 "is_true" => {
3133 let _ = writeln!(
3134 out,
3135 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3136 );
3137 }
3138 "count_min" => {
3139 if let Some(val) = value {
3140 let n = val.as_u64().unwrap_or(0);
3141 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
3142 let _ = writeln!(
3143 out,
3144 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3145 );
3146 }
3147 }
3148 other_check => {
3149 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3150 }
3151 }
3152 let _ = writeln!(out, " free(_method_result);");
3153 let _ = writeln!(out, " }}");
3154 return;
3155 }
3156
3157 match check {
3159 "equals" => {
3160 if let Some(val) = value {
3161 let c_val = json_to_c(val);
3162 let _ = writeln!(
3163 out,
3164 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3165 );
3166 }
3167 }
3168 "is_true" => {
3169 let _ = writeln!(
3170 out,
3171 " assert({call_expr} && \"method_result is_true assertion failed\");"
3172 );
3173 }
3174 "is_false" => {
3175 let _ = writeln!(
3176 out,
3177 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
3178 );
3179 }
3180 "greater_than_or_equal" => {
3181 if let Some(val) = value {
3182 let n = val.as_u64().unwrap_or(0);
3183 let _ = writeln!(
3184 out,
3185 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3186 );
3187 }
3188 }
3189 "count_min" => {
3190 if let Some(val) = value {
3191 let n = val.as_u64().unwrap_or(0);
3192 let _ = writeln!(
3193 out,
3194 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3195 );
3196 }
3197 }
3198 other_check => {
3199 panic!("C e2e generator: unsupported method_result check type: {other_check}");
3200 }
3201 }
3202}
3203
3204fn build_c_method_call(
3211 result_var: &str,
3212 ffi_prefix: &str,
3213 method_name: &str,
3214 args: Option<&serde_json::Value>,
3215) -> String {
3216 let extra_args = if let Some(args_val) = args {
3217 args_val
3218 .as_object()
3219 .map(|obj| {
3220 obj.values()
3221 .map(|v| match v {
3222 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3223 serde_json::Value::Bool(true) => "1".to_string(),
3224 serde_json::Value::Bool(false) => "0".to_string(),
3225 serde_json::Value::Number(n) => n.to_string(),
3226 serde_json::Value::Null => "NULL".to_string(),
3227 other => format!("\"{}\"", escape_c(&other.to_string())),
3228 })
3229 .collect::<Vec<_>>()
3230 .join(", ")
3231 })
3232 .unwrap_or_default()
3233 } else {
3234 String::new()
3235 };
3236
3237 if extra_args.is_empty() {
3238 format!("{ffi_prefix}_{method_name}({result_var})")
3239 } else {
3240 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3241 }
3242}
3243
3244fn json_to_c(value: &serde_json::Value) -> String {
3246 match value {
3247 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3248 serde_json::Value::Bool(true) => "1".to_string(),
3249 serde_json::Value::Bool(false) => "0".to_string(),
3250 serde_json::Value::Number(n) => n.to_string(),
3251 serde_json::Value::Null => "NULL".to_string(),
3252 other => format!("\"{}\"", escape_c(&other.to_string())),
3253 }
3254}