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::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
76 let field_resolver = FieldResolver::new(
78 &e2e_config.fields,
79 &e2e_config.fields_optional,
80 &e2e_config.result_fields,
81 &e2e_config.fields_array,
82 &e2e_config.fields_method_calls,
83 );
84
85 let mut test_filenames: Vec<String> = Vec::new();
87 for group in groups {
88 let active: Vec<&Fixture> = group
89 .fixtures
90 .iter()
91 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
92 .collect();
93
94 if active.is_empty() {
95 continue;
96 }
97
98 let filename = format!("{}_test.zig", sanitize_filename(&group.category));
99 test_filenames.push(filename.clone());
100 let content = render_test_file(
101 &group.category,
102 &active,
103 e2e_config,
104 &function_name,
105 result_var,
106 &e2e_config.call.args,
107 &field_resolver,
108 &e2e_config.fields_enum,
109 &module_name,
110 );
111 files.push(GeneratedFile {
112 path: output_base.join("src").join(filename),
113 content,
114 generated_header: true,
115 });
116 }
117
118 files.insert(
120 files
121 .iter()
122 .position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
123 .unwrap_or(1),
124 GeneratedFile {
125 path: output_base.join("build.zig"),
126 content: render_build_zig(
127 &test_filenames,
128 &pkg_name,
129 &module_name,
130 &config.ffi_lib_name(),
131 &config.ffi_crate_path(),
132 &e2e_config.test_documents_relative_from(0),
133 ),
134 generated_header: false,
135 },
136 );
137
138 Ok(files)
139 }
140
141 fn language_name(&self) -> &'static str {
142 "zig"
143 }
144}
145
146fn render_build_zig_zon(pkg_name: &str, pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
151 let dep_block = match dep_mode {
152 crate::config::DependencyMode::Registry => {
153 format!(
155 r#".{{
156 .url = "https://registry.example.com/{pkg_name}/v0.1.0.tar.gz",
157 .hash = "0000000000000000000000000000000000000000000000000000000000000000",
158 }}"#
159 )
160 }
161 crate::config::DependencyMode::Local => {
162 format!(r#".{{ .path = "{pkg_path}" }}"#)
163 }
164 };
165
166 let min_zig = toolchain::MIN_ZIG_VERSION;
167 let name_bytes: &[u8] = b"e2e_zig";
169 let mut crc: u32 = 0xffff_ffff;
170 for byte in name_bytes {
171 crc ^= *byte as u32;
172 for _ in 0..8 {
173 let mask = (crc & 1).wrapping_neg();
174 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
175 }
176 }
177 let name_crc: u32 = !crc;
178 let mut id: u32 = 0x811c_9dc5;
179 for byte in name_bytes {
180 id ^= *byte as u32;
181 id = id.wrapping_mul(0x0100_0193);
182 }
183 if id == 0 || id == 0xffff_ffff {
184 id = 0x1;
185 }
186 let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
187 format!(
188 r#".{{
189 .name = .e2e_zig,
190 .version = "0.1.0",
191 .fingerprint = 0x{fingerprint:016x},
192 .minimum_zig_version = "{min_zig}",
193 .dependencies = .{{
194 .{pkg_name} = {dep_block},
195 }},
196 .paths = .{{
197 "build.zig",
198 "build.zig.zon",
199 "src",
200 }},
201}}
202"#
203 )
204}
205
206fn render_build_zig(
207 test_filenames: &[String],
208 pkg_name: &str,
209 module_name: &str,
210 ffi_lib_name: &str,
211 ffi_crate_path: &str,
212 test_documents_path: &str,
213) -> String {
214 if test_filenames.is_empty() {
215 return r#"const std = @import("std");
216
217pub fn build(b: *std.Build) void {
218 const target = b.standardTargetOptions(.{});
219 const optimize = b.standardOptimizeOption(.{});
220
221 const test_step = b.step("test", "Run tests");
222}
223"#
224 .to_string();
225 }
226
227 let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
237 content.push_str(" const target = b.standardTargetOptions(.{});\n");
238 content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
239 content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n");
240 let _ = writeln!(
241 content,
242 " const ffi_path = b.option([]const u8, \"ffi_path\", \"Path to directory containing lib{ffi_lib_name}\") orelse \"../../target/debug\";"
243 );
244 let _ = writeln!(
245 content,
246 " const ffi_include = b.option([]const u8, \"ffi_include_path\", \"Path to directory containing FFI header\") orelse \"{ffi_crate_path}/include\";"
247 );
248 let _ = writeln!(content);
249 let _ = writeln!(
250 content,
251 " const {module_name}_module = b.addModule(\"{module_name}\", .{{"
252 );
253 let _ = writeln!(
254 content,
255 " .root_source_file = b.path(\"../../packages/zig/src/{pkg_name}.zig\"),"
256 );
257 content.push_str(" .target = target,\n");
258 content.push_str(" .optimize = optimize,\n");
259 content.push_str(" .link_libc = true,\n");
263 content.push_str(" });\n");
264 let _ = writeln!(
265 content,
266 " {module_name}_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});"
267 );
268 let _ = writeln!(
269 content,
270 " {module_name}_module.addIncludePath(.{{ .cwd_relative = ffi_include }});"
271 );
272 let _ = writeln!(
273 content,
274 " {module_name}_module.linkSystemLibrary(\"{ffi_lib_name}\", .{{}});"
275 );
276 let _ = writeln!(content);
277
278 for filename in test_filenames {
279 let test_name = filename.trim_end_matches("_test.zig");
281 content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
282 content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
283 content.push_str(" .target = target,\n");
284 content.push_str(" .optimize = optimize,\n");
285 content.push_str(" .link_libc = true,\n");
289 content.push_str(" });\n");
290 content.push_str(&format!(
291 " {test_name}_module.addImport(\"{module_name}\", {module_name}_module);\n"
292 ));
293 content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
294 content.push_str(&format!(" .root_module = {test_name}_module,\n"));
295 content.push_str(" });\n");
296 content.push_str(&format!(
297 " const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
298 ));
299 content.push_str(&format!(
300 " {test_name}_run.setCwd(b.path(\"{test_documents_path}\"));\n"
301 ));
302 content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
303 }
304
305 content.push_str("}\n");
306 content
307}
308
309struct ZigTestClientRenderer;
317
318impl client::TestClientRenderer for ZigTestClientRenderer {
319 fn language_name(&self) -> &'static str {
320 "zig"
321 }
322
323 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
324 if let Some(reason) = skip_reason {
325 let _ = writeln!(out, "test \"{fn_name}\" {{");
326 let _ = writeln!(out, " // {description}");
327 let _ = writeln!(out, " // skipped: {reason}");
328 let _ = writeln!(out, " return error.SkipZigTest;");
329 } else {
330 let _ = writeln!(out, "test \"{fn_name}\" {{");
331 let _ = writeln!(out, " // {description}");
332 }
333 }
334
335 fn render_test_close(&self, out: &mut String) {
336 let _ = writeln!(out, "}}");
337 }
338
339 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
340 let method = ctx.method.to_uppercase();
341 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
342
343 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
344 let _ = writeln!(out, " defer _ = gpa.deinit();");
345 let _ = writeln!(out, " const allocator = gpa.allocator();");
346
347 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
348 let _ = writeln!(
349 out,
350 " 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\"}});"
351 );
352
353 if !ctx.headers.is_empty() {
355 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
356 header_pairs.sort_by_key(|(k, _)| k.as_str());
357 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
358 for (k, v) in &header_pairs {
359 let ek = escape_zig(k);
360 let ev = escape_zig(v);
361 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
362 }
363 let _ = writeln!(out, " }};");
364 }
365
366 if let Some(body) = ctx.body {
368 let json_str = serde_json::to_string(body).unwrap_or_default();
369 let escaped = escape_zig(&json_str);
370 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
371 }
372
373 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
374 let has_body = ctx.body.is_some();
375
376 let _ = writeln!(
377 out,
378 " var http_client = std.http.Client{{ .allocator = allocator }};"
379 );
380 let _ = writeln!(out, " defer http_client.deinit();");
381 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
382 let _ = writeln!(out, " defer response_body.deinit();");
383
384 let method_zig = match method.as_str() {
385 "GET" => ".GET",
386 "POST" => ".POST",
387 "PUT" => ".PUT",
388 "DELETE" => ".DELETE",
389 "PATCH" => ".PATCH",
390 "HEAD" => ".HEAD",
391 "OPTIONS" => ".OPTIONS",
392 _ => ".GET",
393 };
394
395 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
396 let _ = writeln!(
397 out,
398 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
399 rv = ctx.response_var,
400 );
401 }
402
403 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
404 let _ = writeln!(
405 out,
406 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
407 );
408 }
409
410 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
411 let ename = escape_zig(&name.to_lowercase());
412 match expected {
413 "<<present>>" => {
414 let _ = writeln!(
415 out,
416 " // assert header '{ename}' is present (header inspection not yet implemented)"
417 );
418 }
419 "<<absent>>" => {
420 let _ = writeln!(
421 out,
422 " // assert header '{ename}' is absent (header inspection not yet implemented)"
423 );
424 }
425 "<<uuid>>" => {
426 let _ = writeln!(
427 out,
428 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
429 );
430 }
431 exact => {
432 let evalue = escape_zig(exact);
433 let _ = writeln!(
434 out,
435 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
436 );
437 }
438 }
439 }
440
441 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
442 let json_str = serde_json::to_string(expected).unwrap_or_default();
443 let escaped = escape_zig(&json_str);
444 let _ = writeln!(
445 out,
446 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
447 );
448 }
449
450 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
451 if let Some(obj) = expected.as_object() {
452 for (key, val) in obj {
453 let ekey = escape_zig(key);
454 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
455 let _ = writeln!(
456 out,
457 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
458 );
459 }
460 }
461 }
462
463 fn render_assert_validation_errors(
464 &self,
465 out: &mut String,
466 _response_var: &str,
467 errors: &[crate::fixture::ValidationErrorExpectation],
468 ) {
469 for ve in errors {
470 let loc = ve.loc.join(".");
471 let escaped_loc = escape_zig(&loc);
472 let escaped_msg = escape_zig(&ve.msg);
473 let _ = writeln!(
474 out,
475 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
476 );
477 }
478 }
479}
480
481fn render_http_test_case(out: &mut String, fixture: &Fixture) {
486 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
487}
488
489#[allow(clippy::too_many_arguments)]
494fn render_test_file(
495 category: &str,
496 fixtures: &[&Fixture],
497 e2e_config: &E2eConfig,
498 function_name: &str,
499 result_var: &str,
500 args: &[crate::config::ArgMapping],
501 field_resolver: &FieldResolver,
502 enum_fields: &HashSet<String>,
503 module_name: &str,
504) -> String {
505 let mut out = String::new();
506 out.push_str(&hash::header(CommentStyle::DoubleSlash));
507 let _ = writeln!(out, "const std = @import(\"std\");");
508 let _ = writeln!(out, "const testing = std.testing;");
509 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
510 let _ = writeln!(out);
511
512 let _ = writeln!(out, "// E2e tests for category: {category}");
513 let _ = writeln!(out);
514
515 for fixture in fixtures {
516 if fixture.http.is_some() {
517 render_http_test_case(&mut out, fixture);
518 } else {
519 render_test_fn(
520 &mut out,
521 fixture,
522 e2e_config,
523 function_name,
524 result_var,
525 args,
526 field_resolver,
527 enum_fields,
528 module_name,
529 );
530 }
531 let _ = writeln!(out);
532 }
533
534 out
535}
536
537#[allow(clippy::too_many_arguments)]
538fn render_test_fn(
539 out: &mut String,
540 fixture: &Fixture,
541 e2e_config: &E2eConfig,
542 _function_name: &str,
543 _result_var: &str,
544 _args: &[crate::config::ArgMapping],
545 field_resolver: &FieldResolver,
546 enum_fields: &HashSet<String>,
547 module_name: &str,
548) {
549 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
551 let lang = "zig";
552 let call_overrides = call_config.overrides.get(lang);
553 let function_name = call_overrides
554 .and_then(|o| o.function.as_ref())
555 .cloned()
556 .unwrap_or_else(|| call_config.function.clone());
557 let result_var = &call_config.result_var;
558 let args = &call_config.args;
559 let client_factory = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
564 e2e_config
565 .call
566 .overrides
567 .get(lang)
568 .and_then(|o| o.client_factory.as_deref())
569 });
570
571 let call_result_is_bytes = call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
588 let result_is_json_struct =
589 !call_result_is_bytes && (call_overrides.is_some_and(|o| o.result_is_json_struct) || client_factory.is_some());
590
591 let result_is_option = call_overrides.is_some_and(|o| o.result_is_option) || call_config.result_is_option;
596
597 let test_name = fixture.id.to_snake_case();
598 let description = &fixture.description;
599 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
600
601 let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
602 let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
608 let args_str = if extra_args.is_empty() {
609 args_str
610 } else if args_str.is_empty() {
611 extra_args.join(", ")
612 } else {
613 format!("{args_str}, {}", extra_args.join(", "))
614 };
615
616 let any_happy_emits_code = fixture
619 .assertions
620 .iter()
621 .any(|a| assertion_emits_code(a, field_resolver));
622 let any_non_error_emits_code = fixture
623 .assertions
624 .iter()
625 .filter(|a| a.assertion_type != "error")
626 .any(|a| assertion_emits_code(a, field_resolver));
627
628 let has_streaming_virtual_assertions = fixture.assertions.iter().any(|a| {
630 a.field
631 .as_ref()
632 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
633 });
634 let is_stream_fn = function_name.contains("stream");
635 let uses_streaming_virtual_path =
636 result_is_json_struct && has_streaming_virtual_assertions && is_stream_fn && client_factory.is_some();
637 let streaming_path_has_non_streaming = uses_streaming_virtual_path
639 && fixture.assertions.iter().any(|a| {
640 !a.field
641 .as_ref()
642 .is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
643 && !matches!(a.assertion_type.as_str(), "not_error" | "error")
644 && a.field
645 .as_ref()
646 .is_some_and(|f| !f.is_empty() && field_resolver.is_valid_for_result(f))
647 });
648
649 let _ = writeln!(out, "test \"{test_name}\" {{");
650 let _ = writeln!(out, " // {description}");
651
652 let needs_gpa = setup_needs_gpa
660 || streaming_path_has_non_streaming
661 || (!uses_streaming_virtual_path && result_is_json_struct && !expects_error && any_happy_emits_code)
662 || (!uses_streaming_virtual_path && result_is_json_struct && expects_error && any_non_error_emits_code);
663 if needs_gpa {
664 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
665 let _ = writeln!(out, " defer _ = gpa.deinit();");
666 let _ = writeln!(out, " const allocator = gpa.allocator();");
667 let _ = writeln!(out);
668 }
669
670 for line in &setup_lines {
671 let _ = writeln!(out, " {line}");
672 }
673
674 let call_prefix = if let Some(factory) = client_factory {
679 let fixture_id = &fixture.id;
680 let _ = writeln!(
681 out,
682 " 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);"
683 );
684 let _ = writeln!(out, " defer std.heap.c_allocator.free(_mock_url);");
685 let _ = writeln!(
686 out,
687 " var _client = try {module_name}.{factory}(\"test-key\", _mock_url, null, null, null);"
688 );
689 let _ = writeln!(out, " defer _client.free();");
690 "_client".to_string()
691 } else {
692 module_name.to_string()
693 };
694
695 if expects_error {
696 if result_is_json_struct {
700 let _ = writeln!(
701 out,
702 " const _result_json = {call_prefix}.{function_name}({args_str}) catch {{"
703 );
704 } else {
705 let _ = writeln!(
706 out,
707 " const result = {call_prefix}.{function_name}({args_str}) catch {{"
708 );
709 }
710 let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
711 let _ = writeln!(out, " return;");
712 let _ = writeln!(out, " }};");
713 let any_emits_code = fixture
717 .assertions
718 .iter()
719 .filter(|a| a.assertion_type != "error")
720 .any(|a| assertion_emits_code(a, field_resolver));
721 if result_is_json_struct && any_emits_code {
722 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
723 let _ = writeln!(
724 out,
725 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
726 );
727 let _ = writeln!(out, " defer _parsed.deinit();");
728 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
729 let _ = writeln!(out, " // Perform success assertions if any");
730 for assertion in &fixture.assertions {
731 if assertion.assertion_type != "error" {
732 render_json_assertion(out, assertion, result_var, field_resolver);
733 }
734 }
735 } else if result_is_json_struct {
736 let _ = writeln!(out, " _ = _result_json;");
737 } else if any_emits_code {
738 let _ = writeln!(out, " // Perform success assertions if any");
739 for assertion in &fixture.assertions {
740 if assertion.assertion_type != "error" {
741 render_assertion(
742 out,
743 assertion,
744 result_var,
745 field_resolver,
746 enum_fields,
747 result_is_option,
748 );
749 }
750 }
751 } else {
752 let _ = writeln!(out, " _ = result;");
753 }
754 } else if fixture.assertions.is_empty() {
755 if result_is_json_struct {
757 let _ = writeln!(
758 out,
759 " const _result_json = try {call_prefix}.{function_name}({args_str});"
760 );
761 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
762 } else {
763 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
764 }
765 } else {
766 let any_emits_code = fixture
770 .assertions
771 .iter()
772 .any(|a| assertion_emits_code(a, field_resolver));
773 if call_result_is_bytes && client_factory.is_some() {
774 let _ = writeln!(
777 out,
778 " const _result_json = try {call_prefix}.{function_name}({args_str});"
779 );
780 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
781 let has_bytes_assertions = fixture
782 .assertions
783 .iter()
784 .any(|a| matches!(a.assertion_type.as_str(), "not_empty" | "is_empty"));
785 if has_bytes_assertions {
786 for assertion in &fixture.assertions {
787 match assertion.assertion_type.as_str() {
788 "not_empty" => {
789 let _ = writeln!(out, " try testing.expect(_result_json.len > 0);");
790 }
791 "is_empty" => {
792 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _result_json.len);");
793 }
794 "not_error" | "error" => {}
795 _ => {
796 let atype = &assertion.assertion_type;
797 let _ = writeln!(
798 out,
799 " // bytes result: assertion '{atype}' not implemented for zig bytes"
800 );
801 }
802 }
803 }
804 }
805 } else if result_is_json_struct {
806 if uses_streaming_virtual_path {
810 let _ = writeln!(
813 out,
814 " const _req_z = try std.heap.c_allocator.dupeZ(u8, {args_str});"
815 );
816 let _ = writeln!(out, " defer std.heap.c_allocator.free(_req_z);");
817 let _ = writeln!(
818 out,
819 " const _req_handle = {module_name}.c.literllm_chat_completion_request_from_json(_req_z.ptr);"
820 );
821 let _ = writeln!(
822 out,
823 " defer {module_name}.c.literllm_chat_completion_request_free(_req_handle);"
824 );
825 let _ = writeln!(
826 out,
827 " const _stream_handle = {module_name}.c.literllm_default_client_chat_stream_start(@as(*{module_name}.c.LITERLLMDefaultClient, @ptrCast(_client._handle)), _req_handle);"
828 );
829 let _ = writeln!(out, " if (_stream_handle == null) return error.StreamStartFailed;");
830 let _ = writeln!(
831 out,
832 " defer {module_name}.c.literllm_default_client_chat_stream_free(_stream_handle);"
833 );
834 if let Some(snip) = StreamingFieldResolver::collect_snippet("zig", "_stream_handle", "chunks") {
836 out.push_str(" ");
837 out.push_str(&snip);
838 out.push('\n');
839 }
840 if streaming_path_has_non_streaming {
843 let _ = writeln!(
844 out,
845 " const _result_json = if (chunks.items.len > 0) chunks.items[chunks.items.len - 1] else &[_]u8{{}};"
846 );
847 let _ = writeln!(
848 out,
849 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
850 );
851 let _ = writeln!(out, " defer _parsed.deinit();");
852 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
853 }
854 for assertion in &fixture.assertions {
855 render_json_assertion(out, assertion, result_var, field_resolver);
856 }
857 } else {
858 let _ = writeln!(
860 out,
861 " const _result_json = try {call_prefix}.{function_name}({args_str});"
862 );
863 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
864 if any_emits_code {
865 let _ = writeln!(
866 out,
867 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
868 );
869 let _ = writeln!(out, " defer _parsed.deinit();");
870 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
871 for assertion in &fixture.assertions {
872 render_json_assertion(out, assertion, result_var, field_resolver);
873 }
874 }
875 }
876 } else if any_emits_code {
877 let _ = writeln!(
878 out,
879 " const {result_var} = try {call_prefix}.{function_name}({args_str});"
880 );
881 for assertion in &fixture.assertions {
882 render_assertion(
883 out,
884 assertion,
885 result_var,
886 field_resolver,
887 enum_fields,
888 result_is_option,
889 );
890 }
891 } else {
892 let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
893 }
894 }
895
896 let _ = writeln!(out, "}}");
897}
898
899const FORMAT_METADATA_VARIANTS: &[&str] = &[
917 "pdf",
918 "docx",
919 "excel",
920 "email",
921 "pptx",
922 "archive",
923 "image",
924 "xml",
925 "text",
926 "html",
927 "ocr",
928 "csv",
929 "bibtex",
930 "citation",
931 "fiction_book",
932 "dbf",
933 "jats",
934 "epub",
935 "pst",
936 "code",
937];
938
939fn json_path_expr(result_var: &str, field_path: &str) -> String {
940 let segments: Vec<&str> = field_path.split('.').collect();
941 let mut expr = result_var.to_string();
942 let mut prev_seg: Option<&str> = None;
943 for seg in &segments {
944 if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
949 prev_seg = Some(seg);
950 continue;
951 }
952 if let Some(key) = seg.strip_suffix("[]") {
956 expr = format!("{expr}.object.get(\"{key}\").?.array.items[0]");
957 } else if let Some(bracket_pos) = seg.find('[') {
958 if let Some(end_pos) = seg.find(']') {
959 if end_pos > bracket_pos + 1 && end_pos == seg.len() - 1 {
960 let key = &seg[..bracket_pos];
961 let idx = &seg[bracket_pos + 1..end_pos];
962 if idx.chars().all(|c| c.is_ascii_digit()) {
963 expr = format!("{expr}.object.get(\"{key}\").?.array.items[{idx}]");
964 prev_seg = Some(seg);
965 continue;
966 }
967 }
968 }
969 expr = format!("{expr}.object.get(\"{seg}\").?");
970 } else {
971 expr = format!("{expr}.object.get(\"{seg}\").?");
972 }
973 prev_seg = Some(seg);
974 }
975 expr
976}
977
978fn render_json_assertion(out: &mut String, assertion: &Assertion, result_var: &str, field_resolver: &FieldResolver) {
983 if let Some(f) = &assertion.field {
985 if !f.is_empty() && is_streaming_virtual_field(f) {
986 if let Some(expr) = StreamingFieldResolver::accessor(f, "zig", "chunks") {
987 match assertion.assertion_type.as_str() {
988 "count_min" => {
989 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
990 let _ = writeln!(out, " try testing.expect({expr}.len >= {n});");
991 }
992 }
993 "count_equals" => {
994 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
995 let _ = writeln!(out, " try testing.expectEqual(@as(usize, {n}), {expr}.len);");
996 }
997 }
998 "equals" => {
999 if let Some(serde_json::Value::String(s)) = &assertion.value {
1000 let escaped = escape_zig(s);
1001 let _ = writeln!(out, " try testing.expectEqualStrings(\"{escaped}\", {expr});");
1002 } else if let Some(v) = &assertion.value {
1003 let zig_val = json_to_zig(v);
1004 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {expr});");
1005 }
1006 }
1007 "not_empty" => {
1008 let _ = writeln!(out, " try testing.expect({expr}.len > 0);");
1009 }
1010 "is_true" => {
1011 let _ = writeln!(out, " try testing.expect({expr});");
1012 }
1013 "is_false" => {
1014 let _ = writeln!(out, " try testing.expect(!{expr});");
1015 }
1016 _ => {
1017 let atype = &assertion.assertion_type;
1018 let _ = writeln!(
1019 out,
1020 " // streaming virtual field '{f}' assertion '{atype}' not implemented for zig"
1021 );
1022 }
1023 }
1024 }
1025 return;
1026 }
1027 }
1028
1029 if let Some(f) = &assertion.field {
1036 if f == "embeddings" && !field_resolver.has_explicit_field("embeddings") {
1037 match assertion.assertion_type.as_str() {
1038 "count_min" => {
1039 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1040 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len >= {n});");
1041 }
1042 return;
1043 }
1044 "count_equals" => {
1045 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1046 let _ = writeln!(
1047 out,
1048 " try testing.expectEqual(@as(usize, {n}), {result_var}.array.items.len);"
1049 );
1050 }
1051 return;
1052 }
1053 "not_empty" => {
1054 let _ = writeln!(out, " try testing.expect({result_var}.array.items.len > 0);");
1055 return;
1056 }
1057 "is_empty" => {
1058 let _ = writeln!(
1059 out,
1060 " try testing.expectEqual(@as(usize, 0), {result_var}.array.items.len);"
1061 );
1062 return;
1063 }
1064 _ => {}
1065 }
1066 }
1067 }
1068
1069 if let Some(f) = &assertion.field {
1071 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1072 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1073 return;
1074 }
1075 }
1076 if matches!(assertion.assertion_type.as_str(), "not_error" | "error") {
1078 return;
1079 }
1080
1081 let raw_field_path = assertion.field.as_deref().unwrap_or("").trim();
1082 let field_path = if raw_field_path.is_empty() {
1083 raw_field_path.to_string()
1084 } else {
1085 field_resolver.resolve(raw_field_path).to_string()
1086 };
1087 let field_path = field_path.trim();
1088
1089 let (field_path_for_expr, is_length_access) = if let Some(parent) = field_path.strip_suffix(".length") {
1091 (parent, true)
1092 } else {
1093 (field_path, false)
1094 };
1095
1096 let field_expr = if field_path_for_expr.is_empty() {
1097 result_var.to_string()
1098 } else {
1099 json_path_expr(result_var, field_path_for_expr)
1100 };
1101
1102 let zig_val = match &assertion.value {
1104 Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_zig(s)),
1105 _ => String::new(),
1106 };
1107 let is_string_val = matches!(&assertion.value, Some(serde_json::Value::String(_)));
1108 let is_bool_val = matches!(&assertion.value, Some(serde_json::Value::Bool(_)));
1109 let bool_val = match &assertion.value {
1110 Some(serde_json::Value::Bool(b)) if *b => "true",
1111 _ => "false",
1112 };
1113 let is_null_val = matches!(&assertion.value, Some(serde_json::Value::Null));
1114 let n = assertion.value.as_ref().map(json_to_zig).unwrap_or_default();
1115 let has_n = assertion.value.as_ref().is_some_and(|v| v.is_number() || v.is_u64());
1116 let is_float_val = matches!(&assertion.value, Some(serde_json::Value::Number(n)) if !n.is_i64() && !n.is_u64());
1121 let values_list: Vec<String> = assertion
1122 .values
1123 .as_deref()
1124 .unwrap_or_default()
1125 .iter()
1126 .filter_map(|v| {
1127 if let serde_json::Value::String(s) = v {
1128 Some(format!("\"{}\"", escape_zig(s)))
1129 } else {
1130 None
1131 }
1132 })
1133 .collect();
1134
1135 let rendered = crate::template_env::render(
1136 "zig/json_assertion.jinja",
1137 minijinja::context! {
1138 assertion_type => assertion.assertion_type.as_str(),
1139 field_expr => field_expr,
1140 is_length_access => is_length_access,
1141 zig_val => zig_val,
1142 is_string_val => is_string_val,
1143 is_bool_val => is_bool_val,
1144 bool_val => bool_val,
1145 is_null_val => is_null_val,
1146 n => n,
1147 has_n => has_n,
1148 is_float_val => is_float_val,
1149 values_list => values_list,
1150 },
1151 );
1152 out.push_str(&rendered);
1153}
1154
1155fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
1158 if let Some(f) = &assertion.field {
1159 if !f.is_empty() && is_streaming_virtual_field(f) {
1160 } else if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1163 return false;
1164 }
1165 }
1166 matches!(
1167 assertion.assertion_type.as_str(),
1168 "equals"
1169 | "contains"
1170 | "contains_all"
1171 | "not_contains"
1172 | "not_empty"
1173 | "is_empty"
1174 | "starts_with"
1175 | "ends_with"
1176 | "min_length"
1177 | "max_length"
1178 | "count_min"
1179 | "count_equals"
1180 | "is_true"
1181 | "is_false"
1182 | "greater_than"
1183 | "less_than"
1184 | "greater_than_or_equal"
1185 | "less_than_or_equal"
1186 | "contains_any"
1187 )
1188}
1189
1190fn build_args_and_setup(
1195 input: &serde_json::Value,
1196 args: &[crate::config::ArgMapping],
1197 fixture_id: &str,
1198 _module_name: &str,
1199) -> (Vec<String>, String, bool) {
1200 if args.is_empty() {
1201 return (Vec::new(), String::new(), false);
1202 }
1203
1204 let mut setup_lines: Vec<String> = Vec::new();
1205 let mut parts: Vec<String> = Vec::new();
1206 let mut setup_needs_gpa = false;
1207
1208 for arg in args {
1209 if arg.arg_type == "mock_url" {
1210 let name = arg.name.clone();
1211 let id_upper = fixture_id.to_uppercase();
1212 setup_lines.push(format!(
1213 "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\"}});"
1214 ));
1215 setup_lines.push(format!("defer allocator.free({name});"));
1216 parts.push(name);
1217 setup_needs_gpa = true;
1218 continue;
1219 }
1220
1221 if arg.arg_type == "handle" {
1224 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1225 let json_str = match input.get(field) {
1226 Some(serde_json::Value::Null) | None => "null".to_string(),
1227 Some(v) => format!("\"{}\"", escape_zig(&serde_json::to_string(v).unwrap_or_default())),
1228 };
1229 parts.push(json_str);
1230 continue;
1231 }
1232
1233 if arg.name == "config" && arg.arg_type == "json_object" {
1240 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1241 let json_str = match input.get(field) {
1242 Some(serde_json::Value::Null) | None => "{}".to_string(),
1243 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1244 };
1245 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1246 continue;
1247 }
1248
1249 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1250 let val = if field.is_empty() || field == "input" {
1256 Some(input)
1257 } else {
1258 input.get(field)
1259 };
1260 match val {
1261 None | Some(serde_json::Value::Null) if arg.optional => {
1262 parts.push("null".to_string());
1265 }
1266 None | Some(serde_json::Value::Null) => {
1267 let default_val = match arg.arg_type.as_str() {
1268 "string" => "\"\"".to_string(),
1269 "int" | "integer" => "0".to_string(),
1270 "float" | "number" => "0.0".to_string(),
1271 "bool" | "boolean" => "false".to_string(),
1272 "json_object" => "\"{}\"".to_string(),
1273 _ => "null".to_string(),
1274 };
1275 parts.push(default_val);
1276 }
1277 Some(v) => {
1278 if arg.arg_type == "json_object" {
1283 let json_str = serde_json::to_string(v).unwrap_or_default();
1284 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1285 } else if arg.arg_type == "bytes" {
1286 if let serde_json::Value::String(path) = v {
1291 let var_name = format!("{}_bytes", arg.name);
1292 let epath = escape_zig(path);
1293 setup_lines.push(format!(
1294 "const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
1295 ));
1296 setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
1297 parts.push(var_name);
1298 } else {
1299 parts.push(json_to_zig(v));
1300 }
1301 } else {
1302 parts.push(json_to_zig(v));
1303 }
1304 }
1305 }
1306 }
1307
1308 (setup_lines, parts.join(", "), setup_needs_gpa)
1309}
1310
1311fn render_assertion(
1312 out: &mut String,
1313 assertion: &Assertion,
1314 result_var: &str,
1315 field_resolver: &FieldResolver,
1316 enum_fields: &HashSet<String>,
1317 result_is_option: bool,
1318) {
1319 let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1322 if bare_result_is_option {
1323 match assertion.assertion_type.as_str() {
1324 "is_empty" => {
1325 let _ = writeln!(out, " try testing.expect({result_var} == null);");
1326 return;
1327 }
1328 "not_empty" | "not_error" => {
1329 let _ = writeln!(out, " try testing.expect({result_var} != null);");
1330 return;
1331 }
1332 _ => {}
1333 }
1334 }
1335 if let Some(f) = &assertion.field {
1339 if f == "embeddings" && !field_resolver.is_valid_for_result(f) {
1340 match assertion.assertion_type.as_str() {
1341 "count_min" | "count_equals" | "not_empty" | "is_empty" => {
1342 let _ = writeln!(out, " {{");
1343 let _ = writeln!(
1344 out,
1345 " var _eparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {result_var}, .{{}});"
1346 );
1347 let _ = writeln!(out, " defer _eparse.deinit();");
1348 let _ = writeln!(out, " const _embeddings_len = _eparse.value.array.items.len;");
1349 match assertion.assertion_type.as_str() {
1350 "count_min" => {
1351 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1352 let _ = writeln!(out, " try testing.expect(_embeddings_len >= {n});");
1353 }
1354 }
1355 "count_equals" => {
1356 if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1357 let _ = writeln!(
1358 out,
1359 " try testing.expectEqual(@as(usize, {n}), _embeddings_len);"
1360 );
1361 }
1362 }
1363 "not_empty" => {
1364 let _ = writeln!(out, " try testing.expect(_embeddings_len > 0);");
1365 }
1366 "is_empty" => {
1367 let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _embeddings_len);");
1368 }
1369 _ => {}
1370 }
1371 let _ = writeln!(out, " }}");
1372 return;
1373 }
1374 _ => {}
1375 }
1376 }
1377 }
1378
1379 if let Some(f) = &assertion.field {
1381 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1382 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
1383 return;
1384 }
1385 }
1386
1387 let _field_is_enum = assertion
1389 .field
1390 .as_deref()
1391 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1392
1393 let field_expr = match &assertion.field {
1394 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
1395 _ => result_var.to_string(),
1396 };
1397
1398 match assertion.assertion_type.as_str() {
1399 "equals" => {
1400 if let Some(expected) = &assertion.value {
1401 let zig_val = json_to_zig(expected);
1402 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
1403 }
1404 }
1405 "contains" => {
1406 if let Some(expected) = &assertion.value {
1407 let zig_val = json_to_zig(expected);
1408 let _ = writeln!(
1409 out,
1410 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1411 );
1412 }
1413 }
1414 "contains_all" => {
1415 if let Some(values) = &assertion.values {
1416 for val in values {
1417 let zig_val = json_to_zig(val);
1418 let _ = writeln!(
1419 out,
1420 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1421 );
1422 }
1423 }
1424 }
1425 "not_contains" => {
1426 if let Some(expected) = &assertion.value {
1427 let zig_val = json_to_zig(expected);
1428 let _ = writeln!(
1429 out,
1430 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1431 );
1432 }
1433 }
1434 "not_empty" => {
1435 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
1436 }
1437 "is_empty" => {
1438 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
1439 }
1440 "starts_with" => {
1441 if let Some(expected) = &assertion.value {
1442 let zig_val = json_to_zig(expected);
1443 let _ = writeln!(
1444 out,
1445 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
1446 );
1447 }
1448 }
1449 "ends_with" => {
1450 if let Some(expected) = &assertion.value {
1451 let zig_val = json_to_zig(expected);
1452 let _ = writeln!(
1453 out,
1454 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
1455 );
1456 }
1457 }
1458 "min_length" => {
1459 if let Some(val) = &assertion.value {
1460 if let Some(n) = val.as_u64() {
1461 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1462 }
1463 }
1464 }
1465 "max_length" => {
1466 if let Some(val) = &assertion.value {
1467 if let Some(n) = val.as_u64() {
1468 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
1469 }
1470 }
1471 }
1472 "count_min" => {
1473 if let Some(val) = &assertion.value {
1474 if let Some(n) = val.as_u64() {
1475 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1476 }
1477 }
1478 }
1479 "count_equals" => {
1480 if let Some(val) = &assertion.value {
1481 if let Some(n) = val.as_u64() {
1482 let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
1486 if has_field {
1487 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
1488 } else {
1489 let _ = writeln!(out, " {{");
1490 let _ = writeln!(
1491 out,
1492 " var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
1493 );
1494 let _ = writeln!(out, " defer _cparse.deinit();");
1495 let _ = writeln!(
1496 out,
1497 " try testing.expectEqual({n}, _cparse.value.array.items.len);"
1498 );
1499 let _ = writeln!(out, " }}");
1500 }
1501 }
1502 }
1503 }
1504 "is_true" => {
1505 let _ = writeln!(out, " try testing.expect({field_expr});");
1506 }
1507 "is_false" => {
1508 let _ = writeln!(out, " try testing.expect(!{field_expr});");
1509 }
1510 "not_error" => {
1511 }
1513 "error" => {
1514 }
1516 "greater_than" => {
1517 if let Some(val) = &assertion.value {
1518 let zig_val = json_to_zig(val);
1519 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
1520 }
1521 }
1522 "less_than" => {
1523 if let Some(val) = &assertion.value {
1524 let zig_val = json_to_zig(val);
1525 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
1526 }
1527 }
1528 "greater_than_or_equal" => {
1529 if let Some(val) = &assertion.value {
1530 let zig_val = json_to_zig(val);
1531 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
1532 }
1533 }
1534 "less_than_or_equal" => {
1535 if let Some(val) = &assertion.value {
1536 let zig_val = json_to_zig(val);
1537 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
1538 }
1539 }
1540 "contains_any" => {
1541 if let Some(values) = &assertion.values {
1543 let string_values: Vec<String> = values
1544 .iter()
1545 .filter_map(|v| {
1546 if let serde_json::Value::String(s) = v {
1547 Some(format!(
1548 "std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
1549 escape_zig(s)
1550 ))
1551 } else {
1552 None
1553 }
1554 })
1555 .collect();
1556 if !string_values.is_empty() {
1557 let condition = string_values.join(" or\n ");
1558 let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
1559 }
1560 }
1561 }
1562 "matches_regex" => {
1563 let _ = writeln!(out, " // regex match not yet implemented for Zig");
1564 }
1565 "method_result" => {
1566 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
1567 }
1568 other => {
1569 panic!("Zig e2e generator: unsupported assertion type: {other}");
1570 }
1571 }
1572}
1573
1574fn json_to_zig(value: &serde_json::Value) -> String {
1576 match value {
1577 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
1578 serde_json::Value::Bool(b) => b.to_string(),
1579 serde_json::Value::Number(n) => n.to_string(),
1580 serde_json::Value::Null => "null".to_string(),
1581 serde_json::Value::Array(arr) => {
1582 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
1583 format!("&.{{{}}}", items.join(", "))
1584 }
1585 serde_json::Value::Object(_) => {
1586 let json_str = serde_json::to_string(value).unwrap_or_default();
1587 format!("\"{}\"", escape_zig(&json_str))
1588 }
1589 }
1590}