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}