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