use crate::diff::SummaryDiff;
use camino::Utf8PathBuf;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet},
fmt,
};
use toml::{Serializer, Value};
pub type PackageMap = BTreeMap<SummaryId, PackageInfo>;
#[derive(Clone, Debug, Deserialize, Eq, Hash, Serialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct SummaryWithMetadata<M = Value> {
#[serde(default = "Option::default")]
pub metadata: Option<M>,
#[serde(
rename = "target-package",
with = "package_map_impl",
default = "PackageMap::new",
skip_serializing_if = "PackageMap::is_empty"
)]
pub target_packages: PackageMap,
#[serde(
rename = "host-package",
with = "package_map_impl",
default = "PackageMap::new",
skip_serializing_if = "PackageMap::is_empty"
)]
pub host_packages: PackageMap,
}
impl<M> SummaryWithMetadata<M> {
pub fn parse<'de>(s: &'de str) -> Result<Self, toml::de::Error>
where
M: Deserialize<'de>,
{
Ok(toml::from_str(s)?)
}
pub fn diff<'a, M2>(&'a self, other: &'a SummaryWithMetadata<M2>) -> SummaryDiff<'a> {
SummaryDiff::new(self, other)
}
pub fn to_string(&self) -> Result<String, toml::ser::Error>
where
M: Serialize,
{
let mut dst = String::new();
self.write_to_string(&mut dst)?;
Ok(dst)
}
pub fn write_to_string(&self, dst: &mut String) -> Result<(), toml::ser::Error>
where
M: Serialize,
{
let mut serializer = Serializer::pretty(dst);
serializer.pretty_array(false);
self.serialize(&mut serializer)
}
}
impl<M> Default for SummaryWithMetadata<M> {
fn default() -> Self {
Self {
metadata: None,
target_packages: PackageMap::new(),
host_packages: PackageMap::new(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
#[serde(rename_all = "kebab-case")]
pub struct SummaryId {
pub name: String,
pub version: Version,
#[serde(flatten)]
pub source: SummarySource,
}
impl SummaryId {
pub fn new(name: impl Into<String>, version: Version, source: SummarySource) -> Self {
Self {
name: name.into(),
version,
source,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
#[serde(rename_all = "kebab-case", untagged)]
pub enum SummarySource {
Workspace {
#[serde(rename = "workspace-path", with = "path_forward_slashes")]
workspace_path: Utf8PathBuf,
},
Path {
#[serde(with = "path_forward_slashes")]
path: Utf8PathBuf,
},
#[serde(with = "crates_io_impl")]
CratesIo,
External {
source: String,
},
}
impl SummarySource {
pub fn workspace(workspace_path: impl Into<Utf8PathBuf>) -> Self {
SummarySource::Workspace {
workspace_path: workspace_path.into(),
}
}
pub fn path(path: impl Into<Utf8PathBuf>) -> Self {
SummarySource::Path { path: path.into() }
}
pub fn crates_io() -> Self {
SummarySource::CratesIo
}
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 {
SummarySource::Workspace { workspace_path } => {
let path_out = path_forward_slashes::replace_slashes(workspace_path);
write!(f, "path '{}'", path_out)
}
SummarySource::Path { path } => {
let path_out = path_forward_slashes::replace_slashes(path);
write!(f, "path '{}'", path_out)
}
SummarySource::CratesIo => write!(f, "crates.io"),
SummarySource::External { source } => write!(f, "external '{}'", source),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Serialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct PackageInfo {
pub status: PackageStatus,
pub features: BTreeSet<String>,
}
#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, Serialize, PartialEq, PartialOrd)]
#[serde(rename_all = "kebab-case")]
pub enum PackageStatus {
Initial,
Workspace,
Direct,
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)
}
}
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,
{
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)
}
#[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)
}
#[derive(Deserialize)]
struct PackageDeserialize {
#[serde(flatten)]
summary_id: SummaryId,
#[serde(flatten)]
info: PackageInfo,
}
}
mod path_forward_slashes {
use super::*;
use camino::Utf8Path;
use cfg_if::cfg_if;
use serde::{Deserializer, Serializer};
pub fn serialize<S>(path: &Utf8PathBuf, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let path_out = replace_slashes(path);
path_out.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Utf8PathBuf, D::Error>
where
D: Deserializer<'de>,
{
cfg_if! {
if #[cfg(windows)] {
let s = String::deserialize(deserializer)?;
let s = s.replace("/", "\\");
Ok(Utf8PathBuf::from(s))
} else {
Utf8PathBuf::deserialize(deserializer)
}
}
}
pub fn replace_slashes(path: &Utf8Path) -> impl fmt::Display + Serialize + '_ {
cfg_if! {
if #[cfg(windows)] {
path.as_str().replace("\\", "/")
} else {
path.as_str()
}
}
}
}
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,
}
}