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