1#![cfg_attr(not(test), warn(missing_docs))]
19
20mod anchors;
170pub mod error;
172pub mod from_yaml;
174mod to_yaml;
175pub mod yaml_scanner;
177
178pub(crate) use hedl_core::convert::DEFAULT_SCHEMA;
180
181pub use error::{ErrorContext, YamlError};
182pub use from_yaml::{
183 from_yaml, from_yaml_value, FromYamlConfig, FromYamlConfigBuilder, DEFAULT_MAX_ARRAY_LENGTH,
184 DEFAULT_MAX_DOCUMENT_SIZE, DEFAULT_MAX_NESTING_DEPTH,
185};
186pub use to_yaml::{to_yaml, to_yaml_value, ToYamlConfig};
187
188use hedl_core::Document;
189
190pub fn hedl_to_yaml(doc: &Document) -> Result<String, String> {
192 to_yaml(doc, &ToYamlConfig::default())
193}
194
195pub fn yaml_to_hedl(yaml: &str) -> Result<Document, String> {
197 from_yaml(yaml, &FromYamlConfig::default())
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use hedl_core::lex::Tensor;
204 use hedl_core::{Document, Item, MatrixList, Node, Reference, Value};
205 use std::collections::BTreeMap;
206
207 #[test]
208 fn test_round_trip_scalars() {
209 let mut doc = Document {
210 version: (1, 0),
211 aliases: BTreeMap::new(),
212 root: BTreeMap::new(),
213 structs: BTreeMap::new(),
214 nests: BTreeMap::new(),
215 schema_versions: BTreeMap::new(),
216 };
217 let mut root = BTreeMap::new();
218
219 root.insert("null_val".to_string(), Item::Scalar(Value::Null));
220 root.insert("bool_val".to_string(), Item::Scalar(Value::Bool(true)));
221 root.insert("int_val".to_string(), Item::Scalar(Value::Int(42)));
222 root.insert("float_val".to_string(), Item::Scalar(Value::Float(3.25)));
223 root.insert(
224 "string_val".to_string(),
225 Item::Scalar(Value::String("hello".to_string().into())),
226 );
227
228 doc.root = root;
229
230 let yaml = hedl_to_yaml(&doc).unwrap();
231 let restored = yaml_to_hedl(&yaml).unwrap();
232
233 assert_eq!(restored.root.len(), 5);
234 assert_eq!(
235 restored.root.get("bool_val").unwrap().as_scalar().unwrap(),
236 &Value::Bool(true)
237 );
238 assert_eq!(
239 restored.root.get("int_val").unwrap().as_scalar().unwrap(),
240 &Value::Int(42)
241 );
242 assert_eq!(
243 restored
244 .root
245 .get("string_val")
246 .unwrap()
247 .as_scalar()
248 .unwrap(),
249 &Value::String("hello".to_string().into())
250 );
251 }
252
253 #[test]
254 fn test_round_trip_reference() {
255 let mut doc = Document {
256 version: (1, 0),
257 aliases: BTreeMap::new(),
258 root: BTreeMap::new(),
259 structs: BTreeMap::new(),
260 nests: BTreeMap::new(),
261 schema_versions: BTreeMap::new(),
262 };
263 let mut root = BTreeMap::new();
264
265 root.insert(
266 "local_ref".to_string(),
267 Item::Scalar(Value::Reference(Reference::local("item1"))),
268 );
269 root.insert(
270 "qualified_ref".to_string(),
271 Item::Scalar(Value::Reference(Reference::qualified("User", "user1"))),
272 );
273
274 doc.root = root;
275
276 let yaml = hedl_to_yaml(&doc).unwrap();
277 let restored = yaml_to_hedl(&yaml).unwrap();
278
279 let local_ref = restored.root.get("local_ref").unwrap().as_scalar().unwrap();
280 if let Value::Reference(r) = local_ref {
281 assert_eq!(r.type_name, None);
282 assert_eq!(r.id.as_ref(), "item1");
283 } else {
284 panic!("Expected reference");
285 }
286
287 let qualified_ref = restored
288 .root
289 .get("qualified_ref")
290 .unwrap()
291 .as_scalar()
292 .unwrap();
293 if let Value::Reference(r) = qualified_ref {
294 assert_eq!(r.type_name.as_deref(), Some("User"));
295 assert_eq!(r.id.as_ref(), "user1");
296 } else {
297 panic!("Expected qualified reference");
298 }
299 }
300
301 #[test]
302 fn test_round_trip_expression() {
303 use hedl_core::lex::{ExprLiteral, Expression, Span};
304
305 let mut doc = Document {
306 version: (1, 0),
307 root: BTreeMap::new(),
308 structs: BTreeMap::new(),
309 nests: BTreeMap::new(),
310 schema_versions: BTreeMap::new(),
311 aliases: BTreeMap::new(),
312 };
313 let mut root = BTreeMap::new();
314
315 let expr = Expression::Call {
317 name: "add".to_string(),
318 args: vec![
319 Expression::Identifier {
320 name: "x".to_string(),
321 span: Span::synthetic(),
322 },
323 Expression::Literal {
324 value: ExprLiteral::Int(1),
325 span: Span::synthetic(),
326 },
327 ],
328 span: Span::synthetic(),
329 };
330
331 root.insert(
332 "expr".to_string(),
333 Item::Scalar(Value::Expression(Box::new(expr))),
334 );
335 doc.root = root;
336
337 let yaml = hedl_to_yaml(&doc).unwrap();
338 let restored = yaml_to_hedl(&yaml).unwrap();
339
340 let restored_expr = restored.root.get("expr").unwrap().as_scalar().unwrap();
341 if let Value::Expression(e) = restored_expr {
342 assert_eq!(e.to_string(), "add(x, 1)");
343 } else {
344 panic!("Expected expression, got {restored_expr:?}");
345 }
346 }
347
348 #[test]
349 fn test_round_trip_tensor() {
350 let mut doc = Document {
351 version: (1, 0),
352 aliases: BTreeMap::new(),
353 root: BTreeMap::new(),
354 structs: BTreeMap::new(),
355 nests: BTreeMap::new(),
356 schema_versions: BTreeMap::new(),
357 };
358 let mut root = BTreeMap::new();
359
360 let tensor = Tensor::Array(vec![
361 Tensor::Scalar(1.0),
362 Tensor::Scalar(2.0),
363 Tensor::Scalar(3.0),
364 ]);
365 root.insert(
366 "tensor".to_string(),
367 Item::Scalar(Value::Tensor(Box::new(tensor))),
368 );
369
370 doc.root = root;
371
372 let yaml = hedl_to_yaml(&doc).unwrap();
373 let restored = yaml_to_hedl(&yaml).unwrap();
374
375 let restored_tensor = restored.root.get("tensor").unwrap().as_scalar().unwrap();
376 if let Value::Tensor(t) = restored_tensor {
377 if let Tensor::Array(ref items) = **t {
378 assert_eq!(items.len(), 3);
379 } else {
380 panic!("Expected tensor array");
381 }
382 } else {
383 panic!("Expected tensor");
384 }
385 }
386
387 #[test]
388 fn test_round_trip_nested_tensor() {
389 let mut doc = Document {
390 version: (1, 0),
391 aliases: BTreeMap::new(),
392 root: BTreeMap::new(),
393 structs: BTreeMap::new(),
394 nests: BTreeMap::new(),
395 schema_versions: BTreeMap::new(),
396 };
397 let mut root = BTreeMap::new();
398
399 let tensor = Tensor::Array(vec![
400 Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]),
401 Tensor::Array(vec![Tensor::Scalar(3.0), Tensor::Scalar(4.0)]),
402 ]);
403 root.insert(
404 "matrix".to_string(),
405 Item::Scalar(Value::Tensor(Box::new(tensor))),
406 );
407
408 doc.root = root;
409
410 let yaml = hedl_to_yaml(&doc).unwrap();
411 let restored = yaml_to_hedl(&yaml).unwrap();
412
413 let restored_tensor = restored.root.get("matrix").unwrap().as_scalar().unwrap();
414 if let Value::Tensor(tensor_box) = restored_tensor {
415 let rows = match tensor_box.as_ref() {
416 Tensor::Array(r) => r,
417 _ => panic!("Expected tensor array"),
418 };
419 assert_eq!(rows.len(), 2);
420 if let Tensor::Array(cols) = &rows[0] {
421 assert_eq!(cols.len(), 2);
422 } else {
423 panic!("Expected nested array");
424 }
425 } else {
426 panic!("Expected nested tensor");
427 }
428 }
429
430 #[test]
431 fn test_round_trip_object() {
432 let mut doc = Document {
433 version: (1, 0),
434 aliases: BTreeMap::new(),
435 root: BTreeMap::new(),
436 structs: BTreeMap::new(),
437 nests: BTreeMap::new(),
438 schema_versions: BTreeMap::new(),
439 };
440 let mut root = BTreeMap::new();
441
442 let mut obj = BTreeMap::new();
443 obj.insert(
444 "name".to_string(),
445 Item::Scalar(Value::String("test".to_string().into())),
446 );
447 obj.insert("age".to_string(), Item::Scalar(Value::Int(30)));
448 root.insert("person".to_string(), Item::Object(obj));
449
450 doc.root = root;
451
452 let yaml = hedl_to_yaml(&doc).unwrap();
453 let restored = yaml_to_hedl(&yaml).unwrap();
454
455 let person_obj = restored.root.get("person").unwrap().as_object().unwrap();
456 assert_eq!(person_obj.len(), 2);
457 assert_eq!(
458 person_obj.get("name").unwrap().as_scalar().unwrap(),
459 &Value::String("test".to_string().into())
460 );
461 assert_eq!(
462 person_obj.get("age").unwrap().as_scalar().unwrap(),
463 &Value::Int(30)
464 );
465 }
466
467 #[test]
468 fn test_round_trip_matrix_list() {
469 let mut doc = Document {
470 version: (1, 0),
471 aliases: BTreeMap::new(),
472 root: BTreeMap::new(),
473 structs: BTreeMap::new(),
474 nests: BTreeMap::new(),
475 schema_versions: BTreeMap::new(),
476 };
477 let mut root = BTreeMap::new();
478
479 let mut list = MatrixList::new(
480 "User",
481 vec!["id".to_string(), "name".to_string(), "age".to_string()],
482 );
483
484 let node1 = Node::new(
486 "User",
487 "user1",
488 vec![
489 Value::String("user1".to_string().into()),
490 Value::String("Alice".to_string().into()),
491 Value::Int(30),
492 ],
493 );
494 let node2 = Node::new(
495 "User",
496 "user2",
497 vec![
498 Value::String("user2".to_string().into()),
499 Value::String("Bob".to_string().into()),
500 Value::Int(25),
501 ],
502 );
503
504 list.add_row(node1);
505 list.add_row(node2);
506
507 root.insert("users".to_string(), Item::List(list));
508 doc.root = root;
509
510 let yaml = hedl_to_yaml(&doc).unwrap();
511 let restored = yaml_to_hedl(&yaml).unwrap();
512
513 let users_list = restored.root.get("users").unwrap().as_list().unwrap();
514 assert_eq!(users_list.rows.len(), 2);
515 assert_eq!(users_list.schema.len(), 3);
516 assert_eq!(
518 users_list.schema,
519 vec!["id".to_string(), "name".to_string(), "age".to_string()]
520 );
521
522 let first_row = &users_list.rows[0];
523 assert_eq!(first_row.id, "user1");
524 assert_eq!(first_row.fields.len(), 3);
526 assert_eq!(
527 first_row.fields[0],
528 Value::String("user1".to_string().into())
529 ); assert_eq!(
531 first_row.fields[1],
532 Value::String("Alice".to_string().into())
533 ); assert_eq!(first_row.fields[2], Value::Int(30)); }
536
537 #[test]
538 fn test_empty_document() {
539 let doc = Document {
540 version: (1, 0),
541 aliases: BTreeMap::new(),
542 root: BTreeMap::new(),
543 structs: BTreeMap::new(),
544 nests: BTreeMap::new(),
545 schema_versions: BTreeMap::new(),
546 };
547 let yaml = hedl_to_yaml(&doc).unwrap();
548 let restored = yaml_to_hedl(&yaml).unwrap();
549 assert_eq!(restored.version, (2, 0));
551 assert_eq!(restored.root.len(), 0);
552 }
553
554 #[test]
555 fn test_nested_objects() {
556 let mut doc = Document {
557 version: (1, 0),
558 aliases: BTreeMap::new(),
559 root: BTreeMap::new(),
560 structs: BTreeMap::new(),
561 nests: BTreeMap::new(),
562 schema_versions: BTreeMap::new(),
563 };
564 let mut root = BTreeMap::new();
565
566 let mut inner = BTreeMap::new();
567 inner.insert("x".to_string(), Item::Scalar(Value::Int(10)));
568 inner.insert("y".to_string(), Item::Scalar(Value::Int(20)));
569
570 let mut outer = BTreeMap::new();
571 outer.insert("point".to_string(), Item::Object(inner));
572 outer.insert(
573 "label".to_string(),
574 Item::Scalar(Value::String("origin".to_string().into())),
575 );
576
577 root.insert("config".to_string(), Item::Object(outer));
578 doc.root = root;
579
580 let yaml = hedl_to_yaml(&doc).unwrap();
581 let restored = yaml_to_hedl(&yaml).unwrap();
582
583 let config_obj = restored.root.get("config").unwrap().as_object().unwrap();
584 let point_obj = config_obj.get("point").unwrap().as_object().unwrap();
585 assert_eq!(
586 point_obj.get("x").unwrap().as_scalar().unwrap(),
587 &Value::Int(10)
588 );
589 assert_eq!(
590 point_obj.get("y").unwrap().as_scalar().unwrap(),
591 &Value::Int(20)
592 );
593 }
594
595 #[test]
596 fn test_yaml_parsing_error() {
597 let invalid_yaml = "{ invalid yaml: [";
598 let result = yaml_to_hedl(invalid_yaml);
599 assert!(result.is_err());
600 assert!(result.unwrap_err().contains("YAML parse error"));
601 }
602
603 #[test]
604 fn test_yaml_non_mapping_root() {
605 let yaml = "- item1\n- item2\n";
606 let result = yaml_to_hedl(yaml);
607 assert!(result.is_err());
608 assert!(result.unwrap_err().contains("Root must be a YAML mapping"));
609 }
610
611 #[test]
612 fn test_yaml_with_anchors_and_aliases() {
613 let yaml = r"
615defaults: &defaults
616 timeout: 30
617 retries: 3
618
619production:
620 config: *defaults
621 host: prod.example.com
622";
623 let doc = yaml_to_hedl(yaml).unwrap();
624
625 let prod = doc.root.get("production").unwrap().as_object().unwrap();
627 let config = prod.get("config").unwrap().as_object().unwrap();
628 assert_eq!(
629 config.get("timeout").unwrap().as_scalar().unwrap(),
630 &Value::Int(30)
631 );
632 assert_eq!(
633 config.get("retries").unwrap().as_scalar().unwrap(),
634 &Value::Int(3)
635 );
636 assert_eq!(
637 prod.get("host").unwrap().as_scalar().unwrap(),
638 &Value::String("prod.example.com".to_string().into())
639 );
640 }
641}