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