apt_edsp/
answer.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use super::scenario::{Package, Version};
6
7/// A stanza telling APT to install a specific new package, or to upgrade or downgrade a package
8/// to a specific version.
9#[derive(Serialize, Deserialize, Debug, Default, Eq, PartialEq)]
10#[serde(rename_all = "PascalCase")]
11pub struct Install {
12    /// The identifier of the package to install.
13    ///
14    /// Must reference the identifier of a package in the package universe
15    /// (see [`Package::id`]).
16    pub install: String,
17
18    /// The name of the package to install.
19    ///
20    /// While optional, it is highly recommend to set this field to the value of the field
21    /// ([`Package::package`]) of the corresponding
22    /// package in the package universe.
23    pub package: Option<String>,
24
25    /// The version of the package to install.
26    ///
27    /// While optional, it is highly recommend to set this field to the value of the field
28    /// ([`Package::version`]) of the corresponding
29    /// package in the package universe.
30    pub version: Option<Version>,
31
32    /// The architecture of the package to install.
33    ///
34    /// While optional, it is highly recommend to set this field to the value of the field
35    /// ([`Package::architecture`]) of the corresponding
36    /// package in the package universe.
37    pub architecture: Option<String>,
38
39    /// Extra optional fields supported by [`Package`] stanzas.
40    #[serde(flatten)]
41    pub extra: HashMap<String, String>,
42}
43
44/// A stanza telling APT to remove a specific package.
45#[derive(Serialize, Deserialize, Debug, Default, Eq, PartialEq)]
46#[serde(rename_all = "PascalCase")]
47pub struct Remove {
48    /// The identifier of the package to remove.
49    ///
50    /// Must reference the identifier of a package in the package universe
51    /// (see [`Package::id`]).
52    pub remove: String,
53
54    /// The name of the package to remove.
55    ///
56    /// While optional, it is highly recommend to set this field to the value of the field
57    /// ([`Package::package`]) of the corresponding
58    /// package in the package universe.
59    pub package: Option<String>,
60
61    /// The version of the package to remove.
62    ///
63    /// While optional, it is highly recommend to set this field to the value of the field
64    /// ([`Package::version`]) of the corresponding
65    /// package in the package universe.
66    pub version: Option<Version>,
67
68    /// The architecture of the package to remove.
69    ///
70    /// While optional, it is highly recommend to set this field to the value of the field
71    /// ([`Package::architecture`]) of the corresponding
72    /// package in the package universe.
73    pub architecture: Option<String>,
74
75    /// Extra optional fields supported by [`Package`] stanzas.
76    #[serde(flatten)]
77    pub extra: HashMap<String, String>,
78}
79
80/// A stanza telling APT that a specific package can be autoremoved as a consequence of the
81/// executed user request.
82#[derive(Serialize, Deserialize, Debug, Default, Eq, PartialEq)]
83#[serde(rename_all = "PascalCase")]
84pub struct Autoremove {
85    /// The identifier of the package that can be autoremoved.
86    ///
87    /// Must reference the identifier of a package in the package universe
88    /// (see [`Package::id`]).
89    pub autoremove: String,
90
91    /// Extra optional fields supported by [`Package`] stanzas.
92    #[serde(flatten)]
93    pub extra: HashMap<String, String>,
94}
95
96impl Package {
97    /// Returns an [`Install`] stanza that can be used to tell APT to install this package.
98    pub fn to_install(&self) -> Install {
99        Install {
100            install: self.id.clone(),
101            package: Some(self.package.clone()),
102            version: Some(self.version.clone()),
103            architecture: Some(self.architecture.clone()),
104            ..Default::default()
105        }
106    }
107
108    /// Returns a [`Remove`] stanza that can be used to tell APT to remove this package.
109    pub fn to_remove(&self) -> Remove {
110        Remove {
111            remove: self.id.clone(),
112            package: Some(self.package.clone()),
113            version: Some(self.version.clone()),
114            architecture: Some(self.architecture.clone()),
115            ..Default::default()
116        }
117    }
118
119    /// Returns an [`Autoremove`] stanza that can be used to tell APT that this package can be
120    /// autoremoved.
121    pub fn to_autoremove(&self) -> Autoremove {
122        Autoremove {
123            autoremove: self.id.clone(),
124            ..Default::default()
125        }
126    }
127}
128
129/// An [Error stanza][error] reporting the error(s) faced when trying to fulfill an
130/// unsatisfiable user request.
131///
132/// [error]: https://salsa.debian.org/apt-team/apt/-/blob/a8367745/doc/external-dependency-solver-protocol.md#error
133#[derive(Serialize, Deserialize, Debug, Default, Eq, PartialEq)]
134#[serde(rename_all = "PascalCase")]
135pub struct Error {
136    /// A unique error identifier, such as a UUID. The value of this field is ignored.
137    pub error: String,
138
139    /// Human-readable text explaining the cause of the solver error.
140    ///
141    /// If multiline, the first line conveys a short message, which is then explained in more
142    /// detail in subsequent lines.
143    pub message: String,
144}
145
146/// A stanza in an [`Answer::Solution`].
147#[derive(Serialize, Debug, Eq, PartialEq)]
148#[serde(untagged)]
149pub enum Action {
150    /// A single [`Install`] stanza in an [`Answer::Solution`].
151    Install(Install),
152    /// A single [`Remove`] stanza in an [`Answer::Solution`].
153    Remove(Remove),
154    /// A single [`Autoremove`] stanza in an [`Answer::Solution`].
155    Autoremove(Autoremove),
156}
157
158impl From<Install> for Action {
159    fn from(value: Install) -> Self {
160        Self::Install(value)
161    }
162}
163
164impl From<Remove> for Action {
165    fn from(value: Remove) -> Self {
166        Self::Remove(value)
167    }
168}
169
170impl From<Autoremove> for Action {
171    fn from(value: Autoremove) -> Self {
172        Self::Autoremove(value)
173    }
174}
175
176/// The [answer] returned from the external solver to APT upon completion of the dependency
177/// resolution process.
178///
179/// [answer]: https://salsa.debian.org/apt-team/apt/-/blob/a8367745/doc/external-dependency-solver-protocol.md#answer
180#[derive(Serialize, Debug, Eq, PartialEq)]
181#[serde(untagged)]
182pub enum Answer {
183    /// A list of stanzas describing the [`Action`]s to be made to the set of installed packages
184    /// to satisfy the user's request.
185    Solution(Vec<Action>),
186    /// A single [`Error`] stanza reporting an error during the dependency resolution process.
187    Error(Error),
188}
189
190impl Answer {
191    /// Writes this [`Answer`] to the given `writer`. On error, returns an [`AnswerWriteError`].
192    pub fn write_to(&self, writer: impl std::io::Write) -> Result<(), AnswerWriteError> {
193        rfc822_like::to_writer(writer, self).map_err(Into::into)
194    }
195}
196
197impl From<Error> for Answer {
198    fn from(value: Error) -> Self {
199        Self::Error(value)
200    }
201}
202
203/// The error returned when [`Answer::write_to`] fails.
204///
205/// Though the implementation details are hidden, the struct implements [`std::error::Error`]
206/// and a human-friendly [`std::fmt::Display`] implementation.
207#[derive(Debug, thiserror::Error)]
208#[error(transparent)]
209pub struct AnswerWriteError(#[from] rfc822_like::ser::Error);
210
211#[cfg(test)]
212mod tests {
213    use indoc::indoc;
214
215    use crate::test_util::ser_test;
216
217    use super::*;
218
219    ser_test! {
220        test_action: {
221            indoc! {"
222                Install: abc
223            "} =>
224            Action::Install(Install {
225                install: "abc".into(),
226                ..Default::default()
227            }),
228        }
229    }
230
231    ser_test! {
232        test_answer: {
233            indoc! {"
234                Install: 123
235                Architecture: amd64
236
237                Remove: 234
238                Package: bar
239                Version: 0.1.2
240
241                Autoremove: 345
242            "} =>
243            Answer::Solution(
244                vec![
245                    Install {
246                        install: "123".into(),
247                        architecture: Some("amd64".into()),
248                        ..Default::default()
249                    }.into(),
250                    Remove {
251                        remove: "234".into(),
252                        package: Some("bar".into()),
253                        version: Some("0.1.2".try_into().unwrap()),
254                        ..Default::default()
255                    }.into(),
256                    Autoremove {
257                        autoremove: "345".into(),
258                        ..Default::default()
259                    }.into(),
260                ]
261            ),
262            indoc! {"
263                Error: foo
264                Message: bar
265                 baz
266            "} =>
267            Answer::Error(Error {
268                error: "foo".to_string(),
269                message: "bar\nbaz".to_string(),
270            }),
271        }
272    }
273}