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 &HashSet::new(),
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!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
244 }
245
246 content.push_str("}\n");
247 content
248}
249
250struct ZigTestClientRenderer;
258
259impl client::TestClientRenderer for ZigTestClientRenderer {
260 fn language_name(&self) -> &'static str {
261 "zig"
262 }
263
264 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
265 if let Some(reason) = skip_reason {
266 let _ = writeln!(out, "test \"{fn_name}\" {{");
267 let _ = writeln!(out, " // {description}");
268 let _ = writeln!(out, " // skipped: {reason}");
269 let _ = writeln!(out, " return error.SkipZigTest;");
270 } else {
271 let _ = writeln!(out, "test \"{fn_name}\" {{");
272 let _ = writeln!(out, " // {description}");
273 }
274 }
275
276 fn render_test_close(&self, out: &mut String) {
277 let _ = writeln!(out, "}}");
278 }
279
280 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
281 let method = ctx.method.to_uppercase();
282 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
283
284 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
285 let _ = writeln!(out, " defer _ = gpa.deinit();");
286 let _ = writeln!(out, " const allocator = gpa.allocator();");
287
288 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
289 let _ = writeln!(
290 out,
291 " const url = try std.fmt.bufPrint(&url_buf, \"{{s}}/fixtures/{fixture_id}\", .{{std.posix.getenv(\"MOCK_SERVER_URL\") orelse \"http://localhost:8080\"}});"
292 );
293
294 if !ctx.headers.is_empty() {
296 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
297 header_pairs.sort_by_key(|(k, _)| k.as_str());
298 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
299 for (k, v) in &header_pairs {
300 let ek = escape_zig(k);
301 let ev = escape_zig(v);
302 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
303 }
304 let _ = writeln!(out, " }};");
305 }
306
307 if let Some(body) = ctx.body {
309 let json_str = serde_json::to_string(body).unwrap_or_default();
310 let escaped = escape_zig(&json_str);
311 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
312 }
313
314 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
315 let has_body = ctx.body.is_some();
316
317 let _ = writeln!(
318 out,
319 " var http_client = std.http.Client{{ .allocator = allocator }};"
320 );
321 let _ = writeln!(out, " defer http_client.deinit();");
322 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
323 let _ = writeln!(out, " defer response_body.deinit();");
324
325 let method_zig = match method.as_str() {
326 "GET" => ".GET",
327 "POST" => ".POST",
328 "PUT" => ".PUT",
329 "DELETE" => ".DELETE",
330 "PATCH" => ".PATCH",
331 "HEAD" => ".HEAD",
332 "OPTIONS" => ".OPTIONS",
333 _ => ".GET",
334 };
335
336 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
337 let _ = writeln!(
338 out,
339 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
340 rv = ctx.response_var,
341 );
342 }
343
344 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
345 let _ = writeln!(
346 out,
347 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
348 );
349 }
350
351 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
352 let ename = escape_zig(&name.to_lowercase());
353 match expected {
354 "<<present>>" => {
355 let _ = writeln!(
356 out,
357 " // assert header '{ename}' is present (header inspection not yet implemented)"
358 );
359 }
360 "<<absent>>" => {
361 let _ = writeln!(
362 out,
363 " // assert header '{ename}' is absent (header inspection not yet implemented)"
364 );
365 }
366 "<<uuid>>" => {
367 let _ = writeln!(
368 out,
369 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
370 );
371 }
372 exact => {
373 let evalue = escape_zig(exact);
374 let _ = writeln!(
375 out,
376 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
377 );
378 }
379 }
380 }
381
382 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
383 let json_str = serde_json::to_string(expected).unwrap_or_default();
384 let escaped = escape_zig(&json_str);
385 let _ = writeln!(
386 out,
387 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
388 );
389 }
390
391 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
392 if let Some(obj) = expected.as_object() {
393 for (key, val) in obj {
394 let ekey = escape_zig(key);
395 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
396 let _ = writeln!(
397 out,
398 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
399 );
400 }
401 }
402 }
403
404 fn render_assert_validation_errors(
405 &self,
406 out: &mut String,
407 _response_var: &str,
408 errors: &[crate::fixture::ValidationErrorExpectation],
409 ) {
410 for ve in errors {
411 let loc = ve.loc.join(".");
412 let escaped_loc = escape_zig(&loc);
413 let escaped_msg = escape_zig(&ve.msg);
414 let _ = writeln!(
415 out,
416 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
417 );
418 }
419 }
420}
421
422fn render_http_test_case(out: &mut String, fixture: &Fixture) {
427 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
428}
429
430#[allow(clippy::too_many_arguments)]
435fn render_test_file(
436 category: &str,
437 fixtures: &[&Fixture],
438 e2e_config: &E2eConfig,
439 function_name: &str,
440 result_var: &str,
441 args: &[crate::config::ArgMapping],
442 field_resolver: &FieldResolver,
443 enum_fields: &HashSet<String>,
444 module_name: &str,
445) -> String {
446 let mut out = String::new();
447 out.push_str(&hash::header(CommentStyle::DoubleSlash));
448 let _ = writeln!(out, "const std = @import(\"std\");");
449 let _ = writeln!(out, "const testing = std.testing;");
450 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
451 let _ = writeln!(out);
452
453 let _ = writeln!(out, "// E2e tests for category: {category}");
454 let _ = writeln!(out);
455
456 for fixture in fixtures {
457 if fixture.http.is_some() {
458 render_http_test_case(&mut out, fixture);
459 } else {
460 render_test_fn(
461 &mut out,
462 fixture,
463 e2e_config,
464 function_name,
465 result_var,
466 args,
467 field_resolver,
468 enum_fields,
469 module_name,
470 );
471 }
472 let _ = writeln!(out);
473 }
474
475 out
476}
477
478#[allow(clippy::too_many_arguments)]
479fn render_test_fn(
480 out: &mut String,
481 fixture: &Fixture,
482 e2e_config: &E2eConfig,
483 _function_name: &str,
484 _result_var: &str,
485 _args: &[crate::config::ArgMapping],
486 field_resolver: &FieldResolver,
487 enum_fields: &HashSet<String>,
488 module_name: &str,
489) {
490 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
492 let lang = "zig";
493 let call_overrides = call_config.overrides.get(lang);
494 let function_name = call_overrides
495 .and_then(|o| o.function.as_ref())
496 .cloned()
497 .unwrap_or_else(|| call_config.function.clone());
498 let result_var = &call_config.result_var;
499 let args = &call_config.args;
500 let is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
501
502 let test_name = fixture.id.to_snake_case();
503 let description = &fixture.description;
504 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
505
506 let (setup_lines, args_str) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
507
508 let _ = writeln!(out, "test \"{test_name}\" {{");
509 let _ = writeln!(out, " // {description}");
510
511 let needs_alloc = !setup_lines.is_empty();
513 if needs_alloc {
514 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
515 let _ = writeln!(out, " defer _ = gpa.deinit();");
516 let _ = writeln!(out, " const allocator = gpa.allocator();");
517 let _ = writeln!(out);
518 }
519
520 for line in &setup_lines {
521 let _ = writeln!(out, " {line}");
522 }
523
524 if expects_error {
525 if is_async {
527 let _ = writeln!(
528 out,
529 " // Note: async functions not yet fully supported; treating as sync"
530 );
531 }
532 let _ = writeln!(
533 out,
534 " const result = {module_name}.{function_name}({args_str}) catch {{"
535 );
536 let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
537 let _ = writeln!(out, " return;");
538 let _ = writeln!(out, " }};");
539 let any_emits_code = fixture
543 .assertions
544 .iter()
545 .filter(|a| a.assertion_type != "error")
546 .any(|a| assertion_emits_code(a, field_resolver));
547 if any_emits_code {
548 let _ = writeln!(out, " // Perform success assertions if any");
549 for assertion in &fixture.assertions {
550 if assertion.assertion_type != "error" {
551 render_assertion(out, assertion, result_var, field_resolver, enum_fields);
552 }
553 }
554 } else {
555 let _ = writeln!(out, " _ = result;");
556 }
557 } else if fixture.assertions.is_empty() {
558 if is_async {
560 let _ = writeln!(
561 out,
562 " // Note: async functions not yet fully supported; treating as sync"
563 );
564 }
565 let _ = writeln!(out, " _ = try {module_name}.{function_name}({args_str});");
566 } else {
567 if is_async {
571 let _ = writeln!(
572 out,
573 " // Note: async functions not yet fully supported; treating as sync"
574 );
575 }
576 let any_emits_code = fixture
577 .assertions
578 .iter()
579 .any(|a| assertion_emits_code(a, field_resolver));
580 if any_emits_code {
581 let _ = writeln!(
582 out,
583 " const {result_var} = try {module_name}.{function_name}({args_str});"
584 );
585 for assertion in &fixture.assertions {
586 render_assertion(out, assertion, result_var, field_resolver, enum_fields);
587 }
588 } else {
589 let _ = writeln!(out, " _ = try {module_name}.{function_name}({args_str});");
590 }
591 }
592
593 let _ = writeln!(out, "}}");
594}
595
596fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
599 if let Some(f) = &assertion.field {
600 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
601 return false;
602 }
603 }
604 matches!(
605 assertion.assertion_type.as_str(),
606 "equals"
607 | "contains"
608 | "contains_all"
609 | "not_contains"
610 | "not_empty"
611 | "is_empty"
612 | "starts_with"
613 | "ends_with"
614 | "min_length"
615 | "max_length"
616 | "count_min"
617 | "count_equals"
618 | "is_true"
619 | "is_false"
620 | "greater_than"
621 | "less_than"
622 | "greater_than_or_equal"
623 | "less_than_or_equal"
624 | "contains_any"
625 )
626}
627
628fn build_args_and_setup(
630 input: &serde_json::Value,
631 args: &[crate::config::ArgMapping],
632 fixture_id: &str,
633 module_name: &str,
634) -> (Vec<String>, String) {
635 if args.is_empty() {
636 return (Vec::new(), String::new());
637 }
638
639 let mut setup_lines: Vec<String> = Vec::new();
640 let mut parts: Vec<String> = Vec::new();
641
642 for arg in args {
643 if arg.arg_type == "mock_url" {
644 setup_lines.push(format!(
645 "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)",
646 arg.name,
647 ));
648 parts.push(arg.name.clone());
649 continue;
650 }
651
652 if arg.name == "config" && arg.arg_type == "json_object" {
664 parts.push(format!(
665 "std.mem.zeroInit({module_name}.ExtractionConfig, .{{ .output_format = {module_name}.OutputFormat{{ .plain = {{}} }} }})"
666 ));
667 continue;
668 }
669
670 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
671 let val = input.get(field);
672 match val {
673 None | Some(serde_json::Value::Null) if arg.optional => {
674 parts.push("null".to_string());
677 }
678 None | Some(serde_json::Value::Null) => {
679 let default_val = match arg.arg_type.as_str() {
680 "string" => "\"\"".to_string(),
681 "int" | "integer" => "0".to_string(),
682 "float" | "number" => "0.0".to_string(),
683 "bool" | "boolean" => "false".to_string(),
684 "json_object" => "\"{}\"".to_string(),
685 _ => "null".to_string(),
686 };
687 parts.push(default_val);
688 }
689 Some(v) => {
690 if arg.arg_type == "json_object" {
695 let json_str = serde_json::to_string(v).unwrap_or_default();
696 parts.push(format!("\"{}\"", escape_zig(&json_str)));
697 } else {
698 parts.push(json_to_zig(v));
699 }
700 }
701 }
702 }
703
704 (setup_lines, parts.join(", "))
705}
706
707fn render_assertion(
708 out: &mut String,
709 assertion: &Assertion,
710 result_var: &str,
711 field_resolver: &FieldResolver,
712 enum_fields: &HashSet<String>,
713) {
714 if let Some(f) = &assertion.field {
716 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
717 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
718 return;
719 }
720 }
721
722 let _field_is_enum = assertion
724 .field
725 .as_deref()
726 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
727
728 let field_expr = match &assertion.field {
729 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
730 _ => result_var.to_string(),
731 };
732
733 match assertion.assertion_type.as_str() {
734 "equals" => {
735 if let Some(expected) = &assertion.value {
736 let zig_val = json_to_zig(expected);
737 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
738 }
739 }
740 "contains" => {
741 if let Some(expected) = &assertion.value {
742 let zig_val = json_to_zig(expected);
743 let _ = writeln!(
744 out,
745 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
746 );
747 }
748 }
749 "contains_all" => {
750 if let Some(values) = &assertion.values {
751 for val in values {
752 let zig_val = json_to_zig(val);
753 let _ = writeln!(
754 out,
755 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
756 );
757 }
758 }
759 }
760 "not_contains" => {
761 if let Some(expected) = &assertion.value {
762 let zig_val = json_to_zig(expected);
763 let _ = writeln!(
764 out,
765 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
766 );
767 }
768 }
769 "not_empty" => {
770 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
771 }
772 "is_empty" => {
773 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
774 }
775 "starts_with" => {
776 if let Some(expected) = &assertion.value {
777 let zig_val = json_to_zig(expected);
778 let _ = writeln!(
779 out,
780 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
781 );
782 }
783 }
784 "ends_with" => {
785 if let Some(expected) = &assertion.value {
786 let zig_val = json_to_zig(expected);
787 let _ = writeln!(
788 out,
789 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
790 );
791 }
792 }
793 "min_length" => {
794 if let Some(val) = &assertion.value {
795 if let Some(n) = val.as_u64() {
796 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
797 }
798 }
799 }
800 "max_length" => {
801 if let Some(val) = &assertion.value {
802 if let Some(n) = val.as_u64() {
803 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
804 }
805 }
806 }
807 "count_min" => {
808 if let Some(val) = &assertion.value {
809 if let Some(n) = val.as_u64() {
810 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
811 }
812 }
813 }
814 "count_equals" => {
815 if let Some(val) = &assertion.value {
816 if let Some(n) = val.as_u64() {
817 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
818 }
819 }
820 }
821 "is_true" => {
822 let _ = writeln!(out, " try testing.expect({field_expr});");
823 }
824 "is_false" => {
825 let _ = writeln!(out, " try testing.expect(!{field_expr});");
826 }
827 "not_error" => {
828 }
830 "error" => {
831 }
833 "greater_than" => {
834 if let Some(val) = &assertion.value {
835 let zig_val = json_to_zig(val);
836 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
837 }
838 }
839 "less_than" => {
840 if let Some(val) = &assertion.value {
841 let zig_val = json_to_zig(val);
842 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
843 }
844 }
845 "greater_than_or_equal" => {
846 if let Some(val) = &assertion.value {
847 let zig_val = json_to_zig(val);
848 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
849 }
850 }
851 "less_than_or_equal" => {
852 if let Some(val) = &assertion.value {
853 let zig_val = json_to_zig(val);
854 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
855 }
856 }
857 "contains_any" => {
858 if let Some(values) = &assertion.values {
859 for val in values {
860 let zig_val = json_to_zig(val);
861 let _ = writeln!(
862 out,
863 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
864 );
865 }
866 }
867 }
868 "matches_regex" => {
869 let _ = writeln!(out, " // regex match not yet implemented for Zig");
870 }
871 "method_result" => {
872 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
873 }
874 other => {
875 panic!("Zig e2e generator: unsupported assertion type: {other}");
876 }
877 }
878}
879
880fn json_to_zig(value: &serde_json::Value) -> String {
882 match value {
883 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
884 serde_json::Value::Bool(b) => b.to_string(),
885 serde_json::Value::Number(n) => n.to_string(),
886 serde_json::Value::Null => "null".to_string(),
887 serde_json::Value::Array(arr) => {
888 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
889 format!("&.{{{}}}", items.join(", "))
890 }
891 serde_json::Value::Object(_) => {
892 let json_str = serde_json::to_string(value).unwrap_or_default();
893 format!("\"{}\"", escape_zig(&json_str))
894 }
895 }
896}