use std::{
io,
path::{Path, PathBuf},
};
use chrono::{DateTime, Utc};
use thiserror::Error;
use uuid::Uuid;
pub use crate::ApplyIf;
use super::{property::Property, BuildSystem, Category, Ide, Language, Metadata};
#[derive(Debug)]
pub struct MetadataBuilder {
id: Option<Uuid>,
directory: Option<PathBuf>,
title: Option<String>,
categories: Vec<Category>,
languages: Vec<Language>,
preferred_ide: Option<Ide>,
build_systems: Vec<BuildSystem>,
description: Option<String>,
repository_url: Option<String>,
created: Option<DateTime<Utc>>,
updated: Option<DateTime<Utc>>,
}
impl MetadataBuilder {
#[allow(clippy::new_without_default)]
#[must_use]
pub(crate) fn new() -> MetadataBuilder {
Self {
id: None,
directory: None,
title: None,
categories: vec![],
languages: vec![],
preferred_ide: None,
build_systems: vec![],
description: None,
repository_url: None,
created: None,
updated: None,
}
}
#[must_use]
pub fn from_metadata(metadata: Metadata) -> MetadataBuilder {
Self {
id: Some(metadata.id),
directory: Some(metadata.directory),
title: Some(metadata.title),
categories: metadata.categories,
languages: metadata.languages,
preferred_ide: metadata.preferred_ide,
build_systems: metadata.build_systems,
description: metadata.description,
repository_url: metadata.repository_url,
created: Some(metadata.created),
updated: None,
}
}
pub fn build(mut self) -> Result<Metadata, Error> {
self.categories.sort();
self.build_systems.sort();
self.languages.sort();
Ok(Metadata {
id: self.id.unwrap_or_else(Uuid::new_v4),
directory: self.directory.ok_or(Error::DirectoryMissing)?,
title: self.title.ok_or(Error::TitleMissing)?,
categories: self.categories,
languages: self.languages,
preferred_ide: self.preferred_ide,
build_systems: self.build_systems,
description: self.description,
repository_url: self.repository_url,
created: self.created.unwrap_or_else(Utc::now),
updated: self.updated.unwrap_or_else(Utc::now),
})
}
#[must_use]
pub fn id(mut self, id: Uuid) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub fn directory(self, directory: &str) -> Self {
self.directory_path(Path::new(directory))
}
#[must_use]
pub fn directory_path(mut self, path: &Path) -> Self {
match path.canonicalize() {
Ok(absolute_path) => {
if absolute_path.is_dir() {
self.directory = Some(absolute_path.join("manifest.toml"));
} else {
self.directory = Some(absolute_path);
}
}
Err(_) => self.directory = Some(path.to_path_buf()),
}
self
}
#[must_use]
pub fn title(mut self, title: &str) -> Self {
self.title = Some(title.to_string());
self
}
#[must_use]
pub fn add_category(mut self, category: &str) -> Self {
if category.is_empty() {
return self;
}
self.categories.push(Category::new(category));
self
}
#[must_use]
pub fn categories(mut self, categories: Vec<Category>) -> Self {
self.categories = categories;
self
}
#[must_use]
pub fn add_language(mut self, language: Language) -> Self {
self.languages.push(language);
self
}
#[must_use]
pub fn languages(mut self, languages: Vec<Language>) -> Self {
self.languages = languages;
self
}
#[must_use]
pub fn add_build_system(mut self, build_system: BuildSystem) -> Self {
self.build_systems.push(build_system);
self
}
#[must_use]
pub fn build_systems(mut self, build_systems: Vec<BuildSystem>) -> Self {
self.build_systems = build_systems;
self
}
#[must_use]
pub fn preferred_ide(mut self, ide: Ide) -> Self {
self.preferred_ide = Some(ide);
self
}
#[must_use]
pub fn update_ide(mut self, ide: Option<Ide>) -> Self {
self.preferred_ide = ide;
self
}
#[must_use]
pub fn description(mut self, description: &str) -> Self {
self.description = match description.len() {
0 => None,
_ => Some(description.to_string()),
};
self
}
#[must_use]
pub fn update_description(mut self, description: Option<String>) -> Self {
self.description = description;
self
}
#[must_use]
pub fn repository_url(mut self, url: &str) -> Self {
self.repository_url = match url.len() {
0 => None,
_ => Some(url.to_string()),
};
self
}
#[must_use]
pub fn update_repository_url(mut self, url: Option<String>) -> Self {
self.repository_url = url;
self
}
#[must_use]
pub fn created(mut self, created: DateTime<Utc>) -> Self {
self.created = Some(created);
self
}
#[must_use]
pub fn updated(mut self, updated: DateTime<Utc>) -> Self {
self.updated = Some(updated);
self
}
}
impl ApplyIf for MetadataBuilder {
fn apply_if<T>(self, value: Option<T>, f: fn(Self, T) -> Self) -> Self {
match value {
Some(x) => f(self, x),
None => self,
}
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("title missing")]
TitleMissing,
#[error("directory missing")]
DirectoryMissing,
#[error("io error")]
Io(#[from] io::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metadata_builder() {
let builder = MetadataBuilder::new()
.title("Test Project")
.directory(".")
.add_category("Category1")
.add_language(Language::with_version("Rust", "1.84.0"))
.preferred_ide(Ide::new("VSCode"))
.add_build_system(BuildSystem::with_version("Cargo", "1.84.0"))
.description("A test project")
.repository_url("https://github.com/test/project");
let metadata = builder.build().unwrap();
assert_eq!(metadata.title, "Test Project");
assert_eq!(metadata.categories.len(), 1);
assert_eq!(metadata.categories[0].name, "Category1");
assert_eq!(metadata.languages.len(), 1);
assert_eq!(metadata.languages[0].name, "Rust");
assert!(metadata.preferred_ide.is_some());
assert_eq!(metadata.build_systems.len(), 1);
assert_eq!(metadata.description, Some("A test project".to_string()));
assert_eq!(
metadata.repository_url,
Some("https://github.com/test/project".to_string())
);
}
#[test]
fn test_metadata_missing_title() {
let builder = MetadataBuilder::new().directory(".");
let result = builder.build();
assert!(result.is_err());
if let Err(err) = result {
match err {
Error::TitleMissing => (),
_ => panic!("Unexpected error type"),
}
}
}
#[test]
fn test_metadata_missing_dir() {
let builder = MetadataBuilder::new().title("Test");
let result = builder.build();
assert!(result.is_err());
if let Err(err) = result {
match err {
Error::DirectoryMissing => (),
_ => panic!("Unexpected error type"),
}
}
}
}