1use crate::{diff::SummaryDiff, toml_compat};
5use camino::{Utf8Path, Utf8PathBuf};
6use semver::Version;
7use serde::{Deserialize, Serialize};
8use std::{
9 collections::{BTreeMap, BTreeSet},
10 fmt,
11};
12use toml::{Table, Value};
13
14pub type PackageMap = BTreeMap<SummaryId, PackageInfo>;
16
17#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
21#[serde(rename_all = "kebab-case")]
22pub struct Summary {
23 #[serde(default, skip_serializing_if = "Table::is_empty")]
29 pub metadata: Table,
30
31 #[serde(
33 rename = "target-package",
34 with = "package_map_impl",
35 default = "PackageMap::new",
36 skip_serializing_if = "PackageMap::is_empty"
37 )]
38 pub target_packages: PackageMap,
39
40 #[serde(
42 rename = "host-package",
43 with = "package_map_impl",
44 default = "PackageMap::new",
45 skip_serializing_if = "PackageMap::is_empty"
46 )]
47 pub host_packages: PackageMap,
48}
49
50impl Summary {
51 pub fn with_metadata(metadata: &impl Serialize) -> Result<Self, toml::ser::Error> {
54 let toml_str = toml::to_string(metadata)?;
64 let metadata = toml_str
65 .parse()
66 .expect("toml::to_string creates a valid TOML string");
67 Ok(Self {
68 metadata,
69 ..Self::default()
70 })
71 }
72
73 pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
75 toml::from_str(s)
76 }
77
78 pub fn diff<'a>(&'a self, other: &'a Summary) -> SummaryDiff<'a> {
82 SummaryDiff::new(self, other)
83 }
84
85 pub fn to_string(&self) -> Result<String, toml::ser::Error> {
87 let mut dst = String::new();
88 self.write_to_string(&mut dst)?;
89 Ok(dst)
90 }
91
92 pub fn write_to_string(&self, dst: &mut String) -> Result<(), toml::ser::Error> {
99 let mut table = Table::new();
107 if !self.metadata.is_empty() {
108 let metadata = self
109 .metadata
110 .iter()
111 .map(|(key, value)| (key.clone(), toml_compat::reorder_value(value)))
112 .collect();
113 table.insert("metadata".to_owned(), Value::Table(metadata));
114 }
115 table.extend(Table::try_from(SummaryPackages {
116 target_packages: &self.target_packages,
117 host_packages: &self.host_packages,
118 })?);
119 toml_compat::write_table(&table, dst)
120 }
121}
122
123#[derive(Serialize)]
124struct SummaryPackages<'a> {
125 #[serde(
126 rename = "target-package",
127 with = "package_map_impl",
128 skip_serializing_if = "PackageMap::is_empty"
129 )]
130 target_packages: &'a PackageMap,
131
132 #[serde(
133 rename = "host-package",
134 with = "package_map_impl",
135 skip_serializing_if = "PackageMap::is_empty"
136 )]
137 host_packages: &'a PackageMap,
138}
139
140#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
142#[serde(rename_all = "kebab-case")]
143pub struct SummaryId {
144 pub name: String,
146
147 pub version: Version,
149
150 #[serde(flatten)]
152 pub source: SummarySource,
153}
154
155impl SummaryId {
156 pub fn new(name: impl Into<String>, version: Version, source: SummarySource) -> Self {
158 Self {
159 name: name.into(),
160 version,
161 source,
162 }
163 }
164}
165
166impl fmt::Display for SummaryId {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 write!(
169 f,
170 "{{ name = \"{}\", version = \"{}\", source = \"{}\"}}",
171 self.name, self.version, self.source
172 )
173 }
174}
175
176#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
178#[serde(rename_all = "kebab-case", untagged)]
179pub enum SummarySource {
180 Workspace {
182 #[serde(
184 rename = "workspace-path",
185 serialize_with = "serialize_forward_slashes"
186 )]
187 workspace_path: Utf8PathBuf,
188 },
189
190 Path {
196 #[serde(serialize_with = "serialize_forward_slashes")]
198 path: Utf8PathBuf,
199 },
200
201 #[serde(with = "crates_io_impl")]
203 CratesIo,
204
205 External {
208 source: String,
210 },
211}
212
213impl SummarySource {
214 pub fn workspace(workspace_path: impl Into<Utf8PathBuf>) -> Self {
216 SummarySource::Workspace {
217 workspace_path: workspace_path.into(),
218 }
219 }
220
221 pub fn path(path: impl Into<Utf8PathBuf>) -> Self {
223 SummarySource::Path { path: path.into() }
224 }
225
226 pub fn crates_io() -> Self {
228 SummarySource::CratesIo
229 }
230
231 pub fn external(source: impl Into<String>) -> Self {
234 SummarySource::External {
235 source: source.into(),
236 }
237 }
238}
239
240impl fmt::Display for SummarySource {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 SummarySource::Workspace { workspace_path } => {
246 let path_out = path_replace_slashes(workspace_path);
247 write!(f, "path '{path_out}'")
248 }
249 SummarySource::Path { path } => {
250 let path_out = path_replace_slashes(path);
251 write!(f, "path '{path_out}'")
252 }
253 SummarySource::CratesIo => write!(f, "crates.io"),
254 SummarySource::External { source } => write!(f, "external '{source}'"),
255 }
256 }
257}
258
259#[derive(Clone, Debug, Deserialize, Eq, Hash, Serialize, PartialEq)]
261#[serde(rename_all = "kebab-case")]
262pub struct PackageInfo {
263 pub status: PackageStatus,
265
266 pub features: BTreeSet<String>,
268
269 #[serde(skip_serializing_if = "BTreeSet::is_empty", default)]
271 pub optional_deps: BTreeSet<String>,
272}
273
274#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
278#[serde(rename_all = "kebab-case")]
279pub enum PackageStatus {
280 Initial,
282
283 Workspace,
285
286 Direct,
290
291 Transitive,
293}
294
295impl fmt::Display for PackageStatus {
296 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
297 let s = match self {
298 PackageStatus::Initial => "initial",
299 PackageStatus::Workspace => "workspace",
300 PackageStatus::Direct => "direct third-party",
301 PackageStatus::Transitive => "transitive third-party",
302 };
303 write!(f, "{s}")
304 }
305}
306
307mod package_map_impl {
309 use super::*;
310 use serde::{Deserializer, Serializer};
311
312 pub fn serialize<S>(package_map: &PackageMap, serializer: S) -> Result<S::Ok, S::Error>
313 where
314 S: Serializer,
315 {
316 let mut package_list: Vec<_> = package_map
320 .iter()
321 .map(|(summary_id, info)| PackageSerialize { summary_id, info })
322 .collect();
323 package_list.sort_unstable_by_key(|package| (&package.info.status, package.summary_id));
324 package_list.serialize(serializer)
325 }
326
327 #[derive(Serialize)]
329 struct PackageSerialize<'a> {
330 #[serde(flatten)]
331 summary_id: &'a SummaryId,
332 #[serde(flatten)]
333 info: &'a PackageInfo,
334 }
335
336 pub fn deserialize<'de, D>(deserializer: D) -> Result<PackageMap, D::Error>
337 where
338 D: Deserializer<'de>,
339 {
340 let packages = Vec::<PackageDeserialize>::deserialize(deserializer)?;
341 let mut package_map: PackageMap = BTreeMap::new();
342
343 for package in packages {
344 package_map.insert(package.summary_id, package.info);
345 }
346 Ok(package_map)
347 }
348
349 #[derive(Deserialize)]
351 struct PackageDeserialize {
352 #[serde(flatten)]
353 summary_id: SummaryId,
354 #[serde(flatten)]
355 info: PackageInfo,
356 }
357}
358
359pub fn serialize_forward_slashes<S>(path: &Utf8PathBuf, serializer: S) -> Result<S::Ok, S::Error>
361where
362 S: serde::Serializer,
363{
364 let path_out = path_replace_slashes(path);
365 path_out.serialize(serializer)
366}
367
368fn path_replace_slashes(path: &Utf8Path) -> impl fmt::Display + Serialize + '_ {
370 cfg_if::cfg_if! {
372 if #[cfg(windows)] {
373 path.as_str().replace("\\", "/")
374 } else {
375 path.as_str()
376 }
377 }
378}
379
380mod crates_io_impl {
382 use super::*;
383 use serde::{Deserializer, Serializer, de::Error, ser::SerializeMap};
384
385 pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
386 where
387 S: Serializer,
388 {
389 let mut map = serializer.serialize_map(Some(1))?;
390 map.serialize_entry("crates-io", &true)?;
391 map.end()
392 }
393
394 pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
395 where
396 D: Deserializer<'de>,
397 {
398 let crates_io = CratesIoDeserialize::deserialize(deserializer)?;
399 if !crates_io.crates_io {
400 return Err(D::Error::custom("crates-io field should be true"));
401 }
402 Ok(())
403 }
404
405 #[derive(Deserialize)]
406 struct CratesIoDeserialize {
407 #[serde(rename = "crates-io")]
408 crates_io: bool,
409 }
410}