1use crate::config::E2eConfig;
7use crate::escape::{escape_c, sanitize_filename, sanitize_ident};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, Fixture, FixtureGroup};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::AlefConfig;
12use anyhow::Result;
13use heck::{ToPascalCase, ToSnakeCase};
14use std::collections::HashMap;
15use std::fmt::Write as FmtWrite;
16use std::path::PathBuf;
17
18use super::E2eCodegen;
19
20pub struct CCodegen;
22
23impl E2eCodegen for CCodegen {
24 fn generate(
25 &self,
26 groups: &[FixtureGroup],
27 e2e_config: &E2eConfig,
28 _alef_config: &AlefConfig,
29 ) -> Result<Vec<GeneratedFile>> {
30 let lang = self.language_name();
31 let output_base = PathBuf::from(&e2e_config.output).join(lang);
32
33 let mut files = Vec::new();
34
35 let call = &e2e_config.call;
37 let overrides = call.overrides.get(lang);
38 let function_name = overrides
39 .and_then(|o| o.function.as_ref())
40 .cloned()
41 .unwrap_or_else(|| call.function.clone());
42 let result_var = &call.result_var;
43 let prefix = overrides.and_then(|o| o.prefix.as_ref()).cloned().unwrap_or_default();
44 let header = overrides
45 .and_then(|o| o.header.as_ref())
46 .cloned()
47 .unwrap_or_else(|| format!("{}.h", call.module));
48
49 let c_pkg = e2e_config.packages.get("c");
51 let include_path = c_pkg
52 .and_then(|p| p.path.as_ref())
53 .map(|p| format!("{p}/include"))
54 .unwrap_or_else(|| "../../crates/ffi/include".to_string());
55 let lib_path = c_pkg
56 .and_then(|p| p.module.as_ref())
57 .cloned()
58 .unwrap_or_else(|| "../../target/release".to_string());
59 let lib_name = c_pkg
60 .and_then(|p| p.name.as_ref())
61 .cloned()
62 .unwrap_or_else(|| call.module.clone());
63
64 let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
66 .iter()
67 .filter_map(|group| {
68 let active: Vec<&Fixture> = group
69 .fixtures
70 .iter()
71 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
72 .collect();
73 if active.is_empty() { None } else { Some((group, active)) }
74 })
75 .collect();
76
77 let category_names: Vec<String> = active_groups
79 .iter()
80 .map(|(g, _)| sanitize_filename(&g.category))
81 .collect();
82 files.push(GeneratedFile {
83 path: output_base.join("Makefile"),
84 content: render_makefile(&category_names, &include_path, &lib_path, &lib_name),
85 generated_header: true,
86 });
87
88 files.push(GeneratedFile {
90 path: output_base.join("test_runner.h"),
91 content: render_test_runner_header(&active_groups),
92 generated_header: true,
93 });
94
95 files.push(GeneratedFile {
97 path: output_base.join("main.c"),
98 content: render_main_c(&active_groups),
99 generated_header: true,
100 });
101
102 let field_resolver = FieldResolver::new(
103 &e2e_config.fields,
104 &e2e_config.fields_optional,
105 &e2e_config.result_fields,
106 &e2e_config.fields_array,
107 );
108
109 for (group, active) in &active_groups {
111 let filename = format!("test_{}.c", sanitize_filename(&group.category));
112 let content = render_test_file(
113 &group.category,
114 active,
115 &header,
116 &prefix,
117 &function_name,
118 result_var,
119 &e2e_config.call.args,
120 &field_resolver,
121 &e2e_config.fields_c_types,
122 );
123 files.push(GeneratedFile {
124 path: output_base.join(filename),
125 content,
126 generated_header: true,
127 });
128 }
129
130 Ok(files)
131 }
132
133 fn language_name(&self) -> &'static str {
134 "c"
135 }
136}
137
138fn render_makefile(categories: &[String], include_path: &str, lib_path: &str, lib_name: &str) -> String {
139 let mut out = String::new();
140 let _ = writeln!(out, "# This file is auto-generated by alef. DO NOT EDIT.");
141 let _ = writeln!(out, "CC = gcc");
142 let _ = writeln!(out, "CFLAGS = -Wall -Wextra -I{include_path}");
143 let _ = writeln!(out, "LDFLAGS = -L{lib_path} -l{lib_name}");
144 let _ = writeln!(out);
145
146 let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
147 let srcs = src_files.join(" ");
148
149 let _ = writeln!(out, "SRCS = main.c {srcs}");
150 let _ = writeln!(out, "TARGET = run_tests");
151 let _ = writeln!(out);
152 let _ = writeln!(out, ".PHONY: all clean test");
153 let _ = writeln!(out);
154 let _ = writeln!(out, "all: $(TARGET)");
155 let _ = writeln!(out);
156 let _ = writeln!(out, "$(TARGET): $(SRCS)");
157 let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
158 let _ = writeln!(out);
159 let _ = writeln!(out, "test: $(TARGET)");
160 let _ = writeln!(out, "\t./$(TARGET)");
161 let _ = writeln!(out);
162 let _ = writeln!(out, "clean:");
163 let _ = writeln!(out, "\trm -f $(TARGET)");
164 out
165}
166
167fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
168 let mut out = String::new();
169 let _ = writeln!(out, "/* This file is auto-generated by alef. DO NOT EDIT. */");
170 let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
171 let _ = writeln!(out, "#define TEST_RUNNER_H");
172 let _ = writeln!(out);
173 let _ = writeln!(out, "#include <string.h>");
174 let _ = writeln!(out, "#include <stdlib.h>");
175 let _ = writeln!(out);
176 let _ = writeln!(out, "/**");
178 let _ = writeln!(
179 out,
180 " * Compare a string against an expected value, trimming trailing whitespace."
181 );
182 let _ = writeln!(
183 out,
184 " * Returns 0 if the trimmed actual string equals the expected string."
185 );
186 let _ = writeln!(out, " */");
187 let _ = writeln!(
188 out,
189 "static inline int str_trim_eq(const char *actual, const char *expected) {{"
190 );
191 let _ = writeln!(
192 out,
193 " if (actual == NULL || expected == NULL) return actual != expected;"
194 );
195 let _ = writeln!(out, " size_t alen = strlen(actual);");
196 let _ = writeln!(
197 out,
198 " while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
199 );
200 let _ = writeln!(out, " size_t elen = strlen(expected);");
201 let _ = writeln!(out, " if (alen != elen) return 1;");
202 let _ = writeln!(out, " return memcmp(actual, expected, elen);");
203 let _ = writeln!(out, "}}");
204 let _ = writeln!(out);
205
206 let _ = writeln!(out, "/**");
207 let _ = writeln!(
208 out,
209 " * Extract a string value for a given key from a JSON object string."
210 );
211 let _ = writeln!(
212 out,
213 " * Returns a heap-allocated copy of the value, or NULL if not found."
214 );
215 let _ = writeln!(out, " * Caller must free() the returned string.");
216 let _ = writeln!(out, " */");
217 let _ = writeln!(
218 out,
219 "static inline char *htm_json_get_string(const char *json, const char *key) {{"
220 );
221 let _ = writeln!(out, " if (json == NULL || key == NULL) return NULL;");
222 let _ = writeln!(out, " /* Build search pattern: \"key\": */");
223 let _ = writeln!(out, " size_t key_len = strlen(key);");
224 let _ = writeln!(out, " char *pattern = (char *)malloc(key_len + 5);");
225 let _ = writeln!(out, " if (!pattern) return NULL;");
226 let _ = writeln!(out, " pattern[0] = '\"';");
227 let _ = writeln!(out, " memcpy(pattern + 1, key, key_len);");
228 let _ = writeln!(out, " pattern[key_len + 1] = '\"';");
229 let _ = writeln!(out, " pattern[key_len + 2] = ':';");
230 let _ = writeln!(out, " pattern[key_len + 3] = '\\0';");
231 let _ = writeln!(out, " const char *found = strstr(json, pattern);");
232 let _ = writeln!(out, " free(pattern);");
233 let _ = writeln!(out, " if (!found) return NULL;");
234 let _ = writeln!(out, " found += key_len + 3; /* skip past \"key\": */");
235 let _ = writeln!(out, " while (*found == ' ' || *found == '\\t') found++;");
236 let _ = writeln!(out, " if (*found != '\"') return NULL; /* not a string value */");
237 let _ = writeln!(out, " found++; /* skip opening quote */");
238 let _ = writeln!(out, " const char *end = found;");
239 let _ = writeln!(out, " while (*end && *end != '\"') {{");
240 let _ = writeln!(out, " if (*end == '\\\\') {{ end++; if (*end) end++; }}");
241 let _ = writeln!(out, " else end++;");
242 let _ = writeln!(out, " }}");
243 let _ = writeln!(out, " size_t val_len = (size_t)(end - found);");
244 let _ = writeln!(out, " char *result_str = (char *)malloc(val_len + 1);");
245 let _ = writeln!(out, " if (!result_str) return NULL;");
246 let _ = writeln!(out, " memcpy(result_str, found, val_len);");
247 let _ = writeln!(out, " result_str[val_len] = '\\0';");
248 let _ = writeln!(out, " return result_str;");
249 let _ = writeln!(out, "}}");
250 let _ = writeln!(out);
251 let _ = writeln!(out, "/**");
252 let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
253 let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
254 let _ = writeln!(out, " */");
255 let _ = writeln!(out, "static inline int htm_json_array_count(const char *json) {{");
256 let _ = writeln!(out, " if (json == NULL) return 0;");
257 let _ = writeln!(out, " /* Skip leading whitespace */");
258 let _ = writeln!(
259 out,
260 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
261 );
262 let _ = writeln!(out, " if (*json != '[') return 0;");
263 let _ = writeln!(out, " json++;");
264 let _ = writeln!(out, " /* Skip whitespace after '[' */");
265 let _ = writeln!(
266 out,
267 " while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
268 );
269 let _ = writeln!(out, " if (*json == ']') return 0;");
270 let _ = writeln!(out, " int count = 1;");
271 let _ = writeln!(out, " int depth = 0;");
272 let _ = writeln!(out, " int in_string = 0;");
273 let _ = writeln!(
274 out,
275 " for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
276 );
277 let _ = writeln!(out, " if (*json == '\\\\' && in_string) {{ json++; continue; }}");
278 let _ = writeln!(
279 out,
280 " if (*json == '\"') {{ in_string = !in_string; continue; }}"
281 );
282 let _ = writeln!(out, " if (in_string) continue;");
283 let _ = writeln!(out, " if (*json == '[' || *json == '{{') depth++;");
284 let _ = writeln!(out, " else if (*json == ']' || *json == '}}') depth--;");
285 let _ = writeln!(out, " else if (*json == ',' && depth == 0) count++;");
286 let _ = writeln!(out, " }}");
287 let _ = writeln!(out, " return count;");
288 let _ = writeln!(out, "}}");
289 let _ = writeln!(out);
290
291 for (group, fixtures) in active_groups {
292 let _ = writeln!(out, "/* Tests for category: {} */", group.category);
293 for fixture in fixtures {
294 let fn_name = sanitize_ident(&fixture.id);
295 let _ = writeln!(out, "void test_{fn_name}(void);");
296 }
297 let _ = writeln!(out);
298 }
299
300 let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
301 out
302}
303
304fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
305 let mut out = String::new();
306 let _ = writeln!(out, "/* This file is auto-generated by alef. DO NOT EDIT. */");
307 let _ = writeln!(out, "#include <stdio.h>");
308 let _ = writeln!(out, "#include \"test_runner.h\"");
309 let _ = writeln!(out);
310 let _ = writeln!(out, "int main(void) {{");
311 let _ = writeln!(out, " int passed = 0;");
312 let _ = writeln!(out, " int failed = 0;");
313 let _ = writeln!(out);
314
315 for (group, fixtures) in active_groups {
316 let _ = writeln!(out, " /* Category: {} */", group.category);
317 for fixture in fixtures {
318 let fn_name = sanitize_ident(&fixture.id);
319 let _ = writeln!(out, " printf(\" Running test_{fn_name}...\");");
320 let _ = writeln!(out, " test_{fn_name}();");
321 let _ = writeln!(out, " printf(\" PASSED\\n\");");
322 let _ = writeln!(out, " passed++;");
323 }
324 let _ = writeln!(out);
325 }
326
327 let _ = writeln!(
328 out,
329 " printf(\"\\nResults: %d passed, %d failed\\n\", passed, failed);"
330 );
331 let _ = writeln!(out, " return failed > 0 ? 1 : 0;");
332 let _ = writeln!(out, "}}");
333 out
334}
335
336#[allow(clippy::too_many_arguments)]
337fn render_test_file(
338 category: &str,
339 fixtures: &[&Fixture],
340 header: &str,
341 prefix: &str,
342 function_name: &str,
343 result_var: &str,
344 args: &[crate::config::ArgMapping],
345 field_resolver: &FieldResolver,
346 fields_c_types: &HashMap<String, String>,
347) -> String {
348 let mut out = String::new();
349 let _ = writeln!(out, "/* This file is auto-generated by alef. DO NOT EDIT. */");
350 let _ = writeln!(out, "/* E2e tests for category: {category} */");
351 let _ = writeln!(out);
352 let _ = writeln!(out, "#include <assert.h>");
353 let _ = writeln!(out, "#include <string.h>");
354 let _ = writeln!(out, "#include <stdio.h>");
355 let _ = writeln!(out, "#include <stdlib.h>");
356 let _ = writeln!(out, "#include \"{header}\"");
357 let _ = writeln!(out, "#include \"test_runner.h\"");
358 let _ = writeln!(out);
359
360 for (i, fixture) in fixtures.iter().enumerate() {
361 render_test_function(
362 &mut out,
363 fixture,
364 prefix,
365 function_name,
366 result_var,
367 args,
368 field_resolver,
369 fields_c_types,
370 );
371 if i + 1 < fixtures.len() {
372 let _ = writeln!(out);
373 }
374 }
375
376 out
377}
378
379#[allow(clippy::too_many_arguments)]
380fn render_test_function(
381 out: &mut String,
382 fixture: &Fixture,
383 prefix: &str,
384 function_name: &str,
385 result_var: &str,
386 args: &[crate::config::ArgMapping],
387 field_resolver: &FieldResolver,
388 fields_c_types: &HashMap<String, String>,
389) {
390 let fn_name = sanitize_ident(&fixture.id);
391 let description = &fixture.description;
392
393 let prefixed_fn = function_name.to_string();
396
397 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
398
399 let _ = writeln!(out, "void test_{fn_name}(void) {{");
400 let _ = writeln!(out, " /* {description} */");
401
402 let mut has_options_handle = false;
404 for arg in args {
405 if arg.arg_type == "json_object" {
406 if let Some(val) = fixture.input.get(&arg.field) {
407 if !val.is_null() {
408 let normalized = super::normalize_json_keys_to_snake_case(val);
412 let json_str = serde_json::to_string(&normalized).unwrap_or_default();
413 let escaped = escape_c(&json_str);
414 let upper = prefix.to_uppercase();
415 let _ = writeln!(
416 out,
417 " {upper}ConversionOptions* options_handle = {prefix}_conversion_options_from_json(\"{escaped}\");"
418 );
419 has_options_handle = true;
420 }
421 }
422 }
423 }
424
425 let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
426
427 if expects_error {
428 let _ = writeln!(
429 out,
430 " HTMConversionResult* {result_var} = {prefixed_fn}({args_str});"
431 );
432 if has_options_handle {
433 let _ = writeln!(out, " {prefix}_conversion_options_free(options_handle);");
434 }
435 let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
436 let _ = writeln!(out, "}}");
437 return;
438 }
439
440 let _ = writeln!(
442 out,
443 " HTMConversionResult* {result_var} = {prefixed_fn}({args_str});"
444 );
445 let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
446
447 let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
455 let mut intermediate_handles: Vec<(String, String)> = Vec::new();
458
459 for assertion in &fixture.assertions {
460 if let Some(f) = &assertion.field {
461 if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
462 let resolved = field_resolver.resolve(f);
463 let local_var = f.replace(['.', '['], "_").replace(']', "");
464 let has_map_access = resolved.contains('[');
465
466 if resolved.contains('.') {
467 emit_nested_accessor(
468 out,
469 prefix,
470 resolved,
471 &local_var,
472 result_var,
473 fields_c_types,
474 &mut intermediate_handles,
475 );
476 } else {
477 let accessor_fn = format!("{prefix}_conversion_result_{resolved}");
478 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
479 }
480 accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
481 }
482 }
483 }
484
485 for assertion in &fixture.assertions {
486 render_assertion(out, assertion, result_var, field_resolver, &accessed_fields);
487 }
488
489 for (_f, local_var, from_json) in &accessed_fields {
491 if *from_json {
492 let _ = writeln!(out, " free({local_var});");
493 } else {
494 let _ = writeln!(out, " {prefix}_free_string({local_var});");
495 }
496 }
497 for (handle_var, snake_type) in intermediate_handles.iter().rev() {
499 if snake_type == "free_string" {
500 let _ = writeln!(out, " {prefix}_free_string({handle_var});");
502 } else {
503 let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
504 }
505 }
506 if has_options_handle {
507 let _ = writeln!(out, " {prefix}_conversion_options_free(options_handle);");
508 }
509 let _ = writeln!(out, " {prefix}_conversion_result_free({result_var});");
510 let _ = writeln!(out, "}}");
511}
512
513fn emit_nested_accessor(
527 out: &mut String,
528 prefix: &str,
529 resolved: &str,
530 local_var: &str,
531 result_var: &str,
532 fields_c_types: &HashMap<String, String>,
533 intermediate_handles: &mut Vec<(String, String)>,
534) {
535 let segments: Vec<&str> = resolved.split('.').collect();
536 let prefix_upper = prefix.to_uppercase();
537
538 let mut current_snake_type = "conversion_result".to_string();
540 let mut current_handle = result_var.to_string();
541
542 for (i, segment) in segments.iter().enumerate() {
543 let is_leaf = i + 1 == segments.len();
544
545 if let Some(bracket_pos) = segment.find('[') {
547 let field_name = &segment[..bracket_pos];
548 let key = segment[bracket_pos + 1..].trim_end_matches(']');
549 let field_snake = field_name.to_snake_case();
550 let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
551
552 let json_var = format!("{field_snake}_json");
555 if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
556 let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
557 let _ = writeln!(out, " assert({json_var} != NULL);");
558 intermediate_handles.push((json_var.clone(), "free_string".to_string()));
560 }
561 let _ = writeln!(
563 out,
564 " char* {local_var} = htm_json_get_string({json_var}, \"{key}\");"
565 );
566 return; }
568
569 let seg_snake = segment.to_snake_case();
570 let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
571
572 if is_leaf {
573 let _ = writeln!(out, " char* {local_var} = {accessor_fn}({current_handle});");
575 } else {
576 let lookup_key = format!("{current_snake_type}.{seg_snake}");
578 let return_type_pascal = match fields_c_types.get(&lookup_key) {
579 Some(t) => t.clone(),
580 None => {
581 segment.to_pascal_case()
583 }
584 };
585 let return_snake = return_type_pascal.to_snake_case();
586 let handle_var = format!("{seg_snake}_handle");
587
588 if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
591 let _ = writeln!(
592 out,
593 " {prefix_upper}{return_type_pascal}* {handle_var} = \
594 {accessor_fn}({current_handle});"
595 );
596 let _ = writeln!(out, " assert({handle_var} != NULL);");
597 intermediate_handles.push((handle_var.clone(), return_snake.clone()));
598 }
599
600 current_snake_type = return_snake;
601 current_handle = handle_var;
602 }
603 }
604}
605
606fn build_args_string_c(
610 input: &serde_json::Value,
611 args: &[crate::config::ArgMapping],
612 has_options_handle: bool,
613) -> String {
614 if args.is_empty() {
615 return json_to_c(input);
616 }
617
618 let parts: Vec<String> = args
619 .iter()
620 .filter_map(|arg| {
621 let val = input.get(&arg.field);
622 match val {
623 None if arg.optional => Some("NULL".to_string()),
625 None => None,
627 Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
629 Some(v) => {
630 if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
633 Some("options_handle".to_string())
634 } else {
635 Some(json_to_c(v))
636 }
637 }
638 }
639 })
640 .collect();
641
642 parts.join(", ")
643}
644
645fn render_assertion(
646 out: &mut String,
647 assertion: &Assertion,
648 result_var: &str,
649 _field_resolver: &FieldResolver,
650 accessed_fields: &[(String, String, bool)],
651) {
652 if let Some(f) = &assertion.field {
654 if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
655 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
656 return;
657 }
658 }
659
660 let field_expr = match &assertion.field {
661 Some(f) if !f.is_empty() => {
662 accessed_fields
664 .iter()
665 .find(|(k, _, _)| k == f)
666 .map(|(_, local, _)| local.clone())
667 .unwrap_or_else(|| result_var.to_string())
668 }
669 _ => result_var.to_string(),
670 };
671
672 match assertion.assertion_type.as_str() {
673 "equals" => {
674 if let Some(expected) = &assertion.value {
675 let c_val = json_to_c(expected);
676 if expected.is_string() {
677 let _ = writeln!(
679 out,
680 " assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
681 );
682 } else {
683 let _ = writeln!(
684 out,
685 " assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
686 );
687 }
688 }
689 }
690 "contains" => {
691 if let Some(expected) = &assertion.value {
692 let c_val = json_to_c(expected);
693 let _ = writeln!(
694 out,
695 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
696 );
697 }
698 }
699 "contains_all" => {
700 if let Some(values) = &assertion.values {
701 for val in values {
702 let c_val = json_to_c(val);
703 let _ = writeln!(
704 out,
705 " assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
706 );
707 }
708 }
709 }
710 "not_contains" => {
711 if let Some(expected) = &assertion.value {
712 let c_val = json_to_c(expected);
713 let _ = writeln!(
714 out,
715 " assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
716 );
717 }
718 }
719 "not_empty" => {
720 let _ = writeln!(
721 out,
722 " assert(strlen({field_expr}) > 0 && \"expected non-empty value\");"
723 );
724 }
725 "is_empty" => {
726 let _ = writeln!(
727 out,
728 " assert(strlen({field_expr}) == 0 && \"expected empty value\");"
729 );
730 }
731 "contains_any" => {
732 if let Some(values) = &assertion.values {
733 let _ = writeln!(out, " {{");
734 let _ = writeln!(out, " int found = 0;");
735 for val in values {
736 let c_val = json_to_c(val);
737 let _ = writeln!(
738 out,
739 " if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
740 );
741 }
742 let _ = writeln!(
743 out,
744 " assert(found && \"expected to contain at least one of the specified values\");"
745 );
746 let _ = writeln!(out, " }}");
747 }
748 }
749 "greater_than" => {
750 if let Some(val) = &assertion.value {
751 let c_val = json_to_c(val);
752 let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
753 }
754 }
755 "less_than" => {
756 if let Some(val) = &assertion.value {
757 let c_val = json_to_c(val);
758 let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
759 }
760 }
761 "greater_than_or_equal" => {
762 if let Some(val) = &assertion.value {
763 let c_val = json_to_c(val);
764 let _ = writeln!(
765 out,
766 " assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
767 );
768 }
769 }
770 "less_than_or_equal" => {
771 if let Some(val) = &assertion.value {
772 let c_val = json_to_c(val);
773 let _ = writeln!(
774 out,
775 " assert({field_expr} <= {c_val} && \"expected less than or equal\");"
776 );
777 }
778 }
779 "starts_with" => {
780 if let Some(expected) = &assertion.value {
781 let c_val = json_to_c(expected);
782 let _ = writeln!(
783 out,
784 " assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
785 );
786 }
787 }
788 "ends_with" => {
789 if let Some(expected) = &assertion.value {
790 let c_val = json_to_c(expected);
791 let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
792 let _ = writeln!(
793 out,
794 " strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
795 );
796 }
797 }
798 "min_length" => {
799 if let Some(val) = &assertion.value {
800 if let Some(n) = val.as_u64() {
801 let _ = writeln!(
802 out,
803 " assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
804 );
805 }
806 }
807 }
808 "max_length" => {
809 if let Some(val) = &assertion.value {
810 if let Some(n) = val.as_u64() {
811 let _ = writeln!(
812 out,
813 " assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
814 );
815 }
816 }
817 }
818 "count_min" => {
819 if let Some(val) = &assertion.value {
820 if let Some(n) = val.as_u64() {
821 let _ = writeln!(out, " {{");
822 let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
823 let _ = writeln!(
824 out,
825 " assert({field_expr} != NULL && \"expected non-null collection JSON\");"
826 );
827 let _ = writeln!(out, " int elem_count = htm_json_array_count({field_expr});");
828 let _ = writeln!(
829 out,
830 " assert(elem_count >= {n} && \"expected at least {n} elements\");"
831 );
832 let _ = writeln!(out, " }}");
833 }
834 }
835 }
836 "not_error" => {
837 }
839 "error" => {
840 }
842 other => {
843 let _ = writeln!(out, " /* TODO: unsupported assertion type: {other} */");
844 }
845 }
846}
847
848fn json_to_c(value: &serde_json::Value) -> String {
850 match value {
851 serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
852 serde_json::Value::Bool(true) => "1".to_string(),
853 serde_json::Value::Bool(false) => "0".to_string(),
854 serde_json::Value::Number(n) => n.to_string(),
855 serde_json::Value::Null => "NULL".to_string(),
856 other => format!("\"{}\"", escape_c(&other.to_string())),
857 }
858}