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