1use crate::config::E2eConfig;
9use crate::escape::{escape_gleam, sanitize_filename, sanitize_ident};
10use crate::field_access::FieldResolver;
11use crate::fixture::{Assertion, Fixture, FixtureGroup, ValidationErrorExpectation};
12use alef_core::backend::GeneratedFile;
13use alef_core::config::ResolvedCrateConfig;
14use alef_core::hash::{self, CommentStyle};
15use anyhow::Result;
16use heck::ToSnakeCase;
17use std::collections::HashSet;
18use std::fmt::Write as FmtWrite;
19use std::path::PathBuf;
20
21use super::E2eCodegen;
22use super::client;
23
24pub struct GleamE2eCodegen;
26
27impl E2eCodegen for GleamE2eCodegen {
28 fn generate(
29 &self,
30 groups: &[FixtureGroup],
31 e2e_config: &E2eConfig,
32 config: &ResolvedCrateConfig,
33 ) -> Result<Vec<GeneratedFile>> {
34 let lang = self.language_name();
35 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
36
37 let mut files = Vec::new();
38
39 let call = &e2e_config.call;
41 let overrides = call.overrides.get(lang);
42 let module_path = overrides
43 .and_then(|o| o.module.as_ref())
44 .cloned()
45 .unwrap_or_else(|| call.module.clone());
46 let function_name = overrides
47 .and_then(|o| o.function.as_ref())
48 .cloned()
49 .unwrap_or_else(|| call.function.clone());
50 let result_var = &call.result_var;
51
52 let gleam_pkg = e2e_config.resolve_package("gleam");
54 let pkg_path = gleam_pkg
55 .as_ref()
56 .and_then(|p| p.path.as_ref())
57 .cloned()
58 .unwrap_or_else(|| "../../packages/gleam".to_string());
59 let pkg_name = gleam_pkg
60 .as_ref()
61 .and_then(|p| p.name.as_ref())
62 .cloned()
63 .unwrap_or_else(|| config.name.to_snake_case());
64
65 files.push(GeneratedFile {
67 path: output_base.join("gleam.toml"),
68 content: render_gleam_toml(&pkg_path, &pkg_name, e2e_config.dep_mode),
69 generated_header: false,
70 });
71
72 files.push(GeneratedFile {
75 path: output_base.join("src").join("e2e_gleam.gleam"),
76 content: "// Generated by alef. Do not edit by hand.\n// Placeholder module — e2e tests live in test/.\npub fn placeholder() -> Nil {\n Nil\n}\n".to_string(),
77 generated_header: false,
78 });
79
80 let mut any_tests = false;
82
83 for group in groups {
85 let active: Vec<&Fixture> = group
86 .fixtures
87 .iter()
88 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
90 .filter(|f| {
94 if let Some(http) = &f.http {
95 let has_upgrade = http
96 .request
97 .headers
98 .iter()
99 .any(|(k, v)| k.eq_ignore_ascii_case("upgrade") && v.eq_ignore_ascii_case("websocket"));
100 !has_upgrade
101 } else {
102 true
103 }
104 })
105 .collect();
108
109 if active.is_empty() {
110 continue;
111 }
112
113 let filename = format!("{}_test.gleam", sanitize_filename(&group.category));
114 let field_resolver = FieldResolver::new(
115 &e2e_config.fields,
116 &e2e_config.fields_optional,
117 &e2e_config.result_fields,
118 &e2e_config.fields_array,
119 &HashSet::new(),
120 );
121 let content = render_test_file(
122 &group.category,
123 &active,
124 e2e_config,
125 &module_path,
126 &function_name,
127 result_var,
128 &e2e_config.call.args,
129 &field_resolver,
130 &e2e_config.fields_enum,
131 );
132 files.push(GeneratedFile {
133 path: output_base.join("test").join(filename),
134 content,
135 generated_header: true,
136 });
137 any_tests = true;
138 }
139
140 let entry = if any_tests {
145 concat!(
146 "// Generated by alef. Do not edit by hand.\n",
147 "import gleeunit\n",
148 "\n",
149 "pub fn main() {\n",
150 " gleeunit.main()\n",
151 "}\n",
152 )
153 .to_string()
154 } else {
155 concat!(
156 "// Generated by alef. Do not edit by hand.\n",
157 "// No fixture-driven tests for Gleam — e2e tests require HTTP fixtures\n",
158 "// or non-HTTP fixtures with gleam-specific call overrides.\n",
159 "import gleeunit\n",
160 "import gleeunit/should\n",
161 "\n",
162 "pub fn main() {\n",
163 " gleeunit.main()\n",
164 "}\n",
165 "\n",
166 "pub fn compilation_smoke_test() {\n",
167 " True |> should.equal(True)\n",
168 "}\n",
169 )
170 .to_string()
171 };
172 files.push(GeneratedFile {
173 path: output_base.join("test").join("e2e_gleam_test.gleam"),
174 content: entry,
175 generated_header: false,
176 });
177
178 Ok(files)
179 }
180
181 fn language_name(&self) -> &'static str {
182 "gleam"
183 }
184}
185
186fn render_gleam_toml(pkg_path: &str, pkg_name: &str, dep_mode: crate::config::DependencyMode) -> String {
191 use alef_core::template_versions::hex;
192 let stdlib = hex::GLEAM_STDLIB_VERSION_RANGE;
193 let gleeunit = hex::GLEEUNIT_VERSION_RANGE;
194 let gleam_httpc = hex::GLEAM_HTTPC_VERSION_RANGE;
195 let envoy = hex::ENVOY_VERSION_RANGE;
196 let deps = match dep_mode {
197 crate::config::DependencyMode::Registry => {
198 format!(
199 r#"{pkg_name} = ">= 0.1.0"
200gleam_stdlib = "{stdlib}"
201gleeunit = "{gleeunit}"
202gleam_httpc = "{gleam_httpc}"
203gleam_http = ">= 4.0.0 and < 5.0.0"
204envoy = "{envoy}""#
205 )
206 }
207 crate::config::DependencyMode::Local => {
208 format!(
209 r#"{pkg_name} = {{ path = "{pkg_path}" }}
210gleam_stdlib = "{stdlib}"
211gleeunit = "{gleeunit}"
212gleam_httpc = "{gleam_httpc}"
213gleam_http = ">= 4.0.0 and < 5.0.0"
214envoy = "{envoy}""#
215 )
216 }
217 };
218
219 format!(
220 r#"name = "e2e_gleam"
221version = "0.1.0"
222target = "erlang"
223
224[dependencies]
225{deps}
226"#
227 )
228}
229
230#[allow(clippy::too_many_arguments)]
231fn render_test_file(
232 _category: &str,
233 fixtures: &[&Fixture],
234 e2e_config: &E2eConfig,
235 module_path: &str,
236 function_name: &str,
237 result_var: &str,
238 args: &[crate::config::ArgMapping],
239 field_resolver: &FieldResolver,
240 enum_fields: &HashSet<String>,
241) -> String {
242 let mut out = String::new();
243 out.push_str(&hash::header(CommentStyle::DoubleSlash));
244 let _ = writeln!(out, "import gleeunit");
245 let _ = writeln!(out, "import gleeunit/should");
246
247 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
249
250 if has_http_fixtures {
252 let _ = writeln!(out, "import gleam/httpc");
253 let _ = writeln!(out, "import gleam/http");
254 let _ = writeln!(out, "import gleam/http/request");
255 let _ = writeln!(out, "import gleam/list");
256 let _ = writeln!(out, "import gleam/result");
257 let _ = writeln!(out, "import gleam/string");
258 let _ = writeln!(out, "import envoy");
259 }
260
261 let has_non_http_with_override = fixtures.iter().any(|f| !f.is_http_test());
263 if has_non_http_with_override {
264 let _ = writeln!(out, "import {module_path}");
265 }
266 let _ = writeln!(out);
267
268 let mut needed_modules: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();
270
271 for fixture in fixtures {
273 if fixture.is_http_test() {
274 continue; }
276 for assertion in &fixture.assertions {
277 match assertion.assertion_type.as_str() {
278 "contains" | "contains_all" | "not_contains" | "starts_with" | "ends_with" | "min_length"
279 | "max_length" | "contains_any" => {
280 needed_modules.insert("string");
281 }
282 "not_empty" | "is_empty" | "count_min" | "count_equals" => {
283 needed_modules.insert("list");
284 }
285 "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
286 needed_modules.insert("int");
287 }
288 _ => {}
289 }
290 }
291 }
292
293 for module in &needed_modules {
295 let _ = writeln!(out, "import gleam/{module}");
296 }
297
298 if !needed_modules.is_empty() {
299 let _ = writeln!(out);
300 }
301
302 for fixture in fixtures {
304 if fixture.is_http_test() {
305 render_http_test_case(&mut out, fixture);
306 } else {
307 render_test_case(
308 &mut out,
309 fixture,
310 e2e_config,
311 module_path,
312 function_name,
313 result_var,
314 args,
315 field_resolver,
316 enum_fields,
317 );
318 }
319 let _ = writeln!(out);
320 }
321
322 out
323}
324
325struct GleamTestClientRenderer;
330
331impl client::TestClientRenderer for GleamTestClientRenderer {
332 fn language_name(&self) -> &'static str {
333 "gleam"
334 }
335
336 fn sanitize_test_name(&self, id: &str) -> String {
341 let raw = sanitize_ident(id);
342 let stripped = raw.trim_start_matches(|c: char| c == '_' || c.is_ascii_digit());
343 if stripped.is_empty() { raw } else { stripped.to_string() }
344 }
345
346 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
352 let _ = writeln!(out, "// {description}");
353 let _ = writeln!(out, "pub fn {fn_name}_test() {{");
354 if let Some(reason) = skip_reason {
355 let escaped = escape_gleam(reason);
358 let _ = writeln!(out, " // skipped: {escaped}");
359 let _ = writeln!(out, " Nil");
360 }
361 }
362
363 fn render_test_close(&self, out: &mut String) {
365 let _ = writeln!(out, "}}");
366 }
367
368 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
374 let path = ctx.path;
375
376 let _ = writeln!(out, " let base_url = case envoy.get(\"MOCK_SERVER_URL\") {{");
378 let _ = writeln!(out, " Ok(u) -> u");
379 let _ = writeln!(out, " Error(_) -> \"http://localhost:8080\"");
380 let _ = writeln!(out, " }}");
381
382 let _ = writeln!(out, " let assert Ok(req) = request.to(base_url <> \"{path}\")");
384
385 let method_const = match ctx.method.to_uppercase().as_str() {
387 "GET" => "Get",
388 "POST" => "Post",
389 "PUT" => "Put",
390 "DELETE" => "Delete",
391 "PATCH" => "Patch",
392 "HEAD" => "Head",
393 "OPTIONS" => "Options",
394 _ => "Post",
395 };
396 let _ = writeln!(out, " let req = request.set_method(req, http.{method_const})");
397
398 if ctx.body.is_some() {
400 let content_type = ctx.content_type.unwrap_or("application/json");
401 let escaped_ct = escape_gleam(content_type);
402 let _ = writeln!(
403 out,
404 " let req = request.set_header(req, \"content-type\", \"{escaped_ct}\")"
405 );
406 }
407
408 for (name, value) in ctx.headers {
410 let lower = name.to_lowercase();
411 if matches!(lower.as_str(), "content-length" | "host" | "transfer-encoding") {
412 continue;
413 }
414 let escaped_name = escape_gleam(name);
415 let escaped_value = escape_gleam(value);
416 let _ = writeln!(
417 out,
418 " let req = request.set_header(req, \"{escaped_name}\", \"{escaped_value}\")"
419 );
420 }
421
422 if !ctx.cookies.is_empty() {
424 let cookie_str: Vec<String> = ctx.cookies.iter().map(|(k, v)| format!("{k}={v}")).collect();
425 let escaped_cookie = escape_gleam(&cookie_str.join("; "));
426 let _ = writeln!(
427 out,
428 " let req = request.set_header(req, \"cookie\", \"{escaped_cookie}\")"
429 );
430 }
431
432 if let Some(body) = ctx.body {
434 let json_str = serde_json::to_string(body).unwrap_or_default();
435 let escaped = escape_gleam(&json_str);
436 let _ = writeln!(out, " let req = request.set_body(req, \"{escaped}\")");
437 }
438
439 let resp = ctx.response_var;
441 let _ = writeln!(out, " let assert Ok({resp}) = httpc.send(req)");
442 }
443
444 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
446 let _ = writeln!(out, " {response_var}.status |> should.equal({status})");
447 }
448
449 fn render_assert_header(&self, out: &mut String, response_var: &str, name: &str, expected: &str) {
455 let escaped_name = escape_gleam(&name.to_lowercase());
456 match expected {
457 "<<absent>>" => {
458 let _ = writeln!(
459 out,
460 " {response_var}.headers\n |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n |> result.is_ok()\n |> should.be_false()"
461 );
462 }
463 "<<present>>" | "<<uuid>>" => {
464 let _ = writeln!(
466 out,
467 " {response_var}.headers\n |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n |> result.is_ok()\n |> should.be_true()"
468 );
469 }
470 literal => {
471 let _escaped_value = escape_gleam(literal);
474 let _ = writeln!(
475 out,
476 " {response_var}.headers\n |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n |> result.is_ok()\n |> should.be_true()"
477 );
478 }
479 }
480 }
481
482 fn render_assert_json_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
488 let escaped = match expected {
489 serde_json::Value::String(s) => escape_gleam(s),
490 other => escape_gleam(&serde_json::to_string(other).unwrap_or_default()),
491 };
492 let _ = writeln!(
493 out,
494 " {response_var}.body |> string.trim |> should.equal(\"{escaped}\")"
495 );
496 }
497
498 fn render_assert_partial_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
504 if let Some(obj) = expected.as_object() {
505 for (key, val) in obj {
506 let fragment = escape_gleam(&format!("\"{}\":", key));
507 let _ = writeln!(
508 out,
509 " {response_var}.body |> string.contains(\"{fragment}\") |> should.equal(True)"
510 );
511 let _ = val; }
513 }
514 }
515
516 fn render_assert_validation_errors(
522 &self,
523 out: &mut String,
524 response_var: &str,
525 errors: &[ValidationErrorExpectation],
526 ) {
527 for err in errors {
528 let escaped_msg = escape_gleam(&err.msg);
529 let _ = writeln!(
530 out,
531 " {response_var}.body |> string.contains(\"{escaped_msg}\") |> should.equal(True)"
532 );
533 }
534 }
535}
536
537fn render_http_test_case(out: &mut String, fixture: &Fixture) {
543 client::http_call::render_http_test(out, &GleamTestClientRenderer, fixture);
544}
545
546#[allow(clippy::too_many_arguments)]
547fn render_test_case(
548 out: &mut String,
549 fixture: &Fixture,
550 e2e_config: &E2eConfig,
551 module_path: &str,
552 _function_name: &str,
553 _result_var: &str,
554 _args: &[crate::config::ArgMapping],
555 field_resolver: &FieldResolver,
556 enum_fields: &HashSet<String>,
557) {
558 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
560 let lang = "gleam";
561 let call_overrides = call_config.overrides.get(lang);
562 let function_name = call_overrides
563 .and_then(|o| o.function.as_ref())
564 .cloned()
565 .unwrap_or_else(|| call_config.function.clone());
566 let result_var = &call_config.result_var;
567 let args = &call_config.args;
568
569 let raw_name = sanitize_ident(&fixture.id);
574 let stripped = raw_name.trim_start_matches(|c: char| c == '_' || c.is_ascii_digit());
575 let test_name = if stripped.is_empty() {
576 raw_name.as_str()
577 } else {
578 stripped
579 };
580 let description = &fixture.description;
581 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
582
583 let (setup_lines, args_str) = build_args_and_setup(&fixture.input, args, &fixture.id);
584
585 let _ = writeln!(out, "// {description}");
588 let _ = writeln!(out, "pub fn {test_name}_test() {{");
589
590 for line in &setup_lines {
591 let _ = writeln!(out, " {line}");
592 }
593
594 if expects_error {
595 let _ = writeln!(out, " {module_path}.{function_name}({args_str}) |> should.be_error()");
596 let _ = writeln!(out, "}}");
597 return;
598 }
599
600 let _ = writeln!(out, " let {result_var} = {module_path}.{function_name}({args_str})");
601 let _ = writeln!(out, " {result_var} |> should.be_ok()");
602 let _ = writeln!(out, " let assert Ok(r) = {result_var}");
603
604 for assertion in &fixture.assertions {
605 render_assertion(out, assertion, "r", field_resolver, enum_fields);
606 }
607
608 let _ = writeln!(out, "}}");
609}
610
611fn build_args_and_setup(
613 input: &serde_json::Value,
614 args: &[crate::config::ArgMapping],
615 fixture_id: &str,
616) -> (Vec<String>, String) {
617 if args.is_empty() {
618 return (Vec::new(), String::new());
619 }
620
621 let mut setup_lines: Vec<String> = Vec::new();
622 let mut parts: Vec<String> = Vec::new();
623
624 for arg in args {
625 if arg.arg_type == "mock_url" {
626 setup_lines.push(format!(
627 "let {} = (import \"os\" as os).get_env(\"MOCK_SERVER_URL\") <> \"/fixtures/{fixture_id}\"",
628 arg.name,
629 ));
630 parts.push(arg.name.clone());
631 continue;
632 }
633
634 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
635 let val = input.get(field);
636 match val {
637 None | Some(serde_json::Value::Null) if arg.optional => {
638 continue;
639 }
640 None | Some(serde_json::Value::Null) => {
641 let default_val = match arg.arg_type.as_str() {
642 "string" => "\"\"".to_string(),
643 "int" | "integer" => "0".to_string(),
644 "float" | "number" => "0.0".to_string(),
645 "bool" | "boolean" => "False".to_string(),
646 _ => "Nil".to_string(),
647 };
648 parts.push(default_val);
649 }
650 Some(v) => {
651 parts.push(json_to_gleam(v));
652 }
653 }
654 }
655
656 (setup_lines, parts.join(", "))
657}
658
659fn render_assertion(
660 out: &mut String,
661 assertion: &Assertion,
662 result_var: &str,
663 field_resolver: &FieldResolver,
664 enum_fields: &HashSet<String>,
665) {
666 if let Some(f) = &assertion.field {
668 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
669 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
670 return;
671 }
672 }
673
674 let _field_is_enum = assertion
676 .field
677 .as_deref()
678 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
679
680 let field_expr = match &assertion.field {
681 Some(f) if !f.is_empty() => field_resolver.accessor(f, "gleam", result_var),
682 _ => result_var.to_string(),
683 };
684
685 match assertion.assertion_type.as_str() {
686 "equals" => {
687 if let Some(expected) = &assertion.value {
688 let gleam_val = json_to_gleam(expected);
689 let _ = writeln!(out, " {field_expr} |> should.equal({gleam_val})");
690 }
691 }
692 "contains" => {
693 if let Some(expected) = &assertion.value {
694 let gleam_val = json_to_gleam(expected);
695 let _ = writeln!(
696 out,
697 " {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
698 );
699 }
700 }
701 "contains_all" => {
702 if let Some(values) = &assertion.values {
703 for val in values {
704 let gleam_val = json_to_gleam(val);
705 let _ = writeln!(
706 out,
707 " {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
708 );
709 }
710 }
711 }
712 "not_contains" => {
713 if let Some(expected) = &assertion.value {
714 let gleam_val = json_to_gleam(expected);
715 let _ = writeln!(
716 out,
717 " {field_expr} |> string.contains({gleam_val}) |> should.equal(False)"
718 );
719 }
720 }
721 "not_empty" => {
722 let _ = writeln!(out, " {field_expr} |> list.is_empty |> should.equal(False)");
723 }
724 "is_empty" => {
725 let _ = writeln!(out, " {field_expr} |> list.is_empty |> should.equal(True)");
726 }
727 "starts_with" => {
728 if let Some(expected) = &assertion.value {
729 let gleam_val = json_to_gleam(expected);
730 let _ = writeln!(
731 out,
732 " {field_expr} |> string.starts_with({gleam_val}) |> should.equal(True)"
733 );
734 }
735 }
736 "ends_with" => {
737 if let Some(expected) = &assertion.value {
738 let gleam_val = json_to_gleam(expected);
739 let _ = writeln!(
740 out,
741 " {field_expr} |> string.ends_with({gleam_val}) |> should.equal(True)"
742 );
743 }
744 }
745 "min_length" => {
746 if let Some(val) = &assertion.value {
747 if let Some(n) = val.as_u64() {
748 let _ = writeln!(
749 out,
750 " {field_expr} |> string.length |> int.is_at_least({n}) |> should.equal(True)"
751 );
752 }
753 }
754 }
755 "max_length" => {
756 if let Some(val) = &assertion.value {
757 if let Some(n) = val.as_u64() {
758 let _ = writeln!(
759 out,
760 " {field_expr} |> string.length |> int.is_at_most({n}) |> should.equal(True)"
761 );
762 }
763 }
764 }
765 "count_min" => {
766 if let Some(val) = &assertion.value {
767 if let Some(n) = val.as_u64() {
768 let _ = writeln!(
769 out,
770 " {field_expr} |> list.length |> int.is_at_least({n}) |> should.equal(True)"
771 );
772 }
773 }
774 }
775 "count_equals" => {
776 if let Some(val) = &assertion.value {
777 if let Some(n) = val.as_u64() {
778 let _ = writeln!(out, " {field_expr} |> list.length |> should.equal({n})");
779 }
780 }
781 }
782 "is_true" => {
783 let _ = writeln!(out, " {field_expr} |> should.equal(True)");
784 }
785 "is_false" => {
786 let _ = writeln!(out, " {field_expr} |> should.equal(False)");
787 }
788 "not_error" => {
789 }
791 "error" => {
792 }
794 "greater_than" => {
795 if let Some(val) = &assertion.value {
796 let gleam_val = json_to_gleam(val);
797 let _ = writeln!(
798 out,
799 " {field_expr} |> int.is_strictly_greater_than({gleam_val}) |> should.equal(True)"
800 );
801 }
802 }
803 "less_than" => {
804 if let Some(val) = &assertion.value {
805 let gleam_val = json_to_gleam(val);
806 let _ = writeln!(
807 out,
808 " {field_expr} |> int.is_strictly_less_than({gleam_val}) |> should.equal(True)"
809 );
810 }
811 }
812 "greater_than_or_equal" => {
813 if let Some(val) = &assertion.value {
814 let gleam_val = json_to_gleam(val);
815 let _ = writeln!(
816 out,
817 " {field_expr} |> int.is_at_least({gleam_val}) |> should.equal(True)"
818 );
819 }
820 }
821 "less_than_or_equal" => {
822 if let Some(val) = &assertion.value {
823 let gleam_val = json_to_gleam(val);
824 let _ = writeln!(
825 out,
826 " {field_expr} |> int.is_at_most({gleam_val}) |> should.equal(True)"
827 );
828 }
829 }
830 "contains_any" => {
831 if let Some(values) = &assertion.values {
832 for val in values {
833 let gleam_val = json_to_gleam(val);
834 let _ = writeln!(
835 out,
836 " {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
837 );
838 }
839 }
840 }
841 "matches_regex" => {
842 let _ = writeln!(out, " // regex match not yet implemented for Gleam");
843 }
844 "method_result" => {
845 let _ = writeln!(out, " // method_result assertions not yet implemented for Gleam");
846 }
847 other => {
848 panic!("Gleam e2e generator: unsupported assertion type: {other}");
849 }
850 }
851}
852
853fn json_to_gleam(value: &serde_json::Value) -> String {
855 match value {
856 serde_json::Value::String(s) => format!("\"{}\"", escape_gleam(s)),
857 serde_json::Value::Bool(b) => {
858 if *b {
859 "True".to_string()
860 } else {
861 "False".to_string()
862 }
863 }
864 serde_json::Value::Number(n) => n.to_string(),
865 serde_json::Value::Null => "Nil".to_string(),
866 serde_json::Value::Array(arr) => {
867 let items: Vec<String> = arr.iter().map(json_to_gleam).collect();
868 format!("[{}]", items.join(", "))
869 }
870 serde_json::Value::Object(_) => {
871 let json_str = serde_json::to_string(value).unwrap_or_default();
872 format!("\"{}\"", escape_gleam(&json_str))
873 }
874 }
875}