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