1use crate::codegen::resolve_field;
8use crate::config::E2eConfig;
9use crate::escape::sanitize_filename;
10use crate::field_access::FieldResolver;
11use crate::fixture::{Assertion, Fixture, FixtureGroup, HttpFixture, ValidationErrorExpectation};
12use alef_core::backend::GeneratedFile;
13use alef_core::config::ResolvedCrateConfig;
14use alef_core::hash::{self, CommentStyle};
15use alef_core::template_versions::pub_dev;
16use anyhow::Result;
17use heck::ToLowerCamelCase;
18use std::cell::Cell;
19use std::fmt::Write as FmtWrite;
20use std::path::PathBuf;
21
22use super::E2eCodegen;
23use super::client;
24
25pub struct DartE2eCodegen;
27
28impl E2eCodegen for DartE2eCodegen {
29 fn generate(
30 &self,
31 groups: &[FixtureGroup],
32 e2e_config: &E2eConfig,
33 config: &ResolvedCrateConfig,
34 _type_defs: &[alef_core::ir::TypeDef],
35 _enums: &[alef_core::ir::EnumDef],
36 ) -> Result<Vec<GeneratedFile>> {
37 let lang = self.language_name();
38 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
39
40 let mut files = Vec::new();
41
42 let dart_pkg = e2e_config.resolve_package("dart");
44 let pkg_name = dart_pkg
45 .as_ref()
46 .and_then(|p| p.name.as_ref())
47 .cloned()
48 .unwrap_or_else(|| config.dart_pubspec_name());
49 let pkg_path = dart_pkg
50 .as_ref()
51 .and_then(|p| p.path.as_ref())
52 .cloned()
53 .unwrap_or_else(|| "../../packages/dart".to_string());
54 let pkg_version = dart_pkg
55 .as_ref()
56 .and_then(|p| p.version.as_ref())
57 .cloned()
58 .or_else(|| config.resolved_version())
59 .unwrap_or_else(|| "0.1.0".to_string());
60
61 files.push(GeneratedFile {
63 path: output_base.join("pubspec.yaml"),
64 content: render_pubspec(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
65 generated_header: false,
66 });
67
68 files.push(GeneratedFile {
71 path: output_base.join("dart_test.yaml"),
72 content: concat!(
73 "# Generated by alef — DO NOT EDIT.\n",
74 "# Run test files sequentially to avoid overwhelming the mock server with\n",
75 "# concurrent keep-alive connections.\n",
76 "concurrency: 1\n",
77 )
78 .to_string(),
79 generated_header: false,
80 });
81
82 let test_base = output_base.join("test");
83
84 let bridge_class = config.dart_bridge_class_name();
86
87 let frb_module_name = config.name.replace('-', "_");
91
92 let dart_stub_methods: std::collections::HashSet<String> = config
97 .dart
98 .as_ref()
99 .map(|d| d.stub_methods.iter().cloned().collect())
100 .unwrap_or_default();
101
102 for group in groups {
103 let active: Vec<&Fixture> = group
104 .fixtures
105 .iter()
106 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
107 .filter(|f| {
108 let call_config = e2e_config.resolve_call_for_fixture(
109 f.call.as_deref(),
110 &f.id,
111 &f.resolved_category(),
112 &f.tags,
113 &f.input,
114 );
115 let resolved_function = call_config
116 .overrides
117 .get(lang)
118 .and_then(|o| o.function.as_ref())
119 .cloned()
120 .unwrap_or_else(|| call_config.function.clone());
121 !dart_stub_methods.contains(&resolved_function)
122 })
123 .collect();
124
125 if active.is_empty() {
126 continue;
127 }
128
129 let filename = format!("{}_test.dart", sanitize_filename(&group.category));
130 let content = render_test_file(
131 &group.category,
132 &active,
133 e2e_config,
134 lang,
135 &pkg_name,
136 &frb_module_name,
137 &bridge_class,
138 );
139 files.push(GeneratedFile {
140 path: test_base.join(filename),
141 content,
142 generated_header: true,
143 });
144 }
145
146 Ok(files)
147 }
148
149 fn language_name(&self) -> &'static str {
150 "dart"
151 }
152}
153
154fn render_pubspec(
159 pkg_name: &str,
160 pkg_path: &str,
161 pkg_version: &str,
162 dep_mode: crate::config::DependencyMode,
163) -> String {
164 let test_ver = pub_dev::TEST_PACKAGE;
165 let http_ver = pub_dev::HTTP_PACKAGE;
166
167 let dep_block = match dep_mode {
168 crate::config::DependencyMode::Registry => {
169 let constraint = if pkg_version.starts_with('^')
171 || pkg_version.starts_with('~')
172 || pkg_version.starts_with('>')
173 || pkg_version.starts_with('<')
174 || pkg_version.starts_with('=')
175 {
176 pkg_version.to_string()
177 } else {
178 format!("^{pkg_version}")
179 };
180 format!(" {pkg_name}: {constraint}")
181 }
182 crate::config::DependencyMode::Local => {
183 format!(" {pkg_name}:\n path: {pkg_path}")
184 }
185 };
186
187 let sdk = alef_core::template_versions::toolchain::DART_SDK_CONSTRAINT;
188 format!(
189 r#"name: e2e_dart
190version: 0.1.0
191publish_to: none
192
193environment:
194 sdk: "{sdk}"
195
196dependencies:
197{dep_block}
198
199dev_dependencies:
200 test: {test_ver}
201 http: {http_ver}
202"#
203 )
204}
205
206fn render_test_file(
207 category: &str,
208 fixtures: &[&Fixture],
209 e2e_config: &E2eConfig,
210 lang: &str,
211 pkg_name: &str,
212 frb_module_name: &str,
213 bridge_class: &str,
214) -> String {
215 let mut out = String::new();
216 out.push_str(&hash::header(CommentStyle::DoubleSlash));
217 out.push_str("// ignore_for_file: unused_local_variable\n\n");
221
222 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
224
225 let has_batch_byte_items = fixtures.iter().any(|f| {
227 let call_config =
228 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
229 call_config.args.iter().any(|a| {
230 a.element_type.as_deref() == Some("BatchBytesItem") && resolve_field(&f.input, &a.field).is_array()
231 })
232 });
233
234 let needs_chdir = fixtures.iter().any(|f| {
238 if f.is_http_test() {
239 return false;
240 }
241 let call_config =
242 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
243 call_config
244 .args
245 .iter()
246 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
247 });
248
249 let has_handle_args = fixtures.iter().any(|f| {
255 if f.is_http_test() {
256 return false;
257 }
258 let call_config =
259 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
260 call_config
261 .args
262 .iter()
263 .any(|a| a.arg_type == "json_object" && super::resolve_field(&f.input, &a.field).is_array())
264 });
265
266 let lang_client_factory = e2e_config
272 .call
273 .overrides
274 .get(lang)
275 .and_then(|o| o.client_factory.as_deref())
276 .is_some();
277 let has_mock_url_refs = lang_client_factory
278 || fixtures.iter().any(|f| {
279 if f.is_http_test() {
280 return false;
281 }
282 let call_config = e2e_config.resolve_call_for_fixture(
283 f.call.as_deref(),
284 &f.id,
285 &f.resolved_category(),
286 &f.tags,
287 &f.input,
288 );
289 if call_config.args.iter().any(|a| a.arg_type == "mock_url") {
290 return true;
291 }
292 call_config
293 .overrides
294 .get(lang)
295 .and_then(|o| o.client_factory.as_deref())
296 .is_some()
297 });
298
299 let _ = writeln!(out, "import 'package:test/test.dart';");
300 if has_http_fixtures || needs_chdir || has_mock_url_refs {
305 let _ = writeln!(out, "import 'dart:io';");
306 }
307 if has_batch_byte_items {
308 let _ = writeln!(out, "import 'dart:typed_data';");
309 }
310 let _ = writeln!(out, "import 'package:{pkg_name}/{pkg_name}.dart';");
311 let _ = writeln!(
317 out,
318 "import 'package:{pkg_name}/src/{frb_module_name}_bridge_generated/frb_generated.dart' show RustLib;"
319 );
320 if has_http_fixtures {
321 let _ = writeln!(out, "import 'dart:async';");
322 }
323 if has_http_fixtures || has_handle_args {
325 let _ = writeln!(out, "import 'dart:convert';");
326 }
327 let _ = writeln!(out);
328
329 if has_http_fixtures {
339 let _ = writeln!(out, "HttpClient _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
340 let _ = writeln!(out);
341 let _ = writeln!(out, "var _lock = Future<void>.value();");
342 let _ = writeln!(out);
343 let _ = writeln!(out, "Future<T> _serialized<T>(Future<T> Function() fn) async {{");
344 let _ = writeln!(out, " final current = _lock;");
345 let _ = writeln!(out, " final next = Completer<void>();");
346 let _ = writeln!(out, " _lock = next.future;");
347 let _ = writeln!(out, " try {{");
348 let _ = writeln!(out, " await current;");
349 let _ = writeln!(out, " return await fn();");
350 let _ = writeln!(out, " }} finally {{");
351 let _ = writeln!(out, " next.complete();");
352 let _ = writeln!(out, " }}");
353 let _ = writeln!(out, "}}");
354 let _ = writeln!(out);
355 let _ = writeln!(out, "Future<T> _withRetry<T>(Future<T> Function() fn) async {{");
358 let _ = writeln!(out, " try {{");
359 let _ = writeln!(out, " return await fn();");
360 let _ = writeln!(out, " }} on SocketException {{");
361 let _ = writeln!(out, " _httpClient.close(force: true);");
362 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
363 let _ = writeln!(out, " return fn();");
364 let _ = writeln!(out, " }} on HttpException {{");
365 let _ = writeln!(out, " _httpClient.close(force: true);");
366 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
367 let _ = writeln!(out, " return fn();");
368 let _ = writeln!(out, " }}");
369 let _ = writeln!(out, "}}");
370 let _ = writeln!(out);
371 }
372
373 let _ = writeln!(out, "// E2e tests for category: {category}");
374 let _ = writeln!(out);
375
376 let _ = writeln!(out, "String _alefE2eText(Object? value) {{");
382 let _ = writeln!(out, " if (value == null) return '';");
383 let _ = writeln!(
384 out,
385 " // Check if it's an enum by examining its toString representation."
386 );
387 let _ = writeln!(out, " final str = value.toString();");
388 let _ = writeln!(out, " if (str.contains('.')) {{");
389 let _ = writeln!(
390 out,
391 " // Enum.toString() returns 'EnumName.variantName'. Extract the variant name."
392 );
393 let _ = writeln!(out, " final parts = str.split('.');");
394 let _ = writeln!(out, " if (parts.length == 2) {{");
395 let _ = writeln!(out, " final variantName = parts[1];");
396 let _ = writeln!(
397 out,
398 " // Convert camelCase variant names to snake_case for serde compatibility."
399 );
400 let _ = writeln!(out, " // E.g. 'toolCalls' -> 'tool_calls', 'stop' -> 'stop'.");
401 let _ = writeln!(out, " return _camelToSnake(variantName);");
402 let _ = writeln!(out, " }}");
403 let _ = writeln!(out, " }}");
404 let _ = writeln!(out, " return str;");
405 let _ = writeln!(out, "}}");
406 let _ = writeln!(out);
407
408 let _ = writeln!(out, "String _camelToSnake(String camel) {{");
410 let _ = writeln!(out, " final buffer = StringBuffer();");
411 let _ = writeln!(out, " for (int i = 0; i < camel.length; i++) {{");
412 let _ = writeln!(out, " final char = camel[i];");
413 let _ = writeln!(out, " if (char.contains(RegExp(r'[A-Z]'))) {{");
414 let _ = writeln!(out, " if (i > 0) buffer.write('_');");
415 let _ = writeln!(out, " buffer.write(char.toLowerCase());");
416 let _ = writeln!(out, " }} else {{");
417 let _ = writeln!(out, " buffer.write(char);");
418 let _ = writeln!(out, " }}");
419 let _ = writeln!(out, " }}");
420 let _ = writeln!(out, " return buffer.toString();");
421 let _ = writeln!(out, "}}");
422 let _ = writeln!(out);
423
424 let _ = writeln!(out, "void main() {{");
425
426 let _ = writeln!(out, " setUpAll(() async {{");
433 let _ = writeln!(out, " await RustLib.init();");
434 if needs_chdir {
435 let test_docs_path = e2e_config.test_documents_relative_from(0);
436 let _ = writeln!(
437 out,
438 " final _testDocs = Platform.environment['FIXTURES_DIR'] ?? '{test_docs_path}';"
439 );
440 let _ = writeln!(out, " final _dir = Directory(_testDocs);");
441 let _ = writeln!(out, " if (_dir.existsSync()) Directory.current = _dir;");
442 }
443 let _ = writeln!(out, " }});");
444 let _ = writeln!(out);
445
446 if has_http_fixtures {
448 let _ = writeln!(out, " tearDownAll(() => _httpClient.close());");
449 let _ = writeln!(out);
450 }
451
452 for fixture in fixtures {
453 render_test_case(&mut out, fixture, e2e_config, lang, bridge_class);
454 }
455
456 let _ = writeln!(out, "}}");
457 out
458}
459
460fn render_test_case(out: &mut String, fixture: &Fixture, e2e_config: &E2eConfig, lang: &str, bridge_class: &str) {
461 if let Some(http) = &fixture.http {
463 render_http_test_case(out, fixture, http);
464 return;
465 }
466
467 let call_config = e2e_config.resolve_call_for_fixture(
469 fixture.call.as_deref(),
470 &fixture.id,
471 &fixture.resolved_category(),
472 &fixture.tags,
473 &fixture.input,
474 );
475 let call_field_resolver = FieldResolver::new(
477 e2e_config.effective_fields(call_config),
478 e2e_config.effective_fields_optional(call_config),
479 e2e_config.effective_result_fields(call_config),
480 e2e_config.effective_fields_array(call_config),
481 e2e_config.effective_fields_method_calls(call_config),
482 );
483 let field_resolver = &call_field_resolver;
484 let enum_fields_base = e2e_config.effective_fields_enum(call_config);
485
486 let effective_enum_fields: std::collections::HashSet<String> = {
491 let dart_overrides = call_config.overrides.get("dart");
492 if let Some(overrides) = dart_overrides {
493 let mut merged = enum_fields_base.clone();
494 merged.extend(overrides.enum_fields.keys().cloned());
495 merged
496 } else {
497 enum_fields_base.clone()
498 }
499 };
500 let enum_fields = &effective_enum_fields;
501 let call_overrides = call_config.overrides.get(lang);
502 let mut function_name = call_overrides
503 .and_then(|o| o.function.as_ref())
504 .cloned()
505 .unwrap_or_else(|| call_config.function.clone());
506 function_name = function_name
508 .split('_')
509 .enumerate()
510 .map(|(i, part)| {
511 if i == 0 {
512 part.to_string()
513 } else {
514 let mut chars = part.chars();
515 match chars.next() {
516 None => String::new(),
517 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
518 }
519 }
520 })
521 .collect::<Vec<_>>()
522 .join("");
523 let result_var = &call_config.result_var;
524 let description = escape_dart(&fixture.description);
525 let fixture_id = &fixture.id;
526 let _is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
529
530 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
531 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
532 let result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple) || call_config.result_is_simple;
537
538 let options_type: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
545 let options_via: &str = call_overrides
546 .and_then(|o| o.options_via.as_deref())
547 .unwrap_or("kwargs");
548
549 let file_path_for_mime: Option<&str> = call_config
557 .args
558 .iter()
559 .find(|a| a.arg_type == "file_path")
560 .and_then(|a| resolve_field(&fixture.input, &a.field).as_str());
561
562 let has_file_path_arg = call_config.args.iter().any(|a| a.arg_type == "file_path");
569 let caller_supplied_override = call_overrides.and_then(|o| o.function.as_ref()).is_some();
572 if has_file_path_arg && !caller_supplied_override {
573 function_name = match function_name.as_str() {
574 "extractFile" => "extractBytes".to_string(),
575 "extractFileSync" => "extractBytesSync".to_string(),
576 other => other.to_string(),
577 };
578 }
579
580 let mut setup_lines: Vec<String> = Vec::new();
583 let mut args = Vec::new();
584
585 for arg_def in &call_config.args {
586 match arg_def.arg_type.as_str() {
587 "mock_url" => {
588 let name = arg_def.name.clone();
589 if fixture.has_host_root_route() {
590 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
591 setup_lines.push(format!(
592 r#"final {name} = Platform.environment["{env_key}"] ?? (Platform.environment["MOCK_SERVER_URL"]! + "/fixtures/{fixture_id}");"#
593 ));
594 } else {
595 setup_lines.push(format!(
596 r#"final {name} = "${{Platform.environment["MOCK_SERVER_URL"] ?? "http://localhost:8080"}}/fixtures/{fixture_id}";"#
597 ));
598 }
599 args.push(name);
600 continue;
601 }
602 "handle" => {
603 let name = arg_def.name.clone();
604 let field = arg_def.field.strip_prefix("input.").unwrap_or(&arg_def.field);
605 let config_value = fixture.input.get(field).cloned().unwrap_or(serde_json::Value::Null);
606 let create_fn = {
608 let mut chars = name.chars();
609 let pascal = match chars.next() {
610 None => String::new(),
611 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
612 };
613 format!("create{pascal}")
614 };
615 if config_value.is_null()
616 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
617 {
618 setup_lines.push(format!("final {name} = await {bridge_class}.{create_fn}();"));
619 } else {
620 let json_str = serde_json::to_string(&config_value).unwrap_or_default();
621 let config_var = format!("{name}Config");
622 setup_lines.push(format!(
627 "final {config_var} = await createCrawlConfigFromJson(json: r'{json_str}');"
628 ));
629 setup_lines.push(format!(
631 "final {name} = await {bridge_class}.{create_fn}(config: {config_var});"
632 ));
633 }
634 args.push(name);
635 continue;
636 }
637 _ => {}
638 }
639
640 let arg_value = resolve_field(&fixture.input, &arg_def.field);
641 match arg_def.arg_type.as_str() {
642 "bytes" | "file_path" => {
643 if let serde_json::Value::String(file_path) = arg_value {
648 args.push(format!("File('{}').readAsBytesSync()", file_path));
649 }
650 }
651 "string" => {
652 let dart_param_name = snake_to_camel(&arg_def.name);
667 match arg_value {
668 serde_json::Value::String(s) => {
669 let literal = format!("'{}'", escape_dart(s));
670 if arg_def.optional {
671 args.push(format!("{dart_param_name}: {literal}"));
672 } else {
673 args.push(literal);
674 }
675 }
676 serde_json::Value::Null
677 if arg_def.optional
678 && arg_def.name == "mime_type" =>
681 {
682 let inferred = file_path_for_mime
683 .and_then(mime_from_extension)
684 .unwrap_or("application/octet-stream");
685 args.push(format!("{dart_param_name}: '{inferred}'"));
686 }
687 _ => {}
689 }
690 }
691 "json_object" => {
692 if let Some(elem_type) = &arg_def.element_type {
694 if (elem_type == "BatchBytesItem" || elem_type == "BatchFileItem") && arg_value.is_array() {
695 let dart_items = emit_dart_batch_item_array(arg_value, elem_type);
696 args.push(dart_items);
697 } else if elem_type == "String" && arg_value.is_array() {
698 let items: Vec<String> = arg_value
705 .as_array()
706 .unwrap()
707 .iter()
708 .filter_map(|v| v.as_str())
709 .map(|s| format!("'{}'", escape_dart(s)))
710 .collect();
711 args.push(format!("<String>[{}]", items.join(", ")));
712 }
713 } else if options_via == "from_json" {
714 if let Some(opts_type) = options_type {
724 if !arg_value.is_null() {
725 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
726 let escaped_json = escape_dart(&json_str);
729 let var_name = format!("_{}", arg_def.name);
730 let dart_fn = type_name_to_create_from_json_dart(opts_type);
731 setup_lines.push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
732 args.push(format!("req: {var_name}"));
735 }
736 }
737 } else if arg_def.name == "config" {
738 if let serde_json::Value::Object(map) = &arg_value {
739 if !map.is_empty() {
740 let explicit_options =
749 options_type.is_some_and(|t| t != "ExtractionConfig" && t != "FileExtractionConfig");
750 let has_non_scalar = map.values().any(|v| {
751 matches!(
752 v,
753 serde_json::Value::String(_)
754 | serde_json::Value::Object(_)
755 | serde_json::Value::Array(_)
756 )
757 });
758 if explicit_options || has_non_scalar {
759 let opts_type = options_type.unwrap_or("ExtractionConfig");
760 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
761 let escaped_json = escape_dart(&json_str);
762 let var_name = format!("_{}", arg_def.name);
763 let dart_fn = type_name_to_create_from_json_dart(opts_type);
764 setup_lines
765 .push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
766 args.push(var_name);
767 } else {
768 args.push(emit_extraction_config_dart(map));
774 }
775 }
776 }
777 } else if arg_value.is_array() {
779 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
782 let var_name = arg_def.name.clone();
783 setup_lines.push(format!(
784 "final {var_name} = (jsonDecode(r'{json_str}') as List<dynamic>).cast<String>();"
785 ));
786 args.push(var_name);
787 } else if let serde_json::Value::Object(map) = &arg_value {
788 if !map.is_empty() {
802 if let Some(opts_type) = options_type {
803 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
804 let escaped_json = escape_dart(&json_str);
805 let dart_param_name = snake_to_camel(&arg_def.name);
806 let var_name = format!("_{}", arg_def.name);
807 let dart_fn = type_name_to_create_from_json_dart(opts_type);
808 if fixture.visitor.is_some() {
809 setup_lines.push(format!(
810 "final {var_name} = await {dart_fn}WithVisitor(json: '{escaped_json}', visitor: _visitor);"
811 ));
812 } else {
813 setup_lines
814 .push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
815 }
816 if arg_def.optional {
817 args.push(format!("{dart_param_name}: {var_name}"));
818 } else {
819 args.push(var_name);
820 }
821 }
822 }
823 }
824 }
825 _ => {}
826 }
827 }
828
829 if let Some(visitor_spec) = &fixture.visitor {
844 let mut visitor_setup: Vec<String> = Vec::new();
845 let _ = super::dart_visitors::build_dart_visitor(&mut visitor_setup, visitor_spec);
846 for line in visitor_setup.into_iter().rev() {
849 setup_lines.insert(0, line);
850 }
851
852 let already_has_options = args.iter().any(|a| a.starts_with("options:") || a == "_options");
856 if !already_has_options {
857 if let Some(opts_type) = options_type {
858 let dart_fn = type_name_to_create_from_json_dart(opts_type);
859 setup_lines.push(format!(
860 "final _options = await {dart_fn}WithVisitor(json: '{{}}', visitor: _visitor);"
861 ));
862 args.push("options: _options".to_string());
863 }
864 }
865 }
866
867 let client_factory: Option<&str> = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
871 e2e_config
872 .call
873 .overrides
874 .get(lang)
875 .and_then(|o| o.client_factory.as_deref())
876 });
877
878 let client_factory_camel: Option<String> = client_factory.map(|f| {
880 f.split('_')
881 .enumerate()
882 .map(|(i, part)| {
883 if i == 0 {
884 part.to_string()
885 } else {
886 let mut chars = part.chars();
887 match chars.next() {
888 None => String::new(),
889 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
890 }
891 }
892 })
893 .collect::<Vec<_>>()
894 .join("")
895 });
896
897 let _ = writeln!(out, " test('{description}', () async {{");
901
902 let args_str = args.join(", ");
903 let receiver_class = call_overrides
904 .and_then(|o| o.class.as_ref())
905 .cloned()
906 .unwrap_or_else(|| bridge_class.to_string());
907
908 let (receiver, extra_setup): (String, Option<String>) = if let Some(factory) = &client_factory_camel {
912 let has_mock_url = call_config.args.iter().any(|a| a.arg_type == "mock_url");
913 let mock_url_setup = if !has_mock_url {
914 if fixture.has_host_root_route() {
916 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
917 Some(format!(
918 "final _mockUrl = Platform.environment[\"{env_key}\"] ?? (Platform.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\");"
919 ))
920 } else {
921 Some(format!(
922 r#"final _mockUrl = "${{Platform.environment["MOCK_SERVER_URL"] ?? "http://localhost:8080"}}/fixtures/{fixture_id}";"#
923 ))
924 }
925 } else {
926 None
927 };
928 let url_expr = if has_mock_url {
929 call_config
932 .args
933 .iter()
934 .find(|a| a.arg_type == "mock_url")
935 .map(|a| a.name.clone())
936 .unwrap_or_else(|| "_mockUrl".to_string())
937 } else {
938 "_mockUrl".to_string()
939 };
940 let create_line = format!("final _client = await {receiver_class}.{factory}('test-key', baseUrl: {url_expr});");
941 let full_setup = if let Some(url_line) = mock_url_setup {
942 Some(format!("{url_line}\n {create_line}"))
943 } else {
944 Some(create_line)
945 };
946 ("_client".to_string(), full_setup)
947 } else {
948 (receiver_class.clone(), None)
949 };
950
951 if expects_error && (!setup_lines.is_empty() || extra_setup.is_some()) {
952 let _ = writeln!(out, " await expectLater(() async {{");
956 for line in &setup_lines {
957 let _ = writeln!(out, " {line}");
958 }
959 if let Some(extra) = &extra_setup {
960 for line in extra.lines() {
961 let _ = writeln!(out, " {line}");
962 }
963 }
964 if is_streaming {
965 let _ = writeln!(out, " return {receiver}.{function_name}({args_str}).toList();");
966 } else {
967 let _ = writeln!(out, " return {receiver}.{function_name}({args_str});");
968 }
969 let _ = writeln!(out, " }}(), throwsA(anything));");
970 } else if expects_error {
971 if let Some(extra) = &extra_setup {
973 for line in extra.lines() {
974 let _ = writeln!(out, " {line}");
975 }
976 }
977 if is_streaming {
978 let _ = writeln!(
979 out,
980 " await expectLater({receiver}.{function_name}({args_str}).toList(), throwsA(anything));"
981 );
982 } else {
983 let _ = writeln!(
984 out,
985 " await expectLater({receiver}.{function_name}({args_str}), throwsA(anything));"
986 );
987 }
988 } else {
989 for line in &setup_lines {
990 let _ = writeln!(out, " {line}");
991 }
992 if let Some(extra) = &extra_setup {
993 for line in extra.lines() {
994 let _ = writeln!(out, " {line}");
995 }
996 }
997 if is_streaming {
998 let _ = writeln!(
999 out,
1000 " final {result_var} = await {receiver}.{function_name}({args_str}).toList();"
1001 );
1002 } else {
1003 let _ = writeln!(
1004 out,
1005 " final {result_var} = await {receiver}.{function_name}({args_str});"
1006 );
1007 }
1008 for assertion in &fixture.assertions {
1009 if is_streaming {
1010 render_streaming_assertion_dart(out, assertion, result_var);
1011 } else {
1012 render_assertion_dart(
1013 out,
1014 assertion,
1015 result_var,
1016 result_is_simple,
1017 field_resolver,
1018 enum_fields,
1019 );
1020 }
1021 }
1022 }
1023
1024 let _ = writeln!(out, " }});");
1025 let _ = writeln!(out);
1026}
1027
1028fn dart_length_expr(field_accessor: &str, field: Option<&str>, field_resolver: &FieldResolver) -> String {
1036 let is_optional = field
1037 .map(|f| {
1038 let resolved = field_resolver.resolve(f);
1039 field_resolver.is_optional(f) || field_resolver.is_optional(resolved)
1040 })
1041 .unwrap_or(false);
1042 if is_optional {
1043 format!("{field_accessor}?.length ?? 0")
1044 } else {
1045 format!("{field_accessor}.length")
1046 }
1047}
1048
1049fn dart_format_value(val: &serde_json::Value) -> String {
1050 match val {
1051 serde_json::Value::String(s) => format!("'{}'", escape_dart(s)),
1052 serde_json::Value::Bool(b) => b.to_string(),
1053 serde_json::Value::Number(n) => n.to_string(),
1054 serde_json::Value::Null => "null".to_string(),
1055 other => format!("'{}'", escape_dart(&other.to_string())),
1056 }
1057}
1058
1059fn render_assertion_dart(
1070 out: &mut String,
1071 assertion: &Assertion,
1072 result_var: &str,
1073 result_is_simple: bool,
1074 field_resolver: &FieldResolver,
1075 enum_fields: &std::collections::HashSet<String>,
1076) {
1077 if !result_is_simple {
1081 if let Some(f) = assertion.field.as_deref() {
1082 let head = f.split("[].").next().unwrap_or(f);
1085 if !head.is_empty() && !field_resolver.is_valid_for_result(head) {
1086 let _ = writeln!(out, " // skipped: field '{f}' not available on dart result type");
1087 return;
1088 }
1089 }
1090 }
1091
1092 if let Some(f) = assertion.field.as_deref() {
1098 if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
1099 let _ = writeln!(
1100 out,
1101 " // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Dart)"
1102 );
1103 return;
1104 }
1105 }
1106
1107 if let Some(f) = assertion.field.as_deref() {
1109 if let Some(dot) = f.find("[].") {
1110 let resolved_full = field_resolver.resolve(f);
1115 let (array_part, elem_part) = match resolved_full.find("[].") {
1116 Some(rdot) => (&resolved_full[..rdot], &resolved_full[rdot + 3..]),
1117 None => (&f[..dot], &f[dot + 3..]),
1120 };
1121 let array_accessor = if array_part.is_empty() {
1122 result_var.to_string()
1123 } else {
1124 field_resolver.accessor(array_part, "dart", result_var)
1125 };
1126 let elem_accessor = field_to_dart_accessor(elem_part);
1127 match assertion.assertion_type.as_str() {
1128 "contains" => {
1129 if let Some(expected) = &assertion.value {
1130 let dart_val = dart_format_value(expected);
1131 let _ = writeln!(
1132 out,
1133 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
1134 );
1135 }
1136 }
1137 "contains_all" => {
1138 if let Some(values) = &assertion.values {
1139 for val in values {
1140 let dart_val = dart_format_value(val);
1141 let _ = writeln!(
1142 out,
1143 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
1144 );
1145 }
1146 }
1147 }
1148 "not_contains" => {
1149 if let Some(expected) = &assertion.value {
1150 let dart_val = dart_format_value(expected);
1151 let _ = writeln!(
1152 out,
1153 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
1154 );
1155 } else if let Some(values) = &assertion.values {
1156 for val in values {
1157 let dart_val = dart_format_value(val);
1158 let _ = writeln!(
1159 out,
1160 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
1161 );
1162 }
1163 }
1164 }
1165 "not_empty" => {
1166 let _ = writeln!(
1167 out,
1168 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().isNotEmpty), isTrue);"
1169 );
1170 }
1171 other => {
1172 let _ = writeln!(
1173 out,
1174 " // skipped: unsupported traversal assertion '{other}' on '{f}'"
1175 );
1176 }
1177 }
1178 return;
1179 }
1180 }
1181
1182 let field_accessor = if result_is_simple {
1183 result_var.to_string()
1187 } else {
1188 match assertion.field.as_deref() {
1189 Some(f) if !f.is_empty() => field_resolver.accessor(f, "dart", result_var),
1194 _ => result_var.to_string(),
1195 }
1196 };
1197
1198 let format_value = |val: &serde_json::Value| -> String { dart_format_value(val) };
1199
1200 match assertion.assertion_type.as_str() {
1201 "equals" | "field_equals" => {
1202 if let Some(expected) = &assertion.value {
1203 let dart_val = format_value(expected);
1204 let is_enum_field = assertion
1207 .field
1208 .as_deref()
1209 .map(|f| {
1210 let resolved = field_resolver.resolve(f);
1211 enum_fields.contains(f) || enum_fields.contains(resolved)
1212 })
1213 .unwrap_or(false);
1214
1215 if expected.is_string() {
1219 if is_enum_field {
1220 let _ = writeln!(
1223 out,
1224 " expect(_alefE2eText({field_accessor}).trim(), equals({dart_val}.toString().trim()));"
1225 );
1226 } else {
1227 let safe_accessor = if result_is_simple && assertion.field.is_none() {
1230 format!("({field_accessor} ?? '').toString().trim()")
1231 } else {
1232 format!("{field_accessor}.toString().trim()")
1233 };
1234 let _ = writeln!(
1235 out,
1236 " expect({safe_accessor}, equals({dart_val}.toString().trim()));"
1237 );
1238 }
1239 } else {
1240 let _ = writeln!(out, " expect({field_accessor}, equals({dart_val}));");
1241 }
1242 } else {
1243 let _ = writeln!(
1244 out,
1245 " // skipped: '{}' assertion missing value",
1246 assertion.assertion_type
1247 );
1248 }
1249 }
1250 "not_equals" => {
1251 if let Some(expected) = &assertion.value {
1252 let dart_val = format_value(expected);
1253 let is_enum_field = assertion
1255 .field
1256 .as_deref()
1257 .map(|f| {
1258 let resolved = field_resolver.resolve(f);
1259 enum_fields.contains(f) || enum_fields.contains(resolved)
1260 })
1261 .unwrap_or(false);
1262
1263 if expected.is_string() {
1264 if is_enum_field {
1265 let _ = writeln!(
1266 out,
1267 " expect(_alefE2eText({field_accessor}).trim(), isNot(equals({dart_val}.toString().trim())));"
1268 );
1269 } else {
1270 let safe_accessor = if result_is_simple && assertion.field.is_none() {
1273 format!("({field_accessor} ?? '').toString().trim()")
1274 } else {
1275 format!("{field_accessor}.toString().trim()")
1276 };
1277 let _ = writeln!(
1278 out,
1279 " expect({safe_accessor}, isNot(equals({dart_val}.toString().trim())));"
1280 );
1281 }
1282 } else {
1283 let _ = writeln!(out, " expect({field_accessor}, isNot(equals({dart_val})));");
1284 }
1285 }
1286 }
1287 "contains" => {
1288 if let Some(expected) = &assertion.value {
1289 let dart_val = format_value(expected);
1290 let _ = writeln!(out, " expect({field_accessor}, contains({dart_val}));");
1291 } else {
1292 let _ = writeln!(out, " // skipped: 'contains' assertion missing value");
1293 }
1294 }
1295 "contains_all" => {
1296 if let Some(values) = &assertion.values {
1297 for val in values {
1298 let dart_val = format_value(val);
1299 let _ = writeln!(out, " expect({field_accessor}, contains({dart_val}));");
1300 }
1301 }
1302 }
1303 "contains_any" => {
1304 if let Some(values) = &assertion.values {
1305 let checks: Vec<String> = values
1306 .iter()
1307 .map(|v| {
1308 let dart_val = format_value(v);
1309 format!("{field_accessor}.contains({dart_val})")
1310 })
1311 .collect();
1312 let joined = checks.join(" || ");
1313 let _ = writeln!(out, " expect({joined}, isTrue);");
1314 }
1315 }
1316 "not_contains" => {
1317 if let Some(expected) = &assertion.value {
1318 let dart_val = format_value(expected);
1319 let _ = writeln!(out, " expect({field_accessor}, isNot(contains({dart_val})));");
1320 } else if let Some(values) = &assertion.values {
1321 for val in values {
1322 let dart_val = format_value(val);
1323 let _ = writeln!(out, " expect({field_accessor}, isNot(contains({dart_val})));");
1324 }
1325 }
1326 }
1327 "not_empty" => {
1328 let is_collection = assertion.field.as_deref().is_some_and(|f| {
1333 let resolved = field_resolver.resolve(f);
1334 field_resolver.is_array(f) || field_resolver.is_array(resolved)
1335 });
1336 if is_collection {
1337 let _ = writeln!(out, " expect({field_accessor}, isNotEmpty);");
1338 } else {
1339 let _ = writeln!(out, " expect({field_accessor}, isNotNull);");
1340 }
1341 }
1342 "is_empty" => {
1343 let _ = writeln!(out, " expect({field_accessor}, anyOf(isNull, isEmpty));");
1347 }
1348 "starts_with" => {
1349 if let Some(expected) = &assertion.value {
1350 let dart_val = format_value(expected);
1351 let _ = writeln!(out, " expect({field_accessor}, startsWith({dart_val}));");
1352 }
1353 }
1354 "ends_with" => {
1355 if let Some(expected) = &assertion.value {
1356 let dart_val = format_value(expected);
1357 let _ = writeln!(out, " expect({field_accessor}, endsWith({dart_val}));");
1358 }
1359 }
1360 "min_length" => {
1361 if let Some(val) = &assertion.value {
1362 if let Some(n) = val.as_u64() {
1363 let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
1364 let _ = writeln!(out, " expect({length_expr}, greaterThanOrEqualTo({n}));");
1365 }
1366 }
1367 }
1368 "max_length" => {
1369 if let Some(val) = &assertion.value {
1370 if let Some(n) = val.as_u64() {
1371 let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
1372 let _ = writeln!(out, " expect({length_expr}, lessThanOrEqualTo({n}));");
1373 }
1374 }
1375 }
1376 "count_equals" => {
1377 if let Some(val) = &assertion.value {
1378 if let Some(n) = val.as_u64() {
1379 let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
1380 let _ = writeln!(out, " expect({length_expr}, equals({n}));");
1381 }
1382 }
1383 }
1384 "count_min" => {
1385 if let Some(val) = &assertion.value {
1386 if let Some(n) = val.as_u64() {
1387 let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
1388 let _ = writeln!(out, " expect({length_expr}, greaterThanOrEqualTo({n}));");
1389 }
1390 }
1391 }
1392 "matches_regex" => {
1393 if let Some(expected) = &assertion.value {
1394 let dart_val = format_value(expected);
1395 let _ = writeln!(out, " expect({field_accessor}, matches(RegExp({dart_val})));");
1396 }
1397 }
1398 "is_true" => {
1399 let _ = writeln!(out, " expect({field_accessor}, isTrue);");
1400 }
1401 "is_false" => {
1402 let _ = writeln!(out, " expect({field_accessor}, isFalse);");
1403 }
1404 "greater_than" => {
1405 if let Some(val) = &assertion.value {
1406 let dart_val = format_value(val);
1407 let _ = writeln!(out, " expect({field_accessor}, greaterThan({dart_val}));");
1408 }
1409 }
1410 "less_than" => {
1411 if let Some(val) = &assertion.value {
1412 let dart_val = format_value(val);
1413 let _ = writeln!(out, " expect({field_accessor}, lessThan({dart_val}));");
1414 }
1415 }
1416 "greater_than_or_equal" => {
1417 if let Some(val) = &assertion.value {
1418 let dart_val = format_value(val);
1419 let _ = writeln!(out, " expect({field_accessor}, greaterThanOrEqualTo({dart_val}));");
1420 }
1421 }
1422 "less_than_or_equal" => {
1423 if let Some(val) = &assertion.value {
1424 let dart_val = format_value(val);
1425 let _ = writeln!(out, " expect({field_accessor}, lessThanOrEqualTo({dart_val}));");
1426 }
1427 }
1428 "not_null" => {
1429 let _ = writeln!(out, " expect({field_accessor}, isNotNull);");
1430 }
1431 "not_error" => {
1432 }
1439 "error" => {
1440 }
1442 "method_result" => {
1443 if let Some(method) = &assertion.method {
1444 let dart_method = method.to_lower_camel_case();
1445 let check = assertion.check.as_deref().unwrap_or("not_null");
1446 let method_call = format!("{field_accessor}.{dart_method}()");
1447 match check {
1448 "equals" => {
1449 if let Some(expected) = &assertion.value {
1450 let dart_val = format_value(expected);
1451 let _ = writeln!(out, " expect({method_call}, equals({dart_val}));");
1452 }
1453 }
1454 "is_true" => {
1455 let _ = writeln!(out, " expect({method_call}, isTrue);");
1456 }
1457 "is_false" => {
1458 let _ = writeln!(out, " expect({method_call}, isFalse);");
1459 }
1460 "greater_than_or_equal" => {
1461 if let Some(val) = &assertion.value {
1462 let dart_val = format_value(val);
1463 let _ = writeln!(out, " expect({method_call}, greaterThanOrEqualTo({dart_val}));");
1464 }
1465 }
1466 "count_min" => {
1467 if let Some(val) = &assertion.value {
1468 if let Some(n) = val.as_u64() {
1469 let _ = writeln!(out, " expect({method_call}.length, greaterThanOrEqualTo({n}));");
1470 }
1471 }
1472 }
1473 _ => {
1474 let _ = writeln!(out, " expect({method_call}, isNotNull);");
1475 }
1476 }
1477 }
1478 }
1479 other => {
1480 let _ = writeln!(out, " // skipped: unknown assertion type '{other}'");
1481 }
1482 }
1483}
1484
1485fn render_streaming_assertion_dart(out: &mut String, assertion: &Assertion, result_var: &str) {
1496 match assertion.assertion_type.as_str() {
1497 "not_error" => {
1498 let _ = writeln!(out, " expect({result_var}, isNotNull);");
1502 }
1503 "count_min" if assertion.field.as_deref() == Some("chunks") => {
1504 if let Some(serde_json::Value::Number(n)) = &assertion.value {
1505 let _ = writeln!(out, " expect({result_var}.length, greaterThanOrEqualTo({n}));");
1506 }
1507 }
1508 "equals" if assertion.field.as_deref() == Some("stream_content") => {
1509 if let Some(serde_json::Value::String(expected)) = &assertion.value {
1510 let escaped = escape_dart(expected);
1511 let _ = writeln!(
1512 out,
1513 " final _content = {result_var}.map((c) => c.choices.firstOrNull?.delta.content ?? '').join();"
1514 );
1515 let _ = writeln!(out, " expect(_content, equals('{escaped}'));");
1516 }
1517 }
1518 other => {
1519 let _ = writeln!(out, " // skipped streaming assertion: '{other}'");
1520 }
1521 }
1522}
1523
1524fn snake_to_camel(s: &str) -> String {
1526 let mut result = String::with_capacity(s.len());
1527 let mut next_upper = false;
1528 for ch in s.chars() {
1529 if ch == '_' {
1530 next_upper = true;
1531 } else if next_upper {
1532 result.extend(ch.to_uppercase());
1533 next_upper = false;
1534 } else {
1535 result.push(ch);
1536 }
1537 }
1538 result
1539}
1540
1541fn field_to_dart_accessor(path: &str) -> String {
1554 let mut result = String::with_capacity(path.len());
1555 for (i, segment) in path.split('.').enumerate() {
1556 if i > 0 {
1557 result.push('.');
1558 }
1559 if let Some(bracket_pos) = segment.find('[') {
1565 let name = &segment[..bracket_pos];
1566 let bracket = &segment[bracket_pos..];
1567 result.push_str(&name.to_lower_camel_case());
1568 result.push('!');
1569 result.push_str(bracket);
1570 } else {
1571 result.push_str(&segment.to_lower_camel_case());
1572 }
1573 }
1574 result
1575}
1576
1577fn emit_extraction_config_dart(overrides: &serde_json::Map<String, serde_json::Value>) -> String {
1583 let mut field_overrides: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1585 for (key, val) in overrides {
1586 let camel = snake_to_camel(key);
1587 let dart_val = match val {
1588 serde_json::Value::Bool(b) => {
1589 if *b {
1590 "true".to_string()
1591 } else {
1592 "false".to_string()
1593 }
1594 }
1595 serde_json::Value::Number(n) => n.to_string(),
1596 serde_json::Value::String(s) => format!("'{s}'"),
1597 _ => continue, };
1599 field_overrides.insert(camel, dart_val);
1600 }
1601
1602 let use_cache = field_overrides.remove("useCache").unwrap_or_else(|| "true".to_string());
1603 let enable_quality_processing = field_overrides
1604 .remove("enableQualityProcessing")
1605 .unwrap_or_else(|| "true".to_string());
1606 let force_ocr = field_overrides
1607 .remove("forceOcr")
1608 .unwrap_or_else(|| "false".to_string());
1609 let disable_ocr = field_overrides
1610 .remove("disableOcr")
1611 .unwrap_or_else(|| "false".to_string());
1612 let include_document_structure = field_overrides
1613 .remove("includeDocumentStructure")
1614 .unwrap_or_else(|| "false".to_string());
1615 let use_layout_for_markdown = field_overrides
1616 .remove("useLayoutForMarkdown")
1617 .unwrap_or_else(|| "false".to_string());
1618 let max_archive_depth = field_overrides
1619 .remove("maxArchiveDepth")
1620 .unwrap_or_else(|| "3".to_string());
1621
1622 format!(
1623 "ExtractionConfig(useCache: {use_cache}, enableQualityProcessing: {enable_quality_processing}, forceOcr: {force_ocr}, disableOcr: {disable_ocr}, resultFormat: ResultFormat.unified, outputFormat: OutputFormat.plain(), includeDocumentStructure: {include_document_structure}, useLayoutForMarkdown: {use_layout_for_markdown}, maxArchiveDepth: {max_archive_depth})"
1624 )
1625}
1626
1627struct DartTestClientRenderer {
1643 in_skip: Cell<bool>,
1646 is_redirect: Cell<bool>,
1649}
1650
1651impl DartTestClientRenderer {
1652 fn new(is_redirect: bool) -> Self {
1653 Self {
1654 in_skip: Cell::new(false),
1655 is_redirect: Cell::new(is_redirect),
1656 }
1657 }
1658}
1659
1660impl client::TestClientRenderer for DartTestClientRenderer {
1661 fn language_name(&self) -> &'static str {
1662 "dart"
1663 }
1664
1665 fn render_test_open(&self, out: &mut String, _fn_name: &str, description: &str, skip_reason: Option<&str>) {
1674 let escaped_desc = escape_dart(description);
1675 if let Some(reason) = skip_reason {
1676 let escaped_reason = escape_dart(reason);
1677 let _ = writeln!(out, " test('{escaped_desc}', () {{");
1678 let _ = writeln!(out, " markTestSkipped('{escaped_reason}');");
1679 let _ = writeln!(out, " }});");
1680 let _ = writeln!(out);
1681 self.in_skip.set(true);
1682 } else {
1683 let _ = writeln!(
1684 out,
1685 " test('{escaped_desc}', () => _serialized(() => _withRetry(() async {{"
1686 );
1687 self.in_skip.set(false);
1688 }
1689 }
1690
1691 fn render_test_close(&self, out: &mut String) {
1696 if self.in_skip.get() {
1697 return;
1699 }
1700 let _ = writeln!(out, " }})));");
1701 let _ = writeln!(out);
1702 }
1703
1704 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1714 const DART_RESTRICTED_HEADERS: &[&str] = &["content-length", "host", "transfer-encoding"];
1716
1717 let method = ctx.method.to_uppercase();
1718 let escaped_method = escape_dart(&method);
1719
1720 let fixture_path = escape_dart(ctx.path);
1722
1723 let has_explicit_content_type = ctx.headers.keys().any(|k| k.to_lowercase() == "content-type");
1725 let effective_content_type = if has_explicit_content_type {
1726 ctx.headers
1727 .iter()
1728 .find(|(k, _)| k.to_lowercase() == "content-type")
1729 .map(|(_, v)| v.as_str())
1730 .unwrap_or("application/json")
1731 } else if ctx.body.is_some() {
1732 ctx.content_type.unwrap_or("application/json")
1733 } else {
1734 ""
1735 };
1736
1737 let _ = writeln!(
1738 out,
1739 " final baseUrl = Platform.environment['MOCK_SERVER_URL'] ?? 'http://localhost:8080';"
1740 );
1741 let _ = writeln!(out, " final uri = Uri.parse('$baseUrl{fixture_path}');");
1742 let _ = writeln!(
1743 out,
1744 " final ioReq = await _httpClient.openUrl('{escaped_method}', uri);"
1745 );
1746
1747 if self.is_redirect.get() {
1750 let _ = writeln!(out, " ioReq.followRedirects = false;");
1751 }
1752
1753 if !effective_content_type.is_empty() {
1755 let escaped_ct = escape_dart(effective_content_type);
1756 let _ = writeln!(out, " ioReq.headers.set('content-type', '{escaped_ct}');");
1757 }
1758
1759 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
1761 header_pairs.sort_by_key(|(k, _)| k.as_str());
1762 for (name, value) in &header_pairs {
1763 if DART_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
1764 continue;
1765 }
1766 if name.to_lowercase() == "content-type" {
1767 continue; }
1769 let escaped_name = escape_dart(&name.to_lowercase());
1770 let escaped_value = escape_dart(value);
1771 let _ = writeln!(out, " ioReq.headers.set('{escaped_name}', '{escaped_value}');");
1772 }
1773
1774 if !ctx.cookies.is_empty() {
1776 let mut cookie_pairs: Vec<(&String, &String)> = ctx.cookies.iter().collect();
1777 cookie_pairs.sort_by_key(|(k, _)| k.as_str());
1778 let cookie_str: Vec<String> = cookie_pairs.iter().map(|(k, v)| format!("{k}={v}")).collect();
1779 let cookie_header = escape_dart(&cookie_str.join("; "));
1780 let _ = writeln!(out, " ioReq.headers.set('cookie', '{cookie_header}');");
1781 }
1782
1783 if let Some(body) = ctx.body {
1785 let json_str = serde_json::to_string(body).unwrap_or_default();
1786 let escaped = escape_dart(&json_str);
1787 let _ = writeln!(out, " final bodyBytes = utf8.encode('{escaped}');");
1788 let _ = writeln!(out, " ioReq.add(bodyBytes);");
1789 }
1790
1791 let _ = writeln!(out, " final ioResp = await ioReq.close();");
1792 if !self.is_redirect.get() {
1796 let _ = writeln!(out, " final bodyStr = await ioResp.transform(utf8.decoder).join();");
1797 };
1798 }
1799
1800 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1801 let _ = writeln!(
1802 out,
1803 " expect(ioResp.statusCode, equals({status}), reason: 'status code mismatch');"
1804 );
1805 }
1806
1807 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1810 let escaped_name = escape_dart(&name.to_lowercase());
1811 match expected {
1812 "<<present>>" => {
1813 let _ = writeln!(
1814 out,
1815 " expect(ioResp.headers.value('{escaped_name}'), isNotNull, reason: 'header {escaped_name} should be present');"
1816 );
1817 }
1818 "<<absent>>" => {
1819 let _ = writeln!(
1820 out,
1821 " expect(ioResp.headers.value('{escaped_name}'), isNull, reason: 'header {escaped_name} should be absent');"
1822 );
1823 }
1824 "<<uuid>>" => {
1825 let _ = writeln!(
1826 out,
1827 " expect(ioResp.headers.value('{escaped_name}'), matches(RegExp(r'^[0-9a-f]{{8}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{12}}$')), reason: 'header {escaped_name} should be a UUID');"
1828 );
1829 }
1830 exact => {
1831 let escaped_value = escape_dart(exact);
1832 let _ = writeln!(
1833 out,
1834 " expect(ioResp.headers.value('{escaped_name}'), contains('{escaped_value}'), reason: 'header {escaped_name} mismatch');"
1835 );
1836 }
1837 }
1838 }
1839
1840 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1845 match expected {
1846 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1847 let json_str = serde_json::to_string(expected).unwrap_or_default();
1848 let escaped = escape_dart(&json_str);
1849 let _ = writeln!(out, " final bodyJson = jsonDecode(bodyStr);");
1850 let _ = writeln!(out, " final expectedJson = jsonDecode('{escaped}');");
1851 let _ = writeln!(
1852 out,
1853 " expect(bodyJson, equals(expectedJson), reason: 'body mismatch');"
1854 );
1855 }
1856 serde_json::Value::String(s) => {
1857 let escaped = escape_dart(s);
1858 let _ = writeln!(
1859 out,
1860 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
1861 );
1862 }
1863 other => {
1864 let escaped = escape_dart(&other.to_string());
1865 let _ = writeln!(
1866 out,
1867 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
1868 );
1869 }
1870 }
1871 }
1872
1873 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1876 let _ = writeln!(
1877 out,
1878 " final partialJson = jsonDecode(bodyStr) as Map<String, dynamic>;"
1879 );
1880 if let Some(obj) = expected.as_object() {
1881 for (idx, (key, val)) in obj.iter().enumerate() {
1882 let escaped_key = escape_dart(key);
1883 let json_val = serde_json::to_string(val).unwrap_or_default();
1884 let escaped_val = escape_dart(&json_val);
1885 let _ = writeln!(out, " final _expectedField{idx} = jsonDecode('{escaped_val}');");
1888 let _ = writeln!(
1889 out,
1890 " expect(partialJson['{escaped_key}'], equals(_expectedField{idx}), reason: 'partial body field \\'{escaped_key}\\' mismatch');"
1891 );
1892 }
1893 }
1894 }
1895
1896 fn render_assert_validation_errors(
1898 &self,
1899 out: &mut String,
1900 _response_var: &str,
1901 errors: &[ValidationErrorExpectation],
1902 ) {
1903 let _ = writeln!(out, " final errBody = jsonDecode(bodyStr) as Map<String, dynamic>;");
1904 let _ = writeln!(out, " final errList = (errBody['errors'] ?? []) as List<dynamic>;");
1905 for ve in errors {
1906 let loc_dart: Vec<String> = ve.loc.iter().map(|s| format!("'{}'", escape_dart(s))).collect();
1907 let loc_str = loc_dart.join(", ");
1908 let escaped_msg = escape_dart(&ve.msg);
1909 let _ = writeln!(
1910 out,
1911 " expect(errList.any((e) => e is Map && (e['loc'] as List?)?.join(',') == [{loc_str}].join(',') && (e['msg'] as String? ?? '').contains('{escaped_msg}')), isTrue, reason: 'validation error not found: {escaped_msg}');"
1912 );
1913 }
1914 }
1915}
1916
1917fn render_http_test_case(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
1924 if http.expected_response.status_code == 101 {
1926 let description = escape_dart(&fixture.description);
1927 let _ = writeln!(out, " test('{description}', () {{");
1928 let _ = writeln!(
1929 out,
1930 " markTestSkipped('Skipped: Dart HttpClient cannot handle 101 Switching Protocols responses');"
1931 );
1932 let _ = writeln!(out, " }});");
1933 let _ = writeln!(out);
1934 return;
1935 }
1936
1937 let is_redirect = http.expected_response.status_code / 100 == 3;
1941 client::http_call::render_http_test(out, &DartTestClientRenderer::new(is_redirect), fixture);
1942}
1943
1944fn mime_from_extension(path: &str) -> Option<&'static str> {
1949 let ext = path.rsplit('.').next()?;
1950 match ext.to_lowercase().as_str() {
1951 "docx" => Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
1952 "xlsx" => Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
1953 "pptx" => Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
1954 "pdf" => Some("application/pdf"),
1955 "txt" | "text" => Some("text/plain"),
1956 "html" | "htm" => Some("text/html"),
1957 "json" => Some("application/json"),
1958 "xml" => Some("application/xml"),
1959 "csv" => Some("text/csv"),
1960 "md" | "markdown" => Some("text/markdown"),
1961 "png" => Some("image/png"),
1962 "jpg" | "jpeg" => Some("image/jpeg"),
1963 "gif" => Some("image/gif"),
1964 "zip" => Some("application/zip"),
1965 "odt" => Some("application/vnd.oasis.opendocument.text"),
1966 "ods" => Some("application/vnd.oasis.opendocument.spreadsheet"),
1967 "odp" => Some("application/vnd.oasis.opendocument.presentation"),
1968 "rtf" => Some("application/rtf"),
1969 "epub" => Some("application/epub+zip"),
1970 "msg" => Some("application/vnd.ms-outlook"),
1971 "eml" => Some("message/rfc822"),
1972 _ => None,
1973 }
1974}
1975
1976fn emit_dart_batch_item_array(arr: &serde_json::Value, elem_type: &str) -> String {
1983 let items: Vec<String> = arr
1984 .as_array()
1985 .map(|a| a.as_slice())
1986 .unwrap_or_default()
1987 .iter()
1988 .filter_map(|item| {
1989 let obj = item.as_object()?;
1990 match elem_type {
1991 "BatchBytesItem" => {
1992 let content_bytes = obj
1993 .get("content")
1994 .and_then(|v| v.as_array())
1995 .map(|arr| {
1996 let nums: Vec<String> =
1997 arr.iter().filter_map(|v| v.as_u64().map(|n| n.to_string())).collect();
1998 format!("Uint8List.fromList([{}])", nums.join(", "))
1999 })
2000 .unwrap_or_else(|| "Uint8List(0)".to_string());
2001 let mime_type = obj
2002 .get("mime_type")
2003 .and_then(|v| v.as_str())
2004 .unwrap_or("application/octet-stream");
2005 Some(format!(
2006 "BatchBytesItem(content: {content_bytes}, mimeType: '{}')",
2007 escape_dart(mime_type)
2008 ))
2009 }
2010 "BatchFileItem" => {
2011 let path = obj.get("path").and_then(|v| v.as_str()).unwrap_or("");
2012 Some(format!("BatchFileItem(path: '{}')", escape_dart(path)))
2013 }
2014 _ => None,
2015 }
2016 })
2017 .collect();
2018 format!("[{}]", items.join(", "))
2019}
2020
2021pub(super) fn escape_dart(s: &str) -> String {
2023 s.replace('\\', "\\\\")
2024 .replace('\'', "\\'")
2025 .replace('\n', "\\n")
2026 .replace('\r', "\\r")
2027 .replace('\t', "\\t")
2028 .replace('$', "\\$")
2029}
2030
2031fn type_name_to_create_from_json_dart(type_name: &str) -> String {
2039 let mut snake = String::with_capacity(type_name.len() + 8);
2041 for (i, ch) in type_name.char_indices() {
2042 if ch.is_uppercase() {
2043 if i > 0 {
2044 snake.push('_');
2045 }
2046 snake.extend(ch.to_lowercase());
2047 } else {
2048 snake.push(ch);
2049 }
2050 }
2051 let rust_fn = format!("create_{snake}_from_json");
2054 rust_fn
2056 .split('_')
2057 .enumerate()
2058 .map(|(i, part)| {
2059 if i == 0 {
2060 part.to_string()
2061 } else {
2062 let mut chars = part.chars();
2063 match chars.next() {
2064 None => String::new(),
2065 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
2066 }
2067 }
2068 })
2069 .collect::<Vec<_>>()
2070 .join("")
2071}