1use crate::config::{CallConfig, E2eConfig};
9use crate::escape::{escape_c, sanitize_filename, sanitize_ident};
10use crate::field_access::FieldResolver;
11use crate::fixture::{Assertion, Fixture, FixtureGroup};
12use alef_core::backend::GeneratedFile;
13use alef_core::config::ResolvedCrateConfig;
14use alef_core::hash::{self, CommentStyle};
15use anyhow::Result;
16use heck::{ToPascalCase, ToSnakeCase};
17use std::collections::{HashMap, HashSet};
18use std::fmt::Write as FmtWrite;
19use std::path::PathBuf;
20
21use super::E2eCodegen;
22
23pub struct CCodegen;
25
26fn is_primitive_c_type(t: &str) -> bool {
30 matches!(
31 t,
32 "uint8_t"
33 | "uint16_t"
34 | "uint32_t"
35 | "uint64_t"
36 | "int8_t"
37 | "int16_t"
38 | "int32_t"
39 | "int64_t"
40 | "uintptr_t"
41 | "intptr_t"
42 | "size_t"
43 | "ssize_t"
44 | "double"
45 | "float"
46 | "bool"
47 | "int"
48 )
49}
50
51fn is_skipped_c_field(fields_c_types: &HashMap<String, String>, parent_snake: &str, field_snake: &str) -> bool {
55 let key = format!("{parent_snake}.{field_snake}");
56 fields_c_types.get(&key).is_some_and(|t| t == "skip")
57}
58
59fn infer_opaque_handle_type(
77 fields_c_types: &HashMap<String, String>,
78 parent_snake_type: &str,
79 field_snake: &str,
80) -> Option<String> {
81 let lookup_key = format!("{parent_snake_type}.{field_snake}");
82 if let Some(t) = fields_c_types.get(&lookup_key) {
83 if !is_primitive_c_type(t) && t != "char*" {
84 return Some(t.clone());
85 }
86 return None;
88 }
89 let nested_prefix = format!("{field_snake}.");
91 if fields_c_types.keys().any(|k| k.starts_with(&nested_prefix)) {
92 return Some(field_snake.to_pascal_case());
93 }
94 None
95}
96
97#[allow(clippy::too_many_arguments)]
114fn try_emit_enum_accessor(
115 out: &mut String,
116 prefix: &str,
117 prefix_upper: &str,
118 raw_field: &str,
119 resolved_field: &str,
120 parent_snake_type: &str,
121 accessor_fn: &str,
122 parent_handle: &str,
123 local_var: &str,
124 fields_c_types: &HashMap<String, String>,
125 fields_enum: &HashSet<String>,
126 intermediate_handles: &mut Vec<(String, String)>,
127) -> bool {
128 if !(fields_enum.contains(raw_field) || fields_enum.contains(resolved_field)) {
129 return false;
130 }
131 let lookup_key = format!("{parent_snake_type}.{resolved_field}");
132 let Some(enum_pascal) = fields_c_types.get(&lookup_key) else {
133 return false;
134 };
135 if is_primitive_c_type(enum_pascal) || enum_pascal == "char*" {
136 return false;
137 }
138 let enum_snake = enum_pascal.to_snake_case();
139 let handle_var = format!("{local_var}_handle");
140 let _ = writeln!(
141 out,
142 " {prefix_upper}{enum_pascal}* {handle_var} = {accessor_fn}({parent_handle});"
143 );
144 let _ = writeln!(out, " assert({handle_var} != NULL);");
145 let _ = writeln!(
146 out,
147 " char* {local_var} = {prefix}_{enum_snake}_to_string({handle_var});"
148 );
149 intermediate_handles.push((handle_var, enum_snake));
150 true
151}
152
153impl E2eCodegen for CCodegen {
154 fn generate(
155 &self,
156 groups: &[FixtureGroup],
157 e2e_config: &E2eConfig,
158 config: &ResolvedCrateConfig,
159 _type_defs: &[alef_core::ir::TypeDef],
160 _enums: &[alef_core::ir::EnumDef],
161 ) -> Result<Vec<GeneratedFile>> {
162 let lang = self.language_name();
163 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
164
165 let mut files = Vec::new();
166
167 let call = &e2e_config.call;
169 let overrides = call.overrides.get(lang);
170 let result_var = &call.result_var;
171 let prefix = overrides
172 .and_then(|o| o.prefix.as_ref())
173 .cloned()
174 .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
175 .unwrap_or_default();
176 let header = overrides
177 .and_then(|o| o.header.as_ref())
178 .cloned()
179 .unwrap_or_else(|| config.ffi_header_name());
180
181 let c_pkg = e2e_config.resolve_package("c");
183 let lib_name = c_pkg
184 .as_ref()
185 .and_then(|p| p.name.as_ref())
186 .cloned()
187 .unwrap_or_else(|| config.ffi_lib_name());
188
189 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
191 .iter()
192 .filter_map(|group| {
193 let active: Vec<&Fixture> = group
194 .fixtures
195 .iter()
196 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
197 .filter(|f| f.visitor.is_none())
198 .collect();
199 if active.is_empty() { None } else { Some((group, active)) }
200 })
201 .collect();
202
203 let visitor_fixtures: Vec<&Fixture> = groups
205 .iter()
206 .flat_map(|group| group.fixtures.iter())
207 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
208 .filter(|f| f.visitor.is_some())
209 .collect();
210
211 let ffi_crate_path = c_pkg
219 .as_ref()
220 .and_then(|p| p.path.as_ref())
221 .cloned()
222 .unwrap_or_else(|| config.ffi_crate_path());
223
224 let mut category_names: Vec<String> = active_groups
226 .iter()
227 .map(|(g, _)| sanitize_filename(&g.category))
228 .collect();
229 if !visitor_fixtures.is_empty() {
230 category_names.push("visitor".to_string());
231 }
232 let needs_mock_server = active_groups
233 .iter()
234 .flat_map(|(_, fixtures)| fixtures.iter())
235 .any(|f| f.needs_mock_server());
236 files.push(GeneratedFile {
237 path: output_base.join("Makefile"),
238 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name, needs_mock_server),
239 generated_header: true,
240 });
241
242 let github_repo = config.github_repo();
244 let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
245 let ffi_pkg_name = e2e_config
246 .registry
247 .packages
248 .get("c")
249 .and_then(|p| p.name.as_ref())
250 .cloned()
251 .unwrap_or_else(|| lib_name.clone());
252 files.push(GeneratedFile {
253 path: output_base.join("download_ffi.sh"),
254 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
255 generated_header: true,
256 });
257
258 files.push(GeneratedFile {
260 path: output_base.join("test_runner.h"),
261 content: render_test_runner_header(&active_groups, &visitor_fixtures),
262 generated_header: true,
263 });
264
265 files.push(GeneratedFile {
267 path: output_base.join("main.c"),
268 content: render_main_c(&active_groups, &visitor_fixtures),
269 generated_header: true,
270 });
271
272 let field_resolver = FieldResolver::new(
273 &e2e_config.fields,
274 &e2e_config.fields_optional,
275 &e2e_config.result_fields,
276 &e2e_config.fields_array,
277 &std::collections::HashSet::new(),
278 );
279
280 for (group, active) in &active_groups {
284 let filename = format!("test_{}.c", sanitize_filename(&group.category));
285 let content = render_test_file(
286 &group.category,
287 active,
288 &header,
289 &prefix,
290 result_var,
291 e2e_config,
292 lang,
293 &field_resolver,
294 );
295 files.push(GeneratedFile {
296 path: output_base.join(filename),
297 content,
298 generated_header: true,
299 });
300 }
301
302 if !visitor_fixtures.is_empty() {
304 files.push(GeneratedFile {
305 path: output_base.join("test_visitor.c"),
306 content: render_visitor_test_file(&visitor_fixtures, &header, &prefix),
307 generated_header: true,
308 });
309 }
310
311 Ok(files)
312 }
313
314 fn language_name(&self) -> &'static str {
315 "c"
316 }
317}
318
319struct ResolvedCallInfo {
321 function_name: String,
322 result_type_name: String,
323 options_type_name: String,
324 client_factory: Option<String>,
325 args: Vec<crate::config::ArgMapping>,
326 raw_c_result_type: Option<String>,
327 c_free_fn: Option<String>,
328 c_engine_factory: Option<String>,
329 result_is_option: bool,
330 result_is_bytes: bool,
336 extra_args: Vec<String>,
340}
341
342fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
343 let overrides = call.overrides.get(lang);
344 let function_name = overrides
345 .and_then(|o| o.function.as_ref())
346 .cloned()
347 .unwrap_or_else(|| call.function.clone());
348 let result_type_name = overrides
353 .and_then(|o| o.result_type.as_ref())
354 .cloned()
355 .unwrap_or_else(|| call.function.to_pascal_case());
356 let options_type_name = overrides
357 .and_then(|o| o.options_type.as_deref())
358 .unwrap_or("ConversionOptions")
359 .to_string();
360 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
361 let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
362 let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
363 let c_engine_factory = overrides.and_then(|o| o.c_engine_factory.clone());
364 let result_is_option = overrides
365 .and_then(|o| if o.result_is_option { Some(true) } else { None })
366 .unwrap_or(call.result_is_option);
367 let result_is_bytes = call.result_is_bytes || overrides.is_some_and(|o| o.result_is_bytes);
372 let extra_args = overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
373 ResolvedCallInfo {
374 function_name,
375 result_type_name,
376 options_type_name,
377 client_factory,
378 args: call.args.clone(),
379 raw_c_result_type,
380 c_free_fn,
381 c_engine_factory,
382 result_is_option,
383 result_is_bytes,
384 extra_args,
385 }
386}
387
388fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
394 let call = e2e_config.resolve_call_for_fixture(
395 fixture.call.as_deref(),
396 &fixture.id,
397 &fixture.resolved_category(),
398 &fixture.tags,
399 &fixture.input,
400 );
401 let mut info = resolve_call_info(call, lang);
402
403 let default_overrides = e2e_config.call.overrides.get(lang);
404
405 if info.client_factory.is_none() {
408 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
409 info.client_factory = Some(factory.clone());
410 }
411 }
412
413 if info.c_engine_factory.is_none() {
416 if let Some(factory) = default_overrides.and_then(|o| o.c_engine_factory.as_ref()) {
417 info.c_engine_factory = Some(factory.clone());
418 }
419 }
420
421 info
422}
423
424fn render_makefile(
425 categories: &[String],
426 header_name: &str,
427 ffi_crate_path: &str,
428 lib_name: &str,
429 needs_mock_server: bool,
430) -> String {
431 let mut out = String::new();
432 out.push_str(&hash::header(CommentStyle::Hash));
433 let _ = writeln!(out, "CC = gcc");
434 let _ = writeln!(out, "FFI_DIR = ffi");
435 let _ = writeln!(out);
436
437 let link_lib_name = lib_name.replace('-', "_");
442
443 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
445 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
446 let _ = writeln!(
447 out,
448 " LDFLAGS = -L$(FFI_DIR)/lib -l{link_lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
449 );
450 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
451 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
452 let _ = writeln!(
453 out,
454 " LDFLAGS = -L../../target/release -l{link_lib_name} -Wl,-rpath,../../target/release"
455 );
456 let _ = writeln!(out, "else");
457 let _ = writeln!(
458 out,
459 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
460 );
461 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
462 let _ = writeln!(out, "endif");
463 let _ = writeln!(out);
464
465 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
466 let srcs = src_files.join(" ");
467
468 let _ = writeln!(out, "SRCS = main.c {srcs}");
469 let _ = writeln!(out, "TARGET = run_tests");
470 let _ = writeln!(out);
471 let _ = writeln!(out, ".PHONY: all clean test");
472 let _ = writeln!(out);
473 let _ = writeln!(out, "all: $(TARGET)");
474 let _ = writeln!(out);
475 let _ = writeln!(out, "$(TARGET): $(SRCS)");
476 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
477 let _ = writeln!(out);
478
479 if !needs_mock_server {
480 let _ = writeln!(out, "test: $(TARGET)");
482 let _ = writeln!(out, "\t./$(TARGET)");
483 let _ = writeln!(out);
484 let _ = writeln!(out, "clean:");
485 let _ = writeln!(out, "\trm -f $(TARGET)");
486 return out;
487 }
488
489 let _ = writeln!(out, "MOCK_SERVER_BIN ?= ../rust/target/release/mock-server");
499 let _ = writeln!(out, "FIXTURES_DIR ?= ../../fixtures");
500 let _ = writeln!(out);
501 let _ = writeln!(out, "test: $(TARGET)");
502 let _ = writeln!(out, "\t@if [ -n \"$$MOCK_SERVER_URL\" ]; then \\");
503 let _ = writeln!(out, "\t\tif [ -n \"$$MOCK_SERVERS\" ]; then \\");
506 let _ = writeln!(
507 out,
508 "\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()))\"); \\"
509 );
510 let _ = writeln!(out, "\t\tfi; \\");
511 let _ = writeln!(out, "\t\t./$(TARGET); \\");
512 let _ = writeln!(out, "\telse \\");
513 let _ = writeln!(out, "\t\tif [ ! -x \"$(MOCK_SERVER_BIN)\" ]; then \\");
514 let _ = writeln!(
515 out,
516 "\t\t\techo \"mock-server binary not found at $(MOCK_SERVER_BIN); run: cargo build -p mock-server --release\" >&2; \\"
517 );
518 let _ = writeln!(out, "\t\t\texit 1; \\");
519 let _ = writeln!(out, "\t\tfi; \\");
520 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
521 let _ = writeln!(out, "\t\tmkfifo mock_server.stdin; \\");
522 let _ = writeln!(
523 out,
524 "\t\t\"$(MOCK_SERVER_BIN)\" \"$(FIXTURES_DIR)\" <mock_server.stdin >mock_server.stdout 2>&1 & \\"
525 );
526 let _ = writeln!(out, "\t\tMOCK_PID=$$!; \\");
527 let _ = writeln!(out, "\t\texec 9>mock_server.stdin; \\");
528 let _ = writeln!(out, "\t\tMOCK_URL=\"\"; MOCK_SERVERS_JSON=\"\"; \\");
529 let _ = writeln!(out, "\t\tfor _ in $$(seq 1 100); do \\");
531 let _ = writeln!(out, "\t\t\tif [ -s mock_server.stdout ]; then \\");
532 let _ = writeln!(
533 out,
534 "\t\t\t\tMOCK_URL=$$(grep -o 'MOCK_SERVER_URL=[^ ]*' mock_server.stdout | head -1 | cut -d= -f2); \\"
535 );
536 let _ = writeln!(out, "\t\t\t\tif [ -n \"$$MOCK_URL\" ]; then break; fi; \\");
537 let _ = writeln!(out, "\t\t\tfi; \\");
538 let _ = writeln!(out, "\t\t\tsleep 0.05; \\");
539 let _ = writeln!(out, "\t\tdone; \\");
540 let _ = writeln!(
542 out,
543 "\t\tMOCK_SERVERS_JSON=$$(grep -o 'MOCK_SERVERS={{.*}}' mock_server.stdout | head -1 | cut -d= -f2-); \\"
544 );
545 let _ = writeln!(
546 out,
547 "\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; \\"
548 );
549 let _ = writeln!(
551 out,
552 "\t\tif [ -n \"$$MOCK_SERVERS_JSON\" ] && command -v python3 >/dev/null 2>&1; then \\"
553 );
554 let _ = writeln!(
555 out,
556 "\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\"); \\"
557 );
558 let _ = writeln!(out, "\t\tfi; \\");
559 let _ = writeln!(out, "\t\tMOCK_SERVER_URL=\"$$MOCK_URL\" ./$(TARGET); STATUS=$$?; \\");
560 let _ = writeln!(out, "\t\texec 9>&-; \\");
561 let _ = writeln!(out, "\t\tkill $$MOCK_PID 2>/dev/null || true; \\");
562 let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
563 let _ = writeln!(out, "\t\texit $$STATUS; \\");
564 let _ = writeln!(out, "\tfi");
565 let _ = writeln!(out);
566 let _ = writeln!(out, "clean:");
567 let _ = writeln!(out, "\trm -f $(TARGET) mock_server.stdout mock_server.stdin");
568 out
569}
570
571fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
572 let mut out = String::new();
573 let _ = writeln!(out, "#!/usr/bin/env bash");
574 out.push_str(&hash::header(CommentStyle::Hash));
575 let _ = writeln!(out, "set -euo pipefail");
576 let _ = writeln!(out);
577 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
578 let _ = writeln!(out, "VERSION=\"{version}\"");
579 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
580 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
581 let _ = writeln!(out);
582 let _ = writeln!(out, "# Detect OS and architecture.");
583 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
584 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
585 let _ = writeln!(out);
586 let _ = writeln!(out, "case \"$ARCH\" in");
587 let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
588 let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
589 let _ = writeln!(out, "*)");
590 let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
591 let _ = writeln!(out, " exit 1");
592 let _ = writeln!(out, " ;;");
593 let _ = writeln!(out, "esac");
594 let _ = writeln!(out);
595 let _ = writeln!(out, "case \"$OS\" in");
596 let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
597 let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
598 let _ = writeln!(out, "*)");
599 let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
600 let _ = writeln!(out, " exit 1");
601 let _ = writeln!(out, " ;;");
602 let _ = writeln!(out, "esac");
603 let _ = writeln!(out);
604 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
605 let _ = writeln!(
606 out,
607 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
608 );
609 let _ = writeln!(out);
610 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
611 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
612 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
613 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
614 out
615}
616
617fn render_test_runner_header(
618 active_groups: &[(&FixtureGroup, Vec<&Fixture>)],
619 visitor_fixtures: &[&Fixture],
620) -> String {
621 let mut out = String::new();
622 out.push_str(&hash::header(CommentStyle::Block));
623 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
624 let _ = writeln!(out, "#define TEST_RUNNER_H");
625 let _ = writeln!(out);
626 let _ = writeln!(out, "#include <string.h>");
627 let _ = writeln!(out, "#include <stdlib.h>");
628 let _ = writeln!(out);
629 let _ = writeln!(out, "/**");
631 let _ = writeln!(
632 out,
633 " * Compare a string against an expected value, trimming trailing whitespace."
634 );
635 let _ = writeln!(
636 out,
637 " * Returns 0 if the trimmed actual string equals the expected string."
638 );
639 let _ = writeln!(out, " */");
640 let _ = writeln!(
641 out,
642 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
643 );
644 let _ = writeln!(
645 out,
646 " if (actual == NULL || expected == NULL) return actual != expected;"
647 );
648 let _ = writeln!(out, " size_t alen = strlen(actual);");
649 let _ = writeln!(
650 out,
651 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
652 );
653 let _ = writeln!(out, " size_t elen = strlen(expected);");
654 let _ = writeln!(out, " if (alen != elen) return 1;");
655 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
656 let _ = writeln!(out, "}}");
657 let _ = writeln!(out);
658
659 let _ = writeln!(
662 out,
663 "static inline char *alef_json_get_object(const char *json, const char *key);"
664 );
665 let _ = writeln!(out);
666 let _ = writeln!(out, "/**");
667 let _ = writeln!(
668 out,
669 " * Extract a string value for a given key from a JSON object string."
670 );
671 let _ = writeln!(
672 out,
673 " * Returns a heap-allocated copy of the value, or NULL if not found."
674 );
675 let _ = writeln!(out, " * Caller must free() the returned string.");
676 let _ = writeln!(out, " */");
677 let _ = writeln!(
678 out,
679 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
680 );
681 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
682 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
683 let _ = writeln!(out, " size_t key_len = strlen(key);");
684 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
685 let _ = writeln!(out, " if (!pattern) return NULL;");
686 let _ = writeln!(out, " pattern[0] = '\"';");
687 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
688 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
689 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
690 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
691 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
692 let _ = writeln!(out, " free(pattern);");
693 let _ = writeln!(out, " if (!found) return NULL;");
694 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
695 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
696 let _ = writeln!(
697 out,
698 " /* Non-string values (arrays/objects) — fall through to alef_json_get_object so"
699 );
700 let _ = writeln!(
701 out,
702 " leaf accessors over collection-typed fields (Vec<T>, Option<Vec<T>>) work for"
703 );
704 let _ = writeln!(
705 out,
706 " not_empty / count_equals assertions without needing per-field type metadata. */"
707 );
708 let _ = writeln!(out, " if (*found == '{{' || *found == '[') {{");
709 let _ = writeln!(out, " return alef_json_get_object(json, key);");
710 let _ = writeln!(out, " }}");
711 let _ = writeln!(
712 out,
713 " /* Primitive non-string value: extract its raw token (numeric / true / false / null)"
714 );
715 let _ = writeln!(
716 out,
717 " so callers asserting on numeric fields can `atoll`/`atof` the result. */"
718 );
719 let _ = writeln!(out, " if (*found != '\"') {{");
720 let _ = writeln!(out, " const char *p = found;");
721 let _ = writeln!(
722 out,
723 " while (*p && *p != ',' && *p != '}}' && *p != ']' && *p != ' ' && *p != '\\t' && *p != '\\n' && *p != '\\r') p++;"
724 );
725 let _ = writeln!(out, " size_t plen = (size_t)(p - found);");
726 let _ = writeln!(out, " if (plen == 0) return NULL;");
727 let _ = writeln!(out, " char *prim = (char *)malloc(plen + 1);");
728 let _ = writeln!(out, " if (!prim) return NULL;");
729 let _ = writeln!(out, " memcpy(prim, found, plen);");
730 let _ = writeln!(out, " prim[plen] = '\\0';");
731 let _ = writeln!(out, " return prim;");
732 let _ = writeln!(out, " }}");
733 let _ = writeln!(out, " found++; /* skip opening quote */");
734 let _ = writeln!(out, " const char *end = found;");
735 let _ = writeln!(out, " while (*end && *end != '\"') {{");
736 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
737 let _ = writeln!(out, " else end++;");
738 let _ = writeln!(out, " }}");
739 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
740 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
741 let _ = writeln!(out, " if (!result_str) return NULL;");
742 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
743 let _ = writeln!(out, " result_str[val_len] = '\\0';");
744 let _ = writeln!(out, " return result_str;");
745 let _ = writeln!(out, "}}");
746 let _ = writeln!(out);
747 let _ = writeln!(out, "/**");
748 let _ = writeln!(
749 out,
750 " * Extract a JSON object/array value `{{...}}` or `[...]` for a given key from"
751 );
752 let _ = writeln!(
753 out,
754 " * a JSON object string. Returns a heap-allocated copy of the value INCLUDING"
755 );
756 let _ = writeln!(
757 out,
758 " * its surrounding braces, or NULL if the key is missing or its value is a"
759 );
760 let _ = writeln!(out, " * primitive. Caller must free() the returned string.");
761 let _ = writeln!(out, " *");
762 let _ = writeln!(
763 out,
764 " * Used by chained-accessor codegen for intermediate object extraction:"
765 );
766 let _ = writeln!(
767 out,
768 " * `choices[0].message.content` first peels off `message` (an object), then"
769 );
770 let _ = writeln!(out, " * looks up `content` (a string) within the extracted substring.");
771 let _ = writeln!(out, " */");
772 let _ = writeln!(
773 out,
774 "static inline char *alef_json_get_object(const char *json, const char *key) {{"
775 );
776 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
777 let _ = writeln!(out, " size_t key_len = strlen(key);");
778 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 4);");
779 let _ = writeln!(out, " if (!pattern) return NULL;");
780 let _ = writeln!(out, " pattern[0] = '\"';");
781 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
782 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
783 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
784 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
785 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
786 let _ = writeln!(out, " free(pattern);");
787 let _ = writeln!(out, " if (!found) return NULL;");
788 let _ = writeln!(out, " found += key_len + 3;");
789 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
790 let _ = writeln!(out, " char open_ch = *found;");
791 let _ = writeln!(out, " char close_ch;");
792 let _ = writeln!(out, " if (open_ch == '{{') close_ch = '}}';");
793 let _ = writeln!(out, " else if (open_ch == '[') close_ch = ']';");
794 let _ = writeln!(
795 out,
796 " else return NULL; /* primitive — caller should use alef_json_get_string */"
797 );
798 let _ = writeln!(out, " int depth = 0;");
799 let _ = writeln!(out, " int in_string = 0;");
800 let _ = writeln!(out, " const char *end = found;");
801 let _ = writeln!(out, " for (; *end; end++) {{");
802 let _ = writeln!(out, " if (in_string) {{");
803 let _ = writeln!(
804 out,
805 " if (*end == '\\\\' && *(end + 1)) {{ end++; continue; }}"
806 );
807 let _ = writeln!(out, " if (*end == '\"') in_string = 0;");
808 let _ = writeln!(out, " continue;");
809 let _ = writeln!(out, " }}");
810 let _ = writeln!(out, " if (*end == '\"') {{ in_string = 1; continue; }}");
811 let _ = writeln!(out, " if (*end == open_ch) depth++;");
812 let _ = writeln!(out, " else if (*end == close_ch) {{");
813 let _ = writeln!(out, " depth--;");
814 let _ = writeln!(out, " if (depth == 0) {{ end++; break; }}");
815 let _ = writeln!(out, " }}");
816 let _ = writeln!(out, " }}");
817 let _ = writeln!(out, " if (depth != 0) return NULL;");
818 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
819 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
820 let _ = writeln!(out, " if (!result_str) return NULL;");
821 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
822 let _ = writeln!(out, " result_str[val_len] = '\\0';");
823 let _ = writeln!(out, " return result_str;");
824 let _ = writeln!(out, "}}");
825 let _ = writeln!(out);
826 let _ = writeln!(out, "/**");
827 let _ = writeln!(
828 out,
829 " * Extract the Nth top-level element of a JSON array as a heap string."
830 );
831 let _ = writeln!(
832 out,
833 " * Returns NULL if the input is not an array, the index is out of bounds, or"
834 );
835 let _ = writeln!(out, " * allocation fails. Caller must free() the returned string.");
836 let _ = writeln!(out, " */");
837 let _ = writeln!(
838 out,
839 "static inline char *alef_json_array_get_index(const char *json, int index) {{"
840 );
841 let _ = writeln!(out, " if (json == NULL || index < 0) return NULL;");
842 let _ = writeln!(
843 out,
844 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
845 );
846 let _ = writeln!(out, " if (*json != '[') return NULL;");
847 let _ = writeln!(out, " json++;");
848 let _ = writeln!(out, " int current = 0;");
849 let _ = writeln!(out, " while (*json) {{");
850 let _ = writeln!(
851 out,
852 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
853 );
854 let _ = writeln!(out, " if (*json == ']') return NULL;");
855 let _ = writeln!(out, " const char *elem_start = json;");
856 let _ = writeln!(out, " int depth = 0;");
857 let _ = writeln!(out, " int in_string = 0;");
858 let _ = writeln!(out, " for (; *json; json++) {{");
859 let _ = writeln!(out, " if (in_string) {{");
860 let _ = writeln!(
861 out,
862 " if (*json == '\\\\' && *(json + 1)) {{ json++; continue; }}"
863 );
864 let _ = writeln!(out, " if (*json == '\"') in_string = 0;");
865 let _ = writeln!(out, " continue;");
866 let _ = writeln!(out, " }}");
867 let _ = writeln!(out, " if (*json == '\"') {{ in_string = 1; continue; }}");
868 let _ = writeln!(out, " if (*json == '{{' || *json == '[') depth++;");
869 let _ = writeln!(out, " else if (*json == '}}' || *json == ']') {{");
870 let _ = writeln!(out, " if (depth == 0) break;");
871 let _ = writeln!(out, " depth--;");
872 let _ = writeln!(out, " }}");
873 let _ = writeln!(out, " else if (*json == ',' && depth == 0) break;");
874 let _ = writeln!(out, " }}");
875 let _ = writeln!(out, " if (current == index) {{");
876 let _ = writeln!(out, " const char *elem_end = json;");
877 let _ = writeln!(
878 out,
879 " while (elem_end > elem_start && (*(elem_end - 1) == ' ' || *(elem_end - 1) == '\\t' || *(elem_end - 1) == '\\n')) elem_end--;"
880 );
881 let _ = writeln!(out, " size_t elem_len = (size_t)(elem_end - elem_start);");
882 let _ = writeln!(out, " char *out_buf = (char *)malloc(elem_len + 1);");
883 let _ = writeln!(out, " if (!out_buf) return NULL;");
884 let _ = writeln!(out, " memcpy(out_buf, elem_start, elem_len);");
885 let _ = writeln!(out, " out_buf[elem_len] = '\\0';");
886 let _ = writeln!(out, " return out_buf;");
887 let _ = writeln!(out, " }}");
888 let _ = writeln!(out, " current++;");
889 let _ = writeln!(out, " if (*json == ']') return NULL;");
890 let _ = writeln!(out, " if (*json == ',') json++;");
891 let _ = writeln!(out, " }}");
892 let _ = writeln!(out, " return NULL;");
893 let _ = writeln!(out, "}}");
894 let _ = writeln!(out);
895 let _ = writeln!(out, "/**");
896 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
897 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
898 let _ = writeln!(out, " */");
899 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
900 let _ = writeln!(out, " if (json == NULL) return 0;");
901 let _ = writeln!(out, " /* Skip leading whitespace */");
902 let _ = writeln!(
903 out,
904 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
905 );
906 let _ = writeln!(out, " if (*json != '[') return 0;");
907 let _ = writeln!(out, " json++;");
908 let _ = writeln!(out, " /* Skip whitespace after '[' */");
909 let _ = writeln!(
910 out,
911 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
912 );
913 let _ = writeln!(out, " if (*json == ']') return 0;");
914 let _ = writeln!(out, " int count = 1;");
915 let _ = writeln!(out, " int depth = 0;");
916 let _ = writeln!(out, " int in_string = 0;");
917 let _ = writeln!(
918 out,
919 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
920 );
921 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
922 let _ = writeln!(
923 out,
924 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
925 );
926 let _ = writeln!(out, " if (in_string) continue;");
927 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
928 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
929 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
930 let _ = writeln!(out, " }}");
931 let _ = writeln!(out, " return count;");
932 let _ = writeln!(out, "}}");
933 let _ = writeln!(out);
934
935 for (group, fixtures) in active_groups {
936 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
937 for fixture in fixtures {
938 let fn_name = sanitize_ident(&fixture.id);
939 let _ = writeln!(out, "void test_{fn_name}(void);");
940 }
941 let _ = writeln!(out);
942 }
943
944 if !visitor_fixtures.is_empty() {
945 let _ = writeln!(out, "/* Tests for category: visitor */");
946 for fixture in visitor_fixtures {
947 let fn_name = sanitize_ident(&fixture.id);
948 let _ = writeln!(out, "void test_{fn_name}(void);");
949 }
950 let _ = writeln!(out);
951 }
952
953 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
954 out
955}
956
957fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)], visitor_fixtures: &[&Fixture]) -> String {
958 let mut out = String::new();
959 out.push_str(&hash::header(CommentStyle::Block));
960 let _ = writeln!(out, "#include <stdio.h>");
961 let _ = writeln!(out, "#include \"test_runner.h\"");
962 let _ = writeln!(out);
963 let _ = writeln!(out, "int main(void) {{");
964 let _ = writeln!(out, " int passed = 0;");
965 let _ = writeln!(out);
966
967 for (group, fixtures) in active_groups {
968 let _ = writeln!(out, " /* Category: {} */", group.category);
969 for fixture in fixtures {
970 let fn_name = sanitize_ident(&fixture.id);
971 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
972 let _ = writeln!(out, " test_{fn_name}();");
973 let _ = writeln!(out, " printf(\" PASSED\\n\");");
974 let _ = writeln!(out, " passed++;");
975 }
976 let _ = writeln!(out);
977 }
978
979 if !visitor_fixtures.is_empty() {
980 let _ = writeln!(out, " /* Category: visitor */");
981 for fixture in visitor_fixtures {
982 let fn_name = sanitize_ident(&fixture.id);
983 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
984 let _ = writeln!(out, " test_{fn_name}();");
985 let _ = writeln!(out, " printf(\" PASSED\\n\");");
986 let _ = writeln!(out, " passed++;");
987 }
988 let _ = writeln!(out);
989 }
990
991 let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
992 let _ = writeln!(out, " return 0;");
993 let _ = writeln!(out, "}}");
994 out
995}
996
997#[allow(clippy::too_many_arguments)]
998fn render_test_file(
999 category: &str,
1000 fixtures: &[&Fixture],
1001 header: &str,
1002 prefix: &str,
1003 result_var: &str,
1004 e2e_config: &E2eConfig,
1005 lang: &str,
1006 field_resolver: &FieldResolver,
1007) -> String {
1008 let mut out = String::new();
1009 out.push_str(&hash::header(CommentStyle::Block));
1010 let _ = writeln!(out, "/* E2e tests for category: {category} */");
1011 let _ = writeln!(out);
1012 let _ = writeln!(out, "#include <assert.h>");
1013 let _ = writeln!(out, "#include <stdint.h>");
1014 let _ = writeln!(out, "#include <string.h>");
1015 let _ = writeln!(out, "#include <stdio.h>");
1016 let _ = writeln!(out, "#include <stdlib.h>");
1017 let _ = writeln!(out, "#include \"{header}\"");
1018 let _ = writeln!(out, "#include \"test_runner.h\"");
1019 let _ = writeln!(out);
1020
1021 for (i, fixture) in fixtures.iter().enumerate() {
1022 if fixture.visitor.is_some() {
1025 panic!(
1026 "C e2e generator: visitor pattern not supported for fixture: {}",
1027 fixture.id
1028 );
1029 }
1030
1031 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
1032
1033 let mut effective_fields_enum = e2e_config.fields_enum.clone();
1039 let fixture_call = e2e_config.resolve_call_for_fixture(
1040 fixture.call.as_deref(),
1041 &fixture.id,
1042 &fixture.resolved_category(),
1043 &fixture.tags,
1044 &fixture.input,
1045 );
1046 if let Some(co) = fixture_call.overrides.get(lang) {
1047 for k in co.enum_fields.keys() {
1048 effective_fields_enum.insert(k.clone());
1049 }
1050 }
1051
1052 let per_call_field_resolver = FieldResolver::new(
1058 e2e_config.effective_fields(fixture_call),
1059 e2e_config.effective_fields_optional(fixture_call),
1060 e2e_config.effective_result_fields(fixture_call),
1061 e2e_config.effective_fields_array(fixture_call),
1062 &std::collections::HashSet::new(),
1063 );
1064 let _ = field_resolver; let field_resolver = &per_call_field_resolver;
1066
1067 render_test_function(
1068 &mut out,
1069 fixture,
1070 prefix,
1071 &call_info.function_name,
1072 result_var,
1073 &call_info.args,
1074 field_resolver,
1075 &e2e_config.fields_c_types,
1076 &effective_fields_enum,
1077 &call_info.result_type_name,
1078 &call_info.options_type_name,
1079 call_info.client_factory.as_deref(),
1080 call_info.raw_c_result_type.as_deref(),
1081 call_info.c_free_fn.as_deref(),
1082 call_info.c_engine_factory.as_deref(),
1083 call_info.result_is_option,
1084 call_info.result_is_bytes,
1085 &call_info.extra_args,
1086 );
1087 if i + 1 < fixtures.len() {
1088 let _ = writeln!(out);
1089 }
1090 }
1091
1092 out
1093}
1094
1095#[allow(clippy::too_many_arguments)]
1096fn render_test_function(
1097 out: &mut String,
1098 fixture: &Fixture,
1099 prefix: &str,
1100 function_name: &str,
1101 result_var: &str,
1102 args: &[crate::config::ArgMapping],
1103 field_resolver: &FieldResolver,
1104 fields_c_types: &HashMap<String, String>,
1105 fields_enum: &HashSet<String>,
1106 result_type_name: &str,
1107 options_type_name: &str,
1108 client_factory: Option<&str>,
1109 raw_c_result_type: Option<&str>,
1110 c_free_fn: Option<&str>,
1111 c_engine_factory: Option<&str>,
1112 result_is_option: bool,
1113 result_is_bytes: bool,
1114 extra_args: &[String],
1115) {
1116 let fn_name = sanitize_ident(&fixture.id);
1117 let description = &fixture.description;
1118
1119 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1120
1121 let _ = writeln!(out, "void test_{fn_name}(void) {{");
1122 let _ = writeln!(out, " /* {description} */");
1123
1124 let has_mock = fixture.needs_mock_server();
1133 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1134 if let Some(env) = &fixture.env {
1135 if let Some(var) = &env.api_key_var {
1136 let fixture_id = &fixture.id;
1137 if has_mock {
1138 let _ = writeln!(out, " const char* api_key = getenv(\"{var}\");");
1139 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1140 let _ = writeln!(out, " char base_url_buf[512];");
1141 let _ = writeln!(out, " int use_mock = !(api_key && api_key[0] != '\\0');");
1142 let _ = writeln!(out, " if (!use_mock) {{");
1143 let _ = writeln!(
1144 out,
1145 " fprintf(stderr, \"{fixture_id}: using real API ({var} is set)\\n\");"
1146 );
1147 let _ = writeln!(out, " }} else {{");
1148 let _ = writeln!(
1149 out,
1150 " fprintf(stderr, \"{fixture_id}: using mock server ({var} not set)\\n\");"
1151 );
1152 let _ = writeln!(
1153 out,
1154 " snprintf(base_url_buf, sizeof(base_url_buf), \"%s/fixtures/{fixture_id}\", mock_base ? mock_base : \"\");"
1155 );
1156 let _ = writeln!(out, " api_key = \"test-key\";");
1157 let _ = writeln!(out, " }}");
1158 } else {
1159 let _ = writeln!(out, " if (getenv(\"{var}\") == NULL) {{ return; }}");
1160 }
1161 }
1162 }
1163
1164 let prefix_upper = prefix.to_uppercase();
1165
1166 if let Some(config_type) = c_engine_factory {
1170 render_engine_factory_test_function(
1171 out,
1172 fixture,
1173 prefix,
1174 function_name,
1175 result_var,
1176 field_resolver,
1177 fields_c_types,
1178 fields_enum,
1179 result_type_name,
1180 config_type,
1181 expects_error,
1182 raw_c_result_type,
1183 );
1184 return;
1185 }
1186
1187 if client_factory.is_some() && function_name == "chat_stream" {
1193 render_chat_stream_test_function(
1194 out,
1195 fixture,
1196 prefix,
1197 result_var,
1198 args,
1199 options_type_name,
1200 expects_error,
1201 api_key_var,
1202 );
1203 return;
1204 }
1205
1206 if let Some(factory) = client_factory {
1214 if result_is_bytes {
1215 render_bytes_test_function(
1216 out,
1217 fixture,
1218 prefix,
1219 function_name,
1220 result_var,
1221 args,
1222 options_type_name,
1223 result_type_name,
1224 factory,
1225 expects_error,
1226 );
1227 return;
1228 }
1229 }
1230
1231 if let Some(factory) = client_factory {
1236 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); let mut inline_method_args: Vec<String> = Vec::new();
1241
1242 for arg in args {
1243 if arg.arg_type == "json_object" {
1244 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1249 options_type_name.to_string()
1250 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1251 format!("{}Request", stripped)
1252 } else {
1253 format!("{result_type_name}Request")
1254 };
1255 let request_type_snake = request_type_pascal.to_snake_case();
1256 let var_name = format!("{request_type_snake}_handle");
1257
1258 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1259 let json_val = if field.is_empty() || field == "input" {
1260 Some(&fixture.input)
1261 } else {
1262 fixture.input.get(field)
1263 };
1264
1265 if let Some(val) = json_val {
1266 if !val.is_null() {
1267 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1268 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1269 let escaped = escape_c(&json_str);
1270 let _ = writeln!(
1271 out,
1272 " {prefix_upper}{request_type_pascal}* {var_name} = \
1273 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1274 );
1275 if expects_error {
1276 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
1284 } else {
1285 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
1286 }
1287 request_handle_vars.push((arg.name.clone(), var_name));
1288 }
1289 }
1290 } else if arg.arg_type == "string" {
1291 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1293 let val = fixture.input.get(field);
1294 match val {
1295 Some(v) if v.is_string() => {
1296 let s = v.as_str().unwrap_or_default();
1297 let escaped = escape_c(s);
1298 inline_method_args.push(format!("\"{escaped}\""));
1299 }
1300 Some(serde_json::Value::Null) | None if arg.optional => {
1301 inline_method_args.push("NULL".to_string());
1302 }
1303 None => {
1304 inline_method_args.push("\"\"".to_string());
1305 }
1306 Some(other) => {
1307 let s = serde_json::to_string(other).unwrap_or_default();
1308 let escaped = escape_c(&s);
1309 inline_method_args.push(format!("\"{escaped}\""));
1310 }
1311 }
1312 } else if arg.optional {
1313 inline_method_args.push("NULL".to_string());
1315 }
1316 }
1317
1318 let fixture_id = &fixture.id;
1319 if has_mock && api_key_var.is_some() {
1324 let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
1328 let _ = writeln!(
1329 out,
1330 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
1331 );
1332 } else if has_mock {
1333 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1334 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1335 let _ = writeln!(out, " char base_url[1024];");
1336 let _ = writeln!(
1337 out,
1338 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1339 );
1340 let _ = writeln!(
1341 out,
1342 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1343 );
1344 } else {
1345 let _ = writeln!(
1346 out,
1347 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1348 );
1349 }
1350 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
1351
1352 let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
1353 String::new()
1354 } else {
1355 let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
1356 let parts: Vec<String> = handles
1357 .into_iter()
1358 .chain(inline_method_args.iter().cloned())
1359 .chain(extra_args.iter().cloned())
1360 .collect();
1361 format!(", {}", parts.join(", "))
1362 };
1363
1364 let call_fn = format!("{prefix}_default_client_{function_name}");
1365
1366 if expects_error {
1367 let _ = writeln!(
1368 out,
1369 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1370 );
1371 for (_, var_name) in &request_handle_vars {
1372 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1373 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1374 }
1375 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1376 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1377 let _ = writeln!(out, "}}");
1378 return;
1379 }
1380
1381 let _ = writeln!(
1382 out,
1383 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1384 );
1385 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1386
1387 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1388 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1389 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1392 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1395
1396 for assertion in &fixture.assertions {
1397 if let Some(f) = &assertion.field {
1398 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1399 let resolved_raw = field_resolver.resolve(f);
1400 let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
1403 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
1404 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
1405 if field_resolver.is_valid_for_result(stripped_first) {
1406 stripped
1407 } else {
1408 resolved_raw
1409 }
1410 } else {
1411 resolved_raw
1412 };
1413 let local_var = f.replace(['.', '['], "_").replace(']', "");
1414 let has_map_access = resolved.contains('[');
1415 if resolved.contains('.') {
1416 let leaf_primitive = emit_nested_accessor(
1417 out,
1418 prefix,
1419 resolved,
1420 &local_var,
1421 result_var,
1422 fields_c_types,
1423 fields_enum,
1424 &mut intermediate_handles,
1425 result_type_name,
1426 f,
1427 );
1428 if let Some(prim) = leaf_primitive {
1429 primitive_locals.insert(local_var.clone(), prim);
1430 }
1431 } else {
1432 let result_type_snake = result_type_name.to_snake_case();
1433 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1434 let lookup_key = format!("{result_type_snake}.{resolved}");
1435 if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
1436 primitive_locals.insert(local_var.clone(), "__skip__".to_string());
1438 } else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1439 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1440 primitive_locals.insert(local_var.clone(), t.clone());
1441 } else if try_emit_enum_accessor(
1442 out,
1443 prefix,
1444 &prefix_upper,
1445 f,
1446 resolved,
1447 &result_type_snake,
1448 &accessor_fn,
1449 result_var,
1450 &local_var,
1451 fields_c_types,
1452 fields_enum,
1453 &mut intermediate_handles,
1454 ) {
1455 } else if let Some(handle_pascal) =
1457 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1458 {
1459 let _ = writeln!(
1461 out,
1462 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1463 );
1464 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1465 } else {
1466 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1467 }
1468 }
1469 accessed_fields.push((f.clone(), local_var, has_map_access));
1470 }
1471 }
1472 }
1473
1474 for assertion in &fixture.assertions {
1475 render_assertion(
1476 out,
1477 assertion,
1478 result_var,
1479 prefix,
1480 field_resolver,
1481 &accessed_fields,
1482 &primitive_locals,
1483 &opaque_handle_locals,
1484 );
1485 }
1486
1487 for (_f, local_var, from_json) in &accessed_fields {
1488 if primitive_locals.contains_key(local_var) {
1489 continue;
1490 }
1491 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1492 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
1493 continue;
1494 }
1495 if *from_json {
1496 let _ = writeln!(out, " free({local_var});");
1497 } else {
1498 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1499 }
1500 }
1501 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1502 if snake_type == "free_string" {
1503 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1504 } else if snake_type == "free" {
1505 let _ = writeln!(out, " free({handle_var});");
1508 } else {
1509 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1510 }
1511 }
1512 let result_type_snake = result_type_name.to_snake_case();
1513 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1514 for (_, var_name) in &request_handle_vars {
1515 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1516 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
1517 }
1518 let _ = writeln!(out, " {prefix}_default_client_free(client);");
1519 let _ = writeln!(out, "}}");
1520 return;
1521 }
1522
1523 if let Some(raw_type) = raw_c_result_type {
1526 let args_str = if args.is_empty() {
1528 String::new()
1529 } else {
1530 let parts: Vec<String> = args
1531 .iter()
1532 .filter_map(|arg| {
1533 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1534 let val = fixture.input.get(field);
1535 match val {
1536 None if arg.optional => Some("NULL".to_string()),
1537 None => None,
1538 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1539 Some(v) => Some(json_to_c(v)),
1540 }
1541 })
1542 .collect();
1543 parts.join(", ")
1544 };
1545
1546 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
1548
1549 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
1551 if has_not_error {
1552 match raw_type {
1553 "char*" if !result_is_option => {
1554 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1555 }
1556 "int32_t" => {
1557 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
1558 }
1559 "uintptr_t" => {
1560 let _ = writeln!(
1561 out,
1562 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
1563 );
1564 }
1565 _ => {}
1566 }
1567 }
1568
1569 for assertion in &fixture.assertions {
1571 match assertion.assertion_type.as_str() {
1572 "not_error" | "error" => {} "not_empty" => {
1574 let _ = writeln!(
1575 out,
1576 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
1577 );
1578 }
1579 "is_empty" => {
1580 if result_is_option && raw_type == "char*" {
1581 let _ = writeln!(
1582 out,
1583 " assert({result_var} == NULL && \"expected empty/null value\");"
1584 );
1585 } else {
1586 let _ = writeln!(
1587 out,
1588 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
1589 );
1590 }
1591 }
1592 "count_min" => {
1593 if let Some(val) = &assertion.value {
1594 if let Some(n) = val.as_u64() {
1595 match raw_type {
1596 "char*" => {
1597 let _ = writeln!(out, " {{");
1598 let _ = writeln!(
1599 out,
1600 " assert({result_var} != NULL && \"expected non-null JSON array\");"
1601 );
1602 let _ =
1603 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
1604 let _ = writeln!(
1605 out,
1606 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1607 );
1608 let _ = writeln!(out, " }}");
1609 }
1610 _ => {
1611 let _ = writeln!(
1612 out,
1613 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1614 );
1615 }
1616 }
1617 }
1618 }
1619 }
1620 "greater_than_or_equal" => {
1621 if let Some(val) = &assertion.value {
1622 let c_val = json_to_c(val);
1623 let _ = writeln!(
1624 out,
1625 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1626 );
1627 }
1628 }
1629 "contains" => {
1630 if let Some(val) = &assertion.value {
1631 let c_val = json_to_c(val);
1632 let _ = writeln!(
1633 out,
1634 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1635 );
1636 }
1637 }
1638 "contains_all" => {
1639 if let Some(values) = &assertion.values {
1640 for val in values {
1641 let c_val = json_to_c(val);
1642 let _ = writeln!(
1643 out,
1644 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1645 );
1646 }
1647 }
1648 }
1649 "equals" => {
1650 if let Some(val) = &assertion.value {
1651 let c_val = json_to_c(val);
1652 if val.is_string() {
1653 let _ = writeln!(
1654 out,
1655 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1656 );
1657 } else {
1658 let _ = writeln!(
1659 out,
1660 " assert({result_var} == {c_val} && \"equals assertion failed\");"
1661 );
1662 }
1663 }
1664 }
1665 "not_contains" => {
1666 if let Some(val) = &assertion.value {
1667 let c_val = json_to_c(val);
1668 let _ = writeln!(
1669 out,
1670 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1671 );
1672 }
1673 }
1674 "starts_with" => {
1675 if let Some(val) = &assertion.value {
1676 let c_val = json_to_c(val);
1677 let _ = writeln!(
1678 out,
1679 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1680 );
1681 }
1682 }
1683 "is_true" => {
1684 let _ = writeln!(out, " assert({result_var});");
1685 }
1686 "is_false" => {
1687 let _ = writeln!(out, " assert(!{result_var});");
1688 }
1689 other => {
1690 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1691 }
1692 }
1693 }
1694
1695 if raw_type == "char*" {
1697 let free_fn = c_free_fn
1698 .map(|s| s.to_string())
1699 .unwrap_or_else(|| format!("{prefix}_free_string"));
1700 if result_is_option {
1701 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1702 } else {
1703 let _ = writeln!(out, " {free_fn}({result_var});");
1704 }
1705 }
1706
1707 let _ = writeln!(out, "}}");
1708 return;
1709 }
1710
1711 let prefixed_fn = function_name.to_string();
1717
1718 let mut has_options_handle = false;
1720 for arg in args {
1721 if arg.arg_type == "json_object" {
1722 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1723 if let Some(val) = fixture.input.get(field) {
1724 if !val.is_null() {
1725 let normalized = super::transform_json_keys_for_language(val, "snake_case");
1729 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1730 let escaped = escape_c(&json_str);
1731 let upper = prefix.to_uppercase();
1732 let options_type_pascal = options_type_name;
1733 let options_type_snake = options_type_name.to_snake_case();
1734 let _ = writeln!(
1735 out,
1736 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1737 );
1738 has_options_handle = true;
1739 }
1740 }
1741 }
1742 }
1743
1744 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1745
1746 if expects_error {
1747 let _ = writeln!(
1748 out,
1749 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1750 );
1751 if has_options_handle {
1752 let options_type_snake = options_type_name.to_snake_case();
1753 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1754 }
1755 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
1756 let _ = writeln!(out, "}}");
1757 return;
1758 }
1759
1760 let _ = writeln!(
1762 out,
1763 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1764 );
1765 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
1766
1767 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1775 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1778 let mut primitive_locals: HashMap<String, String> = HashMap::new();
1780 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1782
1783 for assertion in &fixture.assertions {
1784 if let Some(f) = &assertion.field {
1785 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1786 let resolved_raw = field_resolver.resolve(f);
1787 let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
1790 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
1791 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
1792 if field_resolver.is_valid_for_result(stripped_first) {
1793 stripped
1794 } else {
1795 resolved_raw
1796 }
1797 } else {
1798 resolved_raw
1799 };
1800 let local_var = f.replace(['.', '['], "_").replace(']', "");
1801 let has_map_access = resolved.contains('[');
1802
1803 if resolved.contains('.') {
1804 let leaf_primitive = emit_nested_accessor(
1805 out,
1806 prefix,
1807 resolved,
1808 &local_var,
1809 result_var,
1810 fields_c_types,
1811 fields_enum,
1812 &mut intermediate_handles,
1813 result_type_name,
1814 f,
1815 );
1816 if let Some(prim) = leaf_primitive {
1817 primitive_locals.insert(local_var.clone(), prim);
1818 }
1819 } else {
1820 let result_type_snake = result_type_name.to_snake_case();
1821 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1822 let lookup_key = format!("{result_type_snake}.{resolved}");
1823 if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
1824 primitive_locals.insert(local_var.clone(), "__skip__".to_string());
1826 } else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1827 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1828 primitive_locals.insert(local_var.clone(), t.clone());
1829 } else if try_emit_enum_accessor(
1830 out,
1831 prefix,
1832 &prefix_upper,
1833 f,
1834 resolved,
1835 &result_type_snake,
1836 &accessor_fn,
1837 result_var,
1838 &local_var,
1839 fields_c_types,
1840 fields_enum,
1841 &mut intermediate_handles,
1842 ) {
1843 } else if let Some(handle_pascal) =
1845 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1846 {
1847 let _ = writeln!(
1848 out,
1849 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1850 );
1851 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1852 } else {
1853 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1854 }
1855 }
1856 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1857 }
1858 }
1859 }
1860
1861 for assertion in &fixture.assertions {
1862 render_assertion(
1863 out,
1864 assertion,
1865 result_var,
1866 prefix,
1867 field_resolver,
1868 &accessed_fields,
1869 &primitive_locals,
1870 &opaque_handle_locals,
1871 );
1872 }
1873
1874 for (_f, local_var, from_json) in &accessed_fields {
1876 if primitive_locals.contains_key(local_var) {
1877 continue;
1878 }
1879 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1880 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
1881 continue;
1882 }
1883 if *from_json {
1884 let _ = writeln!(out, " free({local_var});");
1885 } else {
1886 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1887 }
1888 }
1889 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1891 if snake_type == "free_string" {
1892 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1894 } else if snake_type == "free" {
1895 let _ = writeln!(out, " free({handle_var});");
1897 } else {
1898 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1899 }
1900 }
1901 if has_options_handle {
1902 let options_type_snake = options_type_name.to_snake_case();
1903 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1904 }
1905 let result_type_snake = result_type_name.to_snake_case();
1906 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1907 let _ = writeln!(out, "}}");
1908}
1909
1910#[allow(clippy::too_many_arguments)]
1919fn render_engine_factory_test_function(
1920 out: &mut String,
1921 fixture: &Fixture,
1922 prefix: &str,
1923 function_name: &str,
1924 result_var: &str,
1925 field_resolver: &FieldResolver,
1926 fields_c_types: &HashMap<String, String>,
1927 fields_enum: &HashSet<String>,
1928 result_type_name: &str,
1929 config_type: &str,
1930 expects_error: bool,
1931 raw_c_result_type: Option<&str>,
1932) {
1933 let prefix_upper = prefix.to_uppercase();
1934 let config_snake = config_type.to_snake_case();
1935
1936 let config_val = fixture.input.get("config");
1938 let config_json = match config_val {
1939 Some(v) if !v.is_null() => {
1940 let normalized = super::transform_json_keys_for_language(v, "snake_case");
1941 serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1942 }
1943 _ => "{}".to_string(),
1944 };
1945 let config_escaped = escape_c(&config_json);
1946 let fixture_id = &fixture.id;
1947
1948 let has_active_assertions = fixture.assertions.iter().any(|a| {
1952 if let Some(f) = &a.field {
1953 !f.is_empty() && field_resolver.is_valid_for_result(f)
1954 } else {
1955 false
1956 }
1957 });
1958
1959 let _ = writeln!(
1961 out,
1962 " {prefix_upper}{config_type}* config_handle = \
1963 {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1964 );
1965 if expects_error {
1966 let _ = writeln!(out, " if (config_handle == NULL) {{ return; }}");
1969 } else {
1970 let _ = writeln!(out, " assert(config_handle != NULL && \"failed to parse config\");");
1971 }
1972 let _ = writeln!(
1973 out,
1974 " {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1975 );
1976 let _ = writeln!(out, " {prefix}_{config_snake}_free(config_handle);");
1977 if expects_error {
1978 let _ = writeln!(out, " if (engine == NULL) {{ return; }}");
1981 } else {
1982 let _ = writeln!(out, " assert(engine != NULL && \"failed to create engine\");");
1983 }
1984
1985 let fixture_env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1989 let _ = writeln!(out, " const char* mock_per_fixture = getenv(\"{fixture_env_key}\");");
1990 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1991 let _ = writeln!(out, " char url[2048];");
1992 let _ = writeln!(out, " if (mock_per_fixture && mock_per_fixture[0] != '\\0') {{");
1993 let _ = writeln!(out, " snprintf(url, sizeof(url), \"%s\", mock_per_fixture);");
1994 let _ = writeln!(out, " }} else {{");
1995 let _ = writeln!(
1996 out,
1997 " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");"
1998 );
1999 let _ = writeln!(
2000 out,
2001 " snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
2002 );
2003 let _ = writeln!(out, " }}");
2004
2005 let actions_arg = fixture.input.get("actions").and_then(|v| {
2011 if v.is_null() {
2012 None
2013 } else {
2014 let normalized = super::transform_json_keys_for_language(v, "snake_case");
2015 let json = serde_json::to_string(&normalized).ok()?;
2016 let escaped = escape_c(&json);
2017 Some(escaped)
2018 }
2019 });
2020 if let Some(ref escaped_actions) = actions_arg {
2021 let _ = writeln!(out, " const char* actions_json = \"{escaped_actions}\";");
2022 }
2023
2024 let extra_call_args = if actions_arg.is_some() {
2027 ", actions_json".to_string()
2028 } else {
2029 String::new()
2030 };
2031
2032 if let Some(raw_type) = raw_c_result_type {
2041 if raw_type == "char*" {
2042 let _ = writeln!(
2043 out,
2044 " char* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
2045 );
2046 let _ = writeln!(out, " if ({result_var} != NULL) {prefix}_free_string({result_var});");
2047 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2048 let _ = writeln!(out, "}}");
2049 return;
2050 } else {
2051 let raw_snake = raw_type.to_snake_case();
2054 let _ = writeln!(
2055 out,
2056 " {prefix_upper}{raw_type}* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
2057 );
2058 let _ = writeln!(
2059 out,
2060 " if ({result_var} != NULL) {prefix}_{raw_snake}_free({result_var});"
2061 );
2062 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2063 let _ = writeln!(out, "}}");
2064 return;
2065 }
2066 }
2067
2068 let _ = writeln!(
2069 out,
2070 " {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
2071 );
2072
2073 if !has_active_assertions {
2076 let result_type_snake = result_type_name.to_snake_case();
2077 let _ = writeln!(
2078 out,
2079 " if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
2080 );
2081 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2082 let _ = writeln!(out, "}}");
2083 return;
2084 }
2085
2086 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
2087
2088 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
2090 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
2091 let mut primitive_locals: HashMap<String, String> = HashMap::new();
2092 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
2093
2094 for assertion in &fixture.assertions {
2095 if let Some(f) = &assertion.field {
2096 if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
2097 {
2098 let resolved_raw = field_resolver.resolve(f);
2099 let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
2102 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
2103 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
2104 if field_resolver.is_valid_for_result(stripped_first) {
2105 stripped
2106 } else {
2107 resolved_raw
2108 }
2109 } else {
2110 resolved_raw
2111 };
2112 let local_var = f.replace(['.', '['], "_").replace(']', "");
2113 let has_map_access = resolved.contains('[');
2114 if resolved.contains('.') {
2115 let leaf_primitive = emit_nested_accessor(
2116 out,
2117 prefix,
2118 resolved,
2119 &local_var,
2120 result_var,
2121 fields_c_types,
2122 fields_enum,
2123 &mut intermediate_handles,
2124 result_type_name,
2125 f,
2126 );
2127 if let Some(prim) = leaf_primitive {
2128 primitive_locals.insert(local_var.clone(), prim);
2129 }
2130 } else {
2131 let result_type_snake = result_type_name.to_snake_case();
2132 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
2133 let lookup_key = format!("{result_type_snake}.{resolved}");
2134 if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
2135 primitive_locals.insert(local_var.clone(), "__skip__".to_string());
2137 } else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2138 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
2139 primitive_locals.insert(local_var.clone(), t.clone());
2140 } else if try_emit_enum_accessor(
2141 out,
2142 prefix,
2143 &prefix_upper,
2144 f,
2145 resolved,
2146 &result_type_snake,
2147 &accessor_fn,
2148 result_var,
2149 &local_var,
2150 fields_c_types,
2151 fields_enum,
2152 &mut intermediate_handles,
2153 ) {
2154 } else if let Some(handle_pascal) =
2156 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
2157 {
2158 let _ = writeln!(
2159 out,
2160 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
2161 );
2162 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
2163 } else {
2164 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
2165 }
2166 }
2167 accessed_fields.push((f.clone(), local_var, has_map_access));
2168 }
2169 }
2170 }
2171
2172 for assertion in &fixture.assertions {
2173 render_assertion(
2174 out,
2175 assertion,
2176 result_var,
2177 prefix,
2178 field_resolver,
2179 &accessed_fields,
2180 &primitive_locals,
2181 &opaque_handle_locals,
2182 );
2183 }
2184
2185 for (_f, local_var, from_json) in &accessed_fields {
2187 if primitive_locals.contains_key(local_var) {
2188 continue;
2189 }
2190 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
2191 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
2192 continue;
2193 }
2194 if *from_json {
2195 let _ = writeln!(out, " free({local_var});");
2196 } else {
2197 let _ = writeln!(out, " {prefix}_free_string({local_var});");
2198 }
2199 }
2200 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
2201 if snake_type == "free_string" {
2202 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
2203 } else if snake_type == "free" {
2204 let _ = writeln!(out, " free({handle_var});");
2206 } else {
2207 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
2208 }
2209 }
2210
2211 let result_type_snake = result_type_name.to_snake_case();
2212 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
2213 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2214 let _ = writeln!(out, "}}");
2215}
2216
2217#[allow(clippy::too_many_arguments)]
2239fn render_bytes_test_function(
2240 out: &mut String,
2241 fixture: &Fixture,
2242 prefix: &str,
2243 function_name: &str,
2244 _result_var: &str,
2245 args: &[crate::config::ArgMapping],
2246 options_type_name: &str,
2247 result_type_name: &str,
2248 factory: &str,
2249 expects_error: bool,
2250) {
2251 let prefix_upper = prefix.to_uppercase();
2252 let mut request_handle_vars: Vec<(String, String)> = Vec::new();
2253 let mut string_arg_exprs: Vec<String> = Vec::new();
2254
2255 for arg in args {
2256 match arg.arg_type.as_str() {
2257 "json_object" => {
2258 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2259 options_type_name.to_string()
2260 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
2261 format!("{}Request", stripped)
2262 } else {
2263 format!("{result_type_name}Request")
2264 };
2265 let request_type_snake = request_type_pascal.to_snake_case();
2266 let var_name = format!("{request_type_snake}_handle");
2267
2268 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2269 let json_val = if field.is_empty() || field == "input" {
2270 Some(&fixture.input)
2271 } else {
2272 fixture.input.get(field)
2273 };
2274
2275 if let Some(val) = json_val {
2276 if !val.is_null() {
2277 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2278 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2279 let escaped = escape_c(&json_str);
2280 let _ = writeln!(
2281 out,
2282 " {prefix_upper}{request_type_pascal}* {var_name} = \
2283 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2284 );
2285 if expects_error {
2286 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
2294 } else {
2295 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2296 }
2297 request_handle_vars.push((arg.name.clone(), var_name));
2298 }
2299 }
2300 }
2301 "string" => {
2302 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2305 let val = fixture.input.get(field);
2306 let expr = match val {
2307 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
2308 Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
2309 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
2310 None => "NULL".to_string(),
2311 };
2312 string_arg_exprs.push(expr);
2313 }
2314 _ => {
2315 string_arg_exprs.push("NULL".to_string());
2318 }
2319 }
2320 }
2321
2322 let fixture_id = &fixture.id;
2323 if fixture.needs_mock_server() {
2324 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2325 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2326 let _ = writeln!(out, " char base_url[1024];");
2327 let _ = writeln!(
2328 out,
2329 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2330 );
2331 let _ = writeln!(
2336 out,
2337 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2338 );
2339 } else {
2340 let _ = writeln!(
2341 out,
2342 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2343 );
2344 }
2345 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2346
2347 let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
2349 let _ = writeln!(out, " uintptr_t out_len = 0;");
2350 let _ = writeln!(out, " uintptr_t out_cap = 0;");
2351
2352 let mut method_args: Vec<String> = Vec::new();
2354 for (_, v) in &request_handle_vars {
2355 method_args.push(v.clone());
2356 }
2357 method_args.extend(string_arg_exprs.iter().cloned());
2358 let extra_args = if method_args.is_empty() {
2359 String::new()
2360 } else {
2361 format!(", {}", method_args.join(", "))
2362 };
2363
2364 let call_fn = format!("{prefix}_default_client_{function_name}");
2365 let _ = writeln!(
2366 out,
2367 " int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
2368 );
2369
2370 if expects_error {
2371 for (_, var_name) in &request_handle_vars {
2372 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2373 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2374 }
2375 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2376 let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
2377 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2380 let _ = writeln!(out, "}}");
2381 return;
2382 }
2383
2384 let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
2385
2386 let mut emitted_len_check = false;
2391 for assertion in &fixture.assertions {
2392 match assertion.assertion_type.as_str() {
2393 "not_error" => {
2394 }
2396 "not_empty" | "not_null" => {
2397 if !emitted_len_check {
2398 let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
2399 emitted_len_check = true;
2400 }
2401 }
2402 _ => {
2403 let _ = writeln!(
2407 out,
2408 " /* skipped: assertion '{}' not meaningful on raw byte buffer */",
2409 assertion.assertion_type
2410 );
2411 }
2412 }
2413 }
2414
2415 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2416 for (_, var_name) in &request_handle_vars {
2417 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2418 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2419 }
2420 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2421 let _ = writeln!(out, "}}");
2422}
2423
2424#[allow(clippy::too_many_arguments)]
2435fn render_chat_stream_test_function(
2436 out: &mut String,
2437 fixture: &Fixture,
2438 prefix: &str,
2439 result_var: &str,
2440 args: &[crate::config::ArgMapping],
2441 options_type_name: &str,
2442 expects_error: bool,
2443 api_key_var: Option<&str>,
2444) {
2445 let prefix_upper = prefix.to_uppercase();
2446
2447 let mut request_var: Option<String> = None;
2448 for arg in args {
2449 if arg.arg_type == "json_object" {
2450 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2451 options_type_name.to_string()
2452 } else {
2453 "ChatCompletionRequest".to_string()
2454 };
2455 let request_type_snake = request_type_pascal.to_snake_case();
2456 let var_name = format!("{request_type_snake}_handle");
2457
2458 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2459 let json_val = if field.is_empty() || field == "input" {
2460 Some(&fixture.input)
2461 } else {
2462 fixture.input.get(field)
2463 };
2464
2465 if let Some(val) = json_val {
2466 if !val.is_null() {
2467 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2468 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2469 let escaped = escape_c(&json_str);
2470 let _ = writeln!(
2471 out,
2472 " {prefix_upper}{request_type_pascal}* {var_name} = \
2473 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2474 );
2475 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2476 request_var = Some(var_name);
2477 break;
2478 }
2479 }
2480 }
2481 }
2482
2483 let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
2484 let req_snake = request_var
2485 .as_ref()
2486 .and_then(|v| v.strip_suffix("_handle"))
2487 .unwrap_or("chat_completion_request")
2488 .to_string();
2489
2490 let fixture_id = &fixture.id;
2491 let has_mock = fixture.needs_mock_server();
2492 if has_mock && api_key_var.is_some() {
2493 let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
2499 let _ = writeln!(
2500 out,
2501 " {prefix_upper}DefaultClient* client = {prefix}_create_client(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
2502 );
2503 } else if has_mock {
2504 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2505 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2506 let _ = writeln!(out, " char base_url[1024];");
2507 let _ = writeln!(
2508 out,
2509 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2510 );
2511 let _ = writeln!(
2516 out,
2517 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2518 );
2519 } else {
2520 let _ = writeln!(
2521 out,
2522 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2523 );
2524 }
2525 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2526
2527 let pascal_prefix = prefix.to_pascal_case();
2531 let _ = writeln!(
2532 out,
2533 " {prefix_upper}{pascal_prefix}DefaultClientChatStreamStreamHandle* stream_handle = \
2534 {prefix}_default_client_chat_stream_start(client, {req_handle});"
2535 );
2536
2537 if expects_error {
2538 let _ = writeln!(
2539 out,
2540 " assert(stream_handle == NULL && \"expected stream-start to fail\");"
2541 );
2542 if request_var.is_some() {
2543 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2544 }
2545 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2546 let _ = writeln!(out, "}}");
2547 return;
2548 }
2549
2550 let _ = writeln!(
2551 out,
2552 " assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2553 );
2554
2555 let _ = writeln!(out, " size_t chunks_count = 0;");
2556 let _ = writeln!(out, " char* stream_content = (char*)malloc(1);");
2557 let _ = writeln!(out, " assert(stream_content != NULL);");
2558 let _ = writeln!(out, " stream_content[0] = '\\0';");
2559 let _ = writeln!(out, " size_t stream_content_len = 0;");
2560 let _ = writeln!(out, " int stream_complete = 0;");
2561 let _ = writeln!(out, " int no_chunks_after_done = 1;");
2562 let _ = writeln!(out, " char* last_choices_json = NULL;");
2563 let _ = writeln!(out, " uint64_t total_tokens = 0;");
2564 let _ = writeln!(out);
2565
2566 let _ = writeln!(out, " while (1) {{");
2567 let _ = writeln!(
2568 out,
2569 " {prefix_upper}ChatCompletionChunk* {result_var} = \
2570 {prefix}_default_client_chat_stream_next(stream_handle);"
2571 );
2572 let _ = writeln!(out, " if ({result_var} == NULL) {{");
2573 let _ = writeln!(
2574 out,
2575 " if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2576 );
2577 let _ = writeln!(out, " break;");
2578 let _ = writeln!(out, " }}");
2579 let _ = writeln!(out, " chunks_count++;");
2580 let _ = writeln!(
2581 out,
2582 " char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2583 );
2584 let _ = writeln!(out, " if (choices_json != NULL) {{");
2585 let _ = writeln!(
2586 out,
2587 " const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2588 );
2589 let _ = writeln!(out, " if (d != NULL) {{");
2590 let _ = writeln!(out, " d += 10;");
2591 let _ = writeln!(out, " while (*d == ' ' || *d == '\\t') d++;");
2592 let _ = writeln!(out, " if (*d == '\"') {{");
2593 let _ = writeln!(out, " d++;");
2594 let _ = writeln!(out, " const char* e = d;");
2595 let _ = writeln!(out, " while (*e && *e != '\"') {{");
2596 let _ = writeln!(
2597 out,
2598 " if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2599 );
2600 let _ = writeln!(out, " }}");
2601 let _ = writeln!(out, " size_t add = (size_t)(e - d);");
2602 let _ = writeln!(out, " if (add > 0) {{");
2603 let _ = writeln!(
2604 out,
2605 " char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2606 );
2607 let _ = writeln!(out, " if (nc != NULL) {{");
2608 let _ = writeln!(out, " stream_content = nc;");
2609 let _ = writeln!(
2610 out,
2611 " memcpy(stream_content + stream_content_len, d, add);"
2612 );
2613 let _ = writeln!(out, " stream_content_len += add;");
2614 let _ = writeln!(
2615 out,
2616 " stream_content[stream_content_len] = '\\0';"
2617 );
2618 let _ = writeln!(out, " }}");
2619 let _ = writeln!(out, " }}");
2620 let _ = writeln!(out, " }}");
2621 let _ = writeln!(out, " }}");
2622 let _ = writeln!(
2623 out,
2624 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2625 );
2626 let _ = writeln!(out, " last_choices_json = choices_json;");
2627 let _ = writeln!(out, " }}");
2628 let _ = writeln!(
2629 out,
2630 " {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2631 );
2632 let _ = writeln!(out, " if (usage_handle != NULL) {{");
2633 let _ = writeln!(
2634 out,
2635 " total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2636 );
2637 let _ = writeln!(out, " {prefix}_usage_free(usage_handle);");
2638 let _ = writeln!(out, " }}");
2639 let _ = writeln!(out, " {prefix}_chat_completion_chunk_free({result_var});");
2640 let _ = writeln!(out, " }}");
2641 let _ = writeln!(out, " {prefix}_default_client_chat_stream_free(stream_handle);");
2642 let _ = writeln!(out);
2643
2644 let _ = writeln!(out, " char* finish_reason = NULL;");
2645 let _ = writeln!(out, " char* tool_calls_json = NULL;");
2646 let _ = writeln!(out, " char* tool_calls_0_function_name = NULL;");
2647 let _ = writeln!(out, " if (last_choices_json != NULL) {{");
2648 let _ = writeln!(
2649 out,
2650 " finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2651 );
2652 let _ = writeln!(
2653 out,
2654 " const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2655 );
2656 let _ = writeln!(out, " if (tc != NULL) {{");
2657 let _ = writeln!(out, " tc += 13;");
2658 let _ = writeln!(out, " while (*tc == ' ' || *tc == '\\t') tc++;");
2659 let _ = writeln!(out, " if (*tc == '[') {{");
2660 let _ = writeln!(out, " int depth = 0;");
2661 let _ = writeln!(out, " const char* end = tc;");
2662 let _ = writeln!(out, " int in_str = 0;");
2663 let _ = writeln!(out, " for (; *end; end++) {{");
2664 let _ = writeln!(
2665 out,
2666 " if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2667 );
2668 let _ = writeln!(
2669 out,
2670 " if (*end == '\"') {{ in_str = !in_str; continue; }}"
2671 );
2672 let _ = writeln!(out, " if (in_str) continue;");
2673 let _ = writeln!(out, " if (*end == '[' || *end == '{{') depth++;");
2674 let _ = writeln!(
2675 out,
2676 " else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2677 );
2678 let _ = writeln!(out, " }}");
2679 let _ = writeln!(out, " size_t tlen = (size_t)(end - tc);");
2680 let _ = writeln!(out, " tool_calls_json = (char*)malloc(tlen + 1);");
2681 let _ = writeln!(out, " if (tool_calls_json != NULL) {{");
2682 let _ = writeln!(out, " memcpy(tool_calls_json, tc, tlen);");
2683 let _ = writeln!(out, " tool_calls_json[tlen] = '\\0';");
2684 let _ = writeln!(
2685 out,
2686 " const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2687 );
2688 let _ = writeln!(out, " if (fn != NULL) {{");
2689 let _ = writeln!(
2690 out,
2691 " const char* np = strstr(fn, \"\\\"name\\\":\");"
2692 );
2693 let _ = writeln!(out, " if (np != NULL) {{");
2694 let _ = writeln!(out, " np += 7;");
2695 let _ = writeln!(
2696 out,
2697 " while (*np == ' ' || *np == '\\t') np++;"
2698 );
2699 let _ = writeln!(out, " if (*np == '\"') {{");
2700 let _ = writeln!(out, " np++;");
2701 let _ = writeln!(out, " const char* ne = np;");
2702 let _ = writeln!(
2703 out,
2704 " while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2705 );
2706 let _ = writeln!(out, " size_t nlen = (size_t)(ne - np);");
2707 let _ = writeln!(
2708 out,
2709 " tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2710 );
2711 let _ = writeln!(
2712 out,
2713 " if (tool_calls_0_function_name != NULL) {{"
2714 );
2715 let _ = writeln!(
2716 out,
2717 " memcpy(tool_calls_0_function_name, np, nlen);"
2718 );
2719 let _ = writeln!(
2720 out,
2721 " tool_calls_0_function_name[nlen] = '\\0';"
2722 );
2723 let _ = writeln!(out, " }}");
2724 let _ = writeln!(out, " }}");
2725 let _ = writeln!(out, " }}");
2726 let _ = writeln!(out, " }}");
2727 let _ = writeln!(out, " }}");
2728 let _ = writeln!(out, " }}");
2729 let _ = writeln!(out, " }}");
2730 let _ = writeln!(out, " }}");
2731 let _ = writeln!(out);
2732
2733 for assertion in &fixture.assertions {
2734 emit_chat_stream_assertion(out, assertion);
2735 }
2736
2737 let _ = writeln!(out, " free(stream_content);");
2738 let _ = writeln!(
2739 out,
2740 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2741 );
2742 let _ = writeln!(out, " if (finish_reason != NULL) free(finish_reason);");
2743 let _ = writeln!(out, " if (tool_calls_json != NULL) free(tool_calls_json);");
2744 let _ = writeln!(
2745 out,
2746 " if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2747 );
2748 if request_var.is_some() {
2749 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2750 }
2751 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2752 let _ = writeln!(
2753 out,
2754 " /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2755 (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2756 );
2757 let _ = writeln!(out, "}}");
2758}
2759
2760fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2764 let field = assertion.field.as_deref().unwrap_or("");
2765
2766 enum Kind {
2767 IntCount,
2768 Bool,
2769 Str,
2770 IntTokens,
2771 Unsupported,
2772 }
2773
2774 let (expr, kind) = match field {
2775 "chunks" => ("chunks_count", Kind::IntCount),
2776 "stream_content" => ("stream_content", Kind::Str),
2777 "stream_complete" => ("stream_complete", Kind::Bool),
2778 "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2779 "finish_reason" => ("finish_reason", Kind::Str),
2780 "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2789 "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2790 _ => ("", Kind::Unsupported),
2791 };
2792
2793 let atype = assertion.assertion_type.as_str();
2794 if atype == "not_error" || atype == "error" {
2795 return;
2796 }
2797
2798 if matches!(kind, Kind::Unsupported) {
2799 let _ = writeln!(
2800 out,
2801 " /* skipped: streaming assertion on unsupported field '{field}' */"
2802 );
2803 return;
2804 }
2805
2806 match (atype, &kind) {
2807 ("count_min", Kind::IntCount) => {
2808 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2809 let _ = writeln!(out, " assert({expr} >= {n} && \"expected at least {n} chunks\");");
2810 }
2811 }
2812 ("equals", Kind::Str) => {
2813 if let Some(val) = &assertion.value {
2814 let c_val = json_to_c(val);
2815 let _ = writeln!(
2816 out,
2817 " assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2818 );
2819 }
2820 }
2821 ("contains", Kind::Str) => {
2822 if let Some(val) = &assertion.value {
2823 let c_val = json_to_c(val);
2824 let _ = writeln!(
2825 out,
2826 " assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2827 );
2828 }
2829 }
2830 ("not_empty", Kind::Str) => {
2831 let _ = writeln!(
2832 out,
2833 " assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2834 );
2835 }
2836 ("is_true", Kind::Bool) => {
2837 let _ = writeln!(out, " assert({expr} && \"expected {field} to be true\");");
2838 }
2839 ("is_false", Kind::Bool) => {
2840 let _ = writeln!(out, " assert(!{expr} && \"expected {field} to be false\");");
2841 }
2842 ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2843 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2844 let _ = writeln!(out, " assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2845 }
2846 }
2847 ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2848 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2849 let _ = writeln!(out, " assert({expr} == {n} && \"equals assertion failed\");");
2850 }
2851 }
2852 _ => {
2853 let _ = writeln!(
2854 out,
2855 " /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2856 );
2857 }
2858 }
2859}
2860
2861#[allow(clippy::too_many_arguments)]
2875fn emit_nested_accessor(
2876 out: &mut String,
2877 prefix: &str,
2878 resolved: &str,
2879 local_var: &str,
2880 result_var: &str,
2881 fields_c_types: &HashMap<String, String>,
2882 fields_enum: &HashSet<String>,
2883 intermediate_handles: &mut Vec<(String, String)>,
2884 result_type_name: &str,
2885 raw_field: &str,
2886) -> Option<String> {
2887 let segments: Vec<&str> = resolved.split('.').collect();
2888 let prefix_upper = prefix.to_uppercase();
2889
2890 let mut current_snake_type = result_type_name.to_snake_case();
2892 let mut current_handle = result_var.to_string();
2893 let mut json_extract_mode = false;
2896
2897 for (i, segment) in segments.iter().enumerate() {
2898 let is_leaf = i + 1 == segments.len();
2899
2900 if json_extract_mode {
2904 let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2909 Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2910 None => (segment, None),
2911 };
2912 let seg_snake = bare_segment.to_snake_case();
2913 if is_leaf {
2914 let _ = writeln!(
2915 out,
2916 " char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2917 );
2918 return None; }
2920 let json_var = format!("{seg_snake}_json");
2925 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2926 let _ = writeln!(
2927 out,
2928 " char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2929 );
2930 intermediate_handles.push((json_var.clone(), "free".to_string()));
2931 }
2932 if let Some(key) = bracket_key {
2936 if let Ok(idx) = key.parse::<usize>() {
2937 let elem_var = format!("{seg_snake}_{idx}_json");
2938 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2939 let _ = writeln!(
2940 out,
2941 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2942 );
2943 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2944 }
2945 current_handle = elem_var;
2946 continue;
2947 }
2948 }
2949 current_handle = json_var;
2950 continue;
2951 }
2952
2953 if let Some(bracket_pos) = segment.find('[') {
2955 let field_name = &segment[..bracket_pos];
2956 let key = segment[bracket_pos + 1..].trim_end_matches(']');
2957 let field_snake = field_name.to_snake_case();
2958 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2959
2960 let json_var = format!("{field_snake}_json");
2962 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2963 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2964 let _ = writeln!(out, " assert({json_var} != NULL);");
2965 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2967 }
2968
2969 if key.is_empty() {
2975 if !is_leaf {
2976 current_handle = json_var;
2977 json_extract_mode = true;
2978 continue;
2979 }
2980 return None;
2981 }
2982 if let Ok(idx) = key.parse::<usize>() {
2983 let elem_var = format!("{field_snake}_{idx}_json");
2984 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2985 let _ = writeln!(
2986 out,
2987 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2988 );
2989 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2990 }
2991 if !is_leaf {
2992 current_handle = elem_var;
2993 json_extract_mode = true;
2994 continue;
2995 }
2996 return None;
2998 }
2999
3000 let _ = writeln!(
3002 out,
3003 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
3004 );
3005 return None; }
3007
3008 let seg_snake = segment.to_snake_case();
3009 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
3010
3011 if is_skipped_c_field(fields_c_types, ¤t_snake_type, &seg_snake) {
3013 return Some("__skip__".to_string()); }
3015
3016 if is_leaf {
3017 let lookup_key = format!("{current_snake_type}.{seg_snake}");
3020 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
3021 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
3022 return Some(t.clone());
3023 }
3024 if let Some(opaque_type) = fields_c_types.get(&lookup_key).filter(|t| {
3031 *t != "char*"
3032 && *t != "skip"
3033 && !is_primitive_c_type(t)
3034 && t.chars().next().is_some_and(|c| c.is_uppercase())
3035 }) {
3036 let handle_var = format!("{seg_snake}_handle");
3037 let opaque_snake = opaque_type.to_snake_case();
3038 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
3039 let _ = writeln!(
3040 out,
3041 " {prefix_upper}{opaque_type}* {handle_var} = {accessor_fn}({current_handle});"
3042 );
3043 intermediate_handles.push((handle_var.clone(), opaque_snake));
3044 }
3045 if local_var != handle_var {
3048 let _ = writeln!(out, " {prefix_upper}{opaque_type}* {local_var} = {handle_var};");
3049 }
3050 return None; }
3052 if try_emit_enum_accessor(
3054 out,
3055 prefix,
3056 &prefix_upper,
3057 raw_field,
3058 &seg_snake,
3059 ¤t_snake_type,
3060 &accessor_fn,
3061 ¤t_handle,
3062 local_var,
3063 fields_c_types,
3064 fields_enum,
3065 intermediate_handles,
3066 ) {
3067 return None;
3068 }
3069 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
3070 } else {
3071 let lookup_key = format!("{current_snake_type}.{seg_snake}");
3073 let return_type_pascal = match fields_c_types.get(&lookup_key) {
3074 Some(t) => t.clone(),
3075 None => {
3076 segment.to_pascal_case()
3078 }
3079 };
3080
3081 if return_type_pascal == "char*" {
3084 let json_var = format!("{seg_snake}_json");
3085 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
3086 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
3087 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
3088 }
3089 if i + 2 == segments.len() && segments[i + 1] == "length" {
3091 let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
3092 return Some("int".to_string());
3093 }
3094 current_snake_type = seg_snake.clone();
3095 current_handle = json_var;
3096 continue;
3097 }
3098
3099 let return_snake = return_type_pascal.to_snake_case();
3100 let handle_var = format!("{seg_snake}_handle");
3101
3102 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
3105 let _ = writeln!(
3106 out,
3107 " {prefix_upper}{return_type_pascal}* {handle_var} = \
3108 {accessor_fn}({current_handle});"
3109 );
3110 let _ = writeln!(out, " assert({handle_var} != NULL);");
3111 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
3112 }
3113
3114 current_snake_type = return_snake;
3115 current_handle = handle_var;
3116 }
3117 }
3118 None
3119}
3120
3121fn build_args_string_c(
3125 input: &serde_json::Value,
3126 args: &[crate::config::ArgMapping],
3127 has_options_handle: bool,
3128) -> String {
3129 if args.is_empty() {
3130 return json_to_c(input);
3131 }
3132
3133 let parts: Vec<String> = args
3134 .iter()
3135 .filter_map(|arg| {
3136 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
3137 let val = input.get(field);
3138 match val {
3139 None if arg.optional => Some("NULL".to_string()),
3141 None => None,
3143 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
3145 Some(v) => {
3146 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
3149 Some("options_handle".to_string())
3150 } else {
3151 Some(json_to_c(v))
3152 }
3153 }
3154 }
3155 })
3156 .collect();
3157
3158 parts.join(", ")
3159}
3160
3161#[allow(clippy::too_many_arguments)]
3162fn render_assertion(
3163 out: &mut String,
3164 assertion: &Assertion,
3165 result_var: &str,
3166 ffi_prefix: &str,
3167 _field_resolver: &FieldResolver,
3168 accessed_fields: &[(String, String, bool)],
3169 primitive_locals: &HashMap<String, String>,
3170 opaque_handle_locals: &HashMap<String, String>,
3171) {
3172 if let Some(f) = &assertion.field {
3174 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
3175 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
3176 return;
3177 }
3178 }
3179
3180 let field_expr = match &assertion.field {
3181 Some(f) if !f.is_empty() => {
3182 accessed_fields
3184 .iter()
3185 .find(|(k, _, _)| k == f)
3186 .map(|(_, local, _)| local.clone())
3187 .unwrap_or_else(|| result_var.to_string())
3188 }
3189 _ => result_var.to_string(),
3190 };
3191
3192 if primitive_locals.get(&field_expr).is_some_and(|t| t == "__skip__") {
3195 let _ = writeln!(out, " // skipped: field '{field_expr}' not available in C FFI");
3196 return;
3197 }
3198
3199 let field_is_primitive = primitive_locals.contains_key(&field_expr);
3200 let field_primitive_type = primitive_locals.get(&field_expr).cloned();
3201 let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
3206 let field_is_map_access = if let Some(f) = &assertion.field {
3210 accessed_fields.iter().any(|(k, _, m)| k == f && *m)
3211 } else {
3212 false
3213 };
3214
3215 let assertion_field_is_optional = assertion
3219 .field
3220 .as_deref()
3221 .map(|f| {
3222 if f.is_empty() {
3223 return false;
3224 }
3225 if _field_resolver.is_optional(f) {
3226 return true;
3227 }
3228 let resolved = _field_resolver.resolve(f);
3230 _field_resolver.is_optional(resolved)
3231 })
3232 .unwrap_or(false);
3233
3234 match assertion.assertion_type.as_str() {
3235 "equals" => {
3236 if let Some(expected) = &assertion.value {
3237 let c_val = json_to_c(expected);
3238 if field_is_primitive {
3239 let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
3240 match expected.as_bool() {
3241 Some(true) => "1".to_string(),
3242 Some(false) => "0".to_string(),
3243 None => c_val,
3244 }
3245 } else {
3246 c_val
3247 };
3248 let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
3251 if assertion_field_is_optional && is_numeric {
3252 let _ = writeln!(
3253 out,
3254 " assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
3255 );
3256 } else {
3257 let _ = writeln!(
3258 out,
3259 " assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
3260 );
3261 }
3262 } else if expected.is_string() {
3263 let _ = writeln!(
3264 out,
3265 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3266 );
3267 } else if field_is_map_access && expected.is_boolean() {
3268 let lit = match expected.as_bool() {
3269 Some(true) => "\"true\"",
3270 _ => "\"false\"",
3271 };
3272 let _ = writeln!(
3273 out,
3274 " assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
3275 );
3276 } else if field_is_map_access && expected.is_number() {
3277 if expected.is_f64() {
3278 let _ = writeln!(
3279 out,
3280 " assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
3281 );
3282 } else {
3283 let _ = writeln!(
3284 out,
3285 " assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
3286 );
3287 }
3288 } else {
3289 let _ = writeln!(
3290 out,
3291 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3292 );
3293 }
3294 }
3295 }
3296 "contains" => {
3297 if let Some(expected) = &assertion.value {
3298 let c_val = json_to_c(expected);
3299 let _ = writeln!(
3300 out,
3301 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3302 );
3303 }
3304 }
3305 "contains_all" => {
3306 if let Some(values) = &assertion.values {
3307 for val in values {
3308 let c_val = json_to_c(val);
3309 let _ = writeln!(
3310 out,
3311 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3312 );
3313 }
3314 }
3315 }
3316 "not_contains" => {
3317 if let Some(expected) = &assertion.value {
3318 let c_val = json_to_c(expected);
3319 let _ = writeln!(
3320 out,
3321 " assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3322 );
3323 }
3324 }
3325 "not_empty" => {
3326 if field_is_opaque_handle {
3327 let _ = writeln!(out, " assert({field_expr} != NULL && \"expected non-null handle\");");
3331 } else {
3332 let _ = writeln!(
3333 out,
3334 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
3335 );
3336 }
3337 }
3338 "is_empty" => {
3339 if field_is_opaque_handle {
3340 let _ = writeln!(out, " assert({field_expr} == NULL && \"expected null handle\");");
3341 } else if assertion_field_is_optional || !field_is_primitive {
3342 let _ = writeln!(
3344 out,
3345 " assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
3346 );
3347 } else {
3348 let _ = writeln!(
3349 out,
3350 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
3351 );
3352 }
3353 }
3354 "contains_any" => {
3355 if let Some(values) = &assertion.values {
3356 let _ = writeln!(out, " {{");
3357 let _ = writeln!(out, " int found = 0;");
3358 for val in values {
3359 let c_val = json_to_c(val);
3360 let _ = writeln!(
3361 out,
3362 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
3363 );
3364 }
3365 let _ = writeln!(
3366 out,
3367 " assert(found && \"expected to contain at least one of the specified values\");"
3368 );
3369 let _ = writeln!(out, " }}");
3370 }
3371 }
3372 "greater_than" => {
3373 if let Some(val) = &assertion.value {
3374 let c_val = json_to_c(val);
3375 if field_is_map_access && val.is_number() && !field_is_primitive {
3376 let _ = writeln!(
3377 out,
3378 " assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
3379 );
3380 } else {
3381 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
3382 }
3383 }
3384 }
3385 "less_than" => {
3386 if let Some(val) = &assertion.value {
3387 let c_val = json_to_c(val);
3388 if field_is_map_access && val.is_number() && !field_is_primitive {
3389 let _ = writeln!(
3390 out,
3391 " assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
3392 );
3393 } else {
3394 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
3395 }
3396 }
3397 }
3398 "greater_than_or_equal" => {
3399 if let Some(val) = &assertion.value {
3400 let c_val = json_to_c(val);
3401 if field_is_map_access && val.is_number() && !field_is_primitive {
3402 let _ = writeln!(
3403 out,
3404 " assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
3405 );
3406 } else {
3407 let _ = writeln!(
3408 out,
3409 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
3410 );
3411 }
3412 }
3413 }
3414 "less_than_or_equal" => {
3415 if let Some(val) = &assertion.value {
3416 let c_val = json_to_c(val);
3417 if field_is_map_access && val.is_number() && !field_is_primitive {
3418 let _ = writeln!(
3419 out,
3420 " assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
3421 );
3422 } else {
3423 let _ = writeln!(
3424 out,
3425 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
3426 );
3427 }
3428 }
3429 }
3430 "starts_with" => {
3431 if let Some(expected) = &assertion.value {
3432 let c_val = json_to_c(expected);
3433 let _ = writeln!(
3434 out,
3435 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
3436 );
3437 }
3438 }
3439 "ends_with" => {
3440 if let Some(expected) = &assertion.value {
3441 let c_val = json_to_c(expected);
3442 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
3443 let _ = writeln!(
3444 out,
3445 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
3446 );
3447 }
3448 }
3449 "min_length" => {
3450 if let Some(val) = &assertion.value {
3451 if let Some(n) = val.as_u64() {
3452 let _ = writeln!(
3453 out,
3454 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
3455 );
3456 }
3457 }
3458 }
3459 "max_length" => {
3460 if let Some(val) = &assertion.value {
3461 if let Some(n) = val.as_u64() {
3462 let _ = writeln!(
3463 out,
3464 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
3465 );
3466 }
3467 }
3468 }
3469 "count_min" => {
3470 if let Some(val) = &assertion.value {
3471 if let Some(n) = val.as_u64() {
3472 let _ = writeln!(out, " {{");
3473 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
3474 let _ = writeln!(
3475 out,
3476 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3477 );
3478 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3479 let _ = writeln!(
3480 out,
3481 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
3482 );
3483 let _ = writeln!(out, " }}");
3484 }
3485 }
3486 }
3487 "count_equals" => {
3488 if let Some(val) = &assertion.value {
3489 if let Some(n) = val.as_u64() {
3490 let _ = writeln!(out, " {{");
3491 let _ = writeln!(out, " /* count_equals: count elements in array */");
3492 let _ = writeln!(
3493 out,
3494 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3495 );
3496 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3497 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
3498 let _ = writeln!(out, " }}");
3499 }
3500 }
3501 }
3502 "is_true" => {
3503 let _ = writeln!(out, " assert({field_expr});");
3504 }
3505 "is_false" => {
3506 let _ = writeln!(out, " assert(!{field_expr});");
3507 }
3508 "method_result" => {
3509 if let Some(method_name) = &assertion.method {
3510 render_method_result_assertion(
3511 out,
3512 result_var,
3513 ffi_prefix,
3514 method_name,
3515 assertion.args.as_ref(),
3516 assertion.return_type.as_deref(),
3517 assertion.check.as_deref().unwrap_or("is_true"),
3518 assertion.value.as_ref(),
3519 );
3520 } else {
3521 panic!("C e2e generator: method_result assertion missing 'method' field");
3522 }
3523 }
3524 "matches_regex" => {
3525 if let Some(expected) = &assertion.value {
3526 let c_val = json_to_c(expected);
3527 let _ = writeln!(out, " {{");
3528 let _ = writeln!(out, " regex_t _re;");
3529 let _ = writeln!(
3530 out,
3531 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3532 );
3533 let _ = writeln!(
3534 out,
3535 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3536 );
3537 let _ = writeln!(out, " regfree(&_re);");
3538 let _ = writeln!(out, " }}");
3539 }
3540 }
3541 "not_error" => {
3542 }
3544 "error" => {
3545 }
3547 other => {
3548 panic!("C e2e generator: unsupported assertion type: {other}");
3549 }
3550 }
3551}
3552
3553#[allow(clippy::too_many_arguments)]
3562fn render_method_result_assertion(
3563 out: &mut String,
3564 result_var: &str,
3565 ffi_prefix: &str,
3566 method_name: &str,
3567 args: Option<&serde_json::Value>,
3568 return_type: Option<&str>,
3569 check: &str,
3570 value: Option<&serde_json::Value>,
3571) {
3572 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3573
3574 if return_type == Some("string") {
3575 let _ = writeln!(out, " {{");
3577 let _ = writeln!(out, " char* _method_result = {call_expr};");
3578 if check == "is_error" {
3579 let _ = writeln!(
3580 out,
3581 " assert(_method_result == NULL && \"expected method to return error\");"
3582 );
3583 let _ = writeln!(out, " }}");
3584 return;
3585 }
3586 let _ = writeln!(
3587 out,
3588 " assert(_method_result != NULL && \"method_result returned NULL\");"
3589 );
3590 match check {
3591 "contains" => {
3592 if let Some(val) = value {
3593 let c_val = json_to_c(val);
3594 let _ = writeln!(
3595 out,
3596 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3597 );
3598 }
3599 }
3600 "equals" => {
3601 if let Some(val) = value {
3602 let c_val = json_to_c(val);
3603 let _ = writeln!(
3604 out,
3605 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3606 );
3607 }
3608 }
3609 "is_true" => {
3610 let _ = writeln!(
3611 out,
3612 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3613 );
3614 }
3615 "count_min" => {
3616 if let Some(val) = value {
3617 let n = val.as_u64().unwrap_or(0);
3618 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
3619 let _ = writeln!(
3620 out,
3621 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3622 );
3623 }
3624 }
3625 other_check => {
3626 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3627 }
3628 }
3629 let _ = writeln!(out, " free(_method_result);");
3630 let _ = writeln!(out, " }}");
3631 return;
3632 }
3633
3634 match check {
3636 "equals" => {
3637 if let Some(val) = value {
3638 let c_val = json_to_c(val);
3639 let _ = writeln!(
3640 out,
3641 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3642 );
3643 }
3644 }
3645 "is_true" => {
3646 let _ = writeln!(
3647 out,
3648 " assert({call_expr} && \"method_result is_true assertion failed\");"
3649 );
3650 }
3651 "is_false" => {
3652 let _ = writeln!(
3653 out,
3654 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
3655 );
3656 }
3657 "greater_than_or_equal" => {
3658 if let Some(val) = value {
3659 let n = val.as_u64().unwrap_or(0);
3660 let _ = writeln!(
3661 out,
3662 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3663 );
3664 }
3665 }
3666 "count_min" => {
3667 if let Some(val) = value {
3668 let n = val.as_u64().unwrap_or(0);
3669 let _ = writeln!(
3670 out,
3671 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3672 );
3673 }
3674 }
3675 other_check => {
3676 panic!("C e2e generator: unsupported method_result check type: {other_check}");
3677 }
3678 }
3679}
3680
3681fn build_c_method_call(
3688 result_var: &str,
3689 ffi_prefix: &str,
3690 method_name: &str,
3691 args: Option<&serde_json::Value>,
3692) -> String {
3693 let extra_args = if let Some(args_val) = args {
3694 args_val
3695 .as_object()
3696 .map(|obj| {
3697 obj.values()
3698 .map(|v| match v {
3699 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3700 serde_json::Value::Bool(true) => "1".to_string(),
3701 serde_json::Value::Bool(false) => "0".to_string(),
3702 serde_json::Value::Number(n) => n.to_string(),
3703 serde_json::Value::Null => "NULL".to_string(),
3704 other => format!("\"{}\"", escape_c(&other.to_string())),
3705 })
3706 .collect::<Vec<_>>()
3707 .join(", ")
3708 })
3709 .unwrap_or_default()
3710 } else {
3711 String::new()
3712 };
3713
3714 if extra_args.is_empty() {
3715 format!("{ffi_prefix}_{method_name}({result_var})")
3716 } else {
3717 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3718 }
3719}
3720
3721fn json_to_c(value: &serde_json::Value) -> String {
3723 match value {
3724 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3725 serde_json::Value::Bool(true) => "1".to_string(),
3726 serde_json::Value::Bool(false) => "0".to_string(),
3727 serde_json::Value::Number(n) => n.to_string(),
3728 serde_json::Value::Null => "NULL".to_string(),
3729 other => format!("\"{}\"", escape_c(&other.to_string())),
3730 }
3731}
3732
3733fn render_visitor_test_file(fixtures: &[&Fixture], header: &str, prefix: &str) -> String {
3750 use crate::fixture::CallbackAction;
3751
3752 let mut out = String::new();
3753 out.push_str(&hash::header(CommentStyle::Block));
3754 let _ = writeln!(out, "/* E2e tests for category: visitor */");
3755 let _ = writeln!(out);
3756 let _ = writeln!(out, "#include <assert.h>");
3757 let _ = writeln!(out, "#include <stdint.h>");
3758 let _ = writeln!(out, "#include <string.h>");
3759 let _ = writeln!(out, "#include <stdio.h>");
3760 let _ = writeln!(out, "#include <stdlib.h>");
3761 let _ = writeln!(out, "#include \"{header}\"");
3762 let _ = writeln!(out, "#include \"test_runner.h\"");
3763 let _ = writeln!(out);
3764
3765 let prefix_upper = prefix.to_uppercase();
3766
3767 for (i, fixture) in fixtures.iter().enumerate() {
3768 let fn_name = sanitize_ident(&fixture.id);
3769 let description = &fixture.description;
3770
3771 let visitor_spec = match &fixture.visitor {
3772 Some(v) => v,
3773 None => continue,
3774 };
3775
3776 let html = fixture.input.get("html").and_then(|v| v.as_str()).unwrap_or("");
3777 let html_escaped = escape_c(html);
3778
3779 let options_json = match fixture.input.get("options") {
3780 Some(opts) => serde_json::to_string(opts).unwrap_or_else(|_| "{}".to_string()),
3781 None => "{}".to_string(),
3782 };
3783 let options_escaped = escape_c(&options_json);
3784
3785 let mut sorted_callbacks: Vec<(&String, &CallbackAction)> = visitor_spec.callbacks.iter().collect();
3788 sorted_callbacks.sort_by(|a, b| a.0.cmp(b.0));
3789
3790 for (method, action) in &sorted_callbacks {
3791 let cb_name = format!("c_visitor_{fn_name}_{method}");
3792 let params = c_visitor_callback_params(method);
3793 let body = c_visitor_callback_body(method, action);
3794 let _ = writeln!(out, "static int32_t {cb_name}({params}) {{");
3795 out.push_str(&body);
3796 let _ = writeln!(out, "}}");
3797 let _ = writeln!(out);
3798 }
3799
3800 let _ = writeln!(out, "void test_{fn_name}(void) {{");
3802 let _ = writeln!(out, " /* {description} */");
3803 let _ = writeln!(out);
3804
3805 let _ = writeln!(out, " {prefix_upper}HtmVisitorCallbacks _callbacks;");
3807 let _ = writeln!(out, " memset(&_callbacks, 0, sizeof(_callbacks));");
3808 for (method, _) in &sorted_callbacks {
3809 let cb_name = format!("c_visitor_{fn_name}_{method}");
3810 let _ = writeln!(out, " _callbacks.{method} = {cb_name};");
3811 }
3812 let _ = writeln!(out);
3813
3814 let _ = writeln!(
3816 out,
3817 " {prefix_upper}HtmVisitor* _visitor = {prefix}_visitor_create(&_callbacks);"
3818 );
3819 let _ = writeln!(out, " assert(_visitor != NULL && \"htm_visitor_create failed\");");
3820 let _ = writeln!(out);
3821
3822 let _ = writeln!(
3824 out,
3825 " {prefix_upper}ConversionOptions* _options = {prefix}_conversion_options_from_json(\"{options_escaped}\");"
3826 );
3827 let _ = writeln!(
3828 out,
3829 " assert(_options != NULL && \"htm_conversion_options_from_json failed\");"
3830 );
3831 let _ = writeln!(out);
3832
3833 let _ = writeln!(out, " {prefix}_options_set_visitor_handle(_options, _visitor);");
3835 let _ = writeln!(out);
3836
3837 let _ = writeln!(
3839 out,
3840 " {prefix_upper}ConversionResult* _result = {prefix}_convert(\"{html_escaped}\", _options);"
3841 );
3842 let _ = writeln!(out, " assert(_result != NULL && \"htm_convert failed\");");
3843 let _ = writeln!(out);
3844
3845 let _ = writeln!(out, " char* _json = {prefix}_conversion_result_to_json(_result);");
3847 let _ = writeln!(out, " assert(_json != NULL && \"result to_json failed\");");
3848 let _ = writeln!(out, " char* _content = alef_json_get_string(_json, \"content\");");
3849 let _ = writeln!(out);
3850
3851 for assertion in &fixture.assertions {
3853 match assertion.assertion_type.as_str() {
3854 "contains" => {
3855 if let Some(expected) = &assertion.value {
3856 let c_val = json_to_c(expected);
3857 let _ = writeln!(
3858 out,
3859 " assert(_content != NULL && strstr(_content, {c_val}) != NULL && \"expected to contain substring\");"
3860 );
3861 }
3862 }
3863 "not_contains" => {
3864 if let Some(expected) = &assertion.value {
3865 let c_val = json_to_c(expected);
3866 let _ = writeln!(
3867 out,
3868 " assert((_content == NULL || strstr(_content, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3869 );
3870 }
3871 }
3872 other => {
3873 let _ = writeln!(
3874 out,
3875 " /* assertion type '{other}' not supported in C visitor tests */"
3876 );
3877 }
3878 }
3879 }
3880
3881 let _ = writeln!(out);
3882
3883 let _ = writeln!(out, " free(_content);");
3885 let _ = writeln!(out, " {prefix}_free_string(_json);");
3886 let _ = writeln!(out, " {prefix}_conversion_result_free(_result);");
3887 let _ = writeln!(out, " {prefix}_conversion_options_free(_options);");
3888 let _ = writeln!(out, " {prefix}_visitor_free(_visitor);");
3889 let _ = writeln!(out, "}}");
3890
3891 if i + 1 < fixtures.len() {
3892 let _ = writeln!(out);
3893 }
3894 }
3895
3896 out
3897}
3898
3899fn c_visitor_callback_params(method: &str) -> &'static str {
3906 match method {
3907 "visit_text" => {
3908 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _text, char** out_custom, size_t* out_len"
3909 }
3910 "visit_element_start" => "const HTMHtmNodeContext* _ctx, void* _user_data, char** out_custom, size_t* out_len",
3911 "visit_element_end" => {
3912 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _output, char** out_custom, size_t* out_len"
3913 }
3914 "visit_link" => {
3915 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _href, const char* _text, const char* _title, char** out_custom, size_t* out_len"
3916 }
3917 "visit_image" => {
3918 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _src, const char* _alt, const char* _title, char** out_custom, size_t* out_len"
3919 }
3920 "visit_heading" => {
3921 "const HTMHtmNodeContext* _ctx, void* _user_data, uint32_t _level, const char* _text, const char* _id, char** out_custom, size_t* out_len"
3922 }
3923 "visit_code_block" => {
3924 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _lang, const char* _code, char** out_custom, size_t* out_len"
3925 }
3926 "visit_code_inline" => {
3927 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _code, char** out_custom, size_t* out_len"
3928 }
3929 "visit_list_item" => {
3930 "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _ordered, const char* _marker, const char* _text, char** out_custom, size_t* out_len"
3931 }
3932 "visit_list_start" => {
3933 "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _ordered, char** out_custom, size_t* out_len"
3934 }
3935 "visit_list_end" => {
3936 "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _ordered, const char* _output, char** out_custom, size_t* out_len"
3937 }
3938 "visit_table_start" => "const HTMHtmNodeContext* _ctx, void* _user_data, char** out_custom, size_t* out_len",
3939 "visit_table_row" => {
3940 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* const* _cells, size_t _cell_count, int32_t _is_header, char** out_custom, size_t* out_len"
3941 }
3942 "visit_table_end" => {
3943 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _output, char** out_custom, size_t* out_len"
3944 }
3945 "visit_blockquote" => {
3946 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _content, size_t _depth, char** out_custom, size_t* out_len"
3947 }
3948 "visit_line_break" | "visit_horizontal_rule" | "visit_definition_list_start" | "visit_figure_start" => {
3949 "const HTMHtmNodeContext* _ctx, void* _user_data, char** out_custom, size_t* out_len"
3950 }
3951 "visit_custom_element" => {
3952 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _tag_name, const char* _html, char** out_custom, size_t* out_len"
3953 }
3954 "visit_form" => {
3955 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _action, const char* _method, char** out_custom, size_t* out_len"
3956 }
3957 "visit_input" => {
3958 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _input_type, const char* _name, const char* _value, char** out_custom, size_t* out_len"
3959 }
3960 "visit_audio" | "visit_video" | "visit_iframe" => {
3961 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _src, char** out_custom, size_t* out_len"
3962 }
3963 "visit_details" => {
3964 "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _open, char** out_custom, size_t* out_len"
3965 }
3966 "visit_figure_end" | "visit_definition_list_end" => {
3967 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _output, char** out_custom, size_t* out_len"
3968 }
3969 _ => "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _text, char** out_custom, size_t* out_len",
3974 }
3975}
3976
3977fn c_visitor_callback_body(method: &str, action: &crate::fixture::CallbackAction) -> String {
3986 use crate::fixture::CallbackAction;
3987
3988 let mut out = String::new();
3989 let _ = writeln!(out, " (void)_ctx;");
3992 let _ = writeln!(out, " (void)_user_data;");
3993
3994 match action {
3995 CallbackAction::Skip => {
3996 let _ = writeln!(out, " (void)out_custom;");
3997 let _ = writeln!(out, " (void)out_len;");
3998 for param in c_visitor_unused_params(method) {
4000 let _ = writeln!(out, " (void){param};");
4001 }
4002 let _ = writeln!(out, " return 1;");
4003 }
4004 CallbackAction::Continue => {
4005 let _ = writeln!(out, " (void)out_custom;");
4006 let _ = writeln!(out, " (void)out_len;");
4007 for param in c_visitor_unused_params(method) {
4008 let _ = writeln!(out, " (void){param};");
4009 }
4010 let _ = writeln!(out, " return 0;");
4011 }
4012 CallbackAction::PreserveHtml => {
4013 let _ = writeln!(out, " (void)out_custom;");
4014 let _ = writeln!(out, " (void)out_len;");
4015 for param in c_visitor_unused_params(method) {
4016 let _ = writeln!(out, " (void){param};");
4017 }
4018 let _ = writeln!(out, " return 2;");
4019 }
4020 CallbackAction::Custom { output } => {
4021 let escaped = escape_c(output);
4022 for param in c_visitor_unused_params(method) {
4023 let _ = writeln!(out, " (void){param};");
4024 }
4025 let _ = writeln!(out, " char* _buf = strdup(\"{escaped}\");");
4026 let _ = writeln!(out, " if (out_custom) *out_custom = _buf;");
4027 let _ = writeln!(out, " if (out_len) *out_len = _buf ? strlen(_buf) : 0;");
4028 let _ = writeln!(out, " return 3;");
4029 }
4030 CallbackAction::CustomTemplate { template, .. } => {
4031 let (c_fmt, placeholders) = c_visitor_template_to_sprintf(template);
4033 let escaped_fmt = escape_c(&c_fmt);
4034
4035 let used: std::collections::HashSet<&str> = placeholders.iter().map(|s| s.as_str()).collect();
4037 for param in c_visitor_unused_params(method) {
4038 let stripped = param.trim_start_matches('_');
4039 if !used.contains(stripped) {
4040 let _ = writeln!(out, " (void){param};");
4041 }
4042 }
4043
4044 if placeholders.is_empty() {
4045 let _ = writeln!(out, " char* _buf = strdup(\"{escaped_fmt}\");");
4046 } else {
4047 let max_len = template.len() + placeholders.len() * 256 + 64;
4050 let _ = writeln!(out, " char* _buf = (char*)malloc({max_len});");
4051 let _ = writeln!(out, " if (!_buf) {{ (void)out_custom; (void)out_len; return 0; }}");
4052 let args: Vec<String> = placeholders
4054 .iter()
4055 .map(|name| c_visitor_placeholder_to_arg(method, name))
4056 .collect();
4057 let args_str = args.join(", ");
4058 let _ = writeln!(out, " snprintf(_buf, {max_len}, \"{escaped_fmt}\", {args_str});");
4059 }
4060
4061 let _ = writeln!(out, " if (out_custom) *out_custom = _buf;");
4062 let _ = writeln!(out, " if (out_len) *out_len = _buf ? strlen(_buf) : 0;");
4063 let _ = writeln!(out, " return 3;");
4064 }
4065 }
4066
4067 out
4068}
4069
4070fn c_visitor_unused_params(method: &str) -> Vec<&'static str> {
4074 match method {
4075 "visit_text" => vec!["_text"],
4076 "visit_element_start"
4077 | "visit_table_start"
4078 | "visit_line_break"
4079 | "visit_horizontal_rule"
4080 | "visit_definition_list_start"
4081 | "visit_figure_start" => vec![],
4082 "visit_element_end" | "visit_table_end" | "visit_figure_end" | "visit_definition_list_end" => {
4083 vec!["_output"]
4084 }
4085 "visit_link" => vec!["_href", "_text", "_title"],
4086 "visit_image" => vec!["_src", "_alt", "_title"],
4087 "visit_heading" => vec!["_level", "_text", "_id"],
4088 "visit_code_block" => vec!["_lang", "_code"],
4089 "visit_code_inline" => vec!["_code"],
4090 "visit_list_item" => vec!["_ordered", "_marker", "_text"],
4091 "visit_list_start" => vec!["_ordered"],
4092 "visit_list_end" => vec!["_ordered", "_output"],
4093 "visit_table_row" => vec!["_cells", "_cell_count", "_is_header"],
4094 "visit_blockquote" => vec!["_content", "_depth"],
4095 "visit_custom_element" => vec!["_tag_name", "_html"],
4096 "visit_form" => vec!["_action", "_method"],
4097 "visit_input" => vec!["_input_type", "_name", "_value"],
4098 "visit_audio" | "visit_video" | "visit_iframe" => vec!["_src"],
4099 "visit_details" => vec!["_open"],
4100 _ => vec!["_text"],
4102 }
4103}
4104
4105fn c_visitor_template_to_sprintf(template: &str) -> (String, Vec<String>) {
4109 let mut out = String::with_capacity(template.len());
4110 let mut placeholders: Vec<String> = Vec::new();
4111 let mut chars = template.chars().peekable();
4112 while let Some(ch) = chars.next() {
4113 match ch {
4114 '{' => {
4115 if chars.peek() == Some(&'{') {
4116 chars.next();
4117 out.push('{');
4118 continue;
4119 }
4120 let mut name = String::new();
4121 while let Some(&peek) = chars.peek() {
4122 if peek == '}' {
4123 chars.next();
4124 break;
4125 }
4126 name.push(peek);
4127 chars.next();
4128 }
4129 let is_int = matches!(name.as_str(), "level" | "depth" | "ordered" | "open" | "is_header");
4130 if is_int {
4131 out.push_str("%d");
4132 } else {
4133 out.push_str("%s");
4134 }
4135 placeholders.push(name);
4136 }
4137 '}' => {
4138 if chars.peek() == Some(&'}') {
4139 chars.next();
4140 }
4141 out.push('}');
4142 }
4143 '%' => {
4144 out.push_str("%%");
4146 }
4147 other => out.push(other),
4148 }
4149 }
4150 (out, placeholders)
4151}
4152
4153fn c_visitor_placeholder_to_arg(method: &str, name: &str) -> String {
4156 let int_placeholder = matches!(
4157 (method, name),
4158 ("visit_heading", "level")
4159 | ("visit_blockquote", "depth")
4160 | ("visit_list_item", "ordered")
4161 | ("visit_list_start", "ordered")
4162 | ("visit_list_end", "ordered")
4163 | ("visit_details", "open")
4164 | ("visit_table_row", "is_header")
4165 );
4166 if int_placeholder {
4167 return format!("_{name}");
4168 }
4169 format!("(_{name} ? _{name} : \"\")")
4173}