use std::borrow::Cow;
use std::fmt::Write;
use facet_core::{Def, Field, PrimitiveType, Type};
use facet_reflect::Peek;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldPresentation {
Attribute {
name: Cow<'static, str>,
},
Child {
name: Cow<'static, str>,
},
TextContent,
Children {
item_name: Cow<'static, str>,
},
}
pub trait DiffFlavor {
fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result;
fn field_presentation(&self, field: &Field) -> FieldPresentation;
fn struct_open(&self, name: &str) -> Cow<'static, str>;
fn struct_close(&self, name: &str, self_closing: bool) -> Cow<'static, str>;
fn field_separator(&self) -> &'static str;
fn trailing_separator(&self) -> &'static str {
","
}
fn seq_open(&self) -> Cow<'static, str>;
fn seq_close(&self) -> Cow<'static, str>;
fn item_separator(&self) -> &'static str;
fn format_seq_item<'a>(&self, _item_type: &str, value: &'a str) -> Cow<'a, str> {
Cow::Borrowed(value)
}
fn format_seq_field_open(&self, field_name: &str) -> String {
format!(
"{}{}",
self.format_field_prefix(field_name),
self.seq_open()
)
}
fn format_seq_field_close(&self, _field_name: &str) -> Cow<'static, str> {
self.seq_close()
}
fn comment(&self, text: &str) -> String;
fn format_field(&self, name: &str, value: &str) -> String;
fn format_field_prefix(&self, name: &str) -> String;
fn format_field_suffix(&self) -> &'static str;
fn struct_open_close(&self) -> &'static str {
""
}
fn type_comment(&self, _name: &str) -> Option<String> {
None
}
fn format_child_open(&self, name: &str) -> Cow<'static, str> {
Cow::Owned(self.format_field_prefix(name))
}
fn format_child_close(&self, _name: &str) -> Cow<'static, str> {
Cow::Borrowed("")
}
}
#[derive(Debug, Clone, Default)]
pub struct RustFlavor;
impl DiffFlavor for RustFlavor {
fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
format_value_quoted(peek, w)
}
fn field_presentation(&self, field: &Field) -> FieldPresentation {
FieldPresentation::Attribute {
name: Cow::Borrowed(field.name),
}
}
fn struct_open(&self, name: &str) -> Cow<'static, str> {
Cow::Owned(format!("{} {{", name))
}
fn struct_close(&self, _name: &str, _self_closing: bool) -> Cow<'static, str> {
Cow::Borrowed("}")
}
fn field_separator(&self) -> &'static str {
", "
}
fn seq_open(&self) -> Cow<'static, str> {
Cow::Borrowed("[")
}
fn seq_close(&self) -> Cow<'static, str> {
Cow::Borrowed("]")
}
fn item_separator(&self) -> &'static str {
", "
}
fn comment(&self, text: &str) -> String {
format!("/* {} */", text)
}
fn format_field(&self, name: &str, value: &str) -> String {
format!("{}: {}", name, value)
}
fn format_field_prefix(&self, name: &str) -> String {
format!("{}: ", name)
}
fn format_field_suffix(&self) -> &'static str {
""
}
}
#[derive(Debug, Clone, Default)]
pub struct JsonFlavor;
impl DiffFlavor for JsonFlavor {
fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
format_value_quoted(peek, w)
}
fn field_presentation(&self, field: &Field) -> FieldPresentation {
FieldPresentation::Attribute {
name: Cow::Borrowed(field.name),
}
}
fn struct_open(&self, _name: &str) -> Cow<'static, str> {
Cow::Borrowed("{")
}
fn type_comment(&self, name: &str) -> Option<String> {
Some(format!("/* {} */", name))
}
fn struct_close(&self, _name: &str, _self_closing: bool) -> Cow<'static, str> {
Cow::Borrowed("}")
}
fn field_separator(&self) -> &'static str {
", "
}
fn seq_open(&self) -> Cow<'static, str> {
Cow::Borrowed("[")
}
fn seq_close(&self) -> Cow<'static, str> {
Cow::Borrowed("]")
}
fn item_separator(&self) -> &'static str {
", "
}
fn comment(&self, text: &str) -> String {
format!("// {}", text)
}
fn format_field(&self, name: &str, value: &str) -> String {
format!("\"{}\": {}", name, value)
}
fn format_field_prefix(&self, name: &str) -> String {
format!("\"{}\": ", name)
}
fn format_field_suffix(&self) -> &'static str {
""
}
}
#[derive(Debug, Clone, Default)]
pub struct XmlFlavor;
impl DiffFlavor for XmlFlavor {
fn format_value(&self, peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
format_value_raw(peek, w)
}
fn field_presentation(&self, field: &Field) -> FieldPresentation {
if field.has_attr(Some("xml"), "attribute") {
FieldPresentation::Attribute {
name: Cow::Borrowed(field.name),
}
} else if field.has_attr(Some("xml"), "elements") {
FieldPresentation::Children {
item_name: Cow::Borrowed(field.name),
}
} else if field.has_attr(Some("xml"), "text") {
FieldPresentation::TextContent
} else if field.has_attr(Some("xml"), "element") {
FieldPresentation::Child {
name: Cow::Borrowed(field.name),
}
} else {
FieldPresentation::Child {
name: Cow::Borrowed(field.name),
}
}
}
fn struct_open(&self, name: &str) -> Cow<'static, str> {
Cow::Owned(format!("<{}", name))
}
fn struct_close(&self, name: &str, self_closing: bool) -> Cow<'static, str> {
if self_closing {
Cow::Borrowed("/>")
} else {
Cow::Owned(format!("</{}>", name))
}
}
fn field_separator(&self) -> &'static str {
" "
}
fn seq_open(&self) -> Cow<'static, str> {
Cow::Borrowed("")
}
fn seq_close(&self) -> Cow<'static, str> {
Cow::Borrowed("")
}
fn item_separator(&self) -> &'static str {
" "
}
fn format_seq_item<'a>(&self, item_type: &str, value: &'a str) -> Cow<'a, str> {
Cow::Owned(format!("<{}>{}</{}>", item_type, value, item_type))
}
fn comment(&self, text: &str) -> String {
format!("<!-- {} -->", text)
}
fn format_field(&self, name: &str, value: &str) -> String {
format!("{}=\"{}\"", name, value)
}
fn format_field_prefix(&self, name: &str) -> String {
format!("{}=\"", name)
}
fn format_field_suffix(&self) -> &'static str {
"\""
}
fn struct_open_close(&self) -> &'static str {
">"
}
fn format_child_open(&self, _name: &str) -> Cow<'static, str> {
Cow::Borrowed("")
}
fn format_child_close(&self, _name: &str) -> Cow<'static, str> {
Cow::Borrowed("")
}
fn trailing_separator(&self) -> &'static str {
""
}
fn format_seq_field_open(&self, _field_name: &str) -> String {
String::new()
}
fn format_seq_field_close(&self, _field_name: &str) -> Cow<'static, str> {
Cow::Borrowed("")
}
}
fn format_value_quoted(peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
use facet_core::{PointerType, TextualType};
let shape = peek.shape();
match (shape.def, shape.ty) {
(_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
write!(w, "\"{}\"", peek.get::<str>().unwrap())
}
(Def::Scalar, _) if shape.id == <String as facet_core::Facet>::SHAPE.id => {
write!(w, "\"{}\"", peek.get::<String>().unwrap())
}
(_, Type::Pointer(PointerType::Reference(ptr)))
if matches!(
ptr.target.ty,
Type::Primitive(PrimitiveType::Textual(TextualType::Str))
) =>
{
write!(w, "\"{}\"", peek)
}
(Def::Scalar, Type::Primitive(PrimitiveType::Boolean)) => {
let b = peek.get::<bool>().unwrap();
write!(w, "{}", if *b { "true" } else { "false" })
}
(Def::Scalar, Type::Primitive(PrimitiveType::Textual(TextualType::Char))) => {
write!(w, "'{}'", peek.get::<char>().unwrap())
}
_ => {
if shape.is_display() {
write!(w, "{}", peek)
} else if shape.is_debug() {
write!(w, "{:?}", peek)
} else {
write!(w, "<{}>", shape.type_identifier)
}
}
}
}
fn format_value_raw(peek: Peek<'_, '_>, w: &mut dyn Write) -> std::fmt::Result {
use facet_core::{DynValueKind, TextualType};
let shape = peek.shape();
match (shape.def, shape.ty) {
(_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
write!(w, "{}", peek.get::<str>().unwrap())
}
(Def::Scalar, _) if shape.id == <String as facet_core::Facet>::SHAPE.id => {
write!(w, "{}", peek.get::<String>().unwrap())
}
(Def::Scalar, Type::Primitive(PrimitiveType::Boolean)) => {
let b = peek.get::<bool>().unwrap();
write!(w, "{}", if *b { "true" } else { "false" })
}
(Def::Scalar, Type::Primitive(PrimitiveType::Textual(TextualType::Char))) => {
write!(w, "{}", peek.get::<char>().unwrap())
}
(Def::DynamicValue(_), _) => {
if let Ok(dv) = peek.into_dynamic_value()
&& dv.kind() == DynValueKind::String
&& let Some(s) = dv.as_str()
{
return write!(w, "{}", s);
}
if shape.is_display() {
write!(w, "{}", peek)
} else if shape.is_debug() {
write!(w, "{:?}", peek)
} else {
write!(w, "<{}>", shape.type_identifier)
}
}
_ => {
if shape.is_display() {
write!(w, "{}", peek)
} else if shape.is_debug() {
write!(w, "{:?}", peek)
} else {
write!(w, "<{}>", shape.type_identifier)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use facet::Facet;
use facet_core::{Shape, Type, UserType};
fn get_field<'a>(shape: &'a Shape, name: &str) -> &'a Field {
if let Type::User(UserType::Struct(st)) = shape.ty {
st.fields.iter().find(|f| f.name == name).unwrap()
} else {
panic!("expected struct type")
}
}
#[test]
fn test_rust_flavor_field_presentation() {
#[derive(Facet)]
struct Point {
x: i32,
y: i32,
}
let shape = <Point as Facet>::SHAPE;
let flavor = RustFlavor;
let x_field = get_field(shape, "x");
let y_field = get_field(shape, "y");
assert_eq!(
flavor.field_presentation(x_field),
FieldPresentation::Attribute {
name: Cow::Borrowed("x")
}
);
assert_eq!(
flavor.field_presentation(y_field),
FieldPresentation::Attribute {
name: Cow::Borrowed("y")
}
);
}
#[test]
fn test_json_flavor_field_presentation() {
#[derive(Facet)]
struct Point {
x: i32,
y: i32,
}
let shape = <Point as Facet>::SHAPE;
let flavor = JsonFlavor;
let x_field = get_field(shape, "x");
assert_eq!(
flavor.field_presentation(x_field),
FieldPresentation::Attribute {
name: Cow::Borrowed("x")
}
);
}
#[test]
fn test_xml_flavor_field_presentation_default() {
#[derive(Facet)]
struct Book {
title: String,
author: String,
}
let shape = <Book as Facet>::SHAPE;
let flavor = XmlFlavor;
let title_field = get_field(shape, "title");
assert_eq!(
flavor.field_presentation(title_field),
FieldPresentation::Child {
name: Cow::Borrowed("title")
}
);
}
fn format_to_string<F: DiffFlavor>(flavor: &F, peek: Peek<'_, '_>) -> String {
let mut buf = String::new();
flavor.format_value(peek, &mut buf).unwrap();
buf
}
#[test]
fn test_format_value_integers() {
let value = 42i32;
let peek = Peek::new(&value);
assert_eq!(format_to_string(&RustFlavor, peek), "42");
assert_eq!(format_to_string(&JsonFlavor, peek), "42");
assert_eq!(format_to_string(&XmlFlavor, peek), "42");
}
#[test]
fn test_format_value_strings() {
let value = "hello";
let peek = Peek::new(&value);
assert_eq!(format_to_string(&RustFlavor, peek), "\"hello\"");
assert_eq!(format_to_string(&JsonFlavor, peek), "\"hello\"");
assert_eq!(format_to_string(&XmlFlavor, peek), "hello");
}
#[test]
fn test_format_value_booleans() {
let t = true;
let f = false;
assert_eq!(format_to_string(&RustFlavor, Peek::new(&t)), "true");
assert_eq!(format_to_string(&RustFlavor, Peek::new(&f)), "false");
assert_eq!(format_to_string(&JsonFlavor, Peek::new(&t)), "true");
assert_eq!(format_to_string(&JsonFlavor, Peek::new(&f)), "false");
assert_eq!(format_to_string(&XmlFlavor, Peek::new(&t)), "true");
assert_eq!(format_to_string(&XmlFlavor, Peek::new(&f)), "false");
}
#[test]
fn test_syntax_methods() {
let rust = RustFlavor;
let json = JsonFlavor;
let xml = XmlFlavor;
assert_eq!(rust.struct_open("Point"), "Point {");
assert_eq!(json.struct_open("Point"), "{");
assert_eq!(xml.struct_open("Point"), "<Point");
assert_eq!(rust.type_comment("Point"), None);
assert_eq!(json.type_comment("Point"), Some("/* Point */".to_string()));
assert_eq!(xml.type_comment("Point"), None);
assert_eq!(rust.struct_close("Point", false), "}");
assert_eq!(json.struct_close("Point", false), "}");
assert_eq!(xml.struct_close("Point", false), "</Point>");
assert_eq!(rust.struct_close("Point", true), "}");
assert_eq!(json.struct_close("Point", true), "}");
assert_eq!(xml.struct_close("Point", true), "/>");
assert_eq!(rust.field_separator(), ", ");
assert_eq!(json.field_separator(), ", ");
assert_eq!(xml.field_separator(), " ");
assert_eq!(rust.seq_open(), "[");
assert_eq!(rust.seq_close(), "]");
assert_eq!(json.seq_open(), "[");
assert_eq!(json.seq_close(), "]");
assert_eq!(xml.seq_open(), "");
assert_eq!(xml.seq_close(), "");
assert_eq!(rust.comment("5 more"), "/* 5 more */");
assert_eq!(json.comment("5 more"), "// 5 more");
assert_eq!(xml.comment("5 more"), "<!-- 5 more -->");
assert_eq!(rust.format_field("x", "10"), "x: 10");
assert_eq!(json.format_field("x", "10"), "\"x\": 10");
assert_eq!(xml.format_field("x", "10"), "x=\"10\"");
}
}