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