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 field_resolver = FieldResolver::new(
80 &e2e_config.fields,
81 &e2e_config.fields_optional,
82 &e2e_config.result_fields,
83 &e2e_config.fields_array,
84 &e2e_config.fields_method_calls,
85 );
86
87 let mut test_filenames: Vec<String> = Vec::new();
89 for group in groups {
90 let active: Vec<&Fixture> = group
91 .fixtures
92 .iter()
93 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
94 .collect();
95
96 if active.is_empty() {
97 continue;
98 }
99
100 let filename = format!("{}_test.zig", sanitize_filename(&group.category));
101 test_filenames.push(filename.clone());
102 let content = render_test_file(
103 &group.category,
104 &active,
105 e2e_config,
106 &function_name,
107 result_var,
108 &e2e_config.call.args,
109 &field_resolver,
110 &e2e_config.fields_enum,
111 &module_name,
112 &ffi_prefix,
113 );
114 files.push(GeneratedFile {
115 path: output_base.join("src").join(filename),
116 content,
117 generated_header: true,
118 });
119 }
120
121 files.insert(
123 files
124 .iter()
125 .position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
126 .unwrap_or(1),
127 GeneratedFile {
128 path: output_base.join("build.zig"),
129 content: render_build_zig(
130 &test_filenames,
131 &pkg_name,
132 &module_name,
133 &config.ffi_lib_name(),
134 &config.ffi_crate_path(),
135 &e2e_config.test_documents_relative_from(0),
136 ),
137 generated_header: false,
138 },
139 );
140
141 Ok(files)
142 }
143
144 fn language_name(&self) -> &'static str {
145 "zig"
146 }
147}
148
149fn render_build_zig_zon(pkg_name: &str, pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
154 let dep_block = match dep_mode {
155 crate::config::DependencyMode::Registry => {
156 format!(
158 r#".{{
159 .url = "https://registry.example.com/{pkg_name}/v0.1.0.tar.gz",
160 .hash = "0000000000000000000000000000000000000000000000000000000000000000",
161 }}"#
162 )
163 }
164 crate::config::DependencyMode::Local => {
165 format!(r#".{{ .path = "{pkg_path}" }}"#)
166 }
167 };
168
169 let min_zig = toolchain::MIN_ZIG_VERSION;
170 let name_bytes: &[u8] = b"e2e_zig";
172 let mut crc: u32 = 0xffff_ffff;
173 for byte in name_bytes {
174 crc ^= *byte as u32;
175 for _ in 0..8 {
176 let mask = (crc & 1).wrapping_neg();
177 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
178 }
179 }
180 let name_crc: u32 = !crc;
181 let mut id: u32 = 0x811c_9dc5;
182 for byte in name_bytes {
183 id ^= *byte as u32;
184 id = id.wrapping_mul(0x0100_0193);
185 }
186 if id == 0 || id == 0xffff_ffff {
187 id = 0x1;
188 }
189 let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
190 format!(
191 r#".{{
192 .name = .e2e_zig,
193 .version = "0.1.0",
194 .fingerprint = 0x{fingerprint:016x},
195 .minimum_zig_version = "{min_zig}",
196 .dependencies = .{{
197 .{pkg_name} = {dep_block},
198 }},
199 .paths = .{{
200 "build.zig",
201 "build.zig.zon",
202 "src",
203 }},
204}}
205"#
206 )
207}
208
209fn render_build_zig(
210 test_filenames: &[String],
211 pkg_name: &str,
212 module_name: &str,
213 ffi_lib_name: &str,
214 ffi_crate_path: &str,
215 test_documents_path: &str,
216) -> String {
217 if test_filenames.is_empty() {
218 return r#"const std = @import("std");
219
220pub fn build(b: *std.Build) void {
221 const target = b.standardTargetOptions(.{});
222 const optimize = b.standardOptimizeOption(.{});
223
224 const test_step = b.step("test", "Run tests");
225}
226"#
227 .to_string();
228 }
229
230 let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
240 content.push_str(" const target = b.standardTargetOptions(.{});\n");
241 content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
242 content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n");
243 let _ = writeln!(
244 content,
245 " const ffi_path = b.option([]const u8, \"ffi_path\", \"Path to directory containing lib{ffi_lib_name}\") orelse \"../../target/debug\";"
246 );
247 let _ = writeln!(
248 content,
249 " const ffi_include = b.option([]const u8, \"ffi_include_path\", \"Path to directory containing FFI header\") orelse \"{ffi_crate_path}/include\";"
250 );
251 let _ = writeln!(content);
252 let _ = writeln!(
253 content,
254 " const {module_name}_module = b.addModule(\"{module_name}\", .{{"
255 );
256 let _ = writeln!(
257 content,
258 " .root_source_file = b.path(\"../../packages/zig/src/{pkg_name}.zig\"),"
259 );
260 content.push_str(" .target = target,\n");
261 content.push_str(" .optimize = optimize,\n");
262 content.push_str(" .link_libc = true,\n");
266 content.push_str(" });\n");
267 let _ = writeln!(
268 content,
269 " {module_name}_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});"
270 );
271 let _ = writeln!(
272 content,
273 " {module_name}_module.addIncludePath(.{{ .cwd_relative = ffi_include }});"
274 );
275 let _ = writeln!(
276 content,
277 " {module_name}_module.linkSystemLibrary(\"{ffi_lib_name}\", .{{}});"
278 );
279 let _ = writeln!(content);
280
281 for filename in test_filenames {
282 let test_name = filename.trim_end_matches("_test.zig");
284 content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
285 content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
286 content.push_str(" .target = target,\n");
287 content.push_str(" .optimize = optimize,\n");
288 content.push_str(" .link_libc = true,\n");
292 content.push_str(" });\n");
293 content.push_str(&format!(
294 " {test_name}_module.addImport(\"{module_name}\", {module_name}_module);\n"
295 ));
296 content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
312 content.push_str(&format!(" .name = \"{test_name}_test\",\n"));
313 content.push_str(&format!(" .root_module = {test_name}_module,\n"));
314 content.push_str(" .use_llvm = true,\n");
315 content.push_str(" });\n");
316 content.push_str(&format!(
331 " const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
332 ));
333 content.push_str(&format!(
334 " {test_name}_run.setCwd(b.path(\"{test_documents_path}\"));\n"
335 ));
336 content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
337 }
338
339 content.push_str("}\n");
340 content
341}
342
343struct ZigTestClientRenderer;
351
352impl client::TestClientRenderer for ZigTestClientRenderer {
353 fn language_name(&self) -> &'static str {
354 "zig"
355 }
356
357 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
358 if let Some(reason) = skip_reason {
359 let _ = writeln!(out, "test \"{fn_name}\" {{");
360 let _ = writeln!(out, " // {description}");
361 let _ = writeln!(out, " // skipped: {reason}");
362 let _ = writeln!(out, " return error.SkipZigTest;");
363 } else {
364 let _ = writeln!(out, "test \"{fn_name}\" {{");
365 let _ = writeln!(out, " // {description}");
366 }
367 }
368
369 fn render_test_close(&self, out: &mut String) {
370 let _ = writeln!(out, "}}");
371 }
372
373 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
374 let method = ctx.method.to_uppercase();
375 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
376
377 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
378 let _ = writeln!(out, " defer _ = gpa.deinit();");
379 let _ = writeln!(out, " const allocator = gpa.allocator();");
380
381 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
382 let _ = writeln!(
383 out,
384 " 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\"}});"
385 );
386
387 if !ctx.headers.is_empty() {
389 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
390 header_pairs.sort_by_key(|(k, _)| k.as_str());
391 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
392 for (k, v) in &header_pairs {
393 let ek = escape_zig(k);
394 let ev = escape_zig(v);
395 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
396 }
397 let _ = writeln!(out, " }};");
398 }
399
400 if let Some(body) = ctx.body {
402 let json_str = serde_json::to_string(body).unwrap_or_default();
403 let escaped = escape_zig(&json_str);
404 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
405 }
406
407 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
408 let has_body = ctx.body.is_some();
409
410 let _ = writeln!(
411 out,
412 " var http_client = std.http.Client{{ .allocator = allocator }};"
413 );
414 let _ = writeln!(out, " defer http_client.deinit();");
415 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
416 let _ = writeln!(out, " defer response_body.deinit();");
417
418 let method_zig = match method.as_str() {
419 "GET" => ".GET",
420 "POST" => ".POST",
421 "PUT" => ".PUT",
422 "DELETE" => ".DELETE",
423 "PATCH" => ".PATCH",
424 "HEAD" => ".HEAD",
425 "OPTIONS" => ".OPTIONS",
426 _ => ".GET",
427 };
428
429 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
430 let _ = writeln!(
431 out,
432 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
433 rv = ctx.response_var,
434 );
435 }
436
437 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
438 let _ = writeln!(
439 out,
440 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
441 );
442 }
443
444 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
445 let ename = escape_zig(&name.to_lowercase());
446 match expected {
447 "<<present>>" => {
448 let _ = writeln!(
449 out,
450 " // assert header '{ename}' is present (header inspection not yet implemented)"
451 );
452 }
453 "<<absent>>" => {
454 let _ = writeln!(
455 out,
456 " // assert header '{ename}' is absent (header inspection not yet implemented)"
457 );
458 }
459 "<<uuid>>" => {
460 let _ = writeln!(
461 out,
462 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
463 );
464 }
465 exact => {
466 let evalue = escape_zig(exact);
467 let _ = writeln!(
468 out,
469 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
470 );
471 }
472 }
473 }
474
475 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
476 let json_str = serde_json::to_string(expected).unwrap_or_default();
477 let escaped = escape_zig(&json_str);
478 let _ = writeln!(
479 out,
480 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
481 );
482 }
483
484 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
485 if let Some(obj) = expected.as_object() {
486 for (key, val) in obj {
487 let ekey = escape_zig(key);
488 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
489 let _ = writeln!(
490 out,
491 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
492 );
493 }
494 }
495 }
496
497 fn render_assert_validation_errors(
498 &self,
499 out: &mut String,
500 _response_var: &str,
501 errors: &[crate::fixture::ValidationErrorExpectation],
502 ) {
503 for ve in errors {
504 let loc = ve.loc.join(".");
505 let escaped_loc = escape_zig(&loc);
506 let escaped_msg = escape_zig(&ve.msg);
507 let _ = writeln!(
508 out,
509 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
510 );
511 }
512 }
513}
514
515fn render_http_test_case(out: &mut String, fixture: &Fixture) {
520 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
521}
522
523#[allow(clippy::too_many_arguments)]
528fn render_test_file(
529 category: &str,
530 fixtures: &[&Fixture],
531 e2e_config: &E2eConfig,
532 function_name: &str,
533 result_var: &str,
534 args: &[crate::config::ArgMapping],
535 field_resolver: &FieldResolver,
536 enum_fields: &HashSet<String>,
537 module_name: &str,
538 ffi_prefix: &str,
539) -> String {
540 let mut out = String::new();
541 out.push_str(&hash::header(CommentStyle::DoubleSlash));
542 let _ = writeln!(out, "const std = @import(\"std\");");
543 let _ = writeln!(out, "const testing = std.testing;");
544 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
545 let _ = writeln!(out);
546
547 let _ = writeln!(out, "// E2e tests for category: {category}");
548 let _ = writeln!(out);
549
550 for fixture in fixtures {
551 if fixture.http.is_some() {
552 render_http_test_case(&mut out, fixture);
553 } else {
554 render_test_fn(
555 &mut out,
556 fixture,
557 e2e_config,
558 function_name,
559 result_var,
560 args,
561 field_resolver,
562 enum_fields,
563 module_name,
564 ffi_prefix,
565 );
566 }
567 let _ = writeln!(out);
568 }
569
570 out
571}
572
573#[allow(clippy::too_many_arguments)]
574fn render_test_fn(
575 out: &mut String,
576 fixture: &Fixture,
577 e2e_config: &E2eConfig,
578 _function_name: &str,
579 _result_var: &str,
580 _args: &[crate::config::ArgMapping],
581 field_resolver: &FieldResolver,
582 enum_fields: &HashSet<String>,
583 module_name: &str,
584 ffi_prefix: &str,
585) {
586 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
588 let lang = "zig";
589 let call_overrides = call_config.overrides.get(lang);
590 let function_name = call_overrides
591 .and_then(|o| o.function.as_ref())
592 .cloned()
593 .unwrap_or_else(|| call_config.function.clone());
594 let result_var = &call_config.result_var;
595 let args = &call_config.args;
596 let client_factory = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
601 e2e_config
602 .call
603 .overrides
604 .get(lang)
605 .and_then(|o| o.client_factory.as_deref())
606 });
607
608 let call_result_is_bytes = call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
625 let result_is_json_struct =
626 !call_result_is_bytes && (call_overrides.is_some_and(|o| o.result_is_json_struct) || client_factory.is_some());
627
628 let result_is_option = call_overrides.is_some_and(|o| o.result_is_option) || call_config.result_is_option;
633
634 let call_returns_error_union = call_overrides.and_then(|o| o.returns_result) != Some(false);
655
656 let test_name = fixture.id.to_snake_case();
657 let description = &fixture.description;
658 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
659
660 let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
661 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
667 let args_str = if extra_args.is_empty() {
668 args_str
669 } else if args_str.is_empty() {
670 extra_args.join(", ")
671 } else {
672 format!("{args_str}, {}", extra_args.join(", "))
673 };
674
675 let any_happy_emits_code = fixture
678 .assertions
679 .iter()
680 .any(|a| assertion_emits_code(a, field_resolver));
681 let any_non_error_emits_code = fixture
682 .assertions
683 .iter()
684 .filter(|a| a.assertion_type != "error")
685 .any(|a| assertion_emits_code(a, field_resolver));
686
687 let has_streaming_virtual_assertions = fixture.assertions.iter().any(|a| {
689 a.field
690 .as_ref()
691 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
692 });
693 let is_stream_fn = function_name.contains("stream");
694 let uses_streaming_virtual_path =
695 result_is_json_struct && has_streaming_virtual_assertions && is_stream_fn && client_factory.is_some();
696 let streaming_path_has_non_streaming = uses_streaming_virtual_path
698 && fixture.assertions.iter().any(|a| {
699 !a.field
700 .as_ref()
701 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
702 && !matches!(a.assertion_type.as_str(), "not_error" | "error")
703 && a.field
704 .as_ref()
705 .is_some_and(|f| !f.is_empty() && field_resolver.is_valid_for_result(f))
706 });
707
708 let _ = writeln!(out, "test \"{test_name}\" {{");
709 let _ = writeln!(out, " // {description}");
710
711 if let Some(visitor_spec) = &fixture.visitor {
716 let html = fixture.input.get("html").and_then(|v| v.as_str()).unwrap_or_default();
717 let options_value = fixture.input.get("options").cloned();
718 emit_visitor_test_body(
719 out,
720 &fixture.id,
721 html,
722 options_value.as_ref(),
723 visitor_spec,
724 module_name,
725 &fixture.assertions,
726 expects_error,
727 field_resolver,
728 );
729 let _ = writeln!(out, "}}");
730 let _ = writeln!(out);
731 return;
732 }
733
734 let needs_gpa = setup_needs_gpa
742 || streaming_path_has_non_streaming
743 || (!uses_streaming_virtual_path && result_is_json_struct && !expects_error && any_happy_emits_code)
744 || (!uses_streaming_virtual_path && result_is_json_struct && expects_error && any_non_error_emits_code);
745 if needs_gpa {
746 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
747 let _ = writeln!(out, " defer _ = gpa.deinit();");
748 let _ = writeln!(out, " const allocator = gpa.allocator();");
749 let _ = writeln!(out);
750 }
751
752 for line in &setup_lines {
753 let _ = writeln!(out, " {line}");
754 }
755
756 let call_prefix = if let Some(factory) = client_factory {
761 let fixture_id = &fixture.id;
762 let _ = writeln!(
763 out,
764 " 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);"
765 );
766 let _ = writeln!(out, " defer std.heap.c_allocator.free(_mock_url);");
767 let _ = writeln!(
768 out,
769 " var _client = try {module_name}.{factory}(\"test-key\", _mock_url, null, null, null);"
770 );
771 let _ = writeln!(out, " defer _client.free();");
772 "_client".to_string()
773 } else {
774 module_name.to_string()
775 };
776
777 if expects_error {
778 if result_is_json_struct {
782 let _ = writeln!(
783 out,
784 " const _result_json = {call_prefix}.{function_name}({args_str}) catch {{"
785 );
786 } else {
787 let _ = writeln!(
788 out,
789 " const result = {call_prefix}.{function_name}({args_str}) catch {{"
790 );
791 }
792 let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
793 let _ = writeln!(out, " return;");
794 let _ = writeln!(out, " }};");
795 let any_emits_code = fixture
799 .assertions
800 .iter()
801 .filter(|a| a.assertion_type != "error")
802 .any(|a| assertion_emits_code(a, field_resolver));
803 if result_is_json_struct && any_emits_code {
804 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
805 let _ = writeln!(
806 out,
807 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
808 );
809 let _ = writeln!(out, " defer _parsed.deinit();");
810 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
811 let _ = writeln!(out, " // Perform success assertions if any");
812 for assertion in &fixture.assertions {
813 if assertion.assertion_type != "error" {
814 render_json_assertion(out, assertion, result_var, field_resolver, false);
815 }
816 }
817 } else if result_is_json_struct {
818 let _ = writeln!(out, " _ = _result_json;");
819 } else if any_emits_code {
820 let _ = writeln!(out, " // Perform success assertions if any");
821 for assertion in &fixture.assertions {
822 if assertion.assertion_type != "error" {
823 render_assertion(
824 out,
825 assertion,
826 result_var,
827 field_resolver,
828 enum_fields,
829 result_is_option,
830 );
831 }
832 }
833 } else {
834 let _ = writeln!(out, " _ = result;");
835 }
836 } else if fixture.assertions.is_empty() {
837 if result_is_json_struct {
839 let _ = writeln!(
840 out,
841 " const _result_json = try {call_prefix}.{function_name}({args_str});"
842 );
843 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
844 } else if call_returns_error_union {
845 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
846 } else {
847 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
848 }
849 } else {
850 let any_emits_code = fixture
854 .assertions
855 .iter()
856 .any(|a| assertion_emits_code(a, field_resolver));
857 if call_result_is_bytes && client_factory.is_some() {
858 let _ = writeln!(
861 out,
862 " const _result_json = try {call_prefix}.{function_name}({args_str});"
863 );
864 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
865 let has_bytes_assertions = fixture
866 .assertions
867 .iter()
868 .any(|a| matches!(a.assertion_type.as_str(), "not_empty" | "is_empty"));
869 if has_bytes_assertions {
870 for assertion in &fixture.assertions {
871 match assertion.assertion_type.as_str() {
872 "not_empty" => {
873 let _ = writeln!(out, " try testing.expect(_result_json.len > 0);");
874 }
875 "is_empty" => {
876 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _result_json.len);");
877 }
878 "not_error" | "error" => {}
879 _ => {
880 let atype = &assertion.assertion_type;
881 let _ = writeln!(
882 out,
883 " // bytes result: assertion '{atype}' not implemented for zig bytes"
884 );
885 }
886 }
887 }
888 }
889 } else if result_is_json_struct {
890 if uses_streaming_virtual_path {
894 let request_from_json = format!("{ffi_prefix}_chat_completion_request_from_json");
895 let request_free = format!("{ffi_prefix}_chat_completion_request_free");
896 let stream_start = format!("{ffi_prefix}_default_client_chat_stream_start");
897 let stream_free = format!("{ffi_prefix}_default_client_chat_stream_free");
898 let client_c_type = format!("{}DefaultClient", ffi_prefix.to_shouty_snake_case());
899
900 let _ = writeln!(
903 out,
904 " const _req_z = try std.heap.c_allocator.dupeZ(u8, {args_str});"
905 );
906 let _ = writeln!(out, " defer std.heap.c_allocator.free(_req_z);");
907 let _ = writeln!(
908 out,
909 " const _req_handle = {module_name}.c.{request_from_json}(_req_z.ptr);"
910 );
911 let _ = writeln!(out, " defer {module_name}.c.{request_free}(_req_handle);");
912 let _ = writeln!(
913 out,
914 " const _stream_handle = {module_name}.c.{stream_start}(@as(*{module_name}.c.{client_c_type}, @ptrCast(_client._handle)), _req_handle);"
915 );
916 let _ = writeln!(out, " if (_stream_handle == null) return error.StreamStartFailed;");
917 let _ = writeln!(out, " defer {module_name}.c.{stream_free}(_stream_handle);");
918 let snip =
920 StreamingFieldResolver::collect_snippet_zig("_stream_handle", "chunks", module_name, ffi_prefix);
921 out.push_str(" ");
922 out.push_str(&snip);
923 out.push('\n');
924 if streaming_path_has_non_streaming {
927 let _ = writeln!(
928 out,
929 " const _result_json = if (chunks.items.len > 0) chunks.items[chunks.items.len - 1] else &[_]u8{{}};"
930 );
931 let _ = writeln!(
932 out,
933 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
934 );
935 let _ = writeln!(out, " defer _parsed.deinit();");
936 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
937 }
938 for assertion in &fixture.assertions {
939 render_json_assertion(out, assertion, result_var, field_resolver, true);
940 }
941 } else {
942 let _ = writeln!(
944 out,
945 " const _result_json = try {call_prefix}.{function_name}({args_str});"
946 );
947 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
948 if any_emits_code {
949 let _ = writeln!(
950 out,
951 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
952 );
953 let _ = writeln!(out, " defer _parsed.deinit();");
954 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
955 for assertion in &fixture.assertions {
956 render_json_assertion(out, assertion, result_var, field_resolver, false);
957 }
958 }
959 }
960 } else if any_emits_code {
961 let try_kw = if call_returns_error_union { "try " } else { "" };
962 let _ = writeln!(
963 out,
964 " const {result_var} = {try_kw}{call_prefix}.{function_name}({args_str});"
965 );
966 for assertion in &fixture.assertions {
967 render_assertion(
968 out,
969 assertion,
970 result_var,
971 field_resolver,
972 enum_fields,
973 result_is_option,
974 );
975 }
976 } else if call_returns_error_union {
977 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
978 } else {
979 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
980 }
981 }
982
983 let _ = writeln!(out, "}}");
984}
985
986#[allow(clippy::too_many_arguments)]
992fn emit_visitor_test_body(
993 out: &mut String,
994 fixture_id: &str,
995 html: &str,
996 options_value: Option<&serde_json::Value>,
997 visitor_spec: &crate::fixture::VisitorSpec,
998 module_name: &str,
999 assertions: &[Assertion],
1000 expects_error: bool,
1001 field_resolver: &FieldResolver,
1002) {
1003 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
1005 let _ = writeln!(out, " defer _ = gpa.deinit();");
1006 let _ = writeln!(out, " const allocator = gpa.allocator();");
1007 let _ = writeln!(out);
1008
1009 let visitor_block = super::zig_visitors::build_zig_visitor(fixture_id, module_name, visitor_spec);
1011 out.push_str(&visitor_block);
1012
1013 let _ = writeln!(
1016 out,
1017 " const _visitor = {module_name}.c.htm_visitor_create(&_callbacks);"
1018 );
1019 let _ = writeln!(out, " defer {module_name}.c.htm_visitor_free(_visitor);");
1020
1021 let options_json = match options_value {
1025 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1026 None => "{}".to_string(),
1027 };
1028 let escaped_options = escape_zig(&options_json);
1029 let _ = writeln!(
1030 out,
1031 " const _options_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_options}\");"
1032 );
1033 let _ = writeln!(out, " defer std.heap.c_allocator.free(_options_z);");
1034 let _ = writeln!(
1035 out,
1036 " const _options = {module_name}.c.htm_conversion_options_from_json(_options_z.ptr);"
1037 );
1038 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_options_free(_options);");
1039 let _ = writeln!(
1040 out,
1041 " {module_name}.c.htm_options_set_visitor_handle(_options, _visitor);"
1042 );
1043
1044 let escaped_html = escape_zig(html);
1046 let _ = writeln!(
1047 out,
1048 " const _html_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_html}\");"
1049 );
1050 let _ = writeln!(out, " defer std.heap.c_allocator.free(_html_z);");
1051 let _ = writeln!(
1052 out,
1053 " const _result = {module_name}.c.htm_convert(_html_z.ptr, _options);"
1054 );
1055
1056 if expects_error {
1057 let _ = writeln!(
1059 out,
1060 " try testing.expect(_result == null or {module_name}.c.htm_last_error_code() != 0);"
1061 );
1062 let _ = writeln!(
1063 out,
1064 " if (_result) |r| {module_name}.c.htm_conversion_result_free(r);"
1065 );
1066 return;
1067 }
1068
1069 let _ = writeln!(out, " try testing.expect(_result != null);");
1070 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_result_free(_result.?);");
1071 let _ = writeln!(
1072 out,
1073 " const _json_ptr = {module_name}.c.htm_conversion_result_to_json(_result.?);"
1074 );
1075 let _ = writeln!(out, " defer {module_name}.c.htm_free_string(_json_ptr);");
1076 let _ = writeln!(out, " const _result_json = std.mem.sliceTo(_json_ptr, 0);");
1077 let _ = writeln!(
1078 out,
1079 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
1080 );
1081 let _ = writeln!(out, " defer _parsed.deinit();");
1082 let _ = writeln!(out, " const result = &_parsed.value;");
1083
1084 for assertion in assertions {
1085 if assertion.assertion_type != "error" {
1086 render_json_assertion(out, assertion, "result", field_resolver, false);
1087 }
1088 }
1089}
1090
1091const FORMAT_METADATA_VARIANTS: &[&str] = &[
1109 "pdf",
1110 "docx",
1111 "excel",
1112 "email",
1113 "pptx",
1114 "archive",
1115 "image",
1116 "xml",
1117 "text",
1118 "html",
1119 "ocr",
1120 "csv",
1121 "bibtex",
1122 "citation",
1123 "fiction_book",
1124 "dbf",
1125 "jats",
1126 "epub",
1127 "pst",
1128 "code",
1129];
1130
1131fn json_path_expr(result_var: &str, field_path: &str) -> String {
1132 let segments: Vec<&str> = field_path.split('.').collect();
1133 let mut expr = result_var.to_string();
1134 let mut prev_seg: Option<&str> = None;
1135 for seg in &segments {
1136 if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
1141 prev_seg = Some(seg);
1142 continue;
1143 }
1144 if let Some(key) = seg.strip_suffix("[]") {
1148 expr = format!("{expr}.object.get(\"{key}\").?.array.items[0]");
1149 } else if let Some(bracket_pos) = seg.find('[') {
1150 if let Some(end_pos) = seg.find(']') {
1151 if end_pos > bracket_pos + 1 && end_pos == seg.len() - 1 {
1152 let key = &seg[..bracket_pos];
1153 let idx = &seg[bracket_pos + 1..end_pos];
1154 if idx.chars().all(|c| c.is_ascii_digit()) {
1155 expr = format!("{expr}.object.get(\"{key}\").?.array.items[{idx}]");
1156 prev_seg = Some(seg);
1157 continue;
1158 }
1159 expr = format!("{expr}.object.get(\"{key}\").?.object.get(\"{idx}\").?");
1165 prev_seg = Some(seg);
1166 continue;
1167 }
1168 }
1169 expr = format!("{expr}.object.get(\"{seg}\").?");
1170 } else {
1171 expr = format!("{expr}.object.get(\"{seg}\").?");
1172 }
1173 prev_seg = Some(seg);
1174 }
1175 expr
1176}
1177
1178fn render_json_assertion(
1183 out: &mut String,
1184 assertion: &Assertion,
1185 result_var: &str,
1186 field_resolver: &FieldResolver,
1187 uses_streaming: bool,
1188) {
1189 if let Some(f) = &assertion.field {
1196 if uses_streaming && !f.is_empty() && is_streaming_virtual_field(f) {
1197 if let Some(expr) = StreamingFieldResolver::accessor(f, "zig", "chunks") {
1198 match assertion.assertion_type.as_str() {
1199 "count_min" => {
1200 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1201 let _ = writeln!(out, " try testing.expect({expr}.len >= {n});");
1202 }
1203 }
1204 "count_equals" => {
1205 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1206 let _ = writeln!(out, " try testing.expectEqual(@as(usize, {n}), {expr}.len);");
1207 }
1208 }
1209 "equals" => {
1210 if let Some(serde_json::Value::String(s)) = &assertion.value {
1211 let escaped = escape_zig(s);
1212 let _ = writeln!(out, " try testing.expectEqualStrings(\"{escaped}\", {expr});");
1213 } else if let Some(v) = &assertion.value {
1214 let zig_val = json_to_zig(v);
1215 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {expr});");
1216 }
1217 }
1218 "not_empty" => {
1219 let _ = writeln!(out, " try testing.expect({expr}.len > 0);");
1220 }
1221 "is_true" => {
1222 let _ = writeln!(out, " try testing.expect({expr});");
1223 }
1224 "is_false" => {
1225 let _ = writeln!(out, " try testing.expect(!{expr});");
1226 }
1227 _ => {
1228 let atype = &assertion.assertion_type;
1229 let _ = writeln!(
1230 out,
1231 " // streaming virtual field '{f}' assertion '{atype}' not implemented for zig"
1232 );
1233 }
1234 }
1235 }
1236 return;
1237 }
1238 }
1239
1240 if let Some(f) = &assertion.field {
1247 if f == "embeddings" && !field_resolver.has_explicit_field("embeddings") {
1248 match assertion.assertion_type.as_str() {
1249 "count_min" => {
1250 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1251 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len >= {n});");
1252 }
1253 return;
1254 }
1255 "count_equals" => {
1256 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1257 let _ = writeln!(
1258 out,
1259 " try testing.expectEqual(@as(usize, {n}), {result_var}.array.items.len);"
1260 );
1261 }
1262 return;
1263 }
1264 "not_empty" => {
1265 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len > 0);");
1266 return;
1267 }
1268 "is_empty" => {
1269 let _ = writeln!(
1270 out,
1271 " try testing.expectEqual(@as(usize, 0), {result_var}.array.items.len);"
1272 );
1273 return;
1274 }
1275 _ => {}
1276 }
1277 }
1278 }
1279
1280 if let Some(f) = &assertion.field {
1282 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1283 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1284 return;
1285 }
1286 }
1287 if matches!(assertion.assertion_type.as_str(), "not_error" | "error") {
1289 return;
1290 }
1291
1292 let raw_field_path = assertion.field.as_deref().unwrap_or("").trim();
1293 let field_path = if raw_field_path.is_empty() {
1294 raw_field_path.to_string()
1295 } else {
1296 field_resolver.resolve(raw_field_path).to_string()
1297 };
1298 let field_path = field_path.trim();
1299
1300 let (field_path_for_expr, is_length_access) = if let Some(parent) = field_path.strip_suffix(".length") {
1302 (parent, true)
1303 } else {
1304 (field_path, false)
1305 };
1306
1307 let field_expr = if field_path_for_expr.is_empty() {
1308 result_var.to_string()
1309 } else {
1310 json_path_expr(result_var, field_path_for_expr)
1311 };
1312
1313 let zig_val = match &assertion.value {
1315 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_zig(s)),
1316 _ => String::new(),
1317 };
1318 let is_string_val = matches!(&assertion.value, Some(serde_json::Value::String(_)));
1319 let is_bool_val = matches!(&assertion.value, Some(serde_json::Value::Bool(_)));
1320 let bool_val = match &assertion.value {
1321 Some(serde_json::Value::Bool(b)) if *b => "true",
1322 _ => "false",
1323 };
1324 let is_null_val = matches!(&assertion.value, Some(serde_json::Value::Null));
1325 let n = assertion.value.as_ref().map(json_to_zig).unwrap_or_default();
1326 let has_n = assertion.value.as_ref().is_some_and(|v| v.is_number() || v.is_u64());
1327 let is_float_val = matches!(&assertion.value, Some(serde_json::Value::Number(n)) if !n.is_i64() && !n.is_u64());
1332 let values_list: Vec<String> = assertion
1333 .values
1334 .as_deref()
1335 .unwrap_or_default()
1336 .iter()
1337 .filter_map(|v| {
1338 if let serde_json::Value::String(s) = v {
1339 Some(format!("\"{}\"", escape_zig(s)))
1340 } else {
1341 None
1342 }
1343 })
1344 .collect();
1345
1346 let rendered = crate::template_env::render(
1347 "zig/json_assertion.jinja",
1348 minijinja::context! {
1349 assertion_type => assertion.assertion_type.as_str(),
1350 field_expr => field_expr,
1351 is_length_access => is_length_access,
1352 zig_val => zig_val,
1353 is_string_val => is_string_val,
1354 is_bool_val => is_bool_val,
1355 bool_val => bool_val,
1356 is_null_val => is_null_val,
1357 n => n,
1358 has_n => has_n,
1359 is_float_val => is_float_val,
1360 values_list => values_list,
1361 },
1362 );
1363 out.push_str(&rendered);
1364}
1365
1366fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
1369 if let Some(f) = &assertion.field {
1370 if !f.is_empty() && is_streaming_virtual_field(f) {
1371 } else if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1374 return false;
1375 }
1376 }
1377 matches!(
1378 assertion.assertion_type.as_str(),
1379 "equals"
1380 | "contains"
1381 | "contains_all"
1382 | "not_contains"
1383 | "not_empty"
1384 | "is_empty"
1385 | "starts_with"
1386 | "ends_with"
1387 | "min_length"
1388 | "max_length"
1389 | "count_min"
1390 | "count_equals"
1391 | "is_true"
1392 | "is_false"
1393 | "greater_than"
1394 | "less_than"
1395 | "greater_than_or_equal"
1396 | "less_than_or_equal"
1397 | "contains_any"
1398 )
1399}
1400
1401fn build_args_and_setup(
1406 input: &serde_json::Value,
1407 args: &[crate::config::ArgMapping],
1408 fixture_id: &str,
1409 _module_name: &str,
1410) -> (Vec<String>, String, bool) {
1411 if args.is_empty() {
1412 return (Vec::new(), String::new(), false);
1413 }
1414
1415 let mut setup_lines: Vec<String> = Vec::new();
1416 let mut parts: Vec<String> = Vec::new();
1417 let mut setup_needs_gpa = false;
1418
1419 for arg in args {
1420 if arg.arg_type == "mock_url" {
1421 let name = arg.name.clone();
1422 let id_upper = fixture_id.to_uppercase();
1423 setup_lines.push(format!(
1424 "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\"}});"
1425 ));
1426 setup_lines.push(format!("defer allocator.free({name});"));
1427 parts.push(name);
1428 setup_needs_gpa = true;
1429 continue;
1430 }
1431
1432 if arg.arg_type == "handle" {
1435 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1436 let json_str = match input.get(field) {
1437 Some(serde_json::Value::Null) | None => "null".to_string(),
1438 Some(v) => format!("\"{}\"", escape_zig(&serde_json::to_string(v).unwrap_or_default())),
1439 };
1440 parts.push(json_str);
1441 continue;
1442 }
1443
1444 if arg.name == "config" && arg.arg_type == "json_object" {
1451 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1452 let json_str = match input.get(field) {
1453 Some(serde_json::Value::Null) | None => "{}".to_string(),
1454 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1455 };
1456 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1457 continue;
1458 }
1459
1460 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1461 let val = if field.is_empty() || field == "input" {
1467 Some(input)
1468 } else {
1469 input.get(field)
1470 };
1471 match val {
1472 None | Some(serde_json::Value::Null) if arg.optional => {
1473 parts.push("null".to_string());
1476 }
1477 None | Some(serde_json::Value::Null) => {
1478 let default_val = match arg.arg_type.as_str() {
1479 "string" => "\"\"".to_string(),
1480 "int" | "integer" => "0".to_string(),
1481 "float" | "number" => "0.0".to_string(),
1482 "bool" | "boolean" => "false".to_string(),
1483 "json_object" => "\"{}\"".to_string(),
1484 _ => "null".to_string(),
1485 };
1486 parts.push(default_val);
1487 }
1488 Some(v) => {
1489 if arg.arg_type == "json_object" {
1494 let json_str = serde_json::to_string(v).unwrap_or_default();
1495 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1496 } else if arg.arg_type == "bytes" {
1497 if let serde_json::Value::String(path) = v {
1502 let var_name = format!("{}_bytes", arg.name);
1503 let epath = escape_zig(path);
1504 setup_lines.push(format!(
1505 "const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
1506 ));
1507 setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
1508 parts.push(var_name);
1509 } else {
1510 parts.push(json_to_zig(v));
1511 }
1512 } else {
1513 parts.push(json_to_zig(v));
1514 }
1515 }
1516 }
1517 }
1518
1519 (setup_lines, parts.join(", "), setup_needs_gpa)
1520}
1521
1522fn render_assertion(
1523 out: &mut String,
1524 assertion: &Assertion,
1525 result_var: &str,
1526 field_resolver: &FieldResolver,
1527 enum_fields: &HashSet<String>,
1528 result_is_option: bool,
1529) {
1530 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1533 if bare_result_is_option {
1534 match assertion.assertion_type.as_str() {
1535 "is_empty" => {
1536 let _ = writeln!(out, " try testing.expect({result_var} == null);");
1537 return;
1538 }
1539 "not_empty" | "not_error" => {
1540 let _ = writeln!(out, " try testing.expect({result_var} != null);");
1541 return;
1542 }
1543 _ => {}
1544 }
1545 }
1546 if let Some(f) = &assertion.field {
1550 if f == "embeddings" && !field_resolver.is_valid_for_result(f) {
1551 match assertion.assertion_type.as_str() {
1552 "count_min" | "count_equals" | "not_empty" | "is_empty" => {
1553 let _ = writeln!(out, " {{");
1554 let _ = writeln!(
1555 out,
1556 " var _eparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {result_var}, .{{}});"
1557 );
1558 let _ = writeln!(out, " defer _eparse.deinit();");
1559 let _ = writeln!(out, " const _embeddings_len = _eparse.value.array.items.len;");
1560 match assertion.assertion_type.as_str() {
1561 "count_min" => {
1562 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1563 let _ = writeln!(out, " try testing.expect(_embeddings_len >= {n});");
1564 }
1565 }
1566 "count_equals" => {
1567 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1568 let _ = writeln!(
1569 out,
1570 " try testing.expectEqual(@as(usize, {n}), _embeddings_len);"
1571 );
1572 }
1573 }
1574 "not_empty" => {
1575 let _ = writeln!(out, " try testing.expect(_embeddings_len > 0);");
1576 }
1577 "is_empty" => {
1578 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _embeddings_len);");
1579 }
1580 _ => {}
1581 }
1582 let _ = writeln!(out, " }}");
1583 return;
1584 }
1585 _ => {}
1586 }
1587 }
1588 }
1589
1590 if let Some(f) = &assertion.field {
1592 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1593 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
1594 return;
1595 }
1596 }
1597
1598 let _field_is_enum = assertion
1600 .field
1601 .as_deref()
1602 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1603
1604 let field_expr = match &assertion.field {
1605 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
1606 _ => result_var.to_string(),
1607 };
1608
1609 match assertion.assertion_type.as_str() {
1610 "equals" => {
1611 if let Some(expected) = &assertion.value {
1612 let zig_val = json_to_zig(expected);
1613 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
1614 }
1615 }
1616 "contains" => {
1617 if let Some(expected) = &assertion.value {
1618 let zig_val = json_to_zig(expected);
1619 let _ = writeln!(
1620 out,
1621 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1622 );
1623 }
1624 }
1625 "contains_all" => {
1626 if let Some(values) = &assertion.values {
1627 for val in values {
1628 let zig_val = json_to_zig(val);
1629 let _ = writeln!(
1630 out,
1631 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1632 );
1633 }
1634 }
1635 }
1636 "not_contains" => {
1637 if let Some(expected) = &assertion.value {
1638 let zig_val = json_to_zig(expected);
1639 let _ = writeln!(
1640 out,
1641 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1642 );
1643 } else if let Some(values) = &assertion.values {
1644 for val in values {
1648 let zig_val = json_to_zig(val);
1649 let _ = writeln!(
1650 out,
1651 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1652 );
1653 }
1654 }
1655 }
1656 "not_empty" => {
1657 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
1658 }
1659 "is_empty" => {
1660 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
1661 }
1662 "starts_with" => {
1663 if let Some(expected) = &assertion.value {
1664 let zig_val = json_to_zig(expected);
1665 let _ = writeln!(
1666 out,
1667 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
1668 );
1669 }
1670 }
1671 "ends_with" => {
1672 if let Some(expected) = &assertion.value {
1673 let zig_val = json_to_zig(expected);
1674 let _ = writeln!(
1675 out,
1676 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
1677 );
1678 }
1679 }
1680 "min_length" => {
1681 if let Some(val) = &assertion.value {
1682 if let Some(n) = val.as_u64() {
1683 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1684 }
1685 }
1686 }
1687 "max_length" => {
1688 if let Some(val) = &assertion.value {
1689 if let Some(n) = val.as_u64() {
1690 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
1691 }
1692 }
1693 }
1694 "count_min" => {
1695 if let Some(val) = &assertion.value {
1696 if let Some(n) = val.as_u64() {
1697 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1698 }
1699 }
1700 }
1701 "count_equals" => {
1702 if let Some(val) = &assertion.value {
1703 if let Some(n) = val.as_u64() {
1704 let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
1708 if has_field {
1709 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
1710 } else {
1711 let _ = writeln!(out, " {{");
1712 let _ = writeln!(
1713 out,
1714 " var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
1715 );
1716 let _ = writeln!(out, " defer _cparse.deinit();");
1717 let _ = writeln!(
1718 out,
1719 " try testing.expectEqual({n}, _cparse.value.array.items.len);"
1720 );
1721 let _ = writeln!(out, " }}");
1722 }
1723 }
1724 }
1725 }
1726 "is_true" => {
1727 let _ = writeln!(out, " try testing.expect({field_expr});");
1728 }
1729 "is_false" => {
1730 let _ = writeln!(out, " try testing.expect(!{field_expr});");
1731 }
1732 "not_error" => {
1733 }
1735 "error" => {
1736 }
1738 "greater_than" => {
1739 if let Some(val) = &assertion.value {
1740 let zig_val = json_to_zig(val);
1741 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
1742 }
1743 }
1744 "less_than" => {
1745 if let Some(val) = &assertion.value {
1746 let zig_val = json_to_zig(val);
1747 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
1748 }
1749 }
1750 "greater_than_or_equal" => {
1751 if let Some(val) = &assertion.value {
1752 let zig_val = json_to_zig(val);
1753 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
1754 }
1755 }
1756 "less_than_or_equal" => {
1757 if let Some(val) = &assertion.value {
1758 let zig_val = json_to_zig(val);
1759 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
1760 }
1761 }
1762 "contains_any" => {
1763 if let Some(values) = &assertion.values {
1765 let string_values: Vec<String> = values
1766 .iter()
1767 .filter_map(|v| {
1768 if let serde_json::Value::String(s) = v {
1769 Some(format!(
1770 "std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
1771 escape_zig(s)
1772 ))
1773 } else {
1774 None
1775 }
1776 })
1777 .collect();
1778 if !string_values.is_empty() {
1779 let condition = string_values.join(" or\n ");
1780 let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
1781 }
1782 }
1783 }
1784 "matches_regex" => {
1785 let _ = writeln!(out, " // regex match not yet implemented for Zig");
1786 }
1787 "method_result" => {
1788 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
1789 }
1790 other => {
1791 panic!("Zig e2e generator: unsupported assertion type: {other}");
1792 }
1793 }
1794}
1795
1796fn json_to_zig(value: &serde_json::Value) -> String {
1798 match value {
1799 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
1800 serde_json::Value::Bool(b) => b.to_string(),
1801 serde_json::Value::Number(n) => n.to_string(),
1802 serde_json::Value::Null => "null".to_string(),
1803 serde_json::Value::Array(arr) => {
1804 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
1805 format!("&.{{{}}}", items.join(", "))
1806 }
1807 serde_json::Value::Object(_) => {
1808 let json_str = serde_json::to_string(value).unwrap_or_default();
1809 format!("\"{}\"", escape_zig(&json_str))
1810 }
1811 }
1812}