unity_asset_core/
document.rs

1//! Unity document abstraction
2//!
3//! This module provides abstract traits and types for Unity documents
4//! that can be implemented by different format-specific parsers.
5
6use crate::error::Result;
7use crate::unity_class::UnityClass;
8use std::path::{Path, PathBuf};
9
10#[cfg(feature = "async")]
11use async_trait::async_trait;
12#[cfg(feature = "async")]
13use futures::Stream;
14
15/// Supported Unity document formats
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DocumentFormat {
18    Yaml,
19    Binary,
20}
21
22/// Abstract trait for Unity documents
23pub trait UnityDocument {
24    /// Get the first entry (main object) in the document
25    fn entry(&self) -> Option<&UnityClass>;
26
27    /// Get a mutable reference to the first entry
28    fn entry_mut(&mut self) -> Option<&mut UnityClass>;
29
30    /// Get all entries in the document
31    fn entries(&self) -> &[UnityClass];
32
33    /// Get mutable access to all entries
34    fn entries_mut(&mut self) -> &mut Vec<UnityClass>;
35
36    /// Add a new Unity object to the document
37    fn add_entry(&mut self, entry: UnityClass);
38
39    /// Filter entries by class name
40    fn filter_by_class(&self, class_name: &str) -> Vec<&UnityClass> {
41        self.entries()
42            .iter()
43            .filter(|entry| entry.class_name == class_name)
44            .collect()
45    }
46
47    /// Filter entries by multiple class names
48    fn filter_by_classes(&self, class_names: &[&str]) -> Vec<&UnityClass> {
49        self.entries()
50            .iter()
51            .filter(|entry| class_names.contains(&entry.class_name.as_str()))
52            .collect()
53    }
54
55    /// Filter entries by a custom predicate
56    fn filter<F>(&self, predicate: F) -> Vec<&UnityClass>
57    where
58        F: Fn(&UnityClass) -> bool,
59    {
60        self.entries()
61            .iter()
62            .filter(|entry| predicate(entry))
63            .collect()
64    }
65
66    /// Find a single entry by class name and optional property filter
67    fn find_by_class_and_property(&self, class_name: &str, property: &str) -> Option<&UnityClass> {
68        self.entries()
69            .iter()
70            .find(|entry| entry.class_name == class_name && entry.has_property(property))
71    }
72
73    /// Get the file path this document was loaded from
74    fn file_path(&self) -> Option<&Path>;
75
76    /// Check if the document is empty
77    fn is_empty(&self) -> bool {
78        self.entries().is_empty()
79    }
80
81    /// Get the number of entries in the document
82    fn len(&self) -> usize {
83        self.entries().len()
84    }
85
86    /// Save the document back to its original file
87    fn save(&self) -> Result<()>;
88
89    /// Save the document to a specific file
90    fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()>;
91
92    /// Get the document format
93    fn format(&self) -> DocumentFormat;
94}
95
96/// Document metadata
97#[derive(Debug, Clone)]
98pub struct DocumentMetadata {
99    /// Path to the source file
100    pub file_path: Option<PathBuf>,
101    /// Document format
102    pub format: DocumentFormat,
103    /// Format-specific version information
104    pub version: Option<String>,
105    /// Format-specific tags or metadata
106    pub metadata: std::collections::HashMap<String, String>,
107}
108
109impl DocumentMetadata {
110    /// Create new metadata
111    pub fn new(format: DocumentFormat) -> Self {
112        Self {
113            file_path: None,
114            format,
115            version: None,
116            metadata: std::collections::HashMap::new(),
117        }
118    }
119
120    /// Set file path
121    pub fn with_file_path<P: AsRef<Path>>(mut self, path: P) -> Self {
122        self.file_path = Some(path.as_ref().to_path_buf());
123        self
124    }
125
126    /// Set version
127    pub fn with_version<S: Into<String>>(mut self, version: S) -> Self {
128        self.version = Some(version.into());
129        self
130    }
131
132    /// Add metadata entry
133    pub fn with_metadata<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
134        self.metadata.insert(key.into(), value.into());
135        self
136    }
137}
138
139/// Async version of UnityDocument trait for non-blocking I/O operations
140#[cfg(feature = "async")]
141#[async_trait]
142pub trait AsyncUnityDocument: Send + Sync {
143    /// Load document from file path asynchronously
144    async fn load_from_path_async<P: AsRef<Path> + Send>(path: P) -> Result<Self>
145    where
146        Self: Sized;
147
148    /// Save document to file path asynchronously
149    async fn save_to_path_async<P: AsRef<Path> + Send>(&self, path: P) -> Result<()>;
150
151    /// Get all entries in the document (sync access for already loaded data)
152    fn entries(&self) -> &[UnityClass];
153
154    /// Get the first entry (if any) (sync access)
155    fn entry(&self) -> Option<&UnityClass> {
156        self.entries().first()
157    }
158
159    /// Get document file path (sync access)
160    fn file_path(&self) -> Option<&Path>;
161
162    /// Stream entries for processing large documents without loading all into memory
163    fn entries_stream(&self) -> impl Stream<Item = &UnityClass> + Send {
164        futures::stream::iter(self.entries())
165    }
166
167    /// Process entries with an async function
168    async fn process_entries<F, Fut>(&self, mut processor: F) -> Result<()>
169    where
170        F: FnMut(&UnityClass) -> Fut + Send,
171        Fut: std::future::Future<Output = Result<()>> + Send,
172    {
173        for entry in self.entries() {
174            processor(entry).await?;
175        }
176        Ok(())
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::unity_class::UnityClass;
184
185    // Mock implementation for testing
186    struct MockDocument {
187        entries: Vec<UnityClass>,
188        metadata: DocumentMetadata,
189    }
190
191    impl UnityDocument for MockDocument {
192        fn entry(&self) -> Option<&UnityClass> {
193            self.entries.first()
194        }
195
196        fn entry_mut(&mut self) -> Option<&mut UnityClass> {
197            self.entries.first_mut()
198        }
199
200        fn entries(&self) -> &[UnityClass] {
201            &self.entries
202        }
203
204        fn entries_mut(&mut self) -> &mut Vec<UnityClass> {
205            &mut self.entries
206        }
207
208        fn add_entry(&mut self, entry: UnityClass) {
209            self.entries.push(entry);
210        }
211
212        fn file_path(&self) -> Option<&Path> {
213            self.metadata.file_path.as_deref()
214        }
215
216        fn save(&self) -> Result<()> {
217            Ok(()) // Mock implementation
218        }
219
220        fn save_to<P: AsRef<Path>>(&self, _path: P) -> Result<()> {
221            Ok(()) // Mock implementation
222        }
223
224        fn format(&self) -> DocumentFormat {
225            self.metadata.format
226        }
227    }
228
229    #[test]
230    fn test_document_trait() {
231        let mut doc = MockDocument {
232            entries: Vec::new(),
233            metadata: DocumentMetadata::new(DocumentFormat::Yaml),
234        };
235
236        assert!(doc.is_empty());
237        assert_eq!(doc.len(), 0);
238
239        let class = UnityClass::new(1, "GameObject".to_string(), "123".to_string());
240        doc.add_entry(class);
241
242        assert!(!doc.is_empty());
243        assert_eq!(doc.len(), 1);
244        assert_eq!(doc.format(), DocumentFormat::Yaml);
245    }
246
247    #[test]
248    fn test_document_filtering() {
249        let mut doc = MockDocument {
250            entries: Vec::new(),
251            metadata: DocumentMetadata::new(DocumentFormat::Yaml),
252        };
253
254        let game_object = UnityClass::new(1, "GameObject".to_string(), "123".to_string());
255        let behaviour = UnityClass::new(114, "MonoBehaviour".to_string(), "456".to_string());
256
257        doc.add_entry(game_object);
258        doc.add_entry(behaviour);
259
260        let game_objects = doc.filter_by_class("GameObject");
261        assert_eq!(game_objects.len(), 1);
262
263        let behaviours = doc.filter_by_class("MonoBehaviour");
264        assert_eq!(behaviours.len(), 1);
265    }
266}