#![forbid(unsafe_code)]
use serde::{Deserialize, Serialize};
use serde_json;
use std::str::FromStr;
#[cfg(feature = "toml")]
use cargo_lock;
#[cfg(feature = "toml")]
use std::convert::TryInto;
#[cfg(any(feature = "from_metadata",feature = "toml"))]
use std::convert::TryFrom;
#[cfg(feature = "from_metadata")]
use cargo_metadata;
#[cfg(feature = "from_metadata")]
use std::{error::Error, cmp::Ordering::*, cmp::min, fmt::Display, collections::HashMap};
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct VersionInfo {
pub packages: Vec<Package>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct Package {
pub name: String,
pub version: semver::Version,
pub source: String,
#[serde(default)]
#[serde(skip_serializing_if = "is_default")]
pub kind: DependencyKind,
#[serde(default)]
#[serde(skip_serializing_if = "is_default")]
pub dependencies: Vec<usize>,
#[serde(default)]
#[serde(skip_serializing_if = "is_default")]
pub features: Vec<String>,
}
#[allow(non_camel_case_types)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
pub enum DependencyKind {
build,
runtime,
}
impl Default for DependencyKind {
fn default() -> Self {
DependencyKind::runtime
}
}
#[cfg(feature = "from_metadata")]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
enum PrivateDepKind {
Development,
Build,
Runtime,
}
#[cfg(feature = "from_metadata")]
impl From<PrivateDepKind> for DependencyKind {
fn from(priv_kind: PrivateDepKind) -> Self {
match priv_kind {
PrivateDepKind::Development => panic!("Cannot convert development dependency to serializable format"),
PrivateDepKind::Build => DependencyKind::build,
PrivateDepKind::Runtime => DependencyKind::runtime,
}
}
}
fn is_default<T: Default + PartialEq> (value: &T) -> bool {
let default_value = T::default();
value == &default_value
}
impl FromStr for VersionInfo {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
#[cfg(feature = "from_metadata")]
impl From<&cargo_metadata::DependencyKind> for PrivateDepKind {
fn from(kind: &cargo_metadata::DependencyKind) -> Self {
match kind {
cargo_metadata::DependencyKind::Normal => PrivateDepKind::Runtime,
cargo_metadata::DependencyKind::Development => PrivateDepKind::Development,
cargo_metadata::DependencyKind::Build => PrivateDepKind::Build,
_ => panic!("Unknown dependency kind")
}
}
}
#[cfg(feature = "from_metadata")]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum InsufficientMetadata {
NoDeps,
VirtualWorkspace,
}
#[cfg(feature = "from_metadata")]
impl Display for InsufficientMetadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InsufficientMetadata::NoDeps => {
write!(f, "Missing dependency information! Please call 'cargo metadata' without '--no-deps' flag.")
}
InsufficientMetadata::VirtualWorkspace => {
write!(f, "Missing root crate! Please call this from a package directory, not workspace root.")
}
}
}
}
#[cfg(feature = "from_metadata")]
impl Error for InsufficientMetadata {}
#[cfg(feature = "from_metadata")]
impl TryFrom<&cargo_metadata::Metadata> for VersionInfo {
type Error = InsufficientMetadata;
fn try_from(metadata: &cargo_metadata::Metadata) -> Result<Self, Self::Error> {
let toplevel_crate_id = metadata.resolve.as_ref().ok_or(InsufficientMetadata::NoDeps)?
.root.as_ref().ok_or(InsufficientMetadata::VirtualWorkspace)?.repr.as_str();
let nodes = &metadata.resolve.as_ref().unwrap().nodes;
let id_to_node: HashMap<&str, &cargo_metadata::Node> = nodes.iter().map(|n| (n.id.repr.as_str(), n)).collect();
let mut id_to_dep_kind: HashMap<&str, PrivateDepKind> = HashMap::new();
id_to_dep_kind.insert(toplevel_crate_id, PrivateDepKind::Runtime);
let mut current_queue: Vec<&cargo_metadata::Node> = vec![id_to_node[toplevel_crate_id]];
let mut next_step_queue: Vec<&cargo_metadata::Node> = Vec::new();
while current_queue.len() > 0 {
for parent in current_queue.drain(..) {
let parent_dep_kind = id_to_dep_kind[parent.id.repr.as_str()];
for child in &parent.deps {
let child_id = child.pkg.repr.as_str();
let dep_kind = strongest_dep_kind(child.dep_kinds.as_slice());
let dep_kind = min(dep_kind, parent_dep_kind);
let dep_kind_on_previous_visit = id_to_dep_kind.get(child_id);
if dep_kind_on_previous_visit == None || &dep_kind > dep_kind_on_previous_visit.unwrap() {
id_to_dep_kind.insert(child_id, dep_kind);
next_step_queue.push(id_to_node[child_id]);
}
}
}
std::mem::swap(&mut next_step_queue, &mut current_queue);
}
let metadata_package_dep_kind = |p: &cargo_metadata::Package| {
let package_id = p.id.repr.as_str();
id_to_dep_kind.get(package_id)
};
let mut packages: Vec<&cargo_metadata::Package> = metadata.packages.iter().filter(|p| {
let dep_kind = metadata_package_dep_kind(p);
dep_kind.is_some() && dep_kind.unwrap() != &PrivateDepKind::Development
}).collect();
packages.sort_unstable_by(|a, b| {
let names_order = a.name.cmp(&b.name);
if names_order != Equal {return names_order;}
let versions_order = a.name.cmp(&b.name);
if versions_order != Equal {return versions_order;}
a.id.repr.cmp(&b.id.repr)
});
let mut id_to_index = HashMap::new();
for (index, package) in packages.iter().enumerate() {
id_to_index.insert(package.id.repr.as_str(), index);
};
let mut packages: Vec<Package> = packages.into_iter().map(|p| {
Package {
name: p.name.to_owned(),
version: p.version.clone(),
source: source_to_source_string(&p.source),
kind: (*metadata_package_dep_kind(&p).unwrap()).into(),
dependencies: Vec::new(),
features: Vec::new(),
}
}).collect();
for node in metadata.resolve.as_ref().unwrap().nodes.iter() {
let package_id = node.id.repr.as_str();
if id_to_index.contains_key(package_id) {
let package : &mut Package = &mut packages[id_to_index[package_id]];
for dep in node.dependencies.iter() {
let dep_id = dep.repr.as_str();
if id_to_dep_kind[dep_id] != PrivateDepKind::Development {
package.dependencies.push(id_to_index[dep_id]);
}
}
package.dependencies.sort_unstable();
package.features = node.features.clone();
package.features.sort_unstable();
}
}
Ok(VersionInfo {packages})
}
}
#[cfg(feature = "from_metadata")]
fn strongest_dep_kind(deps: &[cargo_metadata::DepKindInfo]) -> PrivateDepKind {
deps.iter().map(|d| PrivateDepKind::from(&d.kind)).max()
.unwrap_or(PrivateDepKind::Runtime)
}
#[cfg(feature = "from_metadata")]
fn source_to_source_string(s: &Option<cargo_metadata::Source>) -> String {
if let Some(source) = s {
source.repr.as_str().split('+').next().unwrap_or("").to_owned()
} else {
"local".to_owned()
}
}
#[cfg(feature = "toml")]
impl TryFrom <&Package> for cargo_lock::Dependency {
type Error = cargo_lock::error::Error;
fn try_from(input: &Package) -> Result<Self, Self::Error> {
Ok(cargo_lock::Dependency {
name: cargo_lock::package::name::Name::from_str(&input.name)?,
version: cargo_lock::package::Version::parse(&input.version.to_string())?,
source: Option::None,
})
}
}
#[cfg(feature = "toml")]
impl TryFrom<&VersionInfo> for cargo_lock::lockfile::Lockfile {
type Error = cargo_lock::error::Error;
fn try_from(input: &VersionInfo) -> Result<Self, Self::Error> {
let mut packages: Vec<cargo_lock::Package> = Vec::new();
for pkg in input.packages.iter() {
packages.push(cargo_lock::package::Package {
name: cargo_lock::package::name::Name::from_str(&pkg.name)?,
version: cargo_lock::package::Version::parse(&pkg.version.to_string())?,
checksum: Option::None,
dependencies: {
let result: Result<Vec<_>, _> = pkg.dependencies.iter().map(|i| {
input.packages.get(*i).ok_or(cargo_lock::error::Error::new(
cargo_lock::error::ErrorKind::Parse,
&format!("There is no dependency with index {} in the input JSON", i))
)?.try_into()
}).collect();
result?
},
replace: None,
source: None,
})
}
Ok(cargo_lock::lockfile::Lockfile {
version: cargo_lock::lockfile::version::ResolveVersion::V2,
packages: packages,
root: None,
metadata: std::collections::BTreeMap::new(),
patch: cargo_lock::patch::Patch { unused: Vec::new() },
})
}
}
#[cfg(test)]
mod tests {
use super::VersionInfo;
use std::{convert::TryInto, path::PathBuf};
#[cfg(feature = "from_metadata")]
fn load_own_metadata() -> cargo_metadata::Metadata {
let mut cmd = cargo_metadata::MetadataCommand::new();
let cargo_toml_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("Cargo.toml");
cmd.manifest_path(cargo_toml_path);
cmd.exec().unwrap()
}
#[test]
#[cfg(feature = "toml")]
#[cfg(feature = "from_metadata")]
fn to_toml() {
let metadata = load_own_metadata();
let version_info_struct: VersionInfo = (&metadata).try_into().unwrap();
let _lockfile_struct: cargo_lock::lockfile::Lockfile = (&version_info_struct).try_into().unwrap();
}
}