pub mod config;
mod error;
mod packages;
mod release;
use std::str::FromStr;
use bytes::Bytes;
use either::Either;
use relative_path::{RelativePath, RelativePathBuf};
use crate::package::lockfile::CargoLockfile;
use crate::package::manifest::CargoManifest;
use crate::package::{BumpOrVersion, Package, PackageKind};
use crate::repository::types::staging::Staging;
use crate::repository::{Remote, RepoSpec, Repository, Stage};
pub use self::config::Config;
pub use self::error::Error;
pub use self::packages::Packages;
pub use self::release::{ReleaseBuilder, ReleaseRequest, ReleaseRequestBuilder};
pub struct Project<T = Staging> {
pub(crate) repository: T,
config: Config,
}
impl Project {
pub fn new(name: impl Into<String>) -> Self {
let config = Config::new(name);
Self {
repository: Staging::new()
.with_file("Ploys.toml", config.to_string().into_bytes())
.expect("infallible"),
config,
}
}
}
impl<T> Project<T>
where
T: Repository,
{
pub fn open(repository: T) -> Result<Self, Error<T::Error>> {
let config = repository
.get_file("Ploys.toml")
.map_err(Error::Repository)?
.ok_or(self::config::Error::Missing)?;
Ok(Self {
config: Config::from_bytes(&config)?,
repository,
})
}
}
impl<T> Project<T> {
pub fn name(&self) -> &str {
self.config.project().name()
}
pub fn description(&self) -> Option<&str> {
self.config.project_description()
}
pub fn set_description(&mut self, description: impl Into<String>) -> &mut Self {
self.config.set_project_description(description);
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.set_description(description);
self
}
pub fn repository(&self) -> Option<RepoSpec> {
self.config.project_repository()
}
pub fn set_repository(&mut self, repository: impl Into<RepoSpec>) -> &mut Self {
self.config.set_project_repository(repository);
self
}
pub fn with_repository(mut self, repository: impl Into<RepoSpec>) -> Self {
self.set_repository(repository);
self
}
pub fn authors(&self) -> impl Iterator<Item = &str> {
self.config.project_authors()
}
pub fn set_authors(
&mut self,
authors: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.config
.set_project_authors(authors.into_iter().map(Into::into));
self
}
pub fn with_authors(mut self, authors: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.set_authors(authors);
self
}
}
impl<T> Project<T>
where
T: Stage,
{
pub fn add_file(
&mut self,
path: impl Into<RelativePathBuf>,
file: impl Into<Bytes>,
) -> Result<&mut Self, Error<T::Error>> {
self.repository
.add_file(path, file)
.map_err(Error::Repository)?;
Ok(self)
}
pub fn with_file(
mut self,
path: impl Into<RelativePathBuf>,
file: impl Into<Bytes>,
) -> Result<Self, Error<T::Error>> {
self.add_file(path, file)?;
Ok(self)
}
}
impl<T> Project<T>
where
T: Repository,
{
pub fn get_file(
&self,
path: impl AsRef<RelativePath>,
) -> Result<Option<Bytes>, Error<T::Error>> {
if path.as_ref() == "Ploys.toml" {
return Ok(Some(self.config.to_string().into()));
}
self.repository.get_file(path).map_err(Error::Repository)
}
#[allow(clippy::type_complexity)]
pub fn get_file_as<U>(
&self,
path: impl AsRef<RelativePath>,
) -> Result<Option<U>, Either<Error<T::Error>, U::Err>>
where
U: FromStr,
{
match self.get_file(path).map_err(Either::Left)? {
Some(bytes) => match std::str::from_utf8(&bytes) {
Ok(str) => str.parse().map(Some).map_err(Either::Right),
Err(err) => Err(Either::Left(Error::Utf8(err))),
},
None => Ok(None),
}
}
}
impl<T> Project<T>
where
T: Stage,
{
pub fn add_package(
&mut self,
package: impl Into<Package>,
) -> Result<&mut Self, Error<T::Error>> {
let package = package.into();
let base_path = RelativePath::new("packages").join(package.name());
for path in package.repository.get_index().expect("infallible") {
if path == package.manifest_path() {
continue;
}
if let Some(file) = package
.repository
.get_file(&path)
.expect("valid path from index")
{
self.add_file(base_path.join(path), file)?;
}
}
self.add_file(
base_path.join(package.manifest_path()),
package.manifest().to_string().into_bytes(),
)?;
match package.kind() {
PackageKind::Cargo => {
let mut manifest = self
.get_file_as::<CargoManifest>("Cargo.toml")
.map_err(|err| {
err.map_right(crate::package::Error::Manifest)
.map_right(Error::Package)
.into_inner()
})?
.unwrap_or_default();
manifest.add_workspace_member("packages/*");
manifest.add_workspace_member(base_path.join(package.path()).as_str());
let mut lockfile = self
.get_file_as::<CargoLockfile>("Cargo.lock")
.map_err(|err| {
err.map_right(crate::package::Error::Lockfile)
.map_right(Error::Package)
.into_inner()
})?
.unwrap_or_default();
lockfile.add_package(package.manifest().try_as_cargo_ref().expect("cargo"));
self.add_file("Cargo.toml", manifest.to_string().into_bytes())?;
self.add_file("Cargo.lock", lockfile.to_string().into_bytes())?;
}
}
Ok(self)
}
pub fn with_package(mut self, package: impl Into<Package>) -> Result<Self, Error<T::Error>> {
self.add_package(package)?;
Ok(self)
}
}
impl<T> Project<T>
where
T: Repository,
{
pub fn get_package(&self, name: impl AsRef<str>) -> Option<Package<&T>> {
self.packages()
.find(|package| package.name() == name.as_ref())
}
pub fn packages(&self) -> Packages<'_, T> {
Packages::new(self)
}
}
impl<T> Project<T>
where
T: Repository,
{
pub fn reload(&mut self) -> Result<&mut Self, Error<T::Error>> {
let config = self
.repository
.get_file("Ploys.toml")
.map_err(Error::Repository)?
.ok_or(self::config::Error::Missing)?;
self.config = Config::from_bytes(&config)?;
Ok(self)
}
pub fn reloaded(mut self) -> Result<Self, Error<T::Error>> {
self.reload()?;
Ok(self)
}
}
impl<T> Project<T>
where
T: Remote + Clone,
{
pub fn create_package_release_request(
&self,
package: impl AsRef<str>,
version: impl Into<BumpOrVersion>,
) -> Result<ReleaseRequestBuilder<'_, T>, Error<T::Error>> {
let package = self
.get_package(package.as_ref())
.ok_or_else(|| {
Error::Package(crate::package::Error::NotFound(
package.as_ref().to_string(),
))
})?
.detached();
Ok(ReleaseRequestBuilder::new(self, package, version.into()))
}
pub fn create_package_release(
&self,
package: impl AsRef<str>,
) -> Result<ReleaseBuilder<'_, T>, Error<T::Error>> {
let package = self
.get_package(package.as_ref())
.ok_or_else(|| {
Error::Package(crate::package::Error::NotFound(
package.as_ref().to_string(),
))
})?
.detached();
Ok(ReleaseBuilder::new(self, package))
}
}
#[cfg(feature = "fs")]
mod fs {
use std::io::{Error as IoError, ErrorKind};
use std::path::PathBuf;
use crate::repository::types::fs::{Error as FsError, FileSystem};
use crate::repository::types::staging::Staging;
use crate::repository::{Commit, Open, Repository, Stage};
use super::{Error, Project};
impl Project<FileSystem> {
pub fn fs<P>(path: P) -> Result<Self, Error<FsError>>
where
P: Into<PathBuf>,
{
Self::open(FileSystem::open(path)?)
}
pub fn current_dir() -> Result<Self, Error<FsError>> {
Self::open(FileSystem::current_dir()?)
}
pub fn write(&mut self) -> Result<&mut Self, Error<FsError>> {
self.repository.commit(())?;
Ok(self)
}
}
impl Project<Staging> {
pub fn write<P>(self, path: P, force: bool) -> Result<Project<FileSystem>, Error<FsError>>
where
P: Into<PathBuf>,
{
let path = path.into();
let repository = FileSystem::open(&path)?;
if !force && repository.get_index()?.count() > 0 {
return Err(Error::Repository(FsError::Io(IoError::new(
ErrorKind::DirectoryNotEmpty,
"Expected an empty directory",
))));
}
Ok(Project {
repository: repository
.with_files(self.repository)?
.with_file("Ploys.toml", self.config.to_string())?
.committed(())?,
config: self.config,
})
}
}
}
#[cfg(feature = "git")]
mod git {
use std::path::PathBuf;
use crate::repository::Open;
use crate::repository::revision::Revision;
use crate::repository::types::git::{Error as GitError, Git};
use super::{Error, Project};
impl Project<Git> {
pub fn git<P>(path: P) -> Result<Self, Error<GitError>>
where
P: Into<PathBuf>,
{
Self::open(Git::open(path).map_err(Error::Repository)?)
}
pub fn git_with_revision<P, V>(path: P, revision: V) -> Result<Self, Error<GitError>>
where
P: Into<PathBuf>,
V: Into<Revision>,
{
Self::open(Git::open(path)?.with_revision(revision))
}
}
impl TryFrom<Git> for Project<Git> {
type Error = Error<GitError>;
fn try_from(repository: Git) -> Result<Self, Self::Error> {
Self::open(repository)
}
}
}
#[cfg(feature = "github")]
mod github {
use crate::repository::Open;
use crate::repository::revision::Revision;
use crate::repository::types::github::{Error as GitHubError, GitHub, GitHubRepoSpec};
use super::{Error, Project};
impl Project<GitHub> {
pub fn github<R>(repo: R) -> Result<Self, Error<GitHubError>>
where
R: TryInto<GitHubRepoSpec, Error: Into<GitHubError>>,
{
Self::open(GitHub::open(repo)?.validated()?)
}
pub fn github_with_revision<R, V>(repo: R, revision: V) -> Result<Self, Error<GitHubError>>
where
R: TryInto<GitHubRepoSpec, Error: Into<GitHubError>>,
V: Into<Revision>,
{
Self::open(GitHub::open(repo)?.with_revision(revision).validated()?)
}
pub fn github_with_authentication_token<R, T>(
repo: R,
token: T,
) -> Result<Self, Error<GitHubError>>
where
R: TryInto<GitHubRepoSpec, Error: Into<GitHubError>>,
T: Into<String>,
{
Self::open(
GitHub::open(repo)?
.with_authentication_token(token)
.validated()?,
)
}
pub fn github_with_revision_and_authentication_token<R, V, T>(
repo: R,
revision: V,
token: T,
) -> Result<Self, Error<GitHubError>>
where
R: TryInto<GitHubRepoSpec, Error: Into<GitHubError>>,
V: Into<Revision>,
T: Into<String>,
{
Self::open(
GitHub::open(repo)?
.with_revision(revision)
.with_authentication_token(token)
.validated()?,
)
}
}
impl TryFrom<GitHub> for Project<GitHub> {
type Error = Error<GitHubError>;
fn try_from(repository: GitHub) -> Result<Self, Self::Error> {
Self::open(repository)
}
}
}
#[cfg(all(feature = "fs", feature = "git"))]
mod fs_git {
use crate::repository::Open;
use crate::repository::revision::Revision;
use crate::repository::types::fs::FileSystem;
use crate::repository::types::git::{Error as GitError, Git};
use super::{Error, Project};
impl Project<FileSystem> {
pub fn into_git(
self,
revision: impl Into<Revision>,
) -> Result<Project<Git>, Error<GitError>> {
Ok(Project {
repository: Git::open(self.repository.path())?.with_revision(revision),
config: self.config,
})
}
pub fn init_git(self) -> Result<Project<Git>, Error<GitError>> {
Ok(Project {
repository: Git::init(self.repository.path())?,
config: self.config,
})
}
}
impl TryFrom<Project<FileSystem>> for Project<Git> {
type Error = Error<GitError>;
fn try_from(project: Project<FileSystem>) -> Result<Self, Self::Error> {
project.into_git(Revision::head())
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use semver::Version;
use crate::changelog::Changelog;
use crate::package::Package;
use crate::package::lockfile::CargoLockfile;
use crate::package::manifest::CargoManifest;
use crate::repository::types::staging::Staging;
use crate::repository::{RepoSpec, Stage};
use super::Project;
#[test]
fn test_builder() {
let project = Project::new("example")
.with_description("An example project.")
.with_repository("ploys/example".parse::<RepoSpec>().unwrap())
.with_authors(["Joe Bloggs <joe.bloggs@example.com>"]);
assert_eq!(project.name(), "example");
assert_eq!(project.description().unwrap(), "An example project.");
assert_eq!(
project.repository().unwrap(),
"ploys/example".parse::<RepoSpec>().unwrap()
);
assert_eq!(
project.authors().collect::<Vec<_>>(),
["Joe Bloggs <joe.bloggs@example.com>"]
);
let mut project = project.reloaded().unwrap();
assert_eq!(project.name(), "example");
assert_eq!(project.description(), None);
assert_eq!(project.repository(), None);
assert_eq!(project.authors().count(), 0);
let package_a = Package::new_cargo("example-one");
let package_b = Package::new_cargo("example-two")
.with_version(Version::new(0, 1, 0))
.with_file("CHANGELOG.md", Changelog::new().to_string().into_bytes())
.unwrap();
project.add_package(package_a).unwrap();
project.add_package(package_b).unwrap();
let package_a = project.get_package("example-one").unwrap();
let package_b = project.get_package("example-two").unwrap();
assert_eq!(package_a.name(), "example-one");
assert_eq!(package_a.version(), Version::new(0, 0, 0));
assert_eq!(package_a.get_file("CHANGELOG.md").unwrap(), None);
assert_eq!(package_b.name(), "example-two");
assert_eq!(package_b.version(), Version::new(0, 1, 0));
assert_eq!(
package_b.get_file_as("CHANGELOG.md").unwrap(),
Some(Changelog::new())
);
let manifest = project
.get_file_as::<CargoManifest>("Cargo.toml")
.unwrap()
.unwrap();
let members = manifest.members().unwrap();
assert!(members.includes(Path::new("packages/example-one")));
assert!(members.includes(Path::new("packages/example-two")));
let lockfile = project
.get_file_as::<CargoLockfile>("Cargo.lock")
.unwrap()
.unwrap();
assert_eq!(
lockfile.get_package_version("example-one"),
Some(Version::new(0, 0, 0))
);
assert_eq!(
lockfile.get_package_version("example-two"),
Some(Version::new(0, 1, 0))
);
}
#[test]
fn test_project_staging_repository() {
let repository = Staging::new()
.with_file("Ploys.toml", "[project]\nname = \"example\"")
.unwrap();
let mut project = Project::open(repository).unwrap();
assert_eq!(project.name(), "example");
assert_eq!(project.description(), None);
project.set_description("An example project.");
assert_eq!(project.description(), Some("An example project."));
let mut project = project.reloaded().unwrap();
assert_eq!(project.name(), "example");
assert_eq!(project.description(), None);
project.add_file("hello-world.txt", "Hello World!").unwrap();
let txt = project.get_file("hello-world.txt").unwrap();
assert_eq!(txt, Some("Hello World!".into()));
}
}