1use std::collections::BTreeMap;
12
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::config::builder::envtrie::{EnvTrie, Error as TrieError};
17use crate::env_vars::{Error as EnvError, PathWithEnv};
18use crate::hoard::PileConfig;
19use crate::newtypes::{EnvironmentName, EnvironmentString, NonEmptyPileName};
20
21type ConfigMultiple = crate::config::hoard::MultipleEntries;
22type ConfigSingle = crate::config::hoard::Pile;
23type ConfigHoard = crate::config::hoard::Hoard;
24
25#[derive(Debug, Error)]
28pub enum Error {
29 #[error("error while processing environment requirements: {0}")]
31 EnvTrie(#[from] TrieError),
32 #[error("error while expanding environment variables in path: {0}")]
34 ExpandEnv(#[from] EnvError),
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
39pub struct Pile {
40 pub config: Option<PileConfig>,
45 #[serde(flatten)]
49 pub items: BTreeMap<EnvironmentString, PathWithEnv>,
50}
51
52impl Pile {
53 #[tracing::instrument(level = "debug", name = "process_pile")]
54 fn process_with(
55 self,
56 envs: &BTreeMap<EnvironmentName, bool>,
57 exclusivity: &[Vec<EnvironmentName>],
58 ) -> Result<ConfigSingle, Error> {
59 let Pile { config, items } = self;
60 let trie = EnvTrie::new(&items, exclusivity)?;
61 let path = trie
62 .get_path(envs)?
63 .cloned()
64 .map(PathWithEnv::process)
65 .transpose()?;
66
67 Ok(ConfigSingle {
68 config: config.unwrap_or_default(),
69 path,
70 })
71 }
72
73 pub(crate) fn layer_config(&mut self, config: Option<&PileConfig>) {
74 PileConfig::layer_options(&mut self.config, config);
75 }
76}
77
78#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
80pub struct MultipleEntries {
81 pub config: Option<PileConfig>,
85 #[serde(flatten)]
87 pub items: BTreeMap<NonEmptyPileName, Pile>,
88}
89
90impl MultipleEntries {
91 #[tracing::instrument(level = "debug", name = "process_multiple_entries")]
92 fn process_with(
93 self,
94 envs: &BTreeMap<EnvironmentName, bool>,
95 exclusivity: &[Vec<EnvironmentName>],
96 ) -> Result<ConfigMultiple, super::Error> {
97 let MultipleEntries { config, items } = self;
98 let items = items
99 .into_iter()
100 .map(|(pile, mut entry)| {
101 tracing::debug!(%pile, "processing pile");
102 entry.layer_config(config.as_ref());
103 let entry = entry.process_with(envs, exclusivity).map_err(Error::from)?;
104 Ok((pile, entry))
105 })
106 .collect::<Result<_, super::Error>>()?;
107
108 Ok(ConfigMultiple { piles: items })
109 }
110
111 pub(crate) fn layer_config(&mut self, config: Option<&PileConfig>) {
112 PileConfig::layer_options(&mut self.config, config);
113 }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(untagged)]
119pub enum Hoard {
120 Single(Pile),
122 Multiple(MultipleEntries),
124}
125
126impl Hoard {
127 #[tracing::instrument(level = "debug", name = "process_hoard")]
136 pub fn process_with(
137 self,
138 envs: &BTreeMap<EnvironmentName, bool>,
139 exclusivity: &[Vec<EnvironmentName>],
140 ) -> Result<crate::config::hoard::Hoard, super::Error> {
141 match self {
142 Hoard::Single(single) => {
143 tracing::debug!("processing anonymous pile");
144 Ok(ConfigHoard::Anonymous(
145 single
146 .process_with(envs, exclusivity)
147 .map_err(super::Error::from)?,
148 ))
149 }
150 Hoard::Multiple(multiple) => {
151 tracing::debug!("processing named pile(s)");
152 Ok(ConfigHoard::Named(
153 multiple.process_with(envs, exclusivity)?,
154 ))
155 }
156 }
157 }
158
159 pub(crate) fn layer_config(&mut self, config: Option<&PileConfig>) {
160 match self {
161 Hoard::Single(pile) => pile.layer_config(config),
162 Hoard::Multiple(multi) => multi.layer_config(config),
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use crate::hoard::pile_config::{
170 AsymmetricEncryption, Config as PileConfig, Encryption, SymmetricEncryption,
171 };
172
173 use super::*;
174
175 mod process {
176 use std::path::PathBuf;
177
178 use maplit::btreemap;
179
180 use crate::hoard::Pile as RealPile;
181 use crate::paths::SystemPath;
182
183 use super::*;
184
185 #[test]
186 fn env_vars_are_expanded() {
187 let pile = Pile {
188 config: None,
189 #[cfg(unix)]
190 items: btreemap! {
191 "foo".parse().unwrap() => "${HOME}/something".into()
192 },
193 #[cfg(windows)]
194 items: btreemap! {
195 "foo".parse().unwrap() => "${USERPROFILE}/something".into()
196 },
197 };
198
199 #[cfg(unix)]
200 let home = std::env::var("HOME").expect("failed to read $HOME");
201 #[cfg(windows)]
202 let home = std::env::var("USERPROFILE").expect("failed to read $USERPROFILE");
203 let expected = RealPile {
204 config: PileConfig::default(),
205 path: Some(
206 SystemPath::try_from(PathBuf::from(format!("{home}/something"))).unwrap(),
207 ),
208 };
209
210 let envs = btreemap! { "foo".parse().unwrap() => true };
211 let result = pile
212 .process_with(&envs, &[])
213 .expect("pile should process without issues");
214
215 assert_eq!(result, expected);
216 }
217 }
218
219 mod serde {
220 use maplit::btreemap;
221 use serde_test::{assert_de_tokens_error, assert_tokens, Token};
222
223 use super::*;
224
225 #[test]
226 fn single_entry_no_config() {
227 let hoard = Hoard::Single(Pile {
228 config: None,
229 items: btreemap! {
230 "bar_env|foo_env".parse().unwrap() => "/some/path".into()
231 },
232 });
233
234 assert_tokens(
235 &hoard,
236 &[
237 Token::Map { len: None },
238 Token::Str("config"),
239 Token::None,
240 Token::Str("bar_env|foo_env"),
241 Token::Str("/some/path"),
242 Token::MapEnd,
243 ],
244 );
245 }
246
247 #[test]
248 fn single_entry_with_config() {
249 let hoard = Hoard::Single(Pile {
250 config: Some(PileConfig {
251 encryption: Some(Encryption::Asymmetric(AsymmetricEncryption {
252 public_key: "public key".to_string(),
253 })),
254 ..PileConfig::default()
255 }),
256 items: btreemap! {
257 "bar_env|foo_env".parse().unwrap() => "/some/path".into()
258 },
259 });
260
261 assert_tokens(
262 &hoard,
263 &[
264 Token::Map { len: None },
265 Token::Str("config"),
266 Token::Some,
267 Token::Struct {
268 name: "Config",
269 len: 5,
270 },
271 Token::Str("hash_algorithm"),
272 Token::None,
273 Token::Str("encrypt"),
274 Token::Some,
275 Token::Struct {
276 name: "AsymmetricEncryption",
277 len: 2,
278 },
279 Token::Str("type"),
280 Token::Str("asymmetric"),
281 Token::Str("public_key"),
282 Token::Str("public key"),
283 Token::StructEnd,
284 Token::Str("ignore"),
285 Token::Seq { len: Some(0) },
286 Token::SeqEnd,
287 Token::Str("file_permissions"),
288 Token::None,
289 Token::Str("folder_permissions"),
290 Token::None,
291 Token::StructEnd,
292 Token::Str("bar_env|foo_env"),
293 Token::Str("/some/path"),
294 Token::MapEnd,
295 ],
296 );
297 }
298
299 #[test]
300 fn multiple_entry_no_config() {
301 let hoard = Hoard::Multiple(MultipleEntries {
302 config: None,
303 items: btreemap! {
304 "item1".parse().unwrap() => Pile {
305 config: None,
306 items: btreemap! {
307 "bar_env|foo_env".parse().unwrap() => "/some/path".into()
308 }
309 },
310 },
311 });
312
313 assert_tokens(
314 &hoard,
315 &[
316 Token::Map { len: None },
317 Token::Str("config"),
318 Token::None,
319 Token::Str("item1"),
320 Token::Map { len: None },
321 Token::Str("config"),
322 Token::None,
323 Token::Str("bar_env|foo_env"),
324 Token::Str("/some/path"),
325 Token::MapEnd,
326 Token::MapEnd,
327 ],
328 );
329 }
330
331 #[test]
332 fn multiple_entry_with_config() {
333 let hoard = Hoard::Multiple(MultipleEntries {
334 config: Some(PileConfig {
335 encryption: Some(Encryption::Symmetric(SymmetricEncryption::Password(
336 "correcthorsebatterystaple".into(),
337 ))),
338 ..PileConfig::default()
339 }),
340 items: btreemap! {
341 "item1".parse().unwrap() => Pile {
342 config: None,
343 items: btreemap! {
344 "bar_env|foo_env".parse().unwrap() => "/some/path".into()
345 }
346 },
347 },
348 });
349
350 assert_tokens(
351 &hoard,
352 &[
353 Token::Map { len: None },
354 Token::Str("config"),
355 Token::Some,
356 Token::Struct {
357 name: "Config",
358 len: 5,
359 },
360 Token::Str("hash_algorithm"),
361 Token::None,
362 Token::Str("encrypt"),
363 Token::Some,
364 Token::Map { len: Some(2) },
365 Token::Str("type"),
366 Token::Str("symmetric"),
367 Token::Str("password"),
368 Token::Str("correcthorsebatterystaple"),
369 Token::MapEnd,
370 Token::Str("ignore"),
371 Token::Seq { len: Some(0) },
372 Token::SeqEnd,
373 Token::Str("file_permissions"),
374 Token::None,
375 Token::Str("folder_permissions"),
376 Token::None,
377 Token::StructEnd,
378 Token::Str("item1"),
379 Token::Map { len: None },
380 Token::Str("config"),
381 Token::None,
382 Token::Str("bar_env|foo_env"),
383 Token::Str("/some/path"),
384 Token::MapEnd,
385 Token::MapEnd,
386 ],
387 );
388 }
389
390 #[test]
391 fn test_invalid_glob() {
392 assert_de_tokens_error::<PileConfig>(
393 &[
394 Token::Struct {
395 name: "Config",
396 len: 5,
397 },
398 Token::Str("hash_algorithm"),
399 Token::None,
400 Token::Str("encrypt"),
401 Token::None,
402 Token::Str("file_permissions"),
403 Token::None,
404 Token::Str("folder_permissions"),
405 Token::None,
406 Token::Str("ignore"),
407 Token::Seq { len: Some(2) },
408 Token::Str("**/valid*"),
409 Token::Str("invalid**"),
410 Token::SeqEnd,
411 Token::StructEnd,
412 ],
413 "Pattern syntax error near position 6: recursive wildcards must form a single path component",
414 );
415 }
416
417 #[test]
418 fn test_valid_globs() {
419 let config = PileConfig {
420 ignore: vec![
421 glob::Pattern::new("**/valid*").unwrap(),
422 glob::Pattern::new("*/also_valid/**").unwrap(),
423 ],
424 ..PileConfig::default()
425 };
426
427 assert_tokens::<PileConfig>(
428 &config,
429 &[
430 Token::Struct {
431 name: "Config",
432 len: 5,
433 },
434 Token::Str("hash_algorithm"),
435 Token::None,
436 Token::Str("encrypt"),
437 Token::None,
438 Token::Str("ignore"),
439 Token::Seq { len: Some(2) },
440 Token::Str("**/valid*"),
441 Token::Str("*/also_valid/**"),
442 Token::SeqEnd,
443 Token::Str("file_permissions"),
444 Token::None,
445 Token::Str("folder_permissions"),
446 Token::None,
447 Token::StructEnd,
448 ],
449 );
450 }
451 }
452}