1use std::future::Future;
15use std::pin::Pin;
16use std::sync::Arc;
17use std::task::{Context, Poll};
18
19use tower::Service;
20
21use camel_api::body::Body;
22use camel_api::{CamelError, ClaimCheckRepository, Exchange, Message};
23
24pub type KeyExpression = Arc<dyn Fn(&Exchange) -> Result<String, CamelError> + Send + Sync>;
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum ClaimCheckOp {
31 Set,
33 Get,
35 GetAndRemove,
37 Push,
39 Pop,
41}
42
43#[derive(Clone)]
50pub struct ClaimCheckService {
51 repository: Arc<dyn ClaimCheckRepository>,
52 operation: ClaimCheckOp,
53 key_expression: KeyExpression,
54 filter: Option<ClaimCheckFilter>,
55}
56
57impl std::fmt::Debug for ClaimCheckService {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct("ClaimCheckService")
60 .field("repository", &self.repository.name())
61 .field("operation", &self.operation)
62 .field("filter", &self.filter)
63 .finish()
64 }
65}
66
67impl ClaimCheckService {
68 pub fn new(
70 repository: Arc<dyn ClaimCheckRepository>,
71 operation: ClaimCheckOp,
72 key_expression: KeyExpression,
73 ) -> Self {
74 Self {
75 repository,
76 operation,
77 key_expression,
78 filter: None,
79 }
80 }
81
82 pub fn with_filter(mut self, filter: ClaimCheckFilter) -> Self {
84 self.filter = Some(filter);
85 self
86 }
87}
88
89fn merge_stashed(current: &mut Message, stashed: &Message, filter: &ClaimCheckFilter) {
93 match filter.body {
94 FilterAction::Include => current.body = stashed.body.clone(),
95 FilterAction::Exclude => {}
96 FilterAction::Remove => current.body = Body::Empty,
97 }
98
99 match &filter.headers_action {
100 HeadersAction::All(action) => match action {
101 FilterAction::Include => {
102 for (k, v) in &stashed.headers {
103 current.headers.insert(k.clone(), v.clone());
104 }
105 }
106 FilterAction::Exclude => {}
107 FilterAction::Remove => current.headers.clear(),
108 },
109 HeadersAction::ByPattern {
110 include,
111 exclude,
112 remove,
113 } => {
114 if !include.is_empty() {
115 for (k, v) in &stashed.headers {
116 if include.iter().any(|p| p.matches(k)) {
117 current.headers.insert(k.clone(), v.clone());
118 }
119 }
120 }
121 if !exclude.is_empty() {
122 for (k, v) in &stashed.headers {
123 if !exclude.iter().any(|p| p.matches(k)) {
124 current.headers.insert(k.clone(), v.clone());
125 }
126 }
127 }
128 if !remove.is_empty() {
129 current
130 .headers
131 .retain(|k, _| !remove.iter().any(|p| p.matches(k)));
132 }
133 }
134 }
135}
136
137impl Service<Exchange> for ClaimCheckService {
138 type Response = Exchange;
139 type Error = CamelError;
140 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
141
142 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
143 Poll::Ready(Ok(()))
144 }
145
146 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
147 let repository = self.repository.clone();
148 let operation = self.operation.clone();
149 let key_result = (self.key_expression)(&exchange);
150 let filter = self.filter.clone();
151
152 Box::pin(async move {
153 let key = key_result?;
154 match operation {
155 ClaimCheckOp::Set => {
156 let stashed = exchange.input.clone();
157 repository.set(&key, stashed).await?;
158 exchange.input.body = Body::Text(key);
159 Ok(exchange)
160 }
161 ClaimCheckOp::Get => {
162 let stashed = repository.get(&key).await?;
163 if let Some(ref f) = filter {
164 merge_stashed(&mut exchange.input, &stashed, f);
165 } else {
166 exchange.input.body = stashed.body;
167 }
168 Ok(exchange)
169 }
170 ClaimCheckOp::GetAndRemove => {
171 let stashed = repository.get_and_remove(&key).await?;
172 if let Some(ref f) = filter {
173 merge_stashed(&mut exchange.input, &stashed, f);
174 } else {
175 exchange.input.body = stashed.body;
176 }
177 Ok(exchange)
178 }
179 ClaimCheckOp::Push => {
180 let stashed = exchange.input.clone();
181 repository.push(&key, stashed).await?;
182 exchange.input.body = Body::Text(key);
183 Ok(exchange)
184 }
185 ClaimCheckOp::Pop => {
186 let stashed = repository.pop(&key).await?;
187 if let Some(ref f) = filter {
188 merge_stashed(&mut exchange.input, &stashed, f);
189 } else {
190 exchange.input.body = stashed.body;
191 }
192 Ok(exchange)
193 }
194 }
195 })
196 }
197}
198
199fn has_regex_metachars(s: &str) -> bool {
200 s.contains(['^', '$', '(', ')', '[', ']', '{', '}', '|', '+', '.', '\\'])
201}
202
203#[derive(Debug, Clone)]
204pub enum HeaderPattern {
205 All,
206 Prefix(String),
207 Exact(String),
208 Regex(regex::Regex),
209}
210
211impl PartialEq for HeaderPattern {
212 fn eq(&self, other: &Self) -> bool {
213 match (self, other) {
214 (Self::All, Self::All) => true,
215 (Self::Prefix(a), Self::Prefix(b)) => a == b,
216 (Self::Exact(a), Self::Exact(b)) => a == b,
217 (Self::Regex(a), Self::Regex(b)) => a.as_str() == b.as_str(),
218 _ => false,
219 }
220 }
221}
222
223impl HeaderPattern {
224 fn compile(pattern: &str) -> Result<Self, FilterParseError> {
225 if pattern == "*" {
226 return Ok(Self::All);
227 }
228 if let Some(prefix) = pattern.strip_suffix('*') {
229 return Ok(Self::Prefix(prefix.to_string()));
230 }
231 if has_regex_metachars(pattern) {
232 match regex::Regex::new(pattern) {
233 Ok(re) => Ok(Self::Regex(re)),
234 Err(_) => Err(FilterParseError::InvalidPattern(pattern.to_string())),
235 }
236 } else {
237 Ok(Self::Exact(pattern.to_string()))
238 }
239 }
240
241 fn matches(&self, header_key: &str) -> bool {
242 match self {
243 Self::All => true,
244 Self::Prefix(prefix) => header_key.starts_with(prefix),
245 Self::Exact(exact) => header_key == exact,
246 Self::Regex(re) => re.is_match(header_key),
247 }
248 }
249}
250
251#[derive(Debug, Clone, PartialEq)]
252pub struct ClaimCheckFilter {
253 pub body: FilterAction,
254 pub headers_action: HeadersAction,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum FilterAction {
259 Include,
260 Exclude,
261 Remove,
262}
263
264#[derive(Debug, Clone, PartialEq)]
265pub enum HeadersAction {
266 All(FilterAction),
267 ByPattern {
268 include: Vec<HeaderPattern>,
269 exclude: Vec<HeaderPattern>,
270 remove: Vec<HeaderPattern>,
271 },
272}
273
274#[derive(Debug)]
275pub enum FilterParseError {
276 InvalidToken(String),
277 InvalidPattern(String),
278 MixedIncludeExclude,
279}
280
281impl std::fmt::Display for FilterParseError {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 match self {
284 Self::InvalidToken(tok) => write!(f, "invalid filter segment '{tok}'"),
285 Self::InvalidPattern(pat) => write!(f, "invalid header pattern '{pat}'"),
286 Self::MixedIncludeExclude => {
287 write!(f, "cannot mix include (+) and exclude (-) header patterns")
288 }
289 }
290 }
291}
292
293impl ClaimCheckFilter {
294 pub fn parse(input: &str) -> Result<Self, FilterParseError> {
297 let mut body_action: Option<FilterAction> = None;
298 let mut headers_action: Option<HeadersAction> = None;
299 let mut has_positive_include = false;
300
301 for token in input.split(',') {
302 let token = token.trim();
303 if token.is_empty() {
304 continue;
305 }
306
307 let (prefix, rule) = if let Some(rest) = token.strip_prefix("--") {
308 (FilterAction::Remove, rest)
309 } else if let Some(rest) = token.strip_prefix('-') {
310 (FilterAction::Exclude, rest)
311 } else {
312 let rest = token.strip_prefix('+').unwrap_or(token);
313 (FilterAction::Include, rest)
314 };
315
316 match rule {
317 "body" => {
318 if prefix == FilterAction::Include {
319 has_positive_include = true;
320 }
321 body_action = Some(prefix);
322 }
323 "headers" | "header" => {
324 if prefix == FilterAction::Include {
325 has_positive_include = true;
326 }
327 headers_action = Some(HeadersAction::All(prefix));
328 }
329 "attachments" | "attachment" => {
330 }
332 r if r.starts_with("header:") || r.starts_with("headers:") => {
333 let (_, pattern_str) = r
334 .split_once(':')
335 .ok_or_else(|| FilterParseError::InvalidToken(r.to_string()))?;
336 let pattern = HeaderPattern::compile(pattern_str)?;
337
338 if prefix == FilterAction::Include {
339 has_positive_include = true;
340 }
341
342 let (mut include, mut exclude, mut remove) = match headers_action.take() {
343 Some(HeadersAction::ByPattern {
344 include,
345 exclude,
346 remove,
347 }) => (include, exclude, remove),
348 _ => (vec![], vec![], vec![]),
349 };
350 match prefix {
351 FilterAction::Include => {
352 if !exclude.is_empty() {
353 return Err(FilterParseError::MixedIncludeExclude);
354 }
355 include.push(pattern);
356 }
357 FilterAction::Exclude => {
358 if !include.is_empty() {
359 return Err(FilterParseError::MixedIncludeExclude);
360 }
361 exclude.push(pattern);
362 }
363 FilterAction::Remove => remove.push(pattern),
364 }
365 headers_action = Some(HeadersAction::ByPattern {
366 include,
367 exclude,
368 remove,
369 });
370 }
371 other => return Err(FilterParseError::InvalidToken(other.to_string())),
372 }
373 }
374
375 let default_if_omitted = if has_positive_include {
376 FilterAction::Exclude
377 } else {
378 FilterAction::Include
379 };
380
381 Ok(ClaimCheckFilter {
382 body: body_action.unwrap_or(default_if_omitted),
383 headers_action: headers_action.unwrap_or(HeadersAction::All(default_if_omitted)),
384 })
385 }
386}
387
388#[cfg(test)]
389#[path = "claim_check_tests.rs"]
390mod tests;