summary refs log tree commit diff stats
path: root/src/data/managers.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/managers.rs')
-rw-r--r--src/data/managers.rs457
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()
     }
 }