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 ) -> Result<Vec<GeneratedFile>> {
36 let lang = self.language_name();
37 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
38
39 let mut files = Vec::new();
40
41 let dart_pkg = e2e_config.resolve_package("dart");
43 let pkg_name = dart_pkg
44 .as_ref()
45 .and_then(|p| p.name.as_ref())
46 .cloned()
47 .unwrap_or_else(|| config.dart_pubspec_name());
48 let pkg_path = dart_pkg
49 .as_ref()
50 .and_then(|p| p.path.as_ref())
51 .cloned()
52 .unwrap_or_else(|| "../../packages/dart".to_string());
53 let pkg_version = dart_pkg
54 .as_ref()
55 .and_then(|p| p.version.as_ref())
56 .cloned()
57 .or_else(|| config.resolved_version())
58 .unwrap_or_else(|| "0.1.0".to_string());
59
60 files.push(GeneratedFile {
62 path: output_base.join("pubspec.yaml"),
63 content: render_pubspec(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
64 generated_header: false,
65 });
66
67 files.push(GeneratedFile {
70 path: output_base.join("dart_test.yaml"),
71 content: concat!(
72 "# Generated by alef — DO NOT EDIT.\n",
73 "# Run test files sequentially to avoid overwhelming the mock server with\n",
74 "# concurrent keep-alive connections.\n",
75 "concurrency: 1\n",
76 )
77 .to_string(),
78 generated_header: false,
79 });
80
81 let test_base = output_base.join("test");
82
83 let bridge_class = config.dart_bridge_class_name();
85
86 let frb_module_name = config.name.replace('-', "_");
90
91 let dart_stub_methods: std::collections::HashSet<String> = config
96 .dart
97 .as_ref()
98 .map(|d| d.stub_methods.iter().cloned().collect())
99 .unwrap_or_default();
100
101 for group in groups {
102 let active: Vec<&Fixture> = group
103 .fixtures
104 .iter()
105 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
106 .filter(|f| {
107 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
108 let resolved_function = call_config
109 .overrides
110 .get(lang)
111 .and_then(|o| o.function.as_ref())
112 .cloned()
113 .unwrap_or_else(|| call_config.function.clone());
114 !dart_stub_methods.contains(&resolved_function)
115 })
116 .collect();
117
118 if active.is_empty() {
119 continue;
120 }
121
122 let filename = format!("{}_test.dart", sanitize_filename(&group.category));
123 let content = render_test_file(
124 &group.category,
125 &active,
126 e2e_config,
127 lang,
128 &pkg_name,
129 &frb_module_name,
130 &bridge_class,
131 );
132 files.push(GeneratedFile {
133 path: test_base.join(filename),
134 content,
135 generated_header: true,
136 });
137 }
138
139 Ok(files)
140 }
141
142 fn language_name(&self) -> &'static str {
143 "dart"
144 }
145}
146
147fn render_pubspec(
152 pkg_name: &str,
153 pkg_path: &str,
154 pkg_version: &str,
155 dep_mode: crate::config::DependencyMode,
156) -> String {
157 let test_ver = pub_dev::TEST_PACKAGE;
158 let http_ver = pub_dev::HTTP_PACKAGE;
159
160 let dep_block = match dep_mode {
161 crate::config::DependencyMode::Registry => {
162 format!(" {pkg_name}: ^{pkg_version}")
163 }
164 crate::config::DependencyMode::Local => {
165 format!(" {pkg_name}:\n path: {pkg_path}")
166 }
167 };
168
169 let sdk = alef_core::template_versions::toolchain::DART_SDK_CONSTRAINT;
170 format!(
171 r#"name: e2e_dart
172version: 0.1.0
173publish_to: none
174
175environment:
176 sdk: "{sdk}"
177
178dependencies:
179{dep_block}
180
181dev_dependencies:
182 test: {test_ver}
183 http: {http_ver}
184"#
185 )
186}
187
188fn render_test_file(
189 category: &str,
190 fixtures: &[&Fixture],
191 e2e_config: &E2eConfig,
192 lang: &str,
193 pkg_name: &str,
194 frb_module_name: &str,
195 bridge_class: &str,
196) -> String {
197 let mut out = String::new();
198 out.push_str(&hash::header(CommentStyle::DoubleSlash));
199
200 let field_resolver = FieldResolver::new(
205 &e2e_config.fields,
206 &e2e_config.fields_optional,
207 &e2e_config.result_fields,
208 &e2e_config.fields_array,
209 &e2e_config.fields_method_calls,
210 );
211
212 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
214
215 let has_batch_byte_items = fixtures.iter().any(|f| {
217 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
218 call_config.args.iter().any(|a| {
219 a.element_type.as_deref() == Some("BatchBytesItem") && resolve_field(&f.input, &a.field).is_array()
220 })
221 });
222
223 let needs_chdir = fixtures.iter().any(|f| {
227 if f.is_http_test() {
228 return false;
229 }
230 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
231 call_config
232 .args
233 .iter()
234 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
235 });
236
237 let has_handle_args = fixtures.iter().any(|f| {
243 if f.is_http_test() {
244 return false;
245 }
246 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
247 call_config
248 .args
249 .iter()
250 .any(|a| a.arg_type == "json_object" && super::resolve_field(&f.input, &a.field).is_array())
251 });
252
253 let lang_client_factory = e2e_config
259 .call
260 .overrides
261 .get(lang)
262 .and_then(|o| o.client_factory.as_deref())
263 .is_some();
264 let has_mock_url_refs = lang_client_factory
265 || fixtures.iter().any(|f| {
266 if f.is_http_test() {
267 return false;
268 }
269 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
270 if call_config.args.iter().any(|a| a.arg_type == "mock_url") {
271 return true;
272 }
273 call_config
274 .overrides
275 .get(lang)
276 .and_then(|o| o.client_factory.as_deref())
277 .is_some()
278 });
279
280 let _ = writeln!(out, "import 'package:test/test.dart';");
281 if has_http_fixtures || needs_chdir || has_mock_url_refs {
286 let _ = writeln!(out, "import 'dart:io';");
287 }
288 if has_batch_byte_items {
289 let _ = writeln!(out, "import 'dart:typed_data';");
290 }
291 let _ = writeln!(out, "import 'package:{pkg_name}/{pkg_name}.dart';");
292 let _ = writeln!(
298 out,
299 "import 'package:{pkg_name}/src/{frb_module_name}_bridge_generated/frb_generated.dart' show RustLib;"
300 );
301 if has_http_fixtures {
302 let _ = writeln!(out, "import 'dart:async';");
303 }
304 if has_http_fixtures || has_handle_args {
306 let _ = writeln!(out, "import 'dart:convert';");
307 }
308 let _ = writeln!(out);
309
310 if has_http_fixtures {
320 let _ = writeln!(out, "HttpClient _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
321 let _ = writeln!(out);
322 let _ = writeln!(out, "var _lock = Future<void>.value();");
323 let _ = writeln!(out);
324 let _ = writeln!(out, "Future<T> _serialized<T>(Future<T> Function() fn) async {{");
325 let _ = writeln!(out, " final current = _lock;");
326 let _ = writeln!(out, " final next = Completer<void>();");
327 let _ = writeln!(out, " _lock = next.future;");
328 let _ = writeln!(out, " try {{");
329 let _ = writeln!(out, " await current;");
330 let _ = writeln!(out, " return await fn();");
331 let _ = writeln!(out, " }} finally {{");
332 let _ = writeln!(out, " next.complete();");
333 let _ = writeln!(out, " }}");
334 let _ = writeln!(out, "}}");
335 let _ = writeln!(out);
336 let _ = writeln!(out, "Future<T> _withRetry<T>(Future<T> Function() fn) async {{");
339 let _ = writeln!(out, " try {{");
340 let _ = writeln!(out, " return await fn();");
341 let _ = writeln!(out, " }} on SocketException {{");
342 let _ = writeln!(out, " _httpClient.close(force: true);");
343 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
344 let _ = writeln!(out, " return fn();");
345 let _ = writeln!(out, " }} on HttpException {{");
346 let _ = writeln!(out, " _httpClient.close(force: true);");
347 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
348 let _ = writeln!(out, " return fn();");
349 let _ = writeln!(out, " }}");
350 let _ = writeln!(out, "}}");
351 let _ = writeln!(out);
352 }
353
354 let _ = writeln!(out, "// E2e tests for category: {category}");
355 let _ = writeln!(out, "void main() {{");
356
357 let _ = writeln!(out, " setUpAll(() async {{");
364 let _ = writeln!(out, " await RustLib.init();");
365 if needs_chdir {
366 let test_docs_path = e2e_config.test_documents_relative_from(0);
367 let _ = writeln!(
368 out,
369 " final _testDocs = Platform.environment['FIXTURES_DIR'] ?? '{test_docs_path}';"
370 );
371 let _ = writeln!(out, " final _dir = Directory(_testDocs);");
372 let _ = writeln!(out, " if (_dir.existsSync()) Directory.current = _dir;");
373 }
374 let _ = writeln!(out, " }});");
375 let _ = writeln!(out);
376
377 if has_http_fixtures {
379 let _ = writeln!(out, " tearDownAll(() => _httpClient.close());");
380 let _ = writeln!(out);
381 }
382
383 for fixture in fixtures {
384 render_test_case(&mut out, fixture, e2e_config, lang, bridge_class, &field_resolver);
385 }
386
387 let _ = writeln!(out, "}}");
388 out
389}
390
391fn render_test_case(
392 out: &mut String,
393 fixture: &Fixture,
394 e2e_config: &E2eConfig,
395 lang: &str,
396 bridge_class: &str,
397 field_resolver: &FieldResolver,
398) {
399 if let Some(http) = &fixture.http {
401 render_http_test_case(out, fixture, http);
402 return;
403 }
404
405 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
407 let call_overrides = call_config.overrides.get(lang);
408 let mut function_name = call_overrides
409 .and_then(|o| o.function.as_ref())
410 .cloned()
411 .unwrap_or_else(|| call_config.function.clone());
412 function_name = function_name
414 .split('_')
415 .enumerate()
416 .map(|(i, part)| {
417 if i == 0 {
418 part.to_string()
419 } else {
420 let mut chars = part.chars();
421 match chars.next() {
422 None => String::new(),
423 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
424 }
425 }
426 })
427 .collect::<Vec<_>>()
428 .join("");
429 let result_var = &call_config.result_var;
430 let description = escape_dart(&fixture.description);
431 let fixture_id = &fixture.id;
432 let _is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
435
436 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
437 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
438 let result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple) || call_config.result_is_simple;
443
444 let options_type: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
451 let options_via: &str = call_overrides
452 .and_then(|o| o.options_via.as_deref())
453 .unwrap_or("kwargs");
454
455 let file_path_for_mime: Option<&str> = call_config
463 .args
464 .iter()
465 .find(|a| a.arg_type == "file_path")
466 .and_then(|a| resolve_field(&fixture.input, &a.field).as_str());
467
468 let has_file_path_arg = call_config.args.iter().any(|a| a.arg_type == "file_path");
475 let caller_supplied_override = call_overrides.and_then(|o| o.function.as_ref()).is_some();
478 if has_file_path_arg && !caller_supplied_override {
479 function_name = match function_name.as_str() {
480 "extractFile" => "extractBytes".to_string(),
481 "extractFileSync" => "extractBytesSync".to_string(),
482 other => other.to_string(),
483 };
484 }
485
486 let mut setup_lines: Vec<String> = Vec::new();
489 let mut args = Vec::new();
490
491 for arg_def in &call_config.args {
492 match arg_def.arg_type.as_str() {
493 "mock_url" => {
494 let name = arg_def.name.clone();
495 if fixture.has_host_root_route() {
496 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
497 setup_lines.push(format!(
498 r#"final {name} = Platform.environment["{env_key}"] ?? (Platform.environment["MOCK_SERVER_URL"]! + "/fixtures/{fixture_id}");"#
499 ));
500 } else {
501 setup_lines.push(format!(
502 r#"final {name} = "${{Platform.environment["MOCK_SERVER_URL"] ?? "http://localhost:8080"}}/fixtures/{fixture_id}";"#
503 ));
504 }
505 args.push(name);
506 continue;
507 }
508 "handle" => {
509 let name = arg_def.name.clone();
510 let field = arg_def.field.strip_prefix("input.").unwrap_or(&arg_def.field);
511 let config_value = fixture.input.get(field).cloned().unwrap_or(serde_json::Value::Null);
512 let create_fn = {
514 let mut chars = name.chars();
515 let pascal = match chars.next() {
516 None => String::new(),
517 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
518 };
519 format!("create{pascal}")
520 };
521 if config_value.is_null()
522 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
523 {
524 setup_lines.push(format!("final {name} = await {bridge_class}.{create_fn}();"));
525 } else {
526 let json_str = serde_json::to_string(&config_value).unwrap_or_default();
527 let config_var = format!("{name}Config");
528 setup_lines.push(format!(
533 "final {config_var} = await createCrawlConfigFromJson(json: r'{json_str}');"
534 ));
535 setup_lines.push(format!(
537 "final {name} = await {bridge_class}.{create_fn}(config: {config_var});"
538 ));
539 }
540 args.push(name);
541 continue;
542 }
543 _ => {}
544 }
545
546 let arg_value = resolve_field(&fixture.input, &arg_def.field);
547 match arg_def.arg_type.as_str() {
548 "bytes" | "file_path" => {
549 if let serde_json::Value::String(file_path) = arg_value {
554 args.push(format!("File('{}').readAsBytesSync()", file_path));
555 }
556 }
557 "string" => {
558 let dart_param_name = snake_to_camel(&arg_def.name);
573 let mime_required_due_to_remap = has_file_path_arg
574 && arg_def.name == "mime_type"
575 && (function_name == "extractBytes" || function_name == "extractBytesSync");
576 let is_optional = arg_def.optional && !mime_required_due_to_remap;
577 match arg_value {
578 serde_json::Value::String(s) => {
579 let literal = format!("'{}'", escape_dart(s));
580 if is_optional {
581 args.push(format!("{dart_param_name}: {literal}"));
582 } else {
583 args.push(literal);
584 }
585 }
586 serde_json::Value::Null
587 if arg_def.optional
588 && arg_def.name == "mime_type" =>
591 {
592 let inferred = file_path_for_mime
593 .and_then(mime_from_extension)
594 .unwrap_or("application/octet-stream");
595 if is_optional {
596 args.push(format!("{dart_param_name}: '{inferred}'"));
597 } else {
598 args.push(format!("'{inferred}'"));
599 }
600 }
601 _ => {}
603 }
604 }
605 "json_object" => {
606 if let Some(elem_type) = &arg_def.element_type {
608 if (elem_type == "BatchBytesItem" || elem_type == "BatchFileItem") && arg_value.is_array() {
609 let dart_items = emit_dart_batch_item_array(arg_value, elem_type);
610 args.push(dart_items);
611 } else if elem_type == "String" && arg_value.is_array() {
612 let items: Vec<String> = arg_value
619 .as_array()
620 .unwrap()
621 .iter()
622 .filter_map(|v| v.as_str())
623 .map(|s| format!("'{}'", escape_dart(s)))
624 .collect();
625 args.push(format!("<String>[{}]", items.join(", ")));
626 }
627 } else if options_via == "from_json" {
628 if let Some(opts_type) = options_type {
638 if !arg_value.is_null() {
639 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
640 let escaped_json = escape_dart(&json_str);
643 let var_name = format!("_{}", arg_def.name);
644 let dart_fn = type_name_to_create_from_json_dart(opts_type);
645 setup_lines.push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
646 args.push(format!("req: {var_name}"));
649 }
650 }
651 } else if arg_def.name == "config" {
652 if let serde_json::Value::Object(map) = &arg_value {
653 if !map.is_empty() {
654 let explicit_options =
663 options_type.is_some_and(|t| t != "ExtractionConfig" && t != "FileExtractionConfig");
664 let has_non_scalar = map.values().any(|v| {
665 matches!(
666 v,
667 serde_json::Value::String(_)
668 | serde_json::Value::Object(_)
669 | serde_json::Value::Array(_)
670 )
671 });
672 if explicit_options || has_non_scalar {
673 let opts_type = options_type.unwrap_or("ExtractionConfig");
674 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
675 let escaped_json = escape_dart(&json_str);
676 let var_name = format!("_{}", arg_def.name);
677 let dart_fn = type_name_to_create_from_json_dart(opts_type);
678 setup_lines
679 .push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
680 args.push(var_name);
681 } else {
682 args.push(emit_extraction_config_dart(map));
688 }
689 }
690 }
691 } else if arg_value.is_array() {
693 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
696 let var_name = arg_def.name.clone();
697 setup_lines.push(format!(
698 "final {var_name} = (jsonDecode(r'{json_str}') as List<dynamic>).cast<String>();"
699 ));
700 args.push(var_name);
701 } else if let serde_json::Value::Object(map) = &arg_value {
702 if !map.is_empty() {
716 if let Some(opts_type) = options_type {
717 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
718 let escaped_json = escape_dart(&json_str);
719 let dart_param_name = snake_to_camel(&arg_def.name);
720 let var_name = format!("_{}", arg_def.name);
721 let dart_fn = type_name_to_create_from_json_dart(opts_type);
722 if fixture.visitor.is_some() {
723 setup_lines.push(format!(
724 "final {var_name} = await {dart_fn}WithVisitor(json: '{escaped_json}', visitor: _visitor);"
725 ));
726 } else {
727 setup_lines
728 .push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
729 }
730 if arg_def.optional {
731 args.push(format!("{dart_param_name}: {var_name}"));
732 } else {
733 args.push(var_name);
734 }
735 }
736 }
737 }
738 }
739 _ => {}
740 }
741 }
742
743 if let Some(visitor_spec) = &fixture.visitor {
758 let mut visitor_setup: Vec<String> = Vec::new();
759 let _ = super::dart_visitors::build_dart_visitor(&mut visitor_setup, visitor_spec);
760 for line in visitor_setup.into_iter().rev() {
763 setup_lines.insert(0, line);
764 }
765
766 let already_has_options = args.iter().any(|a| a.starts_with("options:") || a == "_options");
770 if !already_has_options {
771 if let Some(opts_type) = options_type {
772 let dart_fn = type_name_to_create_from_json_dart(opts_type);
773 setup_lines.push(format!(
774 "final _options = await {dart_fn}WithVisitor(json: '{{}}', visitor: _visitor);"
775 ));
776 args.push("options: _options".to_string());
777 }
778 }
779 }
780
781 let client_factory: Option<&str> = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
785 e2e_config
786 .call
787 .overrides
788 .get(lang)
789 .and_then(|o| o.client_factory.as_deref())
790 });
791
792 let client_factory_camel: Option<String> = client_factory.map(|f| {
794 f.split('_')
795 .enumerate()
796 .map(|(i, part)| {
797 if i == 0 {
798 part.to_string()
799 } else {
800 let mut chars = part.chars();
801 match chars.next() {
802 None => String::new(),
803 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
804 }
805 }
806 })
807 .collect::<Vec<_>>()
808 .join("")
809 });
810
811 let _ = writeln!(out, " test('{description}', () async {{");
815
816 let args_str = args.join(", ");
817 let receiver_class = call_overrides
818 .and_then(|o| o.class.as_ref())
819 .cloned()
820 .unwrap_or_else(|| bridge_class.to_string());
821
822 let (receiver, extra_setup): (String, Option<String>) = if let Some(factory) = &client_factory_camel {
826 let has_mock_url = call_config.args.iter().any(|a| a.arg_type == "mock_url");
827 let mock_url_setup = if !has_mock_url {
828 if fixture.has_host_root_route() {
830 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
831 Some(format!(
832 "final _mockUrl = Platform.environment[\"{env_key}\"] ?? (Platform.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\");"
833 ))
834 } else {
835 Some(format!(
836 r#"final _mockUrl = "${{Platform.environment["MOCK_SERVER_URL"] ?? "http://localhost:8080"}}/fixtures/{fixture_id}";"#
837 ))
838 }
839 } else {
840 None
841 };
842 let url_expr = if has_mock_url {
843 call_config
846 .args
847 .iter()
848 .find(|a| a.arg_type == "mock_url")
849 .map(|a| a.name.clone())
850 .unwrap_or_else(|| "_mockUrl".to_string())
851 } else {
852 "_mockUrl".to_string()
853 };
854 let create_line = format!("final _client = await {receiver_class}.{factory}('test-key', baseUrl: {url_expr});");
855 let full_setup = if let Some(url_line) = mock_url_setup {
856 Some(format!("{url_line}\n {create_line}"))
857 } else {
858 Some(create_line)
859 };
860 ("_client".to_string(), full_setup)
861 } else {
862 (receiver_class.clone(), None)
863 };
864
865 if expects_error && (!setup_lines.is_empty() || extra_setup.is_some()) {
866 let _ = writeln!(out, " await expectLater(() async {{");
870 for line in &setup_lines {
871 let _ = writeln!(out, " {line}");
872 }
873 if let Some(extra) = &extra_setup {
874 for line in extra.lines() {
875 let _ = writeln!(out, " {line}");
876 }
877 }
878 if is_streaming {
879 let _ = writeln!(out, " return {receiver}.{function_name}({args_str}).toList();");
880 } else {
881 let _ = writeln!(out, " return {receiver}.{function_name}({args_str});");
882 }
883 let _ = writeln!(out, " }}(), throwsA(anything));");
884 } else if expects_error {
885 if let Some(extra) = &extra_setup {
887 for line in extra.lines() {
888 let _ = writeln!(out, " {line}");
889 }
890 }
891 if is_streaming {
892 let _ = writeln!(
893 out,
894 " await expectLater({receiver}.{function_name}({args_str}).toList(), throwsA(anything));"
895 );
896 } else {
897 let _ = writeln!(
898 out,
899 " await expectLater({receiver}.{function_name}({args_str}), throwsA(anything));"
900 );
901 }
902 } else {
903 for line in &setup_lines {
904 let _ = writeln!(out, " {line}");
905 }
906 if let Some(extra) = &extra_setup {
907 for line in extra.lines() {
908 let _ = writeln!(out, " {line}");
909 }
910 }
911 if is_streaming {
912 let _ = writeln!(
913 out,
914 " final {result_var} = await {receiver}.{function_name}({args_str}).toList();"
915 );
916 } else {
917 let _ = writeln!(
918 out,
919 " final {result_var} = await {receiver}.{function_name}({args_str});"
920 );
921 }
922 for assertion in &fixture.assertions {
923 if is_streaming {
924 render_streaming_assertion_dart(out, assertion, result_var);
925 } else {
926 render_assertion_dart(out, assertion, result_var, result_is_simple, field_resolver);
927 }
928 }
929 }
930
931 let _ = writeln!(out, " }});");
932 let _ = writeln!(out);
933}
934
935fn dart_length_expr(field_accessor: &str, field: Option<&str>, field_resolver: &FieldResolver) -> String {
943 let is_optional = field
944 .map(|f| {
945 let resolved = field_resolver.resolve(f);
946 field_resolver.is_optional(f) || field_resolver.is_optional(resolved)
947 })
948 .unwrap_or(false);
949 if is_optional {
950 format!("{field_accessor}?.length ?? 0")
951 } else {
952 format!("{field_accessor}.length")
953 }
954}
955
956fn dart_format_value(val: &serde_json::Value) -> String {
957 match val {
958 serde_json::Value::String(s) => format!("'{}'", escape_dart(s)),
959 serde_json::Value::Bool(b) => b.to_string(),
960 serde_json::Value::Number(n) => n.to_string(),
961 serde_json::Value::Null => "null".to_string(),
962 other => format!("'{}'", escape_dart(&other.to_string())),
963 }
964}
965
966fn render_assertion_dart(
977 out: &mut String,
978 assertion: &Assertion,
979 result_var: &str,
980 result_is_simple: bool,
981 field_resolver: &FieldResolver,
982) {
983 if !result_is_simple {
987 if let Some(f) = assertion.field.as_deref() {
988 let head = f.split("[].").next().unwrap_or(f);
991 if !head.is_empty() && !field_resolver.is_valid_for_result(head) {
992 let _ = writeln!(out, " // skipped: field '{f}' not available on dart result type");
993 return;
994 }
995 }
996 }
997
998 if let Some(f) = assertion.field.as_deref() {
1004 if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
1005 let _ = writeln!(
1006 out,
1007 " // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Dart)"
1008 );
1009 return;
1010 }
1011 }
1012
1013 if let Some(f) = assertion.field.as_deref() {
1015 if let Some(dot) = f.find("[].") {
1016 let resolved_full = field_resolver.resolve(f);
1021 let (array_part, elem_part) = match resolved_full.find("[].") {
1022 Some(rdot) => (&resolved_full[..rdot], &resolved_full[rdot + 3..]),
1023 None => (&f[..dot], &f[dot + 3..]),
1026 };
1027 let array_accessor = if array_part.is_empty() {
1028 result_var.to_string()
1029 } else {
1030 field_resolver.accessor(array_part, "dart", result_var)
1031 };
1032 let elem_accessor = field_to_dart_accessor(elem_part);
1033 match assertion.assertion_type.as_str() {
1034 "contains" => {
1035 if let Some(expected) = &assertion.value {
1036 let dart_val = dart_format_value(expected);
1037 let _ = writeln!(
1038 out,
1039 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
1040 );
1041 }
1042 }
1043 "contains_all" => {
1044 if let Some(values) = &assertion.values {
1045 for val in values {
1046 let dart_val = dart_format_value(val);
1047 let _ = writeln!(
1048 out,
1049 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
1050 );
1051 }
1052 }
1053 }
1054 "not_contains" => {
1055 if let Some(expected) = &assertion.value {
1056 let dart_val = dart_format_value(expected);
1057 let _ = writeln!(
1058 out,
1059 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
1060 );
1061 } else if let Some(values) = &assertion.values {
1062 for val in values {
1063 let dart_val = dart_format_value(val);
1064 let _ = writeln!(
1065 out,
1066 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
1067 );
1068 }
1069 }
1070 }
1071 "not_empty" => {
1072 let _ = writeln!(
1073 out,
1074 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().isNotEmpty), isTrue);"
1075 );
1076 }
1077 other => {
1078 let _ = writeln!(
1079 out,
1080 " // skipped: unsupported traversal assertion '{other}' on '{f}'"
1081 );
1082 }
1083 }
1084 return;
1085 }
1086 }
1087
1088 let field_accessor = if result_is_simple {
1089 result_var.to_string()
1093 } else {
1094 match assertion.field.as_deref() {
1095 Some(f) if !f.is_empty() => field_resolver.accessor(f, "dart", result_var),
1100 _ => result_var.to_string(),
1101 }
1102 };
1103
1104 let format_value = |val: &serde_json::Value| -> String { dart_format_value(val) };
1105
1106 match assertion.assertion_type.as_str() {
1107 "equals" | "field_equals" => {
1108 if let Some(expected) = &assertion.value {
1109 let dart_val = format_value(expected);
1110 if expected.is_string() {
1114 let _ = writeln!(
1115 out,
1116 " expect({field_accessor}.toString().trim(), equals({dart_val}.toString().trim()));"
1117 );
1118 } else {
1119 let _ = writeln!(out, " expect({field_accessor}, equals({dart_val}));");
1120 }
1121 } else {
1122 let _ = writeln!(
1123 out,
1124 " // skipped: '{}' assertion missing value",
1125 assertion.assertion_type
1126 );
1127 }
1128 }
1129 "not_equals" => {
1130 if let Some(expected) = &assertion.value {
1131 let dart_val = format_value(expected);
1132 if expected.is_string() {
1133 let _ = writeln!(
1134 out,
1135 " expect({field_accessor}.toString().trim(), isNot(equals({dart_val}.toString().trim())));"
1136 );
1137 } else {
1138 let _ = writeln!(out, " expect({field_accessor}, isNot(equals({dart_val})));");
1139 }
1140 }
1141 }
1142 "contains" => {
1143 if let Some(expected) = &assertion.value {
1144 let dart_val = format_value(expected);
1145 let _ = writeln!(out, " expect({field_accessor}, contains({dart_val}));");
1146 } else {
1147 let _ = writeln!(out, " // skipped: 'contains' assertion missing value");
1148 }
1149 }
1150 "contains_all" => {
1151 if let Some(values) = &assertion.values {
1152 for val in values {
1153 let dart_val = format_value(val);
1154 let _ = writeln!(out, " expect({field_accessor}, contains({dart_val}));");
1155 }
1156 }
1157 }
1158 "contains_any" => {
1159 if let Some(values) = &assertion.values {
1160 let checks: Vec<String> = values
1161 .iter()
1162 .map(|v| {
1163 let dart_val = format_value(v);
1164 format!("{field_accessor}.contains({dart_val})")
1165 })
1166 .collect();
1167 let joined = checks.join(" || ");
1168 let _ = writeln!(out, " expect({joined}, isTrue);");
1169 }
1170 }
1171 "not_contains" => {
1172 if let Some(expected) = &assertion.value {
1173 let dart_val = format_value(expected);
1174 let _ = writeln!(out, " expect({field_accessor}, isNot(contains({dart_val})));");
1175 } else if let Some(values) = &assertion.values {
1176 for val in values {
1177 let dart_val = format_value(val);
1178 let _ = writeln!(out, " expect({field_accessor}, isNot(contains({dart_val})));");
1179 }
1180 }
1181 }
1182 "not_empty" => {
1183 let is_collection = assertion.field.as_deref().is_some_and(|f| {
1188 let resolved = field_resolver.resolve(f);
1189 field_resolver.is_array(f) || field_resolver.is_array(resolved)
1190 });
1191 if is_collection {
1192 let _ = writeln!(out, " expect({field_accessor}, isNotEmpty);");
1193 } else {
1194 let _ = writeln!(out, " expect({field_accessor}, isNotNull);");
1195 }
1196 }
1197 "is_empty" => {
1198 let _ = writeln!(out, " expect({field_accessor}, anyOf(isNull, isEmpty));");
1202 }
1203 "starts_with" => {
1204 if let Some(expected) = &assertion.value {
1205 let dart_val = format_value(expected);
1206 let _ = writeln!(out, " expect({field_accessor}, startsWith({dart_val}));");
1207 }
1208 }
1209 "ends_with" => {
1210 if let Some(expected) = &assertion.value {
1211 let dart_val = format_value(expected);
1212 let _ = writeln!(out, " expect({field_accessor}, endsWith({dart_val}));");
1213 }
1214 }
1215 "min_length" => {
1216 if let Some(val) = &assertion.value {
1217 if let Some(n) = val.as_u64() {
1218 let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
1219 let _ = writeln!(out, " expect({length_expr}, greaterThanOrEqualTo({n}));");
1220 }
1221 }
1222 }
1223 "max_length" => {
1224 if let Some(val) = &assertion.value {
1225 if let Some(n) = val.as_u64() {
1226 let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
1227 let _ = writeln!(out, " expect({length_expr}, lessThanOrEqualTo({n}));");
1228 }
1229 }
1230 }
1231 "count_equals" => {
1232 if let Some(val) = &assertion.value {
1233 if let Some(n) = val.as_u64() {
1234 let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
1235 let _ = writeln!(out, " expect({length_expr}, equals({n}));");
1236 }
1237 }
1238 }
1239 "count_min" => {
1240 if let Some(val) = &assertion.value {
1241 if let Some(n) = val.as_u64() {
1242 let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
1243 let _ = writeln!(out, " expect({length_expr}, greaterThanOrEqualTo({n}));");
1244 }
1245 }
1246 }
1247 "matches_regex" => {
1248 if let Some(expected) = &assertion.value {
1249 let dart_val = format_value(expected);
1250 let _ = writeln!(out, " expect({field_accessor}, matches(RegExp({dart_val})));");
1251 }
1252 }
1253 "is_true" => {
1254 let _ = writeln!(out, " expect({field_accessor}, isTrue);");
1255 }
1256 "is_false" => {
1257 let _ = writeln!(out, " expect({field_accessor}, isFalse);");
1258 }
1259 "greater_than" => {
1260 if let Some(val) = &assertion.value {
1261 let dart_val = format_value(val);
1262 let _ = writeln!(out, " expect({field_accessor}, greaterThan({dart_val}));");
1263 }
1264 }
1265 "less_than" => {
1266 if let Some(val) = &assertion.value {
1267 let dart_val = format_value(val);
1268 let _ = writeln!(out, " expect({field_accessor}, lessThan({dart_val}));");
1269 }
1270 }
1271 "greater_than_or_equal" => {
1272 if let Some(val) = &assertion.value {
1273 let dart_val = format_value(val);
1274 let _ = writeln!(out, " expect({field_accessor}, greaterThanOrEqualTo({dart_val}));");
1275 }
1276 }
1277 "less_than_or_equal" => {
1278 if let Some(val) = &assertion.value {
1279 let dart_val = format_value(val);
1280 let _ = writeln!(out, " expect({field_accessor}, lessThanOrEqualTo({dart_val}));");
1281 }
1282 }
1283 "not_null" => {
1284 let _ = writeln!(out, " expect({field_accessor}, isNotNull);");
1285 }
1286 "not_error" => {
1287 }
1289 "error" => {
1290 }
1292 "method_result" => {
1293 if let Some(method) = &assertion.method {
1294 let dart_method = method.to_lower_camel_case();
1295 let check = assertion.check.as_deref().unwrap_or("not_null");
1296 let method_call = format!("{field_accessor}.{dart_method}()");
1297 match check {
1298 "equals" => {
1299 if let Some(expected) = &assertion.value {
1300 let dart_val = format_value(expected);
1301 let _ = writeln!(out, " expect({method_call}, equals({dart_val}));");
1302 }
1303 }
1304 "is_true" => {
1305 let _ = writeln!(out, " expect({method_call}, isTrue);");
1306 }
1307 "is_false" => {
1308 let _ = writeln!(out, " expect({method_call}, isFalse);");
1309 }
1310 "greater_than_or_equal" => {
1311 if let Some(val) = &assertion.value {
1312 let dart_val = format_value(val);
1313 let _ = writeln!(out, " expect({method_call}, greaterThanOrEqualTo({dart_val}));");
1314 }
1315 }
1316 "count_min" => {
1317 if let Some(val) = &assertion.value {
1318 if let Some(n) = val.as_u64() {
1319 let _ = writeln!(out, " expect({method_call}.length, greaterThanOrEqualTo({n}));");
1320 }
1321 }
1322 }
1323 _ => {
1324 let _ = writeln!(out, " expect({method_call}, isNotNull);");
1325 }
1326 }
1327 }
1328 }
1329 other => {
1330 let _ = writeln!(out, " // skipped: unknown assertion type '{other}'");
1331 }
1332 }
1333}
1334
1335fn render_streaming_assertion_dart(out: &mut String, assertion: &Assertion, result_var: &str) {
1345 match assertion.assertion_type.as_str() {
1346 "not_error" => {
1347 }
1349 "count_min" if assertion.field.as_deref() == Some("chunks") => {
1350 if let Some(serde_json::Value::Number(n)) = &assertion.value {
1351 let _ = writeln!(out, " expect({result_var}.length, greaterThanOrEqualTo({n}));");
1352 }
1353 }
1354 "equals" if assertion.field.as_deref() == Some("stream_content") => {
1355 if let Some(serde_json::Value::String(expected)) = &assertion.value {
1356 let escaped = escape_dart(expected);
1357 let _ = writeln!(
1358 out,
1359 " final _content = {result_var}.map((c) => c.choices.firstOrNull?.delta.content ?? '').join();"
1360 );
1361 let _ = writeln!(out, " expect(_content, equals('{escaped}'));");
1362 }
1363 }
1364 other => {
1365 let _ = writeln!(out, " // skipped streaming assertion: '{other}'");
1366 }
1367 }
1368}
1369
1370fn snake_to_camel(s: &str) -> String {
1372 let mut result = String::with_capacity(s.len());
1373 let mut next_upper = false;
1374 for ch in s.chars() {
1375 if ch == '_' {
1376 next_upper = true;
1377 } else if next_upper {
1378 result.extend(ch.to_uppercase());
1379 next_upper = false;
1380 } else {
1381 result.push(ch);
1382 }
1383 }
1384 result
1385}
1386
1387fn field_to_dart_accessor(path: &str) -> String {
1400 let mut result = String::with_capacity(path.len());
1401 for (i, segment) in path.split('.').enumerate() {
1402 if i > 0 {
1403 result.push('.');
1404 }
1405 if let Some(bracket_pos) = segment.find('[') {
1411 let name = &segment[..bracket_pos];
1412 let bracket = &segment[bracket_pos..];
1413 result.push_str(&name.to_lower_camel_case());
1414 result.push('!');
1415 result.push_str(bracket);
1416 } else {
1417 result.push_str(&segment.to_lower_camel_case());
1418 }
1419 }
1420 result
1421}
1422
1423fn emit_extraction_config_dart(overrides: &serde_json::Map<String, serde_json::Value>) -> String {
1429 let mut field_overrides: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1431 for (key, val) in overrides {
1432 let camel = snake_to_camel(key);
1433 let dart_val = match val {
1434 serde_json::Value::Bool(b) => {
1435 if *b {
1436 "true".to_string()
1437 } else {
1438 "false".to_string()
1439 }
1440 }
1441 serde_json::Value::Number(n) => n.to_string(),
1442 serde_json::Value::String(s) => format!("'{s}'"),
1443 _ => continue, };
1445 field_overrides.insert(camel, dart_val);
1446 }
1447
1448 let use_cache = field_overrides.remove("useCache").unwrap_or_else(|| "true".to_string());
1449 let enable_quality_processing = field_overrides
1450 .remove("enableQualityProcessing")
1451 .unwrap_or_else(|| "true".to_string());
1452 let force_ocr = field_overrides
1453 .remove("forceOcr")
1454 .unwrap_or_else(|| "false".to_string());
1455 let disable_ocr = field_overrides
1456 .remove("disableOcr")
1457 .unwrap_or_else(|| "false".to_string());
1458 let include_document_structure = field_overrides
1459 .remove("includeDocumentStructure")
1460 .unwrap_or_else(|| "false".to_string());
1461 let use_layout_for_markdown = field_overrides
1462 .remove("useLayoutForMarkdown")
1463 .unwrap_or_else(|| "false".to_string());
1464 let max_archive_depth = field_overrides
1465 .remove("maxArchiveDepth")
1466 .unwrap_or_else(|| "3".to_string());
1467
1468 format!(
1469 "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})"
1470 )
1471}
1472
1473struct DartTestClientRenderer {
1489 in_skip: Cell<bool>,
1492 is_redirect: Cell<bool>,
1495}
1496
1497impl DartTestClientRenderer {
1498 fn new(is_redirect: bool) -> Self {
1499 Self {
1500 in_skip: Cell::new(false),
1501 is_redirect: Cell::new(is_redirect),
1502 }
1503 }
1504}
1505
1506impl client::TestClientRenderer for DartTestClientRenderer {
1507 fn language_name(&self) -> &'static str {
1508 "dart"
1509 }
1510
1511 fn render_test_open(&self, out: &mut String, _fn_name: &str, description: &str, skip_reason: Option<&str>) {
1520 let escaped_desc = escape_dart(description);
1521 if let Some(reason) = skip_reason {
1522 let escaped_reason = escape_dart(reason);
1523 let _ = writeln!(out, " test('{escaped_desc}', () {{");
1524 let _ = writeln!(out, " markTestSkipped('{escaped_reason}');");
1525 let _ = writeln!(out, " }});");
1526 let _ = writeln!(out);
1527 self.in_skip.set(true);
1528 } else {
1529 let _ = writeln!(
1530 out,
1531 " test('{escaped_desc}', () => _serialized(() => _withRetry(() async {{"
1532 );
1533 self.in_skip.set(false);
1534 }
1535 }
1536
1537 fn render_test_close(&self, out: &mut String) {
1542 if self.in_skip.get() {
1543 return;
1545 }
1546 let _ = writeln!(out, " }})));");
1547 let _ = writeln!(out);
1548 }
1549
1550 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1560 const DART_RESTRICTED_HEADERS: &[&str] = &["content-length", "host", "transfer-encoding"];
1562
1563 let method = ctx.method.to_uppercase();
1564 let escaped_method = escape_dart(&method);
1565
1566 let fixture_path = escape_dart(ctx.path);
1568
1569 let has_explicit_content_type = ctx.headers.keys().any(|k| k.to_lowercase() == "content-type");
1571 let effective_content_type = if has_explicit_content_type {
1572 ctx.headers
1573 .iter()
1574 .find(|(k, _)| k.to_lowercase() == "content-type")
1575 .map(|(_, v)| v.as_str())
1576 .unwrap_or("application/json")
1577 } else if ctx.body.is_some() {
1578 ctx.content_type.unwrap_or("application/json")
1579 } else {
1580 ""
1581 };
1582
1583 let _ = writeln!(
1584 out,
1585 " final baseUrl = Platform.environment['MOCK_SERVER_URL'] ?? 'http://localhost:8080';"
1586 );
1587 let _ = writeln!(out, " final uri = Uri.parse('$baseUrl{fixture_path}');");
1588 let _ = writeln!(
1589 out,
1590 " final ioReq = await _httpClient.openUrl('{escaped_method}', uri);"
1591 );
1592
1593 if self.is_redirect.get() {
1596 let _ = writeln!(out, " ioReq.followRedirects = false;");
1597 }
1598
1599 if !effective_content_type.is_empty() {
1601 let escaped_ct = escape_dart(effective_content_type);
1602 let _ = writeln!(out, " ioReq.headers.set('content-type', '{escaped_ct}');");
1603 }
1604
1605 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
1607 header_pairs.sort_by_key(|(k, _)| k.as_str());
1608 for (name, value) in &header_pairs {
1609 if DART_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
1610 continue;
1611 }
1612 if name.to_lowercase() == "content-type" {
1613 continue; }
1615 let escaped_name = escape_dart(&name.to_lowercase());
1616 let escaped_value = escape_dart(value);
1617 let _ = writeln!(out, " ioReq.headers.set('{escaped_name}', '{escaped_value}');");
1618 }
1619
1620 if !ctx.cookies.is_empty() {
1622 let mut cookie_pairs: Vec<(&String, &String)> = ctx.cookies.iter().collect();
1623 cookie_pairs.sort_by_key(|(k, _)| k.as_str());
1624 let cookie_str: Vec<String> = cookie_pairs.iter().map(|(k, v)| format!("{k}={v}")).collect();
1625 let cookie_header = escape_dart(&cookie_str.join("; "));
1626 let _ = writeln!(out, " ioReq.headers.set('cookie', '{cookie_header}');");
1627 }
1628
1629 if let Some(body) = ctx.body {
1631 let json_str = serde_json::to_string(body).unwrap_or_default();
1632 let escaped = escape_dart(&json_str);
1633 let _ = writeln!(out, " final bodyBytes = utf8.encode('{escaped}');");
1634 let _ = writeln!(out, " ioReq.add(bodyBytes);");
1635 }
1636
1637 let _ = writeln!(out, " final ioResp = await ioReq.close();");
1638 if !self.is_redirect.get() {
1642 let _ = writeln!(out, " final bodyStr = await ioResp.transform(utf8.decoder).join();");
1643 };
1644 }
1645
1646 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1647 let _ = writeln!(
1648 out,
1649 " expect(ioResp.statusCode, equals({status}), reason: 'status code mismatch');"
1650 );
1651 }
1652
1653 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1656 let escaped_name = escape_dart(&name.to_lowercase());
1657 match expected {
1658 "<<present>>" => {
1659 let _ = writeln!(
1660 out,
1661 " expect(ioResp.headers.value('{escaped_name}'), isNotNull, reason: 'header {escaped_name} should be present');"
1662 );
1663 }
1664 "<<absent>>" => {
1665 let _ = writeln!(
1666 out,
1667 " expect(ioResp.headers.value('{escaped_name}'), isNull, reason: 'header {escaped_name} should be absent');"
1668 );
1669 }
1670 "<<uuid>>" => {
1671 let _ = writeln!(
1672 out,
1673 " 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');"
1674 );
1675 }
1676 exact => {
1677 let escaped_value = escape_dart(exact);
1678 let _ = writeln!(
1679 out,
1680 " expect(ioResp.headers.value('{escaped_name}'), contains('{escaped_value}'), reason: 'header {escaped_name} mismatch');"
1681 );
1682 }
1683 }
1684 }
1685
1686 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1691 match expected {
1692 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1693 let json_str = serde_json::to_string(expected).unwrap_or_default();
1694 let escaped = escape_dart(&json_str);
1695 let _ = writeln!(out, " final bodyJson = jsonDecode(bodyStr);");
1696 let _ = writeln!(out, " final expectedJson = jsonDecode('{escaped}');");
1697 let _ = writeln!(
1698 out,
1699 " expect(bodyJson, equals(expectedJson), reason: 'body mismatch');"
1700 );
1701 }
1702 serde_json::Value::String(s) => {
1703 let escaped = escape_dart(s);
1704 let _ = writeln!(
1705 out,
1706 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
1707 );
1708 }
1709 other => {
1710 let escaped = escape_dart(&other.to_string());
1711 let _ = writeln!(
1712 out,
1713 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
1714 );
1715 }
1716 }
1717 }
1718
1719 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1722 let _ = writeln!(
1723 out,
1724 " final partialJson = jsonDecode(bodyStr) as Map<String, dynamic>;"
1725 );
1726 if let Some(obj) = expected.as_object() {
1727 for (idx, (key, val)) in obj.iter().enumerate() {
1728 let escaped_key = escape_dart(key);
1729 let json_val = serde_json::to_string(val).unwrap_or_default();
1730 let escaped_val = escape_dart(&json_val);
1731 let _ = writeln!(out, " final _expectedField{idx} = jsonDecode('{escaped_val}');");
1734 let _ = writeln!(
1735 out,
1736 " expect(partialJson['{escaped_key}'], equals(_expectedField{idx}), reason: 'partial body field \\'{escaped_key}\\' mismatch');"
1737 );
1738 }
1739 }
1740 }
1741
1742 fn render_assert_validation_errors(
1744 &self,
1745 out: &mut String,
1746 _response_var: &str,
1747 errors: &[ValidationErrorExpectation],
1748 ) {
1749 let _ = writeln!(out, " final errBody = jsonDecode(bodyStr) as Map<String, dynamic>;");
1750 let _ = writeln!(out, " final errList = (errBody['errors'] ?? []) as List<dynamic>;");
1751 for ve in errors {
1752 let loc_dart: Vec<String> = ve.loc.iter().map(|s| format!("'{}'", escape_dart(s))).collect();
1753 let loc_str = loc_dart.join(", ");
1754 let escaped_msg = escape_dart(&ve.msg);
1755 let _ = writeln!(
1756 out,
1757 " 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}');"
1758 );
1759 }
1760 }
1761}
1762
1763fn render_http_test_case(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
1770 if http.expected_response.status_code == 101 {
1772 let description = escape_dart(&fixture.description);
1773 let _ = writeln!(out, " test('{description}', () {{");
1774 let _ = writeln!(
1775 out,
1776 " markTestSkipped('Skipped: Dart HttpClient cannot handle 101 Switching Protocols responses');"
1777 );
1778 let _ = writeln!(out, " }});");
1779 let _ = writeln!(out);
1780 return;
1781 }
1782
1783 let is_redirect = http.expected_response.status_code / 100 == 3;
1787 client::http_call::render_http_test(out, &DartTestClientRenderer::new(is_redirect), fixture);
1788}
1789
1790fn mime_from_extension(path: &str) -> Option<&'static str> {
1795 let ext = path.rsplit('.').next()?;
1796 match ext.to_lowercase().as_str() {
1797 "docx" => Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
1798 "xlsx" => Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
1799 "pptx" => Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
1800 "pdf" => Some("application/pdf"),
1801 "txt" | "text" => Some("text/plain"),
1802 "html" | "htm" => Some("text/html"),
1803 "json" => Some("application/json"),
1804 "xml" => Some("application/xml"),
1805 "csv" => Some("text/csv"),
1806 "md" | "markdown" => Some("text/markdown"),
1807 "png" => Some("image/png"),
1808 "jpg" | "jpeg" => Some("image/jpeg"),
1809 "gif" => Some("image/gif"),
1810 "zip" => Some("application/zip"),
1811 "odt" => Some("application/vnd.oasis.opendocument.text"),
1812 "ods" => Some("application/vnd.oasis.opendocument.spreadsheet"),
1813 "odp" => Some("application/vnd.oasis.opendocument.presentation"),
1814 "rtf" => Some("application/rtf"),
1815 "epub" => Some("application/epub+zip"),
1816 "msg" => Some("application/vnd.ms-outlook"),
1817 "eml" => Some("message/rfc822"),
1818 _ => None,
1819 }
1820}
1821
1822fn emit_dart_batch_item_array(arr: &serde_json::Value, elem_type: &str) -> String {
1829 let items: Vec<String> = arr
1830 .as_array()
1831 .map(|a| a.as_slice())
1832 .unwrap_or_default()
1833 .iter()
1834 .filter_map(|item| {
1835 let obj = item.as_object()?;
1836 match elem_type {
1837 "BatchBytesItem" => {
1838 let content_bytes = obj
1839 .get("content")
1840 .and_then(|v| v.as_array())
1841 .map(|arr| {
1842 let nums: Vec<String> =
1843 arr.iter().filter_map(|v| v.as_u64().map(|n| n.to_string())).collect();
1844 format!("Uint8List.fromList([{}])", nums.join(", "))
1845 })
1846 .unwrap_or_else(|| "Uint8List(0)".to_string());
1847 let mime_type = obj
1848 .get("mime_type")
1849 .and_then(|v| v.as_str())
1850 .unwrap_or("application/octet-stream");
1851 Some(format!(
1852 "BatchBytesItem(content: {content_bytes}, mimeType: '{}')",
1853 escape_dart(mime_type)
1854 ))
1855 }
1856 "BatchFileItem" => {
1857 let path = obj.get("path").and_then(|v| v.as_str()).unwrap_or("");
1858 Some(format!("BatchFileItem(path: '{}')", escape_dart(path)))
1859 }
1860 _ => None,
1861 }
1862 })
1863 .collect();
1864 format!("[{}]", items.join(", "))
1865}
1866
1867pub(super) fn escape_dart(s: &str) -> String {
1869 s.replace('\\', "\\\\")
1870 .replace('\'', "\\'")
1871 .replace('\n', "\\n")
1872 .replace('\r', "\\r")
1873 .replace('\t', "\\t")
1874 .replace('$', "\\$")
1875}
1876
1877fn type_name_to_create_from_json_dart(type_name: &str) -> String {
1885 let mut snake = String::with_capacity(type_name.len() + 8);
1887 for (i, ch) in type_name.char_indices() {
1888 if ch.is_uppercase() {
1889 if i > 0 {
1890 snake.push('_');
1891 }
1892 snake.extend(ch.to_lowercase());
1893 } else {
1894 snake.push(ch);
1895 }
1896 }
1897 let rust_fn = format!("create_{snake}_from_json");
1900 rust_fn
1902 .split('_')
1903 .enumerate()
1904 .map(|(i, part)| {
1905 if i == 0 {
1906 part.to_string()
1907 } else {
1908 let mut chars = part.chars();
1909 match chars.next() {
1910 None => String::new(),
1911 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1912 }
1913 }
1914 })
1915 .collect::<Vec<_>>()
1916 .join("")
1917}