Skip to content

Commit 3370abc

Browse files
committed
Add support for regex match/replace on request & response bodies
1 parent a40ecc9 commit 3370abc

File tree

4 files changed

+125
-10
lines changed

4 files changed

+125
-10
lines changed

Diff for: src/components/common/editable-headers.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const EditableRawHeaders = observer((
7777

7878
allowEmptyValues={allowEmptyValues}
7979

80-
keyPattern={HEADER_NAME_PATTERN}
80+
keyValidation={HEADER_NAME_PATTERN}
8181
keyTitle="Header names must contain only alphanumeric characters and !#$%&'*+-.^_`|~ symbols"
8282

8383
keyPlaceholder='Header name'

Diff for: src/components/common/editable-pairs.tsx

+33-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import * as _ from 'lodash';
22
import * as React from 'react';
3-
import { action, observable, reaction } from 'mobx';
3+
import { action, autorun, comparer, observable, reaction } from 'mobx';
44
import { disposeOnUnmount, observer } from 'mobx-react';
55

66
import { styled } from '../../styles';
77

8-
import { clickOnEnter } from '../component-utils';
98
import { Button, TextInput } from './inputs';
109
import { Icon } from '../../icons';
1110

1211
export type Pair = { key: string, value: string, disabled?: true };
1312
export type PairsArray = Array<Pair>;
1413

1514
interface EditablePairsProps<R = PairsArray> {
15+
className?: string;
16+
1617
pairs: PairsArray;
1718

1819
onChange: (pairs: R) => void;
@@ -29,7 +30,8 @@ interface EditablePairsProps<R = PairsArray> {
2930
transformInput?: (pairs: PairsArray) => PairsArray;
3031

3132
keyTitle?: string;
32-
keyPattern?: string;
33+
// Either a pattern string, or a validation function
34+
keyValidation?: string | ((key: string) => true | string);
3335

3436
keyPlaceholder: string;
3537
valuePlaceholder: string;
@@ -111,6 +113,27 @@ export class EditablePairs<R> extends React.Component<EditablePairsProps<R>> {
111113
}
112114
}
113115
));
116+
117+
disposeOnUnmount(this, autorun(() => {
118+
const { keyValidation } = this.props;
119+
if (!_.isFunction(keyValidation)) return;
120+
121+
const inputs = this.containerRef?.current?.querySelectorAll('input');
122+
if (!inputs) return;
123+
124+
this.values.forEach((pair, i) => {
125+
const keyInput = inputs?.[i * 2];
126+
const validationResult = keyValidation(pair.key);
127+
128+
if (validationResult === true) {
129+
keyInput.setCustomValidity('');
130+
keyInput.reportValidity();
131+
} else {
132+
keyInput.setCustomValidity(validationResult);
133+
keyInput.reportValidity();
134+
}
135+
});
136+
}));
114137
}
115138

116139
private convert = (pairs: PairsArray): R => {
@@ -150,16 +173,22 @@ export class EditablePairs<R> extends React.Component<EditablePairsProps<R>> {
150173

151174
render() {
152175
const {
176+
className,
153177
keyTitle,
154-
keyPattern,
178+
keyValidation,
155179
keyPlaceholder,
156180
valuePlaceholder,
157181
allowEmptyValues
158182
} = this.props;
159183

160184
const { values, onChangeValues, containerRef } = this;
161185

186+
const keyPattern = typeof keyValidation === 'string'
187+
? keyValidation
188+
: undefined;
189+
162190
return <EditablePairsContainer
191+
className={className}
163192
ref={containerRef}
164193
>
165194
{ _.flatMap(values, ({ key, value, disabled }, i) => [

Diff for: src/components/mock/handler-config.tsx

+83-5
Original file line numberDiff line numberDiff line change
@@ -1117,8 +1117,9 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
11171117
'replaceBody',
11181118
'replaceBodyFromFile',
11191119
'updateJsonBody',
1120-
'patchJsonBody'
1121-
] as const;
1120+
'patchJsonBody',
1121+
'matchReplaceBody'
1122+
] as const satisfies ReadonlyArray<keyof RequestTransform & ResponseTransform>;
11221123

11231124
@computed
11241125
get bodyReplacementBuffer() {
@@ -1152,11 +1153,12 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
11521153
<option value='updateJsonBody'>Update a JSON { type } body by merging data</option>
11531154
{ advancedPatchesSupported && <>
11541155
<option value='patchJsonBody'>Update a JSON { type } body using JSON patch</option>
1156+
<option value='matchReplaceBody'>Match & replace text in the { type } body</option>
11551157
</> }
11561158
</SelectTransform>
11571159
{
11581160
selected === 'replaceBody'
1159-
? <RawBodyTransfomConfig
1161+
? <RawBodyTransformConfig
11601162
type={type}
11611163
body={bodyReplacementBuffer}
11621164
updateBody={setBodyReplacement}
@@ -1189,6 +1191,12 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
11891191
operations={transform.patchJsonBody!}
11901192
updateOperations={setJsonBodyPatch}
11911193
/>
1194+
: selected === 'matchReplaceBody'
1195+
? <MatchReplaceBodyTransformConfig
1196+
type={type}
1197+
replacements={transform.matchReplaceBody!}
1198+
updateReplacements={this.props.onChange('matchReplaceBody')}
1199+
/>
11921200
: selected === 'none'
11931201
? null
11941202
: unreachableCheck(selected)
@@ -1208,6 +1216,8 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
12081216
this.props.onChange('replaceBody')('');
12091217
} else if (value === 'replaceBodyFromFile') {
12101218
this.props.onChange('replaceBodyFromFile')('');
1219+
} else if (value === 'matchReplaceBody') {
1220+
this.props.onChange('matchReplaceBody')([]);
12111221
} else if (value === 'none') {
12121222
return;
12131223
} else unreachableCheck(value);
@@ -1249,7 +1259,7 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
12491259
};
12501260
};
12511261

1252-
const RawBodyTransfomConfig = (props: {
1262+
const RawBodyTransformConfig = (props: {
12531263
type: 'request' | 'response',
12541264
body: Buffer,
12551265
updateBody: (body: string) => void
@@ -1381,6 +1391,74 @@ const JsonPatchTransformConfig = (props: {
13811391
</TransformDetails>;
13821392
};
13831393

1394+
const MatchReplaceBodyTransformConfig = (props: {
1395+
type: 'request' | 'response',
1396+
replacements: Array<[RegExp | string, string]>,
1397+
updateReplacements: (replacements: Array<[RegExp | string, string]>) => void
1398+
}) => {
1399+
const [error, setError] = React.useState<Error>();
1400+
1401+
const [replacementPairs, updatePairs] = React.useState<PairsArray>(
1402+
props.replacements.map(([match, replace]) => ({
1403+
key: match instanceof RegExp
1404+
? match.source
1405+
// It's type-possible to get a string here (since Mockttp supports it)
1406+
// but it shouldn't be runtime-possible as we always use regex
1407+
: _.escapeRegExp(match),
1408+
value: replace
1409+
}))
1410+
);
1411+
1412+
React.useEffect(() => {
1413+
const validPairs = replacementPairs.filter((pair) =>
1414+
validateRegexMatcher(pair.key) === true
1415+
);
1416+
const invalidCount = replacementPairs.length - validPairs.length;
1417+
1418+
if (invalidCount > 0) {
1419+
setError(new Error(
1420+
`${invalidCount} regular expression${invalidCount === 1 ? ' is' : 's are'} invalid`
1421+
));
1422+
} else {
1423+
setError(undefined);
1424+
}
1425+
1426+
props.updateReplacements(validPairs.map(({ key, value }) =>
1427+
[new RegExp(key, 'g'), value]
1428+
));
1429+
}, [replacementPairs]);
1430+
1431+
return <TransformDetails>
1432+
<BodyHeader>
1433+
<SectionLabel>Regex matchers & replacements</SectionLabel>
1434+
{ error && <WarningIcon title={error.message} /> }
1435+
</BodyHeader>
1436+
<MonoKeyEditablePairs
1437+
pairs={replacementPairs}
1438+
onChange={updatePairs}
1439+
keyPlaceholder='Regular expression to match'
1440+
keyValidation={validateRegexMatcher}
1441+
valuePlaceholder='Replacement value'
1442+
allowEmptyValues={true}
1443+
/>
1444+
</TransformDetails>;
1445+
};
1446+
1447+
const MonoKeyEditablePairs = styled(EditablePairs<PairsArray>)`
1448+
input:nth-of-type(odd) {
1449+
font-family: ${p => p.theme.monoFontFamily};
1450+
}
1451+
`;
1452+
1453+
const validateRegexMatcher = (value: string) => {
1454+
try {
1455+
new RegExp(value, 'g');
1456+
return true;
1457+
} catch (e: any) {
1458+
return e.message ?? e.toString();
1459+
}
1460+
}
1461+
13841462
@observer
13851463
class PassThroughHandlerConfig extends HandlerConfig<
13861464
| PassThroughHandler
@@ -1583,7 +1661,7 @@ class EthCallResultHandlerConfig extends HandlerConfig<EthereumCallResultHandler
15831661
pairs={typeValuePairs}
15841662
onChange={this.onChange}
15851663
keyPlaceholder='Return value type (e.g. string, int256, etc)'
1586-
keyPattern={NATIVE_ETH_TYPES_PATTERN}
1664+
keyValidation={NATIVE_ETH_TYPES_PATTERN}
15871665
valuePlaceholder='Return value'
15881666
allowEmptyValues={true}
15891667
/>

Diff for: src/model/rules/definitions/http-rule-definitions.ts

+8
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,14 @@ serializr.createModelSchema(TransformingHandler, {
212212
updateHeaders: serializeWithUndefineds,
213213
updateJsonBody: serializeWithUndefineds,
214214
replaceBody: serializeBuffer,
215+
matchReplaceBody: serializr.list(
216+
serializr.custom(
217+
([key, value]: [RegExp, string]) =>
218+
[{ source: key.source, flags: key.flags }, value],
219+
([key, value]: [{ source: string, flags: string }, string]) =>
220+
[new RegExp(key.source, key.flags), value]
221+
)
222+
),
215223
'*': Object.assign(serializr.raw(), { pattern: { test: () => true } })
216224
})
217225
),

0 commit comments

Comments
 (0)