1use crate::constants::{LineEnding, UNITY_TAG_URI, UNITY_YAML_VERSION};
11use std::fmt::Write;
12use unity_asset_core::{Result, UnityAssetError, UnityClass, UnityValue};
13
14pub struct UnityYamlSerializer {
16 line_ending: LineEnding,
18 indent_size: usize,
20 indent_level: usize,
22 first_document: bool,
24}
25
26impl UnityYamlSerializer {
27 pub fn new() -> Self {
29 Self {
30 line_ending: LineEnding::default(),
31 indent_size: 2,
32 indent_level: 0,
33 first_document: true,
34 }
35 }
36
37 pub fn with_line_ending(mut self, line_ending: LineEnding) -> Self {
39 self.line_ending = line_ending;
40 self
41 }
42
43 pub fn serialize_to_string(&mut self, classes: &[UnityClass]) -> Result<String> {
45 let mut output = String::new();
46 self.serialize_to_writer(&mut output, classes)?;
47 Ok(output)
48 }
49
50 pub fn serialize_to_writer<W: Write>(
52 &mut self,
53 writer: &mut W,
54 classes: &[UnityClass],
55 ) -> Result<()> {
56 self.first_document = true;
57
58 if !classes.is_empty() {
60 self.write_yaml_header(writer)?;
61 }
62
63 for (index, class) in classes.iter().enumerate() {
65 if index > 0 {
66 self.first_document = false;
67 }
68 self.serialize_unity_class(writer, class)?;
69 }
70
71 Ok(())
72 }
73
74 fn write_yaml_header<W: Write>(&self, writer: &mut W) -> Result<()> {
76 write!(
78 writer,
79 "%YAML {}.{}{}",
80 UNITY_YAML_VERSION.0,
81 UNITY_YAML_VERSION.1,
82 self.line_ending.as_str()
83 )
84 .map_err(|e| UnityAssetError::format(format!("Failed to write YAML version: {}", e)))?;
85
86 write!(
88 writer,
89 "%TAG !u! {}{}",
90 UNITY_TAG_URI,
91 self.line_ending.as_str()
92 )
93 .map_err(|e| UnityAssetError::format(format!("Failed to write Unity tag: {}", e)))?;
94
95 Ok(())
96 }
97
98 fn serialize_unity_class<W: Write>(
100 &mut self,
101 writer: &mut W,
102 class: &UnityClass,
103 ) -> Result<()> {
104 write!(writer, "--- !u!{} &{}", class.class_id, class.anchor).map_err(|e| {
106 UnityAssetError::format(format!("Failed to write document header: {}", e))
107 })?;
108
109 if !class.extra_anchor_data.is_empty() {
111 write!(writer, " {}", class.extra_anchor_data).map_err(|e| {
112 UnityAssetError::format(format!("Failed to write extra anchor data: {}", e))
113 })?;
114 }
115
116 write!(writer, "{}", self.line_ending.as_str())
117 .map_err(|e| UnityAssetError::format(format!("Failed to write line ending: {}", e)))?;
118
119 write!(writer, "{}:{}", class.class_name, self.line_ending.as_str())
121 .map_err(|e| UnityAssetError::format(format!("Failed to write class name: {}", e)))?;
122
123 self.indent_level = 1;
125 for (key, value) in class.properties() {
126 self.serialize_property(writer, key, value)?;
127 }
128
129 Ok(())
130 }
131
132 fn serialize_property<W: Write>(
134 &mut self,
135 writer: &mut W,
136 key: &str,
137 value: &UnityValue,
138 ) -> Result<()> {
139 self.write_indent(writer)?;
141
142 write!(writer, "{}: ", key)
144 .map_err(|e| UnityAssetError::format(format!("Failed to write property key: {}", e)))?;
145
146 self.serialize_value(writer, value, false)?;
148
149 Ok(())
150 }
151
152 fn serialize_value<W: Write>(
154 &mut self,
155 writer: &mut W,
156 value: &UnityValue,
157 inline: bool,
158 ) -> Result<()> {
159 match value {
160 UnityValue::Null => {
161 write!(writer, "{{fileID: 0}}{}", self.line_ending.as_str()).map_err(|e| {
162 UnityAssetError::format(format!("Failed to write null value: {}", e))
163 })?;
164 }
165 UnityValue::Bool(b) => {
166 write!(
167 writer,
168 "{}{}",
169 if *b { "1" } else { "0" },
170 self.line_ending.as_str()
171 )
172 .map_err(|e| {
173 UnityAssetError::format(format!("Failed to write bool value: {}", e))
174 })?;
175 }
176 UnityValue::Integer(i) => {
177 write!(writer, "{}{}", i, self.line_ending.as_str()).map_err(|e| {
178 UnityAssetError::format(format!("Failed to write integer value: {}", e))
179 })?;
180 }
181 UnityValue::Float(f) => {
182 write!(writer, "{}{}", f, self.line_ending.as_str()).map_err(|e| {
183 UnityAssetError::format(format!("Failed to write float value: {}", e))
184 })?;
185 }
186 UnityValue::String(s) => {
187 if self.needs_quoting(s) {
189 write!(
190 writer,
191 "\"{}\"{}",
192 self.escape_string(s),
193 self.line_ending.as_str()
194 )
195 } else {
196 write!(writer, "{}{}", s, self.line_ending.as_str())
197 }
198 .map_err(|e| {
199 UnityAssetError::format(format!("Failed to write string value: {}", e))
200 })?;
201 }
202 UnityValue::Array(arr) => {
203 if arr.is_empty() {
204 write!(writer, "[]{}", self.line_ending.as_str()).map_err(|e| {
205 UnityAssetError::format(format!("Failed to write empty array: {}", e))
206 })?;
207 } else if inline || self.is_simple_array(arr) {
208 write!(writer, "[").map_err(|e| {
210 UnityAssetError::format(format!("Failed to write array start: {}", e))
211 })?;
212 for (i, item) in arr.iter().enumerate() {
213 if i > 0 {
214 write!(writer, ", ").map_err(|e| {
215 UnityAssetError::format(format!(
216 "Failed to write array separator: {}",
217 e
218 ))
219 })?;
220 }
221 self.serialize_value_inline(writer, item)?;
222 }
223 write!(writer, "]{}", self.line_ending.as_str()).map_err(|e| {
224 UnityAssetError::format(format!("Failed to write inline array end: {}", e))
225 })?;
226 } else {
227 write!(writer, "{}", self.line_ending.as_str()).map_err(|e| {
229 UnityAssetError::format(format!("Failed to write array start: {}", e))
230 })?;
231 self.indent_level += 1;
232 for item in arr {
233 self.write_indent(writer)?;
234 write!(writer, "- ").map_err(|e| {
235 UnityAssetError::format(format!(
236 "Failed to write array item prefix: {}",
237 e
238 ))
239 })?;
240 self.serialize_value(writer, item, true)?;
241 }
242 self.indent_level -= 1;
243 }
244 }
245 UnityValue::Bytes(b) => {
246 if b.is_empty() {
247 write!(writer, "[]{}", self.line_ending.as_str()).map_err(|e| {
248 UnityAssetError::format(format!("Failed to write empty bytes: {}", e))
249 })?;
250 } else if inline || b.len() <= 64 {
251 write!(writer, "[").map_err(|e| {
252 UnityAssetError::format(format!("Failed to write bytes start: {}", e))
253 })?;
254 for (i, item) in b.iter().enumerate() {
255 if i > 0 {
256 write!(writer, ", ").map_err(|e| {
257 UnityAssetError::format(format!(
258 "Failed to write bytes separator: {}",
259 e
260 ))
261 })?;
262 }
263 write!(writer, "{}", item).map_err(|e| {
264 UnityAssetError::format(format!("Failed to write byte value: {}", e))
265 })?;
266 }
267 write!(writer, "]{}", self.line_ending.as_str()).map_err(|e| {
268 UnityAssetError::format(format!("Failed to write bytes end: {}", e))
269 })?;
270 } else {
271 write!(writer, "{}", self.line_ending.as_str()).map_err(|e| {
272 UnityAssetError::format(format!("Failed to write bytes start: {}", e))
273 })?;
274 self.indent_level += 1;
275 for item in b {
276 self.write_indent(writer)?;
277 write!(writer, "- {}", item).map_err(|e| {
278 UnityAssetError::format(format!(
279 "Failed to write bytes item prefix: {}",
280 e
281 ))
282 })?;
283 write!(writer, "{}", self.line_ending.as_str()).map_err(|e| {
284 UnityAssetError::format(format!(
285 "Failed to write bytes line ending: {}",
286 e
287 ))
288 })?;
289 }
290 self.indent_level -= 1;
291 }
292 }
293 UnityValue::Object(obj) => {
294 if obj.is_empty() {
295 write!(writer, "{{}}{}", self.line_ending.as_str()).map_err(|e| {
296 UnityAssetError::format(format!("Failed to write empty object: {}", e))
297 })?;
298 } else if inline || self.is_simple_object(obj) {
299 write!(writer, "{{").map_err(|e| {
301 UnityAssetError::format(format!("Failed to write object start: {}", e))
302 })?;
303 for (i, (key, value)) in obj.iter().enumerate() {
304 if i > 0 {
305 write!(writer, ", ").map_err(|e| {
306 UnityAssetError::format(format!(
307 "Failed to write object separator: {}",
308 e
309 ))
310 })?;
311 }
312 write!(writer, "{}: ", key).map_err(|e| {
313 UnityAssetError::format(format!("Failed to write object key: {}", e))
314 })?;
315 self.serialize_value_inline(writer, value)?;
316 }
317 write!(writer, "}}{}", self.line_ending.as_str()).map_err(|e| {
318 UnityAssetError::format(format!("Failed to write inline object end: {}", e))
319 })?;
320 } else {
321 write!(writer, "{}", self.line_ending.as_str()).map_err(|e| {
323 UnityAssetError::format(format!("Failed to write object start: {}", e))
324 })?;
325 self.indent_level += 1;
326 for (key, value) in obj {
327 self.serialize_property(writer, key, value)?;
328 }
329 self.indent_level -= 1;
330 }
331 }
332 }
333 Ok(())
334 }
335
336 fn serialize_value_inline<W: Write>(&self, writer: &mut W, value: &UnityValue) -> Result<()> {
338 match value {
339 UnityValue::Null => {
340 write!(writer, "{{fileID: 0}}").map_err(|e| {
341 UnityAssetError::format(format!("Failed to write null value: {}", e))
342 })?;
343 }
344 UnityValue::Bool(b) => {
345 write!(writer, "{}", if *b { "1" } else { "0" }).map_err(|e| {
346 UnityAssetError::format(format!("Failed to write bool value: {}", e))
347 })?;
348 }
349 UnityValue::Integer(i) => {
350 write!(writer, "{}", i).map_err(|e| {
351 UnityAssetError::format(format!("Failed to write integer value: {}", e))
352 })?;
353 }
354 UnityValue::Float(f) => {
355 write!(writer, "{}", f).map_err(|e| {
356 UnityAssetError::format(format!("Failed to write float value: {}", e))
357 })?;
358 }
359 UnityValue::String(s) => {
360 if self.needs_quoting(s) {
361 write!(writer, "\"{}\"", self.escape_string(s)).map_err(|e| {
362 UnityAssetError::format(format!("Failed to write quoted string: {}", e))
363 })?;
364 } else {
365 write!(writer, "{}", s).map_err(|e| {
366 UnityAssetError::format(format!("Failed to write string: {}", e))
367 })?;
368 }
369 }
370 UnityValue::Bytes(b) => {
371 write!(writer, "<bytes len={}>", b.len()).map_err(|e| {
372 UnityAssetError::format(format!("Failed to write bytes: {}", e))
373 })?;
374 }
375 UnityValue::Array(_) | UnityValue::Object(_) => {
376 write!(writer, "{{...}}").map_err(|e| {
378 UnityAssetError::format(format!("Failed to write complex value: {}", e))
379 })?;
380 }
381 }
382 Ok(())
383 }
384
385 fn write_indent<W: Write>(&self, writer: &mut W) -> Result<()> {
387 for _ in 0..(self.indent_level * self.indent_size) {
388 write!(writer, " ").map_err(|e| {
389 UnityAssetError::format(format!("Failed to write indentation: {}", e))
390 })?;
391 }
392 Ok(())
393 }
394
395 fn needs_quoting(&self, s: &str) -> bool {
397 s.is_empty()
398 || s.contains('\n')
399 || s.contains('\r')
400 || s.contains('"')
401 || s.contains('\'')
402 || s.contains(':')
403 || s.contains('[')
404 || s.contains(']')
405 || s.contains('{')
406 || s.contains('}')
407 || s.starts_with(' ')
408 || s.ends_with(' ')
409 }
410
411 fn escape_string(&self, s: &str) -> String {
413 s.replace('\\', "\\\\")
414 .replace('"', "\\\"")
415 .replace('\n', "\\n")
416 .replace('\r', "\\r")
417 .replace('\t', "\\t")
418 }
419
420 fn is_simple_array(&self, arr: &[UnityValue]) -> bool {
422 arr.len() <= 3
423 && arr.iter().all(|v| match v {
424 UnityValue::Integer(_) | UnityValue::Float(_) | UnityValue::Bool(_) => true,
425 UnityValue::String(s) => s.len() < 20,
426 _ => false,
427 })
428 }
429
430 fn is_simple_object(&self, obj: &indexmap::IndexMap<String, UnityValue>) -> bool {
432 obj.len() <= 3
433 && obj.values().all(|v| match v {
434 UnityValue::Integer(_) | UnityValue::Float(_) | UnityValue::Bool(_) => true,
435 UnityValue::String(s) => s.len() < 20,
436 _ => false,
437 })
438 }
439}
440
441impl Default for UnityYamlSerializer {
442 fn default() -> Self {
443 Self::new()
444 }
445}