From d4b4cdd94e79ae7eb2e5f0fd11724e27c4f2549f Mon Sep 17 00:00:00 2001 From: Ryan Bottriell Date: Sun, 2 Oct 2022 21:43:18 -0700 Subject: [PATCH] Add filter to perform replacements via regular expressions Signed-off-by: Ryan Bottriell --- Cargo.lock | 1 + crates/spk-schema/crates/liquid/Cargo.toml | 1 + .../crates/liquid/src/filter_replace_regex.rs | 64 +++++++++++++++++++ .../liquid/src/filter_replace_regex_test.rs | 44 +++++++++++++ crates/spk-schema/crates/liquid/src/lib.rs | 2 + 5 files changed, 112 insertions(+) create mode 100644 crates/spk-schema/crates/liquid/src/filter_replace_regex.rs create mode 100644 crates/spk-schema/crates/liquid/src/filter_replace_regex_test.rs diff --git a/Cargo.lock b/Cargo.lock index 2368d6ba69..883063057f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3337,6 +3337,7 @@ version = "0.36.0" dependencies = [ "liquid", "liquid-core", + "regex", "rstest", "serde", "serde_json", diff --git a/crates/spk-schema/crates/liquid/Cargo.toml b/crates/spk-schema/crates/liquid/Cargo.toml index cbda09db86..54c1679f00 100644 --- a/crates/spk-schema/crates/liquid/Cargo.toml +++ b/crates/spk-schema/crates/liquid/Cargo.toml @@ -10,6 +10,7 @@ migration-to-components = ["spk-schema-foundation/migration-to-components"] [dependencies] liquid = "0.26.0" liquid-core = "0.26.0" +regex = "1.6.0" serde = "1.0" serde_json = "1.0" spk-schema-foundation = { path = "../foundation" } diff --git a/crates/spk-schema/crates/liquid/src/filter_replace_regex.rs b/crates/spk-schema/crates/liquid/src/filter_replace_regex.rs new file mode 100644 index 0000000000..75b42b5d74 --- /dev/null +++ b/crates/spk-schema/crates/liquid/src/filter_replace_regex.rs @@ -0,0 +1,64 @@ +// Copyright (c) Sony Pictures Imageworks, et al. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/imageworks/spk + +use liquid::ValueView; +use liquid_core::{ + Display_filter, + Expression, + Filter, + FilterParameters, + FilterReflection, + FromFilterParameters, + ParseFilter, + Result, + Runtime, + Value, +}; + +#[cfg(test)] +#[path = "./filter_replace_regex_test.rs"] +mod filter_replace_regex_test; + +#[derive(Debug, FilterParameters)] +struct ReplaceRegexArgs { + #[parameter(description = "The regular expression to search.", arg_type = "str")] + search: Expression, + #[parameter( + description = "The text to replace search results with. If not given, the filter will just delete search results. Capture groups can be substituted using `$`", + arg_type = "str" + )] + replace: Option, +} + +#[derive(Clone, ParseFilter, FilterReflection)] +#[filter( + name = "replace_re", + description = "Like `replace`, but searches using a regular expression.", + parameters(ReplaceRegexArgs), + parsed(ReplaceRegexFilter) +)] +pub struct ReplaceRegex; + +#[derive(Debug, FromFilterParameters, Display_filter)] +#[name = "replace_re"] +struct ReplaceRegexFilter { + #[parameters] + args: ReplaceRegexArgs, +} + +impl Filter for ReplaceRegexFilter { + fn evaluate(&self, input: &dyn ValueView, runtime: &dyn Runtime) -> Result { + let args = self.args.evaluate(runtime)?; + + let input = input.to_kstr(); + + let search = regex::Regex::new(&args.search) + .map_err(|err| liquid::Error::with_msg(err.to_string()))?; + let replace = args.replace.unwrap_or_else(|| "".into()); + + Ok(Value::scalar( + search.replace_all(&input, replace.as_str()).to_string(), + )) + } +} diff --git a/crates/spk-schema/crates/liquid/src/filter_replace_regex_test.rs b/crates/spk-schema/crates/liquid/src/filter_replace_regex_test.rs new file mode 100644 index 0000000000..dc6aabdf26 --- /dev/null +++ b/crates/spk-schema/crates/liquid/src/filter_replace_regex_test.rs @@ -0,0 +1,44 @@ +// Copyright (c) Sony Pictures Imageworks, et al. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/imageworks/spk + +use rstest::rstest; +use serde_json::json; + +#[rstest] +fn test_replace_regex_basic() { + let options = json!({}); + static TPL: &str = r#"{{ "1992-02-25" | replace_re: "(\d+)-(\d+)-(\d+)", "$3/$2/$1" }}"#; + static EXPECTED: &str = r#"25/02/1992"#; + let rendered = + crate::render_template(TPL, &options).expect("template should not fail to render"); + assert_eq!(rendered, EXPECTED); +} + +#[rstest] +fn test_replace_regex_empty() { + let options = json!({}); + static TPL: &str = r#"{{ "Hello, World!" | replace_re: "[A-Z]" }}"#; + static EXPECTED: &str = r#"ello, orld!"#; + let rendered = + crate::render_template(TPL, &options).expect("template should not fail to render"); + assert_eq!(rendered, EXPECTED); +} + +#[rstest] +fn test_replace_regex_compile_error() { + let options = json!({"version": "1.2.3.4.5-beta.1+r.0"}); + static TPL: &str = r#"{{ "something" | replace_re: "(some]" }}"#; + static EXPECTED_ERR: &str = r#" +liquid: regex parse error: + (some] + ^ +error: unclosed group +from: Filter error + with: + filter=replace_re : "(some]" + input="something" +"#; + let err = crate::render_template(TPL, &options).expect_err("template should fail on bad regex"); + assert_eq!(err.to_string().trim(), EXPECTED_ERR.trim()); +} diff --git a/crates/spk-schema/crates/liquid/src/lib.rs b/crates/spk-schema/crates/liquid/src/lib.rs index 17b9aec401..54a1da8226 100644 --- a/crates/spk-schema/crates/liquid/src/lib.rs +++ b/crates/spk-schema/crates/liquid/src/lib.rs @@ -5,6 +5,7 @@ mod filter_compare_version; mod filter_parse_version; +mod filter_replace_regex; mod tag_default; pub use liquid::Error; @@ -19,6 +20,7 @@ pub fn default_parser() -> liquid::Parser { .tag(tag_default::DefaultTag) .filter(filter_parse_version::ParseVersion) .filter(filter_compare_version::CompareVersion) + .filter(filter_replace_regex::ReplaceRegex) .build(); debug_assert!(matches!(res, Ok(_)), "default template parser is valid"); res.unwrap()