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 ) -> Result<Vec<GeneratedFile>> {
33 let lang = self.language_name();
34 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
35
36 let mut files = Vec::new();
37
38 let dart_pkg = e2e_config.resolve_package("dart");
40 let pkg_name = dart_pkg
41 .as_ref()
42 .and_then(|p| p.name.as_ref())
43 .cloned()
44 .unwrap_or_else(|| config.dart_pubspec_name());
45 let pkg_path = dart_pkg
46 .as_ref()
47 .and_then(|p| p.path.as_ref())
48 .cloned()
49 .unwrap_or_else(|| "../../packages/dart".to_string());
50 let pkg_version = dart_pkg
51 .as_ref()
52 .and_then(|p| p.version.as_ref())
53 .cloned()
54 .or_else(|| config.resolved_version())
55 .unwrap_or_else(|| "0.1.0".to_string());
56
57 files.push(GeneratedFile {
59 path: output_base.join("pubspec.yaml"),
60 content: render_pubspec(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
61 generated_header: false,
62 });
63
64 files.push(GeneratedFile {
67 path: output_base.join("dart_test.yaml"),
68 content: concat!(
69 "# Generated by alef — DO NOT EDIT.\n",
70 "# Run test files sequentially to avoid overwhelming the mock server with\n",
71 "# concurrent keep-alive connections.\n",
72 "concurrency: 1\n",
73 )
74 .to_string(),
75 generated_header: false,
76 });
77
78 let test_base = output_base.join("test");
79
80 for group in groups {
82 let active: Vec<&Fixture> = group
83 .fixtures
84 .iter()
85 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
86 .collect();
87
88 if active.is_empty() {
89 continue;
90 }
91
92 let filename = format!("{}_test.dart", sanitize_filename(&group.category));
93 let content = render_test_file(&group.category, &active, e2e_config, lang);
94 files.push(GeneratedFile {
95 path: test_base.join(filename),
96 content,
97 generated_header: true,
98 });
99 }
100
101 Ok(files)
102 }
103
104 fn language_name(&self) -> &'static str {
105 "dart"
106 }
107}
108
109fn render_pubspec(
114 pkg_name: &str,
115 pkg_path: &str,
116 pkg_version: &str,
117 dep_mode: crate::config::DependencyMode,
118) -> String {
119 let test_ver = pub_dev::TEST_PACKAGE;
120 let http_ver = pub_dev::HTTP_PACKAGE;
121
122 let dep_block = match dep_mode {
123 crate::config::DependencyMode::Registry => {
124 format!(" {pkg_name}: ^{pkg_version}")
125 }
126 crate::config::DependencyMode::Local => {
127 format!(" {pkg_name}:\n path: {pkg_path}")
128 }
129 };
130
131 format!(
132 r#"name: e2e_dart
133version: 0.1.0
134publish_to: none
135
136environment:
137 sdk: ">=3.0.0 <4.0.0"
138
139dependencies:
140{dep_block}
141
142dev_dependencies:
143 test: {test_ver}
144 http: {http_ver}
145"#
146 )
147}
148
149fn render_test_file(category: &str, fixtures: &[&Fixture], e2e_config: &E2eConfig, lang: &str) -> String {
150 let mut out = String::new();
151 out.push_str(&hash::header(CommentStyle::DoubleSlash));
152
153 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
155
156 let has_batch_byte_items = fixtures.iter().any(|f| {
158 let call_config = e2e_config.resolve_call(f.call.as_deref());
159 call_config.args.iter().any(|a| {
160 a.element_type.as_deref() == Some("BatchBytesItem") && resolve_field(&f.input, &a.field).is_array()
161 })
162 });
163
164 let needs_chdir = fixtures.iter().any(|f| {
168 if f.is_http_test() {
169 return false;
170 }
171 let call_config = e2e_config.resolve_call(f.call.as_deref());
172 call_config
173 .args
174 .iter()
175 .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
176 });
177
178 let _ = writeln!(out, "import 'package:test/test.dart';");
179 let _ = writeln!(out, "import 'dart:io';");
180 if has_batch_byte_items {
181 let _ = writeln!(out, "import 'dart:typed_data';");
182 }
183 let _ = writeln!(out, "import 'package:kreuzberg/kreuzberg.dart';");
184 let _ = writeln!(
187 out,
188 "import 'package:kreuzberg/src/kreuzberg_bridge_generated/frb_generated.dart' show RustLib;"
189 );
190 if has_http_fixtures {
191 let _ = writeln!(out, "import 'dart:async';");
192 let _ = writeln!(out, "import 'dart:convert';");
193 }
194 let _ = writeln!(out);
195
196 if has_http_fixtures {
206 let _ = writeln!(out, "HttpClient _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
207 let _ = writeln!(out);
208 let _ = writeln!(out, "var _lock = Future<void>.value();");
209 let _ = writeln!(out);
210 let _ = writeln!(out, "Future<T> _serialized<T>(Future<T> Function() fn) async {{");
211 let _ = writeln!(out, " final current = _lock;");
212 let _ = writeln!(out, " final next = Completer<void>();");
213 let _ = writeln!(out, " _lock = next.future;");
214 let _ = writeln!(out, " try {{");
215 let _ = writeln!(out, " await current;");
216 let _ = writeln!(out, " return await fn();");
217 let _ = writeln!(out, " }} finally {{");
218 let _ = writeln!(out, " next.complete();");
219 let _ = writeln!(out, " }}");
220 let _ = writeln!(out, "}}");
221 let _ = writeln!(out);
222 let _ = writeln!(out, "Future<T> _withRetry<T>(Future<T> Function() fn) async {{");
225 let _ = writeln!(out, " try {{");
226 let _ = writeln!(out, " return await fn();");
227 let _ = writeln!(out, " }} on SocketException {{");
228 let _ = writeln!(out, " _httpClient.close(force: true);");
229 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
230 let _ = writeln!(out, " return fn();");
231 let _ = writeln!(out, " }} on HttpException {{");
232 let _ = writeln!(out, " _httpClient.close(force: true);");
233 let _ = writeln!(out, " _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
234 let _ = writeln!(out, " return fn();");
235 let _ = writeln!(out, " }}");
236 let _ = writeln!(out, "}}");
237 let _ = writeln!(out);
238 }
239
240 let _ = writeln!(out, "// E2e tests for category: {category}");
241 let _ = writeln!(out, "void main() {{");
242
243 let _ = writeln!(out, " setUpAll(() async {{");
250 let _ = writeln!(out, " await RustLib.init();");
251 if needs_chdir {
252 let _ = writeln!(
253 out,
254 " final _testDocs = Platform.environment['FIXTURES_DIR'] ?? '../../test_documents';"
255 );
256 let _ = writeln!(out, " final _dir = Directory(_testDocs);");
257 let _ = writeln!(out, " if (_dir.existsSync()) Directory.current = _dir;");
258 }
259 let _ = writeln!(out, " }});");
260 let _ = writeln!(out);
261
262 if has_http_fixtures {
264 let _ = writeln!(out, " tearDownAll(() => _httpClient.close());");
265 let _ = writeln!(out);
266 }
267
268 for fixture in fixtures {
269 render_test_case(&mut out, fixture, e2e_config, lang);
270 }
271
272 let _ = writeln!(out, "}}");
273 out
274}
275
276fn render_test_case(out: &mut String, fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) {
277 if let Some(http) = &fixture.http {
279 render_http_test_case(out, fixture, http);
280 return;
281 }
282
283 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
285 let call_overrides = call_config.overrides.get(lang);
286 let mut function_name = call_overrides
287 .and_then(|o| o.function.as_ref())
288 .cloned()
289 .unwrap_or_else(|| call_config.function.clone());
290 function_name = function_name
292 .split('_')
293 .enumerate()
294 .map(|(i, part)| {
295 if i == 0 {
296 part.to_string()
297 } else {
298 let mut chars = part.chars();
299 match chars.next() {
300 None => String::new(),
301 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
302 }
303 }
304 })
305 .collect::<Vec<_>>()
306 .join("");
307 let result_var = &call_config.result_var;
308 let description = escape_dart(&fixture.description);
309 let _is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
312
313 let file_path_for_mime: Option<&str> = call_config
321 .args
322 .iter()
323 .find(|a| a.arg_type == "file_path")
324 .and_then(|a| resolve_field(&fixture.input, &a.field).as_str());
325
326 let has_file_path_arg = call_config.args.iter().any(|a| a.arg_type == "file_path");
333 let caller_supplied_override = call_overrides.and_then(|o| o.function.as_ref()).is_some();
336 if has_file_path_arg && !caller_supplied_override {
337 function_name = match function_name.as_str() {
338 "extractFile" => "extractBytes".to_string(),
339 "extractFileSync" => "extractBytesSync".to_string(),
340 other => other.to_string(),
341 };
342 }
343
344 let mut args = Vec::new();
345 for arg_def in &call_config.args {
346 let arg_value = resolve_field(&fixture.input, &arg_def.field);
347 match arg_def.arg_type.as_str() {
348 "bytes" | "file_path" => {
349 if let serde_json::Value::String(file_path) = arg_value {
354 args.push(format!("File('{}').readAsBytesSync()", file_path));
355 }
356 }
357 "string" => {
358 match arg_value {
359 serde_json::Value::String(s) => {
360 args.push(format!("'{}'", escape_dart(s)));
361 }
362 serde_json::Value::Null
363 if arg_def.optional
364 && arg_def.name == "mime_type" =>
367 {
368 let inferred = file_path_for_mime
369 .and_then(mime_from_extension)
370 .unwrap_or("application/octet-stream");
371 args.push(format!("'{inferred}'"));
372 }
373 _ => {}
375 }
376 }
377 "json_object" => {
378 if let Some(elem_type) = &arg_def.element_type {
380 if (elem_type == "BatchBytesItem" || elem_type == "BatchFileItem") && arg_value.is_array() {
381 let dart_items = emit_dart_batch_item_array(arg_value, elem_type);
382 args.push(dart_items);
383 }
384 } else if arg_def.name == "config" {
385 if let serde_json::Value::Object(map) = &arg_value {
386 if !map.is_empty() {
390 args.push(emit_extraction_config_dart(map));
391 }
392 }
393 }
395 }
396 _ => {}
397 }
398 }
399
400 let _ = writeln!(out, " test('{description}', () async {{");
404
405 let args_str = args.join(", ");
407 let receiver_class = call_overrides
408 .and_then(|o| o.class.as_ref())
409 .cloned()
410 .unwrap_or_else(|| "KreuzbergBridge".to_string());
411
412 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
413
414 if expects_error {
415 let _ = writeln!(
420 out,
421 " await expectLater({receiver_class}.{function_name}({args_str}), throwsA(anything));"
422 );
423 } else {
424 let _ = writeln!(
425 out,
426 " final {result_var} = await {receiver_class}.{function_name}({args_str});"
427 );
428 }
429
430 let _ = writeln!(out, " }});");
431 let _ = writeln!(out);
432}
433
434fn snake_to_camel(s: &str) -> String {
436 let mut result = String::with_capacity(s.len());
437 let mut next_upper = false;
438 for ch in s.chars() {
439 if ch == '_' {
440 next_upper = true;
441 } else if next_upper {
442 result.extend(ch.to_uppercase());
443 next_upper = false;
444 } else {
445 result.push(ch);
446 }
447 }
448 result
449}
450
451fn emit_extraction_config_dart(overrides: &serde_json::Map<String, serde_json::Value>) -> String {
457 let mut field_overrides: std::collections::HashMap<String, String> = std::collections::HashMap::new();
459 for (key, val) in overrides {
460 let camel = snake_to_camel(key);
461 let dart_val = match val {
462 serde_json::Value::Bool(b) => {
463 if *b {
464 "true".to_string()
465 } else {
466 "false".to_string()
467 }
468 }
469 serde_json::Value::Number(n) => n.to_string(),
470 serde_json::Value::String(s) => format!("'{s}'"),
471 _ => continue, };
473 field_overrides.insert(camel, dart_val);
474 }
475
476 let use_cache = field_overrides.remove("useCache").unwrap_or_else(|| "true".to_string());
477 let enable_quality_processing = field_overrides
478 .remove("enableQualityProcessing")
479 .unwrap_or_else(|| "true".to_string());
480 let force_ocr = field_overrides
481 .remove("forceOcr")
482 .unwrap_or_else(|| "false".to_string());
483 let disable_ocr = field_overrides
484 .remove("disableOcr")
485 .unwrap_or_else(|| "false".to_string());
486 let include_document_structure = field_overrides
487 .remove("includeDocumentStructure")
488 .unwrap_or_else(|| "false".to_string());
489 let max_archive_depth = field_overrides
490 .remove("maxArchiveDepth")
491 .unwrap_or_else(|| "3".to_string());
492
493 format!(
494 "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})"
495 )
496}
497
498struct DartTestClientRenderer {
514 in_skip: Cell<bool>,
517 is_redirect: Cell<bool>,
520}
521
522impl DartTestClientRenderer {
523 fn new(is_redirect: bool) -> Self {
524 Self {
525 in_skip: Cell::new(false),
526 is_redirect: Cell::new(is_redirect),
527 }
528 }
529}
530
531impl client::TestClientRenderer for DartTestClientRenderer {
532 fn language_name(&self) -> &'static str {
533 "dart"
534 }
535
536 fn render_test_open(&self, out: &mut String, _fn_name: &str, description: &str, skip_reason: Option<&str>) {
545 let escaped_desc = escape_dart(description);
546 if let Some(reason) = skip_reason {
547 let escaped_reason = escape_dart(reason);
548 let _ = writeln!(out, " test('{escaped_desc}', () {{");
549 let _ = writeln!(out, " markTestSkipped('{escaped_reason}');");
550 let _ = writeln!(out, " }});");
551 let _ = writeln!(out);
552 self.in_skip.set(true);
553 } else {
554 let _ = writeln!(
555 out,
556 " test('{escaped_desc}', () => _serialized(() => _withRetry(() async {{"
557 );
558 self.in_skip.set(false);
559 }
560 }
561
562 fn render_test_close(&self, out: &mut String) {
567 if self.in_skip.get() {
568 return;
570 }
571 let _ = writeln!(out, " }})));");
572 let _ = writeln!(out);
573 }
574
575 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
585 const DART_RESTRICTED_HEADERS: &[&str] = &["content-length", "host", "transfer-encoding"];
587
588 let method = ctx.method.to_uppercase();
589 let escaped_method = escape_dart(&method);
590
591 let fixture_path = escape_dart(ctx.path);
593
594 let has_explicit_content_type = ctx.headers.keys().any(|k| k.to_lowercase() == "content-type");
596 let effective_content_type = if has_explicit_content_type {
597 ctx.headers
598 .iter()
599 .find(|(k, _)| k.to_lowercase() == "content-type")
600 .map(|(_, v)| v.as_str())
601 .unwrap_or("application/json")
602 } else if ctx.body.is_some() {
603 ctx.content_type.unwrap_or("application/json")
604 } else {
605 ""
606 };
607
608 let _ = writeln!(
609 out,
610 " final baseUrl = Platform.environment['MOCK_SERVER_URL'] ?? 'http://localhost:8080';"
611 );
612 let _ = writeln!(out, " final uri = Uri.parse('$baseUrl{fixture_path}');");
613 let _ = writeln!(
614 out,
615 " final ioReq = await _httpClient.openUrl('{escaped_method}', uri);"
616 );
617
618 if self.is_redirect.get() {
621 let _ = writeln!(out, " ioReq.followRedirects = false;");
622 }
623
624 if !effective_content_type.is_empty() {
626 let escaped_ct = escape_dart(effective_content_type);
627 let _ = writeln!(out, " ioReq.headers.set('content-type', '{escaped_ct}');");
628 }
629
630 let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
632 header_pairs.sort_by_key(|(k, _)| k.as_str());
633 for (name, value) in &header_pairs {
634 if DART_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
635 continue;
636 }
637 if name.to_lowercase() == "content-type" {
638 continue; }
640 let escaped_name = escape_dart(&name.to_lowercase());
641 let escaped_value = escape_dart(value);
642 let _ = writeln!(out, " ioReq.headers.set('{escaped_name}', '{escaped_value}');");
643 }
644
645 if !ctx.cookies.is_empty() {
647 let mut cookie_pairs: Vec<(&String, &String)> = ctx.cookies.iter().collect();
648 cookie_pairs.sort_by_key(|(k, _)| k.as_str());
649 let cookie_str: Vec<String> = cookie_pairs.iter().map(|(k, v)| format!("{k}={v}")).collect();
650 let cookie_header = escape_dart(&cookie_str.join("; "));
651 let _ = writeln!(out, " ioReq.headers.set('cookie', '{cookie_header}');");
652 }
653
654 if let Some(body) = ctx.body {
656 let json_str = serde_json::to_string(body).unwrap_or_default();
657 let escaped = escape_dart(&json_str);
658 let _ = writeln!(out, " final bodyBytes = utf8.encode('{escaped}');");
659 let _ = writeln!(out, " ioReq.add(bodyBytes);");
660 }
661
662 let _ = writeln!(out, " final ioResp = await ioReq.close();");
663 if !self.is_redirect.get() {
667 let _ = writeln!(out, " final bodyStr = await ioResp.transform(utf8.decoder).join();");
668 };
669 }
670
671 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
672 let _ = writeln!(
673 out,
674 " expect(ioResp.statusCode, equals({status}), reason: 'status code mismatch');"
675 );
676 }
677
678 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
681 let escaped_name = escape_dart(&name.to_lowercase());
682 match expected {
683 "<<present>>" => {
684 let _ = writeln!(
685 out,
686 " expect(ioResp.headers.value('{escaped_name}'), isNotNull, reason: 'header {escaped_name} should be present');"
687 );
688 }
689 "<<absent>>" => {
690 let _ = writeln!(
691 out,
692 " expect(ioResp.headers.value('{escaped_name}'), isNull, reason: 'header {escaped_name} should be absent');"
693 );
694 }
695 "<<uuid>>" => {
696 let _ = writeln!(
697 out,
698 " 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');"
699 );
700 }
701 exact => {
702 let escaped_value = escape_dart(exact);
703 let _ = writeln!(
704 out,
705 " expect(ioResp.headers.value('{escaped_name}'), contains('{escaped_value}'), reason: 'header {escaped_name} mismatch');"
706 );
707 }
708 }
709 }
710
711 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
716 match expected {
717 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
718 let json_str = serde_json::to_string(expected).unwrap_or_default();
719 let escaped = escape_dart(&json_str);
720 let _ = writeln!(out, " final bodyJson = jsonDecode(bodyStr);");
721 let _ = writeln!(out, " final expectedJson = jsonDecode('{escaped}');");
722 let _ = writeln!(
723 out,
724 " expect(bodyJson, equals(expectedJson), reason: 'body mismatch');"
725 );
726 }
727 serde_json::Value::String(s) => {
728 let escaped = escape_dart(s);
729 let _ = writeln!(
730 out,
731 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
732 );
733 }
734 other => {
735 let escaped = escape_dart(&other.to_string());
736 let _ = writeln!(
737 out,
738 " expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
739 );
740 }
741 }
742 }
743
744 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
747 let _ = writeln!(
748 out,
749 " final partialJson = jsonDecode(bodyStr) as Map<String, dynamic>;"
750 );
751 if let Some(obj) = expected.as_object() {
752 for (idx, (key, val)) in obj.iter().enumerate() {
753 let escaped_key = escape_dart(key);
754 let json_val = serde_json::to_string(val).unwrap_or_default();
755 let escaped_val = escape_dart(&json_val);
756 let _ = writeln!(out, " final _expectedField{idx} = jsonDecode('{escaped_val}');");
759 let _ = writeln!(
760 out,
761 " expect(partialJson['{escaped_key}'], equals(_expectedField{idx}), reason: 'partial body field \\'{escaped_key}\\' mismatch');"
762 );
763 }
764 }
765 }
766
767 fn render_assert_validation_errors(
769 &self,
770 out: &mut String,
771 _response_var: &str,
772 errors: &[ValidationErrorExpectation],
773 ) {
774 let _ = writeln!(out, " final errBody = jsonDecode(bodyStr) as Map<String, dynamic>;");
775 let _ = writeln!(out, " final errList = (errBody['errors'] ?? []) as List<dynamic>;");
776 for ve in errors {
777 let loc_dart: Vec<String> = ve.loc.iter().map(|s| format!("'{}'", escape_dart(s))).collect();
778 let loc_str = loc_dart.join(", ");
779 let escaped_msg = escape_dart(&ve.msg);
780 let _ = writeln!(
781 out,
782 " 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}');"
783 );
784 }
785 }
786}
787
788fn render_http_test_case(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
795 if http.expected_response.status_code == 101 {
797 let description = escape_dart(&fixture.description);
798 let _ = writeln!(out, " test('{description}', () {{");
799 let _ = writeln!(
800 out,
801 " markTestSkipped('Skipped: Dart HttpClient cannot handle 101 Switching Protocols responses');"
802 );
803 let _ = writeln!(out, " }});");
804 let _ = writeln!(out);
805 return;
806 }
807
808 let is_redirect = http.expected_response.status_code / 100 == 3;
812 client::http_call::render_http_test(out, &DartTestClientRenderer::new(is_redirect), fixture);
813}
814
815fn mime_from_extension(path: &str) -> Option<&'static str> {
820 let ext = path.rsplit('.').next()?;
821 match ext.to_lowercase().as_str() {
822 "docx" => Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
823 "xlsx" => Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
824 "pptx" => Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
825 "pdf" => Some("application/pdf"),
826 "txt" | "text" => Some("text/plain"),
827 "html" | "htm" => Some("text/html"),
828 "json" => Some("application/json"),
829 "xml" => Some("application/xml"),
830 "csv" => Some("text/csv"),
831 "md" | "markdown" => Some("text/markdown"),
832 "png" => Some("image/png"),
833 "jpg" | "jpeg" => Some("image/jpeg"),
834 "gif" => Some("image/gif"),
835 "zip" => Some("application/zip"),
836 "odt" => Some("application/vnd.oasis.opendocument.text"),
837 "ods" => Some("application/vnd.oasis.opendocument.spreadsheet"),
838 "odp" => Some("application/vnd.oasis.opendocument.presentation"),
839 "rtf" => Some("application/rtf"),
840 "epub" => Some("application/epub+zip"),
841 "msg" => Some("application/vnd.ms-outlook"),
842 "eml" => Some("message/rfc822"),
843 _ => None,
844 }
845}
846
847fn emit_dart_batch_item_array(arr: &serde_json::Value, elem_type: &str) -> String {
854 let items: Vec<String> = arr
855 .as_array()
856 .map(|a| a.as_slice())
857 .unwrap_or_default()
858 .iter()
859 .filter_map(|item| {
860 let obj = item.as_object()?;
861 match elem_type {
862 "BatchBytesItem" => {
863 let content_bytes = obj
864 .get("content")
865 .and_then(|v| v.as_array())
866 .map(|arr| {
867 let nums: Vec<String> =
868 arr.iter().filter_map(|v| v.as_u64().map(|n| n.to_string())).collect();
869 format!("Uint8List.fromList([{}])", nums.join(", "))
870 })
871 .unwrap_or_else(|| "Uint8List(0)".to_string());
872 let mime_type = obj
873 .get("mime_type")
874 .and_then(|v| v.as_str())
875 .unwrap_or("application/octet-stream");
876 Some(format!(
877 "BatchBytesItem(content: {content_bytes}, mimeType: '{}')",
878 escape_dart(mime_type)
879 ))
880 }
881 "BatchFileItem" => {
882 let path = obj.get("path").and_then(|v| v.as_str()).unwrap_or("");
883 Some(format!("BatchFileItem(path: '{}')", escape_dart(path)))
884 }
885 _ => None,
886 }
887 })
888 .collect();
889 format!("[{}]", items.join(", "))
890}
891
892fn escape_dart(s: &str) -> String {
894 s.replace('\\', "\\\\")
895 .replace('\'', "\\'")
896 .replace('\n', "\\n")
897 .replace('\r', "\\r")
898 .replace('\t', "\\t")
899 .replace('$', "\\$")
900}