ic_backup/policy/selection/
mod.rs1use crate::model::{
4 artifacts::ArtifactChecksumRecord,
5 inventory::{
6 InventoryRecord, InventoryRecordError, InventoryTargetRecord, MAX_INVENTORY_TARGETS,
7 },
8};
9use std::collections::{BTreeMap, BTreeSet, VecDeque};
10use thiserror::Error;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum SelectionExpansion {
15 Exact,
17 DirectChildren,
19 Descendants,
21}
22
23#[derive(Clone, Debug)]
25pub struct SelectionRequest {
26 pub canister_ids: Vec<String>,
28 pub expansion: SelectionExpansion,
30}
31
32#[derive(Clone, Debug)]
34pub struct SelectionView<'a> {
35 pub inventory: ArtifactChecksumRecord,
37 pub targets: Vec<&'a InventoryTargetRecord>,
41}
42
43pub fn select<'a>(
48 inventory: &'a InventoryRecord,
49 request: &SelectionRequest,
50) -> Result<SelectionView<'a>, SelectionError> {
51 if request.canister_ids.is_empty() {
52 return Err(SelectionError::EmptySelection);
53 }
54 if request.canister_ids.len() > MAX_INVENTORY_TARGETS {
55 return Err(SelectionError::TooManySelectors);
56 }
57 let mut selected = BTreeSet::new();
58 let mut queue = VecDeque::new();
59 for id in &request.canister_ids {
60 let target = inventory.target(id)?;
61 if !selected.insert(target.canister_id()) {
62 return Err(SelectionError::DuplicateSelector(
63 target.canister_id().into(),
64 ));
65 }
66 queue.push_back(target.canister_id());
67 }
68 if request.expansion != SelectionExpansion::Exact {
69 let mut children: BTreeMap<_, Vec<_>> = BTreeMap::new();
70 for target in inventory.targets() {
71 if let Some(parent) = target.parent_canister_id() {
72 children
73 .entry(parent)
74 .or_default()
75 .push(target.canister_id());
76 }
77 }
78 while let Some(parent) = queue.pop_front() {
79 for child in children.get(parent).into_iter().flatten() {
80 if selected.insert(*child) && request.expansion == SelectionExpansion::Descendants {
81 queue.push_back(*child);
82 }
83 }
84 }
85 }
86 Ok(SelectionView {
87 inventory: inventory.digest(),
88 targets: inventory
89 .targets()
90 .iter()
91 .filter(|target| selected.contains(target.canister_id()))
92 .collect(),
93 })
94}
95
96#[derive(Debug, Error)]
98pub enum SelectionError {
99 #[error("selection contains no targets")]
101 EmptySelection,
102 #[error("selection exceeds {MAX_INVENTORY_TARGETS} selectors")]
104 TooManySelectors,
105 #[error("duplicate physical selector {0}")]
107 DuplicateSelector(String),
108 #[error(transparent)]
110 Inventory(#[from] InventoryRecordError),
111}
112
113#[cfg(test)]
114mod tests;