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