summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorSoniEx2 <endermoneymod@gmail.com>2021-09-13 23:08:19 -0300
committerSoniEx2 <endermoneymod@gmail.com>2021-09-13 23:08:19 -0300
commit22e7a62e9529cc4a59941e8342f69d0f6ded60f9 (patch)
tree5f2301c2e270b452fef08ed116b0d351a07c972c
parent4ba9626f711805d99c447a862682b568ad6e1e70 (diff)
Mostly finish data source interactions
-rw-r--r--Cargo.lock12
-rw-r--r--Cargo.toml1
-rw-r--r--src/data.rs39
-rw-r--r--src/data/effective.rs36
-rw-r--r--src/data/effective/tests.rs108
-rw-r--r--src/data/kinds.rs156
-rw-r--r--src/data/managers.rs457
-rw-r--r--src/data/sources.rs21
-rw-r--r--src/data/sources/defaults.rs107
-rw-r--r--src/data/tests.rs19
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