Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support 'replace' string helper #481

Merged
merged 1 commit into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions book/src/stubs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion book/src/stubs/response.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/src/model/response/template/helpers/any/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/model/response/template/helpers/any/of.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
2 changes: 1 addition & 1 deletion lib/src/model/response/template/helpers/any/regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
2 changes: 1 addition & 1 deletion lib/src/model/response/template/helpers/base64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
13 changes: 4 additions & 9 deletions lib/src/model/response/template/helpers/datetime.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -23,7 +22,7 @@ impl NowHelper {
}

fn fmt_with_custom_format(now: DateTime<Utc>, h: &Helper) -> Option<String> {
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)
Expand All @@ -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<Utc>, h: &Helper) -> DateTime<Utc> {
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)
Expand All @@ -65,7 +60,7 @@ impl NowHelper {
}

fn apply_timezone(now: DateTime<Utc>, h: &Helper) -> DateTime<Utc> {
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)
Expand Down
35 changes: 34 additions & 1 deletion lib/src/model/response/template/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion lib/src/model/response/template/helpers/numbers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
18 changes: 8 additions & 10 deletions lib/src/model/response/template/helpers/string.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::model::response::template::helpers::HelperExt;
use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson};
use serde_json::Value;

Expand All @@ -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)
}
Expand All @@ -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<ScopedJson<'reg, 'rc>, 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)
Expand Down
30 changes: 30 additions & 0 deletions lib/src/model/response/template/helpers/string_replace.rs
Original file line number Diff line number Diff line change
@@ -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<ScopedJson<'reg, 'rc>, 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())
}
}
7 changes: 2 additions & 5 deletions lib/src/model/response/template/helpers/trim.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
use crate::model::response::template::helpers::HelperExt;
use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson};
use serde_json::Value;

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<ScopedJson<'reg, 'rc>, RenderError> {
Self::value(h)
h.get_first_str_value()
.ok_or_else(|| RenderError::new("Invalid trim response template"))
.map(str::trim)
.map(Value::from)
Expand Down
2 changes: 1 addition & 1 deletion lib/src/model/response/template/helpers/url_encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
17 changes: 0 additions & 17 deletions lib/src/model/response/template/helpers/utils_str.rs

This file was deleted.

2 changes: 2 additions & 0 deletions lib/src/model/response/template/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use helpers::{
numbers::NumberHelper,
size::SizeHelper,
string::StringHelper,
string_replace::StringReplaceHelper,
trim::TrimHelper,
url_encode::UrlEncodingHelper,
};
Expand All @@ -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));
Expand Down
11 changes: 11 additions & 0 deletions lib/tests/resp/template/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
12 changes: 12 additions & 0 deletions lib/tests/stubs/resp/template/string/replace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"request": {
"method": "POST"
},
"response": {
"status": 200,
"body": "{{replace request.body 'a' 'b'}}",
"transformers": [
"response-template"
]
}
}