Skip to main content

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}