Skip to main content

coil_storage/policy/
rules.rs

1use coil_config::StorageClass;
2use thiserror::Error;
3
4use super::{PathPolicyKind, StoragePolicy, StoragePolicyOverride};
5use crate::policy::paths::{normalize_relative_path, normalize_rule_prefix};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct PathPolicyRule {
9    pub kind: PathPolicyKind,
10    pub path_prefix: String,
11    pub storage_class: Option<StorageClass>,
12    pub policy: StoragePolicy,
13    pub object_prefix: Option<String>,
14    pub local_subdir: Option<String>,
15}
16
17impl PathPolicyRule {
18    pub fn new(
19        path_prefix: impl Into<String>,
20        storage_class: Option<StorageClass>,
21        policy: StoragePolicy,
22    ) -> Result<Self, StoragePolicyError> {
23        Self::folder(path_prefix, storage_class, policy)
24    }
25
26    pub fn folder(
27        path_prefix: impl Into<String>,
28        storage_class: Option<StorageClass>,
29        policy: StoragePolicy,
30    ) -> Result<Self, StoragePolicyError> {
31        let path_prefix = normalize_rule_prefix(&path_prefix.into())?;
32        policy.validate()?;
33        Ok(Self {
34            kind: PathPolicyKind::Folder,
35            path_prefix,
36            storage_class,
37            policy,
38            object_prefix: None,
39            local_subdir: None,
40        })
41    }
42
43    pub fn upload(
44        path_prefix: impl Into<String>,
45        storage_class: Option<StorageClass>,
46        policy: StoragePolicy,
47    ) -> Result<Self, StoragePolicyError> {
48        let path_prefix = normalize_relative_path(&path_prefix.into())?;
49        policy.validate()?;
50        Ok(Self {
51            kind: PathPolicyKind::Upload,
52            path_prefix,
53            storage_class,
54            policy,
55            object_prefix: None,
56            local_subdir: None,
57        })
58    }
59
60    pub fn with_object_prefix(
61        mut self,
62        prefix: impl Into<String>,
63    ) -> Result<Self, StoragePolicyError> {
64        self.object_prefix = Some(normalize_rule_prefix(&prefix.into())?);
65        Ok(self)
66    }
67
68    pub fn with_local_subdir(
69        mut self,
70        subdir: impl Into<String>,
71    ) -> Result<Self, StoragePolicyError> {
72        self.local_subdir = Some(normalize_rule_prefix(&subdir.into())?);
73        Ok(self)
74    }
75
76    pub(crate) fn matches(&self, logical_path: &str) -> bool {
77        match self.kind {
78            PathPolicyKind::Folder => {
79                self.path_prefix.is_empty()
80                    || logical_path == self.path_prefix
81                    || logical_path.starts_with(&format!("{}/", self.path_prefix))
82            }
83            PathPolicyKind::Upload => logical_path == self.path_prefix,
84        }
85    }
86
87    pub(crate) fn specificity(&self) -> (u8, usize) {
88        match self.kind {
89            PathPolicyKind::Upload => (2, self.path_prefix.len()),
90            PathPolicyKind::Folder => (1, self.path_prefix.len()),
91        }
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct StoragePolicyGraph {
97    rules: Vec<PathPolicyRule>,
98}
99
100impl StoragePolicyGraph {
101    pub fn new() -> Self {
102        Self { rules: Vec::new() }
103    }
104
105    pub fn with_rule(mut self, rule: PathPolicyRule) -> Self {
106        self.rules.push(rule);
107        self.rules
108            .sort_by(|left, right| right.specificity().cmp(&left.specificity()));
109        self
110    }
111
112    pub fn with_folder_rule(self, rule: PathPolicyRule) -> Self {
113        debug_assert!(matches!(rule.kind, PathPolicyKind::Folder));
114        self.with_rule(rule)
115    }
116
117    pub fn with_upload_rule(self, rule: PathPolicyRule) -> Self {
118        debug_assert!(matches!(rule.kind, PathPolicyKind::Upload));
119        self.with_rule(rule)
120    }
121
122    pub fn rules(&self) -> &[PathPolicyRule] {
123        &self.rules
124    }
125
126    pub fn resolve(
127        &self,
128        storage_class: StorageClass,
129        logical_path: &str,
130        override_policy: Option<&StoragePolicyOverride>,
131    ) -> Result<ResolvedStoragePolicy, StoragePolicyError> {
132        let logical_path = normalize_relative_path(logical_path)?;
133        let matched_rule = self.rules.iter().find(|rule| rule.matches(&logical_path));
134
135        let derived_class = matched_rule
136            .and_then(|rule| rule.storage_class)
137            .unwrap_or(storage_class);
138
139        let base_policy = matched_rule
140            .map(|rule| rule.policy)
141            .unwrap_or_else(|| derived_class.into());
142        let policy = override_policy
143            .map(|policy_override| policy_override.apply_to(base_policy))
144            .unwrap_or(base_policy);
145        policy.validate()?;
146
147        Ok(ResolvedStoragePolicy {
148            storage_class: derived_class,
149            policy,
150            matched_rule_prefix: matched_rule.map(|rule| rule.path_prefix.clone()),
151            matched_rule_kind: matched_rule.map(|rule| rule.kind),
152            object_prefix: matched_rule.and_then(|rule| rule.object_prefix.clone()),
153            local_subdir: matched_rule.and_then(|rule| rule.local_subdir.clone()),
154        })
155    }
156}
157
158impl Default for StoragePolicyGraph {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct ResolvedStoragePolicy {
166    pub storage_class: StorageClass,
167    pub policy: StoragePolicy,
168    pub matched_rule_prefix: Option<String>,
169    pub matched_rule_kind: Option<PathPolicyKind>,
170    pub object_prefix: Option<String>,
171    pub local_subdir: Option<String>,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct StoragePolicySet {
176    graph: StoragePolicyGraph,
177}
178
179impl StoragePolicySet {
180    pub fn new() -> Self {
181        Self {
182            graph: StoragePolicyGraph::new(),
183        }
184    }
185
186    pub fn with_rule(mut self, rule: PathPolicyRule) -> Self {
187        self.graph = self.graph.with_rule(rule);
188        self
189    }
190
191    pub fn with_folder_rule(mut self, rule: PathPolicyRule) -> Self {
192        self.graph = self.graph.with_folder_rule(rule);
193        self
194    }
195
196    pub fn with_upload_rule(mut self, rule: PathPolicyRule) -> Self {
197        self.graph = self.graph.with_upload_rule(rule);
198        self
199    }
200
201    pub fn graph(&self) -> &StoragePolicyGraph {
202        &self.graph
203    }
204
205    pub fn resolve(
206        &self,
207        storage_class: StorageClass,
208        logical_path: &str,
209        override_policy: Option<&StoragePolicyOverride>,
210    ) -> Result<ResolvedStoragePolicy, StoragePolicyError> {
211        self.graph
212            .resolve(storage_class, logical_path, override_policy)
213    }
214}
215
216impl Default for StoragePolicySet {
217    fn default() -> Self {
218        Self::new()
219    }
220}
221
222#[derive(Debug, Error, PartialEq, Eq)]
223pub enum StoragePolicyError {
224    #[error("storage policy contains an invalid combination: {detail}")]
225    InvalidCombination { detail: String },
226    #[error("storage paths must be relative and non-empty, got `{path}`")]
227    InvalidRelativePath { path: String },
228    #[error("storage paths cannot traverse parent segments, got `{path}`")]
229    ParentTraversal { path: String },
230}