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 mut test_filenames: Vec<String> = Vec::new();
102 for group in groups {
103 let active: Vec<&Fixture> = group
104 .fixtures
105 .iter()
106 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
107 .collect();
108
109 if active.is_empty() {
110 continue;
111 }
112
113 let filename = format!("{}_test.zig", sanitize_filename(&group.category));
114 test_filenames.push(filename.clone());
115 let content = render_test_file(
116 &group.category,
117 &active,
118 e2e_config,
119 &function_name,
120 result_var,
121 &e2e_config.call.args,
122 &module_name,
123 &ffi_prefix,
124 );
125 files.push(GeneratedFile {
126 path: output_base.join("src").join(filename),
127 content,
128 generated_header: true,
129 });
130 }
131
132 files.insert(
134 files
135 .iter()
136 .position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
137 .unwrap_or(1),
138 GeneratedFile {
139 path: output_base.join("build.zig"),
140 content: render_build_zig(
141 &test_filenames,
142 &pkg_name,
143 &module_name,
144 &config.ffi_lib_name(),
145 &config.ffi_crate_path(),
146 has_file_fixtures,
147 &e2e_config.test_documents_relative_from(0),
148 ),
149 generated_header: false,
150 },
151 );
152
153 Ok(files)
154 }
155
156 fn language_name(&self) -> &'static str {
157 "zig"
158 }
159}
160
161fn render_build_zig_zon(pkg_name: &str, pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
166 let dep_block = match dep_mode {
167 crate::config::DependencyMode::Registry => {
168 format!(
170 r#".{{
171 .url = "https://registry.example.com/{pkg_name}/v0.1.0.tar.gz",
172 .hash = "0000000000000000000000000000000000000000000000000000000000000000",
173 }}"#
174 )
175 }
176 crate::config::DependencyMode::Local => {
177 format!(r#".{{ .path = "{pkg_path}" }}"#)
178 }
179 };
180
181 let min_zig = toolchain::MIN_ZIG_VERSION;
182 let name_bytes: &[u8] = b"e2e_zig";
184 let mut crc: u32 = 0xffff_ffff;
185 for byte in name_bytes {
186 crc ^= *byte as u32;
187 for _ in 0..8 {
188 let mask = (crc & 1).wrapping_neg();
189 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
190 }
191 }
192 let name_crc: u32 = !crc;
193 let mut id: u32 = 0x811c_9dc5;
194 for byte in name_bytes {
195 id ^= *byte as u32;
196 id = id.wrapping_mul(0x0100_0193);
197 }
198 if id == 0 || id == 0xffff_ffff {
199 id = 0x1;
200 }
201 let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
202 format!(
203 r#".{{
204 .name = .e2e_zig,
205 .version = "0.1.0",
206 .fingerprint = 0x{fingerprint:016x},
207 .minimum_zig_version = "{min_zig}",
208 .dependencies = .{{
209 .{pkg_name} = {dep_block},
210 }},
211 .paths = .{{
212 "build.zig",
213 "build.zig.zon",
214 "src",
215 }},
216}}
217"#
218 )
219}
220
221fn render_build_zig(
222 test_filenames: &[String],
223 pkg_name: &str,
224 module_name: &str,
225 ffi_lib_name: &str,
226 ffi_crate_path: &str,
227 has_file_fixtures: bool,
228 test_documents_path: &str,
229) -> String {
230 if test_filenames.is_empty() {
231 return r#"const std = @import("std");
232
233pub fn build(b: *std.Build) void {
234 const target = b.standardTargetOptions(.{});
235 const optimize = b.standardOptimizeOption(.{});
236
237 const test_step = b.step("test", "Run tests");
238}
239"#
240 .to_string();
241 }
242
243 let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
253 content.push_str(" const target = b.standardTargetOptions(.{});\n");
254 content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
255 content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n");
256 let _ = writeln!(
257 content,
258 " const ffi_path = b.option([]const u8, \"ffi_path\", \"Path to directory containing lib{ffi_lib_name}\") orelse \"../../target/debug\";"
259 );
260 let _ = writeln!(
261 content,
262 " const ffi_include = b.option([]const u8, \"ffi_include_path\", \"Path to directory containing FFI header\") orelse \"{ffi_crate_path}/include\";"
263 );
264 let _ = writeln!(content);
265 let _ = writeln!(
266 content,
267 " const {module_name}_module = b.addModule(\"{module_name}\", .{{"
268 );
269 let _ = writeln!(
270 content,
271 " .root_source_file = b.path(\"../../packages/zig/src/{pkg_name}.zig\"),"
272 );
273 content.push_str(" .target = target,\n");
274 content.push_str(" .optimize = optimize,\n");
275 content.push_str(" .link_libc = true,\n");
279 content.push_str(" });\n");
280 let _ = writeln!(
281 content,
282 " {module_name}_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});"
283 );
284 let _ = writeln!(
285 content,
286 " {module_name}_module.addIncludePath(.{{ .cwd_relative = ffi_include }});"
287 );
288 let _ = writeln!(
289 content,
290 " {module_name}_module.linkSystemLibrary(\"{ffi_lib_name}\", .{{}});"
291 );
292 let _ = writeln!(content);
293
294 for filename in test_filenames {
295 let test_name = filename.trim_end_matches("_test.zig");
297 content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
298 content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
299 content.push_str(" .target = target,\n");
300 content.push_str(" .optimize = optimize,\n");
301 content.push_str(" .link_libc = true,\n");
305 content.push_str(" });\n");
306 content.push_str(&format!(
307 " {test_name}_module.addImport(\"{module_name}\", {module_name}_module);\n"
308 ));
309 content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
325 content.push_str(&format!(" .name = \"{test_name}_test\",\n"));
326 content.push_str(&format!(" .root_module = {test_name}_module,\n"));
327 content.push_str(" .use_llvm = true,\n");
328 content.push_str(" });\n");
329 content.push_str(&format!(
346 " const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
347 ));
348 if has_file_fixtures {
349 content.push_str(&format!(
350 " {test_name}_run.setCwd(b.path(\"{test_documents_path}\"));\n"
351 ));
352 }
353 content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
354 }
355
356 content.push_str("}\n");
357 content
358}
359
360struct ZigTestClientRenderer;
368
369impl client::TestClientRenderer for ZigTestClientRenderer {
370 fn language_name(&self) -> &'static str {
371 "zig"
372 }
373
374 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
375 if let Some(reason) = skip_reason {
376 let _ = writeln!(out, "test \"{fn_name}\" {{");
377 let _ = writeln!(out, " // {description}");
378 let _ = writeln!(out, " // skipped: {reason}");
379 let _ = writeln!(out, " return error.SkipZigTest;");
380 } else {
381 let _ = writeln!(out, "test \"{fn_name}\" {{");
382 let _ = writeln!(out, " // {description}");
383 }
384 }
385
386 fn render_test_close(&self, out: &mut String) {
387 let _ = writeln!(out, "}}");
388 }
389
390 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
391 let method = ctx.method.to_uppercase();
392 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
393
394 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
395 let _ = writeln!(out, " defer _ = gpa.deinit();");
396 let _ = writeln!(out, " const allocator = gpa.allocator();");
397
398 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
399 let _ = writeln!(
400 out,
401 " 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\"}});"
402 );
403
404 if !ctx.headers.is_empty() {
406 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
407 header_pairs.sort_by_key(|(k, _)| k.as_str());
408 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
409 for (k, v) in &header_pairs {
410 let ek = escape_zig(k);
411 let ev = escape_zig(v);
412 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
413 }
414 let _ = writeln!(out, " }};");
415 }
416
417 if let Some(body) = ctx.body {
419 let json_str = serde_json::to_string(body).unwrap_or_default();
420 let escaped = escape_zig(&json_str);
421 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
422 }
423
424 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
425 let has_body = ctx.body.is_some();
426
427 let _ = writeln!(
428 out,
429 " var http_client = std.http.Client{{ .allocator = allocator }};"
430 );
431 let _ = writeln!(out, " defer http_client.deinit();");
432 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
433 let _ = writeln!(out, " defer response_body.deinit();");
434
435 let method_zig = match method.as_str() {
436 "GET" => ".GET",
437 "POST" => ".POST",
438 "PUT" => ".PUT",
439 "DELETE" => ".DELETE",
440 "PATCH" => ".PATCH",
441 "HEAD" => ".HEAD",
442 "OPTIONS" => ".OPTIONS",
443 _ => ".GET",
444 };
445
446 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
447 let _ = writeln!(
448 out,
449 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
450 rv = ctx.response_var,
451 );
452 }
453
454 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
455 let _ = writeln!(
456 out,
457 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
458 );
459 }
460
461 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
462 let ename = escape_zig(&name.to_lowercase());
463 match expected {
464 "<<present>>" => {
465 let _ = writeln!(
466 out,
467 " // assert header '{ename}' is present (header inspection not yet implemented)"
468 );
469 }
470 "<<absent>>" => {
471 let _ = writeln!(
472 out,
473 " // assert header '{ename}' is absent (header inspection not yet implemented)"
474 );
475 }
476 "<<uuid>>" => {
477 let _ = writeln!(
478 out,
479 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
480 );
481 }
482 exact => {
483 let evalue = escape_zig(exact);
484 let _ = writeln!(
485 out,
486 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
487 );
488 }
489 }
490 }
491
492 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
493 let json_str = serde_json::to_string(expected).unwrap_or_default();
494 let escaped = escape_zig(&json_str);
495 let _ = writeln!(
496 out,
497 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
498 );
499 }
500
501 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
502 if let Some(obj) = expected.as_object() {
503 for (key, val) in obj {
504 let ekey = escape_zig(key);
505 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
506 let _ = writeln!(
507 out,
508 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
509 );
510 }
511 }
512 }
513
514 fn render_assert_validation_errors(
515 &self,
516 out: &mut String,
517 _response_var: &str,
518 errors: &[crate::fixture::ValidationErrorExpectation],
519 ) {
520 for ve in errors {
521 let loc = ve.loc.join(".");
522 let escaped_loc = escape_zig(&loc);
523 let escaped_msg = escape_zig(&ve.msg);
524 let _ = writeln!(
525 out,
526 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
527 );
528 }
529 }
530}
531
532fn render_http_test_case(out: &mut String, fixture: &Fixture) {
537 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
538}
539
540#[allow(clippy::too_many_arguments)]
545fn render_test_file(
546 category: &str,
547 fixtures: &[&Fixture],
548 e2e_config: &E2eConfig,
549 function_name: &str,
550 result_var: &str,
551 args: &[crate::config::ArgMapping],
552 module_name: &str,
553 ffi_prefix: &str,
554) -> String {
555 let mut out = String::new();
556 out.push_str(&hash::header(CommentStyle::DoubleSlash));
557 let _ = writeln!(out, "const std = @import(\"std\");");
558 let _ = writeln!(out, "const testing = std.testing;");
559 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
560 let _ = writeln!(out);
561
562 let _ = writeln!(out, "// E2e tests for category: {category}");
563 let _ = writeln!(out);
564
565 for fixture in fixtures {
566 if fixture.http.is_some() {
567 render_http_test_case(&mut out, fixture);
568 } else {
569 render_test_fn(
570 &mut out,
571 fixture,
572 e2e_config,
573 function_name,
574 result_var,
575 args,
576 module_name,
577 ffi_prefix,
578 );
579 }
580 let _ = writeln!(out);
581 }
582
583 out
584}
585
586#[allow(clippy::too_many_arguments)]
587fn render_test_fn(
588 out: &mut String,
589 fixture: &Fixture,
590 e2e_config: &E2eConfig,
591 _function_name: &str,
592 _result_var: &str,
593 _args: &[crate::config::ArgMapping],
594 module_name: &str,
595 ffi_prefix: &str,
596) {
597 let call_config = e2e_config.resolve_call_for_fixture(
599 fixture.call.as_deref(),
600 &fixture.id,
601 &fixture.resolved_category(),
602 &fixture.tags,
603 &fixture.input,
604 );
605 let call_field_resolver = FieldResolver::new(
606 e2e_config.effective_fields(call_config),
607 e2e_config.effective_fields_optional(call_config),
608 e2e_config.effective_result_fields(call_config),
609 e2e_config.effective_fields_array(call_config),
610 e2e_config.effective_fields_method_calls(call_config),
611 );
612 let field_resolver = &call_field_resolver;
613 let enum_fields = e2e_config.effective_fields_enum(call_config);
614 let lang = "zig";
615 let call_overrides = call_config.overrides.get(lang);
616 let function_name = call_overrides
617 .and_then(|o| o.function.as_ref())
618 .cloned()
619 .unwrap_or_else(|| call_config.function.clone());
620 let result_var = &call_config.result_var;
621 let args = &call_config.args;
622 let client_factory = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
627 e2e_config
628 .call
629 .overrides
630 .get(lang)
631 .and_then(|o| o.client_factory.as_deref())
632 });
633
634 let call_result_is_bytes = call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
651 let result_is_json_struct =
652 !call_result_is_bytes && (call_overrides.is_some_and(|o| o.result_is_json_struct) || client_factory.is_some());
653
654 let result_is_option = call_overrides.is_some_and(|o| o.result_is_option) || call_config.result_is_option;
659
660 let call_returns_error_union = call_overrides.and_then(|o| o.returns_result) != Some(false);
681
682 let test_name = fixture.id.to_snake_case();
683 let description = &fixture.description;
684 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
685
686 let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
687 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
693 let args_str = if extra_args.is_empty() {
694 args_str
695 } else if args_str.is_empty() {
696 extra_args.join(", ")
697 } else {
698 format!("{args_str}, {}", extra_args.join(", "))
699 };
700
701 let any_happy_emits_code = fixture
704 .assertions
705 .iter()
706 .any(|a| assertion_emits_code(a, field_resolver));
707 let any_non_error_emits_code = fixture
708 .assertions
709 .iter()
710 .filter(|a| a.assertion_type != "error")
711 .any(|a| assertion_emits_code(a, field_resolver));
712
713 let has_streaming_virtual_assertions = fixture.assertions.iter().any(|a| {
715 a.field
716 .as_ref()
717 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
718 });
719 let is_stream_fn = function_name.contains("stream");
720 let uses_streaming_virtual_path =
721 result_is_json_struct && has_streaming_virtual_assertions && is_stream_fn && client_factory.is_some();
722 let streaming_path_has_non_streaming = uses_streaming_virtual_path
724 && fixture.assertions.iter().any(|a| {
725 !a.field
726 .as_ref()
727 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
728 && !matches!(a.assertion_type.as_str(), "not_error" | "error")
729 && a.field
730 .as_ref()
731 .is_some_and(|f| !f.is_empty() && field_resolver.is_valid_for_result(f))
732 });
733
734 let _ = writeln!(out, "test \"{test_name}\" {{");
735 let _ = writeln!(out, " // {description}");
736
737 if let Some(visitor_spec) = &fixture.visitor {
742 let html = fixture.input.get("html").and_then(|v| v.as_str()).unwrap_or_default();
743 let options_value = fixture.input.get("options").cloned();
744 emit_visitor_test_body(
745 out,
746 &fixture.id,
747 html,
748 options_value.as_ref(),
749 visitor_spec,
750 module_name,
751 &fixture.assertions,
752 expects_error,
753 field_resolver,
754 );
755 let _ = writeln!(out, "}}");
756 let _ = writeln!(out);
757 return;
758 }
759
760 let needs_gpa = setup_needs_gpa
768 || streaming_path_has_non_streaming
769 || (!uses_streaming_virtual_path && result_is_json_struct && !expects_error && any_happy_emits_code)
770 || (!uses_streaming_virtual_path && result_is_json_struct && expects_error && any_non_error_emits_code);
771 if needs_gpa {
772 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
773 let _ = writeln!(out, " defer _ = gpa.deinit();");
774 let _ = writeln!(out, " const allocator = gpa.allocator();");
775 let _ = writeln!(out);
776 }
777
778 for line in &setup_lines {
779 let _ = writeln!(out, " {line}");
780 }
781
782 let call_prefix = if let Some(factory) = client_factory {
787 let fixture_id = &fixture.id;
788 let _ = writeln!(
789 out,
790 " 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);"
791 );
792 let _ = writeln!(out, " defer std.heap.c_allocator.free(_mock_url);");
793 let _ = writeln!(
794 out,
795 " var _client = try {module_name}.{factory}(\"test-key\", _mock_url, null, null, null);"
796 );
797 let _ = writeln!(out, " defer _client.free();");
798 "_client".to_string()
799 } else {
800 module_name.to_string()
801 };
802
803 if expects_error {
804 if result_is_json_struct {
808 let _ = writeln!(
809 out,
810 " const _result_json = {call_prefix}.{function_name}({args_str}) catch {{"
811 );
812 } else {
813 let _ = writeln!(
814 out,
815 " const result = {call_prefix}.{function_name}({args_str}) catch {{"
816 );
817 }
818 let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
819 let _ = writeln!(out, " return;");
820 let _ = writeln!(out, " }};");
821 let any_emits_code = fixture
825 .assertions
826 .iter()
827 .filter(|a| a.assertion_type != "error")
828 .any(|a| assertion_emits_code(a, field_resolver));
829 if result_is_json_struct && any_emits_code {
830 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
831 let _ = writeln!(
832 out,
833 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
834 );
835 let _ = writeln!(out, " defer _parsed.deinit();");
836 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
837 let _ = writeln!(out, " // Perform success assertions if any");
838 for assertion in &fixture.assertions {
839 if assertion.assertion_type != "error" {
840 render_json_assertion(out, assertion, result_var, field_resolver, false);
841 }
842 }
843 } else if result_is_json_struct {
844 let _ = writeln!(out, " _ = _result_json;");
845 } else if any_emits_code {
846 let _ = writeln!(out, " // Perform success assertions if any");
847 for assertion in &fixture.assertions {
848 if assertion.assertion_type != "error" {
849 render_assertion(
850 out,
851 assertion,
852 result_var,
853 field_resolver,
854 enum_fields,
855 result_is_option,
856 );
857 }
858 }
859 } else {
860 let _ = writeln!(out, " _ = result;");
861 }
862 } else if fixture.assertions.is_empty() {
863 if result_is_json_struct {
865 let _ = writeln!(
866 out,
867 " const _result_json = try {call_prefix}.{function_name}({args_str});"
868 );
869 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
870 } else if call_returns_error_union {
871 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
872 } else {
873 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
874 }
875 } else {
876 let any_emits_code = fixture
880 .assertions
881 .iter()
882 .any(|a| assertion_emits_code(a, field_resolver));
883 if call_result_is_bytes && client_factory.is_some() {
884 let _ = writeln!(
887 out,
888 " const _result_json = try {call_prefix}.{function_name}({args_str});"
889 );
890 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
891 let has_bytes_assertions = fixture
892 .assertions
893 .iter()
894 .any(|a| matches!(a.assertion_type.as_str(), "not_empty" | "is_empty"));
895 if has_bytes_assertions {
896 for assertion in &fixture.assertions {
897 match assertion.assertion_type.as_str() {
898 "not_empty" => {
899 let _ = writeln!(out, " try testing.expect(_result_json.len > 0);");
900 }
901 "is_empty" => {
902 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _result_json.len);");
903 }
904 "not_error" | "error" => {}
905 _ => {
906 let atype = &assertion.assertion_type;
907 let _ = writeln!(
908 out,
909 " // bytes result: assertion '{atype}' not implemented for zig bytes"
910 );
911 }
912 }
913 }
914 }
915 } else if result_is_json_struct {
916 if uses_streaming_virtual_path {
920 let request_from_json = format!("{ffi_prefix}_chat_completion_request_from_json");
921 let request_free = format!("{ffi_prefix}_chat_completion_request_free");
922 let stream_start = format!("{ffi_prefix}_default_client_chat_stream_start");
923 let stream_free = format!("{ffi_prefix}_default_client_chat_stream_free");
924 let client_c_type = format!("{}DefaultClient", ffi_prefix.to_shouty_snake_case());
925
926 let _ = writeln!(
929 out,
930 " const _req_z = try std.heap.c_allocator.dupeZ(u8, {args_str});"
931 );
932 let _ = writeln!(out, " defer std.heap.c_allocator.free(_req_z);");
933 let _ = writeln!(
934 out,
935 " const _req_handle = {module_name}.c.{request_from_json}(_req_z.ptr);"
936 );
937 let _ = writeln!(out, " defer {module_name}.c.{request_free}(_req_handle);");
938 let _ = writeln!(
939 out,
940 " const _stream_handle = {module_name}.c.{stream_start}(@as(*{module_name}.c.{client_c_type}, @ptrCast(_client._handle)), _req_handle);"
941 );
942 let _ = writeln!(out, " if (_stream_handle == null) return error.StreamStartFailed;");
943 let _ = writeln!(out, " defer {module_name}.c.{stream_free}(_stream_handle);");
944 let snip =
946 StreamingFieldResolver::collect_snippet_zig("_stream_handle", "chunks", module_name, ffi_prefix);
947 out.push_str(" ");
948 out.push_str(&snip);
949 out.push('\n');
950 if streaming_path_has_non_streaming {
953 let _ = writeln!(
954 out,
955 " const _result_json = if (chunks.items.len > 0) chunks.items[chunks.items.len - 1] else &[_]u8{{}};"
956 );
957 let _ = writeln!(
958 out,
959 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
960 );
961 let _ = writeln!(out, " defer _parsed.deinit();");
962 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
963 }
964 for assertion in &fixture.assertions {
965 render_json_assertion(out, assertion, result_var, field_resolver, true);
966 }
967 } else {
968 let _ = writeln!(
970 out,
971 " const _result_json = try {call_prefix}.{function_name}({args_str});"
972 );
973 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
974 if any_emits_code {
975 let _ = writeln!(
976 out,
977 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
978 );
979 let _ = writeln!(out, " defer _parsed.deinit();");
980 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
981 for assertion in &fixture.assertions {
982 render_json_assertion(out, assertion, result_var, field_resolver, false);
983 }
984 }
985 }
986 } else if any_emits_code {
987 let try_kw = if call_returns_error_union { "try " } else { "" };
988 let _ = writeln!(
989 out,
990 " const {result_var} = {try_kw}{call_prefix}.{function_name}({args_str});"
991 );
992 for assertion in &fixture.assertions {
993 render_assertion(
994 out,
995 assertion,
996 result_var,
997 field_resolver,
998 enum_fields,
999 result_is_option,
1000 );
1001 }
1002 } else if call_returns_error_union {
1003 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
1004 } else {
1005 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
1006 }
1007 }
1008
1009 let _ = writeln!(out, "}}");
1010}
1011
1012#[allow(clippy::too_many_arguments)]
1018fn emit_visitor_test_body(
1019 out: &mut String,
1020 fixture_id: &str,
1021 html: &str,
1022 options_value: Option<&serde_json::Value>,
1023 visitor_spec: &crate::fixture::VisitorSpec,
1024 module_name: &str,
1025 assertions: &[Assertion],
1026 expects_error: bool,
1027 field_resolver: &FieldResolver,
1028) {
1029 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
1031 let _ = writeln!(out, " defer _ = gpa.deinit();");
1032 let _ = writeln!(out, " const allocator = gpa.allocator();");
1033 let _ = writeln!(out);
1034
1035 let visitor_block = super::zig_visitors::build_zig_visitor(fixture_id, module_name, visitor_spec);
1037 out.push_str(&visitor_block);
1038
1039 let _ = writeln!(
1042 out,
1043 " const _visitor = {module_name}.c.htm_visitor_create(&_callbacks);"
1044 );
1045 let _ = writeln!(out, " defer {module_name}.c.htm_visitor_free(_visitor);");
1046
1047 let options_json = match options_value {
1051 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1052 None => "{}".to_string(),
1053 };
1054 let escaped_options = escape_zig(&options_json);
1055 let _ = writeln!(
1056 out,
1057 " const _options_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_options}\");"
1058 );
1059 let _ = writeln!(out, " defer std.heap.c_allocator.free(_options_z);");
1060 let _ = writeln!(
1061 out,
1062 " const _options = {module_name}.c.htm_conversion_options_from_json(_options_z.ptr);"
1063 );
1064 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_options_free(_options);");
1065 let _ = writeln!(
1066 out,
1067 " {module_name}.c.htm_options_set_visitor_handle(_options, _visitor);"
1068 );
1069
1070 let escaped_html = escape_zig(html);
1072 let _ = writeln!(
1073 out,
1074 " const _html_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_html}\");"
1075 );
1076 let _ = writeln!(out, " defer std.heap.c_allocator.free(_html_z);");
1077 let _ = writeln!(
1078 out,
1079 " const _result = {module_name}.c.htm_convert(_html_z.ptr, _options);"
1080 );
1081
1082 if expects_error {
1083 let _ = writeln!(
1085 out,
1086 " try testing.expect(_result == null or {module_name}.c.htm_last_error_code() != 0);"
1087 );
1088 let _ = writeln!(
1089 out,
1090 " if (_result) |r| {module_name}.c.htm_conversion_result_free(r);"
1091 );
1092 return;
1093 }
1094
1095 let _ = writeln!(out, " try testing.expect(_result != null);");
1096 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_result_free(_result.?);");
1097 let _ = writeln!(
1098 out,
1099 " const _json_ptr = {module_name}.c.htm_conversion_result_to_json(_result.?);"
1100 );
1101 let _ = writeln!(out, " defer {module_name}.c.htm_free_string(_json_ptr);");
1102 let _ = writeln!(out, " const _result_json = std.mem.sliceTo(_json_ptr, 0);");
1103 let _ = writeln!(
1104 out,
1105 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
1106 );
1107 let _ = writeln!(out, " defer _parsed.deinit();");
1108 let _ = writeln!(out, " const result = &_parsed.value;");
1109
1110 for assertion in assertions {
1111 if assertion.assertion_type != "error" {
1112 render_json_assertion(out, assertion, "result", field_resolver, false);
1113 }
1114 }
1115}
1116
1117const FORMAT_METADATA_VARIANTS: &[&str] = &[
1135 "pdf",
1136 "docx",
1137 "excel",
1138 "email",
1139 "pptx",
1140 "archive",
1141 "image",
1142 "xml",
1143 "text",
1144 "html",
1145 "ocr",
1146 "csv",
1147 "bibtex",
1148 "citation",
1149 "fiction_book",
1150 "dbf",
1151 "jats",
1152 "epub",
1153 "pst",
1154 "code",
1155];
1156
1157fn json_path_expr(result_var: &str, field_path: &str) -> String {
1158 let segments: Vec<&str> = field_path.split('.').collect();
1159 let mut expr = result_var.to_string();
1160 let mut prev_seg: Option<&str> = None;
1161 for seg in &segments {
1162 if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
1167 prev_seg = Some(seg);
1168 continue;
1169 }
1170 if let Some(key) = seg.strip_suffix("[]") {
1174 expr = format!("{expr}.object.get(\"{key}\").?.array.items[0]");
1175 } else if let Some(bracket_pos) = seg.find('[') {
1176 if let Some(end_pos) = seg.find(']') {
1177 if end_pos > bracket_pos + 1 && end_pos == seg.len() - 1 {
1178 let key = &seg[..bracket_pos];
1179 let idx = &seg[bracket_pos + 1..end_pos];
1180 if idx.chars().all(|c| c.is_ascii_digit()) {
1181 expr = format!("{expr}.object.get(\"{key}\").?.array.items[{idx}]");
1182 prev_seg = Some(seg);
1183 continue;
1184 }
1185 expr = format!("{expr}.object.get(\"{key}\").?.object.get(\"{idx}\").?");
1191 prev_seg = Some(seg);
1192 continue;
1193 }
1194 }
1195 expr = format!("{expr}.object.get(\"{seg}\").?");
1196 } else {
1197 expr = format!("{expr}.object.get(\"{seg}\").?");
1198 }
1199 prev_seg = Some(seg);
1200 }
1201 expr
1202}
1203
1204fn render_json_assertion(
1209 out: &mut String,
1210 assertion: &Assertion,
1211 result_var: &str,
1212 field_resolver: &FieldResolver,
1213 uses_streaming: bool,
1214) {
1215 if let Some(f) = &assertion.field {
1222 if uses_streaming && !f.is_empty() && is_streaming_virtual_field(f) {
1223 if let Some(expr) = StreamingFieldResolver::accessor(f, "zig", "chunks") {
1224 match assertion.assertion_type.as_str() {
1225 "count_min" => {
1226 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1227 let _ = writeln!(out, " try testing.expect({expr}.len >= {n});");
1228 }
1229 }
1230 "count_equals" => {
1231 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1232 let _ = writeln!(out, " try testing.expectEqual(@as(usize, {n}), {expr}.len);");
1233 }
1234 }
1235 "equals" => {
1236 if let Some(serde_json::Value::String(s)) = &assertion.value {
1237 let escaped = escape_zig(s);
1238 let _ = writeln!(out, " try testing.expectEqualStrings(\"{escaped}\", {expr});");
1239 } else if let Some(v) = &assertion.value {
1240 let zig_val = json_to_zig(v);
1241 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {expr});");
1242 }
1243 }
1244 "not_empty" => {
1245 let _ = writeln!(out, " try testing.expect({expr}.len > 0);");
1246 }
1247 "is_true" => {
1248 let _ = writeln!(out, " try testing.expect({expr});");
1249 }
1250 "is_false" => {
1251 let _ = writeln!(out, " try testing.expect(!{expr});");
1252 }
1253 _ => {
1254 let atype = &assertion.assertion_type;
1255 let _ = writeln!(
1256 out,
1257 " // streaming virtual field '{f}' assertion '{atype}' not implemented for zig"
1258 );
1259 }
1260 }
1261 }
1262 return;
1263 }
1264 }
1265
1266 if let Some(f) = &assertion.field {
1273 if f == "embeddings" && !field_resolver.has_explicit_field("embeddings") {
1274 match assertion.assertion_type.as_str() {
1275 "count_min" => {
1276 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1277 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len >= {n});");
1278 }
1279 return;
1280 }
1281 "count_equals" => {
1282 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1283 let _ = writeln!(
1284 out,
1285 " try testing.expectEqual(@as(usize, {n}), {result_var}.array.items.len);"
1286 );
1287 }
1288 return;
1289 }
1290 "not_empty" => {
1291 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len > 0);");
1292 return;
1293 }
1294 "is_empty" => {
1295 let _ = writeln!(
1296 out,
1297 " try testing.expectEqual(@as(usize, 0), {result_var}.array.items.len);"
1298 );
1299 return;
1300 }
1301 _ => {}
1302 }
1303 }
1304 }
1305
1306 if let Some(f) = &assertion.field {
1308 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1309 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1310 return;
1311 }
1312 }
1313 if matches!(assertion.assertion_type.as_str(), "not_error" | "error") {
1315 return;
1316 }
1317
1318 let raw_field_path = assertion.field.as_deref().unwrap_or("").trim();
1319 let field_path = if raw_field_path.is_empty() {
1320 raw_field_path.to_string()
1321 } else {
1322 field_resolver.resolve(raw_field_path).to_string()
1323 };
1324 let field_path = field_path.trim();
1325
1326 let (field_path_for_expr, is_length_access) = if let Some(parent) = field_path.strip_suffix(".length") {
1328 (parent, true)
1329 } else {
1330 (field_path, false)
1331 };
1332
1333 let field_expr = if field_path_for_expr.is_empty() {
1334 result_var.to_string()
1335 } else {
1336 json_path_expr(result_var, field_path_for_expr)
1337 };
1338
1339 let zig_val = match &assertion.value {
1341 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_zig(s)),
1342 _ => String::new(),
1343 };
1344 let is_string_val = matches!(&assertion.value, Some(serde_json::Value::String(_)));
1345 let is_bool_val = matches!(&assertion.value, Some(serde_json::Value::Bool(_)));
1346 let bool_val = match &assertion.value {
1347 Some(serde_json::Value::Bool(b)) if *b => "true",
1348 _ => "false",
1349 };
1350 let is_null_val = matches!(&assertion.value, Some(serde_json::Value::Null));
1351 let n = assertion.value.as_ref().map(json_to_zig).unwrap_or_default();
1352 let has_n = assertion.value.as_ref().is_some_and(|v| v.is_number() || v.is_u64());
1353 let is_float_val = matches!(&assertion.value, Some(serde_json::Value::Number(n)) if !n.is_i64() && !n.is_u64());
1358 let values_list: Vec<String> = assertion
1359 .values
1360 .as_deref()
1361 .unwrap_or_default()
1362 .iter()
1363 .filter_map(|v| {
1364 if let serde_json::Value::String(s) = v {
1365 Some(format!("\"{}\"", escape_zig(s)))
1366 } else {
1367 None
1368 }
1369 })
1370 .collect();
1371
1372 let rendered = crate::template_env::render(
1373 "zig/json_assertion.jinja",
1374 minijinja::context! {
1375 assertion_type => assertion.assertion_type.as_str(),
1376 field_expr => field_expr,
1377 is_length_access => is_length_access,
1378 zig_val => zig_val,
1379 is_string_val => is_string_val,
1380 is_bool_val => is_bool_val,
1381 bool_val => bool_val,
1382 is_null_val => is_null_val,
1383 n => n,
1384 has_n => has_n,
1385 is_float_val => is_float_val,
1386 values_list => values_list,
1387 },
1388 );
1389 out.push_str(&rendered);
1390}
1391
1392fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
1395 if let Some(f) = &assertion.field {
1396 if !f.is_empty() && is_streaming_virtual_field(f) {
1397 } else if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1400 return false;
1401 }
1402 }
1403 matches!(
1404 assertion.assertion_type.as_str(),
1405 "equals"
1406 | "contains"
1407 | "contains_all"
1408 | "not_contains"
1409 | "not_empty"
1410 | "is_empty"
1411 | "starts_with"
1412 | "ends_with"
1413 | "min_length"
1414 | "max_length"
1415 | "count_min"
1416 | "count_equals"
1417 | "is_true"
1418 | "is_false"
1419 | "greater_than"
1420 | "less_than"
1421 | "greater_than_or_equal"
1422 | "less_than_or_equal"
1423 | "contains_any"
1424 )
1425}
1426
1427fn build_args_and_setup(
1432 input: &serde_json::Value,
1433 args: &[crate::config::ArgMapping],
1434 fixture_id: &str,
1435 _module_name: &str,
1436) -> (Vec<String>, String, bool) {
1437 if args.is_empty() {
1438 return (Vec::new(), String::new(), false);
1439 }
1440
1441 let mut setup_lines: Vec<String> = Vec::new();
1442 let mut parts: Vec<String> = Vec::new();
1443 let mut setup_needs_gpa = false;
1444
1445 for arg in args {
1446 if arg.arg_type == "mock_url" {
1447 let name = arg.name.clone();
1448 let id_upper = fixture_id.to_uppercase();
1449 setup_lines.push(format!(
1450 "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\"}});"
1451 ));
1452 setup_lines.push(format!("defer allocator.free({name});"));
1453 parts.push(name);
1454 setup_needs_gpa = true;
1455 continue;
1456 }
1457
1458 if arg.arg_type == "handle" {
1461 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1462 let json_str = match input.get(field) {
1463 Some(serde_json::Value::Null) | None => "null".to_string(),
1464 Some(v) => format!("\"{}\"", escape_zig(&serde_json::to_string(v).unwrap_or_default())),
1465 };
1466 parts.push(json_str);
1467 continue;
1468 }
1469
1470 if arg.name == "config" && arg.arg_type == "json_object" {
1477 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1478 let json_str = match input.get(field) {
1479 Some(serde_json::Value::Null) | None => "{}".to_string(),
1480 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1481 };
1482 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1483 continue;
1484 }
1485
1486 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1487 let val = if field.is_empty() || field == "input" {
1493 Some(input)
1494 } else {
1495 input.get(field)
1496 };
1497 match val {
1498 None | Some(serde_json::Value::Null) if arg.optional => {
1499 parts.push("null".to_string());
1502 }
1503 None | Some(serde_json::Value::Null) => {
1504 let default_val = match arg.arg_type.as_str() {
1505 "string" => "\"\"".to_string(),
1506 "int" | "integer" => "0".to_string(),
1507 "float" | "number" => "0.0".to_string(),
1508 "bool" | "boolean" => "false".to_string(),
1509 "json_object" => "\"{}\"".to_string(),
1510 _ => "null".to_string(),
1511 };
1512 parts.push(default_val);
1513 }
1514 Some(v) => {
1515 if arg.arg_type == "json_object" {
1520 let json_str = serde_json::to_string(v).unwrap_or_default();
1521 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1522 } else if arg.arg_type == "bytes" {
1523 if let serde_json::Value::String(path) = v {
1528 let var_name = format!("{}_bytes", arg.name);
1529 let epath = escape_zig(path);
1530 setup_lines.push(format!(
1531 "const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
1532 ));
1533 setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
1534 parts.push(var_name);
1535 } else {
1536 parts.push(json_to_zig(v));
1537 }
1538 } else {
1539 parts.push(json_to_zig(v));
1540 }
1541 }
1542 }
1543 }
1544
1545 (setup_lines, parts.join(", "), setup_needs_gpa)
1546}
1547
1548fn render_assertion(
1549 out: &mut String,
1550 assertion: &Assertion,
1551 result_var: &str,
1552 field_resolver: &FieldResolver,
1553 enum_fields: &HashSet<String>,
1554 result_is_option: bool,
1555) {
1556 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1559 if bare_result_is_option {
1560 match assertion.assertion_type.as_str() {
1561 "is_empty" => {
1562 let _ = writeln!(out, " try testing.expect({result_var} == null);");
1563 return;
1564 }
1565 "not_empty" | "not_error" => {
1566 let _ = writeln!(out, " try testing.expect({result_var} != null);");
1567 return;
1568 }
1569 _ => {}
1570 }
1571 }
1572 if let Some(f) = &assertion.field {
1585 if f == "embeddings" && !field_resolver.has_explicit_field(f) {
1586 match assertion.assertion_type.as_str() {
1587 "count_min" | "count_equals" | "not_empty" | "is_empty" => {
1588 let _ = writeln!(out, " {{");
1589 let _ = writeln!(
1590 out,
1591 " var _eparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {result_var}, .{{}});"
1592 );
1593 let _ = writeln!(out, " defer _eparse.deinit();");
1594 let _ = writeln!(out, " const _embeddings_len = _eparse.value.array.items.len;");
1595 match assertion.assertion_type.as_str() {
1596 "count_min" => {
1597 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1598 let _ = writeln!(out, " try testing.expect(_embeddings_len >= {n});");
1599 }
1600 }
1601 "count_equals" => {
1602 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1603 let _ = writeln!(
1604 out,
1605 " try testing.expectEqual(@as(usize, {n}), _embeddings_len);"
1606 );
1607 }
1608 }
1609 "not_empty" => {
1610 let _ = writeln!(out, " try testing.expect(_embeddings_len > 0);");
1611 }
1612 "is_empty" => {
1613 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _embeddings_len);");
1614 }
1615 _ => {}
1616 }
1617 let _ = writeln!(out, " }}");
1618 return;
1619 }
1620 _ => {}
1621 }
1622 }
1623 }
1624
1625 if let Some(f) = &assertion.field {
1631 if f == "result" && !field_resolver.has_explicit_field(f) {
1632 match assertion.assertion_type.as_str() {
1633 "contains" => {
1634 if let Some(expected) = &assertion.value {
1635 let zig_val = json_to_zig(expected);
1636 let _ = writeln!(
1637 out,
1638 " try testing.expect(std.mem.indexOf(u8, {result_var}, {zig_val}) != null);"
1639 );
1640 return;
1641 }
1642 }
1643 "not_contains" => {
1644 if let Some(expected) = &assertion.value {
1645 let zig_val = json_to_zig(expected);
1646 let _ = writeln!(
1647 out,
1648 " try testing.expect(std.mem.indexOf(u8, {result_var}, {zig_val}) == null);"
1649 );
1650 return;
1651 }
1652 }
1653 "equals" => {
1654 if let Some(expected) = &assertion.value {
1655 let zig_val = json_to_zig(expected);
1656 let _ = writeln!(out, " try testing.expectEqualStrings({zig_val}, {result_var});");
1657 return;
1658 }
1659 }
1660 "not_empty" => {
1661 let _ = writeln!(out, " try testing.expect({result_var}.len > 0);");
1662 return;
1663 }
1664 "is_empty" => {
1665 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), {result_var}.len);");
1666 return;
1667 }
1668 _ => {}
1669 }
1670 }
1671 }
1672
1673 if let Some(f) = &assertion.field {
1675 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1676 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
1677 return;
1678 }
1679 }
1680
1681 let _field_is_enum = assertion
1683 .field
1684 .as_deref()
1685 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1686
1687 let field_expr = match &assertion.field {
1688 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
1689 _ => result_var.to_string(),
1690 };
1691
1692 match assertion.assertion_type.as_str() {
1693 "equals" => {
1694 if let Some(expected) = &assertion.value {
1695 let zig_val = json_to_zig(expected);
1696 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
1697 }
1698 }
1699 "contains" => {
1700 if let Some(expected) = &assertion.value {
1701 let zig_val = json_to_zig(expected);
1702 let _ = writeln!(
1703 out,
1704 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1705 );
1706 }
1707 }
1708 "contains_all" => {
1709 if let Some(values) = &assertion.values {
1710 for val in values {
1711 let zig_val = json_to_zig(val);
1712 let _ = writeln!(
1713 out,
1714 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1715 );
1716 }
1717 }
1718 }
1719 "not_contains" => {
1720 if let Some(expected) = &assertion.value {
1721 let zig_val = json_to_zig(expected);
1722 let _ = writeln!(
1723 out,
1724 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1725 );
1726 } else if let Some(values) = &assertion.values {
1727 for val in values {
1731 let zig_val = json_to_zig(val);
1732 let _ = writeln!(
1733 out,
1734 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1735 );
1736 }
1737 }
1738 }
1739 "not_empty" => {
1740 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
1741 }
1742 "is_empty" => {
1743 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
1744 }
1745 "starts_with" => {
1746 if let Some(expected) = &assertion.value {
1747 let zig_val = json_to_zig(expected);
1748 let _ = writeln!(
1749 out,
1750 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
1751 );
1752 }
1753 }
1754 "ends_with" => {
1755 if let Some(expected) = &assertion.value {
1756 let zig_val = json_to_zig(expected);
1757 let _ = writeln!(
1758 out,
1759 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
1760 );
1761 }
1762 }
1763 "min_length" => {
1764 if let Some(val) = &assertion.value {
1765 if let Some(n) = val.as_u64() {
1766 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1767 }
1768 }
1769 }
1770 "max_length" => {
1771 if let Some(val) = &assertion.value {
1772 if let Some(n) = val.as_u64() {
1773 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
1774 }
1775 }
1776 }
1777 "count_min" => {
1778 if let Some(val) = &assertion.value {
1779 if let Some(n) = val.as_u64() {
1780 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1781 }
1782 }
1783 }
1784 "count_equals" => {
1785 if let Some(val) = &assertion.value {
1786 if let Some(n) = val.as_u64() {
1787 let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
1791 if has_field {
1792 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
1793 } else {
1794 let _ = writeln!(out, " {{");
1795 let _ = writeln!(
1796 out,
1797 " var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
1798 );
1799 let _ = writeln!(out, " defer _cparse.deinit();");
1800 let _ = writeln!(
1801 out,
1802 " try testing.expectEqual({n}, _cparse.value.array.items.len);"
1803 );
1804 let _ = writeln!(out, " }}");
1805 }
1806 }
1807 }
1808 }
1809 "is_true" => {
1810 let _ = writeln!(out, " try testing.expect({field_expr});");
1811 }
1812 "is_false" => {
1813 let _ = writeln!(out, " try testing.expect(!{field_expr});");
1814 }
1815 "not_error" => {
1816 }
1818 "error" => {
1819 }
1821 "greater_than" => {
1822 if let Some(val) = &assertion.value {
1823 let zig_val = json_to_zig(val);
1824 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
1825 }
1826 }
1827 "less_than" => {
1828 if let Some(val) = &assertion.value {
1829 let zig_val = json_to_zig(val);
1830 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
1831 }
1832 }
1833 "greater_than_or_equal" => {
1834 if let Some(val) = &assertion.value {
1835 let zig_val = json_to_zig(val);
1836 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
1837 }
1838 }
1839 "less_than_or_equal" => {
1840 if let Some(val) = &assertion.value {
1841 let zig_val = json_to_zig(val);
1842 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
1843 }
1844 }
1845 "contains_any" => {
1846 if let Some(values) = &assertion.values {
1848 let string_values: Vec<String> = values
1849 .iter()
1850 .filter_map(|v| {
1851 if let serde_json::Value::String(s) = v {
1852 Some(format!(
1853 "std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
1854 escape_zig(s)
1855 ))
1856 } else {
1857 None
1858 }
1859 })
1860 .collect();
1861 if !string_values.is_empty() {
1862 let condition = string_values.join(" or\n ");
1863 let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
1864 }
1865 }
1866 }
1867 "matches_regex" => {
1868 let _ = writeln!(out, " // regex match not yet implemented for Zig");
1869 }
1870 "method_result" => {
1871 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
1872 }
1873 other => {
1874 panic!("Zig e2e generator: unsupported assertion type: {other}");
1875 }
1876 }
1877}
1878
1879fn json_to_zig(value: &serde_json::Value) -> String {
1881 match value {
1882 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
1883 serde_json::Value::Bool(b) => b.to_string(),
1884 serde_json::Value::Number(n) => n.to_string(),
1885 serde_json::Value::Null => "null".to_string(),
1886 serde_json::Value::Array(arr) => {
1887 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
1888 format!("&.{{{}}}", items.join(", "))
1889 }
1890 serde_json::Value::Object(_) => {
1891 let json_str = serde_json::to_string(value).unwrap_or_default();
1892 format!("\"{}\"", escape_zig(&json_str))
1893 }
1894 }
1895}