guppy-summaries/src/summary.rs (260 lines of code) (raw):
// Copyright (c) The cargo-guppy Contributors
// SPDX-License-Identifier: MIT OR Apache-2.0
use crate::diff::SummaryDiff;
use camino::{Utf8Path, Utf8PathBuf};
use semver::Version;
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet},
fmt,
};
use toml::{value::Table, Serializer};
/// A type representing a package map as used in `Summary` instances.
pub type PackageMap = BTreeMap<SummaryId, PackageInfo>;
/// An in-memory representation of a build summary.
///
/// The metadata parameter is customizable.
///
/// For more, see the crate-level documentation.
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct Summary {
/// Extra metadata associated with the summary.
///
/// This may be used for storing extra information about the summary.
///
/// The type defaults to `toml::Value` but is customizable.
#[serde(default, skip_serializing_if = "Table::is_empty")]
pub metadata: Table,
/// The packages and features built on the target platform.
#[serde(
rename = "target-package",
with = "package_map_impl",
default = "PackageMap::new",
skip_serializing_if = "PackageMap::is_empty"
)]
pub target_packages: PackageMap,
/// The packages and features built on the host platform.
#[serde(
rename = "host-package",
with = "package_map_impl",
default = "PackageMap::new",
skip_serializing_if = "PackageMap::is_empty"
)]
pub host_packages: PackageMap,
}
impl Summary {
/// Constructs a new summary with the provided metadata, and an empty `target_packages` and
/// `host_packages`.
pub fn with_metadata(metadata: &impl Serialize) -> Result<Self, toml::ser::Error> {
let toml_str = toml::to_string(metadata)?;
let metadata =
toml::from_str(&toml_str).expect("toml::to_string creates a valid TOML string");
Ok(Self {
metadata,
..Self::default()
})
}
/// Deserializes a summary from the given string, with optional custom metadata.
pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
toml::from_str(s)
}
/// Perform a diff of this summary against another.
///
/// This doesn't diff the metadata, just the initials and packages.
pub fn diff<'a>(&'a self, other: &'a Summary) -> SummaryDiff<'a> {
SummaryDiff::new(self, other)
}
/// Serializes this summary to a TOML string.
pub fn to_string(&self) -> Result<String, toml::ser::Error> {
let mut dst = String::new();
self.write_to_string(&mut dst)?;
Ok(dst)
}
/// Serializes this summary into the given TOML string, using pretty TOML syntax.
pub fn write_to_string(&self, dst: &mut String) -> Result<(), toml::ser::Error> {
let mut serializer = Serializer::pretty(dst);
serializer.pretty_array(false);
self.serialize(&mut serializer)
}
}
/// A unique identifier for a package in a build summary.
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
#[serde(rename_all = "kebab-case")]
pub struct SummaryId {
/// The name of the package.
pub name: String,
/// The version number of the package.
pub version: Version,
/// The source for this package.
#[serde(flatten)]
pub source: SummarySource,
}
impl SummaryId {
/// Creates a new `SummaryId`.
pub fn new(name: impl Into<String>, version: Version, source: SummarySource) -> Self {
Self {
name: name.into(),
version,
source,
}
}
}
impl fmt::Display for SummaryId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{{ name = \"{}\", version = \"{}\", source = \"{}\"}}",
self.name, self.version, self.source
)
}
}
/// The location of a package.
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
#[serde(rename_all = "kebab-case", untagged)]
pub enum SummarySource {
/// A workspace path.
Workspace {
/// The path of this package, relative to the workspace root.
#[serde(
rename = "workspace-path",
serialize_with = "serialize_forward_slashes"
)]
workspace_path: Utf8PathBuf,
},
/// A non-workspace path.
///
/// The path is expected to be relative to the workspace root.
Path {
/// The path of this package, relative to the workspace root.
#[serde(serialize_with = "serialize_forward_slashes")]
path: Utf8PathBuf,
},
/// The `crates.io` registry.
#[serde(with = "crates_io_impl")]
CratesIo,
/// An external source that's not the `crates.io` registry, such as an alternate registry or
/// a `git` repository.
External {
/// The external source.
source: String,
},
}
impl SummarySource {
/// Creates a new `SummarySource` representing a workspace source.
pub fn workspace(workspace_path: impl Into<Utf8PathBuf>) -> Self {
SummarySource::Workspace {
workspace_path: workspace_path.into(),
}
}
/// Creates a new `SummarySource` representing a non-workspace path source.
pub fn path(path: impl Into<Utf8PathBuf>) -> Self {
SummarySource::Path { path: path.into() }
}
/// Creates a new `SummarySource` representing the `crates.io` registry.
pub fn crates_io() -> Self {
SummarySource::CratesIo
}
/// Creates a new `SummarySource` representing an external source like a Git repository or a
/// custom registry.
pub fn external(source: impl Into<String>) -> Self {
SummarySource::External {
source: source.into(),
}
}
}
impl fmt::Display for SummarySource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
// Don't differentiate here between workspace and non-workspace paths because
// PackageStatus provides that info.
SummarySource::Workspace { workspace_path } => {
let path_out = path_replace_slashes(workspace_path);
write!(f, "path '{}'", path_out)
}
SummarySource::Path { path } => {
let path_out = path_replace_slashes(path);
write!(f, "path '{}'", path_out)
}
SummarySource::CratesIo => write!(f, "crates.io"),
SummarySource::External { source } => write!(f, "external '{}'", source),
}
}
}
/// Information about a package in a summary that isn't part of the unique identifier.
#[derive(Clone, Debug, Deserialize, Eq, Hash, Serialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct PackageInfo {
/// Where this package lies in the dependency graph.
pub status: PackageStatus,
/// The features built for this package.
pub features: BTreeSet<String>,
/// The optional dependencies built for this package.
#[serde(skip_serializing_if = "BTreeSet::is_empty", default)]
pub optional_deps: BTreeSet<String>,
}
/// The status of a package in a summary, such as whether it is part of the initial build set.
///
/// The ordering here determines what order packages will be written out in the summary.
#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
#[serde(rename_all = "kebab-case")]
pub enum PackageStatus {
/// This package is part of the requested build set.
Initial,
/// This is a workspace package that isn't part of the requested build set.
Workspace,
/// This package is a direct non-workspace dependency.
///
/// A `Direct` package may also be transitively included.
Direct,
/// This package is a transitive non-workspace dependency.
Transitive,
}
impl fmt::Display for PackageStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
PackageStatus::Initial => "initial",
PackageStatus::Workspace => "workspace",
PackageStatus::Direct => "direct third-party",
PackageStatus::Transitive => "transitive third-party",
};
write!(f, "{}", s)
}
}
/// Serialization and deserialization for `PackageMap` instances.
mod package_map_impl {
use super::*;
use serde::{Deserializer, Serializer};
pub fn serialize<S>(package_map: &PackageMap, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Make a list of `PackageSerialize` instances and sort by:
// * status (to ensure initials come first)
// * summary ID
let mut package_list: Vec<_> = package_map
.iter()
.map(|(summary_id, info)| PackageSerialize { summary_id, info })
.collect();
package_list.sort_unstable_by_key(|package| (&package.info.status, package.summary_id));
package_list.serialize(serializer)
}
/// TOML representation of a package in a build summary, for serialization.
#[derive(Serialize)]
struct PackageSerialize<'a> {
#[serde(flatten)]
summary_id: &'a SummaryId,
#[serde(flatten)]
info: &'a PackageInfo,
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<PackageMap, D::Error>
where
D: Deserializer<'de>,
{
let packages = Vec::<PackageDeserialize>::deserialize(deserializer)?;
let mut package_map: PackageMap = BTreeMap::new();
for package in packages {
package_map.insert(package.summary_id, package.info);
}
Ok(package_map)
}
/// TOML representation of a package in a build summary, for deserialization.
#[derive(Deserialize)]
struct PackageDeserialize {
#[serde(flatten)]
summary_id: SummaryId,
#[serde(flatten)]
info: PackageInfo,
}
}
/// Serializes a path with forward slashes on Windows.
pub fn serialize_forward_slashes<S>(path: &Utf8PathBuf, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let path_out = path_replace_slashes(path);
path_out.serialize(serializer)
}
/// Replaces backslashes with forward slashes on Windows.
fn path_replace_slashes(path: &Utf8Path) -> impl fmt::Display + Serialize + '_ {
// (Note: serde doesn't support non-Unicode paths anyway.)
cfg_if::cfg_if! {
if #[cfg(windows)] {
path.as_str().replace("\\", "/")
} else {
path.as_str()
}
}
}
/// Serialization and deserialization for the `CratesIo` variant.
mod crates_io_impl {
use super::*;
use serde::{de::Error, ser::SerializeMap, Deserializer, Serializer};
pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("crates-io", &true)?;
map.end()
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
where
D: Deserializer<'de>,
{
let crates_io = CratesIoDeserialize::deserialize(deserializer)?;
if !crates_io.crates_io {
return Err(D::Error::custom("crates-io field should be true"));
}
Ok(())
}
#[derive(Deserialize)]
struct CratesIoDeserialize {
#[serde(rename = "crates-io")]
crates_io: bool,
}
}