1use std::borrow::Cow;
7use std::fmt::Write;
8
9use facet_core::{Def, Field, PrimitiveType, Type};
10use facet_reflect::Peek;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum FieldPresentation {
15 Attribute {
20 name: Cow<'static, str>,
22 },
23
24 Child {
28 name: Cow<'static, str>,
30 },
31
32 TextContent,
36
37 Children {
41 item_name: Cow<'static, str>,
43 },
44}
45
46pub trait DiffFlavor {
48 fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result;
53
54 fn field_presentation(&self, field: &Field) -> FieldPresentation;
56
57 fn struct_open(&self, name: &str) -> Cow<'static, str>;
62
63 fn struct_close(&self, name: &str, self_closing: bool) -> Cow<'static, str>;
68
69 fn field_separator(&self) -> &'static str;
74
75 fn trailing_separator(&self) -> &'static str {
81 ","
82 }
83
84 fn seq_open(&self) -> Cow<'static, str>;
89
90 fn seq_close(&self) -> Cow<'static, str>;
95
96 fn item_separator(&self) -> &'static str;
101
102 fn format_seq_item<'a>(&self, _item_type: &str, value: &'a str) -> Cow<'a, str> {
107 Cow::Borrowed(value)
109 }
110
111 fn format_seq_field_open(&self, field_name: &str) -> String {
116 format!(
118 "{}{}",
119 self.format_field_prefix(field_name),
120 self.seq_open()
121 )
122 }
123
124 fn format_seq_field_close(&self, _field_name: &str) -> Cow<'static, str> {
129 self.seq_close()
131 }
132
133 fn comment(&self, text: &str) -> String;
138
139 fn format_field(&self, name: &str, value: &str) -> String;
144
145 fn format_field_prefix(&self, name: &str) -> String;
150
151 fn format_field_suffix(&self) -> &'static str;
156
157 fn struct_open_close(&self) -> &'static str {
162 ""
163 }
164
165 fn type_comment(&self, _name: &str) -> Option<String> {
171 None
172 }
173
174 fn format_child_open(&self, name: &str) -> Cow<'static, str> {
179 Cow::Owned(self.format_field_prefix(name))
181 }
182
183 fn format_child_close(&self, _name: &str) -> Cow<'static, str> {
188 Cow::Borrowed("")
189 }
190}
191
192#[derive(Debug, Clone, Default)]
196pub struct RustFlavor;
197
198impl DiffFlavor for RustFlavor {
199 fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
200 format_value_quoted(peek, w)
201 }
202
203 fn field_presentation(&self, field: &Field) -> FieldPresentation {
204 FieldPresentation::Attribute {
206 name: Cow::Borrowed(field.name),
207 }
208 }
209
210 fn struct_open(&self, name: &str) -> Cow<'static, str> {
211 Cow::Owned(format!("{} {{", name))
212 }
213
214 fn struct_close(&self, _name: &str, _self_closing: bool) -> Cow<'static, str> {
215 Cow::Borrowed("}")
216 }
217
218 fn field_separator(&self) -> &'static str {
219 ", "
220 }
221
222 fn seq_open(&self) -> Cow<'static, str> {
223 Cow::Borrowed("[")
224 }
225
226 fn seq_close(&self) -> Cow<'static, str> {
227 Cow::Borrowed("]")
228 }
229
230 fn item_separator(&self) -> &'static str {
231 ", "
232 }
233
234 fn comment(&self, text: &str) -> String {
235 format!("/* {} */", text)
236 }
237
238 fn format_field(&self, name: &str, value: &str) -> String {
239 format!("{}: {}", name, value)
240 }
241
242 fn format_field_prefix(&self, name: &str) -> String {
243 format!("{}: ", name)
244 }
245
246 fn format_field_suffix(&self) -> &'static str {
247 ""
248 }
249}
250
251#[derive(Debug, Clone, Default)]
255pub struct JsonFlavor;
256
257impl DiffFlavor for JsonFlavor {
258 fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
259 format_value_quoted(peek, w)
260 }
261
262 fn field_presentation(&self, field: &Field) -> FieldPresentation {
263 FieldPresentation::Attribute {
265 name: Cow::Borrowed(field.name),
266 }
267 }
268
269 fn struct_open(&self, _name: &str) -> Cow<'static, str> {
270 Cow::Borrowed("{")
271 }
272
273 fn type_comment(&self, name: &str) -> Option<String> {
274 Some(format!("/* {} */", name))
275 }
276
277 fn struct_close(&self, _name: &str, _self_closing: bool) -> Cow<'static, str> {
278 Cow::Borrowed("}")
279 }
280
281 fn field_separator(&self) -> &'static str {
282 ", "
283 }
284
285 fn seq_open(&self) -> Cow<'static, str> {
286 Cow::Borrowed("[")
287 }
288
289 fn seq_close(&self) -> Cow<'static, str> {
290 Cow::Borrowed("]")
291 }
292
293 fn item_separator(&self) -> &'static str {
294 ", "
295 }
296
297 fn comment(&self, text: &str) -> String {
298 format!("// {}", text)
299 }
300
301 fn format_field(&self, name: &str, value: &str) -> String {
302 format!("\"{}\": {}", name, value)
303 }
304
305 fn format_field_prefix(&self, name: &str) -> String {
306 format!("\"{}\": ", name)
307 }
308
309 fn format_field_suffix(&self) -> &'static str {
310 ""
311 }
312}
313
314#[derive(Debug, Clone, Default)]
320pub struct XmlFlavor;
321
322impl DiffFlavor for XmlFlavor {
323 fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
324 format_value_raw(peek, w)
325 }
326
327 fn field_presentation(&self, field: &Field) -> FieldPresentation {
328 if field.has_attr(Some("xml"), "attribute") {
336 FieldPresentation::Attribute {
337 name: Cow::Borrowed(field.name),
338 }
339 } else if field.has_attr(Some("xml"), "elements") {
340 FieldPresentation::Children {
341 item_name: Cow::Borrowed(field.name),
342 }
343 } else if field.has_attr(Some("xml"), "text") {
344 FieldPresentation::TextContent
345 } else if field.has_attr(Some("xml"), "element") {
346 FieldPresentation::Child {
347 name: Cow::Borrowed(field.name),
348 }
349 } else {
350 FieldPresentation::Child {
353 name: Cow::Borrowed(field.name),
354 }
355 }
356 }
357
358 fn struct_open(&self, name: &str) -> Cow<'static, str> {
359 Cow::Owned(format!("<{}", name))
360 }
361
362 fn struct_close(&self, name: &str, self_closing: bool) -> Cow<'static, str> {
363 if self_closing {
364 Cow::Borrowed("/>")
365 } else {
366 Cow::Owned(format!("</{}>", name))
367 }
368 }
369
370 fn field_separator(&self) -> &'static str {
371 " "
372 }
373
374 fn seq_open(&self) -> Cow<'static, str> {
375 Cow::Borrowed("")
377 }
378
379 fn seq_close(&self) -> Cow<'static, str> {
380 Cow::Borrowed("")
382 }
383
384 fn item_separator(&self) -> &'static str {
385 " "
386 }
387
388 fn format_seq_item<'a>(&self, item_type: &str, value: &'a str) -> Cow<'a, str> {
389 Cow::Owned(format!("<{}>{}</{}>", item_type, value, item_type))
391 }
392
393 fn comment(&self, text: &str) -> String {
394 format!("<!-- {} -->", text)
395 }
396
397 fn format_field(&self, name: &str, value: &str) -> String {
398 format!("{}=\"{}\"", name, value)
399 }
400
401 fn format_field_prefix(&self, name: &str) -> String {
402 format!("{}=\"", name)
403 }
404
405 fn format_field_suffix(&self) -> &'static str {
406 "\""
407 }
408
409 fn struct_open_close(&self) -> &'static str {
410 ">"
411 }
412
413 fn format_child_open(&self, _name: &str) -> Cow<'static, str> {
414 Cow::Borrowed("")
417 }
418
419 fn format_child_close(&self, _name: &str) -> Cow<'static, str> {
420 Cow::Borrowed("")
421 }
422
423 fn trailing_separator(&self) -> &'static str {
424 ""
426 }
427
428 fn format_seq_field_open(&self, _field_name: &str) -> String {
429 String::new()
432 }
433
434 fn format_seq_field_close(&self, _field_name: &str) -> Cow<'static, str> {
435 Cow::Borrowed("")
437 }
438}
439
440fn format_value_quoted(peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
442 use facet_core::{PointerType, TextualType};
443
444 let shape = peek.shape();
445
446 match (shape.def, shape.ty) {
447 (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
449 write!(w, "\"{}\"", peek.get::<str>().unwrap())
450 }
451 (Def::Scalar, _) if shape.id == <String as facet_core::Facet>::SHAPE.id => {
453 write!(w, "\"{}\"", peek.get::<String>().unwrap())
454 }
455 (_, Type::Pointer(PointerType::Reference(ptr)))
457 if matches!(
458 ptr.target.ty,
459 Type::Primitive(PrimitiveType::Textual(TextualType::Str))
460 ) =>
461 {
462 write!(w, "\"{}\"", peek)
464 }
465 (Def::Scalar, Type::Primitive(PrimitiveType::Boolean)) => {
467 let b = peek.get::<bool>().unwrap();
468 write!(w, "{}", if *b { "true" } else { "false" })
469 }
470 (Def::Scalar, Type::Primitive(PrimitiveType::Textual(TextualType::Char))) => {
472 write!(w, "'{}'", peek.get::<char>().unwrap())
473 }
474 _ => {
476 if shape.is_display() {
477 write!(w, "{}", peek)
478 } else if shape.is_debug() {
479 write!(w, "{:?}", peek)
480 } else {
481 write!(w, "<{}>", shape.type_identifier)
482 }
483 }
484 }
485}
486
487fn format_value_raw(peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
489 use facet_core::{DynValueKind, TextualType};
490
491 let shape = peek.shape();
492
493 match (shape.def, shape.ty) {
494 (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
496 write!(w, "{}", peek.get::<str>().unwrap())
497 }
498 (Def::Scalar, _) if shape.id == <String as facet_core::Facet>::SHAPE.id => {
500 write!(w, "{}", peek.get::<String>().unwrap())
501 }
502 (Def::Scalar, Type::Primitive(PrimitiveType::Boolean)) => {
504 let b = peek.get::<bool>().unwrap();
505 write!(w, "{}", if *b { "true" } else { "false" })
506 }
507 (Def::Scalar, Type::Primitive(PrimitiveType::Textual(TextualType::Char))) => {
509 write!(w, "{}", peek.get::<char>().unwrap())
510 }
511 (Def::DynamicValue(_), _) => {
513 if let Ok(dv) = peek.into_dynamic_value()
515 && dv.kind() == DynValueKind::String
516 && let Some(s) = dv.as_str()
517 {
518 return write!(w, "{}", s);
519 }
520 if shape.is_display() {
522 write!(w, "{}", peek)
523 } else if shape.is_debug() {
524 write!(w, "{:?}", peek)
525 } else {
526 write!(w, "<{}>", shape.type_identifier)
527 }
528 }
529 _ => {
531 if shape.is_display() {
532 write!(w, "{}", peek)
533 } else if shape.is_debug() {
534 write!(w, "{:?}", peek)
535 } else {
536 write!(w, "<{}>", shape.type_identifier)
537 }
538 }
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545 use facet::Facet;
546 use facet_core::{Shape, Type, UserType};
547
548 fn get_field<'a>(shape: &'a Shape, name: &str) -> &'a Field {
550 if let Type::User(UserType::Struct(st)) = shape.ty {
551 st.fields.iter().find(|f| f.name == name).unwrap()
552 } else {
553 panic!("expected struct type")
554 }
555 }
556
557 #[test]
558 fn test_rust_flavor_field_presentation() {
559 #[derive(Facet)]
560 struct Point {
561 x: i32,
562 y: i32,
563 }
564
565 let shape = <Point as Facet>::SHAPE;
566 let flavor = RustFlavor;
567
568 let x_field = get_field(shape, "x");
569 let y_field = get_field(shape, "y");
570
571 assert_eq!(
573 flavor.field_presentation(x_field),
574 FieldPresentation::Attribute {
575 name: Cow::Borrowed("x")
576 }
577 );
578 assert_eq!(
579 flavor.field_presentation(y_field),
580 FieldPresentation::Attribute {
581 name: Cow::Borrowed("y")
582 }
583 );
584 }
585
586 #[test]
587 fn test_json_flavor_field_presentation() {
588 #[derive(Facet)]
589 struct Point {
590 x: i32,
591 y: i32,
592 }
593
594 let shape = <Point as Facet>::SHAPE;
595 let flavor = JsonFlavor;
596
597 let x_field = get_field(shape, "x");
598
599 assert_eq!(
601 flavor.field_presentation(x_field),
602 FieldPresentation::Attribute {
603 name: Cow::Borrowed("x")
604 }
605 );
606 }
607
608 #[test]
609 fn test_xml_flavor_field_presentation_default() {
610 #[derive(Facet)]
612 struct Book {
613 title: String,
614 author: String,
615 }
616
617 let shape = <Book as Facet>::SHAPE;
618 let flavor = XmlFlavor;
619
620 let title_field = get_field(shape, "title");
621
622 assert_eq!(
624 flavor.field_presentation(title_field),
625 FieldPresentation::Child {
626 name: Cow::Borrowed("title")
627 }
628 );
629 }
630
631 #[test]
632 fn test_xml_flavor_field_presentation_with_attrs() {
633 use facet_xml as xml;
634
635 #[derive(Facet)]
636 struct Element {
637 #[facet(xml::attribute)]
638 id: String,
639 #[facet(xml::element)]
640 title: String,
641 #[facet(xml::text)]
642 content: String,
643 #[facet(xml::elements)]
644 items: Vec<String>,
645 }
646
647 let shape = <Element as Facet>::SHAPE;
648 let flavor = XmlFlavor;
649
650 let id_field = get_field(shape, "id");
651 let title_field = get_field(shape, "title");
652 let content_field = get_field(shape, "content");
653 let items_field = get_field(shape, "items");
654
655 assert_eq!(
656 flavor.field_presentation(id_field),
657 FieldPresentation::Attribute {
658 name: Cow::Borrowed("id")
659 }
660 );
661
662 assert_eq!(
663 flavor.field_presentation(title_field),
664 FieldPresentation::Child {
665 name: Cow::Borrowed("title")
666 }
667 );
668
669 assert_eq!(
670 flavor.field_presentation(content_field),
671 FieldPresentation::TextContent
672 );
673
674 assert_eq!(
675 flavor.field_presentation(items_field),
676 FieldPresentation::Children {
677 item_name: Cow::Borrowed("items")
678 }
679 );
680 }
681
682 fn format_to_string<F: DiffFlavor>(flavor: &F, peek: Peek<'_, '_>) -> String {
683 let mut buf = String::new();
684 flavor.format_value(peek, &mut buf).unwrap();
685 buf
686 }
687
688 #[test]
689 fn test_format_value_integers() {
690 let value = 42i32;
691 let peek = Peek::new(&value);
692
693 assert_eq!(format_to_string(&RustFlavor, peek), "42");
694 assert_eq!(format_to_string(&JsonFlavor, peek), "42");
695 assert_eq!(format_to_string(&XmlFlavor, peek), "42");
696 }
697
698 #[test]
699 fn test_format_value_strings() {
700 let value = "hello";
701 let peek = Peek::new(&value);
702
703 assert_eq!(format_to_string(&RustFlavor, peek), "\"hello\"");
705 assert_eq!(format_to_string(&JsonFlavor, peek), "\"hello\"");
706 assert_eq!(format_to_string(&XmlFlavor, peek), "hello");
707 }
708
709 #[test]
710 fn test_format_value_booleans() {
711 let t = true;
712 let f = false;
713
714 assert_eq!(format_to_string(&RustFlavor, Peek::new(&t)), "true");
715 assert_eq!(format_to_string(&RustFlavor, Peek::new(&f)), "false");
716 assert_eq!(format_to_string(&JsonFlavor, Peek::new(&t)), "true");
717 assert_eq!(format_to_string(&JsonFlavor, Peek::new(&f)), "false");
718 assert_eq!(format_to_string(&XmlFlavor, Peek::new(&t)), "true");
719 assert_eq!(format_to_string(&XmlFlavor, Peek::new(&f)), "false");
720 }
721
722 #[test]
723 fn test_syntax_methods() {
724 let rust = RustFlavor;
725 let json = JsonFlavor;
726 let xml = XmlFlavor;
727
728 assert_eq!(rust.struct_open("Point"), "Point {");
730 assert_eq!(json.struct_open("Point"), "{");
731 assert_eq!(xml.struct_open("Point"), "<Point");
732
733 assert_eq!(rust.type_comment("Point"), None);
735 assert_eq!(json.type_comment("Point"), Some("/* Point */".to_string()));
736 assert_eq!(xml.type_comment("Point"), None);
737
738 assert_eq!(rust.struct_close("Point", false), "}");
740 assert_eq!(json.struct_close("Point", false), "}");
741 assert_eq!(xml.struct_close("Point", false), "</Point>");
742
743 assert_eq!(rust.struct_close("Point", true), "}");
745 assert_eq!(json.struct_close("Point", true), "}");
746 assert_eq!(xml.struct_close("Point", true), "/>");
747
748 assert_eq!(rust.field_separator(), ", ");
750 assert_eq!(json.field_separator(), ", ");
751 assert_eq!(xml.field_separator(), " ");
752
753 assert_eq!(rust.seq_open(), "[");
755 assert_eq!(rust.seq_close(), "]");
756 assert_eq!(json.seq_open(), "[");
757 assert_eq!(json.seq_close(), "]");
758 assert_eq!(xml.seq_open(), "");
760 assert_eq!(xml.seq_close(), "");
761
762 assert_eq!(rust.comment("5 more"), "/* 5 more */");
764 assert_eq!(json.comment("5 more"), "// 5 more");
765 assert_eq!(xml.comment("5 more"), "<!-- 5 more -->");
766
767 assert_eq!(rust.format_field("x", "10"), "x: 10");
769 assert_eq!(json.format_field("x", "10"), "\"x\": 10");
770 assert_eq!(xml.format_field("x", "10"), "x=\"10\"");
771 }
772}