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