1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use std::{fmt::Display, path::PathBuf};

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub(crate) struct UnpinnedDependency {
    pub name: String,
    pub actual: String,
    pub expected: String,
}

impl Display for UnpinnedDependency {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "dependency: {}\texpected: {}\tgot: {}",
            self.name, self.expected, self.actual
        )
    }
}

#[derive(Clone, Debug)]
pub(crate) struct UnpinnedPackageDependencies(PathBuf, Vec<UnpinnedDependency>);

impl Display for UnpinnedPackageDependencies {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(
            f,
            "File contains unpinned dependency versions: {:?}",
            self.0
        )?;
        for unpinned_dependency in self.1.iter() {
            writeln!(f, "\t{}", unpinned_dependency)?;
        }
        Ok(())
    }
}

impl From<(PathBuf, Vec<UnpinnedDependency>)> for UnpinnedPackageDependencies {
    fn from(value: (PathBuf, Vec<UnpinnedDependency>)) -> Self {
        Self(value.0, value.1)
    }
}

#[derive(Clone, Debug)]
pub struct UnpinnedMonorepoDependencies(Vec<UnpinnedPackageDependencies>);

impl Display for UnpinnedMonorepoDependencies {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for unpinned_package_dependencies in self.0.iter() {
            write!(f, "{}", unpinned_package_dependencies)?;
        }
        Ok(())
    }
}

impl From<Vec<UnpinnedPackageDependencies>> for UnpinnedMonorepoDependencies {
    fn from(value: Vec<UnpinnedPackageDependencies>) -> Self {
        Self(value)
    }
}

impl FromIterator<(PathBuf, Vec<UnpinnedDependency>)> for UnpinnedMonorepoDependencies {
    fn from_iter<T: IntoIterator<Item = (PathBuf, Vec<UnpinnedDependency>)>>(iter: T) -> Self {
        let collection = iter
            .into_iter()
            .filter(|(_package_name, unpinned_dependencies)| !unpinned_dependencies.is_empty())
            .map(Into::into)
            .collect();
        Self(collection)
    }
}

impl UnpinnedMonorepoDependencies {
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::{UnpinnedDependency, UnpinnedMonorepoDependencies, UnpinnedPackageDependencies};

    #[test]
    fn should_display_all_data() {
        let unpinned: UnpinnedMonorepoDependencies = vec![UnpinnedPackageDependencies(
            PathBuf::from("packages/a/package.json"),
            vec![
                UnpinnedDependency {
                    name: "one".into(),
                    actual: "0.0.0".into(),
                    expected: "2.0.0".into(),
                },
                UnpinnedDependency {
                    name: "two".into(),
                    actual: "0.0.0".into(),
                    expected: "2.0.0".into(),
                },
            ],
        )]
        .into();

        let expected = r#"
File contains unpinned dependency versions: "packages/a/package.json"
	dependency: one	expected: 2.0.0	got: 0.0.0
	dependency: two	expected: 2.0.0	got: 0.0.0
"#
        .trim_start();
        let actual = format!("{}", unpinned);
        assert_eq!(expected, actual);
    }
}