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