automaat_processor_string_regex/lib.rs
1//! An [Automaat] processor to match and replace strings using regex patterns.
2//!
3//! This processor allows you to match against strings in an Automaat workflow,
4//! and either show an error if the pattern doesn't match, or replace the
5//! pattern with a replacement string.
6//!
7//! It is great for transforming the output of the previous processor into
8//! something that is more readable for the user, before printing it to the
9//! screen using the [`PrintOutput`] processor.
10//!
11//! [Automaat]: automaat_core
12//! [`PrintOutput`]: https://docs.rs/automaat-processor-print-output
13//!
14//! # Examples
15//!
16//! ## Replace input based on regex pattern
17//!
18//! One common example of this processor is to use it after another processor
19//! ran, which provided some output that needs to be rewritten before it is used
20//! by the next processor (or presented to the user).
21//!
22//! In this example, we get a string `Failure #233 - email does not exist`. We
23//! want to rewrite this output to show `error: email does not exist`.
24//!
25//! ```rust
26//! # fn main() -> Result<(), Box<std::error::Error>> {
27//! use automaat_core::{Context, Processor};
28//! use automaat_processor_string_regex::StringRegex;
29//!
30//! let context = Context::new()?;
31//!
32//! let processor = StringRegex {
33//! input: "Failure #233 - email does not exist".to_owned(),
34//! regex: r"\A[^-]+ - (.*)\z".to_owned(),
35//! mismatch_error: None,
36//! replace: Some("error: $1".to_owned())
37//! };
38//!
39//! let output = processor.run(&context)?;
40//!
41//! assert_eq!(output, Some("error: email does not exist".to_owned()));
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! ## Return error on regex mismatch
47//!
48//! Another common use-case is to match against some input, and return an error
49//! if the pattern does not match.
50//!
51//! In this case, we want the string to be a valid UUIDv4 format, and return an
52//! understandable error to the user if it does not match.
53//!
54//! ```rust
55//! # fn main() -> Result<(), Box<std::error::Error>> {
56//! use automaat_core::{Context, Processor};
57//! use automaat_processor_string_regex::StringRegex;
58//!
59//! let context = Context::new()?;
60//!
61//! let processor = StringRegex {
62//! input: "This is not a valid UUID".to_owned(),
63//! regex: r"\A([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})\z".to_owned(),
64//! mismatch_error: Some("provided value is not in a valid UUIDv4 format".to_owned()),
65//! replace: None
66//! };
67//!
68//! let error = processor.run(&context).unwrap_err();
69//!
70//! assert_eq!(error.to_string(), "provided value is not in a valid UUIDv4 format".to_owned());
71//! # Ok(())
72//! # }
73//! ```
74//!
75//! # Package Features
76//!
77//! * `juniper` – creates a set of objects to be used in GraphQL-based
78//! requests/responses.
79#![deny(
80 clippy::all,
81 clippy::cargo,
82 clippy::nursery,
83 clippy::pedantic,
84 deprecated_in_future,
85 future_incompatible,
86 missing_docs,
87 nonstandard_style,
88 rust_2018_idioms,
89 rustdoc,
90 warnings,
91 unused_results,
92 unused_qualifications,
93 unused_lifetimes,
94 unused_import_braces,
95 unsafe_code,
96 unreachable_pub,
97 trivial_casts,
98 trivial_numeric_casts,
99 missing_debug_implementations,
100 missing_copy_implementations
101)]
102#![warn(variant_size_differences)]
103#![allow(clippy::multiple_crate_versions, missing_doc_code_examples)]
104#![doc(html_root_url = "https://docs.rs/automaat-processor-string-regex/0.1.0")]
105
106use automaat_core::{Context, Processor};
107use regex::{Error as RegexError, Regex};
108use serde::{Deserialize, Serialize};
109use std::{error, fmt};
110
111/// The processor configuration.
112#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
113#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
114pub struct StringRegex {
115 /// The string that will be matched against the provided `regex`, and
116 /// optionally replaced by the `replace` pattern.
117 pub input: String,
118
119 /// The regular expression used to match the `input`. See the regex crate
120 /// [syntax documentation] for more details.
121 ///
122 /// [syntax documentation]: https://docs.rs/regex/latest/regex/#syntax
123 pub regex: String,
124
125 /// If the `regex` pattern does not match the `input` value, an error is
126 /// returned. By default, a generic mismatch error is returned.
127 ///
128 /// You can set this value to have it be returned as the error instead.
129 pub mismatch_error: Option<String>,
130
131 /// Optionally use the `regex` pattern and the `input` to construct a
132 /// replacement string to return as this processors output.
133 ///
134 /// You can use variables such as `$1` and `$2` to match against the
135 /// patterns in the regex.
136 pub replace: Option<String>,
137}
138
139/// The GraphQL [Input Object][io] used to initialize the processor via an API.
140///
141/// [`StringRegex`] implements `From<Input>`, so you can directly initialize the
142/// processor using this type.
143///
144/// _requires the `juniper` package feature to be enabled_
145///
146/// [io]: https://graphql.github.io/graphql-spec/June2018/#sec-Input-Objects
147#[cfg(feature = "juniper")]
148#[graphql(name = "StringRegexInput")]
149#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, juniper::GraphQLInputObject)]
150pub struct Input {
151 input: String,
152 regex: String,
153 mismatch_error: Option<String>,
154 replace: Option<String>,
155}
156
157#[cfg(feature = "juniper")]
158impl From<Input> for StringRegex {
159 fn from(input: Input) -> Self {
160 Self {
161 input: input.input,
162 regex: input.regex,
163 mismatch_error: input.mismatch_error,
164 replace: input.replace,
165 }
166 }
167}
168
169impl<'a> Processor<'a> for StringRegex {
170 const NAME: &'static str = "String Regex";
171
172 type Error = Error;
173 type Output = String;
174
175 /// Validate that the provided [`regex`] pattern is valid.
176 ///
177 /// # Errors
178 ///
179 /// If the regex syntax is invalid, the [`Error::Syntax`] error variant is
180 /// returned.
181 ///
182 /// If the regex pattern is too big (highly unlikely), the
183 /// [`Error::CompiledTooBig`] error variant is returned.
184 ///
185 /// Both variants wrap the original [Regex crate errors].
186 ///
187 /// [`regex`]: StringRegex::regex
188 /// [Regex crate errors]: regex::Error
189 fn validate(&self) -> Result<(), Self::Error> {
190 Regex::new(self.regex.as_str())
191 .map(|_| ())
192 .map_err(Into::into)
193 }
194
195 /// Do a regex match (and replace), based on the processor configuration.
196 ///
197 /// # Output
198 ///
199 /// If [`replace`] is set to `None`, the output of the processor will be
200 /// `Ok(None)` if no error occurred.
201 ///
202 /// If [`replace`] is set to `Some`, then `Some` is returned, matching the
203 /// final replaced output value in [`Processor::Output`].
204 ///
205 /// # Errors
206 ///
207 /// If the [`regex`] pattern does not match the [`input`] input, the
208 /// [`Error::Match`] error variant is returned. If [`mismatch_error`] is
209 /// set, the error will contain the provided message. If not, a default
210 /// message is provided.
211 ///
212 /// If the regex pattern is invalid, the same errors are returned as
213 /// [`validate`].
214 ///
215 /// [`replace`]: StringRegex::replace
216 /// [`regex`]: StringRegex::regex
217 /// [`input`]: StringRegex::input
218 /// [`mismatch_error`]: StringRegex::mismatch_error
219 /// [`validate`]: #method.validate
220 fn run(&self, _context: &Context) -> Result<Option<Self::Output>, Self::Error> {
221 let re = Regex::new(self.regex.as_str()).map_err(Into::<Self::Error>::into)?;
222
223 if re.is_match(self.input.as_str()) {
224 match &self.replace {
225 None => Ok(None),
226 Some(replace) => {
227 let out = re
228 .replace(self.input.as_str(), replace.as_str())
229 .into_owned();
230
231 if out.is_empty() {
232 Ok(None)
233 } else {
234 Ok(Some(out))
235 }
236 }
237 }
238 } else if let Some(msg) = &self.mismatch_error {
239 Err(Error::Match(msg.to_owned()))
240 } else {
241 Err(Error::Match(format!(
242 "Match error: \"{}\" does not match pattern: {}",
243 self.input, self.regex
244 )))
245 }
246 }
247}
248
249/// Represents all the ways that [`StringRegex`] can fail.
250///
251/// This type is not intended to be exhaustively matched, and new variants may
252/// be added in the future without a major version bump.
253#[derive(Debug)]
254pub enum Error {
255 /// A syntax error.
256 Syntax(RegexError),
257
258 /// The compiled program exceeded the set size limit. The argument is the
259 /// size limit imposed.
260 CompiledTooBig(RegexError),
261
262 /// The regex pattern did not match the provided [`StringRegex::input`].
263 ///
264 /// The contained string value is either a default mismatch error, or a
265 /// custom error, based on the [`StringRegex::mismatch_error`] value.
266 Match(String),
267
268 #[doc(hidden)]
269 __Unknown, // Match against _ instead, more variants may be added in the future.
270}
271
272impl fmt::Display for Error {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 match *self {
275 Error::Syntax(ref err) | Error::CompiledTooBig(ref err) => {
276 write!(f, "Regex error: {}", err)
277 }
278 Error::Match(ref string) => write!(f, "{}", string),
279 Error::__Unknown => unreachable!(),
280 }
281 }
282}
283
284impl error::Error for Error {
285 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
286 match *self {
287 Error::Syntax(ref err) | Error::CompiledTooBig(ref err) => Some(err),
288 Error::Match(_) => None,
289 Error::__Unknown => unreachable!(),
290 }
291 }
292}
293
294impl From<RegexError> for Error {
295 fn from(err: RegexError) -> Self {
296 match err {
297 RegexError::Syntax(_) => Error::Syntax(err),
298 RegexError::CompiledTooBig(_) => Error::CompiledTooBig(err),
299
300 // Regex crate has a non-exhaustive error enum, similar to this
301 // crates. Should they ever add an error in an upgrade, we will know
302 // because compilation failed, and we'll have to add it as well.
303 RegexError::__Nonexhaustive => unreachable!(),
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 fn processor_stub() -> StringRegex {
313 StringRegex {
314 input: "hello world".to_owned(),
315 regex: r"\Ahello world\z".to_owned(),
316 mismatch_error: None,
317 replace: None,
318 }
319 }
320
321 mod run {
322 use super::*;
323
324 #[test]
325 fn test_match_pattern() {
326 let mut processor = processor_stub();
327 processor.input = "hello world".to_owned();
328 processor.regex = r"hello \w+".to_owned();
329
330 let context = Context::new().unwrap();
331 let output = processor.run(&context).unwrap();
332
333 assert!(output.is_none())
334 }
335
336 #[test]
337 fn test_mismatch_pattern_default_error() {
338 let mut processor = processor_stub();
339 processor.input = "hello world".to_owned();
340 processor.regex = r"hi \w+".to_owned();
341
342 let context = Context::new().unwrap();
343 let error = processor.run(&context).unwrap_err();
344
345 assert_eq!(
346 error.to_string(),
347 r#"Match error: "hello world" does not match pattern: hi \w+"#.to_owned()
348 )
349 }
350
351 #[test]
352 fn test_mismatch_pattern_custom_error() {
353 let mut processor = processor_stub();
354 processor.input = "hello world".to_owned();
355 processor.regex = r"hi \w+".to_owned();
356 processor.mismatch_error = Some("invalid!".to_owned());
357
358 let context = Context::new().unwrap();
359 let error = processor.run(&context).unwrap_err();
360
361 assert_eq!(error.to_string(), "invalid!".to_owned())
362 }
363
364 #[test]
365 fn test_replace_pattern() {
366 let mut processor = processor_stub();
367 processor.input = "hello world".to_owned();
368 processor.regex = r"hello (\w+)".to_owned();
369 processor.replace = Some("hi $1!".to_owned());
370
371 let context = Context::new().unwrap();
372 let output = processor.run(&context).unwrap().expect("Some");
373
374 assert_eq!(output, "hi world!".to_owned())
375 }
376 }
377
378 mod validate {
379 use super::*;
380
381 #[test]
382 fn test_valid_syntax() {
383 let mut processor = processor_stub();
384 processor.regex = r"hello \w+".to_owned();
385
386 processor.validate().unwrap()
387 }
388
389 #[test]
390 #[should_panic]
391 fn test_invalid_syntax() {
392 let mut processor = processor_stub();
393 processor.regex = r"hello \NO".to_owned();
394
395 processor.validate().unwrap()
396 }
397 }
398
399 #[test]
400 fn test_readme_deps() {
401 version_sync::assert_markdown_deps_updated!("README.md");
402 }
403
404 #[test]
405 fn test_html_root_url() {
406 version_sync::assert_html_root_url_updated!("src/lib.rs");
407 }
408}