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("<items>")
376 }
377
378 fn seq_close(&self) -> Cow<'static, str> {
379 Cow::Borrowed("</items>")
380 }
381
382 fn item_separator(&self) -> &'static str {
383 " "
384 }
385
386 fn format_seq_item<'a>(&self, item_type: &str, value: &'a str) -> Cow<'a, str> {
387 Cow::Owned(format!("<{}>{}</{}>", item_type, value, item_type))
389 }
390
391 fn comment(&self, text: &str) -> String {
392 format!("<!-- {} -->", text)
393 }
394
395 fn format_field(&self, name: &str, value: &str) -> String {
396 format!("{}=\"{}\"", name, value)
397 }
398
399 fn format_field_prefix(&self, name: &str) -> String {
400 format!("{}=\"", name)
401 }
402
403 fn format_field_suffix(&self) -> &'static str {
404 "\""
405 }
406
407 fn struct_open_close(&self) -> &'static str {
408 ">"
409 }
410
411 fn format_child_open(&self, _name: &str) -> Cow<'static, str> {
412 Cow::Borrowed("")
415 }
416
417 fn format_child_close(&self, _name: &str) -> Cow<'static, str> {
418 Cow::Borrowed("")
419 }
420
421 fn trailing_separator(&self) -> &'static str {
422 ""
424 }
425
426 fn format_seq_field_open(&self, field_name: &str) -> String {
427 format!("<{}>", field_name)
429 }
430
431 fn format_seq_field_close(&self, field_name: &str) -> Cow<'static, str> {
432 Cow::Owned(format!("</{}>", field_name))
434 }
435}
436
437fn format_value_quoted(peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
439 use facet_core::{PointerType, TextualType};
440
441 let shape = peek.shape();
442
443 match (shape.def, shape.ty) {
444 (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
446 write!(w, "\"{}\"", peek.get::<str>().unwrap())
447 }
448 (Def::Scalar, _) if shape.id == <String as facet_core::Facet>::SHAPE.id => {
450 write!(w, "\"{}\"", peek.get::<String>().unwrap())
451 }
452 (_, Type::Pointer(PointerType::Reference(ptr)))
454 if matches!(
455 ptr.target.ty,
456 Type::Primitive(PrimitiveType::Textual(TextualType::Str))
457 ) =>
458 {
459 write!(w, "\"{}\"", peek)
461 }
462 (Def::Scalar, Type::Primitive(PrimitiveType::Boolean)) => {
464 let b = peek.get::<bool>().unwrap();
465 write!(w, "{}", if *b { "true" } else { "false" })
466 }
467 (Def::Scalar, Type::Primitive(PrimitiveType::Textual(TextualType::Char))) => {
469 write!(w, "'{}'", peek.get::<char>().unwrap())
470 }
471 _ => {
473 if shape.is_display() {
474 write!(w, "{}", peek)
475 } else if shape.is_debug() {
476 write!(w, "{:?}", peek)
477 } else {
478 write!(w, "<{}>", shape.type_identifier)
479 }
480 }
481 }
482}
483
484fn format_value_raw(peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
486 use facet_core::{DynValueKind, TextualType};
487
488 let shape = peek.shape();
489
490 match (shape.def, shape.ty) {
491 (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
493 write!(w, "{}", peek.get::<str>().unwrap())
494 }
495 (Def::Scalar, _) if shape.id == <String as facet_core::Facet>::SHAPE.id => {
497 write!(w, "{}", peek.get::<String>().unwrap())
498 }
499 (Def::Scalar, Type::Primitive(PrimitiveType::Boolean)) => {
501 let b = peek.get::<bool>().unwrap();
502 write!(w, "{}", if *b { "true" } else { "false" })
503 }
504 (Def::Scalar, Type::Primitive(PrimitiveType::Textual(TextualType::Char))) => {
506 write!(w, "{}", peek.get::<char>().unwrap())
507 }
508 (Def::DynamicValue(_), _) => {
510 if let Ok(dv) = peek.into_dynamic_value()
512 && dv.kind() == DynValueKind::String
513 && let Some(s) = dv.as_str()
514 {
515 return write!(w, "{}", s);
516 }
517 if shape.is_display() {
519 write!(w, "{}", peek)
520 } else if shape.is_debug() {
521 write!(w, "{:?}", peek)
522 } else {
523 write!(w, "<{}>", shape.type_identifier)
524 }
525 }
526 _ => {
528 if shape.is_display() {
529 write!(w, "{}", peek)
530 } else if shape.is_debug() {
531 write!(w, "{:?}", peek)
532 } else {
533 write!(w, "<{}>", shape.type_identifier)
534 }
535 }
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use facet::Facet;
543 use facet_core::{Shape, Type, UserType};
544
545 fn get_field<'a>(shape: &'a Shape, name: &str) -> &'a Field {
547 if let Type::User(UserType::Struct(st)) = shape.ty {
548 st.fields.iter().find(|f| f.name == name).unwrap()
549 } else {
550 panic!("expected struct type")
551 }
552 }
553
554 #[test]
555 fn test_rust_flavor_field_presentation() {
556 #[derive(Facet)]
557 struct Point {
558 x: i32,
559 y: i32,
560 }
561
562 let shape = <Point as Facet>::SHAPE;
563 let flavor = RustFlavor;
564
565 let x_field = get_field(shape, "x");
566 let y_field = get_field(shape, "y");
567
568 assert_eq!(
570 flavor.field_presentation(x_field),
571 FieldPresentation::Attribute {
572 name: Cow::Borrowed("x")
573 }
574 );
575 assert_eq!(
576 flavor.field_presentation(y_field),
577 FieldPresentation::Attribute {
578 name: Cow::Borrowed("y")
579 }
580 );
581 }
582
583 #[test]
584 fn test_json_flavor_field_presentation() {
585 #[derive(Facet)]
586 struct Point {
587 x: i32,
588 y: i32,
589 }
590
591 let shape = <Point as Facet>::SHAPE;
592 let flavor = JsonFlavor;
593
594 let x_field = get_field(shape, "x");
595
596 assert_eq!(
598 flavor.field_presentation(x_field),
599 FieldPresentation::Attribute {
600 name: Cow::Borrowed("x")
601 }
602 );
603 }
604
605 #[test]
606 fn test_xml_flavor_field_presentation_default() {
607 #[derive(Facet)]
609 struct Book {
610 title: String,
611 author: String,
612 }
613
614 let shape = <Book as Facet>::SHAPE;
615 let flavor = XmlFlavor;
616
617 let title_field = get_field(shape, "title");
618
619 assert_eq!(
621 flavor.field_presentation(title_field),
622 FieldPresentation::Child {
623 name: Cow::Borrowed("title")
624 }
625 );
626 }
627
628 #[test]
629 fn test_xml_flavor_field_presentation_with_attrs() {
630 use facet_xml as xml;
631
632 #[derive(Facet)]
633 struct Element {
634 #[facet(xml::attribute)]
635 id: String,
636 #[facet(xml::element)]
637 title: String,
638 #[facet(xml::text)]
639 content: String,
640 #[facet(xml::elements)]
641 items: Vec<String>,
642 }
643
644 let shape = <Element as Facet>::SHAPE;
645 let flavor = XmlFlavor;
646
647 let id_field = get_field(shape, "id");
648 let title_field = get_field(shape, "title");
649 let content_field = get_field(shape, "content");
650 let items_field = get_field(shape, "items");
651
652 assert_eq!(
653 flavor.field_presentation(id_field),
654 FieldPresentation::Attribute {
655 name: Cow::Borrowed("id")
656 }
657 );
658
659 assert_eq!(
660 flavor.field_presentation(title_field),
661 FieldPresentation::Child {
662 name: Cow::Borrowed("title")
663 }
664 );
665
666 assert_eq!(
667 flavor.field_presentation(content_field),
668 FieldPresentation::TextContent
669 );
670
671 assert_eq!(
672 flavor.field_presentation(items_field),
673 FieldPresentation::Children {
674 item_name: Cow::Borrowed("items")
675 }
676 );
677 }
678
679 fn format_to_string<F: DiffFlavor>(flavor: &F, peek: Peek<'_, '_>) -> String {
680 let mut buf = String::new();
681 flavor.format_value(peek, &mut buf).unwrap();
682 buf
683 }
684
685 #[test]
686 fn test_format_value_integers() {
687 let value = 42i32;
688 let peek = Peek::new(&value);
689
690 assert_eq!(format_to_string(&RustFlavor, peek), "42");
691 assert_eq!(format_to_string(&JsonFlavor, peek), "42");
692 assert_eq!(format_to_string(&XmlFlavor, peek), "42");
693 }
694
695 #[test]
696 fn test_format_value_strings() {
697 let value = "hello";
698 let peek = Peek::new(&value);
699
700 assert_eq!(format_to_string(&RustFlavor, peek), "\"hello\"");
702 assert_eq!(format_to_string(&JsonFlavor, peek), "\"hello\"");
703 assert_eq!(format_to_string(&XmlFlavor, peek), "hello");
704 }
705
706 #[test]
707 fn test_format_value_booleans() {
708 let t = true;
709 let f = false;
710
711 assert_eq!(format_to_string(&RustFlavor, Peek::new(&t)), "true");
712 assert_eq!(format_to_string(&RustFlavor, Peek::new(&f)), "false");
713 assert_eq!(format_to_string(&JsonFlavor, Peek::new(&t)), "true");
714 assert_eq!(format_to_string(&JsonFlavor, Peek::new(&f)), "false");
715 assert_eq!(format_to_string(&XmlFlavor, Peek::new(&t)), "true");
716 assert_eq!(format_to_string(&XmlFlavor, Peek::new(&f)), "false");
717 }
718
719 #[test]
720 fn test_syntax_methods() {
721 let rust = RustFlavor;
722 let json = JsonFlavor;
723 let xml = XmlFlavor;
724
725 assert_eq!(rust.struct_open("Point"), "Point {");
727 assert_eq!(json.struct_open("Point"), "{");
728 assert_eq!(xml.struct_open("Point"), "<Point");
729
730 assert_eq!(rust.type_comment("Point"), None);
732 assert_eq!(json.type_comment("Point"), Some("/* Point */".to_string()));
733 assert_eq!(xml.type_comment("Point"), None);
734
735 assert_eq!(rust.struct_close("Point", false), "}");
737 assert_eq!(json.struct_close("Point", false), "}");
738 assert_eq!(xml.struct_close("Point", false), "</Point>");
739
740 assert_eq!(rust.struct_close("Point", true), "}");
742 assert_eq!(json.struct_close("Point", true), "}");
743 assert_eq!(xml.struct_close("Point", true), "/>");
744
745 assert_eq!(rust.field_separator(), ", ");
747 assert_eq!(json.field_separator(), ", ");
748 assert_eq!(xml.field_separator(), " ");
749
750 assert_eq!(rust.seq_open(), "[");
752 assert_eq!(rust.seq_close(), "]");
753 assert_eq!(json.seq_open(), "[");
754 assert_eq!(json.seq_close(), "]");
755 assert_eq!(xml.seq_open(), "<items>");
756 assert_eq!(xml.seq_close(), "</items>");
757
758 assert_eq!(rust.comment("5 more"), "/* 5 more */");
760 assert_eq!(json.comment("5 more"), "// 5 more");
761 assert_eq!(xml.comment("5 more"), "<!-- 5 more -->");
762
763 assert_eq!(rust.format_field("x", "10"), "x: 10");
765 assert_eq!(json.format_field("x", "10"), "\"x\": 10");
766 assert_eq!(xml.format_field("x", "10"), "x=\"10\"");
767 }
768}