Repository: lambda-fairy/maud
Pull request #359 (2022-11-19 14:03:48)

Render with Display using autoref specialization
This may not be something you're interested in merging. No hard feelings if you close this outright. :-)

I found for my use case that I needed to write `display(...)` in a lot of places, especially in cases where I was using external types (especially the many datetime formatting types from `icu`).

This PR adds [autoref-based specialization](http://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html) to the `html!` macro such that it will choose a type's `Render` implementation if it is available, and use `Display` as a fallback.

The blanket impl of `Render for T where T: Display` was removed in #271 for good reasons, and I will try to justify this PR in terms of those reasons:

- #266 (Impl Render for borrows): This PR does not add any new blanket impls of `Render` or `Display`.
- Performance: Due to specialization, integers can still be rendered using `itoa`.
- Semantics: Arguably, `Display` is intended to provide a human-readable string. While this isn't necessarily tied to the HTML representation of the thing, it is not unreasonable to want to use the output of `Display` in HTML. This solution treats the output of `Display` as unescaped content (so it will be escaped before appending, by going through `maud::display(...)`). If a situation arises where the `Display` and `Render` implementations should be completely different, specialization allows this on a per-type basis.

The main drawback of this PR is that compiler errors are slightly more confusing when a type implements neither `Render` nor `Display`. I have tried to name the magic inner types used in the macro with that in mind.

Let me know what you think. :-)

lambda-fairy: Thanks and sorry for the delay!

I didn't expect the solution to be so clean – seeing your take validates the approach for me.

Patch:
diff --git a/maud/src/lib.rs b/maud/src/lib.rs
index a23c2b1..9c4a180 100644
--- a/maud/src/lib.rs
+++ b/maud/src/lib.rs
@@ -340,3 +340,54 @@ mod axum_support {
         }
     }
 }
+
+#[doc(hidden)]
+pub mod macro_private {
+    use crate::{display, Render};
+    use alloc::string::String;
+    use core::fmt::Display;
+
+    #[doc(hidden)]
+    #[macro_export]
+    macro_rules! render_to {
+        ($x:expr, $buffer:expr) => {{
+            use $crate::macro_private::*;
+            match ChooseRenderOrDisplay($x) {
+                x => (&&x).implements_render_or_display().render_to(x.0, $buffer),
+            }
+        }};
+    }
+
+    pub use render_to;
+
+    pub struct ChooseRenderOrDisplay<T>(pub T);
+
+    pub struct ViaRenderTag;
+    pub struct ViaDisplayTag;
+
+    pub trait ViaRender {
+        fn implements_render_or_display(&self) -> ViaRenderTag {
+            ViaRenderTag
+        }
+    }
+    pub trait ViaDisplay {
+        fn implements_render_or_display(&self) -> ViaDisplayTag {
+            ViaDisplayTag
+        }
+    }
+
+    impl<T: Render> ViaRender for &ChooseRenderOrDisplay<T> {}
+    impl<T: Display> ViaDisplay for ChooseRenderOrDisplay<T> {}
+
+    impl ViaRenderTag {
+        pub fn render_to<T: Render + ?Sized>(self, value: &T, buffer: &mut String) {
+            value.render_to(buffer);
+        }
+    }
+
+    impl ViaDisplayTag {
+        pub fn render_to<T: Display + ?Sized>(self, value: &T, buffer: &mut String) {
+            display(value).render_to(buffer);
+        }
+    }
+}
diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs
index ad8a088..c9ba9fb 100644
--- a/maud_macros/src/generate.rs
+++ b/maud_macros/src/generate.rs
@@ -103,7 +103,7 @@ impl Generator {
 
     fn splice(&self, expr: TokenStream, build: &mut Builder) {
         let output_ident = self.output_ident.clone();
-        build.push_tokens(quote!(maud::Render::render_to(&#expr, &mut #output_ident);));
+        build.push_tokens(quote!(maud::macro_private::render_to!(&#expr, &mut #output_ident);));
     }
 
     fn element(&self, name: TokenStream, attrs: Vec<Attr>, body: ElementBody, build: &mut Builder) {


Test patch:
diff --git a/maud/tests/misc.rs b/maud/tests/misc.rs
index 741af42..b9aae52 100644
--- a/maud/tests/misc.rs
+++ b/maud/tests/misc.rs
@@ -83,3 +83,50 @@ fn issue_97() {
 
     assert_eq!(html! { (Pinkie) }.into_string(), "42");
 }
+
+#[test]
+fn only_display() {
+    use core::fmt::Display;
+
+    struct OnlyDisplay;
+    impl Display for OnlyDisplay {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(f, "<hello>")
+        }
+    }
+
+    assert_eq!(html! { (OnlyDisplay) }.into_string(), "&lt;hello&gt;");
+    assert_eq!(html! { (&OnlyDisplay) }.into_string(), "&lt;hello&gt;");
+    assert_eq!(html! { (&&OnlyDisplay) }.into_string(), "&lt;hello&gt;");
+    assert_eq!(html! { (&&&OnlyDisplay) }.into_string(), "&lt;hello&gt;");
+    assert_eq!(html! { (&&&&OnlyDisplay) }.into_string(), "&lt;hello&gt;");
+}
+
+#[test]
+fn prefer_render_over_display() {
+    use core::fmt::Display;
+    use maud::Render;
+
+    struct RenderAndDisplay;
+    impl Display for RenderAndDisplay {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            write!(f, "<display>")
+        }
+    }
+    impl Render for RenderAndDisplay {
+        fn render_to(&self, buffer: &mut String) {
+            buffer.push_str("<render>");
+        }
+    }
+
+    assert_eq!(html! { (RenderAndDisplay) }.into_string(), "<render>");
+    assert_eq!(html! { (&RenderAndDisplay) }.into_string(), "<render>");
+    assert_eq!(html! { (&&RenderAndDisplay) }.into_string(), "<render>");
+    assert_eq!(html! { (&&&RenderAndDisplay) }.into_string(), "<render>");
+    assert_eq!(html! { (&&&&RenderAndDisplay) }.into_string(), "<render>");
+
+    assert_eq!(
+        html! { (maud::display(RenderAndDisplay)) }.into_string(),
+        "&lt;display&gt;"
+    );
+}