use regex::Regex;
use super::TransformField;
use crate::{actions::transforms::result::TransformResult, error::BadRegexError};
#[derive(Debug)]
pub struct Extract {
re: Regex,
passthrough: PassthroughIfNotMatched,
}
#[derive(Debug)]
pub enum PassthroughIfNotMatched {
Always,
ReturnError,
}
#[expect(missing_docs, reason = "error message is self-documenting")]
#[derive(thiserror::Error, Debug)]
pub enum ExtractError {
#[error(transparent)]
BadRegex(#[from] BadRegexError),
#[error("Capture group not found but passthrough_if_not_found is not set")]
CaptureGroupNotFound,
}
impl Extract {
pub fn new(re: &str, passthrough: PassthroughIfNotMatched) -> Result<Self, ExtractError> {
let re = Regex::new(re).map_err(BadRegexError)?;
Ok(Self { re, passthrough })
}
}
impl TransformField for Extract {
type Err = ExtractError;
async fn transform_field(
&mut self,
value: Option<&str>,
) -> Result<TransformResult<String>, Self::Err> {
let Some(field) = value else {
return Ok(TransformResult::Previous);
};
let extracted = match extract_captures_from(&self.re, field) {
Some(v) => v,
None if matches!(self.passthrough, PassthroughIfNotMatched::Always) => field.to_owned(),
None => return Err(ExtractError::CaptureGroupNotFound),
};
Ok(TransformResult::New(extracted))
}
}
fn extract_captures_from(regex: &Regex, from: &str) -> Option<String> {
regex.captures(from).map(|captures| {
captures
.iter()
.skip(1 )
.filter_map(|capt| Some(capt?.as_str()))
.collect()
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
const FROM: &str = "HelloxWorld";
#[test]
fn one() {
let re = Regex::new("(?s)(.*)x").unwrap();
assert_eq!(extract_captures_from(&re, FROM).unwrap(), "Hello");
}
#[test]
fn several() {
let re = Regex::new("(?s)(.*)x(.*)").unwrap();
assert_eq!(extract_captures_from(&re, FROM).unwrap(), "HelloWorld");
}
#[test]
fn not_matched() {
let re = Regex::new("(?s)(.*)xxx(.*)").unwrap();
assert!(extract_captures_from(&re, FROM).is_none());
}
}