mod author;
mod builder;
mod identity;
mod workspace;
pub use author::Author;
pub use builder::ProjectBuilder;
pub use identity::Identity;
pub use workspace::Workspace;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use cargo_metadata::Package;
use crate::config::Config;
pub const DEFAULT_MANIFEST_PATH: &str = "Cargo.toml";
pub(crate) const METADATA_KEY: &str = "npmgen";
#[derive(Debug, Clone, Default)]
pub struct Overrides {
pub packages: Vec<String>,
pub workspace: bool,
pub exclude: Vec<String>,
pub bins: Vec<String>,
pub version: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Project {
pub identity: Identity,
pub version: String,
pub description: String,
pub author: Author,
pub license: String,
pub repository: String,
pub bin: String,
pub package: Option<String>,
pub config: Config,
pub workspace_root: PathBuf,
pub target_directory: PathBuf,
}
#[derive(Debug, thiserror::Error)]
pub enum ProjectError {
#[error("running `cargo metadata`")]
Metadata {
#[source]
source: Box<cargo_metadata::Error>,
},
#[error("no workspace package named {name:?}")]
PackageNotFound { name: String },
#[error("package repository must be set to https://<host>/<owner>/<repo>")]
MissingRepository,
#[error("package {package:?} has no bin named {bin:?}")]
UnknownBin { package: String, bin: String },
#[error("no workspace bin named {bin:?}")]
BinNotInWorkspace { bin: String },
#[error("bin {bin:?} is defined by more than one package ({}); select one with --package", packages.join(", "))]
AmbiguousBin { bin: String, packages: Vec<String> },
#[error(
"nothing to publish: no selected package ships a binary (every match is a library or `publish = false`)"
)]
NothingToPublish,
#[error(
"{count} binaries match; narrow with --package/--bin, or use Project::discover for the full set"
)]
NotSingle { count: usize },
#[error("invalid project field {field}: {reason}")]
InvalidField {
field: &'static str,
reason: &'static str,
},
#[error(transparent)]
Config(#[from] crate::config::ConfigError),
}
impl Project {
pub fn builder(
scope: impl Into<String>,
name: impl Into<String>,
version: impl Into<String>,
) -> ProjectBuilder {
ProjectBuilder::new(scope, name, version)
}
pub fn discover(
manifest_path: &Path,
overrides: &Overrides,
) -> Result<Vec<Self>, ProjectError> {
let projects = Workspace::load(manifest_path)?.projects(overrides)?;
if projects.is_empty() {
return Err(ProjectError::NothingToPublish);
}
Ok(projects)
}
pub fn load(manifest_path: &Path, overrides: &Overrides) -> Result<Self, ProjectError> {
let mut projects = Self::discover(manifest_path, overrides)?;
match projects.len() {
1 => Ok(projects.pop().unwrap()),
count => Err(ProjectError::NotSingle { count }),
}
}
pub(crate) fn from_package_bin(
package: &Package,
bin: &str,
config: &Config,
overrides: &Overrides,
workspace_root: &Path,
target_directory: &Path,
) -> Result<Self, ProjectError> {
let repository = package
.repository
.clone()
.ok_or(ProjectError::MissingRepository)?;
let base = Identity::from_repository(&repository, config.scope.as_deref())?;
let identity = Identity {
name: bin.to_owned(),
..base
};
let version = overrides
.version
.clone()
.unwrap_or_else(|| package.version.to_string());
let license = config
.license
.clone()
.or_else(|| package.license.clone())
.unwrap_or_default();
Ok(Self {
identity,
version,
description: package.description.clone().unwrap_or_default(),
author: Author::parse(&package.authors.first().cloned().unwrap_or_default()),
license,
repository,
bin: bin.to_owned(),
package: Some(package.name.as_str().to_owned()),
config: config.clone(),
workspace_root: workspace_root.to_path_buf(),
target_directory: target_directory.to_path_buf(),
})
}
pub fn package_name(&self) -> String {
format!("{}/{}", self.identity.scope, self.identity.name)
}
pub fn variables(&self) -> BTreeMap<String, String> {
BTreeMap::from([
("name".to_owned(), self.identity.name.clone()),
("scope".to_owned(), self.identity.scope.clone()),
("package".to_owned(), self.package_name()),
("version".to_owned(), self.version.clone()),
("description".to_owned(), self.description.clone()),
("license".to_owned(), self.license.clone()),
("repository".to_owned(), self.repository.clone()),
("git_url".to_owned(), self.identity.git_url.clone()),
("bin".to_owned(), self.bin.clone()),
("author".to_owned(), self.author.full.clone()),
("author_name".to_owned(), self.author.name.clone()),
(
"author_email".to_owned(),
self.author.email.clone().unwrap_or_default(),
),
])
}
}
#[cfg(test)]
pub(crate) fn sample_project() -> Project {
Project {
identity: Identity {
scope: "@gglinnk".to_owned(),
name: "nocmd".to_owned(),
git_url: "git+https://github.com/gglinnk/nocmd.git".to_owned(),
},
version: "0.1.1".to_owned(),
description: "a hook".to_owned(),
author: Author::parse("Gabriel GRONDIN <gglinnk@protonmail.com>"),
license: "MIT".to_owned(),
repository: "https://github.com/gglinnk/nocmd".to_owned(),
bin: "nocmd".to_owned(),
package: Some("nocmd".to_owned()),
config: Config::default(),
workspace_root: PathBuf::from("."),
target_directory: PathBuf::from("target"),
}
}