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