grass/dev/strategy/
discovery.rs

1mod local;
2mod mock;
3
4use thiserror::Error;
5
6use crate::{dev::public::api::RepositoryLocation, support_strategy};
7
8pub use local::LocalDiscoveryStrategy;
9pub use mock::MockDiscoveryStrategy;
10
11use super::alias::AliasStrategyError;
12
13/// Error returned by methods of [DiscoveryStrategy]
14///
15/// Each variant has 2 fields:
16///
17/// - `context`: What action was attempted.
18/// - `reason`: What went wrong, often this is provided by errors generated from crates.
19///
20/// See: [crate::dev::strategy::discovery::DiscoveryStrategy]
21#[derive(Error, Debug, PartialEq, Eq, Hash)]
22pub enum DiscoveryStrategyError {
23    #[error("Cannot find repository:\nContext: {context}\nReason: {reason}")]
24    CategoryNotFound { context: String, reason: String },
25    #[error("There is a problem accessing the file system:\nContext: {context}\nReason: {reason}")]
26    FilesystemError { context: String, reason: String },
27    #[error("There is a problem:\nContext: {context}\nReason: {reason}")]
28    UnknownError { context: String, reason: String },
29    #[error("Repository already exists:\nContext: {context}\nReason: {reason}")]
30    RepositoryExists { context: Box<str>, reason: Box<str> },
31    #[error("Repository does not exist:\nContext: {context}\nReason: {reason}")]
32    RepositoryDoesNotExist { context: Box<str>, reason: Box<str> },
33}
34
35/// Methods of [DiscoveryStrategy] return this alias
36///
37/// See: [crate::dev::strategy::discovery::DiscoveryStrategy]
38pub type Result<T> = std::result::Result<T, DiscoveryStrategyError>;
39
40/// Indicates wheter a respository/category exists
41#[derive(Debug, PartialEq, Eq)]
42pub enum DiscoveryExists {
43    Exists,
44    /// The category exists, but the repository does not
45    RepositoryNotFound,
46    CategoryNotFound,
47}
48
49pub type BoxedIterator<'a, T> = Box<dyn Iterator<Item = T> + 'a>;
50
51/// Strategy for finding repositories and categories
52///
53/// The exact behavior of this strategy can vary.
54/// By design, this concept is abstract.
55/// The goal of the discovery strategy, is to provide a way to discover which options there are.
56///
57/// In the case of [LocalDiscoveryStrategy][^local], 2 things are considered:
58///
59/// - The provided configuration.
60/// - Folders in the file system.
61///
62/// # See
63///
64/// [crate::dev::strategy::discovery::SupportsDiscovery]
65///
66/// [^local]: [crate::dev::strategy::discovery::LocalDiscoveryStrategy]
67pub trait DiscoveryStrategy {
68    ///
69    /// # Return
70    ///
71    /// | Variant                               | Meaning                                         |
72    /// | :------------------------------------ | :---------------------------------------------- |
73    /// | [DiscoveryExists::Exists]             | The repository exists                           |
74    /// | [DiscoveryExists::RepositoryNotFound] | The category exists, but the repository doesn't |
75    /// | [DiscoveryExists::CategoryNotFound]   | The category doesn't exist                      |
76    ///
77    /// # Example
78    ///
79    /// ```rust
80    /// # use grass::dev::strategy::discovery::{
81    ///     DiscoveryStrategy, MockDiscoveryStrategy, DiscoveryExists
82    /// };
83    /// # let strategy = MockDiscoveryStrategy;
84    /// fn test_strategy<T: DiscoveryStrategy>(strategy: &T) {
85    ///     assert_eq!(
86    ///         strategy.check_repository_exists(("all_good", "first")),
87    ///         Ok(DiscoveryExists::Exists),
88    ///     );
89    ///
90    ///     assert_eq!(
91    ///         strategy.check_repository_exists(("all_good", "missing")),
92    ///         Ok(DiscoveryExists::RepositoryNotFound),
93    ///     );
94    ///
95    ///     assert_eq!(
96    ///         strategy.check_repository_exists(("missing", "first")),
97    ///         Ok(DiscoveryExists::CategoryNotFound),
98    ///     );
99    /// }
100    ///
101    /// test_strategy(&strategy);
102    /// ```
103    fn check_repository_exists<T>(&self, repository: T) -> Result<DiscoveryExists>
104    where
105        T: Into<RepositoryLocation>;
106
107    ///
108    /// # Return
109    ///
110    /// | Variant                             | Meaning                    |
111    /// | :---------------------------------- | :------------------------- |
112    /// | [DiscoveryExists::Exists]           | The repository exists      |
113    /// | [DiscoveryExists::CategoryNotFound] | The category doesn't exist |
114    ///
115    /// # Example
116    ///
117    /// ```rust
118    /// # use grass::dev::strategy::discovery::{
119    ///     DiscoveryStrategy, MockDiscoveryStrategy, DiscoveryExists
120    /// };
121    /// # let strategy = MockDiscoveryStrategy;
122    /// fn test_strategy<T: DiscoveryStrategy>(strategy: &T) {
123    ///     assert_eq!(
124    ///         strategy.check_category_exists("all_good"),
125    ///         Ok(DiscoveryExists::Exists),
126    ///     );
127    ///
128    ///     assert_eq!(
129    ///         strategy.check_category_exists("missing"),
130    ///         Ok(DiscoveryExists::CategoryNotFound),
131    ///     );
132    /// }
133    ///
134    /// test_strategy(&strategy);
135    /// ```
136    fn check_category_exists<T>(&self, category: T) -> Result<DiscoveryExists>
137    where
138        T: AsRef<str>;
139
140    /// Get an iterator over all repositories in a category
141    ///
142    /// Discovery depends on the implementation.
143    /// In the case of [LocalDiscoveryStrategy][^1], this means all folders in a specified folder.
144    ///
145    /// The result will be an iterator over the objects using a `Result`.
146    /// Individual repositories may fail for one reason or another.
147    /// You are expected to filter these out if you want to ignore them.
148    ///
149    /// # Example:
150    ///
151    /// ```rust
152    /// # use grass::dev::strategy::discovery::{
153    ///     DiscoveryStrategy, MockDiscoveryStrategy, DiscoveryStrategyError
154    /// };
155    /// # use grass::dev::RepositoryLocation;
156    /// # let strategy = MockDiscoveryStrategy;
157    /// fn test_strategy<T: DiscoveryStrategy>(strategy: &T) {
158    ///     let list_repositories_result: Vec<_> = strategy
159    ///         .list_repositories_in_category("all_good")
160    ///         .unwrap()
161    ///         .collect();
162    ///
163    ///     assert_eq!(
164    ///         list_repositories_result,
165    ///         vec![
166    ///             Ok(RepositoryLocation {
167    ///                 category: "all_good".into(),
168    ///                 repository: "first".into(),
169    ///             }),
170    ///             Ok(RepositoryLocation {
171    ///                 category: "all_good".into(),
172    ///                 repository: "second".into(),
173    ///             }),
174    ///             Ok(RepositoryLocation {
175    ///                 category: "all_good".into(),
176    ///                 repository: "third".into(),
177    ///             }),
178    ///         ],
179    ///     );
180    ///
181    ///     assert!(matches!(
182    ///         strategy.list_repositories_in_category("missing"),
183    ///         Err(DiscoveryStrategyError::CategoryNotFound{..}),
184    ///     ));
185    /// }
186    ///
187    /// test_strategy(&strategy);
188    /// ```
189    ///
190    /// [^1]: [crate::dev::strategy::discovery::LocalDiscoveryStrategy]
191    fn list_repositories_in_category<T>(
192        &self,
193        category: T,
194    ) -> Result<BoxedIterator<Result<RepositoryLocation>>>
195    where
196        T: AsRef<str>;
197
198    /// Retrieve a list of all categories
199    ///
200    /// The implementation can differ per strategy.
201    /// In the case of [LocalDiscoveryStrategy][^1], a configuration file is used.
202    ///
203    /// # Example
204    ///
205    /// ```rust
206    /// # use grass::dev::strategy::discovery::{
207    /// # DiscoveryStrategy, MockDiscoveryStrategy, DiscoveryStrategyError
208    /// # };
209    /// # let strategy = MockDiscoveryStrategy;
210    /// fn test_strategy<T: DiscoveryStrategy>(strategy: &T) {
211    ///     let list_categories_result: Vec<_> = strategy
212    ///         .list_categories()
213    ///         .unwrap();
214    ///
215    ///     assert_eq!(
216    ///         list_categories_result,
217    ///         vec!["all_good", "with_changes", "with_error"],
218    ///     )
219    /// }
220    ///
221    /// test_strategy(&strategy)
222    /// ```
223    ///
224    /// [^1]: [crate::dev::strategy::discovery::LocalDiscoveryStrategy]
225    fn list_categories<T>(&self) -> Result<T>
226    where
227        T: FromIterator<String>;
228
229    fn create_repository(&self, location: RepositoryLocation) -> Result<()>;
230
231    fn move_repository(
232        &self,
233        old_location: RepositoryLocation,
234        new_location: RepositoryLocation,
235    ) -> Result<()>;
236}
237
238support_strategy!(SupportsDiscovery, get_discovery_strategy, DiscoveryStrategy);
239
240impl From<AliasStrategyError> for DiscoveryStrategyError {
241    fn from(value: AliasStrategyError) -> Self {
242        match value {
243            AliasStrategyError::UnkownError { context, reason } => {
244                DiscoveryStrategyError::UnknownError { context, reason }
245            }
246            AliasStrategyError::CategoryNotFound { context, reason } => {
247                DiscoveryStrategyError::CategoryNotFound { context, reason }
248            }
249        }
250    }
251}
252
253impl From<std::io::Error> for DiscoveryStrategyError {
254    fn from(value: std::io::Error) -> Self {
255        Self::FilesystemError {
256            context: "".into(),
257            reason: value.to_string(),
258        }
259    }
260}