use anyhow::{Result, anyhow};
use assert_fs::{TempDir, prelude::*};
use std::{
collections::{HashMap, HashSet},
fmt::{Debug, Write},
};
pub const CARGO_MANIFEST_FILENAME: &str = "Cargo.toml";
pub const ABOUT_CONFIG_FILENAME: &str = "about.toml";
pub const ABOUT_TEMPLATE_FILENAME: &str = "about.hbs";
pub struct Package {
pub dir: TempDir,
pub name: String,
pub version: String,
pub template_filename: Option<String>,
}
impl Package {
pub fn template(&self) -> Result<&str> {
match self.template_filename.as_ref() {
Some(template) => Ok(template),
None => Err(anyhow!("Package '{}' has no template", self.name)),
}
}
pub fn builder<'a>() -> PackageBuilder<'a> {
PackageBuilder::default()
}
}
impl Debug for Package {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Package")?;
writeln!(f, "{CARGO_MANIFEST_FILENAME}:")?;
writeln!(
f,
"{}",
std::fs::read_to_string(self.dir.child(CARGO_MANIFEST_FILENAME))
.unwrap_or_else(|_| "Couldn't read file.".into())
)?;
writeln!(f, "{ABOUT_CONFIG_FILENAME}:")?;
writeln!(
f,
"{}",
std::fs::read_to_string(self.dir.child(ABOUT_CONFIG_FILENAME))
.unwrap_or_else(|_| "Couldn't read file.".into())
)?;
writeln!(
f,
"src/main.rs exists: {}",
self.dir.child("src/main.rs").exists()
)
}
}
pub struct PackageBuilder<'a> {
name: String,
version: String,
license: Option<String>,
license_filename: Option<String>,
accepted: HashSet<String>,
files: HashMap<String, String>,
excludes: HashSet<String>,
dependencies: Vec<&'a Package>,
}
impl Default for PackageBuilder<'_> {
fn default() -> Self {
PackageBuilder {
name: "package".into(),
version: "0.0.0".into(),
license: None,
license_filename: None,
accepted: HashSet::new(),
files: HashMap::new(),
excludes: HashSet::new(),
dependencies: Vec::new(),
}
}
}
impl<'a> PackageBuilder<'a> {
pub fn no_manifest(&mut self) -> &mut Self {
self.excludes.insert(CARGO_MANIFEST_FILENAME.into());
self
}
#[allow(dead_code)]
pub fn no_about_config(&mut self) -> &mut Self {
self.excludes.insert(ABOUT_CONFIG_FILENAME.into());
self
}
pub fn no_template(&mut self) -> &mut Self {
self.excludes.insert(ABOUT_TEMPLATE_FILENAME.into());
self
}
pub fn name(&mut self, name: &str) -> &mut Self {
self.name = name.into();
self
}
#[allow(dead_code)]
pub fn version(&mut self, version: &str) -> &mut Self {
self.version = version.into();
self
}
pub fn license(&mut self, license: Option<&str>) -> &mut Self {
self.license = license.map(|s| s.into());
self
}
pub fn license_file(&mut self, filename: &str, content: Option<&str>) -> &mut Self {
self.license_filename = Some(filename.into());
if let Some(content) = content {
self.files.insert(filename.into(), content.into());
} else {
self.files.remove(filename);
}
self
}
pub fn file(&mut self, filename: &str, content: &str) -> &mut Self {
self.files.insert(filename.into(), content.into());
self
}
pub fn accepted(&mut self, accepted: &[&str]) -> &mut Self {
self.accepted = accepted
.iter()
.map(|s| (*s).to_owned())
.collect::<HashSet<_>>();
self
}
pub fn dependency(&mut self, package: &'a Package) -> &mut Self {
self.dependencies.push(package);
self
}
fn not_overridden_or_excluded(&self, filename: &str) -> bool {
!self.files.contains_key(filename) && !self.excludes.contains(filename)
}
fn write_default_cargo_manifest(&self, dir: &TempDir) -> Result<()> {
if !self.not_overridden_or_excluded(CARGO_MANIFEST_FILENAME) {
return Ok(());
}
let mut s = String::with_capacity(1024);
s.push_str("[package]\n");
writeln!(&mut s, "name = \"{}\"", self.name)?;
writeln!(&mut s, "version = \"{}\"", self.version)?;
if let Some(license) = &self.license {
writeln!(&mut s, "license = \"{license}\"")?;
}
if let Some(lf) = &self.license_filename {
writeln!(&mut s, "license-file = \"{lf}\"")?;
}
if !self.dependencies.is_empty() {
s.push_str("\n[dependencies]\n");
for package in &self.dependencies {
writeln!(
&mut s,
"{} = {{ version = '{}', path = '{}' }}",
package.name,
package.version,
package.dir.to_str().unwrap()
)?;
}
}
dir.child(CARGO_MANIFEST_FILENAME).write_str(&s)?;
Ok(())
}
fn write_default_about_config(&self, dir: &TempDir) -> Result<()> {
if self.not_overridden_or_excluded(ABOUT_CONFIG_FILENAME) {
let mut s = String::with_capacity(256);
writeln!(&mut s, "accepted = [")?;
for acc in &self.accepted {
writeln!(&mut s, "\"{acc}\",")?;
}
writeln!(&mut s, "]")?;
dir.child(ABOUT_CONFIG_FILENAME).write_str(&s)?;
}
Ok(())
}
fn write_default_lib(&self, dir: &TempDir) -> Result<()> {
let filename = "src/lib.rs";
if self.not_overridden_or_excluded(filename) {
dir.child(filename).write_str("")?;
}
Ok(())
}
fn write_default_template_if_absent(&self, dir: &TempDir) -> Result<()> {
if self.not_overridden_or_excluded(ABOUT_TEMPLATE_FILENAME) {
dir.child(ABOUT_TEMPLATE_FILENAME).write_str(
"\
The following line is used to assert on overview count:\n\
#o:[{{#each overview}}o{{/each}}]\n\
\n\
The following line is used to assert on license count:\n\
#l:[{{#each licenses}}l{{/each}}]\n\
\n\
The following is used to assert on license text (note that\n\
the raw (non-html-escaped) text is used to make assertions\n\
easier:\n\
{{#each licenses}}\n\
{{{text}}}\n\
{{/each}}\n\
",
)?;
}
Ok(())
}
pub fn write_files(&self, dir: &TempDir) -> Result<()> {
for (filename, content) in &self.files {
if !self.excludes.contains(filename) {
let file = dir.child(filename);
file.write_str(content)?;
}
}
Ok(())
}
pub fn build(&self) -> Result<Package> {
let dir = TempDir::new()?;
self.write_default_cargo_manifest(&dir)?;
self.write_default_about_config(&dir)?;
self.write_default_lib(&dir)?;
self.write_default_template_if_absent(&dir)?;
self.write_files(&dir)?;
Ok(Package {
dir,
template_filename: if self.excludes.contains(ABOUT_TEMPLATE_FILENAME) {
None
} else {
Some(ABOUT_TEMPLATE_FILENAME.into())
},
name: self.name.clone(),
version: self.version.clone(),
})
}
}