displaydoc_lite/
lib.rs

1//! Lite version of [`displaydoc`][ddoc].
2//!
3//! This crate is a lite version of the popular crate [`displaydoc`][ddoc].
4//! It provides the same functionality but using a declarative macro instead
5//! and not depending on `syn` or `quote`.
6//!
7//! This crate is also usable in `no_std` environments. No additional features are required for that.
8//!
9//! **Note** that `displaydoc-lite` still has two proc-macro dependencies,
10//! but they are very tiny and do not have any dependencies.
11//!
12//! ## Example
13//!
14//! ```rust
15//! use displaydoc_lite::displaydoc;
16//! use std::io;
17//!
18//! displaydoc! {
19//!     #[derive(Debug)]
20//!     pub enum DataStoreError {
21//!         /// data store disconnected: {_0}
22//!         Disconnect(io::Error),
23//!         /// the data for key `{_0}` is not available
24//!         Redaction(String),
25//!         /// invalid header (expected {expected}, found {found})
26//!         InvalidHeader {
27//!             expected: String,
28//!             found: String,
29//!         },
30//!         /// unknown data store error
31//!         Unknown,
32//!     }
33//! }
34//! # fn main() {
35//! # use std::string::ToString;
36//! # assert_eq!(DataStoreError::Redaction("foo".into()).to_string(),
37//! #           "the data for key `foo` is not available".to_owned());
38//! #
39//! # let header = DataStoreError::InvalidHeader { expected: "foo".into(), found: "bar".into() };
40//! # assert_eq!(header.to_string(), "invalid header (expected foo, found bar)".to_owned());
41//! # assert_eq!(DataStoreError::Unknown.to_string(), "unknown data store error".to_owned());
42//! # }
43//! ```
44//!
45//! Support for interpolating fields is planed, but currently not implemented.
46//!
47//!
48//! ### License
49//!
50//! Licensed under either [Apache License][apache] or the [MIT][mit] license.
51//!
52//!
53//! [apache]: https://github.com/Stupremee/displaydoc-lite/tree/main/LICENSE-APACHE
54//! [mit]: https://github.com/Stupremee/displaydoc-lite/tree/main/LICENSE-MIT
55//! [ddoc]: https://crates.io/crates/displaydoc
56#![no_std]
57#![forbid(unsafe_code)]
58#![deny(rust_2018_idioms, missing_docs, clippy::pedantic)]
59
60#[doc(hidden)]
61pub mod __private {
62    #[doc(hidden)]
63    pub use displaydoc_lite_proc_macros as proc_macros;
64}
65
66/// The main macro of this crate which is used to create the `Display` implementation
67/// for an enum.
68///
69/// See the root module for more documentation.
70#[macro_export]
71macro_rules! displaydoc {
72    ($(#[$enum_attr:meta])*
73    $pub:vis enum $name:ident {
74        $($body:tt)*
75    }) => {
76        $(#[$enum_attr])*
77        $pub enum $name { $($body)* }
78
79        $crate::__parse_enum_variant__! { enum $name { $($body)* } }
80    };
81}
82
83#[macro_export]
84#[doc(hidden)]
85macro_rules! __parse_enum_variant__ {
86    // tuple variant
87    (@data $name:ident $this:ident $f:ident @variant $(#[$attr:meta])* $variant:ident (
88        $(
89            $( #[$field_meta:meta] )*
90            $field_vis:vis $field_ty:ty
91        ),* $(,)?
92    ) $(, $($tt:tt)* )? ) => {
93        #[allow(unused, clippy::used_underscore_binding)]
94        if let $crate::__private::proc_macros::__tuple_bindings__!($name, $variant, $($field_ty,)*) = $this {
95            $crate::__defile_expr__! {
96                $crate::__get_doc_string__!(@@struct $f, $(#[@$attr])*)
97            }
98        } else {
99            // process rest of the enum
100            $crate::__token_or_ok__!( $( $crate::__parse_enum_variant__!(@data $name $this $f @variant $( $tt )*) )? )
101        }
102    };
103
104    // named variant
105    (@data $name:ident $this:ident $f:ident @variant $(#[$attr:meta])* $variant:ident {
106        $(
107            $( #[$field_meta:meta] )*
108            $field_vis:vis $field_name:ident : $field_ty:ty
109        ),* $(,)?
110    } $(, $($tt:tt)* )? ) => {
111        #[allow(unused)]
112        if let $name::$variant { $($field_name),* } = $this {
113            $crate::__defile_expr__! {
114                $crate::__get_doc_string__!(@@struct $f, $(#[@$attr])*)
115            }
116        } else {
117            // process rest of the enum
118            $crate::__token_or_ok__!( $( $crate::__parse_enum_variant__!(@data $name $this $f @variant $( $tt )*) )? )
119        }
120    };
121
122    // unit variant
123    (@data $name:ident $this:ident $f:ident @variant
124        $( #[$field_meta:meta] )*
125        $variant:ident $(, $($tt:tt)* )?
126    ) => {
127        if let $name::$variant = $this {
128            $crate::__defile_expr__! {
129                $crate::__get_doc_string__!(@@unit $f, $(#[@$field_meta])*)
130            }
131        } else {
132            // process rest of the enum
133            $crate::__token_or_ok__!( $( $crate::__parse_enum_variant__!(@data $name $this $f @variant $( $tt )*) )? )
134        }
135    };
136
137    // trailing comma
138    (@data $_:ident $__:ident $___:ident @variant ,) => { unreachable!() };
139
140    // base case
141    (@data $_:ident $__:ident $___:ident @variant) => { unreachable!() };
142
143    // entry point
144    (
145        $( #[$meta:meta] )*
146        $vis:vis enum $name:ident {
147            $($tt:tt)*
148        }
149    ) => {
150        impl ::core::fmt::Display for $name {
151            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
152                $crate::__parse_enum_variant__!(@data $name self f @variant $($tt)*)
153            }
154        }
155    };
156}
157
158#[macro_export]
159#[doc(hidden)]
160macro_rules! __token_or_ok__ {
161    ($x:expr) => {
162        $x
163    };
164
165    () => {
166        ::core::result::Result::Ok(())
167    };
168}
169
170#[macro_export]
171#[doc(hidden)]
172macro_rules! __get_doc_string__ {
173    (@unit $f:ident, #[doc = $($doc:tt)*] $($rest:tt)*) => { $f.write_str(($($doc)*).trim()) };
174    (@unit $f:ident, #[$_:meta] $($rest:tt)*) => { $crate::__get_doc_string__!($f, $($rest)*) };
175    (@unit $f:ident,) => { Ok(()) };
176
177    (@struct $f:ident, #[doc = $($doc:tt)*] $($rest:tt)*) => {
178        $crate::__private::proc_macros::__struct_string__!($f, $($doc)*)
179    };
180    (@struct $f:ident, #[$_:meta] $($rest:tt)*) => { $crate::__get_doc_string__!($f, $($rest)*) };
181    (@struct $f:ident,) => { Ok(()) };
182}
183
184/// This macro is copied from the [`defile`](https://lib.rs/defile) crate
185#[macro_export]
186macro_rules! __defile_expr__ {
187    ( $($input:tt)* ) => (
188        #[allow(non_camel_case_types)]
189        {
190            #[derive($crate::__private::proc_macros::__expr_hack__)]
191            enum __defile__Hack__ {
192                __defile__Hack__ = (stringify!($($input)*), 42).1
193            }
194            __defile__Hack__!()
195        }
196    )
197}
198
199#[cfg(test)]
200mod tests {
201    extern crate std;
202    use std::string::{String, ToString};
203
204    use super::displaydoc;
205
206    displaydoc! {
207        /// Hello
208        #[derive(Debug)]
209        enum Error {
210            /// Hello
211            ///
212            /// How are you
213            Foo,
214            /// {s} is {x}
215            Bar { s: u8, x: String },
216            /// {_0} tuple {_2} works too {_1}
217            Baz(u8, u16, u32),
218            /// debug: {_0:?}
219            Debug(String),
220        }
221    }
222
223    #[test]
224    fn it_works() {
225        assert_eq!(Error::Foo.to_string(), "Hello");
226        assert_eq!(
227            Error::Bar {
228                s: 0,
229                x: String::from("hello")
230            }
231            .to_string(),
232            "0 is hello"
233        );
234        assert_eq!(Error::Baz(0, 1, 2).to_string(), "0 tuple 2 works too 1");
235        assert_eq!(Error::Debug("hallo".into()).to_string(), "debug: \"hallo\"");
236    }
237}