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 let has_mock = fixture.needs_mock_server();
965 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
966 if let Some(env) = &fixture.env {
967 if let Some(var) = &env.api_key_var {
968 let fixture_id = &fixture.id;
969 if has_mock {
970 let _ = writeln!(out, " const char* api_key = getenv(\"{var}\");");
971 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
972 let _ = writeln!(out, " char base_url_buf[512];");
973 let _ = writeln!(out, " if (api_key && api_key[0] != '\\0') {{");
974 let _ = writeln!(
975 out,
976 " fprintf(stderr, \"{fixture_id}: using real API ({var} is set)\\n\");"
977 );
978 let _ = writeln!(out, " }} else {{");
979 let _ = writeln!(
980 out,
981 " fprintf(stderr, \"{fixture_id}: using mock server ({var} not set)\\n\");"
982 );
983 let _ = writeln!(
984 out,
985 " snprintf(base_url_buf, sizeof(base_url_buf), \"%s/fixtures/{fixture_id}\", mock_base ? mock_base : \"\");"
986 );
987 let _ = writeln!(out, " api_key = \"test-key\";");
988 let _ = writeln!(out, " }}");
989 } else {
990 let _ = writeln!(out, " if (getenv(\"{var}\") == NULL) {{ return; }}");
991 }
992 }
993 }
994
995 let prefix_upper = prefix.to_uppercase();
996
997 if let Some(config_type) = c_engine_factory {
1001 render_engine_factory_test_function(
1002 out,
1003 fixture,
1004 prefix,
1005 function_name,
1006 result_var,
1007 field_resolver,
1008 fields_c_types,
1009 fields_enum,
1010 result_type_name,
1011 config_type,
1012 expects_error,
1013 );
1014 return;
1015 }
1016
1017 if client_factory.is_some() && function_name == "chat_stream" {
1023 render_chat_stream_test_function(
1024 out,
1025 fixture,
1026 prefix,
1027 result_var,
1028 args,
1029 options_type_name,
1030 expects_error,
1031 api_key_var,
1032 );
1033 return;
1034 }
1035
1036 if let Some(factory) = client_factory {
1044 if result_is_bytes {
1045 render_bytes_test_function(
1046 out,
1047 fixture,
1048 prefix,
1049 function_name,
1050 result_var,
1051 args,
1052 options_type_name,
1053 result_type_name,
1054 factory,
1055 expects_error,
1056 );
1057 return;
1058 }
1059 }
1060
1061 if let Some(factory) = client_factory {
1066 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); let mut inline_method_args: Vec<String> = Vec::new();
1071
1072 for arg in args {
1073 if arg.arg_type == "json_object" {
1074 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1079 options_type_name.to_string()
1080 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1081 format!("{}Request", stripped)
1082 } else {
1083 format!("{result_type_name}Request")
1084 };
1085 let request_type_snake = request_type_pascal.to_snake_case();
1086 let var_name = format!("{request_type_snake}_handle");
1087
1088 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1089 let json_val = if field.is_empty() || field == "input" {
1090 Some(&fixture.input)
1091 } else {
1092 fixture.input.get(field)
1093 };
1094
1095 if let Some(val) = json_val {
1096 if !val.is_null() {
1097 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1098 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1099 let escaped = escape_c(&json_str);
1100 let _ = writeln!(
1101 out,
1102 " {prefix_upper}{request_type_pascal}* {var_name} = \
1103 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1104 );
1105 if expects_error {
1106 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
1114 } else {
1115 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1116 }
1117 request_handle_vars.push((arg.name.clone(), var_name));
1118 }
1119 }
1120 } else if arg.arg_type == "string" {
1121 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1123 let val = fixture.input.get(field);
1124 match val {
1125 Some(v) if v.is_string() => {
1126 let s = v.as_str().unwrap_or_default();
1127 let escaped = escape_c(s);
1128 inline_method_args.push(format!("\"{escaped}\""));
1129 }
1130 Some(serde_json::Value::Null) | None if arg.optional => {
1131 inline_method_args.push("NULL".to_string());
1132 }
1133 None => {
1134 inline_method_args.push("\"\"".to_string());
1135 }
1136 Some(other) => {
1137 let s = serde_json::to_string(other).unwrap_or_default();
1138 let escaped = escape_c(&s);
1139 inline_method_args.push(format!("\"{escaped}\""));
1140 }
1141 }
1142 } else if arg.optional {
1143 inline_method_args.push("NULL".to_string());
1145 }
1146 }
1147
1148 let fixture_id = &fixture.id;
1149 if has_mock && api_key_var.is_some() {
1154 let _ = writeln!(
1158 out,
1159 " const char* _base_url_arg = (api_key && api_key[0] != '\\0') ? NULL : base_url_buf;"
1160 );
1161 let _ = writeln!(
1162 out,
1163 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
1164 );
1165 } else if has_mock {
1166 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1167 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1168 let _ = writeln!(out, " char base_url[1024];");
1169 let _ = writeln!(
1170 out,
1171 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1172 );
1173 let _ = writeln!(
1174 out,
1175 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1176 );
1177 } else {
1178 let _ = writeln!(
1179 out,
1180 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1181 );
1182 }
1183 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1184
1185 let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
1186 String::new()
1187 } else {
1188 let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
1189 let parts: Vec<String> = handles
1190 .into_iter()
1191 .chain(inline_method_args.iter().cloned())
1192 .chain(extra_args.iter().cloned())
1193 .collect();
1194 format!(", {}", parts.join(", "))
1195 };
1196
1197 let call_fn = format!("{prefix}_default_client_{function_name}");
1198
1199 if expects_error {
1200 let _ = writeln!(
1201 out,
1202 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1203 );
1204 for (_, var_name) in &request_handle_vars {
1205 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1206 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1207 }
1208 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1209 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1210 let _ = writeln!(out, "}}");
1211 return;
1212 }
1213
1214 let _ = writeln!(
1215 out,
1216 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1217 );
1218 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1219
1220 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1221 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1222 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1225
1226 for assertion in &fixture.assertions {
1227 if let Some(f) = &assertion.field {
1228 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1229 let resolved = field_resolver.resolve(f);
1230 let local_var = f.replace(['.', '['], "_").replace(']', "");
1231 let has_map_access = resolved.contains('[');
1232 if resolved.contains('.') {
1233 let leaf_primitive = emit_nested_accessor(
1234 out,
1235 prefix,
1236 resolved,
1237 &local_var,
1238 result_var,
1239 fields_c_types,
1240 fields_enum,
1241 &mut intermediate_handles,
1242 result_type_name,
1243 f,
1244 );
1245 if let Some(prim) = leaf_primitive {
1246 primitive_locals.insert(local_var.clone(), prim);
1247 }
1248 } else {
1249 let result_type_snake = result_type_name.to_snake_case();
1250 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1251 let lookup_key = format!("{result_type_snake}.{resolved}");
1252 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1253 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1254 primitive_locals.insert(local_var.clone(), t.clone());
1255 } else if try_emit_enum_accessor(
1256 out,
1257 prefix,
1258 &prefix_upper,
1259 f,
1260 resolved,
1261 &result_type_snake,
1262 &accessor_fn,
1263 result_var,
1264 &local_var,
1265 fields_c_types,
1266 fields_enum,
1267 &mut intermediate_handles,
1268 ) {
1269 } else {
1271 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1272 }
1273 }
1274 accessed_fields.push((f.clone(), local_var, has_map_access));
1275 }
1276 }
1277 }
1278
1279 for assertion in &fixture.assertions {
1280 render_assertion(
1281 out,
1282 assertion,
1283 result_var,
1284 prefix,
1285 field_resolver,
1286 &accessed_fields,
1287 &primitive_locals,
1288 );
1289 }
1290
1291 for (_f, local_var, from_json) in &accessed_fields {
1292 if primitive_locals.contains_key(local_var) {
1293 continue;
1294 }
1295 if *from_json {
1296 let _ = writeln!(out, " free({local_var});");
1297 } else {
1298 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1299 }
1300 }
1301 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1302 if snake_type == "free_string" {
1303 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1304 } else if snake_type == "free" {
1305 let _ = writeln!(out, " free({handle_var});");
1308 } else {
1309 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1310 }
1311 }
1312 let result_type_snake = result_type_name.to_snake_case();
1313 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1314 for (_, var_name) in &request_handle_vars {
1315 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1316 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1317 }
1318 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1319 let _ = writeln!(out, "}}");
1320 return;
1321 }
1322
1323 if let Some(raw_type) = raw_c_result_type {
1326 let args_str = if args.is_empty() {
1328 String::new()
1329 } else {
1330 let parts: Vec<String> = args
1331 .iter()
1332 .filter_map(|arg| {
1333 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1334 let val = fixture.input.get(field);
1335 match val {
1336 None if arg.optional => Some("NULL".to_string()),
1337 None => None,
1338 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1339 Some(v) => Some(json_to_c(v)),
1340 }
1341 })
1342 .collect();
1343 parts.join(", ")
1344 };
1345
1346 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
1348
1349 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
1351 if has_not_error {
1352 match raw_type {
1353 "char*" if !result_is_option => {
1354 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1355 }
1356 "int32_t" => {
1357 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
1358 }
1359 "uintptr_t" => {
1360 let _ = writeln!(
1361 out,
1362 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
1363 );
1364 }
1365 _ => {}
1366 }
1367 }
1368
1369 for assertion in &fixture.assertions {
1371 match assertion.assertion_type.as_str() {
1372 "not_error" | "error" => {} "not_empty" => {
1374 let _ = writeln!(
1375 out,
1376 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
1377 );
1378 }
1379 "is_empty" => {
1380 if result_is_option && raw_type == "char*" {
1381 let _ = writeln!(
1382 out,
1383 " assert({result_var} == NULL && \"expected empty/null value\");"
1384 );
1385 } else {
1386 let _ = writeln!(
1387 out,
1388 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
1389 );
1390 }
1391 }
1392 "count_min" => {
1393 if let Some(val) = &assertion.value {
1394 if let Some(n) = val.as_u64() {
1395 match raw_type {
1396 "char*" => {
1397 let _ = writeln!(out, " {{");
1398 let _ = writeln!(
1399 out,
1400 " assert({result_var} != NULL && \"expected non-null JSON array\");"
1401 );
1402 let _ =
1403 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
1404 let _ = writeln!(
1405 out,
1406 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1407 );
1408 let _ = writeln!(out, " }}");
1409 }
1410 _ => {
1411 let _ = writeln!(
1412 out,
1413 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1414 );
1415 }
1416 }
1417 }
1418 }
1419 }
1420 "greater_than_or_equal" => {
1421 if let Some(val) = &assertion.value {
1422 let c_val = json_to_c(val);
1423 let _ = writeln!(
1424 out,
1425 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1426 );
1427 }
1428 }
1429 "contains" => {
1430 if let Some(val) = &assertion.value {
1431 let c_val = json_to_c(val);
1432 let _ = writeln!(
1433 out,
1434 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1435 );
1436 }
1437 }
1438 "contains_all" => {
1439 if let Some(values) = &assertion.values {
1440 for val in values {
1441 let c_val = json_to_c(val);
1442 let _ = writeln!(
1443 out,
1444 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1445 );
1446 }
1447 }
1448 }
1449 "equals" => {
1450 if let Some(val) = &assertion.value {
1451 let c_val = json_to_c(val);
1452 if val.is_string() {
1453 let _ = writeln!(
1454 out,
1455 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1456 );
1457 } else {
1458 let _ = writeln!(
1459 out,
1460 " assert({result_var} == {c_val} && \"equals assertion failed\");"
1461 );
1462 }
1463 }
1464 }
1465 "not_contains" => {
1466 if let Some(val) = &assertion.value {
1467 let c_val = json_to_c(val);
1468 let _ = writeln!(
1469 out,
1470 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1471 );
1472 }
1473 }
1474 "starts_with" => {
1475 if let Some(val) = &assertion.value {
1476 let c_val = json_to_c(val);
1477 let _ = writeln!(
1478 out,
1479 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1480 );
1481 }
1482 }
1483 "is_true" => {
1484 let _ = writeln!(out, " assert({result_var});");
1485 }
1486 "is_false" => {
1487 let _ = writeln!(out, " assert(!{result_var});");
1488 }
1489 other => {
1490 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1491 }
1492 }
1493 }
1494
1495 if raw_type == "char*" {
1497 let free_fn = c_free_fn
1498 .map(|s| s.to_string())
1499 .unwrap_or_else(|| format!("{prefix}_free_string"));
1500 if result_is_option {
1501 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1502 } else {
1503 let _ = writeln!(out, " {free_fn}({result_var});");
1504 }
1505 }
1506
1507 let _ = writeln!(out, "}}");
1508 return;
1509 }
1510
1511 let prefixed_fn = function_name.to_string();
1517
1518 let mut has_options_handle = false;
1520 for arg in args {
1521 if arg.arg_type == "json_object" {
1522 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1523 if let Some(val) = fixture.input.get(field) {
1524 if !val.is_null() {
1525 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1529 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1530 let escaped = escape_c(&json_str);
1531 let upper = prefix.to_uppercase();
1532 let options_type_pascal = options_type_name;
1533 let options_type_snake = options_type_name.to_snake_case();
1534 let _ = writeln!(
1535 out,
1536 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1537 );
1538 has_options_handle = true;
1539 }
1540 }
1541 }
1542 }
1543
1544 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1545
1546 if expects_error {
1547 let _ = writeln!(
1548 out,
1549 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1550 );
1551 if has_options_handle {
1552 let options_type_snake = options_type_name.to_snake_case();
1553 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1554 }
1555 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1556 let _ = writeln!(out, "}}");
1557 return;
1558 }
1559
1560 let _ = writeln!(
1562 out,
1563 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1564 );
1565 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1566
1567 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1575 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1578 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1580
1581 for assertion in &fixture.assertions {
1582 if let Some(f) = &assertion.field {
1583 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1584 let resolved = field_resolver.resolve(f);
1585 let local_var = f.replace(['.', '['], "_").replace(']', "");
1586 let has_map_access = resolved.contains('[');
1587
1588 if resolved.contains('.') {
1589 let leaf_primitive = emit_nested_accessor(
1590 out,
1591 prefix,
1592 resolved,
1593 &local_var,
1594 result_var,
1595 fields_c_types,
1596 fields_enum,
1597 &mut intermediate_handles,
1598 result_type_name,
1599 f,
1600 );
1601 if let Some(prim) = leaf_primitive {
1602 primitive_locals.insert(local_var.clone(), prim);
1603 }
1604 } else {
1605 let result_type_snake = result_type_name.to_snake_case();
1606 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1607 let lookup_key = format!("{result_type_snake}.{resolved}");
1608 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1609 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1610 primitive_locals.insert(local_var.clone(), t.clone());
1611 } else if try_emit_enum_accessor(
1612 out,
1613 prefix,
1614 &prefix_upper,
1615 f,
1616 resolved,
1617 &result_type_snake,
1618 &accessor_fn,
1619 result_var,
1620 &local_var,
1621 fields_c_types,
1622 fields_enum,
1623 &mut intermediate_handles,
1624 ) {
1625 } else {
1627 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1628 }
1629 }
1630 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1631 }
1632 }
1633 }
1634
1635 for assertion in &fixture.assertions {
1636 render_assertion(
1637 out,
1638 assertion,
1639 result_var,
1640 prefix,
1641 field_resolver,
1642 &accessed_fields,
1643 &primitive_locals,
1644 );
1645 }
1646
1647 for (_f, local_var, from_json) in &accessed_fields {
1649 if primitive_locals.contains_key(local_var) {
1650 continue;
1651 }
1652 if *from_json {
1653 let _ = writeln!(out, " free({local_var});");
1654 } else {
1655 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1656 }
1657 }
1658 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1660 if snake_type == "free_string" {
1661 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1663 } else {
1664 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1665 }
1666 }
1667 if has_options_handle {
1668 let options_type_snake = options_type_name.to_snake_case();
1669 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1670 }
1671 let result_type_snake = result_type_name.to_snake_case();
1672 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1673 let _ = writeln!(out, "}}");
1674}
1675
1676#[allow(clippy::too_many_arguments)]
1685fn render_engine_factory_test_function(
1686 out: &mut String,
1687 fixture: &Fixture,
1688 prefix: &str,
1689 function_name: &str,
1690 result_var: &str,
1691 field_resolver: &FieldResolver,
1692 fields_c_types: &HashMap<String, String>,
1693 fields_enum: &HashSet<String>,
1694 result_type_name: &str,
1695 config_type: &str,
1696 _expects_error: bool,
1697) {
1698 let prefix_upper = prefix.to_uppercase();
1699 let config_snake = config_type.to_snake_case();
1700
1701 let config_val = fixture.input.get("config");
1703 let config_json = match config_val {
1704 Some(v) if !v.is_null() => {
1705 let normalized = super::transform_json_keys_for_language(v, "snake_case");
1706 serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1707 }
1708 _ => "{}".to_string(),
1709 };
1710 let config_escaped = escape_c(&config_json);
1711 let fixture_id = &fixture.id;
1712
1713 let has_active_assertions = fixture.assertions.iter().any(|a| {
1717 if let Some(f) = &a.field {
1718 !f.is_empty() && field_resolver.is_valid_for_result(f)
1719 } else {
1720 false
1721 }
1722 });
1723
1724 let _ = writeln!(
1726 out,
1727 " {prefix_upper}{config_type}* config_handle = \
1728 {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1729 );
1730 let _ = writeln!(out, " assert(config_handle != NULL && \"failed to parse config\");");
1731 let _ = writeln!(
1732 out,
1733 " {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1734 );
1735 let _ = writeln!(out, " {prefix}_{config_snake}_free(config_handle);");
1736 let _ = writeln!(out, " assert(engine != NULL && \"failed to create engine\");");
1737
1738 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1740 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1741 let _ = writeln!(out, " char url[2048];");
1742 let _ = writeln!(
1743 out,
1744 " snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
1745 );
1746
1747 let _ = writeln!(
1749 out,
1750 " {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url);"
1751 );
1752
1753 if !has_active_assertions {
1756 let result_type_snake = result_type_name.to_snake_case();
1757 let _ = writeln!(
1758 out,
1759 " if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
1760 );
1761 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1762 let _ = writeln!(out, "}}");
1763 return;
1764 }
1765
1766 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1767
1768 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1770 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1771 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1772
1773 for assertion in &fixture.assertions {
1774 if let Some(f) = &assertion.field {
1775 if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
1776 {
1777 let resolved = field_resolver.resolve(f);
1778 let local_var = f.replace(['.', '['], "_").replace(']', "");
1779 let has_map_access = resolved.contains('[');
1780 if resolved.contains('.') {
1781 let leaf_primitive = emit_nested_accessor(
1782 out,
1783 prefix,
1784 resolved,
1785 &local_var,
1786 result_var,
1787 fields_c_types,
1788 fields_enum,
1789 &mut intermediate_handles,
1790 result_type_name,
1791 f,
1792 );
1793 if let Some(prim) = leaf_primitive {
1794 primitive_locals.insert(local_var.clone(), prim);
1795 }
1796 } else {
1797 let result_type_snake = result_type_name.to_snake_case();
1798 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1799 let lookup_key = format!("{result_type_snake}.{resolved}");
1800 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1801 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1802 primitive_locals.insert(local_var.clone(), t.clone());
1803 } else if try_emit_enum_accessor(
1804 out,
1805 prefix,
1806 &prefix_upper,
1807 f,
1808 resolved,
1809 &result_type_snake,
1810 &accessor_fn,
1811 result_var,
1812 &local_var,
1813 fields_c_types,
1814 fields_enum,
1815 &mut intermediate_handles,
1816 ) {
1817 } else {
1819 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1820 }
1821 }
1822 accessed_fields.push((f.clone(), local_var, has_map_access));
1823 }
1824 }
1825 }
1826
1827 for assertion in &fixture.assertions {
1828 render_assertion(
1829 out,
1830 assertion,
1831 result_var,
1832 prefix,
1833 field_resolver,
1834 &accessed_fields,
1835 &primitive_locals,
1836 );
1837 }
1838
1839 for (_f, local_var, from_json) in &accessed_fields {
1841 if primitive_locals.contains_key(local_var) {
1842 continue;
1843 }
1844 if *from_json {
1845 let _ = writeln!(out, " free({local_var});");
1846 } else {
1847 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1848 }
1849 }
1850 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1851 if snake_type == "free_string" {
1852 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1853 } else {
1854 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1855 }
1856 }
1857
1858 let result_type_snake = result_type_name.to_snake_case();
1859 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1860 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1861 let _ = writeln!(out, "}}");
1862}
1863
1864#[allow(clippy::too_many_arguments)]
1886fn render_bytes_test_function(
1887 out: &mut String,
1888 fixture: &Fixture,
1889 prefix: &str,
1890 function_name: &str,
1891 _result_var: &str,
1892 args: &[crate::config::ArgMapping],
1893 options_type_name: &str,
1894 result_type_name: &str,
1895 factory: &str,
1896 expects_error: bool,
1897) {
1898 let prefix_upper = prefix.to_uppercase();
1899 let mut request_handle_vars: Vec<(String, String)> = Vec::new();
1900 let mut string_arg_exprs: Vec<String> = Vec::new();
1901
1902 for arg in args {
1903 match arg.arg_type.as_str() {
1904 "json_object" => {
1905 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1906 options_type_name.to_string()
1907 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1908 format!("{}Request", stripped)
1909 } else {
1910 format!("{result_type_name}Request")
1911 };
1912 let request_type_snake = request_type_pascal.to_snake_case();
1913 let var_name = format!("{request_type_snake}_handle");
1914
1915 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1916 let json_val = if field.is_empty() || field == "input" {
1917 Some(&fixture.input)
1918 } else {
1919 fixture.input.get(field)
1920 };
1921
1922 if let Some(val) = json_val {
1923 if !val.is_null() {
1924 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1925 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1926 let escaped = escape_c(&json_str);
1927 let _ = writeln!(
1928 out,
1929 " {prefix_upper}{request_type_pascal}* {var_name} = \
1930 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1931 );
1932 if expects_error {
1933 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
1941 } else {
1942 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1943 }
1944 request_handle_vars.push((arg.name.clone(), var_name));
1945 }
1946 }
1947 }
1948 "string" => {
1949 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1952 let val = fixture.input.get(field);
1953 let expr = match val {
1954 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
1955 Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
1956 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
1957 None => "NULL".to_string(),
1958 };
1959 string_arg_exprs.push(expr);
1960 }
1961 _ => {
1962 string_arg_exprs.push("NULL".to_string());
1965 }
1966 }
1967 }
1968
1969 let fixture_id = &fixture.id;
1970 if fixture.needs_mock_server() {
1971 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1972 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1973 let _ = writeln!(out, " char base_url[1024];");
1974 let _ = writeln!(
1975 out,
1976 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1977 );
1978 let _ = writeln!(
1983 out,
1984 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1985 );
1986 } else {
1987 let _ = writeln!(
1988 out,
1989 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1990 );
1991 }
1992 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1993
1994 let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
1996 let _ = writeln!(out, " uintptr_t out_len = 0;");
1997 let _ = writeln!(out, " uintptr_t out_cap = 0;");
1998
1999 let mut method_args: Vec<String> = Vec::new();
2001 for (_, v) in &request_handle_vars {
2002 method_args.push(v.clone());
2003 }
2004 method_args.extend(string_arg_exprs.iter().cloned());
2005 let extra_args = if method_args.is_empty() {
2006 String::new()
2007 } else {
2008 format!(", {}", method_args.join(", "))
2009 };
2010
2011 let call_fn = format!("{prefix}_default_client_{function_name}");
2012 let _ = writeln!(
2013 out,
2014 " int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
2015 );
2016
2017 if expects_error {
2018 for (_, var_name) in &request_handle_vars {
2019 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2020 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2021 }
2022 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2023 let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
2024 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2027 let _ = writeln!(out, "}}");
2028 return;
2029 }
2030
2031 let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
2032
2033 let mut emitted_len_check = false;
2038 for assertion in &fixture.assertions {
2039 match assertion.assertion_type.as_str() {
2040 "not_error" => {
2041 }
2043 "not_empty" | "not_null" => {
2044 if !emitted_len_check {
2045 let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
2046 emitted_len_check = true;
2047 }
2048 }
2049 _ => {
2050 let _ = writeln!(
2054 out,
2055 " /* skipped: assertion '{}' not meaningful on raw byte buffer */",
2056 assertion.assertion_type
2057 );
2058 }
2059 }
2060 }
2061
2062 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2063 for (_, var_name) in &request_handle_vars {
2064 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2065 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2066 }
2067 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2068 let _ = writeln!(out, "}}");
2069}
2070
2071fn render_chat_stream_test_function(
2082 out: &mut String,
2083 fixture: &Fixture,
2084 prefix: &str,
2085 result_var: &str,
2086 args: &[crate::config::ArgMapping],
2087 options_type_name: &str,
2088 expects_error: bool,
2089 api_key_var: Option<&str>,
2090) {
2091 let prefix_upper = prefix.to_uppercase();
2092
2093 let mut request_var: Option<String> = None;
2094 for arg in args {
2095 if arg.arg_type == "json_object" {
2096 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2097 options_type_name.to_string()
2098 } else {
2099 "ChatCompletionRequest".to_string()
2100 };
2101 let request_type_snake = request_type_pascal.to_snake_case();
2102 let var_name = format!("{request_type_snake}_handle");
2103
2104 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2105 let json_val = if field.is_empty() || field == "input" {
2106 Some(&fixture.input)
2107 } else {
2108 fixture.input.get(field)
2109 };
2110
2111 if let Some(val) = json_val {
2112 if !val.is_null() {
2113 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2114 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2115 let escaped = escape_c(&json_str);
2116 let _ = writeln!(
2117 out,
2118 " {prefix_upper}{request_type_pascal}* {var_name} = \
2119 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2120 );
2121 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2122 request_var = Some(var_name);
2123 break;
2124 }
2125 }
2126 }
2127 }
2128
2129 let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
2130 let req_snake = request_var
2131 .as_ref()
2132 .and_then(|v| v.strip_suffix("_handle"))
2133 .unwrap_or("chat_completion_request")
2134 .to_string();
2135
2136 let fixture_id = &fixture.id;
2137 let has_mock = fixture.needs_mock_server();
2138 if has_mock && api_key_var.is_some() {
2139 let _ = writeln!(
2143 out,
2144 " const char* _base_url_arg = (api_key && api_key[0] != '\\0') ? NULL : base_url_buf;"
2145 );
2146 let _ = writeln!(
2147 out,
2148 " {prefix_upper}DefaultClient* client = {prefix}_create_client(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
2149 );
2150 } else if has_mock {
2151 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2152 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2153 let _ = writeln!(out, " char base_url[1024];");
2154 let _ = writeln!(
2155 out,
2156 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2157 );
2158 let _ = writeln!(
2163 out,
2164 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2165 );
2166 } else {
2167 let _ = writeln!(
2168 out,
2169 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2170 );
2171 }
2172 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2173
2174 let _ = writeln!(
2175 out,
2176 " {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
2177 {prefix}_default_client_chat_stream_start(client, {req_handle});"
2178 );
2179
2180 if expects_error {
2181 let _ = writeln!(
2182 out,
2183 " assert(stream_handle == NULL && \"expected stream-start to fail\");"
2184 );
2185 if request_var.is_some() {
2186 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2187 }
2188 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2189 let _ = writeln!(out, "}}");
2190 return;
2191 }
2192
2193 let _ = writeln!(
2194 out,
2195 " assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2196 );
2197
2198 let _ = writeln!(out, " size_t chunks_count = 0;");
2199 let _ = writeln!(out, " char* stream_content = (char*)malloc(1);");
2200 let _ = writeln!(out, " assert(stream_content != NULL);");
2201 let _ = writeln!(out, " stream_content[0] = '\\0';");
2202 let _ = writeln!(out, " size_t stream_content_len = 0;");
2203 let _ = writeln!(out, " int stream_complete = 0;");
2204 let _ = writeln!(out, " int no_chunks_after_done = 1;");
2205 let _ = writeln!(out, " char* last_choices_json = NULL;");
2206 let _ = writeln!(out, " uint64_t total_tokens = 0;");
2207 let _ = writeln!(out);
2208
2209 let _ = writeln!(out, " while (1) {{");
2210 let _ = writeln!(
2211 out,
2212 " {prefix_upper}ChatCompletionChunk* {result_var} = \
2213 {prefix}_default_client_chat_stream_next(stream_handle);"
2214 );
2215 let _ = writeln!(out, " if ({result_var} == NULL) {{");
2216 let _ = writeln!(
2217 out,
2218 " if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2219 );
2220 let _ = writeln!(out, " break;");
2221 let _ = writeln!(out, " }}");
2222 let _ = writeln!(out, " chunks_count++;");
2223 let _ = writeln!(
2224 out,
2225 " char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2226 );
2227 let _ = writeln!(out, " if (choices_json != NULL) {{");
2228 let _ = writeln!(
2229 out,
2230 " const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2231 );
2232 let _ = writeln!(out, " if (d != NULL) {{");
2233 let _ = writeln!(out, " d += 10;");
2234 let _ = writeln!(out, " while (*d == ' ' || *d == '\\t') d++;");
2235 let _ = writeln!(out, " if (*d == '\"') {{");
2236 let _ = writeln!(out, " d++;");
2237 let _ = writeln!(out, " const char* e = d;");
2238 let _ = writeln!(out, " while (*e && *e != '\"') {{");
2239 let _ = writeln!(
2240 out,
2241 " if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2242 );
2243 let _ = writeln!(out, " }}");
2244 let _ = writeln!(out, " size_t add = (size_t)(e - d);");
2245 let _ = writeln!(out, " if (add > 0) {{");
2246 let _ = writeln!(
2247 out,
2248 " char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2249 );
2250 let _ = writeln!(out, " if (nc != NULL) {{");
2251 let _ = writeln!(out, " stream_content = nc;");
2252 let _ = writeln!(
2253 out,
2254 " memcpy(stream_content + stream_content_len, d, add);"
2255 );
2256 let _ = writeln!(out, " stream_content_len += add;");
2257 let _ = writeln!(
2258 out,
2259 " stream_content[stream_content_len] = '\\0';"
2260 );
2261 let _ = writeln!(out, " }}");
2262 let _ = writeln!(out, " }}");
2263 let _ = writeln!(out, " }}");
2264 let _ = writeln!(out, " }}");
2265 let _ = writeln!(
2266 out,
2267 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2268 );
2269 let _ = writeln!(out, " last_choices_json = choices_json;");
2270 let _ = writeln!(out, " }}");
2271 let _ = writeln!(
2272 out,
2273 " {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2274 );
2275 let _ = writeln!(out, " if (usage_handle != NULL) {{");
2276 let _ = writeln!(
2277 out,
2278 " total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2279 );
2280 let _ = writeln!(out, " {prefix}_usage_free(usage_handle);");
2281 let _ = writeln!(out, " }}");
2282 let _ = writeln!(out, " {prefix}_chat_completion_chunk_free({result_var});");
2283 let _ = writeln!(out, " }}");
2284 let _ = writeln!(out, " {prefix}_default_client_chat_stream_free(stream_handle);");
2285 let _ = writeln!(out);
2286
2287 let _ = writeln!(out, " char* finish_reason = NULL;");
2288 let _ = writeln!(out, " char* tool_calls_json = NULL;");
2289 let _ = writeln!(out, " char* tool_calls_0_function_name = NULL;");
2290 let _ = writeln!(out, " if (last_choices_json != NULL) {{");
2291 let _ = writeln!(
2292 out,
2293 " finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2294 );
2295 let _ = writeln!(
2296 out,
2297 " const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2298 );
2299 let _ = writeln!(out, " if (tc != NULL) {{");
2300 let _ = writeln!(out, " tc += 13;");
2301 let _ = writeln!(out, " while (*tc == ' ' || *tc == '\\t') tc++;");
2302 let _ = writeln!(out, " if (*tc == '[') {{");
2303 let _ = writeln!(out, " int depth = 0;");
2304 let _ = writeln!(out, " const char* end = tc;");
2305 let _ = writeln!(out, " int in_str = 0;");
2306 let _ = writeln!(out, " for (; *end; end++) {{");
2307 let _ = writeln!(
2308 out,
2309 " if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2310 );
2311 let _ = writeln!(
2312 out,
2313 " if (*end == '\"') {{ in_str = !in_str; continue; }}"
2314 );
2315 let _ = writeln!(out, " if (in_str) continue;");
2316 let _ = writeln!(out, " if (*end == '[' || *end == '{{') depth++;");
2317 let _ = writeln!(
2318 out,
2319 " else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2320 );
2321 let _ = writeln!(out, " }}");
2322 let _ = writeln!(out, " size_t tlen = (size_t)(end - tc);");
2323 let _ = writeln!(out, " tool_calls_json = (char*)malloc(tlen + 1);");
2324 let _ = writeln!(out, " if (tool_calls_json != NULL) {{");
2325 let _ = writeln!(out, " memcpy(tool_calls_json, tc, tlen);");
2326 let _ = writeln!(out, " tool_calls_json[tlen] = '\\0';");
2327 let _ = writeln!(
2328 out,
2329 " const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2330 );
2331 let _ = writeln!(out, " if (fn != NULL) {{");
2332 let _ = writeln!(
2333 out,
2334 " const char* np = strstr(fn, \"\\\"name\\\":\");"
2335 );
2336 let _ = writeln!(out, " if (np != NULL) {{");
2337 let _ = writeln!(out, " np += 7;");
2338 let _ = writeln!(
2339 out,
2340 " while (*np == ' ' || *np == '\\t') np++;"
2341 );
2342 let _ = writeln!(out, " if (*np == '\"') {{");
2343 let _ = writeln!(out, " np++;");
2344 let _ = writeln!(out, " const char* ne = np;");
2345 let _ = writeln!(
2346 out,
2347 " while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2348 );
2349 let _ = writeln!(out, " size_t nlen = (size_t)(ne - np);");
2350 let _ = writeln!(
2351 out,
2352 " tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2353 );
2354 let _ = writeln!(
2355 out,
2356 " if (tool_calls_0_function_name != NULL) {{"
2357 );
2358 let _ = writeln!(
2359 out,
2360 " memcpy(tool_calls_0_function_name, np, nlen);"
2361 );
2362 let _ = writeln!(
2363 out,
2364 " tool_calls_0_function_name[nlen] = '\\0';"
2365 );
2366 let _ = writeln!(out, " }}");
2367 let _ = writeln!(out, " }}");
2368 let _ = writeln!(out, " }}");
2369 let _ = writeln!(out, " }}");
2370 let _ = writeln!(out, " }}");
2371 let _ = writeln!(out, " }}");
2372 let _ = writeln!(out, " }}");
2373 let _ = writeln!(out, " }}");
2374 let _ = writeln!(out);
2375
2376 for assertion in &fixture.assertions {
2377 emit_chat_stream_assertion(out, assertion);
2378 }
2379
2380 let _ = writeln!(out, " free(stream_content);");
2381 let _ = writeln!(
2382 out,
2383 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2384 );
2385 let _ = writeln!(out, " if (finish_reason != NULL) free(finish_reason);");
2386 let _ = writeln!(out, " if (tool_calls_json != NULL) free(tool_calls_json);");
2387 let _ = writeln!(
2388 out,
2389 " if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2390 );
2391 if request_var.is_some() {
2392 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2393 }
2394 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2395 let _ = writeln!(
2396 out,
2397 " /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2398 (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2399 );
2400 let _ = writeln!(out, "}}");
2401}
2402
2403fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2407 let field = assertion.field.as_deref().unwrap_or("");
2408
2409 enum Kind {
2410 IntCount,
2411 Bool,
2412 Str,
2413 IntTokens,
2414 Unsupported,
2415 }
2416
2417 let (expr, kind) = match field {
2418 "chunks" => ("chunks_count", Kind::IntCount),
2419 "stream_content" => ("stream_content", Kind::Str),
2420 "stream_complete" => ("stream_complete", Kind::Bool),
2421 "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2422 "finish_reason" => ("finish_reason", Kind::Str),
2423 "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2432 "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2433 _ => ("", Kind::Unsupported),
2434 };
2435
2436 let atype = assertion.assertion_type.as_str();
2437 if atype == "not_error" || atype == "error" {
2438 return;
2439 }
2440
2441 if matches!(kind, Kind::Unsupported) {
2442 let _ = writeln!(
2443 out,
2444 " /* skipped: streaming assertion on unsupported field '{field}' */"
2445 );
2446 return;
2447 }
2448
2449 match (atype, &kind) {
2450 ("count_min", Kind::IntCount) => {
2451 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2452 let _ = writeln!(out, " assert({expr} >= {n} && \"expected at least {n} chunks\");");
2453 }
2454 }
2455 ("equals", Kind::Str) => {
2456 if let Some(val) = &assertion.value {
2457 let c_val = json_to_c(val);
2458 let _ = writeln!(
2459 out,
2460 " assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2461 );
2462 }
2463 }
2464 ("contains", Kind::Str) => {
2465 if let Some(val) = &assertion.value {
2466 let c_val = json_to_c(val);
2467 let _ = writeln!(
2468 out,
2469 " assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2470 );
2471 }
2472 }
2473 ("not_empty", Kind::Str) => {
2474 let _ = writeln!(
2475 out,
2476 " assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2477 );
2478 }
2479 ("is_true", Kind::Bool) => {
2480 let _ = writeln!(out, " assert({expr} && \"expected {field} to be true\");");
2481 }
2482 ("is_false", Kind::Bool) => {
2483 let _ = writeln!(out, " assert(!{expr} && \"expected {field} to be false\");");
2484 }
2485 ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2486 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2487 let _ = writeln!(out, " assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2488 }
2489 }
2490 ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2491 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2492 let _ = writeln!(out, " assert({expr} == {n} && \"equals assertion failed\");");
2493 }
2494 }
2495 _ => {
2496 let _ = writeln!(
2497 out,
2498 " /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2499 );
2500 }
2501 }
2502}
2503
2504#[allow(clippy::too_many_arguments)]
2518fn emit_nested_accessor(
2519 out: &mut String,
2520 prefix: &str,
2521 resolved: &str,
2522 local_var: &str,
2523 result_var: &str,
2524 fields_c_types: &HashMap<String, String>,
2525 fields_enum: &HashSet<String>,
2526 intermediate_handles: &mut Vec<(String, String)>,
2527 result_type_name: &str,
2528 raw_field: &str,
2529) -> Option<String> {
2530 let segments: Vec<&str> = resolved.split('.').collect();
2531 let prefix_upper = prefix.to_uppercase();
2532
2533 let mut current_snake_type = result_type_name.to_snake_case();
2535 let mut current_handle = result_var.to_string();
2536 let mut json_extract_mode = false;
2539
2540 for (i, segment) in segments.iter().enumerate() {
2541 let is_leaf = i + 1 == segments.len();
2542
2543 if json_extract_mode {
2547 let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2552 Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2553 None => (segment, None),
2554 };
2555 let seg_snake = bare_segment.to_snake_case();
2556 if is_leaf {
2557 let _ = writeln!(
2558 out,
2559 " char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2560 );
2561 return None; }
2563 let json_var = format!("{seg_snake}_json");
2568 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2569 let _ = writeln!(
2570 out,
2571 " char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2572 );
2573 intermediate_handles.push((json_var.clone(), "free".to_string()));
2574 }
2575 if let Some(key) = bracket_key {
2579 if let Ok(idx) = key.parse::<usize>() {
2580 let elem_var = format!("{seg_snake}_{idx}_json");
2581 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2582 let _ = writeln!(
2583 out,
2584 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2585 );
2586 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2587 }
2588 current_handle = elem_var;
2589 continue;
2590 }
2591 }
2592 current_handle = json_var;
2593 continue;
2594 }
2595
2596 if let Some(bracket_pos) = segment.find('[') {
2598 let field_name = &segment[..bracket_pos];
2599 let key = segment[bracket_pos + 1..].trim_end_matches(']');
2600 let field_snake = field_name.to_snake_case();
2601 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2602
2603 let json_var = format!("{field_snake}_json");
2605 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2606 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2607 let _ = writeln!(out, " assert({json_var} != NULL);");
2608 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2610 }
2611
2612 if key.is_empty() {
2618 if !is_leaf {
2619 current_handle = json_var;
2620 json_extract_mode = true;
2621 continue;
2622 }
2623 return None;
2624 }
2625 if let Ok(idx) = key.parse::<usize>() {
2626 let elem_var = format!("{field_snake}_{idx}_json");
2627 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2628 let _ = writeln!(
2629 out,
2630 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2631 );
2632 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2633 }
2634 if !is_leaf {
2635 current_handle = elem_var;
2636 json_extract_mode = true;
2637 continue;
2638 }
2639 return None;
2641 }
2642
2643 let _ = writeln!(
2645 out,
2646 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
2647 );
2648 return None; }
2650
2651 let seg_snake = segment.to_snake_case();
2652 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
2653
2654 if is_leaf {
2655 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2658 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2659 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
2660 return Some(t.clone());
2661 }
2662 if try_emit_enum_accessor(
2664 out,
2665 prefix,
2666 &prefix_upper,
2667 raw_field,
2668 &seg_snake,
2669 ¤t_snake_type,
2670 &accessor_fn,
2671 ¤t_handle,
2672 local_var,
2673 fields_c_types,
2674 fields_enum,
2675 intermediate_handles,
2676 ) {
2677 return None;
2678 }
2679 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
2680 } else {
2681 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2683 let return_type_pascal = match fields_c_types.get(&lookup_key) {
2684 Some(t) => t.clone(),
2685 None => {
2686 segment.to_pascal_case()
2688 }
2689 };
2690
2691 if return_type_pascal == "char*" {
2694 let json_var = format!("{seg_snake}_json");
2695 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2696 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2697 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2698 }
2699 if i + 2 == segments.len() && segments[i + 1] == "length" {
2701 let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
2702 return Some("int".to_string());
2703 }
2704 current_snake_type = seg_snake.clone();
2705 current_handle = json_var;
2706 continue;
2707 }
2708
2709 let return_snake = return_type_pascal.to_snake_case();
2710 let handle_var = format!("{seg_snake}_handle");
2711
2712 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
2715 let _ = writeln!(
2716 out,
2717 " {prefix_upper}{return_type_pascal}* {handle_var} = \
2718 {accessor_fn}({current_handle});"
2719 );
2720 let _ = writeln!(out, " assert({handle_var} != NULL);");
2721 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
2722 }
2723
2724 current_snake_type = return_snake;
2725 current_handle = handle_var;
2726 }
2727 }
2728 None
2729}
2730
2731fn build_args_string_c(
2735 input: &serde_json::Value,
2736 args: &[crate::config::ArgMapping],
2737 has_options_handle: bool,
2738) -> String {
2739 if args.is_empty() {
2740 return json_to_c(input);
2741 }
2742
2743 let parts: Vec<String> = args
2744 .iter()
2745 .filter_map(|arg| {
2746 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2747 let val = input.get(field);
2748 match val {
2749 None if arg.optional => Some("NULL".to_string()),
2751 None => None,
2753 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
2755 Some(v) => {
2756 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
2759 Some("options_handle".to_string())
2760 } else {
2761 Some(json_to_c(v))
2762 }
2763 }
2764 }
2765 })
2766 .collect();
2767
2768 parts.join(", ")
2769}
2770
2771fn render_assertion(
2772 out: &mut String,
2773 assertion: &Assertion,
2774 result_var: &str,
2775 ffi_prefix: &str,
2776 _field_resolver: &FieldResolver,
2777 accessed_fields: &[(String, String, bool)],
2778 primitive_locals: &HashMap<String, String>,
2779) {
2780 if let Some(f) = &assertion.field {
2782 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
2783 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
2784 return;
2785 }
2786 }
2787
2788 let field_expr = match &assertion.field {
2789 Some(f) if !f.is_empty() => {
2790 accessed_fields
2792 .iter()
2793 .find(|(k, _, _)| k == f)
2794 .map(|(_, local, _)| local.clone())
2795 .unwrap_or_else(|| result_var.to_string())
2796 }
2797 _ => result_var.to_string(),
2798 };
2799
2800 let field_is_primitive = primitive_locals.contains_key(&field_expr);
2801 let field_primitive_type = primitive_locals.get(&field_expr).cloned();
2802 let field_is_map_access = if let Some(f) = &assertion.field {
2806 accessed_fields.iter().any(|(k, _, m)| k == f && *m)
2807 } else {
2808 false
2809 };
2810
2811 let assertion_field_is_optional = assertion
2815 .field
2816 .as_deref()
2817 .map(|f| {
2818 if f.is_empty() {
2819 return false;
2820 }
2821 if _field_resolver.is_optional(f) {
2822 return true;
2823 }
2824 let resolved = _field_resolver.resolve(f);
2826 _field_resolver.is_optional(resolved)
2827 })
2828 .unwrap_or(false);
2829
2830 match assertion.assertion_type.as_str() {
2831 "equals" => {
2832 if let Some(expected) = &assertion.value {
2833 let c_val = json_to_c(expected);
2834 if field_is_primitive {
2835 let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
2836 match expected.as_bool() {
2837 Some(true) => "1".to_string(),
2838 Some(false) => "0".to_string(),
2839 None => c_val,
2840 }
2841 } else {
2842 c_val
2843 };
2844 let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
2847 if assertion_field_is_optional && is_numeric {
2848 let _ = writeln!(
2849 out,
2850 " assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
2851 );
2852 } else {
2853 let _ = writeln!(
2854 out,
2855 " assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
2856 );
2857 }
2858 } else if expected.is_string() {
2859 let _ = writeln!(
2860 out,
2861 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2862 );
2863 } else if field_is_map_access && expected.is_boolean() {
2864 let lit = match expected.as_bool() {
2865 Some(true) => "\"true\"",
2866 _ => "\"false\"",
2867 };
2868 let _ = writeln!(
2869 out,
2870 " assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
2871 );
2872 } else if field_is_map_access && expected.is_number() {
2873 if expected.is_f64() {
2874 let _ = writeln!(
2875 out,
2876 " assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
2877 );
2878 } else {
2879 let _ = writeln!(
2880 out,
2881 " assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
2882 );
2883 }
2884 } else {
2885 let _ = writeln!(
2886 out,
2887 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2888 );
2889 }
2890 }
2891 }
2892 "contains" => {
2893 if let Some(expected) = &assertion.value {
2894 let c_val = json_to_c(expected);
2895 let _ = writeln!(
2896 out,
2897 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2898 );
2899 }
2900 }
2901 "contains_all" => {
2902 if let Some(values) = &assertion.values {
2903 for val in values {
2904 let c_val = json_to_c(val);
2905 let _ = writeln!(
2906 out,
2907 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2908 );
2909 }
2910 }
2911 }
2912 "not_contains" => {
2913 if let Some(expected) = &assertion.value {
2914 let c_val = json_to_c(expected);
2915 let _ = writeln!(
2916 out,
2917 " assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
2918 );
2919 }
2920 }
2921 "not_empty" => {
2922 let _ = writeln!(
2923 out,
2924 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
2925 );
2926 }
2927 "is_empty" => {
2928 if assertion_field_is_optional || !field_is_primitive {
2929 let _ = writeln!(
2931 out,
2932 " assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
2933 );
2934 } else {
2935 let _ = writeln!(
2936 out,
2937 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
2938 );
2939 }
2940 }
2941 "contains_any" => {
2942 if let Some(values) = &assertion.values {
2943 let _ = writeln!(out, " {{");
2944 let _ = writeln!(out, " int found = 0;");
2945 for val in values {
2946 let c_val = json_to_c(val);
2947 let _ = writeln!(
2948 out,
2949 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
2950 );
2951 }
2952 let _ = writeln!(
2953 out,
2954 " assert(found && \"expected to contain at least one of the specified values\");"
2955 );
2956 let _ = writeln!(out, " }}");
2957 }
2958 }
2959 "greater_than" => {
2960 if let Some(val) = &assertion.value {
2961 let c_val = json_to_c(val);
2962 if field_is_map_access && val.is_number() && !field_is_primitive {
2963 let _ = writeln!(
2964 out,
2965 " assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
2966 );
2967 } else {
2968 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
2969 }
2970 }
2971 }
2972 "less_than" => {
2973 if let Some(val) = &assertion.value {
2974 let c_val = json_to_c(val);
2975 if field_is_map_access && val.is_number() && !field_is_primitive {
2976 let _ = writeln!(
2977 out,
2978 " assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
2979 );
2980 } else {
2981 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
2982 }
2983 }
2984 }
2985 "greater_than_or_equal" => {
2986 if let Some(val) = &assertion.value {
2987 let c_val = json_to_c(val);
2988 if field_is_map_access && val.is_number() && !field_is_primitive {
2989 let _ = writeln!(
2990 out,
2991 " assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
2992 );
2993 } else {
2994 let _ = writeln!(
2995 out,
2996 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
2997 );
2998 }
2999 }
3000 }
3001 "less_than_or_equal" => {
3002 if let Some(val) = &assertion.value {
3003 let c_val = json_to_c(val);
3004 if field_is_map_access && val.is_number() && !field_is_primitive {
3005 let _ = writeln!(
3006 out,
3007 " assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
3008 );
3009 } else {
3010 let _ = writeln!(
3011 out,
3012 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
3013 );
3014 }
3015 }
3016 }
3017 "starts_with" => {
3018 if let Some(expected) = &assertion.value {
3019 let c_val = json_to_c(expected);
3020 let _ = writeln!(
3021 out,
3022 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
3023 );
3024 }
3025 }
3026 "ends_with" => {
3027 if let Some(expected) = &assertion.value {
3028 let c_val = json_to_c(expected);
3029 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
3030 let _ = writeln!(
3031 out,
3032 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
3033 );
3034 }
3035 }
3036 "min_length" => {
3037 if let Some(val) = &assertion.value {
3038 if let Some(n) = val.as_u64() {
3039 let _ = writeln!(
3040 out,
3041 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
3042 );
3043 }
3044 }
3045 }
3046 "max_length" => {
3047 if let Some(val) = &assertion.value {
3048 if let Some(n) = val.as_u64() {
3049 let _ = writeln!(
3050 out,
3051 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
3052 );
3053 }
3054 }
3055 }
3056 "count_min" => {
3057 if let Some(val) = &assertion.value {
3058 if let Some(n) = val.as_u64() {
3059 let _ = writeln!(out, " {{");
3060 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
3061 let _ = writeln!(
3062 out,
3063 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3064 );
3065 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3066 let _ = writeln!(
3067 out,
3068 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
3069 );
3070 let _ = writeln!(out, " }}");
3071 }
3072 }
3073 }
3074 "count_equals" => {
3075 if let Some(val) = &assertion.value {
3076 if let Some(n) = val.as_u64() {
3077 let _ = writeln!(out, " {{");
3078 let _ = writeln!(out, " /* count_equals: count elements in array */");
3079 let _ = writeln!(
3080 out,
3081 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3082 );
3083 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3084 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
3085 let _ = writeln!(out, " }}");
3086 }
3087 }
3088 }
3089 "is_true" => {
3090 let _ = writeln!(out, " assert({field_expr});");
3091 }
3092 "is_false" => {
3093 let _ = writeln!(out, " assert(!{field_expr});");
3094 }
3095 "method_result" => {
3096 if let Some(method_name) = &assertion.method {
3097 render_method_result_assertion(
3098 out,
3099 result_var,
3100 ffi_prefix,
3101 method_name,
3102 assertion.args.as_ref(),
3103 assertion.return_type.as_deref(),
3104 assertion.check.as_deref().unwrap_or("is_true"),
3105 assertion.value.as_ref(),
3106 );
3107 } else {
3108 panic!("C e2e generator: method_result assertion missing 'method' field");
3109 }
3110 }
3111 "matches_regex" => {
3112 if let Some(expected) = &assertion.value {
3113 let c_val = json_to_c(expected);
3114 let _ = writeln!(out, " {{");
3115 let _ = writeln!(out, " regex_t _re;");
3116 let _ = writeln!(
3117 out,
3118 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3119 );
3120 let _ = writeln!(
3121 out,
3122 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3123 );
3124 let _ = writeln!(out, " regfree(&_re);");
3125 let _ = writeln!(out, " }}");
3126 }
3127 }
3128 "not_error" => {
3129 }
3131 "error" => {
3132 }
3134 other => {
3135 panic!("C e2e generator: unsupported assertion type: {other}");
3136 }
3137 }
3138}
3139
3140#[allow(clippy::too_many_arguments)]
3149fn render_method_result_assertion(
3150 out: &mut String,
3151 result_var: &str,
3152 ffi_prefix: &str,
3153 method_name: &str,
3154 args: Option<&serde_json::Value>,
3155 return_type: Option<&str>,
3156 check: &str,
3157 value: Option<&serde_json::Value>,
3158) {
3159 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3160
3161 if return_type == Some("string") {
3162 let _ = writeln!(out, " {{");
3164 let _ = writeln!(out, " char* _method_result = {call_expr};");
3165 if check == "is_error" {
3166 let _ = writeln!(
3167 out,
3168 " assert(_method_result == NULL && \"expected method to return error\");"
3169 );
3170 let _ = writeln!(out, " }}");
3171 return;
3172 }
3173 let _ = writeln!(
3174 out,
3175 " assert(_method_result != NULL && \"method_result returned NULL\");"
3176 );
3177 match check {
3178 "contains" => {
3179 if let Some(val) = value {
3180 let c_val = json_to_c(val);
3181 let _ = writeln!(
3182 out,
3183 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3184 );
3185 }
3186 }
3187 "equals" => {
3188 if let Some(val) = value {
3189 let c_val = json_to_c(val);
3190 let _ = writeln!(
3191 out,
3192 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3193 );
3194 }
3195 }
3196 "is_true" => {
3197 let _ = writeln!(
3198 out,
3199 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3200 );
3201 }
3202 "count_min" => {
3203 if let Some(val) = value {
3204 let n = val.as_u64().unwrap_or(0);
3205 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
3206 let _ = writeln!(
3207 out,
3208 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3209 );
3210 }
3211 }
3212 other_check => {
3213 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3214 }
3215 }
3216 let _ = writeln!(out, " free(_method_result);");
3217 let _ = writeln!(out, " }}");
3218 return;
3219 }
3220
3221 match check {
3223 "equals" => {
3224 if let Some(val) = value {
3225 let c_val = json_to_c(val);
3226 let _ = writeln!(
3227 out,
3228 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3229 );
3230 }
3231 }
3232 "is_true" => {
3233 let _ = writeln!(
3234 out,
3235 " assert({call_expr} && \"method_result is_true assertion failed\");"
3236 );
3237 }
3238 "is_false" => {
3239 let _ = writeln!(
3240 out,
3241 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
3242 );
3243 }
3244 "greater_than_or_equal" => {
3245 if let Some(val) = value {
3246 let n = val.as_u64().unwrap_or(0);
3247 let _ = writeln!(
3248 out,
3249 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3250 );
3251 }
3252 }
3253 "count_min" => {
3254 if let Some(val) = value {
3255 let n = val.as_u64().unwrap_or(0);
3256 let _ = writeln!(
3257 out,
3258 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3259 );
3260 }
3261 }
3262 other_check => {
3263 panic!("C e2e generator: unsupported method_result check type: {other_check}");
3264 }
3265 }
3266}
3267
3268fn build_c_method_call(
3275 result_var: &str,
3276 ffi_prefix: &str,
3277 method_name: &str,
3278 args: Option<&serde_json::Value>,
3279) -> String {
3280 let extra_args = if let Some(args_val) = args {
3281 args_val
3282 .as_object()
3283 .map(|obj| {
3284 obj.values()
3285 .map(|v| match v {
3286 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3287 serde_json::Value::Bool(true) => "1".to_string(),
3288 serde_json::Value::Bool(false) => "0".to_string(),
3289 serde_json::Value::Number(n) => n.to_string(),
3290 serde_json::Value::Null => "NULL".to_string(),
3291 other => format!("\"{}\"", escape_c(&other.to_string())),
3292 })
3293 .collect::<Vec<_>>()
3294 .join(", ")
3295 })
3296 .unwrap_or_default()
3297 } else {
3298 String::new()
3299 };
3300
3301 if extra_args.is_empty() {
3302 format!("{ffi_prefix}_{method_name}({result_var})")
3303 } else {
3304 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3305 }
3306}
3307
3308fn json_to_c(value: &serde_json::Value) -> String {
3310 match value {
3311 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3312 serde_json::Value::Bool(true) => "1".to_string(),
3313 serde_json::Value::Bool(false) => "0".to_string(),
3314 serde_json::Value::Number(n) => n.to_string(),
3315 serde_json::Value::Null => "NULL".to_string(),
3316 other => format!("\"{}\"", escape_c(&other.to_string())),
3317 }
3318}