1use crate::config::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 function_name = overrides
41 .and_then(|o| o.function.as_ref())
42 .cloned()
43 .unwrap_or_else(|| call.function.clone());
44 let result_var = &call.result_var;
45 let prefix = overrides.and_then(|o| o.prefix.as_ref()).cloned().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 .collect();
68 if active.is_empty() { None } else { Some((group, active)) }
69 })
70 .collect();
71
72 let ffi_crate_path = c_pkg
74 .as_ref()
75 .and_then(|p| p.path.as_ref())
76 .cloned()
77 .unwrap_or_else(|| "../../crates/ffi".to_string());
78
79 let category_names: Vec<String> = active_groups
81 .iter()
82 .map(|(g, _)| sanitize_filename(&g.category))
83 .collect();
84 files.push(GeneratedFile {
85 path: output_base.join("Makefile"),
86 content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
87 generated_header: true,
88 });
89
90 let github_repo = alef_config.github_repo();
92 let version = alef_config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
93 let ffi_pkg_name = e2e_config
94 .registry
95 .packages
96 .get("c")
97 .and_then(|p| p.name.as_ref())
98 .cloned()
99 .unwrap_or_else(|| lib_name.clone());
100 files.push(GeneratedFile {
101 path: output_base.join("download_ffi.sh"),
102 content: render_download_script(&github_repo, &version, &ffi_pkg_name),
103 generated_header: true,
104 });
105
106 files.push(GeneratedFile {
108 path: output_base.join("test_runner.h"),
109 content: render_test_runner_header(&active_groups),
110 generated_header: true,
111 });
112
113 files.push(GeneratedFile {
115 path: output_base.join("main.c"),
116 content: render_main_c(&active_groups),
117 generated_header: true,
118 });
119
120 let field_resolver = FieldResolver::new(
121 &e2e_config.fields,
122 &e2e_config.fields_optional,
123 &e2e_config.result_fields,
124 &e2e_config.fields_array,
125 );
126
127 for (group, active) in &active_groups {
129 let filename = format!("test_{}.c", sanitize_filename(&group.category));
130 let content = render_test_file(
131 &group.category,
132 active,
133 &header,
134 &prefix,
135 &function_name,
136 result_var,
137 &e2e_config.call.args,
138 &field_resolver,
139 &e2e_config.fields_c_types,
140 );
141 files.push(GeneratedFile {
142 path: output_base.join(filename),
143 content,
144 generated_header: true,
145 });
146 }
147
148 Ok(files)
149 }
150
151 fn language_name(&self) -> &'static str {
152 "c"
153 }
154}
155
156fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
157 let mut out = String::new();
158 let _ = writeln!(out, "# This file is auto-generated by alef. DO NOT EDIT.");
159 let _ = writeln!(out, "CC = gcc");
160 let _ = writeln!(out, "FFI_DIR = ffi");
161 let _ = writeln!(out);
162
163 let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
165 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I$(FFI_DIR)/include");
166 let _ = writeln!(
167 out,
168 " LDFLAGS = -L$(FFI_DIR)/lib -l{lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
169 );
170 let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
171 let _ = writeln!(out, " CFLAGS = -Wall -Wextra -I{ffi_crate_path}/include");
172 let _ = writeln!(
173 out,
174 " LDFLAGS = -L../../target/release -l{lib_name} -Wl,-rpath,../../target/release"
175 );
176 let _ = writeln!(out, "else");
177 let _ = writeln!(
178 out,
179 " CFLAGS = -Wall -Wextra $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
180 );
181 let _ = writeln!(out, " LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
182 let _ = writeln!(out, "endif");
183 let _ = writeln!(out);
184
185 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
186 let srcs = src_files.join(" ");
187
188 let _ = writeln!(out, "SRCS = main.c {srcs}");
189 let _ = writeln!(out, "TARGET = run_tests");
190 let _ = writeln!(out);
191 let _ = writeln!(out, ".PHONY: all clean test");
192 let _ = writeln!(out);
193 let _ = writeln!(out, "all: $(TARGET)");
194 let _ = writeln!(out);
195 let _ = writeln!(out, "$(TARGET): $(SRCS)");
196 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
197 let _ = writeln!(out);
198 let _ = writeln!(out, "test: $(TARGET)");
199 let _ = writeln!(out, "\t./$(TARGET)");
200 let _ = writeln!(out);
201 let _ = writeln!(out, "clean:");
202 let _ = writeln!(out, "\trm -f $(TARGET)");
203 out
204}
205
206fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
207 let mut out = String::new();
208 let _ = writeln!(out, "#!/usr/bin/env bash");
209 let _ = writeln!(out, "# This file is auto-generated by alef. DO NOT EDIT.");
210 let _ = writeln!(out, "set -euo pipefail");
211 let _ = writeln!(out);
212 let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
213 let _ = writeln!(out, "VERSION=\"{version}\"");
214 let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
215 let _ = writeln!(out, "FFI_DIR=\"ffi\"");
216 let _ = writeln!(out);
217 let _ = writeln!(out, "# Detect OS and architecture.");
218 let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
219 let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
220 let _ = writeln!(out);
221 let _ = writeln!(out, "case \"$ARCH\" in");
222 let _ = writeln!(out, " x86_64|amd64) ARCH=\"x86_64\" ;;");
223 let _ = writeln!(out, " arm64|aarch64) ARCH=\"aarch64\" ;;");
224 let _ = writeln!(out, " *) echo \"Unsupported architecture: $ARCH\" >&2; exit 1 ;;");
225 let _ = writeln!(out, "esac");
226 let _ = writeln!(out);
227 let _ = writeln!(out, "case \"$OS\" in");
228 let _ = writeln!(out, " linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
229 let _ = writeln!(out, " darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
230 let _ = writeln!(out, " *) echo \"Unsupported OS: $OS\" >&2; exit 1 ;;");
231 let _ = writeln!(out, "esac");
232 let _ = writeln!(out);
233 let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
234 let _ = writeln!(
235 out,
236 "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
237 );
238 let _ = writeln!(out);
239 let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
240 let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
241 let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
242 let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
243 out
244}
245
246fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
247 let mut out = String::new();
248 let _ = writeln!(out, "/* This file is auto-generated by alef. DO NOT EDIT. */");
249 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
250 let _ = writeln!(out, "#define TEST_RUNNER_H");
251 let _ = writeln!(out);
252 let _ = writeln!(out, "#include <string.h>");
253 let _ = writeln!(out, "#include <stdlib.h>");
254 let _ = writeln!(out);
255 let _ = writeln!(out, "/**");
257 let _ = writeln!(
258 out,
259 " * Compare a string against an expected value, trimming trailing whitespace."
260 );
261 let _ = writeln!(
262 out,
263 " * Returns 0 if the trimmed actual string equals the expected string."
264 );
265 let _ = writeln!(out, " */");
266 let _ = writeln!(
267 out,
268 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
269 );
270 let _ = writeln!(
271 out,
272 " if (actual == NULL || expected == NULL) return actual != expected;"
273 );
274 let _ = writeln!(out, " size_t alen = strlen(actual);");
275 let _ = writeln!(
276 out,
277 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
278 );
279 let _ = writeln!(out, " size_t elen = strlen(expected);");
280 let _ = writeln!(out, " if (alen != elen) return 1;");
281 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
282 let _ = writeln!(out, "}}");
283 let _ = writeln!(out);
284
285 let _ = writeln!(out, "/**");
286 let _ = writeln!(
287 out,
288 " * Extract a string value for a given key from a JSON object string."
289 );
290 let _ = writeln!(
291 out,
292 " * Returns a heap-allocated copy of the value, or NULL if not found."
293 );
294 let _ = writeln!(out, " * Caller must free() the returned string.");
295 let _ = writeln!(out, " */");
296 let _ = writeln!(
297 out,
298 "static inline char *htm_json_get_string(const char *json, const char *key) {{"
299 );
300 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
301 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
302 let _ = writeln!(out, " size_t key_len = strlen(key);");
303 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
304 let _ = writeln!(out, " if (!pattern) return NULL;");
305 let _ = writeln!(out, " pattern[0] = '\"';");
306 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
307 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
308 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
309 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
310 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
311 let _ = writeln!(out, " free(pattern);");
312 let _ = writeln!(out, " if (!found) return NULL;");
313 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
314 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
315 let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
316 let _ = writeln!(out, " found++; /* skip opening quote */");
317 let _ = writeln!(out, " const char *end = found;");
318 let _ = writeln!(out, " while (*end && *end != '\"') {{");
319 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
320 let _ = writeln!(out, " else end++;");
321 let _ = writeln!(out, " }}");
322 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
323 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
324 let _ = writeln!(out, " if (!result_str) return NULL;");
325 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
326 let _ = writeln!(out, " result_str[val_len] = '\\0';");
327 let _ = writeln!(out, " return result_str;");
328 let _ = writeln!(out, "}}");
329 let _ = writeln!(out);
330 let _ = writeln!(out, "/**");
331 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
332 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
333 let _ = writeln!(out, " */");
334 let _ = writeln!(out, "static inline int htm_json_array_count(const char *json) {{");
335 let _ = writeln!(out, " if (json == NULL) return 0;");
336 let _ = writeln!(out, " /* Skip leading whitespace */");
337 let _ = writeln!(
338 out,
339 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
340 );
341 let _ = writeln!(out, " if (*json != '[') return 0;");
342 let _ = writeln!(out, " json++;");
343 let _ = writeln!(out, " /* Skip whitespace after '[' */");
344 let _ = writeln!(
345 out,
346 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
347 );
348 let _ = writeln!(out, " if (*json == ']') return 0;");
349 let _ = writeln!(out, " int count = 1;");
350 let _ = writeln!(out, " int depth = 0;");
351 let _ = writeln!(out, " int in_string = 0;");
352 let _ = writeln!(
353 out,
354 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
355 );
356 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
357 let _ = writeln!(
358 out,
359 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
360 );
361 let _ = writeln!(out, " if (in_string) continue;");
362 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
363 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
364 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
365 let _ = writeln!(out, " }}");
366 let _ = writeln!(out, " return count;");
367 let _ = writeln!(out, "}}");
368 let _ = writeln!(out);
369
370 for (group, fixtures) in active_groups {
371 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
372 for fixture in fixtures {
373 let fn_name = sanitize_ident(&fixture.id);
374 let _ = writeln!(out, "void test_{fn_name}(void);");
375 }
376 let _ = writeln!(out);
377 }
378
379 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
380 out
381}
382
383fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
384 let mut out = String::new();
385 let _ = writeln!(out, "/* This file is auto-generated by alef. DO NOT EDIT. */");
386 let _ = writeln!(out, "#include <stdio.h>");
387 let _ = writeln!(out, "#include \"test_runner.h\"");
388 let _ = writeln!(out);
389 let _ = writeln!(out, "int main(void) {{");
390 let _ = writeln!(out, " int passed = 0;");
391 let _ = writeln!(out, " int failed = 0;");
392 let _ = writeln!(out);
393
394 for (group, fixtures) in active_groups {
395 let _ = writeln!(out, " /* Category: {} */", group.category);
396 for fixture in fixtures {
397 let fn_name = sanitize_ident(&fixture.id);
398 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
399 let _ = writeln!(out, " test_{fn_name}();");
400 let _ = writeln!(out, " printf(\" PASSED\\n\");");
401 let _ = writeln!(out, " passed++;");
402 }
403 let _ = writeln!(out);
404 }
405
406 let _ = writeln!(
407 out,
408 " printf(\"\\nResults: %d passed, %d failed\\n\", passed, failed);"
409 );
410 let _ = writeln!(out, " return failed > 0 ? 1 : 0;");
411 let _ = writeln!(out, "}}");
412 out
413}
414
415#[allow(clippy::too_many_arguments)]
416fn render_test_file(
417 category: &str,
418 fixtures: &[&Fixture],
419 header: &str,
420 prefix: &str,
421 function_name: &str,
422 result_var: &str,
423 args: &[crate::config::ArgMapping],
424 field_resolver: &FieldResolver,
425 fields_c_types: &HashMap<String, String>,
426) -> String {
427 let mut out = String::new();
428 let _ = writeln!(out, "/* This file is auto-generated by alef. DO NOT EDIT. */");
429 let _ = writeln!(out, "/* E2e tests for category: {category} */");
430 let _ = writeln!(out);
431 let _ = writeln!(out, "#include <assert.h>");
432 let _ = writeln!(out, "#include <string.h>");
433 let _ = writeln!(out, "#include <stdio.h>");
434 let _ = writeln!(out, "#include <stdlib.h>");
435 let _ = writeln!(out, "#include \"{header}\"");
436 let _ = writeln!(out, "#include \"test_runner.h\"");
437 let _ = writeln!(out);
438
439 for (i, fixture) in fixtures.iter().enumerate() {
440 render_test_function(
441 &mut out,
442 fixture,
443 prefix,
444 function_name,
445 result_var,
446 args,
447 field_resolver,
448 fields_c_types,
449 );
450 if i + 1 < fixtures.len() {
451 let _ = writeln!(out);
452 }
453 }
454
455 out
456}
457
458#[allow(clippy::too_many_arguments)]
459fn render_test_function(
460 out: &mut String,
461 fixture: &Fixture,
462 prefix: &str,
463 function_name: &str,
464 result_var: &str,
465 args: &[crate::config::ArgMapping],
466 field_resolver: &FieldResolver,
467 fields_c_types: &HashMap<String, String>,
468) {
469 let fn_name = sanitize_ident(&fixture.id);
470 let description = &fixture.description;
471
472 let prefixed_fn = function_name.to_string();
475
476 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
477
478 let _ = writeln!(out, "void test_{fn_name}(void) {{");
479 let _ = writeln!(out, " /* {description} */");
480
481 let mut has_options_handle = false;
483 for arg in args {
484 if arg.arg_type == "json_object" {
485 if let Some(val) = fixture.input.get(&arg.field) {
486 if !val.is_null() {
487 let normalized = super::normalize_json_keys_to_snake_case(val);
491 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
492 let escaped = escape_c(&json_str);
493 let upper = prefix.to_uppercase();
494 let _ = writeln!(
495 out,
496 " {upper}ConversionOptions* options_handle = {prefix}_conversion_options_from_json(\"{escaped}\");"
497 );
498 has_options_handle = true;
499 }
500 }
501 }
502 }
503
504 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
505
506 if expects_error {
507 let _ = writeln!(
508 out,
509 " HTMConversionResult* {result_var} = {prefixed_fn}({args_str});"
510 );
511 if has_options_handle {
512 let _ = writeln!(out, " {prefix}_conversion_options_free(options_handle);");
513 }
514 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
515 let _ = writeln!(out, "}}");
516 return;
517 }
518
519 let _ = writeln!(
521 out,
522 " HTMConversionResult* {result_var} = {prefixed_fn}({args_str});"
523 );
524 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
525
526 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
534 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
537
538 for assertion in &fixture.assertions {
539 if let Some(f) = &assertion.field {
540 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
541 let resolved = field_resolver.resolve(f);
542 let local_var = f.replace(['.', '['], "_").replace(']', "");
543 let has_map_access = resolved.contains('[');
544
545 if resolved.contains('.') {
546 emit_nested_accessor(
547 out,
548 prefix,
549 resolved,
550 &local_var,
551 result_var,
552 fields_c_types,
553 &mut intermediate_handles,
554 );
555 } else {
556 let accessor_fn = format!("{prefix}_conversion_result_{resolved}");
557 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
558 }
559 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
560 }
561 }
562 }
563
564 for assertion in &fixture.assertions {
565 render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
566 }
567
568 for (_f, local_var, from_json) in &accessed_fields {
570 if *from_json {
571 let _ = writeln!(out, " free({local_var});");
572 } else {
573 let _ = writeln!(out, " {prefix}_free_string({local_var});");
574 }
575 }
576 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
578 if snake_type == "free_string" {
579 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
581 } else {
582 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
583 }
584 }
585 if has_options_handle {
586 let _ = writeln!(out, " {prefix}_conversion_options_free(options_handle);");
587 }
588 let _ = writeln!(out, " {prefix}_conversion_result_free({result_var});");
589 let _ = writeln!(out, "}}");
590}
591
592fn emit_nested_accessor(
606 out: &mut String,
607 prefix: &str,
608 resolved: &str,
609 local_var: &str,
610 result_var: &str,
611 fields_c_types: &HashMap<String, String>,
612 intermediate_handles: &mut Vec<(String, String)>,
613) {
614 let segments: Vec<&str> = resolved.split('.').collect();
615 let prefix_upper = prefix.to_uppercase();
616
617 let mut current_snake_type = "conversion_result".to_string();
619 let mut current_handle = result_var.to_string();
620
621 for (i, segment) in segments.iter().enumerate() {
622 let is_leaf = i + 1 == segments.len();
623
624 if let Some(bracket_pos) = segment.find('[') {
626 let field_name = &segment[..bracket_pos];
627 let key = segment[bracket_pos + 1..].trim_end_matches(']');
628 let field_snake = field_name.to_snake_case();
629 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
630
631 let json_var = format!("{field_snake}_json");
634 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
635 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
636 let _ = writeln!(out, " assert({json_var} != NULL);");
637 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
639 }
640 let _ = writeln!(
642 out,
643 " char* {local_var} = htm_json_get_string({json_var}, \"{key}\");"
644 );
645 return; }
647
648 let seg_snake = segment.to_snake_case();
649 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
650
651 if is_leaf {
652 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
654 } else {
655 let lookup_key = format!("{current_snake_type}.{seg_snake}");
657 let return_type_pascal = match fields_c_types.get(&lookup_key) {
658 Some(t) => t.clone(),
659 None => {
660 segment.to_pascal_case()
662 }
663 };
664 let return_snake = return_type_pascal.to_snake_case();
665 let handle_var = format!("{seg_snake}_handle");
666
667 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
670 let _ = writeln!(
671 out,
672 " {prefix_upper}{return_type_pascal}* {handle_var} = \
673 {accessor_fn}({current_handle});"
674 );
675 let _ = writeln!(out, " assert({handle_var} != NULL);");
676 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
677 }
678
679 current_snake_type = return_snake;
680 current_handle = handle_var;
681 }
682 }
683}
684
685fn build_args_string_c(
689 input: &serde_json::Value,
690 args: &[crate::config::ArgMapping],
691 has_options_handle: bool,
692) -> String {
693 if args.is_empty() {
694 return json_to_c(input);
695 }
696
697 let parts: Vec<String> = args
698 .iter()
699 .filter_map(|arg| {
700 let val = input.get(&arg.field);
701 match val {
702 None if arg.optional => Some("NULL".to_string()),
704 None => None,
706 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
708 Some(v) => {
709 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
712 Some("options_handle".to_string())
713 } else {
714 Some(json_to_c(v))
715 }
716 }
717 }
718 })
719 .collect();
720
721 parts.join(", ")
722}
723
724fn render_assertion(
725 out: &mut String,
726 assertion: &Assertion,
727 result_var: &str,
728 _field_resolver: &FieldResolver,
729 accessed_fields: &[(String, String, bool)],
730) {
731 if let Some(f) = &assertion.field {
733 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
734 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
735 return;
736 }
737 }
738
739 let field_expr = match &assertion.field {
740 Some(f) if !f.is_empty() => {
741 accessed_fields
743 .iter()
744 .find(|(k, _, _)| k == f)
745 .map(|(_, local, _)| local.clone())
746 .unwrap_or_else(|| result_var.to_string())
747 }
748 _ => result_var.to_string(),
749 };
750
751 match assertion.assertion_type.as_str() {
752 "equals" => {
753 if let Some(expected) = &assertion.value {
754 let c_val = json_to_c(expected);
755 if expected.is_string() {
756 let _ = writeln!(
758 out,
759 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
760 );
761 } else {
762 let _ = writeln!(
763 out,
764 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
765 );
766 }
767 }
768 }
769 "contains" => {
770 if let Some(expected) = &assertion.value {
771 let c_val = json_to_c(expected);
772 let _ = writeln!(
773 out,
774 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
775 );
776 }
777 }
778 "contains_all" => {
779 if let Some(values) = &assertion.values {
780 for val in values {
781 let c_val = json_to_c(val);
782 let _ = writeln!(
783 out,
784 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
785 );
786 }
787 }
788 }
789 "not_contains" => {
790 if let Some(expected) = &assertion.value {
791 let c_val = json_to_c(expected);
792 let _ = writeln!(
793 out,
794 " assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
795 );
796 }
797 }
798 "not_empty" => {
799 let _ = writeln!(
800 out,
801 " assert(strlen({field_expr}) > 0 && \"expected non-empty value\");"
802 );
803 }
804 "is_empty" => {
805 let _ = writeln!(
806 out,
807 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
808 );
809 }
810 "contains_any" => {
811 if let Some(values) = &assertion.values {
812 let _ = writeln!(out, " {{");
813 let _ = writeln!(out, " int found = 0;");
814 for val in values {
815 let c_val = json_to_c(val);
816 let _ = writeln!(
817 out,
818 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
819 );
820 }
821 let _ = writeln!(
822 out,
823 " assert(found && \"expected to contain at least one of the specified values\");"
824 );
825 let _ = writeln!(out, " }}");
826 }
827 }
828 "greater_than" => {
829 if let Some(val) = &assertion.value {
830 let c_val = json_to_c(val);
831 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
832 }
833 }
834 "less_than" => {
835 if let Some(val) = &assertion.value {
836 let c_val = json_to_c(val);
837 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
838 }
839 }
840 "greater_than_or_equal" => {
841 if let Some(val) = &assertion.value {
842 let c_val = json_to_c(val);
843 let _ = writeln!(
844 out,
845 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
846 );
847 }
848 }
849 "less_than_or_equal" => {
850 if let Some(val) = &assertion.value {
851 let c_val = json_to_c(val);
852 let _ = writeln!(
853 out,
854 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
855 );
856 }
857 }
858 "starts_with" => {
859 if let Some(expected) = &assertion.value {
860 let c_val = json_to_c(expected);
861 let _ = writeln!(
862 out,
863 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
864 );
865 }
866 }
867 "ends_with" => {
868 if let Some(expected) = &assertion.value {
869 let c_val = json_to_c(expected);
870 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
871 let _ = writeln!(
872 out,
873 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
874 );
875 }
876 }
877 "min_length" => {
878 if let Some(val) = &assertion.value {
879 if let Some(n) = val.as_u64() {
880 let _ = writeln!(
881 out,
882 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
883 );
884 }
885 }
886 }
887 "max_length" => {
888 if let Some(val) = &assertion.value {
889 if let Some(n) = val.as_u64() {
890 let _ = writeln!(
891 out,
892 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
893 );
894 }
895 }
896 }
897 "count_min" => {
898 if let Some(val) = &assertion.value {
899 if let Some(n) = val.as_u64() {
900 let _ = writeln!(out, " {{");
901 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
902 let _ = writeln!(
903 out,
904 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
905 );
906 let _ = writeln!(out, " int elem_count = htm_json_array_count({field_expr});");
907 let _ = writeln!(
908 out,
909 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
910 );
911 let _ = writeln!(out, " }}");
912 }
913 }
914 }
915 "not_error" => {
916 }
918 "error" => {
919 }
921 other => {
922 let _ = writeln!(out, " /* TODO: unsupported assertion type: {other} */");
923 }
924 }
925}
926
927fn json_to_c(value: &serde_json::Value) -> String {
929 match value {
930 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
931 serde_json::Value::Bool(true) => "1".to_string(),
932 serde_json::Value::Bool(false) => "0".to_string(),
933 serde_json::Value::Number(n) => n.to_string(),
934 serde_json::Value::Null => "NULL".to_string(),
935 other => format!("\"{}\"", escape_c(&other.to_string())),
936 }
937}