object_store/aws/
precondition.rs1use crate::aws::dynamo::DynamoCommit;
19use crate::config::Parse;
20
21use itertools::Itertools;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum S3CopyIfNotExists {
30 Header(String, String),
44 HeaderWithStatus(String, String, reqwest::StatusCode),
49 Dynamo(DynamoCommit),
58}
59
60impl std::fmt::Display for S3CopyIfNotExists {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Self::Header(k, v) => write!(f, "header: {}: {}", k, v),
64 Self::HeaderWithStatus(k, v, code) => {
65 write!(f, "header-with-status: {k}: {v}: {}", code.as_u16())
66 }
67 Self::Dynamo(lock) => write!(f, "dynamo: {}", lock.table_name()),
68 }
69 }
70}
71
72impl S3CopyIfNotExists {
73 fn from_str(s: &str) -> Option<Self> {
74 let (variant, value) = s.split_once(':')?;
75 match variant.trim() {
76 "header" => {
77 let (k, v) = value.split_once(':')?;
78 Some(Self::Header(k.trim().to_string(), v.trim().to_string()))
79 }
80 "header-with-status" => {
81 let (k, v, status) = value.split(':').collect_tuple()?;
82
83 let code = status.trim().parse().ok()?;
84
85 Some(Self::HeaderWithStatus(
86 k.trim().to_string(),
87 v.trim().to_string(),
88 code,
89 ))
90 }
91 "dynamo" => Some(Self::Dynamo(DynamoCommit::from_str(value)?)),
92 _ => None,
93 }
94 }
95}
96
97impl Parse for S3CopyIfNotExists {
98 fn parse(v: &str) -> crate::Result<Self> {
99 Self::from_str(v).ok_or_else(|| crate::Error::Generic {
100 store: "Config",
101 source: format!("Failed to parse \"{v}\" as S3CopyIfNotExists").into(),
102 })
103 }
104}
105
106#[derive(Debug, Clone, Eq, PartialEq)]
110#[allow(missing_copy_implementations)]
111#[non_exhaustive]
112pub enum S3ConditionalPut {
113 ETagMatch,
120
121 Dynamo(DynamoCommit),
130}
131
132impl std::fmt::Display for S3ConditionalPut {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self {
135 Self::ETagMatch => write!(f, "etag"),
136 Self::Dynamo(lock) => write!(f, "dynamo: {}", lock.table_name()),
137 }
138 }
139}
140
141impl S3ConditionalPut {
142 fn from_str(s: &str) -> Option<Self> {
143 match s.trim() {
144 "etag" => Some(Self::ETagMatch),
145 trimmed => match trimmed.split_once(':')? {
146 ("dynamo", s) => Some(Self::Dynamo(DynamoCommit::from_str(s)?)),
147 _ => None,
148 },
149 }
150 }
151}
152
153impl Parse for S3ConditionalPut {
154 fn parse(v: &str) -> crate::Result<Self> {
155 Self::from_str(v).ok_or_else(|| crate::Error::Generic {
156 store: "Config",
157 source: format!("Failed to parse \"{v}\" as S3PutConditional").into(),
158 })
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::S3CopyIfNotExists;
165 use crate::aws::{DynamoCommit, S3ConditionalPut};
166
167 #[test]
168 fn parse_s3_copy_if_not_exists_header() {
169 let input = "header: cf-copy-destination-if-none-match: *";
170 let expected = Some(S3CopyIfNotExists::Header(
171 "cf-copy-destination-if-none-match".to_owned(),
172 "*".to_owned(),
173 ));
174
175 assert_eq!(expected, S3CopyIfNotExists::from_str(input));
176 }
177
178 #[test]
179 fn parse_s3_copy_if_not_exists_header_with_status() {
180 let input = "header-with-status:key:value:403";
181 let expected = Some(S3CopyIfNotExists::HeaderWithStatus(
182 "key".to_owned(),
183 "value".to_owned(),
184 reqwest::StatusCode::FORBIDDEN,
185 ));
186
187 assert_eq!(expected, S3CopyIfNotExists::from_str(input));
188 }
189
190 #[test]
191 fn parse_s3_copy_if_not_exists_dynamo() {
192 let input = "dynamo: table:100";
193 let expected = Some(S3CopyIfNotExists::Dynamo(
194 DynamoCommit::new("table".into()).with_timeout(100),
195 ));
196 assert_eq!(expected, S3CopyIfNotExists::from_str(input));
197 }
198
199 #[test]
200 fn parse_s3_condition_put_dynamo() {
201 let input = "dynamo: table:1300";
202 let expected = Some(S3ConditionalPut::Dynamo(
203 DynamoCommit::new("table".into()).with_timeout(1300),
204 ));
205 assert_eq!(expected, S3ConditionalPut::from_str(input));
206 }
207
208 #[test]
209 fn parse_s3_copy_if_not_exists_header_whitespace_invariant() {
210 let expected = Some(S3CopyIfNotExists::Header(
211 "cf-copy-destination-if-none-match".to_owned(),
212 "*".to_owned(),
213 ));
214
215 const INPUTS: &[&str] = &[
216 "header:cf-copy-destination-if-none-match:*",
217 "header: cf-copy-destination-if-none-match:*",
218 "header: cf-copy-destination-if-none-match: *",
219 "header : cf-copy-destination-if-none-match: *",
220 "header : cf-copy-destination-if-none-match : *",
221 "header : cf-copy-destination-if-none-match : * ",
222 ];
223
224 for input in INPUTS {
225 assert_eq!(expected, S3CopyIfNotExists::from_str(input));
226 }
227 }
228
229 #[test]
230 fn parse_s3_copy_if_not_exists_header_with_status_whitespace_invariant() {
231 let expected = Some(S3CopyIfNotExists::HeaderWithStatus(
232 "key".to_owned(),
233 "value".to_owned(),
234 reqwest::StatusCode::FORBIDDEN,
235 ));
236
237 const INPUTS: &[&str] = &[
238 "header-with-status:key:value:403",
239 "header-with-status: key:value:403",
240 "header-with-status: key: value:403",
241 "header-with-status: key: value: 403",
242 "header-with-status : key: value: 403",
243 "header-with-status : key : value: 403",
244 "header-with-status : key : value : 403",
245 "header-with-status : key : value : 403 ",
246 ];
247
248 for input in INPUTS {
249 assert_eq!(expected, S3CopyIfNotExists::from_str(input));
250 }
251 }
252}