1use crate::codegen::resolve_field;
8use crate::config::E2eConfig;
9use crate::escape::sanitize_filename;
10use crate::fixture::{Fixture, FixtureGroup, HttpFixture, ValidationErrorExpectation};
11use alef_core::backend::GeneratedFile;
12use alef_core::config::ResolvedCrateConfig;
13use alef_core::hash::{self, CommentStyle};
14use alef_core::template_versions::pub_dev;
15use anyhow::Result;
16use std::cell::Cell;
17use std::fmt::Write as FmtWrite;
18use std::path::PathBuf;
19
20use super::E2eCodegen;
21use super::client;
22
23pub struct DartE2eCodegen;
25
26impl E2eCodegen for DartE2eCodegen {
27 fn generate(
28 &self,
29 groups: &[FixtureGroup],
30 e2e_config: &E2eConfig,
31 config: &ResolvedCrateConfig,
32 _type_defs: &[alef_core::ir::TypeDef],
33 ) -> Result<Vec<GeneratedFile>> {
34 let lang = self.language_name();
35 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
36
37 let mut files = Vec::new();
38
39 let dart_pkg = e2e_config.resolve_package("dart");
41 let pkg_name = dart_pkg
42 .as_ref()
43 .and_then(|p| p.name.as_ref())
44 .cloned()
45 .unwrap_or_else(|| config.dart_pubspec_name());
46 let pkg_path = dart_pkg
47 .as_ref()
48 .and_then(|p| p.path.as_ref())
49 .cloned()
50 .unwrap_or_else(|| "../../packages/dart".to_string());
51 let pkg_version = dart_pkg
52 .as_ref()
53 .and_then(|p| p.version.as_ref())
54 .cloned()
55 .or_else(|| config.resolved_version())
56 .unwrap_or_else(|| "0.1.0".to_string());
57
58 files.push(GeneratedFile {
60 path: output_base.join("pubspec.yaml"),
61 content: render_pubspec(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
62 generated_header: false,
63 });
64
65 files.push(GeneratedFile {
68 path: output_base.join("dart_test.yaml"),
69 content: concat!(
70 "# Generated by alef — DO NOT EDIT.\n",
71 "# Run test files sequentially to avoid overwhelming the mock server with\n",
72 "# concurrent keep-alive connections.\n",
73 "concurrency: 1\n",
74 )
75 .to_string(),
76 generated_header: false,
77 });
78
79 let test_base = output_base.join("test");
80
81 for group in groups {
83 let active: Vec<&Fixture> = group
84 .fixtures
85 .iter()
86 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
87 .collect();
88
89 if active.is_empty() {
90 continue;
91 }
92
93 let filename = format!("{}_test.dart", sanitize_filename(&group.category));
94 let content = render_test_file(&group.category, &active, e2e_config, lang, &pkg_name);
95 files.push(GeneratedFile {
96 path: test_base.join(filename),
97 content,
98 generated_header: true,
99 });
100 }
101
102 Ok(files)
103 }
104
105 fn language_name(&self) -> &'static str {
106 "dart"
107 }
108}
109
110fn render_pubspec(
115 pkg_name: &str,
116 pkg_path: &str,
117 pkg_version: &str,
118 dep_mode: crate::config::DependencyMode,
119) -> String {
120 let test_ver = pub_dev::TEST_PACKAGE;
121 let http_ver = pub_dev::HTTP_PACKAGE;
122
123 let dep_block = match dep_mode {
124 crate::config::DependencyMode::Registry => {
125 format!(" {pkg_name}: ^{pkg_version}")
126 }
127 crate::config::DependencyMode::Local => {
128 format!(" {pkg_name}:\n path: {pkg_path}")
129 }
130 };
131
132 format!(
133 r#"name: e2e_dart
134version: 0.1.0
135publish_to: none
136
137environment:
138 sdk: ">=3.0.0 <4.0.0"
139
140dependencies:
141{dep_block}
142
143dev_dependencies:
144 test: {test_ver}
145 http: {http_ver}
146"#
147 )
148}
149
150fn render_test_file(
151 category: &str,
152 fixtures: &[&Fixture],
153 e2e_config: &E2eConfig,
154 lang: &str,
155 pkg_name: &str,
156) -> String {
157 let mut out = String::new();
158 out.push_str(&hash::header(CommentStyle::DoubleSlash));
159
160 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
162
163 let has_batch_byte_items = fixtures.iter().any(|f| {
165 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
166 call_config.args.iter().any(|a| {
167 a.element_type.as_deref() == Some("BatchBytesItem") && resolve_field(&f.input, &a.field).is_array()
168 })
169 });
170
171 let needs_chdir = fixtures.iter().any(|f| {
175 if f.is_http_test() {
176 return false;
177 }
178 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
179 call_config
180 .args
181 .iter()
182 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
183 });
184
185 let _ = writeln!(out, "import 'package:test/test.dart';");
186 let _ = writeln!(out, "import 'dart:io';");
187 if has_batch_byte_items {
188 let _ = writeln!(out, "import 'dart:typed_data';");
189 }
190 let _ = writeln!(out, "import 'package:{pkg_name}/{pkg_name}.dart';");
191 let _ = writeln!(
194 out,
195 "import 'package:{pkg_name}/src/{pkg_name}_bridge_generated/frb_generated.dart' show RustLib;"
196 );
197 if has_http_fixtures {
198 let _ = writeln!(out, "import 'dart:async';");
199 let _ = writeln!(out, "import 'dart:convert';");
200 }
201 let _ = writeln!(out);
202
203 if has_http_fixtures {
213 let _ = writeln!(out, "HttpClient _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
214 let _ = writeln!(out);
215 let _ = writeln!(out, "var _lock = Future<void>.value();");
216 let _ = writeln!(out);
217 let _ = writeln!(out, "Future<T> _serialized<T>(Future<T> Function() fn) async {{");
218 let _ = writeln!(out, " final current = _lock;");
219 let _ = writeln!(out, " final next = Completer<void>();");
220 let _ = writeln!(out, " _lock = next.future;");
221 let _ = writeln!(out, " try {{");
222 let _ = writeln!(out, " await current;");
223 let _ = writeln!(out, " return await fn();");
224 let _ = writeln!(out, " }} finally {{");
225 let _ = writeln!(out, " next.complete();");
226 let _ = writeln!(out, " }}");
227 let _ = writeln!(out, "}}");
228 let _ = writeln!(out);
229 let _ = writeln!(out, "Future<T> _withRetry<T>(Future<T> Function() fn) async {{");
232 let _ = writeln!(out, " try {{");
233 let _ = writeln!(out, " return await fn();");
234 let _ = writeln!(out, " }} on SocketException {{");
235 let _ = writeln!(out, " _httpClient.close(force: true);");
236 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
237 let _ = writeln!(out, " return fn();");
238 let _ = writeln!(out, " }} on HttpException {{");
239 let _ = writeln!(out, " _httpClient.close(force: true);");
240 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
241 let _ = writeln!(out, " return fn();");
242 let _ = writeln!(out, " }}");
243 let _ = writeln!(out, "}}");
244 let _ = writeln!(out);
245 }
246
247 let _ = writeln!(out, "// E2e tests for category: {category}");
248 let _ = writeln!(out, "void main() {{");
249
250 let _ = writeln!(out, " setUpAll(() async {{");
257 let _ = writeln!(out, " await RustLib.init();");
258 if needs_chdir {
259 let test_docs_path = e2e_config.test_documents_relative_from(0);
260 let _ = writeln!(
261 out,
262 " final _testDocs = Platform.environment['FIXTURES_DIR'] ?? '{test_docs_path}';"
263 );
264 let _ = writeln!(out, " final _dir = Directory(_testDocs);");
265 let _ = writeln!(out, " if (_dir.existsSync()) Directory.current = _dir;");
266 }
267 let _ = writeln!(out, " }});");
268 let _ = writeln!(out);
269
270 if has_http_fixtures {
272 let _ = writeln!(out, " tearDownAll(() => _httpClient.close());");
273 let _ = writeln!(out);
274 }
275
276 for fixture in fixtures {
277 render_test_case(&mut out, fixture, e2e_config, lang);
278 }
279
280 let _ = writeln!(out, "}}");
281 out
282}
283
284fn render_test_case(out: &mut String, fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) {
285 if let Some(http) = &fixture.http {
287 render_http_test_case(out, fixture, http);
288 return;
289 }
290
291 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
293 let call_overrides = call_config.overrides.get(lang);
294 let mut function_name = call_overrides
295 .and_then(|o| o.function.as_ref())
296 .cloned()
297 .unwrap_or_else(|| call_config.function.clone());
298 function_name = function_name
300 .split('_')
301 .enumerate()
302 .map(|(i, part)| {
303 if i == 0 {
304 part.to_string()
305 } else {
306 let mut chars = part.chars();
307 match chars.next() {
308 None => String::new(),
309 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
310 }
311 }
312 })
313 .collect::<Vec<_>>()
314 .join("");
315 let result_var = &call_config.result_var;
316 let description = escape_dart(&fixture.description);
317 let _is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
320
321 let file_path_for_mime: Option<&str> = call_config
329 .args
330 .iter()
331 .find(|a| a.arg_type == "file_path")
332 .and_then(|a| resolve_field(&fixture.input, &a.field).as_str());
333
334 let has_file_path_arg = call_config.args.iter().any(|a| a.arg_type == "file_path");
341 let caller_supplied_override = call_overrides.and_then(|o| o.function.as_ref()).is_some();
344 if has_file_path_arg && !caller_supplied_override {
345 function_name = match function_name.as_str() {
346 "extractFile" => "extractBytes".to_string(),
347 "extractFileSync" => "extractBytesSync".to_string(),
348 other => other.to_string(),
349 };
350 }
351
352 let mut args = Vec::new();
353 for arg_def in &call_config.args {
354 let arg_value = resolve_field(&fixture.input, &arg_def.field);
355 match arg_def.arg_type.as_str() {
356 "bytes" | "file_path" => {
357 if let serde_json::Value::String(file_path) = arg_value {
362 args.push(format!("File('{}').readAsBytesSync()", file_path));
363 }
364 }
365 "string" => {
366 match arg_value {
367 serde_json::Value::String(s) => {
368 args.push(format!("'{}'", escape_dart(s)));
369 }
370 serde_json::Value::Null
371 if arg_def.optional
372 && arg_def.name == "mime_type" =>
375 {
376 let inferred = file_path_for_mime
377 .and_then(mime_from_extension)
378 .unwrap_or("application/octet-stream");
379 args.push(format!("'{inferred}'"));
380 }
381 _ => {}
383 }
384 }
385 "json_object" => {
386 if let Some(elem_type) = &arg_def.element_type {
388 if (elem_type == "BatchBytesItem" || elem_type == "BatchFileItem") && arg_value.is_array() {
389 let dart_items = emit_dart_batch_item_array(arg_value, elem_type);
390 args.push(dart_items);
391 }
392 } else if arg_def.name == "config" {
393 if let serde_json::Value::Object(map) = &arg_value {
394 if !map.is_empty() {
398 args.push(emit_extraction_config_dart(map));
399 }
400 }
401 }
403 }
404 _ => {}
405 }
406 }
407
408 let _ = writeln!(out, " test('{description}', () async {{");
412
413 let args_str = args.join(", ");
415 let receiver_class = call_overrides
416 .and_then(|o| o.class.as_ref())
417 .cloned()
418 .unwrap_or_else(|| "KreuzbergBridge".to_string());
419
420 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
421
422 if expects_error {
423 let _ = writeln!(
428 out,
429 " await expectLater({receiver_class}.{function_name}({args_str}), throwsA(anything));"
430 );
431 } else {
432 let _ = writeln!(
433 out,
434 " final {result_var} = await {receiver_class}.{function_name}({args_str});"
435 );
436 }
437
438 let _ = writeln!(out, " }});");
439 let _ = writeln!(out);
440}
441
442fn snake_to_camel(s: &str) -> String {
444 let mut result = String::with_capacity(s.len());
445 let mut next_upper = false;
446 for ch in s.chars() {
447 if ch == '_' {
448 next_upper = true;
449 } else if next_upper {
450 result.extend(ch.to_uppercase());
451 next_upper = false;
452 } else {
453 result.push(ch);
454 }
455 }
456 result
457}
458
459fn emit_extraction_config_dart(overrides: &serde_json::Map<String, serde_json::Value>) -> String {
465 let mut field_overrides: std::collections::HashMap<String, String> = std::collections::HashMap::new();
467 for (key, val) in overrides {
468 let camel = snake_to_camel(key);
469 let dart_val = match val {
470 serde_json::Value::Bool(b) => {
471 if *b {
472 "true".to_string()
473 } else {
474 "false".to_string()
475 }
476 }
477 serde_json::Value::Number(n) => n.to_string(),
478 serde_json::Value::String(s) => format!("'{s}'"),
479 _ => continue, };
481 field_overrides.insert(camel, dart_val);
482 }
483
484 let use_cache = field_overrides.remove("useCache").unwrap_or_else(|| "true".to_string());
485 let enable_quality_processing = field_overrides
486 .remove("enableQualityProcessing")
487 .unwrap_or_else(|| "true".to_string());
488 let force_ocr = field_overrides
489 .remove("forceOcr")
490 .unwrap_or_else(|| "false".to_string());
491 let disable_ocr = field_overrides
492 .remove("disableOcr")
493 .unwrap_or_else(|| "false".to_string());
494 let include_document_structure = field_overrides
495 .remove("includeDocumentStructure")
496 .unwrap_or_else(|| "false".to_string());
497 let max_archive_depth = field_overrides
498 .remove("maxArchiveDepth")
499 .unwrap_or_else(|| "3".to_string());
500
501 format!(
502 "ExtractionConfig(useCache: {use_cache}, enableQualityProcessing: {enable_quality_processing}, forceOcr: {force_ocr}, disableOcr: {disable_ocr}, resultFormat: ResultFormat.unified, outputFormat: OutputFormat.plain(), includeDocumentStructure: {include_document_structure}, maxArchiveDepth: {max_archive_depth})"
503 )
504}
505
506struct DartTestClientRenderer {
522 in_skip: Cell<bool>,
525 is_redirect: Cell<bool>,
528}
529
530impl DartTestClientRenderer {
531 fn new(is_redirect: bool) -> Self {
532 Self {
533 in_skip: Cell::new(false),
534 is_redirect: Cell::new(is_redirect),
535 }
536 }
537}
538
539impl client::TestClientRenderer for DartTestClientRenderer {
540 fn language_name(&self) -> &'static str {
541 "dart"
542 }
543
544 fn render_test_open(&self, out: &mut String, _fn_name: &str, description: &str, skip_reason: Option<&str>) {
553 let escaped_desc = escape_dart(description);
554 if let Some(reason) = skip_reason {
555 let escaped_reason = escape_dart(reason);
556 let _ = writeln!(out, " test('{escaped_desc}', () {{");
557 let _ = writeln!(out, " markTestSkipped('{escaped_reason}');");
558 let _ = writeln!(out, " }});");
559 let _ = writeln!(out);
560 self.in_skip.set(true);
561 } else {
562 let _ = writeln!(
563 out,
564 " test('{escaped_desc}', () => _serialized(() => _withRetry(() async {{"
565 );
566 self.in_skip.set(false);
567 }
568 }
569
570 fn render_test_close(&self, out: &mut String) {
575 if self.in_skip.get() {
576 return;
578 }
579 let _ = writeln!(out, " }})));");
580 let _ = writeln!(out);
581 }
582
583 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
593 const DART_RESTRICTED_HEADERS: &[&str] = &["content-length", "host", "transfer-encoding"];
595
596 let method = ctx.method.to_uppercase();
597 let escaped_method = escape_dart(&method);
598
599 let fixture_path = escape_dart(ctx.path);
601
602 let has_explicit_content_type = ctx.headers.keys().any(|k| k.to_lowercase() == "content-type");
604 let effective_content_type = if has_explicit_content_type {
605 ctx.headers
606 .iter()
607 .find(|(k, _)| k.to_lowercase() == "content-type")
608 .map(|(_, v)| v.as_str())
609 .unwrap_or("application/json")
610 } else if ctx.body.is_some() {
611 ctx.content_type.unwrap_or("application/json")
612 } else {
613 ""
614 };
615
616 let _ = writeln!(
617 out,
618 " final baseUrl = Platform.environment['MOCK_SERVER_URL'] ?? 'http://localhost:8080';"
619 );
620 let _ = writeln!(out, " final uri = Uri.parse('$baseUrl{fixture_path}');");
621 let _ = writeln!(
622 out,
623 " final ioReq = await _httpClient.openUrl('{escaped_method}', uri);"
624 );
625
626 if self.is_redirect.get() {
629 let _ = writeln!(out, " ioReq.followRedirects = false;");
630 }
631
632 if !effective_content_type.is_empty() {
634 let escaped_ct = escape_dart(effective_content_type);
635 let _ = writeln!(out, " ioReq.headers.set('content-type', '{escaped_ct}');");
636 }
637
638 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
640 header_pairs.sort_by_key(|(k, _)| k.as_str());
641 for (name, value) in &header_pairs {
642 if DART_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
643 continue;
644 }
645 if name.to_lowercase() == "content-type" {
646 continue; }
648 let escaped_name = escape_dart(&name.to_lowercase());
649 let escaped_value = escape_dart(value);
650 let _ = writeln!(out, " ioReq.headers.set('{escaped_name}', '{escaped_value}');");
651 }
652
653 if !ctx.cookies.is_empty() {
655 let mut cookie_pairs: Vec<(&String, &String)> = ctx.cookies.iter().collect();
656 cookie_pairs.sort_by_key(|(k, _)| k.as_str());
657 let cookie_str: Vec<String> = cookie_pairs.iter().map(|(k, v)| format!("{k}={v}")).collect();
658 let cookie_header = escape_dart(&cookie_str.join("; "));
659 let _ = writeln!(out, " ioReq.headers.set('cookie', '{cookie_header}');");
660 }
661
662 if let Some(body) = ctx.body {
664 let json_str = serde_json::to_string(body).unwrap_or_default();
665 let escaped = escape_dart(&json_str);
666 let _ = writeln!(out, " final bodyBytes = utf8.encode('{escaped}');");
667 let _ = writeln!(out, " ioReq.add(bodyBytes);");
668 }
669
670 let _ = writeln!(out, " final ioResp = await ioReq.close();");
671 if !self.is_redirect.get() {
675 let _ = writeln!(out, " final bodyStr = await ioResp.transform(utf8.decoder).join();");
676 };
677 }
678
679 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
680 let _ = writeln!(
681 out,
682 " expect(ioResp.statusCode, equals({status}), reason: 'status code mismatch');"
683 );
684 }
685
686 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
689 let escaped_name = escape_dart(&name.to_lowercase());
690 match expected {
691 "<<present>>" => {
692 let _ = writeln!(
693 out,
694 " expect(ioResp.headers.value('{escaped_name}'), isNotNull, reason: 'header {escaped_name} should be present');"
695 );
696 }
697 "<<absent>>" => {
698 let _ = writeln!(
699 out,
700 " expect(ioResp.headers.value('{escaped_name}'), isNull, reason: 'header {escaped_name} should be absent');"
701 );
702 }
703 "<<uuid>>" => {
704 let _ = writeln!(
705 out,
706 " 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');"
707 );
708 }
709 exact => {
710 let escaped_value = escape_dart(exact);
711 let _ = writeln!(
712 out,
713 " expect(ioResp.headers.value('{escaped_name}'), contains('{escaped_value}'), reason: 'header {escaped_name} mismatch');"
714 );
715 }
716 }
717 }
718
719 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
724 match expected {
725 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
726 let json_str = serde_json::to_string(expected).unwrap_or_default();
727 let escaped = escape_dart(&json_str);
728 let _ = writeln!(out, " final bodyJson = jsonDecode(bodyStr);");
729 let _ = writeln!(out, " final expectedJson = jsonDecode('{escaped}');");
730 let _ = writeln!(
731 out,
732 " expect(bodyJson, equals(expectedJson), reason: 'body mismatch');"
733 );
734 }
735 serde_json::Value::String(s) => {
736 let escaped = escape_dart(s);
737 let _ = writeln!(
738 out,
739 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
740 );
741 }
742 other => {
743 let escaped = escape_dart(&other.to_string());
744 let _ = writeln!(
745 out,
746 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
747 );
748 }
749 }
750 }
751
752 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
755 let _ = writeln!(
756 out,
757 " final partialJson = jsonDecode(bodyStr) as Map<String, dynamic>;"
758 );
759 if let Some(obj) = expected.as_object() {
760 for (idx, (key, val)) in obj.iter().enumerate() {
761 let escaped_key = escape_dart(key);
762 let json_val = serde_json::to_string(val).unwrap_or_default();
763 let escaped_val = escape_dart(&json_val);
764 let _ = writeln!(out, " final _expectedField{idx} = jsonDecode('{escaped_val}');");
767 let _ = writeln!(
768 out,
769 " expect(partialJson['{escaped_key}'], equals(_expectedField{idx}), reason: 'partial body field \\'{escaped_key}\\' mismatch');"
770 );
771 }
772 }
773 }
774
775 fn render_assert_validation_errors(
777 &self,
778 out: &mut String,
779 _response_var: &str,
780 errors: &[ValidationErrorExpectation],
781 ) {
782 let _ = writeln!(out, " final errBody = jsonDecode(bodyStr) as Map<String, dynamic>;");
783 let _ = writeln!(out, " final errList = (errBody['errors'] ?? []) as List<dynamic>;");
784 for ve in errors {
785 let loc_dart: Vec<String> = ve.loc.iter().map(|s| format!("'{}'", escape_dart(s))).collect();
786 let loc_str = loc_dart.join(", ");
787 let escaped_msg = escape_dart(&ve.msg);
788 let _ = writeln!(
789 out,
790 " 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}');"
791 );
792 }
793 }
794}
795
796fn render_http_test_case(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
803 if http.expected_response.status_code == 101 {
805 let description = escape_dart(&fixture.description);
806 let _ = writeln!(out, " test('{description}', () {{");
807 let _ = writeln!(
808 out,
809 " markTestSkipped('Skipped: Dart HttpClient cannot handle 101 Switching Protocols responses');"
810 );
811 let _ = writeln!(out, " }});");
812 let _ = writeln!(out);
813 return;
814 }
815
816 let is_redirect = http.expected_response.status_code / 100 == 3;
820 client::http_call::render_http_test(out, &DartTestClientRenderer::new(is_redirect), fixture);
821}
822
823fn mime_from_extension(path: &str) -> Option<&'static str> {
828 let ext = path.rsplit('.').next()?;
829 match ext.to_lowercase().as_str() {
830 "docx" => Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
831 "xlsx" => Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
832 "pptx" => Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
833 "pdf" => Some("application/pdf"),
834 "txt" | "text" => Some("text/plain"),
835 "html" | "htm" => Some("text/html"),
836 "json" => Some("application/json"),
837 "xml" => Some("application/xml"),
838 "csv" => Some("text/csv"),
839 "md" | "markdown" => Some("text/markdown"),
840 "png" => Some("image/png"),
841 "jpg" | "jpeg" => Some("image/jpeg"),
842 "gif" => Some("image/gif"),
843 "zip" => Some("application/zip"),
844 "odt" => Some("application/vnd.oasis.opendocument.text"),
845 "ods" => Some("application/vnd.oasis.opendocument.spreadsheet"),
846 "odp" => Some("application/vnd.oasis.opendocument.presentation"),
847 "rtf" => Some("application/rtf"),
848 "epub" => Some("application/epub+zip"),
849 "msg" => Some("application/vnd.ms-outlook"),
850 "eml" => Some("message/rfc822"),
851 _ => None,
852 }
853}
854
855fn emit_dart_batch_item_array(arr: &serde_json::Value, elem_type: &str) -> String {
862 let items: Vec<String> = arr
863 .as_array()
864 .map(|a| a.as_slice())
865 .unwrap_or_default()
866 .iter()
867 .filter_map(|item| {
868 let obj = item.as_object()?;
869 match elem_type {
870 "BatchBytesItem" => {
871 let content_bytes = obj
872 .get("content")
873 .and_then(|v| v.as_array())
874 .map(|arr| {
875 let nums: Vec<String> =
876 arr.iter().filter_map(|v| v.as_u64().map(|n| n.to_string())).collect();
877 format!("Uint8List.fromList([{}])", nums.join(", "))
878 })
879 .unwrap_or_else(|| "Uint8List(0)".to_string());
880 let mime_type = obj
881 .get("mime_type")
882 .and_then(|v| v.as_str())
883 .unwrap_or("application/octet-stream");
884 Some(format!(
885 "BatchBytesItem(content: {content_bytes}, mimeType: '{}')",
886 escape_dart(mime_type)
887 ))
888 }
889 "BatchFileItem" => {
890 let path = obj.get("path").and_then(|v| v.as_str()).unwrap_or("");
891 Some(format!("BatchFileItem(path: '{}')", escape_dart(path)))
892 }
893 _ => None,
894 }
895 })
896 .collect();
897 format!("[{}]", items.join(", "))
898}
899
900fn escape_dart(s: &str) -> String {
902 s.replace('\\', "\\\\")
903 .replace('\'', "\\'")
904 .replace('\n', "\\n")
905 .replace('\r', "\\r")
906 .replace('\t', "\\t")
907 .replace('$', "\\$")
908}