Skip to content

Commit 735e69a

Browse files
feat: implementing cleaner handling of JSON in cURL snippets (Kong#256)
Co-authored-by: Dimitri Mitropoulos <[email protected]>
1 parent 2c8b558 commit 735e69a

7 files changed

Lines changed: 151 additions & 5 deletions

File tree

src/fixtures/runCustomFixtures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { writeFileSync } from 'fs';
2-
import { readFile, writeFile } from 'fs/promises';
2+
import { readFile } from 'fs/promises';
33
import path from 'path';
44

55
import { HTTPSnippet, Request } from '../httpsnippet';

src/helpers/headers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ValueOf } from 'type-fest';
2+
13
type Headers<T> = Record<string, T>;
24

35
/**
@@ -22,3 +24,19 @@ export const getHeader = <T>(headers: Headers<T>, name: string) => {
2224
*/
2325
export const hasHeader = <T>(headers: Headers<T>, name: string) =>
2426
Boolean(getHeaderName(headers, name));
27+
28+
const mimeTypeJson = [
29+
'application/json',
30+
'application/x-json',
31+
'text/json',
32+
'text/x-json',
33+
'+json',
34+
] as const;
35+
36+
type MimeTypeJson = `${string}${typeof mimeTypeJson[number]}${string}`;
37+
38+
/**
39+
* Determines if a given mimetype is JSON, or a variant of such.
40+
*/
41+
export const isMimeTypeJSON = (mimeType: string): mimeType is MimeTypeJson =>
42+
mimeTypeJson.some(type => mimeType.includes(type));

src/targets/shell/curl/client.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import applicationFormEncoded from '../../../fixtures/requests/application-form-encoded.json';
2+
import applicationJson from '../../../fixtures/requests/application-json.json';
23
import full from '../../../fixtures/requests/full.json';
34
import https from '../../../fixtures/requests/https.json';
45
import nested from '../../../fixtures/requests/nested.json';
@@ -96,5 +97,55 @@ runCustomFixtures({
9697
},
9798
expected: 'insecure-skip-verify.sh',
9899
},
100+
{
101+
it: 'should send JSON-encoded data with single quotes within a HEREDOC',
102+
input: {
103+
method: 'POST',
104+
url: 'http://mockbin.com/har',
105+
headers: [
106+
{
107+
name: 'content-type',
108+
value: 'application/json',
109+
},
110+
],
111+
postData: {
112+
mimeType: 'application/json',
113+
text: '{"number":1,"string":"f\'oo"}',
114+
},
115+
} as Request,
116+
options: {
117+
prettifyJson: true,
118+
},
119+
expected: 'jsonObj-with-singlequotes.sh',
120+
},
121+
{
122+
it: 'should prettify simple/short JSON if prettifyJson is true',
123+
input: {
124+
url: 'http://mockbin.com/har',
125+
method: 'POST',
126+
headers: [
127+
{
128+
name: 'content-type',
129+
value: 'application/json',
130+
},
131+
],
132+
postData: {
133+
text: '{"foo": "bar"}',
134+
mimeType: 'application/json',
135+
},
136+
} as Request,
137+
options: {
138+
prettifyJson: true,
139+
},
140+
expected: 'prettify-short-json.sh',
141+
},
142+
{
143+
it: 'should prettify complex json if prettifyJson is true',
144+
input: applicationJson as Request,
145+
options: {
146+
prettifyJson: true,
147+
},
148+
expected: 'application-json-prettified.sh',
149+
},
99150
],
100151
});

src/targets/shell/curl/client.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*/
1111

1212
import { CodeBuilder } from '../../../helpers/code-builder';
13-
import { getHeaderName } from '../../../helpers/headers';
13+
import { getHeaderName, isMimeTypeJSON } from '../../../helpers/headers';
1414
import { quote } from '../../../helpers/shell';
1515
import { Client } from '../../targets';
1616

@@ -19,6 +19,7 @@ export interface CurlOptions {
1919
globOff?: boolean;
2020
indent?: string | false;
2121
insecureSkipVerify?: boolean;
22+
prettifyJson?: boolean;
2223
short?: boolean;
2324
}
2425

@@ -61,6 +62,7 @@ export const curl: Client<CurlOptions> = {
6162
globOff = false,
6263
indent = ' ',
6364
insecureSkipVerify = false,
65+
prettifyJson = false,
6466
short = false,
6567
} = options;
6668

@@ -148,11 +150,44 @@ export const curl: Client<CurlOptions> = {
148150
}
149151
break;
150152

151-
default:
153+
default: {
152154
// raw request body
153-
if (postData.text) {
154-
push(`${binary ? '--data-binary' : arg('data')} ${quote(postData.text)}`);
155+
if (!postData.text) {
156+
break;
157+
}
158+
159+
const flag = binary ? '--data-binary' : arg('data');
160+
161+
let builtPayload = false;
162+
// If we're dealing with a JSON variant, and our payload is JSON let's make it look a little nicer.
163+
if (isMimeTypeJSON(postData.mimeType)) {
164+
// If our postData is less than 20 characters, let's keep it all on one line so as to not make the snippet overly lengthy.
165+
const couldBeJSON = postData.text.length > 2;
166+
if (couldBeJSON && prettifyJson) {
167+
try {
168+
const jsonPayload = JSON.parse(postData.text);
169+
170+
// If the JSON object has a single quote we should prepare it inside of a HEREDOC because the single quote in something like `string's` can't be escaped when used with `--data`.
171+
//
172+
// Basically this boils down to `--data @- <<EOF...EOF` vs `--data '...'`.
173+
builtPayload = true;
174+
175+
const payload = JSON.stringify(jsonPayload, undefined, indent as string);
176+
if (postData.text.indexOf("'") > 0) {
177+
push(`${flag} @- <<EOF\n${payload}\nEOF`);
178+
} else {
179+
push(`${flag} '\n${payload}\n'`);
180+
}
181+
} catch (err) {
182+
// no-op
183+
}
184+
}
155185
}
186+
187+
if (!builtPayload) {
188+
push(`${flag} ${quote(postData.text)}`);
189+
}
190+
}
156191
}
157192

158193
return join();
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
curl --request POST \
2+
--url http://mockbin.com/har \
3+
--header 'content-type: application/json' \
4+
--data '
5+
{
6+
"number": 1,
7+
"string": "f\"oo",
8+
"arr": [
9+
1,
10+
2,
11+
3
12+
],
13+
"nested": {
14+
"a": "b"
15+
},
16+
"arr_mix": [
17+
1,
18+
"a",
19+
{
20+
"arr_mix_nested": {}
21+
}
22+
],
23+
"boolean": false
24+
}
25+
'
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
curl --request POST \
2+
--url http://mockbin.com/har \
3+
--header 'content-type: application/json' \
4+
--data @- <<EOF
5+
{
6+
"number": 1,
7+
"string": "f'oo"
8+
}
9+
EOF
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
curl --request POST \
2+
--url http://mockbin.com/har \
3+
--header 'content-type: application/json' \
4+
--data '
5+
{
6+
"foo": "bar"
7+
}
8+
'

0 commit comments

Comments
 (0)