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#[derive(Debug, Error)]
11pub enum Error {
12 #[error("Error parsing JSON: {0}")]
15 SerdeJsonError(#[from] serde_json::Error),
16 #[error("Error interpreting program output as UTF-8: {0}")]
19 Utf8Error(#[from] std::str::Utf8Error),
20 #[error("I/O Error: {0}")]
22 StdIoError(#[from] std::io::Error),
23}
24
25#[derive(Debug, clap::Parser)]
27pub struct ComposerOutdatedOptions {
28 #[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#[derive(Debug, serde::Serialize, serde::Deserialize)]
41pub struct ComposerOutdatedData {
42 pub locked: Vec<PackageStatus>,
45}
46
47#[derive(Debug, serde::Serialize, serde::Deserialize)]
49pub struct PackageStatus {
50 pub name: String,
52 pub version: String,
54 pub latest: String,
56 #[serde(rename = "latest-status")]
58 pub latest_status: UpdateRequirement,
59 pub description: String,
61 pub warning: Option<String>,
63}
64
65#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
67#[serde(rename_all = "kebab-case")]
68pub enum UpdateRequirement {
69 UpToDate,
71 SemverSafeUpdate,
73 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#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
95pub enum IndicatedUpdateRequirement {
96 UpToDate,
98 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
115pub 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 #[test]
171 fn test_run_composer_outdated() -> Result<(), Error> {
172 outdated(&ComposerOutdatedOptions {
173 ignored_packages: vec![],
174 })?;
175 Ok(())
176 }
177}