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