1use crate::diff::SummaryDiff;
5use camino::{Utf8Path, Utf8PathBuf};
6use semver::Version;
7use serde::{Deserialize, Serialize};
8use std::{
9 collections::{BTreeMap, BTreeSet},
10 fmt,
11};
12use toml::{value::Table, Serializer};
13
14pub type PackageMap = BTreeMap<SummaryId, PackageInfo>;
16
17#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
23#[serde(rename_all = "kebab-case")]
24pub struct Summary {
25 #[serde(default, skip_serializing_if = "Table::is_empty")]
31 pub metadata: Table,
32
33 #[serde(
35 rename = "target-package",
36 with = "package_map_impl",
37 default = "PackageMap::new",
38 skip_serializing_if = "PackageMap::is_empty"
39 )]
40 pub target_packages: PackageMap,
41
42 #[serde(
44 rename = "host-package",
45 with = "package_map_impl",
46 default = "PackageMap::new",
47 skip_serializing_if = "PackageMap::is_empty"
48 )]
49 pub host_packages: PackageMap,
50}
51
52impl Summary {
53 pub fn with_metadata(metadata: &impl Serialize) -> Result<Self, toml::ser::Error> {
56 let toml_str = toml::to_string(metadata)?;
57 let metadata =
58 toml::from_str(&toml_str).expect("toml::to_string creates a valid TOML string");
59 Ok(Self {
60 metadata,
61 ..Self::default()
62 })
63 }
64
65 pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
67 toml::from_str(s)
68 }
69
70 pub fn diff<'a>(&'a self, other: &'a Summary) -> SummaryDiff<'a> {
74 SummaryDiff::new(self, other)
75 }
76
77 pub fn to_string(&self) -> Result<String, toml::ser::Error> {
79 let mut dst = String::new();
80 self.write_to_string(&mut dst)?;
81 Ok(dst)
82 }
83
84 pub fn write_to_string(&self, dst: &mut String) -> Result<(), toml::ser::Error> {
86 let mut serializer = Serializer::pretty(dst);
87 serializer.pretty_array(false);
88 self.serialize(&mut serializer)
89 }
90}
91
92#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
94#[serde(rename_all = "kebab-case")]
95pub struct SummaryId {
96 pub name: String,
98
99 pub version: Version,
101
102 #[serde(flatten)]
104 pub source: SummarySource,
105}
106
107impl SummaryId {
108 pub fn new(name: impl Into<String>, version: Version, source: SummarySource) -> Self {
110 Self {
111 name: name.into(),
112 version,
113 source,
114 }
115 }
116}
117
118impl fmt::Display for SummaryId {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 write!(
121 f,
122 "{{ name = \"{}\", version = \"{}\", source = \"{}\"}}",
123 self.name, self.version, self.source
124 )
125 }
126}
127
128#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
130#[serde(rename_all = "kebab-case", untagged)]
131pub enum SummarySource {
132 Workspace {
134 #[serde(
136 rename = "workspace-path",
137 serialize_with = "serialize_forward_slashes"
138 )]
139 workspace_path: Utf8PathBuf,
140 },
141
142 Path {
148 #[serde(serialize_with = "serialize_forward_slashes")]
150 path: Utf8PathBuf,
151 },
152
153 #[serde(with = "crates_io_impl")]
155 CratesIo,
156
157 External {
160 source: String,
162 },
163}
164
165impl SummarySource {
166 pub fn workspace(workspace_path: impl Into<Utf8PathBuf>) -> Self {
168 SummarySource::Workspace {
169 workspace_path: workspace_path.into(),
170 }
171 }
172
173 pub fn path(path: impl Into<Utf8PathBuf>) -> Self {
175 SummarySource::Path { path: path.into() }
176 }
177
178 pub fn crates_io() -> Self {
180 SummarySource::CratesIo
181 }
182
183 pub fn external(source: impl Into<String>) -> Self {
186 SummarySource::External {
187 source: source.into(),
188 }
189 }
190}
191
192impl fmt::Display for SummarySource {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 SummarySource::Workspace { workspace_path } => {
198 let path_out = path_replace_slashes(workspace_path);
199 write!(f, "path '{}'", path_out)
200 }
201 SummarySource::Path { path } => {
202 let path_out = path_replace_slashes(path);
203 write!(f, "path '{}'", path_out)
204 }
205 SummarySource::CratesIo => write!(f, "crates.io"),
206 SummarySource::External { source } => write!(f, "external '{}'", source),
207 }
208 }
209}
210
211#[derive(Clone, Debug, Deserialize, Eq, Hash, Serialize, PartialEq)]
213#[serde(rename_all = "kebab-case")]
214pub struct PackageInfo {
215 pub status: PackageStatus,
217
218 pub features: BTreeSet<String>,
220
221 #[serde(skip_serializing_if = "BTreeSet::is_empty", default)]
223 pub optional_deps: BTreeSet<String>,
224}
225
226#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
230#[serde(rename_all = "kebab-case")]
231pub enum PackageStatus {
232 Initial,
234
235 Workspace,
237
238 Direct,
242
243 Transitive,
245}
246
247impl fmt::Display for PackageStatus {
248 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
249 let s = match self {
250 PackageStatus::Initial => "initial",
251 PackageStatus::Workspace => "workspace",
252 PackageStatus::Direct => "direct third-party",
253 PackageStatus::Transitive => "transitive third-party",
254 };
255 write!(f, "{}", s)
256 }
257}
258
259mod package_map_impl {
261 use super::*;
262 use serde::{Deserializer, Serializer};
263
264 pub fn serialize<S>(package_map: &PackageMap, serializer: S) -> Result<S::Ok, S::Error>
265 where
266 S: Serializer,
267 {
268 let mut package_list: Vec<_> = package_map
272 .iter()
273 .map(|(summary_id, info)| PackageSerialize { summary_id, info })
274 .collect();
275 package_list.sort_unstable_by_key(|package| (&package.info.status, package.summary_id));
276 package_list.serialize(serializer)
277 }
278
279 #[derive(Serialize)]
281 struct PackageSerialize<'a> {
282 #[serde(flatten)]
283 summary_id: &'a SummaryId,
284 #[serde(flatten)]
285 info: &'a PackageInfo,
286 }
287
288 pub fn deserialize<'de, D>(deserializer: D) -> Result<PackageMap, D::Error>
289 where
290 D: Deserializer<'de>,
291 {
292 let packages = Vec::<PackageDeserialize>::deserialize(deserializer)?;
293 let mut package_map: PackageMap = BTreeMap::new();
294
295 for package in packages {
296 package_map.insert(package.summary_id, package.info);
297 }
298 Ok(package_map)
299 }
300
301 #[derive(Deserialize)]
303 struct PackageDeserialize {
304 #[serde(flatten)]
305 summary_id: SummaryId,
306 #[serde(flatten)]
307 info: PackageInfo,
308 }
309}
310
311pub fn serialize_forward_slashes<S>(path: &Utf8PathBuf, serializer: S) -> Result<S::Ok, S::Error>
313where
314 S: serde::Serializer,
315{
316 let path_out = path_replace_slashes(path);
317 path_out.serialize(serializer)
318}
319
320fn path_replace_slashes(path: &Utf8Path) -> impl fmt::Display + Serialize + '_ {
322 cfg_if::cfg_if! {
324 if #[cfg(windows)] {
325 path.as_str().replace("\\", "/")
326 } else {
327 path.as_str()
328 }
329 }
330}
331
332mod crates_io_impl {
334 use super::*;
335 use serde::{de::Error, ser::SerializeMap, Deserializer, Serializer};
336
337 pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
338 where
339 S: Serializer,
340 {
341 let mut map = serializer.serialize_map(Some(1))?;
342 map.serialize_entry("crates-io", &true)?;
343 map.end()
344 }
345
346 pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
347 where
348 D: Deserializer<'de>,
349 {
350 let crates_io = CratesIoDeserialize::deserialize(deserializer)?;
351 if !crates_io.crates_io {
352 return Err(D::Error::custom("crates-io field should be true"));
353 }
354 Ok(())
355 }
356
357 #[derive(Deserialize)]
358 struct CratesIoDeserialize {
359 #[serde(rename = "crates-io")]
360 crates_io: bool,
361 }
362}