1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

//! This module contains the [`Extract`] field transform, as well as all errors that can happen while creating or executing it

use regex::Regex;

use super::TransformField;
use crate::{action::transform::result::TransformResult, error::BadRegexError};

/// Extract the contents of capture groups using a regular expression and concat them
#[derive(Debug)]
pub struct Extract {
	/// The regular expression to match against. Replace the value of the field with the contents of capture groups
	re: Regex,

	/// Passthrough the old value if the regex didn't match
	passthrough_if_not_found: bool,
}

#[allow(missing_docs)] // 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 {
	/// Create a new [`Extract`] with regular expression `re` and `passthrough_if_not_found`
	///
	/// # Errors
	/// * if the regex is invalid
	/// * if the regex doesn't contains capture group <[`CAPTURE_GROUP_NAME`]>
	pub fn new(re: &str, passthrough_if_not_found: bool) -> Result<Self, ExtractError> {
		let re = Regex::new(re).map_err(BadRegexError)?;

		Ok(Self {
			re,
			passthrough_if_not_found,
		})
	}
}

impl TransformField for Extract {
	type Err = ExtractError;

	fn transform_field(&self, old_val: Option<&str>) -> Result<TransformResult<String>, Self::Err> {
		let Some(field) = old_val else {
			return Ok(TransformResult::Old(None));
		};

		let extracted = match extract_captures_from(&self.re, field) {
			Some(v) => v,
			None if self.passthrough_if_not_found => field.to_owned(),
			None => return Err(ExtractError::CaptureGroupNotFound),
		};

		Ok(TransformResult::New(Some(extracted)))
	}
}

fn extract_captures_from(regex: &Regex, from: &str) -> Option<String> {
	regex.captures(from).map(|captures| {
		captures
			.iter()
			.skip(1 /* the first match that matches the entire regex */)
			.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());
	}
}