diff --git a/tests/tests/derive.rs b/tests/tests/derive.rs index 996881b..f755fa9 100644 --- a/tests/tests/derive.rs +++ b/tests/tests/derive.rs @@ -5,6 +5,7 @@ use valuable::*; use std::collections::HashMap; use std::env; +use std::fmt::{Debug, Display}; #[test] fn test_derive_struct() { @@ -205,3 +206,72 @@ fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/*.rs"); } + +struct NotValuable; + +impl Display for NotValuable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Display NotValuable") + } +} + +impl Debug for NotValuable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Debug NotValuable") + } +} + +#[test] +fn test_rendering_not_valuable_named() { + #[derive(Valuable)] + struct S { + #[valuable(debug)] + debug_struct: NotValuable, + #[valuable(display)] + display_struct: NotValuable, + } + + let s = S { + debug_struct: NotValuable, + display_struct: NotValuable, + }; + + let v_s = Structable::definition(&s); + + let fields = match v_s.fields() { + Fields::Named(fields) => fields, + _ => unreachable!(), + }; + assert_eq!(fields.len(), 2); + assert_eq!( + fields.iter().map(|f| f.name()).collect::>(), + ["debug_struct", "display_struct"] + ); + + let v = Valuable::as_value(&s); + assert_eq!( + format!("{:?}", v), + "S { debug_struct: Debug NotValuable, display_struct: Display NotValuable }" + ); +} + +#[test] +fn test_rendering_not_valuable_unnamed() { + #[derive(Valuable)] + struct S( + #[valuable(debug)] NotValuable, + #[valuable(display)] NotValuable, + ); + + let s = S(NotValuable, NotValuable); + + let v_s = Structable::definition(&s); + + assert!(matches!(v_s.fields(), Fields::Unnamed(2))); + + let v = Valuable::as_value(&s); + assert_eq!( + format!("{:?}", v), + "S(Debug NotValuable, Display NotValuable)" + ); +} diff --git a/tests/tests/renderable.rs b/tests/tests/renderable.rs new file mode 100644 index 0000000..2b69b23 --- /dev/null +++ b/tests/tests/renderable.rs @@ -0,0 +1,55 @@ +use valuable::{Renderable, Valuable}; + +#[derive(Debug)] +struct NotTuplable<'a> { + s: &'a str, + i: usize, +} + +impl core::fmt::Display for NotTuplable<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "The string is \"{}\", and the integer is {}.", + self.s, self.i + ) + } +} + +#[test] +fn test_renderable_struct_from_debug() { + let s_owned = "Hello, Valuable World".to_owned(); + let s = NotTuplable { s: &s_owned, i: 42 }; + + let r = Renderable::Debug(&s); + let v = r.as_value(); + + // Rendering should produce the debug output for the struct + assert_eq!(r.render_to_string(), format!("{:?}", s)); + + // Writing the value itself as `Debug` should print the same debug output + // as the struct + assert_eq!(format!("{:?}", v), format!("{:?}", s)); + assert_eq!(format!("{:#?}", v), format!("{:#?}", s)); +} + +#[test] +fn test_renderable_struct_from_display() { + let s_owned = "Hello, Valuable World".to_owned(); + let s = NotTuplable { s: &s_owned, i: 42 }; + + let r = Renderable::Display(&s); + let v = r.as_value(); + + // Rendering should produce the display output for the struct + assert_eq!(r.render_to_string(), s.to_string()); + + // Just to make sure, the display output should be different for the debug + // output + assert_ne!(r.render_to_string(), format!("{:?}", s)); + + // Writing the value itself as `Debug` should print the same display output + // as the struct + assert_eq!(format!("{:?}", v), format!("{}", s)); + assert_eq!(format!("{:#?}", v), format!("{:#}", s)); +} diff --git a/valuable-derive/src/attr.rs b/valuable-derive/src/attr.rs index 45a0e0b..947fe45 100644 --- a/valuable-derive/src/attr.rs +++ b/valuable-derive/src/attr.rs @@ -42,12 +42,28 @@ static ATTRS: &[AttrDef] = &[ ], style: &[MetaStyle::Ident], }, + // #[valuable(debug)] + AttrDef { + name: "debug", + conflicts_with: &["skip", "rename", "display"], + position: &[Position::NamedField, Position::UnnamedField], + style: &[MetaStyle::Ident], + }, + // #[valuable(display)] + AttrDef { + name: "display", + conflicts_with: &["skip", "rename", "debug"], + position: &[Position::NamedField, Position::UnnamedField], + style: &[MetaStyle::Ident], + }, ]; pub(crate) struct Attrs { rename: Option<(syn::MetaNameValue, syn::LitStr)>, transparent: Option, skip: Option, + pub(crate) debug: bool, + pub(crate) display: bool, } impl Attrs { @@ -71,6 +87,8 @@ pub(crate) fn parse_attrs(cx: &Context, attrs: &[syn::Attribute], pos: Position) let mut rename = None; let mut transparent = None; let mut skip = None; + let mut debug = false; + let mut display = false; let attrs = filter_attrs(cx, attrs, pos); for (def, meta) in &attrs { @@ -104,6 +122,8 @@ pub(crate) fn parse_attrs(cx: &Context, attrs: &[syn::Attribute], pos: Position) "transparent" => transparent = Some(meta.span()), // #[valuable(skip)] "skip" => skip = Some(meta.span()), + "debug" => debug = true, + "display" => display = true, _ => unreachable!("{}", def.name), } @@ -113,6 +133,8 @@ pub(crate) fn parse_attrs(cx: &Context, attrs: &[syn::Attribute], pos: Position) rename, transparent, skip, + display, + debug, } } diff --git a/valuable-derive/src/expand.rs b/valuable-derive/src/expand.rs index 25fd30c..45816d7 100644 --- a/valuable-derive/src/expand.rs +++ b/valuable-derive/src/expand.rs @@ -101,11 +101,22 @@ fn derive_struct( .iter() .enumerate() .filter(|(i, _)| !field_attrs[*i].skip()) - .map(|(_, field)| { + .map(|(i, field)| { let f = field.ident.as_ref(); - let tokens = quote! { - &self.#f + let tokens = if field_attrs[i].debug { + quote! { + &valuable::Renderable::Debug(&self.#f) + } + } else if field_attrs[i].display { + quote! { + &valuable::Renderable::Display(&self.#f) + } + } else { + quote! { + &self.#f + } }; + respan(tokens, &field.ty) }); visit_fields = quote! { @@ -125,9 +136,20 @@ fn derive_struct( .filter(|(i, _)| !field_attrs[*i].skip()) .map(|(i, field)| { let index = syn::Index::from(i); - let tokens = quote! { - &self.#index + let tokens = if field_attrs[i].debug { + quote! { + &valuable::Renderable::Debug(&self.#index) + } + } else if field_attrs[i].display { + quote! { + &valuable::Renderable::Display(&self.#index) + } + } else { + quote! { + &self.#index + } }; + respan(tokens, &field.ty) }) .collect(); diff --git a/valuable-derive/src/lib.rs b/valuable-derive/src/lib.rs index 7da1257..1f8d833 100644 --- a/valuable-derive/src/lib.rs +++ b/valuable-derive/src/lib.rs @@ -27,6 +27,16 @@ use syn::parse_macro_input; /// /// Skip the field. /// +/// ## `#[valuable(debug)]` +/// +/// Include the debug representation of a field instead of interpreting it as +/// a complete `Valuable`. +/// +/// ## `#[valuable(display)]` +/// +/// Include the display representation of a field instead of interpreting it as +/// a complete `Valuable`. +/// /// # Examples /// /// ``` diff --git a/valuable-serde/src/lib.rs b/valuable-serde/src/lib.rs index 6b53930..e4028b4 100644 --- a/valuable-serde/src/lib.rs +++ b/valuable-serde/src/lib.rs @@ -265,6 +265,13 @@ where Value::Path(p) => Serialize::serialize(p, serializer), #[cfg(feature = "std")] Value::Error(e) => SerializeError(e).serialize(serializer), + #[cfg(feature = "alloc")] + Value::Renderable(r) => serializer.serialize_str(&r.render_to_string()), + #[cfg(not(feature = "alloc"))] + Value::Renderable(_) => { + // Can't serialize a renderable without allocating + serializer.serialize_none() + } v => unimplemented!("{:?}", v), } diff --git a/valuable/src/lib.rs b/valuable/src/lib.rs index 255ee5a..c16faba 100644 --- a/valuable/src/lib.rs +++ b/valuable/src/lib.rs @@ -135,6 +135,9 @@ pub use mappable::Mappable; mod named_values; pub use named_values::NamedValues; +mod renderable; +pub use renderable::Renderable; + mod slice; pub use slice::Slice; diff --git a/valuable/src/renderable.rs b/valuable/src/renderable.rs new file mode 100644 index 0000000..b960c57 --- /dev/null +++ b/valuable/src/renderable.rs @@ -0,0 +1,286 @@ +use core::fmt::{self, Debug, Display, Formatter, Write}; + +use crate::{Slice, Valuable, Value}; + +/// A [`Valuable`] sub-type for using ordinarily non-`valuable` types by +/// rendering them to a string with [`Debug`] or [`Display`]. +/// +/// This is most useful when defining a [`Structable`](crate::Structable) value that includes +/// fields of types where [`Valuable`] cannot be implemented like types +/// contained in external crates. +/// +/// ``` +/// use valuable::{Valuable, Value, Visit, Renderable}; +/// +/// #[derive(Debug)] +/// struct NotValuable { +/// foo: u32, +/// bar: u32, +/// } +/// +/// struct Render(String); +/// +/// impl Visit for Render { +/// fn visit_value(&mut self, value: Value<'_>) { +/// let Value::Renderable(v) = value else { return }; +/// self.0 = v.render_to_string(); +/// } +/// } +/// +/// let my_struct = NotValuable { +/// foo: 123, +/// bar: 456, +/// }; +/// +/// let mut renderer = Render(String::default()); +/// +/// // Render it plain +/// +/// valuable::visit(&Renderable::Debug(&my_struct), &mut renderer); +/// assert_eq!(renderer.0, "NotValuable { foo: 123, bar: 456 }"); +/// +/// // Or render it pretty +/// assert_eq!(Renderable::Debug(&my_struct).render_to_string_with_prettiness(true), +/// "NotValuable { +/// foo: 123, +/// bar: 456, +/// }"); +/// +/// ``` +#[derive(Clone, Copy)] +pub enum Renderable<'a> { + /// Renderable sub-type that is rendered via its [`Debug`] implementation + /// ``` + /// use valuable::{Valuable, Value, Visit, Renderable}; + /// + /// #[derive(Debug)] + /// struct NotValuable { + /// foo: u32, + /// bar: u32, + /// } + /// + /// struct Renderer(String); + /// + /// impl Visit for Renderer { + /// fn visit_value(&mut self, value: Value<'_>) { + /// let Value::Renderable(v) = value else { return }; + /// self.0 = v.render_to_string(); + /// } + /// } + /// + /// let my_struct = NotValuable { + /// foo: 123, + /// bar: 456, + /// }; + /// + /// let mut renderer = Renderer(String::default()); + /// + /// valuable::visit(&Renderable::Debug(&my_struct), &mut renderer); + /// assert_eq!(renderer.0, "NotValuable { foo: 123, bar: 456 }"); + /// ``` + Debug( + /// The actual type to be rendered with [`Debug::fmt`] + &'a dyn Debug, + ), + + /// Renderable sub-type that is rendered via its [`Display`] implementation + /// ``` + /// use valuable::{Valuable, Value, Visit, Renderable}; + /// use core::fmt; + /// + /// struct NotValuable { + /// foo: u32, + /// bar: u32, + /// } + /// + /// struct Renderer(String); + /// + /// impl Visit for Renderer { + /// fn visit_value(&mut self, value: Value<'_>) { + /// let Value::Renderable(v) = value else { return }; + /// self.0 = v.render_to_string(); + /// } + /// } + /// + /// let my_struct = NotValuable { + /// foo: 123, + /// bar: 456, + /// }; + /// + /// impl fmt::Display for NotValuable { + /// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + /// write!(f, "[Foo: {}, Bar: {}]", &self.foo, &self.bar) + /// } + /// } + /// + /// let mut renderer = Renderer(String::default()); + /// + /// valuable::visit( + /// &Renderable::Display(&my_struct), + /// &mut renderer); + /// assert_eq!(renderer.0, "[Foo: 123, Bar: 456]"); + /// ``` + Display( + /// The actual type to be rendered with [`Display::fmt`] + &'a dyn Display, + ), +} + +impl<'a> Debug for Renderable<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Renderable::Debug(inner) => { + if f.alternate() { + write!(f, "{:#?}", inner) + } else { + write!(f, "{:?}", inner) + } + } + Renderable::Display(inner) => { + if f.alternate() { + write!(f, "{:#}", inner) + } else { + write!(f, "{}", inner) + } + } + } + } +} + +impl<'a> Renderable<'a> { + /// Render this [`Renderable`] to the given [`Write`] target + /// ``` + /// use valuable::{Valuable, Value, Visit, Renderable}; + /// use core::fmt; + /// + /// #[derive(Debug)] + /// struct NotValuable { + /// foo: u32, + /// bar: u32, + /// } + /// + /// let my_struct = NotValuable { + /// foo: 123, + /// bar: 456, + /// }; + /// + /// let mut buf = String::new(); + /// Renderable::Debug(&my_struct).render(&mut buf); + /// assert_eq!(buf, "NotValuable { foo: 123, bar: 456 }"); + /// ``` + #[inline] + pub fn render(&self, target: &mut dyn Write) -> fmt::Result { + write!(target, "{:?}", self) + } + + /// Render this [`Renderable`] to the given [`Write`] target, but force the + /// prettiness/alternate to be the given value + /// ``` + /// use valuable::{Valuable, Value, Visit, Renderable}; + /// use core::fmt; + /// + /// #[derive(Debug)] + /// struct NotValuable { + /// foo: u32, + /// bar: u32, + /// } + /// + /// let my_struct = NotValuable { + /// foo: 123, + /// bar: 456, + /// }; + /// + /// let mut buf = String::new(); + /// Renderable::Debug(&my_struct).render_with_prettiness(&mut buf, true); + /// assert_eq!(buf, + /// "NotValuable { + /// foo: 123, + /// bar: 456, + /// }"); + /// ``` + #[inline] + pub fn render_with_prettiness(&self, target: &mut dyn Write, pretty: bool) -> fmt::Result { + if pretty { + write!(target, "{:#?}", self) + } else { + write!(target, "{:?}", self) + } + } + + /// Render this [`Renderable`] to an owned [`String`] + /// ``` + /// use valuable::{Valuable, Value, Visit, Renderable}; + /// use core::fmt; + /// + /// #[derive(Debug)] + /// struct NotValuable { + /// foo: u32, + /// bar: u32, + /// } + /// + /// let my_struct = NotValuable { + /// foo: 123, + /// bar: 456, + /// }; + /// + /// let rendered = Renderable::Debug(&my_struct).render_to_string(); + /// assert_eq!(rendered, "NotValuable { foo: 123, bar: 456 }"); + /// ``` + #[cfg(feature = "alloc")] + #[inline] + pub fn render_to_string(&self) -> alloc::string::String { + alloc::format!("{:?}", self) + } + + /// Render this [`Renderable`] to an owned [`String`], but force the + /// prettiness/alternate to be the given value + /// ``` + /// use valuable::{Valuable, Value, Visit, Renderable}; + /// use core::fmt; + /// + /// #[derive(Debug)] + /// struct NotValuable { + /// foo: u32, + /// bar: u32, + /// } + /// + /// let my_struct = NotValuable { + /// foo: 123, + /// bar: 456, + /// }; + /// + /// let rendered = Renderable::Debug(&my_struct) + /// .render_to_string_with_prettiness(true); + /// assert_eq!(rendered, + /// "NotValuable { + /// foo: 123, + /// bar: 456, + /// }"); + /// ``` + #[cfg(feature = "alloc")] + #[inline] + pub fn render_to_string_with_prettiness(&self, pretty: bool) -> alloc::string::String { + if pretty { + alloc::format!("{:#?}", self) + } else { + alloc::format!("{:?}", self) + } + } +} + +impl<'a> Valuable for Renderable<'a> { + fn as_value(&self) -> crate::Value<'_> { + Value::Renderable(*self) + } + + fn visit(&self, visit: &mut dyn crate::Visit) { + visit.visit_value(self.as_value()); + } + + fn visit_slice(slice: &[Self], visit: &mut dyn crate::Visit) + where + Self: Sized, + { + visit.visit_primitive_slice(Slice::Renderable(slice)); + } +} diff --git a/valuable/src/slice.rs b/valuable/src/slice.rs index a6ada6e..5544625 100644 --- a/valuable/src/slice.rs +++ b/valuable/src/slice.rs @@ -326,6 +326,20 @@ slice! { /// ``` Isize(isize), + /// A slice containing `Renderable` values. + /// + /// # Examples + /// + /// ``` + /// use valuable::{Slice, Renderable}; + /// + /// let v = Slice::Renderable(&[ + /// Renderable::Debug(&"foo"), + /// Renderable::Display(&"bar") + /// ]); + /// ``` + Renderable(Renderable<'a>), + /// A slice containing `str` values. /// /// # Examples diff --git a/valuable/src/value.rs b/valuable/src/value.rs index d0ada87..13f0f35 100644 --- a/valuable/src/value.rs +++ b/valuable/src/value.rs @@ -1,4 +1,4 @@ -use crate::{Enumerable, Listable, Mappable, Structable, Tuplable, Valuable, Visit}; +use crate::{Enumerable, Listable, Mappable, Renderable, Structable, Tuplable, Valuable, Visit}; use core::fmt; @@ -404,6 +404,27 @@ value! { /// let v = Value::Tuplable(&my_tuple); /// ``` Tuplable(&'a dyn Tuplable), + + /// A value that can be rendered to a string + /// + /// # Examples + /// + /// ``` + /// use valuable::{Value, Renderable}; + /// + /// #[derive(Debug)] + /// struct NotValuable{ + /// inner: u32 + /// }; + /// + /// let nv = NotValuable { + /// inner: 42 + /// }; + /// + /// let v = Value::Renderable(Renderable::Debug(&nv)); + /// ``` + /// + Renderable(Renderable<'a>), } impl Valuable for Value<'_> {