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::{ToPascalCase, 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 _type_defs: &[alef_core::ir::TypeDef],
34 ) -> Result<Vec<GeneratedFile>> {
35 let lang = self.language_name();
36 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
37
38 let mut files = Vec::new();
39
40 let call = &e2e_config.call;
42 let overrides = call.overrides.get(lang);
43 let module_path = overrides
44 .and_then(|o| o.module.as_ref())
45 .cloned()
46 .unwrap_or_else(|| call.module.clone());
47 let function_name = overrides
48 .and_then(|o| o.function.as_ref())
49 .cloned()
50 .unwrap_or_else(|| call.function.clone());
51 let result_var = &call.result_var;
52
53 let gleam_pkg = e2e_config.resolve_package("gleam");
55 let pkg_path = gleam_pkg
56 .as_ref()
57 .and_then(|p| p.path.as_ref())
58 .cloned()
59 .unwrap_or_else(|| "../../packages/gleam".to_string());
60 let pkg_name = gleam_pkg
61 .as_ref()
62 .and_then(|p| p.name.as_ref())
63 .cloned()
64 .unwrap_or_else(|| config.name.to_snake_case());
65
66 files.push(GeneratedFile {
68 path: output_base.join("gleam.toml"),
69 content: render_gleam_toml(&pkg_path, &pkg_name, e2e_config.dep_mode),
70 generated_header: false,
71 });
72
73 let e2e_helpers = concat!(
77 "// Generated by alef. Do not edit by hand.\n",
78 "// E2e helper module — provides file-reading utilities for Gleam tests.\n",
79 "import gleam/dynamic\n",
80 "\n",
81 "/// Read a file into a BitArray via the Erlang :file module.\n",
82 "/// The path is relative to the e2e working directory when `gleam test` runs.\n",
83 "@external(erlang, \"file\", \"read_file\")\n",
84 "pub fn read_file_bytes(path: String) -> Result(BitArray, dynamic.Dynamic)\n",
85 "\n",
86 "/// Ensure the kreuzberg OTP application and all its dependencies are started.\n",
87 "/// This is required when running `gleam test` outside of `mix test`, since the\n",
88 "/// Rustler NIF init hook needs the :kreuzberg application to be started before\n",
89 "/// any Kreuzberg.Native functions can be called.\n",
90 "/// Calls the Erlang shim e2e_startup:start_kreuzberg/0.\n",
91 "@external(erlang, \"e2e_startup\", \"start_kreuzberg\")\n",
92 "pub fn start_kreuzberg() -> Nil\n",
93 );
94 let erlang_startup = concat!(
99 "%% Generated by alef. Do not edit by hand.\n",
100 "%% Starts the kreuzberg OTP application and all its dependencies.\n",
101 "%% Called by e2e_gleam_test.main/0 before gleeunit.main/0.\n",
102 "-module(e2e_startup).\n",
103 "-export([start_kreuzberg/0]).\n",
104 "\n",
105 "start_kreuzberg() ->\n",
106 " %% Elixir runtime must be started before kreuzberg NIF init\n",
107 " %% because Rustler uses Elixir.Application.app_dir/2 to locate the .so.\n",
108 " {ok, _} = application:ensure_all_started(elixir),\n",
109 " {ok, _} = application:ensure_all_started(kreuzberg),\n",
110 " nil.\n",
111 );
112 files.push(GeneratedFile {
113 path: output_base.join("src").join("e2e_gleam.gleam"),
114 content: e2e_helpers.to_string(),
115 generated_header: false,
116 });
117 files.push(GeneratedFile {
118 path: output_base.join("src").join("e2e_startup.erl"),
119 content: erlang_startup.to_string(),
120 generated_header: false,
121 });
122
123 let mut any_tests = false;
125
126 for group in groups {
128 let active: Vec<&Fixture> = group
129 .fixtures
130 .iter()
131 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
133 .filter(|f| {
137 if let Some(http) = &f.http {
138 let has_upgrade = http
139 .request
140 .headers
141 .iter()
142 .any(|(k, v)| k.eq_ignore_ascii_case("upgrade") && v.eq_ignore_ascii_case("websocket"));
143 !has_upgrade
144 } else {
145 true
146 }
147 })
148 .collect();
151
152 if active.is_empty() {
153 continue;
154 }
155
156 let filename = format!("{}_test.gleam", sanitize_filename(&group.category));
157 let field_resolver = FieldResolver::new(
158 &e2e_config.fields,
159 &e2e_config.fields_optional,
160 &e2e_config.result_fields,
161 &e2e_config.fields_array,
162 &e2e_config.fields_method_calls,
163 );
164 let content = render_test_file(
165 &group.category,
166 &active,
167 e2e_config,
168 &module_path,
169 &function_name,
170 result_var,
171 &e2e_config.call.args,
172 &field_resolver,
173 &e2e_config.fields_enum,
174 );
175 files.push(GeneratedFile {
176 path: output_base.join("test").join(filename),
177 content,
178 generated_header: true,
179 });
180 any_tests = true;
181 }
182
183 let entry = if any_tests {
188 concat!(
189 "// Generated by alef. Do not edit by hand.\n",
190 "import gleeunit\n",
191 "import e2e_gleam\n",
192 "\n",
193 "pub fn main() {\n",
194 " let _ = e2e_gleam.start_kreuzberg()\n",
195 " gleeunit.main()\n",
196 "}\n",
197 )
198 .to_string()
199 } else {
200 concat!(
201 "// Generated by alef. Do not edit by hand.\n",
202 "// No fixture-driven tests for Gleam — e2e tests require HTTP fixtures\n",
203 "// or non-HTTP fixtures with gleam-specific call overrides.\n",
204 "import gleeunit\n",
205 "import gleeunit/should\n",
206 "\n",
207 "pub fn main() {\n",
208 " gleeunit.main()\n",
209 "}\n",
210 "\n",
211 "pub fn compilation_smoke_test() {\n",
212 " True |> should.equal(True)\n",
213 "}\n",
214 )
215 .to_string()
216 };
217 files.push(GeneratedFile {
218 path: output_base.join("test").join("e2e_gleam_test.gleam"),
219 content: entry,
220 generated_header: false,
221 });
222
223 Ok(files)
224 }
225
226 fn language_name(&self) -> &'static str {
227 "gleam"
228 }
229}
230
231fn render_gleam_toml(pkg_path: &str, pkg_name: &str, dep_mode: crate::config::DependencyMode) -> String {
236 use alef_core::template_versions::hex;
237 let stdlib = hex::GLEAM_STDLIB_VERSION_RANGE;
238 let gleeunit = hex::GLEEUNIT_VERSION_RANGE;
239 let gleam_httpc = hex::GLEAM_HTTPC_VERSION_RANGE;
240 let envoy = hex::ENVOY_VERSION_RANGE;
241 let deps = match dep_mode {
242 crate::config::DependencyMode::Registry => {
243 format!(
244 r#"{pkg_name} = ">= 0.1.0"
245gleam_stdlib = "{stdlib}"
246gleeunit = "{gleeunit}"
247gleam_httpc = "{gleam_httpc}"
248gleam_http = ">= 4.0.0 and < 5.0.0"
249envoy = "{envoy}""#
250 )
251 }
252 crate::config::DependencyMode::Local => {
253 format!(
254 r#"{pkg_name} = {{ path = "{pkg_path}" }}
255gleam_stdlib = "{stdlib}"
256gleeunit = "{gleeunit}"
257gleam_httpc = "{gleam_httpc}"
258gleam_http = ">= 4.0.0 and < 5.0.0"
259envoy = "{envoy}""#
260 )
261 }
262 };
263
264 format!(
265 r#"name = "e2e_gleam"
266version = "0.1.0"
267target = "erlang"
268
269[dependencies]
270{deps}
271"#
272 )
273}
274
275#[allow(clippy::too_many_arguments)]
276fn render_test_file(
277 _category: &str,
278 fixtures: &[&Fixture],
279 e2e_config: &E2eConfig,
280 module_path: &str,
281 function_name: &str,
282 result_var: &str,
283 args: &[crate::config::ArgMapping],
284 field_resolver: &FieldResolver,
285 enum_fields: &HashSet<String>,
286) -> String {
287 let mut out = String::new();
288 out.push_str(&hash::header(CommentStyle::DoubleSlash));
289 let _ = writeln!(out, "import gleeunit");
290 let _ = writeln!(out, "import gleeunit/should");
291
292 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
294
295 if has_http_fixtures {
297 let _ = writeln!(out, "import gleam/httpc");
298 let _ = writeln!(out, "import gleam/http");
299 let _ = writeln!(out, "import gleam/http/request");
300 let _ = writeln!(out, "import gleam/list");
301 let _ = writeln!(out, "import gleam/result");
302 let _ = writeln!(out, "import gleam/string");
303 let _ = writeln!(out, "import envoy");
304 }
305
306 let has_non_http_with_override = fixtures.iter().any(|f| !f.is_http_test());
308 if has_non_http_with_override {
309 let _ = writeln!(out, "import {module_path}");
310 let _ = writeln!(out, "import e2e_gleam");
311 }
312 let _ = writeln!(out);
313
314 let mut needed_modules: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();
316
317 for fixture in fixtures {
319 if fixture.is_http_test() {
320 continue; }
322 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
324 let has_bytes_arg = call_config.args.iter().any(|a| a.arg_type == "bytes");
325 let has_optional_string_arg = call_config.args.iter().any(|a| a.arg_type == "string" && a.optional);
327 let has_json_object_arg = call_config.args.iter().any(|a| a.arg_type == "json_object");
329 if has_bytes_arg || has_optional_string_arg || has_json_object_arg {
330 needed_modules.insert("option");
331 }
332 for assertion in &fixture.assertions {
333 let needs_case_expr = assertion
336 .field
337 .as_deref()
338 .is_some_and(|f| field_resolver.tagged_union_split(f).is_some());
339 if needs_case_expr {
340 needed_modules.insert("option");
341 }
342 if let Some(f) = &assertion.field {
344 if field_resolver.is_optional(f) {
345 needed_modules.insert("option");
346 }
347 }
348 match assertion.assertion_type.as_str() {
349 "contains_any" => {
350 needed_modules.insert("string");
352 needed_modules.insert("list");
353 }
354 "contains" | "contains_all" | "not_contains" | "starts_with" | "ends_with" => {
355 needed_modules.insert("string");
356 if let Some(f) = &assertion.field {
358 let resolved = field_resolver.resolve(f);
359 if field_resolver.is_array(f) || field_resolver.is_array(resolved) {
360 needed_modules.insert("list");
361 }
362 } else {
363 if call_config.result_is_array
365 || call_config.result_is_vec
366 || field_resolver.is_array("")
367 || field_resolver.is_array(field_resolver.resolve(""))
368 {
369 needed_modules.insert("list");
370 }
371 }
372 }
373 "not_empty" | "is_empty" | "count_min" | "count_equals" => {
374 needed_modules.insert("list");
375 }
377 "min_length" | "max_length" => {
378 needed_modules.insert("string");
379 }
381 "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
382 }
384 _ => {}
385 }
386 if needs_case_expr {
388 if let Some(f) = &assertion.field {
389 let resolved = field_resolver.resolve(f);
390 if field_resolver.is_array(resolved) {
391 needed_modules.insert("list");
392 }
393 }
394 }
395 if let Some(f) = &assertion.field {
398 if !f.is_empty() {
399 let parts: Vec<&str> = f.split('.').collect();
400 let has_opt_prefix = (1..parts.len()).any(|i| {
401 let prefix_path = parts[..i].join(".");
402 field_resolver.is_optional(&prefix_path)
403 });
404 if has_opt_prefix {
405 needed_modules.insert("option");
406 }
407 }
408 }
409 }
410 }
411
412 for module in &needed_modules {
414 let _ = writeln!(out, "import gleam/{module}");
415 }
416
417 if !needed_modules.is_empty() {
418 let _ = writeln!(out);
419 }
420
421 for fixture in fixtures {
423 if fixture.is_http_test() {
424 render_http_test_case(&mut out, fixture);
425 } else {
426 render_test_case(
427 &mut out,
428 fixture,
429 e2e_config,
430 module_path,
431 function_name,
432 result_var,
433 args,
434 field_resolver,
435 enum_fields,
436 );
437 }
438 let _ = writeln!(out);
439 }
440
441 out
442}
443
444struct GleamTestClientRenderer;
449
450impl client::TestClientRenderer for GleamTestClientRenderer {
451 fn language_name(&self) -> &'static str {
452 "gleam"
453 }
454
455 fn sanitize_test_name(&self, id: &str) -> String {
460 let raw = sanitize_ident(id);
461 let stripped = raw.trim_start_matches(|c: char| c == '_' || c.is_ascii_digit());
462 if stripped.is_empty() { raw } else { stripped.to_string() }
463 }
464
465 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
471 let _ = writeln!(out, "// {description}");
472 let _ = writeln!(out, "pub fn {fn_name}_test() {{");
473 if let Some(reason) = skip_reason {
474 let escaped = escape_gleam(reason);
477 let _ = writeln!(out, " // skipped: {escaped}");
478 let _ = writeln!(out, " Nil");
479 }
480 }
481
482 fn render_test_close(&self, out: &mut String) {
484 let _ = writeln!(out, "}}");
485 }
486
487 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
493 let path = ctx.path;
494
495 let _ = writeln!(out, " let base_url = case envoy.get(\"MOCK_SERVER_URL\") {{");
497 let _ = writeln!(out, " Ok(u) -> u");
498 let _ = writeln!(out, " Error(_) -> \"http://localhost:8080\"");
499 let _ = writeln!(out, " }}");
500
501 let _ = writeln!(out, " let assert Ok(req) = request.to(base_url <> \"{path}\")");
503
504 let method_const = match ctx.method.to_uppercase().as_str() {
506 "GET" => "Get",
507 "POST" => "Post",
508 "PUT" => "Put",
509 "DELETE" => "Delete",
510 "PATCH" => "Patch",
511 "HEAD" => "Head",
512 "OPTIONS" => "Options",
513 _ => "Post",
514 };
515 let _ = writeln!(out, " let req = request.set_method(req, http.{method_const})");
516
517 if ctx.body.is_some() {
519 let content_type = ctx.content_type.unwrap_or("application/json");
520 let escaped_ct = escape_gleam(content_type);
521 let _ = writeln!(
522 out,
523 " let req = request.set_header(req, \"content-type\", \"{escaped_ct}\")"
524 );
525 }
526
527 for (name, value) in ctx.headers {
529 let lower = name.to_lowercase();
530 if matches!(lower.as_str(), "content-length" | "host" | "transfer-encoding") {
531 continue;
532 }
533 let escaped_name = escape_gleam(name);
534 let escaped_value = escape_gleam(value);
535 let _ = writeln!(
536 out,
537 " let req = request.set_header(req, \"{escaped_name}\", \"{escaped_value}\")"
538 );
539 }
540
541 if !ctx.cookies.is_empty() {
543 let cookie_str: Vec<String> = ctx.cookies.iter().map(|(k, v)| format!("{k}={v}")).collect();
544 let escaped_cookie = escape_gleam(&cookie_str.join("; "));
545 let _ = writeln!(
546 out,
547 " let req = request.set_header(req, \"cookie\", \"{escaped_cookie}\")"
548 );
549 }
550
551 if let Some(body) = ctx.body {
553 let json_str = serde_json::to_string(body).unwrap_or_default();
554 let escaped = escape_gleam(&json_str);
555 let _ = writeln!(out, " let req = request.set_body(req, \"{escaped}\")");
556 }
557
558 let resp = ctx.response_var;
560 let _ = writeln!(out, " let assert Ok({resp}) = httpc.send(req)");
561 }
562
563 fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
565 let _ = writeln!(out, " {response_var}.status |> should.equal({status})");
566 }
567
568 fn render_assert_header(&self, out: &mut String, response_var: &str, name: &str, expected: &str) {
574 let escaped_name = escape_gleam(&name.to_lowercase());
575 match expected {
576 "<<absent>>" => {
577 let _ = writeln!(
578 out,
579 " {response_var}.headers\n |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n |> result.is_ok()\n |> should.be_false()"
580 );
581 }
582 "<<present>>" | "<<uuid>>" => {
583 let _ = writeln!(
585 out,
586 " {response_var}.headers\n |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n |> result.is_ok()\n |> should.be_true()"
587 );
588 }
589 literal => {
590 let _escaped_value = escape_gleam(literal);
593 let _ = writeln!(
594 out,
595 " {response_var}.headers\n |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n |> result.is_ok()\n |> should.be_true()"
596 );
597 }
598 }
599 }
600
601 fn render_assert_json_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
607 let escaped = match expected {
608 serde_json::Value::String(s) => escape_gleam(s),
609 other => escape_gleam(&serde_json::to_string(other).unwrap_or_default()),
610 };
611 let _ = writeln!(
612 out,
613 " {response_var}.body |> string.trim |> should.equal(\"{escaped}\")"
614 );
615 }
616
617 fn render_assert_partial_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
623 if let Some(obj) = expected.as_object() {
624 for (key, val) in obj {
625 let fragment = escape_gleam(&format!("\"{}\":", key));
626 let _ = writeln!(
627 out,
628 " {response_var}.body |> string.contains(\"{fragment}\") |> should.equal(True)"
629 );
630 let _ = val; }
632 }
633 }
634
635 fn render_assert_validation_errors(
641 &self,
642 out: &mut String,
643 response_var: &str,
644 errors: &[ValidationErrorExpectation],
645 ) {
646 for err in errors {
647 let escaped_msg = escape_gleam(&err.msg);
648 let _ = writeln!(
649 out,
650 " {response_var}.body |> string.contains(\"{escaped_msg}\") |> should.equal(True)"
651 );
652 }
653 }
654}
655
656fn render_http_test_case(out: &mut String, fixture: &Fixture) {
662 client::http_call::render_http_test(out, &GleamTestClientRenderer, fixture);
663}
664
665#[allow(clippy::too_many_arguments)]
666fn render_test_case(
667 out: &mut String,
668 fixture: &Fixture,
669 e2e_config: &E2eConfig,
670 module_path: &str,
671 _function_name: &str,
672 _result_var: &str,
673 _args: &[crate::config::ArgMapping],
674 field_resolver: &FieldResolver,
675 enum_fields: &HashSet<String>,
676) {
677 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
679 let lang = "gleam";
680 let call_overrides = call_config.overrides.get(lang);
681 let function_name = call_overrides
682 .and_then(|o| o.function.as_ref())
683 .cloned()
684 .unwrap_or_else(|| call_config.function.clone());
685 let result_var = &call_config.result_var;
686 let args = &call_config.args;
687
688 let raw_name = sanitize_ident(&fixture.id);
693 let stripped = raw_name.trim_start_matches(|c: char| c == '_' || c.is_ascii_digit());
694 let test_name = if stripped.is_empty() {
695 raw_name.as_str()
696 } else {
697 stripped
698 };
699 let description = &fixture.description;
700 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
701
702 let (setup_lines, args_str) = build_args_and_setup(&fixture.input, args, &fixture.id);
703
704 let _ = writeln!(out, "// {description}");
707 let _ = writeln!(out, "pub fn {test_name}_test() {{");
708
709 for line in &setup_lines {
710 let _ = writeln!(out, " {line}");
711 }
712
713 if expects_error {
714 let _ = writeln!(out, " {module_path}.{function_name}({args_str}) |> should.be_error()");
715 let _ = writeln!(out, "}}");
716 return;
717 }
718
719 let _ = writeln!(out, " let {result_var} = {module_path}.{function_name}({args_str})");
720 let _ = writeln!(out, " {result_var} |> should.be_ok()");
721 let _ = writeln!(out, " let assert Ok(r) = {result_var}");
722
723 let result_is_array = call_config.result_is_array || call_config.result_is_vec;
724 for assertion in &fixture.assertions {
725 render_assertion(out, assertion, "r", field_resolver, enum_fields, result_is_array);
726 }
727
728 let _ = writeln!(out, "}}");
729}
730
731fn build_args_and_setup(
742 input: &serde_json::Value,
743 args: &[crate::config::ArgMapping],
744 _fixture_id: &str,
745) -> (Vec<String>, String) {
746 if args.is_empty() {
747 return (Vec::new(), String::new());
748 }
749
750 let mut setup_lines: Vec<String> = Vec::new();
751 let mut parts: Vec<String> = Vec::new();
752 let mut bytes_var_counter = 0usize;
753
754 for arg in args {
755 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
756 let val = input.get(field);
757
758 match arg.arg_type.as_str() {
759 "file_path" => {
760 let path = val.and_then(|v| v.as_str()).unwrap_or("");
763 let full_path = format!("../../test_documents/{path}");
764 parts.push(format!("\"{}\"", escape_gleam(&full_path)));
765 }
766 "bytes" => {
767 let path = val.and_then(|v| v.as_str()).unwrap_or("");
770 let var_name = if bytes_var_counter == 0 {
771 "data_bytes__".to_string()
772 } else {
773 format!("data_bytes_{bytes_var_counter}__")
774 };
775 bytes_var_counter += 1;
776 let full_path = format!("../../test_documents/{path}");
778 setup_lines.push(format!(
779 "let assert Ok({var_name}) = e2e_gleam.read_file_bytes(\"{}\")",
780 escape_gleam(&full_path)
781 ));
782 parts.push(var_name);
783 }
784 "string" if arg.optional => {
785 match val {
787 None | Some(serde_json::Value::Null) => {
788 parts.push("option.None".to_string());
789 }
790 Some(serde_json::Value::String(s)) if s.is_empty() => {
791 parts.push("option.None".to_string());
792 }
793 Some(serde_json::Value::String(s)) => {
794 parts.push(format!("option.Some(\"{}\")", escape_gleam(s)));
795 }
796 Some(v) => {
797 parts.push(format!("option.Some({})", json_to_gleam(v)));
798 }
799 }
800 }
801 "string" => {
802 match val {
804 None | Some(serde_json::Value::Null) => {
805 parts.push("\"\"".to_string());
806 }
807 Some(serde_json::Value::String(s)) => {
808 parts.push(format!("\"{}\"", escape_gleam(s)));
809 }
810 Some(v) => {
811 parts.push(json_to_gleam(v));
812 }
813 }
814 }
815 "json_object" => {
816 let element_type = arg.element_type.as_deref().unwrap_or("");
818 match element_type {
819 "BatchFileItem" => {
820 let items_expr = match val {
823 Some(serde_json::Value::Array(arr)) => {
824 let items: Vec<String> = arr
825 .iter()
826 .map(|item| {
827 let path = item.get("path").and_then(|v| v.as_str()).unwrap_or("");
828 let full_path = if path.starts_with('/') {
831 path.to_string()
832 } else {
833 format!("../../test_documents/{path}")
834 };
835 format!(
836 "kreuzberg.BatchFileItem(path: \"{}\", config: option.None)",
837 escape_gleam(&full_path)
838 )
839 })
840 .collect();
841 format!("[{}]", items.join(", "))
842 }
843 _ => "[]".to_string(),
844 };
845 if arg.optional && (val.is_none() || val == Some(&serde_json::Value::Null)) {
846 parts.push("[]".to_string());
847 } else {
848 parts.push(items_expr);
849 }
850 }
851 "BatchBytesItem" => {
852 let items_expr = match val {
854 Some(serde_json::Value::Array(arr)) => {
855 let items: Vec<String> = arr
856 .iter()
857 .map(|item| {
858 let content = item
859 .get("content")
860 .and_then(|v| v.as_array())
861 .map(|bytes| {
862 let byte_strs: Vec<String> = bytes
863 .iter()
864 .map(|b| b.as_u64().unwrap_or(0).to_string())
865 .collect();
866 format!("<<{}>>", byte_strs.join(", "))
867 })
868 .unwrap_or_else(|| "<<>>".to_string());
869 let mime_type = item
870 .get("mime_type")
871 .and_then(|v| v.as_str())
872 .unwrap_or("text/plain");
873 format!(
874 "kreuzberg.BatchBytesItem(content: {content}, mime_type: \"{}\", config: option.None)",
875 escape_gleam(mime_type)
876 )
877 })
878 .collect();
879 format!("[{}]", items.join(", "))
880 }
881 _ => "[]".to_string(),
882 };
883 if arg.optional && (val.is_none() || val == Some(&serde_json::Value::Null)) {
884 parts.push("[]".to_string());
885 } else {
886 parts.push(items_expr);
887 }
888 }
889 _ => {
890 if arg.optional && (val.is_none() || val == Some(&serde_json::Value::Null)) {
892 parts.push(build_gleam_default_extraction_config());
894 } else {
895 let empty_obj = serde_json::Value::Object(Default::default());
896 let config_val = val.unwrap_or(&empty_obj);
897 parts.push(build_gleam_extraction_config(config_val));
898 }
899 }
900 }
901 }
902 "int" | "integer" => match val {
903 None | Some(serde_json::Value::Null) if arg.optional => {}
904 None | Some(serde_json::Value::Null) => parts.push("0".to_string()),
905 Some(v) => parts.push(json_to_gleam(v)),
906 },
907 "bool" | "boolean" => match val {
908 Some(serde_json::Value::Bool(true)) => parts.push("True".to_string()),
909 Some(serde_json::Value::Bool(false)) | None | Some(serde_json::Value::Null) => {
910 if !arg.optional {
911 parts.push("False".to_string());
912 }
913 }
914 Some(v) => parts.push(json_to_gleam(v)),
915 },
916 _ => {
917 match val {
919 None | Some(serde_json::Value::Null) if arg.optional => {}
920 None | Some(serde_json::Value::Null) => parts.push("Nil".to_string()),
921 Some(v) => parts.push(json_to_gleam(v)),
922 }
923 }
924 }
925 }
926
927 (setup_lines, parts.join(", "))
928}
929
930fn build_gleam_default_extraction_config() -> String {
932 build_gleam_extraction_config(&serde_json::Value::Object(Default::default()))
933}
934
935fn build_gleam_extraction_config(config: &serde_json::Value) -> String {
945 let obj = config.as_object();
946 let get_bool = |key: &str, default: bool| -> &'static str {
947 if obj
948 .and_then(|o| o.get(key))
949 .and_then(|v| v.as_bool())
950 .unwrap_or(default)
951 {
952 "True"
953 } else {
954 "False"
955 }
956 };
957 let get_opt_int = |key: &str| -> String {
958 obj.and_then(|o| o.get(key))
959 .and_then(|v| v.as_i64())
960 .map(|n| format!("option.Some({n})"))
961 .unwrap_or_else(|| "option.None".to_string())
962 };
963 let get_opt_str = |key: &str| -> String {
964 obj.and_then(|o| o.get(key))
965 .and_then(|v| v.as_str())
966 .map(|s| format!("option.Some(\"{}\")", escape_gleam(s)))
967 .unwrap_or_else(|| "option.None".to_string())
968 };
969 let get_int =
970 |key: &str, default: i64| -> i64 { obj.and_then(|o| o.get(key)).and_then(|v| v.as_i64()).unwrap_or(default) };
971
972 let output_format = obj
974 .and_then(|o| o.get("output_format"))
975 .and_then(|v| v.as_str())
976 .map(|s| match s {
977 "markdown" => "kreuzberg.OutputFormatMarkdown",
978 "html" => "kreuzberg.OutputFormatHtml",
979 "djot" => "kreuzberg.Djot",
980 "json" => "kreuzberg.Json",
981 "structured" => "kreuzberg.Structured",
982 "plain" | "" => "kreuzberg.Plain",
983 _ => "kreuzberg.Plain",
984 })
985 .unwrap_or("kreuzberg.Plain");
986
987 let security_limits = obj
989 .and_then(|o| o.get("security_limits"))
990 .and_then(|v| v.as_object())
991 .map(|sl| {
992 let get_sl_int = |k: &str, def: i64| -> i64 {
993 sl.get(k).and_then(|v| v.as_i64()).unwrap_or(def)
994 };
995 format!(
996 "option.Some(kreuzberg.SecurityLimits(max_archive_size: {}, max_compression_ratio: {}, max_files_in_archive: {}, max_nesting_depth: {}, max_entity_length: {}, max_content_size: {}, max_iterations: {}, max_xml_depth: {}, max_table_cells: {}))",
997 get_sl_int("max_archive_size", 524_288_000),
998 get_sl_int("max_compression_ratio", 100),
999 get_sl_int("max_files_in_archive", 10_000),
1000 get_sl_int("max_nesting_depth", 10),
1001 get_sl_int("max_entity_length", 8_192),
1002 get_sl_int("max_content_size", 104_857_600),
1003 get_sl_int("max_iterations", 1_000_000),
1004 get_sl_int("max_xml_depth", 100),
1005 get_sl_int("max_table_cells", 10_000),
1006 )
1007 })
1008 .unwrap_or_else(|| "option.None".to_string());
1009
1010 let use_cache = get_bool("use_cache", true);
1011 let enable_quality = get_bool("enable_quality_processing", true);
1012 let force_ocr = get_bool("force_ocr", false);
1013 let disable_ocr = get_bool("disable_ocr", false);
1014 let include_doc_struct = get_bool("include_document_structure", false);
1015 let max_archive_depth = get_int("max_archive_depth", 10);
1016 let extraction_timeout_secs = get_opt_int("extraction_timeout_secs");
1017 let concurrency_str = get_opt_str("concurrency");
1018 let cache_namespace = get_opt_str("cache_namespace");
1019 let cache_ttl_secs = get_opt_int("cache_ttl_secs");
1020 let max_concurrent = get_opt_int("max_concurrent_extractions");
1021 let html_options = get_opt_str("html_options");
1022
1023 format!(
1024 "kreuzberg.ExtractionConfig(use_cache: {use_cache}, enable_quality_processing: {enable_quality}, ocr: option.None, force_ocr: {force_ocr}, force_ocr_pages: option.None, disable_ocr: {disable_ocr}, chunking: option.None, content_filter: option.None, images: option.None, pdf_options: option.None, token_reduction: option.None, language_detection: option.None, pages: option.None, keywords: option.None, postprocessor: option.None, html_options: {html_options}, html_output: option.None, extraction_timeout_secs: {extraction_timeout_secs}, max_concurrent_extractions: {max_concurrent}, result_format: kreuzberg.Unified, security_limits: {security_limits}, output_format: {output_format}, layout: option.None, include_document_structure: {include_doc_struct}, acceleration: option.None, cache_namespace: {cache_namespace}, cache_ttl_secs: {cache_ttl_secs}, email: option.None, concurrency: {concurrency_str}, max_archive_depth: {max_archive_depth}, tree_sitter: option.None, structured_extraction: option.None, cancel_token: option.None)"
1025 )
1026}
1027
1028fn render_tagged_union_assertion(
1044 out: &mut String,
1045 assertion: &Assertion,
1046 result_var: &str,
1047 prefix: &str,
1048 variant: &str,
1049 suffix: &str,
1050 field_resolver: &FieldResolver,
1051) {
1052 let prefix_expr = if prefix.is_empty() {
1055 result_var.to_string()
1056 } else {
1057 format!("{result_var}.{prefix}")
1058 };
1059
1060 let constructor = variant.to_pascal_case();
1064 let module_qualifier = "kreuzberg";
1067
1068 let inner_var = "fmt_inner__";
1070
1071 let full_suffix_path = if prefix.is_empty() {
1074 format!("{variant}.{suffix}")
1075 } else {
1076 format!("{prefix}.{variant}.{suffix}")
1077 };
1078 let suffix_is_optional = field_resolver.is_optional(&full_suffix_path);
1079 let suffix_is_array = field_resolver.is_array(&full_suffix_path);
1080
1081 let _ = writeln!(out, " case {prefix_expr} {{");
1083 let _ = writeln!(
1084 out,
1085 " option.Some({module_qualifier}.{constructor}({inner_var})) -> {{"
1086 );
1087
1088 let inner_field_expr = if suffix.is_empty() {
1090 inner_var.to_string()
1091 } else {
1092 format!("{inner_var}.{suffix}")
1093 };
1094
1095 match assertion.assertion_type.as_str() {
1097 "equals" => {
1098 if let Some(expected) = &assertion.value {
1099 let gleam_val = json_to_gleam(expected);
1100 if suffix_is_optional {
1101 let default = default_gleam_value_for_optional(&gleam_val);
1102 let _ = writeln!(
1103 out,
1104 " {inner_field_expr} |> option.unwrap({default}) |> should.equal({gleam_val})"
1105 );
1106 } else {
1107 let _ = writeln!(out, " {inner_field_expr} |> should.equal({gleam_val})");
1108 }
1109 }
1110 }
1111 "contains" => {
1112 if let Some(expected) = &assertion.value {
1113 let gleam_val = json_to_gleam(expected);
1114 if suffix_is_array {
1115 let _ = writeln!(out, " let items__ = {inner_field_expr} |> option.unwrap([])");
1117 let _ = writeln!(
1118 out,
1119 " items__ |> list.any(fn(item__) {{ string.contains(item__, {gleam_val}) }}) |> should.equal(True)"
1120 );
1121 } else if suffix_is_optional {
1122 let _ = writeln!(
1123 out,
1124 " {inner_field_expr} |> option.unwrap(\"\") |> string.contains({gleam_val}) |> should.equal(True)"
1125 );
1126 } else {
1127 let _ = writeln!(
1128 out,
1129 " {inner_field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
1130 );
1131 }
1132 }
1133 }
1134 "contains_all" => {
1135 if let Some(values) = &assertion.values {
1136 if suffix_is_array {
1137 let _ = writeln!(out, " let items__ = {inner_field_expr} |> option.unwrap([])");
1139 for val in values {
1140 let gleam_val = json_to_gleam(val);
1141 let _ = writeln!(
1142 out,
1143 " items__ |> list.any(fn(item__) {{ string.contains(item__, {gleam_val}) }}) |> should.equal(True)"
1144 );
1145 }
1146 } else if suffix_is_optional {
1147 for val in values {
1148 let gleam_val = json_to_gleam(val);
1149 let _ = writeln!(
1150 out,
1151 " {inner_field_expr} |> option.unwrap(\"\") |> string.contains({gleam_val}) |> should.equal(True)"
1152 );
1153 }
1154 } else {
1155 for val in values {
1156 let gleam_val = json_to_gleam(val);
1157 let _ = writeln!(
1158 out,
1159 " {inner_field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
1160 );
1161 }
1162 }
1163 }
1164 }
1165 "greater_than_or_equal" => {
1166 if let Some(val) = &assertion.value {
1167 let gleam_val = json_to_gleam(val);
1168 if suffix_is_optional {
1169 let _ = writeln!(
1170 out,
1171 " {inner_field_expr} |> option.unwrap(0) |> fn(n__) {{ n__ >= {gleam_val} }} |> should.equal(True)"
1172 );
1173 } else {
1174 let _ = writeln!(
1175 out,
1176 " {inner_field_expr} |> fn(n__) {{ n__ >= {gleam_val} }} |> should.equal(True)"
1177 );
1178 }
1179 }
1180 }
1181 "greater_than" => {
1182 if let Some(val) = &assertion.value {
1183 let gleam_val = json_to_gleam(val);
1184 if suffix_is_optional {
1185 let _ = writeln!(
1186 out,
1187 " {inner_field_expr} |> option.unwrap(0) |> fn(n__) {{ n__ > {gleam_val} }} |> should.equal(True)"
1188 );
1189 } else {
1190 let _ = writeln!(
1191 out,
1192 " {inner_field_expr} |> fn(n__) {{ n__ > {gleam_val} }} |> should.equal(True)"
1193 );
1194 }
1195 }
1196 }
1197 "less_than" => {
1198 if let Some(val) = &assertion.value {
1199 let gleam_val = json_to_gleam(val);
1200 if suffix_is_optional {
1201 let _ = writeln!(
1202 out,
1203 " {inner_field_expr} |> option.unwrap(0) |> fn(n__) {{ n__ < {gleam_val} }} |> should.equal(True)"
1204 );
1205 } else {
1206 let _ = writeln!(
1207 out,
1208 " {inner_field_expr} |> fn(n__) {{ n__ < {gleam_val} }} |> should.equal(True)"
1209 );
1210 }
1211 }
1212 }
1213 "less_than_or_equal" => {
1214 if let Some(val) = &assertion.value {
1215 let gleam_val = json_to_gleam(val);
1216 if suffix_is_optional {
1217 let _ = writeln!(
1218 out,
1219 " {inner_field_expr} |> option.unwrap(0) |> fn(n__) {{ n__ <= {gleam_val} }} |> should.equal(True)"
1220 );
1221 } else {
1222 let _ = writeln!(
1223 out,
1224 " {inner_field_expr} |> fn(n__) {{ n__ <= {gleam_val} }} |> should.equal(True)"
1225 );
1226 }
1227 }
1228 }
1229 "count_min" => {
1230 if let Some(val) = &assertion.value {
1231 if let Some(n) = val.as_u64() {
1232 if suffix_is_optional {
1233 let _ = writeln!(
1234 out,
1235 " {inner_field_expr} |> option.unwrap([]) |> list.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
1236 );
1237 } else {
1238 let _ = writeln!(
1239 out,
1240 " {inner_field_expr} |> list.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
1241 );
1242 }
1243 }
1244 }
1245 }
1246 "count_equals" => {
1247 if let Some(val) = &assertion.value {
1248 if let Some(n) = val.as_u64() {
1249 if suffix_is_optional {
1250 let _ = writeln!(
1251 out,
1252 " {inner_field_expr} |> option.unwrap([]) |> list.length |> should.equal({n})"
1253 );
1254 } else {
1255 let _ = writeln!(out, " {inner_field_expr} |> list.length |> should.equal({n})");
1256 }
1257 }
1258 }
1259 }
1260 "not_empty" => {
1261 if suffix_is_optional {
1262 let _ = writeln!(
1263 out,
1264 " {inner_field_expr} |> option.unwrap([]) |> list.is_empty |> should.equal(False)"
1265 );
1266 } else {
1267 let _ = writeln!(out, " {inner_field_expr} |> list.is_empty |> should.equal(False)");
1268 }
1269 }
1270 "is_empty" => {
1271 if suffix_is_optional {
1272 let _ = writeln!(
1273 out,
1274 " {inner_field_expr} |> option.unwrap([]) |> list.is_empty |> should.equal(True)"
1275 );
1276 } else {
1277 let _ = writeln!(out, " {inner_field_expr} |> list.is_empty |> should.equal(True)");
1278 }
1279 }
1280 "is_true" => {
1281 let _ = writeln!(out, " {inner_field_expr} |> should.equal(True)");
1282 }
1283 "is_false" => {
1284 let _ = writeln!(out, " {inner_field_expr} |> should.equal(False)");
1285 }
1286 other => {
1287 let _ = writeln!(
1288 out,
1289 " // tagged-union assertion '{other}' not yet implemented for Gleam"
1290 );
1291 }
1292 }
1293
1294 let _ = writeln!(out, " }}");
1296 let _ = writeln!(
1297 out,
1298 " _ -> panic as \"expected {module_qualifier}.{constructor} format metadata\""
1299 );
1300 let _ = writeln!(out, " }}");
1301}
1302
1303fn default_gleam_value_for_optional(gleam_val: &str) -> &'static str {
1306 if gleam_val.starts_with('"') {
1307 "\"\""
1308 } else if gleam_val == "True" || gleam_val == "False" {
1309 "False"
1310 } else if gleam_val.contains('.') {
1311 "0.0"
1312 } else {
1313 "0"
1314 }
1315}
1316
1317fn render_assertion(
1318 out: &mut String,
1319 assertion: &Assertion,
1320 result_var: &str,
1321 field_resolver: &FieldResolver,
1322 enum_fields: &HashSet<String>,
1323 result_is_array: bool,
1324) {
1325 if let Some(f) = &assertion.field {
1327 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1328 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1329 return;
1330 }
1331 }
1332
1333 if let Some(f) = &assertion.field {
1337 if !f.is_empty() {
1338 if let Some((prefix, variant, suffix)) = field_resolver.tagged_union_split(f) {
1339 render_tagged_union_assertion(out, assertion, result_var, &prefix, &variant, &suffix, field_resolver);
1340 return;
1341 }
1342 }
1343 }
1344
1345 if let Some(f) = &assertion.field {
1348 if !f.is_empty() {
1349 let parts: Vec<&str> = f.split('.').collect();
1350 let mut opt_prefix: Option<(String, usize)> = None;
1351 for i in 1..parts.len() {
1352 let prefix_path = parts[..i].join(".");
1353 if field_resolver.is_optional(&prefix_path) {
1354 opt_prefix = Some((prefix_path, i));
1355 break;
1356 }
1357 }
1358 if let Some((optional_prefix, suffix_start)) = opt_prefix {
1359 let prefix_expr = format!("{result_var}.{optional_prefix}");
1360 let suffix_parts = &parts[suffix_start..];
1361 let suffix_str = suffix_parts.join(".");
1362 let inner_var = "opt_inner__";
1363 let inner_expr = if suffix_str.is_empty() {
1364 inner_var.to_string()
1365 } else {
1366 format!("{inner_var}.{suffix_str}")
1367 };
1368 let _ = writeln!(out, " case {prefix_expr} {{");
1369 let _ = writeln!(out, " option.Some({inner_var}) -> {{");
1370 match assertion.assertion_type.as_str() {
1371 "count_min" => {
1372 if let Some(val) = &assertion.value {
1373 if let Some(n) = val.as_u64() {
1374 let _ = writeln!(
1375 out,
1376 " {inner_expr} |> list.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
1377 );
1378 }
1379 }
1380 }
1381 "count_equals" => {
1382 if let Some(val) = &assertion.value {
1383 if let Some(n) = val.as_u64() {
1384 let _ = writeln!(out, " {inner_expr} |> list.length |> should.equal({n})");
1385 }
1386 }
1387 }
1388 "not_empty" => {
1389 let _ = writeln!(out, " {inner_expr} |> list.is_empty |> should.equal(False)");
1390 }
1391 "min_length" => {
1392 if let Some(val) = &assertion.value {
1393 if let Some(n) = val.as_u64() {
1394 let _ = writeln!(
1395 out,
1396 " {inner_expr} |> string.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
1397 );
1398 }
1399 }
1400 }
1401 other => {
1402 let _ = writeln!(
1403 out,
1404 " // optional-prefix assertion '{other}' not yet implemented for Gleam"
1405 );
1406 }
1407 }
1408 let _ = writeln!(out, " }}");
1409 let _ = writeln!(out, " option.None -> should.fail()");
1410 let _ = writeln!(out, " }}");
1411 return;
1412 }
1413 }
1414 }
1415
1416 let field_is_optional = assertion
1419 .field
1420 .as_deref()
1421 .is_some_and(|f| !f.is_empty() && field_resolver.is_optional(f));
1422
1423 let _field_is_enum = assertion
1425 .field
1426 .as_deref()
1427 .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1428
1429 let field_expr = match &assertion.field {
1430 Some(f) if !f.is_empty() => field_resolver.accessor(f, "gleam", result_var),
1431 _ => result_var.to_string(),
1432 };
1433
1434 let field_is_array = {
1437 let f = assertion.field.as_deref().unwrap_or("");
1438 let is_root = f.is_empty();
1439 (is_root && result_is_array) || field_resolver.is_array(f) || field_resolver.is_array(field_resolver.resolve(f))
1440 };
1441
1442 match assertion.assertion_type.as_str() {
1443 "equals" => {
1444 if let Some(expected) = &assertion.value {
1445 let gleam_val = json_to_gleam(expected);
1446 if field_is_optional {
1447 let _ = writeln!(out, " {field_expr} |> should.equal(option.Some({gleam_val}))");
1449 } else {
1450 let _ = writeln!(out, " {field_expr} |> should.equal({gleam_val})");
1451 }
1452 }
1453 }
1454 "contains" => {
1455 if let Some(expected) = &assertion.value {
1456 let gleam_val = json_to_gleam(expected);
1457 if field_is_array {
1458 let _ = writeln!(
1460 out,
1461 " {field_expr} |> list.any(fn(item__) {{ string.contains(item__, {gleam_val}) }}) |> should.equal(True)"
1462 );
1463 } else if field_is_optional {
1464 let _ = writeln!(
1465 out,
1466 " {field_expr} |> option.unwrap(\"\") |> string.contains({gleam_val}) |> should.equal(True)"
1467 );
1468 } else {
1469 let _ = writeln!(
1470 out,
1471 " {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
1472 );
1473 }
1474 }
1475 }
1476 "contains_all" => {
1477 if let Some(values) = &assertion.values {
1478 for val in values {
1479 let gleam_val = json_to_gleam(val);
1480 if field_is_optional {
1481 let _ = writeln!(
1482 out,
1483 " {field_expr} |> option.unwrap(\"\") |> string.contains({gleam_val}) |> should.equal(True)"
1484 );
1485 } else {
1486 let _ = writeln!(
1487 out,
1488 " {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
1489 );
1490 }
1491 }
1492 }
1493 }
1494 "not_contains" => {
1495 if let Some(expected) = &assertion.value {
1496 let gleam_val = json_to_gleam(expected);
1497 let _ = writeln!(
1498 out,
1499 " {field_expr} |> string.contains({gleam_val}) |> should.equal(False)"
1500 );
1501 }
1502 }
1503 "not_empty" => {
1504 if field_is_optional {
1505 let _ = writeln!(out, " {field_expr} |> option.is_some |> should.equal(True)");
1507 } else {
1508 let _ = writeln!(out, " {field_expr} |> list.is_empty |> should.equal(False)");
1509 }
1510 }
1511 "is_empty" => {
1512 if field_is_optional {
1513 let _ = writeln!(out, " {field_expr} |> option.is_none |> should.equal(True)");
1514 } else {
1515 let _ = writeln!(out, " {field_expr} |> list.is_empty |> should.equal(True)");
1516 }
1517 }
1518 "starts_with" => {
1519 if let Some(expected) = &assertion.value {
1520 let gleam_val = json_to_gleam(expected);
1521 let _ = writeln!(
1522 out,
1523 " {field_expr} |> string.starts_with({gleam_val}) |> should.equal(True)"
1524 );
1525 }
1526 }
1527 "ends_with" => {
1528 if let Some(expected) = &assertion.value {
1529 let gleam_val = json_to_gleam(expected);
1530 let _ = writeln!(
1531 out,
1532 " {field_expr} |> string.ends_with({gleam_val}) |> should.equal(True)"
1533 );
1534 }
1535 }
1536 "min_length" => {
1537 if let Some(val) = &assertion.value {
1538 if let Some(n) = val.as_u64() {
1539 let _ = writeln!(
1540 out,
1541 " {field_expr} |> string.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
1542 );
1543 }
1544 }
1545 }
1546 "max_length" => {
1547 if let Some(val) = &assertion.value {
1548 if let Some(n) = val.as_u64() {
1549 let _ = writeln!(
1550 out,
1551 " {field_expr} |> string.length |> fn(n__) {{ n__ <= {n} }} |> should.equal(True)"
1552 );
1553 }
1554 }
1555 }
1556 "count_min" => {
1557 if let Some(val) = &assertion.value {
1558 if let Some(n) = val.as_u64() {
1559 let _ = writeln!(
1560 out,
1561 " {field_expr} |> list.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
1562 );
1563 }
1564 }
1565 }
1566 "count_equals" => {
1567 if let Some(val) = &assertion.value {
1568 if let Some(n) = val.as_u64() {
1569 let _ = writeln!(out, " {field_expr} |> list.length |> should.equal({n})");
1570 }
1571 }
1572 }
1573 "is_true" => {
1574 let _ = writeln!(out, " {field_expr} |> should.equal(True)");
1575 }
1576 "is_false" => {
1577 let _ = writeln!(out, " {field_expr} |> should.equal(False)");
1578 }
1579 "not_error" => {
1580 }
1582 "error" => {
1583 }
1585 "greater_than" => {
1586 if let Some(val) = &assertion.value {
1587 let gleam_val = json_to_gleam(val);
1588 let _ = writeln!(
1589 out,
1590 " {field_expr} |> fn(n__) {{ n__ > {gleam_val} }} |> should.equal(True)"
1591 );
1592 }
1593 }
1594 "less_than" => {
1595 if let Some(val) = &assertion.value {
1596 let gleam_val = json_to_gleam(val);
1597 let _ = writeln!(
1598 out,
1599 " {field_expr} |> fn(n__) {{ n__ < {gleam_val} }} |> should.equal(True)"
1600 );
1601 }
1602 }
1603 "greater_than_or_equal" => {
1604 if let Some(val) = &assertion.value {
1605 let gleam_val = json_to_gleam(val);
1606 let _ = writeln!(
1607 out,
1608 " {field_expr} |> fn(n__) {{ n__ >= {gleam_val} }} |> should.equal(True)"
1609 );
1610 }
1611 }
1612 "less_than_or_equal" => {
1613 if let Some(val) = &assertion.value {
1614 let gleam_val = json_to_gleam(val);
1615 let _ = writeln!(
1616 out,
1617 " {field_expr} |> fn(n__) {{ n__ <= {gleam_val} }} |> should.equal(True)"
1618 );
1619 }
1620 }
1621 "contains_any" => {
1622 if let Some(values) = &assertion.values {
1623 let vals_list = values.iter().map(json_to_gleam).collect::<Vec<_>>().join(", ");
1624 let _ = writeln!(
1625 out,
1626 " [{vals_list}] |> list.any(fn(v__) {{ string.contains({field_expr}, v__) }}) |> should.equal(True)"
1627 );
1628 }
1629 }
1630 "matches_regex" => {
1631 let _ = writeln!(out, " // regex match not yet implemented for Gleam");
1632 }
1633 "method_result" => {
1634 let _ = writeln!(out, " // method_result assertions not yet implemented for Gleam");
1635 }
1636 other => {
1637 panic!("Gleam e2e generator: unsupported assertion type: {other}");
1638 }
1639 }
1640}
1641
1642fn json_to_gleam(value: &serde_json::Value) -> String {
1644 match value {
1645 serde_json::Value::String(s) => format!("\"{}\"", escape_gleam(s)),
1646 serde_json::Value::Bool(b) => {
1647 if *b {
1648 "True".to_string()
1649 } else {
1650 "False".to_string()
1651 }
1652 }
1653 serde_json::Value::Number(n) => n.to_string(),
1654 serde_json::Value::Null => "Nil".to_string(),
1655 serde_json::Value::Array(arr) => {
1656 let items: Vec<String> = arr.iter().map(json_to_gleam).collect();
1657 format!("[{}]", items.join(", "))
1658 }
1659 serde_json::Value::Object(_) => {
1660 let json_str = serde_json::to_string(value).unwrap_or_default();
1661 format!("\"{}\"", escape_gleam(&json_str))
1662 }
1663 }
1664}