Skip to content

Commit 481d4fa

Browse files
committed
Add JSON patch to transform rules
1 parent 900a875 commit 481d4fa

File tree

5 files changed

+136
-27
lines changed

5 files changed

+136
-27
lines changed

package-lock.json

Lines changed: 32 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"dedent": "^0.7.0",
9090
"deserialize-error": "0.0.3",
9191
"dompurify": "^2.1.1",
92+
"fast-json-patch": "^3.1.1",
9293
"graphql": "^15.8.0",
9394
"har-validator": "^5.1.3",
9495
"http-encoding": "^1.3.0",
@@ -103,7 +104,7 @@
103104
"mobx-shallow-undo": "^1.0.0",
104105
"mobx-utils": "^5.1.0",
105106
"mockrtc": "^0.3.1",
106-
"mockttp": "^3.11.0",
107+
"mockttp": "^3.13.0",
107108
"monaco-editor": "^0.27.0",
108109
"node-forge": "^1.3.0",
109110
"openapi-directory": "^1.3.0",

src/components/mock/handler-config.tsx

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import * as React from 'react';
33
import { action, observable, reaction, autorun, observe, runInAction, computed } from 'mobx';
44
import { observer, disposeOnUnmount, inject } from 'mobx-react';
55
import * as dedent from 'dedent';
6+
import {
7+
Operation as JsonPatchOperation,
8+
validate as validateJsonPatch
9+
} from 'fast-json-patch';
610

711
import { Headers, RawHeaders } from '../../types';
812
import { css, styled } from '../../styles';
@@ -23,6 +27,10 @@ import {
2327
HEADER_NAME_REGEX,
2428
setHeaderValue
2529
} from '../../util/headers';
30+
import {
31+
ADVANCED_PATCH_TRANSFORMS,
32+
serverSupports
33+
} from '../../services/service-versions';
2634

2735
import {
2836
Handler,
@@ -1105,7 +1113,8 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
11051113
private static readonly FIELDS = [
11061114
'replaceBody',
11071115
'replaceBodyFromFile',
1108-
'updateJsonBody'
1116+
'updateJsonBody',
1117+
'patchJsonBody'
11091118
] as const;
11101119

11111120
@computed
@@ -1120,9 +1129,12 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
11201129
onTransformTypeChange,
11211130
setBodyReplacement,
11221131
selectBodyReplacementFile,
1123-
setJsonBodyUpdate
1132+
setJsonBodyUpdate,
1133+
setJsonBodyPatch
11241134
} = this;
11251135

1136+
const advancedPatchesSupported = serverSupports(ADVANCED_PATCH_TRANSFORMS);
1137+
11261138
const selected = _.find(BodyTransformConfig.FIELDS, (field) =>
11271139
transform[field] !== undefined
11281140
) ?? 'none';
@@ -1134,7 +1146,10 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
11341146
<option value='none'>Pass through the real { type } body</option>
11351147
<option value='replaceBody'>Replace the { type } body with a fixed value</option>
11361148
<option value='replaceBodyFromFile'>Replace the { type } body with a file</option>
1137-
<option value='updateJsonBody'>Update values within a JSON { type } body</option>
1149+
<option value='updateJsonBody'>Update a JSON { type } body by merging data</option>
1150+
{ advancedPatchesSupported && <>
1151+
<option value='patchJsonBody'>Update a JSON { type } body using JSON patch</option>
1152+
</> }
11381153
</SelectTransform>
11391154
{
11401155
selected === 'replaceBody'
@@ -1165,7 +1180,15 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
11651180
body={transform.updateJsonBody!}
11661181
updateBody={setJsonBodyUpdate}
11671182
/>
1168-
: null
1183+
: selected === 'patchJsonBody'
1184+
? <JsonPatchTransformConfig
1185+
type={type}
1186+
operations={transform.patchJsonBody!}
1187+
updateOperations={setJsonBodyPatch}
1188+
/>
1189+
: selected === 'none'
1190+
? null
1191+
: unreachableCheck(selected)
11691192
}
11701193
</TransformConfig>;
11711194
}
@@ -1176,11 +1199,15 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
11761199
this.clearValues();
11771200
if (value === 'updateJsonBody') {
11781201
this.props.onChange('updateJsonBody')({});
1202+
} else if (value === 'patchJsonBody') {
1203+
this.props.onChange('patchJsonBody')([]);
11791204
} else if (value === 'replaceBody') {
11801205
this.props.onChange('replaceBody')('');
11811206
} else if (value === 'replaceBodyFromFile') {
11821207
this.props.onChange('replaceBodyFromFile')('');
1183-
}
1208+
} else if (value === 'none') {
1209+
return;
1210+
} else unreachableCheck(value);
11841211
};
11851212

11861213
@action.bound
@@ -1211,6 +1238,12 @@ class BodyTransformConfig<T extends RequestTransform | ResponseTransform> extend
12111238
this.clearValues();
12121239
this.props.onChange('updateJsonBody')(body);
12131240
};
1241+
1242+
@action.bound
1243+
setJsonBodyPatch(operations: Array<JsonPatchOperation>) {
1244+
this.clearValues();
1245+
this.props.onChange('patchJsonBody')(operations);
1246+
};
12141247
};
12151248

12161249
const RawBodyTransfomConfig = (props: {
@@ -1276,7 +1309,7 @@ const JsonUpdateTransformConfig = (props: {
12761309

12771310
return <TransformDetails>
12781311
<BodyHeader>
1279-
<SectionLabel>JSON { props.type } body patch</SectionLabel>
1312+
<SectionLabel>JSON to merge into { props.type } body</SectionLabel>
12801313
{ error && <WarningIcon title={error.message} /> }
12811314

12821315
<StandaloneFormatButton
@@ -1296,6 +1329,55 @@ const JsonUpdateTransformConfig = (props: {
12961329
</TransformDetails>;
12971330
};
12981331

1332+
const JsonPatchTransformConfig = (props: {
1333+
type: 'request' | 'response',
1334+
operations: Array<JsonPatchOperation>,
1335+
updateOperations: (operations: Array<JsonPatchOperation>) => void
1336+
}) => {
1337+
const [error, setError] = React.useState<Error>();
1338+
1339+
const [operationsString, setOperationsString] = React.useState<string>(
1340+
JSON.stringify(props.operations, null, 2)
1341+
);
1342+
1343+
React.useEffect(() => {
1344+
try {
1345+
const parsedInput = JSON.parse(operationsString);
1346+
1347+
const validationError = validateJsonPatch(parsedInput);
1348+
if (validationError) throw validationError;
1349+
1350+
props.updateOperations(parsedInput);
1351+
setError(undefined);
1352+
} catch (e) {
1353+
setError(asError(e));
1354+
}
1355+
}, [operationsString]);
1356+
1357+
return <TransformDetails>
1358+
<BodyHeader>
1359+
<SectionLabel>JSON { props.type } body patch (see <a
1360+
href="https://jsonpatch.com/"
1361+
>jsonpatch.com</a>)</SectionLabel>
1362+
{ error && <WarningIcon title={error.message} /> }
1363+
1364+
<StandaloneFormatButton
1365+
format='json'
1366+
content={asBuffer(operationsString)}
1367+
onFormatted={setOperationsString}
1368+
/>
1369+
</BodyHeader>
1370+
<BodyContainer>
1371+
<SelfSizedEditor
1372+
contentId={null}
1373+
language='json'
1374+
value={operationsString}
1375+
onChange={(content) => setOperationsString(content)}
1376+
/>
1377+
</BodyContainer>
1378+
</TransformDetails>;
1379+
};
1380+
12991381
@observer
13001382
class PassThroughHandlerConfig extends HandlerConfig<
13011383
| PassThroughHandler

src/model/rules/rules.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as _ from 'lodash';
22
import {
3-
serverVersion as serverVersionObservable,
4-
versionSatisfies,
3+
serverSupports,
54
BODY_MATCHING_RANGE,
65
HOST_MATCHER_SERVER_RANGE,
76
FROM_FILE_HANDLER_SERVER_RANGE,
@@ -94,16 +93,6 @@ const PartVersionRequirements: {
9493
'reset-connection': CONNECTION_RESET_SUPPORTED
9594
};
9695

97-
const serverSupports = (versionRequirement: string | undefined) => {
98-
if (!versionRequirement || versionRequirement === '*') return true;
99-
100-
// If we haven't got the server version yet, assume it doesn't support this
101-
if (serverVersionObservable.state !== 'fulfilled') return false;
102-
103-
const version = serverVersionObservable.value as string; // Fulfilled -> string value
104-
return versionSatisfies(version, versionRequirement);
105-
}
106-
10796
/// --- Matchers ---
10897

10998
const MatchersByType = {

src/services/service-versions.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ export function versionSatisfies(version: string | Error | undefined, range: str
4242
semver.satisfies(version, range, { includePrerelease: true });
4343
}
4444

45+
// A quick way to check server support synchronously
46+
export function serverSupports(versionRequirement: string | undefined) {
47+
if (!versionRequirement || versionRequirement === '*') return true;
48+
49+
// If we haven't got the server version yet, assume anything specific is unsupported
50+
if (serverVersion.state !== 'fulfilled') return false;
51+
52+
const version = serverVersion.value as string; // Fulfilled -> string value
53+
return versionSatisfies(version, versionRequirement);
54+
}
55+
4556
// Notable desktop versions:
4657
export const DESKTOP_HEADER_LIMIT_CONFIGURABLE = "^0.1.20 || ^1.0.0";
4758

@@ -72,4 +83,5 @@ export const RTC_RULES_SUPPORTED = '^1.11.0';
7283
export const TLS_PASSTHROUGH_SUPPORTED = '^1.12.0';
7384
export const CONNECTION_RESET_SUPPORTED = '^1.12.0';
7485
export const SERVER_REST_API_SUPPORTED = '^1.13.0';
75-
export const SERVER_SEND_API_SUPPORTED = '^1.13.0';
86+
export const SERVER_SEND_API_SUPPORTED = '^1.13.0';
87+
export const ADVANCED_PATCH_TRANSFORMS = '^1.18.0';

0 commit comments

Comments
 (0)