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