1use crate::config::E2eConfig;
7use crate::escape::{escape_zig, sanitize_filename};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, Fixture, FixtureGroup};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::ResolvedCrateConfig;
12use alef_core::hash::{self, CommentStyle};
13use alef_core::template_versions::toolchain;
14use anyhow::Result;
15use heck::ToSnakeCase;
16use std::collections::HashSet;
17use std::fmt::Write as FmtWrite;
18use std::path::PathBuf;
19
20use super::E2eCodegen;
21use super::client;
22
23pub struct ZigE2eCodegen;
25
26impl E2eCodegen for ZigE2eCodegen {
27 fn generate(
28 &self,
29 groups: &[FixtureGroup],
30 e2e_config: &E2eConfig,
31 config: &ResolvedCrateConfig,
32 ) -> Result<Vec<GeneratedFile>> {
33 let lang = self.language_name();
34 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
35
36 let mut files = Vec::new();
37
38 let call = &e2e_config.call;
40 let overrides = call.overrides.get(lang);
41 let _module_path = overrides
42 .and_then(|o| o.module.as_ref())
43 .cloned()
44 .unwrap_or_else(|| call.module.clone());
45 let function_name = overrides
46 .and_then(|o| o.function.as_ref())
47 .cloned()
48 .unwrap_or_else(|| call.function.clone());
49 let result_var = &call.result_var;
50
51 let zig_pkg = e2e_config.resolve_package("zig");
53 let pkg_path = zig_pkg
54 .as_ref()
55 .and_then(|p| p.path.as_ref())
56 .cloned()
57 .unwrap_or_else(|| "../../packages/zig".to_string());
58 let pkg_name = zig_pkg
59 .as_ref()
60 .and_then(|p| p.name.as_ref())
61 .cloned()
62 .unwrap_or_else(|| config.name.to_snake_case());
63
64 files.push(GeneratedFile {
66 path: output_base.join("build.zig.zon"),
67 content: render_build_zig_zon(&pkg_name, &pkg_path, e2e_config.dep_mode),
68 generated_header: false,
69 });
70
71 let module_name = config.zig_module_name();
73
74 let field_resolver = FieldResolver::new(
76 &e2e_config.fields,
77 &e2e_config.fields_optional,
78 &e2e_config.result_fields,
79 &e2e_config.fields_array,
80 &HashSet::new(),
81 );
82
83 let mut test_filenames: Vec<String> = Vec::new();
85 for group in groups {
86 let active: Vec<&Fixture> = group
87 .fixtures
88 .iter()
89 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
90 .collect();
91
92 if active.is_empty() {
93 continue;
94 }
95
96 let filename = format!("{}_test.zig", sanitize_filename(&group.category));
97 test_filenames.push(filename.clone());
98 let content = render_test_file(
99 &group.category,
100 &active,
101 e2e_config,
102 &function_name,
103 result_var,
104 &e2e_config.call.args,
105 &field_resolver,
106 &e2e_config.fields_enum,
107 &module_name,
108 );
109 files.push(GeneratedFile {
110 path: output_base.join("src").join(filename),
111 content,
112 generated_header: true,
113 });
114 }
115
116 files.insert(
118 files
119 .iter()
120 .position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
121 .unwrap_or(1),
122 GeneratedFile {
123 path: output_base.join("build.zig"),
124 content: render_build_zig(&test_filenames),
125 generated_header: false,
126 },
127 );
128
129 Ok(files)
130 }
131
132 fn language_name(&self) -> &'static str {
133 "zig"
134 }
135}
136
137fn render_build_zig_zon(pkg_name: &str, pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
142 let dep_block = match dep_mode {
143 crate::config::DependencyMode::Registry => {
144 format!(
146 r#".{{
147 .url = "https://registry.example.com/{pkg_name}/v0.1.0.tar.gz",
148 .hash = "0000000000000000000000000000000000000000000000000000000000000000",
149 }}"#
150 )
151 }
152 crate::config::DependencyMode::Local => {
153 format!(r#".{{ .path = "{pkg_path}" }}"#)
154 }
155 };
156
157 let min_zig = toolchain::MIN_ZIG_VERSION;
158 let name_bytes: &[u8] = b"e2e_zig";
160 let mut crc: u32 = 0xffff_ffff;
161 for byte in name_bytes {
162 crc ^= *byte as u32;
163 for _ in 0..8 {
164 let mask = (crc & 1).wrapping_neg();
165 crc = (crc >> 1) ^ (0xedb8_8320 & mask);
166 }
167 }
168 let name_crc: u32 = !crc;
169 let mut id: u32 = 0x811c_9dc5;
170 for byte in name_bytes {
171 id ^= *byte as u32;
172 id = id.wrapping_mul(0x0100_0193);
173 }
174 if id == 0 || id == 0xffff_ffff {
175 id = 0x1;
176 }
177 let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
178 format!(
179 r#".{{
180 .name = .e2e_zig,
181 .version = "0.1.0",
182 .fingerprint = 0x{fingerprint:016x},
183 .minimum_zig_version = "{min_zig}",
184 .dependencies = .{{
185 .{pkg_name} = {dep_block},
186 }},
187 .paths = .{{
188 "build.zig",
189 "build.zig.zon",
190 "src",
191 }},
192}}
193"#
194 )
195}
196
197fn render_build_zig(test_filenames: &[String]) -> String {
198 if test_filenames.is_empty() {
199 return r#"const std = @import("std");
200
201pub fn build(b: *std.Build) void {
202 const target = b.standardTargetOptions(.{});
203 const optimize = b.standardOptimizeOption(.{});
204
205 const test_step = b.step("test", "Run tests");
206}
207"#
208 .to_string();
209 }
210
211 let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
212 content.push_str(" const target = b.standardTargetOptions(.{});\n");
213 content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
214 content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n\n");
215
216 for filename in test_filenames {
217 let test_name = filename.trim_end_matches("_test.zig");
219 content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
220 content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
221 content.push_str(" .target = target,\n");
222 content.push_str(" .optimize = optimize,\n");
223 content.push_str(" });\n");
224 content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
225 content.push_str(&format!(" .root_module = {test_name}_module,\n"));
226 content.push_str(" });\n");
227 content.push_str(&format!(
228 " const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
229 ));
230 content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
231 }
232
233 content.push_str("}\n");
234 content
235}
236
237struct ZigTestClientRenderer;
245
246impl client::TestClientRenderer for ZigTestClientRenderer {
247 fn language_name(&self) -> &'static str {
248 "zig"
249 }
250
251 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
252 if let Some(reason) = skip_reason {
253 let _ = writeln!(out, "test \"{fn_name}\" {{");
254 let _ = writeln!(out, " // {description}");
255 let _ = writeln!(out, " // skipped: {reason}");
256 let _ = writeln!(out, " return error.SkipZigTest;");
257 } else {
258 let _ = writeln!(out, "test \"{fn_name}\" {{");
259 let _ = writeln!(out, " // {description}");
260 }
261 }
262
263 fn render_test_close(&self, out: &mut String) {
264 let _ = writeln!(out, "}}");
265 }
266
267 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
268 let method = ctx.method.to_uppercase();
269 let fixture_id = ctx.path.trim_start_matches("/fixtures/");
270
271 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
272 let _ = writeln!(out, " defer _ = gpa.deinit();");
273 let _ = writeln!(out, " const allocator = gpa.allocator();");
274
275 let _ = writeln!(out, " var url_buf: [512]u8 = undefined;");
276 let _ = writeln!(
277 out,
278 " const url = try std.fmt.bufPrint(&url_buf, \"{{s}}/fixtures/{fixture_id}\", .{{std.posix.getenv(\"MOCK_SERVER_URL\") orelse \"http://localhost:8080\"}});"
279 );
280
281 if !ctx.headers.is_empty() {
283 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
284 header_pairs.sort_by_key(|(k, _)| k.as_str());
285 let _ = writeln!(out, " const headers = [_]std.http.Header{{");
286 for (k, v) in &header_pairs {
287 let ek = escape_zig(k);
288 let ev = escape_zig(v);
289 let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
290 }
291 let _ = writeln!(out, " }};");
292 }
293
294 if let Some(body) = ctx.body {
296 let json_str = serde_json::to_string(body).unwrap_or_default();
297 let escaped = escape_zig(&json_str);
298 let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
299 }
300
301 let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
302 let has_body = ctx.body.is_some();
303
304 let _ = writeln!(
305 out,
306 " var http_client = std.http.Client{{ .allocator = allocator }};"
307 );
308 let _ = writeln!(out, " defer http_client.deinit();");
309 let _ = writeln!(out, " var response_body = std.ArrayList(u8).init(allocator);");
310 let _ = writeln!(out, " defer response_body.deinit();");
311
312 let method_zig = match method.as_str() {
313 "GET" => ".GET",
314 "POST" => ".POST",
315 "PUT" => ".PUT",
316 "DELETE" => ".DELETE",
317 "PATCH" => ".PATCH",
318 "HEAD" => ".HEAD",
319 "OPTIONS" => ".OPTIONS",
320 _ => ".GET",
321 };
322
323 let payload_field = if has_body { ", .payload = body_bytes" } else { "" };
324 let _ = writeln!(
325 out,
326 " const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .response_storage = .{{ .dynamic = &response_body }} }});",
327 rv = ctx.response_var,
328 );
329 }
330
331 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
332 let _ = writeln!(
333 out,
334 " try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
335 );
336 }
337
338 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
339 let ename = escape_zig(&name.to_lowercase());
340 match expected {
341 "<<present>>" => {
342 let _ = writeln!(
343 out,
344 " // assert header '{ename}' is present (header inspection not yet implemented)"
345 );
346 }
347 "<<absent>>" => {
348 let _ = writeln!(
349 out,
350 " // assert header '{ename}' is absent (header inspection not yet implemented)"
351 );
352 }
353 "<<uuid>>" => {
354 let _ = writeln!(
355 out,
356 " // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
357 );
358 }
359 exact => {
360 let evalue = escape_zig(exact);
361 let _ = writeln!(
362 out,
363 " // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
364 );
365 }
366 }
367 }
368
369 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
370 let json_str = serde_json::to_string(expected).unwrap_or_default();
371 let escaped = escape_zig(&json_str);
372 let _ = writeln!(
373 out,
374 " try testing.expectEqualStrings(\"{escaped}\", response_body.items);"
375 );
376 }
377
378 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
379 if let Some(obj) = expected.as_object() {
380 for (key, val) in obj {
381 let ekey = escape_zig(key);
382 let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
383 let _ = writeln!(
384 out,
385 " // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
386 );
387 }
388 }
389 }
390
391 fn render_assert_validation_errors(
392 &self,
393 out: &mut String,
394 _response_var: &str,
395 errors: &[crate::fixture::ValidationErrorExpectation],
396 ) {
397 for ve in errors {
398 let loc = ve.loc.join(".");
399 let escaped_loc = escape_zig(&loc);
400 let escaped_msg = escape_zig(&ve.msg);
401 let _ = writeln!(
402 out,
403 " // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
404 );
405 }
406 }
407}
408
409fn render_http_test_case(out: &mut String, fixture: &Fixture) {
414 client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
415}
416
417#[allow(clippy::too_many_arguments)]
422fn render_test_file(
423 category: &str,
424 fixtures: &[&Fixture],
425 e2e_config: &E2eConfig,
426 function_name: &str,
427 result_var: &str,
428 args: &[crate::config::ArgMapping],
429 field_resolver: &FieldResolver,
430 enum_fields: &HashSet<String>,
431 module_name: &str,
432) -> String {
433 let mut out = String::new();
434 out.push_str(&hash::header(CommentStyle::DoubleSlash));
435 let _ = writeln!(out, "const std = @import(\"std\");");
436 let _ = writeln!(out, "const testing = std.testing;");
437 let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
438 let _ = writeln!(out);
439
440 let _ = writeln!(out, "// E2e tests for category: {category}");
441 let _ = writeln!(out);
442
443 for fixture in fixtures {
444 if fixture.http.is_some() {
445 render_http_test_case(&mut out, fixture);
446 } else {
447 render_test_fn(
448 &mut out,
449 fixture,
450 e2e_config,
451 function_name,
452 result_var,
453 args,
454 field_resolver,
455 enum_fields,
456 module_name,
457 );
458 }
459 let _ = writeln!(out);
460 }
461
462 out
463}
464
465#[allow(clippy::too_many_arguments)]
466fn render_test_fn(
467 out: &mut String,
468 fixture: &Fixture,
469 e2e_config: &E2eConfig,
470 _function_name: &str,
471 _result_var: &str,
472 _args: &[crate::config::ArgMapping],
473 field_resolver: &FieldResolver,
474 enum_fields: &HashSet<String>,
475 module_name: &str,
476) {
477 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
479 let lang = "zig";
480 let call_overrides = call_config.overrides.get(lang);
481 let function_name = call_overrides
482 .and_then(|o| o.function.as_ref())
483 .cloned()
484 .unwrap_or_else(|| call_config.function.clone());
485 let result_var = &call_config.result_var;
486 let args = &call_config.args;
487
488 let test_name = fixture.id.to_snake_case();
489 let description = &fixture.description;
490 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
491
492 let (setup_lines, args_str) = build_args_and_setup(&fixture.input, args, &fixture.id);
493
494 let _ = writeln!(out, "test \"{test_name}\" {{");
495 let _ = writeln!(out, " // {description}");
496
497 let needs_alloc = !setup_lines.is_empty();
499 if needs_alloc {
500 let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
501 let _ = writeln!(out, " defer _ = gpa.deinit();");
502 let _ = writeln!(out, " const allocator = gpa.allocator();");
503 let _ = writeln!(out);
504 }
505
506 for line in &setup_lines {
507 let _ = writeln!(out, " {line}");
508 }
509
510 if expects_error {
511 let _ = writeln!(
513 out,
514 " // TODO: call {module_name}.{function_name}({args_str}) and assert error"
515 );
516 let _ = writeln!(out, " _ = testing;");
517 let _ = writeln!(out, "}}");
518 return;
519 }
520
521 if fixture.assertions.is_empty() {
522 let _ = writeln!(out, " // TODO: call {module_name}.{function_name}({args_str})");
524 let _ = writeln!(out, " _ = testing;");
525 } else {
526 let _ = writeln!(
527 out,
528 " const {result_var} = {module_name}.{function_name}({args_str});"
529 );
530 for assertion in &fixture.assertions {
531 render_assertion(out, assertion, result_var, field_resolver, enum_fields);
532 }
533 }
534
535 let _ = writeln!(out, "}}");
536}
537
538fn build_args_and_setup(
540 input: &serde_json::Value,
541 args: &[crate::config::ArgMapping],
542 fixture_id: &str,
543) -> (Vec<String>, String) {
544 if args.is_empty() {
545 return (Vec::new(), String::new());
546 }
547
548 let mut setup_lines: Vec<String> = Vec::new();
549 let mut parts: Vec<String> = Vec::new();
550
551 for arg in args {
552 if arg.arg_type == "mock_url" {
553 setup_lines.push(format!(
554 "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)",
555 arg.name,
556 ));
557 parts.push(arg.name.clone());
558 continue;
559 }
560
561 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
562 let val = input.get(field);
563 match val {
564 None | Some(serde_json::Value::Null) if arg.optional => {
565 continue;
566 }
567 None | Some(serde_json::Value::Null) => {
568 let default_val = match arg.arg_type.as_str() {
569 "string" => "\"\"".to_string(),
570 "int" | "integer" => "0".to_string(),
571 "float" | "number" => "0.0".to_string(),
572 "bool" | "boolean" => "false".to_string(),
573 _ => "null".to_string(),
574 };
575 parts.push(default_val);
576 }
577 Some(v) => {
578 parts.push(json_to_zig(v));
579 }
580 }
581 }
582
583 (setup_lines, parts.join(", "))
584}
585
586fn render_assertion(
587 out: &mut String,
588 assertion: &Assertion,
589 result_var: &str,
590 field_resolver: &FieldResolver,
591 enum_fields: &HashSet<String>,
592) {
593 if let Some(f) = &assertion.field {
595 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
596 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
597 return;
598 }
599 }
600
601 let _field_is_enum = assertion
603 .field
604 .as_deref()
605 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
606
607 let field_expr = match &assertion.field {
608 Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
609 _ => result_var.to_string(),
610 };
611
612 match assertion.assertion_type.as_str() {
613 "equals" => {
614 if let Some(expected) = &assertion.value {
615 let zig_val = json_to_zig(expected);
616 let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
617 }
618 }
619 "contains" => {
620 if let Some(expected) = &assertion.value {
621 let zig_val = json_to_zig(expected);
622 let _ = writeln!(
623 out,
624 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
625 );
626 }
627 }
628 "contains_all" => {
629 if let Some(values) = &assertion.values {
630 for val in values {
631 let zig_val = json_to_zig(val);
632 let _ = writeln!(
633 out,
634 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
635 );
636 }
637 }
638 }
639 "not_contains" => {
640 if let Some(expected) = &assertion.value {
641 let zig_val = json_to_zig(expected);
642 let _ = writeln!(
643 out,
644 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
645 );
646 }
647 }
648 "not_empty" => {
649 let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
650 }
651 "is_empty" => {
652 let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
653 }
654 "starts_with" => {
655 if let Some(expected) = &assertion.value {
656 let zig_val = json_to_zig(expected);
657 let _ = writeln!(
658 out,
659 " try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
660 );
661 }
662 }
663 "ends_with" => {
664 if let Some(expected) = &assertion.value {
665 let zig_val = json_to_zig(expected);
666 let _ = writeln!(
667 out,
668 " try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
669 );
670 }
671 }
672 "min_length" => {
673 if let Some(val) = &assertion.value {
674 if let Some(n) = val.as_u64() {
675 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
676 }
677 }
678 }
679 "max_length" => {
680 if let Some(val) = &assertion.value {
681 if let Some(n) = val.as_u64() {
682 let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
683 }
684 }
685 }
686 "count_min" => {
687 if let Some(val) = &assertion.value {
688 if let Some(n) = val.as_u64() {
689 let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
690 }
691 }
692 }
693 "count_equals" => {
694 if let Some(val) = &assertion.value {
695 if let Some(n) = val.as_u64() {
696 let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
697 }
698 }
699 }
700 "is_true" => {
701 let _ = writeln!(out, " try testing.expect({field_expr});");
702 }
703 "is_false" => {
704 let _ = writeln!(out, " try testing.expect(!{field_expr});");
705 }
706 "not_error" => {
707 }
709 "error" => {
710 }
712 "greater_than" => {
713 if let Some(val) = &assertion.value {
714 let zig_val = json_to_zig(val);
715 let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
716 }
717 }
718 "less_than" => {
719 if let Some(val) = &assertion.value {
720 let zig_val = json_to_zig(val);
721 let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
722 }
723 }
724 "greater_than_or_equal" => {
725 if let Some(val) = &assertion.value {
726 let zig_val = json_to_zig(val);
727 let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
728 }
729 }
730 "less_than_or_equal" => {
731 if let Some(val) = &assertion.value {
732 let zig_val = json_to_zig(val);
733 let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
734 }
735 }
736 "contains_any" => {
737 if let Some(values) = &assertion.values {
738 for val in values {
739 let zig_val = json_to_zig(val);
740 let _ = writeln!(
741 out,
742 " try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
743 );
744 }
745 }
746 }
747 "matches_regex" => {
748 let _ = writeln!(out, " // regex match not yet implemented for Zig");
749 }
750 "method_result" => {
751 let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
752 }
753 other => {
754 panic!("Zig e2e generator: unsupported assertion type: {other}");
755 }
756 }
757}
758
759fn json_to_zig(value: &serde_json::Value) -> String {
761 match value {
762 serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
763 serde_json::Value::Bool(b) => b.to_string(),
764 serde_json::Value::Number(n) => n.to_string(),
765 serde_json::Value::Null => "null".to_string(),
766 serde_json::Value::Array(arr) => {
767 let items: Vec<String> = arr.iter().map(json_to_zig).collect();
768 format!("&.{{{}}}", items.join(", "))
769 }
770 serde_json::Value::Object(_) => {
771 let json_str = serde_json::to_string(value).unwrap_or_default();
772 format!("\"{}\"", escape_zig(&json_str))
773 }
774 }
775}