vergen_pretty/pretty/
mod.rs

1// Copyright (c) 2022 vergen developers
2//
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. All files in the project carrying such notice may not be copied,
7// modified, or distributed except according to those terms.
8
9use self::{prefix::Prefix, suffix::Suffix};
10use crate::utils::{has_value, split_key, split_kv};
11use anyhow::Result;
12use bon::Builder;
13#[cfg(feature = "color")]
14use console::Style;
15use std::{collections::BTreeMap, io::Write};
16#[cfg(feature = "trace")]
17use tracing::Level;
18
19pub(crate) mod feature;
20pub(crate) mod prefix;
21pub(crate) mod suffix;
22
23/// Configuration for `vergen` environment variable pretty printing
24///
25/// Build this with [`Pretty`]
26///
27/// # Display
28/// ```
29/// # use anyhow::Result;
30/// # use vergen_pretty::{vergen_pretty_env, Pretty};
31/// #
32/// # pub fn main() -> Result<()> {
33/// let mut stdout = vec![];
34/// Pretty::builder()
35///     .env(vergen_pretty_env!())
36///     .build()
37///     .display(&mut stdout)?;
38/// #     Ok(())
39/// # }
40/// ```
41///
42#[cfg_attr(
43    feature = "trace",
44    doc = r"
45# Trace
46
47Generate [`tracing`] output
48
49```
50# use vergen_pretty::{vergen_pretty_env, Pretty};
51# 
52# pub fn main() {
53Pretty::builder()
54    .env(vergen_pretty_env!())
55    .build()
56    .trace();
57# }
58```
59"
60)]
61///
62/// # Output a prefix/suffix
63///
64/// [`Prefix`] and [`Suffix`] also have configurable styles if you enable
65/// the `color` feature
66///
67/// ```
68/// # use anyhow::Result;
69/// # use vergen_pretty::{vergen_pretty_env, Pretty, Prefix, Suffix};
70/// #
71/// const TEST_PREFIX: &str = r#"A wonderful introduction
72/// "#;
73/// const TEST_SUFFIX: &str = r#"An outro"#;
74///
75/// # pub fn main() -> Result<()> {
76/// let mut stdout = vec![];
77/// let prefix = Prefix::builder()
78///     .lines(TEST_PREFIX.lines().map(str::to_string).collect())
79///     .build();
80/// let suffix = Suffix::builder()
81///     .lines(TEST_SUFFIX.lines().map(str::to_string).collect())
82///     .build();
83/// Pretty::builder()
84///     .env(vergen_pretty_env!())
85///     .prefix(prefix)
86///     .suffix(suffix)
87///     .build()
88///     .display(&mut stdout)?;
89/// #     Ok(())
90/// # }
91/// ```
92///
93#[cfg_attr(
94    feature = "color",
95    doc = r"
96# Customize Colorized Output
97
98Uses [`Style`](console::Style) to colorize output
99
100```
101# use anyhow::Result;
102# use vergen_pretty::{vergen_pretty_env, Pretty, Style};
103# 
104# pub fn main() -> Result<()> {
105let mut stdout = vec![];
106let red_bold = Style::new().bold().red();
107let yellow_bold = Style::new().bold().yellow();
108Pretty::builder()
109    .env(vergen_pretty_env!())
110    .key_style(red_bold)
111    .value_style(yellow_bold)
112    .build()
113    .display(&mut stdout)?;
114#     Ok(())
115# }
116```
117"
118)]
119///
120#[derive(Builder, Clone, Debug, PartialEq)]
121pub struct Pretty {
122    #[builder(field)]
123    vars: Vec<(String, String, String)>,
124    #[builder(field)]
125    max_label: usize,
126    #[builder(field)]
127    max_category: usize,
128
129    /// Set the optional [`Prefix`] configuration
130    prefix: Option<Prefix>,
131    /// The `vergen` env variables.  Should be set with [`vergen_pretty_env!`](crate::vergen_pretty_env) macro.
132    env: BTreeMap<&'static str, Option<&'static str>>,
133    /// A set of `vergen` env variable names that should be filtered regardless if they are set or not.
134    filter: Option<Vec<&'static str>>,
135    /// Include category output.  Default: true
136    #[builder(default = true)]
137    category: bool,
138    /// The [`Style`] to apply to the keys in the output
139    #[cfg(feature = "color")]
140    key_style: Option<Style>,
141    /// The [`Style`] to apply to the values in the output
142    #[cfg(feature = "color")]
143    value_style: Option<Style>,
144    /// Set the optional [`Suffix`] configuration
145    suffix: Option<Suffix>,
146    /// Set the tracing [`Level`]
147    #[cfg(feature = "trace")]
148    #[builder(default = Level::INFO)]
149    level: Level,
150    /// Flatten the serde output if no prefix/suffix are defined. Default: false
151    #[cfg(feature = "serde")]
152    #[builder(default = false)]
153    flatten: bool,
154}
155
156impl Pretty {
157    /// Write the `vergen` environment variables that are set in table format to
158    /// the given [`writer`](std::io::Write)
159    ///
160    /// # Errors
161    /// * The [`writeln!`](std::writeln!) macro can throw a [`std::io::Error`]
162    ///
163    pub fn display<T>(mut self, writer: &mut T) -> Result<()>
164    where
165        T: Write + ?Sized,
166    {
167        self.populate_fmt();
168
169        if let Some(prefix) = &self.prefix {
170            prefix.display(writer)?;
171        }
172
173        for (category, label, value) in &self.vars {
174            let max_label = self.max_label;
175            let max_category = self.max_category;
176            let key = if self.category {
177                format!("{label:>max_label$} ({category:>max_category$})")
178            } else {
179                format!("{label:>max_label$}")
180            };
181            self.inner_display(writer, &key, value)?;
182        }
183
184        if let Some(suffix) = &self.suffix {
185            suffix.display(writer)?;
186        }
187
188        Ok(())
189    }
190
191    fn populate_fmt(&mut self) {
192        let filter_fn = |tuple: &(&'static str, &'static str)| -> bool {
193            let (key, _) = tuple;
194            if let Some(filter) = &self.filter {
195                !filter.contains(key)
196            } else {
197                true
198            }
199        };
200        let vm_iter: Vec<(String, String, String)> = self
201            .env
202            .iter()
203            .filter_map(has_value)
204            .filter(filter_fn)
205            .filter_map(split_key)
206            .filter_map(split_kv)
207            .collect();
208        let max_label = vm_iter
209            .iter()
210            .map(|(_, label, _)| label.len())
211            .max()
212            .map_or_else(|| 16, |x| x);
213        let max_category = vm_iter
214            .iter()
215            .map(|(category, _, _)| category.len())
216            .max()
217            .map_or_else(|| 7, |x| x);
218        self.vars = vm_iter;
219        self.max_label = max_label;
220        self.max_category = max_category;
221    }
222
223    #[cfg(not(feature = "color"))]
224    #[allow(clippy::unused_self)]
225    fn inner_display<T>(&self, writer: &mut T, key: &str, value: &str) -> Result<()>
226    where
227        T: Write + ?Sized,
228    {
229        Ok(writeln!(writer, "{key}: {value}")?)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::Pretty;
236    use crate::{utils::test_utils::is_empty, vergen_pretty_env};
237    use anyhow::Result;
238    use std::io::Write;
239
240    #[test]
241    #[allow(clippy::clone_on_copy, clippy::redundant_clone)]
242    fn pretty_clone_works() {
243        let map = vergen_pretty_env!();
244        let pretty = Pretty::builder().env(map).build();
245        let another = pretty.clone();
246        assert_eq!(pretty, another);
247    }
248
249    #[test]
250    fn pretty_debug_works() -> Result<()> {
251        let map = vergen_pretty_env!();
252        let pretty = Pretty::builder().env(map).build();
253        let mut buf = vec![];
254        write!(buf, "{pretty:?}")?;
255        assert!(!buf.is_empty());
256        Ok(())
257    }
258
259    #[test]
260    fn default_display_works() -> Result<()> {
261        let mut stdout = vec![];
262        let map = vergen_pretty_env!();
263        let empty = is_empty(&map);
264        let fmt = Pretty::builder().env(map).build();
265        fmt.display(&mut stdout)?;
266        if empty {
267            assert!(stdout.is_empty());
268        } else {
269            assert!(!stdout.is_empty());
270        }
271        Ok(())
272    }
273
274    #[test]
275    fn no_category_works() -> Result<()> {
276        let mut stdout = vec![];
277        let map = vergen_pretty_env!();
278        let empty = is_empty(&map);
279        let fmt = Pretty::builder().env(map).category(false).build();
280        fmt.display(&mut stdout)?;
281        if empty {
282            assert!(stdout.is_empty());
283        } else {
284            assert!(!stdout.is_empty());
285        }
286        Ok(())
287    }
288
289    #[test]
290    fn custom_display_works() -> Result<()> {
291        let mut stdout = vec![];
292        let map = vergen_pretty_env!("vergen-cl");
293        let empty = is_empty(&map);
294        let fmt = Pretty::builder().env(map).build();
295        fmt.display(&mut stdout)?;
296        if empty {
297            assert!(stdout.is_empty());
298        } else {
299            assert!(!stdout.is_empty());
300        }
301        Ok(())
302    }
303}