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