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