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;
18use std::fmt::Write as FmtWrite;
19use std::path::PathBuf;
20
21use super::E2eCodegen;
22
23pub struct CCodegen;
25
26impl E2eCodegen for CCodegen {
27 fn generate(
28 &self,
29 groups: &[FixtureGroup],
30 e2e_config: &E2eConfig,
31 config: &ResolvedCrateConfig,
32 ) -> Result<Vec<GeneratedFile>> {
33 let lang = self.language_name();
34 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
35
36 let mut files = Vec::new();
37
38 let call = &e2e_config.call;
40 let overrides = call.overrides.get(lang);
41 let result_var = &call.result_var;
42 let prefix = overrides
43 .and_then(|o| o.prefix.as_ref())
44 .cloned()
45 .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
46 .unwrap_or_default();
47 let header = overrides.and_then(|o| o.header.as_ref()).cloned().unwrap_or_else(|| {
48 if config.name == "tree-sitter-language-pack" {
50 "ts_pack.h".to_string()
51 } else {
52 config
53 .ffi
54 .as_ref()
55 .and_then(|ffi| ffi.header_name.as_ref().cloned())
56 .unwrap_or_else(|| {
57 let ffi_prefix = config
59 .ffi
60 .as_ref()
61 .and_then(|ffi| ffi.prefix.as_ref())
62 .map(|p| p.to_string())
63 .unwrap_or_default();
64 if ffi_prefix.is_empty() {
65 format!("{}.h", call.module)
66 } else {
67 format!("{}.h", ffi_prefix)
68 }
69 })
70 }
71 });
72
73 let c_pkg = e2e_config.resolve_package("c");
75 let lib_name = c_pkg
76 .as_ref()
77 .and_then(|p| p.name.as_ref())
78 .cloned()
79 .or_else(|| {
80 if config.name == "tree-sitter-language-pack" {
82 Some("ts_pack_core_ffi".to_string())
83 } else {
84 config.ffi.as_ref().and_then(|ffi| ffi.lib_name.as_ref()).cloned()
85 }
86 })
87 .unwrap_or_else(|| call.module.clone());
88
89 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
91 .iter()
92 .filter_map(|group| {
93 let active: Vec<&Fixture> = group
94 .fixtures
95 .iter()
96 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
97 .filter(|f| f.visitor.is_none())
98 .collect();
99 if active.is_empty() { None } else { Some((group, active)) }
100 })
101 .collect();
102
103 let ffi_crate_path = c_pkg
110 .as_ref()
111 .and_then(|p| p.path.as_ref())
112 .cloned()
113 .unwrap_or_else(|| {
114 if config.name == "tree-sitter-language-pack" {
116 "../../crates/ts-pack-core-ffi".to_string()
117 } else {
118 format!("../../crates/{}-ffi", config.name)
119 }
120 });
121
122 let category_names: Vec<String> = active_groups
124 .iter()
125 .map(|(g, _)| sanitize_filename(&g.category))
126 .collect();
127 files.push(GeneratedFile {
128 path: output_base.join("Makefile"),
129 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
130 generated_header: true,
131 });
132
133 let github_repo = config.github_repo();
135 let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
136 let ffi_pkg_name = e2e_config
137 .registry
138 .packages
139 .get("c")
140 .and_then(|p| p.name.as_ref())
141 .cloned()
142 .unwrap_or_else(|| lib_name.clone());
143 files.push(GeneratedFile {
144 path: output_base.join("download_ffi.sh"),
145 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
146 generated_header: true,
147 });
148
149 files.push(GeneratedFile {
151 path: output_base.join("test_runner.h"),
152 content: render_test_runner_header(&active_groups),
153 generated_header: true,
154 });
155
156 files.push(GeneratedFile {
158 path: output_base.join("main.c"),
159 content: render_main_c(&active_groups),
160 generated_header: true,
161 });
162
163 let field_resolver = FieldResolver::new(
164 &e2e_config.fields,
165 &e2e_config.fields_optional,
166 &e2e_config.result_fields,
167 &e2e_config.fields_array,
168 &std::collections::HashSet::new(),
169 );
170
171 for (group, active) in &active_groups {
175 let filename = format!("test_{}.c", sanitize_filename(&group.category));
176 let content = render_test_file(
177 &group.category,
178 active,
179 &header,
180 &prefix,
181 result_var,
182 e2e_config,
183 lang,
184 &field_resolver,
185 );
186 files.push(GeneratedFile {
187 path: output_base.join(filename),
188 content,
189 generated_header: true,
190 });
191 }
192
193 Ok(files)
194 }
195
196 fn language_name(&self) -> &'static str {
197 "c"
198 }
199}
200
201struct ResolvedCallInfo {
203 function_name: String,
204 result_type_name: String,
205 options_type_name: String,
206 client_factory: Option<String>,
207 args: Vec<crate::config::ArgMapping>,
208 raw_c_result_type: Option<String>,
209 c_free_fn: Option<String>,
210 result_is_option: bool,
211}
212
213fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
214 let overrides = call.overrides.get(lang);
215 let function_name = overrides
216 .and_then(|o| o.function.as_ref())
217 .cloned()
218 .unwrap_or_else(|| call.function.clone());
219 let result_type_name = overrides
224 .and_then(|o| o.result_type.as_ref())
225 .cloned()
226 .unwrap_or_else(|| call.function.to_pascal_case());
227 let options_type_name = overrides
228 .and_then(|o| o.options_type.as_deref())
229 .unwrap_or("ConversionOptions")
230 .to_string();
231 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
232 let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
233 let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
234 let result_is_option = overrides
235 .and_then(|o| if o.result_is_option { Some(true) } else { None })
236 .unwrap_or(call.result_is_option);
237 ResolvedCallInfo {
238 function_name,
239 result_type_name,
240 options_type_name,
241 client_factory,
242 args: call.args.clone(),
243 raw_c_result_type,
244 c_free_fn,
245 result_is_option,
246 }
247}
248
249fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
255 let call = e2e_config.resolve_call(fixture.call.as_deref());
256 let mut info = resolve_call_info(call, lang);
257
258 if info.client_factory.is_none() {
261 let default_overrides = e2e_config.call.overrides.get(lang);
262 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
263 info.client_factory = Some(factory.clone());
264 }
265 }
266
267 info
268}
269
270fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
271 let mut out = String::new();
272 out.push_str(&hash::header(CommentStyle::Hash));
273 let _ = writeln!(out, "CC = gcc");
274 let _ = writeln!(out, "FFI_DIR = ffi");
275 let _ = writeln!(out);
276
277 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
279 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
280 let _ = writeln!(
281 out,
282 " LDFLAGS = -L$(FFI_DIR)/lib -l{lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
283 );
284 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
285 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
286 let _ = writeln!(
287 out,
288 " LDFLAGS = -L../../target/release -l{lib_name} -Wl,-rpath,../../target/release"
289 );
290 let _ = writeln!(out, "else");
291 let _ = writeln!(
292 out,
293 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
294 );
295 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
296 let _ = writeln!(out, "endif");
297 let _ = writeln!(out);
298
299 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
300 let srcs = src_files.join(" ");
301
302 let _ = writeln!(out, "SRCS = main.c {srcs}");
303 let _ = writeln!(out, "TARGET = run_tests");
304 let _ = writeln!(out);
305 let _ = writeln!(out, ".PHONY: all clean test");
306 let _ = writeln!(out);
307 let _ = writeln!(out, "all: $(TARGET)");
308 let _ = writeln!(out);
309 let _ = writeln!(out, "$(TARGET): $(SRCS)");
310 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
311 let _ = writeln!(out);
312 let _ = writeln!(out, "test: $(TARGET)");
313 let _ = writeln!(out, "\t./$(TARGET)");
314 let _ = writeln!(out);
315 let _ = writeln!(out, "clean:");
316 let _ = writeln!(out, "\trm -f $(TARGET)");
317 out
318}
319
320fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
321 let mut out = String::new();
322 let _ = writeln!(out, "#!/usr/bin/env bash");
323 out.push_str(&hash::header(CommentStyle::Hash));
324 let _ = writeln!(out, "set -euo pipefail");
325 let _ = writeln!(out);
326 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
327 let _ = writeln!(out, "VERSION=\"{version}\"");
328 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
329 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
330 let _ = writeln!(out);
331 let _ = writeln!(out, "# Detect OS and architecture.");
332 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
333 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
334 let _ = writeln!(out);
335 let _ = writeln!(out, "case \"$ARCH\" in");
336 let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
337 let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
338 let _ = writeln!(out, "*)");
339 let _ = writeln!(out, " echo \"Unsupported architecture: $ARCH\" >&2");
340 let _ = writeln!(out, " exit 1");
341 let _ = writeln!(out, " ;;");
342 let _ = writeln!(out, "esac");
343 let _ = writeln!(out);
344 let _ = writeln!(out, "case \"$OS\" in");
345 let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
346 let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
347 let _ = writeln!(out, "*)");
348 let _ = writeln!(out, " echo \"Unsupported OS: $OS\" >&2");
349 let _ = writeln!(out, " exit 1");
350 let _ = writeln!(out, " ;;");
351 let _ = writeln!(out, "esac");
352 let _ = writeln!(out);
353 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
354 let _ = writeln!(
355 out,
356 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
357 );
358 let _ = writeln!(out);
359 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
360 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
361 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
362 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
363 out
364}
365
366fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
367 let mut out = String::new();
368 out.push_str(&hash::header(CommentStyle::Block));
369 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
370 let _ = writeln!(out, "#define TEST_RUNNER_H");
371 let _ = writeln!(out);
372 let _ = writeln!(out, "#include <string.h>");
373 let _ = writeln!(out, "#include <stdlib.h>");
374 let _ = writeln!(out);
375 let _ = writeln!(out, "/**");
377 let _ = writeln!(
378 out,
379 " * Compare a string against an expected value, trimming trailing whitespace."
380 );
381 let _ = writeln!(
382 out,
383 " * Returns 0 if the trimmed actual string equals the expected string."
384 );
385 let _ = writeln!(out, " */");
386 let _ = writeln!(
387 out,
388 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
389 );
390 let _ = writeln!(
391 out,
392 " if (actual == NULL || expected == NULL) return actual != expected;"
393 );
394 let _ = writeln!(out, " size_t alen = strlen(actual);");
395 let _ = writeln!(
396 out,
397 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
398 );
399 let _ = writeln!(out, " size_t elen = strlen(expected);");
400 let _ = writeln!(out, " if (alen != elen) return 1;");
401 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
402 let _ = writeln!(out, "}}");
403 let _ = writeln!(out);
404
405 let _ = writeln!(out, "/**");
406 let _ = writeln!(
407 out,
408 " * Extract a string value for a given key from a JSON object string."
409 );
410 let _ = writeln!(
411 out,
412 " * Returns a heap-allocated copy of the value, or NULL if not found."
413 );
414 let _ = writeln!(out, " * Caller must free() the returned string.");
415 let _ = writeln!(out, " */");
416 let _ = writeln!(
417 out,
418 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
419 );
420 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
421 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
422 let _ = writeln!(out, " size_t key_len = strlen(key);");
423 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
424 let _ = writeln!(out, " if (!pattern) return NULL;");
425 let _ = writeln!(out, " pattern[0] = '\"';");
426 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
427 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
428 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
429 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
430 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
431 let _ = writeln!(out, " free(pattern);");
432 let _ = writeln!(out, " if (!found) return NULL;");
433 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
434 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
435 let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
436 let _ = writeln!(out, " found++; /* skip opening quote */");
437 let _ = writeln!(out, " const char *end = found;");
438 let _ = writeln!(out, " while (*end && *end != '\"') {{");
439 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
440 let _ = writeln!(out, " else end++;");
441 let _ = writeln!(out, " }}");
442 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
443 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
444 let _ = writeln!(out, " if (!result_str) return NULL;");
445 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
446 let _ = writeln!(out, " result_str[val_len] = '\\0';");
447 let _ = writeln!(out, " return result_str;");
448 let _ = writeln!(out, "}}");
449 let _ = writeln!(out);
450 let _ = writeln!(out, "/**");
451 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
452 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
453 let _ = writeln!(out, " */");
454 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
455 let _ = writeln!(out, " if (json == NULL) return 0;");
456 let _ = writeln!(out, " /* Skip leading whitespace */");
457 let _ = writeln!(
458 out,
459 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
460 );
461 let _ = writeln!(out, " if (*json != '[') return 0;");
462 let _ = writeln!(out, " json++;");
463 let _ = writeln!(out, " /* Skip whitespace after '[' */");
464 let _ = writeln!(
465 out,
466 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
467 );
468 let _ = writeln!(out, " if (*json == ']') return 0;");
469 let _ = writeln!(out, " int count = 1;");
470 let _ = writeln!(out, " int depth = 0;");
471 let _ = writeln!(out, " int in_string = 0;");
472 let _ = writeln!(
473 out,
474 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
475 );
476 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
477 let _ = writeln!(
478 out,
479 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
480 );
481 let _ = writeln!(out, " if (in_string) continue;");
482 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
483 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
484 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
485 let _ = writeln!(out, " }}");
486 let _ = writeln!(out, " return count;");
487 let _ = writeln!(out, "}}");
488 let _ = writeln!(out);
489
490 for (group, fixtures) in active_groups {
491 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
492 for fixture in fixtures {
493 let fn_name = sanitize_ident(&fixture.id);
494 let _ = writeln!(out, "void test_{fn_name}(void);");
495 }
496 let _ = writeln!(out);
497 }
498
499 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
500 out
501}
502
503fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
504 let mut out = String::new();
505 out.push_str(&hash::header(CommentStyle::Block));
506 let _ = writeln!(out, "#include <stdio.h>");
507 let _ = writeln!(out, "#include \"test_runner.h\"");
508 let _ = writeln!(out);
509 let _ = writeln!(out, "int main(void) {{");
510 let _ = writeln!(out, " int passed = 0;");
511 let _ = writeln!(out);
512
513 for (group, fixtures) in active_groups {
514 let _ = writeln!(out, " /* Category: {} */", group.category);
515 for fixture in fixtures {
516 let fn_name = sanitize_ident(&fixture.id);
517 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
518 let _ = writeln!(out, " test_{fn_name}();");
519 let _ = writeln!(out, " printf(\" PASSED\\n\");");
520 let _ = writeln!(out, " passed++;");
521 }
522 let _ = writeln!(out);
523 }
524
525 let _ = writeln!(out, " printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
526 let _ = writeln!(out, " return 0;");
527 let _ = writeln!(out, "}}");
528 out
529}
530
531#[allow(clippy::too_many_arguments)]
532fn render_test_file(
533 category: &str,
534 fixtures: &[&Fixture],
535 header: &str,
536 prefix: &str,
537 result_var: &str,
538 e2e_config: &E2eConfig,
539 lang: &str,
540 field_resolver: &FieldResolver,
541) -> String {
542 let mut out = String::new();
543 out.push_str(&hash::header(CommentStyle::Block));
544 let _ = writeln!(out, "/* E2e tests for category: {category} */");
545 let _ = writeln!(out);
546 let _ = writeln!(out, "#include <assert.h>");
547 let _ = writeln!(out, "#include <string.h>");
548 let _ = writeln!(out, "#include <stdio.h>");
549 let _ = writeln!(out, "#include <stdlib.h>");
550 let _ = writeln!(out, "#include \"{header}\"");
551 let _ = writeln!(out, "#include \"test_runner.h\"");
552 let _ = writeln!(out);
553
554 for (i, fixture) in fixtures.iter().enumerate() {
555 if fixture.visitor.is_some() {
558 panic!(
559 "C e2e generator: visitor pattern not supported for fixture: {}",
560 fixture.id
561 );
562 }
563
564 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
565 render_test_function(
566 &mut out,
567 fixture,
568 prefix,
569 &call_info.function_name,
570 result_var,
571 &call_info.args,
572 field_resolver,
573 &e2e_config.fields_c_types,
574 &call_info.result_type_name,
575 &call_info.options_type_name,
576 call_info.client_factory.as_deref(),
577 call_info.raw_c_result_type.as_deref(),
578 call_info.c_free_fn.as_deref(),
579 call_info.result_is_option,
580 );
581 if i + 1 < fixtures.len() {
582 let _ = writeln!(out);
583 }
584 }
585
586 out
587}
588
589#[allow(clippy::too_many_arguments)]
590fn render_test_function(
591 out: &mut String,
592 fixture: &Fixture,
593 prefix: &str,
594 function_name: &str,
595 result_var: &str,
596 args: &[crate::config::ArgMapping],
597 field_resolver: &FieldResolver,
598 fields_c_types: &HashMap<String, String>,
599 result_type_name: &str,
600 options_type_name: &str,
601 client_factory: Option<&str>,
602 raw_c_result_type: Option<&str>,
603 c_free_fn: Option<&str>,
604 result_is_option: bool,
605) {
606 let fn_name = sanitize_ident(&fixture.id);
607 let description = &fixture.description;
608
609 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
610
611 let _ = writeln!(out, "void test_{fn_name}(void) {{");
612 let _ = writeln!(out, " /* {description} */");
613
614 let prefix_upper = prefix.to_uppercase();
615
616 if let Some(factory) = client_factory {
621 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); for arg in args {
624 if arg.arg_type == "json_object" {
625 let request_type_pascal = if let Some(stripped) = result_type_name.strip_suffix("Response") {
628 format!("{}Request", stripped)
629 } else {
630 format!("{result_type_name}Request")
631 };
632 let request_type_snake = request_type_pascal.to_snake_case();
633 let var_name = format!("{request_type_snake}_handle");
634
635 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
636 let json_val = if field.is_empty() || field == "input" {
637 Some(&fixture.input)
638 } else {
639 fixture.input.get(field)
640 };
641
642 if let Some(val) = json_val {
643 if !val.is_null() {
644 let normalized = super::normalize_json_keys_to_snake_case(val);
645 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
646 let escaped = escape_c(&json_str);
647 let _ = writeln!(
648 out,
649 " {prefix_upper}{request_type_pascal}* {var_name} = \
650 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
651 );
652 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
653 request_handle_vars.push((arg.name.clone(), var_name));
654 }
655 }
656 }
657 }
658
659 let _ = writeln!(
660 out,
661 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
662 );
663 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
664
665 let method_args = if request_handle_vars.is_empty() {
666 String::new()
667 } else {
668 let handles: Vec<&str> = request_handle_vars.iter().map(|(_, v)| v.as_str()).collect();
669 format!(", {}", handles.join(", "))
670 };
671
672 let call_fn = format!("{prefix}_default_client_{function_name}");
673
674 if expects_error {
675 let _ = writeln!(
676 out,
677 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
678 );
679 for (_, var_name) in &request_handle_vars {
680 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
681 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
682 }
683 let _ = writeln!(out, " {prefix}_default_client_free(client);");
684 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
685 let _ = writeln!(out, "}}");
686 return;
687 }
688
689 let _ = writeln!(
690 out,
691 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
692 );
693 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
694
695 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
696 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
697
698 for assertion in &fixture.assertions {
699 if let Some(f) = &assertion.field {
700 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
701 let resolved = field_resolver.resolve(f);
702 let local_var = f.replace(['.', '['], "_").replace(']', "");
703 let has_map_access = resolved.contains('[');
704 if resolved.contains('.') {
705 emit_nested_accessor(
706 out,
707 prefix,
708 resolved,
709 &local_var,
710 result_var,
711 fields_c_types,
712 &mut intermediate_handles,
713 result_type_name,
714 );
715 } else {
716 let result_type_snake = result_type_name.to_snake_case();
717 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
718 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
719 }
720 accessed_fields.push((f.clone(), local_var, has_map_access));
721 }
722 }
723 }
724
725 for assertion in &fixture.assertions {
726 render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
727 }
728
729 for (_f, local_var, from_json) in &accessed_fields {
730 if *from_json {
731 let _ = writeln!(out, " free({local_var});");
732 } else {
733 let _ = writeln!(out, " {prefix}_free_string({local_var});");
734 }
735 }
736 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
737 if snake_type == "free_string" {
738 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
739 } else {
740 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
741 }
742 }
743 let result_type_snake = result_type_name.to_snake_case();
744 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
745 for (_, var_name) in &request_handle_vars {
746 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
747 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
748 }
749 let _ = writeln!(out, " {prefix}_default_client_free(client);");
750 let _ = writeln!(out, "}}");
751 return;
752 }
753
754 if let Some(raw_type) = raw_c_result_type {
757 let args_str = if args.is_empty() {
759 String::new()
760 } else {
761 let parts: Vec<String> = args
762 .iter()
763 .filter_map(|arg| {
764 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
765 let val = fixture.input.get(field);
766 match val {
767 None if arg.optional => Some("NULL".to_string()),
768 None => None,
769 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
770 Some(v) => Some(json_to_c(v)),
771 }
772 })
773 .collect();
774 parts.join(", ")
775 };
776
777 let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
779
780 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
782 if has_not_error {
783 match raw_type {
784 "char*" if !result_is_option => {
785 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
786 }
787 "int32_t" => {
788 let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
789 }
790 "uintptr_t" => {
791 let _ = writeln!(
792 out,
793 " assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
794 );
795 }
796 _ => {}
797 }
798 }
799
800 for assertion in &fixture.assertions {
802 match assertion.assertion_type.as_str() {
803 "not_error" | "error" => {} "not_empty" => {
805 let _ = writeln!(
806 out,
807 " assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
808 );
809 }
810 "is_empty" => {
811 if result_is_option && raw_type == "char*" {
812 let _ = writeln!(
813 out,
814 " assert({result_var} == NULL && \"expected empty/null value\");"
815 );
816 } else {
817 let _ = writeln!(
818 out,
819 " assert(strlen({result_var}) == 0 && \"expected empty value\");"
820 );
821 }
822 }
823 "count_min" => {
824 if let Some(val) = &assertion.value {
825 if let Some(n) = val.as_u64() {
826 match raw_type {
827 "char*" => {
828 let _ = writeln!(out, " {{");
829 let _ = writeln!(
830 out,
831 " assert({result_var} != NULL && \"expected non-null JSON array\");"
832 );
833 let _ =
834 writeln!(out, " int elem_count = alef_json_array_count({result_var});");
835 let _ = writeln!(
836 out,
837 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
838 );
839 let _ = writeln!(out, " }}");
840 }
841 _ => {
842 let _ = writeln!(
843 out,
844 " assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
845 );
846 }
847 }
848 }
849 }
850 }
851 "greater_than_or_equal" => {
852 if let Some(val) = &assertion.value {
853 let c_val = json_to_c(val);
854 let _ = writeln!(
855 out,
856 " assert({result_var} >= {c_val} && \"expected greater than or equal\");"
857 );
858 }
859 }
860 "contains" => {
861 if let Some(val) = &assertion.value {
862 let c_val = json_to_c(val);
863 let _ = writeln!(
864 out,
865 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
866 );
867 }
868 }
869 "contains_all" => {
870 if let Some(values) = &assertion.values {
871 for val in values {
872 let c_val = json_to_c(val);
873 let _ = writeln!(
874 out,
875 " assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
876 );
877 }
878 }
879 }
880 "equals" => {
881 if let Some(val) = &assertion.value {
882 let c_val = json_to_c(val);
883 if val.is_string() {
884 let _ = writeln!(
885 out,
886 " assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
887 );
888 } else {
889 let _ = writeln!(
890 out,
891 " assert({result_var} == {c_val} && \"equals assertion failed\");"
892 );
893 }
894 }
895 }
896 "not_contains" => {
897 if let Some(val) = &assertion.value {
898 let c_val = json_to_c(val);
899 let _ = writeln!(
900 out,
901 " assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
902 );
903 }
904 }
905 "starts_with" => {
906 if let Some(val) = &assertion.value {
907 let c_val = json_to_c(val);
908 let _ = writeln!(
909 out,
910 " assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
911 );
912 }
913 }
914 "is_true" => {
915 let _ = writeln!(out, " assert({result_var});");
916 }
917 "is_false" => {
918 let _ = writeln!(out, " assert(!{result_var});");
919 }
920 other => {
921 panic!("C e2e raw-result generator: unsupported assertion type: {other}");
922 }
923 }
924 }
925
926 if raw_type == "char*" {
928 let free_fn = c_free_fn
929 .map(|s| s.to_string())
930 .unwrap_or_else(|| format!("{prefix}_free_string"));
931 if result_is_option {
932 let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
933 } else {
934 let _ = writeln!(out, " {free_fn}({result_var});");
935 }
936 }
937
938 let _ = writeln!(out, "}}");
939 return;
940 }
941
942 let prefixed_fn = function_name.to_string();
948
949 let mut has_options_handle = false;
951 for arg in args {
952 if arg.arg_type == "json_object" {
953 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
954 if let Some(val) = fixture.input.get(field) {
955 if !val.is_null() {
956 let normalized = super::normalize_json_keys_to_snake_case(val);
960 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
961 let escaped = escape_c(&json_str);
962 let upper = prefix.to_uppercase();
963 let options_type_pascal = options_type_name;
964 let options_type_snake = options_type_name.to_snake_case();
965 let _ = writeln!(
966 out,
967 " {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
968 );
969 has_options_handle = true;
970 }
971 }
972 }
973 }
974
975 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
976
977 if expects_error {
978 let _ = writeln!(
979 out,
980 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
981 );
982 if has_options_handle {
983 let options_type_snake = options_type_name.to_snake_case();
984 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
985 }
986 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
987 let _ = writeln!(out, "}}");
988 return;
989 }
990
991 let _ = writeln!(
993 out,
994 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
995 );
996 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
997
998 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1006 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1009
1010 for assertion in &fixture.assertions {
1011 if let Some(f) = &assertion.field {
1012 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1013 let resolved = field_resolver.resolve(f);
1014 let local_var = f.replace(['.', '['], "_").replace(']', "");
1015 let has_map_access = resolved.contains('[');
1016
1017 if resolved.contains('.') {
1018 emit_nested_accessor(
1019 out,
1020 prefix,
1021 resolved,
1022 &local_var,
1023 result_var,
1024 fields_c_types,
1025 &mut intermediate_handles,
1026 result_type_name,
1027 );
1028 } else {
1029 let result_type_snake = result_type_name.to_snake_case();
1030 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1031 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
1032 }
1033 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1034 }
1035 }
1036 }
1037
1038 for assertion in &fixture.assertions {
1039 render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
1040 }
1041
1042 for (_f, local_var, from_json) in &accessed_fields {
1044 if *from_json {
1045 let _ = writeln!(out, " free({local_var});");
1046 } else {
1047 let _ = writeln!(out, " {prefix}_free_string({local_var});");
1048 }
1049 }
1050 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1052 if snake_type == "free_string" {
1053 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
1055 } else {
1056 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
1057 }
1058 }
1059 if has_options_handle {
1060 let options_type_snake = options_type_name.to_snake_case();
1061 let _ = writeln!(out, " {prefix}_{options_type_snake}_free(options_handle);");
1062 }
1063 let result_type_snake = result_type_name.to_snake_case();
1064 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
1065 let _ = writeln!(out, "}}");
1066}
1067
1068#[allow(clippy::too_many_arguments)]
1082fn emit_nested_accessor(
1083 out: &mut String,
1084 prefix: &str,
1085 resolved: &str,
1086 local_var: &str,
1087 result_var: &str,
1088 fields_c_types: &HashMap<String, String>,
1089 intermediate_handles: &mut Vec<(String, String)>,
1090 result_type_name: &str,
1091) {
1092 let segments: Vec<&str> = resolved.split('.').collect();
1093 let prefix_upper = prefix.to_uppercase();
1094
1095 let mut current_snake_type = result_type_name.to_snake_case();
1097 let mut current_handle = result_var.to_string();
1098
1099 for (i, segment) in segments.iter().enumerate() {
1100 let is_leaf = i + 1 == segments.len();
1101
1102 if let Some(bracket_pos) = segment.find('[') {
1104 let field_name = &segment[..bracket_pos];
1105 let key = segment[bracket_pos + 1..].trim_end_matches(']');
1106 let field_snake = field_name.to_snake_case();
1107 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
1108
1109 let json_var = format!("{field_snake}_json");
1112 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
1113 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
1114 let _ = writeln!(out, " assert({json_var} != NULL);");
1115 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
1117 }
1118 let _ = writeln!(
1120 out,
1121 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
1122 );
1123 return; }
1125
1126 let seg_snake = segment.to_snake_case();
1127 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
1128
1129 if is_leaf {
1130 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
1132 } else {
1133 let lookup_key = format!("{current_snake_type}.{seg_snake}");
1135 let return_type_pascal = match fields_c_types.get(&lookup_key) {
1136 Some(t) => t.clone(),
1137 None => {
1138 segment.to_pascal_case()
1140 }
1141 };
1142 let return_snake = return_type_pascal.to_snake_case();
1143 let handle_var = format!("{seg_snake}_handle");
1144
1145 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
1148 let _ = writeln!(
1149 out,
1150 " {prefix_upper}{return_type_pascal}* {handle_var} = \
1151 {accessor_fn}({current_handle});"
1152 );
1153 let _ = writeln!(out, " assert({handle_var} != NULL);");
1154 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
1155 }
1156
1157 current_snake_type = return_snake;
1158 current_handle = handle_var;
1159 }
1160 }
1161}
1162
1163fn build_args_string_c(
1167 input: &serde_json::Value,
1168 args: &[crate::config::ArgMapping],
1169 has_options_handle: bool,
1170) -> String {
1171 if args.is_empty() {
1172 return json_to_c(input);
1173 }
1174
1175 let parts: Vec<String> = args
1176 .iter()
1177 .filter_map(|arg| {
1178 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1179 let val = input.get(field);
1180 match val {
1181 None if arg.optional => Some("NULL".to_string()),
1183 None => None,
1185 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1187 Some(v) => {
1188 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
1191 Some("options_handle".to_string())
1192 } else {
1193 Some(json_to_c(v))
1194 }
1195 }
1196 }
1197 })
1198 .collect();
1199
1200 parts.join(", ")
1201}
1202
1203fn render_assertion(
1204 out: &mut String,
1205 assertion: &Assertion,
1206 result_var: &str,
1207 _field_resolver: &FieldResolver,
1208 accessed_fields: &[(String, String, bool)],
1209) {
1210 if let Some(f) = &assertion.field {
1212 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
1213 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1214 return;
1215 }
1216 }
1217
1218 let field_expr = match &assertion.field {
1219 Some(f) if !f.is_empty() => {
1220 accessed_fields
1222 .iter()
1223 .find(|(k, _, _)| k == f)
1224 .map(|(_, local, _)| local.clone())
1225 .unwrap_or_else(|| result_var.to_string())
1226 }
1227 _ => result_var.to_string(),
1228 };
1229
1230 match assertion.assertion_type.as_str() {
1231 "equals" => {
1232 if let Some(expected) = &assertion.value {
1233 let c_val = json_to_c(expected);
1234 if expected.is_string() {
1235 let _ = writeln!(
1237 out,
1238 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
1239 );
1240 } else {
1241 let _ = writeln!(
1242 out,
1243 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
1244 );
1245 }
1246 }
1247 }
1248 "contains" => {
1249 if let Some(expected) = &assertion.value {
1250 let c_val = json_to_c(expected);
1251 let _ = writeln!(
1252 out,
1253 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1254 );
1255 }
1256 }
1257 "contains_all" => {
1258 if let Some(values) = &assertion.values {
1259 for val in values {
1260 let c_val = json_to_c(val);
1261 let _ = writeln!(
1262 out,
1263 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1264 );
1265 }
1266 }
1267 }
1268 "not_contains" => {
1269 if let Some(expected) = &assertion.value {
1270 let c_val = json_to_c(expected);
1271 let _ = writeln!(
1272 out,
1273 " assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1274 );
1275 }
1276 }
1277 "not_empty" => {
1278 let _ = writeln!(
1279 out,
1280 " assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
1281 );
1282 }
1283 "is_empty" => {
1284 let _ = writeln!(
1285 out,
1286 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
1287 );
1288 }
1289 "contains_any" => {
1290 if let Some(values) = &assertion.values {
1291 let _ = writeln!(out, " {{");
1292 let _ = writeln!(out, " int found = 0;");
1293 for val in values {
1294 let c_val = json_to_c(val);
1295 let _ = writeln!(
1296 out,
1297 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
1298 );
1299 }
1300 let _ = writeln!(
1301 out,
1302 " assert(found && \"expected to contain at least one of the specified values\");"
1303 );
1304 let _ = writeln!(out, " }}");
1305 }
1306 }
1307 "greater_than" => {
1308 if let Some(val) = &assertion.value {
1309 let c_val = json_to_c(val);
1310 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
1311 }
1312 }
1313 "less_than" => {
1314 if let Some(val) = &assertion.value {
1315 let c_val = json_to_c(val);
1316 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
1317 }
1318 }
1319 "greater_than_or_equal" => {
1320 if let Some(val) = &assertion.value {
1321 let c_val = json_to_c(val);
1322 let _ = writeln!(
1323 out,
1324 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
1325 );
1326 }
1327 }
1328 "less_than_or_equal" => {
1329 if let Some(val) = &assertion.value {
1330 let c_val = json_to_c(val);
1331 let _ = writeln!(
1332 out,
1333 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
1334 );
1335 }
1336 }
1337 "starts_with" => {
1338 if let Some(expected) = &assertion.value {
1339 let c_val = json_to_c(expected);
1340 let _ = writeln!(
1341 out,
1342 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1343 );
1344 }
1345 }
1346 "ends_with" => {
1347 if let Some(expected) = &assertion.value {
1348 let c_val = json_to_c(expected);
1349 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
1350 let _ = writeln!(
1351 out,
1352 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
1353 );
1354 }
1355 }
1356 "min_length" => {
1357 if let Some(val) = &assertion.value {
1358 if let Some(n) = val.as_u64() {
1359 let _ = writeln!(
1360 out,
1361 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
1362 );
1363 }
1364 }
1365 }
1366 "max_length" => {
1367 if let Some(val) = &assertion.value {
1368 if let Some(n) = val.as_u64() {
1369 let _ = writeln!(
1370 out,
1371 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
1372 );
1373 }
1374 }
1375 }
1376 "count_min" => {
1377 if let Some(val) = &assertion.value {
1378 if let Some(n) = val.as_u64() {
1379 let _ = writeln!(out, " {{");
1380 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
1381 let _ = writeln!(
1382 out,
1383 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1384 );
1385 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1386 let _ = writeln!(
1387 out,
1388 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1389 );
1390 let _ = writeln!(out, " }}");
1391 }
1392 }
1393 }
1394 "count_equals" => {
1395 if let Some(val) = &assertion.value {
1396 if let Some(n) = val.as_u64() {
1397 let _ = writeln!(out, " {{");
1398 let _ = writeln!(out, " /* count_equals: count elements in array */");
1399 let _ = writeln!(
1400 out,
1401 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1402 );
1403 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1404 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
1405 let _ = writeln!(out, " }}");
1406 }
1407 }
1408 }
1409 "is_true" => {
1410 let _ = writeln!(out, " assert({field_expr});");
1411 }
1412 "is_false" => {
1413 let _ = writeln!(out, " assert(!{field_expr});");
1414 }
1415 "method_result" => {
1416 if let Some(method_name) = &assertion.method {
1417 render_method_result_assertion(
1418 out,
1419 result_var,
1420 method_name,
1421 assertion.args.as_ref(),
1422 assertion.check.as_deref().unwrap_or("is_true"),
1423 assertion.value.as_ref(),
1424 );
1425 } else {
1426 panic!("C e2e generator: method_result assertion missing 'method' field");
1427 }
1428 }
1429 "matches_regex" => {
1430 if let Some(expected) = &assertion.value {
1431 let c_val = json_to_c(expected);
1432 let _ = writeln!(out, " {{");
1433 let _ = writeln!(out, " regex_t _re;");
1434 let _ = writeln!(
1435 out,
1436 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
1437 );
1438 let _ = writeln!(
1439 out,
1440 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
1441 );
1442 let _ = writeln!(out, " regfree(&_re);");
1443 let _ = writeln!(out, " }}");
1444 }
1445 }
1446 "not_error" => {
1447 }
1449 "error" => {
1450 }
1452 other => {
1453 panic!("C e2e generator: unsupported assertion type: {other}");
1454 }
1455 }
1456}
1457
1458fn render_method_result_assertion(
1463 out: &mut String,
1464 result_var: &str,
1465 method_name: &str,
1466 args: Option<&serde_json::Value>,
1467 check: &str,
1468 value: Option<&serde_json::Value>,
1469) {
1470 let call_expr = build_c_method_call(result_var, method_name, args);
1471
1472 match method_name {
1473 "has_error_nodes" | "error_count" | "tree_error_count" => match check {
1475 "equals" => {
1476 if let Some(val) = value {
1477 let c_val = json_to_c(val);
1478 let _ = writeln!(
1479 out,
1480 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
1481 );
1482 }
1483 }
1484 "is_true" => {
1485 let _ = writeln!(
1486 out,
1487 " assert({call_expr} && \"method_result is_true assertion failed\");"
1488 );
1489 }
1490 "is_false" => {
1491 let _ = writeln!(
1492 out,
1493 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
1494 );
1495 }
1496 "greater_than_or_equal" => {
1497 if let Some(val) = value {
1498 let n = val.as_u64().unwrap_or(0);
1499 let _ = writeln!(
1500 out,
1501 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
1502 );
1503 }
1504 }
1505 other_check => {
1506 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1507 }
1508 },
1509
1510 "root_child_count" | "named_children_count" => {
1512 let _ = writeln!(out, " {{");
1513 let _ = writeln!(
1514 out,
1515 " TS_PACKNodeInfo* _node_info = ts_pack_root_node_info({result_var});"
1516 );
1517 let _ = writeln!(
1518 out,
1519 " assert(_node_info != NULL && \"root_node_info returned NULL\");"
1520 );
1521 let _ = writeln!(
1522 out,
1523 " size_t _count = ts_pack_node_info_named_child_count(_node_info);"
1524 );
1525 let _ = writeln!(out, " ts_pack_node_info_free(_node_info);");
1526 match check {
1527 "equals" => {
1528 if let Some(val) = value {
1529 let c_val = json_to_c(val);
1530 let _ = writeln!(
1531 out,
1532 " assert(_count == (size_t){c_val} && \"method_result equals assertion failed\");"
1533 );
1534 }
1535 }
1536 "greater_than_or_equal" => {
1537 if let Some(val) = value {
1538 let n = val.as_u64().unwrap_or(0);
1539 let _ = writeln!(
1540 out,
1541 " assert(_count >= {n} && \"method_result >= {n} assertion failed\");"
1542 );
1543 }
1544 }
1545 "is_true" => {
1546 let _ = writeln!(
1547 out,
1548 " assert(_count > 0 && \"method_result is_true assertion failed\");"
1549 );
1550 }
1551 other_check => {
1552 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1553 }
1554 }
1555 let _ = writeln!(out, " }}");
1556 }
1557
1558 "tree_to_sexp" => {
1560 let _ = writeln!(out, " {{");
1561 let _ = writeln!(out, " char* _method_result = {call_expr};");
1562 let _ = writeln!(
1563 out,
1564 " assert(_method_result != NULL && \"method_result returned NULL\");"
1565 );
1566 match check {
1567 "contains" => {
1568 if let Some(val) = value {
1569 let c_val = json_to_c(val);
1570 let _ = writeln!(
1571 out,
1572 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1573 );
1574 }
1575 }
1576 "equals" => {
1577 if let Some(val) = value {
1578 let c_val = json_to_c(val);
1579 let _ = writeln!(
1580 out,
1581 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
1582 );
1583 }
1584 }
1585 "is_true" => {
1586 let _ = writeln!(
1587 out,
1588 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
1589 );
1590 }
1591 other_check => {
1592 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1593 }
1594 }
1595 let _ = writeln!(out, " ts_pack_free_string(_method_result);");
1596 let _ = writeln!(out, " }}");
1597 }
1598
1599 "contains_node_type" => match check {
1601 "equals" => {
1602 if let Some(val) = value {
1603 let c_val = json_to_c(val);
1604 let _ = writeln!(
1605 out,
1606 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
1607 );
1608 }
1609 }
1610 "is_true" => {
1611 let _ = writeln!(
1612 out,
1613 " assert({call_expr} && \"method_result is_true assertion failed\");"
1614 );
1615 }
1616 "is_false" => {
1617 let _ = writeln!(
1618 out,
1619 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
1620 );
1621 }
1622 other_check => {
1623 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1624 }
1625 },
1626
1627 "find_nodes_by_type" => {
1629 let _ = writeln!(out, " {{");
1630 let _ = writeln!(out, " char* _method_result = {call_expr};");
1631 let _ = writeln!(
1632 out,
1633 " assert(_method_result != NULL && \"method_result returned NULL\");"
1634 );
1635 match check {
1636 "count_min" => {
1637 if let Some(val) = value {
1638 let n = val.as_u64().unwrap_or(0);
1639 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1640 let _ = writeln!(
1641 out,
1642 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
1643 );
1644 }
1645 }
1646 "is_true" => {
1647 let _ = writeln!(
1648 out,
1649 " assert(alef_json_array_count(_method_result) > 0 && \"method_result is_true assertion failed\");"
1650 );
1651 }
1652 "is_false" => {
1653 let _ = writeln!(
1654 out,
1655 " assert(alef_json_array_count(_method_result) == 0 && \"method_result is_false assertion failed\");"
1656 );
1657 }
1658 "equals" => {
1659 if let Some(val) = value {
1660 let n = val.as_u64().unwrap_or(0);
1661 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1662 let _ = writeln!(
1663 out,
1664 " assert(_elem_count == {n} && \"method_result equals assertion failed\");"
1665 );
1666 }
1667 }
1668 "greater_than_or_equal" => {
1669 if let Some(val) = value {
1670 let n = val.as_u64().unwrap_or(0);
1671 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1672 let _ = writeln!(
1673 out,
1674 " assert(_elem_count >= {n} && \"method_result greater_than_or_equal assertion failed\");"
1675 );
1676 }
1677 }
1678 "contains" => {
1679 if let Some(val) = value {
1680 let c_val = json_to_c(val);
1681 let _ = writeln!(
1682 out,
1683 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1684 );
1685 }
1686 }
1687 other_check => {
1688 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1689 }
1690 }
1691 let _ = writeln!(out, " ts_pack_free_string(_method_result);");
1692 let _ = writeln!(out, " }}");
1693 }
1694
1695 "run_query" => {
1697 let _ = writeln!(out, " {{");
1698 let _ = writeln!(out, " char* _method_result = {call_expr};");
1699 if check == "is_error" {
1700 let _ = writeln!(
1701 out,
1702 " assert(_method_result == NULL && \"expected method to return error\");"
1703 );
1704 let _ = writeln!(out, " }}");
1705 return;
1706 }
1707 let _ = writeln!(
1708 out,
1709 " assert(_method_result != NULL && \"method_result returned NULL\");"
1710 );
1711 match check {
1712 "count_min" => {
1713 if let Some(val) = value {
1714 let n = val.as_u64().unwrap_or(0);
1715 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1716 let _ = writeln!(
1717 out,
1718 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
1719 );
1720 }
1721 }
1722 "is_true" => {
1723 let _ = writeln!(
1724 out,
1725 " assert(alef_json_array_count(_method_result) > 0 && \"method_result is_true assertion failed\");"
1726 );
1727 }
1728 "contains" => {
1729 if let Some(val) = value {
1730 let c_val = json_to_c(val);
1731 let _ = writeln!(
1732 out,
1733 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1734 );
1735 }
1736 }
1737 other_check => {
1738 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1739 }
1740 }
1741 let _ = writeln!(out, " ts_pack_free_string(_method_result);");
1742 let _ = writeln!(out, " }}");
1743 }
1744
1745 "root_node_type" => {
1747 let _ = writeln!(out, " {{");
1748 let _ = writeln!(
1749 out,
1750 " TS_PACKNodeInfo* _node_info = ts_pack_root_node_info({result_var});"
1751 );
1752 let _ = writeln!(
1753 out,
1754 " assert(_node_info != NULL && \"root_node_info returned NULL\");"
1755 );
1756 let _ = writeln!(out, " char* _node_json = ts_pack_node_info_to_json(_node_info);");
1757 let _ = writeln!(
1758 out,
1759 " assert(_node_json != NULL && \"node_info_to_json returned NULL\");"
1760 );
1761 let _ = writeln!(out, " char* _kind = alef_json_get_string(_node_json, \"kind\");");
1762 let _ = writeln!(
1763 out,
1764 " assert(_kind != NULL && \"kind field not found in NodeInfo JSON\");"
1765 );
1766 match check {
1767 "equals" => {
1768 if let Some(val) = value {
1769 let c_val = json_to_c(val);
1770 let _ = writeln!(
1771 out,
1772 " assert(strcmp(_kind, {c_val}) == 0 && \"method_result equals assertion failed\");"
1773 );
1774 }
1775 }
1776 "contains" => {
1777 if let Some(val) = value {
1778 let c_val = json_to_c(val);
1779 let _ = writeln!(
1780 out,
1781 " assert(strstr(_kind, {c_val}) != NULL && \"method_result contains assertion failed\");"
1782 );
1783 }
1784 }
1785 "is_true" => {
1786 let _ = writeln!(
1787 out,
1788 " assert(_kind != NULL && strlen(_kind) > 0 && \"method_result is_true assertion failed\");"
1789 );
1790 }
1791 other_check => {
1792 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1793 }
1794 }
1795 let _ = writeln!(out, " free(_kind);");
1796 let _ = writeln!(out, " ts_pack_free_string(_node_json);");
1797 let _ = writeln!(out, " ts_pack_node_info_free(_node_info);");
1798 let _ = writeln!(out, " }}");
1799 }
1800
1801 other_method => {
1802 panic!("C e2e generator: unsupported method_result method: {other_method}");
1803 }
1804 }
1805}
1806
1807fn build_c_method_call(result_var: &str, method_name: &str, args: Option<&serde_json::Value>) -> String {
1813 match method_name {
1814 "root_child_count" => {
1815 format!("ts_pack_node_info_named_child_count(ts_pack_root_node_info({result_var}))")
1817 }
1818 "has_error_nodes" => format!("ts_pack_tree_has_error_nodes({result_var})"),
1819 "error_count" | "tree_error_count" => format!("ts_pack_tree_error_count({result_var})"),
1820 "tree_to_sexp" => format!("ts_pack_tree_to_sexp({result_var})"),
1821 "named_children_count" => {
1822 format!("ts_pack_node_info_named_child_count(ts_pack_root_node_info({result_var}))")
1823 }
1824 "contains_node_type" => {
1825 let node_type = args
1826 .and_then(|a| a.get("node_type"))
1827 .and_then(|v| v.as_str())
1828 .unwrap_or("");
1829 format!("ts_pack_tree_contains_node_type({result_var}, \"{node_type}\")")
1830 }
1831 "find_nodes_by_type" => {
1832 let node_type = args
1833 .and_then(|a| a.get("node_type"))
1834 .and_then(|v| v.as_str())
1835 .unwrap_or("");
1836 format!("ts_pack_find_nodes_by_type({result_var}, \"{node_type}\")")
1837 }
1838 "run_query" => {
1839 let query_source = args
1840 .and_then(|a| a.get("query_source"))
1841 .and_then(|v| v.as_str())
1842 .unwrap_or("");
1843 let language = args
1844 .and_then(|a| a.get("language"))
1845 .and_then(|v| v.as_str())
1846 .unwrap_or("");
1847 format!("ts_pack_run_query({result_var}, \"{language}\", \"{query_source}\", NULL, 0)")
1849 }
1850 "root_node_type" => String::new(),
1852 other_method => format!("ts_pack_{other_method}({result_var})"),
1853 }
1854}
1855
1856fn json_to_c(value: &serde_json::Value) -> String {
1858 match value {
1859 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
1860 serde_json::Value::Bool(true) => "1".to_string(),
1861 serde_json::Value::Bool(false) => "0".to_string(),
1862 serde_json::Value::Number(n) => n.to_string(),
1863 serde_json::Value::Null => "NULL".to_string(),
1864 other => format!("\"{}\"", escape_c(&other.to_string())),
1865 }
1866}