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::AlefConfig;
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 alef_config: &AlefConfig,
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(|| alef_config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
46 .unwrap_or_default();
47 let header = overrides
48 .and_then(|o| o.header.as_ref())
49 .cloned()
50 .unwrap_or_else(|| format!("{}.h", call.module));
51
52 let c_pkg = e2e_config.resolve_package("c");
54 let lib_name = c_pkg
55 .as_ref()
56 .and_then(|p| p.name.as_ref())
57 .cloned()
58 .unwrap_or_else(|| call.module.clone());
59
60 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
62 .iter()
63 .filter_map(|group| {
64 let active: Vec<&Fixture> = group
65 .fixtures
66 .iter()
67 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
68 .filter(|f| f.visitor.is_none())
69 .collect();
70 if active.is_empty() { None } else { Some((group, active)) }
71 })
72 .collect();
73
74 let ffi_crate_path = c_pkg
79 .as_ref()
80 .and_then(|p| p.path.as_ref())
81 .cloned()
82 .unwrap_or_else(|| format!("../../crates/{}-ffi", alef_config.crate_config.name));
83
84 let category_names: Vec<String> = active_groups
86 .iter()
87 .map(|(g, _)| sanitize_filename(&g.category))
88 .collect();
89 files.push(GeneratedFile {
90 path: output_base.join("Makefile"),
91 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
92 generated_header: true,
93 });
94
95 let github_repo = alef_config.github_repo();
97 let version = alef_config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
98 let ffi_pkg_name = e2e_config
99 .registry
100 .packages
101 .get("c")
102 .and_then(|p| p.name.as_ref())
103 .cloned()
104 .unwrap_or_else(|| lib_name.clone());
105 files.push(GeneratedFile {
106 path: output_base.join("download_ffi.sh"),
107 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
108 generated_header: true,
109 });
110
111 files.push(GeneratedFile {
113 path: output_base.join("test_runner.h"),
114 content: render_test_runner_header(&active_groups),
115 generated_header: true,
116 });
117
118 files.push(GeneratedFile {
120 path: output_base.join("main.c"),
121 content: render_main_c(&active_groups),
122 generated_header: true,
123 });
124
125 let field_resolver = FieldResolver::new(
126 &e2e_config.fields,
127 &e2e_config.fields_optional,
128 &e2e_config.result_fields,
129 &e2e_config.fields_array,
130 );
131
132 for (group, active) in &active_groups {
136 let filename = format!("test_{}.c", sanitize_filename(&group.category));
137 let content = render_test_file(
138 &group.category,
139 active,
140 &header,
141 &prefix,
142 result_var,
143 e2e_config,
144 lang,
145 &field_resolver,
146 );
147 files.push(GeneratedFile {
148 path: output_base.join(filename),
149 content,
150 generated_header: true,
151 });
152 }
153
154 Ok(files)
155 }
156
157 fn language_name(&self) -> &'static str {
158 "c"
159 }
160}
161
162struct ResolvedCallInfo {
164 function_name: String,
165 result_type_name: String,
166 client_factory: Option<String>,
167 args: Vec<crate::config::ArgMapping>,
168}
169
170fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
171 let overrides = call.overrides.get(lang);
172 let function_name = overrides
173 .and_then(|o| o.function.as_ref())
174 .cloned()
175 .unwrap_or_else(|| call.function.clone());
176 let result_type_name = overrides
181 .and_then(|o| o.result_type.as_ref())
182 .cloned()
183 .unwrap_or_else(|| call.function.to_pascal_case());
184 let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
185 ResolvedCallInfo {
186 function_name,
187 result_type_name,
188 client_factory,
189 args: call.args.clone(),
190 }
191}
192
193fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
199 let call = e2e_config.resolve_call(fixture.call.as_deref());
200 let mut info = resolve_call_info(call, lang);
201
202 if info.client_factory.is_none() {
205 let default_overrides = e2e_config.call.overrides.get(lang);
206 if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
207 info.client_factory = Some(factory.clone());
208 }
209 }
210
211 info
212}
213
214fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
215 let mut out = String::new();
216 out.push_str(&hash::header(CommentStyle::Hash));
217 let _ = writeln!(out, "CC = gcc");
218 let _ = writeln!(out, "FFI_DIR = ffi");
219 let _ = writeln!(out);
220
221 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
223 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
224 let _ = writeln!(
225 out,
226 " LDFLAGS = -L$(FFI_DIR)/lib -l{lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
227 );
228 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
229 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
230 let _ = writeln!(
231 out,
232 " LDFLAGS = -L../../target/release -l{lib_name} -Wl,-rpath,../../target/release"
233 );
234 let _ = writeln!(out, "else");
235 let _ = writeln!(
236 out,
237 " CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
238 );
239 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
240 let _ = writeln!(out, "endif");
241 let _ = writeln!(out);
242
243 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
244 let srcs = src_files.join(" ");
245
246 let _ = writeln!(out, "SRCS = main.c {srcs}");
247 let _ = writeln!(out, "TARGET = run_tests");
248 let _ = writeln!(out);
249 let _ = writeln!(out, ".PHONY: all clean test");
250 let _ = writeln!(out);
251 let _ = writeln!(out, "all: $(TARGET)");
252 let _ = writeln!(out);
253 let _ = writeln!(out, "$(TARGET): $(SRCS)");
254 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
255 let _ = writeln!(out);
256 let _ = writeln!(out, "test: $(TARGET)");
257 let _ = writeln!(out, "\t./$(TARGET)");
258 let _ = writeln!(out);
259 let _ = writeln!(out, "clean:");
260 let _ = writeln!(out, "\trm -f $(TARGET)");
261 out
262}
263
264fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
265 let mut out = String::new();
266 let _ = writeln!(out, "#!/usr/bin/env bash");
267 out.push_str(&hash::header(CommentStyle::Hash));
268 let _ = writeln!(out, "set -euo pipefail");
269 let _ = writeln!(out);
270 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
271 let _ = writeln!(out, "VERSION=\"{version}\"");
272 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
273 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
274 let _ = writeln!(out);
275 let _ = writeln!(out, "# Detect OS and architecture.");
276 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
277 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
278 let _ = writeln!(out);
279 let _ = writeln!(out, "case \"$ARCH\" in");
280 let _ = writeln!(out, " x86_64|amd64) ARCH=\"x86_64\" ;;");
281 let _ = writeln!(out, " arm64|aarch64) ARCH=\"aarch64\" ;;");
282 let _ = writeln!(out, " *) echo \"Unsupported architecture: $ARCH\" >&2; exit 1 ;;");
283 let _ = writeln!(out, "esac");
284 let _ = writeln!(out);
285 let _ = writeln!(out, "case \"$OS\" in");
286 let _ = writeln!(out, " linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
287 let _ = writeln!(out, " darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
288 let _ = writeln!(out, " *) echo \"Unsupported OS: $OS\" >&2; exit 1 ;;");
289 let _ = writeln!(out, "esac");
290 let _ = writeln!(out);
291 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
292 let _ = writeln!(
293 out,
294 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
295 );
296 let _ = writeln!(out);
297 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
298 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
299 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
300 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
301 out
302}
303
304fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
305 let mut out = String::new();
306 out.push_str(&hash::header(CommentStyle::Block));
307 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
308 let _ = writeln!(out, "#define TEST_RUNNER_H");
309 let _ = writeln!(out);
310 let _ = writeln!(out, "#include <string.h>");
311 let _ = writeln!(out, "#include <stdlib.h>");
312 let _ = writeln!(out);
313 let _ = writeln!(out, "/**");
315 let _ = writeln!(
316 out,
317 " * Compare a string against an expected value, trimming trailing whitespace."
318 );
319 let _ = writeln!(
320 out,
321 " * Returns 0 if the trimmed actual string equals the expected string."
322 );
323 let _ = writeln!(out, " */");
324 let _ = writeln!(
325 out,
326 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
327 );
328 let _ = writeln!(
329 out,
330 " if (actual == NULL || expected == NULL) return actual != expected;"
331 );
332 let _ = writeln!(out, " size_t alen = strlen(actual);");
333 let _ = writeln!(
334 out,
335 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
336 );
337 let _ = writeln!(out, " size_t elen = strlen(expected);");
338 let _ = writeln!(out, " if (alen != elen) return 1;");
339 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
340 let _ = writeln!(out, "}}");
341 let _ = writeln!(out);
342
343 let _ = writeln!(out, "/**");
344 let _ = writeln!(
345 out,
346 " * Extract a string value for a given key from a JSON object string."
347 );
348 let _ = writeln!(
349 out,
350 " * Returns a heap-allocated copy of the value, or NULL if not found."
351 );
352 let _ = writeln!(out, " * Caller must free() the returned string.");
353 let _ = writeln!(out, " */");
354 let _ = writeln!(
355 out,
356 "static inline char *alef_json_get_string(const char *json, const char *key) {{"
357 );
358 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
359 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
360 let _ = writeln!(out, " size_t key_len = strlen(key);");
361 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
362 let _ = writeln!(out, " if (!pattern) return NULL;");
363 let _ = writeln!(out, " pattern[0] = '\"';");
364 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
365 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
366 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
367 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
368 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
369 let _ = writeln!(out, " free(pattern);");
370 let _ = writeln!(out, " if (!found) return NULL;");
371 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
372 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
373 let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
374 let _ = writeln!(out, " found++; /* skip opening quote */");
375 let _ = writeln!(out, " const char *end = found;");
376 let _ = writeln!(out, " while (*end && *end != '\"') {{");
377 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
378 let _ = writeln!(out, " else end++;");
379 let _ = writeln!(out, " }}");
380 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
381 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
382 let _ = writeln!(out, " if (!result_str) return NULL;");
383 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
384 let _ = writeln!(out, " result_str[val_len] = '\\0';");
385 let _ = writeln!(out, " return result_str;");
386 let _ = writeln!(out, "}}");
387 let _ = writeln!(out);
388 let _ = writeln!(out, "/**");
389 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
390 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
391 let _ = writeln!(out, " */");
392 let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
393 let _ = writeln!(out, " if (json == NULL) return 0;");
394 let _ = writeln!(out, " /* Skip leading whitespace */");
395 let _ = writeln!(
396 out,
397 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
398 );
399 let _ = writeln!(out, " if (*json != '[') return 0;");
400 let _ = writeln!(out, " json++;");
401 let _ = writeln!(out, " /* Skip whitespace after '[' */");
402 let _ = writeln!(
403 out,
404 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
405 );
406 let _ = writeln!(out, " if (*json == ']') return 0;");
407 let _ = writeln!(out, " int count = 1;");
408 let _ = writeln!(out, " int depth = 0;");
409 let _ = writeln!(out, " int in_string = 0;");
410 let _ = writeln!(
411 out,
412 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
413 );
414 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
415 let _ = writeln!(
416 out,
417 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
418 );
419 let _ = writeln!(out, " if (in_string) continue;");
420 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
421 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
422 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
423 let _ = writeln!(out, " }}");
424 let _ = writeln!(out, " return count;");
425 let _ = writeln!(out, "}}");
426 let _ = writeln!(out);
427
428 for (group, fixtures) in active_groups {
429 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
430 for fixture in fixtures {
431 let fn_name = sanitize_ident(&fixture.id);
432 let _ = writeln!(out, "void test_{fn_name}(void);");
433 }
434 let _ = writeln!(out);
435 }
436
437 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
438 out
439}
440
441fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
442 let mut out = String::new();
443 out.push_str(&hash::header(CommentStyle::Block));
444 let _ = writeln!(out, "#include <stdio.h>");
445 let _ = writeln!(out, "#include \"test_runner.h\"");
446 let _ = writeln!(out);
447 let _ = writeln!(out, "int main(void) {{");
448 let _ = writeln!(out, " int passed = 0;");
449 let _ = writeln!(out, " int failed = 0;");
450 let _ = writeln!(out);
451
452 for (group, fixtures) in active_groups {
453 let _ = writeln!(out, " /* Category: {} */", group.category);
454 for fixture in fixtures {
455 let fn_name = sanitize_ident(&fixture.id);
456 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
457 let _ = writeln!(out, " test_{fn_name}();");
458 let _ = writeln!(out, " printf(\" PASSED\\n\");");
459 let _ = writeln!(out, " passed++;");
460 }
461 let _ = writeln!(out);
462 }
463
464 let _ = writeln!(
465 out,
466 " printf(\"\\nResults: %d passed, %d failed\\n\", passed, failed);"
467 );
468 let _ = writeln!(out, " return failed > 0 ? 1 : 0;");
469 let _ = writeln!(out, "}}");
470 out
471}
472
473#[allow(clippy::too_many_arguments)]
474fn render_test_file(
475 category: &str,
476 fixtures: &[&Fixture],
477 header: &str,
478 prefix: &str,
479 result_var: &str,
480 e2e_config: &E2eConfig,
481 lang: &str,
482 field_resolver: &FieldResolver,
483) -> String {
484 let mut out = String::new();
485 out.push_str(&hash::header(CommentStyle::Block));
486 let _ = writeln!(out, "/* E2e tests for category: {category} */");
487 let _ = writeln!(out);
488 let _ = writeln!(out, "#include <assert.h>");
489 let _ = writeln!(out, "#include <string.h>");
490 let _ = writeln!(out, "#include <stdio.h>");
491 let _ = writeln!(out, "#include <stdlib.h>");
492 let _ = writeln!(out, "#include \"{header}\"");
493 let _ = writeln!(out, "#include \"test_runner.h\"");
494 let _ = writeln!(out);
495
496 for (i, fixture) in fixtures.iter().enumerate() {
497 if fixture.visitor.is_some() {
500 panic!(
501 "C e2e generator: visitor pattern not supported for fixture: {}",
502 fixture.id
503 );
504 }
505
506 let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
507 render_test_function(
508 &mut out,
509 fixture,
510 prefix,
511 &call_info.function_name,
512 result_var,
513 &call_info.args,
514 field_resolver,
515 &e2e_config.fields_c_types,
516 &call_info.result_type_name,
517 call_info.client_factory.as_deref(),
518 );
519 if i + 1 < fixtures.len() {
520 let _ = writeln!(out);
521 }
522 }
523
524 out
525}
526
527#[allow(clippy::too_many_arguments)]
528fn render_test_function(
529 out: &mut String,
530 fixture: &Fixture,
531 prefix: &str,
532 function_name: &str,
533 result_var: &str,
534 args: &[crate::config::ArgMapping],
535 field_resolver: &FieldResolver,
536 fields_c_types: &HashMap<String, String>,
537 result_type_name: &str,
538 client_factory: Option<&str>,
539) {
540 let fn_name = sanitize_ident(&fixture.id);
541 let description = &fixture.description;
542
543 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
544
545 let _ = writeln!(out, "void test_{fn_name}(void) {{");
546 let _ = writeln!(out, " /* {description} */");
547
548 let prefix_upper = prefix.to_uppercase();
549
550 if let Some(factory) = client_factory {
555 let mut request_handle_vars: Vec<(String, String)> = Vec::new(); for arg in args {
558 if arg.arg_type == "json_object" {
559 let request_type_pascal = if let Some(stripped) = result_type_name.strip_suffix("Response") {
562 format!("{}Request", stripped)
563 } else {
564 format!("{result_type_name}Request")
565 };
566 let request_type_snake = request_type_pascal.to_snake_case();
567 let var_name = format!("{request_type_snake}_handle");
568
569 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
570 let json_val = if field.is_empty() || field == "input" {
571 Some(&fixture.input)
572 } else {
573 fixture.input.get(field)
574 };
575
576 if let Some(val) = json_val {
577 if !val.is_null() {
578 let normalized = super::normalize_json_keys_to_snake_case(val);
579 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
580 let escaped = escape_c(&json_str);
581 let _ = writeln!(
582 out,
583 " {prefix_upper}{request_type_pascal}* {var_name} = \
584 {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
585 );
586 let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
587 request_handle_vars.push((arg.name.clone(), var_name));
588 }
589 }
590 }
591 }
592
593 let _ = writeln!(
594 out,
595 " {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
596 );
597 let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
598
599 let method_args = if request_handle_vars.is_empty() {
600 String::new()
601 } else {
602 let handles: Vec<&str> = request_handle_vars.iter().map(|(_, v)| v.as_str()).collect();
603 format!(", {}", handles.join(", "))
604 };
605
606 let call_fn = format!("{prefix}_default_client_{function_name}");
607
608 if expects_error {
609 let _ = writeln!(
610 out,
611 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
612 );
613 for (_, var_name) in &request_handle_vars {
614 let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
615 let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
616 }
617 let _ = writeln!(out, " {prefix}_default_client_free(client);");
618 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
619 let _ = writeln!(out, "}}");
620 return;
621 }
622
623 let _ = writeln!(
624 out,
625 " {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
626 );
627 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
628
629 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
630 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
631
632 for assertion in &fixture.assertions {
633 if let Some(f) = &assertion.field {
634 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
635 let resolved = field_resolver.resolve(f);
636 let local_var = f.replace(['.', '['], "_").replace(']', "");
637 let has_map_access = resolved.contains('[');
638 if resolved.contains('.') {
639 emit_nested_accessor(
640 out,
641 prefix,
642 resolved,
643 &local_var,
644 result_var,
645 fields_c_types,
646 &mut intermediate_handles,
647 result_type_name,
648 );
649 } else {
650 let result_type_snake = result_type_name.to_snake_case();
651 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
652 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
653 }
654 accessed_fields.push((f.clone(), local_var, has_map_access));
655 }
656 }
657 }
658
659 for assertion in &fixture.assertions {
660 render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
661 }
662
663 for (_f, local_var, from_json) in &accessed_fields {
664 if *from_json {
665 let _ = writeln!(out, " free({local_var});");
666 } else {
667 let _ = writeln!(out, " {prefix}_free_string({local_var});");
668 }
669 }
670 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
671 if snake_type == "free_string" {
672 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
673 } else {
674 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
675 }
676 }
677 let result_type_snake = result_type_name.to_snake_case();
678 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
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, "}}");
685 return;
686 }
687
688 let prefixed_fn = function_name.to_string();
694
695 let mut has_options_handle = false;
697 for arg in args {
698 if arg.arg_type == "json_object" {
699 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
700 if let Some(val) = fixture.input.get(field) {
701 if !val.is_null() {
702 let normalized = super::normalize_json_keys_to_snake_case(val);
706 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
707 let escaped = escape_c(&json_str);
708 let upper = prefix.to_uppercase();
709 let _ = writeln!(
710 out,
711 " {upper}ConversionOptions* options_handle = {prefix}_conversion_options_from_json(\"{escaped}\");"
712 );
713 has_options_handle = true;
714 }
715 }
716 }
717 }
718
719 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
720
721 if expects_error {
722 let _ = writeln!(
723 out,
724 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
725 );
726 if has_options_handle {
727 let _ = writeln!(out, " {prefix}_conversion_options_free(options_handle);");
728 }
729 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
730 let _ = writeln!(out, "}}");
731 return;
732 }
733
734 let _ = writeln!(
736 out,
737 " {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
738 );
739 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
740
741 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
749 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
752
753 for assertion in &fixture.assertions {
754 if let Some(f) = &assertion.field {
755 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
756 let resolved = field_resolver.resolve(f);
757 let local_var = f.replace(['.', '['], "_").replace(']', "");
758 let has_map_access = resolved.contains('[');
759
760 if resolved.contains('.') {
761 emit_nested_accessor(
762 out,
763 prefix,
764 resolved,
765 &local_var,
766 result_var,
767 fields_c_types,
768 &mut intermediate_handles,
769 result_type_name,
770 );
771 } else {
772 let result_type_snake = result_type_name.to_snake_case();
773 let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
774 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
775 }
776 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
777 }
778 }
779 }
780
781 for assertion in &fixture.assertions {
782 render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
783 }
784
785 for (_f, local_var, from_json) in &accessed_fields {
787 if *from_json {
788 let _ = writeln!(out, " free({local_var});");
789 } else {
790 let _ = writeln!(out, " {prefix}_free_string({local_var});");
791 }
792 }
793 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
795 if snake_type == "free_string" {
796 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
798 } else {
799 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
800 }
801 }
802 if has_options_handle {
803 let _ = writeln!(out, " {prefix}_conversion_options_free(options_handle);");
804 }
805 let result_type_snake = result_type_name.to_snake_case();
806 let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
807 let _ = writeln!(out, "}}");
808}
809
810#[allow(clippy::too_many_arguments)]
824fn emit_nested_accessor(
825 out: &mut String,
826 prefix: &str,
827 resolved: &str,
828 local_var: &str,
829 result_var: &str,
830 fields_c_types: &HashMap<String, String>,
831 intermediate_handles: &mut Vec<(String, String)>,
832 result_type_name: &str,
833) {
834 let segments: Vec<&str> = resolved.split('.').collect();
835 let prefix_upper = prefix.to_uppercase();
836
837 let mut current_snake_type = result_type_name.to_snake_case();
839 let mut current_handle = result_var.to_string();
840
841 for (i, segment) in segments.iter().enumerate() {
842 let is_leaf = i + 1 == segments.len();
843
844 if let Some(bracket_pos) = segment.find('[') {
846 let field_name = &segment[..bracket_pos];
847 let key = segment[bracket_pos + 1..].trim_end_matches(']');
848 let field_snake = field_name.to_snake_case();
849 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
850
851 let json_var = format!("{field_snake}_json");
854 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
855 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
856 let _ = writeln!(out, " assert({json_var} != NULL);");
857 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
859 }
860 let _ = writeln!(
862 out,
863 " char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
864 );
865 return; }
867
868 let seg_snake = segment.to_snake_case();
869 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
870
871 if is_leaf {
872 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
874 } else {
875 let lookup_key = format!("{current_snake_type}.{seg_snake}");
877 let return_type_pascal = match fields_c_types.get(&lookup_key) {
878 Some(t) => t.clone(),
879 None => {
880 segment.to_pascal_case()
882 }
883 };
884 let return_snake = return_type_pascal.to_snake_case();
885 let handle_var = format!("{seg_snake}_handle");
886
887 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
890 let _ = writeln!(
891 out,
892 " {prefix_upper}{return_type_pascal}* {handle_var} = \
893 {accessor_fn}({current_handle});"
894 );
895 let _ = writeln!(out, " assert({handle_var} != NULL);");
896 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
897 }
898
899 current_snake_type = return_snake;
900 current_handle = handle_var;
901 }
902 }
903}
904
905fn build_args_string_c(
909 input: &serde_json::Value,
910 args: &[crate::config::ArgMapping],
911 has_options_handle: bool,
912) -> String {
913 if args.is_empty() {
914 return json_to_c(input);
915 }
916
917 let parts: Vec<String> = args
918 .iter()
919 .filter_map(|arg| {
920 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
921 let val = input.get(field);
922 match val {
923 None if arg.optional => Some("NULL".to_string()),
925 None => None,
927 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
929 Some(v) => {
930 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
933 Some("options_handle".to_string())
934 } else {
935 Some(json_to_c(v))
936 }
937 }
938 }
939 })
940 .collect();
941
942 parts.join(", ")
943}
944
945fn render_assertion(
946 out: &mut String,
947 assertion: &Assertion,
948 result_var: &str,
949 _field_resolver: &FieldResolver,
950 accessed_fields: &[(String, String, bool)],
951) {
952 if let Some(f) = &assertion.field {
954 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
955 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
956 return;
957 }
958 }
959
960 let field_expr = match &assertion.field {
961 Some(f) if !f.is_empty() => {
962 accessed_fields
964 .iter()
965 .find(|(k, _, _)| k == f)
966 .map(|(_, local, _)| local.clone())
967 .unwrap_or_else(|| result_var.to_string())
968 }
969 _ => result_var.to_string(),
970 };
971
972 match assertion.assertion_type.as_str() {
973 "equals" => {
974 if let Some(expected) = &assertion.value {
975 let c_val = json_to_c(expected);
976 if expected.is_string() {
977 let _ = writeln!(
979 out,
980 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
981 );
982 } else {
983 let _ = writeln!(
984 out,
985 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
986 );
987 }
988 }
989 }
990 "contains" => {
991 if let Some(expected) = &assertion.value {
992 let c_val = json_to_c(expected);
993 let _ = writeln!(
994 out,
995 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
996 );
997 }
998 }
999 "contains_all" => {
1000 if let Some(values) = &assertion.values {
1001 for val in values {
1002 let c_val = json_to_c(val);
1003 let _ = writeln!(
1004 out,
1005 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
1006 );
1007 }
1008 }
1009 }
1010 "not_contains" => {
1011 if let Some(expected) = &assertion.value {
1012 let c_val = json_to_c(expected);
1013 let _ = writeln!(
1014 out,
1015 " assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1016 );
1017 }
1018 }
1019 "not_empty" => {
1020 let _ = writeln!(
1021 out,
1022 " assert(strlen({field_expr}) > 0 && \"expected non-empty value\");"
1023 );
1024 }
1025 "is_empty" => {
1026 let _ = writeln!(
1027 out,
1028 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
1029 );
1030 }
1031 "contains_any" => {
1032 if let Some(values) = &assertion.values {
1033 let _ = writeln!(out, " {{");
1034 let _ = writeln!(out, " int found = 0;");
1035 for val in values {
1036 let c_val = json_to_c(val);
1037 let _ = writeln!(
1038 out,
1039 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
1040 );
1041 }
1042 let _ = writeln!(
1043 out,
1044 " assert(found && \"expected to contain at least one of the specified values\");"
1045 );
1046 let _ = writeln!(out, " }}");
1047 }
1048 }
1049 "greater_than" => {
1050 if let Some(val) = &assertion.value {
1051 let c_val = json_to_c(val);
1052 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
1053 }
1054 }
1055 "less_than" => {
1056 if let Some(val) = &assertion.value {
1057 let c_val = json_to_c(val);
1058 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
1059 }
1060 }
1061 "greater_than_or_equal" => {
1062 if let Some(val) = &assertion.value {
1063 let c_val = json_to_c(val);
1064 let _ = writeln!(
1065 out,
1066 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
1067 );
1068 }
1069 }
1070 "less_than_or_equal" => {
1071 if let Some(val) = &assertion.value {
1072 let c_val = json_to_c(val);
1073 let _ = writeln!(
1074 out,
1075 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
1076 );
1077 }
1078 }
1079 "starts_with" => {
1080 if let Some(expected) = &assertion.value {
1081 let c_val = json_to_c(expected);
1082 let _ = writeln!(
1083 out,
1084 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1085 );
1086 }
1087 }
1088 "ends_with" => {
1089 if let Some(expected) = &assertion.value {
1090 let c_val = json_to_c(expected);
1091 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
1092 let _ = writeln!(
1093 out,
1094 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
1095 );
1096 }
1097 }
1098 "min_length" => {
1099 if let Some(val) = &assertion.value {
1100 if let Some(n) = val.as_u64() {
1101 let _ = writeln!(
1102 out,
1103 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
1104 );
1105 }
1106 }
1107 }
1108 "max_length" => {
1109 if let Some(val) = &assertion.value {
1110 if let Some(n) = val.as_u64() {
1111 let _ = writeln!(
1112 out,
1113 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
1114 );
1115 }
1116 }
1117 }
1118 "count_min" => {
1119 if let Some(val) = &assertion.value {
1120 if let Some(n) = val.as_u64() {
1121 let _ = writeln!(out, " {{");
1122 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
1123 let _ = writeln!(
1124 out,
1125 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1126 );
1127 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1128 let _ = writeln!(
1129 out,
1130 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
1131 );
1132 let _ = writeln!(out, " }}");
1133 }
1134 }
1135 }
1136 "count_equals" => {
1137 if let Some(val) = &assertion.value {
1138 if let Some(n) = val.as_u64() {
1139 let _ = writeln!(out, " {{");
1140 let _ = writeln!(out, " /* count_equals: count elements in array */");
1141 let _ = writeln!(
1142 out,
1143 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
1144 );
1145 let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
1146 let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
1147 let _ = writeln!(out, " }}");
1148 }
1149 }
1150 }
1151 "is_true" => {
1152 let _ = writeln!(out, " assert({field_expr});");
1153 }
1154 "is_false" => {
1155 let _ = writeln!(out, " assert(!{field_expr});");
1156 }
1157 "method_result" => {
1158 if let Some(method_name) = &assertion.method {
1159 render_method_result_assertion(
1160 out,
1161 result_var,
1162 method_name,
1163 assertion.args.as_ref(),
1164 assertion.check.as_deref().unwrap_or("is_true"),
1165 assertion.value.as_ref(),
1166 );
1167 } else {
1168 panic!("C e2e generator: method_result assertion missing 'method' field");
1169 }
1170 }
1171 "matches_regex" => {
1172 if let Some(expected) = &assertion.value {
1173 let c_val = json_to_c(expected);
1174 let _ = writeln!(out, " {{");
1175 let _ = writeln!(out, " regex_t _re;");
1176 let _ = writeln!(
1177 out,
1178 " assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
1179 );
1180 let _ = writeln!(
1181 out,
1182 " assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
1183 );
1184 let _ = writeln!(out, " regfree(&_re);");
1185 let _ = writeln!(out, " }}");
1186 }
1187 }
1188 "not_error" => {
1189 }
1191 "error" => {
1192 }
1194 other => {
1195 panic!("C e2e generator: unsupported assertion type: {other}");
1196 }
1197 }
1198}
1199
1200fn render_method_result_assertion(
1205 out: &mut String,
1206 result_var: &str,
1207 method_name: &str,
1208 args: Option<&serde_json::Value>,
1209 check: &str,
1210 value: Option<&serde_json::Value>,
1211) {
1212 let call_expr = build_c_method_call(result_var, method_name, args);
1213
1214 match method_name {
1215 "has_error_nodes" | "error_count" | "tree_error_count" => match check {
1217 "equals" => {
1218 if let Some(val) = value {
1219 let c_val = json_to_c(val);
1220 let _ = writeln!(
1221 out,
1222 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
1223 );
1224 }
1225 }
1226 "is_true" => {
1227 let _ = writeln!(
1228 out,
1229 " assert({call_expr} && \"method_result is_true assertion failed\");"
1230 );
1231 }
1232 "is_false" => {
1233 let _ = writeln!(
1234 out,
1235 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
1236 );
1237 }
1238 "greater_than_or_equal" => {
1239 if let Some(val) = value {
1240 let n = val.as_u64().unwrap_or(0);
1241 let _ = writeln!(
1242 out,
1243 " assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
1244 );
1245 }
1246 }
1247 other_check => {
1248 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1249 }
1250 },
1251
1252 "root_child_count" | "named_children_count" => {
1254 let _ = writeln!(out, " {{");
1255 let _ = writeln!(
1256 out,
1257 " TS_PACKNodeInfo* _node_info = ts_pack_root_node_info({result_var});"
1258 );
1259 let _ = writeln!(
1260 out,
1261 " assert(_node_info != NULL && \"root_node_info returned NULL\");"
1262 );
1263 let _ = writeln!(
1264 out,
1265 " size_t _count = ts_pack_node_info_named_child_count(_node_info);"
1266 );
1267 let _ = writeln!(out, " ts_pack_node_info_free(_node_info);");
1268 match check {
1269 "equals" => {
1270 if let Some(val) = value {
1271 let c_val = json_to_c(val);
1272 let _ = writeln!(
1273 out,
1274 " assert(_count == (size_t){c_val} && \"method_result equals assertion failed\");"
1275 );
1276 }
1277 }
1278 "greater_than_or_equal" => {
1279 if let Some(val) = value {
1280 let n = val.as_u64().unwrap_or(0);
1281 let _ = writeln!(
1282 out,
1283 " assert(_count >= {n} && \"method_result >= {n} assertion failed\");"
1284 );
1285 }
1286 }
1287 "is_true" => {
1288 let _ = writeln!(
1289 out,
1290 " assert(_count > 0 && \"method_result is_true assertion failed\");"
1291 );
1292 }
1293 other_check => {
1294 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1295 }
1296 }
1297 let _ = writeln!(out, " }}");
1298 }
1299
1300 "tree_to_sexp" => {
1302 let _ = writeln!(out, " {{");
1303 let _ = writeln!(out, " char* _method_result = {call_expr};");
1304 let _ = writeln!(
1305 out,
1306 " assert(_method_result != NULL && \"method_result returned NULL\");"
1307 );
1308 match check {
1309 "contains" => {
1310 if let Some(val) = value {
1311 let c_val = json_to_c(val);
1312 let _ = writeln!(
1313 out,
1314 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1315 );
1316 }
1317 }
1318 "equals" => {
1319 if let Some(val) = value {
1320 let c_val = json_to_c(val);
1321 let _ = writeln!(
1322 out,
1323 " assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
1324 );
1325 }
1326 }
1327 "is_true" => {
1328 let _ = writeln!(
1329 out,
1330 " assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
1331 );
1332 }
1333 other_check => {
1334 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1335 }
1336 }
1337 let _ = writeln!(out, " ts_pack_free_string(_method_result);");
1338 let _ = writeln!(out, " }}");
1339 }
1340
1341 "contains_node_type" => match check {
1343 "equals" => {
1344 if let Some(val) = value {
1345 let c_val = json_to_c(val);
1346 let _ = writeln!(
1347 out,
1348 " assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
1349 );
1350 }
1351 }
1352 "is_true" => {
1353 let _ = writeln!(
1354 out,
1355 " assert({call_expr} && \"method_result is_true assertion failed\");"
1356 );
1357 }
1358 "is_false" => {
1359 let _ = writeln!(
1360 out,
1361 " assert(!{call_expr} && \"method_result is_false assertion failed\");"
1362 );
1363 }
1364 other_check => {
1365 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1366 }
1367 },
1368
1369 "find_nodes_by_type" => {
1371 let _ = writeln!(out, " {{");
1372 let _ = writeln!(out, " char* _method_result = {call_expr};");
1373 let _ = writeln!(
1374 out,
1375 " assert(_method_result != NULL && \"method_result returned NULL\");"
1376 );
1377 match check {
1378 "count_min" => {
1379 if let Some(val) = value {
1380 let n = val.as_u64().unwrap_or(0);
1381 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1382 let _ = writeln!(
1383 out,
1384 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
1385 );
1386 }
1387 }
1388 "is_true" => {
1389 let _ = writeln!(
1390 out,
1391 " assert(alef_json_array_count(_method_result) > 0 && \"method_result is_true assertion failed\");"
1392 );
1393 }
1394 "is_false" => {
1395 let _ = writeln!(
1396 out,
1397 " assert(alef_json_array_count(_method_result) == 0 && \"method_result is_false assertion failed\");"
1398 );
1399 }
1400 "equals" => {
1401 if let Some(val) = value {
1402 let n = val.as_u64().unwrap_or(0);
1403 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1404 let _ = writeln!(
1405 out,
1406 " assert(_elem_count == {n} && \"method_result equals assertion failed\");"
1407 );
1408 }
1409 }
1410 "greater_than_or_equal" => {
1411 if let Some(val) = value {
1412 let n = val.as_u64().unwrap_or(0);
1413 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1414 let _ = writeln!(
1415 out,
1416 " assert(_elem_count >= {n} && \"method_result greater_than_or_equal assertion failed\");"
1417 );
1418 }
1419 }
1420 "contains" => {
1421 if let Some(val) = value {
1422 let c_val = json_to_c(val);
1423 let _ = writeln!(
1424 out,
1425 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1426 );
1427 }
1428 }
1429 other_check => {
1430 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1431 }
1432 }
1433 let _ = writeln!(out, " ts_pack_free_string(_method_result);");
1434 let _ = writeln!(out, " }}");
1435 }
1436
1437 "run_query" => {
1439 let _ = writeln!(out, " {{");
1440 let _ = writeln!(out, " char* _method_result = {call_expr};");
1441 if check == "is_error" {
1442 let _ = writeln!(
1443 out,
1444 " assert(_method_result == NULL && \"expected method to return error\");"
1445 );
1446 let _ = writeln!(out, " }}");
1447 return;
1448 }
1449 let _ = writeln!(
1450 out,
1451 " assert(_method_result != NULL && \"method_result returned NULL\");"
1452 );
1453 match check {
1454 "count_min" => {
1455 if let Some(val) = value {
1456 let n = val.as_u64().unwrap_or(0);
1457 let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
1458 let _ = writeln!(
1459 out,
1460 " assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
1461 );
1462 }
1463 }
1464 "is_true" => {
1465 let _ = writeln!(
1466 out,
1467 " assert(alef_json_array_count(_method_result) > 0 && \"method_result is_true assertion failed\");"
1468 );
1469 }
1470 "contains" => {
1471 if let Some(val) = value {
1472 let c_val = json_to_c(val);
1473 let _ = writeln!(
1474 out,
1475 " assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
1476 );
1477 }
1478 }
1479 other_check => {
1480 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1481 }
1482 }
1483 let _ = writeln!(out, " ts_pack_free_string(_method_result);");
1484 let _ = writeln!(out, " }}");
1485 }
1486
1487 "root_node_type" => {
1489 let _ = writeln!(out, " {{");
1490 let _ = writeln!(
1491 out,
1492 " TS_PACKNodeInfo* _node_info = ts_pack_root_node_info({result_var});"
1493 );
1494 let _ = writeln!(
1495 out,
1496 " assert(_node_info != NULL && \"root_node_info returned NULL\");"
1497 );
1498 let _ = writeln!(out, " char* _node_json = ts_pack_node_info_to_json(_node_info);");
1499 let _ = writeln!(
1500 out,
1501 " assert(_node_json != NULL && \"node_info_to_json returned NULL\");"
1502 );
1503 let _ = writeln!(out, " char* _kind = alef_json_get_string(_node_json, \"kind\");");
1504 let _ = writeln!(
1505 out,
1506 " assert(_kind != NULL && \"kind field not found in NodeInfo JSON\");"
1507 );
1508 match check {
1509 "equals" => {
1510 if let Some(val) = value {
1511 let c_val = json_to_c(val);
1512 let _ = writeln!(
1513 out,
1514 " assert(strcmp(_kind, {c_val}) == 0 && \"method_result equals assertion failed\");"
1515 );
1516 }
1517 }
1518 "contains" => {
1519 if let Some(val) = value {
1520 let c_val = json_to_c(val);
1521 let _ = writeln!(
1522 out,
1523 " assert(strstr(_kind, {c_val}) != NULL && \"method_result contains assertion failed\");"
1524 );
1525 }
1526 }
1527 "is_true" => {
1528 let _ = writeln!(
1529 out,
1530 " assert(_kind != NULL && strlen(_kind) > 0 && \"method_result is_true assertion failed\");"
1531 );
1532 }
1533 other_check => {
1534 panic!("C e2e generator: unsupported method_result check type: {other_check}");
1535 }
1536 }
1537 let _ = writeln!(out, " free(_kind);");
1538 let _ = writeln!(out, " ts_pack_free_string(_node_json);");
1539 let _ = writeln!(out, " ts_pack_node_info_free(_node_info);");
1540 let _ = writeln!(out, " }}");
1541 }
1542
1543 other_method => {
1544 panic!("C e2e generator: unsupported method_result method: {other_method}");
1545 }
1546 }
1547}
1548
1549fn build_c_method_call(result_var: &str, method_name: &str, args: Option<&serde_json::Value>) -> String {
1555 match method_name {
1556 "root_child_count" => {
1557 format!("ts_pack_node_info_named_child_count(ts_pack_root_node_info({result_var}))")
1559 }
1560 "has_error_nodes" => format!("ts_pack_tree_has_error_nodes({result_var})"),
1561 "error_count" | "tree_error_count" => format!("ts_pack_tree_error_count({result_var})"),
1562 "tree_to_sexp" => format!("ts_pack_tree_to_sexp({result_var})"),
1563 "named_children_count" => {
1564 format!("ts_pack_node_info_named_child_count(ts_pack_root_node_info({result_var}))")
1565 }
1566 "contains_node_type" => {
1567 let node_type = args
1568 .and_then(|a| a.get("node_type"))
1569 .and_then(|v| v.as_str())
1570 .unwrap_or("");
1571 format!("ts_pack_tree_contains_node_type({result_var}, \"{node_type}\")")
1572 }
1573 "find_nodes_by_type" => {
1574 let node_type = args
1575 .and_then(|a| a.get("node_type"))
1576 .and_then(|v| v.as_str())
1577 .unwrap_or("");
1578 format!("ts_pack_find_nodes_by_type({result_var}, \"{node_type}\")")
1579 }
1580 "run_query" => {
1581 let query_source = args
1582 .and_then(|a| a.get("query_source"))
1583 .and_then(|v| v.as_str())
1584 .unwrap_or("");
1585 let language = args
1586 .and_then(|a| a.get("language"))
1587 .and_then(|v| v.as_str())
1588 .unwrap_or("");
1589 format!("ts_pack_run_query({result_var}, \"{language}\", \"{query_source}\", NULL, 0)")
1591 }
1592 "root_node_type" => String::new(),
1594 other_method => format!("ts_pack_{other_method}({result_var})"),
1595 }
1596}
1597
1598fn json_to_c(value: &serde_json::Value) -> String {
1600 match value {
1601 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
1602 serde_json::Value::Bool(true) => "1".to_string(),
1603 serde_json::Value::Bool(false) => "0".to_string(),
1604 serde_json::Value::Number(n) => n.to_string(),
1605 serde_json::Value::Null => "NULL".to_string(),
1606 other => format!("\"{}\"", escape_c(&other.to_string())),
1607 }
1608}