unity_asset_yaml/
yaml_document.rs1use 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#[derive(Debug)]
21pub struct YamlDocument {
22 data: Vec<UnityClass>,
24 metadata: DocumentMetadata,
26 newline: LineEnding,
28}
29
30impl YamlDocument {
31 pub fn new() -> Self {
33 Self {
34 data: Vec::new(),
35 metadata: DocumentMetadata::new(DocumentFormat::Yaml),
36 newline: LineEnding::default(),
37 }
38 }
39
40 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 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 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 let loader = SerdeUnityLoader::new();
78 let (unity_classes, warnings) = loader.load_from_reader_detailed(reader)?;
79
80 let mut yaml_doc = YamlDocument::new();
82 yaml_doc.metadata.file_path = Some(path.to_path_buf());
83
84 for unity_class in unity_classes {
86 yaml_doc.add_entry(unity_class);
87 }
88
89 Ok((yaml_doc, warnings))
90 }
91
92 #[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 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 let loader = SerdeUnityLoader::new();
141 let (unity_classes, warnings) = loader.load_from_async_reader_detailed(reader).await?;
142
143 let mut yaml_doc = YamlDocument::new();
145 yaml_doc.metadata.file_path = Some(path.to_path_buf());
146
147 for unity_class in unity_classes {
149 yaml_doc.add_entry(unity_class);
150 }
151
152 Ok((yaml_doc, warnings))
153 }
154
155 pub fn line_ending(&self) -> LineEnding {
157 self.newline
158 }
159
160 pub fn set_line_ending(&mut self, newline: LineEnding) {
162 self.newline = newline;
163 }
164
165 pub fn version(&self) -> Option<&str> {
167 self.metadata.version.as_deref()
168 }
169
170 pub fn yaml_metadata(&self) -> &std::collections::HashMap<String, String> {
172 &self.metadata.metadata
173 }
174
175 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 pub fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()> {
215 let path = path.as_ref();
216
217 let mut serializer = UnityYamlSerializer::new().with_line_ending(self.newline);
219
220 let yaml_content = serializer.serialize_to_string(&self.data)?;
222
223 fs::write(path, yaml_content).map_err(UnityAssetError::from)?;
225
226 Ok(())
227 }
228
229 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 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 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 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 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 let yaml_content = self.dump_yaml()?;
392
393 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#[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 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 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 let mut stream = doc.entries_stream();
509 assert!(stream.next().await.is_none());
510 }
511}