typescript_tools/
pin.rs

1use std::collections::HashMap;
2use std::fmt::Display;
3use std::path::Path;
4
5use crate::configuration_file::{ConfigurationFile, WriteError};
6use crate::io::FromFileError;
7use crate::monorepo_manifest::{EnumeratePackageManifestsError, MonorepoManifest};
8use crate::package_manifest::{DependencyGroup, PackageManifest};
9use crate::types::PackageName;
10use crate::unpinned_dependencies::{UnpinnedDependency, UnpinnedMonorepoDependencies};
11
12#[derive(Debug)]
13#[non_exhaustive]
14pub struct PinError {
15    pub kind: PinErrorKind,
16}
17
18impl Display for PinError {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match &self.kind {
21            PinErrorKind::NonStringVersionNumber {
22                package_name,
23                dependency_name,
24            } => {
25                write!(f, "unable to parse `{}` package.json: encountered non-string version for dependency `{}`", package_name, dependency_name)
26            }
27            _ => write!(f, "error pinning dependency versions"),
28        }
29    }
30}
31
32impl std::error::Error for PinError {
33    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
34        match &self.kind {
35            PinErrorKind::FromFile(err) => Some(err),
36            PinErrorKind::EnumeratePackageManifests(err) => Some(err),
37            PinErrorKind::Write(err) => Some(err),
38            PinErrorKind::NonStringVersionNumber {
39                package_name: _,
40                dependency_name: _,
41            } => None,
42        }
43    }
44}
45
46impl From<FromFileError> for PinError {
47    fn from(err: FromFileError) -> Self {
48        Self {
49            kind: PinErrorKind::FromFile(err),
50        }
51    }
52}
53
54impl From<EnumeratePackageManifestsError> for PinError {
55    fn from(err: EnumeratePackageManifestsError) -> Self {
56        Self {
57            kind: PinErrorKind::EnumeratePackageManifests(err),
58        }
59    }
60}
61
62impl From<WriteError> for PinError {
63    fn from(err: WriteError) -> Self {
64        Self {
65            kind: PinErrorKind::Write(err),
66        }
67    }
68}
69
70impl From<PinErrorKind> for PinError {
71    fn from(kind: PinErrorKind) -> Self {
72        Self { kind }
73    }
74}
75
76#[derive(Debug)]
77pub enum PinErrorKind {
78    #[non_exhaustive]
79    FromFile(FromFileError),
80    #[non_exhaustive]
81    EnumeratePackageManifests(EnumeratePackageManifestsError),
82    #[non_exhaustive]
83    Write(WriteError),
84    #[non_exhaustive]
85    NonStringVersionNumber {
86        package_name: PackageName,
87        dependency_name: String,
88    },
89}
90
91fn needs_modification<'a, 'b>(
92    dependency_name: &'a String,
93    dependency_version: &'a String,
94    package_version_by_package_name: &'b HashMap<PackageName, String>,
95) -> Option<&'b String> {
96    package_version_by_package_name
97        .get(&PackageName::from(dependency_name))
98        .and_then(|expected| match expected == dependency_version {
99            true => None,
100            false => Some(expected),
101        })
102}
103
104fn get_dependency_group_mut<'a>(
105    package_manifest: &'a mut PackageManifest,
106    dependency_group: &str,
107) -> Option<&'a mut serde_json::Map<String, serde_json::Value>> {
108    package_manifest
109        .contents
110        .extra_fields
111        .get_mut(dependency_group)
112        .and_then(serde_json::Value::as_object_mut)
113}
114
115pub fn modify<P>(root: P) -> Result<(), PinError>
116where
117    P: AsRef<Path>,
118{
119    let root = root.as_ref();
120    let lerna_manifest = MonorepoManifest::from_directory(root)?;
121
122    let package_manifest_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
123
124    let package_version_by_package_name: HashMap<PackageName, String> =
125        package_manifest_by_package_name
126            .values()
127            .map(|package| {
128                (
129                    package.contents.name.clone(),
130                    package.contents.version.clone(),
131                )
132            })
133            .collect();
134
135    for (package_name, mut package_manifest) in package_manifest_by_package_name {
136        let mut dirty = false;
137        for dependency_group in DependencyGroup::VALUES {
138            let dependencies = get_dependency_group_mut(&mut package_manifest, dependency_group);
139            if dependencies.is_none() {
140                continue;
141            }
142            let dependencies = dependencies.unwrap();
143
144            dependencies
145                .into_iter()
146                .try_for_each(
147                    |(dependency_name, dependency_version)| match &dependency_version {
148                        serde_json::Value::String(dep_version) => {
149                            if let Some(expected) = needs_modification(
150                                dependency_name,
151                                dep_version,
152                                &package_version_by_package_name,
153                            ) {
154                                *dependency_version = expected.to_owned().into();
155                                dirty = true;
156                            }
157                            Ok(())
158                        }
159                        _ => Err(PinErrorKind::NonStringVersionNumber {
160                            package_name: package_name.clone(),
161                            dependency_name: dependency_name.to_owned(),
162                        }),
163                    },
164                )?;
165        }
166
167        if dirty {
168            PackageManifest::write(root, package_manifest)?
169        }
170    }
171
172    Ok(())
173}
174
175#[derive(Debug)]
176#[non_exhaustive]
177pub struct PinLintError {
178    pub kind: PinLintErrorKind,
179}
180
181impl Display for PinLintError {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match &self.kind {
184            PinLintErrorKind::NonStringVersionNumber {
185                package_name,
186                dependency_name,
187            } => {
188                write!(f, "unable to parse `{}` package.json: encountered non-string version for dependency `{}`", package_name, dependency_name)
189            }
190            PinLintErrorKind::UnpinnedDependencies(unpinned_dependencies) => {
191                writeln!(f, "found unpinned dependency versions\n")?;
192                write!(f, "{}", unpinned_dependencies)
193            }
194            _ => write!(f, "error linting internal dependency versions"),
195        }
196    }
197}
198
199impl std::error::Error for PinLintError {
200    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
201        match &self.kind {
202            PinLintErrorKind::FromFile(err) => Some(err),
203            PinLintErrorKind::EnumeratePackageManifests(err) => Some(err),
204            PinLintErrorKind::NonStringVersionNumber {
205                package_name: _,
206                dependency_name: _,
207            } => None,
208            PinLintErrorKind::UnpinnedDependencies(_) => None,
209        }
210    }
211}
212
213impl From<FromFileError> for PinLintError {
214    fn from(err: FromFileError) -> Self {
215        Self {
216            kind: PinLintErrorKind::FromFile(err),
217        }
218    }
219}
220
221impl From<EnumeratePackageManifestsError> for PinLintError {
222    fn from(err: EnumeratePackageManifestsError) -> Self {
223        Self {
224            kind: PinLintErrorKind::EnumeratePackageManifests(err),
225        }
226    }
227}
228
229impl From<PinLintErrorKind> for PinLintError {
230    fn from(kind: PinLintErrorKind) -> Self {
231        Self { kind }
232    }
233}
234
235#[derive(Debug)]
236pub enum PinLintErrorKind {
237    #[non_exhaustive]
238    FromFile(FromFileError),
239    #[non_exhaustive]
240    EnumeratePackageManifests(EnumeratePackageManifestsError),
241    #[non_exhaustive]
242    NonStringVersionNumber {
243        package_name: PackageName,
244        dependency_name: PackageName,
245    },
246    #[non_exhaustive]
247    UnpinnedDependencies(UnpinnedMonorepoDependencies),
248}
249
250fn get_unpinned_dependency(
251    dependency_name: PackageName,
252    dependency_version: &String,
253    package_version_by_package_name: &HashMap<PackageName, String>,
254) -> Option<UnpinnedDependency> {
255    package_version_by_package_name
256        .get(&dependency_name)
257        .and_then(|expected| match expected == dependency_version {
258            true => None,
259            false => Some(UnpinnedDependency {
260                name: dependency_name.to_owned(),
261                actual: dependency_version.to_owned(),
262                expected: expected.to_owned(),
263            }),
264        })
265}
266
267pub fn lint<P>(root: P) -> Result<(), PinLintError>
268where
269    P: AsRef<Path>,
270{
271    let root = root.as_ref();
272    let lerna_manifest = MonorepoManifest::from_directory(root)?;
273
274    let package_manifest_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
275
276    let package_version_by_package_name: HashMap<PackageName, String> =
277        package_manifest_by_package_name
278            .values()
279            .map(|package| {
280                (
281                    package.contents.name.clone(),
282                    package.contents.version.clone(),
283                )
284            })
285            .collect();
286
287    let unpinned_dependencies: UnpinnedMonorepoDependencies = package_manifest_by_package_name
288        .into_iter()
289        .map(|(package_name, package_manifest)| {
290            let unpinned_deps = package_manifest
291                .dependencies_iter()
292                .filter_map(|(dependency_name, dependency_version)| -> Option<Result<UnpinnedDependency, PinLintErrorKind>> {
293                    match dependency_version {
294                        serde_json::Value::String(dep_version) => {
295                            get_unpinned_dependency(
296                                dependency_name,
297                                dep_version,
298                                &package_version_by_package_name,
299                            ).map(Ok)
300                        }
301                        _ => Some(Err(PinLintErrorKind::NonStringVersionNumber {
302                            package_name: package_name.clone(),
303                            dependency_name: dependency_name.to_owned(),
304                        })),
305                    }
306                })
307                .collect::<Result<_, _>>()?;
308            Ok((package_manifest.path(), unpinned_deps))
309        })
310        .collect::<Result<_, PinLintErrorKind>>()?;
311
312    match unpinned_dependencies.is_empty() {
313        true => Ok(()),
314        false => Err(PinLintErrorKind::UnpinnedDependencies(
315            unpinned_dependencies,
316        ))?,
317    }
318}