diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2021-09-13 23:08:19 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2021-09-13 23:08:19 -0300 |
commit | 22e7a62e9529cc4a59941e8342f69d0f6ded60f9 (patch) | |
tree | 5f2301c2e270b452fef08ed116b0d351a07c972c | |
parent | 4ba9626f711805d99c447a862682b568ad6e1e70 (diff) |
Mostly finish data source interactions
-rw-r--r-- | Cargo.lock | 12 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/data.rs | 39 | ||||
-rw-r--r-- | src/data/effective.rs | 36 | ||||
-rw-r--r-- | src/data/effective/tests.rs | 108 | ||||
-rw-r--r-- | src/data/kinds.rs | 156 | ||||
-rw-r--r-- | src/data/managers.rs | 457 | ||||
-rw-r--r-- | src/data/sources.rs | 21 | ||||
-rw-r--r-- | src/data/sources/defaults.rs | 107 | ||||
-rw-r--r-- | src/data/tests.rs | 19 |
10 files changed, 892 insertions, 64 deletions
diff --git a/Cargo.lock b/Cargo.lock index 18724b0..0307d8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" version = "0.7.15" @@ -148,6 +150,7 @@ dependencies = [ "assert_fs", "impl_trait", "predicates", + "qcell", "testserver", "url", ] @@ -341,6 +344,15 @@ dependencies = [ ] [[package]] +name = "qcell" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30d13d440673c6c73ecf8af12b60a53e106517a97b06ec64b3a26bd08c8ac5c" +dependencies = [ + "once_cell", +] + +[[package]] name = "quote" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 94bf88c..b75f901 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" impl_trait = "0.1.6" url = "2.2.2" arcstr = "1.1.1" +qcell = "0.4.2" [dev-dependencies] assert_fs = "1.0.1" diff --git a/src/data.rs b/src/data.rs index 3910dda..75ce565 100644 --- a/src/data.rs +++ b/src/data.rs @@ -14,9 +14,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -//! This module handles data source retrieval, parsing and processing. +//! Data sources and their functionality. //! -//! Data sources identify where to find repos and projects, among other things. +//! Data sources are the primary means by which GAnarchy figures out what +//! data to work with, and managers help combine and filter data sources into +//! producing the desired data for the operation of an instance. use std::error; use std::hash::Hash; @@ -25,20 +27,16 @@ use std::time::Duration; pub mod effective; pub mod kinds; pub mod managers; +pub mod sources; + +#[cfg(test)] +mod tests; /// A source of data. -pub trait DataSourceBase { - /// Refreshes the data associated with this source, if necessary, and - /// returns the interval at which this should be called again. - /// - /// # Errors - /// - /// Returns an error if the source could not be updated. If an error is - /// returned, an attempt should be made to **not** invalidate this data - /// source, but instead carry on using stale data. Note that an aggregate - /// data source may partially update and return an aggregate error type. +pub trait DataSourceBase: std::fmt::Display { + /// Refreshes the data associated with this source. fn update(&mut self) -> ( - Duration, + Option<Duration>, Result<(), Box<dyn error::Error + Send + Sync + 'static>>, ); @@ -63,8 +61,8 @@ pub trait DataSource<T: Kind>: DataSourceBase { /// Returns the value associated with this kind. /// - /// This is the same as [`get_values`] but allows using the singular name, - /// for kinds with up to only one value. + /// This is the same as [`DataSource::get_values`] but allows using the + /// singular name, for kinds with up to only one value. /// /// # Panics /// @@ -82,7 +80,7 @@ pub trait Kind { } /// A kind of value that can be retrieved from a data source, which can -/// override values of the same kind. +/// override values of the same kind based on a key. pub trait OverridableKind: Kind { /// The key, as in a map key, that gets overridden. type Key: Hash + Eq + Ord; @@ -91,3 +89,12 @@ pub trait OverridableKind: Kind { /// a set such as `BTreeSet`. fn as_key(&self) -> &Self::Key; } + +/// Stats about an update. +#[derive(Default)] +pub struct Update { + /// Time until next refresh, if known. + pub refresh: Option<Duration>, + /// Errors collected in an update. + pub errors: Vec<Box<dyn error::Error + Send + Sync + 'static>>, +} diff --git a/src/data/effective.rs b/src/data/effective.rs index 12167dc..54b524f 100644 --- a/src/data/effective.rs +++ b/src/data/effective.rs @@ -14,8 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -//! A wrapper for [`DataSource`] that automatically handles -//! [`OverridableKind`]. +//! Effective data sources and kinds. +//! +//! An [`EffectiveDataSource`] is a wrapper around [`DataSource`] that +//! handles [`OverridableKind`], particularly those built around [`Vec`]. +//! +//! An [`EffectiveKind`] is a wrapper around [`Kind`] that handles +//! [`OverridableKind`], particularly those built around [`BTreeSet`]. +//! +//! This asymmetry is necessary for the correct, order-dependent, semantics. use std::collections::BTreeSet; use std::collections::HashSet; @@ -24,16 +31,25 @@ use std::time::Duration; use impl_trait::impl_trait; -use super::DataSourceBase; use super::DataSource; +use super::DataSourceBase; use super::Kind; use super::OverridableKind; -/// A wrapper for [`DataSource`] that automatically handles -/// [`OverridableKind`]. +#[cfg(test)] +mod tests; + +/// A wrapper for [`DataSource`] that handles [`OverridableKind`]. +/// +/// This filters [`Vec`]-type [`Kind`]s to return only the first occurrence +/// (by key) of a value. +#[derive(Debug)] pub struct EffectiveDataSource<T: DataSourceBase>(T); -/// A wrapper for [`OverridableKind`] that acts like the key for Eq/Hash/Ord. +/// A wrapper for [`OverridableKind`] for use in [`BTreeSet`]. +/// +/// This compares like the key, allowing one to extend a [`BTreeSet`] and get +/// the appropriate override semantics. #[repr(transparent)] #[derive(Debug)] pub struct EffectiveKind<T: OverridableKind>(pub T); @@ -100,7 +116,7 @@ impl_trait! { /// Forwards to the inner [`DataSourceBase`]. impl trait DataSourceBase { fn update(&mut self) -> ( - Duration, + Option<Duration>, Result<(), Box<dyn error::Error + Send + Sync + 'static>>, ) { self.0.update() @@ -111,6 +127,12 @@ impl_trait! { } } + impl trait std::fmt::Display { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.0.fmt(f) + } + } + /// Filters the inner [`DataSource`] using the appropriate impl. impl trait<K: Kind> DataSource<K> where diff --git a/src/data/effective/tests.rs b/src/data/effective/tests.rs new file mode 100644 index 0000000..dab503f --- /dev/null +++ b/src/data/effective/tests.rs @@ -0,0 +1,108 @@ +// This file is part of GAnarchy - decentralized development hub +// Copyright (C) 2021 Soni L. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Unit tests for the effective module. + +use std::collections::BTreeSet; +use std::error; +use std::time::Duration; + +use impl_trait::impl_trait; +use url::Url; + +use crate::data::{ + DataSource, + DataSourceBase, +}; +use crate::data::kinds::{ + InstanceBaseUrl, + InstanceTitle, + ProjectFork, + RepoListUrl, +}; +use super::{EffectiveDataSource, EffectiveKind}; + +/// A helper to test [`EffectiveDataSource`] and [`EffectiveKind`]. +struct EffectiveTester; + +impl_trait! { + impl EffectiveTester { + /// Always updates successfully, with an unknown refresh interval. + impl trait DataSourceBase { + fn update(&mut self) -> ( + Option<Duration>, + Result<(), Box<dyn error::Error + Send + Sync + 'static>>, + ) { + (None, Ok(())) + } + + fn exists(&self) -> bool { + true + } + } + + /// [`InstanceTitle`] effectiveness tester. + impl trait DataSource<InstanceTitle> { + fn get_values(&self) -> Option<InstanceTitle> { + Some(String::new().into()) + } + } + + /// [`InstanceBaseUrl`] effectiveness tester. + impl trait DataSource<InstanceBaseUrl> { + fn get_values(&self) -> Option<InstanceBaseUrl> { + None + } + } + + /// [`RepoListUrl`] effectiveness tester. + impl trait DataSource<RepoListUrl> { + fn get_values(&self) -> Vec<RepoListUrl> { + // TWO for the same url + vec![ + RepoListUrl::new_for({ + Url::parse("https://example.org").unwrap() + }), + RepoListUrl::new_for({ + Url::parse("https://example.org").unwrap() + }), + ] + } + } + + /// [`ProjectFork`] effectiveness tester. + impl trait DataSource<EffectiveKind<ProjectFork>> { + fn get_values(&self) -> BTreeSet<EffectiveKind<ProjectFork>> { + vec![ + ].into_iter().collect() + } + } + + impl trait std::fmt::Display { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "tester") + } + } + } +} + +/// Tests that the `Option` data sources pass through. +#[test] +fn test_options() { + let eds = EffectiveDataSource(EffectiveTester); + assert_eq!(None, DataSource::<InstanceBaseUrl>::get_values(&eds)); + assert_eq!("", DataSource::<InstanceTitle>::get_values(&eds).unwrap().0); +} diff --git a/src/data/kinds.rs b/src/data/kinds.rs index d492a92..f9dd14f 100644 --- a/src/data/kinds.rs +++ b/src/data/kinds.rs @@ -16,6 +16,8 @@ //! Kinds of data for use with `DataSource`. +use std::collections::BTreeSet; + use arcstr::ArcStr; use url::Url; @@ -23,35 +25,156 @@ use url::Url; use super::Kind; use super::OverridableKind; +// InstanceTitle and InstanceBaseUrl are all pub because there's nothing else +// they could be, nothing else to add to them, etc. They're as API-stable as it +// gets. + /// Title of an instance. -#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Debug)] pub struct InstanceTitle(pub String); +impl From<String> for InstanceTitle { + fn from(s: String) -> InstanceTitle { + InstanceTitle(s) + } +} + impl Kind for InstanceTitle { /// A source may only have one `InstanceTitle`. type Values = Option<Self>; } /// URL of an instance. -#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Debug)] pub struct InstanceBaseUrl(pub Url); +impl From<Url> for InstanceBaseUrl { + fn from(url: Url) -> InstanceBaseUrl { + InstanceBaseUrl(url) + } +} + impl Kind for InstanceBaseUrl { /// A source may only have one `InstanceBaseUrl`. type Values = Option<Self>; } /// URL of a repo list. -#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Debug)] pub struct RepoListUrl { // `Url` is actually fairly expensive, but we don't usually have a lot of // `RepoListUrl` around. Additionally these are generally unique. /// The actual URL that references a repo list. - pub url: Url, + url: Url, /// Whether this entry is active. - pub active: bool, + active: bool, /// Whether this repo list is allowed to have negative entries. - pub allow_negative_entries: bool, + allow_negative_entries: bool, +} + +impl RepoListUrl { + /// Creates a new `RepoListUrl` for the given [`Url`]. + /// + /// The resulting `RepoListUrl` is active by default, and, if the `Url` is + /// a `file://` URL, also allows negative entries. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// use ganarchy::data::kinds::RepoListUrl; + /// use url::Url; + /// + /// let remote_repo_list = RepoListUrl::new_for({ + /// Url::parse("https://example.org").unwrap() + /// }); + /// assert!(!remote_repo_list.allows_negative_entries()); + /// + /// let local_repo_list = RepoListUrl::new_for({ + /// Url::parse("file:///etc/xdg/ganarchy/repos.toml").unwrap() + /// }); + /// assert!(local_repo_list.allows_negative_entries()); + /// ``` + pub fn new_for(url: Url) -> RepoListUrl { + RepoListUrl { + allow_negative_entries: url.scheme() == "file", + url: url, + active: true, + } + } + + /// Returns whether negative entries are allowed for this `RepoListUrl`. + pub fn allows_negative_entries(&self) -> bool { + self.allow_negative_entries + } + + /// Forbids negative entries for this `RepoListUrl`. + /// + /// # Examples + /// + /// ```rust + /// use ganarchy::data::kinds::RepoListUrl; + /// use url::Url; + /// + /// let mut local_repo_list = RepoListUrl::new_for({ + /// Url::parse("file:///etc/xdg/ganarchy/repos.toml").unwrap() + /// }); + /// assert!(local_repo_list.allows_negative_entries()); + /// local_repo_list.forbid_negative_entries(); + /// assert!(!local_repo_list.allows_negative_entries()); + /// ``` + pub fn forbid_negative_entries(&mut self) { + self.allow_negative_entries = false; + } + + /// Activates this `RepoListUrl`. + pub fn activate(&mut self) { + self.active = true; + } + + /// Deactivates this `RepoListUrl`. + pub fn deactivate(&mut self) { + self.active = false; + } + + /// Returns whether this `RepoListUrl` is active. + /// + /// # Examples + /// + /// ```rust + /// use ganarchy::data::kinds::RepoListUrl; + /// use url::Url; + /// + /// let mut local_repo_list = RepoListUrl::new_for({ + /// Url::parse("file:///etc/xdg/ganarchy/repos.toml").unwrap() + /// }); + /// assert!(local_repo_list.is_active()); + /// local_repo_list.deactivate(); + /// assert!(!local_repo_list.is_active()); + /// local_repo_list.activate(); + /// assert!(local_repo_list.is_active()); + /// ``` + pub fn is_active(&self) -> bool { + self.active + } + + /// Returns the `Url` associated with this `RepoListUrl`. + /// + /// # Examples + /// + /// ```rust + /// use ganarchy::data::kinds::RepoListUrl; + /// use url::Url; + /// + /// let remote_repo_list = RepoListUrl::new_for({ + /// Url::parse("https://example.org").unwrap() + /// }); + /// assert_eq!("https://example.org/", remote_repo_list.get_url().as_ref()); + /// ``` + pub fn get_url(&self) -> &Url { + &self.url + } } impl Kind for RepoListUrl { @@ -68,23 +191,34 @@ impl OverridableKind for RepoListUrl { } /// The key for a [`ProjectFork`]. -#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Debug)] pub struct ProjectForkKey { + /// The project's commit ID. project: ArcStr, + /// The fork's URL. url: ArcStr, + /// The fork's branch. branch: ArcStr, } /// A fork of a project. -#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Debug)] pub struct ProjectFork { - pub key: ProjectForkKey, - pub active: bool, + /// The fork. + key: ProjectForkKey, + /// Whether this entry is active. + active: bool, + /// Whether this entry is local to this instance. + local: bool, +} + +impl ProjectFork { + //pub fn new_for( } impl Kind for ProjectFork { /// A source may have many `ProjectFork`. - type Values = Vec<Self>; + type Values = BTreeSet<Self>; } impl OverridableKind for ProjectFork { diff --git a/src/data/managers.rs b/src/data/managers.rs index 71a9484..9a82a8e 100644 --- a/src/data/managers.rs +++ b/src/data/managers.rs @@ -14,64 +14,293 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. +//! Standard managers. +//! +//! Managers combine multiple data sources together and apply any desired +//! filtering. + +use std::collections::BTreeSet; use std::error; +use std::fmt; use std::sync::Arc; -use std::sync::Mutex; -use std::sync::RwLock; use std::time::Duration; use impl_trait::impl_trait; +use qcell::{QCell, QCellOwner}; use super::DataSourceBase; use super::DataSource; +#[cfg(doc)] +use super::effective::EffectiveDataSource; use super::effective::EffectiveKind; -//use super::Kind; use super::kinds::{InstanceTitle, InstanceBaseUrl, RepoListUrl, ProjectFork}; -//use super::OverridableKind; +use super::sources::DefaultsDataSource; -/// Stores multiple DataSource capable of ProjectFork +/// A wrapper around multiple [`DataSource`]s of [`ProjectFork`]s. +/// +/// While a `RepoListManager` doesn't have to be wrapped in an +/// [`EffectiveDataSource`] for correctness, it's usually desired to do so for +/// consistency. #[derive(Default)] pub struct RepoListManager { repos: Vec<Box<dyn DataSource<EffectiveKind<ProjectFork>> + Send + Sync>>, - durations: Vec<Duration>, + durations: Vec<Option<Duration>>, valid: usize, } -/// Stores multiple DataSource capable of InstanceTitle, InstanceBaseUrl and -/// RepoListUrl +/// A wrapper around multiple [`DataSource`]s of kinds generally used for +/// configuration. +/// +/// `ConfigManager` wraps the following [`DataSource`] kinds: +/// +/// - [`InstanceTitle`] +/// - [`InstanceBaseUrl`] +/// - [`RepoListUrl`] +/// +/// Generally, a `ConfigManager` must be wrapped in an [`EffectiveDataSource`] +/// for correctness. #[derive(Default)] pub struct ConfigManager { - // conceptually the actual objects - bases: Vec<Arc<RwLock<dyn DataSourceBase + Send + Sync>>>, - // conceptually just views of the above objects - titles: Vec<Option<Arc<RwLock<dyn DataSource<InstanceTitle> + Send + Sync>>>>, - urls: Vec<Option<Arc<RwLock<dyn DataSource<InstanceBaseUrl> + Send + Sync>>>>, - repolists: Vec<Option<Arc<RwLock<dyn DataSource<RepoListUrl> + Send + Sync>>>>, - durations: Vec<Duration>, + owner: QCellOwner, + resources: Vec<Resource>, + durations: Vec<Option<Duration>>, // add_source can be called after update. valid: usize, } +/// Helper for adding [`DataSource`]s to [`ConfigManager`]s. +/// +/// See the documentation for [`ConfigManager::add_source`] for details. +pub struct AddConfigSource<'a, T: DataSourceBase + Send + Sync + 'static> { + resource: &'a mut Resource, + source: Arc<QCell<T>>, +} + +/// Error type for managers in general. +#[derive(Debug)] +pub struct MultiResult { + pub results: Vec<Result<(), Box<dyn error::Error + Send + Sync>>>, +} + +struct Resource { + // actual resource + base: Arc<QCell<dyn DataSourceBase + Send + Sync>>, + // views of the actual resource + title: Option<Arc<QCell<dyn DataSource<InstanceTitle> + Send + Sync>>>, + url: Option<Arc<QCell<dyn DataSource<InstanceBaseUrl> + Send + Sync>>>, + repolist: Option<Arc<QCell<dyn DataSource<RepoListUrl> + Send + Sync>>>, +} + +impl Resource { + fn new(base: Arc<QCell<dyn DataSourceBase + Send + Sync>>) -> Self { + Self { + base, + title: None, + url: None, + repolist: None, + } + } +} + impl_trait! { - impl ConfigManager { - /// Creates a new `ConfigManager`. + impl MultiResult { + impl trait error::Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + // TODO return first error? + None + } + } + impl trait fmt::Display { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Multiple errors.") + } + } + } +} + +impl_trait! { + impl RepoListManager { + /// Creates a new, empty `RepoListManager`. Equivalent to + /// `Default::default()`. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::managers::RepoListManager; + /// + /// let repos = RepoListManager::new(); + /// ``` pub fn new() -> Self { Default::default() } - /// Adds the given combined `DataSource` to this `ConfigManager`. + /// Adds the given [`DataSource`] to this `RepoListManager`. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::sources::DefaultsDataSource; + /// use ganarchy::data::managers::RepoListManager; + /// + /// let mut repos = RepoListManager::default(); + /// repos.add_source(DefaultsDataSource); + /// ``` pub fn add_source<T>(&mut self, source: T) where - T: DataSource<InstanceTitle>, - T: DataSource<InstanceBaseUrl>, - T: DataSource<RepoListUrl>, - T: Send + Sync + 'static, + T: DataSource<EffectiveKind<ProjectFork>> + Send + Sync + 'static, + { + self.repos.push(Box::new(source)); + } + + impl trait DataSourceBase { + /// Updates the contained `DataSource`s, and returns the shortest + /// duration for the next update. + /// + /// # Errors + /// + /// Returns an error if any `DataSource` returns an error. Always + /// updates all `DataSource`s. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::managers::RepoListManager; + /// use ganarchy::data::sources::DefaultsDataSource; + /// use ganarchy::data::DataSourceBase; + /// + /// let mut repos = RepoListManager::default(); + /// repos.add_source(DefaultsDataSource); + /// let (duration, result) = repos.update(); + /// # assert!(duration.is_none()); + /// # assert!(result.is_ok()); + /// ``` + fn update(&mut self) -> ( + Option<Duration>, + Result<(), Box<dyn error::Error + Send + Sync + 'static>>, + ) { + let mut results = Vec::with_capacity(self.repos.len()); + let mut ok = true; + self.durations.resize(self.repos.len(), None); + Iterator::zip( + self.repos.iter_mut(), + self.durations.iter_mut(), + ).for_each(|e| { + if !matches!(*e.1, Some(d) if d.is_zero()) { + results.push(Ok(())); + return; + } + let (duration, result) = e.0.update(); + *e.1 = duration; + ok &= result.is_ok(); + results.push(result); + }); + let try_min = self.durations.iter().flatten().min().copied(); + let min = try_min.unwrap_or(Duration::ZERO); + for duration in self.durations.iter_mut().flatten() { + *duration -= min; + } + self.valid = results.len(); + (try_min, ok.then(|| ()).ok_or_else(|| { + Box::new(MultiResult { results }) as _ + })) + } + + /// Returns whether this data source contains any (valid) data. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::managers::RepoListManager; + /// use ganarchy::data::DataSourceBase; + /// + /// let mut repos = RepoListManager::default(); + /// // An empty RepoListManager has no data. + /// assert!(!repos.exists()); + /// ``` + fn exists(&self) -> bool { + self.repos[..self.valid].iter().any(|e| e.exists()) + } + } + + impl trait std::fmt::Display { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "repo list: [")?; + for e in self.repos.iter() { + write!(f, "{}, ", e)?; + } + write!(f, "]") + } + } + + /// Returns all [`ProjectFork`]s. + impl trait DataSource<EffectiveKind<ProjectFork>> { + fn get_values(&self) -> BTreeSet<EffectiveKind<ProjectFork>> { + self.repos[..self.valid].iter().flat_map(|e| { + e.get_values() + }).collect() + } + } + } +} + +impl_trait! { + impl ConfigManager { + /// Creates a new, empty `ConfigManager`. Equivalent to + /// `Default::default()`. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::managers::ConfigManager; + /// + /// let cm = ConfigManager::new(); + /// ``` + pub fn new() -> Self { + Default::default() + } + + /// Adds a [`DefaultsDataSource`] to this `ConfigManager`, for all + /// supported properties. + /// + /// The added source will override any further data sources, so this + /// should be called last, after all other data sources have been + /// added. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::managers::ConfigManager; + /// + /// let mut cm = ConfigManager::default(); + /// cm.add_defaults(); + /// ``` + pub fn add_defaults(&mut self) { + self.add_source(DefaultsDataSource).for_all(); + } + + /// Adds the given [`DataSource`] to this `ConfigManager` and returns + /// a helper for setting which properties this `DataSource` will + /// provide through this `ConfigManager`. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::sources::DefaultsDataSource; + /// use ganarchy::data::managers::ConfigManager; + /// + /// let mut cm = ConfigManager::default(); + /// cm.add_source(DefaultsDataSource).for_repo_lists(); + /// ``` + pub fn add_source<T>(&mut self, source: T) -> AddConfigSource<'_, T> + where + T: DataSourceBase + Send + Sync + 'static, { - let arc = Arc::new(RwLock::new(source)); - self.bases.push(arc.clone()); - self.titles.push(Some(arc.clone())); - self.urls.push(Some(arc.clone())); - self.repolists.push(Some(arc)); + let arc = Arc::new(QCell::new(&self.owner, source)); + self.resources.push(Resource::new(arc.clone())); + AddConfigSource { + resource: self.resources.last_mut().unwrap(), + source: arc, + } } impl trait DataSourceBase { @@ -82,17 +311,185 @@ impl_trait! { /// /// Returns an error if any `DataSource` returns an error. Always /// updates all `DataSource`s. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::managers::ConfigManager; + /// use ganarchy::data::DataSourceBase; + /// + /// let mut cm = ConfigManager::default(); + /// cm.add_defaults(); + /// let (duration, result) = cm.update(); + /// # assert!(duration.is_none()); + /// # assert!(result.is_ok()); + /// ``` fn update(&mut self) -> ( - Duration, + Option<Duration>, Result<(), Box<dyn error::Error + Send + Sync + 'static>>, ) { - todo!() + let owner = &mut self.owner; + let mut results = Vec::with_capacity(self.resources.len()); + let mut ok = true; + self.durations.resize(self.resources.len(), None); + Iterator::zip( + self.resources.iter(), + self.durations.iter_mut(), + ).for_each(|e| { + if !matches!(*e.1, Some(d) if d.is_zero()) { + results.push(Ok(())); + return; + } + let ret = owner.rw(&*e.0.base).update(); + let (duration, result) = ret; + *e.1 = duration; + ok &= result.is_ok(); + results.push(result); + }); + let try_min = self.durations.iter().flatten().min().copied(); + let min = try_min.unwrap_or(Duration::ZERO); + for duration in self.durations.iter_mut().flatten() { + *duration -= min; + } + self.valid = results.len(); + (try_min, ok.then(|| ()).ok_or_else(|| { + Box::new(MultiResult { results }) as _ + })) } /// Returns whether this data source contains any (valid) data. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::managers::ConfigManager; + /// use ganarchy::data::DataSourceBase; + /// + /// let mut cm = ConfigManager::default(); + /// // An empty ConfigManager has no data. + /// assert!(!cm.exists()); + /// ``` fn exists(&self) -> bool { - todo!() + self.resources[..self.valid].iter().any(|e| { + self.owner.ro(&*e.base).exists() + }) + } + } + + impl trait std::fmt::Display { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "config: [")?; + for e in self.resources.iter() { + write!(f, "{}, ", self.owner.ro(&*e.base))?; + } + write!(f, "]") + } + } + + /// Returns the first [`InstanceTitle`] available. + impl trait DataSource<InstanceTitle> { + fn get_values(&self) -> Option<InstanceTitle> { + self.resources[..self.valid].iter().flat_map(|e| { + e.title.as_ref() + }).flat_map(|e| { + self.owner.ro(&*e).get_values() + }).next() + } + } + + /// Returns the first [`InstanceBaseUrl`] available. + impl trait DataSource<InstanceBaseUrl> { + fn get_values(&self) -> Option<InstanceBaseUrl> { + self.resources[..self.valid].iter().flat_map(|e| { + e.url.as_ref() + }).flat_map(|e| { + self.owner.ro(&*e).get_values() + }).next() } } + + /// Returns all [`RepoListUrl`]s. + /// + /// For correct results, wrap this [`ConfigManager`] in an + /// [`EffectiveDataSource`]. + impl trait DataSource<RepoListUrl> { + fn get_values(&self) -> Vec<RepoListUrl> { + self.resources[..self.valid].iter().flat_map(|e| { + e.repolist.as_ref() + }).flat_map(|e| { + self.owner.ro(&*e).get_values() + }).collect() + } + } + } +} + +impl<'a, T: DataSourceBase + Send + Sync + 'static> AddConfigSource<'a, T> { + /// Adds this data source as a provider for [`InstanceTitle`]. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::sources::DefaultsDataSource; + /// use ganarchy::data::managers::ConfigManager; + /// + /// let mut cm = ConfigManager::default(); + /// cm.add_source(DefaultsDataSource).for_title(); + /// ``` + pub fn for_title(self) -> Self where T: DataSource<InstanceTitle> { + let arc = &self.source; + self.resource.title.get_or_insert_with(|| { + arc.clone() + }); + self + } + + /// Adds this data source as a provider for [`InstanceBaseUrl`]. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::sources::DefaultsDataSource; + /// use ganarchy::data::managers::ConfigManager; + /// + /// let mut cm = ConfigManager::default(); + /// cm.add_source(DefaultsDataSource).for_base_url(); + /// ``` + pub fn for_base_url(self) -> Self where T: DataSource<InstanceBaseUrl> { + let arc = &self.source; + self.resource.url.get_or_insert_with(|| { + arc.clone() + }); + self + } + + /// Adds this data source as a provider for [`RepoListUrl`]. + /// + /// # Examples + /// + /// ``` + /// use ganarchy::data::sources::DefaultsDataSource; + /// use ganarchy::data::managers::ConfigManager; + /// + /// let mut cm = ConfigManager::default(); + /// cm.add_source(DefaultsDataSource).for_repo_lists(); + /// ``` + pub fn for_repo_lists(self) -> Self where T: DataSource<RepoListUrl> { + let arc = &self.source; + self.resource.repolist.get_or_insert_with(|| { + arc.clone() + }); + self + } + + // pub(crate): we wanna be able to make breaking changes to this function + // without affecting downstream users. + pub(crate) fn for_all(self) -> Self + where + T: DataSource<InstanceTitle>, + T: DataSource<InstanceBaseUrl>, + T: DataSource<RepoListUrl>, + { + self.for_title().for_base_url().for_repo_lists() } } diff --git a/src/data/sources.rs b/src/data/sources.rs new file mode 100644 index 0000000..5d43961 --- /dev/null +++ b/src/data/sources.rs @@ -0,0 +1,21 @@ +// This file is part of GAnarchy - decentralized development hub +// Copyright (C) 2021 Soni L. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Standard data sources. + +pub mod defaults; + +pub use defaults::DefaultsDataSource; diff --git a/src/data/sources/defaults.rs b/src/data/sources/defaults.rs new file mode 100644 index 0000000..e8ffa33 --- /dev/null +++ b/src/data/sources/defaults.rs @@ -0,0 +1,107 @@ +// This file is part of GAnarchy - decentralized development hub +// Copyright (C) 2021 Soni L. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Data source for compile-time defaults. + +use std::collections::BTreeSet; +use std::error; +use std::time::Duration; + +use impl_trait::impl_trait; + +use super::super::DataSource; +use super::super::DataSourceBase; +use super::super::effective::EffectiveKind; +use super::super::kinds::{ + InstanceBaseUrl, + InstanceTitle, + ProjectFork, + RepoListUrl, +}; + +/// Data source that provides compile-time defaults. +/// +/// # Examples +/// +/// Constructing a `DefaultsDataSource`: +/// +/// ```rust +/// use ganarchy::data::sources::DefaultsDataSource; +/// +/// let x = DefaultsDataSource; +/// # let _ = x; +/// ``` +#[derive(Copy, Clone, Debug, Default)] +pub struct DefaultsDataSource; + +impl_trait! { + impl DefaultsDataSource { + /// Always updates successfully, with an unknown refresh interval. + impl trait DataSourceBase { + fn update(&mut self) -> ( + Option<Duration>, + Result<(), Box<dyn error::Error + Send + Sync + 'static>>, + ) { + (None, Ok(())) + } + + fn exists(&self) -> bool { + true + } + } + + impl trait std::fmt::Display { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "compile-time defaults") + } + } + + /// Default [`InstanceTitle`] source. Always `None`. + impl trait DataSource<InstanceTitle> { + fn get_values(&self) -> Option<InstanceTitle> { + None + } + } + + /// Default [`InstanceBaseUrl`] source. Always `None`. + impl trait DataSource<InstanceBaseUrl> { + fn get_values(&self) -> Option<InstanceBaseUrl> { + None + } + } + + /// Default [`RepoListUrl`] source. + impl trait DataSource<RepoListUrl> { + fn get_values(&self) -> Vec<RepoListUrl> { + // Forks may wish to add stuff here. + vec![ + ] + } + } + + /// Default [`ProjectFork`] source. + impl trait DataSource<EffectiveKind<ProjectFork>> { + fn get_values(&self) -> BTreeSet<EffectiveKind<ProjectFork>> { + // Forks may wish to add stuff here. + // In particular, as this DataSource is a kind of config, + // these override external repo lists (but not local config), + // including for setting certain repos to off. + vec![ + ].into_iter().collect() + } + } + } +} diff --git a/src/data/tests.rs b/src/data/tests.rs new file mode 100644 index 0000000..9608afd --- /dev/null +++ b/src/data/tests.rs @@ -0,0 +1,19 @@ +// This file is part of GAnarchy - decentralized development hub +// Copyright (C) 2021 Soni L. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Unit tests for the data module. + +// TODO |