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    ) -> 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        // Resolve call config with overrides.
39        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        // Resolve package config.
52        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        // Generate build.zig.zon (Zig package manifest).
65        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        // Get the module name for imports.
72        let module_name = config.zig_module_name();
73
74        // Generate build.zig - collect test file names first.
75        let field_resolver = FieldResolver::new(
76            &e2e_config.fields,
77            &e2e_config.fields_optional,
78            &e2e_config.result_fields,
79            &e2e_config.fields_array,
80            &e2e_config.fields_method_calls,
81        );
82
83        // Generate test files per category and collect their names.
84        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        // Generate build.zig with collected test files.
117        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
137// ---------------------------------------------------------------------------
138// Rendering
139// ---------------------------------------------------------------------------
140
141fn 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            // For registry mode, use a dummy hash (in real Zig, hash must be computed).
145            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    // Zig 0.16+ requires a fingerprint of the form (crc32_ieee(name) << 32) | id.
159    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        // Convert filename like "basic_test.zig" to a test name
228        let test_name = filename.trim_end_matches("_test.zig");
229        content.push_str(&format!("    const {test_name}_module = b.createModule(.{{\n"));
230        content.push_str(&format!("        .root_source_file = b.path(\"src/{filename}\"),\n"));
231        content.push_str("        .target = target,\n");
232        content.push_str("        .optimize = optimize,\n");
233        content.push_str("    });\n");
234        content.push_str(&format!(
235            "    {test_name}_module.addImport(\"kreuzberg\", kreuzberg_module);\n"
236        ));
237        content.push_str(&format!("    const {test_name}_tests = b.addTest(.{{\n"));
238        content.push_str(&format!("        .root_module = {test_name}_module,\n"));
239        content.push_str("    });\n");
240        content.push_str(&format!(
241            "    const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
242        ));
243        content.push_str(&format!(
244            "    {test_name}_run.setCwd(b.path(\"../../test_documents\"));\n"
245        ));
246        content.push_str(&format!("    test_step.dependOn(&{test_name}_run.step);\n\n"));
247    }
248
249    content.push_str("}\n");
250    content
251}
252
253// ---------------------------------------------------------------------------
254// HTTP server test rendering — shared-driver integration
255// ---------------------------------------------------------------------------
256
257/// Renderer that emits Zig `test "..." { ... }` blocks targeting a mock server
258/// via `std.http.Client`. Satisfies [`client::TestClientRenderer`] so the shared
259/// [`client::http_call::render_http_test`] driver drives the call sequence.
260struct ZigTestClientRenderer;
261
262impl client::TestClientRenderer for ZigTestClientRenderer {
263    fn language_name(&self) -> &'static str {
264        "zig"
265    }
266
267    fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
268        if let Some(reason) = skip_reason {
269            let _ = writeln!(out, "test \"{fn_name}\" {{");
270            let _ = writeln!(out, "    // {description}");
271            let _ = writeln!(out, "    // skipped: {reason}");
272            let _ = writeln!(out, "    return error.SkipZigTest;");
273        } else {
274            let _ = writeln!(out, "test \"{fn_name}\" {{");
275            let _ = writeln!(out, "    // {description}");
276        }
277    }
278
279    fn render_test_close(&self, out: &mut String) {
280        let _ = writeln!(out, "}}");
281    }
282
283    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
284        let method = ctx.method.to_uppercase();
285        let fixture_id = ctx.path.trim_start_matches("/fixtures/");
286
287        let _ = writeln!(out, "    var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
288        let _ = writeln!(out, "    defer _ = gpa.deinit();");
289        let _ = writeln!(out, "    const allocator = gpa.allocator();");
290
291        let _ = writeln!(out, "    var url_buf: [512]u8 = undefined;");
292        let _ = writeln!(
293            out,
294            "    const url = try std.fmt.bufPrint(&url_buf, \"{{s}}/fixtures/{fixture_id}\", .{{std.posix.getenv(\"MOCK_SERVER_URL\") orelse \"http://localhost:8080\"}});"
295        );
296
297        // Headers
298        if !ctx.headers.is_empty() {
299            let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
300            header_pairs.sort_by_key(|(k, _)| k.as_str());
301            let _ = writeln!(out, "    const headers = [_]std.http.Header{{");
302            for (k, v) in &header_pairs {
303                let ek = escape_zig(k);
304                let ev = escape_zig(v);
305                let _ = writeln!(out, "        .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
306            }
307            let _ = writeln!(out, "    }};");
308        }
309
310        // Body
311        if let Some(body) = ctx.body {
312            let json_str = serde_json::to_string(body).unwrap_or_default();
313            let escaped = escape_zig(&json_str);
314            let _ = writeln!(out, "    const body_bytes: []const u8 = \"{escaped}\";");
315        }
316
317        let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
318        let has_body = ctx.body.is_some();
319
320        let _ = writeln!(
321            out,
322            "    var http_client = std.http.Client{{ .allocator = allocator }};"
323        );
324        let _ = writeln!(out, "    defer http_client.deinit();");
325        let _ = writeln!(out, "    var response_body = std.ArrayList(u8).init(allocator);");
326        let _ = writeln!(out, "    defer response_body.deinit();");
327
328        let method_zig = match method.as_str() {
329            "GET" => ".GET",
330            "POST" => ".POST",
331            "PUT" => ".PUT",
332            "DELETE" => ".DELETE",
333            "PATCH" => ".PATCH",
334            "HEAD" => ".HEAD",
335            "OPTIONS" => ".OPTIONS",
336            _ => ".GET",
337        };
338
339        let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
340        let _ = writeln!(
341            out,
342            "    const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
343            rv = ctx.response_var,
344        );
345    }
346
347    fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
348        let _ = writeln!(
349            out,
350            "    try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
351        );
352    }
353
354    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
355        let ename = escape_zig(&name.to_lowercase());
356        match expected {
357            "<<present>>" => {
358                let _ = writeln!(
359                    out,
360                    "    // assert header '{ename}' is present (header inspection not yet implemented)"
361                );
362            }
363            "<<absent>>" => {
364                let _ = writeln!(
365                    out,
366                    "    // assert header '{ename}' is absent (header inspection not yet implemented)"
367                );
368            }
369            "<<uuid>>" => {
370                let _ = writeln!(
371                    out,
372                    "    // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
373                );
374            }
375            exact => {
376                let evalue = escape_zig(exact);
377                let _ = writeln!(
378                    out,
379                    "    // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
380                );
381            }
382        }
383    }
384
385    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
386        let json_str = serde_json::to_string(expected).unwrap_or_default();
387        let escaped = escape_zig(&json_str);
388        let _ = writeln!(
389            out,
390            "    try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
391        );
392    }
393
394    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
395        if let Some(obj) = expected.as_object() {
396            for (key, val) in obj {
397                let ekey = escape_zig(key);
398                let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
399                let _ = writeln!(
400                    out,
401                    "    // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
402                );
403            }
404        }
405    }
406
407    fn render_assert_validation_errors(
408        &self,
409        out: &mut String,
410        _response_var: &str,
411        errors: &[crate::fixture::ValidationErrorExpectation],
412    ) {
413        for ve in errors {
414            let loc = ve.loc.join(".");
415            let escaped_loc = escape_zig(&loc);
416            let escaped_msg = escape_zig(&ve.msg);
417            let _ = writeln!(
418                out,
419                "    // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
420            );
421        }
422    }
423}
424
425/// Render a Zig `test "..." { ... }` block for an HTTP server fixture.
426///
427/// Delegates to the shared [`client::http_call::render_http_test`] driver via
428/// [`ZigTestClientRenderer`].
429fn render_http_test_case(out: &mut String, fixture: &Fixture) {
430    client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
431}
432
433// ---------------------------------------------------------------------------
434// Function-call test rendering
435// ---------------------------------------------------------------------------
436
437#[allow(clippy::too_many_arguments)]
438fn render_test_file(
439    category: &str,
440    fixtures: &[&Fixture],
441    e2e_config: &E2eConfig,
442    function_name: &str,
443    result_var: &str,
444    args: &[crate::config::ArgMapping],
445    field_resolver: &FieldResolver,
446    enum_fields: &HashSet<String>,
447    module_name: &str,
448) -> String {
449    let mut out = String::new();
450    out.push_str(&hash::header(CommentStyle::DoubleSlash));
451    let _ = writeln!(out, "const std = @import(\"std\");");
452    let _ = writeln!(out, "const testing = std.testing;");
453    let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
454    let _ = writeln!(out);
455
456    let _ = writeln!(out, "// E2e tests for category: {category}");
457    let _ = writeln!(out);
458
459    for fixture in fixtures {
460        if fixture.http.is_some() {
461            render_http_test_case(&mut out, fixture);
462        } else {
463            render_test_fn(
464                &mut out,
465                fixture,
466                e2e_config,
467                function_name,
468                result_var,
469                args,
470                field_resolver,
471                enum_fields,
472                module_name,
473            );
474        }
475        let _ = writeln!(out);
476    }
477
478    out
479}
480
481#[allow(clippy::too_many_arguments)]
482fn render_test_fn(
483    out: &mut String,
484    fixture: &Fixture,
485    e2e_config: &E2eConfig,
486    _function_name: &str,
487    _result_var: &str,
488    _args: &[crate::config::ArgMapping],
489    field_resolver: &FieldResolver,
490    enum_fields: &HashSet<String>,
491    module_name: &str,
492) {
493    // Resolve per-fixture call config.
494    let call_config = e2e_config.resolve_call(fixture.call.as_deref());
495    let lang = "zig";
496    let call_overrides = call_config.overrides.get(lang);
497    let function_name = call_overrides
498        .and_then(|o| o.function.as_ref())
499        .cloned()
500        .unwrap_or_else(|| call_config.function.clone());
501    let result_var = &call_config.result_var;
502    let args = &call_config.args;
503    let is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
504    // When `result_is_json_struct = true`, the Zig function returns `[]u8` JSON.
505    // The test parses it with `std.json.parseFromSlice(std.json.Value, ...)` and
506    // traverses the dynamic JSON object for field assertions.
507    let result_is_json_struct = call_overrides.is_some_and(|o| o.result_is_json_struct);
508
509    let test_name = fixture.id.to_snake_case();
510    let description = &fixture.description;
511    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
512
513    let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(&fixture.input, args, &fixture.id, module_name);
514
515    // Pre-compute whether any assertion will emit code that references `result` /
516    // `allocator`. Used to decide whether to emit the GPA allocator binding.
517    let any_happy_emits_code = fixture
518        .assertions
519        .iter()
520        .any(|a| assertion_emits_code(a, field_resolver));
521    let any_non_error_emits_code = fixture
522        .assertions
523        .iter()
524        .filter(|a| a.assertion_type != "error")
525        .any(|a| assertion_emits_code(a, field_resolver));
526
527    let _ = writeln!(out, "test \"{test_name}\" {{");
528    let _ = writeln!(out, "    // {description}");
529
530    // Emit GPA allocator only when it will actually be used: setup lines that
531    // need GPA allocation (mock_url), or a JSON-struct result path where the test
532    // will call `std.json.parseFromSlice`. The binding is not needed for
533    // error-only paths or tests with no field assertions.
534    // Note: `bytes` arg setup uses c_allocator directly and does NOT require GPA.
535    let needs_gpa = setup_needs_gpa
536        || (result_is_json_struct && !expects_error && any_happy_emits_code)
537        || (result_is_json_struct && expects_error && any_non_error_emits_code);
538    if needs_gpa {
539        let _ = writeln!(out, "    var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
540        let _ = writeln!(out, "    defer _ = gpa.deinit();");
541        let _ = writeln!(out, "    const allocator = gpa.allocator();");
542        let _ = writeln!(out);
543    }
544
545    for line in &setup_lines {
546        let _ = writeln!(out, "    {line}");
547    }
548
549    if expects_error {
550        // Error-path test: use error union syntax `!T` and try-catch.
551        if is_async {
552            let _ = writeln!(
553                out,
554                "    // Note: async functions not yet fully supported; treating as sync"
555            );
556        }
557        if result_is_json_struct {
558            let _ = writeln!(
559                out,
560                "    const _result_json = {module_name}.{function_name}({args_str}) catch {{"
561            );
562        } else {
563            let _ = writeln!(
564                out,
565                "    const result = {module_name}.{function_name}({args_str}) catch {{"
566            );
567        }
568        let _ = writeln!(out, "        try testing.expect(true); // Error occurred as expected");
569        let _ = writeln!(out, "        return;");
570        let _ = writeln!(out, "    }};");
571        // Whether any non-error assertion will emit code that references `result`.
572        // If not, we must explicitly discard `result` to satisfy Zig's
573        // strict-unused-locals rule.
574        let any_emits_code = fixture
575            .assertions
576            .iter()
577            .filter(|a| a.assertion_type != "error")
578            .any(|a| assertion_emits_code(a, field_resolver));
579        if result_is_json_struct && any_emits_code {
580            let _ = writeln!(out, "    defer std.heap.c_allocator.free(_result_json);");
581            let _ = writeln!(
582                out,
583                "    var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
584            );
585            let _ = writeln!(out, "    defer _parsed.deinit();");
586            let _ = writeln!(out, "    const {result_var} = &_parsed.value;");
587            let _ = writeln!(out, "    // Perform success assertions if any");
588            for assertion in &fixture.assertions {
589                if assertion.assertion_type != "error" {
590                    render_json_assertion(out, assertion, result_var);
591                }
592            }
593        } else if result_is_json_struct {
594            let _ = writeln!(out, "    _ = _result_json;");
595        } else if any_emits_code {
596            let _ = writeln!(out, "    // Perform success assertions if any");
597            for assertion in &fixture.assertions {
598                if assertion.assertion_type != "error" {
599                    render_assertion(out, assertion, result_var, field_resolver, enum_fields);
600                }
601            }
602        } else {
603            let _ = writeln!(out, "    _ = result;");
604        }
605    } else if fixture.assertions.is_empty() {
606        // No assertions: emit a call to verify compilation.
607        if is_async {
608            let _ = writeln!(
609                out,
610                "    // Note: async functions not yet fully supported; treating as sync"
611            );
612        }
613        if result_is_json_struct {
614            let _ = writeln!(
615                out,
616                "    const _result_json = try {module_name}.{function_name}({args_str});"
617            );
618            let _ = writeln!(out, "    defer std.heap.c_allocator.free(_result_json);");
619        } else {
620            let _ = writeln!(out, "    _ = try {module_name}.{function_name}({args_str});");
621        }
622    } else {
623        // Happy path: call and assert. Detect whether any assertion actually
624        // emits code that references `result` (some — like `not_error` — emit
625        // nothing) so we don't leave an unused local, which Zig 0.16 rejects.
626        if is_async {
627            let _ = writeln!(
628                out,
629                "    // Note: async functions not yet fully supported; treating as sync"
630            );
631        }
632        let any_emits_code = fixture
633            .assertions
634            .iter()
635            .any(|a| assertion_emits_code(a, field_resolver));
636        if result_is_json_struct {
637            // JSON struct path: parse result JSON and access fields dynamically.
638            let _ = writeln!(
639                out,
640                "    const _result_json = try {module_name}.{function_name}({args_str});"
641            );
642            let _ = writeln!(out, "    defer std.heap.c_allocator.free(_result_json);");
643            if any_emits_code {
644                let _ = writeln!(
645                    out,
646                    "    var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
647                );
648                let _ = writeln!(out, "    defer _parsed.deinit();");
649                let _ = writeln!(out, "    const {result_var} = &_parsed.value;");
650                for assertion in &fixture.assertions {
651                    render_json_assertion(out, assertion, result_var);
652                }
653            }
654        } else if any_emits_code {
655            let _ = writeln!(
656                out,
657                "    const {result_var} = try {module_name}.{function_name}({args_str});"
658            );
659            for assertion in &fixture.assertions {
660                render_assertion(out, assertion, result_var, field_resolver, enum_fields);
661            }
662        } else {
663            let _ = writeln!(out, "    _ = try {module_name}.{function_name}({args_str});");
664        }
665    }
666
667    let _ = writeln!(out, "}}");
668}
669
670// ---------------------------------------------------------------------------
671// JSON-struct assertion rendering (for result_is_json_struct = true)
672// ---------------------------------------------------------------------------
673
674/// Convert a dot-separated field path into a chain of `std.json.Value` lookups.
675///
676/// Each segment uses `.object.get("key").?` to traverse the JSON object tree.
677/// The final segment stops before the leaf-type accessor so callers can append
678/// the appropriate accessor (`.string`, `.integer`, `.array.items`, etc.).
679///
680/// Returns `(base_expr, last_key)` where `base_expr` already includes all
681/// intermediate `.object.get("…").?` dereferences up to (but not including)
682/// the leaf, and `last_key` is the last path segment.
683/// Variant names of `FormatMetadata` (snake_case, from `#[serde(rename_all = "snake_case")]`).
684/// These appear as typed accessors in fixture paths (e.g. `format.excel.sheet_count`)
685/// but are NOT JSON keys — `FormatMetadata` is internally tagged so variant fields are
686/// flattened directly into the `format` object alongside the `format_type` discriminant.
687const FORMAT_METADATA_VARIANTS: &[&str] = &[
688    "pdf",
689    "docx",
690    "excel",
691    "email",
692    "pptx",
693    "archive",
694    "image",
695    "xml",
696    "text",
697    "html",
698    "ocr",
699    "csv",
700    "bibtex",
701    "citation",
702    "fiction_book",
703    "dbf",
704    "jats",
705    "epub",
706    "pst",
707    "code",
708];
709
710fn json_path_expr(result_var: &str, field_path: &str) -> String {
711    let segments: Vec<&str> = field_path.split('.').collect();
712    let mut expr = result_var.to_string();
713    let mut prev_seg: Option<&str> = None;
714    for seg in &segments {
715        // Skip variant-name accessor segments that follow a `format` key.
716        // FormatMetadata is an internally-tagged enum (`#[serde(tag = "format_type")]`),
717        // so variant fields are flattened directly into the format object — there is no
718        // intermediate JSON key for the variant name.
719        if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
720            prev_seg = Some(seg);
721            continue;
722        }
723        expr = format!("{expr}.object.get(\"{seg}\").?");
724        prev_seg = Some(seg);
725    }
726    expr
727}
728
729/// Render a single assertion for a JSON-struct result (result_is_json_struct = true).
730///
731/// The `result_var` variable is `*std.json.Value` (pointer to the parsed root object).
732/// Field paths are traversed via `.object.get("key").?` chains.
733fn render_json_assertion(out: &mut String, assertion: &Assertion, result_var: &str) {
734    let field_path = assertion.field.as_deref().unwrap_or("").trim();
735
736    // Build the JSON traversal expression up to the leaf.
737    let field_expr = if field_path.is_empty() {
738        result_var.to_string()
739    } else {
740        json_path_expr(result_var, field_path)
741    };
742
743    match assertion.assertion_type.as_str() {
744        "equals" => {
745            if let Some(expected) = &assertion.value {
746                match expected {
747                    serde_json::Value::String(s) => {
748                        let zig_val = format!("\"{}\"", escape_zig(s));
749                        let _ = writeln!(
750                            out,
751                            "    try testing.expectEqualStrings({zig_val}, {field_expr}.string);"
752                        );
753                    }
754                    serde_json::Value::Bool(b) => {
755                        let _ = writeln!(out, "    try testing.expectEqual({b}, {field_expr}.bool);");
756                    }
757                    serde_json::Value::Number(n) => {
758                        let _ = writeln!(out, "    try testing.expectEqual({n}, {field_expr}.integer);");
759                    }
760                    _ => {}
761                }
762            }
763        }
764        "contains" => {
765            if let Some(serde_json::Value::String(s)) = &assertion.value {
766                let zig_val = format!("\"{}\"", escape_zig(s));
767                // Serialize the JSON value to a string and search.
768                // Works for both string fields (value is the string) and array/object
769                // fields (value is the JSON-encoded representation).
770                let _ = writeln!(out, "    {{");
771                let _ = writeln!(out, "        const _jv = {field_expr};");
772                let _ = writeln!(
773                    out,
774                    "        const _js = if (_jv == .string) _jv.string else try std.json.Stringify.valueAlloc(std.heap.c_allocator, _jv, .{{}});"
775                );
776                let _ = writeln!(out, "        defer if (_jv != .string) std.heap.c_allocator.free(_js);");
777                let _ = writeln!(
778                    out,
779                    "        try testing.expect(std.mem.indexOf(u8, _js, {zig_val}) != null);"
780                );
781                let _ = writeln!(out, "    }}");
782            }
783        }
784        "contains_all" => {
785            // For string fields: search the string value. For array/object fields:
786            // serialize to JSON and search the JSON text (e.g., ["a","b"] contains "b").
787            if let Some(values) = &assertion.values {
788                for (idx, val) in values.iter().enumerate() {
789                    if let serde_json::Value::String(s) = val {
790                        let zig_val = format!("\"{}\"", escape_zig(s));
791                        let jv = format!("_jva{idx}");
792                        let js = format!("_jsa{idx}");
793                        let _ = writeln!(out, "    {{");
794                        let _ = writeln!(out, "        const {jv} = {field_expr};");
795                        let _ = writeln!(
796                            out,
797                            "        const {js} = if ({jv} == .string) {jv}.string else try std.json.Stringify.valueAlloc(std.heap.c_allocator, {jv}, .{{}});"
798                        );
799                        let _ = writeln!(
800                            out,
801                            "        defer if ({jv} != .string) std.heap.c_allocator.free({js});"
802                        );
803                        let _ = writeln!(
804                            out,
805                            "        try testing.expect(std.mem.indexOf(u8, {js}, {zig_val}) != null);"
806                        );
807                        let _ = writeln!(out, "    }}");
808                    }
809                }
810            }
811        }
812        "not_contains" => {
813            if let Some(serde_json::Value::String(s)) = &assertion.value {
814                let zig_val = format!("\"{}\"", escape_zig(s));
815                let _ = writeln!(out, "    {{");
816                let _ = writeln!(out, "        const _jvnc = {field_expr};");
817                let _ = writeln!(
818                    out,
819                    "        const _jsnc = if (_jvnc == .string) _jvnc.string else try std.json.Stringify.valueAlloc(std.heap.c_allocator, _jvnc, .{{}});"
820                );
821                let _ = writeln!(
822                    out,
823                    "        defer if (_jvnc != .string) std.heap.c_allocator.free(_jsnc);"
824                );
825                let _ = writeln!(
826                    out,
827                    "        try testing.expect(std.mem.indexOf(u8, _jsnc, {zig_val}) == null);"
828                );
829                let _ = writeln!(out, "    }}");
830            }
831        }
832        "not_empty" => {
833            // For a JSON object field: check it's present and non-null.
834            // For a string field: check length > 0.
835            // We emit a check that the field is not a JSON null.
836            let _ = writeln!(out, "    try testing.expect({field_expr} != .null);");
837        }
838        "is_empty" => {
839            let _ = writeln!(out, "    try testing.expectEqual(.null, {field_expr});");
840        }
841        "min_length" => {
842            if let Some(val) = &assertion.value {
843                if let Some(n) = val.as_u64() {
844                    let _ = writeln!(out, "    try testing.expect({field_expr}.string.len >= {n});");
845                }
846            }
847        }
848        "count_min" => {
849            if let Some(val) = &assertion.value {
850                if let Some(n) = val.as_u64() {
851                    let _ = writeln!(out, "    try testing.expect({field_expr}.array.items.len >= {n});");
852                }
853            }
854        }
855        "count_equals" => {
856            if let Some(val) = &assertion.value {
857                if let Some(n) = val.as_u64() {
858                    let _ = writeln!(out, "    try testing.expectEqual({n}, {field_expr}.array.items.len);");
859                }
860            }
861        }
862        "greater_than" => {
863            if let Some(val) = &assertion.value {
864                let n = json_to_zig(val);
865                let _ = writeln!(out, "    try testing.expect({field_expr}.integer > {n});");
866            }
867        }
868        "less_than" => {
869            if let Some(val) = &assertion.value {
870                let n = json_to_zig(val);
871                let _ = writeln!(out, "    try testing.expect({field_expr}.integer < {n});");
872            }
873        }
874        "greater_than_or_equal" => {
875            if let Some(val) = &assertion.value {
876                let n = json_to_zig(val);
877                let _ = writeln!(out, "    try testing.expect({field_expr}.integer >= {n});");
878            }
879        }
880        "less_than_or_equal" => {
881            if let Some(val) = &assertion.value {
882                let n = json_to_zig(val);
883                let _ = writeln!(out, "    try testing.expect({field_expr}.integer <= {n});");
884            }
885        }
886        "is_true" => {
887            let _ = writeln!(out, "    try testing.expect({field_expr}.bool);");
888        }
889        "is_false" => {
890            let _ = writeln!(out, "    try testing.expect(!{field_expr}.bool);");
891        }
892        "not_error" | "error" => {
893            // Handled at the call level.
894        }
895        "starts_with" => {
896            if let Some(serde_json::Value::String(s)) = &assertion.value {
897                let zig_val = format!("\"{}\"", escape_zig(s));
898                let _ = writeln!(
899                    out,
900                    "    try testing.expect(std.mem.startsWith(u8, {field_expr}.string, {zig_val}));"
901                );
902            }
903        }
904        "ends_with" => {
905            if let Some(serde_json::Value::String(s)) = &assertion.value {
906                let zig_val = format!("\"{}\"", escape_zig(s));
907                let _ = writeln!(
908                    out,
909                    "    try testing.expect(std.mem.endsWith(u8, {field_expr}.string, {zig_val}));"
910                );
911            }
912        }
913        "contains_any" => {
914            // At least ONE of the values must be found in the field (OR logic).
915            if let Some(values) = &assertion.values {
916                let string_values: Vec<String> = values
917                    .iter()
918                    .filter_map(|v| {
919                        if let serde_json::Value::String(s) = v {
920                            Some(format!(
921                                "std.mem.indexOf(u8, {field_expr}.string, \"{}\") != null",
922                                escape_zig(s)
923                            ))
924                        } else {
925                            None
926                        }
927                    })
928                    .collect();
929                if !string_values.is_empty() {
930                    let condition = string_values.join(" or\n        ");
931                    let _ = writeln!(out, "    try testing.expect(\n        {condition}\n    );");
932                }
933            }
934        }
935        other => {
936            let _ = writeln!(out, "    // json assertion '{other}' not implemented for Zig");
937        }
938    }
939}
940
941/// Predicate matching `render_assertion`: returns true when the assertion
942/// would emit at least one statement that references the result variable.
943fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
944    if let Some(f) = &assertion.field {
945        if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
946            return false;
947        }
948    }
949    matches!(
950        assertion.assertion_type.as_str(),
951        "equals"
952            | "contains"
953            | "contains_all"
954            | "not_contains"
955            | "not_empty"
956            | "is_empty"
957            | "starts_with"
958            | "ends_with"
959            | "min_length"
960            | "max_length"
961            | "count_min"
962            | "count_equals"
963            | "is_true"
964            | "is_false"
965            | "greater_than"
966            | "less_than"
967            | "greater_than_or_equal"
968            | "less_than_or_equal"
969            | "contains_any"
970    )
971}
972
973/// Build setup lines and the argument list for the function call.
974///
975/// Returns `(setup_lines, args_str, setup_needs_gpa)` where `setup_needs_gpa`
976/// is `true` when at least one setup line requires the GPA `allocator` binding.
977fn build_args_and_setup(
978    input: &serde_json::Value,
979    args: &[crate::config::ArgMapping],
980    fixture_id: &str,
981    _module_name: &str,
982) -> (Vec<String>, String, bool) {
983    if args.is_empty() {
984        return (Vec::new(), String::new(), false);
985    }
986
987    let mut setup_lines: Vec<String> = Vec::new();
988    let mut parts: Vec<String> = Vec::new();
989    let mut setup_needs_gpa = false;
990
991    for arg in args {
992        if arg.arg_type == "mock_url" {
993            setup_lines.push(format!(
994                "var {} = try allocator.alloc(u8, std.fmt.bufPrint(undefined, \"{{s}}/fixtures/{fixture_id}\", .{{std.posix.getenv(\"MOCK_SERVER_URL\") orelse \"http://localhost:8080\"}}) catch 0)",
995                arg.name,
996            ));
997            parts.push(arg.name.clone());
998            setup_needs_gpa = true;
999            continue;
1000        }
1001
1002        // The Zig wrapper accepts struct parameters (e.g. `ExtractionConfig`)
1003        // as JSON `[]const u8`, converting them to opaque FFI handles via the
1004        // `<prefix>_<snake>_from_json` helper at the binding layer. Emit the
1005        // fixture's configuration value as a JSON string literal, falling back
1006        // to `"{}"` when the fixture omits a config so callers exercise the
1007        // default path.
1008        if arg.name == "config" && arg.arg_type == "json_object" {
1009            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1010            let json_str = match input.get(field) {
1011                Some(serde_json::Value::Null) | None => "{}".to_string(),
1012                Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1013            };
1014            parts.push(format!("\"{}\"", escape_zig(&json_str)));
1015            continue;
1016        }
1017
1018        let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1019        let val = input.get(field);
1020        match val {
1021            None | Some(serde_json::Value::Null) if arg.optional => {
1022                // Zig functions don't have default arguments, so we must
1023                // pass `null` explicitly for every optional parameter.
1024                parts.push("null".to_string());
1025            }
1026            None | Some(serde_json::Value::Null) => {
1027                let default_val = match arg.arg_type.as_str() {
1028                    "string" => "\"\"".to_string(),
1029                    "int" | "integer" => "0".to_string(),
1030                    "float" | "number" => "0.0".to_string(),
1031                    "bool" | "boolean" => "false".to_string(),
1032                    "json_object" => "\"{}\"".to_string(),
1033                    _ => "null".to_string(),
1034                };
1035                parts.push(default_val);
1036            }
1037            Some(v) => {
1038                // For `json_object` arguments other than `config` (handled
1039                // above) the Zig binding accepts a JSON `[]const u8`, so we
1040                // serialize the entire fixture value as a single JSON string
1041                // literal rather than rendering it as a Zig array/struct.
1042                if arg.arg_type == "json_object" {
1043                    let json_str = serde_json::to_string(v).unwrap_or_default();
1044                    parts.push(format!("\"{}\"", escape_zig(&json_str)));
1045                } else if arg.arg_type == "bytes" {
1046                    // `bytes` args are file paths in fixtures — read the file into a
1047                    // local buffer. The cwd is set to test_documents/ at runtime.
1048                    // Zig 0.16 uses std.Io.Dir.cwd() (not std.fs.cwd()) and requires
1049                    // an `io` instance from std.testing.io in test context.
1050                    if let serde_json::Value::String(path) = v {
1051                        let var_name = format!("{}_bytes", arg.name);
1052                        let epath = escape_zig(path);
1053                        setup_lines.push(format!(
1054                            "const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
1055                        ));
1056                        setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
1057                        parts.push(var_name);
1058                    } else {
1059                        parts.push(json_to_zig(v));
1060                    }
1061                } else {
1062                    parts.push(json_to_zig(v));
1063                }
1064            }
1065        }
1066    }
1067
1068    (setup_lines, parts.join(", "), setup_needs_gpa)
1069}
1070
1071fn render_assertion(
1072    out: &mut String,
1073    assertion: &Assertion,
1074    result_var: &str,
1075    field_resolver: &FieldResolver,
1076    enum_fields: &HashSet<String>,
1077) {
1078    // Skip assertions on fields that don't exist on the result type.
1079    if let Some(f) = &assertion.field {
1080        if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1081            let _ = writeln!(out, "    // skipped: field '{{f}}' not available on result type");
1082            return;
1083        }
1084    }
1085
1086    // Determine if this field is an enum type.
1087    let _field_is_enum = assertion
1088        .field
1089        .as_deref()
1090        .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1091
1092    let field_expr = match &assertion.field {
1093        Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
1094        _ => result_var.to_string(),
1095    };
1096
1097    match assertion.assertion_type.as_str() {
1098        "equals" => {
1099            if let Some(expected) = &assertion.value {
1100                let zig_val = json_to_zig(expected);
1101                let _ = writeln!(out, "    try testing.expectEqual({zig_val}, {field_expr});");
1102            }
1103        }
1104        "contains" => {
1105            if let Some(expected) = &assertion.value {
1106                let zig_val = json_to_zig(expected);
1107                let _ = writeln!(
1108                    out,
1109                    "    try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1110                );
1111            }
1112        }
1113        "contains_all" => {
1114            if let Some(values) = &assertion.values {
1115                for val in values {
1116                    let zig_val = json_to_zig(val);
1117                    let _ = writeln!(
1118                        out,
1119                        "    try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
1120                    );
1121                }
1122            }
1123        }
1124        "not_contains" => {
1125            if let Some(expected) = &assertion.value {
1126                let zig_val = json_to_zig(expected);
1127                let _ = writeln!(
1128                    out,
1129                    "    try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
1130                );
1131            }
1132        }
1133        "not_empty" => {
1134            let _ = writeln!(out, "    try testing.expect({field_expr}.len > 0);");
1135        }
1136        "is_empty" => {
1137            let _ = writeln!(out, "    try testing.expect({field_expr}.len == 0);");
1138        }
1139        "starts_with" => {
1140            if let Some(expected) = &assertion.value {
1141                let zig_val = json_to_zig(expected);
1142                let _ = writeln!(
1143                    out,
1144                    "    try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
1145                );
1146            }
1147        }
1148        "ends_with" => {
1149            if let Some(expected) = &assertion.value {
1150                let zig_val = json_to_zig(expected);
1151                let _ = writeln!(
1152                    out,
1153                    "    try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
1154                );
1155            }
1156        }
1157        "min_length" => {
1158            if let Some(val) = &assertion.value {
1159                if let Some(n) = val.as_u64() {
1160                    let _ = writeln!(out, "    try testing.expect({field_expr}.len >= {n});");
1161                }
1162            }
1163        }
1164        "max_length" => {
1165            if let Some(val) = &assertion.value {
1166                if let Some(n) = val.as_u64() {
1167                    let _ = writeln!(out, "    try testing.expect({field_expr}.len <= {n});");
1168                }
1169            }
1170        }
1171        "count_min" => {
1172            if let Some(val) = &assertion.value {
1173                if let Some(n) = val.as_u64() {
1174                    let _ = writeln!(out, "    try testing.expect({field_expr}.len >= {n});");
1175                }
1176            }
1177        }
1178        "count_equals" => {
1179            if let Some(val) = &assertion.value {
1180                if let Some(n) = val.as_u64() {
1181                    // When there is no field (field_expr == result_var), the result
1182                    // is `[]u8` JSON (e.g. batch functions). Parse the JSON array
1183                    // and count its elements; `.len` would give byte count, not item count.
1184                    let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
1185                    if has_field {
1186                        let _ = writeln!(out, "    try testing.expectEqual({n}, {field_expr}.len);");
1187                    } else {
1188                        let _ = writeln!(out, "    {{");
1189                        let _ = writeln!(
1190                            out,
1191                            "        var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
1192                        );
1193                        let _ = writeln!(out, "        defer _cparse.deinit();");
1194                        let _ = writeln!(
1195                            out,
1196                            "        try testing.expectEqual({n}, _cparse.value.array.items.len);"
1197                        );
1198                        let _ = writeln!(out, "    }}");
1199                    }
1200                }
1201            }
1202        }
1203        "is_true" => {
1204            let _ = writeln!(out, "    try testing.expect({field_expr});");
1205        }
1206        "is_false" => {
1207            let _ = writeln!(out, "    try testing.expect(!{field_expr});");
1208        }
1209        "not_error" => {
1210            // Already handled by the call succeeding.
1211        }
1212        "error" => {
1213            // Handled at the test function level.
1214        }
1215        "greater_than" => {
1216            if let Some(val) = &assertion.value {
1217                let zig_val = json_to_zig(val);
1218                let _ = writeln!(out, "    try testing.expect({field_expr} > {zig_val});");
1219            }
1220        }
1221        "less_than" => {
1222            if let Some(val) = &assertion.value {
1223                let zig_val = json_to_zig(val);
1224                let _ = writeln!(out, "    try testing.expect({field_expr} < {zig_val});");
1225            }
1226        }
1227        "greater_than_or_equal" => {
1228            if let Some(val) = &assertion.value {
1229                let zig_val = json_to_zig(val);
1230                let _ = writeln!(out, "    try testing.expect({field_expr} >= {zig_val});");
1231            }
1232        }
1233        "less_than_or_equal" => {
1234            if let Some(val) = &assertion.value {
1235                let zig_val = json_to_zig(val);
1236                let _ = writeln!(out, "    try testing.expect({field_expr} <= {zig_val});");
1237            }
1238        }
1239        "contains_any" => {
1240            // At least ONE of the values must be found in the field (OR logic).
1241            if let Some(values) = &assertion.values {
1242                let string_values: Vec<String> = values
1243                    .iter()
1244                    .filter_map(|v| {
1245                        if let serde_json::Value::String(s) = v {
1246                            Some(format!(
1247                                "std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
1248                                escape_zig(s)
1249                            ))
1250                        } else {
1251                            None
1252                        }
1253                    })
1254                    .collect();
1255                if !string_values.is_empty() {
1256                    let condition = string_values.join(" or\n        ");
1257                    let _ = writeln!(out, "    try testing.expect(\n        {condition}\n    );");
1258                }
1259            }
1260        }
1261        "matches_regex" => {
1262            let _ = writeln!(out, "    // regex match not yet implemented for Zig");
1263        }
1264        "method_result" => {
1265            let _ = writeln!(out, "    // method_result assertions not yet implemented for Zig");
1266        }
1267        other => {
1268            panic!("Zig e2e generator: unsupported assertion type: {other}");
1269        }
1270    }
1271}
1272
1273/// Convert a `serde_json::Value` to a Zig literal string.
1274fn json_to_zig(value: &serde_json::Value) -> String {
1275    match value {
1276        serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
1277        serde_json::Value::Bool(b) => b.to_string(),
1278        serde_json::Value::Number(n) => n.to_string(),
1279        serde_json::Value::Null => "null".to_string(),
1280        serde_json::Value::Array(arr) => {
1281            let items: Vec<String> = arr.iter().map(json_to_zig).collect();
1282            format!("&.{{{}}}", items.join(", "))
1283        }
1284        serde_json::Value::Object(_) => {
1285            let json_str = serde_json::to_string(value).unwrap_or_default();
1286            format!("\"{}\"", escape_zig(&json_str))
1287        }
1288    }
1289}