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;
22
23pub struct ZigE2eCodegen;
25
26impl E2eCodegen for ZigE2eCodegen {
27 fn generate(
28 &self,
29 groups: &[FixtureGroup],
30 e2e_config: &E2eConfig,
31 config: &ResolvedCrateConfig,
32 ) -> Result<Vec<GeneratedFile>> {
33 let lang = self.language_name();
34 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
35
36 let mut files = Vec::new();
37
38 let call = &e2e_config.call;
40 let overrides = call.overrides.get(lang);
41 let _module_path = overrides
42 .and_then(|o| o.module.as_ref())
43 .cloned()
44 .unwrap_or_else(|| call.module.clone());
45 let function_name = overrides
46 .and_then(|o| o.function.as_ref())
47 .cloned()
48 .unwrap_or_else(|| call.function.clone());
49 let result_var = &call.result_var;
50
51 let zig_pkg = e2e_config.resolve_package("zig");
53 let pkg_path = zig_pkg
54 .as_ref()
55 .and_then(|p| p.path.as_ref())
56 .cloned()
57 .unwrap_or_else(|| "../../packages/zig".to_string());
58 let pkg_name = zig_pkg
59 .as_ref()
60 .and_then(|p| p.name.as_ref())
61 .cloned()
62 .unwrap_or_else(|| config.name.to_snake_case());
63
64 files.push(GeneratedFile {
66 path: output_base.join("build.zig.zon"),
67 content: render_build_zig_zon(&pkg_name, &pkg_path, e2e_config.dep_mode),
68 generated_header: false,
69 });
70
71 let module_name = config.zig_module_name();
73
74 let field_resolver = FieldResolver::new(
76 &e2e_config.fields,
77 &e2e_config.fields_optional,
78 &e2e_config.result_fields,
79 &e2e_config.fields_array,
80 &e2e_config.fields_method_calls,
81 );
82
83 let mut test_filenames: Vec<String> = Vec::new();
85 for group in groups {
86 let active: Vec<&Fixture> = group
87 .fixtures
88 .iter()
89 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
90 .collect();
91
92 if active.is_empty() {
93 continue;
94 }
95
96 let filename = format!("{}_test.zig", sanitize_filename(&group.category));
97 test_filenames.push(filename.clone());
98 let content = render_test_file(
99 &group.category,
100 &active,
101 e2e_config,
102 &function_name,
103 result_var,
104 &e2e_config.call.args,
105 &field_resolver,
106 &e2e_config.fields_enum,
107 &module_name,
108 );
109 files.push(GeneratedFile {
110 path: output_base.join("src").join(filename),
111 content,
112 generated_header: true,
113 });
114 }
115
116 files.insert(
118 files
119 .iter()
120 .position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
121 .unwrap_or(1),
122 GeneratedFile {
123 path: output_base.join("build.zig"),
124 content: render_build_zig(&test_filenames),
125 generated_header: false,
126 },
127 );
128
129 Ok(files)
130 }
131
132 fn language_name(&self) -> &'static str {
133 "zig"
134 }
135}
136
137fn render_build_zig_zon(pkg_name: &str, pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
142 let dep_block = match dep_mode {
143 crate::config::DependencyMode::Registry => {
144 format!(
146 r#".{{
147 .url = "https://registry.example.com/{pkg_name}/v0.1.0.tar.gz",
148 .hash = "0000000000000000000000000000000000000000000000000000000000000000",
149 }}"#
150 )
151 }
152 crate::config::DependencyMode::Local => {
153 format!(r#".{{ .path = "{pkg_path}" }}"#)
154 }
155 };
156
157 let min_zig = toolchain::MIN_ZIG_VERSION;
158 let name_bytes: &[u8] = b"e2e_zig";
160 let mut crc: u32 = 0xffff_ffff;
161 for byte in name_bytes {
162 crc ^= *byte as u32;
163 for _ in 0..8 {
164 let mask = (crc & 1).wrapping_neg();
165 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
166 }
167 }
168 let name_crc: u32 = !crc;
169 let mut id: u32 = 0x811c_9dc5;
170 for byte in name_bytes {
171 id ^= *byte as u32;
172 id = id.wrapping_mul(0x0100_0193);
173 }
174 if id == 0 || id == 0xffff_ffff {
175 id = 0x1;
176 }
177 let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
178 format!(
179 r#".{{
180 .name = .e2e_zig,
181 .version = "0.1.0",
182 .fingerprint = 0x{fingerprint:016x},
183 .minimum_zig_version = "{min_zig}",
184 .dependencies = .{{
185 .{pkg_name} = {dep_block},
186 }},
187 .paths = .{{
188 "build.zig",
189 "build.zig.zon",
190 "src",
191 }},
192}}
193"#
194 )
195}
196
197fn render_build_zig(test_filenames: &[String]) -> String {
198 if test_filenames.is_empty() {
199 return r#"const std = @import("std");
200
201pub fn build(b: *std.Build) void {
202 const target = b.standardTargetOptions(.{});
203 const optimize = b.standardOptimizeOption(.{});
204
205 const test_step = b.step("test", "Run tests");
206}
207"#
208 .to_string();
209 }
210
211 let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
212 content.push_str(" const target = b.standardTargetOptions(.{});\n");
213 content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
214 content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n");
215 content.push_str(" const ffi_path = b.option([]const u8, \"ffi_path\", \"Path to directory containing libkreuzberg_ffi\") orelse \"../../target/debug\";\n");
216 content.push_str(" const ffi_include = b.option([]const u8, \"ffi_include_path\", \"Path to directory containing kreuzberg FFI header\") orelse \"../../crates/kreuzberg-ffi/include\";\n\n");
217 content.push_str(" const kreuzberg_module = b.addModule(\"kreuzberg\", .{\n");
218 content.push_str(" .root_source_file = b.path(\"../../packages/zig/src/kreuzberg.zig\"),\n");
219 content.push_str(" .target = target,\n");
220 content.push_str(" .optimize = optimize,\n");
221 content.push_str(" });\n");
222 content.push_str(" kreuzberg_module.addLibraryPath(.{ .cwd_relative = ffi_path });\n");
223 content.push_str(" kreuzberg_module.addIncludePath(.{ .cwd_relative = ffi_include });\n");
224 content.push_str(" kreuzberg_module.linkSystemLibrary(\"kreuzberg_ffi\", .{});\n\n");
225
226 for filename in test_filenames {
227 let test_name = filename.trim_end_matches("_test.zig");
229 content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
230 content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
231 content.push_str(" .target = target,\n");
232 content.push_str(" .optimize = optimize,\n");
233 content.push_str(" });\n");
234 content.push_str(&format!(
235 " {test_name}_module.addImport(\"kreuzberg\", kreuzberg_module);\n"
236 ));
237 content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
238 content.push_str(&format!(" .root_module = {test_name}_module,\n"));
239 content.push_str(" });\n");
240 content.push_str(&format!(
241 " const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
242 ));
243 content.push_str(&format!(
244 " {test_name}_run.setCwd(b.path(\"../../test_documents\"));\n"
245 ));
246 content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
247 }
248
249 content.push_str("}\n");
250 content
251}
252
253struct ZigTestClientRenderer;
261
262impl client::TestClientRenderer for ZigTestClientRenderer {
263 fn language_name(&self) -> &'static str {
264 "zig"
265 }
266
267 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
268 if let Some(reason) = skip_reason {
269 let _ = writeln!(out, "test \"{fn_name}\" {{");
270 let _ = writeln!(out, " // {description}");
271 let _ = writeln!(out, " // skipped: {reason}");
272 let _ = writeln!(out, " return error.SkipZigTest;");
273 } else {
274 let _ = writeln!(out, "test \"{fn_name}\" {{");
275 let _ = writeln!(out, " // {description}");
276 }
277 }
278
279 fn render_test_close(&self, out: &mut String) {
280 let _ = writeln!(out, "}}");
281 }
282
283 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
284 let method = ctx.method.to_uppercase();
285 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
286
287 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
288 let _ = writeln!(out, " defer _ = gpa.deinit();");
289 let _ = writeln!(out, " const allocator = gpa.allocator();");
290
291 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
292 let _ = writeln!(
293 out,
294 " const url = try std.fmt.bufPrint(&url_buf, \"{{s}}/fixtures/{fixture_id}\", .{{std.posix.getenv(\"MOCK_SERVER_URL\") orelse \"http://localhost:8080\"}});"
295 );
296
297 if !ctx.headers.is_empty() {
299 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
300 header_pairs.sort_by_key(|(k, _)| k.as_str());
301 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
302 for (k, v) in &header_pairs {
303 let ek = escape_zig(k);
304 let ev = escape_zig(v);
305 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
306 }
307 let _ = writeln!(out, " }};");
308 }
309
310 if let Some(body) = ctx.body {
312 let json_str = serde_json::to_string(body).unwrap_or_default();
313 let escaped = escape_zig(&json_str);
314 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
315 }
316
317 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
318 let has_body = ctx.body.is_some();
319
320 let _ = writeln!(
321 out,
322 " var http_client = std.http.Client{{ .allocator = allocator }};"
323 );
324 let _ = writeln!(out, " defer http_client.deinit();");
325 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
326 let _ = writeln!(out, " defer response_body.deinit();");
327
328 let method_zig = match method.as_str() {
329 "GET" => ".GET",
330 "POST" => ".POST",
331 "PUT" => ".PUT",
332 "DELETE" => ".DELETE",
333 "PATCH" => ".PATCH",
334 "HEAD" => ".HEAD",
335 "OPTIONS" => ".OPTIONS",
336 _ => ".GET",
337 };
338
339 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
340 let _ = writeln!(
341 out,
342 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
343 rv = ctx.response_var,
344 );
345 }
346
347 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
348 let _ = writeln!(
349 out,
350 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
351 );
352 }
353
354 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
355 let ename = escape_zig(&name.to_lowercase());
356 match expected {
357 "<<present>>" => {
358 let _ = writeln!(
359 out,
360 " // assert header '{ename}' is present (header inspection not yet implemented)"
361 );
362 }
363 "<<absent>>" => {
364 let _ = writeln!(
365 out,
366 " // assert header '{ename}' is absent (header inspection not yet implemented)"
367 );
368 }
369 "<<uuid>>" => {
370 let _ = writeln!(
371 out,
372 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
373 );
374 }
375 exact => {
376 let evalue = escape_zig(exact);
377 let _ = writeln!(
378 out,
379 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
380 );
381 }
382 }
383 }
384
385 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
386 let json_str = serde_json::to_string(expected).unwrap_or_default();
387 let escaped = escape_zig(&json_str);
388 let _ = writeln!(
389 out,
390 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
391 );
392 }
393
394 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
395 if let Some(obj) = expected.as_object() {
396 for (key, val) in obj {
397 let ekey = escape_zig(key);
398 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
399 let _ = writeln!(
400 out,
401 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
402 );
403 }
404 }
405 }
406
407 fn render_assert_validation_errors(
408 &self,
409 out: &mut String,
410 _response_var: &str,
411 errors: &[crate::fixture::ValidationErrorExpectation],
412 ) {
413 for ve in errors {
414 let loc = ve.loc.join(".");
415 let escaped_loc = escape_zig(&loc);
416 let escaped_msg = escape_zig(&ve.msg);
417 let _ = writeln!(
418 out,
419 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
420 );
421 }
422 }
423}
424
425fn render_http_test_case(out: &mut String, fixture: &Fixture) {
430 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
431}
432
433#[allow(clippy::too_many_arguments)]
438fn render_test_file(
439 category: &str,
440 fixtures: &[&Fixture],
441 e2e_config: &E2eConfig,
442 function_name: &str,
443 result_var: &str,
444 args: &[crate::config::ArgMapping],
445 field_resolver: &FieldResolver,
446 enum_fields: &HashSet<String>,
447 module_name: &str,
448) -> String {
449 let mut out = String::new();
450 out.push_str(&hash::header(CommentStyle::DoubleSlash));
451 let _ = writeln!(out, "const std = @import(\"std\");");
452 let _ = writeln!(out, "const testing = std.testing;");
453 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
454 let _ = writeln!(out);
455
456 let _ = writeln!(out, "// E2e tests for category: {category}");
457 let _ = writeln!(out);
458
459 for fixture in fixtures {
460 if fixture.http.is_some() {
461 render_http_test_case(&mut out, fixture);
462 } else {
463 render_test_fn(
464 &mut out,
465 fixture,
466 e2e_config,
467 function_name,
468 result_var,
469 args,
470 field_resolver,
471 enum_fields,
472 module_name,
473 );
474 }
475 let _ = writeln!(out);
476 }
477
478 out
479}
480
481#[allow(clippy::too_many_arguments)]
482fn render_test_fn(
483 out: &mut String,
484 fixture: &Fixture,
485 e2e_config: &E2eConfig,
486 _function_name: &str,
487 _result_var: &str,
488 _args: &[crate::config::ArgMapping],
489 field_resolver: &FieldResolver,
490 enum_fields: &HashSet<String>,
491 module_name: &str,
492) {
493 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
495 let lang = "zig";
496 let call_overrides = call_config.overrides.get(lang);
497 let function_name = call_overrides
498 .and_then(|o| o.function.as_ref())
499 .cloned()
500 .unwrap_or_else(|| call_config.function.clone());
501 let result_var = &call_config.result_var;
502 let args = &call_config.args;
503 let is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
504 let result_is_json_struct = call_overrides.is_some_and(|o| o.result_is_json_struct);
508
509 let test_name = fixture.id.to_snake_case();
510 let description = &fixture.description;
511 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
512
513 let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
514
515 let any_happy_emits_code = fixture
518 .assertions
519 .iter()
520 .any(|a| assertion_emits_code(a, field_resolver));
521 let any_non_error_emits_code = fixture
522 .assertions
523 .iter()
524 .filter(|a| a.assertion_type != "error")
525 .any(|a| assertion_emits_code(a, field_resolver));
526
527 let _ = writeln!(out, "test \"{test_name}\" {{");
528 let _ = writeln!(out, " // {description}");
529
530 let needs_gpa = setup_needs_gpa
536 || (result_is_json_struct && !expects_error && any_happy_emits_code)
537 || (result_is_json_struct && expects_error && any_non_error_emits_code);
538 if needs_gpa {
539 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
540 let _ = writeln!(out, " defer _ = gpa.deinit();");
541 let _ = writeln!(out, " const allocator = gpa.allocator();");
542 let _ = writeln!(out);
543 }
544
545 for line in &setup_lines {
546 let _ = writeln!(out, " {line}");
547 }
548
549 if expects_error {
550 if is_async {
552 let _ = writeln!(
553 out,
554 " // Note: async functions not yet fully supported; treating as sync"
555 );
556 }
557 if result_is_json_struct {
558 let _ = writeln!(
559 out,
560 " const _result_json = {module_name}.{function_name}({args_str}) catch {{"
561 );
562 } else {
563 let _ = writeln!(
564 out,
565 " const result = {module_name}.{function_name}({args_str}) catch {{"
566 );
567 }
568 let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
569 let _ = writeln!(out, " return;");
570 let _ = writeln!(out, " }};");
571 let any_emits_code = fixture
575 .assertions
576 .iter()
577 .filter(|a| a.assertion_type != "error")
578 .any(|a| assertion_emits_code(a, field_resolver));
579 if result_is_json_struct && any_emits_code {
580 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
581 let _ = writeln!(
582 out,
583 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
584 );
585 let _ = writeln!(out, " defer _parsed.deinit();");
586 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
587 let _ = writeln!(out, " // Perform success assertions if any");
588 for assertion in &fixture.assertions {
589 if assertion.assertion_type != "error" {
590 render_json_assertion(out, assertion, result_var);
591 }
592 }
593 } else if result_is_json_struct {
594 let _ = writeln!(out, " _ = _result_json;");
595 } else if any_emits_code {
596 let _ = writeln!(out, " // Perform success assertions if any");
597 for assertion in &fixture.assertions {
598 if assertion.assertion_type != "error" {
599 render_assertion(out, assertion, result_var, field_resolver, enum_fields);
600 }
601 }
602 } else {
603 let _ = writeln!(out, " _ = result;");
604 }
605 } else if fixture.assertions.is_empty() {
606 if is_async {
608 let _ = writeln!(
609 out,
610 " // Note: async functions not yet fully supported; treating as sync"
611 );
612 }
613 if result_is_json_struct {
614 let _ = writeln!(
615 out,
616 " const _result_json = try {module_name}.{function_name}({args_str});"
617 );
618 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
619 } else {
620 let _ = writeln!(out, " _ = try {module_name}.{function_name}({args_str});");
621 }
622 } else {
623 if is_async {
627 let _ = writeln!(
628 out,
629 " // Note: async functions not yet fully supported; treating as sync"
630 );
631 }
632 let any_emits_code = fixture
633 .assertions
634 .iter()
635 .any(|a| assertion_emits_code(a, field_resolver));
636 if result_is_json_struct {
637 let _ = writeln!(
639 out,
640 " const _result_json = try {module_name}.{function_name}({args_str});"
641 );
642 let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
643 if any_emits_code {
644 let _ = writeln!(
645 out,
646 " var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
647 );
648 let _ = writeln!(out, " defer _parsed.deinit();");
649 let _ = writeln!(out, " const {result_var} = &_parsed.value;");
650 for assertion in &fixture.assertions {
651 render_json_assertion(out, assertion, result_var);
652 }
653 }
654 } else if any_emits_code {
655 let _ = writeln!(
656 out,
657 " const {result_var} = try {module_name}.{function_name}({args_str});"
658 );
659 for assertion in &fixture.assertions {
660 render_assertion(out, assertion, result_var, field_resolver, enum_fields);
661 }
662 } else {
663 let _ = writeln!(out, " _ = try {module_name}.{function_name}({args_str});");
664 }
665 }
666
667 let _ = writeln!(out, "}}");
668}
669
670const FORMAT_METADATA_VARIANTS: &[&str] = &[
688 "pdf",
689 "docx",
690 "excel",
691 "email",
692 "pptx",
693 "archive",
694 "image",
695 "xml",
696 "text",
697 "html",
698 "ocr",
699 "csv",
700 "bibtex",
701 "citation",
702 "fiction_book",
703 "dbf",
704 "jats",
705 "epub",
706 "pst",
707 "code",
708];
709
710fn json_path_expr(result_var: &str, field_path: &str) -> String {
711 let segments: Vec<&str> = field_path.split('.').collect();
712 let mut expr = result_var.to_string();
713 let mut prev_seg: Option<&str> = None;
714 for seg in &segments {
715 if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
720 prev_seg = Some(seg);
721 continue;
722 }
723 expr = format!("{expr}.object.get(\"{seg}\").?");
724 prev_seg = Some(seg);
725 }
726 expr
727}
728
729fn render_json_assertion(out: &mut String, assertion: &Assertion, result_var: &str) {
734 let field_path = assertion.field.as_deref().unwrap_or("").trim();
735
736 let field_expr = if field_path.is_empty() {
738 result_var.to_string()
739 } else {
740 json_path_expr(result_var, field_path)
741 };
742
743 match assertion.assertion_type.as_str() {
744 "equals" => {
745 if let Some(expected) = &assertion.value {
746 match expected {
747 serde_json::Value::String(s) => {
748 let zig_val = format!("\"{}\"", escape_zig(s));
749 let _ = writeln!(
750 out,
751 " try testing.expectEqualStrings({zig_val}, {field_expr}.string);"
752 );
753 }
754 serde_json::Value::Bool(b) => {
755 let _ = writeln!(out, " try testing.expectEqual({b}, {field_expr}.bool);");
756 }
757 serde_json::Value::Number(n) => {
758 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.integer);");
759 }
760 _ => {}
761 }
762 }
763 }
764 "contains" => {
765 if let Some(serde_json::Value::String(s)) = &assertion.value {
766 let zig_val = format!("\"{}\"", escape_zig(s));
767 let _ = writeln!(out, " {{");
771 let _ = writeln!(out, " const _jv = {field_expr};");
772 let _ = writeln!(
773 out,
774 " const _js = if (_jv == .string) _jv.string else try std.json.Stringify.valueAlloc(std.heap.c_allocator, _jv, .{{}});"
775 );
776 let _ = writeln!(out, " defer if (_jv != .string) std.heap.c_allocator.free(_js);");
777 let _ = writeln!(
778 out,
779 " try testing.expect(std.mem.indexOf(u8, _js, {zig_val}) != null);"
780 );
781 let _ = writeln!(out, " }}");
782 }
783 }
784 "contains_all" => {
785 if let Some(values) = &assertion.values {
788 for (idx, val) in values.iter().enumerate() {
789 if let serde_json::Value::String(s) = val {
790 let zig_val = format!("\"{}\"", escape_zig(s));
791 let jv = format!("_jva{idx}");
792 let js = format!("_jsa{idx}");
793 let _ = writeln!(out, " {{");
794 let _ = writeln!(out, " const {jv} = {field_expr};");
795 let _ = writeln!(
796 out,
797 " const {js} = if ({jv} == .string) {jv}.string else try std.json.Stringify.valueAlloc(std.heap.c_allocator, {jv}, .{{}});"
798 );
799 let _ = writeln!(
800 out,
801 " defer if ({jv} != .string) std.heap.c_allocator.free({js});"
802 );
803 let _ = writeln!(
804 out,
805 " try testing.expect(std.mem.indexOf(u8, {js}, {zig_val}) != null);"
806 );
807 let _ = writeln!(out, " }}");
808 }
809 }
810 }
811 }
812 "not_contains" => {
813 if let Some(serde_json::Value::String(s)) = &assertion.value {
814 let zig_val = format!("\"{}\"", escape_zig(s));
815 let _ = writeln!(out, " {{");
816 let _ = writeln!(out, " const _jvnc = {field_expr};");
817 let _ = writeln!(
818 out,
819 " const _jsnc = if (_jvnc == .string) _jvnc.string else try std.json.Stringify.valueAlloc(std.heap.c_allocator, _jvnc, .{{}});"
820 );
821 let _ = writeln!(
822 out,
823 " defer if (_jvnc != .string) std.heap.c_allocator.free(_jsnc);"
824 );
825 let _ = writeln!(
826 out,
827 " try testing.expect(std.mem.indexOf(u8, _jsnc, {zig_val}) == null);"
828 );
829 let _ = writeln!(out, " }}");
830 }
831 }
832 "not_empty" => {
833 let _ = writeln!(out, " try testing.expect({field_expr} != .null);");
837 }
838 "is_empty" => {
839 let _ = writeln!(out, " try testing.expectEqual(.null, {field_expr});");
840 }
841 "min_length" => {
842 if let Some(val) = &assertion.value {
843 if let Some(n) = val.as_u64() {
844 let _ = writeln!(out, " try testing.expect({field_expr}.string.len >= {n});");
845 }
846 }
847 }
848 "count_min" => {
849 if let Some(val) = &assertion.value {
850 if let Some(n) = val.as_u64() {
851 let _ = writeln!(out, " try testing.expect({field_expr}.array.items.len >= {n});");
852 }
853 }
854 }
855 "count_equals" => {
856 if let Some(val) = &assertion.value {
857 if let Some(n) = val.as_u64() {
858 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.array.items.len);");
859 }
860 }
861 }
862 "greater_than" => {
863 if let Some(val) = &assertion.value {
864 let n = json_to_zig(val);
865 let _ = writeln!(out, " try testing.expect({field_expr}.integer > {n});");
866 }
867 }
868 "less_than" => {
869 if let Some(val) = &assertion.value {
870 let n = json_to_zig(val);
871 let _ = writeln!(out, " try testing.expect({field_expr}.integer < {n});");
872 }
873 }
874 "greater_than_or_equal" => {
875 if let Some(val) = &assertion.value {
876 let n = json_to_zig(val);
877 let _ = writeln!(out, " try testing.expect({field_expr}.integer >= {n});");
878 }
879 }
880 "less_than_or_equal" => {
881 if let Some(val) = &assertion.value {
882 let n = json_to_zig(val);
883 let _ = writeln!(out, " try testing.expect({field_expr}.integer <= {n});");
884 }
885 }
886 "is_true" => {
887 let _ = writeln!(out, " try testing.expect({field_expr}.bool);");
888 }
889 "is_false" => {
890 let _ = writeln!(out, " try testing.expect(!{field_expr}.bool);");
891 }
892 "not_error" | "error" => {
893 }
895 "starts_with" => {
896 if let Some(serde_json::Value::String(s)) = &assertion.value {
897 let zig_val = format!("\"{}\"", escape_zig(s));
898 let _ = writeln!(
899 out,
900 " try testing.expect(std.mem.startsWith(u8, {field_expr}.string, {zig_val}));"
901 );
902 }
903 }
904 "ends_with" => {
905 if let Some(serde_json::Value::String(s)) = &assertion.value {
906 let zig_val = format!("\"{}\"", escape_zig(s));
907 let _ = writeln!(
908 out,
909 " try testing.expect(std.mem.endsWith(u8, {field_expr}.string, {zig_val}));"
910 );
911 }
912 }
913 "contains_any" => {
914 if let Some(values) = &assertion.values {
916 let string_values: Vec<String> = values
917 .iter()
918 .filter_map(|v| {
919 if let serde_json::Value::String(s) = v {
920 Some(format!(
921 "std.mem.indexOf(u8, {field_expr}.string, \"{}\") != null",
922 escape_zig(s)
923 ))
924 } else {
925 None
926 }
927 })
928 .collect();
929 if !string_values.is_empty() {
930 let condition = string_values.join(" or\n ");
931 let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
932 }
933 }
934 }
935 other => {
936 let _ = writeln!(out, " // json assertion '{other}' not implemented for Zig");
937 }
938 }
939}
940
941fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
944 if let Some(f) = &assertion.field {
945 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
946 return false;
947 }
948 }
949 matches!(
950 assertion.assertion_type.as_str(),
951 "equals"
952 | "contains"
953 | "contains_all"
954 | "not_contains"
955 | "not_empty"
956 | "is_empty"
957 | "starts_with"
958 | "ends_with"
959 | "min_length"
960 | "max_length"
961 | "count_min"
962 | "count_equals"
963 | "is_true"
964 | "is_false"
965 | "greater_than"
966 | "less_than"
967 | "greater_than_or_equal"
968 | "less_than_or_equal"
969 | "contains_any"
970 )
971}
972
973fn build_args_and_setup(
978 input: &serde_json::Value,
979 args: &[crate::config::ArgMapping],
980 fixture_id: &str,
981 _module_name: &str,
982) -> (Vec<String>, String, bool) {
983 if args.is_empty() {
984 return (Vec::new(), String::new(), false);
985 }
986
987 let mut setup_lines: Vec<String> = Vec::new();
988 let mut parts: Vec<String> = Vec::new();
989 let mut setup_needs_gpa = false;
990
991 for arg in args {
992 if arg.arg_type == "mock_url" {
993 setup_lines.push(format!(
994 "var {} = try allocator.alloc(u8, std.fmt.bufPrint(undefined, \"{{s}}/fixtures/{fixture_id}\", .{{std.posix.getenv(\"MOCK_SERVER_URL\") orelse \"http://localhost:8080\"}}) catch 0)",
995 arg.name,
996 ));
997 parts.push(arg.name.clone());
998 setup_needs_gpa = true;
999 continue;
1000 }
1001
1002 if arg.name == "config" && arg.arg_type == "json_object" {
1009 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1010 let json_str = match input.get(field) {
1011 Some(serde_json::Value::Null) | None => "{}".to_string(),
1012 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1013 };
1014 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1015 continue;
1016 }
1017
1018 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1019 let val = input.get(field);
1020 match val {
1021 None | Some(serde_json::Value::Null) if arg.optional => {
1022 parts.push("null".to_string());
1025 }
1026 None | Some(serde_json::Value::Null) => {
1027 let default_val = match arg.arg_type.as_str() {
1028 "string" => "\"\"".to_string(),
1029 "int" | "integer" => "0".to_string(),
1030 "float" | "number" => "0.0".to_string(),
1031 "bool" | "boolean" => "false".to_string(),
1032 "json_object" => "\"{}\"".to_string(),
1033 _ => "null".to_string(),
1034 };
1035 parts.push(default_val);
1036 }
1037 Some(v) => {
1038 if arg.arg_type == "json_object" {
1043 let json_str = serde_json::to_string(v).unwrap_or_default();
1044 parts.push(format!("\"{}\"", escape_zig(&json_str)));
1045 } else if arg.arg_type == "bytes" {
1046 if let serde_json::Value::String(path) = v {
1051 let var_name = format!("{}_bytes", arg.name);
1052 let epath = escape_zig(path);
1053 setup_lines.push(format!(
1054 "const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
1055 ));
1056 setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
1057 parts.push(var_name);
1058 } else {
1059 parts.push(json_to_zig(v));
1060 }
1061 } else {
1062 parts.push(json_to_zig(v));
1063 }
1064 }
1065 }
1066 }
1067
1068 (setup_lines, parts.join(", "), setup_needs_gpa)
1069}
1070
1071fn render_assertion(
1072 out: &mut String,
1073 assertion: &Assertion,
1074 result_var: &str,
1075 field_resolver: &FieldResolver,
1076 enum_fields: &HashSet<String>,
1077) {
1078 if let Some(f) = &assertion.field {
1080 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1081 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
1082 return;
1083 }
1084 }
1085
1086 let _field_is_enum = assertion
1088 .field
1089 .as_deref()
1090 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1091
1092 let field_expr = match &assertion.field {
1093 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
1094 _ => result_var.to_string(),
1095 };
1096
1097 match assertion.assertion_type.as_str() {
1098 "equals" => {
1099 if let Some(expected) = &assertion.value {
1100 let zig_val = json_to_zig(expected);
1101 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
1102 }
1103 }
1104 "contains" => {
1105 if let Some(expected) = &assertion.value {
1106 let zig_val = json_to_zig(expected);
1107 let _ = writeln!(
1108 out,
1109 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1110 );
1111 }
1112 }
1113 "contains_all" => {
1114 if let Some(values) = &assertion.values {
1115 for val in values {
1116 let zig_val = json_to_zig(val);
1117 let _ = writeln!(
1118 out,
1119 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1120 );
1121 }
1122 }
1123 }
1124 "not_contains" => {
1125 if let Some(expected) = &assertion.value {
1126 let zig_val = json_to_zig(expected);
1127 let _ = writeln!(
1128 out,
1129 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1130 );
1131 }
1132 }
1133 "not_empty" => {
1134 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
1135 }
1136 "is_empty" => {
1137 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
1138 }
1139 "starts_with" => {
1140 if let Some(expected) = &assertion.value {
1141 let zig_val = json_to_zig(expected);
1142 let _ = writeln!(
1143 out,
1144 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
1145 );
1146 }
1147 }
1148 "ends_with" => {
1149 if let Some(expected) = &assertion.value {
1150 let zig_val = json_to_zig(expected);
1151 let _ = writeln!(
1152 out,
1153 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
1154 );
1155 }
1156 }
1157 "min_length" => {
1158 if let Some(val) = &assertion.value {
1159 if let Some(n) = val.as_u64() {
1160 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1161 }
1162 }
1163 }
1164 "max_length" => {
1165 if let Some(val) = &assertion.value {
1166 if let Some(n) = val.as_u64() {
1167 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
1168 }
1169 }
1170 }
1171 "count_min" => {
1172 if let Some(val) = &assertion.value {
1173 if let Some(n) = val.as_u64() {
1174 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
1175 }
1176 }
1177 }
1178 "count_equals" => {
1179 if let Some(val) = &assertion.value {
1180 if let Some(n) = val.as_u64() {
1181 let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
1185 if has_field {
1186 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
1187 } else {
1188 let _ = writeln!(out, " {{");
1189 let _ = writeln!(
1190 out,
1191 " var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
1192 );
1193 let _ = writeln!(out, " defer _cparse.deinit();");
1194 let _ = writeln!(
1195 out,
1196 " try testing.expectEqual({n}, _cparse.value.array.items.len);"
1197 );
1198 let _ = writeln!(out, " }}");
1199 }
1200 }
1201 }
1202 }
1203 "is_true" => {
1204 let _ = writeln!(out, " try testing.expect({field_expr});");
1205 }
1206 "is_false" => {
1207 let _ = writeln!(out, " try testing.expect(!{field_expr});");
1208 }
1209 "not_error" => {
1210 }
1212 "error" => {
1213 }
1215 "greater_than" => {
1216 if let Some(val) = &assertion.value {
1217 let zig_val = json_to_zig(val);
1218 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
1219 }
1220 }
1221 "less_than" => {
1222 if let Some(val) = &assertion.value {
1223 let zig_val = json_to_zig(val);
1224 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
1225 }
1226 }
1227 "greater_than_or_equal" => {
1228 if let Some(val) = &assertion.value {
1229 let zig_val = json_to_zig(val);
1230 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
1231 }
1232 }
1233 "less_than_or_equal" => {
1234 if let Some(val) = &assertion.value {
1235 let zig_val = json_to_zig(val);
1236 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
1237 }
1238 }
1239 "contains_any" => {
1240 if let Some(values) = &assertion.values {
1242 let string_values: Vec<String> = values
1243 .iter()
1244 .filter_map(|v| {
1245 if let serde_json::Value::String(s) = v {
1246 Some(format!(
1247 "std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
1248 escape_zig(s)
1249 ))
1250 } else {
1251 None
1252 }
1253 })
1254 .collect();
1255 if !string_values.is_empty() {
1256 let condition = string_values.join(" or\n ");
1257 let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
1258 }
1259 }
1260 }
1261 "matches_regex" => {
1262 let _ = writeln!(out, " // regex match not yet implemented for Zig");
1263 }
1264 "method_result" => {
1265 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
1266 }
1267 other => {
1268 panic!("Zig e2e generator: unsupported assertion type: {other}");
1269 }
1270 }
1271}
1272
1273fn json_to_zig(value: &serde_json::Value) -> String {
1275 match value {
1276 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
1277 serde_json::Value::Bool(b) => b.to_string(),
1278 serde_json::Value::Number(n) => n.to_string(),
1279 serde_json::Value::Null => "null".to_string(),
1280 serde_json::Value::Array(arr) => {
1281 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
1282 format!("&.{{{}}}", items.join(", "))
1283 }
1284 serde_json::Value::Object(_) => {
1285 let json_str = serde_json::to_string(value).unwrap_or_default();
1286 format!("\"{}\"", escape_zig(&json_str))
1287 }
1288 }
1289}