diff options
Diffstat (limited to 'src/data/managers.rs')
-rw-r--r-- | src/data/managers.rs | 457 |
1 files changed, 427 insertions, 30 deletions
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() } } |