Skip to main content

alef_e2e/codegen/
zig.rs

1//! Zig e2e test generator using std.testing.
2//!
3//! Generates `packages/zig/src/<crate>_test.zig` files from JSON fixtures,
4//! driven entirely by `E2eConfig` and `CallConfig`.
5
6use 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
23/// Zig e2e code generator.
24pub 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        // Resolve call config with overrides.
40        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        // Resolve package config.
53        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        // Generate build.zig.zon (Zig package manifest).
66        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        // Get the module name for imports.
73        let module_name = config.zig_module_name();
74
75        // Generate build.zig - collect test file names first.
76        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        // Generate test files per category and collect their names.
85        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        // Generate build.zig with collected test files.
118        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
138// ---------------------------------------------------------------------------
139// Rendering
140// ---------------------------------------------------------------------------
141
142fn 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            // For registry mode, use a dummy hash (in real Zig, hash must be computed).
146            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    // Zig 0.16+ requires a fingerprint of the form (crc32_ieee(name) << 32) | id.
160    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        // Convert filename like "basic_test.zig" to a test name
229        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
254// ---------------------------------------------------------------------------
255// HTTP server test rendering — shared-driver integration
256// ---------------------------------------------------------------------------
257
258/// Renderer that emits Zig `test "..." { ... }` blocks targeting a mock server
259/// via `std.http.Client`. Satisfies [`client::TestClientRenderer`] so the shared
260/// [`client::http_call::render_http_test`] driver drives the call sequence.
261struct 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        // Headers
299        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        // Body
312        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
426/// Render a Zig `test "..." { ... }` block for an HTTP server fixture.
427///
428/// Delegates to the shared [`client::http_call::render_http_test`] driver via
429/// [`ZigTestClientRenderer`].
430fn render_http_test_case(out: &mut String, fixture: &Fixture) {
431    client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
432}
433
434// ---------------------------------------------------------------------------
435// Function-call test rendering
436// ---------------------------------------------------------------------------
437
438#[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    // Resolve per-fixture call config.
495    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    // When `result_is_json_struct = true`, the Zig function returns `[]u8` JSON.
506    // The test parses it with `std.json.parseFromSlice(std.json.Value, ...)` and
507    // traverses the dynamic JSON object for field assertions.
508    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    // Pre-compute whether any assertion will emit code that references `result` /
517    // `allocator`. Used to decide whether to emit the GPA allocator binding.
518    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    // Emit GPA allocator only when it will actually be used: setup lines that
532    // need GPA allocation (mock_url), or a JSON-struct result path where the test
533    // will call `std.json.parseFromSlice`. The binding is not needed for
534    // error-only paths or tests with no field assertions.
535    // Note: `bytes` arg setup uses c_allocator directly and does NOT require GPA.
536    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        // Error-path test: use error union syntax `!T` and try-catch.
552        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        // Whether any non-error assertion will emit code that references `result`.
573        // If not, we must explicitly discard `result` to satisfy Zig's
574        // strict-unused-locals rule.
575        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        // No assertions: emit a call to verify compilation.
608        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        // Happy path: call and assert. Detect whether any assertion actually
625        // emits code that references `result` (some — like `not_error` — emit
626        // nothing) so we don't leave an unused local, which Zig 0.16 rejects.
627        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            // JSON struct path: parse result JSON and access fields dynamically.
639            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
671// ---------------------------------------------------------------------------
672// JSON-struct assertion rendering (for result_is_json_struct = true)
673// ---------------------------------------------------------------------------
674
675/// Convert a dot-separated field path into a chain of `std.json.Value` lookups.
676///
677/// Each segment uses `.object.get("key").?` to traverse the JSON object tree.
678/// The final segment stops before the leaf-type accessor so callers can append
679/// the appropriate accessor (`.string`, `.integer`, `.array.items`, etc.).
680///
681/// Returns `(base_expr, last_key)` where `base_expr` already includes all
682/// intermediate `.object.get("…").?` dereferences up to (but not including)
683/// the leaf, and `last_key` is the last path segment.
684/// Variant names of `FormatMetadata` (snake_case, from `#[serde(rename_all = "snake_case")]`).
685/// These appear as typed accessors in fixture paths (e.g. `format.excel.sheet_count`)
686/// but are NOT JSON keys — `FormatMetadata` is internally tagged so variant fields are
687/// flattened directly into the `format` object alongside the `format_type` discriminant.
688const 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        // Skip variant-name accessor segments that follow a `format` key.
717        // FormatMetadata is an internally-tagged enum (`#[serde(tag = "format_type")]`),
718        // so variant fields are flattened directly into the format object — there is no
719        // intermediate JSON key for the variant name.
720        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
730/// Render a single assertion for a JSON-struct result (result_is_json_struct = true).
731///
732/// The `result_var` variable is `*std.json.Value` (pointer to the parsed root object).
733/// Field paths are traversed via `.object.get("key").?` chains.
734fn render_json_assertion(out: &mut String, assertion: &Assertion, result_var: &str) {
735    let field_path = assertion.field.as_deref().unwrap_or("").trim();
736
737    // Build the JSON traversal expression up to the leaf.
738    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                // Serialize the JSON value to a string and search.
769                // Works for both string fields (value is the string) and array/object
770                // fields (value is the JSON-encoded representation).
771                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            // For string fields: search the string value. For array/object fields:
787            // serialize to JSON and search the JSON text (e.g., ["a","b"] contains "b").
788            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            // For a JSON object field: check it's present and non-null.
835            // For a string field: check length > 0.
836            // We emit a check that the field is not a JSON null.
837            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            // Handled at the call level.
895        }
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            // At least ONE of the values must be found in the field (OR logic).
916            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
942/// Predicate matching `render_assertion`: returns true when the assertion
943/// would emit at least one statement that references the result variable.
944fn 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
974/// Build setup lines and the argument list for the function call.
975///
976/// Returns `(setup_lines, args_str, setup_needs_gpa)` where `setup_needs_gpa`
977/// is `true` when at least one setup line requires the GPA `allocator` binding.
978fn 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        // The Zig wrapper accepts struct parameters (e.g. `ExtractionConfig`)
1004        // as JSON `[]const u8`, converting them to opaque FFI handles via the
1005        // `<prefix>_<snake>_from_json` helper at the binding layer. Emit the
1006        // fixture's configuration value as a JSON string literal, falling back
1007        // to `"{}"` when the fixture omits a config so callers exercise the
1008        // default path.
1009        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                // Zig functions don't have default arguments, so we must
1024                // pass `null` explicitly for every optional parameter.
1025                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                // For `json_object` arguments other than `config` (handled
1040                // above) the Zig binding accepts a JSON `[]const u8`, so we
1041                // serialize the entire fixture value as a single JSON string
1042                // literal rather than rendering it as a Zig array/struct.
1043                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                    // `bytes` args are file paths in fixtures — read the file into a
1048                    // local buffer. The cwd is set to test_documents/ at runtime.
1049                    // Zig 0.16 uses std.Io.Dir.cwd() (not std.fs.cwd()) and requires
1050                    // an `io` instance from std.testing.io in test context.
1051                    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    // Skip assertions on fields that don't exist on the result type.
1080    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    // Determine if this field is an enum type.
1088    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                    // When there is no field (field_expr == result_var), the result
1183                    // is `[]u8` JSON (e.g. batch functions). Parse the JSON array
1184                    // and count its elements; `.len` would give byte count, not item count.
1185                    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            // Already handled by the call succeeding.
1212        }
1213        "error" => {
1214            // Handled at the test function level.
1215        }
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            // At least ONE of the values must be found in the field (OR logic).
1242            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
1274/// Convert a `serde_json::Value` to a Zig literal string.
1275fn 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}