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 has_file_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
95 let cc = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
96 cc.args
97 .iter()
98 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
99 });
100
101 let mut test_filenames: Vec<String> = Vec::new();
103 for group in groups {
104 let active: Vec<&Fixture> = group
105 .fixtures
106 .iter()
107 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
108 .collect();
109
110 if active.is_empty() {
111 continue;
112 }
113
114 let filename = format!("{}_test.zig", sanitize_filename(&group.category));
115 test_filenames.push(filename.clone());
116 let content = render_test_file(
117 &group.category,
118 &active,
119 e2e_config,
120 &function_name,
121 result_var,
122 &e2e_config.call.args,
123 &field_resolver,
124 &e2e_config.fields_enum,
125 &module_name,
126 &ffi_prefix,
127 );
128 files.push(GeneratedFile {
129 path: output_base.join("src").join(filename),
130 content,
131 generated_header: true,
132 });
133 }
134
135 files.insert(
137 files
138 .iter()
139 .position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
140 .unwrap_or(1),
141 GeneratedFile {
142 path: output_base.join("build.zig"),
143 content: render_build_zig(
144 &test_filenames,
145 &pkg_name,
146 &module_name,
147 &config.ffi_lib_name(),
148 &config.ffi_crate_path(),
149 has_file_fixtures,
150 &e2e_config.test_documents_relative_from(0),
151 ),
152 generated_header: false,
153 },
154 );
155
156 Ok(files)
157 }
158
159 fn language_name(&self) -> &'static str {
160 "zig"
161 }
162}
163
164fn render_build_zig_zon(pkg_name: &str, pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
169 let dep_block = match dep_mode {
170 crate::config::DependencyMode::Registry => {
171 format!(
173 r#".{{
174 .url = "https://registry.example.com/{pkg_name}/v0.1.0.tar.gz",
175 .hash = "0000000000000000000000000000000000000000000000000000000000000000",
176 }}"#
177 )
178 }
179 crate::config::DependencyMode::Local => {
180 format!(r#".{{ .path = "{pkg_path}" }}"#)
181 }
182 };
183
184 let min_zig = toolchain::MIN_ZIG_VERSION;
185 let name_bytes: &[u8] = b"e2e_zig";
187 let mut crc: u32 = 0xffff_ffff;
188 for byte in name_bytes {
189 crc ^= *byte as u32;
190 for _ in 0..8 {
191 let mask = (crc & 1).wrapping_neg();
192 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
193 }
194 }
195 let name_crc: u32 = !crc;
196 let mut id: u32 = 0x811c_9dc5;
197 for byte in name_bytes {
198 id ^= *byte as u32;
199 id = id.wrapping_mul(0x0100_0193);
200 }
201 if id == 0 || id == 0xffff_ffff {
202 id = 0x1;
203 }
204 let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
205 format!(
206 r#".{{
207 .name = .e2e_zig,
208 .version = "0.1.0",
209 .fingerprint = 0x{fingerprint:016x},
210 .minimum_zig_version = "{min_zig}",
211 .dependencies = .{{
212 .{pkg_name} = {dep_block},
213 }},
214 .paths = .{{
215 "build.zig",
216 "build.zig.zon",
217 "src",
218 }},
219}}
220"#
221 )
222}
223
224fn render_build_zig(
225 test_filenames: &[String],
226 pkg_name: &str,
227 module_name: &str,
228 ffi_lib_name: &str,
229 ffi_crate_path: &str,
230 has_file_fixtures: bool,
231 test_documents_path: &str,
232) -> String {
233 if test_filenames.is_empty() {
234 return r#"const std = @import("std");
235
236pub fn build(b: *std.Build) void {
237 const target = b.standardTargetOptions(.{});
238 const optimize = b.standardOptimizeOption(.{});
239
240 const test_step = b.step("test", "Run tests");
241}
242"#
243 .to_string();
244 }
245
246 let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
256 content.push_str(" const target = b.standardTargetOptions(.{});\n");
257 content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
258 content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n");
259 let _ = writeln!(
260 content,
261 " const ffi_path = b.option([]const u8, \"ffi_path\", \"Path to directory containing lib{ffi_lib_name}\") orelse \"../../target/debug\";"
262 );
263 let _ = writeln!(
264 content,
265 " const ffi_include = b.option([]const u8, \"ffi_include_path\", \"Path to directory containing FFI header\") orelse \"{ffi_crate_path}/include\";"
266 );
267 let _ = writeln!(content);
268 let _ = writeln!(
269 content,
270 " const {module_name}_module = b.addModule(\"{module_name}\", .{{"
271 );
272 let _ = writeln!(
273 content,
274 " .root_source_file = b.path(\"../../packages/zig/src/{pkg_name}.zig\"),"
275 );
276 content.push_str(" .target = target,\n");
277 content.push_str(" .optimize = optimize,\n");
278 content.push_str(" .link_libc = true,\n");
282 content.push_str(" });\n");
283 let _ = writeln!(
284 content,
285 " {module_name}_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});"
286 );
287 let _ = writeln!(
288 content,
289 " {module_name}_module.addIncludePath(.{{ .cwd_relative = ffi_include }});"
290 );
291 let _ = writeln!(
292 content,
293 " {module_name}_module.linkSystemLibrary(\"{ffi_lib_name}\", .{{}});"
294 );
295 let _ = writeln!(content);
296
297 for filename in test_filenames {
298 let test_name = filename.trim_end_matches("_test.zig");
300 content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
301 content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
302 content.push_str(" .target = target,\n");
303 content.push_str(" .optimize = optimize,\n");
304 content.push_str(" .link_libc = true,\n");
308 content.push_str(" });\n");
309 content.push_str(&format!(
310 " {test_name}_module.addImport(\"{module_name}\", {module_name}_module);\n"
311 ));
312 content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
328 content.push_str(&format!(" .name = \"{test_name}_test\",\n"));
329 content.push_str(&format!(" .root_module = {test_name}_module,\n"));
330 content.push_str(" .use_llvm = true,\n");
331 content.push_str(" });\n");
332 content.push_str(&format!(
349 " const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
350 ));
351 if has_file_fixtures {
352 content.push_str(&format!(
353 " {test_name}_run.setCwd(b.path(\"{test_documents_path}\"));\n"
354 ));
355 }
356 content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
357 }
358
359 content.push_str("}\n");
360 content
361}
362
363struct ZigTestClientRenderer;
371
372impl client::TestClientRenderer for ZigTestClientRenderer {
373 fn language_name(&self) -> &'static str {
374 "zig"
375 }
376
377 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
378 if let Some(reason) = skip_reason {
379 let _ = writeln!(out, "test \"{fn_name}\" {{");
380 let _ = writeln!(out, " // {description}");
381 let _ = writeln!(out, " // skipped: {reason}");
382 let _ = writeln!(out, " return error.SkipZigTest;");
383 } else {
384 let _ = writeln!(out, "test \"{fn_name}\" {{");
385 let _ = writeln!(out, " // {description}");
386 }
387 }
388
389 fn render_test_close(&self, out: &mut String) {
390 let _ = writeln!(out, "}}");
391 }
392
393 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
394 let method = ctx.method.to_uppercase();
395 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
396
397 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
398 let _ = writeln!(out, " defer _ = gpa.deinit();");
399 let _ = writeln!(out, " const allocator = gpa.allocator();");
400
401 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
402 let _ = writeln!(
403 out,
404 " 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\"}});"
405 );
406
407 if !ctx.headers.is_empty() {
409 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
410 header_pairs.sort_by_key(|(k, _)| k.as_str());
411 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
412 for (k, v) in &header_pairs {
413 let ek = escape_zig(k);
414 let ev = escape_zig(v);
415 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
416 }
417 let _ = writeln!(out, " }};");
418 }
419
420 if let Some(body) = ctx.body {
422 let json_str = serde_json::to_string(body).unwrap_or_default();
423 let escaped = escape_zig(&json_str);
424 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
425 }
426
427 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
428 let has_body = ctx.body.is_some();
429
430 let _ = writeln!(
431 out,
432 " var http_client = std.http.Client{{ .allocator = allocator }};"
433 );
434 let _ = writeln!(out, " defer http_client.deinit();");
435 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
436 let _ = writeln!(out, " defer response_body.deinit();");
437
438 let method_zig = match method.as_str() {
439 "GET" => ".GET",
440 "POST" => ".POST",
441 "PUT" => ".PUT",
442 "DELETE" => ".DELETE",
443 "PATCH" => ".PATCH",
444 "HEAD" => ".HEAD",
445 "OPTIONS" => ".OPTIONS",
446 _ => ".GET",
447 };
448
449 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
450 let _ = writeln!(
451 out,
452 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
453 rv = ctx.response_var,
454 );
455 }
456
457 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
458 let _ = writeln!(
459 out,
460 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
461 );
462 }
463
464 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
465 let ename = escape_zig(&name.to_lowercase());
466 match expected {
467 "<<present>>" => {
468 let _ = writeln!(
469 out,
470 " // assert header '{ename}' is present (header inspection not yet implemented)"
471 );
472 }
473 "<<absent>>" => {
474 let _ = writeln!(
475 out,
476 " // assert header '{ename}' is absent (header inspection not yet implemented)"
477 );
478 }
479 "<<uuid>>" => {
480 let _ = writeln!(
481 out,
482 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
483 );
484 }
485 exact => {
486 let evalue = escape_zig(exact);
487 let _ = writeln!(
488 out,
489 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
490 );
491 }
492 }
493 }
494
495 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
496 let json_str = serde_json::to_string(expected).unwrap_or_default();
497 let escaped = escape_zig(&json_str);
498 let _ = writeln!(
499 out,
500 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
501 );
502 }
503
504 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
505 if let Some(obj) = expected.as_object() {
506 for (key, val) in obj {
507 let ekey = escape_zig(key);
508 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
509 let _ = writeln!(
510 out,
511 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
512 );
513 }
514 }
515 }
516
517 fn render_assert_validation_errors(
518 &self,
519 out: &mut String,
520 _response_var: &str,
521 errors: &[crate::fixture::ValidationErrorExpectation],
522 ) {
523 for ve in errors {
524 let loc = ve.loc.join(".");
525 let escaped_loc = escape_zig(&loc);
526 let escaped_msg = escape_zig(&ve.msg);
527 let _ = writeln!(
528 out,
529 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
530 );
531 }
532 }
533}
534
535fn render_http_test_case(out: &mut String, fixture: &Fixture) {
540 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
541}
542
543#[allow(clippy::too_many_arguments)]
548fn render_test_file(
549 category: &str,
550 fixtures: &[&Fixture],
551 e2e_config: &E2eConfig,
552 function_name: &str,
553 result_var: &str,
554 args: &[crate::config::ArgMapping],
555 field_resolver: &FieldResolver,
556 enum_fields: &HashSet<String>,
557 module_name: &str,
558 ffi_prefix: &str,
559) -> String {
560 let mut out = String::new();
561 out.push_str(&hash::header(CommentStyle::DoubleSlash));
562 let _ = writeln!(out, "const std = @import(\"std\");");
563 let _ = writeln!(out, "const testing = std.testing;");
564 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
565 let _ = writeln!(out);
566
567 let _ = writeln!(out, "// E2e tests for category: {category}");
568 let _ = writeln!(out);
569
570 for fixture in fixtures {
571 if fixture.http.is_some() {
572 render_http_test_case(&mut out, fixture);
573 } else {
574 render_test_fn(
575 &mut out,
576 fixture,
577 e2e_config,
578 function_name,
579 result_var,
580 args,
581 field_resolver,
582 enum_fields,
583 module_name,
584 ffi_prefix,
585 );
586 }
587 let _ = writeln!(out);
588 }
589
590 out
591}
592
593#[allow(clippy::too_many_arguments)]
594fn render_test_fn(
595 out: &mut String,
596 fixture: &Fixture,
597 e2e_config: &E2eConfig,
598 _function_name: &str,
599 _result_var: &str,
600 _args: &[crate::config::ArgMapping],
601 field_resolver: &FieldResolver,
602 enum_fields: &HashSet<String>,
603 module_name: &str,
604 ffi_prefix: &str,
605) {
606 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
608 let lang = "zig";
609 let call_overrides = call_config.overrides.get(lang);
610 let function_name = call_overrides
611 .and_then(|o| o.function.as_ref())
612 .cloned()
613 .unwrap_or_else(|| call_config.function.clone());
614 let result_var = &call_config.result_var;
615 let args = &call_config.args;
616 let client_factory = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
621 e2e_config
622 .call
623 .overrides
624 .get(lang)
625 .and_then(|o| o.client_factory.as_deref())
626 });
627
628 let call_result_is_bytes = call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
645 let result_is_json_struct =
646 !call_result_is_bytes && (call_overrides.is_some_and(|o| o.result_is_json_struct) || client_factory.is_some());
647
648 let result_is_option = call_overrides.is_some_and(|o| o.result_is_option) || call_config.result_is_option;
653
654 let call_returns_error_union = call_overrides.and_then(|o| o.returns_result) != Some(false);
675
676 let test_name = fixture.id.to_snake_case();
677 let description = &fixture.description;
678 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
679
680 let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
681 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
687 let args_str = if extra_args.is_empty() {
688 args_str
689 } else if args_str.is_empty() {
690 extra_args.join(", ")
691 } else {
692 format!("{args_str}, {}", extra_args.join(", "))
693 };
694
695 let any_happy_emits_code = fixture
698 .assertions
699 .iter()
700 .any(|a| assertion_emits_code(a, field_resolver));
701 let any_non_error_emits_code = fixture
702 .assertions
703 .iter()
704 .filter(|a| a.assertion_type != "error")
705 .any(|a| assertion_emits_code(a, field_resolver));
706
707 let has_streaming_virtual_assertions = fixture.assertions.iter().any(|a| {
709 a.field
710 .as_ref()
711 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
712 });
713 let is_stream_fn = function_name.contains("stream");
714 let uses_streaming_virtual_path =
715 result_is_json_struct && has_streaming_virtual_assertions && is_stream_fn && client_factory.is_some();
716 let streaming_path_has_non_streaming = uses_streaming_virtual_path
718 && fixture.assertions.iter().any(|a| {
719 !a.field
720 .as_ref()
721 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
722 && !matches!(a.assertion_type.as_str(), "not_error" | "error")
723 && a.field
724 .as_ref()
725 .is_some_and(|f| !f.is_empty() && field_resolver.is_valid_for_result(f))
726 });
727
728 let _ = writeln!(out, "test \"{test_name}\" {{");
729 let _ = writeln!(out, " // {description}");
730
731 if let Some(visitor_spec) = &fixture.visitor {
736 let html = fixture.input.get("html").and_then(|v| v.as_str()).unwrap_or_default();
737 let options_value = fixture.input.get("options").cloned();
738 emit_visitor_test_body(
739 out,
740 &fixture.id,
741 html,
742 options_value.as_ref(),
743 visitor_spec,
744 module_name,
745 &fixture.assertions,
746 expects_error,
747 field_resolver,
748 );
749 let _ = writeln!(out, "}}");
750 let _ = writeln!(out);
751 return;
752 }
753
754 let needs_gpa = setup_needs_gpa
762 || streaming_path_has_non_streaming
763 || (!uses_streaming_virtual_path && result_is_json_struct && !expects_error && any_happy_emits_code)
764 || (!uses_streaming_virtual_path && result_is_json_struct && expects_error && any_non_error_emits_code);
765 if needs_gpa {
766 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
767 let _ = writeln!(out, " defer _ = gpa.deinit();");
768 let _ = writeln!(out, " const allocator = gpa.allocator();");
769 let _ = writeln!(out);
770 }
771
772 for line in &setup_lines {
773 let _ = writeln!(out, " {line}");
774 }
775
776 let call_prefix = if let Some(factory) = client_factory {
781 let fixture_id = &fixture.id;
782 let _ = writeln!(
783 out,
784 " 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);"
785 );
786 let _ = writeln!(out, " defer std.heap.c_allocator.free(_mock_url);");
787 let _ = writeln!(
788 out,
789 " var _client = try {module_name}.{factory}(\"test-key\", _mock_url, null, null, null);"
790 );
791 let _ = writeln!(out, " defer _client.free();");
792 "_client".to_string()
793 } else {
794 module_name.to_string()
795 };
796
797 if expects_error {
798 if result_is_json_struct {
802 let _ = writeln!(
803 out,
804 " const _result_json = {call_prefix}.{function_name}({args_str}) catch {{"
805 );
806 } else {
807 let _ = writeln!(
808 out,
809 " const result = {call_prefix}.{function_name}({args_str}) catch {{"
810 );
811 }
812 let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
813 let _ = writeln!(out, " return;");
814 let _ = writeln!(out, " }};");
815 let any_emits_code = fixture
819 .assertions
820 .iter()
821 .filter(|a| a.assertion_type != "error")
822 .any(|a| assertion_emits_code(a, field_resolver));
823 if result_is_json_struct && any_emits_code {
824 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
825 let _ = writeln!(
826 out,
827 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
828 );
829 let _ = writeln!(out, " defer _parsed.deinit();");
830 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
831 let _ = writeln!(out, " // Perform success assertions if any");
832 for assertion in &fixture.assertions {
833 if assertion.assertion_type != "error" {
834 render_json_assertion(out, assertion, result_var, field_resolver, false);
835 }
836 }
837 } else if result_is_json_struct {
838 let _ = writeln!(out, " _ = _result_json;");
839 } else if any_emits_code {
840 let _ = writeln!(out, " // Perform success assertions if any");
841 for assertion in &fixture.assertions {
842 if assertion.assertion_type != "error" {
843 render_assertion(
844 out,
845 assertion,
846 result_var,
847 field_resolver,
848 enum_fields,
849 result_is_option,
850 );
851 }
852 }
853 } else {
854 let _ = writeln!(out, " _ = result;");
855 }
856 } else if fixture.assertions.is_empty() {
857 if result_is_json_struct {
859 let _ = writeln!(
860 out,
861 " const _result_json = try {call_prefix}.{function_name}({args_str});"
862 );
863 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
864 } else if call_returns_error_union {
865 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
866 } else {
867 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
868 }
869 } else {
870 let any_emits_code = fixture
874 .assertions
875 .iter()
876 .any(|a| assertion_emits_code(a, field_resolver));
877 if call_result_is_bytes && client_factory.is_some() {
878 let _ = writeln!(
881 out,
882 " const _result_json = try {call_prefix}.{function_name}({args_str});"
883 );
884 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
885 let has_bytes_assertions = fixture
886 .assertions
887 .iter()
888 .any(|a| matches!(a.assertion_type.as_str(), "not_empty" | "is_empty"));
889 if has_bytes_assertions {
890 for assertion in &fixture.assertions {
891 match assertion.assertion_type.as_str() {
892 "not_empty" => {
893 let _ = writeln!(out, " try testing.expect(_result_json.len > 0);");
894 }
895 "is_empty" => {
896 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _result_json.len);");
897 }
898 "not_error" | "error" => {}
899 _ => {
900 let atype = &assertion.assertion_type;
901 let _ = writeln!(
902 out,
903 " // bytes result: assertion '{atype}' not implemented for zig bytes"
904 );
905 }
906 }
907 }
908 }
909 } else if result_is_json_struct {
910 if uses_streaming_virtual_path {
914 let request_from_json = format!("{ffi_prefix}_chat_completion_request_from_json");
915 let request_free = format!("{ffi_prefix}_chat_completion_request_free");
916 let stream_start = format!("{ffi_prefix}_default_client_chat_stream_start");
917 let stream_free = format!("{ffi_prefix}_default_client_chat_stream_free");
918 let client_c_type = format!("{}DefaultClient", ffi_prefix.to_shouty_snake_case());
919
920 let _ = writeln!(
923 out,
924 " const _req_z = try std.heap.c_allocator.dupeZ(u8, {args_str});"
925 );
926 let _ = writeln!(out, " defer std.heap.c_allocator.free(_req_z);");
927 let _ = writeln!(
928 out,
929 " const _req_handle = {module_name}.c.{request_from_json}(_req_z.ptr);"
930 );
931 let _ = writeln!(out, " defer {module_name}.c.{request_free}(_req_handle);");
932 let _ = writeln!(
933 out,
934 " const _stream_handle = {module_name}.c.{stream_start}(@as(*{module_name}.c.{client_c_type}, @ptrCast(_client._handle)), _req_handle);"
935 );
936 let _ = writeln!(out, " if (_stream_handle == null) return error.StreamStartFailed;");
937 let _ = writeln!(out, " defer {module_name}.c.{stream_free}(_stream_handle);");
938 let snip =
940 StreamingFieldResolver::collect_snippet_zig("_stream_handle", "chunks", module_name, ffi_prefix);
941 out.push_str(" ");
942 out.push_str(&snip);
943 out.push('\n');
944 if streaming_path_has_non_streaming {
947 let _ = writeln!(
948 out,
949 " const _result_json = if (chunks.items.len > 0) chunks.items[chunks.items.len - 1] else &[_]u8{{}};"
950 );
951 let _ = writeln!(
952 out,
953 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
954 );
955 let _ = writeln!(out, " defer _parsed.deinit();");
956 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
957 }
958 for assertion in &fixture.assertions {
959 render_json_assertion(out, assertion, result_var, field_resolver, true);
960 }
961 } else {
962 let _ = writeln!(
964 out,
965 " const _result_json = try {call_prefix}.{function_name}({args_str});"
966 );
967 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
968 if any_emits_code {
969 let _ = writeln!(
970 out,
971 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
972 );
973 let _ = writeln!(out, " defer _parsed.deinit();");
974 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
975 for assertion in &fixture.assertions {
976 render_json_assertion(out, assertion, result_var, field_resolver, false);
977 }
978 }
979 }
980 } else if any_emits_code {
981 let try_kw = if call_returns_error_union { "try " } else { "" };
982 let _ = writeln!(
983 out,
984 " const {result_var} = {try_kw}{call_prefix}.{function_name}({args_str});"
985 );
986 for assertion in &fixture.assertions {
987 render_assertion(
988 out,
989 assertion,
990 result_var,
991 field_resolver,
992 enum_fields,
993 result_is_option,
994 );
995 }
996 } else if call_returns_error_union {
997 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
998 } else {
999 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
1000 }
1001 }
1002
1003 let _ = writeln!(out, "}}");
1004}
1005
1006#[allow(clippy::too_many_arguments)]
1012fn emit_visitor_test_body(
1013 out: &mut String,
1014 fixture_id: &str,
1015 html: &str,
1016 options_value: Option<&serde_json::Value>,
1017 visitor_spec: &crate::fixture::VisitorSpec,
1018 module_name: &str,
1019 assertions: &[Assertion],
1020 expects_error: bool,
1021 field_resolver: &FieldResolver,
1022) {
1023 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
1025 let _ = writeln!(out, " defer _ = gpa.deinit();");
1026 let _ = writeln!(out, " const allocator = gpa.allocator();");
1027 let _ = writeln!(out);
1028
1029 let visitor_block = super::zig_visitors::build_zig_visitor(fixture_id, module_name, visitor_spec);
1031 out.push_str(&visitor_block);
1032
1033 let _ = writeln!(
1036 out,
1037 " const _visitor = {module_name}.c.htm_visitor_create(&_callbacks);"
1038 );
1039 let _ = writeln!(out, " defer {module_name}.c.htm_visitor_free(_visitor);");
1040
1041 let options_json = match options_value {
1045 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1046 None => "{}".to_string(),
1047 };
1048 let escaped_options = escape_zig(&options_json);
1049 let _ = writeln!(
1050 out,
1051 " const _options_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_options}\");"
1052 );
1053 let _ = writeln!(out, " defer std.heap.c_allocator.free(_options_z);");
1054 let _ = writeln!(
1055 out,
1056 " const _options = {module_name}.c.htm_conversion_options_from_json(_options_z.ptr);"
1057 );
1058 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_options_free(_options);");
1059 let _ = writeln!(
1060 out,
1061 " {module_name}.c.htm_options_set_visitor_handle(_options, _visitor);"
1062 );
1063
1064 let escaped_html = escape_zig(html);
1066 let _ = writeln!(
1067 out,
1068 " const _html_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_html}\");"
1069 );
1070 let _ = writeln!(out, " defer std.heap.c_allocator.free(_html_z);");
1071 let _ = writeln!(
1072 out,
1073 " const _result = {module_name}.c.htm_convert(_html_z.ptr, _options);"
1074 );
1075
1076 if expects_error {
1077 let _ = writeln!(
1079 out,
1080 " try testing.expect(_result == null or {module_name}.c.htm_last_error_code() != 0);"
1081 );
1082 let _ = writeln!(
1083 out,
1084 " if (_result) |r| {module_name}.c.htm_conversion_result_free(r);"
1085 );
1086 return;
1087 }
1088
1089 let _ = writeln!(out, " try testing.expect(_result != null);");
1090 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_result_free(_result.?);");
1091 let _ = writeln!(
1092 out,
1093 " const _json_ptr = {module_name}.c.htm_conversion_result_to_json(_result.?);"
1094 );
1095 let _ = writeln!(out, " defer {module_name}.c.htm_free_string(_json_ptr);");
1096 let _ = writeln!(out, " const _result_json = std.mem.sliceTo(_json_ptr, 0);");
1097 let _ = writeln!(
1098 out,
1099 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
1100 );
1101 let _ = writeln!(out, " defer _parsed.deinit();");
1102 let _ = writeln!(out, " const result = &_parsed.value;");
1103
1104 for assertion in assertions {
1105 if assertion.assertion_type != "error" {
1106 render_json_assertion(out, assertion, "result", field_resolver, false);
1107 }
1108 }
1109}
1110
1111const FORMAT_METADATA_VARIANTS: &[&str] = &[
1129 "pdf",
1130 "docx",
1131 "excel",
1132 "email",
1133 "pptx",
1134 "archive",
1135 "image",
1136 "xml",
1137 "text",
1138 "html",
1139 "ocr",
1140 "csv",
1141 "bibtex",
1142 "citation",
1143 "fiction_book",
1144 "dbf",
1145 "jats",
1146 "epub",
1147 "pst",
1148 "code",
1149];
1150
1151fn json_path_expr(result_var: &str, field_path: &str) -> String {
1152 let segments: Vec<&str> = field_path.split('.').collect();
1153 let mut expr = result_var.to_string();
1154 let mut prev_seg: Option<&str> = None;
1155 for seg in &segments {
1156 if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
1161 prev_seg = Some(seg);
1162 continue;
1163 }
1164 if let Some(key) = seg.strip_suffix("[]") {
1168 expr = format!("{expr}.object.get(\"{key}\").?.array.items[0]");
1169 } else if let Some(bracket_pos) = seg.find('[') {
1170 if let Some(end_pos) = seg.find(']') {
1171 if end_pos > bracket_pos + 1 && end_pos == seg.len() - 1 {
1172 let key = &seg[..bracket_pos];
1173 let idx = &seg[bracket_pos + 1..end_pos];
1174 if idx.chars().all(|c| c.is_ascii_digit()) {
1175 expr = format!("{expr}.object.get(\"{key}\").?.array.items[{idx}]");
1176 prev_seg = Some(seg);
1177 continue;
1178 }
1179 expr = format!("{expr}.object.get(\"{key}\").?.object.get(\"{idx}\").?");
1185 prev_seg = Some(seg);
1186 continue;
1187 }
1188 }
1189 expr = format!("{expr}.object.get(\"{seg}\").?");
1190 } else {
1191 expr = format!("{expr}.object.get(\"{seg}\").?");
1192 }
1193 prev_seg = Some(seg);
1194 }
1195 expr
1196}
1197
1198fn render_json_assertion(
1203 out: &mut String,
1204 assertion: &Assertion,
1205 result_var: &str,
1206 field_resolver: &FieldResolver,
1207 uses_streaming: bool,
1208) {
1209 if let Some(f) = &assertion.field {
1216 if uses_streaming && !f.is_empty() && is_streaming_virtual_field(f) {
1217 if let Some(expr) = StreamingFieldResolver::accessor(f, "zig", "chunks") {
1218 match assertion.assertion_type.as_str() {
1219 "count_min" => {
1220 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1221 let _ = writeln!(out, " try testing.expect({expr}.len >= {n});");
1222 }
1223 }
1224 "count_equals" => {
1225 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1226 let _ = writeln!(out, " try testing.expectEqual(@as(usize, {n}), {expr}.len);");
1227 }
1228 }
1229 "equals" => {
1230 if let Some(serde_json::Value::String(s)) = &assertion.value {
1231 let escaped = escape_zig(s);
1232 let _ = writeln!(out, " try testing.expectEqualStrings(\"{escaped}\", {expr});");
1233 } else if let Some(v) = &assertion.value {
1234 let zig_val = json_to_zig(v);
1235 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {expr});");
1236 }
1237 }
1238 "not_empty" => {
1239 let _ = writeln!(out, " try testing.expect({expr}.len > 0);");
1240 }
1241 "is_true" => {
1242 let _ = writeln!(out, " try testing.expect({expr});");
1243 }
1244 "is_false" => {
1245 let _ = writeln!(out, " try testing.expect(!{expr});");
1246 }
1247 _ => {
1248 let atype = &assertion.assertion_type;
1249 let _ = writeln!(
1250 out,
1251 " // streaming virtual field '{f}' assertion '{atype}' not implemented for zig"
1252 );
1253 }
1254 }
1255 }
1256 return;
1257 }
1258 }
1259
1260 if let Some(f) = &assertion.field {
1267 if f == "embeddings" && !field_resolver.has_explicit_field("embeddings") {
1268 match assertion.assertion_type.as_str() {
1269 "count_min" => {
1270 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1271 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len >= {n});");
1272 }
1273 return;
1274 }
1275 "count_equals" => {
1276 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1277 let _ = writeln!(
1278 out,
1279 " try testing.expectEqual(@as(usize, {n}), {result_var}.array.items.len);"
1280 );
1281 }
1282 return;
1283 }
1284 "not_empty" => {
1285 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len > 0);");
1286 return;
1287 }
1288 "is_empty" => {
1289 let _ = writeln!(
1290 out,
1291 " try testing.expectEqual(@as(usize, 0), {result_var}.array.items.len);"
1292 );
1293 return;
1294 }
1295 _ => {}
1296 }
1297 }
1298 }
1299
1300 if let Some(f) = &assertion.field {
1302 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1303 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1304 return;
1305 }
1306 }
1307 if matches!(assertion.assertion_type.as_str(), "not_error" | "error") {
1309 return;
1310 }
1311
1312 let raw_field_path = assertion.field.as_deref().unwrap_or("").trim();
1313 let field_path = if raw_field_path.is_empty() {
1314 raw_field_path.to_string()
1315 } else {
1316 field_resolver.resolve(raw_field_path).to_string()
1317 };
1318 let field_path = field_path.trim();
1319
1320 let (field_path_for_expr, is_length_access) = if let Some(parent) = field_path.strip_suffix(".length") {
1322 (parent, true)
1323 } else {
1324 (field_path, false)
1325 };
1326
1327 let field_expr = if field_path_for_expr.is_empty() {
1328 result_var.to_string()
1329 } else {
1330 json_path_expr(result_var, field_path_for_expr)
1331 };
1332
1333 let zig_val = match &assertion.value {
1335 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_zig(s)),
1336 _ => String::new(),
1337 };
1338 let is_string_val = matches!(&assertion.value, Some(serde_json::Value::String(_)));
1339 let is_bool_val = matches!(&assertion.value, Some(serde_json::Value::Bool(_)));
1340 let bool_val = match &assertion.value {
1341 Some(serde_json::Value::Bool(b)) if *b => "true",
1342 _ => "false",
1343 };
1344 let is_null_val = matches!(&assertion.value, Some(serde_json::Value::Null));
1345 let n = assertion.value.as_ref().map(json_to_zig).unwrap_or_default();
1346 let has_n = assertion.value.as_ref().is_some_and(|v| v.is_number() || v.is_u64());
1347 let is_float_val = matches!(&assertion.value, Some(serde_json::Value::Number(n)) if !n.is_i64() && !n.is_u64());
1352 let values_list: Vec<String> = assertion
1353 .values
1354 .as_deref()
1355 .unwrap_or_default()
1356 .iter()
1357 .filter_map(|v| {
1358 if let serde_json::Value::String(s) = v {
1359 Some(format!("\"{}\"", escape_zig(s)))
1360 } else {
1361 None
1362 }
1363 })
1364 .collect();
1365
1366 let rendered = crate::template_env::render(
1367 "zig/json_assertion.jinja",
1368 minijinja::context! {
1369 assertion_type => assertion.assertion_type.as_str(),
1370 field_expr => field_expr,
1371 is_length_access => is_length_access,
1372 zig_val => zig_val,
1373 is_string_val => is_string_val,
1374 is_bool_val => is_bool_val,
1375 bool_val => bool_val,
1376 is_null_val => is_null_val,
1377 n => n,
1378 has_n => has_n,
1379 is_float_val => is_float_val,
1380 values_list => values_list,
1381 },
1382 );
1383 out.push_str(&rendered);
1384}
1385
1386fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
1389 if let Some(f) = &assertion.field {
1390 if !f.is_empty() && is_streaming_virtual_field(f) {
1391 } else if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1394 return false;
1395 }
1396 }
1397 matches!(
1398 assertion.assertion_type.as_str(),
1399 "equals"
1400 | "contains"
1401 | "contains_all"
1402 | "not_contains"
1403 | "not_empty"
1404 | "is_empty"
1405 | "starts_with"
1406 | "ends_with"
1407 | "min_length"
1408 | "max_length"
1409 | "count_min"
1410 | "count_equals"
1411 | "is_true"
1412 | "is_false"
1413 | "greater_than"
1414 | "less_than"
1415 | "greater_than_or_equal"
1416 | "less_than_or_equal"
1417 | "contains_any"
1418 )
1419}
1420
1421fn build_args_and_setup(
1426 input: &serde_json::Value,
1427 args: &[crate::config::ArgMapping],
1428 fixture_id: &str,
1429 _module_name: &str,
1430) -> (Vec<String>, String, bool) {
1431 if args.is_empty() {
1432 return (Vec::new(), String::new(), false);
1433 }
1434
1435 let mut setup_lines: Vec<String> = Vec::new();
1436 let mut parts: Vec<String> = Vec::new();
1437 let mut setup_needs_gpa = false;
1438
1439 for arg in args {
1440 if arg.arg_type == "mock_url" {
1441 let name = arg.name.clone();
1442 let id_upper = fixture_id.to_uppercase();
1443 setup_lines.push(format!(
1444 "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\"}});"
1445 ));
1446 setup_lines.push(format!("defer allocator.free({name});"));
1447 parts.push(name);
1448 setup_needs_gpa = true;
1449 continue;
1450 }
1451
1452 if arg.arg_type == "handle" {
1455 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1456 let json_str = match input.get(field) {
1457 Some(serde_json::Value::Null) | None => "null".to_string(),
1458 Some(v) => format!("\"{}\"", escape_zig(&serde_json::to_string(v).unwrap_or_default())),
1459 };
1460 parts.push(json_str);
1461 continue;
1462 }
1463
1464 if arg.name == "config" && arg.arg_type == "json_object" {
1471 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1472 let json_str = match input.get(field) {
1473 Some(serde_json::Value::Null) | None => "{}".to_string(),
1474 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1475 };
1476 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1477 continue;
1478 }
1479
1480 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1481 let val = if field.is_empty() || field == "input" {
1487 Some(input)
1488 } else {
1489 input.get(field)
1490 };
1491 match val {
1492 None | Some(serde_json::Value::Null) if arg.optional => {
1493 parts.push("null".to_string());
1496 }
1497 None | Some(serde_json::Value::Null) => {
1498 let default_val = match arg.arg_type.as_str() {
1499 "string" => "\"\"".to_string(),
1500 "int" | "integer" => "0".to_string(),
1501 "float" | "number" => "0.0".to_string(),
1502 "bool" | "boolean" => "false".to_string(),
1503 "json_object" => "\"{}\"".to_string(),
1504 _ => "null".to_string(),
1505 };
1506 parts.push(default_val);
1507 }
1508 Some(v) => {
1509 if arg.arg_type == "json_object" {
1514 let json_str = serde_json::to_string(v).unwrap_or_default();
1515 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1516 } else if arg.arg_type == "bytes" {
1517 if let serde_json::Value::String(path) = v {
1522 let var_name = format!("{}_bytes", arg.name);
1523 let epath = escape_zig(path);
1524 setup_lines.push(format!(
1525 "const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
1526 ));
1527 setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
1528 parts.push(var_name);
1529 } else {
1530 parts.push(json_to_zig(v));
1531 }
1532 } else {
1533 parts.push(json_to_zig(v));
1534 }
1535 }
1536 }
1537 }
1538
1539 (setup_lines, parts.join(", "), setup_needs_gpa)
1540}
1541
1542fn render_assertion(
1543 out: &mut String,
1544 assertion: &Assertion,
1545 result_var: &str,
1546 field_resolver: &FieldResolver,
1547 enum_fields: &HashSet<String>,
1548 result_is_option: bool,
1549) {
1550 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1553 if bare_result_is_option {
1554 match assertion.assertion_type.as_str() {
1555 "is_empty" => {
1556 let _ = writeln!(out, " try testing.expect({result_var} == null);");
1557 return;
1558 }
1559 "not_empty" | "not_error" => {
1560 let _ = writeln!(out, " try testing.expect({result_var} != null);");
1561 return;
1562 }
1563 _ => {}
1564 }
1565 }
1566 if let Some(f) = &assertion.field {
1570 if f == "embeddings" && !field_resolver.is_valid_for_result(f) {
1571 match assertion.assertion_type.as_str() {
1572 "count_min" | "count_equals" | "not_empty" | "is_empty" => {
1573 let _ = writeln!(out, " {{");
1574 let _ = writeln!(
1575 out,
1576 " var _eparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {result_var}, .{{}});"
1577 );
1578 let _ = writeln!(out, " defer _eparse.deinit();");
1579 let _ = writeln!(out, " const _embeddings_len = _eparse.value.array.items.len;");
1580 match assertion.assertion_type.as_str() {
1581 "count_min" => {
1582 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1583 let _ = writeln!(out, " try testing.expect(_embeddings_len >= {n});");
1584 }
1585 }
1586 "count_equals" => {
1587 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1588 let _ = writeln!(
1589 out,
1590 " try testing.expectEqual(@as(usize, {n}), _embeddings_len);"
1591 );
1592 }
1593 }
1594 "not_empty" => {
1595 let _ = writeln!(out, " try testing.expect(_embeddings_len > 0);");
1596 }
1597 "is_empty" => {
1598 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _embeddings_len);");
1599 }
1600 _ => {}
1601 }
1602 let _ = writeln!(out, " }}");
1603 return;
1604 }
1605 _ => {}
1606 }
1607 }
1608 }
1609
1610 if let Some(f) = &assertion.field {
1612 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1613 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
1614 return;
1615 }
1616 }
1617
1618 let _field_is_enum = assertion
1620 .field
1621 .as_deref()
1622 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1623
1624 let field_expr = match &assertion.field {
1625 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
1626 _ => result_var.to_string(),
1627 };
1628
1629 match assertion.assertion_type.as_str() {
1630 "equals" => {
1631 if let Some(expected) = &assertion.value {
1632 let zig_val = json_to_zig(expected);
1633 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
1634 }
1635 }
1636 "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 }
1644 }
1645 "contains_all" => {
1646 if let Some(values) = &assertion.values {
1647 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_contains" => {
1657 if let Some(expected) = &assertion.value {
1658 let zig_val = json_to_zig(expected);
1659 let _ = writeln!(
1660 out,
1661 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1662 );
1663 } else if let Some(values) = &assertion.values {
1664 for val in values {
1668 let zig_val = json_to_zig(val);
1669 let _ = writeln!(
1670 out,
1671 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1672 );
1673 }
1674 }
1675 }
1676 "not_empty" => {
1677 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
1678 }
1679 "is_empty" => {
1680 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
1681 }
1682 "starts_with" => {
1683 if let Some(expected) = &assertion.value {
1684 let zig_val = json_to_zig(expected);
1685 let _ = writeln!(
1686 out,
1687 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
1688 );
1689 }
1690 }
1691 "ends_with" => {
1692 if let Some(expected) = &assertion.value {
1693 let zig_val = json_to_zig(expected);
1694 let _ = writeln!(
1695 out,
1696 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
1697 );
1698 }
1699 }
1700 "min_length" => {
1701 if let Some(val) = &assertion.value {
1702 if let Some(n) = val.as_u64() {
1703 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1704 }
1705 }
1706 }
1707 "max_length" => {
1708 if let Some(val) = &assertion.value {
1709 if let Some(n) = val.as_u64() {
1710 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
1711 }
1712 }
1713 }
1714 "count_min" => {
1715 if let Some(val) = &assertion.value {
1716 if let Some(n) = val.as_u64() {
1717 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1718 }
1719 }
1720 }
1721 "count_equals" => {
1722 if let Some(val) = &assertion.value {
1723 if let Some(n) = val.as_u64() {
1724 let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
1728 if has_field {
1729 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
1730 } else {
1731 let _ = writeln!(out, " {{");
1732 let _ = writeln!(
1733 out,
1734 " var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
1735 );
1736 let _ = writeln!(out, " defer _cparse.deinit();");
1737 let _ = writeln!(
1738 out,
1739 " try testing.expectEqual({n}, _cparse.value.array.items.len);"
1740 );
1741 let _ = writeln!(out, " }}");
1742 }
1743 }
1744 }
1745 }
1746 "is_true" => {
1747 let _ = writeln!(out, " try testing.expect({field_expr});");
1748 }
1749 "is_false" => {
1750 let _ = writeln!(out, " try testing.expect(!{field_expr});");
1751 }
1752 "not_error" => {
1753 }
1755 "error" => {
1756 }
1758 "greater_than" => {
1759 if let Some(val) = &assertion.value {
1760 let zig_val = json_to_zig(val);
1761 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
1762 }
1763 }
1764 "less_than" => {
1765 if let Some(val) = &assertion.value {
1766 let zig_val = json_to_zig(val);
1767 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
1768 }
1769 }
1770 "greater_than_or_equal" => {
1771 if let Some(val) = &assertion.value {
1772 let zig_val = json_to_zig(val);
1773 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
1774 }
1775 }
1776 "less_than_or_equal" => {
1777 if let Some(val) = &assertion.value {
1778 let zig_val = json_to_zig(val);
1779 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
1780 }
1781 }
1782 "contains_any" => {
1783 if let Some(values) = &assertion.values {
1785 let string_values: Vec<String> = values
1786 .iter()
1787 .filter_map(|v| {
1788 if let serde_json::Value::String(s) = v {
1789 Some(format!(
1790 "std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
1791 escape_zig(s)
1792 ))
1793 } else {
1794 None
1795 }
1796 })
1797 .collect();
1798 if !string_values.is_empty() {
1799 let condition = string_values.join(" or\n ");
1800 let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
1801 }
1802 }
1803 }
1804 "matches_regex" => {
1805 let _ = writeln!(out, " // regex match not yet implemented for Zig");
1806 }
1807 "method_result" => {
1808 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
1809 }
1810 other => {
1811 panic!("Zig e2e generator: unsupported assertion type: {other}");
1812 }
1813 }
1814}
1815
1816fn json_to_zig(value: &serde_json::Value) -> String {
1818 match value {
1819 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
1820 serde_json::Value::Bool(b) => b.to_string(),
1821 serde_json::Value::Number(n) => n.to_string(),
1822 serde_json::Value::Null => "null".to_string(),
1823 serde_json::Value::Array(arr) => {
1824 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
1825 format!("&.{{{}}}", items.join(", "))
1826 }
1827 serde_json::Value::Object(_) => {
1828 let json_str = serde_json::to_string(value).unwrap_or_default();
1829 format!("\"{}\"", escape_zig(&json_str))
1830 }
1831 }
1832}