use crate::cargo_toml::{CargoTomlMetadata, CargoTomlMetadataPyo3Pack};
use crate::CargoToml;
use failure::{Error, ResultExt};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::read_to_string;
use std::path::Path;
use std::str;
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
pub struct WheelMetadata {
pub metadata21: Metadata21,
pub scripts: HashMap<String, String>,
pub module_name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
#[allow(missing_docs)]
pub struct Metadata21 {
pub metadata_version: String,
pub name: String,
pub version: String,
pub platform: Vec<String>,
pub supported_platform: Vec<String>,
pub summary: Option<String>,
pub description: Option<String>,
pub description_content_type: Option<String>,
pub keywords: Option<String>,
pub home_page: Option<String>,
pub download_url: Option<String>,
pub author: Option<String>,
pub author_email: Option<String>,
pub maintainer: Option<String>,
pub maintainer_email: Option<String>,
pub license: Option<String>,
pub classifier: Vec<String>,
pub requires_dist: Vec<String>,
pub provides_dist: Vec<String>,
pub obsoletes_dist: Vec<String>,
pub requires_python: Option<String>,
pub requires_external: Vec<String>,
pub project_url: Vec<String>,
pub provides_extra: Vec<String>,
}
impl Metadata21 {
pub fn from_cargo_toml(
cargo_toml: &CargoToml,
manifest_path: &Path,
) -> Result<Metadata21, Error> {
let authors = cargo_toml.package.authors.join(", ");
let description = if let Some(ref readme) = cargo_toml.package.readme {
Some(read_to_string(manifest_path.join(readme)).context(format!(
"Failed to read readme specified in Cargo.toml, which should be at {}",
manifest_path.join(readme).display()
))?)
} else {
None
};
let description_content_type = if description.is_some() {
Some("text/markdown; charset=UTF-8; variant=GFM".to_owned())
} else {
None
};
let classifier = match cargo_toml.package.metadata {
Some(CargoTomlMetadata {
pyo3_pack:
Some(CargoTomlMetadataPyo3Pack {
classifier: Some(ref classifier),
..
}),
}) => classifier.clone(),
_ => Vec::new(),
};
Ok(Metadata21 {
metadata_version: "2.1".to_owned(),
name: cargo_toml.package.name.to_owned(),
version: cargo_toml.package.version.clone(),
platform: Vec::new(),
supported_platform: Vec::new(),
summary: cargo_toml.package.description.clone(),
description,
description_content_type,
keywords: cargo_toml
.package
.keywords
.clone()
.map(|keywords| keywords.join(" ")),
home_page: cargo_toml.package.homepage.clone(),
download_url: None,
author: Some(authors.to_owned()),
author_email: Some(authors.to_owned()),
maintainer: None,
maintainer_email: None,
license: cargo_toml.package.license.clone(),
classifier,
requires_dist: Vec::new(),
provides_dist: Vec::new(),
obsoletes_dist: Vec::new(),
requires_python: None,
requires_external: Vec::new(),
project_url: Vec::new(),
provides_extra: Vec::new(),
})
}
pub fn to_vec(&self) -> Vec<(String, String)> {
let mut fields = vec![
("Metadata-Version", self.metadata_version.clone()),
("Name", self.name.clone()),
("Version", self.version.clone()),
];
macro_rules! vec_types {
($(($name:tt : $value:ident)),*) => {
$(
for i in &self.$value {
fields.push(($name, i.to_string()));
}
)*
}
};
macro_rules! option_types {
($(($name:tt : $value:ident)),*) => {
$(
if let Some(some) = self.$value.clone() {
fields.push(($name, some));
}
)*
}
}
vec_types![
("Supported-Platform": supported_platform),
("Platform": platform),
("Supported-Platform": supported_platform),
("Classifier": classifier),
("Requires-Dist": requires_dist),
("Provides-Dist": provides_dist),
("Obsoletes-Dist": obsoletes_dist),
("Requires-External": requires_external),
("Project-Url": project_url),
("Provides-Extra": provides_extra)
];
option_types![
("Summary": summary),
("Keywords": keywords),
("Home-Page": home_page),
("Download-Url": download_url),
("Author": author),
("Author-Email": author_email),
("Maintainer": maintainer),
("Maintainer-Email": maintainer_email),
("License": license),
("Requires-Python": requires_python),
("Description-Content-Type": description_content_type),
("Description": description)
];
fields
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect()
}
pub fn to_file_contents(&self) -> String {
let mut fields = self.to_vec();
let mut out = "".to_string();
let body = match fields.clone().last() {
Some((key, description)) if key == "Description" => {
fields.pop().unwrap();
Some(description.clone())
}
Some((_, _)) => None,
None => None,
};
for (key, value) in fields {
out += &format!("{}: {}\n", key, value);
}
if let Some(body) = body {
out += &format!("\n{}\n", body);
}
out
}
pub fn get_distribution_escaped(&self) -> String {
let re = Regex::new(r"[^\w\d.]+").unwrap();
re.replace_all(&self.name, "_").to_string()
}
pub fn get_version_escaped(&self) -> String {
let re = Regex::new(r"[^\w\d.]+").unwrap();
re.replace_all(&self.version, "_").to_string()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::cargo_toml::CargoTomlMetadata;
use crate::cargo_toml::CargoTomlMetadataPyo3Pack;
use indoc::indoc;
use std::io::Write;
use tempfile;
use toml;
#[test]
fn test_metadata_from_cargo_toml() {
let readme = indoc!(
r#"
# Some test package
This is the readme for a test package
"#
);
let mut readme_md = tempfile::NamedTempFile::new().unwrap();
let readme_path = if cfg!(windows) {
readme_md.path().to_str().unwrap().replace("\\", "/")
} else {
readme_md.path().to_str().unwrap().to_string()
};
readme_md.write_all(readme.as_bytes()).unwrap();
let cargo_toml = indoc!(
r#"
[package]
authors = ["konstin <konstin@mailbox.org>"]
name = "info-project"
version = "0.1.0"
description = "A test project"
homepage = "https://example.org"
readme = "readme.md"
keywords = ["ffi", "test"]
[lib]
crate-type = ["cdylib"]
name = "get_fourtytwo"
[package.metadata.pyo3-pack.scripts]
ph = "pyo3_pack:print_hello"
[package.metadata.pyo3-pack]
classifier = ["Programming Language :: Python"]
"#
)
.replace("readme.md", &readme_path);
let cargo_toml: CargoToml = toml::from_str(&cargo_toml).unwrap();
let metadata =
Metadata21::from_cargo_toml(&cargo_toml, &readme_md.path().parent().unwrap()).unwrap();
let expected = indoc!(
r#"
Metadata-Version: 2.1
Name: info-project
Version: 0.1.0
Classifier: Programming Language :: Python
Summary: A test project
Keywords: ffi test
Home-Page: https://example.org
Author: konstin <konstin@mailbox.org>
Author-Email: konstin <konstin@mailbox.org>
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
# Some test package
This is the readme for a test package
"#
);
let actual = metadata.to_file_contents();
assert_eq!(actual.trim(), expected.trim());
let mut scripts = HashMap::new();
scripts.insert("ph".to_string(), "pyo3_pack:print_hello".to_string());
let classifier = vec!["Programming Language :: Python".to_string()];
assert_eq!(
cargo_toml.package.metadata,
Some(CargoTomlMetadata {
pyo3_pack: Some(CargoTomlMetadataPyo3Pack {
scripts: Some(scripts),
classifier: Some(classifier),
}),
})
);
}
}