composer_parser/
lib.rs

1#![deny(unknown_lints)]
2#![deny(renamed_and_removed_lints)]
3#![forbid(unsafe_code)]
4#![deny(deprecated)]
5#![forbid(private_interfaces)]
6#![forbid(private_bounds)]
7#![forbid(non_fmt_panics)]
8#![deny(unreachable_code)]
9#![deny(unreachable_patterns)]
10#![forbid(unused_doc_comments)]
11#![forbid(unused_must_use)]
12#![deny(while_true)]
13#![deny(unused_parens)]
14#![deny(redundant_semicolons)]
15#![deny(non_ascii_idents)]
16#![deny(confusable_idents)]
17#![warn(missing_docs)]
18#![warn(clippy::missing_docs_in_private_items)]
19#![warn(clippy::cargo_common_metadata)]
20#![warn(rustdoc::missing_crate_level_docs)]
21#![deny(rustdoc::broken_intra_doc_links)]
22#![warn(missing_debug_implementations)]
23#![doc = include_str!("../README.md")]
24
25use thiserror::Error;
26
27use std::process::Command;
28use std::str::from_utf8;
29use tracing::{debug, warn};
30
31/// Error type for composer_parser
32#[derive(Debug, Error)]
33pub enum Error {
34    /// This means something went wrong when we were parsing the JSON output
35    /// of the program
36    #[error("Error parsing JSON: {0}")]
37    SerdeJsonError(#[from] serde_json::Error),
38    /// This means the output of the program contained some string that was not
39    /// valid UTF-8
40    #[error("Error interpreting program output as UTF-8: {0}")]
41    Utf8Error(#[from] std::str::Utf8Error),
42    /// This is likely to be an error when executing the program using std::process
43    #[error("I/O Error: {0}")]
44    StdIoError(#[from] std::io::Error),
45}
46
47/// These are options to modify the behaviour of the program.
48#[derive(Debug, clap::Parser)]
49pub struct ComposerOutdatedOptions {
50    /// Dependencies that should be ignored
51    #[clap(
52        short = 'i',
53        long = "ignore",
54        value_name = "PACKAGE_NAME",
55        number_of_values = 1,
56        help = "Dependencies that should be ignored"
57    )]
58    ignored_packages: Vec<String>,
59}
60
61/// Outer structure for parsing composer-outdated output
62#[derive(Debug, serde::Serialize, serde::Deserialize)]
63pub struct ComposerOutdatedData {
64    /// Since we call composer oudated with --locked it returns all package
65    /// information in this field
66    pub locked: Vec<PackageStatus>,
67}
68
69/// Inner, per-package structure when parsing composer-outdated output
70#[derive(Debug, serde::Serialize, serde::Deserialize)]
71pub struct PackageStatus {
72    /// Package name
73    pub name: String,
74    /// Package version in use
75    pub version: String,
76    /// Latest package version available
77    pub latest: String,
78    /// Is an update required and if so, is it semver-compatible or not
79    #[serde(rename = "latest-status")]
80    pub latest_status: UpdateRequirement,
81    /// Decsription for the package
82    pub description: String,
83    /// Further notes, e.g. if a package has been abandonded
84    pub warning: Option<String>,
85}
86
87/// What kind of update, if any, is required for a package
88#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
89#[serde(rename_all = "kebab-case")]
90pub enum UpdateRequirement {
91    /// No update is required
92    UpToDate,
93    /// An update is required but it is semver-compatible to the version in use
94    SemverSafeUpdate,
95    /// An update is required to a version that is not semver-compatible
96    UpdatePossible,
97}
98
99impl std::fmt::Display for UpdateRequirement {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            UpdateRequirement::UpToDate => {
103                write!(f, "up-to-date")
104            }
105            UpdateRequirement::SemverSafeUpdate => {
106                write!(f, "semver-safe-update")
107            }
108            UpdateRequirement::UpdatePossible => {
109                write!(f, "update-possible")
110            }
111        }
112    }
113}
114
115/// What the exit code indicated about required updates
116#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
117pub enum IndicatedUpdateRequirement {
118    /// No update is required
119    UpToDate,
120    /// An update is required
121    UpdateRequired,
122}
123
124impl std::fmt::Display for IndicatedUpdateRequirement {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        match self {
127            IndicatedUpdateRequirement::UpToDate => {
128                write!(f, "up-to-date")
129            }
130            IndicatedUpdateRequirement::UpdateRequired => {
131                write!(f, "update-required")
132            }
133        }
134    }
135}
136
137/// main entry point for the composer-oudated call
138pub fn outdated(
139    options: &ComposerOutdatedOptions,
140) -> Result<(IndicatedUpdateRequirement, ComposerOutdatedData), Error> {
141    let mut cmd = Command::new("composer");
142
143    cmd.args([
144        "outdated",
145        "-f",
146        "json",
147        "--no-plugins",
148        "--strict",
149        "--locked",
150        "-m",
151    ]);
152
153    for package_name in &options.ignored_packages {
154        cmd.args(["--ignore", package_name]);
155    }
156
157    let output = cmd.output()?;
158
159    if !output.status.success() {
160        warn!(
161            "composer outdated did not return with a successful exit code: {}",
162            output.status
163        );
164        debug!("stdout:\n{}", from_utf8(&output.stdout)?);
165        if !output.stderr.is_empty() {
166            warn!("stderr:\n{}", from_utf8(&output.stderr)?);
167        }
168    }
169
170    let update_requirement = if output.status.success() {
171        IndicatedUpdateRequirement::UpToDate
172    } else {
173        IndicatedUpdateRequirement::UpdateRequired
174    };
175
176    let json_str = from_utf8(&output.stdout)?;
177    let data: ComposerOutdatedData = serde_json::from_str(json_str)?;
178    Ok((update_requirement, data))
179}
180
181#[cfg(test)]
182mod test {
183    use super::*;
184    //use pretty_assertions::{assert_eq, assert_ne};
185
186    /// this test requires a composer.json and composer.lock in the main crate
187    /// directory (working dir of the tests)
188    #[test]
189    fn test_run_composer_outdated() -> Result<(), Error> {
190        outdated(&ComposerOutdatedOptions {
191            ignored_packages: vec![],
192        })?;
193        Ok(())
194    }
195}