From 97c9d40560c3d6190d4c3d5de729f831cf0d07fd Mon Sep 17 00:00:00 2001 From: beltram Date: Wed, 28 Jun 2023 23:05:18 +0200 Subject: [PATCH] feat: support 'replace' string helper --- book/src/stubs/index.md | 1 + book/src/stubs/response.md | 6 +++- .../response/template/helpers/any/mod.rs | 2 +- .../model/response/template/helpers/any/of.rs | 2 +- .../response/template/helpers/any/regex.rs | 2 +- .../model/response/template/helpers/base64.rs | 2 +- .../response/template/helpers/datetime.rs | 13 +++---- .../model/response/template/helpers/mod.rs | 35 ++++++++++++++++++- .../response/template/helpers/numbers.rs | 2 +- .../model/response/template/helpers/string.rs | 18 +++++----- .../template/helpers/string_replace.rs | 30 ++++++++++++++++ .../model/response/template/helpers/trim.rs | 7 ++-- .../response/template/helpers/url_encode.rs | 2 +- .../response/template/helpers/utils_str.rs | 17 --------- lib/src/model/response/template/mod.rs | 2 ++ lib/tests/resp/template/string.rs | 11 ++++++ .../stubs/resp/template/string/replace.json | 12 +++++++ 17 files changed, 115 insertions(+), 49 deletions(-) create mode 100644 lib/src/model/response/template/helpers/string_replace.rs delete mode 100644 lib/src/model/response/template/helpers/utils_str.rs create mode 100644 lib/tests/stubs/resp/template/string/replace.json diff --git a/book/src/stubs/index.md b/book/src/stubs/index.md index 817ecbf5..7ec01e67 100644 --- a/book/src/stubs/index.md +++ b/book/src/stubs/index.md @@ -99,6 +99,7 @@ You will find here in a single snippet **ALL** the fields/helpers available to y "number-is-odd": "{{isOdd 3}}", // or 'isEven' "string-capitalized": "{{capitalize mister}}", // or 'decapitalize' "string-uppercase": "{{upper mister}}", // or 'lower' + "string-replace": "{{replace request.body 'a' 'b'}}", // e.g. given "Handlebars" in request body returns "Hbndlebbrs" "number-stripes": "{{stripes request.body 'if-even' 'if-odd'}}", "string-trim": "{{trim request.body}}", // removes leading & trailing whitespaces "size": "{{size request.body}}", // string length or array length diff --git a/book/src/stubs/response.md b/book/src/stubs/response.md index 27da3564..ab1afbaf 100644 --- a/book/src/stubs/response.md +++ b/book/src/stubs/response.md @@ -193,6 +193,7 @@ You also sometimes have to generate dynamic data or to transform existing one: "number-stripes": "{{stripes request.body 'if-even' 'if-odd'}}", "string-capitalized": "{{capitalize request.body}}", "string-uppercase": "{{upper request.body}}", + "string-replace": "{{replace request.body 'a' 'b'}}", "string-trim": "{{trim request.body}}", "size": "{{size request.body}}", "base64-encode": "{{base64 request.body padding=false}}", @@ -217,8 +218,11 @@ You also sometimes have to generate dynamic data or to transform existing one: * `timezone` for using a string timezone ( see [list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)) * `isOdd` or `isEven` returns a boolean whether the numeric value is an even or odd integer -* `capitalize` first letter to uppercase e.g. `mister` becomes `Mister` +* `capitalize` first letter to uppercase e.g. `mister` becomes `Mister`. There's also a `decapitalize` to do the + opposite. * `upper` or `lower` recapitalizes the whole word +* `replace` for replacing a pattern with given input e.g. `{{replace request.body 'a' 'b'}}` will replace all the `a` in + the request body with `b` * `stripes` returns alternate values depending if the tested value is even or odd * `trim` removes leading & trailing whitespaces * `size` returns the number of bytes for a string (⚠️ not the number of characters) or the size of an array diff --git a/lib/src/model/response/template/helpers/any/mod.rs b/lib/src/model/response/template/helpers/any/mod.rs index bb9c24e9..489b40b8 100644 --- a/lib/src/model/response/template/helpers/any/mod.rs +++ b/lib/src/model/response/template/helpers/any/mod.rs @@ -1,7 +1,7 @@ use crate::StubrResult; use handlebars::{Context, Helper, Output, RenderContext, RenderError}; -use super::{super::verify::Verifiable, utils_str::ValueExt, verify::VerifyDetect}; +use super::{super::verify::Verifiable, verify::VerifyDetect, ValueExt}; pub mod alpha_numeric; pub mod boolean; diff --git a/lib/src/model/response/template/helpers/any/of.rs b/lib/src/model/response/template/helpers/any/of.rs index 3cd9d6d9..a5ee548f 100644 --- a/lib/src/model/response/template/helpers/any/of.rs +++ b/lib/src/model/response/template/helpers/any/of.rs @@ -2,7 +2,7 @@ use handlebars::{Context, Handlebars, Helper, HelperDef, HelperResult, Output, P use itertools::Itertools; use rand::prelude::IteratorRandom; -use crate::{model::response::template::helpers::utils_str::ValueExt, StubrError, StubrResult}; +use crate::{model::response::template::helpers::ValueExt, StubrError, StubrResult}; use super::{super::verify::VerifyDetect, AnyTemplate}; diff --git a/lib/src/model/response/template/helpers/any/regex.rs b/lib/src/model/response/template/helpers/any/regex.rs index e892b79f..5045689a 100644 --- a/lib/src/model/response/template/helpers/any/regex.rs +++ b/lib/src/model/response/template/helpers/any/regex.rs @@ -5,7 +5,7 @@ use crate::gen::regex::RegexRndGenerator; use crate::{StubrError, StubrResult}; use super::{ - super::{utils_str::ValueExt, verify::VerifyDetect}, + super::{verify::VerifyDetect, ValueExt}, AnyTemplate, }; diff --git a/lib/src/model/response/template/helpers/base64.rs b/lib/src/model/response/template/helpers/base64.rs index e8a47dd2..4a5fd4cc 100644 --- a/lib/src/model/response/template/helpers/base64.rs +++ b/lib/src/model/response/template/helpers/base64.rs @@ -3,7 +3,7 @@ use std::str::from_utf8; use handlebars::{Context, Handlebars, Helper, HelperDef, HelperResult, Output, PathAndJson, RenderContext, RenderError}; use serde_json::Value; -use super::utils_str::ValueExt; +use super::ValueExt; pub struct Base64Helper; diff --git a/lib/src/model/response/template/helpers/datetime.rs b/lib/src/model/response/template/helpers/datetime.rs index 53d2eafa..5068791d 100644 --- a/lib/src/model/response/template/helpers/datetime.rs +++ b/lib/src/model/response/template/helpers/datetime.rs @@ -1,13 +1,12 @@ use std::time::{SystemTime, UNIX_EPOCH}; +use super::HelperExt; use chrono::{prelude::*, Duration}; use chrono_tz::Tz; use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson}; use humantime::parse_duration; use serde_json::Value; -use super::utils_str::ValueExt; - pub struct NowHelper; impl NowHelper { @@ -23,7 +22,7 @@ impl NowHelper { } fn fmt_with_custom_format(now: DateTime, h: &Helper) -> Option { - if let Some(format) = Self::get_hash(h, Self::FORMAT) { + if let Some(format) = h.get_str_hash(Self::FORMAT) { match format { Self::EPOCH => SystemTime::now() .duration_since(UNIX_EPOCH) @@ -40,12 +39,8 @@ impl NowHelper { } } - fn get_hash<'a>(h: &'a Helper, key: &str) -> Option<&'a str> { - h.hash_get(key)?.relative_path().map(String::escape_single_quotes) - } - fn apply_offset(now: DateTime, h: &Helper) -> DateTime { - Self::get_hash(h, Self::OFFSET) + h.get_str_hash(Self::OFFSET) .map(|it| it.replace(' ', "")) .and_then(|offset| Self::compute_offset(now, offset)) .unwrap_or(now) @@ -65,7 +60,7 @@ impl NowHelper { } fn apply_timezone(now: DateTime, h: &Helper) -> DateTime { - Self::get_hash(h, Self::TIMEZONE) + h.get_str_hash(Self::TIMEZONE) .and_then(|timezone| timezone.parse().ok()) .map(|tz: Tz| tz.offset_from_utc_datetime(&now.naive_utc()).fix().local_minus_utc()) .map(i64::from) diff --git a/lib/src/model/response/template/helpers/mod.rs b/lib/src/model/response/template/helpers/mod.rs index 01438791..43ed6eca 100644 --- a/lib/src/model/response/template/helpers/mod.rs +++ b/lib/src/model/response/template/helpers/mod.rs @@ -5,7 +5,40 @@ pub mod json_path; pub mod numbers; pub mod size; pub mod string; +pub mod string_replace; pub mod trim; pub mod url_encode; -pub mod utils_str; pub mod verify; + +trait HelperExt { + fn get_str_hash(&self, key: &str) -> Option<&str>; + fn get_first_str_value(&self) -> Option<&str>; +} + +impl HelperExt for handlebars::Helper<'_, '_> { + fn get_str_hash(&self, key: &str) -> Option<&str> { + self.hash_get(key)?.relative_path().map(String::escape_single_quotes) + } + + fn get_first_str_value(&self) -> Option<&str> { + self.param(0)?.value().as_str() + } +} + +pub trait ValueExt { + const QUOTE: char = '\''; + + fn escape_single_quotes(&self) -> &str; +} + +impl ValueExt for String { + fn escape_single_quotes(&self) -> &str { + self.trim_start_matches(Self::QUOTE).trim_end_matches(Self::QUOTE) + } +} + +impl ValueExt for str { + fn escape_single_quotes(&self) -> &str { + self.trim_start_matches(Self::QUOTE).trim_end_matches(Self::QUOTE) + } +} diff --git a/lib/src/model/response/template/helpers/numbers.rs b/lib/src/model/response/template/helpers/numbers.rs index 5e6f57ef..640b8997 100644 --- a/lib/src/model/response/template/helpers/numbers.rs +++ b/lib/src/model/response/template/helpers/numbers.rs @@ -3,7 +3,7 @@ use std::ops::Not; use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson}; use serde_json::Value; -use super::utils_str::ValueExt; +use super::ValueExt; pub struct NumberHelper; diff --git a/lib/src/model/response/template/helpers/string.rs b/lib/src/model/response/template/helpers/string.rs index 5429dcf9..fe1d9ad4 100644 --- a/lib/src/model/response/template/helpers/string.rs +++ b/lib/src/model/response/template/helpers/string.rs @@ -1,3 +1,4 @@ +use crate::model::response::template::helpers::HelperExt; use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson}; use serde_json::Value; @@ -9,10 +10,6 @@ impl StringHelper { pub const UPPER: &'static str = "upper"; pub const LOWER: &'static str = "lower"; - fn value<'a>(h: &'a Helper) -> Option<&'a str> { - h.params().get(0)?.value().as_str() - } - fn capitalize(value: &str) -> String { Self::map_first(value, char::to_ascii_uppercase) } @@ -33,14 +30,15 @@ impl HelperDef for StringHelper { fn call_inner<'reg: 'rc, 'rc>( &self, h: &Helper<'reg, 'rc>, _: &'reg Handlebars<'reg>, _: &'rc Context, _: &mut RenderContext<'reg, 'rc>, ) -> Result, RenderError> { - Self::value(h) + h.get_first_str_value() .map(|value| match h.name() { - Self::UPPER => value.to_uppercase(), - Self::LOWER => value.to_lowercase(), - Self::CAPITALIZE => Self::capitalize(value), - Self::DECAPITALIZE => Self::decapitalize(value), - _ => panic!("Unexpected error"), + Self::UPPER => Ok(value.to_uppercase()), + Self::LOWER => Ok(value.to_lowercase()), + Self::CAPITALIZE => Ok(Self::capitalize(value)), + Self::DECAPITALIZE => Ok(Self::decapitalize(value)), + _ => Err(RenderError::new("Unsupported string helper")), }) + .transpose()? .ok_or_else(|| RenderError::new("Invalid string case transform response template")) .map(Value::from) .map(ScopedJson::from) diff --git a/lib/src/model/response/template/helpers/string_replace.rs b/lib/src/model/response/template/helpers/string_replace.rs new file mode 100644 index 00000000..fb5b01cf --- /dev/null +++ b/lib/src/model/response/template/helpers/string_replace.rs @@ -0,0 +1,30 @@ +use super::ValueExt; +use crate::model::response::template::helpers::HelperExt; +use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson}; +use serde_json::Value; + +pub struct StringReplaceHelper; + +impl StringReplaceHelper { + pub const REPLACE: &'static str = "replace"; +} + +impl HelperDef for StringReplaceHelper { + fn call_inner<'reg: 'rc, 'rc>( + &self, h: &Helper<'reg, 'rc>, _: &'reg Handlebars<'reg>, _: &'rc Context, _: &mut RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + let value = h.get_first_str_value().ok_or(RenderError::new( + "Missing value after 'replace' helper e.g. {{replace request.body ...}}", + ))?; + let (placeholder, replacer) = h + .param(1) + .zip(h.param(2)) + .and_then(|(p, r)| p.relative_path().zip(r.relative_path())) + .map(|(p, r)| (p.escape_single_quotes(), r.escape_single_quotes())) + .ok_or(RenderError::new( + "Missing values after 'replace' helper e.g. {{replace request.body 'apple' 'peach'}}", + ))?; + let replaced = value.replace(placeholder, replacer); + Ok(Value::from(replaced).into()) + } +} diff --git a/lib/src/model/response/template/helpers/trim.rs b/lib/src/model/response/template/helpers/trim.rs index 78357b7d..6a41ef3d 100644 --- a/lib/src/model/response/template/helpers/trim.rs +++ b/lib/src/model/response/template/helpers/trim.rs @@ -1,3 +1,4 @@ +use crate::model::response::template::helpers::HelperExt; use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson}; use serde_json::Value; @@ -5,17 +6,13 @@ pub struct TrimHelper; impl TrimHelper { pub const NAME: &'static str = "trim"; - - fn value<'a>(h: &'a Helper) -> Option<&'a str> { - h.params().get(0)?.value().as_str() - } } impl HelperDef for TrimHelper { fn call_inner<'reg: 'rc, 'rc>( &self, h: &Helper<'reg, 'rc>, _: &'reg Handlebars<'reg>, _: &'rc Context, _: &mut RenderContext<'reg, 'rc>, ) -> Result, RenderError> { - Self::value(h) + h.get_first_str_value() .ok_or_else(|| RenderError::new("Invalid trim response template")) .map(str::trim) .map(Value::from) diff --git a/lib/src/model/response/template/helpers/url_encode.rs b/lib/src/model/response/template/helpers/url_encode.rs index e594e497..e7ad6528 100644 --- a/lib/src/model/response/template/helpers/url_encode.rs +++ b/lib/src/model/response/template/helpers/url_encode.rs @@ -2,7 +2,7 @@ use handlebars::{Context, Handlebars, Helper, HelperDef, PathAndJson, RenderCont use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC}; use serde_json::Value; -use super::utils_str::ValueExt; +use super::ValueExt; pub struct UrlEncodingHelper; diff --git a/lib/src/model/response/template/helpers/utils_str.rs b/lib/src/model/response/template/helpers/utils_str.rs deleted file mode 100644 index 521db6e7..00000000 --- a/lib/src/model/response/template/helpers/utils_str.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub trait ValueExt { - const QUOTE: char = '\''; - - fn escape_single_quotes(&self) -> &str; -} - -impl ValueExt for String { - fn escape_single_quotes(&self) -> &str { - self.trim_start_matches(Self::QUOTE).trim_end_matches(Self::QUOTE) - } -} - -impl ValueExt for str { - fn escape_single_quotes(&self) -> &str { - self.trim_start_matches(Self::QUOTE).trim_end_matches(Self::QUOTE) - } -} diff --git a/lib/src/model/response/template/mod.rs b/lib/src/model/response/template/mod.rs index 724351d2..de76c35f 100644 --- a/lib/src/model/response/template/mod.rs +++ b/lib/src/model/response/template/mod.rs @@ -22,6 +22,7 @@ use helpers::{ numbers::NumberHelper, size::SizeHelper, string::StringHelper, + string_replace::StringReplaceHelper, trim::TrimHelper, url_encode::UrlEncodingHelper, }; @@ -47,6 +48,7 @@ lazy_static! { handlebars.register_helper(StringHelper::DECAPITALIZE, Box::new(StringHelper)); handlebars.register_helper(StringHelper::UPPER, Box::new(StringHelper)); handlebars.register_helper(StringHelper::LOWER, Box::new(StringHelper)); + handlebars.register_helper(StringReplaceHelper::REPLACE, Box::new(StringReplaceHelper)); handlebars.register_helper(SizeHelper::NAME, Box::new(SizeHelper)); handlebars.register_helper(AnyRegex::NAME, Box::new(AnyRegex)); handlebars.register_helper(AnyNonBlank::NAME, Box::new(AnyNonBlank)); diff --git a/lib/tests/resp/template/string.rs b/lib/tests/resp/template/string.rs index 7da75008..b70554cf 100644 --- a/lib/tests/resp/template/string.rs +++ b/lib/tests/resp/template/string.rs @@ -44,3 +44,14 @@ async fn should_template_lowercase() { .expect_body_text_eq("john") .expect_content_type_text(); } + +#[async_std::test] +#[stubr::mock("resp/template/string/replace.json")] +async fn should_template_replace() { + post(stubr.uri()) + .body("Handlebars") + .await + .expect_status_ok() + .expect_body_text_eq("Hbndlebbrs") + .expect_content_type_text(); +} diff --git a/lib/tests/stubs/resp/template/string/replace.json b/lib/tests/stubs/resp/template/string/replace.json new file mode 100644 index 00000000..b2ed29e7 --- /dev/null +++ b/lib/tests/stubs/resp/template/string/replace.json @@ -0,0 +1,12 @@ +{ + "request": { + "method": "POST" + }, + "response": { + "status": 200, + "body": "{{replace request.body 'a' 'b'}}", + "transformers": [ + "response-template" + ] + } +} \ No newline at end of file