1use crate::config::E2eConfig;
7use crate::escape::{escape_zig, sanitize_filename};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, Fixture, FixtureGroup};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::ResolvedCrateConfig;
12use alef_core::hash::{self, CommentStyle};
13use alef_core::template_versions::toolchain;
14use anyhow::Result;
15use heck::{ToShoutySnakeCase, ToSnakeCase};
16use std::collections::HashSet;
17use std::fmt::Write as FmtWrite;
18use std::path::PathBuf;
19
20use super::E2eCodegen;
21use super::client;
22use super::streaming_assertions::{StreamingFieldResolver, is_streaming_virtual_field};
23
24pub struct ZigE2eCodegen;
26
27impl E2eCodegen for ZigE2eCodegen {
28 fn generate(
29 &self,
30 groups: &[FixtureGroup],
31 e2e_config: &E2eConfig,
32 config: &ResolvedCrateConfig,
33 _type_defs: &[alef_core::ir::TypeDef],
34 ) -> Result<Vec<GeneratedFile>> {
35 let lang = self.language_name();
36 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
37
38 let mut files = Vec::new();
39
40 let call = &e2e_config.call;
42 let overrides = call.overrides.get(lang);
43 let _module_path = overrides
44 .and_then(|o| o.module.as_ref())
45 .cloned()
46 .unwrap_or_else(|| call.module.clone());
47 let function_name = overrides
48 .and_then(|o| o.function.as_ref())
49 .cloned()
50 .unwrap_or_else(|| call.function.clone());
51 let result_var = &call.result_var;
52
53 let zig_pkg = e2e_config.resolve_package("zig");
55 let pkg_path = zig_pkg
56 .as_ref()
57 .and_then(|p| p.path.as_ref())
58 .cloned()
59 .unwrap_or_else(|| "../../packages/zig".to_string());
60 let pkg_name = zig_pkg
61 .as_ref()
62 .and_then(|p| p.name.as_ref())
63 .cloned()
64 .unwrap_or_else(|| config.name.to_snake_case());
65
66 files.push(GeneratedFile {
68 path: output_base.join("build.zig.zon"),
69 content: render_build_zig_zon(&pkg_name, &pkg_path, e2e_config.dep_mode),
70 generated_header: false,
71 });
72
73 let module_name = config.zig_module_name();
75 let ffi_prefix = config.ffi_prefix();
76
77 let field_resolver = FieldResolver::new(
79 &e2e_config.fields,
80 &e2e_config.fields_optional,
81 &e2e_config.result_fields,
82 &e2e_config.fields_array,
83 &e2e_config.fields_method_calls,
84 );
85
86 let mut test_filenames: Vec<String> = Vec::new();
88 for group in groups {
89 let active: Vec<&Fixture> = group
90 .fixtures
91 .iter()
92 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
93 .collect();
94
95 if active.is_empty() {
96 continue;
97 }
98
99 let filename = format!("{}_test.zig", sanitize_filename(&group.category));
100 test_filenames.push(filename.clone());
101 let content = render_test_file(
102 &group.category,
103 &active,
104 e2e_config,
105 &function_name,
106 result_var,
107 &e2e_config.call.args,
108 &field_resolver,
109 &e2e_config.fields_enum,
110 &module_name,
111 &ffi_prefix,
112 );
113 files.push(GeneratedFile {
114 path: output_base.join("src").join(filename),
115 content,
116 generated_header: true,
117 });
118 }
119
120 files.insert(
122 files
123 .iter()
124 .position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
125 .unwrap_or(1),
126 GeneratedFile {
127 path: output_base.join("build.zig"),
128 content: render_build_zig(
129 &test_filenames,
130 &pkg_name,
131 &module_name,
132 &config.ffi_lib_name(),
133 &config.ffi_crate_path(),
134 &e2e_config.test_documents_relative_from(0),
135 ),
136 generated_header: false,
137 },
138 );
139
140 Ok(files)
141 }
142
143 fn language_name(&self) -> &'static str {
144 "zig"
145 }
146}
147
148fn render_build_zig_zon(pkg_name: &str, pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
153 let dep_block = match dep_mode {
154 crate::config::DependencyMode::Registry => {
155 format!(
157 r#".{{
158 .url = "https://registry.example.com/{pkg_name}/v0.1.0.tar.gz",
159 .hash = "0000000000000000000000000000000000000000000000000000000000000000",
160 }}"#
161 )
162 }
163 crate::config::DependencyMode::Local => {
164 format!(r#".{{ .path = "{pkg_path}" }}"#)
165 }
166 };
167
168 let min_zig = toolchain::MIN_ZIG_VERSION;
169 let name_bytes: &[u8] = b"e2e_zig";
171 let mut crc: u32 = 0xffff_ffff;
172 for byte in name_bytes {
173 crc ^= *byte as u32;
174 for _ in 0..8 {
175 let mask = (crc & 1).wrapping_neg();
176 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
177 }
178 }
179 let name_crc: u32 = !crc;
180 let mut id: u32 = 0x811c_9dc5;
181 for byte in name_bytes {
182 id ^= *byte as u32;
183 id = id.wrapping_mul(0x0100_0193);
184 }
185 if id == 0 || id == 0xffff_ffff {
186 id = 0x1;
187 }
188 let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
189 format!(
190 r#".{{
191 .name = .e2e_zig,
192 .version = "0.1.0",
193 .fingerprint = 0x{fingerprint:016x},
194 .minimum_zig_version = "{min_zig}",
195 .dependencies = .{{
196 .{pkg_name} = {dep_block},
197 }},
198 .paths = .{{
199 "build.zig",
200 "build.zig.zon",
201 "src",
202 }},
203}}
204"#
205 )
206}
207
208fn render_build_zig(
209 test_filenames: &[String],
210 pkg_name: &str,
211 module_name: &str,
212 ffi_lib_name: &str,
213 ffi_crate_path: &str,
214 test_documents_path: &str,
215) -> String {
216 if test_filenames.is_empty() {
217 return r#"const std = @import("std");
218
219pub fn build(b: *std.Build) void {
220 const target = b.standardTargetOptions(.{});
221 const optimize = b.standardOptimizeOption(.{});
222
223 const test_step = b.step("test", "Run tests");
224}
225"#
226 .to_string();
227 }
228
229 let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
239 content.push_str(" const target = b.standardTargetOptions(.{});\n");
240 content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
241 content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n");
242 let _ = writeln!(
243 content,
244 " const ffi_path = b.option([]const u8, \"ffi_path\", \"Path to directory containing lib{ffi_lib_name}\") orelse \"../../target/debug\";"
245 );
246 let _ = writeln!(
247 content,
248 " const ffi_include = b.option([]const u8, \"ffi_include_path\", \"Path to directory containing FFI header\") orelse \"{ffi_crate_path}/include\";"
249 );
250 let _ = writeln!(content);
251 let _ = writeln!(
252 content,
253 " const {module_name}_module = b.addModule(\"{module_name}\", .{{"
254 );
255 let _ = writeln!(
256 content,
257 " .root_source_file = b.path(\"../../packages/zig/src/{pkg_name}.zig\"),"
258 );
259 content.push_str(" .target = target,\n");
260 content.push_str(" .optimize = optimize,\n");
261 content.push_str(" .link_libc = true,\n");
265 content.push_str(" });\n");
266 let _ = writeln!(
267 content,
268 " {module_name}_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});"
269 );
270 let _ = writeln!(
271 content,
272 " {module_name}_module.addIncludePath(.{{ .cwd_relative = ffi_include }});"
273 );
274 let _ = writeln!(
275 content,
276 " {module_name}_module.linkSystemLibrary(\"{ffi_lib_name}\", .{{}});"
277 );
278 let _ = writeln!(content);
279
280 for filename in test_filenames {
281 let test_name = filename.trim_end_matches("_test.zig");
283 content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
284 content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
285 content.push_str(" .target = target,\n");
286 content.push_str(" .optimize = optimize,\n");
287 content.push_str(" .link_libc = true,\n");
291 content.push_str(" });\n");
292 content.push_str(&format!(
293 " {test_name}_module.addImport(\"{module_name}\", {module_name}_module);\n"
294 ));
295 content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
301 content.push_str(&format!(" .name = \"{test_name}_test\",\n"));
302 content.push_str(&format!(" .root_module = {test_name}_module,\n"));
303 content.push_str(" });\n");
304 content.push_str(&format!(
305 " const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
306 ));
307 content.push_str(&format!(
308 " {test_name}_run.setCwd(b.path(\"{test_documents_path}\"));\n"
309 ));
310 content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
311 }
312
313 content.push_str("}\n");
314 content
315}
316
317struct ZigTestClientRenderer;
325
326impl client::TestClientRenderer for ZigTestClientRenderer {
327 fn language_name(&self) -> &'static str {
328 "zig"
329 }
330
331 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
332 if let Some(reason) = skip_reason {
333 let _ = writeln!(out, "test \"{fn_name}\" {{");
334 let _ = writeln!(out, " // {description}");
335 let _ = writeln!(out, " // skipped: {reason}");
336 let _ = writeln!(out, " return error.SkipZigTest;");
337 } else {
338 let _ = writeln!(out, "test \"{fn_name}\" {{");
339 let _ = writeln!(out, " // {description}");
340 }
341 }
342
343 fn render_test_close(&self, out: &mut String) {
344 let _ = writeln!(out, "}}");
345 }
346
347 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
348 let method = ctx.method.to_uppercase();
349 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
350
351 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
352 let _ = writeln!(out, " defer _ = gpa.deinit();");
353 let _ = writeln!(out, " const allocator = gpa.allocator();");
354
355 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
356 let _ = writeln!(
357 out,
358 " 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\"}});"
359 );
360
361 if !ctx.headers.is_empty() {
363 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
364 header_pairs.sort_by_key(|(k, _)| k.as_str());
365 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
366 for (k, v) in &header_pairs {
367 let ek = escape_zig(k);
368 let ev = escape_zig(v);
369 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
370 }
371 let _ = writeln!(out, " }};");
372 }
373
374 if let Some(body) = ctx.body {
376 let json_str = serde_json::to_string(body).unwrap_or_default();
377 let escaped = escape_zig(&json_str);
378 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
379 }
380
381 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
382 let has_body = ctx.body.is_some();
383
384 let _ = writeln!(
385 out,
386 " var http_client = std.http.Client{{ .allocator = allocator }};"
387 );
388 let _ = writeln!(out, " defer http_client.deinit();");
389 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
390 let _ = writeln!(out, " defer response_body.deinit();");
391
392 let method_zig = match method.as_str() {
393 "GET" => ".GET",
394 "POST" => ".POST",
395 "PUT" => ".PUT",
396 "DELETE" => ".DELETE",
397 "PATCH" => ".PATCH",
398 "HEAD" => ".HEAD",
399 "OPTIONS" => ".OPTIONS",
400 _ => ".GET",
401 };
402
403 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
404 let _ = writeln!(
405 out,
406 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
407 rv = ctx.response_var,
408 );
409 }
410
411 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
412 let _ = writeln!(
413 out,
414 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
415 );
416 }
417
418 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
419 let ename = escape_zig(&name.to_lowercase());
420 match expected {
421 "<<present>>" => {
422 let _ = writeln!(
423 out,
424 " // assert header '{ename}' is present (header inspection not yet implemented)"
425 );
426 }
427 "<<absent>>" => {
428 let _ = writeln!(
429 out,
430 " // assert header '{ename}' is absent (header inspection not yet implemented)"
431 );
432 }
433 "<<uuid>>" => {
434 let _ = writeln!(
435 out,
436 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
437 );
438 }
439 exact => {
440 let evalue = escape_zig(exact);
441 let _ = writeln!(
442 out,
443 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
444 );
445 }
446 }
447 }
448
449 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
450 let json_str = serde_json::to_string(expected).unwrap_or_default();
451 let escaped = escape_zig(&json_str);
452 let _ = writeln!(
453 out,
454 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
455 );
456 }
457
458 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
459 if let Some(obj) = expected.as_object() {
460 for (key, val) in obj {
461 let ekey = escape_zig(key);
462 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
463 let _ = writeln!(
464 out,
465 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
466 );
467 }
468 }
469 }
470
471 fn render_assert_validation_errors(
472 &self,
473 out: &mut String,
474 _response_var: &str,
475 errors: &[crate::fixture::ValidationErrorExpectation],
476 ) {
477 for ve in errors {
478 let loc = ve.loc.join(".");
479 let escaped_loc = escape_zig(&loc);
480 let escaped_msg = escape_zig(&ve.msg);
481 let _ = writeln!(
482 out,
483 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
484 );
485 }
486 }
487}
488
489fn render_http_test_case(out: &mut String, fixture: &Fixture) {
494 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
495}
496
497#[allow(clippy::too_many_arguments)]
502fn render_test_file(
503 category: &str,
504 fixtures: &[&Fixture],
505 e2e_config: &E2eConfig,
506 function_name: &str,
507 result_var: &str,
508 args: &[crate::config::ArgMapping],
509 field_resolver: &FieldResolver,
510 enum_fields: &HashSet<String>,
511 module_name: &str,
512 ffi_prefix: &str,
513) -> String {
514 let mut out = String::new();
515 out.push_str(&hash::header(CommentStyle::DoubleSlash));
516 let _ = writeln!(out, "const std = @import(\"std\");");
517 let _ = writeln!(out, "const testing = std.testing;");
518 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
519 let _ = writeln!(out);
520
521 let _ = writeln!(out, "// E2e tests for category: {category}");
522 let _ = writeln!(out);
523
524 for fixture in fixtures {
525 if fixture.http.is_some() {
526 render_http_test_case(&mut out, fixture);
527 } else {
528 render_test_fn(
529 &mut out,
530 fixture,
531 e2e_config,
532 function_name,
533 result_var,
534 args,
535 field_resolver,
536 enum_fields,
537 module_name,
538 ffi_prefix,
539 );
540 }
541 let _ = writeln!(out);
542 }
543
544 out
545}
546
547#[allow(clippy::too_many_arguments)]
548fn render_test_fn(
549 out: &mut String,
550 fixture: &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) {
560 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
562 let lang = "zig";
563 let call_overrides = call_config.overrides.get(lang);
564 let function_name = call_overrides
565 .and_then(|o| o.function.as_ref())
566 .cloned()
567 .unwrap_or_else(|| call_config.function.clone());
568 let result_var = &call_config.result_var;
569 let args = &call_config.args;
570 let client_factory = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
575 e2e_config
576 .call
577 .overrides
578 .get(lang)
579 .and_then(|o| o.client_factory.as_deref())
580 });
581
582 let call_result_is_bytes = call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
599 let result_is_json_struct =
600 !call_result_is_bytes && (call_overrides.is_some_and(|o| o.result_is_json_struct) || client_factory.is_some());
601
602 let result_is_option = call_overrides.is_some_and(|o| o.result_is_option) || call_config.result_is_option;
607
608 let test_name = fixture.id.to_snake_case();
609 let description = &fixture.description;
610 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
611
612 let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
613 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
619 let args_str = if extra_args.is_empty() {
620 args_str
621 } else if args_str.is_empty() {
622 extra_args.join(", ")
623 } else {
624 format!("{args_str}, {}", extra_args.join(", "))
625 };
626
627 let any_happy_emits_code = fixture
630 .assertions
631 .iter()
632 .any(|a| assertion_emits_code(a, field_resolver));
633 let any_non_error_emits_code = fixture
634 .assertions
635 .iter()
636 .filter(|a| a.assertion_type != "error")
637 .any(|a| assertion_emits_code(a, field_resolver));
638
639 let has_streaming_virtual_assertions = fixture.assertions.iter().any(|a| {
641 a.field
642 .as_ref()
643 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
644 });
645 let is_stream_fn = function_name.contains("stream");
646 let uses_streaming_virtual_path =
647 result_is_json_struct && has_streaming_virtual_assertions && is_stream_fn && client_factory.is_some();
648 let streaming_path_has_non_streaming = uses_streaming_virtual_path
650 && fixture.assertions.iter().any(|a| {
651 !a.field
652 .as_ref()
653 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
654 && !matches!(a.assertion_type.as_str(), "not_error" | "error")
655 && a.field
656 .as_ref()
657 .is_some_and(|f| !f.is_empty() && field_resolver.is_valid_for_result(f))
658 });
659
660 let _ = writeln!(out, "test \"{test_name}\" {{");
661 let _ = writeln!(out, " // {description}");
662
663 let needs_gpa = setup_needs_gpa
671 || streaming_path_has_non_streaming
672 || (!uses_streaming_virtual_path && result_is_json_struct && !expects_error && any_happy_emits_code)
673 || (!uses_streaming_virtual_path && result_is_json_struct && expects_error && any_non_error_emits_code);
674 if needs_gpa {
675 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
676 let _ = writeln!(out, " defer _ = gpa.deinit();");
677 let _ = writeln!(out, " const allocator = gpa.allocator();");
678 let _ = writeln!(out);
679 }
680
681 for line in &setup_lines {
682 let _ = writeln!(out, " {line}");
683 }
684
685 let call_prefix = if let Some(factory) = client_factory {
690 let fixture_id = &fixture.id;
691 let _ = writeln!(
692 out,
693 " 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);"
694 );
695 let _ = writeln!(out, " defer std.heap.c_allocator.free(_mock_url);");
696 let _ = writeln!(
697 out,
698 " var _client = try {module_name}.{factory}(\"test-key\", _mock_url, null, null, null);"
699 );
700 let _ = writeln!(out, " defer _client.free();");
701 "_client".to_string()
702 } else {
703 module_name.to_string()
704 };
705
706 if expects_error {
707 if result_is_json_struct {
711 let _ = writeln!(
712 out,
713 " const _result_json = {call_prefix}.{function_name}({args_str}) catch {{"
714 );
715 } else {
716 let _ = writeln!(
717 out,
718 " const result = {call_prefix}.{function_name}({args_str}) catch {{"
719 );
720 }
721 let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
722 let _ = writeln!(out, " return;");
723 let _ = writeln!(out, " }};");
724 let any_emits_code = fixture
728 .assertions
729 .iter()
730 .filter(|a| a.assertion_type != "error")
731 .any(|a| assertion_emits_code(a, field_resolver));
732 if result_is_json_struct && any_emits_code {
733 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
734 let _ = writeln!(
735 out,
736 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
737 );
738 let _ = writeln!(out, " defer _parsed.deinit();");
739 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
740 let _ = writeln!(out, " // Perform success assertions if any");
741 for assertion in &fixture.assertions {
742 if assertion.assertion_type != "error" {
743 render_json_assertion(out, assertion, result_var, field_resolver);
744 }
745 }
746 } else if result_is_json_struct {
747 let _ = writeln!(out, " _ = _result_json;");
748 } else if any_emits_code {
749 let _ = writeln!(out, " // Perform success assertions if any");
750 for assertion in &fixture.assertions {
751 if assertion.assertion_type != "error" {
752 render_assertion(
753 out,
754 assertion,
755 result_var,
756 field_resolver,
757 enum_fields,
758 result_is_option,
759 );
760 }
761 }
762 } else {
763 let _ = writeln!(out, " _ = result;");
764 }
765 } else if fixture.assertions.is_empty() {
766 if result_is_json_struct {
768 let _ = writeln!(
769 out,
770 " const _result_json = try {call_prefix}.{function_name}({args_str});"
771 );
772 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
773 } else {
774 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
775 }
776 } else {
777 let any_emits_code = fixture
781 .assertions
782 .iter()
783 .any(|a| assertion_emits_code(a, field_resolver));
784 if call_result_is_bytes && client_factory.is_some() {
785 let _ = writeln!(
788 out,
789 " const _result_json = try {call_prefix}.{function_name}({args_str});"
790 );
791 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
792 let has_bytes_assertions = fixture
793 .assertions
794 .iter()
795 .any(|a| matches!(a.assertion_type.as_str(), "not_empty" | "is_empty"));
796 if has_bytes_assertions {
797 for assertion in &fixture.assertions {
798 match assertion.assertion_type.as_str() {
799 "not_empty" => {
800 let _ = writeln!(out, " try testing.expect(_result_json.len > 0);");
801 }
802 "is_empty" => {
803 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _result_json.len);");
804 }
805 "not_error" | "error" => {}
806 _ => {
807 let atype = &assertion.assertion_type;
808 let _ = writeln!(
809 out,
810 " // bytes result: assertion '{atype}' not implemented for zig bytes"
811 );
812 }
813 }
814 }
815 }
816 } else if result_is_json_struct {
817 if uses_streaming_virtual_path {
821 let request_from_json = format!("{ffi_prefix}_chat_completion_request_from_json");
822 let request_free = format!("{ffi_prefix}_chat_completion_request_free");
823 let stream_start = format!("{ffi_prefix}_default_client_chat_stream_start");
824 let stream_free = format!("{ffi_prefix}_default_client_chat_stream_free");
825 let client_c_type = format!("{}DefaultClient", ffi_prefix.to_shouty_snake_case());
826
827 let _ = writeln!(
830 out,
831 " const _req_z = try std.heap.c_allocator.dupeZ(u8, {args_str});"
832 );
833 let _ = writeln!(out, " defer std.heap.c_allocator.free(_req_z);");
834 let _ = writeln!(
835 out,
836 " const _req_handle = {module_name}.c.{request_from_json}(_req_z.ptr);"
837 );
838 let _ = writeln!(out, " defer {module_name}.c.{request_free}(_req_handle);");
839 let _ = writeln!(
840 out,
841 " const _stream_handle = {module_name}.c.{stream_start}(@as(*{module_name}.c.{client_c_type}, @ptrCast(_client._handle)), _req_handle);"
842 );
843 let _ = writeln!(out, " if (_stream_handle == null) return error.StreamStartFailed;");
844 let _ = writeln!(out, " defer {module_name}.c.{stream_free}(_stream_handle);");
845 let snip =
847 StreamingFieldResolver::collect_snippet_zig("_stream_handle", "chunks", module_name, ffi_prefix);
848 out.push_str(" ");
849 out.push_str(&snip);
850 out.push('\n');
851 if streaming_path_has_non_streaming {
854 let _ = writeln!(
855 out,
856 " const _result_json = if (chunks.items.len > 0) chunks.items[chunks.items.len - 1] else &[_]u8{{}};"
857 );
858 let _ = writeln!(
859 out,
860 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
861 );
862 let _ = writeln!(out, " defer _parsed.deinit();");
863 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
864 }
865 for assertion in &fixture.assertions {
866 render_json_assertion(out, assertion, result_var, field_resolver);
867 }
868 } else {
869 let _ = writeln!(
871 out,
872 " const _result_json = try {call_prefix}.{function_name}({args_str});"
873 );
874 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
875 if any_emits_code {
876 let _ = writeln!(
877 out,
878 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
879 );
880 let _ = writeln!(out, " defer _parsed.deinit();");
881 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
882 for assertion in &fixture.assertions {
883 render_json_assertion(out, assertion, result_var, field_resolver);
884 }
885 }
886 }
887 } else if any_emits_code {
888 let _ = writeln!(
889 out,
890 " const {result_var} = try {call_prefix}.{function_name}({args_str});"
891 );
892 for assertion in &fixture.assertions {
893 render_assertion(
894 out,
895 assertion,
896 result_var,
897 field_resolver,
898 enum_fields,
899 result_is_option,
900 );
901 }
902 } else {
903 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
904 }
905 }
906
907 let _ = writeln!(out, "}}");
908}
909
910const FORMAT_METADATA_VARIANTS: &[&str] = &[
928 "pdf",
929 "docx",
930 "excel",
931 "email",
932 "pptx",
933 "archive",
934 "image",
935 "xml",
936 "text",
937 "html",
938 "ocr",
939 "csv",
940 "bibtex",
941 "citation",
942 "fiction_book",
943 "dbf",
944 "jats",
945 "epub",
946 "pst",
947 "code",
948];
949
950fn json_path_expr(result_var: &str, field_path: &str) -> String {
951 let segments: Vec<&str> = field_path.split('.').collect();
952 let mut expr = result_var.to_string();
953 let mut prev_seg: Option<&str> = None;
954 for seg in &segments {
955 if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
960 prev_seg = Some(seg);
961 continue;
962 }
963 if let Some(key) = seg.strip_suffix("[]") {
967 expr = format!("{expr}.object.get(\"{key}\").?.array.items[0]");
968 } else if let Some(bracket_pos) = seg.find('[') {
969 if let Some(end_pos) = seg.find(']') {
970 if end_pos > bracket_pos + 1 && end_pos == seg.len() - 1 {
971 let key = &seg[..bracket_pos];
972 let idx = &seg[bracket_pos + 1..end_pos];
973 if idx.chars().all(|c| c.is_ascii_digit()) {
974 expr = format!("{expr}.object.get(\"{key}\").?.array.items[{idx}]");
975 prev_seg = Some(seg);
976 continue;
977 }
978 }
979 }
980 expr = format!("{expr}.object.get(\"{seg}\").?");
981 } else {
982 expr = format!("{expr}.object.get(\"{seg}\").?");
983 }
984 prev_seg = Some(seg);
985 }
986 expr
987}
988
989fn render_json_assertion(out: &mut String, assertion: &Assertion, result_var: &str, field_resolver: &FieldResolver) {
994 if let Some(f) = &assertion.field {
996 if !f.is_empty() && is_streaming_virtual_field(f) {
997 if let Some(expr) = StreamingFieldResolver::accessor(f, "zig", "chunks") {
998 match assertion.assertion_type.as_str() {
999 "count_min" => {
1000 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1001 let _ = writeln!(out, " try testing.expect({expr}.len >= {n});");
1002 }
1003 }
1004 "count_equals" => {
1005 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1006 let _ = writeln!(out, " try testing.expectEqual(@as(usize, {n}), {expr}.len);");
1007 }
1008 }
1009 "equals" => {
1010 if let Some(serde_json::Value::String(s)) = &assertion.value {
1011 let escaped = escape_zig(s);
1012 let _ = writeln!(out, " try testing.expectEqualStrings(\"{escaped}\", {expr});");
1013 } else if let Some(v) = &assertion.value {
1014 let zig_val = json_to_zig(v);
1015 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {expr});");
1016 }
1017 }
1018 "not_empty" => {
1019 let _ = writeln!(out, " try testing.expect({expr}.len > 0);");
1020 }
1021 "is_true" => {
1022 let _ = writeln!(out, " try testing.expect({expr});");
1023 }
1024 "is_false" => {
1025 let _ = writeln!(out, " try testing.expect(!{expr});");
1026 }
1027 _ => {
1028 let atype = &assertion.assertion_type;
1029 let _ = writeln!(
1030 out,
1031 " // streaming virtual field '{f}' assertion '{atype}' not implemented for zig"
1032 );
1033 }
1034 }
1035 }
1036 return;
1037 }
1038 }
1039
1040 if let Some(f) = &assertion.field {
1047 if f == "embeddings" && !field_resolver.has_explicit_field("embeddings") {
1048 match assertion.assertion_type.as_str() {
1049 "count_min" => {
1050 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1051 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len >= {n});");
1052 }
1053 return;
1054 }
1055 "count_equals" => {
1056 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1057 let _ = writeln!(
1058 out,
1059 " try testing.expectEqual(@as(usize, {n}), {result_var}.array.items.len);"
1060 );
1061 }
1062 return;
1063 }
1064 "not_empty" => {
1065 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len > 0);");
1066 return;
1067 }
1068 "is_empty" => {
1069 let _ = writeln!(
1070 out,
1071 " try testing.expectEqual(@as(usize, 0), {result_var}.array.items.len);"
1072 );
1073 return;
1074 }
1075 _ => {}
1076 }
1077 }
1078 }
1079
1080 if let Some(f) = &assertion.field {
1082 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1083 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1084 return;
1085 }
1086 }
1087 if matches!(assertion.assertion_type.as_str(), "not_error" | "error") {
1089 return;
1090 }
1091
1092 let raw_field_path = assertion.field.as_deref().unwrap_or("").trim();
1093 let field_path = if raw_field_path.is_empty() {
1094 raw_field_path.to_string()
1095 } else {
1096 field_resolver.resolve(raw_field_path).to_string()
1097 };
1098 let field_path = field_path.trim();
1099
1100 let (field_path_for_expr, is_length_access) = if let Some(parent) = field_path.strip_suffix(".length") {
1102 (parent, true)
1103 } else {
1104 (field_path, false)
1105 };
1106
1107 let field_expr = if field_path_for_expr.is_empty() {
1108 result_var.to_string()
1109 } else {
1110 json_path_expr(result_var, field_path_for_expr)
1111 };
1112
1113 let zig_val = match &assertion.value {
1115 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_zig(s)),
1116 _ => String::new(),
1117 };
1118 let is_string_val = matches!(&assertion.value, Some(serde_json::Value::String(_)));
1119 let is_bool_val = matches!(&assertion.value, Some(serde_json::Value::Bool(_)));
1120 let bool_val = match &assertion.value {
1121 Some(serde_json::Value::Bool(b)) if *b => "true",
1122 _ => "false",
1123 };
1124 let is_null_val = matches!(&assertion.value, Some(serde_json::Value::Null));
1125 let n = assertion.value.as_ref().map(json_to_zig).unwrap_or_default();
1126 let has_n = assertion.value.as_ref().is_some_and(|v| v.is_number() || v.is_u64());
1127 let is_float_val = matches!(&assertion.value, Some(serde_json::Value::Number(n)) if !n.is_i64() && !n.is_u64());
1132 let values_list: Vec<String> = assertion
1133 .values
1134 .as_deref()
1135 .unwrap_or_default()
1136 .iter()
1137 .filter_map(|v| {
1138 if let serde_json::Value::String(s) = v {
1139 Some(format!("\"{}\"", escape_zig(s)))
1140 } else {
1141 None
1142 }
1143 })
1144 .collect();
1145
1146 let rendered = crate::template_env::render(
1147 "zig/json_assertion.jinja",
1148 minijinja::context! {
1149 assertion_type => assertion.assertion_type.as_str(),
1150 field_expr => field_expr,
1151 is_length_access => is_length_access,
1152 zig_val => zig_val,
1153 is_string_val => is_string_val,
1154 is_bool_val => is_bool_val,
1155 bool_val => bool_val,
1156 is_null_val => is_null_val,
1157 n => n,
1158 has_n => has_n,
1159 is_float_val => is_float_val,
1160 values_list => values_list,
1161 },
1162 );
1163 out.push_str(&rendered);
1164}
1165
1166fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
1169 if let Some(f) = &assertion.field {
1170 if !f.is_empty() && is_streaming_virtual_field(f) {
1171 } else if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1174 return false;
1175 }
1176 }
1177 matches!(
1178 assertion.assertion_type.as_str(),
1179 "equals"
1180 | "contains"
1181 | "contains_all"
1182 | "not_contains"
1183 | "not_empty"
1184 | "is_empty"
1185 | "starts_with"
1186 | "ends_with"
1187 | "min_length"
1188 | "max_length"
1189 | "count_min"
1190 | "count_equals"
1191 | "is_true"
1192 | "is_false"
1193 | "greater_than"
1194 | "less_than"
1195 | "greater_than_or_equal"
1196 | "less_than_or_equal"
1197 | "contains_any"
1198 )
1199}
1200
1201fn build_args_and_setup(
1206 input: &serde_json::Value,
1207 args: &[crate::config::ArgMapping],
1208 fixture_id: &str,
1209 _module_name: &str,
1210) -> (Vec<String>, String, bool) {
1211 if args.is_empty() {
1212 return (Vec::new(), String::new(), false);
1213 }
1214
1215 let mut setup_lines: Vec<String> = Vec::new();
1216 let mut parts: Vec<String> = Vec::new();
1217 let mut setup_needs_gpa = false;
1218
1219 for arg in args {
1220 if arg.arg_type == "mock_url" {
1221 let name = arg.name.clone();
1222 let id_upper = fixture_id.to_uppercase();
1223 setup_lines.push(format!(
1224 "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\"}});"
1225 ));
1226 setup_lines.push(format!("defer allocator.free({name});"));
1227 parts.push(name);
1228 setup_needs_gpa = true;
1229 continue;
1230 }
1231
1232 if arg.arg_type == "handle" {
1235 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1236 let json_str = match input.get(field) {
1237 Some(serde_json::Value::Null) | None => "null".to_string(),
1238 Some(v) => format!("\"{}\"", escape_zig(&serde_json::to_string(v).unwrap_or_default())),
1239 };
1240 parts.push(json_str);
1241 continue;
1242 }
1243
1244 if arg.name == "config" && arg.arg_type == "json_object" {
1251 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1252 let json_str = match input.get(field) {
1253 Some(serde_json::Value::Null) | None => "{}".to_string(),
1254 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1255 };
1256 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1257 continue;
1258 }
1259
1260 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1261 let val = if field.is_empty() || field == "input" {
1267 Some(input)
1268 } else {
1269 input.get(field)
1270 };
1271 match val {
1272 None | Some(serde_json::Value::Null) if arg.optional => {
1273 parts.push("null".to_string());
1276 }
1277 None | Some(serde_json::Value::Null) => {
1278 let default_val = match arg.arg_type.as_str() {
1279 "string" => "\"\"".to_string(),
1280 "int" | "integer" => "0".to_string(),
1281 "float" | "number" => "0.0".to_string(),
1282 "bool" | "boolean" => "false".to_string(),
1283 "json_object" => "\"{}\"".to_string(),
1284 _ => "null".to_string(),
1285 };
1286 parts.push(default_val);
1287 }
1288 Some(v) => {
1289 if arg.arg_type == "json_object" {
1294 let json_str = serde_json::to_string(v).unwrap_or_default();
1295 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1296 } else if arg.arg_type == "bytes" {
1297 if let serde_json::Value::String(path) = v {
1302 let var_name = format!("{}_bytes", arg.name);
1303 let epath = escape_zig(path);
1304 setup_lines.push(format!(
1305 "const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
1306 ));
1307 setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
1308 parts.push(var_name);
1309 } else {
1310 parts.push(json_to_zig(v));
1311 }
1312 } else {
1313 parts.push(json_to_zig(v));
1314 }
1315 }
1316 }
1317 }
1318
1319 (setup_lines, parts.join(", "), setup_needs_gpa)
1320}
1321
1322fn render_assertion(
1323 out: &mut String,
1324 assertion: &Assertion,
1325 result_var: &str,
1326 field_resolver: &FieldResolver,
1327 enum_fields: &HashSet<String>,
1328 result_is_option: bool,
1329) {
1330 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1333 if bare_result_is_option {
1334 match assertion.assertion_type.as_str() {
1335 "is_empty" => {
1336 let _ = writeln!(out, " try testing.expect({result_var} == null);");
1337 return;
1338 }
1339 "not_empty" | "not_error" => {
1340 let _ = writeln!(out, " try testing.expect({result_var} != null);");
1341 return;
1342 }
1343 _ => {}
1344 }
1345 }
1346 if let Some(f) = &assertion.field {
1350 if f == "embeddings" && !field_resolver.is_valid_for_result(f) {
1351 match assertion.assertion_type.as_str() {
1352 "count_min" | "count_equals" | "not_empty" | "is_empty" => {
1353 let _ = writeln!(out, " {{");
1354 let _ = writeln!(
1355 out,
1356 " var _eparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {result_var}, .{{}});"
1357 );
1358 let _ = writeln!(out, " defer _eparse.deinit();");
1359 let _ = writeln!(out, " const _embeddings_len = _eparse.value.array.items.len;");
1360 match assertion.assertion_type.as_str() {
1361 "count_min" => {
1362 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1363 let _ = writeln!(out, " try testing.expect(_embeddings_len >= {n});");
1364 }
1365 }
1366 "count_equals" => {
1367 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1368 let _ = writeln!(
1369 out,
1370 " try testing.expectEqual(@as(usize, {n}), _embeddings_len);"
1371 );
1372 }
1373 }
1374 "not_empty" => {
1375 let _ = writeln!(out, " try testing.expect(_embeddings_len > 0);");
1376 }
1377 "is_empty" => {
1378 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _embeddings_len);");
1379 }
1380 _ => {}
1381 }
1382 let _ = writeln!(out, " }}");
1383 return;
1384 }
1385 _ => {}
1386 }
1387 }
1388 }
1389
1390 if let Some(f) = &assertion.field {
1392 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1393 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
1394 return;
1395 }
1396 }
1397
1398 let _field_is_enum = assertion
1400 .field
1401 .as_deref()
1402 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1403
1404 let field_expr = match &assertion.field {
1405 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
1406 _ => result_var.to_string(),
1407 };
1408
1409 match assertion.assertion_type.as_str() {
1410 "equals" => {
1411 if let Some(expected) = &assertion.value {
1412 let zig_val = json_to_zig(expected);
1413 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
1414 }
1415 }
1416 "contains" => {
1417 if let Some(expected) = &assertion.value {
1418 let zig_val = json_to_zig(expected);
1419 let _ = writeln!(
1420 out,
1421 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1422 );
1423 }
1424 }
1425 "contains_all" => {
1426 if let Some(values) = &assertion.values {
1427 for val in values {
1428 let zig_val = json_to_zig(val);
1429 let _ = writeln!(
1430 out,
1431 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1432 );
1433 }
1434 }
1435 }
1436 "not_contains" => {
1437 if let Some(expected) = &assertion.value {
1438 let zig_val = json_to_zig(expected);
1439 let _ = writeln!(
1440 out,
1441 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1442 );
1443 }
1444 }
1445 "not_empty" => {
1446 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
1447 }
1448 "is_empty" => {
1449 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
1450 }
1451 "starts_with" => {
1452 if let Some(expected) = &assertion.value {
1453 let zig_val = json_to_zig(expected);
1454 let _ = writeln!(
1455 out,
1456 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
1457 );
1458 }
1459 }
1460 "ends_with" => {
1461 if let Some(expected) = &assertion.value {
1462 let zig_val = json_to_zig(expected);
1463 let _ = writeln!(
1464 out,
1465 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
1466 );
1467 }
1468 }
1469 "min_length" => {
1470 if let Some(val) = &assertion.value {
1471 if let Some(n) = val.as_u64() {
1472 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1473 }
1474 }
1475 }
1476 "max_length" => {
1477 if let Some(val) = &assertion.value {
1478 if let Some(n) = val.as_u64() {
1479 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
1480 }
1481 }
1482 }
1483 "count_min" => {
1484 if let Some(val) = &assertion.value {
1485 if let Some(n) = val.as_u64() {
1486 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1487 }
1488 }
1489 }
1490 "count_equals" => {
1491 if let Some(val) = &assertion.value {
1492 if let Some(n) = val.as_u64() {
1493 let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
1497 if has_field {
1498 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
1499 } else {
1500 let _ = writeln!(out, " {{");
1501 let _ = writeln!(
1502 out,
1503 " var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
1504 );
1505 let _ = writeln!(out, " defer _cparse.deinit();");
1506 let _ = writeln!(
1507 out,
1508 " try testing.expectEqual({n}, _cparse.value.array.items.len);"
1509 );
1510 let _ = writeln!(out, " }}");
1511 }
1512 }
1513 }
1514 }
1515 "is_true" => {
1516 let _ = writeln!(out, " try testing.expect({field_expr});");
1517 }
1518 "is_false" => {
1519 let _ = writeln!(out, " try testing.expect(!{field_expr});");
1520 }
1521 "not_error" => {
1522 }
1524 "error" => {
1525 }
1527 "greater_than" => {
1528 if let Some(val) = &assertion.value {
1529 let zig_val = json_to_zig(val);
1530 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
1531 }
1532 }
1533 "less_than" => {
1534 if let Some(val) = &assertion.value {
1535 let zig_val = json_to_zig(val);
1536 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
1537 }
1538 }
1539 "greater_than_or_equal" => {
1540 if let Some(val) = &assertion.value {
1541 let zig_val = json_to_zig(val);
1542 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
1543 }
1544 }
1545 "less_than_or_equal" => {
1546 if let Some(val) = &assertion.value {
1547 let zig_val = json_to_zig(val);
1548 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
1549 }
1550 }
1551 "contains_any" => {
1552 if let Some(values) = &assertion.values {
1554 let string_values: Vec<String> = values
1555 .iter()
1556 .filter_map(|v| {
1557 if let serde_json::Value::String(s) = v {
1558 Some(format!(
1559 "std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
1560 escape_zig(s)
1561 ))
1562 } else {
1563 None
1564 }
1565 })
1566 .collect();
1567 if !string_values.is_empty() {
1568 let condition = string_values.join(" or\n ");
1569 let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
1570 }
1571 }
1572 }
1573 "matches_regex" => {
1574 let _ = writeln!(out, " // regex match not yet implemented for Zig");
1575 }
1576 "method_result" => {
1577 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
1578 }
1579 other => {
1580 panic!("Zig e2e generator: unsupported assertion type: {other}");
1581 }
1582 }
1583}
1584
1585fn json_to_zig(value: &serde_json::Value) -> String {
1587 match value {
1588 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
1589 serde_json::Value::Bool(b) => b.to_string(),
1590 serde_json::Value::Number(n) => n.to_string(),
1591 serde_json::Value::Null => "null".to_string(),
1592 serde_json::Value::Array(arr) => {
1593 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
1594 format!("&.{{{}}}", items.join(", "))
1595 }
1596 serde_json::Value::Object(_) => {
1597 let json_str = serde_json::to_string(value).unwrap_or_default();
1598 format!("\"{}\"", escape_zig(&json_str))
1599 }
1600 }
1601}