svp_client/
lib.rs

1//! # svp-client
2//!
3//! `svp-client` is a library to interact with the [SVP
4//! protocol](https://github.com/jelmer/silver-platter/blob/master/codemod-protocol.md), as supported by
5//! the `svp` command-line tool.
6
7use std::collections::HashMap;
8
9#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
10/// Behaviour for updating the changelog.
11pub struct ChangelogBehaviour {
12    #[serde(rename = "update")]
13    /// Whether the changelog should be updated.
14    pub update_changelog: bool,
15
16    /// Explanation for the decision.
17    pub explanation: String,
18}
19
20#[derive(Debug, serde::Serialize)]
21struct Failure {
22    pub result_code: String,
23    pub versions: HashMap<String, String>,
24    pub description: String,
25    pub transient: Option<bool>,
26}
27
28impl std::fmt::Display for Failure {
29    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
30        write!(f, "{}: {}", self.result_code, self.description)
31    }
32}
33
34impl std::error::Error for Failure {}
35
36impl std::fmt::Display for Success {
37    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
38        write!(f, "Success")
39    }
40}
41
42#[derive(Debug, serde::Serialize)]
43struct DebianContext {
44    pub changelog: Option<ChangelogBehaviour>,
45}
46
47#[derive(Debug, serde::Serialize)]
48struct Success {
49    pub versions: HashMap<String, String>,
50    pub value: Option<i32>,
51    pub context: Option<serde_json::Value>,
52    pub debian: Option<DebianContext>,
53    #[serde(rename = "target-branch-url")]
54    pub target_branch_url: Option<url::Url>,
55    #[serde(rename = "commit-message")]
56    pub commit_message: Option<String>,
57}
58
59/// Write a success to the SVP API
60fn write_svp_success(data: &Success) -> std::io::Result<()> {
61    if enabled() {
62        let f = std::fs::File::create(std::env::var("SVP_RESULT").unwrap()).unwrap();
63
64        Ok(serde_json::to_writer(f, data)?)
65    } else {
66        Ok(())
67    }
68}
69
70/// Write a failure to the SVP API
71fn write_svp_failure(data: &Failure) -> std::io::Result<()> {
72    if enabled() {
73        let f = std::fs::File::create(std::env::var("SVP_RESULT").unwrap()).unwrap();
74
75        Ok(serde_json::to_writer(f, data)?)
76    } else {
77        Ok(())
78    }
79}
80
81/// Report success
82pub fn report_success<T>(versions: HashMap<String, String>, value: Option<i32>, context: Option<T>)
83where
84    T: serde::Serialize,
85{
86    write_svp_success(&Success {
87        versions,
88        value,
89        context: context.map(|x| serde_json::to_value(x).unwrap()),
90        debian: None,
91        target_branch_url: None,
92        commit_message: None,
93    })
94    .unwrap();
95}
96
97/// Report success with Debian-specific context
98pub fn report_success_debian<T>(
99    versions: HashMap<String, String>,
100    value: Option<i32>,
101    context: Option<T>,
102    changelog: Option<ChangelogBehaviour>,
103) where
104    T: serde::Serialize,
105{
106    write_svp_success(&Success {
107        versions,
108        value,
109        context: context.map(|x| serde_json::to_value(x).unwrap()),
110        debian: Some(DebianContext { changelog }),
111        target_branch_url: None,
112        commit_message: None,
113    })
114    .unwrap();
115}
116
117/// Report that there is nothing to do
118pub fn report_nothing_to_do(
119    versions: HashMap<String, String>,
120    description: Option<&str>,
121    hint: Option<&str>,
122) -> ! {
123    let description = description.unwrap_or("Nothing to do");
124    write_svp_failure(&Failure {
125        result_code: "nothing-to-do".to_string(),
126        versions,
127        description: description.to_string(),
128        transient: None,
129    })
130    .unwrap();
131    log::error!("{}", description);
132    if let Some(hint) = hint {
133        log::info!("{}", hint);
134    }
135
136    std::process::exit(0);
137}
138
139/// Report a fatal error
140pub fn report_fatal(
141    versions: HashMap<String, String>,
142    code: &str,
143    description: &str,
144    hint: Option<&str>,
145    transient: Option<bool>,
146) -> ! {
147    write_svp_failure(&Failure {
148        result_code: code.to_string(),
149        versions,
150        description: description.to_string(),
151        transient,
152    })
153    .unwrap();
154    log::error!("{}", description);
155    if let Some(hint) = hint {
156        log::info!("{}", hint);
157    }
158    std::process::exit(1);
159}
160
161/// Load the resume file if it exists
162pub fn load_resume<T: serde::de::DeserializeOwned>() -> Option<T> {
163    if enabled() {
164        if let Ok(resume_path) = std::env::var("SVP_RESUME") {
165            let f = std::fs::File::open(resume_path).unwrap();
166            let resume: T = serde_json::from_reader(f).unwrap();
167            Some(resume)
168        } else {
169            None
170        }
171    } else {
172        None
173    }
174}
175
176/// Check if the SVP API is enabled
177pub fn enabled() -> bool {
178    std::env::var("SVP_API").ok().as_deref() == Some("1")
179}
180
181/// A reporter for the SVP API
182pub struct Reporter {
183    versions: HashMap<String, String>,
184    target_branch_url: Option<url::Url>,
185    commit_message: Option<String>,
186}
187
188impl Reporter {
189    /// Create a new reporter
190    pub fn new(versions: HashMap<String, String>) -> Self {
191        Self {
192            versions,
193            target_branch_url: None,
194            commit_message: None,
195        }
196    }
197
198    /// Check if the SVP API is enabled
199    pub fn enabled(&self) -> bool {
200        enabled()
201    }
202
203    /// Load the resume file if it exists
204    pub fn load_resume<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
205        load_resume()
206    }
207
208    /// Set the target branch URL
209    pub fn set_target_branch_url(&mut self, url: url::Url) {
210        self.target_branch_url = Some(url);
211    }
212
213    /// Set the commit message
214    pub fn set_commit_message(&mut self, message: String) {
215        self.commit_message = Some(message);
216    }
217
218    /// Report success
219    pub fn report_success<T>(self, value: Option<i32>, context: Option<T>)
220    where
221        T: serde::Serialize,
222    {
223        write_svp_success(&Success {
224            versions: self.versions,
225            value,
226            context: context.map(|x| serde_json::to_value(x).unwrap()),
227            debian: None,
228            target_branch_url: self.target_branch_url,
229            commit_message: self.commit_message,
230        })
231        .unwrap();
232    }
233
234    /// Report success with Debian-specific context
235    pub fn report_success_debian<T>(
236        self,
237        value: Option<i32>,
238        context: Option<T>,
239        changelog: Option<ChangelogBehaviour>,
240    ) where
241        T: serde::Serialize,
242    {
243        write_svp_success(&Success {
244            versions: self.versions,
245            value,
246            context: context.map(|x| serde_json::to_value(x).unwrap()),
247            debian: Some(DebianContext { changelog }),
248            target_branch_url: self.target_branch_url,
249            commit_message: self.commit_message,
250        })
251        .unwrap();
252    }
253
254    /// Report that there is nothing to do
255    pub fn report_nothing_to_do(self, description: Option<&str>, hint: Option<&str>) -> ! {
256        report_nothing_to_do(self.versions, description, hint);
257    }
258
259    /// Report a fatal error
260    pub fn report_fatal(
261        self,
262        code: &str,
263        description: &str,
264        hint: Option<&str>,
265        transient: Option<bool>,
266    ) -> ! {
267        report_fatal(self.versions, code, description, hint, transient);
268    }
269}