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 let per_call_field_resolver = FieldResolver::new(
1006 e2e_config.effective_fields(fixture_call),
1007 e2e_config.effective_fields_optional(fixture_call),
1008 e2e_config.effective_result_fields(fixture_call),
1009 e2e_config.effective_fields_array(fixture_call),
1010 &std::collections::HashSet::new(),
1011 );
1012 let _ = field_resolver; let field_resolver = &per_call_field_resolver;
1014
1015 render_test_function(
1016 &mut out,
1017 fixture,
1018 prefix,
1019 &call_info.function_name,
1020 result_var,
1021 &call_info.args,
1022 field_resolver,
1023 &e2e_config.fields_c_types,
1024 &effective_fields_enum,
1025 &call_info.result_type_name,
1026 &call_info.options_type_name,
1027 call_info.client_factory.as_deref(),
1028 call_info.raw_c_result_type.as_deref(),
1029 call_info.c_free_fn.as_deref(),
1030 call_info.c_engine_factory.as_deref(),
1031 call_info.result_is_option,
1032 call_info.result_is_bytes,
1033 &call_info.extra_args,
1034 );
1035 if i + 1 < fixtures.len() {
1036 let _ = writeln!(out);
1037 }
1038 }
1039
1040 out
1041}
1042
1043#[allow(clippy::too_many_arguments)]
1044fn render_test_function(
1045 out: &mut String,
1046 fixture: &Fixture,
1047 prefix: &str,
1048 function_name: &str,
1049 result_var: &str,
1050 args: &[crate::config::ArgMapping],
1051 field_resolver: &FieldResolver,
1052 fields_c_types: &HashMap<String, String>,
1053 fields_enum: &HashSet<String>,
1054 result_type_name: &str,
1055 options_type_name: &str,
1056 client_factory: Option<&str>,
1057 raw_c_result_type: Option<&str>,
1058 c_free_fn: Option<&str>,
1059 c_engine_factory: Option<&str>,
1060 result_is_option: bool,
1061 result_is_bytes: bool,
1062 extra_args: &[String],
1063) {
1064 let fn_name = sanitize_ident(&fixture.id);
1065 let description = &fixture.description;
1066
1067 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1068
1069 let _ = writeln!(out, "void test_{fn_name}(void) {{");
1070 let _ = writeln!(out, " /* {description} */");
1071
1072 let has_mock = fixture.needs_mock_server();
1081 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1082 if let Some(env) = &fixture.env {
1083 if let Some(var) = &env.api_key_var {
1084 let fixture_id = &fixture.id;
1085 if has_mock {
1086 let _ = writeln!(out, " const char* api_key = getenv(\"{var}\");");
1087 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1088 let _ = writeln!(out, " char base_url_buf[512];");
1089 let _ = writeln!(out, " int use_mock = !(api_key && api_key[0] != '\\0');");
1090 let _ = writeln!(out, " if (!use_mock) {{");
1091 let _ = writeln!(
1092 out,
1093 " fprintf(stderr, \"{fixture_id}: using real API ({var} is set)\\n\");"
1094 );
1095 let _ = writeln!(out, " }} else {{");
1096 let _ = writeln!(
1097 out,
1098 " fprintf(stderr, \"{fixture_id}: using mock server ({var} not set)\\n\");"
1099 );
1100 let _ = writeln!(
1101 out,
1102 " snprintf(base_url_buf, sizeof(base_url_buf), \"%s/fixtures/{fixture_id}\", mock_base ? mock_base : \"\");"
1103 );
1104 let _ = writeln!(out, " api_key = \"test-key\";");
1105 let _ = writeln!(out, " }}");
1106 } else {
1107 let _ = writeln!(out, " if (getenv(\"{var}\") == NULL) {{ return; }}");
1108 }
1109 }
1110 }
1111
1112 let prefix_upper = prefix.to_uppercase();
1113
1114 if let Some(config_type) = c_engine_factory {
1118 render_engine_factory_test_function(
1119 out,
1120 fixture,
1121 prefix,
1122 function_name,
1123 result_var,
1124 field_resolver,
1125 fields_c_types,
1126 fields_enum,
1127 result_type_name,
1128 config_type,
1129 expects_error,
1130 raw_c_result_type,
1131 );
1132 return;
1133 }
1134
1135 if client_factory.is_some() && function_name == "chat_stream" {
1141 render_chat_stream_test_function(
1142 out,
1143 fixture,
1144 prefix,
1145 result_var,
1146 args,
1147 options_type_name,
1148 expects_error,
1149 api_key_var,
1150 );
1151 return;
1152 }
1153
1154 if let Some(factory) = client_factory {
1162 if result_is_bytes {
1163 render_bytes_test_function(
1164 out,
1165 fixture,
1166 prefix,
1167 function_name,
1168 result_var,
1169 args,
1170 options_type_name,
1171 result_type_name,
1172 factory,
1173 expects_error,
1174 );
1175 return;
1176 }
1177 }
1178
1179 if let Some(factory) = client_factory {
1184 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); let mut inline_method_args: Vec<String> = Vec::new();
1189
1190 for arg in args {
1191 if arg.arg_type == "json_object" {
1192 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1197 options_type_name.to_string()
1198 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1199 format!("{}Request", stripped)
1200 } else {
1201 format!("{result_type_name}Request")
1202 };
1203 let request_type_snake = request_type_pascal.to_snake_case();
1204 let var_name = format!("{request_type_snake}_handle");
1205
1206 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1207 let json_val = if field.is_empty() || field == "input" {
1208 Some(&fixture.input)
1209 } else {
1210 fixture.input.get(field)
1211 };
1212
1213 if let Some(val) = json_val {
1214 if !val.is_null() {
1215 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1216 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1217 let escaped = escape_c(&json_str);
1218 let _ = writeln!(
1219 out,
1220 " {prefix_upper}{request_type_pascal}* {var_name} = \
1221 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1222 );
1223 if expects_error {
1224 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
1232 } else {
1233 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1234 }
1235 request_handle_vars.push((arg.name.clone(), var_name));
1236 }
1237 }
1238 } else if arg.arg_type == "string" {
1239 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1241 let val = fixture.input.get(field);
1242 match val {
1243 Some(v) if v.is_string() => {
1244 let s = v.as_str().unwrap_or_default();
1245 let escaped = escape_c(s);
1246 inline_method_args.push(format!("\"{escaped}\""));
1247 }
1248 Some(serde_json::Value::Null) | None if arg.optional => {
1249 inline_method_args.push("NULL".to_string());
1250 }
1251 None => {
1252 inline_method_args.push("\"\"".to_string());
1253 }
1254 Some(other) => {
1255 let s = serde_json::to_string(other).unwrap_or_default();
1256 let escaped = escape_c(&s);
1257 inline_method_args.push(format!("\"{escaped}\""));
1258 }
1259 }
1260 } else if arg.optional {
1261 inline_method_args.push("NULL".to_string());
1263 }
1264 }
1265
1266 let fixture_id = &fixture.id;
1267 if has_mock && api_key_var.is_some() {
1272 let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
1276 let _ = writeln!(
1277 out,
1278 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
1279 );
1280 } else if has_mock {
1281 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1282 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1283 let _ = writeln!(out, " char base_url[1024];");
1284 let _ = writeln!(
1285 out,
1286 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1287 );
1288 let _ = writeln!(
1289 out,
1290 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1291 );
1292 } else {
1293 let _ = writeln!(
1294 out,
1295 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1296 );
1297 }
1298 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1299
1300 let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
1301 String::new()
1302 } else {
1303 let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
1304 let parts: Vec<String> = handles
1305 .into_iter()
1306 .chain(inline_method_args.iter().cloned())
1307 .chain(extra_args.iter().cloned())
1308 .collect();
1309 format!(", {}", parts.join(", "))
1310 };
1311
1312 let call_fn = format!("{prefix}_default_client_{function_name}");
1313
1314 if expects_error {
1315 let _ = writeln!(
1316 out,
1317 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1318 );
1319 for (_, var_name) in &request_handle_vars {
1320 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1321 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1322 }
1323 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1324 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1325 let _ = writeln!(out, "}}");
1326 return;
1327 }
1328
1329 let _ = writeln!(
1330 out,
1331 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1332 );
1333 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1334
1335 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1336 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1337 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1340 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1343
1344 for assertion in &fixture.assertions {
1345 if let Some(f) = &assertion.field {
1346 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1347 let resolved = field_resolver.resolve(f);
1348 let local_var = f.replace(['.', '['], "_").replace(']', "");
1349 let has_map_access = resolved.contains('[');
1350 if resolved.contains('.') {
1351 let leaf_primitive = emit_nested_accessor(
1352 out,
1353 prefix,
1354 resolved,
1355 &local_var,
1356 result_var,
1357 fields_c_types,
1358 fields_enum,
1359 &mut intermediate_handles,
1360 result_type_name,
1361 f,
1362 );
1363 if let Some(prim) = leaf_primitive {
1364 primitive_locals.insert(local_var.clone(), prim);
1365 }
1366 } else {
1367 let result_type_snake = result_type_name.to_snake_case();
1368 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1369 let lookup_key = format!("{result_type_snake}.{resolved}");
1370 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1371 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1372 primitive_locals.insert(local_var.clone(), t.clone());
1373 } else if try_emit_enum_accessor(
1374 out,
1375 prefix,
1376 &prefix_upper,
1377 f,
1378 resolved,
1379 &result_type_snake,
1380 &accessor_fn,
1381 result_var,
1382 &local_var,
1383 fields_c_types,
1384 fields_enum,
1385 &mut intermediate_handles,
1386 ) {
1387 } else if let Some(handle_pascal) =
1389 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1390 {
1391 let _ = writeln!(
1393 out,
1394 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1395 );
1396 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1397 } else {
1398 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1399 }
1400 }
1401 accessed_fields.push((f.clone(), local_var, has_map_access));
1402 }
1403 }
1404 }
1405
1406 for assertion in &fixture.assertions {
1407 render_assertion(
1408 out,
1409 assertion,
1410 result_var,
1411 prefix,
1412 field_resolver,
1413 &accessed_fields,
1414 &primitive_locals,
1415 &opaque_handle_locals,
1416 );
1417 }
1418
1419 for (_f, local_var, from_json) in &accessed_fields {
1420 if primitive_locals.contains_key(local_var) {
1421 continue;
1422 }
1423 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1424 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
1425 continue;
1426 }
1427 if *from_json {
1428 let _ = writeln!(out, " free({local_var});");
1429 } else {
1430 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1431 }
1432 }
1433 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1434 if snake_type == "free_string" {
1435 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1436 } else if snake_type == "free" {
1437 let _ = writeln!(out, " free({handle_var});");
1440 } else {
1441 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1442 }
1443 }
1444 let result_type_snake = result_type_name.to_snake_case();
1445 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1446 for (_, var_name) in &request_handle_vars {
1447 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1448 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1449 }
1450 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1451 let _ = writeln!(out, "}}");
1452 return;
1453 }
1454
1455 if let Some(raw_type) = raw_c_result_type {
1458 let args_str = if args.is_empty() {
1460 String::new()
1461 } else {
1462 let parts: Vec<String> = args
1463 .iter()
1464 .filter_map(|arg| {
1465 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1466 let val = fixture.input.get(field);
1467 match val {
1468 None if arg.optional => Some("NULL".to_string()),
1469 None => None,
1470 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1471 Some(v) => Some(json_to_c(v)),
1472 }
1473 })
1474 .collect();
1475 parts.join(", ")
1476 };
1477
1478 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
1480
1481 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
1483 if has_not_error {
1484 match raw_type {
1485 "char*" if !result_is_option => {
1486 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1487 }
1488 "int32_t" => {
1489 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
1490 }
1491 "uintptr_t" => {
1492 let _ = writeln!(
1493 out,
1494 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
1495 );
1496 }
1497 _ => {}
1498 }
1499 }
1500
1501 for assertion in &fixture.assertions {
1503 match assertion.assertion_type.as_str() {
1504 "not_error" | "error" => {} "not_empty" => {
1506 let _ = writeln!(
1507 out,
1508 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
1509 );
1510 }
1511 "is_empty" => {
1512 if result_is_option && raw_type == "char*" {
1513 let _ = writeln!(
1514 out,
1515 " assert({result_var} == NULL && \"expected empty/null value\");"
1516 );
1517 } else {
1518 let _ = writeln!(
1519 out,
1520 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
1521 );
1522 }
1523 }
1524 "count_min" => {
1525 if let Some(val) = &assertion.value {
1526 if let Some(n) = val.as_u64() {
1527 match raw_type {
1528 "char*" => {
1529 let _ = writeln!(out, " {{");
1530 let _ = writeln!(
1531 out,
1532 " assert({result_var} != NULL && \"expected non-null JSON array\");"
1533 );
1534 let _ =
1535 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
1536 let _ = writeln!(
1537 out,
1538 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1539 );
1540 let _ = writeln!(out, " }}");
1541 }
1542 _ => {
1543 let _ = writeln!(
1544 out,
1545 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1546 );
1547 }
1548 }
1549 }
1550 }
1551 }
1552 "greater_than_or_equal" => {
1553 if let Some(val) = &assertion.value {
1554 let c_val = json_to_c(val);
1555 let _ = writeln!(
1556 out,
1557 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1558 );
1559 }
1560 }
1561 "contains" => {
1562 if let Some(val) = &assertion.value {
1563 let c_val = json_to_c(val);
1564 let _ = writeln!(
1565 out,
1566 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1567 );
1568 }
1569 }
1570 "contains_all" => {
1571 if let Some(values) = &assertion.values {
1572 for val in values {
1573 let c_val = json_to_c(val);
1574 let _ = writeln!(
1575 out,
1576 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1577 );
1578 }
1579 }
1580 }
1581 "equals" => {
1582 if let Some(val) = &assertion.value {
1583 let c_val = json_to_c(val);
1584 if val.is_string() {
1585 let _ = writeln!(
1586 out,
1587 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1588 );
1589 } else {
1590 let _ = writeln!(
1591 out,
1592 " assert({result_var} == {c_val} && \"equals assertion failed\");"
1593 );
1594 }
1595 }
1596 }
1597 "not_contains" => {
1598 if let Some(val) = &assertion.value {
1599 let c_val = json_to_c(val);
1600 let _ = writeln!(
1601 out,
1602 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1603 );
1604 }
1605 }
1606 "starts_with" => {
1607 if let Some(val) = &assertion.value {
1608 let c_val = json_to_c(val);
1609 let _ = writeln!(
1610 out,
1611 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1612 );
1613 }
1614 }
1615 "is_true" => {
1616 let _ = writeln!(out, " assert({result_var});");
1617 }
1618 "is_false" => {
1619 let _ = writeln!(out, " assert(!{result_var});");
1620 }
1621 other => {
1622 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1623 }
1624 }
1625 }
1626
1627 if raw_type == "char*" {
1629 let free_fn = c_free_fn
1630 .map(|s| s.to_string())
1631 .unwrap_or_else(|| format!("{prefix}_free_string"));
1632 if result_is_option {
1633 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1634 } else {
1635 let _ = writeln!(out, " {free_fn}({result_var});");
1636 }
1637 }
1638
1639 let _ = writeln!(out, "}}");
1640 return;
1641 }
1642
1643 let prefixed_fn = function_name.to_string();
1649
1650 let mut has_options_handle = false;
1652 for arg in args {
1653 if arg.arg_type == "json_object" {
1654 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1655 if let Some(val) = fixture.input.get(field) {
1656 if !val.is_null() {
1657 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1661 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1662 let escaped = escape_c(&json_str);
1663 let upper = prefix.to_uppercase();
1664 let options_type_pascal = options_type_name;
1665 let options_type_snake = options_type_name.to_snake_case();
1666 let _ = writeln!(
1667 out,
1668 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1669 );
1670 has_options_handle = true;
1671 }
1672 }
1673 }
1674 }
1675
1676 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1677
1678 if expects_error {
1679 let _ = writeln!(
1680 out,
1681 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1682 );
1683 if has_options_handle {
1684 let options_type_snake = options_type_name.to_snake_case();
1685 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1686 }
1687 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1688 let _ = writeln!(out, "}}");
1689 return;
1690 }
1691
1692 let _ = writeln!(
1694 out,
1695 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1696 );
1697 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1698
1699 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1707 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1710 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1712 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1714
1715 for assertion in &fixture.assertions {
1716 if let Some(f) = &assertion.field {
1717 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1718 let resolved = field_resolver.resolve(f);
1719 let local_var = f.replace(['.', '['], "_").replace(']', "");
1720 let has_map_access = resolved.contains('[');
1721
1722 if resolved.contains('.') {
1723 let leaf_primitive = emit_nested_accessor(
1724 out,
1725 prefix,
1726 resolved,
1727 &local_var,
1728 result_var,
1729 fields_c_types,
1730 fields_enum,
1731 &mut intermediate_handles,
1732 result_type_name,
1733 f,
1734 );
1735 if let Some(prim) = leaf_primitive {
1736 primitive_locals.insert(local_var.clone(), prim);
1737 }
1738 } else {
1739 let result_type_snake = result_type_name.to_snake_case();
1740 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1741 let lookup_key = format!("{result_type_snake}.{resolved}");
1742 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1743 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1744 primitive_locals.insert(local_var.clone(), t.clone());
1745 } else if try_emit_enum_accessor(
1746 out,
1747 prefix,
1748 &prefix_upper,
1749 f,
1750 resolved,
1751 &result_type_snake,
1752 &accessor_fn,
1753 result_var,
1754 &local_var,
1755 fields_c_types,
1756 fields_enum,
1757 &mut intermediate_handles,
1758 ) {
1759 } else if let Some(handle_pascal) =
1761 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1762 {
1763 let _ = writeln!(
1764 out,
1765 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1766 );
1767 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1768 } else {
1769 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1770 }
1771 }
1772 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1773 }
1774 }
1775 }
1776
1777 for assertion in &fixture.assertions {
1778 render_assertion(
1779 out,
1780 assertion,
1781 result_var,
1782 prefix,
1783 field_resolver,
1784 &accessed_fields,
1785 &primitive_locals,
1786 &opaque_handle_locals,
1787 );
1788 }
1789
1790 for (_f, local_var, from_json) in &accessed_fields {
1792 if primitive_locals.contains_key(local_var) {
1793 continue;
1794 }
1795 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1796 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
1797 continue;
1798 }
1799 if *from_json {
1800 let _ = writeln!(out, " free({local_var});");
1801 } else {
1802 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1803 }
1804 }
1805 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1807 if snake_type == "free_string" {
1808 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1810 } else {
1811 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1812 }
1813 }
1814 if has_options_handle {
1815 let options_type_snake = options_type_name.to_snake_case();
1816 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1817 }
1818 let result_type_snake = result_type_name.to_snake_case();
1819 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1820 let _ = writeln!(out, "}}");
1821}
1822
1823#[allow(clippy::too_many_arguments)]
1832fn render_engine_factory_test_function(
1833 out: &mut String,
1834 fixture: &Fixture,
1835 prefix: &str,
1836 function_name: &str,
1837 result_var: &str,
1838 field_resolver: &FieldResolver,
1839 fields_c_types: &HashMap<String, String>,
1840 fields_enum: &HashSet<String>,
1841 result_type_name: &str,
1842 config_type: &str,
1843 expects_error: bool,
1844 raw_c_result_type: Option<&str>,
1845) {
1846 let prefix_upper = prefix.to_uppercase();
1847 let config_snake = config_type.to_snake_case();
1848
1849 let config_val = fixture.input.get("config");
1851 let config_json = match config_val {
1852 Some(v) if !v.is_null() => {
1853 let normalized = super::transform_json_keys_for_language(v, "snake_case");
1854 serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1855 }
1856 _ => "{}".to_string(),
1857 };
1858 let config_escaped = escape_c(&config_json);
1859 let fixture_id = &fixture.id;
1860
1861 let has_active_assertions = fixture.assertions.iter().any(|a| {
1865 if let Some(f) = &a.field {
1866 !f.is_empty() && field_resolver.is_valid_for_result(f)
1867 } else {
1868 false
1869 }
1870 });
1871
1872 let _ = writeln!(
1874 out,
1875 " {prefix_upper}{config_type}* config_handle = \
1876 {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1877 );
1878 if expects_error {
1879 let _ = writeln!(out, " if (config_handle == NULL) {{ return; }}");
1882 } else {
1883 let _ = writeln!(out, " assert(config_handle != NULL && \"failed to parse config\");");
1884 }
1885 let _ = writeln!(
1886 out,
1887 " {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1888 );
1889 let _ = writeln!(out, " {prefix}_{config_snake}_free(config_handle);");
1890 if expects_error {
1891 let _ = writeln!(out, " if (engine == NULL) {{ return; }}");
1894 } else {
1895 let _ = writeln!(out, " assert(engine != NULL && \"failed to create engine\");");
1896 }
1897
1898 let fixture_env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1902 let _ = writeln!(out, " const char* mock_per_fixture = getenv(\"{fixture_env_key}\");");
1903 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1904 let _ = writeln!(out, " char url[2048];");
1905 let _ = writeln!(out, " if (mock_per_fixture && mock_per_fixture[0] != '\\0') {{");
1906 let _ = writeln!(out, " snprintf(url, sizeof(url), \"%s\", mock_per_fixture);");
1907 let _ = writeln!(out, " }} else {{");
1908 let _ = writeln!(
1909 out,
1910 " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");"
1911 );
1912 let _ = writeln!(
1913 out,
1914 " snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
1915 );
1916 let _ = writeln!(out, " }}");
1917
1918 if raw_c_result_type == Some("char*") {
1923 let _ = writeln!(out, " char* {result_var} = {prefix}_{function_name}(engine, url);");
1924 let _ = writeln!(out, " if ({result_var} != NULL) {prefix}_free_string({result_var});");
1927 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1928 let _ = writeln!(out, "}}");
1929 return;
1930 }
1931
1932 let _ = writeln!(
1933 out,
1934 " {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url);"
1935 );
1936
1937 if !has_active_assertions {
1940 let result_type_snake = result_type_name.to_snake_case();
1941 let _ = writeln!(
1942 out,
1943 " if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
1944 );
1945 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
1946 let _ = writeln!(out, "}}");
1947 return;
1948 }
1949
1950 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1951
1952 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1954 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1955 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1956 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1957
1958 for assertion in &fixture.assertions {
1959 if let Some(f) = &assertion.field {
1960 if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
1961 {
1962 let resolved = field_resolver.resolve(f);
1963 let local_var = f.replace(['.', '['], "_").replace(']', "");
1964 let has_map_access = resolved.contains('[');
1965 if resolved.contains('.') {
1966 let leaf_primitive = emit_nested_accessor(
1967 out,
1968 prefix,
1969 resolved,
1970 &local_var,
1971 result_var,
1972 fields_c_types,
1973 fields_enum,
1974 &mut intermediate_handles,
1975 result_type_name,
1976 f,
1977 );
1978 if let Some(prim) = leaf_primitive {
1979 primitive_locals.insert(local_var.clone(), prim);
1980 }
1981 } else {
1982 let result_type_snake = result_type_name.to_snake_case();
1983 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1984 let lookup_key = format!("{result_type_snake}.{resolved}");
1985 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1986 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1987 primitive_locals.insert(local_var.clone(), t.clone());
1988 } else if try_emit_enum_accessor(
1989 out,
1990 prefix,
1991 &prefix_upper,
1992 f,
1993 resolved,
1994 &result_type_snake,
1995 &accessor_fn,
1996 result_var,
1997 &local_var,
1998 fields_c_types,
1999 fields_enum,
2000 &mut intermediate_handles,
2001 ) {
2002 } else if let Some(handle_pascal) =
2004 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
2005 {
2006 let _ = writeln!(
2007 out,
2008 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
2009 );
2010 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
2011 } else {
2012 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
2013 }
2014 }
2015 accessed_fields.push((f.clone(), local_var, has_map_access));
2016 }
2017 }
2018 }
2019
2020 for assertion in &fixture.assertions {
2021 render_assertion(
2022 out,
2023 assertion,
2024 result_var,
2025 prefix,
2026 field_resolver,
2027 &accessed_fields,
2028 &primitive_locals,
2029 &opaque_handle_locals,
2030 );
2031 }
2032
2033 for (_f, local_var, from_json) in &accessed_fields {
2035 if primitive_locals.contains_key(local_var) {
2036 continue;
2037 }
2038 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
2039 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
2040 continue;
2041 }
2042 if *from_json {
2043 let _ = writeln!(out, " free({local_var});");
2044 } else {
2045 let _ = writeln!(out, " {prefix}_free_string({local_var});");
2046 }
2047 }
2048 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
2049 if snake_type == "free_string" {
2050 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
2051 } else {
2052 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
2053 }
2054 }
2055
2056 let result_type_snake = result_type_name.to_snake_case();
2057 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
2058 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2059 let _ = writeln!(out, "}}");
2060}
2061
2062#[allow(clippy::too_many_arguments)]
2084fn render_bytes_test_function(
2085 out: &mut String,
2086 fixture: &Fixture,
2087 prefix: &str,
2088 function_name: &str,
2089 _result_var: &str,
2090 args: &[crate::config::ArgMapping],
2091 options_type_name: &str,
2092 result_type_name: &str,
2093 factory: &str,
2094 expects_error: bool,
2095) {
2096 let prefix_upper = prefix.to_uppercase();
2097 let mut request_handle_vars: Vec<(String, String)> = Vec::new();
2098 let mut string_arg_exprs: Vec<String> = Vec::new();
2099
2100 for arg in args {
2101 match arg.arg_type.as_str() {
2102 "json_object" => {
2103 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2104 options_type_name.to_string()
2105 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
2106 format!("{}Request", stripped)
2107 } else {
2108 format!("{result_type_name}Request")
2109 };
2110 let request_type_snake = request_type_pascal.to_snake_case();
2111 let var_name = format!("{request_type_snake}_handle");
2112
2113 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2114 let json_val = if field.is_empty() || field == "input" {
2115 Some(&fixture.input)
2116 } else {
2117 fixture.input.get(field)
2118 };
2119
2120 if let Some(val) = json_val {
2121 if !val.is_null() {
2122 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2123 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2124 let escaped = escape_c(&json_str);
2125 let _ = writeln!(
2126 out,
2127 " {prefix_upper}{request_type_pascal}* {var_name} = \
2128 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2129 );
2130 if expects_error {
2131 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
2139 } else {
2140 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2141 }
2142 request_handle_vars.push((arg.name.clone(), var_name));
2143 }
2144 }
2145 }
2146 "string" => {
2147 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2150 let val = fixture.input.get(field);
2151 let expr = match val {
2152 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
2153 Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
2154 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
2155 None => "NULL".to_string(),
2156 };
2157 string_arg_exprs.push(expr);
2158 }
2159 _ => {
2160 string_arg_exprs.push("NULL".to_string());
2163 }
2164 }
2165 }
2166
2167 let fixture_id = &fixture.id;
2168 if fixture.needs_mock_server() {
2169 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2170 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2171 let _ = writeln!(out, " char base_url[1024];");
2172 let _ = writeln!(
2173 out,
2174 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2175 );
2176 let _ = writeln!(
2181 out,
2182 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2183 );
2184 } else {
2185 let _ = writeln!(
2186 out,
2187 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2188 );
2189 }
2190 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2191
2192 let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
2194 let _ = writeln!(out, " uintptr_t out_len = 0;");
2195 let _ = writeln!(out, " uintptr_t out_cap = 0;");
2196
2197 let mut method_args: Vec<String> = Vec::new();
2199 for (_, v) in &request_handle_vars {
2200 method_args.push(v.clone());
2201 }
2202 method_args.extend(string_arg_exprs.iter().cloned());
2203 let extra_args = if method_args.is_empty() {
2204 String::new()
2205 } else {
2206 format!(", {}", method_args.join(", "))
2207 };
2208
2209 let call_fn = format!("{prefix}_default_client_{function_name}");
2210 let _ = writeln!(
2211 out,
2212 " int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
2213 );
2214
2215 if expects_error {
2216 for (_, var_name) in &request_handle_vars {
2217 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2218 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2219 }
2220 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2221 let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
2222 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2225 let _ = writeln!(out, "}}");
2226 return;
2227 }
2228
2229 let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
2230
2231 let mut emitted_len_check = false;
2236 for assertion in &fixture.assertions {
2237 match assertion.assertion_type.as_str() {
2238 "not_error" => {
2239 }
2241 "not_empty" | "not_null" => {
2242 if !emitted_len_check {
2243 let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
2244 emitted_len_check = true;
2245 }
2246 }
2247 _ => {
2248 let _ = writeln!(
2252 out,
2253 " /* skipped: assertion '{}' not meaningful on raw byte buffer */",
2254 assertion.assertion_type
2255 );
2256 }
2257 }
2258 }
2259
2260 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2261 for (_, var_name) in &request_handle_vars {
2262 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2263 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2264 }
2265 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2266 let _ = writeln!(out, "}}");
2267}
2268
2269#[allow(clippy::too_many_arguments)]
2280fn render_chat_stream_test_function(
2281 out: &mut String,
2282 fixture: &Fixture,
2283 prefix: &str,
2284 result_var: &str,
2285 args: &[crate::config::ArgMapping],
2286 options_type_name: &str,
2287 expects_error: bool,
2288 api_key_var: Option<&str>,
2289) {
2290 let prefix_upper = prefix.to_uppercase();
2291
2292 let mut request_var: Option<String> = None;
2293 for arg in args {
2294 if arg.arg_type == "json_object" {
2295 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2296 options_type_name.to_string()
2297 } else {
2298 "ChatCompletionRequest".to_string()
2299 };
2300 let request_type_snake = request_type_pascal.to_snake_case();
2301 let var_name = format!("{request_type_snake}_handle");
2302
2303 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2304 let json_val = if field.is_empty() || field == "input" {
2305 Some(&fixture.input)
2306 } else {
2307 fixture.input.get(field)
2308 };
2309
2310 if let Some(val) = json_val {
2311 if !val.is_null() {
2312 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2313 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2314 let escaped = escape_c(&json_str);
2315 let _ = writeln!(
2316 out,
2317 " {prefix_upper}{request_type_pascal}* {var_name} = \
2318 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2319 );
2320 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2321 request_var = Some(var_name);
2322 break;
2323 }
2324 }
2325 }
2326 }
2327
2328 let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
2329 let req_snake = request_var
2330 .as_ref()
2331 .and_then(|v| v.strip_suffix("_handle"))
2332 .unwrap_or("chat_completion_request")
2333 .to_string();
2334
2335 let fixture_id = &fixture.id;
2336 let has_mock = fixture.needs_mock_server();
2337 if has_mock && api_key_var.is_some() {
2338 let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
2344 let _ = writeln!(
2345 out,
2346 " {prefix_upper}DefaultClient* client = {prefix}_create_client(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
2347 );
2348 } else if has_mock {
2349 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2350 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2351 let _ = writeln!(out, " char base_url[1024];");
2352 let _ = writeln!(
2353 out,
2354 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2355 );
2356 let _ = writeln!(
2361 out,
2362 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2363 );
2364 } else {
2365 let _ = writeln!(
2366 out,
2367 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2368 );
2369 }
2370 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2371
2372 let _ = writeln!(
2373 out,
2374 " {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
2375 {prefix}_default_client_chat_stream_start(client, {req_handle});"
2376 );
2377
2378 if expects_error {
2379 let _ = writeln!(
2380 out,
2381 " assert(stream_handle == NULL && \"expected stream-start to fail\");"
2382 );
2383 if request_var.is_some() {
2384 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2385 }
2386 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2387 let _ = writeln!(out, "}}");
2388 return;
2389 }
2390
2391 let _ = writeln!(
2392 out,
2393 " assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2394 );
2395
2396 let _ = writeln!(out, " size_t chunks_count = 0;");
2397 let _ = writeln!(out, " char* stream_content = (char*)malloc(1);");
2398 let _ = writeln!(out, " assert(stream_content != NULL);");
2399 let _ = writeln!(out, " stream_content[0] = '\\0';");
2400 let _ = writeln!(out, " size_t stream_content_len = 0;");
2401 let _ = writeln!(out, " int stream_complete = 0;");
2402 let _ = writeln!(out, " int no_chunks_after_done = 1;");
2403 let _ = writeln!(out, " char* last_choices_json = NULL;");
2404 let _ = writeln!(out, " uint64_t total_tokens = 0;");
2405 let _ = writeln!(out);
2406
2407 let _ = writeln!(out, " while (1) {{");
2408 let _ = writeln!(
2409 out,
2410 " {prefix_upper}ChatCompletionChunk* {result_var} = \
2411 {prefix}_default_client_chat_stream_next(stream_handle);"
2412 );
2413 let _ = writeln!(out, " if ({result_var} == NULL) {{");
2414 let _ = writeln!(
2415 out,
2416 " if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2417 );
2418 let _ = writeln!(out, " break;");
2419 let _ = writeln!(out, " }}");
2420 let _ = writeln!(out, " chunks_count++;");
2421 let _ = writeln!(
2422 out,
2423 " char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2424 );
2425 let _ = writeln!(out, " if (choices_json != NULL) {{");
2426 let _ = writeln!(
2427 out,
2428 " const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2429 );
2430 let _ = writeln!(out, " if (d != NULL) {{");
2431 let _ = writeln!(out, " d += 10;");
2432 let _ = writeln!(out, " while (*d == ' ' || *d == '\\t') d++;");
2433 let _ = writeln!(out, " if (*d == '\"') {{");
2434 let _ = writeln!(out, " d++;");
2435 let _ = writeln!(out, " const char* e = d;");
2436 let _ = writeln!(out, " while (*e && *e != '\"') {{");
2437 let _ = writeln!(
2438 out,
2439 " if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2440 );
2441 let _ = writeln!(out, " }}");
2442 let _ = writeln!(out, " size_t add = (size_t)(e - d);");
2443 let _ = writeln!(out, " if (add > 0) {{");
2444 let _ = writeln!(
2445 out,
2446 " char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2447 );
2448 let _ = writeln!(out, " if (nc != NULL) {{");
2449 let _ = writeln!(out, " stream_content = nc;");
2450 let _ = writeln!(
2451 out,
2452 " memcpy(stream_content + stream_content_len, d, add);"
2453 );
2454 let _ = writeln!(out, " stream_content_len += add;");
2455 let _ = writeln!(
2456 out,
2457 " stream_content[stream_content_len] = '\\0';"
2458 );
2459 let _ = writeln!(out, " }}");
2460 let _ = writeln!(out, " }}");
2461 let _ = writeln!(out, " }}");
2462 let _ = writeln!(out, " }}");
2463 let _ = writeln!(
2464 out,
2465 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2466 );
2467 let _ = writeln!(out, " last_choices_json = choices_json;");
2468 let _ = writeln!(out, " }}");
2469 let _ = writeln!(
2470 out,
2471 " {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2472 );
2473 let _ = writeln!(out, " if (usage_handle != NULL) {{");
2474 let _ = writeln!(
2475 out,
2476 " total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2477 );
2478 let _ = writeln!(out, " {prefix}_usage_free(usage_handle);");
2479 let _ = writeln!(out, " }}");
2480 let _ = writeln!(out, " {prefix}_chat_completion_chunk_free({result_var});");
2481 let _ = writeln!(out, " }}");
2482 let _ = writeln!(out, " {prefix}_default_client_chat_stream_free(stream_handle);");
2483 let _ = writeln!(out);
2484
2485 let _ = writeln!(out, " char* finish_reason = NULL;");
2486 let _ = writeln!(out, " char* tool_calls_json = NULL;");
2487 let _ = writeln!(out, " char* tool_calls_0_function_name = NULL;");
2488 let _ = writeln!(out, " if (last_choices_json != NULL) {{");
2489 let _ = writeln!(
2490 out,
2491 " finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2492 );
2493 let _ = writeln!(
2494 out,
2495 " const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2496 );
2497 let _ = writeln!(out, " if (tc != NULL) {{");
2498 let _ = writeln!(out, " tc += 13;");
2499 let _ = writeln!(out, " while (*tc == ' ' || *tc == '\\t') tc++;");
2500 let _ = writeln!(out, " if (*tc == '[') {{");
2501 let _ = writeln!(out, " int depth = 0;");
2502 let _ = writeln!(out, " const char* end = tc;");
2503 let _ = writeln!(out, " int in_str = 0;");
2504 let _ = writeln!(out, " for (; *end; end++) {{");
2505 let _ = writeln!(
2506 out,
2507 " if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2508 );
2509 let _ = writeln!(
2510 out,
2511 " if (*end == '\"') {{ in_str = !in_str; continue; }}"
2512 );
2513 let _ = writeln!(out, " if (in_str) continue;");
2514 let _ = writeln!(out, " if (*end == '[' || *end == '{{') depth++;");
2515 let _ = writeln!(
2516 out,
2517 " else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2518 );
2519 let _ = writeln!(out, " }}");
2520 let _ = writeln!(out, " size_t tlen = (size_t)(end - tc);");
2521 let _ = writeln!(out, " tool_calls_json = (char*)malloc(tlen + 1);");
2522 let _ = writeln!(out, " if (tool_calls_json != NULL) {{");
2523 let _ = writeln!(out, " memcpy(tool_calls_json, tc, tlen);");
2524 let _ = writeln!(out, " tool_calls_json[tlen] = '\\0';");
2525 let _ = writeln!(
2526 out,
2527 " const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2528 );
2529 let _ = writeln!(out, " if (fn != NULL) {{");
2530 let _ = writeln!(
2531 out,
2532 " const char* np = strstr(fn, \"\\\"name\\\":\");"
2533 );
2534 let _ = writeln!(out, " if (np != NULL) {{");
2535 let _ = writeln!(out, " np += 7;");
2536 let _ = writeln!(
2537 out,
2538 " while (*np == ' ' || *np == '\\t') np++;"
2539 );
2540 let _ = writeln!(out, " if (*np == '\"') {{");
2541 let _ = writeln!(out, " np++;");
2542 let _ = writeln!(out, " const char* ne = np;");
2543 let _ = writeln!(
2544 out,
2545 " while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2546 );
2547 let _ = writeln!(out, " size_t nlen = (size_t)(ne - np);");
2548 let _ = writeln!(
2549 out,
2550 " tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2551 );
2552 let _ = writeln!(
2553 out,
2554 " if (tool_calls_0_function_name != NULL) {{"
2555 );
2556 let _ = writeln!(
2557 out,
2558 " memcpy(tool_calls_0_function_name, np, nlen);"
2559 );
2560 let _ = writeln!(
2561 out,
2562 " tool_calls_0_function_name[nlen] = '\\0';"
2563 );
2564 let _ = writeln!(out, " }}");
2565 let _ = writeln!(out, " }}");
2566 let _ = writeln!(out, " }}");
2567 let _ = writeln!(out, " }}");
2568 let _ = writeln!(out, " }}");
2569 let _ = writeln!(out, " }}");
2570 let _ = writeln!(out, " }}");
2571 let _ = writeln!(out, " }}");
2572 let _ = writeln!(out);
2573
2574 for assertion in &fixture.assertions {
2575 emit_chat_stream_assertion(out, assertion);
2576 }
2577
2578 let _ = writeln!(out, " free(stream_content);");
2579 let _ = writeln!(
2580 out,
2581 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2582 );
2583 let _ = writeln!(out, " if (finish_reason != NULL) free(finish_reason);");
2584 let _ = writeln!(out, " if (tool_calls_json != NULL) free(tool_calls_json);");
2585 let _ = writeln!(
2586 out,
2587 " if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2588 );
2589 if request_var.is_some() {
2590 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2591 }
2592 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2593 let _ = writeln!(
2594 out,
2595 " /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2596 (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2597 );
2598 let _ = writeln!(out, "}}");
2599}
2600
2601fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2605 let field = assertion.field.as_deref().unwrap_or("");
2606
2607 enum Kind {
2608 IntCount,
2609 Bool,
2610 Str,
2611 IntTokens,
2612 Unsupported,
2613 }
2614
2615 let (expr, kind) = match field {
2616 "chunks" => ("chunks_count", Kind::IntCount),
2617 "stream_content" => ("stream_content", Kind::Str),
2618 "stream_complete" => ("stream_complete", Kind::Bool),
2619 "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2620 "finish_reason" => ("finish_reason", Kind::Str),
2621 "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2630 "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2631 _ => ("", Kind::Unsupported),
2632 };
2633
2634 let atype = assertion.assertion_type.as_str();
2635 if atype == "not_error" || atype == "error" {
2636 return;
2637 }
2638
2639 if matches!(kind, Kind::Unsupported) {
2640 let _ = writeln!(
2641 out,
2642 " /* skipped: streaming assertion on unsupported field '{field}' */"
2643 );
2644 return;
2645 }
2646
2647 match (atype, &kind) {
2648 ("count_min", Kind::IntCount) => {
2649 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2650 let _ = writeln!(out, " assert({expr} >= {n} && \"expected at least {n} chunks\");");
2651 }
2652 }
2653 ("equals", Kind::Str) => {
2654 if let Some(val) = &assertion.value {
2655 let c_val = json_to_c(val);
2656 let _ = writeln!(
2657 out,
2658 " assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2659 );
2660 }
2661 }
2662 ("contains", Kind::Str) => {
2663 if let Some(val) = &assertion.value {
2664 let c_val = json_to_c(val);
2665 let _ = writeln!(
2666 out,
2667 " assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2668 );
2669 }
2670 }
2671 ("not_empty", Kind::Str) => {
2672 let _ = writeln!(
2673 out,
2674 " assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2675 );
2676 }
2677 ("is_true", Kind::Bool) => {
2678 let _ = writeln!(out, " assert({expr} && \"expected {field} to be true\");");
2679 }
2680 ("is_false", Kind::Bool) => {
2681 let _ = writeln!(out, " assert(!{expr} && \"expected {field} to be false\");");
2682 }
2683 ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2684 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2685 let _ = writeln!(out, " assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2686 }
2687 }
2688 ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2689 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2690 let _ = writeln!(out, " assert({expr} == {n} && \"equals assertion failed\");");
2691 }
2692 }
2693 _ => {
2694 let _ = writeln!(
2695 out,
2696 " /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2697 );
2698 }
2699 }
2700}
2701
2702#[allow(clippy::too_many_arguments)]
2716fn emit_nested_accessor(
2717 out: &mut String,
2718 prefix: &str,
2719 resolved: &str,
2720 local_var: &str,
2721 result_var: &str,
2722 fields_c_types: &HashMap<String, String>,
2723 fields_enum: &HashSet<String>,
2724 intermediate_handles: &mut Vec<(String, String)>,
2725 result_type_name: &str,
2726 raw_field: &str,
2727) -> Option<String> {
2728 let segments: Vec<&str> = resolved.split('.').collect();
2729 let prefix_upper = prefix.to_uppercase();
2730
2731 let mut current_snake_type = result_type_name.to_snake_case();
2733 let mut current_handle = result_var.to_string();
2734 let mut json_extract_mode = false;
2737
2738 for (i, segment) in segments.iter().enumerate() {
2739 let is_leaf = i + 1 == segments.len();
2740
2741 if json_extract_mode {
2745 let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2750 Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2751 None => (segment, None),
2752 };
2753 let seg_snake = bare_segment.to_snake_case();
2754 if is_leaf {
2755 let _ = writeln!(
2756 out,
2757 " char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2758 );
2759 return None; }
2761 let json_var = format!("{seg_snake}_json");
2766 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2767 let _ = writeln!(
2768 out,
2769 " char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2770 );
2771 intermediate_handles.push((json_var.clone(), "free".to_string()));
2772 }
2773 if let Some(key) = bracket_key {
2777 if let Ok(idx) = key.parse::<usize>() {
2778 let elem_var = format!("{seg_snake}_{idx}_json");
2779 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2780 let _ = writeln!(
2781 out,
2782 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2783 );
2784 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2785 }
2786 current_handle = elem_var;
2787 continue;
2788 }
2789 }
2790 current_handle = json_var;
2791 continue;
2792 }
2793
2794 if let Some(bracket_pos) = segment.find('[') {
2796 let field_name = &segment[..bracket_pos];
2797 let key = segment[bracket_pos + 1..].trim_end_matches(']');
2798 let field_snake = field_name.to_snake_case();
2799 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2800
2801 let json_var = format!("{field_snake}_json");
2803 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2804 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2805 let _ = writeln!(out, " assert({json_var} != NULL);");
2806 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2808 }
2809
2810 if key.is_empty() {
2816 if !is_leaf {
2817 current_handle = json_var;
2818 json_extract_mode = true;
2819 continue;
2820 }
2821 return None;
2822 }
2823 if let Ok(idx) = key.parse::<usize>() {
2824 let elem_var = format!("{field_snake}_{idx}_json");
2825 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2826 let _ = writeln!(
2827 out,
2828 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2829 );
2830 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2831 }
2832 if !is_leaf {
2833 current_handle = elem_var;
2834 json_extract_mode = true;
2835 continue;
2836 }
2837 return None;
2839 }
2840
2841 let _ = writeln!(
2843 out,
2844 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
2845 );
2846 return None; }
2848
2849 let seg_snake = segment.to_snake_case();
2850 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
2851
2852 if is_leaf {
2853 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2856 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2857 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
2858 return Some(t.clone());
2859 }
2860 if try_emit_enum_accessor(
2862 out,
2863 prefix,
2864 &prefix_upper,
2865 raw_field,
2866 &seg_snake,
2867 ¤t_snake_type,
2868 &accessor_fn,
2869 ¤t_handle,
2870 local_var,
2871 fields_c_types,
2872 fields_enum,
2873 intermediate_handles,
2874 ) {
2875 return None;
2876 }
2877 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
2878 } else {
2879 let lookup_key = format!("{current_snake_type}.{seg_snake}");
2881 let return_type_pascal = match fields_c_types.get(&lookup_key) {
2882 Some(t) => t.clone(),
2883 None => {
2884 segment.to_pascal_case()
2886 }
2887 };
2888
2889 if return_type_pascal == "char*" {
2892 let json_var = format!("{seg_snake}_json");
2893 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2894 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2895 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2896 }
2897 if i + 2 == segments.len() && segments[i + 1] == "length" {
2899 let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
2900 return Some("int".to_string());
2901 }
2902 current_snake_type = seg_snake.clone();
2903 current_handle = json_var;
2904 continue;
2905 }
2906
2907 let return_snake = return_type_pascal.to_snake_case();
2908 let handle_var = format!("{seg_snake}_handle");
2909
2910 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
2913 let _ = writeln!(
2914 out,
2915 " {prefix_upper}{return_type_pascal}* {handle_var} = \
2916 {accessor_fn}({current_handle});"
2917 );
2918 let _ = writeln!(out, " assert({handle_var} != NULL);");
2919 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
2920 }
2921
2922 current_snake_type = return_snake;
2923 current_handle = handle_var;
2924 }
2925 }
2926 None
2927}
2928
2929fn build_args_string_c(
2933 input: &serde_json::Value,
2934 args: &[crate::config::ArgMapping],
2935 has_options_handle: bool,
2936) -> String {
2937 if args.is_empty() {
2938 return json_to_c(input);
2939 }
2940
2941 let parts: Vec<String> = args
2942 .iter()
2943 .filter_map(|arg| {
2944 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2945 let val = input.get(field);
2946 match val {
2947 None if arg.optional => Some("NULL".to_string()),
2949 None => None,
2951 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
2953 Some(v) => {
2954 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
2957 Some("options_handle".to_string())
2958 } else {
2959 Some(json_to_c(v))
2960 }
2961 }
2962 }
2963 })
2964 .collect();
2965
2966 parts.join(", ")
2967}
2968
2969#[allow(clippy::too_many_arguments)]
2970fn render_assertion(
2971 out: &mut String,
2972 assertion: &Assertion,
2973 result_var: &str,
2974 ffi_prefix: &str,
2975 _field_resolver: &FieldResolver,
2976 accessed_fields: &[(String, String, bool)],
2977 primitive_locals: &HashMap<String, String>,
2978 opaque_handle_locals: &HashMap<String, String>,
2979) {
2980 if let Some(f) = &assertion.field {
2982 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
2983 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
2984 return;
2985 }
2986 }
2987
2988 let field_expr = match &assertion.field {
2989 Some(f) if !f.is_empty() => {
2990 accessed_fields
2992 .iter()
2993 .find(|(k, _, _)| k == f)
2994 .map(|(_, local, _)| local.clone())
2995 .unwrap_or_else(|| result_var.to_string())
2996 }
2997 _ => result_var.to_string(),
2998 };
2999
3000 let field_is_primitive = primitive_locals.contains_key(&field_expr);
3001 let field_primitive_type = primitive_locals.get(&field_expr).cloned();
3002 let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
3007 let field_is_map_access = if let Some(f) = &assertion.field {
3011 accessed_fields.iter().any(|(k, _, m)| k == f && *m)
3012 } else {
3013 false
3014 };
3015
3016 let assertion_field_is_optional = assertion
3020 .field
3021 .as_deref()
3022 .map(|f| {
3023 if f.is_empty() {
3024 return false;
3025 }
3026 if _field_resolver.is_optional(f) {
3027 return true;
3028 }
3029 let resolved = _field_resolver.resolve(f);
3031 _field_resolver.is_optional(resolved)
3032 })
3033 .unwrap_or(false);
3034
3035 match assertion.assertion_type.as_str() {
3036 "equals" => {
3037 if let Some(expected) = &assertion.value {
3038 let c_val = json_to_c(expected);
3039 if field_is_primitive {
3040 let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
3041 match expected.as_bool() {
3042 Some(true) => "1".to_string(),
3043 Some(false) => "0".to_string(),
3044 None => c_val,
3045 }
3046 } else {
3047 c_val
3048 };
3049 let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
3052 if assertion_field_is_optional && is_numeric {
3053 let _ = writeln!(
3054 out,
3055 " assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
3056 );
3057 } else {
3058 let _ = writeln!(
3059 out,
3060 " assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
3061 );
3062 }
3063 } else if expected.is_string() {
3064 let _ = writeln!(
3065 out,
3066 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3067 );
3068 } else if field_is_map_access && expected.is_boolean() {
3069 let lit = match expected.as_bool() {
3070 Some(true) => "\"true\"",
3071 _ => "\"false\"",
3072 };
3073 let _ = writeln!(
3074 out,
3075 " assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
3076 );
3077 } else if field_is_map_access && expected.is_number() {
3078 if expected.is_f64() {
3079 let _ = writeln!(
3080 out,
3081 " assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
3082 );
3083 } else {
3084 let _ = writeln!(
3085 out,
3086 " assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
3087 );
3088 }
3089 } else {
3090 let _ = writeln!(
3091 out,
3092 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3093 );
3094 }
3095 }
3096 }
3097 "contains" => {
3098 if let Some(expected) = &assertion.value {
3099 let c_val = json_to_c(expected);
3100 let _ = writeln!(
3101 out,
3102 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3103 );
3104 }
3105 }
3106 "contains_all" => {
3107 if let Some(values) = &assertion.values {
3108 for val in values {
3109 let c_val = json_to_c(val);
3110 let _ = writeln!(
3111 out,
3112 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3113 );
3114 }
3115 }
3116 }
3117 "not_contains" => {
3118 if let Some(expected) = &assertion.value {
3119 let c_val = json_to_c(expected);
3120 let _ = writeln!(
3121 out,
3122 " assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3123 );
3124 }
3125 }
3126 "not_empty" => {
3127 if field_is_opaque_handle {
3128 let _ = writeln!(out, " assert({field_expr} != NULL && \"expected non-null handle\");");
3132 } else {
3133 let _ = writeln!(
3134 out,
3135 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
3136 );
3137 }
3138 }
3139 "is_empty" => {
3140 if field_is_opaque_handle {
3141 let _ = writeln!(out, " assert({field_expr} == NULL && \"expected null handle\");");
3142 } else if assertion_field_is_optional || !field_is_primitive {
3143 let _ = writeln!(
3145 out,
3146 " assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
3147 );
3148 } else {
3149 let _ = writeln!(
3150 out,
3151 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
3152 );
3153 }
3154 }
3155 "contains_any" => {
3156 if let Some(values) = &assertion.values {
3157 let _ = writeln!(out, " {{");
3158 let _ = writeln!(out, " int found = 0;");
3159 for val in values {
3160 let c_val = json_to_c(val);
3161 let _ = writeln!(
3162 out,
3163 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
3164 );
3165 }
3166 let _ = writeln!(
3167 out,
3168 " assert(found && \"expected to contain at least one of the specified values\");"
3169 );
3170 let _ = writeln!(out, " }}");
3171 }
3172 }
3173 "greater_than" => {
3174 if let Some(val) = &assertion.value {
3175 let c_val = json_to_c(val);
3176 if field_is_map_access && val.is_number() && !field_is_primitive {
3177 let _ = writeln!(
3178 out,
3179 " assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
3180 );
3181 } else {
3182 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
3183 }
3184 }
3185 }
3186 "less_than" => {
3187 if let Some(val) = &assertion.value {
3188 let c_val = json_to_c(val);
3189 if field_is_map_access && val.is_number() && !field_is_primitive {
3190 let _ = writeln!(
3191 out,
3192 " assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
3193 );
3194 } else {
3195 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
3196 }
3197 }
3198 }
3199 "greater_than_or_equal" => {
3200 if let Some(val) = &assertion.value {
3201 let c_val = json_to_c(val);
3202 if field_is_map_access && val.is_number() && !field_is_primitive {
3203 let _ = writeln!(
3204 out,
3205 " assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
3206 );
3207 } else {
3208 let _ = writeln!(
3209 out,
3210 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
3211 );
3212 }
3213 }
3214 }
3215 "less_than_or_equal" => {
3216 if let Some(val) = &assertion.value {
3217 let c_val = json_to_c(val);
3218 if field_is_map_access && val.is_number() && !field_is_primitive {
3219 let _ = writeln!(
3220 out,
3221 " assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
3222 );
3223 } else {
3224 let _ = writeln!(
3225 out,
3226 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
3227 );
3228 }
3229 }
3230 }
3231 "starts_with" => {
3232 if let Some(expected) = &assertion.value {
3233 let c_val = json_to_c(expected);
3234 let _ = writeln!(
3235 out,
3236 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
3237 );
3238 }
3239 }
3240 "ends_with" => {
3241 if let Some(expected) = &assertion.value {
3242 let c_val = json_to_c(expected);
3243 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
3244 let _ = writeln!(
3245 out,
3246 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
3247 );
3248 }
3249 }
3250 "min_length" => {
3251 if let Some(val) = &assertion.value {
3252 if let Some(n) = val.as_u64() {
3253 let _ = writeln!(
3254 out,
3255 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
3256 );
3257 }
3258 }
3259 }
3260 "max_length" => {
3261 if let Some(val) = &assertion.value {
3262 if let Some(n) = val.as_u64() {
3263 let _ = writeln!(
3264 out,
3265 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
3266 );
3267 }
3268 }
3269 }
3270 "count_min" => {
3271 if let Some(val) = &assertion.value {
3272 if let Some(n) = val.as_u64() {
3273 let _ = writeln!(out, " {{");
3274 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
3275 let _ = writeln!(
3276 out,
3277 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3278 );
3279 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3280 let _ = writeln!(
3281 out,
3282 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
3283 );
3284 let _ = writeln!(out, " }}");
3285 }
3286 }
3287 }
3288 "count_equals" => {
3289 if let Some(val) = &assertion.value {
3290 if let Some(n) = val.as_u64() {
3291 let _ = writeln!(out, " {{");
3292 let _ = writeln!(out, " /* count_equals: count elements in array */");
3293 let _ = writeln!(
3294 out,
3295 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3296 );
3297 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3298 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
3299 let _ = writeln!(out, " }}");
3300 }
3301 }
3302 }
3303 "is_true" => {
3304 let _ = writeln!(out, " assert({field_expr});");
3305 }
3306 "is_false" => {
3307 let _ = writeln!(out, " assert(!{field_expr});");
3308 }
3309 "method_result" => {
3310 if let Some(method_name) = &assertion.method {
3311 render_method_result_assertion(
3312 out,
3313 result_var,
3314 ffi_prefix,
3315 method_name,
3316 assertion.args.as_ref(),
3317 assertion.return_type.as_deref(),
3318 assertion.check.as_deref().unwrap_or("is_true"),
3319 assertion.value.as_ref(),
3320 );
3321 } else {
3322 panic!("C e2e generator: method_result assertion missing 'method' field");
3323 }
3324 }
3325 "matches_regex" => {
3326 if let Some(expected) = &assertion.value {
3327 let c_val = json_to_c(expected);
3328 let _ = writeln!(out, " {{");
3329 let _ = writeln!(out, " regex_t _re;");
3330 let _ = writeln!(
3331 out,
3332 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3333 );
3334 let _ = writeln!(
3335 out,
3336 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3337 );
3338 let _ = writeln!(out, " regfree(&_re);");
3339 let _ = writeln!(out, " }}");
3340 }
3341 }
3342 "not_error" => {
3343 }
3345 "error" => {
3346 }
3348 other => {
3349 panic!("C e2e generator: unsupported assertion type: {other}");
3350 }
3351 }
3352}
3353
3354#[allow(clippy::too_many_arguments)]
3363fn render_method_result_assertion(
3364 out: &mut String,
3365 result_var: &str,
3366 ffi_prefix: &str,
3367 method_name: &str,
3368 args: Option<&serde_json::Value>,
3369 return_type: Option<&str>,
3370 check: &str,
3371 value: Option<&serde_json::Value>,
3372) {
3373 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3374
3375 if return_type == Some("string") {
3376 let _ = writeln!(out, " {{");
3378 let _ = writeln!(out, " char* _method_result = {call_expr};");
3379 if check == "is_error" {
3380 let _ = writeln!(
3381 out,
3382 " assert(_method_result == NULL && \"expected method to return error\");"
3383 );
3384 let _ = writeln!(out, " }}");
3385 return;
3386 }
3387 let _ = writeln!(
3388 out,
3389 " assert(_method_result != NULL && \"method_result returned NULL\");"
3390 );
3391 match check {
3392 "contains" => {
3393 if let Some(val) = value {
3394 let c_val = json_to_c(val);
3395 let _ = writeln!(
3396 out,
3397 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3398 );
3399 }
3400 }
3401 "equals" => {
3402 if let Some(val) = value {
3403 let c_val = json_to_c(val);
3404 let _ = writeln!(
3405 out,
3406 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3407 );
3408 }
3409 }
3410 "is_true" => {
3411 let _ = writeln!(
3412 out,
3413 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3414 );
3415 }
3416 "count_min" => {
3417 if let Some(val) = value {
3418 let n = val.as_u64().unwrap_or(0);
3419 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
3420 let _ = writeln!(
3421 out,
3422 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3423 );
3424 }
3425 }
3426 other_check => {
3427 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3428 }
3429 }
3430 let _ = writeln!(out, " free(_method_result);");
3431 let _ = writeln!(out, " }}");
3432 return;
3433 }
3434
3435 match check {
3437 "equals" => {
3438 if let Some(val) = value {
3439 let c_val = json_to_c(val);
3440 let _ = writeln!(
3441 out,
3442 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3443 );
3444 }
3445 }
3446 "is_true" => {
3447 let _ = writeln!(
3448 out,
3449 " assert({call_expr} && \"method_result is_true assertion failed\");"
3450 );
3451 }
3452 "is_false" => {
3453 let _ = writeln!(
3454 out,
3455 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
3456 );
3457 }
3458 "greater_than_or_equal" => {
3459 if let Some(val) = value {
3460 let n = val.as_u64().unwrap_or(0);
3461 let _ = writeln!(
3462 out,
3463 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3464 );
3465 }
3466 }
3467 "count_min" => {
3468 if let Some(val) = value {
3469 let n = val.as_u64().unwrap_or(0);
3470 let _ = writeln!(
3471 out,
3472 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3473 );
3474 }
3475 }
3476 other_check => {
3477 panic!("C e2e generator: unsupported method_result check type: {other_check}");
3478 }
3479 }
3480}
3481
3482fn build_c_method_call(
3489 result_var: &str,
3490 ffi_prefix: &str,
3491 method_name: &str,
3492 args: Option<&serde_json::Value>,
3493) -> String {
3494 let extra_args = if let Some(args_val) = args {
3495 args_val
3496 .as_object()
3497 .map(|obj| {
3498 obj.values()
3499 .map(|v| match v {
3500 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3501 serde_json::Value::Bool(true) => "1".to_string(),
3502 serde_json::Value::Bool(false) => "0".to_string(),
3503 serde_json::Value::Number(n) => n.to_string(),
3504 serde_json::Value::Null => "NULL".to_string(),
3505 other => format!("\"{}\"", escape_c(&other.to_string())),
3506 })
3507 .collect::<Vec<_>>()
3508 .join(", ")
3509 })
3510 .unwrap_or_default()
3511 } else {
3512 String::new()
3513 };
3514
3515 if extra_args.is_empty() {
3516 format!("{ffi_prefix}_{method_name}({result_var})")
3517 } else {
3518 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3519 }
3520}
3521
3522fn json_to_c(value: &serde_json::Value) -> String {
3524 match value {
3525 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3526 serde_json::Value::Bool(true) => "1".to_string(),
3527 serde_json::Value::Bool(false) => "0".to_string(),
3528 serde_json::Value::Number(n) => n.to_string(),
3529 serde_json::Value::Null => "NULL".to_string(),
3530 other => format!("\"{}\"", escape_c(&other.to_string())),
3531 }
3532}