summary refs log blame commit diff stats
path: root/src/data/managers.rs
blob: 9a82a8ebf61ca3f55145e0b53a256facb8eeda00 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16















                                                                              





                                                                         
               
             
                   


                           
                               


                          

                                          
                                    
                                                                             
                                       
 




                                                                              


                                                                              
                                     


                 










                                                                              

                          


                                     



                                             

































                                                                            
             


























                                                                       



                              










                                                                    

                                                  


















































































































































                                                                              
         





                                                                









                                                                             












                                                            
                                     
                                 

                                                                          


























                                                                             


                                                                           










                                                            
                                      


































                                                                            

             



















































































                                                                             

     
// 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 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::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::kinds::{InstanceTitle, InstanceBaseUrl, RepoListUrl, ProjectFork};
use super::sources::DefaultsDataSource;

/// 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<Option<Duration>>,
    valid: usize,
}

/// 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 {
    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 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 [`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<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(QCell::new(&self.owner, source));
            self.resources.push(Resource::new(arc.clone()));
            AddConfigSource {
                resource: self.resources.last_mut().unwrap(),
                source: arc,
            }
        }

        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::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) -> (
                Option<Duration>,
                Result<(), Box<dyn error::Error + Send + Sync + 'static>>,
            ) {
                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 {
                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()
    }
}