guppy_summaries/
report.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    diff::{changed_sort_key, PackageDiff, SummaryDiff, SummaryDiffStatus},
6    SummaryId,
7};
8use std::fmt;
9
10/// A report of a diff between two summaries.
11///
12/// This report can be generated or written to a file through `fmt::Display`.
13#[derive(Clone, Debug)]
14pub struct SummaryReport<'a, 'b> {
15    diff: &'b SummaryDiff<'a>,
16    sorted_target: Vec<(&'a SummaryId, &'b SummaryDiffStatus<'a>)>,
17    sorted_host: Vec<(&'a SummaryId, &'b SummaryDiffStatus<'a>)>,
18}
19
20impl<'a, 'b> SummaryReport<'a, 'b> {
21    /// Creates a new `SummaryReport` that can be displayed.
22    pub fn new(diff: &'b SummaryDiff<'a>) -> Self {
23        let sorted_target = Self::make_sorted(&diff.target_packages);
24        let sorted_host = Self::make_sorted(&diff.host_packages);
25
26        Self {
27            diff,
28            sorted_target,
29            sorted_host,
30        }
31    }
32
33    fn make_sorted(
34        packages: &'b PackageDiff<'a>,
35    ) -> Vec<(&'a SummaryId, &'b SummaryDiffStatus<'a>)> {
36        let mut v: Vec<_> = packages
37            .changed
38            .iter()
39            .map(|(summary_id, status)| (*summary_id, status))
40            .collect();
41        v.sort_by_key(|(summary_id, status)| changed_sort_key(summary_id, status));
42
43        v
44    }
45}
46
47impl fmt::Display for SummaryReport<'_, '_> {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        if !self.diff.target_packages.is_unchanged() {
50            writeln!(
51                f,
52                "target packages:\n{}",
53                PackageReport::new(&self.diff.target_packages, &self.sorted_target)
54            )?;
55        }
56        if !self.diff.host_packages.is_unchanged() {
57            writeln!(
58                f,
59                "host packages:\n{}",
60                PackageReport::new(&self.diff.host_packages, &self.sorted_host)
61            )?;
62        }
63
64        Ok(())
65    }
66}
67
68// Collapse the lifetime params into one because three is too annoying, all the params here are
69// covariant anyway, and this is an internal struct.
70struct PackageReport<'x> {
71    package_diff: &'x PackageDiff<'x>,
72    sorted: &'x [(&'x SummaryId, &'x SummaryDiffStatus<'x>)],
73}
74
75impl<'x> PackageReport<'x> {
76    fn new(
77        package_diff: &'x PackageDiff<'x>,
78        sorted: &'x [(&'x SummaryId, &'x SummaryDiffStatus<'x>)],
79    ) -> Self {
80        Self {
81            package_diff,
82            sorted,
83        }
84    }
85}
86
87impl fmt::Display for PackageReport<'_> {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        for (summary_id, status) in self.sorted {
90            write!(
91                f,
92                "  {} {} {} ({}, {})",
93                status.tag(),
94                summary_id.name,
95                summary_id.version,
96                status.latest_status(),
97                summary_id.source
98            )?;
99
100            // Print out other versions if available.
101            if let Some(unchanged_list) = self.package_diff.unchanged.get(summary_id.name.as_str())
102            {
103                write!(f, " (other versions: ")?;
104                display_list(f, unchanged_list.iter().map(|(version, _, _)| *version))?;
105                write!(f, ")")?;
106            }
107
108            writeln!(f)?;
109
110            match status {
111                SummaryDiffStatus::Added { info } => {
112                    write!(f, "    * features: ")?;
113                    display_list(f, &info.features)?;
114                    writeln!(f)?;
115                }
116                SummaryDiffStatus::Removed { old_info } => {
117                    write!(f, "    * (old features: ")?;
118                    display_list(f, &old_info.features)?;
119                    writeln!(f, ")")?;
120                }
121                SummaryDiffStatus::Modified {
122                    old_version,
123                    old_source,
124                    old_status,
125                    // The new status is printed in the package header.
126                    new_status: _,
127                    added_features,
128                    removed_features,
129                    unchanged_features,
130                    added_optional_deps,
131                    removed_optional_deps,
132                    unchanged_optional_deps,
133                } => {
134                    if let Some(old_version) = old_version {
135                        let change_str = if summary_id.version > **old_version {
136                            "upgraded"
137                        } else {
138                            "DOWNGRADED"
139                        };
140                        writeln!(f, "    * version {} from {}", change_str, old_version)?;
141                    }
142                    if let Some(old_source) = old_source {
143                        writeln!(f, "    * source changed from {}", old_source)?;
144                    }
145                    if let Some(old_status) = old_status {
146                        writeln!(f, "    * status changed from {}", old_status)?;
147                    }
148
149                    // ---
150
151                    if !added_features.is_empty() {
152                        write!(f, "    * added features: ")?;
153                        display_list(f, added_features.iter().copied())?;
154                        writeln!(f)?;
155                    }
156                    if !removed_features.is_empty() {
157                        write!(f, "    * removed features: ")?;
158                        display_list(f, removed_features.iter().copied())?;
159                        writeln!(f)?;
160                    }
161                    write!(f, "    * (unchanged features: ")?;
162                    display_list(f, unchanged_features.iter().copied())?;
163                    writeln!(f, ")")?;
164
165                    // ---
166
167                    if !added_optional_deps.is_empty() {
168                        write!(f, "    * added optional dependencies: ")?;
169                        display_list(f, added_optional_deps.iter().copied())?;
170                        writeln!(f)?;
171                    }
172                    if !removed_optional_deps.is_empty() {
173                        write!(f, "    * removed optional dependencies: ")?;
174                        display_list(f, removed_optional_deps.iter().copied())?;
175                        writeln!(f)?;
176                    }
177                    write!(f, "    * (unchanged optional dependencies: ")?;
178                    display_list(f, unchanged_optional_deps.iter().copied())?;
179                    writeln!(f, ")")?;
180                }
181            }
182        }
183
184        Ok(())
185    }
186}
187
188fn display_list<I>(f: &mut fmt::Formatter, items: I) -> fmt::Result
189where
190    I: IntoIterator,
191    I::Item: fmt::Display,
192    I::IntoIter: ExactSizeIterator,
193{
194    let items = items.into_iter();
195    let len = items.len();
196    if len == 0 {
197        write!(f, "[none]")?;
198    }
199
200    for (idx, item) in items.enumerate() {
201        write!(f, "{}", item)?;
202        // Add a comma for all items except the last one.
203        if idx + 1 < len {
204            write!(f, ", ")?;
205        }
206    }
207
208    Ok(())
209}