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}