1use crate::config::E2eConfig;
7use crate::escape::{escape_zig, sanitize_filename};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, Fixture, FixtureGroup};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::ResolvedCrateConfig;
12use alef_core::hash::{self, CommentStyle};
13use alef_core::template_versions::toolchain;
14use anyhow::Result;
15use heck::{ToShoutySnakeCase, ToSnakeCase};
16use std::collections::HashSet;
17use std::fmt::Write as FmtWrite;
18use std::path::PathBuf;
19
20use super::E2eCodegen;
21use super::client;
22use super::streaming_assertions::{StreamingFieldResolver, is_streaming_virtual_field};
23
24pub struct ZigE2eCodegen;
26
27impl E2eCodegen for ZigE2eCodegen {
28 fn generate(
29 &self,
30 groups: &[FixtureGroup],
31 e2e_config: &E2eConfig,
32 config: &ResolvedCrateConfig,
33 _type_defs: &[alef_core::ir::TypeDef],
34 _enums: &[alef_core::ir::EnumDef],
35 ) -> Result<Vec<GeneratedFile>> {
36 let lang = self.language_name();
37 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
38
39 let mut files = Vec::new();
40
41 let call = &e2e_config.call;
43 let overrides = call.overrides.get(lang);
44 let _module_path = overrides
45 .and_then(|o| o.module.as_ref())
46 .cloned()
47 .unwrap_or_else(|| call.module.clone());
48 let function_name = overrides
49 .and_then(|o| o.function.as_ref())
50 .cloned()
51 .unwrap_or_else(|| call.function.clone());
52 let result_var = &call.result_var;
53
54 let zig_pkg = e2e_config.resolve_package("zig");
56 let pkg_path = zig_pkg
57 .as_ref()
58 .and_then(|p| p.path.as_ref())
59 .cloned()
60 .unwrap_or_else(|| "../../packages/zig".to_string());
61 let pkg_name = zig_pkg
62 .as_ref()
63 .and_then(|p| p.name.as_ref())
64 .cloned()
65 .unwrap_or_else(|| config.name.to_snake_case());
66
67 files.push(GeneratedFile {
69 path: output_base.join("build.zig.zon"),
70 content: render_build_zig_zon(&pkg_name, &pkg_path, e2e_config.dep_mode),
71 generated_header: false,
72 });
73
74 let module_name = config.zig_module_name();
76 let ffi_prefix = config.ffi_prefix();
77
78 let has_file_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
88 let cc = e2e_config.resolve_call_for_fixture(
89 f.call.as_deref(),
90 &f.id,
91 &f.resolved_category(),
92 &f.tags,
93 &f.input,
94 );
95 cc.args
96 .iter()
97 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
98 });
99
100 let zig_languages = config.zig.as_ref().and_then(|z| {
107 if z.languages.is_empty() {
108 None
109 } else {
110 Some(z.languages.clone())
111 }
112 });
113
114 let mut test_filenames: Vec<String> = Vec::new();
123 for group in groups {
124 let active: Vec<&Fixture> = group
125 .fixtures
126 .iter()
127 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
128 .filter(|f| {
129 if let Some(ref zig_langs) = zig_languages {
135 let fix_lang = f.input.get("language").and_then(|v| v.as_str()).or_else(|| {
136 f.input
137 .get("config")
138 .and_then(|c| c.get("language"))
139 .and_then(|v| v.as_str())
140 });
141 if let Some(fix_lang) = fix_lang
142 && !zig_langs.iter().any(|l| l == fix_lang)
143 {
144 return false;
145 }
146 }
147 true
148 })
149 .filter(|f| {
150 let cc = e2e_config.resolve_call_for_fixture(
151 f.call.as_deref(),
152 &f.id,
153 &f.resolved_category(),
154 &f.tags,
155 &f.input,
156 );
157 cc.streaming != Some(true)
158 })
159 .collect();
160
161 if active.is_empty() {
162 continue;
163 }
164
165 let filename = format!("{}_test.zig", sanitize_filename(&group.category));
166 test_filenames.push(filename.clone());
167 let content = render_test_file(
168 &group.category,
169 &active,
170 e2e_config,
171 &function_name,
172 result_var,
173 &e2e_config.call.args,
174 &module_name,
175 &ffi_prefix,
176 );
177 files.push(GeneratedFile {
178 path: output_base.join("src").join(filename),
179 content,
180 generated_header: true,
181 });
182 }
183
184 files.insert(
186 files
187 .iter()
188 .position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
189 .unwrap_or(1),
190 GeneratedFile {
191 path: output_base.join("build.zig"),
192 content: render_build_zig(
193 &test_filenames,
194 &pkg_name,
195 &module_name,
196 &config.ffi_lib_name(),
197 &config.ffi_crate_path(),
198 has_file_fixtures,
199 &e2e_config.test_documents_relative_from(0),
200 ),
201 generated_header: false,
202 },
203 );
204
205 Ok(files)
206 }
207
208 fn language_name(&self) -> &'static str {
209 "zig"
210 }
211}
212
213fn render_build_zig_zon(pkg_name: &str, pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
218 let dep_block = match dep_mode {
219 crate::config::DependencyMode::Registry => {
220 format!(r#".{{ .path = "{pkg_path}" }}"#)
223 }
224 crate::config::DependencyMode::Local => {
225 format!(r#".{{ .path = "{pkg_path}" }}"#)
226 }
227 };
228
229 let min_zig = toolchain::MIN_ZIG_VERSION;
230 let name_bytes: &[u8] = b"e2e_zig";
232 let mut crc: u32 = 0xffff_ffff;
233 for byte in name_bytes {
234 crc ^= *byte as u32;
235 for _ in 0..8 {
236 let mask = (crc & 1).wrapping_neg();
237 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
238 }
239 }
240 let name_crc: u32 = !crc;
241 let mut id: u32 = 0x811c_9dc5;
242 for byte in name_bytes {
243 id ^= *byte as u32;
244 id = id.wrapping_mul(0x0100_0193);
245 }
246 if id == 0 || id == 0xffff_ffff {
247 id = 0x1;
248 }
249 let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
250 format!(
251 r#".{{
252 .name = .e2e_zig,
253 .version = "0.1.0",
254 .fingerprint = 0x{fingerprint:016x},
255 .minimum_zig_version = "{min_zig}",
256 .dependencies = .{{
257 .{pkg_name} = {dep_block},
258 }},
259 .paths = .{{
260 "build.zig",
261 "build.zig.zon",
262 "src",
263 }},
264}}
265"#
266 )
267}
268
269fn render_build_zig(
270 test_filenames: &[String],
271 _pkg_name: &str,
272 module_name: &str,
273 ffi_lib_name: &str,
274 ffi_crate_path: &str,
275 has_file_fixtures: bool,
276 test_documents_path: &str,
277) -> String {
278 if test_filenames.is_empty() {
279 return r#"const std = @import("std");
280
281pub fn build(b: *std.Build) void {
282 const target = b.standardTargetOptions(.{});
283 const optimize = b.standardOptimizeOption(.{});
284
285 const test_step = b.step("test", "Run tests");
286}
287"#
288 .to_string();
289 }
290
291 let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
301 content.push_str(" const target = b.standardTargetOptions(.{});\n");
302 content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
303 content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n");
304 let _ = writeln!(
305 content,
306 " const ffi_path = b.option([]const u8, \"ffi_path\", \"Path to directory containing lib{ffi_lib_name}\") orelse \"../../target/release\";"
307 );
308 let _ = writeln!(
309 content,
310 " const ffi_include = b.option([]const u8, \"ffi_include_path\", \"Path to directory containing FFI header\") orelse \"{ffi_crate_path}/include\";"
311 );
312 let _ = writeln!(content);
313 let _ = writeln!(
314 content,
315 " const {module_name}_module = b.addModule(\"{module_name}\", .{{"
316 );
317 let _ = writeln!(
318 content,
319 " .root_source_file = b.path(\"../../packages/zig/src/{module_name}.zig\"),"
320 );
321 content.push_str(" .target = target,\n");
322 content.push_str(" .optimize = optimize,\n");
323 content.push_str(" .link_libc = true,\n");
327 content.push_str(" });\n");
328 let _ = writeln!(
329 content,
330 " {module_name}_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});"
331 );
332 let _ = writeln!(
333 content,
334 " {module_name}_module.addIncludePath(.{{ .cwd_relative = ffi_include }});"
335 );
336 let _ = writeln!(
337 content,
338 " {module_name}_module.linkSystemLibrary(\"{ffi_lib_name}\", .{{}});"
339 );
340 let _ = writeln!(content);
341
342 for filename in test_filenames {
343 let test_name = filename.trim_end_matches("_test.zig");
345 content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
346 content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
347 content.push_str(" .target = target,\n");
348 content.push_str(" .optimize = optimize,\n");
349 content.push_str(" .link_libc = true,\n");
353 content.push_str(" });\n");
354 content.push_str(&format!(
355 " {test_name}_module.addImport(\"{module_name}\", {module_name}_module);\n"
356 ));
357 content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
373 content.push_str(&format!(" .name = \"{test_name}_test\",\n"));
374 content.push_str(&format!(" .root_module = {test_name}_module,\n"));
375 content.push_str(" .use_llvm = true,\n");
376 content.push_str(" });\n");
377 content.push_str(&format!(
394 " const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
395 ));
396 if has_file_fixtures {
397 content.push_str(&format!(
398 " {test_name}_run.setCwd(b.path(\"{test_documents_path}\"));\n"
399 ));
400 }
401 content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
402 }
403
404 content.push_str("}\n");
405 content
406}
407
408struct ZigTestClientRenderer;
416
417impl client::TestClientRenderer for ZigTestClientRenderer {
418 fn language_name(&self) -> &'static str {
419 "zig"
420 }
421
422 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
423 if let Some(reason) = skip_reason {
424 let _ = writeln!(out, "test \"{fn_name}\" {{");
425 let _ = writeln!(out, " // {description}");
426 let _ = writeln!(out, " // skipped: {reason}");
427 let _ = writeln!(out, " return error.SkipZigTest;");
428 } else {
429 let _ = writeln!(out, "test \"{fn_name}\" {{");
430 let _ = writeln!(out, " // {description}");
431 }
432 }
433
434 fn render_test_close(&self, out: &mut String) {
435 let _ = writeln!(out, "}}");
436 }
437
438 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
439 let method = ctx.method.to_uppercase();
440 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
441
442 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
443 let _ = writeln!(out, " defer _ = gpa.deinit();");
444 let _ = writeln!(out, " const allocator = gpa.allocator();");
445
446 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
447 let _ = writeln!(
448 out,
449 " const url = try std.fmt.bufPrint(&url_buf, \"{{s}}/fixtures/{fixture_id}\", .{{if (std.c.getenv(\"MOCK_SERVER_URL\")) |v| std.mem.span(v) else \"http://localhost:8080\"}});"
450 );
451
452 if !ctx.headers.is_empty() {
454 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
455 header_pairs.sort_by_key(|(k, _)| k.as_str());
456 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
457 for (k, v) in &header_pairs {
458 let ek = escape_zig(k);
459 let ev = escape_zig(v);
460 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
461 }
462 let _ = writeln!(out, " }};");
463 }
464
465 if let Some(body) = ctx.body {
467 let json_str = serde_json::to_string(body).unwrap_or_default();
468 let escaped = escape_zig(&json_str);
469 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
470 }
471
472 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
473 let has_body = ctx.body.is_some();
474
475 let _ = writeln!(
476 out,
477 " var http_client = std.http.Client{{ .allocator = allocator }};"
478 );
479 let _ = writeln!(out, " defer http_client.deinit();");
480 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
481 let _ = writeln!(out, " defer response_body.deinit();");
482
483 let method_zig = match method.as_str() {
484 "GET" => ".GET",
485 "POST" => ".POST",
486 "PUT" => ".PUT",
487 "DELETE" => ".DELETE",
488 "PATCH" => ".PATCH",
489 "HEAD" => ".HEAD",
490 "OPTIONS" => ".OPTIONS",
491 _ => ".GET",
492 };
493
494 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
495 let _ = writeln!(
496 out,
497 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
498 rv = ctx.response_var,
499 );
500 }
501
502 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
503 let _ = writeln!(
504 out,
505 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
506 );
507 }
508
509 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
510 let ename = escape_zig(&name.to_lowercase());
511 match expected {
512 "<<present>>" => {
513 let _ = writeln!(
514 out,
515 " // assert header '{ename}' is present (header inspection not yet implemented)"
516 );
517 }
518 "<<absent>>" => {
519 let _ = writeln!(
520 out,
521 " // assert header '{ename}' is absent (header inspection not yet implemented)"
522 );
523 }
524 "<<uuid>>" => {
525 let _ = writeln!(
526 out,
527 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
528 );
529 }
530 exact => {
531 let evalue = escape_zig(exact);
532 let _ = writeln!(
533 out,
534 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
535 );
536 }
537 }
538 }
539
540 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
541 let json_str = serde_json::to_string(expected).unwrap_or_default();
542 let escaped = escape_zig(&json_str);
543 let _ = writeln!(
544 out,
545 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
546 );
547 }
548
549 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
550 if let Some(obj) = expected.as_object() {
551 for (key, val) in obj {
552 let ekey = escape_zig(key);
553 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
554 let _ = writeln!(
555 out,
556 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
557 );
558 }
559 }
560 }
561
562 fn render_assert_validation_errors(
563 &self,
564 out: &mut String,
565 _response_var: &str,
566 errors: &[crate::fixture::ValidationErrorExpectation],
567 ) {
568 for ve in errors {
569 let loc = ve.loc.join(".");
570 let escaped_loc = escape_zig(&loc);
571 let escaped_msg = escape_zig(&ve.msg);
572 let _ = writeln!(
573 out,
574 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
575 );
576 }
577 }
578}
579
580fn render_http_test_case(out: &mut String, fixture: &Fixture) {
585 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
586}
587
588#[allow(clippy::too_many_arguments)]
593fn render_test_file(
594 category: &str,
595 fixtures: &[&Fixture],
596 e2e_config: &E2eConfig,
597 function_name: &str,
598 result_var: &str,
599 args: &[crate::config::ArgMapping],
600 module_name: &str,
601 ffi_prefix: &str,
602) -> String {
603 let mut out = String::new();
604 out.push_str(&hash::header(CommentStyle::DoubleSlash));
605 let _ = writeln!(out, "const std = @import(\"std\");");
606 let _ = writeln!(out, "const testing = std.testing;");
607 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
608 let _ = writeln!(out);
609
610 let _ = writeln!(out, "// E2e tests for category: {category}");
611 let _ = writeln!(out);
612
613 for fixture in fixtures {
614 if fixture.http.is_some() {
615 render_http_test_case(&mut out, fixture);
616 } else {
617 render_test_fn(
618 &mut out,
619 fixture,
620 e2e_config,
621 function_name,
622 result_var,
623 args,
624 module_name,
625 ffi_prefix,
626 );
627 }
628 let _ = writeln!(out);
629 }
630
631 out
632}
633
634#[allow(clippy::too_many_arguments)]
635fn render_test_fn(
636 out: &mut String,
637 fixture: &Fixture,
638 e2e_config: &E2eConfig,
639 _function_name: &str,
640 _result_var: &str,
641 _args: &[crate::config::ArgMapping],
642 module_name: &str,
643 ffi_prefix: &str,
644) {
645 let call_config = e2e_config.resolve_call_for_fixture(
647 fixture.call.as_deref(),
648 &fixture.id,
649 &fixture.resolved_category(),
650 &fixture.tags,
651 &fixture.input,
652 );
653 let call_field_resolver = FieldResolver::new(
654 e2e_config.effective_fields(call_config),
655 e2e_config.effective_fields_optional(call_config),
656 e2e_config.effective_result_fields(call_config),
657 e2e_config.effective_fields_array(call_config),
658 e2e_config.effective_fields_method_calls(call_config),
659 );
660 let field_resolver = &call_field_resolver;
661 let enum_fields = e2e_config.effective_fields_enum(call_config);
662 let lang = "zig";
663 let call_overrides = call_config.overrides.get(lang);
664 let function_name = call_overrides
665 .and_then(|o| o.function.as_ref())
666 .cloned()
667 .unwrap_or_else(|| call_config.function.clone());
668 let result_var = &call_config.result_var;
669 let args = &call_config.args;
670 let client_factory = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
675 e2e_config
676 .call
677 .overrides
678 .get(lang)
679 .and_then(|o| o.client_factory.as_deref())
680 });
681
682 let call_result_is_bytes = call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
699 let result_is_json_struct =
700 !call_result_is_bytes && (call_overrides.is_some_and(|o| o.result_is_json_struct) || client_factory.is_some());
701
702 let result_is_option = call_overrides.is_some_and(|o| o.result_is_option) || call_config.result_is_option;
707
708 let call_returns_error_union = call_overrides.and_then(|o| o.returns_result) != Some(false);
729
730 let test_name = fixture.id.to_snake_case();
731 let description = &fixture.description;
732 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
733
734 let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
735 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
741 let args_str = if extra_args.is_empty() {
742 args_str
743 } else if args_str.is_empty() {
744 extra_args.join(", ")
745 } else {
746 format!("{args_str}, {}", extra_args.join(", "))
747 };
748
749 let any_happy_emits_code = fixture
752 .assertions
753 .iter()
754 .any(|a| assertion_emits_code(a, field_resolver));
755 let any_non_error_emits_code = fixture
756 .assertions
757 .iter()
758 .filter(|a| a.assertion_type != "error")
759 .any(|a| assertion_emits_code(a, field_resolver));
760
761 let has_streaming_virtual_assertions = fixture.assertions.iter().any(|a| {
763 a.field
764 .as_ref()
765 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
766 });
767 let is_stream_fn = function_name.contains("stream");
768 let uses_streaming_virtual_path =
769 result_is_json_struct && has_streaming_virtual_assertions && is_stream_fn && client_factory.is_some();
770 let streaming_path_has_non_streaming = uses_streaming_virtual_path
772 && fixture.assertions.iter().any(|a| {
773 !a.field
774 .as_ref()
775 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
776 && !matches!(a.assertion_type.as_str(), "not_error" | "error")
777 && a.field
778 .as_ref()
779 .is_some_and(|f| !f.is_empty() && field_resolver.is_valid_for_result(f))
780 });
781
782 let _ = writeln!(out, "test \"{test_name}\" {{");
783 let _ = writeln!(out, " // {description}");
784
785 if let Some(visitor_spec) = &fixture.visitor {
790 let html = fixture.input.get("html").and_then(|v| v.as_str()).unwrap_or_default();
791 let options_value = fixture.input.get("options").cloned();
792 emit_visitor_test_body(
793 out,
794 &fixture.id,
795 html,
796 options_value.as_ref(),
797 visitor_spec,
798 module_name,
799 &fixture.assertions,
800 expects_error,
801 field_resolver,
802 );
803 let _ = writeln!(out, "}}");
804 let _ = writeln!(out);
805 return;
806 }
807
808 let needs_gpa = setup_needs_gpa
816 || streaming_path_has_non_streaming
817 || (!uses_streaming_virtual_path && result_is_json_struct && !expects_error && any_happy_emits_code)
818 || (!uses_streaming_virtual_path && result_is_json_struct && expects_error && any_non_error_emits_code);
819 if needs_gpa {
820 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
821 let _ = writeln!(out, " defer _ = gpa.deinit();");
822 let _ = writeln!(out, " const allocator = gpa.allocator();");
823 let _ = writeln!(out);
824 }
825
826 for line in &setup_lines {
827 let _ = writeln!(out, " {line}");
828 }
829
830 let call_prefix = if let Some(factory) = client_factory {
835 let fixture_id = &fixture.id;
836 let _ = writeln!(
837 out,
838 " const _mock_url = try std.fmt.allocPrintSentinel(std.heap.c_allocator, \"{{s}}/fixtures/{fixture_id}\", .{{if (std.c.getenv(\"MOCK_SERVER_URL\")) |v| std.mem.span(v) else \"http://localhost:8080\"}}, 0);"
839 );
840 let _ = writeln!(out, " defer std.heap.c_allocator.free(_mock_url);");
841 let _ = writeln!(
842 out,
843 " var _client = try {module_name}.{factory}(\"test-key\", _mock_url, null, null, null);"
844 );
845 let _ = writeln!(out, " defer _client.free();");
846 "_client".to_string()
847 } else {
848 module_name.to_string()
849 };
850
851 if expects_error {
852 if result_is_json_struct {
856 let _ = writeln!(
857 out,
858 " const _result_json = {call_prefix}.{function_name}({args_str}) catch {{"
859 );
860 } else {
861 let _ = writeln!(
862 out,
863 " const result = {call_prefix}.{function_name}({args_str}) catch {{"
864 );
865 }
866 let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
867 let _ = writeln!(out, " return;");
868 let _ = writeln!(out, " }};");
869 let any_emits_code = fixture
873 .assertions
874 .iter()
875 .filter(|a| a.assertion_type != "error")
876 .any(|a| assertion_emits_code(a, field_resolver));
877 if result_is_json_struct && any_emits_code {
878 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
879 let _ = writeln!(
880 out,
881 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
882 );
883 let _ = writeln!(out, " defer _parsed.deinit();");
884 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
885 let _ = writeln!(out, " // Perform success assertions if any");
886 for assertion in &fixture.assertions {
887 if assertion.assertion_type != "error" {
888 render_json_assertion(out, assertion, result_var, field_resolver, false);
889 }
890 }
891 } else if result_is_json_struct {
892 let _ = writeln!(out, " _ = _result_json;");
893 } else if any_emits_code {
894 let _ = writeln!(out, " // Perform success assertions if any");
895 for assertion in &fixture.assertions {
896 if assertion.assertion_type != "error" {
897 render_assertion(
898 out,
899 assertion,
900 result_var,
901 field_resolver,
902 enum_fields,
903 result_is_option,
904 );
905 }
906 }
907 } else {
908 let _ = writeln!(out, " _ = result;");
909 }
910 } else if fixture.assertions.is_empty() {
911 if result_is_json_struct {
913 let _ = writeln!(
914 out,
915 " const _result_json = try {call_prefix}.{function_name}({args_str});"
916 );
917 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
918 } else if call_returns_error_union {
919 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
920 } else {
921 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
922 }
923 } else {
924 let any_emits_code = fixture
928 .assertions
929 .iter()
930 .any(|a| assertion_emits_code(a, field_resolver));
931 if call_result_is_bytes && client_factory.is_some() {
932 let _ = writeln!(
935 out,
936 " const _result_json = try {call_prefix}.{function_name}({args_str});"
937 );
938 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
939 let has_bytes_assertions = fixture
940 .assertions
941 .iter()
942 .any(|a| matches!(a.assertion_type.as_str(), "not_empty" | "is_empty"));
943 if has_bytes_assertions {
944 for assertion in &fixture.assertions {
945 match assertion.assertion_type.as_str() {
946 "not_empty" => {
947 let _ = writeln!(out, " try testing.expect(_result_json.len > 0);");
948 }
949 "is_empty" => {
950 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _result_json.len);");
951 }
952 "not_error" | "error" => {}
953 _ => {
954 let atype = &assertion.assertion_type;
955 let _ = writeln!(
956 out,
957 " // bytes result: assertion '{atype}' not implemented for zig bytes"
958 );
959 }
960 }
961 }
962 }
963 } else if result_is_json_struct {
964 if uses_streaming_virtual_path {
968 let request_from_json = format!("{ffi_prefix}_chat_completion_request_from_json");
969 let request_free = format!("{ffi_prefix}_chat_completion_request_free");
970 let stream_start = format!("{ffi_prefix}_default_client_chat_stream_start");
971 let stream_free = format!("{ffi_prefix}_default_client_chat_stream_free");
972 let client_c_type = format!("{}DefaultClient", ffi_prefix.to_shouty_snake_case());
973
974 let _ = writeln!(
977 out,
978 " const _req_z = try std.heap.c_allocator.dupeZ(u8, {args_str});"
979 );
980 let _ = writeln!(out, " defer std.heap.c_allocator.free(_req_z);");
981 let _ = writeln!(
982 out,
983 " const _req_handle = {module_name}.c.{request_from_json}(_req_z.ptr);"
984 );
985 let _ = writeln!(out, " defer {module_name}.c.{request_free}(_req_handle);");
986 let _ = writeln!(
987 out,
988 " const _stream_handle = {module_name}.c.{stream_start}(@as(*{module_name}.c.{client_c_type}, @ptrCast(_client._handle)), _req_handle);"
989 );
990 let _ = writeln!(out, " if (_stream_handle == null) return error.StreamStartFailed;");
991 let _ = writeln!(out, " defer {module_name}.c.{stream_free}(_stream_handle);");
992 let snip =
994 StreamingFieldResolver::collect_snippet_zig("_stream_handle", "chunks", module_name, ffi_prefix);
995 out.push_str(" ");
996 out.push_str(&snip);
997 out.push('\n');
998 if streaming_path_has_non_streaming {
1001 let _ = writeln!(
1002 out,
1003 " const _result_json = if (chunks.items.len > 0) chunks.items[chunks.items.len - 1] else &[_]u8{{}};"
1004 );
1005 let _ = writeln!(
1006 out,
1007 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
1008 );
1009 let _ = writeln!(out, " defer _parsed.deinit();");
1010 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
1011 }
1012 for assertion in &fixture.assertions {
1013 render_json_assertion(out, assertion, result_var, field_resolver, true);
1014 }
1015 } else {
1016 let _ = writeln!(
1018 out,
1019 " const _result_json = try {call_prefix}.{function_name}({args_str});"
1020 );
1021 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
1022 if any_emits_code {
1023 let wrap_field = match function_name.as_str() {
1028 "interact" => Some("interaction"),
1029 _ => None,
1030 };
1031
1032 let parse_json_var = if let Some(field) = wrap_field {
1033 let _ = writeln!(
1037 out,
1038 " const _wrapped_json = try std.fmt.allocPrint(allocator, \"{{\\\"{}\\\":{{s}}}}\", .{{_result_json}});",
1039 field
1040 );
1041 let _ = writeln!(out, " defer allocator.free(_wrapped_json);");
1042 "_wrapped_json".to_string()
1043 } else {
1044 "_result_json".to_string()
1045 };
1046
1047 let _ = writeln!(
1048 out,
1049 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, {parse_json_var}, .{{}});"
1050 );
1051 let _ = writeln!(out, " defer _parsed.deinit();");
1052 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
1053 for assertion in &fixture.assertions {
1054 render_json_assertion(out, assertion, result_var, field_resolver, false);
1055 }
1056 }
1057 }
1058 } else if any_emits_code {
1059 let try_kw = if call_returns_error_union { "try " } else { "" };
1060 let _ = writeln!(
1061 out,
1062 " const {result_var} = {try_kw}{call_prefix}.{function_name}({args_str});"
1063 );
1064 for assertion in &fixture.assertions {
1065 render_assertion(
1066 out,
1067 assertion,
1068 result_var,
1069 field_resolver,
1070 enum_fields,
1071 result_is_option,
1072 );
1073 }
1074 } else if call_returns_error_union {
1075 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
1076 } else {
1077 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
1078 }
1079 }
1080
1081 let _ = writeln!(out, "}}");
1082}
1083
1084#[allow(clippy::too_many_arguments)]
1090fn emit_visitor_test_body(
1091 out: &mut String,
1092 fixture_id: &str,
1093 html: &str,
1094 options_value: Option<&serde_json::Value>,
1095 visitor_spec: &crate::fixture::VisitorSpec,
1096 module_name: &str,
1097 assertions: &[Assertion],
1098 expects_error: bool,
1099 field_resolver: &FieldResolver,
1100) {
1101 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
1103 let _ = writeln!(out, " defer _ = gpa.deinit();");
1104 let _ = writeln!(out, " const allocator = gpa.allocator();");
1105 let _ = writeln!(out);
1106
1107 let visitor_block = super::zig_visitors::build_zig_visitor(fixture_id, module_name, visitor_spec);
1109 out.push_str(&visitor_block);
1110
1111 let _ = writeln!(
1114 out,
1115 " const _visitor = {module_name}.c.htm_visitor_create(&_callbacks);"
1116 );
1117 let _ = writeln!(out, " defer {module_name}.c.htm_visitor_free(_visitor);");
1118
1119 let options_json = match options_value {
1123 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1124 None => "{}".to_string(),
1125 };
1126 let escaped_options = escape_zig(&options_json);
1127 let _ = writeln!(
1128 out,
1129 " const _options_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_options}\");"
1130 );
1131 let _ = writeln!(out, " defer std.heap.c_allocator.free(_options_z);");
1132 let _ = writeln!(
1133 out,
1134 " const _options = {module_name}.c.htm_conversion_options_from_json(_options_z.ptr);"
1135 );
1136 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_options_free(_options);");
1137 let _ = writeln!(
1138 out,
1139 " {module_name}.c.htm_options_set_visitor_handle(_options, _visitor);"
1140 );
1141
1142 let escaped_html = escape_zig(html);
1144 let _ = writeln!(
1145 out,
1146 " const _html_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_html}\");"
1147 );
1148 let _ = writeln!(out, " defer std.heap.c_allocator.free(_html_z);");
1149 let _ = writeln!(
1150 out,
1151 " const _result = {module_name}.c.htm_convert(_html_z.ptr, _options);"
1152 );
1153
1154 if expects_error {
1155 let _ = writeln!(
1157 out,
1158 " try testing.expect(_result == null or {module_name}.c.htm_last_error_code() != 0);"
1159 );
1160 let _ = writeln!(
1161 out,
1162 " if (_result) |r| {module_name}.c.htm_conversion_result_free(r);"
1163 );
1164 return;
1165 }
1166
1167 let _ = writeln!(out, " try testing.expect(_result != null);");
1168 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_result_free(_result.?);");
1169 let _ = writeln!(
1170 out,
1171 " const _json_ptr = {module_name}.c.htm_conversion_result_to_json(_result.?);"
1172 );
1173 let _ = writeln!(out, " defer {module_name}.c.htm_free_string(_json_ptr);");
1174 let _ = writeln!(out, " const _result_json = std.mem.sliceTo(_json_ptr, 0);");
1175 let _ = writeln!(
1176 out,
1177 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
1178 );
1179 let _ = writeln!(out, " defer _parsed.deinit();");
1180 let _ = writeln!(out, " const result = &_parsed.value;");
1181
1182 for assertion in assertions {
1183 if assertion.assertion_type != "error" {
1184 render_json_assertion(out, assertion, "result", field_resolver, false);
1185 }
1186 }
1187}
1188
1189const FORMAT_METADATA_VARIANTS: &[&str] = &[
1207 "pdf",
1208 "docx",
1209 "excel",
1210 "email",
1211 "pptx",
1212 "archive",
1213 "image",
1214 "xml",
1215 "text",
1216 "html",
1217 "ocr",
1218 "csv",
1219 "bibtex",
1220 "citation",
1221 "fiction_book",
1222 "dbf",
1223 "jats",
1224 "epub",
1225 "pst",
1226 "code",
1227];
1228
1229fn json_path_expr(result_var: &str, field_path: &str) -> String {
1230 let segments: Vec<&str> = field_path.split('.').collect();
1231 let mut expr = result_var.to_string();
1232 let mut prev_seg: Option<&str> = None;
1233 for seg in &segments {
1234 if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
1239 prev_seg = Some(seg);
1240 continue;
1241 }
1242 if let Some(key) = seg.strip_suffix("[]") {
1246 expr = format!("{expr}.object.get(\"{key}\").?.array.items[0]");
1247 } else if let Some(bracket_pos) = seg.find('[') {
1248 if let Some(end_pos) = seg.find(']') {
1249 if end_pos > bracket_pos + 1 && end_pos == seg.len() - 1 {
1250 let key = &seg[..bracket_pos];
1251 let idx = &seg[bracket_pos + 1..end_pos];
1252 if idx.chars().all(|c| c.is_ascii_digit()) {
1253 expr = format!("{expr}.object.get(\"{key}\").?.array.items[{idx}]");
1254 prev_seg = Some(seg);
1255 continue;
1256 }
1257 expr = format!("{expr}.object.get(\"{key}\").?.object.get(\"{idx}\").?");
1263 prev_seg = Some(seg);
1264 continue;
1265 }
1266 }
1267 expr = format!("{expr}.object.get(\"{seg}\").?");
1268 } else {
1269 expr = format!("{expr}.object.get(\"{seg}\").?");
1270 }
1271 prev_seg = Some(seg);
1272 }
1273 expr
1274}
1275
1276fn emit_zig_chunks_predicate(
1281 out: &mut String,
1282 result_var: &str,
1283 assertion_type: &str,
1284 chunk_field_accessor: &str,
1285 field_name: &str,
1286 require_non_empty_string: bool,
1287) {
1288 let _ = writeln!(out, " {{");
1289 let _ = writeln!(out, " const _chunks_opt = {result_var}.object.get(\"chunks\");");
1290 let _ = writeln!(out, " var _all: bool = true;");
1291 let _ = writeln!(out, " if (_chunks_opt) |_chunks_val| {{");
1292 let _ = writeln!(out, " if (_chunks_val == .array) {{");
1293 let _ = writeln!(
1294 out,
1295 " if (_chunks_val.array.items.len == 0) _all = false;"
1296 );
1297 let _ = writeln!(out, " for (_chunks_val.array.items) |c| {{");
1298 let _ = writeln!(out, " if (c != .object) {{ _all = false; break; }}");
1299 let _ = writeln!(out, " const _v = {chunk_field_accessor};");
1300 if require_non_empty_string {
1301 let _ = writeln!(
1302 out,
1303 " if (_v == null or _v.? != .string or _v.?.string.len == 0) {{ _all = false; break; }}"
1304 );
1305 } else {
1306 let _ = writeln!(
1307 out,
1308 " if (_v == null or _v.? == .null) {{ _all = false; break; }}"
1309 );
1310 }
1311 let _ = writeln!(out, " }}");
1312 let _ = writeln!(out, " }} else {{ _all = false; }}");
1313 let _ = writeln!(out, " }} else {{ _all = false; }}");
1314 match assertion_type {
1315 "is_true" => {
1316 let _ = writeln!(out, " try testing.expect(_all);");
1317 }
1318 "is_false" => {
1319 let _ = writeln!(out, " try testing.expect(!_all);");
1320 }
1321 _ => {
1322 let _ = writeln!(
1323 out,
1324 " // skipped: unsupported assertion type on synthetic field '{field_name}'"
1325 );
1326 }
1327 }
1328 let _ = writeln!(out, " }}");
1329}
1330
1331fn render_json_assertion(
1336 out: &mut String,
1337 assertion: &Assertion,
1338 result_var: &str,
1339 field_resolver: &FieldResolver,
1340 uses_streaming: bool,
1341) {
1342 if let Some(f) = &assertion.field {
1349 if uses_streaming && !f.is_empty() && is_streaming_virtual_field(f) {
1350 if let Some(expr) = StreamingFieldResolver::accessor(f, "zig", "chunks") {
1351 match assertion.assertion_type.as_str() {
1352 "count_min" => {
1353 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1354 let _ = writeln!(out, " try testing.expect({expr}.len >= {n});");
1355 }
1356 }
1357 "count_equals" => {
1358 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1359 let _ = writeln!(out, " try testing.expectEqual(@as(usize, {n}), {expr}.len);");
1360 }
1361 }
1362 "equals" => {
1363 if let Some(serde_json::Value::String(s)) = &assertion.value {
1364 let escaped = escape_zig(s);
1365 let _ = writeln!(out, " try testing.expectEqualStrings(\"{escaped}\", {expr});");
1366 } else if let Some(v) = &assertion.value {
1367 let zig_val = json_to_zig(v);
1368 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {expr});");
1369 }
1370 }
1371 "not_empty" => {
1372 let _ = writeln!(out, " try testing.expect({expr}.len > 0);");
1373 }
1374 "is_true" => {
1375 let _ = writeln!(out, " try testing.expect({expr});");
1376 }
1377 "is_false" => {
1378 let _ = writeln!(out, " try testing.expect(!{expr});");
1379 }
1380 _ => {
1381 let atype = &assertion.assertion_type;
1382 let _ = writeln!(
1383 out,
1384 " // streaming virtual field '{f}' assertion '{atype}' not implemented for zig"
1385 );
1386 }
1387 }
1388 }
1389 return;
1390 }
1391 }
1392
1393 if let Some(f) = &assertion.field {
1400 if f == "embeddings" && !field_resolver.has_explicit_field("embeddings") {
1401 match assertion.assertion_type.as_str() {
1402 "count_min" => {
1403 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1404 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len >= {n});");
1405 }
1406 return;
1407 }
1408 "count_equals" => {
1409 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1410 let _ = writeln!(
1411 out,
1412 " try testing.expectEqual(@as(usize, {n}), {result_var}.array.items.len);"
1413 );
1414 }
1415 return;
1416 }
1417 "not_empty" => {
1418 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len > 0);");
1419 return;
1420 }
1421 "is_empty" => {
1422 let _ = writeln!(
1423 out,
1424 " try testing.expectEqual(@as(usize, 0), {result_var}.array.items.len);"
1425 );
1426 return;
1427 }
1428 _ => {}
1429 }
1430 }
1431 }
1432
1433 if let Some(f) = &assertion.field {
1439 match f.as_str() {
1440 "chunks_have_content" => {
1441 emit_zig_chunks_predicate(
1442 out,
1443 result_var,
1444 assertion.assertion_type.as_str(),
1445 "c.object.get(\"content\")",
1446 "chunks_have_content",
1447 true,
1448 );
1449 return;
1450 }
1451 "chunks_have_heading_context" => {
1452 let _ = writeln!(
1457 out,
1458 " // skipped: synthetic field 'chunks_have_heading_context' not derivable from JSON value alone"
1459 );
1460 return;
1461 }
1462 "first_chunk_starts_with_heading" => {
1463 let _ = writeln!(
1464 out,
1465 " // skipped: synthetic field 'first_chunk_starts_with_heading' not derivable from JSON value alone"
1466 );
1467 return;
1468 }
1469 "chunks_have_embeddings" => {
1470 emit_zig_chunks_predicate(
1471 out,
1472 result_var,
1473 assertion.assertion_type.as_str(),
1474 "c.object.get(\"embedding\")",
1475 "chunks_have_embeddings",
1476 false,
1477 );
1478 return;
1479 }
1480 "keywords" | "keywords_count" => {
1485 let _ = writeln!(
1486 out,
1487 " // skipped: field '{f}' not available on JSON-struct ExtractionResult"
1488 );
1489 return;
1490 }
1491 _ => {}
1492 }
1493 }
1494
1495 if let Some(f) = &assertion.field {
1497 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1498 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1499 return;
1500 }
1501 }
1502 if matches!(assertion.assertion_type.as_str(), "not_error" | "error") {
1504 return;
1505 }
1506
1507 let raw_field_path = assertion.field.as_deref().unwrap_or("").trim();
1508 let field_path = if raw_field_path.is_empty() {
1509 raw_field_path.to_string()
1510 } else {
1511 field_resolver.resolve(raw_field_path).to_string()
1512 };
1513 let field_path = field_path.trim();
1514
1515 let (field_path_for_expr, is_length_access) = if let Some(parent) = field_path.strip_suffix(".length") {
1517 (parent, true)
1518 } else {
1519 (field_path, false)
1520 };
1521
1522 let field_expr = if field_path_for_expr.is_empty() {
1523 result_var.to_string()
1524 } else {
1525 json_path_expr(result_var, field_path_for_expr)
1526 };
1527
1528 if field_path_for_expr == "metadata.format"
1534 && matches!(
1535 assertion.assertion_type.as_str(),
1536 "equals" | "contains" | "not_empty" | "is_empty" | "starts_with" | "ends_with"
1537 )
1538 {
1539 let base = json_path_expr(result_var, field_path_for_expr);
1540 let _ = writeln!(out, " {{");
1541 let _ = writeln!(out, " const _fmt_obj = {base}.object;");
1542 let _ = writeln!(out, " const _fmt_type = _fmt_obj.get(\"format_type\").?.string;");
1543 let _ = writeln!(
1544 out,
1545 " const _fmt_display: []const u8 = if (std.mem.eql(u8, _fmt_type, \"image\")) _fmt_obj.get(\"format\").?.string else _fmt_type;"
1546 );
1547 match assertion.assertion_type.as_str() {
1548 "equals" => {
1549 if let Some(serde_json::Value::String(s)) = &assertion.value {
1550 let escaped = escape_zig(s);
1551 let _ = writeln!(
1552 out,
1553 " try testing.expectEqualStrings(\"{escaped}\", std.mem.trim(u8, _fmt_display, \" \\n\\r\\t\"));"
1554 );
1555 }
1556 }
1557 "contains" => {
1558 if let Some(serde_json::Value::String(s)) = &assertion.value {
1559 let escaped = escape_zig(s);
1560 let _ = writeln!(
1561 out,
1562 " try testing.expect(std.mem.indexOf(u8, _fmt_display, \"{escaped}\") != null);"
1563 );
1564 }
1565 }
1566 "starts_with" => {
1567 if let Some(serde_json::Value::String(s)) = &assertion.value {
1568 let escaped = escape_zig(s);
1569 let _ = writeln!(
1570 out,
1571 " try testing.expect(std.mem.startsWith(u8, _fmt_display, \"{escaped}\"));"
1572 );
1573 }
1574 }
1575 "ends_with" => {
1576 if let Some(serde_json::Value::String(s)) = &assertion.value {
1577 let escaped = escape_zig(s);
1578 let _ = writeln!(
1579 out,
1580 " try testing.expect(std.mem.endsWith(u8, _fmt_display, \"{escaped}\"));"
1581 );
1582 }
1583 }
1584 "not_empty" => {
1585 let _ = writeln!(out, " try testing.expect(_fmt_display.len > 0);");
1586 }
1587 "is_empty" => {
1588 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _fmt_display.len);");
1589 }
1590 _ => {}
1591 }
1592 let _ = writeln!(out, " }}");
1593 return;
1594 }
1595
1596 let zig_val = match &assertion.value {
1598 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_zig(s)),
1599 _ => String::new(),
1600 };
1601 let is_string_val = matches!(&assertion.value, Some(serde_json::Value::String(_)));
1602 let is_bool_val = matches!(&assertion.value, Some(serde_json::Value::Bool(_)));
1603 let bool_val = match &assertion.value {
1604 Some(serde_json::Value::Bool(b)) if *b => "true",
1605 _ => "false",
1606 };
1607 let is_null_val = matches!(&assertion.value, Some(serde_json::Value::Null));
1608 let n = assertion.value.as_ref().map(json_to_zig).unwrap_or_default();
1609 let has_n = assertion.value.as_ref().is_some_and(|v| v.is_number() || v.is_u64());
1610 let is_float_val = matches!(&assertion.value, Some(serde_json::Value::Number(n)) if !n.is_i64() && !n.is_u64());
1615 let values_list: Vec<String> = assertion
1616 .values
1617 .as_deref()
1618 .unwrap_or_default()
1619 .iter()
1620 .filter_map(|v| {
1621 if let serde_json::Value::String(s) = v {
1622 Some(format!("\"{}\"", escape_zig(s)))
1623 } else {
1624 None
1625 }
1626 })
1627 .collect();
1628
1629 let rendered = crate::template_env::render(
1630 "zig/json_assertion.jinja",
1631 minijinja::context! {
1632 assertion_type => assertion.assertion_type.as_str(),
1633 field_expr => field_expr,
1634 is_length_access => is_length_access,
1635 zig_val => zig_val,
1636 is_string_val => is_string_val,
1637 is_bool_val => is_bool_val,
1638 bool_val => bool_val,
1639 is_null_val => is_null_val,
1640 n => n,
1641 has_n => has_n,
1642 is_float_val => is_float_val,
1643 values_list => values_list,
1644 },
1645 );
1646 out.push_str(&rendered);
1647}
1648
1649fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
1652 if let Some(f) = &assertion.field {
1653 if !f.is_empty() && is_streaming_virtual_field(f) {
1654 } else if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1657 return false;
1658 }
1659 }
1660 matches!(
1661 assertion.assertion_type.as_str(),
1662 "equals"
1663 | "contains"
1664 | "contains_all"
1665 | "not_contains"
1666 | "not_empty"
1667 | "is_empty"
1668 | "starts_with"
1669 | "ends_with"
1670 | "min_length"
1671 | "max_length"
1672 | "count_min"
1673 | "count_equals"
1674 | "is_true"
1675 | "is_false"
1676 | "greater_than"
1677 | "less_than"
1678 | "greater_than_or_equal"
1679 | "less_than_or_equal"
1680 | "contains_any"
1681 )
1682}
1683
1684fn build_args_and_setup(
1689 input: &serde_json::Value,
1690 args: &[crate::config::ArgMapping],
1691 fixture_id: &str,
1692 _module_name: &str,
1693) -> (Vec<String>, String, bool) {
1694 if args.is_empty() {
1695 return (Vec::new(), String::new(), false);
1696 }
1697
1698 let mut setup_lines: Vec<String> = Vec::new();
1699 let mut parts: Vec<String> = Vec::new();
1700 let mut setup_needs_gpa = false;
1701
1702 for arg in args {
1703 if arg.arg_type == "mock_url" {
1704 let name = arg.name.clone();
1705 let id_upper = fixture_id.to_uppercase();
1706 setup_lines.push(format!(
1707 "const {name} = if (std.c.getenv(\"MOCK_SERVER_{id_upper}\")) |_pf| try std.fmt.allocPrint(allocator, \"{{s}}\", .{{std.mem.span(_pf)}}) else try std.fmt.allocPrint(allocator, \"{{s}}/fixtures/{fixture_id}\", .{{if (std.c.getenv(\"MOCK_SERVER_URL\")) |v| std.mem.span(v) else \"http://localhost:8080\"}});"
1708 ));
1709 setup_lines.push(format!("defer allocator.free({name});"));
1710 parts.push(name);
1711 setup_needs_gpa = true;
1712 continue;
1713 }
1714
1715 if arg.arg_type == "handle" {
1718 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1719 let json_str = match input.get(field) {
1720 Some(serde_json::Value::Null) | None => "null".to_string(),
1721 Some(v) => format!("\"{}\"", escape_zig(&serde_json::to_string(v).unwrap_or_default())),
1722 };
1723 parts.push(json_str);
1724 continue;
1725 }
1726
1727 if arg.name == "config" && arg.arg_type == "json_object" {
1734 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1735 let json_str = match input.get(field) {
1736 Some(serde_json::Value::Null) | None => "{}".to_string(),
1737 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1738 };
1739 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1740 continue;
1741 }
1742
1743 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1744 let val = if field.is_empty() || field == "input" {
1750 Some(input)
1751 } else {
1752 input.get(field)
1753 };
1754 match val {
1755 None | Some(serde_json::Value::Null) if arg.optional => {
1756 parts.push("null".to_string());
1759 }
1760 None | Some(serde_json::Value::Null) => {
1761 let default_val = match arg.arg_type.as_str() {
1762 "string" => "\"\"".to_string(),
1763 "int" | "integer" => "0".to_string(),
1764 "float" | "number" => "0.0".to_string(),
1765 "bool" | "boolean" => "false".to_string(),
1766 "json_object" => "\"{}\"".to_string(),
1767 _ => "null".to_string(),
1768 };
1769 parts.push(default_val);
1770 }
1771 Some(v) => {
1772 if arg.arg_type == "json_object" {
1777 let json_str = serde_json::to_string(v).unwrap_or_default();
1778 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1779 } else if arg.arg_type == "bytes" {
1780 if let serde_json::Value::String(path) = v {
1785 let var_name = format!("{}_bytes", arg.name);
1786 let epath = escape_zig(path);
1787 setup_lines.push(format!(
1788 "const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
1789 ));
1790 setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
1791 parts.push(var_name);
1792 } else {
1793 parts.push(json_to_zig(v));
1794 }
1795 } else {
1796 parts.push(json_to_zig(v));
1797 }
1798 }
1799 }
1800 }
1801
1802 (setup_lines, parts.join(", "), setup_needs_gpa)
1803}
1804
1805fn render_assertion(
1806 out: &mut String,
1807 assertion: &Assertion,
1808 result_var: &str,
1809 field_resolver: &FieldResolver,
1810 enum_fields: &HashSet<String>,
1811 result_is_option: bool,
1812) {
1813 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1816 if bare_result_is_option {
1817 match assertion.assertion_type.as_str() {
1818 "is_empty" => {
1819 let _ = writeln!(out, " try testing.expect({result_var} == null);");
1820 return;
1821 }
1822 "not_empty" => {
1823 let _ = writeln!(out, " try testing.expect({result_var} != null);");
1824 return;
1825 }
1826 "not_error" => {
1827 let _ = writeln!(out, " // not_error: covered by try propagation");
1832 return;
1833 }
1834 "equals" => {
1835 if let Some(expected) = &assertion.value {
1836 let zig_val = json_to_zig(expected);
1837 let _ = writeln!(out, " try testing.expectEqualStrings({zig_val}, {result_var}.?);");
1838 return;
1839 }
1840 }
1841 _ => {}
1842 }
1843 }
1844 if let Some(f) = &assertion.field {
1857 if f == "embeddings" && !field_resolver.has_explicit_field(f) {
1858 match assertion.assertion_type.as_str() {
1859 "count_min" | "count_equals" | "not_empty" | "is_empty" => {
1860 let _ = writeln!(out, " {{");
1861 let _ = writeln!(
1862 out,
1863 " var _eparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {result_var}, .{{}});"
1864 );
1865 let _ = writeln!(out, " defer _eparse.deinit();");
1866 let _ = writeln!(out, " const _embeddings_len = _eparse.value.array.items.len;");
1867 match assertion.assertion_type.as_str() {
1868 "count_min" => {
1869 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1870 let _ = writeln!(out, " try testing.expect(_embeddings_len >= {n});");
1871 }
1872 }
1873 "count_equals" => {
1874 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1875 let _ = writeln!(
1876 out,
1877 " try testing.expectEqual(@as(usize, {n}), _embeddings_len);"
1878 );
1879 }
1880 }
1881 "not_empty" => {
1882 let _ = writeln!(out, " try testing.expect(_embeddings_len > 0);");
1883 }
1884 "is_empty" => {
1885 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _embeddings_len);");
1886 }
1887 _ => {}
1888 }
1889 let _ = writeln!(out, " }}");
1890 return;
1891 }
1892 _ => {}
1893 }
1894 }
1895 }
1896
1897 if let Some(f) = &assertion.field {
1903 if f == "result" && !field_resolver.has_explicit_field(f) {
1904 match assertion.assertion_type.as_str() {
1905 "contains" => {
1906 if let Some(expected) = &assertion.value {
1907 let zig_val = json_to_zig(expected);
1908 let _ = writeln!(
1909 out,
1910 " try testing.expect(std.mem.indexOf(u8, {result_var}, {zig_val}) != null);"
1911 );
1912 return;
1913 }
1914 }
1915 "not_contains" => {
1916 if let Some(expected) = &assertion.value {
1917 let zig_val = json_to_zig(expected);
1918 let _ = writeln!(
1919 out,
1920 " try testing.expect(std.mem.indexOf(u8, {result_var}, {zig_val}) == null);"
1921 );
1922 return;
1923 }
1924 }
1925 "equals" => {
1926 if let Some(expected) = &assertion.value {
1927 let zig_val = json_to_zig(expected);
1928 let _ = writeln!(out, " try testing.expectEqualStrings({zig_val}, {result_var});");
1929 return;
1930 }
1931 }
1932 "not_empty" => {
1933 let _ = writeln!(out, " try testing.expect({result_var}.len > 0);");
1934 return;
1935 }
1936 "is_empty" => {
1937 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), {result_var}.len);");
1938 return;
1939 }
1940 _ => {}
1941 }
1942 }
1943 }
1944
1945 if let Some(f) = &assertion.field {
1947 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1948 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
1949 return;
1950 }
1951 }
1952
1953 let _field_is_enum = assertion
1955 .field
1956 .as_deref()
1957 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1958
1959 let field_expr = match &assertion.field {
1960 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
1961 _ => result_var.to_string(),
1962 };
1963
1964 match assertion.assertion_type.as_str() {
1965 "equals" => {
1966 if let Some(expected) = &assertion.value {
1967 let zig_val = json_to_zig(expected);
1968 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
1969 }
1970 }
1971 "contains" => {
1972 if let Some(expected) = &assertion.value {
1973 let zig_val = json_to_zig(expected);
1974 let _ = writeln!(
1975 out,
1976 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1977 );
1978 }
1979 }
1980 "contains_all" => {
1981 if let Some(values) = &assertion.values {
1982 for val in values {
1983 let zig_val = json_to_zig(val);
1984 let _ = writeln!(
1985 out,
1986 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1987 );
1988 }
1989 }
1990 }
1991 "not_contains" => {
1992 if let Some(expected) = &assertion.value {
1993 let zig_val = json_to_zig(expected);
1994 let _ = writeln!(
1995 out,
1996 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1997 );
1998 } else if let Some(values) = &assertion.values {
1999 for val in values {
2003 let zig_val = json_to_zig(val);
2004 let _ = writeln!(
2005 out,
2006 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
2007 );
2008 }
2009 }
2010 }
2011 "not_empty" => {
2012 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
2013 }
2014 "is_empty" => {
2015 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
2016 }
2017 "starts_with" => {
2018 if let Some(expected) = &assertion.value {
2019 let zig_val = json_to_zig(expected);
2020 let _ = writeln!(
2021 out,
2022 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
2023 );
2024 }
2025 }
2026 "ends_with" => {
2027 if let Some(expected) = &assertion.value {
2028 let zig_val = json_to_zig(expected);
2029 let _ = writeln!(
2030 out,
2031 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
2032 );
2033 }
2034 }
2035 "min_length" => {
2036 if let Some(val) = &assertion.value {
2037 if let Some(n) = val.as_u64() {
2038 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
2039 }
2040 }
2041 }
2042 "max_length" => {
2043 if let Some(val) = &assertion.value {
2044 if let Some(n) = val.as_u64() {
2045 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
2046 }
2047 }
2048 }
2049 "count_min" => {
2050 if let Some(val) = &assertion.value {
2051 if let Some(n) = val.as_u64() {
2052 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
2053 }
2054 }
2055 }
2056 "count_equals" => {
2057 if let Some(val) = &assertion.value {
2058 if let Some(n) = val.as_u64() {
2059 let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
2063 if has_field {
2064 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
2065 } else {
2066 let _ = writeln!(out, " {{");
2067 let _ = writeln!(
2068 out,
2069 " var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
2070 );
2071 let _ = writeln!(out, " defer _cparse.deinit();");
2072 let _ = writeln!(
2073 out,
2074 " try testing.expectEqual({n}, _cparse.value.array.items.len);"
2075 );
2076 let _ = writeln!(out, " }}");
2077 }
2078 }
2079 }
2080 }
2081 "is_true" => {
2082 let _ = writeln!(out, " try testing.expect({field_expr});");
2083 }
2084 "is_false" => {
2085 let _ = writeln!(out, " try testing.expect(!{field_expr});");
2086 }
2087 "not_error" => {
2088 }
2090 "error" => {
2091 }
2093 "greater_than" => {
2094 if let Some(val) = &assertion.value {
2095 let zig_val = json_to_zig(val);
2096 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
2097 }
2098 }
2099 "less_than" => {
2100 if let Some(val) = &assertion.value {
2101 let zig_val = json_to_zig(val);
2102 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
2103 }
2104 }
2105 "greater_than_or_equal" => {
2106 if let Some(val) = &assertion.value {
2107 let zig_val = json_to_zig(val);
2108 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
2109 }
2110 }
2111 "less_than_or_equal" => {
2112 if let Some(val) = &assertion.value {
2113 let zig_val = json_to_zig(val);
2114 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
2115 }
2116 }
2117 "contains_any" => {
2118 if let Some(values) = &assertion.values {
2120 let string_values: Vec<String> = values
2121 .iter()
2122 .filter_map(|v| {
2123 if let serde_json::Value::String(s) = v {
2124 Some(format!(
2125 "std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
2126 escape_zig(s)
2127 ))
2128 } else {
2129 None
2130 }
2131 })
2132 .collect();
2133 if !string_values.is_empty() {
2134 let condition = string_values.join(" or\n ");
2135 let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
2136 }
2137 }
2138 }
2139 "matches_regex" => {
2140 let _ = writeln!(out, " // regex match not yet implemented for Zig");
2141 }
2142 "method_result" => {
2143 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
2144 }
2145 other => {
2146 panic!("Zig e2e generator: unsupported assertion type: {other}");
2147 }
2148 }
2149}
2150
2151fn json_to_zig(value: &serde_json::Value) -> String {
2153 match value {
2154 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
2155 serde_json::Value::Bool(b) => b.to_string(),
2156 serde_json::Value::Number(n) => n.to_string(),
2157 serde_json::Value::Null => "null".to_string(),
2158 serde_json::Value::Array(arr) => {
2159 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
2160 format!("&.{{{}}}", items.join(", "))
2161 }
2162 serde_json::Value::Object(_) => {
2163 let json_str = serde_json::to_string(value).unwrap_or_default();
2164 format!("\"{}\"", escape_zig(&json_str))
2165 }
2166 }
2167}