1use crate::config::E2eConfig;
7use crate::escape::{escape_zig, sanitize_filename};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, Fixture, FixtureGroup};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::ResolvedCrateConfig;
12use alef_core::hash::{self, CommentStyle};
13use alef_core::template_versions::toolchain;
14use anyhow::Result;
15use heck::{ToShoutySnakeCase, ToSnakeCase};
16use std::collections::HashSet;
17use std::fmt::Write as FmtWrite;
18use std::path::PathBuf;
19
20use super::E2eCodegen;
21use super::client;
22use super::streaming_assertions::{StreamingFieldResolver, is_streaming_virtual_field};
23
24pub struct ZigE2eCodegen;
26
27impl E2eCodegen for ZigE2eCodegen {
28 fn generate(
29 &self,
30 groups: &[FixtureGroup],
31 e2e_config: &E2eConfig,
32 config: &ResolvedCrateConfig,
33 _type_defs: &[alef_core::ir::TypeDef],
34 _enums: &[alef_core::ir::EnumDef],
35 ) -> Result<Vec<GeneratedFile>> {
36 let lang = self.language_name();
37 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
38
39 let mut files = Vec::new();
40
41 let call = &e2e_config.call;
43 let overrides = call.overrides.get(lang);
44 let _module_path = overrides
45 .and_then(|o| o.module.as_ref())
46 .cloned()
47 .unwrap_or_else(|| call.module.clone());
48 let function_name = overrides
49 .and_then(|o| o.function.as_ref())
50 .cloned()
51 .unwrap_or_else(|| call.function.clone());
52 let result_var = &call.result_var;
53
54 let zig_pkg = e2e_config.resolve_package("zig");
56 let pkg_path = zig_pkg
57 .as_ref()
58 .and_then(|p| p.path.as_ref())
59 .cloned()
60 .unwrap_or_else(|| "../../packages/zig".to_string());
61 let pkg_name = zig_pkg
62 .as_ref()
63 .and_then(|p| p.name.as_ref())
64 .cloned()
65 .unwrap_or_else(|| config.name.to_snake_case());
66
67 files.push(GeneratedFile {
69 path: output_base.join("build.zig.zon"),
70 content: render_build_zig_zon(&pkg_name, &pkg_path, e2e_config.dep_mode),
71 generated_header: false,
72 });
73
74 let module_name = config.zig_module_name();
76 let ffi_prefix = config.ffi_prefix();
77
78 let has_file_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
88 let cc = e2e_config.resolve_call_for_fixture(
89 f.call.as_deref(),
90 &f.id,
91 &f.resolved_category(),
92 &f.tags,
93 &f.input,
94 );
95 cc.args
96 .iter()
97 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
98 });
99
100 let mut test_filenames: Vec<String> = Vec::new();
109 for group in groups {
110 let active: Vec<&Fixture> = group
111 .fixtures
112 .iter()
113 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
114 .filter(|f| {
115 let cc = e2e_config.resolve_call_for_fixture(
116 f.call.as_deref(),
117 &f.id,
118 &f.resolved_category(),
119 &f.tags,
120 &f.input,
121 );
122 cc.streaming != Some(true)
123 })
124 .collect();
125
126 if active.is_empty() {
127 continue;
128 }
129
130 let filename = format!("{}_test.zig", sanitize_filename(&group.category));
131 test_filenames.push(filename.clone());
132 let content = render_test_file(
133 &group.category,
134 &active,
135 e2e_config,
136 &function_name,
137 result_var,
138 &e2e_config.call.args,
139 &module_name,
140 &ffi_prefix,
141 );
142 files.push(GeneratedFile {
143 path: output_base.join("src").join(filename),
144 content,
145 generated_header: true,
146 });
147 }
148
149 files.insert(
151 files
152 .iter()
153 .position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
154 .unwrap_or(1),
155 GeneratedFile {
156 path: output_base.join("build.zig"),
157 content: render_build_zig(
158 &test_filenames,
159 &pkg_name,
160 &module_name,
161 &config.ffi_lib_name(),
162 &config.ffi_crate_path(),
163 has_file_fixtures,
164 &e2e_config.test_documents_relative_from(0),
165 ),
166 generated_header: false,
167 },
168 );
169
170 Ok(files)
171 }
172
173 fn language_name(&self) -> &'static str {
174 "zig"
175 }
176}
177
178fn render_build_zig_zon(pkg_name: &str, pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
183 let dep_block = match dep_mode {
184 crate::config::DependencyMode::Registry => {
185 format!(
187 r#".{{
188 .url = "https://registry.example.com/{pkg_name}/v0.1.0.tar.gz",
189 .hash = "0000000000000000000000000000000000000000000000000000000000000000",
190 }}"#
191 )
192 }
193 crate::config::DependencyMode::Local => {
194 format!(r#".{{ .path = "{pkg_path}" }}"#)
195 }
196 };
197
198 let min_zig = toolchain::MIN_ZIG_VERSION;
199 let name_bytes: &[u8] = b"e2e_zig";
201 let mut crc: u32 = 0xffff_ffff;
202 for byte in name_bytes {
203 crc ^= *byte as u32;
204 for _ in 0..8 {
205 let mask = (crc & 1).wrapping_neg();
206 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
207 }
208 }
209 let name_crc: u32 = !crc;
210 let mut id: u32 = 0x811c_9dc5;
211 for byte in name_bytes {
212 id ^= *byte as u32;
213 id = id.wrapping_mul(0x0100_0193);
214 }
215 if id == 0 || id == 0xffff_ffff {
216 id = 0x1;
217 }
218 let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
219 format!(
220 r#".{{
221 .name = .e2e_zig,
222 .version = "0.1.0",
223 .fingerprint = 0x{fingerprint:016x},
224 .minimum_zig_version = "{min_zig}",
225 .dependencies = .{{
226 .{pkg_name} = {dep_block},
227 }},
228 .paths = .{{
229 "build.zig",
230 "build.zig.zon",
231 "src",
232 }},
233}}
234"#
235 )
236}
237
238fn render_build_zig(
239 test_filenames: &[String],
240 pkg_name: &str,
241 module_name: &str,
242 ffi_lib_name: &str,
243 ffi_crate_path: &str,
244 has_file_fixtures: bool,
245 test_documents_path: &str,
246) -> String {
247 if test_filenames.is_empty() {
248 return r#"const std = @import("std");
249
250pub fn build(b: *std.Build) void {
251 const target = b.standardTargetOptions(.{});
252 const optimize = b.standardOptimizeOption(.{});
253
254 const test_step = b.step("test", "Run tests");
255}
256"#
257 .to_string();
258 }
259
260 let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
270 content.push_str(" const target = b.standardTargetOptions(.{});\n");
271 content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
272 content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n");
273 let _ = writeln!(
274 content,
275 " const ffi_path = b.option([]const u8, \"ffi_path\", \"Path to directory containing lib{ffi_lib_name}\") orelse \"../../target/release\";"
276 );
277 let _ = writeln!(
278 content,
279 " const ffi_include = b.option([]const u8, \"ffi_include_path\", \"Path to directory containing FFI header\") orelse \"{ffi_crate_path}/include\";"
280 );
281 let _ = writeln!(content);
282 let _ = writeln!(
283 content,
284 " const {module_name}_module = b.addModule(\"{module_name}\", .{{"
285 );
286 let _ = writeln!(
287 content,
288 " .root_source_file = b.path(\"../../packages/zig/src/{pkg_name}.zig\"),"
289 );
290 content.push_str(" .target = target,\n");
291 content.push_str(" .optimize = optimize,\n");
292 content.push_str(" .link_libc = true,\n");
296 content.push_str(" });\n");
297 let _ = writeln!(
298 content,
299 " {module_name}_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});"
300 );
301 let _ = writeln!(
302 content,
303 " {module_name}_module.addIncludePath(.{{ .cwd_relative = ffi_include }});"
304 );
305 let _ = writeln!(
306 content,
307 " {module_name}_module.linkSystemLibrary(\"{ffi_lib_name}\", .{{}});"
308 );
309 let _ = writeln!(content);
310
311 for filename in test_filenames {
312 let test_name = filename.trim_end_matches("_test.zig");
314 content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
315 content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
316 content.push_str(" .target = target,\n");
317 content.push_str(" .optimize = optimize,\n");
318 content.push_str(" .link_libc = true,\n");
322 content.push_str(" });\n");
323 content.push_str(&format!(
324 " {test_name}_module.addImport(\"{module_name}\", {module_name}_module);\n"
325 ));
326 content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
342 content.push_str(&format!(" .name = \"{test_name}_test\",\n"));
343 content.push_str(&format!(" .root_module = {test_name}_module,\n"));
344 content.push_str(" .use_llvm = true,\n");
345 content.push_str(" });\n");
346 content.push_str(&format!(
363 " const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
364 ));
365 if has_file_fixtures {
366 content.push_str(&format!(
367 " {test_name}_run.setCwd(b.path(\"{test_documents_path}\"));\n"
368 ));
369 }
370 content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
371 }
372
373 content.push_str("}\n");
374 content
375}
376
377struct ZigTestClientRenderer;
385
386impl client::TestClientRenderer for ZigTestClientRenderer {
387 fn language_name(&self) -> &'static str {
388 "zig"
389 }
390
391 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
392 if let Some(reason) = skip_reason {
393 let _ = writeln!(out, "test \"{fn_name}\" {{");
394 let _ = writeln!(out, " // {description}");
395 let _ = writeln!(out, " // skipped: {reason}");
396 let _ = writeln!(out, " return error.SkipZigTest;");
397 } else {
398 let _ = writeln!(out, "test \"{fn_name}\" {{");
399 let _ = writeln!(out, " // {description}");
400 }
401 }
402
403 fn render_test_close(&self, out: &mut String) {
404 let _ = writeln!(out, "}}");
405 }
406
407 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
408 let method = ctx.method.to_uppercase();
409 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
410
411 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
412 let _ = writeln!(out, " defer _ = gpa.deinit();");
413 let _ = writeln!(out, " const allocator = gpa.allocator();");
414
415 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
416 let _ = writeln!(
417 out,
418 " 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\"}});"
419 );
420
421 if !ctx.headers.is_empty() {
423 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
424 header_pairs.sort_by_key(|(k, _)| k.as_str());
425 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
426 for (k, v) in &header_pairs {
427 let ek = escape_zig(k);
428 let ev = escape_zig(v);
429 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
430 }
431 let _ = writeln!(out, " }};");
432 }
433
434 if let Some(body) = ctx.body {
436 let json_str = serde_json::to_string(body).unwrap_or_default();
437 let escaped = escape_zig(&json_str);
438 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
439 }
440
441 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
442 let has_body = ctx.body.is_some();
443
444 let _ = writeln!(
445 out,
446 " var http_client = std.http.Client{{ .allocator = allocator }};"
447 );
448 let _ = writeln!(out, " defer http_client.deinit();");
449 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
450 let _ = writeln!(out, " defer response_body.deinit();");
451
452 let method_zig = match method.as_str() {
453 "GET" => ".GET",
454 "POST" => ".POST",
455 "PUT" => ".PUT",
456 "DELETE" => ".DELETE",
457 "PATCH" => ".PATCH",
458 "HEAD" => ".HEAD",
459 "OPTIONS" => ".OPTIONS",
460 _ => ".GET",
461 };
462
463 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
464 let _ = writeln!(
465 out,
466 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
467 rv = ctx.response_var,
468 );
469 }
470
471 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
472 let _ = writeln!(
473 out,
474 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
475 );
476 }
477
478 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
479 let ename = escape_zig(&name.to_lowercase());
480 match expected {
481 "<<present>>" => {
482 let _ = writeln!(
483 out,
484 " // assert header '{ename}' is present (header inspection not yet implemented)"
485 );
486 }
487 "<<absent>>" => {
488 let _ = writeln!(
489 out,
490 " // assert header '{ename}' is absent (header inspection not yet implemented)"
491 );
492 }
493 "<<uuid>>" => {
494 let _ = writeln!(
495 out,
496 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
497 );
498 }
499 exact => {
500 let evalue = escape_zig(exact);
501 let _ = writeln!(
502 out,
503 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
504 );
505 }
506 }
507 }
508
509 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
510 let json_str = serde_json::to_string(expected).unwrap_or_default();
511 let escaped = escape_zig(&json_str);
512 let _ = writeln!(
513 out,
514 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
515 );
516 }
517
518 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
519 if let Some(obj) = expected.as_object() {
520 for (key, val) in obj {
521 let ekey = escape_zig(key);
522 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
523 let _ = writeln!(
524 out,
525 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
526 );
527 }
528 }
529 }
530
531 fn render_assert_validation_errors(
532 &self,
533 out: &mut String,
534 _response_var: &str,
535 errors: &[crate::fixture::ValidationErrorExpectation],
536 ) {
537 for ve in errors {
538 let loc = ve.loc.join(".");
539 let escaped_loc = escape_zig(&loc);
540 let escaped_msg = escape_zig(&ve.msg);
541 let _ = writeln!(
542 out,
543 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
544 );
545 }
546 }
547}
548
549fn render_http_test_case(out: &mut String, fixture: &Fixture) {
554 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
555}
556
557#[allow(clippy::too_many_arguments)]
562fn render_test_file(
563 category: &str,
564 fixtures: &[&Fixture],
565 e2e_config: &E2eConfig,
566 function_name: &str,
567 result_var: &str,
568 args: &[crate::config::ArgMapping],
569 module_name: &str,
570 ffi_prefix: &str,
571) -> String {
572 let mut out = String::new();
573 out.push_str(&hash::header(CommentStyle::DoubleSlash));
574 let _ = writeln!(out, "const std = @import(\"std\");");
575 let _ = writeln!(out, "const testing = std.testing;");
576 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
577 let _ = writeln!(out);
578
579 let _ = writeln!(out, "// E2e tests for category: {category}");
580 let _ = writeln!(out);
581
582 for fixture in fixtures {
583 if fixture.http.is_some() {
584 render_http_test_case(&mut out, fixture);
585 } else {
586 render_test_fn(
587 &mut out,
588 fixture,
589 e2e_config,
590 function_name,
591 result_var,
592 args,
593 module_name,
594 ffi_prefix,
595 );
596 }
597 let _ = writeln!(out);
598 }
599
600 out
601}
602
603#[allow(clippy::too_many_arguments)]
604fn render_test_fn(
605 out: &mut String,
606 fixture: &Fixture,
607 e2e_config: &E2eConfig,
608 _function_name: &str,
609 _result_var: &str,
610 _args: &[crate::config::ArgMapping],
611 module_name: &str,
612 ffi_prefix: &str,
613) {
614 let call_config = e2e_config.resolve_call_for_fixture(
616 fixture.call.as_deref(),
617 &fixture.id,
618 &fixture.resolved_category(),
619 &fixture.tags,
620 &fixture.input,
621 );
622 let call_field_resolver = FieldResolver::new(
623 e2e_config.effective_fields(call_config),
624 e2e_config.effective_fields_optional(call_config),
625 e2e_config.effective_result_fields(call_config),
626 e2e_config.effective_fields_array(call_config),
627 e2e_config.effective_fields_method_calls(call_config),
628 );
629 let field_resolver = &call_field_resolver;
630 let enum_fields = e2e_config.effective_fields_enum(call_config);
631 let lang = "zig";
632 let call_overrides = call_config.overrides.get(lang);
633 let function_name = call_overrides
634 .and_then(|o| o.function.as_ref())
635 .cloned()
636 .unwrap_or_else(|| call_config.function.clone());
637 let result_var = &call_config.result_var;
638 let args = &call_config.args;
639 let client_factory = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
644 e2e_config
645 .call
646 .overrides
647 .get(lang)
648 .and_then(|o| o.client_factory.as_deref())
649 });
650
651 let call_result_is_bytes = call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
668 let result_is_json_struct =
669 !call_result_is_bytes && (call_overrides.is_some_and(|o| o.result_is_json_struct) || client_factory.is_some());
670
671 let result_is_option = call_overrides.is_some_and(|o| o.result_is_option) || call_config.result_is_option;
676
677 let call_returns_error_union = call_overrides.and_then(|o| o.returns_result) != Some(false);
698
699 let test_name = fixture.id.to_snake_case();
700 let description = &fixture.description;
701 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
702
703 let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
704 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
710 let args_str = if extra_args.is_empty() {
711 args_str
712 } else if args_str.is_empty() {
713 extra_args.join(", ")
714 } else {
715 format!("{args_str}, {}", extra_args.join(", "))
716 };
717
718 let any_happy_emits_code = fixture
721 .assertions
722 .iter()
723 .any(|a| assertion_emits_code(a, field_resolver));
724 let any_non_error_emits_code = fixture
725 .assertions
726 .iter()
727 .filter(|a| a.assertion_type != "error")
728 .any(|a| assertion_emits_code(a, field_resolver));
729
730 let has_streaming_virtual_assertions = fixture.assertions.iter().any(|a| {
732 a.field
733 .as_ref()
734 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
735 });
736 let is_stream_fn = function_name.contains("stream");
737 let uses_streaming_virtual_path =
738 result_is_json_struct && has_streaming_virtual_assertions && is_stream_fn && client_factory.is_some();
739 let streaming_path_has_non_streaming = uses_streaming_virtual_path
741 && fixture.assertions.iter().any(|a| {
742 !a.field
743 .as_ref()
744 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
745 && !matches!(a.assertion_type.as_str(), "not_error" | "error")
746 && a.field
747 .as_ref()
748 .is_some_and(|f| !f.is_empty() && field_resolver.is_valid_for_result(f))
749 });
750
751 let _ = writeln!(out, "test \"{test_name}\" {{");
752 let _ = writeln!(out, " // {description}");
753
754 if let Some(visitor_spec) = &fixture.visitor {
759 let html = fixture.input.get("html").and_then(|v| v.as_str()).unwrap_or_default();
760 let options_value = fixture.input.get("options").cloned();
761 emit_visitor_test_body(
762 out,
763 &fixture.id,
764 html,
765 options_value.as_ref(),
766 visitor_spec,
767 module_name,
768 &fixture.assertions,
769 expects_error,
770 field_resolver,
771 );
772 let _ = writeln!(out, "}}");
773 let _ = writeln!(out);
774 return;
775 }
776
777 let needs_gpa = setup_needs_gpa
785 || streaming_path_has_non_streaming
786 || (!uses_streaming_virtual_path && result_is_json_struct && !expects_error && any_happy_emits_code)
787 || (!uses_streaming_virtual_path && result_is_json_struct && expects_error && any_non_error_emits_code);
788 if needs_gpa {
789 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
790 let _ = writeln!(out, " defer _ = gpa.deinit();");
791 let _ = writeln!(out, " const allocator = gpa.allocator();");
792 let _ = writeln!(out);
793 }
794
795 for line in &setup_lines {
796 let _ = writeln!(out, " {line}");
797 }
798
799 let call_prefix = if let Some(factory) = client_factory {
804 let fixture_id = &fixture.id;
805 let _ = writeln!(
806 out,
807 " 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);"
808 );
809 let _ = writeln!(out, " defer std.heap.c_allocator.free(_mock_url);");
810 let _ = writeln!(
811 out,
812 " var _client = try {module_name}.{factory}(\"test-key\", _mock_url, null, null, null);"
813 );
814 let _ = writeln!(out, " defer _client.free();");
815 "_client".to_string()
816 } else {
817 module_name.to_string()
818 };
819
820 if expects_error {
821 if result_is_json_struct {
825 let _ = writeln!(
826 out,
827 " const _result_json = {call_prefix}.{function_name}({args_str}) catch {{"
828 );
829 } else {
830 let _ = writeln!(
831 out,
832 " const result = {call_prefix}.{function_name}({args_str}) catch {{"
833 );
834 }
835 let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
836 let _ = writeln!(out, " return;");
837 let _ = writeln!(out, " }};");
838 let any_emits_code = fixture
842 .assertions
843 .iter()
844 .filter(|a| a.assertion_type != "error")
845 .any(|a| assertion_emits_code(a, field_resolver));
846 if result_is_json_struct && any_emits_code {
847 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
848 let _ = writeln!(
849 out,
850 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
851 );
852 let _ = writeln!(out, " defer _parsed.deinit();");
853 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
854 let _ = writeln!(out, " // Perform success assertions if any");
855 for assertion in &fixture.assertions {
856 if assertion.assertion_type != "error" {
857 render_json_assertion(out, assertion, result_var, field_resolver, false);
858 }
859 }
860 } else if result_is_json_struct {
861 let _ = writeln!(out, " _ = _result_json;");
862 } else if any_emits_code {
863 let _ = writeln!(out, " // Perform success assertions if any");
864 for assertion in &fixture.assertions {
865 if assertion.assertion_type != "error" {
866 render_assertion(
867 out,
868 assertion,
869 result_var,
870 field_resolver,
871 enum_fields,
872 result_is_option,
873 );
874 }
875 }
876 } else {
877 let _ = writeln!(out, " _ = result;");
878 }
879 } else if fixture.assertions.is_empty() {
880 if result_is_json_struct {
882 let _ = writeln!(
883 out,
884 " const _result_json = try {call_prefix}.{function_name}({args_str});"
885 );
886 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
887 } else if call_returns_error_union {
888 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
889 } else {
890 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
891 }
892 } else {
893 let any_emits_code = fixture
897 .assertions
898 .iter()
899 .any(|a| assertion_emits_code(a, field_resolver));
900 if call_result_is_bytes && client_factory.is_some() {
901 let _ = writeln!(
904 out,
905 " const _result_json = try {call_prefix}.{function_name}({args_str});"
906 );
907 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
908 let has_bytes_assertions = fixture
909 .assertions
910 .iter()
911 .any(|a| matches!(a.assertion_type.as_str(), "not_empty" | "is_empty"));
912 if has_bytes_assertions {
913 for assertion in &fixture.assertions {
914 match assertion.assertion_type.as_str() {
915 "not_empty" => {
916 let _ = writeln!(out, " try testing.expect(_result_json.len > 0);");
917 }
918 "is_empty" => {
919 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _result_json.len);");
920 }
921 "not_error" | "error" => {}
922 _ => {
923 let atype = &assertion.assertion_type;
924 let _ = writeln!(
925 out,
926 " // bytes result: assertion '{atype}' not implemented for zig bytes"
927 );
928 }
929 }
930 }
931 }
932 } else if result_is_json_struct {
933 if uses_streaming_virtual_path {
937 let request_from_json = format!("{ffi_prefix}_chat_completion_request_from_json");
938 let request_free = format!("{ffi_prefix}_chat_completion_request_free");
939 let stream_start = format!("{ffi_prefix}_default_client_chat_stream_start");
940 let stream_free = format!("{ffi_prefix}_default_client_chat_stream_free");
941 let client_c_type = format!("{}DefaultClient", ffi_prefix.to_shouty_snake_case());
942
943 let _ = writeln!(
946 out,
947 " const _req_z = try std.heap.c_allocator.dupeZ(u8, {args_str});"
948 );
949 let _ = writeln!(out, " defer std.heap.c_allocator.free(_req_z);");
950 let _ = writeln!(
951 out,
952 " const _req_handle = {module_name}.c.{request_from_json}(_req_z.ptr);"
953 );
954 let _ = writeln!(out, " defer {module_name}.c.{request_free}(_req_handle);");
955 let _ = writeln!(
956 out,
957 " const _stream_handle = {module_name}.c.{stream_start}(@as(*{module_name}.c.{client_c_type}, @ptrCast(_client._handle)), _req_handle);"
958 );
959 let _ = writeln!(out, " if (_stream_handle == null) return error.StreamStartFailed;");
960 let _ = writeln!(out, " defer {module_name}.c.{stream_free}(_stream_handle);");
961 let snip =
963 StreamingFieldResolver::collect_snippet_zig("_stream_handle", "chunks", module_name, ffi_prefix);
964 out.push_str(" ");
965 out.push_str(&snip);
966 out.push('\n');
967 if streaming_path_has_non_streaming {
970 let _ = writeln!(
971 out,
972 " const _result_json = if (chunks.items.len > 0) chunks.items[chunks.items.len - 1] else &[_]u8{{}};"
973 );
974 let _ = writeln!(
975 out,
976 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
977 );
978 let _ = writeln!(out, " defer _parsed.deinit();");
979 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
980 }
981 for assertion in &fixture.assertions {
982 render_json_assertion(out, assertion, result_var, field_resolver, true);
983 }
984 } else {
985 let _ = writeln!(
987 out,
988 " const _result_json = try {call_prefix}.{function_name}({args_str});"
989 );
990 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
991 if any_emits_code {
992 let _ = writeln!(
993 out,
994 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
995 );
996 let _ = writeln!(out, " defer _parsed.deinit();");
997 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
998 for assertion in &fixture.assertions {
999 render_json_assertion(out, assertion, result_var, field_resolver, false);
1000 }
1001 }
1002 }
1003 } else if any_emits_code {
1004 let try_kw = if call_returns_error_union { "try " } else { "" };
1005 let _ = writeln!(
1006 out,
1007 " const {result_var} = {try_kw}{call_prefix}.{function_name}({args_str});"
1008 );
1009 for assertion in &fixture.assertions {
1010 render_assertion(
1011 out,
1012 assertion,
1013 result_var,
1014 field_resolver,
1015 enum_fields,
1016 result_is_option,
1017 );
1018 }
1019 } else if call_returns_error_union {
1020 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
1021 } else {
1022 let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
1023 }
1024 }
1025
1026 let _ = writeln!(out, "}}");
1027}
1028
1029#[allow(clippy::too_many_arguments)]
1035fn emit_visitor_test_body(
1036 out: &mut String,
1037 fixture_id: &str,
1038 html: &str,
1039 options_value: Option<&serde_json::Value>,
1040 visitor_spec: &crate::fixture::VisitorSpec,
1041 module_name: &str,
1042 assertions: &[Assertion],
1043 expects_error: bool,
1044 field_resolver: &FieldResolver,
1045) {
1046 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
1048 let _ = writeln!(out, " defer _ = gpa.deinit();");
1049 let _ = writeln!(out, " const allocator = gpa.allocator();");
1050 let _ = writeln!(out);
1051
1052 let visitor_block = super::zig_visitors::build_zig_visitor(fixture_id, module_name, visitor_spec);
1054 out.push_str(&visitor_block);
1055
1056 let _ = writeln!(
1059 out,
1060 " const _visitor = {module_name}.c.htm_visitor_create(&_callbacks);"
1061 );
1062 let _ = writeln!(out, " defer {module_name}.c.htm_visitor_free(_visitor);");
1063
1064 let options_json = match options_value {
1068 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1069 None => "{}".to_string(),
1070 };
1071 let escaped_options = escape_zig(&options_json);
1072 let _ = writeln!(
1073 out,
1074 " const _options_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_options}\");"
1075 );
1076 let _ = writeln!(out, " defer std.heap.c_allocator.free(_options_z);");
1077 let _ = writeln!(
1078 out,
1079 " const _options = {module_name}.c.htm_conversion_options_from_json(_options_z.ptr);"
1080 );
1081 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_options_free(_options);");
1082 let _ = writeln!(
1083 out,
1084 " {module_name}.c.htm_options_set_visitor_handle(_options, _visitor);"
1085 );
1086
1087 let escaped_html = escape_zig(html);
1089 let _ = writeln!(
1090 out,
1091 " const _html_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_html}\");"
1092 );
1093 let _ = writeln!(out, " defer std.heap.c_allocator.free(_html_z);");
1094 let _ = writeln!(
1095 out,
1096 " const _result = {module_name}.c.htm_convert(_html_z.ptr, _options);"
1097 );
1098
1099 if expects_error {
1100 let _ = writeln!(
1102 out,
1103 " try testing.expect(_result == null or {module_name}.c.htm_last_error_code() != 0);"
1104 );
1105 let _ = writeln!(
1106 out,
1107 " if (_result) |r| {module_name}.c.htm_conversion_result_free(r);"
1108 );
1109 return;
1110 }
1111
1112 let _ = writeln!(out, " try testing.expect(_result != null);");
1113 let _ = writeln!(out, " defer {module_name}.c.htm_conversion_result_free(_result.?);");
1114 let _ = writeln!(
1115 out,
1116 " const _json_ptr = {module_name}.c.htm_conversion_result_to_json(_result.?);"
1117 );
1118 let _ = writeln!(out, " defer {module_name}.c.htm_free_string(_json_ptr);");
1119 let _ = writeln!(out, " const _result_json = std.mem.sliceTo(_json_ptr, 0);");
1120 let _ = writeln!(
1121 out,
1122 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
1123 );
1124 let _ = writeln!(out, " defer _parsed.deinit();");
1125 let _ = writeln!(out, " const result = &_parsed.value;");
1126
1127 for assertion in assertions {
1128 if assertion.assertion_type != "error" {
1129 render_json_assertion(out, assertion, "result", field_resolver, false);
1130 }
1131 }
1132}
1133
1134const FORMAT_METADATA_VARIANTS: &[&str] = &[
1152 "pdf",
1153 "docx",
1154 "excel",
1155 "email",
1156 "pptx",
1157 "archive",
1158 "image",
1159 "xml",
1160 "text",
1161 "html",
1162 "ocr",
1163 "csv",
1164 "bibtex",
1165 "citation",
1166 "fiction_book",
1167 "dbf",
1168 "jats",
1169 "epub",
1170 "pst",
1171 "code",
1172];
1173
1174fn json_path_expr(result_var: &str, field_path: &str) -> String {
1175 let segments: Vec<&str> = field_path.split('.').collect();
1176 let mut expr = result_var.to_string();
1177 let mut prev_seg: Option<&str> = None;
1178 for seg in &segments {
1179 if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
1184 prev_seg = Some(seg);
1185 continue;
1186 }
1187 if let Some(key) = seg.strip_suffix("[]") {
1191 expr = format!("{expr}.object.get(\"{key}\").?.array.items[0]");
1192 } else if let Some(bracket_pos) = seg.find('[') {
1193 if let Some(end_pos) = seg.find(']') {
1194 if end_pos > bracket_pos + 1 && end_pos == seg.len() - 1 {
1195 let key = &seg[..bracket_pos];
1196 let idx = &seg[bracket_pos + 1..end_pos];
1197 if idx.chars().all(|c| c.is_ascii_digit()) {
1198 expr = format!("{expr}.object.get(\"{key}\").?.array.items[{idx}]");
1199 prev_seg = Some(seg);
1200 continue;
1201 }
1202 expr = format!("{expr}.object.get(\"{key}\").?.object.get(\"{idx}\").?");
1208 prev_seg = Some(seg);
1209 continue;
1210 }
1211 }
1212 expr = format!("{expr}.object.get(\"{seg}\").?");
1213 } else {
1214 expr = format!("{expr}.object.get(\"{seg}\").?");
1215 }
1216 prev_seg = Some(seg);
1217 }
1218 expr
1219}
1220
1221fn render_json_assertion(
1226 out: &mut String,
1227 assertion: &Assertion,
1228 result_var: &str,
1229 field_resolver: &FieldResolver,
1230 uses_streaming: bool,
1231) {
1232 if let Some(f) = &assertion.field {
1239 if uses_streaming && !f.is_empty() && is_streaming_virtual_field(f) {
1240 if let Some(expr) = StreamingFieldResolver::accessor(f, "zig", "chunks") {
1241 match assertion.assertion_type.as_str() {
1242 "count_min" => {
1243 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1244 let _ = writeln!(out, " try testing.expect({expr}.len >= {n});");
1245 }
1246 }
1247 "count_equals" => {
1248 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1249 let _ = writeln!(out, " try testing.expectEqual(@as(usize, {n}), {expr}.len);");
1250 }
1251 }
1252 "equals" => {
1253 if let Some(serde_json::Value::String(s)) = &assertion.value {
1254 let escaped = escape_zig(s);
1255 let _ = writeln!(out, " try testing.expectEqualStrings(\"{escaped}\", {expr});");
1256 } else if let Some(v) = &assertion.value {
1257 let zig_val = json_to_zig(v);
1258 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {expr});");
1259 }
1260 }
1261 "not_empty" => {
1262 let _ = writeln!(out, " try testing.expect({expr}.len > 0);");
1263 }
1264 "is_true" => {
1265 let _ = writeln!(out, " try testing.expect({expr});");
1266 }
1267 "is_false" => {
1268 let _ = writeln!(out, " try testing.expect(!{expr});");
1269 }
1270 _ => {
1271 let atype = &assertion.assertion_type;
1272 let _ = writeln!(
1273 out,
1274 " // streaming virtual field '{f}' assertion '{atype}' not implemented for zig"
1275 );
1276 }
1277 }
1278 }
1279 return;
1280 }
1281 }
1282
1283 if let Some(f) = &assertion.field {
1290 if f == "embeddings" && !field_resolver.has_explicit_field("embeddings") {
1291 match assertion.assertion_type.as_str() {
1292 "count_min" => {
1293 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1294 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len >= {n});");
1295 }
1296 return;
1297 }
1298 "count_equals" => {
1299 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1300 let _ = writeln!(
1301 out,
1302 " try testing.expectEqual(@as(usize, {n}), {result_var}.array.items.len);"
1303 );
1304 }
1305 return;
1306 }
1307 "not_empty" => {
1308 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len > 0);");
1309 return;
1310 }
1311 "is_empty" => {
1312 let _ = writeln!(
1313 out,
1314 " try testing.expectEqual(@as(usize, 0), {result_var}.array.items.len);"
1315 );
1316 return;
1317 }
1318 _ => {}
1319 }
1320 }
1321 }
1322
1323 if let Some(f) = &assertion.field {
1325 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1326 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1327 return;
1328 }
1329 }
1330 if matches!(assertion.assertion_type.as_str(), "not_error" | "error") {
1332 return;
1333 }
1334
1335 let raw_field_path = assertion.field.as_deref().unwrap_or("").trim();
1336 let field_path = if raw_field_path.is_empty() {
1337 raw_field_path.to_string()
1338 } else {
1339 field_resolver.resolve(raw_field_path).to_string()
1340 };
1341 let field_path = field_path.trim();
1342
1343 let (field_path_for_expr, is_length_access) = if let Some(parent) = field_path.strip_suffix(".length") {
1345 (parent, true)
1346 } else {
1347 (field_path, false)
1348 };
1349
1350 let field_expr = if field_path_for_expr.is_empty() {
1351 result_var.to_string()
1352 } else {
1353 json_path_expr(result_var, field_path_for_expr)
1354 };
1355
1356 if field_path_for_expr == "metadata.format"
1362 && matches!(
1363 assertion.assertion_type.as_str(),
1364 "equals" | "contains" | "not_empty" | "is_empty" | "starts_with" | "ends_with"
1365 )
1366 {
1367 let base = json_path_expr(result_var, field_path_for_expr);
1368 let _ = writeln!(out, " {{");
1369 let _ = writeln!(out, " const _fmt_obj = {base}.object;");
1370 let _ = writeln!(out, " const _fmt_type = _fmt_obj.get(\"format_type\").?.string;");
1371 let _ = writeln!(
1372 out,
1373 " const _fmt_display: []const u8 = if (std.mem.eql(u8, _fmt_type, \"image\")) _fmt_obj.get(\"format\").?.string else _fmt_type;"
1374 );
1375 match assertion.assertion_type.as_str() {
1376 "equals" => {
1377 if let Some(serde_json::Value::String(s)) = &assertion.value {
1378 let escaped = escape_zig(s);
1379 let _ = writeln!(
1380 out,
1381 " try testing.expectEqualStrings(\"{escaped}\", std.mem.trim(u8, _fmt_display, \" \\n\\r\\t\"));"
1382 );
1383 }
1384 }
1385 "contains" => {
1386 if let Some(serde_json::Value::String(s)) = &assertion.value {
1387 let escaped = escape_zig(s);
1388 let _ = writeln!(
1389 out,
1390 " try testing.expect(std.mem.indexOf(u8, _fmt_display, \"{escaped}\") != null);"
1391 );
1392 }
1393 }
1394 "starts_with" => {
1395 if let Some(serde_json::Value::String(s)) = &assertion.value {
1396 let escaped = escape_zig(s);
1397 let _ = writeln!(
1398 out,
1399 " try testing.expect(std.mem.startsWith(u8, _fmt_display, \"{escaped}\"));"
1400 );
1401 }
1402 }
1403 "ends_with" => {
1404 if let Some(serde_json::Value::String(s)) = &assertion.value {
1405 let escaped = escape_zig(s);
1406 let _ = writeln!(
1407 out,
1408 " try testing.expect(std.mem.endsWith(u8, _fmt_display, \"{escaped}\"));"
1409 );
1410 }
1411 }
1412 "not_empty" => {
1413 let _ = writeln!(out, " try testing.expect(_fmt_display.len > 0);");
1414 }
1415 "is_empty" => {
1416 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _fmt_display.len);");
1417 }
1418 _ => {}
1419 }
1420 let _ = writeln!(out, " }}");
1421 return;
1422 }
1423
1424 let zig_val = match &assertion.value {
1426 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_zig(s)),
1427 _ => String::new(),
1428 };
1429 let is_string_val = matches!(&assertion.value, Some(serde_json::Value::String(_)));
1430 let is_bool_val = matches!(&assertion.value, Some(serde_json::Value::Bool(_)));
1431 let bool_val = match &assertion.value {
1432 Some(serde_json::Value::Bool(b)) if *b => "true",
1433 _ => "false",
1434 };
1435 let is_null_val = matches!(&assertion.value, Some(serde_json::Value::Null));
1436 let n = assertion.value.as_ref().map(json_to_zig).unwrap_or_default();
1437 let has_n = assertion.value.as_ref().is_some_and(|v| v.is_number() || v.is_u64());
1438 let is_float_val = matches!(&assertion.value, Some(serde_json::Value::Number(n)) if !n.is_i64() && !n.is_u64());
1443 let values_list: Vec<String> = assertion
1444 .values
1445 .as_deref()
1446 .unwrap_or_default()
1447 .iter()
1448 .filter_map(|v| {
1449 if let serde_json::Value::String(s) = v {
1450 Some(format!("\"{}\"", escape_zig(s)))
1451 } else {
1452 None
1453 }
1454 })
1455 .collect();
1456
1457 let rendered = crate::template_env::render(
1458 "zig/json_assertion.jinja",
1459 minijinja::context! {
1460 assertion_type => assertion.assertion_type.as_str(),
1461 field_expr => field_expr,
1462 is_length_access => is_length_access,
1463 zig_val => zig_val,
1464 is_string_val => is_string_val,
1465 is_bool_val => is_bool_val,
1466 bool_val => bool_val,
1467 is_null_val => is_null_val,
1468 n => n,
1469 has_n => has_n,
1470 is_float_val => is_float_val,
1471 values_list => values_list,
1472 },
1473 );
1474 out.push_str(&rendered);
1475}
1476
1477fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
1480 if let Some(f) = &assertion.field {
1481 if !f.is_empty() && is_streaming_virtual_field(f) {
1482 } else if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1485 return false;
1486 }
1487 }
1488 matches!(
1489 assertion.assertion_type.as_str(),
1490 "equals"
1491 | "contains"
1492 | "contains_all"
1493 | "not_contains"
1494 | "not_empty"
1495 | "is_empty"
1496 | "starts_with"
1497 | "ends_with"
1498 | "min_length"
1499 | "max_length"
1500 | "count_min"
1501 | "count_equals"
1502 | "is_true"
1503 | "is_false"
1504 | "greater_than"
1505 | "less_than"
1506 | "greater_than_or_equal"
1507 | "less_than_or_equal"
1508 | "contains_any"
1509 )
1510}
1511
1512fn build_args_and_setup(
1517 input: &serde_json::Value,
1518 args: &[crate::config::ArgMapping],
1519 fixture_id: &str,
1520 _module_name: &str,
1521) -> (Vec<String>, String, bool) {
1522 if args.is_empty() {
1523 return (Vec::new(), String::new(), false);
1524 }
1525
1526 let mut setup_lines: Vec<String> = Vec::new();
1527 let mut parts: Vec<String> = Vec::new();
1528 let mut setup_needs_gpa = false;
1529
1530 for arg in args {
1531 if arg.arg_type == "mock_url" {
1532 let name = arg.name.clone();
1533 let id_upper = fixture_id.to_uppercase();
1534 setup_lines.push(format!(
1535 "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\"}});"
1536 ));
1537 setup_lines.push(format!("defer allocator.free({name});"));
1538 parts.push(name);
1539 setup_needs_gpa = true;
1540 continue;
1541 }
1542
1543 if arg.arg_type == "handle" {
1546 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1547 let json_str = match input.get(field) {
1548 Some(serde_json::Value::Null) | None => "null".to_string(),
1549 Some(v) => format!("\"{}\"", escape_zig(&serde_json::to_string(v).unwrap_or_default())),
1550 };
1551 parts.push(json_str);
1552 continue;
1553 }
1554
1555 if arg.name == "config" && arg.arg_type == "json_object" {
1562 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1563 let json_str = match input.get(field) {
1564 Some(serde_json::Value::Null) | None => "{}".to_string(),
1565 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1566 };
1567 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1568 continue;
1569 }
1570
1571 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1572 let val = if field.is_empty() || field == "input" {
1578 Some(input)
1579 } else {
1580 input.get(field)
1581 };
1582 match val {
1583 None | Some(serde_json::Value::Null) if arg.optional => {
1584 parts.push("null".to_string());
1587 }
1588 None | Some(serde_json::Value::Null) => {
1589 let default_val = match arg.arg_type.as_str() {
1590 "string" => "\"\"".to_string(),
1591 "int" | "integer" => "0".to_string(),
1592 "float" | "number" => "0.0".to_string(),
1593 "bool" | "boolean" => "false".to_string(),
1594 "json_object" => "\"{}\"".to_string(),
1595 _ => "null".to_string(),
1596 };
1597 parts.push(default_val);
1598 }
1599 Some(v) => {
1600 if arg.arg_type == "json_object" {
1605 let json_str = serde_json::to_string(v).unwrap_or_default();
1606 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1607 } else if arg.arg_type == "bytes" {
1608 if let serde_json::Value::String(path) = v {
1613 let var_name = format!("{}_bytes", arg.name);
1614 let epath = escape_zig(path);
1615 setup_lines.push(format!(
1616 "const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
1617 ));
1618 setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
1619 parts.push(var_name);
1620 } else {
1621 parts.push(json_to_zig(v));
1622 }
1623 } else {
1624 parts.push(json_to_zig(v));
1625 }
1626 }
1627 }
1628 }
1629
1630 (setup_lines, parts.join(", "), setup_needs_gpa)
1631}
1632
1633fn render_assertion(
1634 out: &mut String,
1635 assertion: &Assertion,
1636 result_var: &str,
1637 field_resolver: &FieldResolver,
1638 enum_fields: &HashSet<String>,
1639 result_is_option: bool,
1640) {
1641 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1644 if bare_result_is_option {
1645 match assertion.assertion_type.as_str() {
1646 "is_empty" => {
1647 let _ = writeln!(out, " try testing.expect({result_var} == null);");
1648 return;
1649 }
1650 "not_empty" => {
1651 let _ = writeln!(out, " try testing.expect({result_var} != null);");
1652 return;
1653 }
1654 "not_error" => {
1655 let _ = writeln!(out, " // not_error: covered by try propagation");
1660 return;
1661 }
1662 "equals" => {
1663 if let Some(expected) = &assertion.value {
1664 let zig_val = json_to_zig(expected);
1665 let _ = writeln!(out, " try testing.expectEqualStrings({zig_val}, {result_var}.?);");
1666 return;
1667 }
1668 }
1669 _ => {}
1670 }
1671 }
1672 if let Some(f) = &assertion.field {
1685 if f == "embeddings" && !field_resolver.has_explicit_field(f) {
1686 match assertion.assertion_type.as_str() {
1687 "count_min" | "count_equals" | "not_empty" | "is_empty" => {
1688 let _ = writeln!(out, " {{");
1689 let _ = writeln!(
1690 out,
1691 " var _eparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {result_var}, .{{}});"
1692 );
1693 let _ = writeln!(out, " defer _eparse.deinit();");
1694 let _ = writeln!(out, " const _embeddings_len = _eparse.value.array.items.len;");
1695 match assertion.assertion_type.as_str() {
1696 "count_min" => {
1697 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1698 let _ = writeln!(out, " try testing.expect(_embeddings_len >= {n});");
1699 }
1700 }
1701 "count_equals" => {
1702 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1703 let _ = writeln!(
1704 out,
1705 " try testing.expectEqual(@as(usize, {n}), _embeddings_len);"
1706 );
1707 }
1708 }
1709 "not_empty" => {
1710 let _ = writeln!(out, " try testing.expect(_embeddings_len > 0);");
1711 }
1712 "is_empty" => {
1713 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _embeddings_len);");
1714 }
1715 _ => {}
1716 }
1717 let _ = writeln!(out, " }}");
1718 return;
1719 }
1720 _ => {}
1721 }
1722 }
1723 }
1724
1725 if let Some(f) = &assertion.field {
1731 if f == "result" && !field_resolver.has_explicit_field(f) {
1732 match assertion.assertion_type.as_str() {
1733 "contains" => {
1734 if let Some(expected) = &assertion.value {
1735 let zig_val = json_to_zig(expected);
1736 let _ = writeln!(
1737 out,
1738 " try testing.expect(std.mem.indexOf(u8, {result_var}, {zig_val}) != null);"
1739 );
1740 return;
1741 }
1742 }
1743 "not_contains" => {
1744 if let Some(expected) = &assertion.value {
1745 let zig_val = json_to_zig(expected);
1746 let _ = writeln!(
1747 out,
1748 " try testing.expect(std.mem.indexOf(u8, {result_var}, {zig_val}) == null);"
1749 );
1750 return;
1751 }
1752 }
1753 "equals" => {
1754 if let Some(expected) = &assertion.value {
1755 let zig_val = json_to_zig(expected);
1756 let _ = writeln!(out, " try testing.expectEqualStrings({zig_val}, {result_var});");
1757 return;
1758 }
1759 }
1760 "not_empty" => {
1761 let _ = writeln!(out, " try testing.expect({result_var}.len > 0);");
1762 return;
1763 }
1764 "is_empty" => {
1765 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), {result_var}.len);");
1766 return;
1767 }
1768 _ => {}
1769 }
1770 }
1771 }
1772
1773 if let Some(f) = &assertion.field {
1775 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1776 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
1777 return;
1778 }
1779 }
1780
1781 let _field_is_enum = assertion
1783 .field
1784 .as_deref()
1785 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1786
1787 let field_expr = match &assertion.field {
1788 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
1789 _ => result_var.to_string(),
1790 };
1791
1792 match assertion.assertion_type.as_str() {
1793 "equals" => {
1794 if let Some(expected) = &assertion.value {
1795 let zig_val = json_to_zig(expected);
1796 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
1797 }
1798 }
1799 "contains" => {
1800 if let Some(expected) = &assertion.value {
1801 let zig_val = json_to_zig(expected);
1802 let _ = writeln!(
1803 out,
1804 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1805 );
1806 }
1807 }
1808 "contains_all" => {
1809 if let Some(values) = &assertion.values {
1810 for val in values {
1811 let zig_val = json_to_zig(val);
1812 let _ = writeln!(
1813 out,
1814 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1815 );
1816 }
1817 }
1818 }
1819 "not_contains" => {
1820 if let Some(expected) = &assertion.value {
1821 let zig_val = json_to_zig(expected);
1822 let _ = writeln!(
1823 out,
1824 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1825 );
1826 } else if let Some(values) = &assertion.values {
1827 for val in values {
1831 let zig_val = json_to_zig(val);
1832 let _ = writeln!(
1833 out,
1834 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1835 );
1836 }
1837 }
1838 }
1839 "not_empty" => {
1840 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
1841 }
1842 "is_empty" => {
1843 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
1844 }
1845 "starts_with" => {
1846 if let Some(expected) = &assertion.value {
1847 let zig_val = json_to_zig(expected);
1848 let _ = writeln!(
1849 out,
1850 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
1851 );
1852 }
1853 }
1854 "ends_with" => {
1855 if let Some(expected) = &assertion.value {
1856 let zig_val = json_to_zig(expected);
1857 let _ = writeln!(
1858 out,
1859 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
1860 );
1861 }
1862 }
1863 "min_length" => {
1864 if let Some(val) = &assertion.value {
1865 if let Some(n) = val.as_u64() {
1866 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1867 }
1868 }
1869 }
1870 "max_length" => {
1871 if let Some(val) = &assertion.value {
1872 if let Some(n) = val.as_u64() {
1873 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
1874 }
1875 }
1876 }
1877 "count_min" => {
1878 if let Some(val) = &assertion.value {
1879 if let Some(n) = val.as_u64() {
1880 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1881 }
1882 }
1883 }
1884 "count_equals" => {
1885 if let Some(val) = &assertion.value {
1886 if let Some(n) = val.as_u64() {
1887 let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
1891 if has_field {
1892 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
1893 } else {
1894 let _ = writeln!(out, " {{");
1895 let _ = writeln!(
1896 out,
1897 " var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
1898 );
1899 let _ = writeln!(out, " defer _cparse.deinit();");
1900 let _ = writeln!(
1901 out,
1902 " try testing.expectEqual({n}, _cparse.value.array.items.len);"
1903 );
1904 let _ = writeln!(out, " }}");
1905 }
1906 }
1907 }
1908 }
1909 "is_true" => {
1910 let _ = writeln!(out, " try testing.expect({field_expr});");
1911 }
1912 "is_false" => {
1913 let _ = writeln!(out, " try testing.expect(!{field_expr});");
1914 }
1915 "not_error" => {
1916 }
1918 "error" => {
1919 }
1921 "greater_than" => {
1922 if let Some(val) = &assertion.value {
1923 let zig_val = json_to_zig(val);
1924 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
1925 }
1926 }
1927 "less_than" => {
1928 if let Some(val) = &assertion.value {
1929 let zig_val = json_to_zig(val);
1930 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
1931 }
1932 }
1933 "greater_than_or_equal" => {
1934 if let Some(val) = &assertion.value {
1935 let zig_val = json_to_zig(val);
1936 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
1937 }
1938 }
1939 "less_than_or_equal" => {
1940 if let Some(val) = &assertion.value {
1941 let zig_val = json_to_zig(val);
1942 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
1943 }
1944 }
1945 "contains_any" => {
1946 if let Some(values) = &assertion.values {
1948 let string_values: Vec<String> = values
1949 .iter()
1950 .filter_map(|v| {
1951 if let serde_json::Value::String(s) = v {
1952 Some(format!(
1953 "std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
1954 escape_zig(s)
1955 ))
1956 } else {
1957 None
1958 }
1959 })
1960 .collect();
1961 if !string_values.is_empty() {
1962 let condition = string_values.join(" or\n ");
1963 let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
1964 }
1965 }
1966 }
1967 "matches_regex" => {
1968 let _ = writeln!(out, " // regex match not yet implemented for Zig");
1969 }
1970 "method_result" => {
1971 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
1972 }
1973 other => {
1974 panic!("Zig e2e generator: unsupported assertion type: {other}");
1975 }
1976 }
1977}
1978
1979fn json_to_zig(value: &serde_json::Value) -> String {
1981 match value {
1982 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
1983 serde_json::Value::Bool(b) => b.to_string(),
1984 serde_json::Value::Number(n) => n.to_string(),
1985 serde_json::Value::Null => "null".to_string(),
1986 serde_json::Value::Array(arr) => {
1987 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
1988 format!("&.{{{}}}", items.join(", "))
1989 }
1990 serde_json::Value::Object(_) => {
1991 let json_str = serde_json::to_string(value).unwrap_or_default();
1992 format!("\"{}\"", escape_zig(&json_str))
1993 }
1994 }
1995}