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}