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::AlefConfig;
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 alef_config: &AlefConfig,
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(|| alef_config.crate_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| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
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 .filter(|f| {
107 if f.is_http_test() {
108 true
109 } else {
110 let call_cfg = e2e_config.resolve_call(f.call.as_deref());
111 call_cfg.overrides.contains_key(lang)
112 }
113 })
114 .collect();
115
116 if active.is_empty() {
117 continue;
118 }
119
120 let filename = format!("{}_test.gleam", sanitize_filename(&group.category));
121 let field_resolver = FieldResolver::new(
122 &e2e_config.fields,
123 &e2e_config.fields_optional,
124 &e2e_config.result_fields,
125 &e2e_config.fields_array,
126 );
127 let content = render_test_file(
128 &group.category,
129 &active,
130 e2e_config,
131 &module_path,
132 &function_name,
133 result_var,
134 &e2e_config.call.args,
135 &field_resolver,
136 &e2e_config.fields_enum,
137 );
138 files.push(GeneratedFile {
139 path: output_base.join("test").join(filename),
140 content,
141 generated_header: true,
142 });
143 any_tests = true;
144 }
145
146 let entry = if any_tests {
151 concat!(
152 "// Generated by alef. Do not edit by hand.\n",
153 "import gleeunit\n",
154 "\n",
155 "pub fn main() {\n",
156 " gleeunit.main()\n",
157 "}\n",
158 )
159 .to_string()
160 } else {
161 concat!(
162 "// Generated by alef. Do not edit by hand.\n",
163 "// No fixture-driven tests for Gleam — e2e tests require HTTP fixtures\n",
164 "// or non-HTTP fixtures with gleam-specific call overrides.\n",
165 "import gleeunit\n",
166 "import gleeunit/should\n",
167 "\n",
168 "pub fn main() {\n",
169 " gleeunit.main()\n",
170 "}\n",
171 "\n",
172 "pub fn compilation_smoke_test() {\n",
173 " True |> should.equal(True)\n",
174 "}\n",
175 )
176 .to_string()
177 };
178 files.push(GeneratedFile {
179 path: output_base.join("test").join("e2e_gleam_test.gleam"),
180 content: entry,
181 generated_header: false,
182 });
183
184 Ok(files)
185 }
186
187 fn language_name(&self) -> &'static str {
188 "gleam"
189 }
190}
191
192fn render_gleam_toml(pkg_path: &str, pkg_name: &str, dep_mode: crate::config::DependencyMode) -> String {
197 use alef_core::template_versions::hex;
198 let stdlib = hex::GLEAM_STDLIB_VERSION_RANGE;
199 let gleeunit = hex::GLEEUNIT_VERSION_RANGE;
200 let gleam_httpc = hex::GLEAM_HTTPC_VERSION_RANGE;
201 let envoy = hex::ENVOY_VERSION_RANGE;
202 let deps = match dep_mode {
203 crate::config::DependencyMode::Registry => {
204 format!(
205 r#"{pkg_name} = ">= 0.1.0"
206gleam_stdlib = "{stdlib}"
207gleeunit = "{gleeunit}"
208gleam_httpc = "{gleam_httpc}"
209gleam_http = ">= 4.0.0 and < 5.0.0"
210envoy = "{envoy}""#
211 )
212 }
213 crate::config::DependencyMode::Local => {
214 format!(
215 r#"{pkg_name} = {{ path = "{pkg_path}" }}
216gleam_stdlib = "{stdlib}"
217gleeunit = "{gleeunit}"
218gleam_httpc = "{gleam_httpc}"
219gleam_http = ">= 4.0.0 and < 5.0.0"
220envoy = "{envoy}""#
221 )
222 }
223 };
224
225 format!(
226 r#"name = "e2e_gleam"
227version = "0.1.0"
228target = "erlang"
229
230[dependencies]
231{deps}
232"#
233 )
234}
235
236#[allow(clippy::too_many_arguments)]
237fn render_test_file(
238 _category: &str,
239 fixtures: &[&Fixture],
240 e2e_config: &E2eConfig,
241 module_path: &str,
242 function_name: &str,
243 result_var: &str,
244 args: &[crate::config::ArgMapping],
245 field_resolver: &FieldResolver,
246 enum_fields: &HashSet<String>,
247) -> String {
248 let mut out = String::new();
249 out.push_str(&hash::header(CommentStyle::DoubleSlash));
250 let _ = writeln!(out, "import gleeunit");
251 let _ = writeln!(out, "import gleeunit/should");
252
253 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
255
256 if has_http_fixtures {
258 let _ = writeln!(out, "import gleam/httpc");
259 let _ = writeln!(out, "import gleam/http");
260 let _ = writeln!(out, "import gleam/http/request");
261 let _ = writeln!(out, "import gleam/list");
262 let _ = writeln!(out, "import gleam/result");
263 let _ = writeln!(out, "import gleam/string");
264 let _ = writeln!(out, "import envoy");
265 }
266
267 let has_non_http_with_override = fixtures.iter().any(|f| !f.is_http_test());
269 if has_non_http_with_override {
270 let _ = writeln!(out, "import {module_path}");
271 }
272 let _ = writeln!(out);
273
274 let mut needed_modules: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();
276
277 for fixture in fixtures {
279 if fixture.is_http_test() {
280 continue; }
282 for assertion in &fixture.assertions {
283 match assertion.assertion_type.as_str() {
284 "contains" | "contains_all" | "not_contains" | "starts_with" | "ends_with" | "min_length"
285 | "max_length" | "contains_any" => {
286 needed_modules.insert("string");
287 }
288 "not_empty" | "is_empty" | "count_min" | "count_equals" => {
289 needed_modules.insert("list");
290 }
291 "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
292 needed_modules.insert("int");
293 }
294 _ => {}
295 }
296 }
297 }
298
299 for module in &needed_modules {
301 let _ = writeln!(out, "import gleam/{module}");
302 }
303
304 if !needed_modules.is_empty() {
305 let _ = writeln!(out);
306 }
307
308 for fixture in fixtures {
310 if fixture.is_http_test() {
311 render_http_test_case(&mut out, fixture);
312 } else {
313 render_test_case(
314 &mut out,
315 fixture,
316 e2e_config,
317 module_path,
318 function_name,
319 result_var,
320 args,
321 field_resolver,
322 enum_fields,
323 );
324 }
325 let _ = writeln!(out);
326 }
327
328 out
329}
330
331struct GleamTestClientRenderer;
336
337impl client::TestClientRenderer for GleamTestClientRenderer {
338 fn language_name(&self) -> &'static str {
339 "gleam"
340 }
341
342 fn sanitize_test_name(&self, id: &str) -> String {
347 let raw = sanitize_ident(id);
348 let stripped = raw.trim_start_matches(|c: char| c == '_' || c.is_ascii_digit());
349 if stripped.is_empty() { raw } else { stripped.to_string() }
350 }
351
352 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
358 let _ = writeln!(out, "// {description}");
359 let _ = writeln!(out, "pub fn {fn_name}_test() {{");
360 if let Some(reason) = skip_reason {
361 let escaped = escape_gleam(reason);
364 let _ = writeln!(out, " // skipped: {escaped}");
365 let _ = writeln!(out, " Nil");
366 }
367 }
368
369 fn render_test_close(&self, out: &mut String) {
371 let _ = writeln!(out, "}}");
372 }
373
374 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
380 let path = ctx.path;
381
382 let _ = writeln!(out, " let base_url = case envoy.get(\"MOCK_SERVER_URL\") {{");
384 let _ = writeln!(out, " Ok(u) -> u");
385 let _ = writeln!(out, " Error(_) -> \"http://localhost:8080\"");
386 let _ = writeln!(out, " }}");
387
388 let _ = writeln!(out, " let assert Ok(req) = request.to(base_url <> \"{path}\")");
390
391 let method_const = match ctx.method.to_uppercase().as_str() {
393 "GET" => "Get",
394 "POST" => "Post",
395 "PUT" => "Put",
396 "DELETE" => "Delete",
397 "PATCH" => "Patch",
398 "HEAD" => "Head",
399 "OPTIONS" => "Options",
400 _ => "Post",
401 };
402 let _ = writeln!(out, " let req = request.set_method(req, http.{method_const})");
403
404 if ctx.body.is_some() {
406 let content_type = ctx.content_type.unwrap_or("application/json");
407 let escaped_ct = escape_gleam(content_type);
408 let _ = writeln!(
409 out,
410 " let req = request.set_header(req, \"content-type\", \"{escaped_ct}\")"
411 );
412 }
413
414 for (name, value) in ctx.headers {
416 let lower = name.to_lowercase();
417 if matches!(lower.as_str(), "content-length" | "host" | "transfer-encoding") {
418 continue;
419 }
420 let escaped_name = escape_gleam(name);
421 let escaped_value = escape_gleam(value);
422 let _ = writeln!(
423 out,
424 " let req = request.set_header(req, \"{escaped_name}\", \"{escaped_value}\")"
425 );
426 }
427
428 if !ctx.cookies.is_empty() {
430 let cookie_str: Vec<String> = ctx.cookies.iter().map(|(k, v)| format!("{k}={v}")).collect();
431 let escaped_cookie = escape_gleam(&cookie_str.join("; "));
432 let _ = writeln!(
433 out,
434 " let req = request.set_header(req, \"cookie\", \"{escaped_cookie}\")"
435 );
436 }
437
438 if let Some(body) = ctx.body {
440 let json_str = serde_json::to_string(body).unwrap_or_default();
441 let escaped = escape_gleam(&json_str);
442 let _ = writeln!(out, " let req = request.set_body(req, \"{escaped}\")");
443 }
444
445 let resp = ctx.response_var;
447 let _ = writeln!(out, " let assert Ok({resp}) = httpc.send(req)");
448 }
449
450 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
452 let _ = writeln!(out, " {response_var}.status |> should.equal({status})");
453 }
454
455 fn render_assert_header(&self, out: &mut String, response_var: &str, name: &str, expected: &str) {
461 let escaped_name = escape_gleam(&name.to_lowercase());
462 match expected {
463 "<<absent>>" => {
464 let _ = writeln!(
465 out,
466 " {response_var}.headers\n |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n |> result.is_ok()\n |> should.be_false()"
467 );
468 }
469 "<<present>>" | "<<uuid>>" => {
470 let _ = writeln!(
472 out,
473 " {response_var}.headers\n |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n |> result.is_ok()\n |> should.be_true()"
474 );
475 }
476 literal => {
477 let _escaped_value = escape_gleam(literal);
480 let _ = writeln!(
481 out,
482 " {response_var}.headers\n |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n |> result.is_ok()\n |> should.be_true()"
483 );
484 }
485 }
486 }
487
488 fn render_assert_json_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
494 let escaped = match expected {
495 serde_json::Value::String(s) => escape_gleam(s),
496 other => escape_gleam(&serde_json::to_string(other).unwrap_or_default()),
497 };
498 let _ = writeln!(
499 out,
500 " {response_var}.body |> string.trim |> should.equal(\"{escaped}\")"
501 );
502 }
503
504 fn render_assert_partial_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
510 if let Some(obj) = expected.as_object() {
511 for (key, val) in obj {
512 let fragment = escape_gleam(&format!("\"{}\":", key));
513 let _ = writeln!(
514 out,
515 " {response_var}.body |> string.contains(\"{fragment}\") |> should.equal(True)"
516 );
517 let _ = val; }
519 }
520 }
521
522 fn render_assert_validation_errors(
528 &self,
529 out: &mut String,
530 response_var: &str,
531 errors: &[ValidationErrorExpectation],
532 ) {
533 for err in errors {
534 let escaped_msg = escape_gleam(&err.msg);
535 let _ = writeln!(
536 out,
537 " {response_var}.body |> string.contains(\"{escaped_msg}\") |> should.equal(True)"
538 );
539 }
540 }
541}
542
543fn render_http_test_case(out: &mut String, fixture: &Fixture) {
549 client::http_call::render_http_test(out, &GleamTestClientRenderer, fixture);
550}
551
552#[allow(clippy::too_many_arguments)]
553fn render_test_case(
554 out: &mut String,
555 fixture: &Fixture,
556 e2e_config: &E2eConfig,
557 module_path: &str,
558 _function_name: &str,
559 _result_var: &str,
560 _args: &[crate::config::ArgMapping],
561 field_resolver: &FieldResolver,
562 enum_fields: &HashSet<String>,
563) {
564 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
566 let lang = "gleam";
567 let call_overrides = call_config.overrides.get(lang);
568 let function_name = call_overrides
569 .and_then(|o| o.function.as_ref())
570 .cloned()
571 .unwrap_or_else(|| call_config.function.clone());
572 let result_var = &call_config.result_var;
573 let args = &call_config.args;
574
575 let raw_name = sanitize_ident(&fixture.id);
580 let stripped = raw_name.trim_start_matches(|c: char| c == '_' || c.is_ascii_digit());
581 let test_name = if stripped.is_empty() {
582 raw_name.as_str()
583 } else {
584 stripped
585 };
586 let description = &fixture.description;
587 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
588
589 let (setup_lines, args_str) = build_args_and_setup(&fixture.input, args, &fixture.id);
590
591 let _ = writeln!(out, "// {description}");
594 let _ = writeln!(out, "pub fn {test_name}_test() {{");
595
596 for line in &setup_lines {
597 let _ = writeln!(out, " {line}");
598 }
599
600 if expects_error {
601 let _ = writeln!(out, " {module_path}.{function_name}({args_str}) |> should.be_error()");
602 let _ = writeln!(out, "}}");
603 return;
604 }
605
606 let _ = writeln!(out, " let {result_var} = {module_path}.{function_name}({args_str})");
607 let _ = writeln!(out, " {result_var} |> should.be_ok()");
608
609 for assertion in &fixture.assertions {
610 render_assertion(out, assertion, result_var, field_resolver, enum_fields);
611 }
612
613 let _ = writeln!(out, "}}");
614}
615
616fn build_args_and_setup(
618 input: &serde_json::Value,
619 args: &[crate::config::ArgMapping],
620 fixture_id: &str,
621) -> (Vec<String>, String) {
622 if args.is_empty() {
623 return (Vec::new(), String::new());
624 }
625
626 let mut setup_lines: Vec<String> = Vec::new();
627 let mut parts: Vec<String> = Vec::new();
628
629 for arg in args {
630 if arg.arg_type == "mock_url" {
631 setup_lines.push(format!(
632 "let {} = (import \"os\" as os).get_env(\"MOCK_SERVER_URL\") <> \"/fixtures/{fixture_id}\"",
633 arg.name,
634 ));
635 parts.push(arg.name.clone());
636 continue;
637 }
638
639 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
640 let val = input.get(field);
641 match val {
642 None | Some(serde_json::Value::Null) if arg.optional => {
643 continue;
644 }
645 None | Some(serde_json::Value::Null) => {
646 let default_val = match arg.arg_type.as_str() {
647 "string" => "\"\"".to_string(),
648 "int" | "integer" => "0".to_string(),
649 "float" | "number" => "0.0".to_string(),
650 "bool" | "boolean" => "False".to_string(),
651 _ => "Nil".to_string(),
652 };
653 parts.push(default_val);
654 }
655 Some(v) => {
656 parts.push(json_to_gleam(v));
657 }
658 }
659 }
660
661 (setup_lines, parts.join(", "))
662}
663
664fn render_assertion(
665 out: &mut String,
666 assertion: &Assertion,
667 result_var: &str,
668 field_resolver: &FieldResolver,
669 enum_fields: &HashSet<String>,
670) {
671 if let Some(f) = &assertion.field {
673 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
674 let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
675 return;
676 }
677 }
678
679 let _field_is_enum = assertion
681 .field
682 .as_deref()
683 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
684
685 let field_expr = match &assertion.field {
686 Some(f) if !f.is_empty() => field_resolver.accessor(f, "gleam", result_var),
687 _ => result_var.to_string(),
688 };
689
690 match assertion.assertion_type.as_str() {
691 "equals" => {
692 if let Some(expected) = &assertion.value {
693 let gleam_val = json_to_gleam(expected);
694 let _ = writeln!(out, " {field_expr} |> should.equal({gleam_val})");
695 }
696 }
697 "contains" => {
698 if let Some(expected) = &assertion.value {
699 let gleam_val = json_to_gleam(expected);
700 let _ = writeln!(
701 out,
702 " {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
703 );
704 }
705 }
706 "contains_all" => {
707 if let Some(values) = &assertion.values {
708 for val in values {
709 let gleam_val = json_to_gleam(val);
710 let _ = writeln!(
711 out,
712 " {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
713 );
714 }
715 }
716 }
717 "not_contains" => {
718 if let Some(expected) = &assertion.value {
719 let gleam_val = json_to_gleam(expected);
720 let _ = writeln!(
721 out,
722 " {field_expr} |> string.contains({gleam_val}) |> should.equal(False)"
723 );
724 }
725 }
726 "not_empty" => {
727 let _ = writeln!(out, " {field_expr} |> list.is_empty |> should.equal(False)");
728 }
729 "is_empty" => {
730 let _ = writeln!(out, " {field_expr} |> list.is_empty |> should.equal(True)");
731 }
732 "starts_with" => {
733 if let Some(expected) = &assertion.value {
734 let gleam_val = json_to_gleam(expected);
735 let _ = writeln!(
736 out,
737 " {field_expr} |> string.starts_with({gleam_val}) |> should.equal(True)"
738 );
739 }
740 }
741 "ends_with" => {
742 if let Some(expected) = &assertion.value {
743 let gleam_val = json_to_gleam(expected);
744 let _ = writeln!(
745 out,
746 " {field_expr} |> string.ends_with({gleam_val}) |> should.equal(True)"
747 );
748 }
749 }
750 "min_length" => {
751 if let Some(val) = &assertion.value {
752 if let Some(n) = val.as_u64() {
753 let _ = writeln!(
754 out,
755 " {field_expr} |> string.length |> int.is_at_least({n}) |> should.equal(True)"
756 );
757 }
758 }
759 }
760 "max_length" => {
761 if let Some(val) = &assertion.value {
762 if let Some(n) = val.as_u64() {
763 let _ = writeln!(
764 out,
765 " {field_expr} |> string.length |> int.is_at_most({n}) |> should.equal(True)"
766 );
767 }
768 }
769 }
770 "count_min" => {
771 if let Some(val) = &assertion.value {
772 if let Some(n) = val.as_u64() {
773 let _ = writeln!(
774 out,
775 " {field_expr} |> list.length |> int.is_at_least({n}) |> should.equal(True)"
776 );
777 }
778 }
779 }
780 "count_equals" => {
781 if let Some(val) = &assertion.value {
782 if let Some(n) = val.as_u64() {
783 let _ = writeln!(out, " {field_expr} |> list.length |> should.equal({n})");
784 }
785 }
786 }
787 "is_true" => {
788 let _ = writeln!(out, " {field_expr} |> should.equal(True)");
789 }
790 "is_false" => {
791 let _ = writeln!(out, " {field_expr} |> should.equal(False)");
792 }
793 "not_error" => {
794 }
796 "error" => {
797 }
799 "greater_than" => {
800 if let Some(val) = &assertion.value {
801 let gleam_val = json_to_gleam(val);
802 let _ = writeln!(
803 out,
804 " {field_expr} |> int.is_strictly_greater_than({gleam_val}) |> should.equal(True)"
805 );
806 }
807 }
808 "less_than" => {
809 if let Some(val) = &assertion.value {
810 let gleam_val = json_to_gleam(val);
811 let _ = writeln!(
812 out,
813 " {field_expr} |> int.is_strictly_less_than({gleam_val}) |> should.equal(True)"
814 );
815 }
816 }
817 "greater_than_or_equal" => {
818 if let Some(val) = &assertion.value {
819 let gleam_val = json_to_gleam(val);
820 let _ = writeln!(
821 out,
822 " {field_expr} |> int.is_at_least({gleam_val}) |> should.equal(True)"
823 );
824 }
825 }
826 "less_than_or_equal" => {
827 if let Some(val) = &assertion.value {
828 let gleam_val = json_to_gleam(val);
829 let _ = writeln!(
830 out,
831 " {field_expr} |> int.is_at_most({gleam_val}) |> should.equal(True)"
832 );
833 }
834 }
835 "contains_any" => {
836 if let Some(values) = &assertion.values {
837 for val in values {
838 let gleam_val = json_to_gleam(val);
839 let _ = writeln!(
840 out,
841 " {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
842 );
843 }
844 }
845 }
846 "matches_regex" => {
847 let _ = writeln!(out, " // regex match not yet implemented for Gleam");
848 }
849 "method_result" => {
850 let _ = writeln!(out, " // method_result assertions not yet implemented for Gleam");
851 }
852 other => {
853 panic!("Gleam e2e generator: unsupported assertion type: {other}");
854 }
855 }
856}
857
858fn json_to_gleam(value: &serde_json::Value) -> String {
860 match value {
861 serde_json::Value::String(s) => format!("\"{}\"", escape_gleam(s)),
862 serde_json::Value::Bool(b) => {
863 if *b {
864 "True".to_string()
865 } else {
866 "False".to_string()
867 }
868 }
869 serde_json::Value::Number(n) => n.to_string(),
870 serde_json::Value::Null => "Nil".to_string(),
871 serde_json::Value::Array(arr) => {
872 let items: Vec<String> = arr.iter().map(json_to_gleam).collect();
873 format!("[{}]", items.join(", "))
874 }
875 serde_json::Value::Object(_) => {
876 let json_str = serde_json::to_string(value).unwrap_or_default();
877 format!("\"{}\"", escape_gleam(&json_str))
878 }
879 }
880}