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_result = 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(returned_type) = leaf_result {
1817 if is_primitive_c_type(&returned_type) {
1819 primitive_locals.insert(local_var.clone(), returned_type);
1820 } else {
1821 opaque_handle_locals.insert(local_var.clone(), returned_type);
1823 }
1824 }
1825 } else {
1826 let result_type_snake = result_type_name.to_snake_case();
1827 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1828 let lookup_key = format!("{result_type_snake}.{resolved}");
1829 if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
1830 primitive_locals.insert(local_var.clone(), "__skip__".to_string());
1832 } else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1833 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
1834 primitive_locals.insert(local_var.clone(), t.clone());
1835 } else if try_emit_enum_accessor(
1836 out,
1837 prefix,
1838 &prefix_upper,
1839 f,
1840 resolved,
1841 &result_type_snake,
1842 &accessor_fn,
1843 result_var,
1844 &local_var,
1845 fields_c_types,
1846 fields_enum,
1847 &mut intermediate_handles,
1848 ) {
1849 } else if let Some(handle_pascal) =
1851 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1852 {
1853 let _ = writeln!(
1854 out,
1855 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1856 );
1857 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1858 } else {
1859 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1860 }
1861 }
1862 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1863 }
1864 }
1865 }
1866
1867 for assertion in &fixture.assertions {
1868 render_assertion(
1869 out,
1870 assertion,
1871 result_var,
1872 prefix,
1873 field_resolver,
1874 &accessed_fields,
1875 &primitive_locals,
1876 &opaque_handle_locals,
1877 );
1878 }
1879
1880 for (_f, local_var, from_json) in &accessed_fields {
1882 if primitive_locals.contains_key(local_var) {
1883 continue;
1884 }
1885 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1886 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
1887 continue;
1888 }
1889 if *from_json {
1890 let _ = writeln!(out, " free({local_var});");
1891 } else {
1892 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1893 }
1894 }
1895 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1897 if snake_type == "free_string" {
1898 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1900 } else if snake_type == "free" {
1901 let _ = writeln!(out, " free({handle_var});");
1903 } else {
1904 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1905 }
1906 }
1907 if has_options_handle {
1908 let options_type_snake = options_type_name.to_snake_case();
1909 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1910 }
1911 let result_type_snake = result_type_name.to_snake_case();
1912 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1913 let _ = writeln!(out, "}}");
1914}
1915
1916#[allow(clippy::too_many_arguments)]
1925fn render_engine_factory_test_function(
1926 out: &mut String,
1927 fixture: &Fixture,
1928 prefix: &str,
1929 function_name: &str,
1930 result_var: &str,
1931 field_resolver: &FieldResolver,
1932 fields_c_types: &HashMap<String, String>,
1933 fields_enum: &HashSet<String>,
1934 result_type_name: &str,
1935 config_type: &str,
1936 expects_error: bool,
1937 raw_c_result_type: Option<&str>,
1938) {
1939 let prefix_upper = prefix.to_uppercase();
1940 let config_snake = config_type.to_snake_case();
1941
1942 let config_val = fixture.input.get("config");
1944 let config_json = match config_val {
1945 Some(v) if !v.is_null() => {
1946 let normalized = super::transform_json_keys_for_language(v, "snake_case");
1947 serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1948 }
1949 _ => "{}".to_string(),
1950 };
1951 let config_escaped = escape_c(&config_json);
1952 let fixture_id = &fixture.id;
1953
1954 let has_active_assertions = fixture.assertions.iter().any(|a| {
1958 if let Some(f) = &a.field {
1959 !f.is_empty() && field_resolver.is_valid_for_result(f)
1960 } else {
1961 false
1962 }
1963 });
1964
1965 let _ = writeln!(
1967 out,
1968 " {prefix_upper}{config_type}* config_handle = \
1969 {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1970 );
1971 if expects_error {
1972 let _ = writeln!(out, " if (config_handle == NULL) {{ return; }}");
1975 } else {
1976 let _ = writeln!(out, " assert(config_handle != NULL && \"failed to parse config\");");
1977 }
1978 let _ = writeln!(
1979 out,
1980 " {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1981 );
1982 let _ = writeln!(out, " {prefix}_{config_snake}_free(config_handle);");
1983 if expects_error {
1984 let _ = writeln!(out, " if (engine == NULL) {{ return; }}");
1987 } else {
1988 let _ = writeln!(out, " assert(engine != NULL && \"failed to create engine\");");
1989 }
1990
1991 let fixture_env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1995 let _ = writeln!(out, " const char* mock_per_fixture = getenv(\"{fixture_env_key}\");");
1996 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1997 let _ = writeln!(out, " char url[2048];");
1998 let _ = writeln!(out, " if (mock_per_fixture && mock_per_fixture[0] != '\\0') {{");
1999 let _ = writeln!(out, " snprintf(url, sizeof(url), \"%s\", mock_per_fixture);");
2000 let _ = writeln!(out, " }} else {{");
2001 let _ = writeln!(
2002 out,
2003 " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");"
2004 );
2005 let _ = writeln!(
2006 out,
2007 " snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
2008 );
2009 let _ = writeln!(out, " }}");
2010
2011 let actions_arg = fixture.input.get("actions").and_then(|v| {
2017 if v.is_null() {
2018 None
2019 } else {
2020 let normalized = super::transform_json_keys_for_language(v, "snake_case");
2021 let json = serde_json::to_string(&normalized).ok()?;
2022 let escaped = escape_c(&json);
2023 Some(escaped)
2024 }
2025 });
2026 if let Some(ref escaped_actions) = actions_arg {
2027 let _ = writeln!(out, " const char* actions_json = \"{escaped_actions}\";");
2028 }
2029
2030 let extra_call_args = if actions_arg.is_some() {
2033 ", actions_json".to_string()
2034 } else {
2035 String::new()
2036 };
2037
2038 if let Some(raw_type) = raw_c_result_type {
2047 if raw_type == "char*" {
2048 let _ = writeln!(
2049 out,
2050 " char* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
2051 );
2052 let _ = writeln!(out, " if ({result_var} != NULL) {prefix}_free_string({result_var});");
2053 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2054 let _ = writeln!(out, "}}");
2055 return;
2056 } else {
2057 let raw_snake = raw_type.to_snake_case();
2060 let _ = writeln!(
2061 out,
2062 " {prefix_upper}{raw_type}* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
2063 );
2064 let _ = writeln!(
2065 out,
2066 " if ({result_var} != NULL) {prefix}_{raw_snake}_free({result_var});"
2067 );
2068 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2069 let _ = writeln!(out, "}}");
2070 return;
2071 }
2072 }
2073
2074 let _ = writeln!(
2075 out,
2076 " {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
2077 );
2078
2079 if !has_active_assertions {
2082 let result_type_snake = result_type_name.to_snake_case();
2083 let _ = writeln!(
2084 out,
2085 " if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
2086 );
2087 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2088 let _ = writeln!(out, "}}");
2089 return;
2090 }
2091
2092 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
2093
2094 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
2096 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
2097 let mut primitive_locals: HashMap<String, String> = HashMap::new();
2098 let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
2099
2100 for assertion in &fixture.assertions {
2101 if let Some(f) = &assertion.field {
2102 if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
2103 {
2104 let resolved_raw = field_resolver.resolve(f);
2105 let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
2108 let stripped_first = stripped.split('.').next().unwrap_or(stripped);
2109 let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
2110 if field_resolver.is_valid_for_result(stripped_first) {
2111 stripped
2112 } else {
2113 resolved_raw
2114 }
2115 } else {
2116 resolved_raw
2117 };
2118 let local_var = f.replace(['.', '['], "_").replace(']', "");
2119 let has_map_access = resolved.contains('[');
2120 if resolved.contains('.') {
2121 let leaf_result = emit_nested_accessor(
2122 out,
2123 prefix,
2124 resolved,
2125 &local_var,
2126 result_var,
2127 fields_c_types,
2128 fields_enum,
2129 &mut intermediate_handles,
2130 result_type_name,
2131 f,
2132 );
2133 if let Some(returned_type) = leaf_result {
2134 if is_primitive_c_type(&returned_type) {
2136 primitive_locals.insert(local_var.clone(), returned_type);
2137 } else {
2138 opaque_handle_locals.insert(local_var.clone(), returned_type);
2140 }
2141 }
2142 } else {
2143 let result_type_snake = result_type_name.to_snake_case();
2144 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
2145 let lookup_key = format!("{result_type_snake}.{resolved}");
2146 if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
2147 primitive_locals.insert(local_var.clone(), "__skip__".to_string());
2149 } else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2150 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
2151 primitive_locals.insert(local_var.clone(), t.clone());
2152 } else if try_emit_enum_accessor(
2153 out,
2154 prefix,
2155 &prefix_upper,
2156 f,
2157 resolved,
2158 &result_type_snake,
2159 &accessor_fn,
2160 result_var,
2161 &local_var,
2162 fields_c_types,
2163 fields_enum,
2164 &mut intermediate_handles,
2165 ) {
2166 } else if let Some(handle_pascal) =
2168 infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
2169 {
2170 let _ = writeln!(
2171 out,
2172 " {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
2173 );
2174 opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
2175 } else {
2176 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
2177 }
2178 }
2179 accessed_fields.push((f.clone(), local_var, has_map_access));
2180 }
2181 }
2182 }
2183
2184 for assertion in &fixture.assertions {
2185 render_assertion(
2186 out,
2187 assertion,
2188 result_var,
2189 prefix,
2190 field_resolver,
2191 &accessed_fields,
2192 &primitive_locals,
2193 &opaque_handle_locals,
2194 );
2195 }
2196
2197 for (_f, local_var, from_json) in &accessed_fields {
2199 if primitive_locals.contains_key(local_var) {
2200 continue;
2201 }
2202 if let Some(snake_type) = opaque_handle_locals.get(local_var) {
2203 let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
2204 continue;
2205 }
2206 if *from_json {
2207 let _ = writeln!(out, " free({local_var});");
2208 } else {
2209 let _ = writeln!(out, " {prefix}_free_string({local_var});");
2210 }
2211 }
2212 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
2213 if snake_type == "free_string" {
2214 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
2215 } else if snake_type == "free" {
2216 let _ = writeln!(out, " free({handle_var});");
2218 } else {
2219 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
2220 }
2221 }
2222
2223 let result_type_snake = result_type_name.to_snake_case();
2224 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
2225 let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
2226 let _ = writeln!(out, "}}");
2227}
2228
2229#[allow(clippy::too_many_arguments)]
2251fn render_bytes_test_function(
2252 out: &mut String,
2253 fixture: &Fixture,
2254 prefix: &str,
2255 function_name: &str,
2256 _result_var: &str,
2257 args: &[crate::config::ArgMapping],
2258 options_type_name: &str,
2259 result_type_name: &str,
2260 factory: &str,
2261 expects_error: bool,
2262) {
2263 let prefix_upper = prefix.to_uppercase();
2264 let mut request_handle_vars: Vec<(String, String)> = Vec::new();
2265 let mut string_arg_exprs: Vec<String> = Vec::new();
2266
2267 for arg in args {
2268 match arg.arg_type.as_str() {
2269 "json_object" => {
2270 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2271 options_type_name.to_string()
2272 } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
2273 format!("{}Request", stripped)
2274 } else {
2275 format!("{result_type_name}Request")
2276 };
2277 let request_type_snake = request_type_pascal.to_snake_case();
2278 let var_name = format!("{request_type_snake}_handle");
2279
2280 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2281 let json_val = if field.is_empty() || field == "input" {
2282 Some(&fixture.input)
2283 } else {
2284 fixture.input.get(field)
2285 };
2286
2287 if let Some(val) = json_val {
2288 if !val.is_null() {
2289 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2290 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2291 let escaped = escape_c(&json_str);
2292 let _ = writeln!(
2293 out,
2294 " {prefix_upper}{request_type_pascal}* {var_name} = \
2295 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2296 );
2297 if expects_error {
2298 let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
2306 } else {
2307 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2308 }
2309 request_handle_vars.push((arg.name.clone(), var_name));
2310 }
2311 }
2312 }
2313 "string" => {
2314 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2317 let val = fixture.input.get(field);
2318 let expr = match val {
2319 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
2320 Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
2321 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
2322 None => "NULL".to_string(),
2323 };
2324 string_arg_exprs.push(expr);
2325 }
2326 _ => {
2327 string_arg_exprs.push("NULL".to_string());
2330 }
2331 }
2332 }
2333
2334 let fixture_id = &fixture.id;
2335 if fixture.needs_mock_server() {
2336 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2337 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2338 let _ = writeln!(out, " char base_url[1024];");
2339 let _ = writeln!(
2340 out,
2341 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2342 );
2343 let _ = writeln!(
2348 out,
2349 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2350 );
2351 } else {
2352 let _ = writeln!(
2353 out,
2354 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2355 );
2356 }
2357 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2358
2359 let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
2361 let _ = writeln!(out, " uintptr_t out_len = 0;");
2362 let _ = writeln!(out, " uintptr_t out_cap = 0;");
2363
2364 let mut method_args: Vec<String> = Vec::new();
2366 for (_, v) in &request_handle_vars {
2367 method_args.push(v.clone());
2368 }
2369 method_args.extend(string_arg_exprs.iter().cloned());
2370 let extra_args = if method_args.is_empty() {
2371 String::new()
2372 } else {
2373 format!(", {}", method_args.join(", "))
2374 };
2375
2376 let call_fn = format!("{prefix}_default_client_{function_name}");
2377 let _ = writeln!(
2378 out,
2379 " int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
2380 );
2381
2382 if expects_error {
2383 for (_, var_name) in &request_handle_vars {
2384 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2385 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2386 }
2387 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2388 let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
2389 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2392 let _ = writeln!(out, "}}");
2393 return;
2394 }
2395
2396 let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
2397
2398 let mut emitted_len_check = false;
2403 for assertion in &fixture.assertions {
2404 match assertion.assertion_type.as_str() {
2405 "not_error" => {
2406 }
2408 "not_empty" | "not_null" => {
2409 if !emitted_len_check {
2410 let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
2411 emitted_len_check = true;
2412 }
2413 }
2414 _ => {
2415 let _ = writeln!(
2419 out,
2420 " /* skipped: assertion '{}' not meaningful on raw byte buffer */",
2421 assertion.assertion_type
2422 );
2423 }
2424 }
2425 }
2426
2427 let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2428 for (_, var_name) in &request_handle_vars {
2429 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2430 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
2431 }
2432 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2433 let _ = writeln!(out, "}}");
2434}
2435
2436#[allow(clippy::too_many_arguments)]
2447fn render_chat_stream_test_function(
2448 out: &mut String,
2449 fixture: &Fixture,
2450 prefix: &str,
2451 result_var: &str,
2452 args: &[crate::config::ArgMapping],
2453 options_type_name: &str,
2454 expects_error: bool,
2455 api_key_var: Option<&str>,
2456) {
2457 let prefix_upper = prefix.to_uppercase();
2458
2459 let mut request_var: Option<String> = None;
2460 for arg in args {
2461 if arg.arg_type == "json_object" {
2462 let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2463 options_type_name.to_string()
2464 } else {
2465 "ChatCompletionRequest".to_string()
2466 };
2467 let request_type_snake = request_type_pascal.to_snake_case();
2468 let var_name = format!("{request_type_snake}_handle");
2469
2470 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2471 let json_val = if field.is_empty() || field == "input" {
2472 Some(&fixture.input)
2473 } else {
2474 fixture.input.get(field)
2475 };
2476
2477 if let Some(val) = json_val {
2478 if !val.is_null() {
2479 let normalized = super::transform_json_keys_for_language(val, "snake_case");
2480 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2481 let escaped = escape_c(&json_str);
2482 let _ = writeln!(
2483 out,
2484 " {prefix_upper}{request_type_pascal}* {var_name} = \
2485 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2486 );
2487 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
2488 request_var = Some(var_name);
2489 break;
2490 }
2491 }
2492 }
2493 }
2494
2495 let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
2496 let req_snake = request_var
2497 .as_ref()
2498 .and_then(|v| v.strip_suffix("_handle"))
2499 .unwrap_or("chat_completion_request")
2500 .to_string();
2501
2502 let fixture_id = &fixture.id;
2503 let has_mock = fixture.needs_mock_server();
2504 if has_mock && api_key_var.is_some() {
2505 let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
2511 let _ = writeln!(
2512 out,
2513 " {prefix_upper}DefaultClient* client = {prefix}_create_client(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
2514 );
2515 } else if has_mock {
2516 let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2517 let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2518 let _ = writeln!(out, " char base_url[1024];");
2519 let _ = writeln!(
2520 out,
2521 " snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2522 );
2523 let _ = writeln!(
2528 out,
2529 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2530 );
2531 } else {
2532 let _ = writeln!(
2533 out,
2534 " {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2535 );
2536 }
2537 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
2538
2539 let pascal_prefix = prefix.to_pascal_case();
2543 let _ = writeln!(
2544 out,
2545 " {prefix_upper}{pascal_prefix}DefaultClientChatStreamStreamHandle* stream_handle = \
2546 {prefix}_default_client_chat_stream_start(client, {req_handle});"
2547 );
2548
2549 if expects_error {
2550 let _ = writeln!(
2551 out,
2552 " assert(stream_handle == NULL && \"expected stream-start to fail\");"
2553 );
2554 if request_var.is_some() {
2555 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2556 }
2557 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2558 let _ = writeln!(out, "}}");
2559 return;
2560 }
2561
2562 let _ = writeln!(
2563 out,
2564 " assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2565 );
2566
2567 let _ = writeln!(out, " size_t chunks_count = 0;");
2568 let _ = writeln!(out, " char* stream_content = (char*)malloc(1);");
2569 let _ = writeln!(out, " assert(stream_content != NULL);");
2570 let _ = writeln!(out, " stream_content[0] = '\\0';");
2571 let _ = writeln!(out, " size_t stream_content_len = 0;");
2572 let _ = writeln!(out, " int stream_complete = 0;");
2573 let _ = writeln!(out, " int no_chunks_after_done = 1;");
2574 let _ = writeln!(out, " char* last_choices_json = NULL;");
2575 let _ = writeln!(out, " uint64_t total_tokens = 0;");
2576 let _ = writeln!(out);
2577
2578 let _ = writeln!(out, " while (1) {{");
2579 let _ = writeln!(
2580 out,
2581 " {prefix_upper}ChatCompletionChunk* {result_var} = \
2582 {prefix}_default_client_chat_stream_next(stream_handle);"
2583 );
2584 let _ = writeln!(out, " if ({result_var} == NULL) {{");
2585 let _ = writeln!(
2586 out,
2587 " if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2588 );
2589 let _ = writeln!(out, " break;");
2590 let _ = writeln!(out, " }}");
2591 let _ = writeln!(out, " chunks_count++;");
2592 let _ = writeln!(
2593 out,
2594 " char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2595 );
2596 let _ = writeln!(out, " if (choices_json != NULL) {{");
2597 let _ = writeln!(
2598 out,
2599 " const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2600 );
2601 let _ = writeln!(out, " if (d != NULL) {{");
2602 let _ = writeln!(out, " d += 10;");
2603 let _ = writeln!(out, " while (*d == ' ' || *d == '\\t') d++;");
2604 let _ = writeln!(out, " if (*d == '\"') {{");
2605 let _ = writeln!(out, " d++;");
2606 let _ = writeln!(out, " const char* e = d;");
2607 let _ = writeln!(out, " while (*e && *e != '\"') {{");
2608 let _ = writeln!(
2609 out,
2610 " if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2611 );
2612 let _ = writeln!(out, " }}");
2613 let _ = writeln!(out, " size_t add = (size_t)(e - d);");
2614 let _ = writeln!(out, " if (add > 0) {{");
2615 let _ = writeln!(
2616 out,
2617 " char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2618 );
2619 let _ = writeln!(out, " if (nc != NULL) {{");
2620 let _ = writeln!(out, " stream_content = nc;");
2621 let _ = writeln!(
2622 out,
2623 " memcpy(stream_content + stream_content_len, d, add);"
2624 );
2625 let _ = writeln!(out, " stream_content_len += add;");
2626 let _ = writeln!(
2627 out,
2628 " stream_content[stream_content_len] = '\\0';"
2629 );
2630 let _ = writeln!(out, " }}");
2631 let _ = writeln!(out, " }}");
2632 let _ = writeln!(out, " }}");
2633 let _ = writeln!(out, " }}");
2634 let _ = writeln!(
2635 out,
2636 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2637 );
2638 let _ = writeln!(out, " last_choices_json = choices_json;");
2639 let _ = writeln!(out, " }}");
2640 let _ = writeln!(
2641 out,
2642 " {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2643 );
2644 let _ = writeln!(out, " if (usage_handle != NULL) {{");
2645 let _ = writeln!(
2646 out,
2647 " total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2648 );
2649 let _ = writeln!(out, " {prefix}_usage_free(usage_handle);");
2650 let _ = writeln!(out, " }}");
2651 let _ = writeln!(out, " {prefix}_chat_completion_chunk_free({result_var});");
2652 let _ = writeln!(out, " }}");
2653 let _ = writeln!(out, " {prefix}_default_client_chat_stream_free(stream_handle);");
2654 let _ = writeln!(out);
2655
2656 let _ = writeln!(out, " char* finish_reason = NULL;");
2657 let _ = writeln!(out, " char* tool_calls_json = NULL;");
2658 let _ = writeln!(out, " char* tool_calls_0_function_name = NULL;");
2659 let _ = writeln!(out, " if (last_choices_json != NULL) {{");
2660 let _ = writeln!(
2661 out,
2662 " finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2663 );
2664 let _ = writeln!(
2665 out,
2666 " const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2667 );
2668 let _ = writeln!(out, " if (tc != NULL) {{");
2669 let _ = writeln!(out, " tc += 13;");
2670 let _ = writeln!(out, " while (*tc == ' ' || *tc == '\\t') tc++;");
2671 let _ = writeln!(out, " if (*tc == '[') {{");
2672 let _ = writeln!(out, " int depth = 0;");
2673 let _ = writeln!(out, " const char* end = tc;");
2674 let _ = writeln!(out, " int in_str = 0;");
2675 let _ = writeln!(out, " for (; *end; end++) {{");
2676 let _ = writeln!(
2677 out,
2678 " if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2679 );
2680 let _ = writeln!(
2681 out,
2682 " if (*end == '\"') {{ in_str = !in_str; continue; }}"
2683 );
2684 let _ = writeln!(out, " if (in_str) continue;");
2685 let _ = writeln!(out, " if (*end == '[' || *end == '{{') depth++;");
2686 let _ = writeln!(
2687 out,
2688 " else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2689 );
2690 let _ = writeln!(out, " }}");
2691 let _ = writeln!(out, " size_t tlen = (size_t)(end - tc);");
2692 let _ = writeln!(out, " tool_calls_json = (char*)malloc(tlen + 1);");
2693 let _ = writeln!(out, " if (tool_calls_json != NULL) {{");
2694 let _ = writeln!(out, " memcpy(tool_calls_json, tc, tlen);");
2695 let _ = writeln!(out, " tool_calls_json[tlen] = '\\0';");
2696 let _ = writeln!(
2697 out,
2698 " const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2699 );
2700 let _ = writeln!(out, " if (fn != NULL) {{");
2701 let _ = writeln!(
2702 out,
2703 " const char* np = strstr(fn, \"\\\"name\\\":\");"
2704 );
2705 let _ = writeln!(out, " if (np != NULL) {{");
2706 let _ = writeln!(out, " np += 7;");
2707 let _ = writeln!(
2708 out,
2709 " while (*np == ' ' || *np == '\\t') np++;"
2710 );
2711 let _ = writeln!(out, " if (*np == '\"') {{");
2712 let _ = writeln!(out, " np++;");
2713 let _ = writeln!(out, " const char* ne = np;");
2714 let _ = writeln!(
2715 out,
2716 " while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2717 );
2718 let _ = writeln!(out, " size_t nlen = (size_t)(ne - np);");
2719 let _ = writeln!(
2720 out,
2721 " tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2722 );
2723 let _ = writeln!(
2724 out,
2725 " if (tool_calls_0_function_name != NULL) {{"
2726 );
2727 let _ = writeln!(
2728 out,
2729 " memcpy(tool_calls_0_function_name, np, nlen);"
2730 );
2731 let _ = writeln!(
2732 out,
2733 " tool_calls_0_function_name[nlen] = '\\0';"
2734 );
2735 let _ = writeln!(out, " }}");
2736 let _ = writeln!(out, " }}");
2737 let _ = writeln!(out, " }}");
2738 let _ = writeln!(out, " }}");
2739 let _ = writeln!(out, " }}");
2740 let _ = writeln!(out, " }}");
2741 let _ = writeln!(out, " }}");
2742 let _ = writeln!(out, " }}");
2743 let _ = writeln!(out);
2744
2745 for assertion in &fixture.assertions {
2746 emit_chat_stream_assertion(out, assertion);
2747 }
2748
2749 let _ = writeln!(out, " free(stream_content);");
2750 let _ = writeln!(
2751 out,
2752 " if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2753 );
2754 let _ = writeln!(out, " if (finish_reason != NULL) free(finish_reason);");
2755 let _ = writeln!(out, " if (tool_calls_json != NULL) free(tool_calls_json);");
2756 let _ = writeln!(
2757 out,
2758 " if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2759 );
2760 if request_var.is_some() {
2761 let _ = writeln!(out, " {prefix}_{req_snake}_free({req_handle});");
2762 }
2763 let _ = writeln!(out, " {prefix}_default_client_free(client);");
2764 let _ = writeln!(
2765 out,
2766 " /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2767 (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2768 );
2769 let _ = writeln!(out, "}}");
2770}
2771
2772fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2776 let field = assertion.field.as_deref().unwrap_or("");
2777
2778 enum Kind {
2779 IntCount,
2780 Bool,
2781 Str,
2782 IntTokens,
2783 Unsupported,
2784 }
2785
2786 let (expr, kind) = match field {
2787 "chunks" => ("chunks_count", Kind::IntCount),
2788 "stream_content" => ("stream_content", Kind::Str),
2789 "stream_complete" => ("stream_complete", Kind::Bool),
2790 "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2791 "finish_reason" => ("finish_reason", Kind::Str),
2792 "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2801 "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2802 _ => ("", Kind::Unsupported),
2803 };
2804
2805 let atype = assertion.assertion_type.as_str();
2806 if atype == "not_error" || atype == "error" {
2807 return;
2808 }
2809
2810 if matches!(kind, Kind::Unsupported) {
2811 let _ = writeln!(
2812 out,
2813 " /* skipped: streaming assertion on unsupported field '{field}' */"
2814 );
2815 return;
2816 }
2817
2818 match (atype, &kind) {
2819 ("count_min", Kind::IntCount) => {
2820 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2821 let _ = writeln!(out, " assert({expr} >= {n} && \"expected at least {n} chunks\");");
2822 }
2823 }
2824 ("equals", Kind::Str) => {
2825 if let Some(val) = &assertion.value {
2826 let c_val = json_to_c(val);
2827 let _ = writeln!(
2828 out,
2829 " assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2830 );
2831 }
2832 }
2833 ("contains", Kind::Str) => {
2834 if let Some(val) = &assertion.value {
2835 let c_val = json_to_c(val);
2836 let _ = writeln!(
2837 out,
2838 " assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2839 );
2840 }
2841 }
2842 ("not_empty", Kind::Str) => {
2843 let _ = writeln!(
2844 out,
2845 " assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2846 );
2847 }
2848 ("is_true", Kind::Bool) => {
2849 let _ = writeln!(out, " assert({expr} && \"expected {field} to be true\");");
2850 }
2851 ("is_false", Kind::Bool) => {
2852 let _ = writeln!(out, " assert(!{expr} && \"expected {field} to be false\");");
2853 }
2854 ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2855 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2856 let _ = writeln!(out, " assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2857 }
2858 }
2859 ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2860 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2861 let _ = writeln!(out, " assert({expr} == {n} && \"equals assertion failed\");");
2862 }
2863 }
2864 _ => {
2865 let _ = writeln!(
2866 out,
2867 " /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2868 );
2869 }
2870 }
2871}
2872
2873#[allow(clippy::too_many_arguments)]
2887fn emit_nested_accessor(
2888 out: &mut String,
2889 prefix: &str,
2890 resolved: &str,
2891 local_var: &str,
2892 result_var: &str,
2893 fields_c_types: &HashMap<String, String>,
2894 fields_enum: &HashSet<String>,
2895 intermediate_handles: &mut Vec<(String, String)>,
2896 result_type_name: &str,
2897 raw_field: &str,
2898) -> Option<String> {
2899 let segments: Vec<&str> = resolved.split('.').collect();
2900 let prefix_upper = prefix.to_uppercase();
2901
2902 let mut current_snake_type = result_type_name.to_snake_case();
2904 let mut current_handle = result_var.to_string();
2905 let mut json_extract_mode = false;
2908
2909 for (i, segment) in segments.iter().enumerate() {
2910 let is_leaf = i + 1 == segments.len();
2911
2912 if json_extract_mode {
2916 let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2921 Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2922 None => (segment, None),
2923 };
2924 let seg_snake = bare_segment.to_snake_case();
2925 if is_leaf {
2926 let _ = writeln!(
2927 out,
2928 " char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2929 );
2930 return None; }
2932 let json_var = format!("{seg_snake}_json");
2937 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2938 let _ = writeln!(
2939 out,
2940 " char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2941 );
2942 intermediate_handles.push((json_var.clone(), "free".to_string()));
2943 }
2944 if let Some(key) = bracket_key {
2948 if let Ok(idx) = key.parse::<usize>() {
2949 let elem_var = format!("{seg_snake}_{idx}_json");
2950 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2951 let _ = writeln!(
2952 out,
2953 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2954 );
2955 intermediate_handles.push((elem_var.clone(), "free".to_string()));
2956 }
2957 current_handle = elem_var;
2958 continue;
2959 }
2960 }
2961 current_handle = json_var;
2962 continue;
2963 }
2964
2965 if let Some(bracket_pos) = segment.find('[') {
2967 let field_name = &segment[..bracket_pos];
2968 let key = segment[bracket_pos + 1..].trim_end_matches(']');
2969 let field_snake = field_name.to_snake_case();
2970 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2971
2972 let json_var = format!("{field_snake}_json");
2974 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2975 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
2976 let _ = writeln!(out, " assert({json_var} != NULL);");
2977 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2979 }
2980
2981 if key.is_empty() {
2987 if !is_leaf {
2988 current_handle = json_var;
2989 json_extract_mode = true;
2990 continue;
2991 }
2992 return None;
2993 }
2994 if let Ok(idx) = key.parse::<usize>() {
2995 let elem_var = format!("{field_snake}_{idx}_json");
2996 if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2997 let _ = writeln!(
2998 out,
2999 " char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
3000 );
3001 intermediate_handles.push((elem_var.clone(), "free".to_string()));
3002 }
3003 if !is_leaf {
3004 current_handle = elem_var;
3005 json_extract_mode = true;
3006 continue;
3007 }
3008 return None;
3010 }
3011
3012 let _ = writeln!(
3014 out,
3015 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
3016 );
3017 return None; }
3019
3020 let seg_snake = segment.to_snake_case();
3021 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
3022
3023 if is_skipped_c_field(fields_c_types, ¤t_snake_type, &seg_snake) {
3025 return Some("__skip__".to_string()); }
3027
3028 if is_leaf {
3029 let lookup_key = format!("{current_snake_type}.{seg_snake}");
3032 if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
3033 let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({current_handle});");
3034 return Some(t.clone());
3035 }
3036 if let Some(opaque_type) = fields_c_types.get(&lookup_key).filter(|t| {
3043 *t != "char*"
3044 && *t != "skip"
3045 && !is_primitive_c_type(t)
3046 && t.chars().next().is_some_and(|c| c.is_uppercase())
3047 }) {
3048 let handle_var = format!("{seg_snake}_handle");
3049 let opaque_snake = opaque_type.to_snake_case();
3050 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
3051 let _ = writeln!(
3052 out,
3053 " {prefix_upper}{opaque_type}* {handle_var} = {accessor_fn}({current_handle});"
3054 );
3055 intermediate_handles.push((handle_var.clone(), opaque_snake.clone()));
3056 }
3057 if local_var != handle_var {
3060 let _ = writeln!(out, " {prefix_upper}{opaque_type}* {local_var} = {handle_var};");
3061 }
3062 return Some(opaque_snake); }
3064 if try_emit_enum_accessor(
3066 out,
3067 prefix,
3068 &prefix_upper,
3069 raw_field,
3070 &seg_snake,
3071 ¤t_snake_type,
3072 &accessor_fn,
3073 ¤t_handle,
3074 local_var,
3075 fields_c_types,
3076 fields_enum,
3077 intermediate_handles,
3078 ) {
3079 return None;
3080 }
3081 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
3082 } else {
3083 let lookup_key = format!("{current_snake_type}.{seg_snake}");
3085 let return_type_pascal = match fields_c_types.get(&lookup_key) {
3086 Some(t) => t.clone(),
3087 None => {
3088 segment.to_pascal_case()
3090 }
3091 };
3092
3093 if return_type_pascal == "char*" {
3096 let json_var = format!("{seg_snake}_json");
3097 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
3098 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
3099 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
3100 }
3101 if i + 2 == segments.len() && segments[i + 1] == "length" {
3103 let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
3104 return Some("int".to_string());
3105 }
3106 current_snake_type = seg_snake.clone();
3107 current_handle = json_var;
3108 continue;
3109 }
3110
3111 let return_snake = return_type_pascal.to_snake_case();
3112 let handle_var = format!("{seg_snake}_handle");
3113
3114 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
3117 let _ = writeln!(
3118 out,
3119 " {prefix_upper}{return_type_pascal}* {handle_var} = \
3120 {accessor_fn}({current_handle});"
3121 );
3122 let _ = writeln!(out, " assert({handle_var} != NULL);");
3123 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
3124 }
3125
3126 current_snake_type = return_snake;
3127 current_handle = handle_var;
3128 }
3129 }
3130 None
3131}
3132
3133fn build_args_string_c(
3137 input: &serde_json::Value,
3138 args: &[crate::config::ArgMapping],
3139 has_options_handle: bool,
3140) -> String {
3141 if args.is_empty() {
3142 return json_to_c(input);
3143 }
3144
3145 let parts: Vec<String> = args
3146 .iter()
3147 .filter_map(|arg| {
3148 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
3149 let val = input.get(field);
3150 match val {
3151 None if arg.optional => Some("NULL".to_string()),
3153 None => None,
3155 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
3157 Some(v) => {
3158 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
3161 Some("options_handle".to_string())
3162 } else {
3163 Some(json_to_c(v))
3164 }
3165 }
3166 }
3167 })
3168 .collect();
3169
3170 parts.join(", ")
3171}
3172
3173#[allow(clippy::too_many_arguments)]
3174fn render_assertion(
3175 out: &mut String,
3176 assertion: &Assertion,
3177 result_var: &str,
3178 ffi_prefix: &str,
3179 _field_resolver: &FieldResolver,
3180 accessed_fields: &[(String, String, bool)],
3181 primitive_locals: &HashMap<String, String>,
3182 opaque_handle_locals: &HashMap<String, String>,
3183) {
3184 if let Some(f) = &assertion.field {
3186 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
3187 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
3188 return;
3189 }
3190 }
3191
3192 let field_expr = match &assertion.field {
3193 Some(f) if !f.is_empty() => {
3194 accessed_fields
3196 .iter()
3197 .find(|(k, _, _)| k == f)
3198 .map(|(_, local, _)| local.clone())
3199 .unwrap_or_else(|| result_var.to_string())
3200 }
3201 _ => result_var.to_string(),
3202 };
3203
3204 if primitive_locals.get(&field_expr).is_some_and(|t| t == "__skip__") {
3207 let _ = writeln!(out, " // skipped: field '{field_expr}' not available in C FFI");
3208 return;
3209 }
3210
3211 let field_is_primitive = primitive_locals.contains_key(&field_expr);
3212 let field_primitive_type = primitive_locals.get(&field_expr).cloned();
3213 let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
3218 let field_is_map_access = if let Some(f) = &assertion.field {
3222 accessed_fields.iter().any(|(k, _, m)| k == f && *m)
3223 } else {
3224 false
3225 };
3226
3227 let assertion_field_is_optional = assertion
3231 .field
3232 .as_deref()
3233 .map(|f| {
3234 if f.is_empty() {
3235 return false;
3236 }
3237 if _field_resolver.is_optional(f) {
3238 return true;
3239 }
3240 let resolved = _field_resolver.resolve(f);
3242 _field_resolver.is_optional(resolved)
3243 })
3244 .unwrap_or(false);
3245
3246 match assertion.assertion_type.as_str() {
3247 "equals" => {
3248 if let Some(expected) = &assertion.value {
3249 let c_val = json_to_c(expected);
3250 if field_is_primitive {
3251 let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
3252 match expected.as_bool() {
3253 Some(true) => "1".to_string(),
3254 Some(false) => "0".to_string(),
3255 None => c_val,
3256 }
3257 } else {
3258 c_val
3259 };
3260 let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
3263 if assertion_field_is_optional && is_numeric {
3264 let _ = writeln!(
3265 out,
3266 " assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
3267 );
3268 } else {
3269 let _ = writeln!(
3270 out,
3271 " assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
3272 );
3273 }
3274 } else if expected.is_string() {
3275 let _ = writeln!(
3276 out,
3277 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3278 );
3279 } else if field_is_map_access && expected.is_boolean() {
3280 let lit = match expected.as_bool() {
3281 Some(true) => "\"true\"",
3282 _ => "\"false\"",
3283 };
3284 let _ = writeln!(
3285 out,
3286 " assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
3287 );
3288 } else if field_is_map_access && expected.is_number() {
3289 if expected.is_f64() {
3290 let _ = writeln!(
3291 out,
3292 " assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
3293 );
3294 } else {
3295 let _ = writeln!(
3296 out,
3297 " assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
3298 );
3299 }
3300 } else {
3301 let _ = writeln!(
3302 out,
3303 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3304 );
3305 }
3306 }
3307 }
3308 "contains" => {
3309 if let Some(expected) = &assertion.value {
3310 let c_val = json_to_c(expected);
3311 let _ = writeln!(
3312 out,
3313 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3314 );
3315 }
3316 }
3317 "contains_all" => {
3318 if let Some(values) = &assertion.values {
3319 for val in values {
3320 let c_val = json_to_c(val);
3321 let _ = writeln!(
3322 out,
3323 " assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3324 );
3325 }
3326 }
3327 }
3328 "not_contains" => {
3329 if let Some(expected) = &assertion.value {
3330 let c_val = json_to_c(expected);
3331 let _ = writeln!(
3332 out,
3333 " assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3334 );
3335 }
3336 }
3337 "not_empty" => {
3338 if field_is_opaque_handle {
3339 let _ = writeln!(out, " assert({field_expr} != NULL && \"expected non-null handle\");");
3343 } else {
3344 let _ = writeln!(
3345 out,
3346 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
3347 );
3348 }
3349 }
3350 "is_empty" => {
3351 if field_is_opaque_handle {
3352 let _ = writeln!(out, " assert({field_expr} == NULL && \"expected null handle\");");
3353 } else if assertion_field_is_optional || !field_is_primitive {
3354 let _ = writeln!(
3356 out,
3357 " assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
3358 );
3359 } else {
3360 let _ = writeln!(
3361 out,
3362 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
3363 );
3364 }
3365 }
3366 "contains_any" => {
3367 if let Some(values) = &assertion.values {
3368 let _ = writeln!(out, " {{");
3369 let _ = writeln!(out, " int found = 0;");
3370 for val in values {
3371 let c_val = json_to_c(val);
3372 let _ = writeln!(
3373 out,
3374 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
3375 );
3376 }
3377 let _ = writeln!(
3378 out,
3379 " assert(found && \"expected to contain at least one of the specified values\");"
3380 );
3381 let _ = writeln!(out, " }}");
3382 }
3383 }
3384 "greater_than" => {
3385 if let Some(val) = &assertion.value {
3386 let c_val = json_to_c(val);
3387 if field_is_map_access && val.is_number() && !field_is_primitive {
3388 let _ = writeln!(
3389 out,
3390 " assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
3391 );
3392 } else {
3393 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
3394 }
3395 }
3396 }
3397 "less_than" => {
3398 if let Some(val) = &assertion.value {
3399 let c_val = json_to_c(val);
3400 if field_is_map_access && val.is_number() && !field_is_primitive {
3401 let _ = writeln!(
3402 out,
3403 " assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
3404 );
3405 } else {
3406 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
3407 }
3408 }
3409 }
3410 "greater_than_or_equal" => {
3411 if let Some(val) = &assertion.value {
3412 let c_val = json_to_c(val);
3413 if field_is_map_access && val.is_number() && !field_is_primitive {
3414 let _ = writeln!(
3415 out,
3416 " assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
3417 );
3418 } else {
3419 let _ = writeln!(
3420 out,
3421 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
3422 );
3423 }
3424 }
3425 }
3426 "less_than_or_equal" => {
3427 if let Some(val) = &assertion.value {
3428 let c_val = json_to_c(val);
3429 if field_is_map_access && val.is_number() && !field_is_primitive {
3430 let _ = writeln!(
3431 out,
3432 " assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
3433 );
3434 } else {
3435 let _ = writeln!(
3436 out,
3437 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
3438 );
3439 }
3440 }
3441 }
3442 "starts_with" => {
3443 if let Some(expected) = &assertion.value {
3444 let c_val = json_to_c(expected);
3445 let _ = writeln!(
3446 out,
3447 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
3448 );
3449 }
3450 }
3451 "ends_with" => {
3452 if let Some(expected) = &assertion.value {
3453 let c_val = json_to_c(expected);
3454 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
3455 let _ = writeln!(
3456 out,
3457 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
3458 );
3459 }
3460 }
3461 "min_length" => {
3462 if let Some(val) = &assertion.value {
3463 if let Some(n) = val.as_u64() {
3464 let _ = writeln!(
3465 out,
3466 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
3467 );
3468 }
3469 }
3470 }
3471 "max_length" => {
3472 if let Some(val) = &assertion.value {
3473 if let Some(n) = val.as_u64() {
3474 let _ = writeln!(
3475 out,
3476 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
3477 );
3478 }
3479 }
3480 }
3481 "count_min" => {
3482 if let Some(val) = &assertion.value {
3483 if let Some(n) = val.as_u64() {
3484 let _ = writeln!(out, " {{");
3485 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
3486 let _ = writeln!(
3487 out,
3488 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3489 );
3490 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3491 let _ = writeln!(
3492 out,
3493 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
3494 );
3495 let _ = writeln!(out, " }}");
3496 }
3497 }
3498 }
3499 "count_equals" => {
3500 if let Some(val) = &assertion.value {
3501 if let Some(n) = val.as_u64() {
3502 let _ = writeln!(out, " {{");
3503 let _ = writeln!(out, " /* count_equals: count elements in array */");
3504 let _ = writeln!(
3505 out,
3506 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3507 );
3508 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
3509 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
3510 let _ = writeln!(out, " }}");
3511 }
3512 }
3513 }
3514 "is_true" => {
3515 let _ = writeln!(out, " assert({field_expr});");
3516 }
3517 "is_false" => {
3518 let _ = writeln!(out, " assert(!{field_expr});");
3519 }
3520 "method_result" => {
3521 if let Some(method_name) = &assertion.method {
3522 render_method_result_assertion(
3523 out,
3524 result_var,
3525 ffi_prefix,
3526 method_name,
3527 assertion.args.as_ref(),
3528 assertion.return_type.as_deref(),
3529 assertion.check.as_deref().unwrap_or("is_true"),
3530 assertion.value.as_ref(),
3531 );
3532 } else {
3533 panic!("C e2e generator: method_result assertion missing 'method' field");
3534 }
3535 }
3536 "matches_regex" => {
3537 if let Some(expected) = &assertion.value {
3538 let c_val = json_to_c(expected);
3539 let _ = writeln!(out, " {{");
3540 let _ = writeln!(out, " regex_t _re;");
3541 let _ = writeln!(
3542 out,
3543 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3544 );
3545 let _ = writeln!(
3546 out,
3547 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3548 );
3549 let _ = writeln!(out, " regfree(&_re);");
3550 let _ = writeln!(out, " }}");
3551 }
3552 }
3553 "not_error" => {
3554 }
3556 "error" => {
3557 }
3559 other => {
3560 panic!("C e2e generator: unsupported assertion type: {other}");
3561 }
3562 }
3563}
3564
3565#[allow(clippy::too_many_arguments)]
3574fn render_method_result_assertion(
3575 out: &mut String,
3576 result_var: &str,
3577 ffi_prefix: &str,
3578 method_name: &str,
3579 args: Option<&serde_json::Value>,
3580 return_type: Option<&str>,
3581 check: &str,
3582 value: Option<&serde_json::Value>,
3583) {
3584 let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3585
3586 if return_type == Some("string") {
3587 let _ = writeln!(out, " {{");
3589 let _ = writeln!(out, " char* _method_result = {call_expr};");
3590 if check == "is_error" {
3591 let _ = writeln!(
3592 out,
3593 " assert(_method_result == NULL && \"expected method to return error\");"
3594 );
3595 let _ = writeln!(out, " }}");
3596 return;
3597 }
3598 let _ = writeln!(
3599 out,
3600 " assert(_method_result != NULL && \"method_result returned NULL\");"
3601 );
3602 match check {
3603 "contains" => {
3604 if let Some(val) = value {
3605 let c_val = json_to_c(val);
3606 let _ = writeln!(
3607 out,
3608 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3609 );
3610 }
3611 }
3612 "equals" => {
3613 if let Some(val) = value {
3614 let c_val = json_to_c(val);
3615 let _ = writeln!(
3616 out,
3617 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3618 );
3619 }
3620 }
3621 "is_true" => {
3622 let _ = writeln!(
3623 out,
3624 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3625 );
3626 }
3627 "count_min" => {
3628 if let Some(val) = value {
3629 let n = val.as_u64().unwrap_or(0);
3630 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
3631 let _ = writeln!(
3632 out,
3633 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3634 );
3635 }
3636 }
3637 other_check => {
3638 panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3639 }
3640 }
3641 let _ = writeln!(out, " free(_method_result);");
3642 let _ = writeln!(out, " }}");
3643 return;
3644 }
3645
3646 match check {
3648 "equals" => {
3649 if let Some(val) = value {
3650 let c_val = json_to_c(val);
3651 let _ = writeln!(
3652 out,
3653 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3654 );
3655 }
3656 }
3657 "is_true" => {
3658 let _ = writeln!(
3659 out,
3660 " assert({call_expr} && \"method_result is_true assertion failed\");"
3661 );
3662 }
3663 "is_false" => {
3664 let _ = writeln!(
3665 out,
3666 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
3667 );
3668 }
3669 "greater_than_or_equal" => {
3670 if let Some(val) = value {
3671 let n = val.as_u64().unwrap_or(0);
3672 let _ = writeln!(
3673 out,
3674 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3675 );
3676 }
3677 }
3678 "count_min" => {
3679 if let Some(val) = value {
3680 let n = val.as_u64().unwrap_or(0);
3681 let _ = writeln!(
3682 out,
3683 " assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3684 );
3685 }
3686 }
3687 other_check => {
3688 panic!("C e2e generator: unsupported method_result check type: {other_check}");
3689 }
3690 }
3691}
3692
3693fn build_c_method_call(
3700 result_var: &str,
3701 ffi_prefix: &str,
3702 method_name: &str,
3703 args: Option<&serde_json::Value>,
3704) -> String {
3705 let extra_args = if let Some(args_val) = args {
3706 args_val
3707 .as_object()
3708 .map(|obj| {
3709 obj.values()
3710 .map(|v| match v {
3711 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3712 serde_json::Value::Bool(true) => "1".to_string(),
3713 serde_json::Value::Bool(false) => "0".to_string(),
3714 serde_json::Value::Number(n) => n.to_string(),
3715 serde_json::Value::Null => "NULL".to_string(),
3716 other => format!("\"{}\"", escape_c(&other.to_string())),
3717 })
3718 .collect::<Vec<_>>()
3719 .join(", ")
3720 })
3721 .unwrap_or_default()
3722 } else {
3723 String::new()
3724 };
3725
3726 if extra_args.is_empty() {
3727 format!("{ffi_prefix}_{method_name}({result_var})")
3728 } else {
3729 format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3730 }
3731}
3732
3733fn json_to_c(value: &serde_json::Value) -> String {
3735 match value {
3736 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3737 serde_json::Value::Bool(true) => "1".to_string(),
3738 serde_json::Value::Bool(false) => "0".to_string(),
3739 serde_json::Value::Number(n) => n.to_string(),
3740 serde_json::Value::Null => "NULL".to_string(),
3741 other => format!("\"{}\"", escape_c(&other.to_string())),
3742 }
3743}
3744
3745fn render_visitor_test_file(fixtures: &[&Fixture], header: &str, prefix: &str) -> String {
3762 use crate::fixture::CallbackAction;
3763
3764 let mut out = String::new();
3765 out.push_str(&hash::header(CommentStyle::Block));
3766 let _ = writeln!(out, "/* E2e tests for category: visitor */");
3767 let _ = writeln!(out);
3768 let _ = writeln!(out, "#include <assert.h>");
3769 let _ = writeln!(out, "#include <stdint.h>");
3770 let _ = writeln!(out, "#include <string.h>");
3771 let _ = writeln!(out, "#include <stdio.h>");
3772 let _ = writeln!(out, "#include <stdlib.h>");
3773 let _ = writeln!(out, "#include \"{header}\"");
3774 let _ = writeln!(out, "#include \"test_runner.h\"");
3775 let _ = writeln!(out);
3776
3777 let prefix_upper = prefix.to_uppercase();
3778
3779 for (i, fixture) in fixtures.iter().enumerate() {
3780 let fn_name = sanitize_ident(&fixture.id);
3781 let description = &fixture.description;
3782
3783 let visitor_spec = match &fixture.visitor {
3784 Some(v) => v,
3785 None => continue,
3786 };
3787
3788 let html = fixture.input.get("html").and_then(|v| v.as_str()).unwrap_or("");
3789 let html_escaped = escape_c(html);
3790
3791 let options_json = match fixture.input.get("options") {
3792 Some(opts) => serde_json::to_string(opts).unwrap_or_else(|_| "{}".to_string()),
3793 None => "{}".to_string(),
3794 };
3795 let options_escaped = escape_c(&options_json);
3796
3797 let mut sorted_callbacks: Vec<(&String, &CallbackAction)> = visitor_spec.callbacks.iter().collect();
3800 sorted_callbacks.sort_by(|a, b| a.0.cmp(b.0));
3801
3802 for (method, action) in &sorted_callbacks {
3803 let cb_name = format!("c_visitor_{fn_name}_{method}");
3804 let params = c_visitor_callback_params(method);
3805 let body = c_visitor_callback_body(method, action);
3806 let _ = writeln!(out, "static int32_t {cb_name}({params}) {{");
3807 out.push_str(&body);
3808 let _ = writeln!(out, "}}");
3809 let _ = writeln!(out);
3810 }
3811
3812 let _ = writeln!(out, "void test_{fn_name}(void) {{");
3814 let _ = writeln!(out, " /* {description} */");
3815 let _ = writeln!(out);
3816
3817 let _ = writeln!(out, " {prefix_upper}HtmVisitorCallbacks _callbacks;");
3819 let _ = writeln!(out, " memset(&_callbacks, 0, sizeof(_callbacks));");
3820 for (method, _) in &sorted_callbacks {
3821 let cb_name = format!("c_visitor_{fn_name}_{method}");
3822 let _ = writeln!(out, " _callbacks.{method} = {cb_name};");
3823 }
3824 let _ = writeln!(out);
3825
3826 let _ = writeln!(
3828 out,
3829 " {prefix_upper}HtmVisitor* _visitor = {prefix}_visitor_create(&_callbacks);"
3830 );
3831 let _ = writeln!(out, " assert(_visitor != NULL && \"htm_visitor_create failed\");");
3832 let _ = writeln!(out);
3833
3834 let _ = writeln!(
3836 out,
3837 " {prefix_upper}ConversionOptions* _options = {prefix}_conversion_options_from_json(\"{options_escaped}\");"
3838 );
3839 let _ = writeln!(
3840 out,
3841 " assert(_options != NULL && \"htm_conversion_options_from_json failed\");"
3842 );
3843 let _ = writeln!(out);
3844
3845 let _ = writeln!(out, " {prefix}_options_set_visitor_handle(_options, _visitor);");
3847 let _ = writeln!(out);
3848
3849 let _ = writeln!(
3851 out,
3852 " {prefix_upper}ConversionResult* _result = {prefix}_convert(\"{html_escaped}\", _options);"
3853 );
3854 let _ = writeln!(out, " assert(_result != NULL && \"htm_convert failed\");");
3855 let _ = writeln!(out);
3856
3857 let _ = writeln!(out, " char* _json = {prefix}_conversion_result_to_json(_result);");
3859 let _ = writeln!(out, " assert(_json != NULL && \"result to_json failed\");");
3860 let _ = writeln!(out, " char* _content = alef_json_get_string(_json, \"content\");");
3861 let _ = writeln!(out);
3862
3863 for assertion in &fixture.assertions {
3865 match assertion.assertion_type.as_str() {
3866 "contains" => {
3867 if let Some(expected) = &assertion.value {
3868 let c_val = json_to_c(expected);
3869 let _ = writeln!(
3870 out,
3871 " assert(_content != NULL && strstr(_content, {c_val}) != NULL && \"expected to contain substring\");"
3872 );
3873 }
3874 }
3875 "not_contains" => {
3876 if let Some(expected) = &assertion.value {
3877 let c_val = json_to_c(expected);
3878 let _ = writeln!(
3879 out,
3880 " assert((_content == NULL || strstr(_content, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3881 );
3882 }
3883 }
3884 other => {
3885 let _ = writeln!(
3886 out,
3887 " /* assertion type '{other}' not supported in C visitor tests */"
3888 );
3889 }
3890 }
3891 }
3892
3893 let _ = writeln!(out);
3894
3895 let _ = writeln!(out, " free(_content);");
3897 let _ = writeln!(out, " {prefix}_free_string(_json);");
3898 let _ = writeln!(out, " {prefix}_conversion_result_free(_result);");
3899 let _ = writeln!(out, " {prefix}_conversion_options_free(_options);");
3900 let _ = writeln!(out, " {prefix}_visitor_free(_visitor);");
3901 let _ = writeln!(out, "}}");
3902
3903 if i + 1 < fixtures.len() {
3904 let _ = writeln!(out);
3905 }
3906 }
3907
3908 out
3909}
3910
3911fn c_visitor_callback_params(method: &str) -> &'static str {
3918 match method {
3919 "visit_text" => {
3920 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _text, char** out_custom, size_t* out_len"
3921 }
3922 "visit_element_start" => "const HTMHtmNodeContext* _ctx, void* _user_data, char** out_custom, size_t* out_len",
3923 "visit_element_end" => {
3924 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _output, char** out_custom, size_t* out_len"
3925 }
3926 "visit_link" => {
3927 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _href, const char* _text, const char* _title, char** out_custom, size_t* out_len"
3928 }
3929 "visit_image" => {
3930 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _src, const char* _alt, const char* _title, char** out_custom, size_t* out_len"
3931 }
3932 "visit_heading" => {
3933 "const HTMHtmNodeContext* _ctx, void* _user_data, uint32_t _level, const char* _text, const char* _id, char** out_custom, size_t* out_len"
3934 }
3935 "visit_code_block" => {
3936 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _lang, const char* _code, char** out_custom, size_t* out_len"
3937 }
3938 "visit_code_inline" => {
3939 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _code, char** out_custom, size_t* out_len"
3940 }
3941 "visit_list_item" => {
3942 "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _ordered, const char* _marker, const char* _text, char** out_custom, size_t* out_len"
3943 }
3944 "visit_list_start" => {
3945 "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _ordered, char** out_custom, size_t* out_len"
3946 }
3947 "visit_list_end" => {
3948 "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _ordered, const char* _output, char** out_custom, size_t* out_len"
3949 }
3950 "visit_table_start" => "const HTMHtmNodeContext* _ctx, void* _user_data, char** out_custom, size_t* out_len",
3951 "visit_table_row" => {
3952 "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"
3953 }
3954 "visit_table_end" => {
3955 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _output, char** out_custom, size_t* out_len"
3956 }
3957 "visit_blockquote" => {
3958 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _content, size_t _depth, char** out_custom, size_t* out_len"
3959 }
3960 "visit_line_break" | "visit_horizontal_rule" | "visit_definition_list_start" | "visit_figure_start" => {
3961 "const HTMHtmNodeContext* _ctx, void* _user_data, char** out_custom, size_t* out_len"
3962 }
3963 "visit_custom_element" => {
3964 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _tag_name, const char* _html, char** out_custom, size_t* out_len"
3965 }
3966 "visit_form" => {
3967 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _action, const char* _method, char** out_custom, size_t* out_len"
3968 }
3969 "visit_input" => {
3970 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _input_type, const char* _name, const char* _value, char** out_custom, size_t* out_len"
3971 }
3972 "visit_audio" | "visit_video" | "visit_iframe" => {
3973 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _src, char** out_custom, size_t* out_len"
3974 }
3975 "visit_details" => {
3976 "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _open, char** out_custom, size_t* out_len"
3977 }
3978 "visit_figure_end" | "visit_definition_list_end" => {
3979 "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _output, char** out_custom, size_t* out_len"
3980 }
3981 _ => "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _text, char** out_custom, size_t* out_len",
3986 }
3987}
3988
3989fn c_visitor_callback_body(method: &str, action: &crate::fixture::CallbackAction) -> String {
3998 use crate::fixture::CallbackAction;
3999
4000 let mut out = String::new();
4001 let _ = writeln!(out, " (void)_ctx;");
4004 let _ = writeln!(out, " (void)_user_data;");
4005
4006 match action {
4007 CallbackAction::Skip => {
4008 let _ = writeln!(out, " (void)out_custom;");
4009 let _ = writeln!(out, " (void)out_len;");
4010 for param in c_visitor_unused_params(method) {
4012 let _ = writeln!(out, " (void){param};");
4013 }
4014 let _ = writeln!(out, " return 1;");
4015 }
4016 CallbackAction::Continue => {
4017 let _ = writeln!(out, " (void)out_custom;");
4018 let _ = writeln!(out, " (void)out_len;");
4019 for param in c_visitor_unused_params(method) {
4020 let _ = writeln!(out, " (void){param};");
4021 }
4022 let _ = writeln!(out, " return 0;");
4023 }
4024 CallbackAction::PreserveHtml => {
4025 let _ = writeln!(out, " (void)out_custom;");
4026 let _ = writeln!(out, " (void)out_len;");
4027 for param in c_visitor_unused_params(method) {
4028 let _ = writeln!(out, " (void){param};");
4029 }
4030 let _ = writeln!(out, " return 2;");
4031 }
4032 CallbackAction::Custom { output } => {
4033 let escaped = escape_c(output);
4034 for param in c_visitor_unused_params(method) {
4035 let _ = writeln!(out, " (void){param};");
4036 }
4037 let _ = writeln!(out, " char* _buf = strdup(\"{escaped}\");");
4038 let _ = writeln!(out, " if (out_custom) *out_custom = _buf;");
4039 let _ = writeln!(out, " if (out_len) *out_len = _buf ? strlen(_buf) : 0;");
4040 let _ = writeln!(out, " return 3;");
4041 }
4042 CallbackAction::CustomTemplate { template, .. } => {
4043 let (c_fmt, placeholders) = c_visitor_template_to_sprintf(template);
4045 let escaped_fmt = escape_c(&c_fmt);
4046
4047 let used: std::collections::HashSet<&str> = placeholders.iter().map(|s| s.as_str()).collect();
4049 for param in c_visitor_unused_params(method) {
4050 let stripped = param.trim_start_matches('_');
4051 if !used.contains(stripped) {
4052 let _ = writeln!(out, " (void){param};");
4053 }
4054 }
4055
4056 if placeholders.is_empty() {
4057 let _ = writeln!(out, " char* _buf = strdup(\"{escaped_fmt}\");");
4058 } else {
4059 let max_len = template.len() + placeholders.len() * 256 + 64;
4062 let _ = writeln!(out, " char* _buf = (char*)malloc({max_len});");
4063 let _ = writeln!(out, " if (!_buf) {{ (void)out_custom; (void)out_len; return 0; }}");
4064 let args: Vec<String> = placeholders
4066 .iter()
4067 .map(|name| c_visitor_placeholder_to_arg(method, name))
4068 .collect();
4069 let args_str = args.join(", ");
4070 let _ = writeln!(out, " snprintf(_buf, {max_len}, \"{escaped_fmt}\", {args_str});");
4071 }
4072
4073 let _ = writeln!(out, " if (out_custom) *out_custom = _buf;");
4074 let _ = writeln!(out, " if (out_len) *out_len = _buf ? strlen(_buf) : 0;");
4075 let _ = writeln!(out, " return 3;");
4076 }
4077 }
4078
4079 out
4080}
4081
4082fn c_visitor_unused_params(method: &str) -> Vec<&'static str> {
4086 match method {
4087 "visit_text" => vec!["_text"],
4088 "visit_element_start"
4089 | "visit_table_start"
4090 | "visit_line_break"
4091 | "visit_horizontal_rule"
4092 | "visit_definition_list_start"
4093 | "visit_figure_start" => vec![],
4094 "visit_element_end" | "visit_table_end" | "visit_figure_end" | "visit_definition_list_end" => {
4095 vec!["_output"]
4096 }
4097 "visit_link" => vec!["_href", "_text", "_title"],
4098 "visit_image" => vec!["_src", "_alt", "_title"],
4099 "visit_heading" => vec!["_level", "_text", "_id"],
4100 "visit_code_block" => vec!["_lang", "_code"],
4101 "visit_code_inline" => vec!["_code"],
4102 "visit_list_item" => vec!["_ordered", "_marker", "_text"],
4103 "visit_list_start" => vec!["_ordered"],
4104 "visit_list_end" => vec!["_ordered", "_output"],
4105 "visit_table_row" => vec!["_cells", "_cell_count", "_is_header"],
4106 "visit_blockquote" => vec!["_content", "_depth"],
4107 "visit_custom_element" => vec!["_tag_name", "_html"],
4108 "visit_form" => vec!["_action", "_method"],
4109 "visit_input" => vec!["_input_type", "_name", "_value"],
4110 "visit_audio" | "visit_video" | "visit_iframe" => vec!["_src"],
4111 "visit_details" => vec!["_open"],
4112 _ => vec!["_text"],
4114 }
4115}
4116
4117fn c_visitor_template_to_sprintf(template: &str) -> (String, Vec<String>) {
4121 let mut out = String::with_capacity(template.len());
4122 let mut placeholders: Vec<String> = Vec::new();
4123 let mut chars = template.chars().peekable();
4124 while let Some(ch) = chars.next() {
4125 match ch {
4126 '{' => {
4127 if chars.peek() == Some(&'{') {
4128 chars.next();
4129 out.push('{');
4130 continue;
4131 }
4132 let mut name = String::new();
4133 while let Some(&peek) = chars.peek() {
4134 if peek == '}' {
4135 chars.next();
4136 break;
4137 }
4138 name.push(peek);
4139 chars.next();
4140 }
4141 let is_int = matches!(name.as_str(), "level" | "depth" | "ordered" | "open" | "is_header");
4142 if is_int {
4143 out.push_str("%d");
4144 } else {
4145 out.push_str("%s");
4146 }
4147 placeholders.push(name);
4148 }
4149 '}' => {
4150 if chars.peek() == Some(&'}') {
4151 chars.next();
4152 }
4153 out.push('}');
4154 }
4155 '%' => {
4156 out.push_str("%%");
4158 }
4159 other => out.push(other),
4160 }
4161 }
4162 (out, placeholders)
4163}
4164
4165fn c_visitor_placeholder_to_arg(method: &str, name: &str) -> String {
4168 let int_placeholder = matches!(
4169 (method, name),
4170 ("visit_heading", "level")
4171 | ("visit_blockquote", "depth")
4172 | ("visit_list_item", "ordered")
4173 | ("visit_list_start", "ordered")
4174 | ("visit_list_end", "ordered")
4175 | ("visit_details", "open")
4176 | ("visit_table_row", "is_header")
4177 );
4178 if int_placeholder {
4179 return format!("_{name}");
4180 }
4181 format!("(_{name} ? _{name} : \"\")")
4185}