use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct BuildSystem {
pub requires: Vec<String>,
pub build_backend: Option<String>,
pub backend_path: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct PyProjectToml {
pub build_system: BuildSystem,
pub project: Option<Project>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Project {
pub name: String,
pub version: Option<String>,
pub description: Option<String>,
pub readme: Option<ReadMe>,
pub requires_python: Option<String>,
pub license: Option<License>,
pub authors: Option<Vec<Contact>>,
pub maintainers: Option<Vec<Contact>>,
pub keywords: Option<Vec<String>>,
pub classifiers: Option<Vec<String>>,
pub urls: Option<IndexMap<String, String>>,
pub entry_points: Option<IndexMap<String, IndexMap<String, String>>>,
pub scripts: Option<IndexMap<String, String>>,
pub gui_scripts: Option<IndexMap<String, String>>,
pub dependencies: Option<Vec<String>>,
pub optional_dependencies: Option<IndexMap<String, Vec<String>>>,
pub dynamic: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "kebab-case")]
#[serde(untagged)]
pub enum ReadMe {
RelativePath(String),
#[serde(rename_all = "kebab-case")]
Table {
file: Option<String>,
text: Option<String>,
content_type: Option<String>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(expecting = "a table with 'file' or 'text' key")]
pub struct License {
pub file: Option<String>,
pub text: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(expecting = "a table with 'name' and 'email' keys")]
pub struct Contact {
pub name: Option<String>,
pub email: Option<String>,
}
impl PyProjectToml {
pub fn new(content: &str) -> Result<Self, toml::de::Error> {
toml::from_str(content)
}
}
#[cfg(test)]
mod tests {
use super::{PyProjectToml, ReadMe};
#[test]
fn test_parse_pyproject_toml() {
let source = r#"[build-system]
requires = ["maturin"]
build-backend = "maturin"
[project]
name = "spam"
version = "2020.0.0"
description = "Lovely Spam! Wonderful Spam!"
readme = "README.rst"
requires-python = ">=3.8"
license = {file = "LICENSE.txt"}
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
authors = [
{email = "hi@pradyunsg.me"},
{name = "Tzu-Ping Chung"}
]
maintainers = [
{name = "Brett Cannon", email = "brett@python.org"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python"
]
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
"django>2.0; os_name == 'nt'"
]
[project.optional-dependencies]
test = [
"pytest < 5.0.0",
"pytest-cov[all]"
]
[project.urls]
homepage = "example.com"
documentation = "readthedocs.org"
repository = "github.com"
changelog = "github.com/me/spam/blob/master/CHANGELOG.md"
[project.scripts]
spam-cli = "spam:main_cli"
[project.gui-scripts]
spam-gui = "spam:main_gui"
[project.entry-points."spam.magical"]
tomatoes = "spam:main_tomatoes""#;
let project_toml = PyProjectToml::new(source).unwrap();
let build_system = &project_toml.build_system;
assert_eq!(build_system.requires, &["maturin"]);
assert_eq!(build_system.build_backend.as_deref(), Some("maturin"));
let project = project_toml.project.as_ref().unwrap();
assert_eq!(project.name, "spam");
assert_eq!(project.version.as_deref(), Some("2020.0.0"));
assert_eq!(
project.description.as_deref(),
Some("Lovely Spam! Wonderful Spam!")
);
assert_eq!(
project.readme,
Some(ReadMe::RelativePath("README.rst".to_string()))
);
assert_eq!(project.requires_python.as_deref(), Some(">=3.8"));
assert_eq!(
project.license.as_ref().unwrap().file.as_deref(),
Some("LICENSE.txt")
);
assert_eq!(
project.keywords.as_ref().unwrap(),
&["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
);
assert_eq!(
project.scripts.as_ref().unwrap()["spam-cli"],
"spam:main_cli"
);
assert_eq!(
project.gui_scripts.as_ref().unwrap()["spam-gui"],
"spam:main_gui"
);
}
#[test]
fn test_parse_pyproject_toml_readme_content_type() {
let source = r#"[build-system]
requires = ["maturin"]
build-backend = "maturin"
[project]
name = "spam"
readme = {text = "ReadMe!", content-type = "text/plain"}
"#;
let project_toml = PyProjectToml::new(source).unwrap();
let project = project_toml.project.as_ref().unwrap();
assert_eq!(
project.readme,
Some(ReadMe::Table {
file: None,
text: Some("ReadMe!".to_string()),
content_type: Some("text/plain".to_string())
})
);
}
}