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