hedl_xml/async_api.rs
1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Async API for XML conversion with Tokio
19//!
20//! This module provides asynchronous versions of all XML conversion functions,
21//! enabling high-performance concurrent I/O operations with Tokio.
22//!
23//! # Features
24//!
25//! - **Async File I/O**: Read/write XML files without blocking
26//! - **Async Streaming**: Stream large XML files incrementally
27//! - **Concurrency**: Process multiple files concurrently
28//! - **Backpressure**: Built-in flow control for streaming
29//!
30//! # Examples
31//!
32//! ## Async file conversion
33//!
34//! ```no_run
35//! use hedl_xml::async_api::{from_xml_file_async, to_xml_file_async};
36//! use hedl_xml::{FromXmlConfig, ToXmlConfig};
37//!
38//! #[tokio::main]
39//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
40//! // Read XML file asynchronously
41//! let doc = from_xml_file_async("input.xml", &FromXmlConfig::default()).await?;
42//!
43//! // Process document...
44//!
45//! // Write XML file asynchronously
46//! to_xml_file_async(&doc, "output.xml", &ToXmlConfig::default()).await?;
47//!
48//! Ok(())
49//! }
50//! ```
51//!
52//! ## Async streaming for large files
53//!
54//! ```no_run
55//! use hedl_xml::async_api::from_xml_stream_async;
56//! use hedl_xml::streaming::StreamConfig;
57//! use tokio::fs::File;
58//!
59//! #[tokio::main]
60//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
61//! let file = File::open("large.xml").await?;
62//! let config = StreamConfig::default();
63//!
64//! let mut stream = from_xml_stream_async(file, &config).await?;
65//!
66//! while let Some(result) = stream.next().await {
67//! match result {
68//! Ok(item) => println!("Processed: {}", item.key),
69//! Err(e) => eprintln!("Error: {}", e),
70//! }
71//! }
72//!
73//! Ok(())
74//! }
75//! ```
76//!
77//! ## Concurrent processing
78//!
79//! ```no_run
80//! use hedl_xml::async_api::from_xml_file_async;
81//! use hedl_xml::FromXmlConfig;
82//! use tokio::task;
83//!
84//! #[tokio::main]
85//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
86//! let config = FromXmlConfig::default();
87//!
88//! // Process multiple files concurrently
89//! let handles: Vec<_> = vec!["file1.xml", "file2.xml", "file3.xml"]
90//! .into_iter()
91//! .map(|path| {
92//! let config = config.clone();
93//! task::spawn(async move {
94//! from_xml_file_async(path, &config).await
95//! })
96//! })
97//! .collect();
98//!
99//! // Wait for all to complete
100//! for handle in handles {
101//! let doc = handle.await??;
102//! // Process document...
103//! }
104//!
105//! Ok(())
106//! }
107//! ```
108
109use crate::streaming::{StreamConfig, StreamItem, StreamPosition};
110use crate::{from_xml, to_xml, FromXmlConfig, ToXmlConfig};
111use hedl_core::Document;
112use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
113
114// ============================================================================
115// Async File I/O Functions
116// ============================================================================
117
118/// Read and parse an XML file asynchronously
119///
120/// This function reads the entire file into memory and parses it. For large files,
121/// consider using `from_xml_stream_async` instead.
122///
123/// # Examples
124///
125/// ```no_run
126/// use hedl_xml::async_api::from_xml_file_async;
127/// use hedl_xml::FromXmlConfig;
128///
129/// # #[tokio::main]
130/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
131/// let config = FromXmlConfig::default();
132/// let doc = from_xml_file_async("data.xml", &config).await?;
133/// println!("Parsed document with {} root items", doc.root.len());
134/// # Ok(())
135/// # }
136/// ```
137///
138/// # Errors
139///
140/// Returns an error if the file cannot be read or if XML parsing fails.
141pub async fn from_xml_file_async(
142 path: impl AsRef<std::path::Path>,
143 config: &FromXmlConfig,
144) -> Result<Document, String> {
145 let contents = tokio::fs::read_to_string(path)
146 .await
147 .map_err(|e| format!("Failed to read file: {}", e))?;
148
149 from_xml(&contents, config)
150}
151
152/// Write a HEDL document to an XML file asynchronously
153///
154/// # Examples
155///
156/// ```no_run
157/// use hedl_xml::async_api::to_xml_file_async;
158/// use hedl_xml::ToXmlConfig;
159/// use hedl_core::Document;
160///
161/// # #[tokio::main]
162/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
163/// let doc = Document::new((2, 0));
164/// let config = ToXmlConfig::default();
165/// to_xml_file_async(&doc, "output.xml", &config).await?;
166/// # Ok(())
167/// # }
168/// ```
169///
170/// # Errors
171///
172/// Returns an error if XML generation fails or if the file cannot be written.
173pub async fn to_xml_file_async(
174 doc: &Document,
175 path: impl AsRef<std::path::Path>,
176 config: &ToXmlConfig,
177) -> Result<(), String> {
178 let xml = to_xml(doc, config)?;
179
180 tokio::fs::write(path, xml)
181 .await
182 .map_err(|e| format!("Failed to write file: {}", e))?;
183
184 Ok(())
185}
186
187// ============================================================================
188// Async Reader/Writer Functions
189// ============================================================================
190
191/// Parse XML from an async reader
192///
193/// This function reads the entire content into memory before parsing. For streaming
194/// large files, use `from_xml_stream_async` instead.
195///
196/// # Examples
197///
198/// ```no_run
199/// use hedl_xml::async_api::from_xml_reader_async;
200/// use hedl_xml::FromXmlConfig;
201/// use tokio::fs::File;
202///
203/// # #[tokio::main]
204/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
205/// let file = File::open("data.xml").await?;
206/// let config = FromXmlConfig::default();
207/// let doc = from_xml_reader_async(file, &config).await?;
208/// # Ok(())
209/// # }
210/// ```
211///
212/// # Errors
213///
214/// Returns an error if reading fails or if XML parsing fails.
215pub async fn from_xml_reader_async<R: AsyncRead + Unpin>(
216 mut reader: R,
217 config: &FromXmlConfig,
218) -> Result<Document, String> {
219 let mut contents = String::new();
220 reader
221 .read_to_string(&mut contents)
222 .await
223 .map_err(|e| format!("Failed to read XML: {}", e))?;
224
225 from_xml(&contents, config)
226}
227
228/// Write XML to an async writer
229///
230/// # Examples
231///
232/// ```no_run
233/// use hedl_xml::async_api::to_xml_writer_async;
234/// use hedl_xml::ToXmlConfig;
235/// use hedl_core::Document;
236/// use tokio::fs::File;
237///
238/// # #[tokio::main]
239/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
240/// let doc = Document::new((2, 0));
241/// let file = File::create("output.xml").await?;
242/// let config = ToXmlConfig::default();
243/// to_xml_writer_async(&doc, file, &config).await?;
244/// # Ok(())
245/// # }
246/// ```
247///
248/// # Errors
249///
250/// Returns an error if XML generation fails or if writing fails.
251pub async fn to_xml_writer_async<W: AsyncWrite + Unpin>(
252 doc: &Document,
253 mut writer: W,
254 config: &ToXmlConfig,
255) -> Result<(), String> {
256 let xml = to_xml(doc, config)?;
257
258 writer
259 .write_all(xml.as_bytes())
260 .await
261 .map_err(|e| format!("Failed to write XML: {}", e))?;
262
263 writer
264 .flush()
265 .await
266 .map_err(|e| format!("Failed to flush writer: {}", e))?;
267
268 Ok(())
269}
270
271// ============================================================================
272// Async Streaming Functions
273// ============================================================================
274
275/// Create an async streaming XML parser
276///
277/// This function returns a stream that yields items incrementally, allowing
278/// processing of files larger than available RAM.
279///
280/// # Examples
281///
282/// ```no_run
283/// use hedl_xml::async_api::from_xml_stream_async;
284/// use hedl_xml::streaming::StreamConfig;
285/// use tokio::fs::File;
286///
287/// # #[tokio::main]
288/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
289/// let file = File::open("large.xml").await?;
290/// let config = StreamConfig::default();
291///
292/// let mut stream = from_xml_stream_async(file, &config).await?;
293///
294/// let mut count = 0;
295/// while let Some(result) = stream.next().await {
296/// match result {
297/// Ok(_item) => count += 1,
298/// Err(e) => eprintln!("Parse error: {}", e),
299/// }
300/// }
301/// println!("Processed {} items", count);
302/// # Ok(())
303/// # }
304/// ```
305///
306/// # Errors
307///
308/// Returns an error if the stream cannot be initialized.
309pub async fn from_xml_stream_async<R: AsyncRead + Unpin + Send + 'static>(
310 reader: R,
311 config: &StreamConfig,
312) -> Result<AsyncXmlStream<R>, String> {
313 AsyncXmlStream::new(reader, config.clone())
314}
315
316/// Async streaming XML parser
317///
318/// This stream yields `Result<StreamItem, String>` as items are parsed.
319/// It uses Tokio's async I/O for non-blocking operations.
320///
321/// # Implementation Note
322///
323/// This implementation reads all data into memory first, then parses it
324/// synchronously and yields items one at a time. For true streaming with
325/// backpressure, a more sophisticated implementation using quick-xml's
326/// async features would be needed.
327pub struct AsyncXmlStream<R: AsyncRead + Unpin> {
328 reader: R,
329 config: StreamConfig,
330 buffer: Vec<u8>,
331 /// Byte position in the stream
332 byte_position: usize,
333 chunk_size: usize,
334 /// Parsed items ready to be yielded
335 parsed_items: std::collections::VecDeque<Result<StreamItem, String>>,
336 /// Whether we've read and parsed all data
337 fully_parsed: bool,
338 /// Stream position tracking for progress reporting
339 stream_position: StreamPosition,
340}
341
342impl<R: AsyncRead + Unpin> AsyncXmlStream<R> {
343 /// Create a new async XML stream
344 pub fn new(reader: R, config: StreamConfig) -> Result<Self, String> {
345 let chunk_size = config.buffer_size;
346 Ok(AsyncXmlStream {
347 reader,
348 config,
349 buffer: Vec::new(),
350 byte_position: 0,
351 chunk_size,
352 parsed_items: std::collections::VecDeque::new(),
353 fully_parsed: false,
354 stream_position: StreamPosition::default(),
355 })
356 }
357
358 /// Get the current stream position for progress tracking
359 ///
360 /// Returns information about bytes processed and items parsed.
361 /// Useful for progress bars and error reporting.
362 #[inline]
363 pub fn position(&self) -> StreamPosition {
364 StreamPosition {
365 byte_offset: self.byte_position as u64,
366 items_parsed: self.stream_position.items_parsed,
367 }
368 }
369
370 /// Read the next chunk of data
371 async fn read_chunk(&mut self) -> Result<usize, String> {
372 let mut chunk = vec![0u8; self.chunk_size];
373 let n = self
374 .reader
375 .read(&mut chunk)
376 .await
377 .map_err(|e| format!("Failed to read chunk: {}", e))?;
378
379 if n > 0 {
380 self.buffer.extend_from_slice(&chunk[..n]);
381 self.byte_position += n;
382 }
383
384 Ok(n)
385 }
386
387 /// Read all data and parse into items
388 async fn ensure_parsed(&mut self) -> Result<(), String> {
389 if self.fully_parsed {
390 return Ok(());
391 }
392
393 // Read all remaining data
394 loop {
395 match self.read_chunk().await {
396 Ok(0) => break, // EOF
397 Ok(_) => continue,
398 Err(e) => return Err(e),
399 }
400 }
401
402 // Parse complete buffer using sync streaming parser
403 if !self.buffer.is_empty() {
404 use crate::streaming::from_xml_stream;
405 use std::io::Cursor;
406
407 let cursor = Cursor::new(&self.buffer);
408 match from_xml_stream(cursor, &self.config) {
409 Ok(parser) => {
410 // Collect all items from the parser
411 for result in parser {
412 self.parsed_items
413 .push_back(result.map_err(|e| e.to_string()));
414 }
415 }
416 Err(e) => {
417 self.parsed_items.push_back(Err(e));
418 }
419 }
420 }
421
422 self.fully_parsed = true;
423 Ok(())
424 }
425
426 /// Async version of next() - yields the next parsed item
427 pub async fn next(&mut self) -> Option<Result<StreamItem, String>> {
428 // Ensure we've read and parsed all data
429 if let Err(e) = self.ensure_parsed().await {
430 return Some(Err(e));
431 }
432
433 // Yield the next item from our queue
434 let result = self.parsed_items.pop_front();
435
436 // Update items_parsed counter on successful item
437 if result.as_ref().is_some_and(|r| r.is_ok()) {
438 self.stream_position.items_parsed += 1;
439 }
440
441 result
442 }
443}
444
445// Implement Stream trait for async iteration (requires futures crate)
446// For simplicity, we provide next() method instead of implementing Stream
447
448// ============================================================================
449// Utility Functions
450// ============================================================================
451
452/// Parse XML string asynchronously (runs on tokio threadpool)
453///
454/// This is useful for CPU-bound parsing that shouldn't block the async runtime.
455///
456/// # Examples
457///
458/// ```no_run
459/// use hedl_xml::async_api::from_xml_async;
460/// use hedl_xml::FromXmlConfig;
461///
462/// # #[tokio::main]
463/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
464/// let xml = r#"<?xml version="1.0"?><hedl><name>test</name></hedl>"#;
465/// let config = FromXmlConfig::default();
466/// let doc = from_xml_async(xml, &config).await?;
467/// # Ok(())
468/// # }
469/// ```
470pub async fn from_xml_async(xml: &str, config: &FromXmlConfig) -> Result<Document, String> {
471 let xml = xml.to_string();
472 let config = config.clone();
473
474 tokio::task::spawn_blocking(move || from_xml(&xml, &config))
475 .await
476 .map_err(|e| format!("Task join error: {}", e))?
477}
478
479/// Convert HEDL to XML string asynchronously (runs on tokio threadpool)
480///
481/// This is useful for CPU-bound conversion that shouldn't block the async runtime.
482///
483/// # Examples
484///
485/// ```no_run
486/// use hedl_xml::async_api::to_xml_async;
487/// use hedl_xml::ToXmlConfig;
488/// use hedl_core::Document;
489///
490/// # #[tokio::main]
491/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
492/// let doc = Document::new((2, 0));
493/// let config = ToXmlConfig::default();
494/// let xml = to_xml_async(&doc, &config).await?;
495/// # Ok(())
496/// # }
497/// ```
498pub async fn to_xml_async(doc: &Document, config: &ToXmlConfig) -> Result<String, String> {
499 let doc = doc.clone();
500 let config = config.clone();
501
502 tokio::task::spawn_blocking(move || to_xml(&doc, &config))
503 .await
504 .map_err(|e| format!("Task join error: {}", e))?
505}
506
507// ============================================================================
508// Batch Processing Functions
509// ============================================================================
510
511/// Process multiple XML files concurrently
512///
513/// Returns results in the same order as input paths.
514///
515/// # Examples
516///
517/// ```no_run
518/// use hedl_xml::async_api::from_xml_files_concurrent;
519/// use hedl_xml::FromXmlConfig;
520///
521/// # #[tokio::main]
522/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
523/// let paths = vec!["file1.xml", "file2.xml", "file3.xml"];
524/// let config = FromXmlConfig::default();
525///
526/// let results = from_xml_files_concurrent(&paths, &config, 4).await;
527///
528/// for (path, result) in paths.iter().zip(results.iter()) {
529/// match result {
530/// Ok(doc) => println!("{}: {} items", path, doc.root.len()),
531/// Err(e) => eprintln!("{}: error - {}", path, e),
532/// }
533/// }
534/// # Ok(())
535/// # }
536/// ```
537///
538/// # Arguments
539///
540/// * `paths` - Iterator of file paths to process
541/// * `config` - Configuration for XML parsing
542/// * `concurrency` - Maximum number of concurrent operations
543pub async fn from_xml_files_concurrent<'a, I, P>(
544 paths: I,
545 config: &FromXmlConfig,
546 concurrency: usize,
547) -> Vec<Result<Document, String>>
548where
549 I: IntoIterator<Item = P>,
550 P: AsRef<std::path::Path> + Send + 'a,
551{
552 use tokio::task::JoinSet;
553
554 let mut set: JoinSet<(usize, Result<Document, String>)> = JoinSet::new();
555 let config = config.clone();
556 let paths_vec: Vec<_> = paths.into_iter().collect();
557 let total = paths_vec.len();
558
559 // Pre-allocate results with placeholders
560 let mut results: Vec<Option<Result<Document, String>>> = vec![None; total];
561 let mut paths_iter = paths_vec.into_iter().enumerate();
562 let mut pending = 0;
563
564 // Fill initial batch up to concurrency limit
565 for _ in 0..concurrency {
566 if let Some((idx, path)) = paths_iter.next() {
567 let path = path.as_ref().to_path_buf();
568 let config = config.clone();
569 set.spawn(async move { (idx, from_xml_file_async(&path, &config).await) });
570 pending += 1;
571 } else {
572 break;
573 }
574 }
575
576 // Process remaining items, maintaining concurrency level
577 while pending > 0 {
578 if let Some(join_result) = set.join_next().await {
579 match join_result {
580 Ok((idx, doc_result)) => {
581 results[idx] = Some(doc_result);
582 }
583 Err(e) => {
584 // Task panicked - find first None slot and fill it
585 if let Some(slot) = results.iter_mut().find(|r| r.is_none()) {
586 *slot = Some(Err(format!("Task error: {}", e)));
587 }
588 }
589 }
590 pending -= 1;
591
592 // Add next item if available
593 if let Some((idx, path)) = paths_iter.next() {
594 let path = path.as_ref().to_path_buf();
595 let config = config.clone();
596 set.spawn(async move { (idx, from_xml_file_async(&path, &config).await) });
597 pending += 1;
598 }
599 }
600 }
601
602 // Convert to final results, unwrapping Options (all should be Some now)
603 results
604 .into_iter()
605 .map(|opt| opt.unwrap_or_else(|| Err("Missing result".to_string())))
606 .collect()
607}
608
609/// Write multiple documents to XML files concurrently
610///
611/// # Examples
612///
613/// ```no_run
614/// use hedl_xml::async_api::to_xml_files_concurrent;
615/// use hedl_xml::ToXmlConfig;
616/// use hedl_core::Document;
617///
618/// # #[tokio::main]
619/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
620/// let docs = vec![
621/// Document::new((2, 0)),
622/// Document::new((2, 0)),
623/// ];
624/// let paths = vec!["out1.xml", "out2.xml"];
625/// let config = ToXmlConfig::default();
626///
627/// let results = to_xml_files_concurrent(
628/// docs.iter().zip(paths.iter()),
629/// &config,
630/// 4
631/// ).await;
632///
633/// for (i, result) in results.iter().enumerate() {
634/// match result {
635/// Ok(_) => println!("File {} written successfully", i),
636/// Err(e) => eprintln!("File {} error: {}", i, e),
637/// }
638/// }
639/// # Ok(())
640/// # }
641/// ```
642pub async fn to_xml_files_concurrent<'a, I, P>(
643 docs_and_paths: I,
644 config: &ToXmlConfig,
645 concurrency: usize,
646) -> Vec<Result<(), String>>
647where
648 I: IntoIterator<Item = (&'a Document, P)>,
649 P: AsRef<std::path::Path> + Send + 'a,
650{
651 use tokio::task::JoinSet;
652
653 let mut set: JoinSet<(usize, Result<(), String>)> = JoinSet::new();
654 let config = config.clone();
655 let docs_and_paths_vec: Vec<_> = docs_and_paths.into_iter().collect();
656 let total = docs_and_paths_vec.len();
657
658 // Pre-allocate results with placeholders
659 let mut results: Vec<Option<Result<(), String>>> = vec![None; total];
660 let mut iter = docs_and_paths_vec.into_iter().enumerate();
661 let mut pending = 0;
662
663 // Fill initial batch up to concurrency limit
664 for _ in 0..concurrency {
665 if let Some((idx, (doc, path))) = iter.next() {
666 let doc = doc.clone();
667 let path = path.as_ref().to_path_buf();
668 let config = config.clone();
669 set.spawn(async move { (idx, to_xml_file_async(&doc, &path, &config).await) });
670 pending += 1;
671 } else {
672 break;
673 }
674 }
675
676 // Process remaining items, maintaining concurrency level
677 while pending > 0 {
678 if let Some(join_result) = set.join_next().await {
679 match join_result {
680 Ok((idx, write_result)) => {
681 results[idx] = Some(write_result);
682 }
683 Err(e) => {
684 // Task panicked - find first None slot and fill it
685 if let Some(slot) = results.iter_mut().find(|r| r.is_none()) {
686 *slot = Some(Err(format!("Task error: {}", e)));
687 }
688 }
689 }
690 pending -= 1;
691
692 // Add next item if available
693 if let Some((idx, (doc, path))) = iter.next() {
694 let doc = doc.clone();
695 let path = path.as_ref().to_path_buf();
696 let config = config.clone();
697 set.spawn(async move { (idx, to_xml_file_async(&doc, &path, &config).await) });
698 pending += 1;
699 }
700 }
701 }
702
703 // Convert to final results, unwrapping Options (all should be Some now)
704 results
705 .into_iter()
706 .map(|opt| opt.unwrap_or_else(|| Err("Missing result".to_string())))
707 .collect()
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use hedl_core::{Item, Value};
714 use std::io::Cursor;
715
716 // ==================== File I/O tests ====================
717
718 #[tokio::test]
719 async fn test_from_xml_file_async_not_found() {
720 let config = FromXmlConfig::default();
721 let result = from_xml_file_async("/nonexistent/file.xml", &config).await;
722 assert!(result.is_err());
723 assert!(result.unwrap_err().contains("Failed to read file"));
724 }
725
726 #[tokio::test]
727 async fn test_to_xml_file_async_invalid_path() {
728 let doc = Document::new((2, 0));
729 let config = ToXmlConfig::default();
730 let result = to_xml_file_async(&doc, "/invalid/\0/path.xml", &config).await;
731 assert!(result.is_err());
732 }
733
734 // ==================== Reader/Writer tests ====================
735
736 #[tokio::test]
737 async fn test_from_xml_reader_async_valid() {
738 let xml = r#"<?xml version="1.0"?><hedl><val>42</val></hedl>"#;
739 let cursor = Cursor::new(xml.as_bytes());
740 let config = FromXmlConfig::default();
741
742 let doc = from_xml_reader_async(cursor, &config).await.unwrap();
743 assert_eq!(
744 doc.root.get("val").and_then(|i| i.as_scalar()),
745 Some(&Value::Int(42))
746 );
747 }
748
749 #[tokio::test]
750 async fn test_from_xml_reader_async_empty() {
751 let xml = "";
752 let cursor = Cursor::new(xml.as_bytes());
753 let config = FromXmlConfig::default();
754
755 let doc = from_xml_reader_async(cursor, &config).await.unwrap();
756 assert!(doc.root.is_empty());
757 }
758
759 #[tokio::test]
760 async fn test_to_xml_writer_async_valid() {
761 let mut doc = Document::new((2, 0));
762 doc.root
763 .insert("val".to_string(), Item::Scalar(Value::Int(42)));
764
765 let mut buffer = Vec::new();
766 let config = ToXmlConfig::default();
767
768 to_xml_writer_async(&doc, &mut buffer, &config)
769 .await
770 .unwrap();
771
772 let xml = String::from_utf8(buffer).unwrap();
773 assert!(xml.contains("<val>42</val>"));
774 }
775
776 #[tokio::test]
777 async fn test_to_xml_writer_async_empty() {
778 let doc = Document::new((2, 0));
779 let mut buffer = Vec::new();
780 let config = ToXmlConfig::default();
781
782 to_xml_writer_async(&doc, &mut buffer, &config)
783 .await
784 .unwrap();
785
786 let xml = String::from_utf8(buffer).unwrap();
787 assert!(xml.contains("<?xml"));
788 assert!(xml.contains("<hedl"));
789 }
790
791 // ==================== Async string parsing tests ====================
792
793 #[tokio::test]
794 async fn test_from_xml_async_valid() {
795 let xml = r#"<?xml version="1.0"?><hedl><name>test</name></hedl>"#;
796 let config = FromXmlConfig::default();
797
798 let doc = from_xml_async(xml, &config).await.unwrap();
799 assert_eq!(
800 doc.root.get("name").and_then(|i| i.as_scalar()),
801 Some(&Value::String("test".to_string().into()))
802 );
803 }
804
805 #[tokio::test]
806 async fn test_from_xml_async_invalid() {
807 let xml = r#"</invalid>"#;
808 let config = FromXmlConfig::default();
809
810 let result = from_xml_async(xml, &config).await;
811 assert!(result.is_err());
812 }
813
814 #[tokio::test]
815 async fn test_to_xml_async_valid() {
816 let mut doc = Document::new((2, 0));
817 doc.root
818 .insert("val".to_string(), Item::Scalar(Value::Int(123)));
819
820 let config = ToXmlConfig::default();
821 let xml = to_xml_async(&doc, &config).await.unwrap();
822
823 assert!(xml.contains("<val>123</val>"));
824 }
825
826 #[tokio::test]
827 async fn test_to_xml_async_empty() {
828 let doc = Document::new((2, 0));
829 let config = ToXmlConfig::default();
830
831 let xml = to_xml_async(&doc, &config).await.unwrap();
832 assert!(xml.contains("<?xml"));
833 }
834
835 // ==================== Concurrent processing tests ====================
836
837 #[tokio::test]
838 async fn test_from_xml_files_concurrent_empty() {
839 let paths: Vec<&str> = vec![];
840 let config = FromXmlConfig::default();
841
842 let results = from_xml_files_concurrent(&paths, &config, 4).await;
843 assert!(results.is_empty());
844 }
845
846 #[tokio::test]
847 async fn test_to_xml_files_concurrent_empty() {
848 let docs_and_paths: Vec<(&Document, &str)> = vec![];
849 let config = ToXmlConfig::default();
850
851 let results = to_xml_files_concurrent(docs_and_paths, &config, 4).await;
852 assert!(results.is_empty());
853 }
854
855 // ==================== Edge cases ====================
856
857 #[tokio::test]
858 async fn test_from_xml_reader_async_unicode() {
859 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
860 <hedl><name>héllo 世界</name></hedl>"#;
861 let cursor = Cursor::new(xml.as_bytes());
862 let config = FromXmlConfig::default();
863
864 let doc = from_xml_reader_async(cursor, &config).await.unwrap();
865 assert_eq!(
866 doc.root.get("name").and_then(|i| i.as_scalar()),
867 Some(&Value::String("héllo 世界".to_string().into()))
868 );
869 }
870
871 #[tokio::test]
872 async fn test_from_xml_reader_async_large_value() {
873 let large_string = "x".repeat(10000);
874 let xml = format!(
875 r#"<?xml version="1.0"?><hedl><val>{}</val></hedl>"#,
876 large_string
877 );
878 let cursor = Cursor::new(xml.as_bytes());
879 let config = FromXmlConfig::default();
880
881 let doc = from_xml_reader_async(cursor, &config).await.unwrap();
882 assert_eq!(
883 doc.root.get("val").and_then(|i| i.as_scalar()),
884 Some(&Value::String(large_string.into()))
885 );
886 }
887
888 #[tokio::test]
889 async fn test_round_trip_async() {
890 let mut doc = Document::new((2, 0));
891 doc.root
892 .insert("bool_val".to_string(), Item::Scalar(Value::Bool(true)));
893 doc.root
894 .insert("int_val".to_string(), Item::Scalar(Value::Int(42)));
895 doc.root.insert(
896 "string_val".to_string(),
897 Item::Scalar(Value::String("hello".to_string().into())),
898 );
899
900 let config_to = ToXmlConfig::default();
901 let xml = to_xml_async(&doc, &config_to).await.unwrap();
902
903 let config_from = FromXmlConfig::default();
904 let doc2 = from_xml_async(&xml, &config_from).await.unwrap();
905
906 assert_eq!(
907 doc2.root.get("bool_val").and_then(|i| i.as_scalar()),
908 Some(&Value::Bool(true))
909 );
910 assert_eq!(
911 doc2.root.get("int_val").and_then(|i| i.as_scalar()),
912 Some(&Value::Int(42))
913 );
914 assert_eq!(
915 doc2.root.get("string_val").and_then(|i| i.as_scalar()),
916 Some(&Value::String("hello".to_string().into()))
917 );
918 }
919
920 // ==================== Concurrency and parallelism tests ====================
921
922 #[tokio::test]
923 async fn test_concurrent_parsing() {
924 let xml1 = r#"<?xml version="1.0"?><hedl><id>1</id></hedl>"#;
925 let xml2 = r#"<?xml version="1.0"?><hedl><id>2</id></hedl>"#;
926 let xml3 = r#"<?xml version="1.0"?><hedl><id>3</id></hedl>"#;
927
928 let config = FromXmlConfig::default();
929
930 let (r1, r2, r3) = tokio::join!(
931 from_xml_async(xml1, &config),
932 from_xml_async(xml2, &config),
933 from_xml_async(xml3, &config)
934 );
935
936 assert!(r1.is_ok());
937 assert!(r2.is_ok());
938 assert!(r3.is_ok());
939
940 assert_eq!(
941 r1.unwrap().root.get("id").and_then(|i| i.as_scalar()),
942 Some(&Value::Int(1))
943 );
944 assert_eq!(
945 r2.unwrap().root.get("id").and_then(|i| i.as_scalar()),
946 Some(&Value::Int(2))
947 );
948 assert_eq!(
949 r3.unwrap().root.get("id").and_then(|i| i.as_scalar()),
950 Some(&Value::Int(3))
951 );
952 }
953
954 #[tokio::test]
955 async fn test_concurrent_generation() {
956 let mut doc1 = Document::new((2, 0));
957 doc1.root
958 .insert("id".to_string(), Item::Scalar(Value::Int(1)));
959
960 let mut doc2 = Document::new((2, 0));
961 doc2.root
962 .insert("id".to_string(), Item::Scalar(Value::Int(2)));
963
964 let config = ToXmlConfig::default();
965
966 let (r1, r2) = tokio::join!(to_xml_async(&doc1, &config), to_xml_async(&doc2, &config));
967
968 assert!(r1.is_ok());
969 assert!(r2.is_ok());
970
971 assert!(r1.unwrap().contains("<id>1</id>"));
972 assert!(r2.unwrap().contains("<id>2</id>"));
973 }
974}