Skip to content

Commit

Permalink
feat: support 'replace' string helper
Browse files Browse the repository at this point in the history
  • Loading branch information
beltram committed Jun 29, 2023
1 parent 1fff1e8 commit 8b040ff
Show file tree
Hide file tree
Showing 17 changed files with 115 additions and 49 deletions.
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"
]
}
}

0 comments on commit 8b040ff

Please sign in to comment.