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 dart_stub_methods: std::collections::HashSet<String> = config
91 .dart
92 .as_ref()
93 .map(|d| d.stub_methods.iter().cloned().collect())
94 .unwrap_or_default();
95
96 for group in groups {
97 let active: Vec<&Fixture> = group
98 .fixtures
99 .iter()
100 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
101 .filter(|f| {
102 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
103 let resolved_function = call_config
104 .overrides
105 .get(lang)
106 .and_then(|o| o.function.as_ref())
107 .cloned()
108 .unwrap_or_else(|| call_config.function.clone());
109 !dart_stub_methods.contains(&resolved_function)
110 })
111 .collect();
112
113 if active.is_empty() {
114 continue;
115 }
116
117 let filename = format!("{}_test.dart", sanitize_filename(&group.category));
118 let content = render_test_file(&group.category, &active, e2e_config, lang, &pkg_name, &bridge_class);
119 files.push(GeneratedFile {
120 path: test_base.join(filename),
121 content,
122 generated_header: true,
123 });
124 }
125
126 Ok(files)
127 }
128
129 fn language_name(&self) -> &'static str {
130 "dart"
131 }
132}
133
134fn render_pubspec(
139 pkg_name: &str,
140 pkg_path: &str,
141 pkg_version: &str,
142 dep_mode: crate::config::DependencyMode,
143) -> String {
144 let test_ver = pub_dev::TEST_PACKAGE;
145 let http_ver = pub_dev::HTTP_PACKAGE;
146
147 let dep_block = match dep_mode {
148 crate::config::DependencyMode::Registry => {
149 format!(" {pkg_name}: ^{pkg_version}")
150 }
151 crate::config::DependencyMode::Local => {
152 format!(" {pkg_name}:\n path: {pkg_path}")
153 }
154 };
155
156 let sdk = alef_core::template_versions::toolchain::DART_SDK_CONSTRAINT;
157 format!(
158 r#"name: e2e_dart
159version: 0.1.0
160publish_to: none
161
162environment:
163 sdk: "{sdk}"
164
165dependencies:
166{dep_block}
167
168dev_dependencies:
169 test: {test_ver}
170 http: {http_ver}
171"#
172 )
173}
174
175fn render_test_file(
176 category: &str,
177 fixtures: &[&Fixture],
178 e2e_config: &E2eConfig,
179 lang: &str,
180 pkg_name: &str,
181 bridge_class: &str,
182) -> String {
183 let mut out = String::new();
184 out.push_str(&hash::header(CommentStyle::DoubleSlash));
185
186 let field_resolver = FieldResolver::new(
191 &e2e_config.fields,
192 &e2e_config.fields_optional,
193 &e2e_config.result_fields,
194 &e2e_config.fields_array,
195 &e2e_config.fields_method_calls,
196 );
197
198 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
200
201 let has_batch_byte_items = fixtures.iter().any(|f| {
203 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
204 call_config.args.iter().any(|a| {
205 a.element_type.as_deref() == Some("BatchBytesItem") && resolve_field(&f.input, &a.field).is_array()
206 })
207 });
208
209 let needs_chdir = fixtures.iter().any(|f| {
213 if f.is_http_test() {
214 return false;
215 }
216 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
217 call_config
218 .args
219 .iter()
220 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
221 });
222
223 let has_handle_args = fixtures.iter().any(|f| {
229 if f.is_http_test() {
230 return false;
231 }
232 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
233 call_config
234 .args
235 .iter()
236 .any(|a| a.arg_type == "json_object" && super::resolve_field(&f.input, &a.field).is_array())
237 });
238
239 let _ = writeln!(out, "import 'package:test/test.dart';");
240 let _ = writeln!(out, "import 'dart:io';");
241 if has_batch_byte_items {
242 let _ = writeln!(out, "import 'dart:typed_data';");
243 }
244 let _ = writeln!(out, "import 'package:{pkg_name}/{pkg_name}.dart';");
245 let _ = writeln!(
248 out,
249 "import 'package:{pkg_name}/src/{pkg_name}_bridge_generated/frb_generated.dart' show RustLib;"
250 );
251 if has_http_fixtures {
252 let _ = writeln!(out, "import 'dart:async';");
253 }
254 if has_http_fixtures || has_handle_args {
256 let _ = writeln!(out, "import 'dart:convert';");
257 }
258 let _ = writeln!(out);
259
260 if has_http_fixtures {
270 let _ = writeln!(out, "HttpClient _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
271 let _ = writeln!(out);
272 let _ = writeln!(out, "var _lock = Future<void>.value();");
273 let _ = writeln!(out);
274 let _ = writeln!(out, "Future<T> _serialized<T>(Future<T> Function() fn) async {{");
275 let _ = writeln!(out, " final current = _lock;");
276 let _ = writeln!(out, " final next = Completer<void>();");
277 let _ = writeln!(out, " _lock = next.future;");
278 let _ = writeln!(out, " try {{");
279 let _ = writeln!(out, " await current;");
280 let _ = writeln!(out, " return await fn();");
281 let _ = writeln!(out, " }} finally {{");
282 let _ = writeln!(out, " next.complete();");
283 let _ = writeln!(out, " }}");
284 let _ = writeln!(out, "}}");
285 let _ = writeln!(out);
286 let _ = writeln!(out, "Future<T> _withRetry<T>(Future<T> Function() fn) async {{");
289 let _ = writeln!(out, " try {{");
290 let _ = writeln!(out, " return await fn();");
291 let _ = writeln!(out, " }} on SocketException {{");
292 let _ = writeln!(out, " _httpClient.close(force: true);");
293 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
294 let _ = writeln!(out, " return fn();");
295 let _ = writeln!(out, " }} on HttpException {{");
296 let _ = writeln!(out, " _httpClient.close(force: true);");
297 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
298 let _ = writeln!(out, " return fn();");
299 let _ = writeln!(out, " }}");
300 let _ = writeln!(out, "}}");
301 let _ = writeln!(out);
302 }
303
304 let _ = writeln!(out, "// E2e tests for category: {category}");
305 let _ = writeln!(out, "void main() {{");
306
307 let _ = writeln!(out, " setUpAll(() async {{");
314 let _ = writeln!(out, " await RustLib.init();");
315 if needs_chdir {
316 let test_docs_path = e2e_config.test_documents_relative_from(0);
317 let _ = writeln!(
318 out,
319 " final _testDocs = Platform.environment['FIXTURES_DIR'] ?? '{test_docs_path}';"
320 );
321 let _ = writeln!(out, " final _dir = Directory(_testDocs);");
322 let _ = writeln!(out, " if (_dir.existsSync()) Directory.current = _dir;");
323 }
324 let _ = writeln!(out, " }});");
325 let _ = writeln!(out);
326
327 if has_http_fixtures {
329 let _ = writeln!(out, " tearDownAll(() => _httpClient.close());");
330 let _ = writeln!(out);
331 }
332
333 for fixture in fixtures {
334 render_test_case(&mut out, fixture, e2e_config, lang, bridge_class, &field_resolver);
335 }
336
337 let _ = writeln!(out, "}}");
338 out
339}
340
341fn render_test_case(
342 out: &mut String,
343 fixture: &Fixture,
344 e2e_config: &E2eConfig,
345 lang: &str,
346 bridge_class: &str,
347 field_resolver: &FieldResolver,
348) {
349 if let Some(http) = &fixture.http {
351 render_http_test_case(out, fixture, http);
352 return;
353 }
354
355 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
357 let call_overrides = call_config.overrides.get(lang);
358 let mut function_name = call_overrides
359 .and_then(|o| o.function.as_ref())
360 .cloned()
361 .unwrap_or_else(|| call_config.function.clone());
362 function_name = function_name
364 .split('_')
365 .enumerate()
366 .map(|(i, part)| {
367 if i == 0 {
368 part.to_string()
369 } else {
370 let mut chars = part.chars();
371 match chars.next() {
372 None => String::new(),
373 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
374 }
375 }
376 })
377 .collect::<Vec<_>>()
378 .join("");
379 let result_var = &call_config.result_var;
380 let description = escape_dart(&fixture.description);
381 let fixture_id = &fixture.id;
382 let _is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
385
386 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
387 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
388 let result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple) || call_config.result_is_simple;
393
394 let options_type: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
401 let options_via: &str = call_overrides
402 .and_then(|o| o.options_via.as_deref())
403 .unwrap_or("kwargs");
404
405 let file_path_for_mime: Option<&str> = call_config
413 .args
414 .iter()
415 .find(|a| a.arg_type == "file_path")
416 .and_then(|a| resolve_field(&fixture.input, &a.field).as_str());
417
418 let has_file_path_arg = call_config.args.iter().any(|a| a.arg_type == "file_path");
425 let caller_supplied_override = call_overrides.and_then(|o| o.function.as_ref()).is_some();
428 if has_file_path_arg && !caller_supplied_override {
429 function_name = match function_name.as_str() {
430 "extractFile" => "extractBytes".to_string(),
431 "extractFileSync" => "extractBytesSync".to_string(),
432 other => other.to_string(),
433 };
434 }
435
436 let mut setup_lines: Vec<String> = Vec::new();
439 let mut args = Vec::new();
440
441 for arg_def in &call_config.args {
442 match arg_def.arg_type.as_str() {
443 "mock_url" => {
444 let name = arg_def.name.clone();
445 if fixture.has_host_root_route() {
446 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
447 setup_lines.push(format!(
448 r#"final {name} = Platform.environment["{env_key}"] ?? (Platform.environment["MOCK_SERVER_URL"]! + "/fixtures/{fixture_id}");"#
449 ));
450 } else {
451 setup_lines.push(format!(
452 r#"final {name} = "${{Platform.environment["MOCK_SERVER_URL"] ?? "http://localhost:8080"}}/fixtures/{fixture_id}";"#
453 ));
454 }
455 args.push(name);
456 continue;
457 }
458 "handle" => {
459 let name = arg_def.name.clone();
460 let field = arg_def.field.strip_prefix("input.").unwrap_or(&arg_def.field);
461 let config_value = fixture.input.get(field).cloned().unwrap_or(serde_json::Value::Null);
462 let create_fn = {
464 let mut chars = name.chars();
465 let pascal = match chars.next() {
466 None => String::new(),
467 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
468 };
469 format!("create{pascal}")
470 };
471 if config_value.is_null()
472 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
473 {
474 setup_lines.push(format!("final {name} = await {bridge_class}.{create_fn}();"));
475 } else {
476 let json_str = serde_json::to_string(&config_value).unwrap_or_default();
477 let config_var = format!("{name}Config");
478 setup_lines.push(format!(
483 "final {config_var} = await createCrawlConfigFromJson(json: r'{json_str}');"
484 ));
485 setup_lines.push(format!(
487 "final {name} = await {bridge_class}.{create_fn}(config: {config_var});"
488 ));
489 }
490 args.push(name);
491 continue;
492 }
493 _ => {}
494 }
495
496 let arg_value = resolve_field(&fixture.input, &arg_def.field);
497 match arg_def.arg_type.as_str() {
498 "bytes" | "file_path" => {
499 if let serde_json::Value::String(file_path) = arg_value {
504 args.push(format!("File('{}').readAsBytesSync()", file_path));
505 }
506 }
507 "string" => {
508 let dart_param_name = snake_to_camel(&arg_def.name);
523 let mime_required_due_to_remap = has_file_path_arg
524 && arg_def.name == "mime_type"
525 && (function_name == "extractBytes" || function_name == "extractBytesSync");
526 let is_optional = arg_def.optional && !mime_required_due_to_remap;
527 match arg_value {
528 serde_json::Value::String(s) => {
529 let literal = format!("'{}'", escape_dart(s));
530 if is_optional {
531 args.push(format!("{dart_param_name}: {literal}"));
532 } else {
533 args.push(literal);
534 }
535 }
536 serde_json::Value::Null
537 if arg_def.optional
538 && arg_def.name == "mime_type" =>
541 {
542 let inferred = file_path_for_mime
543 .and_then(mime_from_extension)
544 .unwrap_or("application/octet-stream");
545 if is_optional {
546 args.push(format!("{dart_param_name}: '{inferred}'"));
547 } else {
548 args.push(format!("'{inferred}'"));
549 }
550 }
551 _ => {}
553 }
554 }
555 "json_object" => {
556 if let Some(elem_type) = &arg_def.element_type {
558 if (elem_type == "BatchBytesItem" || elem_type == "BatchFileItem") && arg_value.is_array() {
559 let dart_items = emit_dart_batch_item_array(arg_value, elem_type);
560 args.push(dart_items);
561 } else if elem_type == "String" && arg_value.is_array() {
562 let items: Vec<String> = arg_value
569 .as_array()
570 .unwrap()
571 .iter()
572 .filter_map(|v| v.as_str())
573 .map(|s| format!("'{}'", escape_dart(s)))
574 .collect();
575 args.push(format!("<String>[{}]", items.join(", ")));
576 }
577 } else if options_via == "from_json" {
578 if let Some(opts_type) = options_type {
588 if !arg_value.is_null() {
589 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
590 let escaped_json = escape_dart(&json_str);
593 let var_name = format!("_{}", arg_def.name);
594 let dart_fn = type_name_to_create_from_json_dart(opts_type);
595 setup_lines.push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
596 args.push(format!("req: {var_name}"));
599 }
600 }
601 } else if arg_def.name == "config" {
602 if let serde_json::Value::Object(map) = &arg_value {
603 if !map.is_empty() {
604 let explicit_options =
613 options_type.is_some_and(|t| t != "ExtractionConfig" && t != "FileExtractionConfig");
614 let has_non_scalar = map.values().any(|v| {
615 matches!(
616 v,
617 serde_json::Value::String(_)
618 | serde_json::Value::Object(_)
619 | serde_json::Value::Array(_)
620 )
621 });
622 if explicit_options || has_non_scalar {
623 let opts_type = options_type.unwrap_or("ExtractionConfig");
624 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
625 let escaped_json = escape_dart(&json_str);
626 let var_name = format!("_{}", arg_def.name);
627 let dart_fn = type_name_to_create_from_json_dart(opts_type);
628 setup_lines
629 .push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
630 args.push(var_name);
631 } else {
632 args.push(emit_extraction_config_dart(map));
638 }
639 }
640 }
641 } else if arg_value.is_array() {
643 let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
646 let var_name = arg_def.name.clone();
647 setup_lines.push(format!(
648 "final {var_name} = (jsonDecode(r'{json_str}') as List<dynamic>).cast<String>();"
649 ));
650 args.push(var_name);
651 }
652 }
653 _ => {}
654 }
655 }
656
657 let client_factory: Option<&str> = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
661 e2e_config
662 .call
663 .overrides
664 .get(lang)
665 .and_then(|o| o.client_factory.as_deref())
666 });
667
668 let client_factory_camel: Option<String> = client_factory.map(|f| {
670 f.split('_')
671 .enumerate()
672 .map(|(i, part)| {
673 if i == 0 {
674 part.to_string()
675 } else {
676 let mut chars = part.chars();
677 match chars.next() {
678 None => String::new(),
679 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
680 }
681 }
682 })
683 .collect::<Vec<_>>()
684 .join("")
685 });
686
687 let _ = writeln!(out, " test('{description}', () async {{");
691
692 let args_str = args.join(", ");
693 let receiver_class = call_overrides
694 .and_then(|o| o.class.as_ref())
695 .cloned()
696 .unwrap_or_else(|| bridge_class.to_string());
697
698 let (receiver, extra_setup): (String, Option<String>) = if let Some(factory) = &client_factory_camel {
702 let has_mock_url = call_config.args.iter().any(|a| a.arg_type == "mock_url");
703 let mock_url_setup = if !has_mock_url {
704 if fixture.has_host_root_route() {
706 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
707 Some(format!(
708 "final _mockUrl = Platform.environment[\"{env_key}\"] ?? (Platform.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\");"
709 ))
710 } else {
711 Some(format!(
712 r#"final _mockUrl = "${{Platform.environment["MOCK_SERVER_URL"] ?? "http://localhost:8080"}}/fixtures/{fixture_id}";"#
713 ))
714 }
715 } else {
716 None
717 };
718 let url_expr = if has_mock_url {
719 call_config
722 .args
723 .iter()
724 .find(|a| a.arg_type == "mock_url")
725 .map(|a| a.name.clone())
726 .unwrap_or_else(|| "_mockUrl".to_string())
727 } else {
728 "_mockUrl".to_string()
729 };
730 let create_line = format!("final _client = await {receiver_class}.{factory}('test-key', baseUrl: {url_expr});");
731 let full_setup = if let Some(url_line) = mock_url_setup {
732 Some(format!("{url_line}\n {create_line}"))
733 } else {
734 Some(create_line)
735 };
736 ("_client".to_string(), full_setup)
737 } else {
738 (receiver_class.clone(), None)
739 };
740
741 if expects_error && (!setup_lines.is_empty() || extra_setup.is_some()) {
742 let _ = writeln!(out, " await expectLater(() async {{");
746 for line in &setup_lines {
747 let _ = writeln!(out, " {line}");
748 }
749 if let Some(extra) = &extra_setup {
750 for line in extra.lines() {
751 let _ = writeln!(out, " {line}");
752 }
753 }
754 if is_streaming {
755 let _ = writeln!(out, " return {receiver}.{function_name}({args_str}).toList();");
756 } else {
757 let _ = writeln!(out, " return {receiver}.{function_name}({args_str});");
758 }
759 let _ = writeln!(out, " }}(), throwsA(anything));");
760 } else if expects_error {
761 if let Some(extra) = &extra_setup {
763 for line in extra.lines() {
764 let _ = writeln!(out, " {line}");
765 }
766 }
767 if is_streaming {
768 let _ = writeln!(
769 out,
770 " await expectLater({receiver}.{function_name}({args_str}).toList(), throwsA(anything));"
771 );
772 } else {
773 let _ = writeln!(
774 out,
775 " await expectLater({receiver}.{function_name}({args_str}), throwsA(anything));"
776 );
777 }
778 } else {
779 for line in &setup_lines {
780 let _ = writeln!(out, " {line}");
781 }
782 if let Some(extra) = &extra_setup {
783 for line in extra.lines() {
784 let _ = writeln!(out, " {line}");
785 }
786 }
787 if is_streaming {
788 let _ = writeln!(
789 out,
790 " final {result_var} = await {receiver}.{function_name}({args_str}).toList();"
791 );
792 } else {
793 let _ = writeln!(
794 out,
795 " final {result_var} = await {receiver}.{function_name}({args_str});"
796 );
797 }
798 for assertion in &fixture.assertions {
799 if is_streaming {
800 render_streaming_assertion_dart(out, assertion, result_var);
801 } else {
802 render_assertion_dart(out, assertion, result_var, result_is_simple, field_resolver);
803 }
804 }
805 }
806
807 let _ = writeln!(out, " }});");
808 let _ = writeln!(out);
809}
810
811fn dart_format_value(val: &serde_json::Value) -> String {
812 match val {
813 serde_json::Value::String(s) => format!("'{}'", escape_dart(s)),
814 serde_json::Value::Bool(b) => b.to_string(),
815 serde_json::Value::Number(n) => n.to_string(),
816 serde_json::Value::Null => "null".to_string(),
817 other => format!("'{}'", escape_dart(&other.to_string())),
818 }
819}
820
821fn render_assertion_dart(
832 out: &mut String,
833 assertion: &Assertion,
834 result_var: &str,
835 result_is_simple: bool,
836 field_resolver: &FieldResolver,
837) {
838 if !result_is_simple {
842 if let Some(f) = assertion.field.as_deref() {
843 let head = f.split("[].").next().unwrap_or(f);
846 if !head.is_empty() && !field_resolver.is_valid_for_result(head) {
847 let _ = writeln!(out, " // skipped: field '{f}' not available on dart result type");
848 return;
849 }
850 }
851 }
852
853 if let Some(f) = assertion.field.as_deref() {
859 if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
860 let _ = writeln!(
861 out,
862 " // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Dart)"
863 );
864 return;
865 }
866 }
867
868 if let Some(f) = assertion.field.as_deref() {
870 if let Some(dot) = f.find("[].") {
871 let resolved_full = field_resolver.resolve(f);
876 let (array_part, elem_part) = match resolved_full.find("[].") {
877 Some(rdot) => (&resolved_full[..rdot], &resolved_full[rdot + 3..]),
878 None => (&f[..dot], &f[dot + 3..]),
881 };
882 let array_accessor = if array_part.is_empty() {
883 result_var.to_string()
884 } else {
885 field_resolver.accessor(array_part, "dart", result_var)
886 };
887 let elem_accessor = field_to_dart_accessor(elem_part);
888 match assertion.assertion_type.as_str() {
889 "contains" => {
890 if let Some(expected) = &assertion.value {
891 let dart_val = dart_format_value(expected);
892 let _ = writeln!(
893 out,
894 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
895 );
896 }
897 }
898 "contains_all" => {
899 if let Some(values) = &assertion.values {
900 for val in values {
901 let dart_val = dart_format_value(val);
902 let _ = writeln!(
903 out,
904 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
905 );
906 }
907 }
908 }
909 "not_contains" => {
910 if let Some(expected) = &assertion.value {
911 let dart_val = dart_format_value(expected);
912 let _ = writeln!(
913 out,
914 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
915 );
916 } else if let Some(values) = &assertion.values {
917 for val in values {
918 let dart_val = dart_format_value(val);
919 let _ = writeln!(
920 out,
921 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
922 );
923 }
924 }
925 }
926 "not_empty" => {
927 let _ = writeln!(
928 out,
929 " expect({array_accessor}.any((e) => e.{elem_accessor}.toString().isNotEmpty), isTrue);"
930 );
931 }
932 other => {
933 let _ = writeln!(
934 out,
935 " // skipped: unsupported traversal assertion '{other}' on '{f}'"
936 );
937 }
938 }
939 return;
940 }
941 }
942
943 let field_accessor = if result_is_simple {
944 result_var.to_string()
948 } else {
949 match assertion.field.as_deref() {
950 Some(f) if !f.is_empty() => field_resolver.accessor(f, "dart", result_var),
955 _ => result_var.to_string(),
956 }
957 };
958
959 let format_value = |val: &serde_json::Value| -> String { dart_format_value(val) };
960
961 match assertion.assertion_type.as_str() {
962 "equals" | "field_equals" => {
963 if let Some(expected) = &assertion.value {
964 let dart_val = format_value(expected);
965 let _ = writeln!(out, " expect({field_accessor}, equals({dart_val}));");
966 } else {
967 let _ = writeln!(
968 out,
969 " // skipped: '{}' assertion missing value",
970 assertion.assertion_type
971 );
972 }
973 }
974 "not_equals" => {
975 if let Some(expected) = &assertion.value {
976 let dart_val = format_value(expected);
977 let _ = writeln!(out, " expect({field_accessor}, isNot(equals({dart_val})));");
978 }
979 }
980 "contains" => {
981 if let Some(expected) = &assertion.value {
982 let dart_val = format_value(expected);
983 let _ = writeln!(out, " expect({field_accessor}, contains({dart_val}));");
984 } else {
985 let _ = writeln!(out, " // skipped: 'contains' assertion missing value");
986 }
987 }
988 "contains_all" => {
989 if let Some(values) = &assertion.values {
990 for val in values {
991 let dart_val = format_value(val);
992 let _ = writeln!(out, " expect({field_accessor}, contains({dart_val}));");
993 }
994 }
995 }
996 "contains_any" => {
997 if let Some(values) = &assertion.values {
998 let checks: Vec<String> = values
999 .iter()
1000 .map(|v| {
1001 let dart_val = format_value(v);
1002 format!("{field_accessor}.contains({dart_val})")
1003 })
1004 .collect();
1005 let joined = checks.join(" || ");
1006 let _ = writeln!(out, " expect({joined}, isTrue);");
1007 }
1008 }
1009 "not_contains" => {
1010 if let Some(expected) = &assertion.value {
1011 let dart_val = format_value(expected);
1012 let _ = writeln!(out, " expect({field_accessor}, isNot(contains({dart_val})));");
1013 } else if let Some(values) = &assertion.values {
1014 for val in values {
1015 let dart_val = format_value(val);
1016 let _ = writeln!(out, " expect({field_accessor}, isNot(contains({dart_val})));");
1017 }
1018 }
1019 }
1020 "not_empty" => {
1021 let is_collection = assertion.field.as_deref().is_some_and(|f| {
1026 let resolved = field_resolver.resolve(f);
1027 field_resolver.is_array(f) || field_resolver.is_array(resolved)
1028 });
1029 if is_collection {
1030 let _ = writeln!(out, " expect({field_accessor}, isNotEmpty);");
1031 } else {
1032 let _ = writeln!(out, " expect({field_accessor}, isNotNull);");
1033 }
1034 }
1035 "is_empty" => {
1036 let _ = writeln!(out, " expect({field_accessor}, anyOf(isNull, isEmpty));");
1040 }
1041 "starts_with" => {
1042 if let Some(expected) = &assertion.value {
1043 let dart_val = format_value(expected);
1044 let _ = writeln!(out, " expect({field_accessor}, startsWith({dart_val}));");
1045 }
1046 }
1047 "ends_with" => {
1048 if let Some(expected) = &assertion.value {
1049 let dart_val = format_value(expected);
1050 let _ = writeln!(out, " expect({field_accessor}, endsWith({dart_val}));");
1051 }
1052 }
1053 "min_length" => {
1054 if let Some(val) = &assertion.value {
1055 if let Some(n) = val.as_u64() {
1056 let _ = writeln!(
1060 out,
1061 " expect({field_accessor}?.length ?? 0, greaterThanOrEqualTo({n}));"
1062 );
1063 }
1064 }
1065 }
1066 "max_length" => {
1067 if let Some(val) = &assertion.value {
1068 if let Some(n) = val.as_u64() {
1069 let _ = writeln!(
1070 out,
1071 " expect({field_accessor}?.length ?? 0, lessThanOrEqualTo({n}));"
1072 );
1073 }
1074 }
1075 }
1076 "count_equals" => {
1077 if let Some(val) = &assertion.value {
1078 if let Some(n) = val.as_u64() {
1079 let _ = writeln!(out, " expect({field_accessor}?.length ?? 0, equals({n}));");
1080 }
1081 }
1082 }
1083 "count_min" => {
1084 if let Some(val) = &assertion.value {
1085 if let Some(n) = val.as_u64() {
1086 let _ = writeln!(
1087 out,
1088 " expect({field_accessor}?.length ?? 0, greaterThanOrEqualTo({n}));"
1089 );
1090 }
1091 }
1092 }
1093 "matches_regex" => {
1094 if let Some(expected) = &assertion.value {
1095 let dart_val = format_value(expected);
1096 let _ = writeln!(out, " expect({field_accessor}, matches(RegExp({dart_val})));");
1097 }
1098 }
1099 "is_true" => {
1100 let _ = writeln!(out, " expect({field_accessor}, isTrue);");
1101 }
1102 "is_false" => {
1103 let _ = writeln!(out, " expect({field_accessor}, isFalse);");
1104 }
1105 "greater_than" => {
1106 if let Some(val) = &assertion.value {
1107 let dart_val = format_value(val);
1108 let _ = writeln!(out, " expect({field_accessor}, greaterThan({dart_val}));");
1109 }
1110 }
1111 "less_than" => {
1112 if let Some(val) = &assertion.value {
1113 let dart_val = format_value(val);
1114 let _ = writeln!(out, " expect({field_accessor}, lessThan({dart_val}));");
1115 }
1116 }
1117 "greater_than_or_equal" => {
1118 if let Some(val) = &assertion.value {
1119 let dart_val = format_value(val);
1120 let _ = writeln!(out, " expect({field_accessor}, greaterThanOrEqualTo({dart_val}));");
1121 }
1122 }
1123 "less_than_or_equal" => {
1124 if let Some(val) = &assertion.value {
1125 let dart_val = format_value(val);
1126 let _ = writeln!(out, " expect({field_accessor}, lessThanOrEqualTo({dart_val}));");
1127 }
1128 }
1129 "not_null" => {
1130 let _ = writeln!(out, " expect({field_accessor}, isNotNull);");
1131 }
1132 "not_error" => {
1133 }
1135 "error" => {
1136 }
1138 "method_result" => {
1139 if let Some(method) = &assertion.method {
1140 let dart_method = method.to_lower_camel_case();
1141 let check = assertion.check.as_deref().unwrap_or("not_null");
1142 let method_call = format!("{field_accessor}.{dart_method}()");
1143 match check {
1144 "equals" => {
1145 if let Some(expected) = &assertion.value {
1146 let dart_val = format_value(expected);
1147 let _ = writeln!(out, " expect({method_call}, equals({dart_val}));");
1148 }
1149 }
1150 "is_true" => {
1151 let _ = writeln!(out, " expect({method_call}, isTrue);");
1152 }
1153 "is_false" => {
1154 let _ = writeln!(out, " expect({method_call}, isFalse);");
1155 }
1156 "greater_than_or_equal" => {
1157 if let Some(val) = &assertion.value {
1158 let dart_val = format_value(val);
1159 let _ = writeln!(out, " expect({method_call}, greaterThanOrEqualTo({dart_val}));");
1160 }
1161 }
1162 "count_min" => {
1163 if let Some(val) = &assertion.value {
1164 if let Some(n) = val.as_u64() {
1165 let _ = writeln!(out, " expect({method_call}.length, greaterThanOrEqualTo({n}));");
1166 }
1167 }
1168 }
1169 _ => {
1170 let _ = writeln!(out, " expect({method_call}, isNotNull);");
1171 }
1172 }
1173 }
1174 }
1175 other => {
1176 let _ = writeln!(out, " // skipped: unknown assertion type '{other}'");
1177 }
1178 }
1179}
1180
1181fn render_streaming_assertion_dart(out: &mut String, assertion: &Assertion, result_var: &str) {
1191 match assertion.assertion_type.as_str() {
1192 "not_error" => {
1193 }
1195 "count_min" if assertion.field.as_deref() == Some("chunks") => {
1196 if let Some(serde_json::Value::Number(n)) = &assertion.value {
1197 let _ = writeln!(out, " expect({result_var}.length, greaterThanOrEqualTo({n}));");
1198 }
1199 }
1200 "equals" if assertion.field.as_deref() == Some("stream_content") => {
1201 if let Some(serde_json::Value::String(expected)) = &assertion.value {
1202 let escaped = escape_dart(expected);
1203 let _ = writeln!(
1204 out,
1205 " final _content = {result_var}.map((c) => c.choices.firstOrNull?.delta.content ?? '').join();"
1206 );
1207 let _ = writeln!(out, " expect(_content, equals('{escaped}'));");
1208 }
1209 }
1210 other => {
1211 let _ = writeln!(out, " // skipped streaming assertion: '{other}'");
1212 }
1213 }
1214}
1215
1216fn snake_to_camel(s: &str) -> String {
1218 let mut result = String::with_capacity(s.len());
1219 let mut next_upper = false;
1220 for ch in s.chars() {
1221 if ch == '_' {
1222 next_upper = true;
1223 } else if next_upper {
1224 result.extend(ch.to_uppercase());
1225 next_upper = false;
1226 } else {
1227 result.push(ch);
1228 }
1229 }
1230 result
1231}
1232
1233fn field_to_dart_accessor(path: &str) -> String {
1246 let mut result = String::with_capacity(path.len());
1247 for (i, segment) in path.split('.').enumerate() {
1248 if i > 0 {
1249 result.push('.');
1250 }
1251 if let Some(bracket_pos) = segment.find('[') {
1257 let name = &segment[..bracket_pos];
1258 let bracket = &segment[bracket_pos..];
1259 result.push_str(&name.to_lower_camel_case());
1260 result.push('!');
1261 result.push_str(bracket);
1262 } else {
1263 result.push_str(&segment.to_lower_camel_case());
1264 }
1265 }
1266 result
1267}
1268
1269fn emit_extraction_config_dart(overrides: &serde_json::Map<String, serde_json::Value>) -> String {
1275 let mut field_overrides: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1277 for (key, val) in overrides {
1278 let camel = snake_to_camel(key);
1279 let dart_val = match val {
1280 serde_json::Value::Bool(b) => {
1281 if *b {
1282 "true".to_string()
1283 } else {
1284 "false".to_string()
1285 }
1286 }
1287 serde_json::Value::Number(n) => n.to_string(),
1288 serde_json::Value::String(s) => format!("'{s}'"),
1289 _ => continue, };
1291 field_overrides.insert(camel, dart_val);
1292 }
1293
1294 let use_cache = field_overrides.remove("useCache").unwrap_or_else(|| "true".to_string());
1295 let enable_quality_processing = field_overrides
1296 .remove("enableQualityProcessing")
1297 .unwrap_or_else(|| "true".to_string());
1298 let force_ocr = field_overrides
1299 .remove("forceOcr")
1300 .unwrap_or_else(|| "false".to_string());
1301 let disable_ocr = field_overrides
1302 .remove("disableOcr")
1303 .unwrap_or_else(|| "false".to_string());
1304 let include_document_structure = field_overrides
1305 .remove("includeDocumentStructure")
1306 .unwrap_or_else(|| "false".to_string());
1307 let use_layout_for_markdown = field_overrides
1308 .remove("useLayoutForMarkdown")
1309 .unwrap_or_else(|| "false".to_string());
1310 let max_archive_depth = field_overrides
1311 .remove("maxArchiveDepth")
1312 .unwrap_or_else(|| "3".to_string());
1313
1314 format!(
1315 "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})"
1316 )
1317}
1318
1319struct DartTestClientRenderer {
1335 in_skip: Cell<bool>,
1338 is_redirect: Cell<bool>,
1341}
1342
1343impl DartTestClientRenderer {
1344 fn new(is_redirect: bool) -> Self {
1345 Self {
1346 in_skip: Cell::new(false),
1347 is_redirect: Cell::new(is_redirect),
1348 }
1349 }
1350}
1351
1352impl client::TestClientRenderer for DartTestClientRenderer {
1353 fn language_name(&self) -> &'static str {
1354 "dart"
1355 }
1356
1357 fn render_test_open(&self, out: &mut String, _fn_name: &str, description: &str, skip_reason: Option<&str>) {
1366 let escaped_desc = escape_dart(description);
1367 if let Some(reason) = skip_reason {
1368 let escaped_reason = escape_dart(reason);
1369 let _ = writeln!(out, " test('{escaped_desc}', () {{");
1370 let _ = writeln!(out, " markTestSkipped('{escaped_reason}');");
1371 let _ = writeln!(out, " }});");
1372 let _ = writeln!(out);
1373 self.in_skip.set(true);
1374 } else {
1375 let _ = writeln!(
1376 out,
1377 " test('{escaped_desc}', () => _serialized(() => _withRetry(() async {{"
1378 );
1379 self.in_skip.set(false);
1380 }
1381 }
1382
1383 fn render_test_close(&self, out: &mut String) {
1388 if self.in_skip.get() {
1389 return;
1391 }
1392 let _ = writeln!(out, " }})));");
1393 let _ = writeln!(out);
1394 }
1395
1396 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1406 const DART_RESTRICTED_HEADERS: &[&str] = &["content-length", "host", "transfer-encoding"];
1408
1409 let method = ctx.method.to_uppercase();
1410 let escaped_method = escape_dart(&method);
1411
1412 let fixture_path = escape_dart(ctx.path);
1414
1415 let has_explicit_content_type = ctx.headers.keys().any(|k| k.to_lowercase() == "content-type");
1417 let effective_content_type = if has_explicit_content_type {
1418 ctx.headers
1419 .iter()
1420 .find(|(k, _)| k.to_lowercase() == "content-type")
1421 .map(|(_, v)| v.as_str())
1422 .unwrap_or("application/json")
1423 } else if ctx.body.is_some() {
1424 ctx.content_type.unwrap_or("application/json")
1425 } else {
1426 ""
1427 };
1428
1429 let _ = writeln!(
1430 out,
1431 " final baseUrl = Platform.environment['MOCK_SERVER_URL'] ?? 'http://localhost:8080';"
1432 );
1433 let _ = writeln!(out, " final uri = Uri.parse('$baseUrl{fixture_path}');");
1434 let _ = writeln!(
1435 out,
1436 " final ioReq = await _httpClient.openUrl('{escaped_method}', uri);"
1437 );
1438
1439 if self.is_redirect.get() {
1442 let _ = writeln!(out, " ioReq.followRedirects = false;");
1443 }
1444
1445 if !effective_content_type.is_empty() {
1447 let escaped_ct = escape_dart(effective_content_type);
1448 let _ = writeln!(out, " ioReq.headers.set('content-type', '{escaped_ct}');");
1449 }
1450
1451 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
1453 header_pairs.sort_by_key(|(k, _)| k.as_str());
1454 for (name, value) in &header_pairs {
1455 if DART_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
1456 continue;
1457 }
1458 if name.to_lowercase() == "content-type" {
1459 continue; }
1461 let escaped_name = escape_dart(&name.to_lowercase());
1462 let escaped_value = escape_dart(value);
1463 let _ = writeln!(out, " ioReq.headers.set('{escaped_name}', '{escaped_value}');");
1464 }
1465
1466 if !ctx.cookies.is_empty() {
1468 let mut cookie_pairs: Vec<(&String, &String)> = ctx.cookies.iter().collect();
1469 cookie_pairs.sort_by_key(|(k, _)| k.as_str());
1470 let cookie_str: Vec<String> = cookie_pairs.iter().map(|(k, v)| format!("{k}={v}")).collect();
1471 let cookie_header = escape_dart(&cookie_str.join("; "));
1472 let _ = writeln!(out, " ioReq.headers.set('cookie', '{cookie_header}');");
1473 }
1474
1475 if let Some(body) = ctx.body {
1477 let json_str = serde_json::to_string(body).unwrap_or_default();
1478 let escaped = escape_dart(&json_str);
1479 let _ = writeln!(out, " final bodyBytes = utf8.encode('{escaped}');");
1480 let _ = writeln!(out, " ioReq.add(bodyBytes);");
1481 }
1482
1483 let _ = writeln!(out, " final ioResp = await ioReq.close();");
1484 if !self.is_redirect.get() {
1488 let _ = writeln!(out, " final bodyStr = await ioResp.transform(utf8.decoder).join();");
1489 };
1490 }
1491
1492 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1493 let _ = writeln!(
1494 out,
1495 " expect(ioResp.statusCode, equals({status}), reason: 'status code mismatch');"
1496 );
1497 }
1498
1499 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1502 let escaped_name = escape_dart(&name.to_lowercase());
1503 match expected {
1504 "<<present>>" => {
1505 let _ = writeln!(
1506 out,
1507 " expect(ioResp.headers.value('{escaped_name}'), isNotNull, reason: 'header {escaped_name} should be present');"
1508 );
1509 }
1510 "<<absent>>" => {
1511 let _ = writeln!(
1512 out,
1513 " expect(ioResp.headers.value('{escaped_name}'), isNull, reason: 'header {escaped_name} should be absent');"
1514 );
1515 }
1516 "<<uuid>>" => {
1517 let _ = writeln!(
1518 out,
1519 " 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');"
1520 );
1521 }
1522 exact => {
1523 let escaped_value = escape_dart(exact);
1524 let _ = writeln!(
1525 out,
1526 " expect(ioResp.headers.value('{escaped_name}'), contains('{escaped_value}'), reason: 'header {escaped_name} mismatch');"
1527 );
1528 }
1529 }
1530 }
1531
1532 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1537 match expected {
1538 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1539 let json_str = serde_json::to_string(expected).unwrap_or_default();
1540 let escaped = escape_dart(&json_str);
1541 let _ = writeln!(out, " final bodyJson = jsonDecode(bodyStr);");
1542 let _ = writeln!(out, " final expectedJson = jsonDecode('{escaped}');");
1543 let _ = writeln!(
1544 out,
1545 " expect(bodyJson, equals(expectedJson), reason: 'body mismatch');"
1546 );
1547 }
1548 serde_json::Value::String(s) => {
1549 let escaped = escape_dart(s);
1550 let _ = writeln!(
1551 out,
1552 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
1553 );
1554 }
1555 other => {
1556 let escaped = escape_dart(&other.to_string());
1557 let _ = writeln!(
1558 out,
1559 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
1560 );
1561 }
1562 }
1563 }
1564
1565 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1568 let _ = writeln!(
1569 out,
1570 " final partialJson = jsonDecode(bodyStr) as Map<String, dynamic>;"
1571 );
1572 if let Some(obj) = expected.as_object() {
1573 for (idx, (key, val)) in obj.iter().enumerate() {
1574 let escaped_key = escape_dart(key);
1575 let json_val = serde_json::to_string(val).unwrap_or_default();
1576 let escaped_val = escape_dart(&json_val);
1577 let _ = writeln!(out, " final _expectedField{idx} = jsonDecode('{escaped_val}');");
1580 let _ = writeln!(
1581 out,
1582 " expect(partialJson['{escaped_key}'], equals(_expectedField{idx}), reason: 'partial body field \\'{escaped_key}\\' mismatch');"
1583 );
1584 }
1585 }
1586 }
1587
1588 fn render_assert_validation_errors(
1590 &self,
1591 out: &mut String,
1592 _response_var: &str,
1593 errors: &[ValidationErrorExpectation],
1594 ) {
1595 let _ = writeln!(out, " final errBody = jsonDecode(bodyStr) as Map<String, dynamic>;");
1596 let _ = writeln!(out, " final errList = (errBody['errors'] ?? []) as List<dynamic>;");
1597 for ve in errors {
1598 let loc_dart: Vec<String> = ve.loc.iter().map(|s| format!("'{}'", escape_dart(s))).collect();
1599 let loc_str = loc_dart.join(", ");
1600 let escaped_msg = escape_dart(&ve.msg);
1601 let _ = writeln!(
1602 out,
1603 " 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}');"
1604 );
1605 }
1606 }
1607}
1608
1609fn render_http_test_case(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
1616 if http.expected_response.status_code == 101 {
1618 let description = escape_dart(&fixture.description);
1619 let _ = writeln!(out, " test('{description}', () {{");
1620 let _ = writeln!(
1621 out,
1622 " markTestSkipped('Skipped: Dart HttpClient cannot handle 101 Switching Protocols responses');"
1623 );
1624 let _ = writeln!(out, " }});");
1625 let _ = writeln!(out);
1626 return;
1627 }
1628
1629 let is_redirect = http.expected_response.status_code / 100 == 3;
1633 client::http_call::render_http_test(out, &DartTestClientRenderer::new(is_redirect), fixture);
1634}
1635
1636fn mime_from_extension(path: &str) -> Option<&'static str> {
1641 let ext = path.rsplit('.').next()?;
1642 match ext.to_lowercase().as_str() {
1643 "docx" => Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
1644 "xlsx" => Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
1645 "pptx" => Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
1646 "pdf" => Some("application/pdf"),
1647 "txt" | "text" => Some("text/plain"),
1648 "html" | "htm" => Some("text/html"),
1649 "json" => Some("application/json"),
1650 "xml" => Some("application/xml"),
1651 "csv" => Some("text/csv"),
1652 "md" | "markdown" => Some("text/markdown"),
1653 "png" => Some("image/png"),
1654 "jpg" | "jpeg" => Some("image/jpeg"),
1655 "gif" => Some("image/gif"),
1656 "zip" => Some("application/zip"),
1657 "odt" => Some("application/vnd.oasis.opendocument.text"),
1658 "ods" => Some("application/vnd.oasis.opendocument.spreadsheet"),
1659 "odp" => Some("application/vnd.oasis.opendocument.presentation"),
1660 "rtf" => Some("application/rtf"),
1661 "epub" => Some("application/epub+zip"),
1662 "msg" => Some("application/vnd.ms-outlook"),
1663 "eml" => Some("message/rfc822"),
1664 _ => None,
1665 }
1666}
1667
1668fn emit_dart_batch_item_array(arr: &serde_json::Value, elem_type: &str) -> String {
1675 let items: Vec<String> = arr
1676 .as_array()
1677 .map(|a| a.as_slice())
1678 .unwrap_or_default()
1679 .iter()
1680 .filter_map(|item| {
1681 let obj = item.as_object()?;
1682 match elem_type {
1683 "BatchBytesItem" => {
1684 let content_bytes = obj
1685 .get("content")
1686 .and_then(|v| v.as_array())
1687 .map(|arr| {
1688 let nums: Vec<String> =
1689 arr.iter().filter_map(|v| v.as_u64().map(|n| n.to_string())).collect();
1690 format!("Uint8List.fromList([{}])", nums.join(", "))
1691 })
1692 .unwrap_or_else(|| "Uint8List(0)".to_string());
1693 let mime_type = obj
1694 .get("mime_type")
1695 .and_then(|v| v.as_str())
1696 .unwrap_or("application/octet-stream");
1697 Some(format!(
1698 "BatchBytesItem(content: {content_bytes}, mimeType: '{}')",
1699 escape_dart(mime_type)
1700 ))
1701 }
1702 "BatchFileItem" => {
1703 let path = obj.get("path").and_then(|v| v.as_str()).unwrap_or("");
1704 Some(format!("BatchFileItem(path: '{}')", escape_dart(path)))
1705 }
1706 _ => None,
1707 }
1708 })
1709 .collect();
1710 format!("[{}]", items.join(", "))
1711}
1712
1713fn escape_dart(s: &str) -> String {
1715 s.replace('\\', "\\\\")
1716 .replace('\'', "\\'")
1717 .replace('\n', "\\n")
1718 .replace('\r', "\\r")
1719 .replace('\t', "\\t")
1720 .replace('$', "\\$")
1721}
1722
1723fn type_name_to_create_from_json_dart(type_name: &str) -> String {
1731 let mut snake = String::with_capacity(type_name.len() + 8);
1733 for (i, ch) in type_name.char_indices() {
1734 if ch.is_uppercase() {
1735 if i > 0 {
1736 snake.push('_');
1737 }
1738 snake.extend(ch.to_lowercase());
1739 } else {
1740 snake.push(ch);
1741 }
1742 }
1743 let rust_fn = format!("create_{snake}_from_json");
1746 rust_fn
1748 .split('_')
1749 .enumerate()
1750 .map(|(i, part)| {
1751 if i == 0 {
1752 part.to_string()
1753 } else {
1754 let mut chars = part.chars();
1755 match chars.next() {
1756 None => String::new(),
1757 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1758 }
1759 }
1760 })
1761 .collect::<Vec<_>>()
1762 .join("")
1763}