From d2c0e263e31623aa3d41fd32989a191a3dd07aa1 Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Thu, 1 Sep 2022 19:58:44 +0300 Subject: [PATCH 01/18] add: persistance system --- Cargo.toml | 3 + src/model.rs | 5 + src/model/pacbuild.rs | 51 +++++++ src/model/repository.rs | 13 ++ src/store.rs | 9 ++ src/store/error.rs | 24 ++++ src/store/filesystem.rs | 303 ++++++++++++++++++++++++++++++++++++++++ src/store/filters.rs | 37 +++++ src/store/storable.rs | 145 +++++++++++++++++++ 9 files changed, 590 insertions(+) create mode 100644 src/model.rs create mode 100644 src/model/pacbuild.rs create mode 100644 src/model/repository.rs create mode 100644 src/store.rs create mode 100644 src/store/error.rs create mode 100644 src/store/filesystem.rs create mode 100644 src/store/filters.rs create mode 100644 src/store/storable.rs diff --git a/Cargo.toml b/Cargo.toml index 826a00a..a64d26a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,9 @@ categories = ["caching", "config", "parsing", "os::linux-apis"] figment = { version = "0.10.6", features = ["env", "test", "toml" ] } num_cpus = "1.13.1" serde = { version = "1.0.144", features = ["derive"] } +chrono = { version = "0.4.22", features = ["serde"] } +serde_derive = "1.0.144" +serde_json = "1.0.85" [dev-dependencies] rstest = "0.15.0" diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..4950e78 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,5 @@ +mod pacbuild; +mod repository; + +pub use crate::model::pacbuild::*; +pub use crate::model::repository::Repository; diff --git a/src/model/pacbuild.rs b/src/model/pacbuild.rs new file mode 100644 index 0000000..268c029 --- /dev/null +++ b/src/model/pacbuild.rs @@ -0,0 +1,51 @@ +use chrono::NaiveDateTime as DateTime; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PacBuild { + pub name: PackageId, + pub last_updated: DateTime, + pub repository: URL, + pub maintainer: String, + pub package_name: String, + pub description: String, + pub homepage: URL, + pub repology_version: Version, + pub repology: URL, + pub install_state: InstallState, + pub dependencies: Vec, + pub optional_dependencies: Vec, + pub license: String, + pub url: URL, + pub kind: Kind, +} + +pub type Version = String; +pub type PackageId = String; +pub type URL = String; +pub type Hash = String; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum InstallState { + Direct(DateTime, Version), + Indirect(DateTime, Version), + None, +} + +impl InstallState { + pub fn is_installed(&self) -> bool { + match self { + Self::None => false, + _ => true, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Kind { + AppImage(Hash), + Binary(Hash), + DebFile(Hash), + GitBranch, + GitRelease, +} diff --git a/src/model/repository.rs b/src/model/repository.rs new file mode 100644 index 0000000..077a4d3 --- /dev/null +++ b/src/model/repository.rs @@ -0,0 +1,13 @@ +use chrono::NaiveDateTime as DateTime; +use serde_derive::{Deserialize, Serialize}; + +use crate::model::pacbuild::PacBuild; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Repository { + pub name: String, + pub last_updated: DateTime, + pub url: String, + pub pacbuilds: Vec, + pub priority: u8, +} diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..261d3e0 --- /dev/null +++ b/src/store.rs @@ -0,0 +1,9 @@ +use self::storable::Storable; + +mod error; +mod filesystem; +pub mod filters; +pub mod storable; + +pub use error::StoreError; +pub use filesystem::FileSystemStore; diff --git a/src/store/error.rs b/src/store/error.rs new file mode 100644 index 0000000..f395f04 --- /dev/null +++ b/src/store/error.rs @@ -0,0 +1,24 @@ +#[derive(Clone)] +pub struct StoreError { + pub message: String, +} + +impl StoreError { + pub fn new(message: &str) -> StoreError { + StoreError { + message: message.to_string(), + } + } +} + +impl std::fmt::Display for StoreError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Store error: {}", self.message) + } +} + +impl std::fmt::Debug for StoreError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}:{}] Store error: {}", file!(), line!(), self.message) + } +} diff --git a/src/store/filesystem.rs b/src/store/filesystem.rs new file mode 100644 index 0000000..12884d6 --- /dev/null +++ b/src/store/filesystem.rs @@ -0,0 +1,303 @@ +use std::collections::HashMap; + +use super::StoreError; +use crate::model::{PacBuild, Repository}; +use crate::store::filters::{InstallState, Kind}; +use crate::store::storable::{Storable, UnitStoreResult}; +pub struct FileSystemStore { + repositories: Vec, + packages: HashMap>, + + allow_data_save: bool, +} + +impl FileSystemStore { + pub fn new() -> Box { + Box::new(FileSystemStore { + repositories: vec![], + packages: HashMap::new(), + allow_data_save: true, + }) + } + + fn get_packages_by_repository( + &self, + repository_url: &str, + ) -> Result<&Vec, StoreError> { + self.packages.get(&repository_url.to_owned()).map_or_else( + || { + Err(StoreError::new( + format!("Repository \"{}\" does not exist.", repository_url).as_str(), + )) + }, + |it| Ok(it), + ) + } + + fn save_to_disk(&self) { + if self.allow_data_save { + todo!() + } + } +} + +impl Storable for FileSystemStore { + fn get_pacbuild_by_name_and_url(&self, name: &str, url: &str) -> Option<&PacBuild> { + self.packages + .iter() + .filter(|(repo_url, _)| (*repo_url).to_owned() == url.to_owned()) + .flat_map(|(_, pkgs)| pkgs) + .find(|p| p.name == name.to_owned()) + } + + fn get_repository_by_name(&self, name: &str) -> Option<&Repository> { + self.repositories + .iter() + .find(|repo| repo.name == name.to_owned()) + } + + fn get_repository_by_url(&self, url: &str) -> Option<&Repository> { + self.repositories + .iter() + .find(|repo| repo.url == url.to_owned()) + } + + fn get_all_pacbuilds_by( + &self, + name_like: Option<&str>, + install_state: Option, + kind: Option, + repository_url: Option<&str>, + ) -> Vec<&PacBuild> { + let repos_urls = if let Some(url) = repository_url { + self.repositories + .iter() + .find(|it| it.url == url.to_string()) + .map_or_else(|| vec![], |it| vec![it.url.to_owned()]) + } else { + self.repositories + .iter() + .map(|it| it.url.to_owned()) + .collect() + }; + + self.packages + .iter() + .filter(|(repo_url, _)| repos_urls.contains(repo_url)) + .flat_map(|(_, pkgs)| pkgs) + .filter(|it| { + if let Some(kind_filter) = &kind { + kind_filter.to_owned() == Kind::from_model_kind(it.kind.clone()) + } else { + false + } + }) + .filter(|it| { + if let Some(install_state_filter) = &install_state { + install_state_filter.to_owned() + == InstallState::from_model_install_state(it.install_state.clone()) + } else { + false + } + }) + .filter(|it| { + if let Some(name_like) = name_like { + it.name.contains(name_like) + } else { + false + } + }) + .collect() + } + + fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> Result<(), StoreError> { + let new_list = self + .get_packages_by_repository(repository_url)? + .iter() + .filter(|it| it.name != name.to_owned()) + .map(|it| it.clone()) + .collect::>(); + + self.packages.insert(repository_url.to_owned(), new_list); + + self.save_to_disk(); + Ok(()) + } + + fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult { + let mut new_list = self.get_packages_by_repository(repository_url)?.to_owned(); + + new_list.push(pacbuild.clone()); + self.packages.insert(repository_url.to_owned(), new_list); + + self.save_to_disk(); + + Ok(()) + } + + fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult { + let new_list = self + .get_packages_by_repository(repository_url)? + .iter() + .map(|it| { + if it.name == pacbuild.name.to_owned() { + pacbuild.clone() + } else { + it.clone() + } + }) + .collect(); + + self.packages.insert(repository_url.to_owned(), new_list); + self.save_to_disk(); + + Ok(()) + } + + fn remove_all_pacbuilds( + &mut self, + names: Vec<&str>, + repository_url: &str, + ) -> Result<(), StoreError> { + let str_names: Vec = names.iter().map(|name| name.to_string()).collect(); + + let new_list: Vec = self + .get_packages_by_repository(repository_url)? + .to_owned() + .into_iter() + .filter(|it| str_names.contains(&it.name)) + .collect(); + + self.packages.insert(repository_url.to_owned(), new_list); + self.save_to_disk(); + + Ok(()) + } + + fn add_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> UnitStoreResult { + let mut new_list: Vec = + self.get_packages_by_repository(repository_url)?.to_owned(); + + let already_existing_pkgs: Vec<&PacBuild> = pacbuilds + .iter() + .filter(|it| { + self.get_pacbuild_by_name_and_url(it.name.as_str(), &repository_url) + .is_some() + }) + .collect(); + + if !already_existing_pkgs.is_empty() { + return Err(StoreError::new( + format!( + "The following PACBUILDs already exist: {:#?}", + already_existing_pkgs + ) + .as_str(), + )); + } + + let mut to_add = pacbuilds.to_owned(); + new_list.append(&mut to_add); + self.packages.insert(repository_url.to_owned(), new_list); + self.save_to_disk(); + + Ok(()) + } + + fn update_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> UnitStoreResult { + self.allow_data_save = false; + let errors: Vec = pacbuilds + .iter() + .map(|it| self.update_pacbuild(it.to_owned(), repository_url)) + .filter(|it| it.is_err()) + .collect(); + + self.allow_data_save = true; + + if errors.is_empty() { + self.save_to_disk(); + Ok(()) + } else { + let e = errors.first().unwrap().clone().expect_err("unreachable"); + Err(StoreError::new(e.message.as_str())) + } + } + + fn remove_repository(&mut self, repository_url: &str) -> Result<(), StoreError> { + let repo_exists = self + .repositories + .iter() + .any(|it| it.url.as_str() == repository_url); + + if !repo_exists { + return Err(StoreError::new( + format!("Repository {} does not exist.", repository_url).as_str(), + )); + } + + self.repositories = self + .repositories + .iter() + .filter(|repo| repo.url != repository_url) + .map(|it| it.to_owned()) + .collect(); + + self.packages.remove(&repository_url.to_owned()); + self.save_to_disk(); + + Ok(()) + } + + fn add_repository(&mut self, repository: Repository) -> Result<(), StoreError> { + let repo_exists = self.repositories.iter().any(|it| it.url == repository.url); + + if repo_exists { + return Err(StoreError::new( + format!("Repository {} already exists.", repository.url).as_str(), + )); + } + + self.packages.insert(repository.url.clone(), Vec::new()); + self.repositories.push(repository); + self.save_to_disk(); + + Ok(()) + } + + fn update_repository(&mut self, repository: Repository) -> UnitStoreResult { + let repo_exists = self + .repositories + .iter() + .any(|it| it.url == repository.url.to_owned()); + + if !repo_exists { + return Err(StoreError::new( + format!("Repository {} does not exist.", repository.url).as_str(), + )); + } + + self.repositories = self + .repositories + .iter() + .map(|it| { + if it.url == repository.url.to_owned() { + repository.to_owned() + } else { + it.to_owned() + } + }) + .collect(); + + self.save_to_disk(); + + Ok(()) + } +} diff --git a/src/store/filters.rs b/src/store/filters.rs new file mode 100644 index 0000000..9b0c6ef --- /dev/null +++ b/src/store/filters.rs @@ -0,0 +1,37 @@ +#[derive(Debug, PartialEq, Clone)] +pub enum InstallState { + Direct, + Indirect, + None, +} + +impl InstallState { + pub fn from_model_install_state(other: crate::model::InstallState) -> InstallState { + match other { + crate::model::InstallState::Indirect(..) => InstallState::Indirect, + crate::model::InstallState::Direct(..) => InstallState::Direct, + crate::model::InstallState::None => InstallState::None, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Kind { + AppImage, + Binary, + DebFile, + GitBranch, + GitRelease, +} + +impl Kind { + pub fn from_model_kind(other: crate::model::Kind) -> Kind { + match other { + crate::model::Kind::GitRelease => Kind::GitRelease, + crate::model::Kind::GitBranch => Kind::GitBranch, + crate::model::Kind::AppImage(_) => Kind::AppImage, + crate::model::Kind::Binary(_) => Kind::Binary, + crate::model::Kind::DebFile(_) => Kind::DebFile, + } + } +} diff --git a/src/store/storable.rs b/src/store/storable.rs new file mode 100644 index 0000000..1e7d879 --- /dev/null +++ b/src/store/storable.rs @@ -0,0 +1,145 @@ +use crate::model::{PacBuild, Repository}; +use crate::store::filters::{InstallState, Kind}; +use crate::store::StoreError; + +pub type UnitStoreResult = Result<(), StoreError>; + +pub trait Storable { + fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> UnitStoreResult; + fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult; + fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult; + + fn remove_all_pacbuilds(&mut self, name: Vec<&str>, repository_url: &str) -> UnitStoreResult; + fn add_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> UnitStoreResult; + fn update_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> UnitStoreResult; + + fn remove_repository(&mut self, repository_url: &str) -> UnitStoreResult; + fn add_repository(&mut self, repository: Repository) -> UnitStoreResult; + fn update_repository(&mut self, repository: Repository) -> UnitStoreResult; + + fn get_pacbuild_by_name_and_url(&self, name: &str, repository_url: &str) -> Option<&PacBuild>; + fn get_repository_by_name(&self, name: &str) -> Option<&Repository>; + fn get_repository_by_url(&self, url: &str) -> Option<&Repository>; + + fn get_all_pacbuilds_by( + &self, + name_like: Option<&str>, + install_state: Option, + kind: Option, + repository_url: Option<&str>, + ) -> Vec<&PacBuild>; +} + +impl dyn Storable { + pub fn get_all_pacbuilds_by_name_like(&self, name_like: &str) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), None, None, None) + } + + pub fn get_all_pacbuilds_by_name_like_and_kind( + &self, + name_like: &str, + kind: Kind, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), None, Some(kind), None) + } + + pub fn get_all_pacbuilds_by_name_like_and_install_state( + &self, + name_like: &str, + install_state: InstallState, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), Some(install_state), None, None) + } + + pub fn get_all_pacbuilds_by_name_like_and_repository_url( + &self, + name_like: &str, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), None, None, Some(url)) + } + + pub fn get_all_pacbuilds_by_name_like_and_install_state_and_kind( + &self, + name_like: &str, + install_state: InstallState, + kind: Kind, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), Some(install_state), Some(kind), None) + } + + pub fn get_all_pacbuilds_by_name_like_and_install_state_and_repository_url( + &self, + name_like: &str, + install_state: InstallState, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), Some(install_state), None, Some(url)) + } + + pub fn get_all_pacbuilds_by_name_like_and_install_state_and_kind_and_repository_url( + &self, + name_like: &str, + install_state: InstallState, + kind: Kind, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(Some(name_like), Some(install_state), Some(kind), Some(url)) + } + + pub fn get_all_pacbuilds_by_kind(&self, kind: Kind) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, None, Some(kind), None) + } + + pub fn get_all_pacbuilds_by_kind_and_install_state( + &self, + kind: Kind, + install_state: InstallState, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, Some(install_state), Some(kind), None) + } + + pub fn get_all_pacbuilds_by_kind_and_repository_url( + &self, + kind: Kind, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, None, Some(kind), Some(url)) + } + + pub fn get_all_pacbuilds_by_kind_and_install_state_and_repository_url( + &self, + kind: Kind, + install_state: InstallState, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, Some(install_state), Some(kind), Some(url)) + } + + pub fn get_all_pacbuilds_by_install_state( + &self, + install_state: InstallState, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, Some(install_state), None, None) + } + + pub fn get_all_pacbuilds_by_install_state_and_repository_url( + &self, + install_state: InstallState, + url: &str, + ) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, Some(install_state), None, Some(url)) + } + + pub fn get_all_pacbuilds_by_repository_url(&self, url: &str) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, None, None, Some(url)) + } +} From 94b29dc14237dc1e03616dbe1cb9114eb4c20600 Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Mon, 5 Sep 2022 22:14:29 +0300 Subject: [PATCH 02/18] add: caching system --- Cargo.toml | 1 + src/config.rs | 35 +- src/lib.rs | 2 + src/model.rs | 5 - src/model/mod.rs | 7 + src/model/pacbuild.rs | 78 +++- src/model/repository.rs | 36 +- src/store.rs | 9 - src/store/{storable.rs => base.rs} | 88 +++- src/store/error.rs | 24 -- src/store/errors.rs | 25 ++ src/store/filesystem.rs | 303 -------------- src/store/filters.rs | 12 +- src/store/fs.rs | 642 +++++++++++++++++++++++++++++ src/store/mod.rs | 6 + 15 files changed, 863 insertions(+), 410 deletions(-) delete mode 100644 src/model.rs create mode 100644 src/model/mod.rs delete mode 100644 src/store.rs rename src/store/{storable.rs => base.rs} (66%) delete mode 100644 src/store/error.rs create mode 100644 src/store/errors.rs delete mode 100644 src/store/filesystem.rs create mode 100644 src/store/fs.rs create mode 100644 src/store/mod.rs diff --git a/Cargo.toml b/Cargo.toml index a64d26a..9855aea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ serde = { version = "1.0.144", features = ["derive"] } chrono = { version = "0.4.22", features = ["serde"] } serde_derive = "1.0.144" serde_json = "1.0.85" +thiserror = "1.0" [dev-dependencies] rstest = "0.15.0" diff --git a/src/config.rs b/src/config.rs index fc8f430..29f5ef9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,6 +35,8 @@ use figment::value::{Dict, Map}; use figment::{Error, Figment, Metadata, Profile, Provider}; use serde::{Deserialize, Serialize}; +use crate::model::{default_repository, Repository}; + /// Pacstall's configuration. /// /// Gives access to the [configuration](Config) extracted, and the [Figment] @@ -191,39 +193,6 @@ impl Default for Settings { } } -/// The extracted `repositories` array of tables. -/// -/// Defaults to the official repository. -#[derive(Deserialize, Debug, Eq, PartialEq, Serialize)] -#[serde(deny_unknown_fields)] -pub struct Repository { - /// The name of the repository. - pub name: String, - /// URL of the repository. - /// - /// Note that the URL **isn't verified** during extraction! - pub url: String, - /// Preference of the repository. - /// - /// Specifies which repository to look into first during certain operations - /// like installing a package. If the package isn't present in the first - /// preferred repository, then the second preferred repository is looked - /// into. - pub preference: u32, -} - -fn default_repository() -> Vec { vec![Repository::default()] } - -impl Default for Repository { - fn default() -> Self { - Self { - name: "official".into(), - url: "https://github.com/pacstall/pacstall-programs".into(), - preference: 1, - } - } -} - #[cfg(test)] mod tests { use std::fs::{self, File}; diff --git a/src/lib.rs b/src/lib.rs index 7b8a174..eabe0a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,5 @@ )] #![allow(clippy::must_use_candidate)] pub mod config; +pub mod model; +pub mod store; diff --git a/src/model.rs b/src/model.rs deleted file mode 100644 index 4950e78..0000000 --- a/src/model.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod pacbuild; -mod repository; - -pub use crate::model::pacbuild::*; -pub use crate::model::repository::Repository; diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..e212776 --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,7 @@ +//! Provides structs to handle Pacstall's data models. + +mod pacbuild; +mod repository; + +pub use crate::model::pacbuild::*; +pub use crate::model::repository::{default_repository, Repository}; diff --git a/src/model/pacbuild.rs b/src/model/pacbuild.rs index 268c029..0320f79 100644 --- a/src/model/pacbuild.rs +++ b/src/model/pacbuild.rs @@ -1,7 +1,8 @@ use chrono::NaiveDateTime as DateTime; use serde_derive::{Deserialize, Serialize}; -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Representation of the PACBUILD file. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PacBuild { pub name: PackageId, pub last_updated: DateTime, @@ -20,32 +21,91 @@ pub struct PacBuild { pub kind: Kind, } +/// Represents a `SemVer` version. +/// # Examples +/// ``` +/// use libpacstall::model::Version; +/// +/// let ver: Version = "1.0.0".into(); +/// ``` pub type Version = String; + +/// Represents a `PacBuild` or Apt package name. +/// # Examples +/// ``` +/// use libpacstall::model::PackageId; +/// +/// let identifier: PackageId = "discord-deb".into(); +/// ``` pub type PackageId = String; +/// Represents an URL +/// # Examples +/// ``` +/// use libpacstall::model::URL; +/// +/// let url: URL = "https://example.com".into(); +/// ``` pub type URL = String; +/// Represents a file checksum +/// # Examples +/// ``` +/// use libpacstall::model::Hash; +/// +/// let hash: Hash = "b5c9710f33204498efb64cf8257cd9b19e9d3e6b".into(); +/// ``` pub type Hash = String; -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Represents the install state of a package. +/// # Examples +/// ``` +/// use chrono::NaiveDate; +/// use libpacstall::model::InstallState; +/// +/// let installed_directly = InstallState::Direct( +/// NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11), +/// "0.9.2".into(), +/// ); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum InstallState { + /// Package is installed directly, meaning the user wanted it. Direct(DateTime, Version), + + /// Package is installed as a dependency. Indirect(DateTime, Version), + + /// Package is not installed. None, } impl InstallState { - pub fn is_installed(&self) -> bool { - match self { - Self::None => false, - _ => true, - } - } + /// Returns `true` if the package is installed otherwise `false`. + pub fn is_installed(&self) -> bool { !matches!(self, Self::None) } } -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Represents the type of the package. Usually deduced by the [PacBuild#name] +/// suffix. +/// +/// # Examples +/// ``` +/// use libpacstall::model::Kind; +/// +/// let git_release = Kind::GitRelease; +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum Kind { + /// [PacBuild] will install an `AppImage`. AppImage(Hash), + + /// [PacBuild] will install a prebuilt, usually `tar.gz`, package. Binary(Hash), + + /// [PacBuild] will install an existing `.deb` file. DebFile(Hash), + + /// [PacBuild] will install the source of a given Git branch. GitBranch, + + /// [PacBuild] will install the source of a given Git release. GitRelease, } diff --git a/src/model/repository.rs b/src/model/repository.rs index 077a4d3..0395507 100644 --- a/src/model/repository.rs +++ b/src/model/repository.rs @@ -1,13 +1,35 @@ -use chrono::NaiveDateTime as DateTime; use serde_derive::{Deserialize, Serialize}; -use crate::model::pacbuild::PacBuild; - -#[derive(Debug, Clone, Serialize, Deserialize)] +/// Representation of a Pacstall repository. +/// +/// Defaults to the official repository. +#[derive(Deserialize, Debug, Eq, PartialEq, Serialize, Clone)] +#[serde(deny_unknown_fields)] pub struct Repository { + /// The name of the repository. pub name: String, - pub last_updated: DateTime, + /// URL of the repository. + /// + /// Note that the URL **isn't verified** during extraction! pub url: String, - pub pacbuilds: Vec, - pub priority: u8, + /// Preference of the repository. + /// + /// Specifies which repository to look into first during certain operations + /// like installing a package. If the package isn't present in the first + /// preferred repository, then the second preferred repository is looked + /// into. + pub preference: u32, +} + +#[allow(clippy::module_name_repetitions)] +pub fn default_repository() -> Vec { vec![Repository::default()] } + +impl Default for Repository { + fn default() -> Self { + Self { + name: "official".into(), + url: "https://github.com/pacstall/pacstall-programs".into(), + preference: 1, + } + } } diff --git a/src/store.rs b/src/store.rs deleted file mode 100644 index 261d3e0..0000000 --- a/src/store.rs +++ /dev/null @@ -1,9 +0,0 @@ -use self::storable::Storable; - -mod error; -mod filesystem; -pub mod filters; -pub mod storable; - -pub use error::StoreError; -pub use filesystem::FileSystemStore; diff --git a/src/store/storable.rs b/src/store/base.rs similarity index 66% rename from src/store/storable.rs rename to src/store/base.rs index 1e7d879..f56cb4b 100644 --- a/src/store/storable.rs +++ b/src/store/base.rs @@ -1,33 +1,85 @@ -use crate::model::{PacBuild, Repository}; -use crate::store::filters::{InstallState, Kind}; -use crate::store::StoreError; +//! Abstraction over the caching implementation -pub type UnitStoreResult = Result<(), StoreError>; +use std::fmt::Debug; -pub trait Storable { - fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> UnitStoreResult; - fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult; - fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult; +use crate::model::{PacBuild, Repository}; +use crate::store::errors::StoreError; +use crate::store::filters::{InstallState, Kind}; - fn remove_all_pacbuilds(&mut self, name: Vec<&str>, repository_url: &str) -> UnitStoreResult; +/// Alias for store error results +pub type StoreResult = Result; + +/// Abstraction over the caching implementation +pub trait Base: Debug { + /// Removes `PacBuild` by name that belongs to the given repository. + /// + /// # Errors + /// * `StoreError::RepositoryNotFound` + /// * `StoreError::PacBuildNotFound` + fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> StoreResult<()>; + + /// Adds `PacBuild` to the given repository. + /// + /// # Errors + /// * `StoreError::RepositoryConflict` + /// * `StoreError::PacBuildConflict` + fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()>; + + /// Updates `PacBuild` that belongs to the given repository. + /// + /// # Errors + /// * `StoreError::RepositoryNotFound` + /// * `StoreError::PacBuildNotFound` + fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()>; + + /// Removes all `PacBuild` by name that belongs to the given repository. + /// + /// # Errors + /// * `StoreError::Aggregate` + fn remove_all_pacbuilds(&mut self, name: &[&str], repository_url: &str) -> StoreResult<()>; + + /// Adds all `PacBuild` to the given repository. + /// + /// # Errors + /// * `StoreError::Aggregate` fn add_all_pacbuilds( &mut self, pacbuilds: Vec, repository_url: &str, - ) -> UnitStoreResult; + ) -> StoreResult<()>; + + /// Updates all `PacBuild` that belongs to the given repository. + /// + /// # Errors + /// * `StoreError::Aggregate` fn update_all_pacbuilds( &mut self, pacbuilds: Vec, repository_url: &str, - ) -> UnitStoreResult; - - fn remove_repository(&mut self, repository_url: &str) -> UnitStoreResult; - fn add_repository(&mut self, repository: Repository) -> UnitStoreResult; - fn update_repository(&mut self, repository: Repository) -> UnitStoreResult; + ) -> StoreResult<()>; + + /// Removes [Repository] by url. + /// + /// # Errors + /// * `StoreError::RepositoryNotFound` + fn remove_repository(&mut self, repository_url: &str) -> StoreResult<()>; + + /// Adds `Repository`. + /// + /// # Errors + /// * `StoreError::RepositoryConflict` + fn add_repository(&mut self, repository: Repository) -> StoreResult<()>; + + /// Updates [Repository]. + /// + /// # Errors + /// * `StoreError::RepositoryConflict` + fn update_repository(&mut self, repository: Repository) -> StoreResult<()>; fn get_pacbuild_by_name_and_url(&self, name: &str, repository_url: &str) -> Option<&PacBuild>; fn get_repository_by_name(&self, name: &str) -> Option<&Repository>; fn get_repository_by_url(&self, url: &str) -> Option<&Repository>; + fn get_all_repositories(&self) -> Vec<&Repository>; fn get_all_pacbuilds_by( &self, @@ -38,7 +90,11 @@ pub trait Storable { ) -> Vec<&PacBuild>; } -impl dyn Storable { +impl dyn Base { + pub fn get_all_pacbuilds(&self) -> Vec<&PacBuild> { + self.get_all_pacbuilds_by(None, None, None, None) + } + pub fn get_all_pacbuilds_by_name_like(&self, name_like: &str) -> Vec<&PacBuild> { self.get_all_pacbuilds_by(Some(name_like), None, None, None) } diff --git a/src/store/error.rs b/src/store/error.rs deleted file mode 100644 index f395f04..0000000 --- a/src/store/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[derive(Clone)] -pub struct StoreError { - pub message: String, -} - -impl StoreError { - pub fn new(message: &str) -> StoreError { - StoreError { - message: message.to_string(), - } - } -} - -impl std::fmt::Display for StoreError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Store error: {}", self.message) - } -} - -impl std::fmt::Debug for StoreError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[{}:{}] Store error: {}", file!(), line!(), self.message) - } -} diff --git a/src/store/errors.rs b/src/store/errors.rs new file mode 100644 index 0000000..3fee39a --- /dev/null +++ b/src/store/errors.rs @@ -0,0 +1,25 @@ +//! Errors used by the caching system + +use thiserror::Error; + +/// Errors used by Base +#[derive(Debug, Clone, Error)] +pub enum StoreError { + #[error("repository '{0}' could not be found")] + RepositoryNotFound(String), + + #[error("pacbuild '{name:?}' could not be found in repository {repository:?}")] + PacBuildNotFound { name: String, repository: String }, + + #[error("repository '{0}' already exists")] + RepositoryConflict(String), + + #[error("pacbuild '{name:?}' already exists in repository {repository:?}")] + PacBuildConflict { name: String, repository: String }, + + #[error("unexpected error: {0}")] + Unexpected(String), + + #[error("multiple errors: {0:?}")] + Aggregate(Vec), +} diff --git a/src/store/filesystem.rs b/src/store/filesystem.rs deleted file mode 100644 index 12884d6..0000000 --- a/src/store/filesystem.rs +++ /dev/null @@ -1,303 +0,0 @@ -use std::collections::HashMap; - -use super::StoreError; -use crate::model::{PacBuild, Repository}; -use crate::store::filters::{InstallState, Kind}; -use crate::store::storable::{Storable, UnitStoreResult}; -pub struct FileSystemStore { - repositories: Vec, - packages: HashMap>, - - allow_data_save: bool, -} - -impl FileSystemStore { - pub fn new() -> Box { - Box::new(FileSystemStore { - repositories: vec![], - packages: HashMap::new(), - allow_data_save: true, - }) - } - - fn get_packages_by_repository( - &self, - repository_url: &str, - ) -> Result<&Vec, StoreError> { - self.packages.get(&repository_url.to_owned()).map_or_else( - || { - Err(StoreError::new( - format!("Repository \"{}\" does not exist.", repository_url).as_str(), - )) - }, - |it| Ok(it), - ) - } - - fn save_to_disk(&self) { - if self.allow_data_save { - todo!() - } - } -} - -impl Storable for FileSystemStore { - fn get_pacbuild_by_name_and_url(&self, name: &str, url: &str) -> Option<&PacBuild> { - self.packages - .iter() - .filter(|(repo_url, _)| (*repo_url).to_owned() == url.to_owned()) - .flat_map(|(_, pkgs)| pkgs) - .find(|p| p.name == name.to_owned()) - } - - fn get_repository_by_name(&self, name: &str) -> Option<&Repository> { - self.repositories - .iter() - .find(|repo| repo.name == name.to_owned()) - } - - fn get_repository_by_url(&self, url: &str) -> Option<&Repository> { - self.repositories - .iter() - .find(|repo| repo.url == url.to_owned()) - } - - fn get_all_pacbuilds_by( - &self, - name_like: Option<&str>, - install_state: Option, - kind: Option, - repository_url: Option<&str>, - ) -> Vec<&PacBuild> { - let repos_urls = if let Some(url) = repository_url { - self.repositories - .iter() - .find(|it| it.url == url.to_string()) - .map_or_else(|| vec![], |it| vec![it.url.to_owned()]) - } else { - self.repositories - .iter() - .map(|it| it.url.to_owned()) - .collect() - }; - - self.packages - .iter() - .filter(|(repo_url, _)| repos_urls.contains(repo_url)) - .flat_map(|(_, pkgs)| pkgs) - .filter(|it| { - if let Some(kind_filter) = &kind { - kind_filter.to_owned() == Kind::from_model_kind(it.kind.clone()) - } else { - false - } - }) - .filter(|it| { - if let Some(install_state_filter) = &install_state { - install_state_filter.to_owned() - == InstallState::from_model_install_state(it.install_state.clone()) - } else { - false - } - }) - .filter(|it| { - if let Some(name_like) = name_like { - it.name.contains(name_like) - } else { - false - } - }) - .collect() - } - - fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> Result<(), StoreError> { - let new_list = self - .get_packages_by_repository(repository_url)? - .iter() - .filter(|it| it.name != name.to_owned()) - .map(|it| it.clone()) - .collect::>(); - - self.packages.insert(repository_url.to_owned(), new_list); - - self.save_to_disk(); - Ok(()) - } - - fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult { - let mut new_list = self.get_packages_by_repository(repository_url)?.to_owned(); - - new_list.push(pacbuild.clone()); - self.packages.insert(repository_url.to_owned(), new_list); - - self.save_to_disk(); - - Ok(()) - } - - fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> UnitStoreResult { - let new_list = self - .get_packages_by_repository(repository_url)? - .iter() - .map(|it| { - if it.name == pacbuild.name.to_owned() { - pacbuild.clone() - } else { - it.clone() - } - }) - .collect(); - - self.packages.insert(repository_url.to_owned(), new_list); - self.save_to_disk(); - - Ok(()) - } - - fn remove_all_pacbuilds( - &mut self, - names: Vec<&str>, - repository_url: &str, - ) -> Result<(), StoreError> { - let str_names: Vec = names.iter().map(|name| name.to_string()).collect(); - - let new_list: Vec = self - .get_packages_by_repository(repository_url)? - .to_owned() - .into_iter() - .filter(|it| str_names.contains(&it.name)) - .collect(); - - self.packages.insert(repository_url.to_owned(), new_list); - self.save_to_disk(); - - Ok(()) - } - - fn add_all_pacbuilds( - &mut self, - pacbuilds: Vec, - repository_url: &str, - ) -> UnitStoreResult { - let mut new_list: Vec = - self.get_packages_by_repository(repository_url)?.to_owned(); - - let already_existing_pkgs: Vec<&PacBuild> = pacbuilds - .iter() - .filter(|it| { - self.get_pacbuild_by_name_and_url(it.name.as_str(), &repository_url) - .is_some() - }) - .collect(); - - if !already_existing_pkgs.is_empty() { - return Err(StoreError::new( - format!( - "The following PACBUILDs already exist: {:#?}", - already_existing_pkgs - ) - .as_str(), - )); - } - - let mut to_add = pacbuilds.to_owned(); - new_list.append(&mut to_add); - self.packages.insert(repository_url.to_owned(), new_list); - self.save_to_disk(); - - Ok(()) - } - - fn update_all_pacbuilds( - &mut self, - pacbuilds: Vec, - repository_url: &str, - ) -> UnitStoreResult { - self.allow_data_save = false; - let errors: Vec = pacbuilds - .iter() - .map(|it| self.update_pacbuild(it.to_owned(), repository_url)) - .filter(|it| it.is_err()) - .collect(); - - self.allow_data_save = true; - - if errors.is_empty() { - self.save_to_disk(); - Ok(()) - } else { - let e = errors.first().unwrap().clone().expect_err("unreachable"); - Err(StoreError::new(e.message.as_str())) - } - } - - fn remove_repository(&mut self, repository_url: &str) -> Result<(), StoreError> { - let repo_exists = self - .repositories - .iter() - .any(|it| it.url.as_str() == repository_url); - - if !repo_exists { - return Err(StoreError::new( - format!("Repository {} does not exist.", repository_url).as_str(), - )); - } - - self.repositories = self - .repositories - .iter() - .filter(|repo| repo.url != repository_url) - .map(|it| it.to_owned()) - .collect(); - - self.packages.remove(&repository_url.to_owned()); - self.save_to_disk(); - - Ok(()) - } - - fn add_repository(&mut self, repository: Repository) -> Result<(), StoreError> { - let repo_exists = self.repositories.iter().any(|it| it.url == repository.url); - - if repo_exists { - return Err(StoreError::new( - format!("Repository {} already exists.", repository.url).as_str(), - )); - } - - self.packages.insert(repository.url.clone(), Vec::new()); - self.repositories.push(repository); - self.save_to_disk(); - - Ok(()) - } - - fn update_repository(&mut self, repository: Repository) -> UnitStoreResult { - let repo_exists = self - .repositories - .iter() - .any(|it| it.url == repository.url.to_owned()); - - if !repo_exists { - return Err(StoreError::new( - format!("Repository {} does not exist.", repository.url).as_str(), - )); - } - - self.repositories = self - .repositories - .iter() - .map(|it| { - if it.url == repository.url.to_owned() { - repository.to_owned() - } else { - it.to_owned() - } - }) - .collect(); - - self.save_to_disk(); - - Ok(()) - } -} diff --git a/src/store/filters.rs b/src/store/filters.rs index 9b0c6ef..aefe474 100644 --- a/src/store/filters.rs +++ b/src/store/filters.rs @@ -1,4 +1,7 @@ -#[derive(Debug, PartialEq, Clone)] +//! Provides various structs for querying and filtering packages. + +/// Used to query packages by installation state +#[derive(Debug, PartialEq, Eq, Clone)] pub enum InstallState { Direct, Indirect, @@ -6,7 +9,7 @@ pub enum InstallState { } impl InstallState { - pub fn from_model_install_state(other: crate::model::InstallState) -> InstallState { + pub fn from_model_install_state(other: &crate::model::InstallState) -> InstallState { match other { crate::model::InstallState::Indirect(..) => InstallState::Indirect, crate::model::InstallState::Direct(..) => InstallState::Direct, @@ -15,7 +18,8 @@ impl InstallState { } } -#[derive(Debug, PartialEq, Clone)] +/// Used to query packages by kind. +#[derive(Debug, PartialEq, Eq, Clone)] pub enum Kind { AppImage, Binary, @@ -25,7 +29,7 @@ pub enum Kind { } impl Kind { - pub fn from_model_kind(other: crate::model::Kind) -> Kind { + pub fn from_model_kind(other: &crate::model::Kind) -> Kind { match other { crate::model::Kind::GitRelease => Kind::GitRelease, crate::model::Kind::GitBranch => Kind::GitBranch, diff --git a/src/store/fs.rs b/src/store/fs.rs new file mode 100644 index 0000000..2769ff0 --- /dev/null +++ b/src/store/fs.rs @@ -0,0 +1,642 @@ +//! Provides a JSON file-based implementation for the caching system. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::errors::StoreError; +use crate::model::{PacBuild, Repository}; +use crate::store::base::{Base, StoreResult}; +use crate::store::filters::{InstallState, Kind}; + +#[cfg(not(test))] +const FSS_PATH: &str = "/etc/pacstall/fss.json"; +#[cfg(test)] +const FSS_PATH: &str = "./fss.json"; + +/// `FileSystem` implementation for the caching system +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FileSystemStore { + repositories: Vec, + packages: HashMap>, + + #[serde(skip_serializing)] + allow_data_save: bool, +} + +impl FileSystemStore { + #[allow(clippy::new_ret_no_self)] + pub fn new() -> Box { + Box::new(FileSystemStore { + repositories: Vec::new(), + packages: HashMap::new(), + allow_data_save: true, + }) + } + + /// # Private + fn get_packages_by_repository( + &mut self, + repository_url: &str, + ) -> Result<&mut Vec, StoreError> { + self.packages + .get_mut(&repository_url.to_owned()) + .map_or_else( + || Err(StoreError::RepositoryNotFound(repository_url.to_owned())), + Ok, + ) + } + + /// # Private + fn save_to_disk(&self) -> StoreResult<()> { + if self.allow_data_save { + use std::fs; + use std::path::Path; + + let json = serde_json::to_vec_pretty(self).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to serialize database.".to_string(), + )) + }, + Ok, + )?; + + return fs::write(Path::new(FSS_PATH), &json).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to write database to disk.".to_string(), + )) + }, + |_| Ok(()), + ); + } + + Ok(()) + } + + /// # Errors + pub fn load_from_disk() -> Result, StoreError> { + use std::fs; + use std::path::Path; + + let contents = fs::read_to_string(Path::new(FSS_PATH)).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to read database from disk.".to_string(), + )) + }, + Ok, + )?; + let mut obj: FileSystemStore = serde_json::from_str(&contents).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to deserialize database.".to_string(), + )) + }, + Ok, + )?; + obj.allow_data_save = true; + + Ok(Box::new(obj)) + } +} + +impl Base for FileSystemStore { + fn get_pacbuild_by_name_and_url(&self, name: &str, url: &str) -> Option<&PacBuild> { + self.packages + .get(&url.to_string()) + .and_then(|pkgs| pkgs.iter().find(|it| it.name == *name)) + } + + fn get_repository_by_name(&self, name: &str) -> Option<&Repository> { + self.repositories.iter().find(|repo| repo.name == *name) + } + + fn get_repository_by_url(&self, url: &str) -> Option<&Repository> { + self.repositories.iter().find(|repo| repo.url == *url) + } + + fn get_all_pacbuilds_by( + &self, + name_like: Option<&str>, + install_state: Option, + kind: Option, + repository_url: Option<&str>, + ) -> Vec<&PacBuild> { + let repos_urls = if let Some(url) = repository_url { + self.repositories + .iter() + .find(|it| it.url == *url) + .map_or_else(Vec::new, |it| vec![it.url.clone()]) + } else { + self.repositories.iter().map(|it| it.url.clone()).collect() + }; + + self.packages + .iter() + .filter(|(repo_url, _)| repos_urls.contains(repo_url)) + .flat_map(|(_, pkgs)| pkgs) + .filter(|it| { + if let Some(kind_filter) = &kind { + *kind_filter == Kind::from_model_kind(&it.kind) + } else { + true + } + }) + .filter(|it| { + if let Some(install_state_filter) = &install_state { + *install_state_filter + == InstallState::from_model_install_state(&it.install_state) + } else { + true + } + }) + .filter(|it| { + if let Some(name_like) = name_like { + it.name.contains(name_like) + } else { + true + } + }) + .collect() + } + + fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> Result<(), StoreError> { + let repo = self + .packages + .get_mut(&repository_url.to_owned()) + .ok_or_else(|| StoreError::RepositoryNotFound(repository_url.to_string()))?; + + repo.swap_remove(repo.iter().position(|it| it.name == *name).ok_or( + StoreError::PacBuildNotFound { + name: name.to_string(), + repository: repository_url.to_string(), + }, + )?); + + self.save_to_disk()?; + Ok(()) + } + + fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()> { + if self + .get_pacbuild_by_name_and_url(&pacbuild.name, repository_url) + .is_some() + { + return Err(StoreError::PacBuildConflict { + name: pacbuild.name, + repository: repository_url.to_string(), + }); + } + + self.get_packages_by_repository(repository_url)? + .push(pacbuild); + self.save_to_disk()?; + + Ok(()) + } + + fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()> { + if self + .get_pacbuild_by_name_and_url(&pacbuild.name, repository_url) + .is_none() + { + return Err(StoreError::PacBuildNotFound { + name: pacbuild.name, + repository: repository_url.to_string(), + }); + } + + let pkgs = self.get_packages_by_repository(repository_url)?; + let idx = pkgs.iter().position(|pb| pb.name == pacbuild.name).unwrap(); + + pkgs.swap_remove(idx); + pkgs.push(pacbuild); + self.save_to_disk()?; + + Ok(()) + } + + fn remove_all_pacbuilds( + &mut self, + names: &[&str], + repository_url: &str, + ) -> Result<(), StoreError> { + let errors = names + .iter() + .map( + |it| match self.get_pacbuild_by_name_and_url(*it, repository_url) { + Some(_) => None, + None => Some(StoreError::PacBuildNotFound { + name: (*it).to_string(), + repository: repository_url.to_string(), + }), + }, + ) + .fold(vec![], |acc, it| { + if let Some(err) = it { + let mut acc = acc; + acc.push(err); + acc + } else { + acc + } + }); + + if !errors.is_empty() { + return Err(StoreError::Aggregate(errors)); + } + + let str_names: Vec = names.iter().map(ToString::to_string).collect(); + + let new_list: Vec = self + .get_packages_by_repository(repository_url)? + .clone() + .into_iter() + .filter(|it| str_names.contains(&it.name)) + .collect(); + + self.packages.insert(repository_url.to_owned(), new_list); + self.save_to_disk()?; + + Ok(()) + } + + fn add_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> StoreResult<()> { + let already_existing_pkgs: Vec<&PacBuild> = pacbuilds + .iter() + .filter(|it| { + self.get_pacbuild_by_name_and_url(it.name.as_str(), repository_url) + .is_some() + }) + .collect(); + + if !already_existing_pkgs.is_empty() { + return Err(StoreError::Aggregate( + already_existing_pkgs + .iter() + .map(|it| StoreError::PacBuildConflict { + name: it.name.to_string(), + repository: it.repository.to_string(), + }) + .collect(), + )); + } + + let mut new_list: Vec = self.get_packages_by_repository(repository_url)?.clone(); + let mut to_add = pacbuilds.clone(); + new_list.append(&mut to_add); + self.packages.insert(repository_url.to_owned(), new_list); + self.save_to_disk()?; + + Ok(()) + } + + fn update_all_pacbuilds( + &mut self, + pacbuilds: Vec, + repository_url: &str, + ) -> StoreResult<()> { + for name in pacbuilds.iter().map(|it| &it.name) { + if self + .get_pacbuild_by_name_and_url(name, repository_url) + .is_none() + { + return Err(StoreError::PacBuildNotFound { + name: name.to_string(), + repository: repository_url.to_string(), + }); + } + } + + self.allow_data_save = false; + let errors: Vec> = pacbuilds + .iter() + .map(|it| self.update_pacbuild(it.clone(), repository_url)) + .filter(Result::is_err) + .collect(); + + self.allow_data_save = true; + + if errors.is_empty() { + self.save_to_disk()?; + Ok(()) + } else { + Err(StoreError::Aggregate( + errors.iter().map(|it| it.clone().unwrap_err()).collect(), + )) + } + } + + fn remove_repository(&mut self, repository_url: &str) -> Result<(), StoreError> { + let repo_idx = self + .repositories + .iter() + .position(|it| it.url.as_str() == repository_url) + .ok_or_else(|| StoreError::RepositoryNotFound(repository_url.to_string()))?; + + self.repositories.swap_remove(repo_idx); + self.packages.remove(&repository_url.to_owned()); + self.save_to_disk()?; + + Ok(()) + } + + fn add_repository(&mut self, repository: Repository) -> Result<(), StoreError> { + let repo_exists = self.repositories.iter().any(|it| it.url == repository.url); + + if repo_exists { + return Err(StoreError::RepositoryConflict(repository.url)); + } + + self.packages.insert(repository.url.clone(), Vec::new()); + self.repositories.push(repository); + self.save_to_disk()?; + + Ok(()) + } + + fn update_repository(&mut self, repository: Repository) -> StoreResult<()> { + let repo_idx = self + .repositories + .iter() + .position(|it| it.url == repository.url) + .ok_or_else(|| StoreError::RepositoryNotFound(repository.url.to_string()))?; + + self.repositories.swap_remove(repo_idx); + self.repositories.push(repository); + + self.save_to_disk()?; + + Ok(()) + } + + fn get_all_repositories(&self) -> Vec<&Repository> { self.repositories.iter().collect() } +} + +#[cfg(test)] +mod test { + use super::FileSystemStore; + use crate::model::Repository; + + mod util { + use chrono::NaiveDateTime; + + use crate::model::{InstallState, Kind, PacBuild, Repository}; + use crate::store::base::Base; + use crate::store::fs::FileSystemStore; + + pub fn create_store_with_sample_data() -> (Box, Repository, PacBuild) { + let mut fss = FileSystemStore::new(); + let repo = Repository::default(); + let pacbuild_to_add = PacBuild { + name: "dummy-pacbuild-deb".into(), + package_name: "dummy-pacbuild".into(), + description: "blah".into(), + dependencies: Vec::new(), + homepage: "https://example.com".into(), + install_state: InstallState::Direct( + NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), + "1.0.0".into(), + ), + kind: Kind::DebFile("hashash".into()), + last_updated: NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), + license: "BSD".into(), + maintainer: "saenai255".into(), + optional_dependencies: Vec::new(), + repology: "filter".into(), + repology_version: "1.0.1".into(), + repository: repo.url.clone(), + url: "https://example.com/dummy-pacbuild-1.0.0.deb".into(), + }; + + fss.add_repository(repo.clone()).unwrap(); + fss.add_pacbuild(pacbuild_to_add.clone(), &repo.url) + .unwrap(); + + (fss, repo, pacbuild_to_add) + } + } + + #[test] + fn new_creates_empty_fs_store() { + let fss = FileSystemStore::new(); + let pacbuilds = fss.get_all_pacbuilds(); + let repos = fss.get_all_repositories(); + + assert_eq!(pacbuilds.len(), 0); + assert_eq!(repos.len(), 0); + } + + #[test] + fn add_repository_works() { + let mut fss = FileSystemStore::new(); + + fss.add_repository(Repository::default()).unwrap(); + let repos = fss.get_all_repositories(); + + assert_eq!(repos.len(), 1); + } + + #[test] + fn get_repository_by_name_works() { + let mut fss = FileSystemStore::new(); + let repo = Repository::default(); + + fss.add_repository(repo.clone()).unwrap(); + let found_repo = fss.get_repository_by_name(&repo.name).unwrap(); + + assert_eq!(repo, found_repo.clone()); + } + + #[test] + fn get_repository_by_url_works() { + let mut fss = FileSystemStore::new(); + let repo = Repository::default(); + + fss.add_repository(repo.clone()).unwrap(); + let found_repo = fss.get_repository_by_url(&repo.url).unwrap(); + + assert_eq!(repo, found_repo.clone()); + } + + #[test] + fn add_pacbuild_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let pbs = fss.get_all_pacbuilds(); + + println!("{:#?}", pbs); + + assert_eq!(pbs.len(), 1); + } + + #[test] + fn get_pacbuild_by_name_and_url_works() { + let (fss, _, pacbuild) = util::create_store_with_sample_data(); + let found = fss + .get_pacbuild_by_name_and_url(&pacbuild.name, &pacbuild.repository) + .unwrap(); + + assert_eq!(found.clone(), pacbuild); + } + + #[test] + fn get_all_pacbuilds_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds(); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_name_like_works() { + let (fss, _, pb) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_name_like(&pb.name); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_name_like_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_name_like("blalblala"); + + assert_eq!(found.len(), 0); + } + + #[test] + fn get_all_pacbuilds_by_install_state_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = + fss.get_all_pacbuilds_by_install_state(crate::store::filters::InstallState::Direct); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_install_state_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = + fss.get_all_pacbuilds_by_install_state(crate::store::filters::InstallState::Indirect); + + assert_eq!(found.len(), 0); + } + + #[test] + fn get_all_pacbuilds_by_kind_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_kind(crate::store::filters::Kind::DebFile); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_kind_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_kind(crate::store::filters::Kind::Binary); + + assert_eq!(found.len(), 0); + } + + #[test] + fn get_all_pacbuilds_by_repository_url_works() { + let (fss, repo, _) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_repository_url(&repo.url); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_repository_url_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.get_all_pacbuilds_by_repository_url("this repo url does not exist"); + + assert_eq!(found.len(), 0); + } + + #[test] + fn update_pacbuild_works() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.description = "something else".into(); + + fss.update_pacbuild(pb.clone(), &pb.repository).unwrap(); + let found = fss + .get_pacbuild_by_name_and_url(&pb.name, &pb.repository) + .unwrap(); + + assert_eq!(pb, *found); + } + + #[test] + fn update_all_pacbuilds_works() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.description = "something else".into(); + + fss.update_all_pacbuilds(vec![pb.clone()], &pb.repository) + .unwrap(); + let found = fss + .get_pacbuild_by_name_and_url(&pb.name, &pb.repository) + .unwrap(); + + assert_eq!(pb, *found); + } + + #[test] + #[should_panic] + fn update_pacbuild_panics_when_pacbuild_not_found() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.name = "lala".into(); + pb.description = "something else".into(); + + fss.update_pacbuild(pb.clone(), &pb.repository).unwrap(); + } + + #[test] + #[should_panic] + fn update_all_pacbuilds_panics_when_pacbuild_not_found() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.name = "lala".into(); + pb.description = "something else".into(); + + fss.update_all_pacbuilds(vec![pb.clone()], &pb.repository) + .unwrap(); + } + + #[test] + #[should_panic] + fn remove_pacbuild_panics_when_pacbuild_not_found() { + let (mut fss, _, pb) = util::create_store_with_sample_data(); + + fss.remove_pacbuild("does-not-exist", &pb.repository) + .unwrap(); + } + + #[test] + #[should_panic] + fn remove_all_pacbuilds_panics_when_pacbuild_not_found() { + let (mut fss, _, pb) = util::create_store_with_sample_data(); + + fss.remove_all_pacbuilds(&(&vec!["does-not-exist"])[..], &pb.repository) + .unwrap(); + } + + #[test] + #[should_panic] + fn add_pacbuild_panics_when_pacbuild_already_exists() { + let (mut fss, _, pb) = util::create_store_with_sample_data(); + fss.add_pacbuild(pb.clone(), &pb.repository).unwrap(); + } + + #[test] + #[should_panic] + fn update_all_pacbuilds_panics_when_pacbuild_already_exists() { + let (mut fss, _, pb) = util::create_store_with_sample_data(); + fss.add_all_pacbuilds(vec![pb.clone()], &pb.repository) + .unwrap(); + } +} diff --git a/src/store/mod.rs b/src/store/mod.rs new file mode 100644 index 0000000..4908eed --- /dev/null +++ b/src/store/mod.rs @@ -0,0 +1,6 @@ +//! Provides traits and structs to handle Pacstall's cache. + +pub mod base; +pub mod errors; +pub mod filters; +pub mod fs; From 945b3eab2f0e819dbbe85f99330fc12576e28cfa Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 6 Sep 2022 09:40:18 +0300 Subject: [PATCH 03/18] deps: remove time 0.1 dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9855aea..1a1189e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ categories = ["caching", "config", "parsing", "os::linux-apis"] figment = { version = "0.10.6", features = ["env", "test", "toml" ] } num_cpus = "1.13.1" serde = { version = "1.0.144", features = ["derive"] } -chrono = { version = "0.4.22", features = ["serde"] } +chrono = { version = "0.4.22", features = ["serde", "clock", "std"], default-features = false } serde_derive = "1.0.144" serde_json = "1.0.85" thiserror = "1.0" From 817e873f4edc7d2964a731ba8f7bea3ccaa00585 Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 6 Sep 2022 20:27:07 +0300 Subject: [PATCH 04/18] docs: improve style --- src/model/pacbuild.rs | 6 ++++++ src/store/base.rs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/model/pacbuild.rs b/src/model/pacbuild.rs index 0320f79..6145f08 100644 --- a/src/model/pacbuild.rs +++ b/src/model/pacbuild.rs @@ -23,6 +23,7 @@ pub struct PacBuild { /// Represents a `SemVer` version. /// # Examples +/// /// ``` /// use libpacstall::model::Version; /// @@ -32,6 +33,7 @@ pub type Version = String; /// Represents a `PacBuild` or Apt package name. /// # Examples +/// /// ``` /// use libpacstall::model::PackageId; /// @@ -40,6 +42,7 @@ pub type Version = String; pub type PackageId = String; /// Represents an URL /// # Examples +/// /// ``` /// use libpacstall::model::URL; /// @@ -48,6 +51,7 @@ pub type PackageId = String; pub type URL = String; /// Represents a file checksum /// # Examples +/// /// ``` /// use libpacstall::model::Hash; /// @@ -57,6 +61,7 @@ pub type Hash = String; /// Represents the install state of a package. /// # Examples +/// /// ``` /// use chrono::NaiveDate; /// use libpacstall::model::InstallState; @@ -87,6 +92,7 @@ impl InstallState { /// suffix. /// /// # Examples +/// /// ``` /// use libpacstall::model::Kind; /// diff --git a/src/store/base.rs b/src/store/base.rs index f56cb4b..700ddd4 100644 --- a/src/store/base.rs +++ b/src/store/base.rs @@ -76,11 +76,19 @@ pub trait Base: Debug { /// * `StoreError::RepositoryConflict` fn update_repository(&mut self, repository: Repository) -> StoreResult<()>; + /// Find first by name in the given repository fn get_pacbuild_by_name_and_url(&self, name: &str, repository_url: &str) -> Option<&PacBuild>; + + /// Find repository by name fn get_repository_by_name(&self, name: &str) -> Option<&Repository>; + /// Find repository by url fn get_repository_by_url(&self, url: &str) -> Option<&Repository>; + + /// Find all repositories fn get_all_repositories(&self) -> Vec<&Repository>; + /// Find all packages that match all the given params. `None` params are + /// skipped. fn get_all_pacbuilds_by( &self, name_like: Option<&str>, @@ -91,6 +99,7 @@ pub trait Base: Debug { } impl dyn Base { + /// Find all pacbuilds from all repositories pub fn get_all_pacbuilds(&self) -> Vec<&PacBuild> { self.get_all_pacbuilds_by(None, None, None, None) } From 7668b2a80f04567c4bce2d328263000ee0297987 Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 6 Sep 2022 20:47:36 +0300 Subject: [PATCH 05/18] chore: impl `From` for filters --- src/model/pacbuild.rs | 4 +++- src/store/base.rs | 36 ++++++++++++++++++------------------ src/store/filters.rs | 10 ++++++++++ src/store/fs.rs | 5 ++--- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/model/pacbuild.rs b/src/model/pacbuild.rs index 6145f08..5f03d36 100644 --- a/src/model/pacbuild.rs +++ b/src/model/pacbuild.rs @@ -1,3 +1,5 @@ +//! + use chrono::NaiveDateTime as DateTime; use serde_derive::{Deserialize, Serialize}; @@ -31,7 +33,7 @@ pub struct PacBuild { /// ``` pub type Version = String; -/// Represents a `PacBuild` or Apt package name. +/// Represents a [`PacBuild`] or Apt package name. /// # Examples /// /// ``` diff --git a/src/store/base.rs b/src/store/base.rs index 700ddd4..8868499 100644 --- a/src/store/base.rs +++ b/src/store/base.rs @@ -11,47 +11,47 @@ pub type StoreResult = Result; /// Abstraction over the caching implementation pub trait Base: Debug { - /// Removes `PacBuild` by name that belongs to the given repository. + /// Removes [`PacBuild`] by name that belongs to the given repository. /// /// # Errors - /// * `StoreError::RepositoryNotFound` - /// * `StoreError::PacBuildNotFound` + /// * [`StoreError::RepositoryNotFound`] + /// * [`StoreError::PacBuildNotFound`] fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> StoreResult<()>; - /// Adds `PacBuild` to the given repository. + /// Adds [`PacBuild`] to the given repository. /// /// # Errors - /// * `StoreError::RepositoryConflict` - /// * `StoreError::PacBuildConflict` + /// * [`StoreError::RepositoryConflict`] + /// * [`StoreError::PacBuildConflict`] fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()>; - /// Updates `PacBuild` that belongs to the given repository. + /// Updates [`PacBuild`] that belongs to the given repository. /// /// # Errors - /// * `StoreError::RepositoryNotFound` - /// * `StoreError::PacBuildNotFound` + /// * [`StoreError::RepositoryNotFound`] + /// * [`StoreError::PacBuildNotFound`] fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()>; - /// Removes all `PacBuild` by name that belongs to the given repository. + /// Removes all [`PacBuild`] by name that belongs to the given repository. /// /// # Errors - /// * `StoreError::Aggregate` + /// * [`StoreError::Aggregate`] fn remove_all_pacbuilds(&mut self, name: &[&str], repository_url: &str) -> StoreResult<()>; - /// Adds all `PacBuild` to the given repository. + /// Adds all [`PacBuild`] to the given repository. /// /// # Errors - /// * `StoreError::Aggregate` + /// * [`StoreError::Aggregate`] fn add_all_pacbuilds( &mut self, pacbuilds: Vec, repository_url: &str, ) -> StoreResult<()>; - /// Updates all `PacBuild` that belongs to the given repository. + /// Updates all [`PacBuild`] that belongs to the given repository. /// /// # Errors - /// * `StoreError::Aggregate` + /// * [`StoreError::Aggregate`] fn update_all_pacbuilds( &mut self, pacbuilds: Vec, @@ -61,19 +61,19 @@ pub trait Base: Debug { /// Removes [Repository] by url. /// /// # Errors - /// * `StoreError::RepositoryNotFound` + /// * [`StoreError::RepositoryNotFound`] fn remove_repository(&mut self, repository_url: &str) -> StoreResult<()>; /// Adds `Repository`. /// /// # Errors - /// * `StoreError::RepositoryConflict` + /// * [`StoreError::RepositoryConflict`] fn add_repository(&mut self, repository: Repository) -> StoreResult<()>; /// Updates [Repository]. /// /// # Errors - /// * `StoreError::RepositoryConflict` + /// * [`StoreError::RepositoryConflict`] fn update_repository(&mut self, repository: Repository) -> StoreResult<()>; /// Find first by name in the given repository diff --git a/src/store/filters.rs b/src/store/filters.rs index aefe474..d8195ca 100644 --- a/src/store/filters.rs +++ b/src/store/filters.rs @@ -8,6 +8,12 @@ pub enum InstallState { None, } +impl From<&crate::model::InstallState> for InstallState { + fn from(other: &crate::model::InstallState) -> Self { + InstallState::from_model_install_state(other) + } +} + impl InstallState { pub fn from_model_install_state(other: &crate::model::InstallState) -> InstallState { match other { @@ -28,6 +34,10 @@ pub enum Kind { GitRelease, } +impl From<&crate::model::Kind> for Kind { + fn from(other: &crate::model::Kind) -> Self { Kind::from_model_kind(other) } +} + impl Kind { pub fn from_model_kind(other: &crate::model::Kind) -> Kind { match other { diff --git a/src/store/fs.rs b/src/store/fs.rs index 2769ff0..0f5e283 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -139,15 +139,14 @@ impl Base for FileSystemStore { .flat_map(|(_, pkgs)| pkgs) .filter(|it| { if let Some(kind_filter) = &kind { - *kind_filter == Kind::from_model_kind(&it.kind) + *kind_filter == (&it.kind).into() } else { true } }) .filter(|it| { if let Some(install_state_filter) = &install_state { - *install_state_filter - == InstallState::from_model_install_state(&it.install_state) + *install_state_filter == (&it.install_state).into() } else { true } From 53ae5deb79e10ac546be697457702c0ab776d0f9 Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 4 Oct 2022 19:11:36 +0300 Subject: [PATCH 06/18] Update src/store/fs.rs Co-authored-by: Sourajyoti Basak --- src/store/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/fs.rs b/src/store/fs.rs index 0f5e283..34db856 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -38,7 +38,7 @@ impl FileSystemStore { fn get_packages_by_repository( &mut self, repository_url: &str, - ) -> Result<&mut Vec, StoreError> { + ) -> StoreResult<&mut Vec> { self.packages .get_mut(&repository_url.to_owned()) .map_or_else( From acd0afe48639188aeeb3427a6fbef0089fcc3993 Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 4 Oct 2022 19:11:44 +0300 Subject: [PATCH 07/18] Update src/store/fs.rs Co-authored-by: Sourajyoti Basak --- src/store/fs.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/store/fs.rs b/src/store/fs.rs index 34db856..3e69dad 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -9,8 +9,10 @@ use crate::model::{PacBuild, Repository}; use crate::store::base::{Base, StoreResult}; use crate::store::filters::{InstallState, Kind}; +/// Path of the database. #[cfg(not(test))] const FSS_PATH: &str = "/etc/pacstall/fss.json"; +/// Path of the database. #[cfg(test)] const FSS_PATH: &str = "./fss.json"; From af11403614bd9f82f2a23cc0effa5980d00cc15a Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 4 Oct 2022 19:11:56 +0300 Subject: [PATCH 08/18] Update src/store/fs.rs Co-authored-by: Sourajyoti Basak --- src/store/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/fs.rs b/src/store/fs.rs index 3e69dad..2af20e1 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -36,7 +36,7 @@ impl FileSystemStore { }) } - /// # Private + /// Fetches [`PacBuild`]s by their repository URL. fn get_packages_by_repository( &mut self, repository_url: &str, From 43ebc1141ec5e4185ef2500855848f47ce15988a Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 4 Oct 2022 19:12:07 +0300 Subject: [PATCH 09/18] Update src/store/fs.rs Co-authored-by: Sourajyoti Basak --- src/store/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/fs.rs b/src/store/fs.rs index 2af20e1..4267a42 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -49,7 +49,7 @@ impl FileSystemStore { ) } - /// # Private + /// Save the current [`FileSystemStore`] state to disk. fn save_to_disk(&self) -> StoreResult<()> { if self.allow_data_save { use std::fs; From d825b99e75ef9678860f58c986fb296163f7d47a Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 4 Oct 2022 19:12:16 +0300 Subject: [PATCH 10/18] Update src/store/fs.rs Co-authored-by: Sourajyoti Basak --- src/store/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/fs.rs b/src/store/fs.rs index 4267a42..0644e99 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -78,7 +78,7 @@ impl FileSystemStore { } /// # Errors - pub fn load_from_disk() -> Result, StoreError> { + pub fn load_from_disk() -> StoreResult> { use std::fs; use std::path::Path; From 47cba1d88a8275f51759bfb5d5db48e84c8ad720 Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 4 Oct 2022 19:12:27 +0300 Subject: [PATCH 11/18] Update src/store/fs.rs Co-authored-by: Sourajyoti Basak --- src/store/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/fs.rs b/src/store/fs.rs index 0644e99..032a1a1 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -163,7 +163,7 @@ impl Base for FileSystemStore { .collect() } - fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> Result<(), StoreError> { + fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> StoreResult<()> { let repo = self .packages .get_mut(&repository_url.to_owned()) From 6c690f401fee559bcb932fa3fa899bd7b47bfe4e Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 4 Oct 2022 19:12:41 +0300 Subject: [PATCH 12/18] Update src/store/fs.rs Co-authored-by: Sourajyoti Basak --- src/store/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/fs.rs b/src/store/fs.rs index 032a1a1..f1c9e4c 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -334,7 +334,7 @@ impl Base for FileSystemStore { } } - fn remove_repository(&mut self, repository_url: &str) -> Result<(), StoreError> { + fn remove_repository(&mut self, repository_url: &str) -> StoreResult<()> { let repo_idx = self .repositories .iter() From d454cfb65f5468779c360bea365c18d6393bc631 Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Tue, 4 Oct 2022 19:12:49 +0300 Subject: [PATCH 13/18] Update src/store/fs.rs Co-authored-by: Sourajyoti Basak --- src/store/fs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/fs.rs b/src/store/fs.rs index f1c9e4c..97b6d95 100644 --- a/src/store/fs.rs +++ b/src/store/fs.rs @@ -348,7 +348,7 @@ impl Base for FileSystemStore { Ok(()) } - fn add_repository(&mut self, repository: Repository) -> Result<(), StoreError> { + fn add_repository(&mut self, repository: Repository) -> StoreResult<()> { let repo_exists = self.repositories.iter().any(|it| it.url == repository.url); if repo_exists { From 0ef067f8861bfa5af021c0c324f71d6c3af1d8be Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Fri, 7 Oct 2022 18:40:38 +0300 Subject: [PATCH 14/18] ref(store): ref so Base trait is not used --- src/store/base.rs | 747 +++++++++++++++++++++++++++++-------- src/store/errors.rs | 3 + src/store/filters.rs | 4 +- src/store/fs.rs | 641 ------------------------------- src/store/mod.rs | 2 +- src/store/query_builder.rs | 190 ++++++++++ 6 files changed, 784 insertions(+), 803 deletions(-) delete mode 100644 src/store/fs.rs create mode 100644 src/store/query_builder.rs diff --git a/src/store/base.rs b/src/store/base.rs index 8868499..f64e9e0 100644 --- a/src/store/base.rs +++ b/src/store/base.rs @@ -1,210 +1,639 @@ //! Abstraction over the caching implementation +use std::collections::HashMap; use std::fmt::Debug; +use serde::{Deserialize, Serialize}; + +use super::query_builder::{MutationQuery, PacBuildQuery, Query, RepositoryQuery}; use crate::model::{PacBuild, Repository}; use crate::store::errors::StoreError; -use crate::store::filters::{InstallState, Kind}; /// Alias for store error results pub type StoreResult = Result; -/// Abstraction over the caching implementation -pub trait Base: Debug { - /// Removes [`PacBuild`] by name that belongs to the given repository. - /// - /// # Errors - /// * [`StoreError::RepositoryNotFound`] - /// * [`StoreError::PacBuildNotFound`] - fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> StoreResult<()>; +#[cfg(not(test))] +const FSS_PATH: &str = "/etc/pacstall/fss.json"; +#[cfg(test)] +const FSS_PATH: &str = "./fss.json"; - /// Adds [`PacBuild`] to the given repository. - /// - /// # Errors - /// * [`StoreError::RepositoryConflict`] - /// * [`StoreError::PacBuildConflict`] - fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()>; +/// `FileSystem` implementation for the caching system +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Store { + repositories: Vec, + packages: HashMap>, +} - /// Updates [`PacBuild`] that belongs to the given repository. - /// +impl Store { /// # Errors - /// * [`StoreError::RepositoryNotFound`] - /// * [`StoreError::PacBuildNotFound`] - fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()>; + pub fn load() -> Result { + use std::fs; + use std::path::Path; - /// Removes all [`PacBuild`] by name that belongs to the given repository. - /// - /// # Errors - /// * [`StoreError::Aggregate`] - fn remove_all_pacbuilds(&mut self, name: &[&str], repository_url: &str) -> StoreResult<()>; + let contents = fs::read_to_string(Path::new(FSS_PATH)).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to read database from disk.".to_string(), + )) + }, + Ok, + )?; + let obj: Self = serde_json::from_str(&contents).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to deserialize database.".to_string(), + )) + }, + Ok, + )?; - /// Adds all [`PacBuild`] to the given repository. - /// - /// # Errors - /// * [`StoreError::Aggregate`] - fn add_all_pacbuilds( - &mut self, - pacbuilds: Vec, - repository_url: &str, - ) -> StoreResult<()>; - - /// Updates all [`PacBuild`] that belongs to the given repository. - /// - /// # Errors - /// * [`StoreError::Aggregate`] - fn update_all_pacbuilds( - &mut self, - pacbuilds: Vec, - repository_url: &str, - ) -> StoreResult<()>; - - /// Removes [Repository] by url. - /// - /// # Errors - /// * [`StoreError::RepositoryNotFound`] - fn remove_repository(&mut self, repository_url: &str) -> StoreResult<()>; + Ok(obj) + } + + /// # Private + fn save_to_disk(&self) -> StoreResult<()> { + use std::fs; + use std::path::Path; + + let json = serde_json::to_vec_pretty(self).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to serialize database.".to_string(), + )) + }, + Ok, + )?; + + fs::write(Path::new(FSS_PATH), &json).map_or_else( + |_| { + Err(StoreError::Unexpected( + "Unable to write database to disk.".to_string(), + )) + }, + |_| Ok(()), + ) + } +} + +impl Store { + pub fn query_packages(&self, handler: F) -> R + where + F: Fn(Box>) -> R, + { + let query_resolver = Box::new(PacBuildQueryResolver { + packages: self.packages.clone(), + repositories: self.repositories.clone(), + }); + + handler(query_resolver) + } + + pub fn query_repositories(&self, handler: F) -> R + where + F: Fn(Box>) -> R, + { + let query_resolver = Box::new(RepositoryQueryResolver { + packages: self.packages.clone(), + repositories: self.repositories.clone(), + }); + + handler(query_resolver) + } - /// Adds `Repository`. - /// /// # Errors - /// * [`StoreError::RepositoryConflict`] - fn add_repository(&mut self, repository: Repository) -> StoreResult<()>; + pub fn mutate_packages(&mut self, mut handler: F) -> StoreResult + where + F: FnMut(&mut dyn MutationQuery) -> StoreResult, + { + let mut query_resolver = PacBuildQueryResolver { + packages: self.packages.clone(), + repositories: self.repositories.clone(), + }; + + let res = handler(&mut query_resolver); + self.packages = query_resolver.packages; + self.repositories = query_resolver.repositories; + self.save_to_disk()?; + + res + } - /// Updates [Repository]. - /// /// # Errors - /// * [`StoreError::RepositoryConflict`] - fn update_repository(&mut self, repository: Repository) -> StoreResult<()>; - - /// Find first by name in the given repository - fn get_pacbuild_by_name_and_url(&self, name: &str, repository_url: &str) -> Option<&PacBuild>; - - /// Find repository by name - fn get_repository_by_name(&self, name: &str) -> Option<&Repository>; - /// Find repository by url - fn get_repository_by_url(&self, url: &str) -> Option<&Repository>; - - /// Find all repositories - fn get_all_repositories(&self) -> Vec<&Repository>; - - /// Find all packages that match all the given params. `None` params are - /// skipped. - fn get_all_pacbuilds_by( - &self, - name_like: Option<&str>, - install_state: Option, - kind: Option, - repository_url: Option<&str>, - ) -> Vec<&PacBuild>; + pub fn mutate_repositories(&mut self, mut handler: F) -> StoreResult + where + F: FnMut(&mut dyn MutationQuery) -> StoreResult, + { + let mut query_resolver = RepositoryQueryResolver { + packages: self.packages.clone(), + repositories: self.repositories.clone(), + }; + + let res = handler(&mut query_resolver); + self.packages = query_resolver.packages; + self.repositories = query_resolver.repositories; + self.save_to_disk()?; + + res + } +} + +struct PacBuildQueryResolver { + pub(super) repositories: Vec, + pub(super) packages: HashMap>, } -impl dyn Base { - /// Find all pacbuilds from all repositories - pub fn get_all_pacbuilds(&self) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(None, None, None, None) +struct RepositoryQueryResolver { + pub(super) repositories: Vec, + pub(super) packages: HashMap>, +} + +impl Query for RepositoryQueryResolver { + fn single(&self, query: RepositoryQuery) -> Option { + let all = self.find(query); + all.first().cloned() } - pub fn get_all_pacbuilds_by_name_like(&self, name_like: &str) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(Some(name_like), None, None, None) + fn find(&self, query: RepositoryQuery) -> Vec { + self.repositories + .clone() + .into_iter() + .filter(|it| query.matches(it)) + .collect() } - pub fn get_all_pacbuilds_by_name_like_and_kind( - &self, - name_like: &str, - kind: Kind, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(Some(name_like), None, Some(kind), None) + fn page(&self, query: RepositoryQuery, page_no: usize, page_size: usize) -> Vec { + let start_idx = page_no * page_size; + let mut end_idx = start_idx + page_size; + + let found = self.find(query); + + if start_idx > found.len() - 1 { + return Vec::new(); + } + + if found.len() < end_idx { + end_idx = found.len(); + } + + found[start_idx..end_idx].to_vec() } +} + +impl MutationQuery for RepositoryQueryResolver { + fn insert(&mut self, entity: Repository) -> StoreResult<()> { + let found = self.single( + RepositoryQuery::select_all() + .where_name(entity.name.as_str().into()) + .where_url(entity.url.as_str().into()), + ); - pub fn get_all_pacbuilds_by_name_like_and_install_state( - &self, - name_like: &str, - install_state: InstallState, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(Some(name_like), Some(install_state), None, None) + if found.is_some() { + return Err(StoreError::RepositoryConflict(entity.url)); + } + + self.repositories.push(entity); + + Ok(()) } - pub fn get_all_pacbuilds_by_name_like_and_repository_url( - &self, - name_like: &str, - url: &str, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(Some(name_like), None, None, Some(url)) + fn update(&mut self, entity: Repository) -> StoreResult<()> { + let repo = + self.single(RepositoryQuery::select_all().where_url(entity.name.as_str().into())); + + if repo.is_none() { + return Err(StoreError::RepositoryNotFound(entity.url)); + } + + let found = repo.unwrap(); + self.repositories.swap_remove( + self.repositories + .iter() + .position(|it| it.url == found.url) + .unwrap(), + ); + self.repositories.push(entity); + + Ok(()) } - pub fn get_all_pacbuilds_by_name_like_and_install_state_and_kind( - &self, - name_like: &str, - install_state: InstallState, - kind: Kind, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(Some(name_like), Some(install_state), Some(kind), None) + fn remove(&mut self, query: RepositoryQuery) -> StoreResult<()> { + let to_remove: _ = self + .repositories + .clone() + .into_iter() + .filter(|it| query.matches(it)) + .collect::>(); + + if to_remove.is_empty() { + return Err(StoreError::NoQueryMatch); + } + + let new_repos: Vec = self + .repositories + .clone() + .into_iter() + .filter(|it| !query.matches(it)) + .collect(); + + self.repositories = new_repos; + + if let Some(clause) = query.url { + for repo in to_remove { + if clause.matches(&repo.url) { + self.packages.remove(&repo.url); + } + } + } + + Ok(()) } +} - pub fn get_all_pacbuilds_by_name_like_and_install_state_and_repository_url( - &self, - name_like: &str, - install_state: InstallState, - url: &str, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(Some(name_like), Some(install_state), None, Some(url)) +impl Query for PacBuildQueryResolver { + fn single(&self, query: PacBuildQuery) -> Option { + let all = self.find(query); + all.first().cloned() } - pub fn get_all_pacbuilds_by_name_like_and_install_state_and_kind_and_repository_url( - &self, - name_like: &str, - install_state: InstallState, - kind: Kind, - url: &str, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(Some(name_like), Some(install_state), Some(kind), Some(url)) + fn find(&self, query: PacBuildQuery) -> Vec { + self.packages + .clone() + .into_iter() + .flat_map(|(_, it)| it) + .filter(|it| query.matches(it)) + .collect() } - pub fn get_all_pacbuilds_by_kind(&self, kind: Kind) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(None, None, Some(kind), None) + fn page(&self, query: PacBuildQuery, page_no: usize, page_size: usize) -> Vec { + let start_idx = page_no * page_size; + let mut end_idx = start_idx + page_size; + + let found = self.find(query); + + if start_idx > found.len() - 1 { + return Vec::new(); + } + + if found.len() < end_idx { + end_idx = found.len(); + } + + found[start_idx..end_idx].to_vec() } +} + +impl MutationQuery for PacBuildQueryResolver { + fn insert(&mut self, pacbuild: PacBuild) -> StoreResult<()> { + if !self + .repositories + .iter() + .any(|it| it.url == pacbuild.repository) + { + return Err(StoreError::RepositoryNotFound(pacbuild.repository.clone())); + } + + let found = self.single( + PacBuildQuery::select_all() + .where_name(pacbuild.name.as_str().into()) + .where_repository_url(pacbuild.repository.as_str().into()), + ); + + if found.is_some() { + return Err(StoreError::PacBuildConflict { + name: pacbuild.name.clone(), + repository: pacbuild.repository.clone(), + }); + } + + if let Some(packages) = self.packages.get_mut(&pacbuild.repository) { + packages.push(pacbuild); + } else { + self.packages + .insert(pacbuild.repository.clone(), vec![pacbuild]); + } - pub fn get_all_pacbuilds_by_kind_and_install_state( - &self, - kind: Kind, - install_state: InstallState, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(None, Some(install_state), Some(kind), None) + Ok(()) } - pub fn get_all_pacbuilds_by_kind_and_repository_url( - &self, - kind: Kind, - url: &str, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(None, None, Some(kind), Some(url)) + fn update(&mut self, pacbuild: PacBuild) -> StoreResult<()> { + if !self + .repositories + .iter() + .any(|it| it.url == pacbuild.repository) + { + return Err(StoreError::RepositoryNotFound(pacbuild.repository)); + } + + let found = self.single( + PacBuildQuery::select_all() + .where_name(pacbuild.name.as_str().into()) + .where_repository_url(pacbuild.repository.as_str().into()), + ); + + if found.is_none() { + return Err(StoreError::PacBuildNotFound { + name: pacbuild.name, + repository: pacbuild.repository, + }); + } + + let pkg = found.unwrap(); + let repo = self.packages.get_mut(&pkg.repository).unwrap(); + let pos = repo.iter().position(|it| it.name == pkg.name).unwrap(); + repo.remove(pos); + repo.push(pacbuild); + + Ok(()) } - pub fn get_all_pacbuilds_by_kind_and_install_state_and_repository_url( - &self, - kind: Kind, - install_state: InstallState, - url: &str, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(None, Some(install_state), Some(kind), Some(url)) + fn remove(&mut self, query: PacBuildQuery) -> StoreResult<()> { + let mut did_remove = false; + for packages in &mut self.packages.values_mut() { + let pkgs: _ = packages + .iter() + .cloned() + .filter(|it| !query.matches(it)) + .collect::>(); + + if packages.len() != pkgs.len() { + did_remove = true; + } + + *packages = pkgs; + } + + if did_remove { + Ok(()) + } else { + Err(StoreError::NoQueryMatch) + } } +} - pub fn get_all_pacbuilds_by_install_state( - &self, - install_state: InstallState, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(None, Some(install_state), None, None) +#[cfg(test)] +mod test { + use super::Store; + use crate::model::Repository; + use crate::store::filters::{InstallState, Kind}; + use crate::store::query_builder::{PacBuildQuery, RepositoryQuery, StringClause}; + + mod util { + use chrono::NaiveDateTime; + + use crate::model::{InstallState, Kind, PacBuild, Repository}; + use crate::store::base::Store; + + pub fn create_store_with_sample_data() -> (Store, Repository, PacBuild) { + let mut fss = Store::default(); + let repo = Repository::default(); + let pacbuild_to_add = PacBuild { + name: "dummy-pacbuild-deb".into(), + package_name: "dummy-pacbuild".into(), + description: "blah".into(), + dependencies: Vec::new(), + homepage: "https://example.com".into(), + install_state: InstallState::Direct( + NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), + "1.0.0".into(), + ), + kind: Kind::DebFile("hashash".into()), + last_updated: NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), + license: "BSD".into(), + maintainer: "saenai255".into(), + optional_dependencies: Vec::new(), + repology: "filter".into(), + repology_version: "1.0.1".into(), + repository: repo.url.clone(), + url: "https://example.com/dummy-pacbuild-1.0.0.deb".into(), + }; + + fss.mutate_repositories(|store| store.insert(repo.clone())) + .unwrap(); + fss.mutate_packages(|store| store.insert(pacbuild_to_add.clone())) + .unwrap(); + + (fss, repo, pacbuild_to_add) + } + } + + #[test] + fn new_creates_empty_fs_store() { + let fss = Store::default(); + let pacbuilds = fss.query_packages(|store| store.find(PacBuildQuery::select_all())); + let repos = fss.query_repositories(|store| store.find(RepositoryQuery::select_all())); + + assert_eq!(pacbuilds.len(), 0); + assert_eq!(repos.len(), 0); + } + + #[test] + fn add_repository_works() { + let mut fss = Store::default(); + + fss.mutate_repositories(|store| store.insert(Repository::default())) + .unwrap(); + let repos = fss.query_repositories(|store| store.find(RepositoryQuery::select_all())); + + assert_eq!(repos.len(), 1); + } + + #[test] + fn get_repository_by_name_works() { + let mut fss = Store::default(); + let repo = Repository::default(); + + fss.mutate_repositories(|store| store.insert(repo.clone())) + .unwrap(); + let found_repo = fss + .query_repositories(|store| { + store.single(RepositoryQuery::select_all().where_name(repo.name.as_str().into())) + }) + .unwrap(); + + assert_eq!(repo, found_repo); } - pub fn get_all_pacbuilds_by_install_state_and_repository_url( - &self, - install_state: InstallState, - url: &str, - ) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(None, Some(install_state), None, Some(url)) + #[test] + fn get_repository_by_url_works() { + let mut fss = Store::default(); + let repo = Repository::default(); + + fss.mutate_repositories(|store| store.insert(repo.clone())) + .unwrap(); + let found_repo = fss + .query_repositories(|store| { + store.single(RepositoryQuery::select_all().where_url(repo.url.as_str().into())) + }) + .unwrap(); + + assert_eq!(repo, found_repo); + } + + #[test] + fn add_pacbuild_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let pbs = fss.query_packages(|store| store.find(PacBuildQuery::select_all())); + + println!("{:#?}", pbs); + + assert_eq!(pbs.len(), 1); + } + + #[test] + fn get_pacbuild_by_name_and_url_works() { + let (fss, _, pacbuild) = util::create_store_with_sample_data(); + let found = fss + .query_packages(|store| { + store.single( + PacBuildQuery::select_all() + .where_name(pacbuild.name.as_str().into()) + .where_repository_url(pacbuild.repository.as_str().into()), + ) + }) + .unwrap(); + + assert_eq!(found, pacbuild); + } + + #[test] + fn get_all_pacbuilds_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.query_packages(|store| store.find(PacBuildQuery::select_all())); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_name_like_works() { + let (fss, _, pb) = util::create_store_with_sample_data(); + let found = fss.query_packages(|store| { + store.find( + PacBuildQuery::select_all().where_name(StringClause::Contains(pb.name.clone())), + ) + }); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_name_like_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.query_packages(|store| { + store.find( + PacBuildQuery::select_all().where_name(StringClause::Contains("blablabla".into())), + ) + }); + + assert_eq!(found.len(), 0); + } + + #[test] + fn get_all_pacbuilds_by_install_state_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.query_packages(|store| { + store.find(PacBuildQuery::select_all().where_install_state(InstallState::Direct)) + }); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_install_state_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.query_packages(|store| { + store.find(PacBuildQuery::select_all().where_install_state(InstallState::Indirect)) + }); + + assert_eq!(found.len(), 0); + } + + #[test] + fn get_all_pacbuilds_by_kind_works() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.query_packages(|store| { + store.find(PacBuildQuery::select_all().where_kind(Kind::DebFile)) + }); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_kind_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.query_packages(|store| { + store.find(PacBuildQuery::select_all().where_kind(Kind::Binary)) + }); + + assert_eq!(found.len(), 0); + } + + #[test] + fn get_all_pacbuilds_by_repository_url_works() { + let (fss, repo, _) = util::create_store_with_sample_data(); + let found = fss.query_packages(|store| { + store.find(PacBuildQuery::select_all().where_repository_url(repo.url.as_str().into())) + }); + + assert_eq!(found.len(), 1); + } + + #[test] + fn get_all_pacbuilds_by_repository_url_works_when_no_results() { + let (fss, ..) = util::create_store_with_sample_data(); + let found = fss.query_packages(|store| { + store.find(PacBuildQuery::select_all().where_repository_url("does not exist".into())) + }); + + assert_eq!(found.len(), 0); + } + + #[test] + fn update_pacbuild_works() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.description = "something else".into(); + + fss.mutate_packages(|query| query.update(pb.clone())) + .unwrap(); + + let results = fss.query_packages(|query| { + query.find( + PacBuildQuery::select_all() + .where_name(pb.name.as_str().into()) + .where_repository_url(pb.repository.as_str().into()), + ) + }); + let found = results.first().unwrap(); + + assert_eq!(pb, *found); + } + + #[test] + #[should_panic] + fn update_pacbuild_panics_when_pacbuild_not_found() { + let (mut fss, _, mut pb) = util::create_store_with_sample_data(); + pb.name = "lala".into(); + pb.description = "something else".into(); + + fss.mutate_packages(|query| query.update(pb.clone())) + .unwrap(); + } + + #[test] + #[should_panic] + fn remove_pacbuild_panics_when_pacbuild_not_found() { + let (mut fss, ..) = util::create_store_with_sample_data(); + + fss.mutate_packages(|query| { + query.remove(PacBuildQuery::select_all().where_name("asd".into())) + }) + .unwrap(); } - pub fn get_all_pacbuilds_by_repository_url(&self, url: &str) -> Vec<&PacBuild> { - self.get_all_pacbuilds_by(None, None, None, Some(url)) + #[test] + #[should_panic] + fn add_pacbuild_panics_when_pacbuild_already_exists() { + let (mut fss, _, pb) = util::create_store_with_sample_data(); + fss.mutate_packages(|query| query.insert(pb.clone())) + .unwrap(); } } diff --git a/src/store/errors.rs b/src/store/errors.rs index 3fee39a..27202ce 100644 --- a/src/store/errors.rs +++ b/src/store/errors.rs @@ -5,6 +5,9 @@ use thiserror::Error; /// Errors used by Base #[derive(Debug, Clone, Error)] pub enum StoreError { + #[error("the provided query yielded no results")] + NoQueryMatch, + #[error("repository '{0}' could not be found")] RepositoryNotFound(String), diff --git a/src/store/filters.rs b/src/store/filters.rs index d8195ca..718ba8a 100644 --- a/src/store/filters.rs +++ b/src/store/filters.rs @@ -1,7 +1,7 @@ //! Provides various structs for querying and filtering packages. /// Used to query packages by installation state -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum InstallState { Direct, Indirect, @@ -25,7 +25,7 @@ impl InstallState { } /// Used to query packages by kind. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Kind { AppImage, Binary, diff --git a/src/store/fs.rs b/src/store/fs.rs deleted file mode 100644 index 0f5e283..0000000 --- a/src/store/fs.rs +++ /dev/null @@ -1,641 +0,0 @@ -//! Provides a JSON file-based implementation for the caching system. - -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; - -use super::errors::StoreError; -use crate::model::{PacBuild, Repository}; -use crate::store::base::{Base, StoreResult}; -use crate::store::filters::{InstallState, Kind}; - -#[cfg(not(test))] -const FSS_PATH: &str = "/etc/pacstall/fss.json"; -#[cfg(test)] -const FSS_PATH: &str = "./fss.json"; - -/// `FileSystem` implementation for the caching system -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct FileSystemStore { - repositories: Vec, - packages: HashMap>, - - #[serde(skip_serializing)] - allow_data_save: bool, -} - -impl FileSystemStore { - #[allow(clippy::new_ret_no_self)] - pub fn new() -> Box { - Box::new(FileSystemStore { - repositories: Vec::new(), - packages: HashMap::new(), - allow_data_save: true, - }) - } - - /// # Private - fn get_packages_by_repository( - &mut self, - repository_url: &str, - ) -> Result<&mut Vec, StoreError> { - self.packages - .get_mut(&repository_url.to_owned()) - .map_or_else( - || Err(StoreError::RepositoryNotFound(repository_url.to_owned())), - Ok, - ) - } - - /// # Private - fn save_to_disk(&self) -> StoreResult<()> { - if self.allow_data_save { - use std::fs; - use std::path::Path; - - let json = serde_json::to_vec_pretty(self).map_or_else( - |_| { - Err(StoreError::Unexpected( - "Unable to serialize database.".to_string(), - )) - }, - Ok, - )?; - - return fs::write(Path::new(FSS_PATH), &json).map_or_else( - |_| { - Err(StoreError::Unexpected( - "Unable to write database to disk.".to_string(), - )) - }, - |_| Ok(()), - ); - } - - Ok(()) - } - - /// # Errors - pub fn load_from_disk() -> Result, StoreError> { - use std::fs; - use std::path::Path; - - let contents = fs::read_to_string(Path::new(FSS_PATH)).map_or_else( - |_| { - Err(StoreError::Unexpected( - "Unable to read database from disk.".to_string(), - )) - }, - Ok, - )?; - let mut obj: FileSystemStore = serde_json::from_str(&contents).map_or_else( - |_| { - Err(StoreError::Unexpected( - "Unable to deserialize database.".to_string(), - )) - }, - Ok, - )?; - obj.allow_data_save = true; - - Ok(Box::new(obj)) - } -} - -impl Base for FileSystemStore { - fn get_pacbuild_by_name_and_url(&self, name: &str, url: &str) -> Option<&PacBuild> { - self.packages - .get(&url.to_string()) - .and_then(|pkgs| pkgs.iter().find(|it| it.name == *name)) - } - - fn get_repository_by_name(&self, name: &str) -> Option<&Repository> { - self.repositories.iter().find(|repo| repo.name == *name) - } - - fn get_repository_by_url(&self, url: &str) -> Option<&Repository> { - self.repositories.iter().find(|repo| repo.url == *url) - } - - fn get_all_pacbuilds_by( - &self, - name_like: Option<&str>, - install_state: Option, - kind: Option, - repository_url: Option<&str>, - ) -> Vec<&PacBuild> { - let repos_urls = if let Some(url) = repository_url { - self.repositories - .iter() - .find(|it| it.url == *url) - .map_or_else(Vec::new, |it| vec![it.url.clone()]) - } else { - self.repositories.iter().map(|it| it.url.clone()).collect() - }; - - self.packages - .iter() - .filter(|(repo_url, _)| repos_urls.contains(repo_url)) - .flat_map(|(_, pkgs)| pkgs) - .filter(|it| { - if let Some(kind_filter) = &kind { - *kind_filter == (&it.kind).into() - } else { - true - } - }) - .filter(|it| { - if let Some(install_state_filter) = &install_state { - *install_state_filter == (&it.install_state).into() - } else { - true - } - }) - .filter(|it| { - if let Some(name_like) = name_like { - it.name.contains(name_like) - } else { - true - } - }) - .collect() - } - - fn remove_pacbuild(&mut self, name: &str, repository_url: &str) -> Result<(), StoreError> { - let repo = self - .packages - .get_mut(&repository_url.to_owned()) - .ok_or_else(|| StoreError::RepositoryNotFound(repository_url.to_string()))?; - - repo.swap_remove(repo.iter().position(|it| it.name == *name).ok_or( - StoreError::PacBuildNotFound { - name: name.to_string(), - repository: repository_url.to_string(), - }, - )?); - - self.save_to_disk()?; - Ok(()) - } - - fn add_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()> { - if self - .get_pacbuild_by_name_and_url(&pacbuild.name, repository_url) - .is_some() - { - return Err(StoreError::PacBuildConflict { - name: pacbuild.name, - repository: repository_url.to_string(), - }); - } - - self.get_packages_by_repository(repository_url)? - .push(pacbuild); - self.save_to_disk()?; - - Ok(()) - } - - fn update_pacbuild(&mut self, pacbuild: PacBuild, repository_url: &str) -> StoreResult<()> { - if self - .get_pacbuild_by_name_and_url(&pacbuild.name, repository_url) - .is_none() - { - return Err(StoreError::PacBuildNotFound { - name: pacbuild.name, - repository: repository_url.to_string(), - }); - } - - let pkgs = self.get_packages_by_repository(repository_url)?; - let idx = pkgs.iter().position(|pb| pb.name == pacbuild.name).unwrap(); - - pkgs.swap_remove(idx); - pkgs.push(pacbuild); - self.save_to_disk()?; - - Ok(()) - } - - fn remove_all_pacbuilds( - &mut self, - names: &[&str], - repository_url: &str, - ) -> Result<(), StoreError> { - let errors = names - .iter() - .map( - |it| match self.get_pacbuild_by_name_and_url(*it, repository_url) { - Some(_) => None, - None => Some(StoreError::PacBuildNotFound { - name: (*it).to_string(), - repository: repository_url.to_string(), - }), - }, - ) - .fold(vec![], |acc, it| { - if let Some(err) = it { - let mut acc = acc; - acc.push(err); - acc - } else { - acc - } - }); - - if !errors.is_empty() { - return Err(StoreError::Aggregate(errors)); - } - - let str_names: Vec = names.iter().map(ToString::to_string).collect(); - - let new_list: Vec = self - .get_packages_by_repository(repository_url)? - .clone() - .into_iter() - .filter(|it| str_names.contains(&it.name)) - .collect(); - - self.packages.insert(repository_url.to_owned(), new_list); - self.save_to_disk()?; - - Ok(()) - } - - fn add_all_pacbuilds( - &mut self, - pacbuilds: Vec, - repository_url: &str, - ) -> StoreResult<()> { - let already_existing_pkgs: Vec<&PacBuild> = pacbuilds - .iter() - .filter(|it| { - self.get_pacbuild_by_name_and_url(it.name.as_str(), repository_url) - .is_some() - }) - .collect(); - - if !already_existing_pkgs.is_empty() { - return Err(StoreError::Aggregate( - already_existing_pkgs - .iter() - .map(|it| StoreError::PacBuildConflict { - name: it.name.to_string(), - repository: it.repository.to_string(), - }) - .collect(), - )); - } - - let mut new_list: Vec = self.get_packages_by_repository(repository_url)?.clone(); - let mut to_add = pacbuilds.clone(); - new_list.append(&mut to_add); - self.packages.insert(repository_url.to_owned(), new_list); - self.save_to_disk()?; - - Ok(()) - } - - fn update_all_pacbuilds( - &mut self, - pacbuilds: Vec, - repository_url: &str, - ) -> StoreResult<()> { - for name in pacbuilds.iter().map(|it| &it.name) { - if self - .get_pacbuild_by_name_and_url(name, repository_url) - .is_none() - { - return Err(StoreError::PacBuildNotFound { - name: name.to_string(), - repository: repository_url.to_string(), - }); - } - } - - self.allow_data_save = false; - let errors: Vec> = pacbuilds - .iter() - .map(|it| self.update_pacbuild(it.clone(), repository_url)) - .filter(Result::is_err) - .collect(); - - self.allow_data_save = true; - - if errors.is_empty() { - self.save_to_disk()?; - Ok(()) - } else { - Err(StoreError::Aggregate( - errors.iter().map(|it| it.clone().unwrap_err()).collect(), - )) - } - } - - fn remove_repository(&mut self, repository_url: &str) -> Result<(), StoreError> { - let repo_idx = self - .repositories - .iter() - .position(|it| it.url.as_str() == repository_url) - .ok_or_else(|| StoreError::RepositoryNotFound(repository_url.to_string()))?; - - self.repositories.swap_remove(repo_idx); - self.packages.remove(&repository_url.to_owned()); - self.save_to_disk()?; - - Ok(()) - } - - fn add_repository(&mut self, repository: Repository) -> Result<(), StoreError> { - let repo_exists = self.repositories.iter().any(|it| it.url == repository.url); - - if repo_exists { - return Err(StoreError::RepositoryConflict(repository.url)); - } - - self.packages.insert(repository.url.clone(), Vec::new()); - self.repositories.push(repository); - self.save_to_disk()?; - - Ok(()) - } - - fn update_repository(&mut self, repository: Repository) -> StoreResult<()> { - let repo_idx = self - .repositories - .iter() - .position(|it| it.url == repository.url) - .ok_or_else(|| StoreError::RepositoryNotFound(repository.url.to_string()))?; - - self.repositories.swap_remove(repo_idx); - self.repositories.push(repository); - - self.save_to_disk()?; - - Ok(()) - } - - fn get_all_repositories(&self) -> Vec<&Repository> { self.repositories.iter().collect() } -} - -#[cfg(test)] -mod test { - use super::FileSystemStore; - use crate::model::Repository; - - mod util { - use chrono::NaiveDateTime; - - use crate::model::{InstallState, Kind, PacBuild, Repository}; - use crate::store::base::Base; - use crate::store::fs::FileSystemStore; - - pub fn create_store_with_sample_data() -> (Box, Repository, PacBuild) { - let mut fss = FileSystemStore::new(); - let repo = Repository::default(); - let pacbuild_to_add = PacBuild { - name: "dummy-pacbuild-deb".into(), - package_name: "dummy-pacbuild".into(), - description: "blah".into(), - dependencies: Vec::new(), - homepage: "https://example.com".into(), - install_state: InstallState::Direct( - NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), - "1.0.0".into(), - ), - kind: Kind::DebFile("hashash".into()), - last_updated: NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), - license: "BSD".into(), - maintainer: "saenai255".into(), - optional_dependencies: Vec::new(), - repology: "filter".into(), - repology_version: "1.0.1".into(), - repository: repo.url.clone(), - url: "https://example.com/dummy-pacbuild-1.0.0.deb".into(), - }; - - fss.add_repository(repo.clone()).unwrap(); - fss.add_pacbuild(pacbuild_to_add.clone(), &repo.url) - .unwrap(); - - (fss, repo, pacbuild_to_add) - } - } - - #[test] - fn new_creates_empty_fs_store() { - let fss = FileSystemStore::new(); - let pacbuilds = fss.get_all_pacbuilds(); - let repos = fss.get_all_repositories(); - - assert_eq!(pacbuilds.len(), 0); - assert_eq!(repos.len(), 0); - } - - #[test] - fn add_repository_works() { - let mut fss = FileSystemStore::new(); - - fss.add_repository(Repository::default()).unwrap(); - let repos = fss.get_all_repositories(); - - assert_eq!(repos.len(), 1); - } - - #[test] - fn get_repository_by_name_works() { - let mut fss = FileSystemStore::new(); - let repo = Repository::default(); - - fss.add_repository(repo.clone()).unwrap(); - let found_repo = fss.get_repository_by_name(&repo.name).unwrap(); - - assert_eq!(repo, found_repo.clone()); - } - - #[test] - fn get_repository_by_url_works() { - let mut fss = FileSystemStore::new(); - let repo = Repository::default(); - - fss.add_repository(repo.clone()).unwrap(); - let found_repo = fss.get_repository_by_url(&repo.url).unwrap(); - - assert_eq!(repo, found_repo.clone()); - } - - #[test] - fn add_pacbuild_works() { - let (fss, ..) = util::create_store_with_sample_data(); - let pbs = fss.get_all_pacbuilds(); - - println!("{:#?}", pbs); - - assert_eq!(pbs.len(), 1); - } - - #[test] - fn get_pacbuild_by_name_and_url_works() { - let (fss, _, pacbuild) = util::create_store_with_sample_data(); - let found = fss - .get_pacbuild_by_name_and_url(&pacbuild.name, &pacbuild.repository) - .unwrap(); - - assert_eq!(found.clone(), pacbuild); - } - - #[test] - fn get_all_pacbuilds_works() { - let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.get_all_pacbuilds(); - - assert_eq!(found.len(), 1); - } - - #[test] - fn get_all_pacbuilds_by_name_like_works() { - let (fss, _, pb) = util::create_store_with_sample_data(); - let found = fss.get_all_pacbuilds_by_name_like(&pb.name); - - assert_eq!(found.len(), 1); - } - - #[test] - fn get_all_pacbuilds_by_name_like_works_when_no_results() { - let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.get_all_pacbuilds_by_name_like("blalblala"); - - assert_eq!(found.len(), 0); - } - - #[test] - fn get_all_pacbuilds_by_install_state_works() { - let (fss, ..) = util::create_store_with_sample_data(); - let found = - fss.get_all_pacbuilds_by_install_state(crate::store::filters::InstallState::Direct); - - assert_eq!(found.len(), 1); - } - - #[test] - fn get_all_pacbuilds_by_install_state_works_when_no_results() { - let (fss, ..) = util::create_store_with_sample_data(); - let found = - fss.get_all_pacbuilds_by_install_state(crate::store::filters::InstallState::Indirect); - - assert_eq!(found.len(), 0); - } - - #[test] - fn get_all_pacbuilds_by_kind_works() { - let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.get_all_pacbuilds_by_kind(crate::store::filters::Kind::DebFile); - - assert_eq!(found.len(), 1); - } - - #[test] - fn get_all_pacbuilds_by_kind_works_when_no_results() { - let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.get_all_pacbuilds_by_kind(crate::store::filters::Kind::Binary); - - assert_eq!(found.len(), 0); - } - - #[test] - fn get_all_pacbuilds_by_repository_url_works() { - let (fss, repo, _) = util::create_store_with_sample_data(); - let found = fss.get_all_pacbuilds_by_repository_url(&repo.url); - - assert_eq!(found.len(), 1); - } - - #[test] - fn get_all_pacbuilds_by_repository_url_works_when_no_results() { - let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.get_all_pacbuilds_by_repository_url("this repo url does not exist"); - - assert_eq!(found.len(), 0); - } - - #[test] - fn update_pacbuild_works() { - let (mut fss, _, mut pb) = util::create_store_with_sample_data(); - pb.description = "something else".into(); - - fss.update_pacbuild(pb.clone(), &pb.repository).unwrap(); - let found = fss - .get_pacbuild_by_name_and_url(&pb.name, &pb.repository) - .unwrap(); - - assert_eq!(pb, *found); - } - - #[test] - fn update_all_pacbuilds_works() { - let (mut fss, _, mut pb) = util::create_store_with_sample_data(); - pb.description = "something else".into(); - - fss.update_all_pacbuilds(vec![pb.clone()], &pb.repository) - .unwrap(); - let found = fss - .get_pacbuild_by_name_and_url(&pb.name, &pb.repository) - .unwrap(); - - assert_eq!(pb, *found); - } - - #[test] - #[should_panic] - fn update_pacbuild_panics_when_pacbuild_not_found() { - let (mut fss, _, mut pb) = util::create_store_with_sample_data(); - pb.name = "lala".into(); - pb.description = "something else".into(); - - fss.update_pacbuild(pb.clone(), &pb.repository).unwrap(); - } - - #[test] - #[should_panic] - fn update_all_pacbuilds_panics_when_pacbuild_not_found() { - let (mut fss, _, mut pb) = util::create_store_with_sample_data(); - pb.name = "lala".into(); - pb.description = "something else".into(); - - fss.update_all_pacbuilds(vec![pb.clone()], &pb.repository) - .unwrap(); - } - - #[test] - #[should_panic] - fn remove_pacbuild_panics_when_pacbuild_not_found() { - let (mut fss, _, pb) = util::create_store_with_sample_data(); - - fss.remove_pacbuild("does-not-exist", &pb.repository) - .unwrap(); - } - - #[test] - #[should_panic] - fn remove_all_pacbuilds_panics_when_pacbuild_not_found() { - let (mut fss, _, pb) = util::create_store_with_sample_data(); - - fss.remove_all_pacbuilds(&(&vec!["does-not-exist"])[..], &pb.repository) - .unwrap(); - } - - #[test] - #[should_panic] - fn add_pacbuild_panics_when_pacbuild_already_exists() { - let (mut fss, _, pb) = util::create_store_with_sample_data(); - fss.add_pacbuild(pb.clone(), &pb.repository).unwrap(); - } - - #[test] - #[should_panic] - fn update_all_pacbuilds_panics_when_pacbuild_already_exists() { - let (mut fss, _, pb) = util::create_store_with_sample_data(); - fss.add_all_pacbuilds(vec![pb.clone()], &pb.repository) - .unwrap(); - } -} diff --git a/src/store/mod.rs b/src/store/mod.rs index 4908eed..24b6f2b 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -3,4 +3,4 @@ pub mod base; pub mod errors; pub mod filters; -pub mod fs; +pub mod query_builder; diff --git a/src/store/query_builder.rs b/src/store/query_builder.rs new file mode 100644 index 0000000..7d60efb --- /dev/null +++ b/src/store/query_builder.rs @@ -0,0 +1,190 @@ +use super::base::StoreResult; +use super::filters::{InstallState, Kind}; +use crate::model::{PacBuild, Repository}; + +pub trait Query { + fn single(&self, query: Q) -> Option; + fn find(&self, query: Q) -> Vec; + fn page(&self, query: Q, page_no: usize, page_size: usize) -> Vec; +} + +pub trait MutationQuery { + /// # Errors + fn remove(&mut self, query: Q) -> StoreResult<()>; + /// # Errors + fn insert(&mut self, entity: T) -> StoreResult<()>; + /// # Errors + fn update(&mut self, entity: T) -> StoreResult<()>; +} + +#[derive(Clone)] +pub enum QueryClause { + Not(T), + And(Vec), + Or(Vec), +} + +#[derive(Clone)] +pub enum StringClause { + Equals(String), + StartsWith(String), + EndsWith(String), + Contains(String), + Composite(Box>), +} + +impl StringClause { + pub fn matches(&self, value: &str) -> bool { + match self { + Self::Equals(it) => it == value, + Self::Contains(it) => value.contains(it), + Self::StartsWith(it) => value.starts_with(it), + Self::EndsWith(it) => value.ends_with(it), + Self::Composite(query) => match &**query { + QueryClause::Not(str_clause) => !str_clause.matches(value), + QueryClause::And(str_clauses) => str_clauses.iter().all(|it| it.matches(value)), + QueryClause::Or(str_clauses) => str_clauses.iter().any(|it| it.matches(value)), + }, + } + } +} + +impl From for StringClause { + fn from(it: String) -> Self { StringClause::Equals(it) } +} + +impl From<&str> for StringClause { + fn from(it: &str) -> Self { StringClause::Equals(String::from(it)) } +} + +impl From<&String> for StringClause { + fn from(it: &String) -> Self { StringClause::Equals(it.clone()) } +} + +#[derive(Clone)] +pub struct PacBuildQuery { + pub name: Option, + pub install_state: Option, + pub kind: Option, + pub repository_url: Option, +} + +impl PacBuildQuery { + pub fn matches(&self, pacbuild: &PacBuild) -> bool { + if let Some(clause) = &self.name { + if !clause.matches(&pacbuild.name) { + return false; + } + } + + if let Some(clause) = &self.repository_url { + if !clause.matches(&pacbuild.repository) { + return false; + } + } + + if let Some(kind) = &self.kind { + if kind != &Kind::from_model_kind(&pacbuild.kind.clone()) { + return false; + } + } + + if let Some(install_state) = &self.install_state { + if install_state + != &InstallState::from_model_install_state(&pacbuild.install_state.clone()) + { + return false; + } + } + + true + } +} + +#[derive(Clone)] +pub struct RepositoryQuery { + pub name: Option, + pub url: Option, +} + +impl RepositoryQuery { + pub(super) fn matches(&self, repository: &Repository) -> bool { + if let Some(clause) = &self.name { + if !clause.matches(&repository.name) { + return false; + } + } + + if let Some(clause) = &self.url { + if !clause.matches(&repository.url) { + return false; + } + } + + true + } +} + +#[allow(clippy::return_self_not_must_use)] +impl RepositoryQuery { + pub fn select_all() -> Self { + RepositoryQuery { + name: None, + url: None, + } + } + + pub fn where_name(&self, name: StringClause) -> Self { + let mut query = self.clone(); + query.name = Some(name); + + query + } + + pub fn where_url(&self, url: StringClause) -> Self { + let mut query = self.clone(); + query.url = Some(url); + + query + } +} + +#[allow(clippy::return_self_not_must_use)] +impl PacBuildQuery { + pub fn select_all() -> Self { + PacBuildQuery { + name: None, + install_state: None, + kind: None, + repository_url: None, + } + } + + pub fn where_name(&self, name: StringClause) -> Self { + let mut query = self.clone(); + query.name = Some(name); + + query + } + + pub fn where_install_state(&self, install_state: InstallState) -> Self { + let mut query = self.clone(); + query.install_state = Some(install_state); + + query + } + + pub fn where_kind(&self, kind: Kind) -> Self { + let mut query = self.clone(); + query.kind = Some(kind); + + query + } + + pub fn where_repository_url(&self, repository_url: StringClause) -> Self { + let mut query = self.clone(); + query.repository_url = Some(repository_url); + + query + } +} From 6cb663bc340c384910ccbb117a6598c236db2fec Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Mon, 10 Oct 2022 00:08:40 +0300 Subject: [PATCH 15/18] add: integration --- Cargo.toml | 2 +- src/model/pacbuild.rs | 50 ++++++- src/store/base.rs | 267 ++++++++++++++++++++++--------------- src/store/errors.rs | 85 +++++++++--- src/store/filters.rs | 24 +++- src/store/query_builder.rs | 92 +++++++++++-- 6 files changed, 378 insertions(+), 142 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1a1189e..f09d316 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ serde = { version = "1.0.144", features = ["derive"] } chrono = { version = "0.4.22", features = ["serde", "clock", "std"], default-features = false } serde_derive = "1.0.144" serde_json = "1.0.85" -thiserror = "1.0" +error-stack = "0.2.2" [dev-dependencies] rstest = "0.15.0" diff --git a/src/model/pacbuild.rs b/src/model/pacbuild.rs index 5f03d36..0506b96 100644 --- a/src/model/pacbuild.rs +++ b/src/model/pacbuild.rs @@ -6,20 +6,58 @@ use serde_derive::{Deserialize, Serialize}; /// Representation of the PACBUILD file. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PacBuild { + /// PacBuild unique name per [`Repository`](crate::model::Repository). pub name: PackageId, + + /// Last time it was changed. pub last_updated: DateTime, + + /// [`Repository`](crate::model::Repository) url. pub repository: URL, + + /// Maintainer name and possibly email. + /// + /// # Example + /// `Paul Cosma ` pub maintainer: String, + + /// Canonical package name. Usually the `name` without the type extension. + /// + /// # Example + /// - `PacBuild { name: "discord-deb", package_name: "discord" }` pub package_name: String, + + /// Short package description. pub description: String, + + /// Official homepage [URL]. pub homepage: URL, + + /// Latest version fetched from Repology. pub repology_version: Version, - pub repology: URL, + + /// Repology filter. + /// + /// # Example + /// **TBA** + pub repology: String, + + /// Installation state. pub install_state: InstallState, + + /// Runtime dependencies. pub dependencies: Vec, + + /// Optional dependencies. pub optional_dependencies: Vec, + + /// SPDX License. pub license: String, + + /// Download [URL]. pub url: URL, + + /// [`PacBuild`] type, deduced from the name suffix. pub kind: Kind, } @@ -102,18 +140,18 @@ impl InstallState { /// ``` #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum Kind { - /// [PacBuild] will install an `AppImage`. + /// [`PacBuild`] will install an `AppImage`. AppImage(Hash), - /// [PacBuild] will install a prebuilt, usually `tar.gz`, package. + /// [`PacBuild`] will install a prebuilt, usually `tar.gz`, package. Binary(Hash), - /// [PacBuild] will install an existing `.deb` file. + /// [`PacBuild`] will install an existing `.deb` file. DebFile(Hash), - /// [PacBuild] will install the source of a given Git branch. + /// [`PacBuild`] will install the source of a given Git branch. GitBranch, - /// [PacBuild] will install the source of a given Git release. + /// [`PacBuild`] will install the source of a given Git release. GitRelease, } diff --git a/src/store/base.rs b/src/store/base.rs index 0e1a965..2179f99 100644 --- a/src/store/base.rs +++ b/src/store/base.rs @@ -3,13 +3,17 @@ use std::collections::HashMap; use std::fmt::Debug; +use error_stack::{IntoReport, Report, Result, ResultExt}; use serde::{Deserialize, Serialize}; -use super::query_builder::{MutationQuery, PacBuildQuery, Query, RepositoryQuery}; +use super::errors::{ + EntityAlreadyExistsError, EntityMutationError, EntityNotFoundError, IOError, NoQueryMatchError, + StoreError, +}; +use super::query_builder::{Mutable, PacBuildQuery, Queryable, RepositoryQuery}; use crate::model::{PacBuild, Repository}; -use crate::store::errors::StoreError; -/// Alias for store error results +/// Shorthand alias for [`Result`]. pub type StoreResult = Result; /// Path of the database. @@ -20,7 +24,7 @@ const FSS_PATH: &str = "/etc/pacstall/fss.json"; #[cfg(test)] const FSS_PATH: &str = "./fss.json"; -/// `FileSystem` implementation for the caching system +/// Store implementation for metadata caching. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Store { repositories: Vec, @@ -28,27 +32,33 @@ pub struct Store { } impl Store { + /// Loads the store from the disk. + /// /// # Errors - pub fn load() -> Result { + /// + /// The following errors may occur: + /// + /// - [`StoreError`](crate::store::errors::StoreError) - Wrapper for all the + /// other [`Store`] errors + /// - [`IOError`](crate::store::errors::IOError) - When attempting database + /// import fails + pub fn load() -> StoreResult { use std::fs; use std::path::Path; - let contents = fs::read_to_string(Path::new(FSS_PATH)).map_or_else( - |_| { - Err(StoreError::Unexpected( - "Unable to read database from disk.".to_string(), - )) - }, - Ok, - )?; - let obj: Self = serde_json::from_str(&contents).map_or_else( - |_| { - Err(StoreError::Unexpected( - "Unable to deserialize database.".to_string(), - )) - }, - Ok, - )?; + let contents = fs::read_to_string(Path::new(FSS_PATH)) + .into_report() + .attach_printable_lazy(|| format!("failed to read file {FSS_PATH:?}")) + .change_context(IOError) + .change_context(StoreError)?; + + let obj: Self = serde_json::from_str(&contents) + .into_report() + .attach_printable_lazy(|| { + format!("failed to deserialize database contents: '{contents:?}'") + }) + .change_context(IOError) + .change_context(StoreError)?; Ok(obj) } @@ -58,30 +68,29 @@ impl Store { use std::fs; use std::path::Path; - let json = serde_json::to_vec_pretty(self).map_or_else( - |_| { - Err(StoreError::Unexpected( - "Unable to serialize database.".to_string(), - )) - }, - Ok, - )?; - - fs::write(Path::new(FSS_PATH), &json).map_or_else( - |_| { - Err(StoreError::Unexpected( - "Unable to write database to disk.".to_string(), - )) - }, - |_| Ok(()), - ) + let json = serde_json::to_vec_pretty(self) + .into_report() + .attach_printable_lazy(|| "failed to serialize database".to_string()) + .change_context(IOError) + .change_context(StoreError)?; + + fs::write(Path::new(FSS_PATH), &json) + .into_report() + .attach_printable_lazy(|| { + format!("failed to write serialized database to {FSS_PATH:?}") + }) + .change_context(IOError) + .change_context(StoreError)?; + + Ok(()) } } impl Store { - pub fn query_packages(&self, handler: F) -> R + /// Searches for [`PacBuild`]s based on the given query. + pub fn query_pacbuilds(&self, handler: F) -> R where - F: Fn(Box>) -> R, + F: Fn(Box>) -> R, { let query_resolver = Box::new(PacBuildQueryResolver { packages: self.packages.clone(), @@ -91,9 +100,10 @@ impl Store { handler(query_resolver) } + /// Searches for [`Repository`]s based on the given query. pub fn query_repositories(&self, handler: F) -> R where - F: Fn(Box>) -> R, + F: Fn(Box>) -> R, { let query_resolver = Box::new(RepositoryQueryResolver { packages: self.packages.clone(), @@ -103,10 +113,25 @@ impl Store { handler(query_resolver) } + /// Mutates [`PacBuild`]s based on the given query. + /// /// # Errors - pub fn mutate_packages(&mut self, mut handler: F) -> StoreResult + /// + /// The following errors may occur: + /// + /// - [`StoreError`](crate::store::errors::StoreError) - Wrapper for all the + /// other [`Store`] errors + /// - [`EntityNotFoundError`](crate::store::errors::EntityNotFoundError) - + /// When attempting to query a [`PacBuild`] or related entity that does + /// not exist + /// - [`EntityAlreadyExistsError`](crate::store::errors::EntityAlreadyExistsError) - When attempting insert a [`PacBuild`] or related entity that already exists + /// - [`NoQueryMatchError`](crate::store::errors::NoQueryMatchError) - When + /// attempting to remove a [`PacBuild`] that does not exist + /// - [`IOError`](crate::store::errors::IOError) - When attempting database + /// export fails + pub fn mutate_pacbuilds(&mut self, mut handler: F) -> StoreResult where - F: FnMut(&mut dyn MutationQuery) -> StoreResult, + F: FnMut(&mut dyn Mutable) -> StoreResult, { let mut query_resolver = PacBuildQueryResolver { packages: self.packages.clone(), @@ -121,10 +146,25 @@ impl Store { res } + /// Mutates [`Repository`]s based on the given query. + /// /// # Errors + /// + /// The following errors may occur: + /// + /// - [`StoreError`](crate::store::errors::StoreError) - Wrapper for all the + /// other [`Store`] errors + /// - [`EntityNotFoundError`](crate::store::errors::EntityNotFoundError) - + /// When attempting to query a [`Repository`] or related entity that does + /// not exist + /// - [`EntityAlreadyExistsError`](crate::store::errors::EntityAlreadyExistsError) - When attempting insert a [`Repository`] or related entity that already exists + /// - [`NoQueryMatchError`](crate::store::errors::NoQueryMatchError) - When + /// attempting to remove a [`Repository`] that does not exist + /// - [`IOError`](crate::store::errors::IOError) - When attempting database + /// export fails pub fn mutate_repositories(&mut self, mut handler: F) -> StoreResult where - F: FnMut(&mut dyn MutationQuery) -> StoreResult, + F: FnMut(&mut dyn Mutable) -> StoreResult, { let mut query_resolver = RepositoryQueryResolver { packages: self.packages.clone(), @@ -150,7 +190,7 @@ struct RepositoryQueryResolver { pub(super) packages: HashMap>, } -impl Query for RepositoryQueryResolver { +impl Queryable for RepositoryQueryResolver { fn single(&self, query: RepositoryQuery) -> Option { let all = self.find(query); all.first().cloned() @@ -182,16 +222,19 @@ impl Query for RepositoryQueryResolver { } } -impl MutationQuery for RepositoryQueryResolver { +impl Mutable for RepositoryQueryResolver { fn insert(&mut self, entity: Repository) -> StoreResult<()> { let found = self.single( - RepositoryQuery::select_all() + RepositoryQuery::select() .where_name(entity.name.as_str().into()) .where_url(entity.url.as_str().into()), ); if found.is_some() { - return Err(StoreError::RepositoryConflict(entity.url)); + return Err(Report::new(EntityAlreadyExistsError) + .attach_printable(format!("repository '{entity:?}' already exists")) + .change_context(EntityMutationError) + .change_context(StoreError)); } self.repositories.push(entity); @@ -200,11 +243,13 @@ impl MutationQuery for RepositoryQueryResolver { } fn update(&mut self, entity: Repository) -> StoreResult<()> { - let repo = - self.single(RepositoryQuery::select_all().where_url(entity.name.as_str().into())); + let repo = self.single(RepositoryQuery::select().where_url(entity.name.as_str().into())); if repo.is_none() { - return Err(StoreError::RepositoryNotFound(entity.url)); + return Err(Report::new(EntityNotFoundError) + .attach_printable(format!("repository '{entity:?}' does not exist")) + .change_context(EntityMutationError) + .change_context(StoreError)); } let found = repo.unwrap(); @@ -228,7 +273,10 @@ impl MutationQuery for RepositoryQueryResolver { .collect::>(); if to_remove.is_empty() { - return Err(StoreError::NoQueryMatch); + return Err(Report::new(NoQueryMatchError) + .attach_printable(format!("query '{query:?}' found no results")) + .change_context(EntityMutationError) + .change_context(StoreError)); } let new_repos: Vec = self @@ -252,7 +300,7 @@ impl MutationQuery for RepositoryQueryResolver { } } -impl Query for PacBuildQueryResolver { +impl Queryable for PacBuildQueryResolver { fn single(&self, query: PacBuildQuery) -> Option { let all = self.find(query); all.first().cloned() @@ -285,27 +333,32 @@ impl Query for PacBuildQueryResolver { } } -impl MutationQuery for PacBuildQueryResolver { +impl Mutable for PacBuildQueryResolver { fn insert(&mut self, pacbuild: PacBuild) -> StoreResult<()> { if !self .repositories .iter() .any(|it| it.url == pacbuild.repository) { - return Err(StoreError::RepositoryNotFound(pacbuild.repository.clone())); + return Err(Report::new(EntityNotFoundError) + .attach_printable(format!( + "repository of pacbuild {pacbuild:?} does not exist" + )) + .change_context(EntityMutationError) + .change_context(StoreError)); } let found = self.single( - PacBuildQuery::select_all() + PacBuildQuery::select() .where_name(pacbuild.name.as_str().into()) .where_repository_url(pacbuild.repository.as_str().into()), ); if found.is_some() { - return Err(StoreError::PacBuildConflict { - name: pacbuild.name.clone(), - repository: pacbuild.repository.clone(), - }); + return Err(Report::new(EntityAlreadyExistsError) + .attach_printable(format!("pacbuild {found:?} already exists")) + .change_context(EntityMutationError) + .change_context(StoreError)); } if let Some(packages) = self.packages.get_mut(&pacbuild.repository) { @@ -324,20 +377,27 @@ impl MutationQuery for PacBuildQueryResolver { .iter() .any(|it| it.url == pacbuild.repository) { - return Err(StoreError::RepositoryNotFound(pacbuild.repository)); + return Err(Report::new(EntityNotFoundError) + .attach_printable(format!( + "repository of pacbuild {pacbuild:?} does not exist" + )) + .change_context(EntityMutationError) + .change_context(StoreError)); } let found = self.single( - PacBuildQuery::select_all() + PacBuildQuery::select() .where_name(pacbuild.name.as_str().into()) .where_repository_url(pacbuild.repository.as_str().into()), ); if found.is_none() { - return Err(StoreError::PacBuildNotFound { - name: pacbuild.name, - repository: pacbuild.repository, - }); + return Err(Report::new(EntityNotFoundError) + .attach_printable(format!( + "repository of pacbuild {pacbuild:?} does not exist" + )) + .change_context(EntityMutationError) + .change_context(StoreError)); } let pkg = found.unwrap(); @@ -368,7 +428,10 @@ impl MutationQuery for PacBuildQueryResolver { if did_remove { Ok(()) } else { - Err(StoreError::NoQueryMatch) + Err(Report::new(NoQueryMatchError) + .attach_printable(format!("query {query:?} found no results")) + .change_context(EntityMutationError) + .change_context(StoreError)) } } } @@ -412,7 +475,7 @@ mod test { fss.mutate_repositories(|store| store.insert(repo.clone())) .unwrap(); - fss.mutate_packages(|store| store.insert(pacbuild_to_add.clone())) + fss.mutate_pacbuilds(|store| store.insert(pacbuild_to_add.clone())) .unwrap(); (fss, repo, pacbuild_to_add) @@ -422,8 +485,8 @@ mod test { #[test] fn new_creates_empty_fs_store() { let fss = Store::default(); - let pacbuilds = fss.query_packages(|store| store.find(PacBuildQuery::select_all())); - let repos = fss.query_repositories(|store| store.find(RepositoryQuery::select_all())); + let pacbuilds = fss.query_pacbuilds(|store| store.find(PacBuildQuery::select())); + let repos = fss.query_repositories(|store| store.find(RepositoryQuery::select())); assert_eq!(pacbuilds.len(), 0); assert_eq!(repos.len(), 0); @@ -435,7 +498,7 @@ mod test { fss.mutate_repositories(|store| store.insert(Repository::default())) .unwrap(); - let repos = fss.query_repositories(|store| store.find(RepositoryQuery::select_all())); + let repos = fss.query_repositories(|store| store.find(RepositoryQuery::select())); assert_eq!(repos.len(), 1); } @@ -449,7 +512,7 @@ mod test { .unwrap(); let found_repo = fss .query_repositories(|store| { - store.single(RepositoryQuery::select_all().where_name(repo.name.as_str().into())) + store.single(RepositoryQuery::select().where_name(repo.name.as_str().into())) }) .unwrap(); @@ -465,7 +528,7 @@ mod test { .unwrap(); let found_repo = fss .query_repositories(|store| { - store.single(RepositoryQuery::select_all().where_url(repo.url.as_str().into())) + store.single(RepositoryQuery::select().where_url(repo.url.as_str().into())) }) .unwrap(); @@ -475,7 +538,7 @@ mod test { #[test] fn add_pacbuild_works() { let (fss, ..) = util::create_store_with_sample_data(); - let pbs = fss.query_packages(|store| store.find(PacBuildQuery::select_all())); + let pbs = fss.query_pacbuilds(|store| store.find(PacBuildQuery::select())); println!("{:#?}", pbs); @@ -486,9 +549,9 @@ mod test { fn get_pacbuild_by_name_and_url_works() { let (fss, _, pacbuild) = util::create_store_with_sample_data(); let found = fss - .query_packages(|store| { + .query_pacbuilds(|store| { store.single( - PacBuildQuery::select_all() + PacBuildQuery::select() .where_name(pacbuild.name.as_str().into()) .where_repository_url(pacbuild.repository.as_str().into()), ) @@ -501,7 +564,7 @@ mod test { #[test] fn get_all_pacbuilds_works() { let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.query_packages(|store| store.find(PacBuildQuery::select_all())); + let found = fss.query_pacbuilds(|store| store.find(PacBuildQuery::select())); assert_eq!(found.len(), 1); } @@ -509,10 +572,8 @@ mod test { #[test] fn get_all_pacbuilds_by_name_like_works() { let (fss, _, pb) = util::create_store_with_sample_data(); - let found = fss.query_packages(|store| { - store.find( - PacBuildQuery::select_all().where_name(StringClause::Contains(pb.name.clone())), - ) + let found = fss.query_pacbuilds(|store| { + store.find(PacBuildQuery::select().where_name(StringClause::Contains(pb.name.clone()))) }); assert_eq!(found.len(), 1); @@ -521,9 +582,9 @@ mod test { #[test] fn get_all_pacbuilds_by_name_like_works_when_no_results() { let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.query_packages(|store| { + let found = fss.query_pacbuilds(|store| { store.find( - PacBuildQuery::select_all().where_name(StringClause::Contains("blablabla".into())), + PacBuildQuery::select().where_name(StringClause::Contains("blablabla".into())), ) }); @@ -533,8 +594,8 @@ mod test { #[test] fn get_all_pacbuilds_by_install_state_works() { let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.query_packages(|store| { - store.find(PacBuildQuery::select_all().where_install_state(InstallState::Direct)) + let found = fss.query_pacbuilds(|store| { + store.find(PacBuildQuery::select().where_install_state(InstallState::Direct)) }); assert_eq!(found.len(), 1); @@ -543,8 +604,8 @@ mod test { #[test] fn get_all_pacbuilds_by_install_state_works_when_no_results() { let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.query_packages(|store| { - store.find(PacBuildQuery::select_all().where_install_state(InstallState::Indirect)) + let found = fss.query_pacbuilds(|store| { + store.find(PacBuildQuery::select().where_install_state(InstallState::Indirect)) }); assert_eq!(found.len(), 0); @@ -553,9 +614,8 @@ mod test { #[test] fn get_all_pacbuilds_by_kind_works() { let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.query_packages(|store| { - store.find(PacBuildQuery::select_all().where_kind(Kind::DebFile)) - }); + let found = fss + .query_pacbuilds(|store| store.find(PacBuildQuery::select().where_kind(Kind::DebFile))); assert_eq!(found.len(), 1); } @@ -563,9 +623,8 @@ mod test { #[test] fn get_all_pacbuilds_by_kind_works_when_no_results() { let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.query_packages(|store| { - store.find(PacBuildQuery::select_all().where_kind(Kind::Binary)) - }); + let found = fss + .query_pacbuilds(|store| store.find(PacBuildQuery::select().where_kind(Kind::Binary))); assert_eq!(found.len(), 0); } @@ -573,8 +632,8 @@ mod test { #[test] fn get_all_pacbuilds_by_repository_url_works() { let (fss, repo, _) = util::create_store_with_sample_data(); - let found = fss.query_packages(|store| { - store.find(PacBuildQuery::select_all().where_repository_url(repo.url.as_str().into())) + let found = fss.query_pacbuilds(|store| { + store.find(PacBuildQuery::select().where_repository_url(repo.url.as_str().into())) }); assert_eq!(found.len(), 1); @@ -583,8 +642,8 @@ mod test { #[test] fn get_all_pacbuilds_by_repository_url_works_when_no_results() { let (fss, ..) = util::create_store_with_sample_data(); - let found = fss.query_packages(|store| { - store.find(PacBuildQuery::select_all().where_repository_url("does not exist".into())) + let found = fss.query_pacbuilds(|store| { + store.find(PacBuildQuery::select().where_repository_url("does not exist".into())) }); assert_eq!(found.len(), 0); @@ -595,12 +654,12 @@ mod test { let (mut fss, _, mut pb) = util::create_store_with_sample_data(); pb.description = "something else".into(); - fss.mutate_packages(|query| query.update(pb.clone())) + fss.mutate_pacbuilds(|query| query.update(pb.clone())) .unwrap(); - let results = fss.query_packages(|query| { + let results = fss.query_pacbuilds(|query| { query.find( - PacBuildQuery::select_all() + PacBuildQuery::select() .where_name(pb.name.as_str().into()) .where_repository_url(pb.repository.as_str().into()), ) @@ -617,7 +676,7 @@ mod test { pb.name = "lala".into(); pb.description = "something else".into(); - fss.mutate_packages(|query| query.update(pb.clone())) + fss.mutate_pacbuilds(|query| query.update(pb.clone())) .unwrap(); } @@ -626,8 +685,8 @@ mod test { fn remove_pacbuild_panics_when_pacbuild_not_found() { let (mut fss, ..) = util::create_store_with_sample_data(); - fss.mutate_packages(|query| { - query.remove(PacBuildQuery::select_all().where_name("asd".into())) + fss.mutate_pacbuilds(|query| { + query.remove(PacBuildQuery::select().where_name("asd".into())) }) .unwrap(); } @@ -636,7 +695,7 @@ mod test { #[should_panic] fn add_pacbuild_panics_when_pacbuild_already_exists() { let (mut fss, _, pb) = util::create_store_with_sample_data(); - fss.mutate_packages(|query| query.insert(pb.clone())) + fss.mutate_pacbuilds(|query| query.insert(pb.clone())) .unwrap(); } } diff --git a/src/store/errors.rs b/src/store/errors.rs index 27202ce..1c09412 100644 --- a/src/store/errors.rs +++ b/src/store/errors.rs @@ -1,28 +1,75 @@ -//! Errors used by the caching system +//! Errors used by the store -use thiserror::Error; +use std::fmt; -/// Errors used by Base -#[derive(Debug, Clone, Error)] -pub enum StoreError { - #[error("the provided query yielded no results")] - NoQueryMatch, +use error_stack::Context; - #[error("repository '{0}' could not be found")] - RepositoryNotFound(String), +/// Given store query yielded no results +#[derive(Debug, Clone)] +pub struct NoQueryMatchError; - #[error("pacbuild '{name:?}' could not be found in repository {repository:?}")] - PacBuildNotFound { name: String, repository: String }, +impl fmt::Display for NoQueryMatchError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str("query yielded no results") + } +} + +impl Context for NoQueryMatchError {} - #[error("repository '{0}' already exists")] - RepositoryConflict(String), +/// Store mutation failed +#[derive(Debug, Clone)] +pub struct EntityMutationError; - #[error("pacbuild '{name:?}' already exists in repository {repository:?}")] - PacBuildConflict { name: String, repository: String }, +impl fmt::Display for EntityMutationError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str("store mutation failed") + } +} - #[error("unexpected error: {0}")] - Unexpected(String), +impl Context for EntityMutationError {} - #[error("multiple errors: {0:?}")] - Aggregate(Vec), +/// Error representation of a failed IO operation. +#[derive(Debug, Clone)] +pub struct IOError; + +impl fmt::Display for IOError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str("failed to do IO operation") + } } + +impl Context for IOError {} + +/// Generic store error representation. +#[derive(Debug, Clone)] +pub struct StoreError; + +impl fmt::Display for StoreError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str("store operation failed") + } +} + +impl Context for StoreError {} + +/// Error representation for entities that are not found. +#[derive(Debug, Clone)] +pub struct EntityNotFoundError; + +impl fmt::Display for EntityNotFoundError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt.write_str("entity not found") } +} + +impl Context for EntityNotFoundError {} + +/// Error representation for entities that already exist, but shouldn't. +#[derive(Debug, Clone)] +pub struct EntityAlreadyExistsError; + +impl fmt::Display for EntityAlreadyExistsError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str("entity already exists") + } +} + +impl Context for EntityAlreadyExistsError {} diff --git a/src/store/filters.rs b/src/store/filters.rs index 718ba8a..13b3809 100644 --- a/src/store/filters.rs +++ b/src/store/filters.rs @@ -1,10 +1,17 @@ -//! Provides various structs for querying and filtering packages. +//! Provides various structs for querying and filtering +//! [`PacBuild`](crate::model::PacBuild)s. -/// Used to query packages by installation state +/// Used to query [`PacBuild`](crate::model::PacBuild)s by installation state. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum InstallState { + /// [`PacBuild`](crate::model::PacBuild) is installed directly. Direct, + + /// [`PacBuild`](crate::model::PacBuild) is installed, but as a dependency + /// of another. Indirect, + + /// [`PacBuild`](crate::model::PacBuild) is not installed. None, } @@ -24,13 +31,24 @@ impl InstallState { } } -/// Used to query packages by kind. +/// Used to query [`PacBuild`](crate::model::PacBuild)s by kind. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Kind { + /// [`PacBuild`](crate::model::PacBuild) is a prebuilt AppImage. AppImage, + + /// [`PacBuild`](crate::model::PacBuild) is a prebuilt binary. Usually a + /// compressed in a tar or zip file. Binary, + + /// [`PacBuild`](crate::model::PacBuild) is a prebuilt `.deb` file. DebFile, + + /// [`PacBuild`](crate::model::PacBuild) will be built from a git branch. GitBranch, + + /// [`PacBuild`](crate::model::PacBuild) will be built from a fixed git + /// release. GitRelease, } diff --git a/src/store/query_builder.rs b/src/store/query_builder.rs index 7d60efb..212b603 100644 --- a/src/store/query_builder.rs +++ b/src/store/query_builder.rs @@ -1,35 +1,99 @@ +//! Provides query utilities for the cache store + use super::base::StoreResult; use super::filters::{InstallState, Kind}; use crate::model::{PacBuild, Repository}; -pub trait Query { +/// Defines the common methods for querying entities. +pub trait Queryable { + /// Finds a single entity that matches the given query. fn single(&self, query: Q) -> Option; + + /// Finds all entities that match the given query. fn find(&self, query: Q) -> Vec; + + /// Finds a selection of entities that match the given query. fn page(&self, query: Q, page_no: usize, page_size: usize) -> Vec; } -pub trait MutationQuery { +/// Defines the common methods for mutating entities +pub trait Mutable { + /// Removes all entities that match the given + /// /// # Errors + /// + /// The following errors may occur: + /// + /// - [`StoreError`](crate::store::errors::StoreError) - Wrapper for all the + /// other [`Store`](crate::store::base::Store) errors + /// - [`NoQueryMatchError`](crate::store::errors::NoQueryMatchError) - When + /// attempting to remove an entity that does not exist + /// - [`IOError`](crate::store::errors::IOError) - When attempting database + /// export fails fn remove(&mut self, query: Q) -> StoreResult<()>; + + /// Inserts a single entity + /// /// # Errors + /// + /// The following errors may occur: + /// + /// - [`StoreError`](crate::store::errors::StoreError) - Wrapper for all the + /// other [`Store`](crate::store::base::Store) errors + /// - [`EntityNotFoundError`](crate::store::errors::EntityNotFoundError) - + /// When attempting to query an entity or related entity that does not + /// exist + /// - [`EntityAlreadyExistsError`](crate::store::errors::EntityAlreadyExistsError) - When attempting insert an entity or related entity that already exists + /// - [`IOError`](crate::store::errors::IOError) - When attempting database + /// export fails fn insert(&mut self, entity: T) -> StoreResult<()>; + + /// Removes all entities that match the given + /// /// # Errors + /// + /// The following errors may occur: + /// + /// - [`StoreError`](crate::store::errors::StoreError) - Wrapper for all the + /// other [`Store`](crate::store::base::Store) errors + /// - [`EntityNotFoundError`](crate::store::errors::EntityNotFoundError) - + /// When attempting to query a [`Repository`] or related entity that does + /// not exist + /// - [`EntityAlreadyExistsError`](crate::store::errors::EntityAlreadyExistsError) - When attempting insert a [`Repository`] or related entity that already exists + /// - [`IOError`](crate::store::errors::IOError) - When attempting database + /// export fails fn update(&mut self, entity: T) -> StoreResult<()>; } -#[derive(Clone)] +/// Represents a query utility for common verbs. +#[derive(Debug, Clone)] pub enum QueryClause { + /// Represents logical `NOT`. Not(T), + + /// Represents logical `AND`. And(Vec), + + /// Represents logical `OR`. Or(Vec), } -#[derive(Clone)] +/// Represents a string query utility. +#[derive(Debug, Clone)] pub enum StringClause { + /// Equivalent of `==`. Equals(String), + + /// Matches all strings starting with the wrapped string. StartsWith(String), + + /// Matches all strings ending with the wrapped string. EndsWith(String), + + /// Matches all strings containing the wrapped string. Contains(String), + + /// Represents a list of query conditionals. Composite(Box>), } @@ -61,7 +125,8 @@ impl From<&String> for StringClause { fn from(it: &String) -> Self { StringClause::Equals(it.clone()) } } -#[derive(Clone)] +/// Query representation for [`PacBuild`]s. +#[derive(Debug, Clone)] pub struct PacBuildQuery { pub name: Option, pub install_state: Option, @@ -70,7 +135,7 @@ pub struct PacBuildQuery { } impl PacBuildQuery { - pub fn matches(&self, pacbuild: &PacBuild) -> bool { + pub(super) fn matches(&self, pacbuild: &PacBuild) -> bool { if let Some(clause) = &self.name { if !clause.matches(&pacbuild.name) { return false; @@ -101,7 +166,8 @@ impl PacBuildQuery { } } -#[derive(Clone)] +/// Query representation for [`Repository`]s. +#[derive(Debug, Clone)] pub struct RepositoryQuery { pub name: Option, pub url: Option, @@ -127,13 +193,15 @@ impl RepositoryQuery { #[allow(clippy::return_self_not_must_use)] impl RepositoryQuery { - pub fn select_all() -> Self { + /// Initializes the query. + pub fn select() -> Self { RepositoryQuery { name: None, url: None, } } + /// Adds a name clause. pub fn where_name(&self, name: StringClause) -> Self { let mut query = self.clone(); query.name = Some(name); @@ -141,6 +209,7 @@ impl RepositoryQuery { query } + /// Adds a repository url clause. pub fn where_url(&self, url: StringClause) -> Self { let mut query = self.clone(); query.url = Some(url); @@ -151,7 +220,8 @@ impl RepositoryQuery { #[allow(clippy::return_self_not_must_use)] impl PacBuildQuery { - pub fn select_all() -> Self { + /// Initializes the query. + pub fn select() -> Self { PacBuildQuery { name: None, install_state: None, @@ -160,6 +230,7 @@ impl PacBuildQuery { } } + /// Adds a name clause. pub fn where_name(&self, name: StringClause) -> Self { let mut query = self.clone(); query.name = Some(name); @@ -167,6 +238,7 @@ impl PacBuildQuery { query } + /// Adds an [`InstallState`] clause. pub fn where_install_state(&self, install_state: InstallState) -> Self { let mut query = self.clone(); query.install_state = Some(install_state); @@ -174,6 +246,7 @@ impl PacBuildQuery { query } + /// Adds a [`Kind`] clause. pub fn where_kind(&self, kind: Kind) -> Self { let mut query = self.clone(); query.kind = Some(kind); @@ -181,6 +254,7 @@ impl PacBuildQuery { query } + /// Adds a repository url clause. pub fn where_repository_url(&self, repository_url: StringClause) -> Self { let mut query = self.clone(); query.repository_url = Some(repository_url); From 7cf0b38c423aaa50a75ad658cfc96ffcf5f8b75e Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Wed, 12 Oct 2022 22:54:35 +0300 Subject: [PATCH 16/18] chore: use `ensure!` and `report!` Co-authored-by: Sourajyoti Basak --- src/store/base.rs | 98 ++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/src/store/base.rs b/src/store/base.rs index 2179f99..29b4562 100644 --- a/src/store/base.rs +++ b/src/store/base.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::fmt::Debug; -use error_stack::{IntoReport, Report, Result, ResultExt}; +use error_stack::{ensure, report, IntoReport, Result, ResultExt}; use serde::{Deserialize, Serialize}; use super::errors::{ @@ -230,12 +230,13 @@ impl Mutable for RepositoryQueryResolver { .where_url(entity.url.as_str().into()), ); - if found.is_some() { - return Err(Report::new(EntityAlreadyExistsError) + ensure!( + found.is_none(), + report!(EntityAlreadyExistsError) .attach_printable(format!("repository '{entity:?}' already exists")) .change_context(EntityMutationError) - .change_context(StoreError)); - } + .change_context(StoreError) + ); self.repositories.push(entity); @@ -245,12 +246,13 @@ impl Mutable for RepositoryQueryResolver { fn update(&mut self, entity: Repository) -> StoreResult<()> { let repo = self.single(RepositoryQuery::select().where_url(entity.name.as_str().into())); - if repo.is_none() { - return Err(Report::new(EntityNotFoundError) + ensure!( + repo.is_some(), + report!(EntityNotFoundError) .attach_printable(format!("repository '{entity:?}' does not exist")) .change_context(EntityMutationError) - .change_context(StoreError)); - } + .change_context(StoreError) + ); let found = repo.unwrap(); self.repositories.swap_remove( @@ -265,19 +267,20 @@ impl Mutable for RepositoryQueryResolver { } fn remove(&mut self, query: RepositoryQuery) -> StoreResult<()> { - let to_remove: _ = self + let to_remove: Vec = self .repositories .clone() .into_iter() .filter(|it| query.matches(it)) - .collect::>(); + .collect(); - if to_remove.is_empty() { - return Err(Report::new(NoQueryMatchError) + ensure!( + !to_remove.is_empty(), + report!(NoQueryMatchError) .attach_printable(format!("query '{query:?}' found no results")) .change_context(EntityMutationError) - .change_context(StoreError)); - } + .change_context(StoreError) + ); let new_repos: Vec = self .repositories @@ -335,18 +338,17 @@ impl Queryable for PacBuildQueryResolver { impl Mutable for PacBuildQueryResolver { fn insert(&mut self, pacbuild: PacBuild) -> StoreResult<()> { - if !self - .repositories - .iter() - .any(|it| it.url == pacbuild.repository) - { - return Err(Report::new(EntityNotFoundError) + ensure!( + self.repositories + .iter() + .any(|it| it.url == pacbuild.repository), + report!(EntityNotFoundError) .attach_printable(format!( "repository of pacbuild {pacbuild:?} does not exist" )) .change_context(EntityMutationError) - .change_context(StoreError)); - } + .change_context(StoreError) + ); let found = self.single( PacBuildQuery::select() @@ -354,12 +356,13 @@ impl Mutable for PacBuildQueryResolver { .where_repository_url(pacbuild.repository.as_str().into()), ); - if found.is_some() { - return Err(Report::new(EntityAlreadyExistsError) + ensure!( + found.is_none(), + report!(EntityAlreadyExistsError) .attach_printable(format!("pacbuild {found:?} already exists")) .change_context(EntityMutationError) - .change_context(StoreError)); - } + .change_context(StoreError) + ); if let Some(packages) = self.packages.get_mut(&pacbuild.repository) { packages.push(pacbuild); @@ -372,18 +375,17 @@ impl Mutable for PacBuildQueryResolver { } fn update(&mut self, pacbuild: PacBuild) -> StoreResult<()> { - if !self - .repositories - .iter() - .any(|it| it.url == pacbuild.repository) - { - return Err(Report::new(EntityNotFoundError) + ensure!( + self.repositories + .iter() + .any(|it| it.url == pacbuild.repository), + report!(EntityNotFoundError) .attach_printable(format!( "repository of pacbuild {pacbuild:?} does not exist" )) .change_context(EntityMutationError) - .change_context(StoreError)); - } + .change_context(StoreError) + ); let found = self.single( PacBuildQuery::select() @@ -391,14 +393,15 @@ impl Mutable for PacBuildQueryResolver { .where_repository_url(pacbuild.repository.as_str().into()), ); - if found.is_none() { - return Err(Report::new(EntityNotFoundError) + ensure!( + found.is_some(), + report!(EntityNotFoundError) .attach_printable(format!( "repository of pacbuild {pacbuild:?} does not exist" )) .change_context(EntityMutationError) - .change_context(StoreError)); - } + .change_context(StoreError) + ); let pkg = found.unwrap(); let repo = self.packages.get_mut(&pkg.repository).unwrap(); @@ -412,11 +415,11 @@ impl Mutable for PacBuildQueryResolver { fn remove(&mut self, query: PacBuildQuery) -> StoreResult<()> { let mut did_remove = false; for packages in &mut self.packages.values_mut() { - let pkgs: _ = packages + let pkgs: Vec = packages .iter() .cloned() .filter(|it| !query.matches(it)) - .collect::>(); + .collect(); if packages.len() != pkgs.len() { did_remove = true; @@ -425,14 +428,15 @@ impl Mutable for PacBuildQueryResolver { *packages = pkgs; } - if did_remove { - Ok(()) - } else { - Err(Report::new(NoQueryMatchError) + ensure!( + did_remove, + report!(NoQueryMatchError) .attach_printable(format!("query {query:?} found no results")) .change_context(EntityMutationError) - .change_context(StoreError)) - } + .change_context(StoreError) + ); + + Ok(()) } } From 6de8656fa59a6af31915d523e0272e6664f0da6b Mon Sep 17 00:00:00 2001 From: Paul Cosma Date: Thu, 13 Oct 2022 00:18:38 +0300 Subject: [PATCH 17/18] add: store examples --- examples/store_crud.rs | 229 +++++++++++++++++++++++++++++++++++++++++ src/store/base.rs | 33 ++++-- 2 files changed, 251 insertions(+), 11 deletions(-) create mode 100644 examples/store_crud.rs diff --git a/examples/store_crud.rs b/examples/store_crud.rs new file mode 100644 index 0000000..de3dd62 --- /dev/null +++ b/examples/store_crud.rs @@ -0,0 +1,229 @@ +use chrono::NaiveDateTime; +use error_stack::Result; +use libpacstall::model::{InstallState, Kind, PacBuild, Repository}; +use libpacstall::store::base::Store; +use libpacstall::store::errors::StoreError; +use libpacstall::store::filters; +use libpacstall::store::query_builder::{PacBuildQuery, RepositoryQuery, StringClause}; + +fn main() { + let mut store = Store::in_memory(); + + example_entity_insertion(&mut store).unwrap(); + example_entity_query(&store); + example_entity_update(&mut store).unwrap(); + example_entity_deletion(&mut store).unwrap(); +} + +fn example_entity_query(store: &Store) { + println!("## Running [example_entity_query]"); + + println!("\n\tSearching for all pacbuilds that contain the word 'discord' in their name."); + + let pacbuilds = store.query_pacbuilds(|store| { + store.find( + PacBuildQuery::select().where_name(StringClause::Contains(String::from("discord"))), + ) + }); + + println!( + "\n\tWe are expecting to find 1 result. Found: {}.", + pacbuilds.len() + ); + assert_eq!(pacbuilds.len(), 1); + println!("\tDone!"); + + let pacbuild = pacbuilds.first().unwrap(); + + println!( + "\n\tWe are expecting to find 'discord-deb' from 'https://awesome-repository.local' \ + repository." + ); + println!( + "\t\tFound '{}' from repository '{}'", + &pacbuild.name, &pacbuild.repository + ); + assert_eq!(pacbuild.name, String::from("discord-deb")); + assert_eq!( + pacbuild.repository, + String::from("https://awesome-repository.local") + ); + println!("\tDone!\n"); +} + +#[allow(clippy::redundant_pattern_matching)] +fn example_entity_insertion(store: &mut Store) -> Result<(), StoreError> { + println!("\n## Running [example_entity_insertion]\n"); + + // Create dummy data + let repository = create_repository( + String::from("My Awesome Repository"), + String::from("https://awesome-repository.local"), + ); + + let pacbuild = create_pacbuild( + String::from("discord-deb"), + InstallState::None, + Kind::DebFile(String::from("some hash")), + repository.url.clone(), + ); + + // Insert repository first, because the pacbuild depends on it. + println!("\n\tAttempting to insert the new repository into the store."); + store.mutate_repositories(|store| store.insert(repository.clone()))?; + println!("\tDone!\n"); + + // Repository exists so it is safe to add the pacbuild. + println!("\tAttempting to insert the new pacbuild into the store."); + store.mutate_pacbuilds(|store| store.insert(pacbuild.clone()))?; + println!("\tDone!\n"); + + // PacBuild is already inserted, so trying to insert it again would result in a + // conflict error. + println!("\tAttempting to insert the same pacbuild into the store."); + let result = store.mutate_pacbuilds(|store| store.insert(pacbuild.clone())); + if let Err(_) = &result { + println!("\t\tInserting the same pacbuild failed as expected."); + + // Uncomment the next line to see how the stacktrace looks :) + // result.unwrap(); + } else { + panic!("\t\tThis will never be printed.") + } + println!("\tDone!\n"); + + Ok(()) +} + +fn example_entity_update(store: &mut Store) -> Result<(), StoreError> { + println!("## Running [example_entity_update]\n"); + + // Search for the discord package. + println!("\tSearching for a single package called 'discord-deb'."); + let mut pacbuild = store + .query_pacbuilds(|store| { + store.single( + PacBuildQuery::select().where_name("discord-deb".into()) // Same as StringClause::Equals(String::from("discord-deb")) + ) + }) + .unwrap(); + + println!("\tFound: {:#?}\n", pacbuild); + assert_eq!(pacbuild.install_state, InstallState::None); + + // Assume we installed it + println!("\tWe update it so it looks like it is installed."); + pacbuild.install_state = InstallState::Direct(current_time(), String::from("1.0.0")); + store.mutate_pacbuilds(|store| store.update(pacbuild.clone()))?; + println!("\tUpdated pacbuild: {:#?}\n", pacbuild); + + // Search again + println!("\tWe search for the same package again."); + let same_pacbuild = store + .query_pacbuilds(|store| { + store.single(PacBuildQuery::select().where_install_state(filters::InstallState::Direct)) + }) + .unwrap(); + println!( + "\tValue after re-querying the store: {:#?}\n", + same_pacbuild + ); + + println!("\tAsserting that the change propagated."); + assert_eq!(pacbuild, same_pacbuild); + println!("\tDone!"); + + Ok(()) +} + +fn example_entity_deletion(store: &mut Store) -> Result<(), StoreError> { + println!("## Running [example_entity_deletion]\n"); + + // Select the first repository + println!("\tFetching a repository."); + let repository = store + .query_repositories(|store| store.single(RepositoryQuery::select())) + .unwrap(); + println!("\tFound: {:?}\n", repository); + + let pacbuilds = store.query_pacbuilds(|store| { + store.find(PacBuildQuery::select().where_repository_url(repository.url.as_str().into())) + }); + println!( + "\tThis repository has a total of **{}** pacbuilds.\n", + pacbuilds.len() + ); + + // We attempt to delete it + println!("\tAttempting to delete it."); + store.mutate_repositories(|store| { + store.remove(RepositoryQuery::select().where_url(repository.url.as_str().into())) + })?; + println!("\tDone!\n"); + + // Selecting the same repository again + println!("\tSelecting the same repository again."); + let found = store.query_repositories(|store| { + store.single(RepositoryQuery::select().where_url(repository.url.as_str().into())) + }); + assert!(found.is_none()); + println!("\tFound no match.\n"); + + // Find any pacbuild from that repository. + println!("\tAttempting to find any pacbuild from that repository."); + let pacbuilds = store.query_pacbuilds(|store| { + store.find(PacBuildQuery::select().where_repository_url(repository.url.as_str().into())) + }); + + println!("\tWe expect to find none. Found: **{}**\n", pacbuilds.len()); + assert_eq!(pacbuilds.len(), 0); + + Ok(()) +} + +fn create_pacbuild( + name: String, + install_state: InstallState, + kind: Kind, + repository_url: String, +) -> PacBuild { + println!( + "\tCreating dummy PacBuild[name = '{}', install_state = '{:?}', kind = '{:?}', repository \ + = '{}']", + name, install_state, kind, repository_url + ); + + PacBuild { + name, + last_updated: current_time(), + repository: repository_url, + maintainer: String::from(""), + package_name: String::from(""), + description: String::from(""), + homepage: String::from(""), + repology_version: String::from(""), + repology: String::from(""), + install_state, + dependencies: Vec::new(), + optional_dependencies: Vec::new(), + license: String::from("MIT"), + url: String::from("https://pacbuild.pac"), + kind, + } +} + +fn create_repository(name: String, url: String) -> Repository { + println!( + "\tCreating dummy Repository[name = '{}', url = '{}']", + name, url + ); + Repository { + name, + url, + preference: 0, + } +} + +fn current_time() -> NaiveDateTime { + NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0) +} diff --git a/src/store/base.rs b/src/store/base.rs index 29b4562..c8277ea 100644 --- a/src/store/base.rs +++ b/src/store/base.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use std::fmt::Debug; +use std::fs; +use std::path::Path; use error_stack::{ensure, report, IntoReport, Result, ResultExt}; use serde::{Deserialize, Serialize}; @@ -25,10 +27,13 @@ const FSS_PATH: &str = "/etc/pacstall/fss.json"; const FSS_PATH: &str = "./fss.json"; /// Store implementation for metadata caching. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Store { repositories: Vec, packages: HashMap>, + + #[serde(skip)] + in_memory: bool, } impl Store { @@ -43,9 +48,6 @@ impl Store { /// - [`IOError`](crate::store::errors::IOError) - When attempting database /// import fails pub fn load() -> StoreResult { - use std::fs; - use std::path::Path; - let contents = fs::read_to_string(Path::new(FSS_PATH)) .into_report() .attach_printable_lazy(|| format!("failed to read file {FSS_PATH:?}")) @@ -63,10 +65,19 @@ impl Store { Ok(obj) } + pub fn in_memory() -> Self { + Store { + repositories: Vec::new(), + packages: HashMap::new(), + in_memory: true, + } + } + /// # Private fn save_to_disk(&self) -> StoreResult<()> { - use std::fs; - use std::path::Path; + if self.in_memory { + return Ok(()); + } let json = serde_json::to_vec_pretty(self) .into_report() @@ -454,7 +465,7 @@ mod test { use crate::store::base::Store; pub fn create_store_with_sample_data() -> (Store, Repository, PacBuild) { - let mut fss = Store::default(); + let mut fss = Store::in_memory(); let repo = Repository::default(); let pacbuild_to_add = PacBuild { name: "dummy-pacbuild-deb".into(), @@ -488,7 +499,7 @@ mod test { #[test] fn new_creates_empty_fs_store() { - let fss = Store::default(); + let fss = Store::in_memory(); let pacbuilds = fss.query_pacbuilds(|store| store.find(PacBuildQuery::select())); let repos = fss.query_repositories(|store| store.find(RepositoryQuery::select())); @@ -498,7 +509,7 @@ mod test { #[test] fn add_repository_works() { - let mut fss = Store::default(); + let mut fss = Store::in_memory(); fss.mutate_repositories(|store| store.insert(Repository::default())) .unwrap(); @@ -509,7 +520,7 @@ mod test { #[test] fn get_repository_by_name_works() { - let mut fss = Store::default(); + let mut fss = Store::in_memory(); let repo = Repository::default(); fss.mutate_repositories(|store| store.insert(repo.clone())) @@ -525,7 +536,7 @@ mod test { #[test] fn get_repository_by_url_works() { - let mut fss = Store::default(); + let mut fss = Store::in_memory(); let repo = Repository::default(); fss.mutate_repositories(|store| store.insert(repo.clone())) From 8eb4a2030514e67272eec1c758091ee218edc7b6 Mon Sep 17 00:00:00 2001 From: "Paul Cosma (saenai255)" Date: Wed, 19 Oct 2022 00:01:26 +0300 Subject: [PATCH 18/18] add: rest of PKGBUILD spec --- examples/store_crud.rs | 24 +++-- src/model/pacbuild.rs | 216 ++++++++++++++++++++++++++++++++++++++--- src/store/base.rs | 24 +++-- src/store/errors.rs | 10 ++ 4 files changed, 245 insertions(+), 29 deletions(-) diff --git a/examples/store_crud.rs b/examples/store_crud.rs index de3dd62..73f21d0 100644 --- a/examples/store_crud.rs +++ b/examples/store_crud.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; + use chrono::NaiveDateTime; use error_stack::Result; -use libpacstall::model::{InstallState, Kind, PacBuild, Repository}; +use libpacstall::model::{InstallState, Kind, PacBuild, Repository, Version}; use libpacstall::store::base::Store; use libpacstall::store::errors::StoreError; use libpacstall::store::filters; @@ -113,7 +115,7 @@ fn example_entity_update(store: &mut Store) -> Result<(), StoreError> { // Assume we installed it println!("\tWe update it so it looks like it is installed."); - pacbuild.install_state = InstallState::Direct(current_time(), String::from("1.0.0")); + pacbuild.install_state = InstallState::Direct(current_time(), Version::single(1)); store.mutate_pacbuilds(|store| store.update(pacbuild.clone()))?; println!("\tUpdated pacbuild: {:#?}\n", pacbuild); @@ -197,16 +199,24 @@ fn create_pacbuild( name, last_updated: current_time(), repository: repository_url, - maintainer: String::from(""), - package_name: String::from(""), + maintainers: Vec::new(), + package_names: Vec::new(), description: String::from(""), homepage: String::from(""), - repology_version: String::from(""), + repology_version: Version::single(1), repology: String::from(""), install_state, dependencies: Vec::new(), - optional_dependencies: Vec::new(), - license: String::from("MIT"), + optional_dependencies: HashMap::new(), + licenses: vec![String::from("MIT")], + conflicts: Vec::new(), + epoch: 0, + groups: Vec::new(), + make_dependencies: Vec::new(), + package_base: None, + ppas: Vec::new(), + provides: Vec::new(), + replaces: Vec::new(), url: String::from("https://pacbuild.pac"), kind, } diff --git a/src/model/pacbuild.rs b/src/model/pacbuild.rs index 0506b96..060dc9b 100644 --- a/src/model/pacbuild.rs +++ b/src/model/pacbuild.rs @@ -1,8 +1,12 @@ //! +use std::collections::HashMap; + use chrono::NaiveDateTime as DateTime; use serde_derive::{Deserialize, Serialize}; +use crate::store::errors::InvalidVersionError; + /// Representation of the PACBUILD file. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PacBuild { @@ -15,17 +19,17 @@ pub struct PacBuild { /// [`Repository`](crate::model::Repository) url. pub repository: URL, - /// Maintainer name and possibly email. + /// List of maintainers. /// /// # Example /// `Paul Cosma ` - pub maintainer: String, + pub maintainers: Vec, /// Canonical package name. Usually the `name` without the type extension. /// /// # Example - /// - `PacBuild { name: "discord-deb", package_name: "discord" }` - pub package_name: String, + /// - `PacBuild { name: "discord-deb", package_name: vec!["discord"] }` + pub package_names: Vec, /// Short package description. pub description: String, @@ -42,19 +46,58 @@ pub struct PacBuild { /// **TBA** pub repology: String, + /// When building a split package, this variable can be used to explicitly + /// specify the name to be used to refer to the group of packages in the + /// output and in the naming of source-only tarballs. + pub package_base: Option, + /// Installation state. pub install_state: InstallState, - /// Runtime dependencies. - pub dependencies: Vec, + /// An array of packages that must be installed for the software to build + /// and run. + pub dependencies: Vec, + + /// Used to force the package to be seen as newer than any previous version + /// with a lower epoch. This value is required to be a non-negative + /// integer; the default is 0. It is used when the version numbering + /// scheme of a package changes (or is alphanumeric), breaking normal + /// version comparison logic. + pub epoch: i32, + + /// An array of additional packages that the software provides the features + /// of (or a virtual package such as cron or sh). Packages providing the + /// same item can be installed side-by-side, unless at least one of them + /// uses a conflicts array + pub provides: Vec, + + /// An array of packages that conflict with, or cause problems with the + /// package, if installed. All these packages and packages providing + /// this item will need to be removed + pub conflicts: Vec, + + /// An array of obsolete packages that are replaced by the package, e.g. + /// `wireshark-qt` uses `replaces=('wireshark')` + pub replaces: Vec, + + /// An array of PPAs that provide the package. + pub ppas: Vec, + + /// The group the package belongs in. For instance, when installing + /// `plasma`, it installs all packages belonging in that group. + pub groups: Vec, + + /// Optional dependencies. Each Key:Pair is meant to describe the package + /// identifier and the reason for installing. + pub optional_dependencies: HashMap, - /// Optional dependencies. - pub optional_dependencies: Vec, + /// An array of packages that are only required to build the software. + pub make_dependencies: Vec, - /// SPDX License. - pub license: String, + /// The license under which the software is distributed. + pub licenses: Vec, - /// Download [URL]. + /// File required to build the package. pub url: URL, /// [`PacBuild`] type, deduced from the name suffix. @@ -67,9 +110,122 @@ pub struct PacBuild { /// ``` /// use libpacstall::model::Version; /// -/// let ver: Version = "1.0.0".into(); +/// let ver: Version = Version::semver(1, 0, 0); /// ``` -pub type Version = String; +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Version { + pub major: i32, + pub minor: i32, + pub patch: i32, + pub suffix: Option, +} + +impl Version { + pub fn single(major: i32) -> Self { + Version { + major, + minor: 0, + patch: 0, + suffix: None, + } + } + + pub fn double(major: i32, minor: i32) -> Self { + Version { + major, + minor, + patch: 0, + suffix: None, + } + } + + pub fn semver(major: i32, minor: i32, patch: i32) -> Self { + Version { + major, + minor, + patch, + suffix: None, + } + } + + pub fn semver_extended(major: i32, minor: i32, patch: i32, suffix: &str) -> Self { + Version { + major, + minor, + patch, + suffix: Some(suffix.to_string()), + } + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + match self.major.partial_cmp(&other.major) { + Some(core::cmp::Ordering::Equal) => {}, + ord => return ord, + } + + match self.minor.partial_cmp(&other.minor) { + Some(core::cmp::Ordering::Equal) => {}, + ord => return ord, + } + + match self.patch.partial_cmp(&other.patch) { + Some(core::cmp::Ordering::Equal) => {}, + ord => return ord, + } + + self.suffix.partial_cmp(&other.suffix) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.partial_cmp(other) { + Some(ord) => ord, + None => panic!("unreachable"), + } + } +} + +impl TryFrom for Version { + type Error = InvalidVersionError; + + fn try_from(value: String) -> Result { + let parts: Vec<&str> = value.split('.').collect(); + + if parts.is_empty() { + return Err(InvalidVersionError {}); + } + + let major: i32 = parts[0].parse().map_err(|_| InvalidVersionError {})?; + + let minor: i32 = if parts.len() >= 2 { + parts[1].parse().map_err(|_| InvalidVersionError {})? + } else { + 0 + }; + + let patch: i32 = if parts.len() >= 3 { + parts[2].parse().map_err(|_| InvalidVersionError {})? + } else { + 0 + }; + + let suffix = if parts.len() >= 4 { + Some(parts[3..].join(".")) + } else { + None + }; + + Ok(Version { + major, + minor, + patch, + suffix, + }) + } +} /// Represents a [`PacBuild`] or Apt package name. /// # Examples @@ -80,6 +236,36 @@ pub type Version = String; /// let identifier: PackageId = "discord-deb".into(); /// ``` pub type PackageId = String; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VersionConstrainedPackageId { + Any(PackageId), + GreaterThan(Version, PackageId), + GreaterThanEquals(Version, PackageId), + LessThan(Version, PackageId), + LessThanEquals(Version, PackageId), + Between(Version, Version, PackageId), + BetweenInclusive(Version, Version, PackageId), +} + +#[allow(clippy::derive_hash_xor_eq)] +impl std::hash::Hash for VersionConstrainedPackageId { + fn hash(&self, state: &mut H) { + match &self { + Self::Any(p_id) + | Self::GreaterThan(_, p_id) + | Self::Between(_, _, p_id) + | Self::BetweenInclusive(_, _, p_id) + | Self::GreaterThanEquals(_, p_id) + | Self::LessThanEquals(_, p_id) + | Self::LessThan(_, p_id) => p_id.hash(state), + }; + } +} + +/// The group the package belongs in. For instance, when installing `plasma`, it +/// installs all packages belonging in that group. +pub type GroupId = String; /// Represents an URL /// # Examples /// @@ -104,11 +290,11 @@ pub type Hash = String; /// /// ``` /// use chrono::NaiveDate; -/// use libpacstall::model::InstallState; +/// use libpacstall::model::{InstallState, Version}; /// /// let installed_directly = InstallState::Direct( /// NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11), -/// "0.9.2".into(), +/// Version::semver(0, 9, 2), /// ); /// ``` #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/store/base.rs b/src/store/base.rs index c8277ea..60e60e1 100644 --- a/src/store/base.rs +++ b/src/store/base.rs @@ -459,9 +459,11 @@ mod test { use crate::store::query_builder::{PacBuildQuery, RepositoryQuery, StringClause}; mod util { + use std::collections::HashMap; + use chrono::NaiveDateTime; - use crate::model::{InstallState, Kind, PacBuild, Repository}; + use crate::model::{InstallState, Kind, PacBuild, Repository, Version}; use crate::store::base::Store; pub fn create_store_with_sample_data() -> (Store, Repository, PacBuild) { @@ -469,21 +471,29 @@ mod test { let repo = Repository::default(); let pacbuild_to_add = PacBuild { name: "dummy-pacbuild-deb".into(), - package_name: "dummy-pacbuild".into(), + package_names: vec!["dummy-pacbuild".to_string()], description: "blah".into(), dependencies: Vec::new(), homepage: "https://example.com".into(), install_state: InstallState::Direct( NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), - "1.0.0".into(), + Version::single(1), ), kind: Kind::DebFile("hashash".into()), last_updated: NaiveDateTime::from_timestamp(chrono::Utc::now().timestamp(), 0), - license: "BSD".into(), - maintainer: "saenai255".into(), - optional_dependencies: Vec::new(), + licenses: vec![String::from("BSD")], + maintainers: vec![String::from("saenai255")], + optional_dependencies: HashMap::new(), repology: "filter".into(), - repology_version: "1.0.1".into(), + repology_version: Version::semver(1, 0, 1), + conflicts: Vec::new(), + epoch: 0, + groups: Vec::new(), + make_dependencies: Vec::new(), + package_base: None, + ppas: Vec::new(), + provides: Vec::new(), + replaces: Vec::new(), repository: repo.url.clone(), url: "https://example.com/dummy-pacbuild-1.0.0.deb".into(), }; diff --git a/src/store/errors.rs b/src/store/errors.rs index 1c09412..b696fd2 100644 --- a/src/store/errors.rs +++ b/src/store/errors.rs @@ -73,3 +73,13 @@ impl fmt::Display for EntityAlreadyExistsError { } impl Context for EntityAlreadyExistsError {} + +/// Error representation for invalid versions. +#[derive(Debug, Clone)] +pub struct InvalidVersionError; + +impl fmt::Display for InvalidVersionError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt.write_str("invalid version") } +} + +impl Context for InvalidVersionError {}