composer_parser/
lib.rs

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