unity_asset_yaml/
yaml_document.rs

1//! YAML-specific Unity document implementation
2//!
3//! This module provides the concrete implementation of UnityDocument
4//! for YAML format files.
5
6use crate::unity_yaml_serializer::UnityYamlSerializer;
7use std::fs;
8use std::path::Path;
9use unity_asset_core::{
10    DocumentFormat, LineEnding, Result, UnityAssetError, UnityClass, UnityDocument,
11    document::DocumentMetadata,
12};
13
14#[cfg(feature = "async")]
15use async_trait::async_trait;
16#[cfg(feature = "async")]
17use unity_asset_core::document::AsyncUnityDocument;
18
19/// A Unity YAML document containing one or more Unity objects
20#[derive(Debug)]
21pub struct YamlDocument {
22    /// The Unity objects in this document
23    data: Vec<UnityClass>,
24    /// Document metadata
25    metadata: DocumentMetadata,
26    /// Line ending style used in the original file
27    newline: LineEnding,
28}
29
30impl YamlDocument {
31    /// Create a new empty YAML document
32    pub fn new() -> Self {
33        Self {
34            data: Vec::new(),
35            metadata: DocumentMetadata::new(DocumentFormat::Yaml),
36            newline: LineEnding::default(),
37        }
38    }
39
40    /// Load a Unity YAML file
41    ///
42    /// # Arguments
43    ///
44    /// * `path` - Path to the YAML file to load
45    /// * `preserve_types` - If true, try to preserve int/float types instead of converting all to strings
46    ///
47    /// # Examples
48    ///
49    /// ```rust,no_run
50    /// use unity_asset_yaml::YamlDocument;
51    ///
52    /// let doc = YamlDocument::load_yaml("ProjectSettings.asset", false)?;
53    /// # Ok::<(), unity_asset_core::UnityAssetError>(())
54    /// ```
55    pub fn load_yaml<P: AsRef<Path>>(path: P, _preserve_types: bool) -> Result<Self> {
56        Ok(Self::load_yaml_with_warnings(path, _preserve_types)?.0)
57    }
58
59    /// Load a Unity YAML file and return non-fatal conversion warnings.
60    pub fn load_yaml_with_warnings<P: AsRef<Path>>(
61        path: P,
62        _preserve_types: bool,
63    ) -> Result<(Self, Vec<crate::serde_unity_loader::SerdeUnityWarning>)> {
64        use crate::serde_unity_loader::SerdeUnityLoader;
65        use std::fs::File;
66        use std::io::BufReader;
67
68        let path = path.as_ref();
69
70        // Read the file
71        let file = File::open(path).map_err(|e| {
72            UnityAssetError::format(format!("Failed to open file {}: {}", path.display(), e))
73        })?;
74        let reader = BufReader::new(file);
75
76        // Use serde-based loader
77        let loader = SerdeUnityLoader::new();
78        let (unity_classes, warnings) = loader.load_from_reader_detailed(reader)?;
79
80        // Create YamlDocument with metadata
81        let mut yaml_doc = YamlDocument::new();
82        yaml_doc.metadata.file_path = Some(path.to_path_buf());
83
84        // Add all loaded classes
85        for unity_class in unity_classes {
86            yaml_doc.add_entry(unity_class);
87        }
88
89        Ok((yaml_doc, warnings))
90    }
91
92    /// Load a Unity YAML file asynchronously
93    ///
94    /// # Arguments
95    ///
96    /// * `path` - Path to the YAML file to load
97    /// * `preserve_types` - If true, try to preserve int/float types instead of converting all to strings
98    ///
99    /// # Examples
100    ///
101    /// ```rust,no_run
102    /// # #[cfg(feature = "async")]
103    /// # {
104    /// use unity_asset_yaml::YamlDocument;
105    ///
106    /// # tokio_test::block_on(async {
107    /// let doc = YamlDocument::load_yaml_async("ProjectSettings.asset", false).await?;
108    /// # Ok::<(), unity_asset_core::UnityAssetError>(())
109    /// # }).unwrap();
110    /// # }
111    /// ```
112    #[cfg(feature = "async")]
113    pub async fn load_yaml_async<P: AsRef<Path> + Send>(
114        path: P,
115        _preserve_types: bool,
116    ) -> Result<Self> {
117        Ok(Self::load_yaml_async_with_warnings(path, _preserve_types)
118            .await?
119            .0)
120    }
121
122    #[cfg(feature = "async")]
123    pub async fn load_yaml_async_with_warnings<P: AsRef<Path> + Send>(
124        path: P,
125        _preserve_types: bool,
126    ) -> Result<(Self, Vec<crate::serde_unity_loader::SerdeUnityWarning>)> {
127        use crate::serde_unity_loader::SerdeUnityLoader;
128        use tokio::fs::File;
129        use tokio::io::BufReader;
130
131        let path = path.as_ref();
132
133        // Read the file asynchronously
134        let file = File::open(path).await.map_err(|e| {
135            UnityAssetError::format(format!("Failed to open file {}: {}", path.display(), e))
136        })?;
137        let reader = BufReader::new(file);
138
139        // Use serde-based loader (we'll need to make this async too)
140        let loader = SerdeUnityLoader::new();
141        let (unity_classes, warnings) = loader.load_from_async_reader_detailed(reader).await?;
142
143        // Create YamlDocument with metadata
144        let mut yaml_doc = YamlDocument::new();
145        yaml_doc.metadata.file_path = Some(path.to_path_buf());
146
147        // Add all loaded classes
148        for unity_class in unity_classes {
149            yaml_doc.add_entry(unity_class);
150        }
151
152        Ok((yaml_doc, warnings))
153    }
154
155    /// Get the line ending style
156    pub fn line_ending(&self) -> LineEnding {
157        self.newline
158    }
159
160    /// Set the line ending style
161    pub fn set_line_ending(&mut self, newline: LineEnding) {
162        self.newline = newline;
163    }
164
165    /// Get the YAML version
166    pub fn version(&self) -> Option<&str> {
167        self.metadata.version.as_deref()
168    }
169
170    /// Get the YAML metadata
171    pub fn yaml_metadata(&self) -> &std::collections::HashMap<String, String> {
172        &self.metadata.metadata
173    }
174
175    /// Save document to its original file
176    ///
177    /// This method saves the document back to the file it was loaded from.
178    /// If the document was not loaded from a file, this will return an error.
179    ///
180    /// # Examples
181    ///
182    /// ```rust,no_run
183    /// use unity_asset_yaml::YamlDocument;
184    ///
185    /// let mut doc = YamlDocument::load_yaml("ProjectSettings.asset", false)?;
186    /// // ... modify the document ...
187    /// doc.save()?;  // Save back to original file
188    /// # Ok::<(), unity_asset_core::UnityAssetError>(())
189    /// ```
190    pub fn save(&self) -> Result<()> {
191        if let Some(path) = &self.metadata.file_path {
192            self.save_to(path)
193        } else {
194            Err(UnityAssetError::format(
195                "Cannot save document: no file path available. Use save_to() instead.".to_string(),
196            ))
197        }
198    }
199
200    /// Save document to a specific file
201    ///
202    /// This method serializes the document to Unity YAML format and saves it
203    /// to the specified file path.
204    ///
205    /// # Examples
206    ///
207    /// ```rust,no_run
208    /// use unity_asset_yaml::YamlDocument;
209    ///
210    /// let doc = YamlDocument::load_yaml("ProjectSettings.asset", false)?;
211    /// doc.save_to("ProjectSettings_backup.asset")?;
212    /// # Ok::<(), unity_asset_core::UnityAssetError>(())
213    /// ```
214    pub fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()> {
215        let path = path.as_ref();
216
217        // Create serializer with document settings
218        let mut serializer = UnityYamlSerializer::new().with_line_ending(self.newline);
219
220        // Serialize to string
221        let yaml_content = serializer.serialize_to_string(&self.data)?;
222
223        // Write to file
224        fs::write(path, yaml_content).map_err(UnityAssetError::from)?;
225
226        Ok(())
227    }
228
229    /// Get YAML content as string
230    ///
231    /// This method serializes the document to Unity YAML format and returns
232    /// it as a string without writing to a file.
233    ///
234    /// # Examples
235    ///
236    /// ```rust,no_run
237    /// use unity_asset_yaml::YamlDocument;
238    ///
239    /// let doc = YamlDocument::load_yaml("ProjectSettings.asset", false)?;
240    /// let yaml_string = doc.dump_yaml()?;
241    /// println!("{}", yaml_string);
242    /// # Ok::<(), unity_asset_core::UnityAssetError>(())
243    /// ```
244    pub fn dump_yaml(&self) -> Result<String> {
245        let mut serializer = UnityYamlSerializer::new().with_line_ending(self.newline);
246
247        serializer.serialize_to_string(&self.data)
248    }
249
250    /// Filter entries by class names and/or attributes
251    ///
252    /// This method provides advanced filtering capabilities similar to the
253    /// Python reference library's filter() method.
254    ///
255    /// # Arguments
256    ///
257    /// * `class_names` - Optional list of class names to filter by
258    /// * `attributes` - Optional list of attribute names that entries must have
259    ///
260    /// # Examples
261    ///
262    /// ```rust,no_run
263    /// use unity_asset_yaml::YamlDocument;
264    ///
265    /// let doc = YamlDocument::load_yaml("scene.unity", false)?;
266    ///
267    /// // Find all GameObjects
268    /// let gameobjects = doc.filter(Some(&["GameObject"]), None);
269    ///
270    /// // Find all objects with m_Enabled property
271    /// let enabled_objects = doc.filter(None, Some(&["m_Enabled"]));
272    ///
273    /// // Find MonoBehaviours with m_Script property
274    /// let scripts = doc.filter(Some(&["MonoBehaviour"]), Some(&["m_Script"]));
275    /// # Ok::<(), unity_asset_core::UnityAssetError>(())
276    /// ```
277    pub fn filter(
278        &self,
279        class_names: Option<&[&str]>,
280        attributes: Option<&[&str]>,
281    ) -> Vec<&UnityClass> {
282        self.data
283            .iter()
284            .filter(|entry| {
285                // Check class name filter
286                if let Some(names) = class_names
287                    && !names.is_empty()
288                    && !names.contains(&entry.class_name.as_str())
289                {
290                    return false;
291                }
292
293                // Check attribute filter
294                if let Some(attrs) = attributes
295                    && !attrs.is_empty()
296                {
297                    for attr in attrs {
298                        if !entry.has_property(attr) {
299                            return false;
300                        }
301                    }
302                }
303
304                true
305            })
306            .collect()
307    }
308
309    /// Get a single entry by class name and/or attributes
310    ///
311    /// This method returns the first entry that matches the criteria.
312    /// Returns an error if no matching entry is found or if multiple entries match.
313    ///
314    /// # Arguments
315    ///
316    /// * `class_name` - Optional class name to match
317    /// * `attributes` - Optional list of attribute names that the entry must have
318    ///
319    /// # Examples
320    ///
321    /// ```rust,no_run
322    /// use unity_asset_yaml::YamlDocument;
323    ///
324    /// let doc = YamlDocument::load_yaml("scene.unity", false)?;
325    ///
326    /// // Get the first GameObject
327    /// let gameobject = doc.get(Some("GameObject"), None)?;
328    ///
329    /// // Get an object with specific attributes
330    /// let script = doc.get(Some("MonoBehaviour"), Some(&["m_Script", "m_Enabled"]))?;
331    /// # Ok::<(), unity_asset_core::UnityAssetError>(())
332    /// ```
333    pub fn get(
334        &self,
335        class_name: Option<&str>,
336        attributes: Option<&[&str]>,
337    ) -> Result<&UnityClass> {
338        let class_names = class_name.map(|name| vec![name]);
339        let filtered = self.filter(class_names.as_deref(), attributes);
340
341        match filtered.len() {
342            0 => Err(UnityAssetError::format(format!(
343                "No entry found matching criteria: class_name={:?}, attributes={:?}",
344                class_name, attributes
345            ))),
346            1 => Ok(filtered[0]),
347            n => Err(UnityAssetError::format(format!(
348                "Multiple entries ({}) found matching criteria: class_name={:?}, attributes={:?}. Use filter() instead.",
349                n, class_name, attributes
350            ))),
351        }
352    }
353}
354
355impl UnityDocument for YamlDocument {
356    fn entry(&self) -> Option<&UnityClass> {
357        self.data.first()
358    }
359
360    fn entry_mut(&mut self) -> Option<&mut UnityClass> {
361        self.data.first_mut()
362    }
363
364    fn entries(&self) -> &[UnityClass] {
365        &self.data
366    }
367
368    fn entries_mut(&mut self) -> &mut Vec<UnityClass> {
369        &mut self.data
370    }
371
372    fn add_entry(&mut self, entry: UnityClass) {
373        self.data.push(entry);
374    }
375
376    fn file_path(&self) -> Option<&Path> {
377        self.metadata.file_path.as_deref()
378    }
379
380    fn save(&self) -> Result<()> {
381        match &self.metadata.file_path {
382            Some(path) => self.save_to(path),
383            None => Err(UnityAssetError::format("No file path specified for save")),
384        }
385    }
386
387    fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()> {
388        let path = path.as_ref();
389
390        // Serialize the document to YAML format
391        let yaml_content = self.dump_yaml()?;
392
393        // Write to file
394        std::fs::write(path, yaml_content)
395            .map_err(|e| UnityAssetError::format(format!("Failed to write YAML file: {}", e)))?;
396
397        Ok(())
398    }
399
400    fn format(&self) -> DocumentFormat {
401        DocumentFormat::Yaml
402    }
403}
404
405impl Default for YamlDocument {
406    fn default() -> Self {
407        Self::new()
408    }
409}
410
411/// Async implementation of UnityDocument trait for YamlDocument
412#[cfg(feature = "async")]
413#[async_trait]
414impl AsyncUnityDocument for YamlDocument {
415    async fn load_from_path_async<P: AsRef<Path> + Send>(path: P) -> Result<Self>
416    where
417        Self: Sized,
418    {
419        Self::load_yaml_async(path, false).await
420    }
421
422    async fn save_to_path_async<P: AsRef<Path> + Send>(&self, path: P) -> Result<()> {
423        // For now, use the sync version wrapped in spawn_blocking
424        let content = self.dump_yaml()?;
425        let path = path.as_ref().to_path_buf();
426
427        tokio::task::spawn_blocking(move || {
428            std::fs::write(&path, content).map_err(|e| {
429                UnityAssetError::format(format!("Failed to write file {}: {}", path.display(), e))
430            })
431        })
432        .await
433        .map_err(|e| UnityAssetError::format(format!("Task join error: {}", e)))??;
434
435        Ok(())
436    }
437
438    fn entries(&self) -> &[UnityClass] {
439        &self.data
440    }
441
442    fn file_path(&self) -> Option<&Path> {
443        self.metadata.file_path.as_deref()
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use unity_asset_core::UnityClass;
451
452    #[test]
453    fn test_yaml_document_creation() {
454        let doc = YamlDocument::new();
455        assert!(doc.is_empty());
456        assert_eq!(doc.len(), 0);
457        assert_eq!(doc.format(), DocumentFormat::Yaml);
458    }
459
460    #[test]
461    fn test_yaml_document_add_entry() {
462        let mut doc = YamlDocument::new();
463        let class = UnityClass::new(1, "GameObject".to_string(), "123".to_string());
464
465        doc.add_entry(class);
466        assert_eq!(doc.len(), 1);
467        assert!(!doc.is_empty());
468    }
469
470    #[test]
471    fn test_yaml_document_filter() {
472        let mut doc = YamlDocument::new();
473
474        let class1 = UnityClass::new(1, "GameObject".to_string(), "123".to_string());
475        let class2 = UnityClass::new(114, "MonoBehaviour".to_string(), "456".to_string());
476
477        doc.add_entry(class1);
478        doc.add_entry(class2);
479
480        let game_objects = doc.filter_by_class("GameObject");
481        assert_eq!(game_objects.len(), 1);
482
483        let behaviours = doc.filter_by_class("MonoBehaviour");
484        assert_eq!(behaviours.len(), 1);
485    }
486
487    #[test]
488    fn test_yaml_document_metadata() {
489        let doc = YamlDocument::new();
490        assert_eq!(doc.format(), DocumentFormat::Yaml);
491        assert_eq!(doc.line_ending(), LineEnding::default());
492        assert!(doc.version().is_none());
493    }
494
495    #[cfg(feature = "async")]
496    #[tokio::test]
497    async fn test_async_yaml_document_creation() {
498        use futures::StreamExt;
499        use unity_asset_core::document::AsyncUnityDocument;
500
501        // Test that the async trait methods compile and work
502        let doc = YamlDocument::new();
503        assert!(AsyncUnityDocument::entries(&doc).is_empty());
504        assert!(AsyncUnityDocument::entry(&doc).is_none());
505        assert!(AsyncUnityDocument::file_path(&doc).is_none());
506
507        // Test stream functionality
508        let mut stream = doc.entries_stream();
509        assert!(stream.next().await.is_none());
510    }
511}