From 6a9fdd85b369f66b49ffc09cc3a5758d026b3b40 Mon Sep 17 00:00:00 2001 From: David Bernard Date: Sun, 2 Jun 2024 19:30:05 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20helpers=20`regex=5Fcaptures`?= =?UTF-8?q?=20and=20`regex=5Fis=5Fmatch`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 8 ++- README.md | 19 +++++-- src/assign_helpers.rs | 7 ++- src/lib.rs | 4 ++ src/regex_helpers.rs | 125 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 src/regex_helpers.rs diff --git a/Cargo.toml b/Cargo.toml index ff6a816..c478a97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ log = "^0.4" cruet = { version = "^0.14", optional = true } jmespath = { version = "^0.3", optional = true } jsonnet-rs = { version = "^0.17", optional = true } +regex = { version = "^1.10", optional = true } reqwest = { version = "0.12", optional = true, default-features = false, features = [ "blocking", "rustls-tls", @@ -48,8 +49,7 @@ similar-asserts = "1.4" unindent = "0.2" [features] -default = ["string", "http_attohttpc", "json", "jsonnet"] -string = ["dep:cruet", "dep:enquote", "jsontype"] +default = ["string", "http_attohttpc", "json", "jsonnet", "regex"] http_attohttpc = ["dep:attohttpc"] http_reqwest = ["dep:reqwest"] json = [ @@ -59,5 +59,7 @@ json = [ "dep:serde_yaml", "dep:toml", ] -jsontype = ["dep:serde_json"] jsonnet = ["dep:jsonnet-rs"] +jsontype = ["dep:serde_json"] +regex = ["dep:regex"] +string = ["dep:cruet", "dep:enquote", "jsontype"] diff --git a/README.md b/README.md index 4a8c46a..f179e76 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,7 @@ see [Handlebars templating language](https://handlebarsjs.com/) To not "import" useless dependencies, use crate's features: ```toml -[features] -default = ["string", "http_attohttpc", "json", "jsonnet"] -string = ["dep:cruet", "dep:enquote", "jsontype"] +default = ["string", "http_attohttpc", "json", "jsonnet", "regex"] http_attohttpc = ["dep:attohttpc"] http_reqwest = ["dep:reqwest"] json = [ @@ -47,13 +45,16 @@ json = [ "dep:serde_yaml", "dep:toml", ] -jsontype = ["dep:serde_json"] jsonnet = ["dep:jsonnet-rs"] +jsontype = ["dep:serde_json"] +regex = ["dep:regex"] +string = ["dep:cruet", "dep:enquote", "jsontype"] ``` - [String transformation](#string-transformation) +- [Regular expression](#regular-expression) - [Http content](#http-content) - [Path extraction](#path-extraction) - [File](#file) @@ -97,6 +98,16 @@ jsonnet = ["dep:jsonnet-rs"] | `enquote symbol:String s:String` | `enquote "" "foo"` | `"\"foo\""` | | `first_non_empty s:String+` | `first_non_empty "" null "foo" "bar"` | `"foo"` | +## Regular expression + +| usage | output | +| ----------------------------------------- | -------------------------- | +| `{{ regex_is_match pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" }}` | `true` | +| `{{#if (regex_is_match pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}ok{{/if}}` | `ok` | +| `{{ regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" }}` | `[object]` | +| `{{ json_to_str( regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}` | `{"_0":"today","_1":"t","_2":"o","_3":"y","first":"t","last":"y"}` | +| `{{ set captures=( regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}{{ captures.last }}` | `y` | + ## Http content Helper able to render body response from an http request. diff --git a/src/assign_helpers.rs b/src/assign_helpers.rs index d322e2d..5df9799 100644 --- a/src/assign_helpers.rs +++ b/src/assign_helpers.rs @@ -82,6 +82,10 @@ mod tests { (r##"{{ set foo="{}" }}{{ foo }}"##, r##"{}"##), (r##"{{ set foo={} }}{{ foo }}"##, r##"[object]"##), (r##"{{ set foo={"bar": 33} }}{{ foo }}"##, r##"[object]"##,), + ( + r##"{{ set foo={"bar": 33} }}{{ json_to_str foo }}"##, + r##"{"bar":33}"##, + ), (r##"{{ set foo={"bar": 33} }}{{ foo.bar }}"##, r##"33"##,), ( r##"{{ set foo="hello world" }}{{ foo }}"##, @@ -94,7 +98,8 @@ mod tests { ( r##"{{ set foo="world" }}{{ set bar="hello" }}>{{ bar }} {{ foo }}<"##, r##">hello world<"##, - ) + ), + (r##"{{ set foo=(eq 12 12) }}{{ foo }}"##, r##"true"##,) ] } } diff --git a/src/lib.rs b/src/lib.rs index 87f33a5..688d858 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,8 @@ pub mod json_helpers; pub mod jsonnet_helpers; pub mod outputs; pub mod path_helpers; +#[cfg(feature = "regex")] +pub mod regex_helpers; #[cfg(feature = "jsontype")] pub mod region_helpers; #[cfg(feature = "string")] @@ -55,6 +57,8 @@ pub fn register(handlebars: &mut Handlebars) { file_helpers::register(handlebars); #[cfg(feature = "jsontype")] region_helpers::register(handlebars); + #[cfg(feature = "regex")] + regex_helpers::register(handlebars); } #[allow(dead_code)] diff --git a/src/regex_helpers.rs b/src/regex_helpers.rs new file mode 100644 index 0000000..3e1f391 --- /dev/null +++ b/src/regex_helpers.rs @@ -0,0 +1,125 @@ +use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson}; +use regex::Regex; + +#[allow(non_camel_case_types)] +pub struct regex_captures_fct; + +impl HelperDef for regex_captures_fct { + fn call_inner<'reg: 'rc, 'rc>( + &self, + h: &Helper<'rc>, + _: &'reg Handlebars, + _: &'rc Context, + _: &mut RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + let on = h + .hash_get("on") + .and_then(|v| v.value().as_str()) + .unwrap_or_default(); + let pattern = h + .hash_get("pattern") + .and_then(|v| v.value().as_str()) + .unwrap_or_default(); + let re = Regex::new(pattern).map_err(|err| crate::to_other_error(err.to_string()))?; + if let Some(caps) = re.captures(on) { + let collected = re + .capture_names() + .filter_map(|v| { + v.and_then(|name| { + caps.name(name).map(|m| { + ( + name.to_string(), + serde_json::Value::String(m.as_str().to_string()), + ) + }) + }) + }) + .chain(caps.iter().enumerate().filter_map(|(i, mm)| { + mm.map(|m| { + ( + format!("_{}", i), + serde_json::Value::String(m.as_str().to_string()), + ) + }) + })) + .collect::>(); + Ok(ScopedJson::Derived(serde_json::Value::Object(collected))) + } else { + Ok(ScopedJson::Derived(serde_json::Value::Null)) + } + } +} + +#[allow(non_camel_case_types)] +pub struct regex_is_match_fct; + +impl HelperDef for regex_is_match_fct { + fn call_inner<'reg: 'rc, 'rc>( + &self, + h: &Helper<'rc>, + _: &'reg Handlebars, + _: &'rc Context, + _: &mut RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + let on = h + .hash_get("on") + .and_then(|v| v.value().as_str()) + .unwrap_or_default(); + let pattern = h + .hash_get("pattern") + .and_then(|v| v.value().as_str()) + .unwrap_or_default(); + let re = Regex::new(pattern).map_err(|err| crate::to_other_error(err.to_string()))?; + Ok(ScopedJson::Derived(serde_json::Value::Bool( + re.is_match(on), + ))) + } +} + +pub fn register(handlebars: &mut Handlebars) { + handlebars.register_helper("regex_captures", Box::new(regex_captures_fct)); + handlebars.register_helper("regex_is_match", Box::new(regex_is_match_fct)); +} + +#[cfg(test)] +mod tests { + use crate::assert_renders; + use std::error::Error; + + #[test] + fn test_regex_captures() -> Result<(), Box> { + assert_renders![ + (r##"{{ regex_captures pattern="foo" on="" }}"##, r##""##), + ( + r##"{{ regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" }}"##, + r##"[object]"## + ), + ( + r##"{{ json_to_str( regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}"##, + r##"{"_0":"today","_1":"t","_2":"o","_3":"y","first":"t","last":"y"}"## + ), + ( + r##"{{ set captures=( regex_captures pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}{{ captures.last }}"##, + r##"y"## + ), + ] + } + + #[test] + fn test_regex_is_match() -> Result<(), Box> { + assert_renders![ + ( + r##"{{ regex_is_match pattern="foo" on="" }}"##, + r##"false"## + ), + ( + r##"{{ regex_is_match pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" }}"##, + r##"true"## + ), + ( + r##"{{#if (regex_is_match pattern="(?\\w)(\\w)(?:\\w)\\w(?\\w)" on="today" ) }}ok{{/if}}"##, + r##"ok"## + ), + ] + } +}