Skip to content

Commit c4db059

Browse files
author
Ankit Saini
authored
Merge pull request #430 from postmanlabs/issue-426
Use json.dumps in Python codegens if Content-Type is JSON.
2 parents 5e6ee15 + 4eee6fb commit c4db059

File tree

6 files changed

+205
-22
lines changed

6 files changed

+205
-22
lines changed

codegens/python-http.client/lib/python-httpclient.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ self = module.exports = {
9999
var snippet = '',
100100
indentation = '',
101101
identity = '',
102-
url, host, path, query;
102+
url, host, path, query, contentType;
103103

104104
if (_.isFunction(options)) {
105105
callback = options;
@@ -128,7 +128,13 @@ self = module.exports = {
128128
query = '';
129129
}
130130

131+
contentType = request.headers.get('Content-Type');
131132
snippet += 'import http.client\n';
133+
134+
// If contentType is json then include the json module for later use
135+
if (contentType && (contentType === 'application/json' || contentType.match(/\+json$/))) {
136+
snippet += 'import json\n';
137+
}
132138
if (request.body && request.body.mode === 'formdata') {
133139
snippet += 'import mimetypes\n';
134140
snippet += 'from codecs import encode\n';
@@ -179,8 +185,9 @@ self = module.exports = {
179185
formdata: formdataArray
180186
});
181187
}
182-
snippet += parseBody(request.toJSON(), indentation, options.requestBodyTrim);
183-
if (request.body && !request.headers.has('Content-Type')) {
188+
189+
snippet += parseBody(request.toJSON(), indentation, options.requestBodyTrim, contentType);
190+
if (request.body && !contentType) {
184191
if (request.body.mode === 'file') {
185192
request.addHeader({
186193
key: 'Content-Type',

codegens/python-http.client/lib/util/parseBody.js

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,51 @@
11
var _ = require('../lodash'),
22
sanitize = require('./sanitize').sanitize,
3-
path = require('path');
3+
path = require('path'),
4+
trueToken = '__PYTHON#%0True__',
5+
falseToken = '__PYTHON#%0False__',
6+
nullToken = '__PYTHON#%0NULL__';
7+
8+
/**
9+
* Convert true, false and null to Python equivalent True, False and None
10+
*
11+
* @param {String} key
12+
* @param {Object} value
13+
*/
14+
function replacer (key, value) {
15+
if (typeof value === 'boolean') {
16+
return value ? trueToken : falseToken;
17+
}
18+
else if (value === null) {
19+
return nullToken;
20+
}
21+
return value;
22+
}
23+
24+
/**
25+
* Convert JSON into a valid Python dict
26+
* The "true", "false" and "null" tokens are not valid in Python
27+
* so we need to convert them to "True", "False" and "None"
28+
*
29+
* @param {Object} jsonBody - JSON object to be converted
30+
* @param {Number} indentCount - Number of spaces to insert at each indentation level
31+
*/
32+
function pythonify (jsonBody, indentCount) {
33+
return JSON.stringify(jsonBody, replacer, indentCount)
34+
.replace(new RegExp(`"${trueToken}"`, 'g'), 'True')
35+
.replace(new RegExp(`"${falseToken}"`, 'g'), 'False')
36+
.replace(new RegExp(`"${nullToken}"`, 'g'), 'None');
37+
}
438

539
/**
640
* Used to parse the body of the postman SDK-request and return in the desired format
741
*
842
* @param {Object} request - postman SDK-request object
943
* @param {String} indentation - used for indenting snippet's structure
1044
* @param {Boolean} bodyTrim - whether to trim request body fields
45+
* @param {String} contentType - content type of body
1146
* @returns {String} - request body
1247
*/
13-
module.exports = function (request, indentation, bodyTrim) {
48+
module.exports = function (request, indentation, bodyTrim, contentType) {
1449
// used to check whether body is present in the request or not
1550
if (!_.isEmpty(request.body)) {
1651
var requestBody = '',
@@ -19,14 +54,25 @@ module.exports = function (request, indentation, bodyTrim) {
1954

2055
switch (request.body.mode) {
2156
case 'raw':
22-
if (!_.isEmpty(request.body[request.body.mode])) {
23-
requestBody += `payload = ${sanitize(request.body[request.body.mode],
24-
request.body.mode, bodyTrim)}\n`;
57+
if (_.isEmpty(request.body[request.body.mode])) {
58+
return 'payload = \'\'\n';
2559
}
26-
else {
27-
requestBody = 'payload = \'\'\n';
60+
// Match any application type whose underlying structure is json
61+
// For example application/vnd.api+json
62+
// All of them have +json as suffix
63+
if (contentType && (contentType === 'application/json' || contentType.match(/\+json$/))) {
64+
try {
65+
let jsonBody = JSON.parse(request.body[request.body.mode]);
66+
return `payload = json.dumps(${pythonify(jsonBody, indentation.length)})\n`;
67+
68+
}
69+
catch (error) {
70+
// Do nothing
71+
}
2872
}
29-
return requestBody;
73+
return `payload = ${sanitize(request.body[request.body.mode],
74+
request.body.mode, bodyTrim)}\n`;
75+
3076
case 'graphql':
3177
// eslint-disable-next-line no-case-declarations
3278
let query = request.body[request.body.mode].query,

codegens/python-http.client/test/unit/converter.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,43 @@ describe('Python-http.client converter', function () {
9999
});
100100
});
101101

102+
it('should convert JSON tokens into appropriate python tokens', function () {
103+
var request = new sdk.Request({
104+
'method': 'POST',
105+
'header': [
106+
{
107+
'key': 'Content-Type',
108+
'value': 'application/json',
109+
'type': 'text'
110+
}
111+
],
112+
'body': {
113+
'mode': 'raw',
114+
'raw': `{
115+
"true": true,
116+
"false": false,
117+
"null": null
118+
}`
119+
},
120+
'url': {
121+
'raw': 'https://example.com',
122+
'protocol': 'https',
123+
'host': [
124+
'example',
125+
'com'
126+
]
127+
}
128+
});
129+
convert(request, {}, function (error, snippet) {
130+
if (error) {
131+
expect.fail(null, null, error);
132+
}
133+
expect(snippet).to.be.a('string');
134+
expect(snippet).to.include('"true": True');
135+
expect(snippet).to.include('"false": False');
136+
expect(snippet).to.include('"null": None');
137+
});
138+
});
102139

103140
it('should generate snippet with Tab as an indent type', function () {
104141
convert(request, { indentType: 'Tab', indentCount: 1 }, function (error, snippet) {

codegens/python-requests/lib/python-requests.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ self = module.exports = {
9090
convert: function (request, options, callback) {
9191
var snippet = '',
9292
indentation = '',
93-
identity = '';
93+
identity = '',
94+
contentType;
9495

9596
if (_.isFunction(options)) {
9697
callback = options;
@@ -103,7 +104,15 @@ self = module.exports = {
103104

104105
identity = options.indentType === 'Tab' ? '\t' : ' ';
105106
indentation = identity.repeat(options.indentCount);
106-
snippet += 'import requests\n\n';
107+
contentType = request.headers.get('Content-Type');
108+
snippet += 'import requests\n';
109+
110+
// If contentType is json then include the json module for later use
111+
if (contentType && (contentType === 'application/json' || contentType.match(/\+json$/))) {
112+
snippet += 'import json\n';
113+
}
114+
115+
snippet += '\n';
107116
snippet += `url = "${sanitize(request.url.toString(), 'url')}"\n\n`;
108117

109118
// The following code handles multiple files in the same formdata param.
@@ -147,8 +156,8 @@ self = module.exports = {
147156
formdata: formdataArray
148157
});
149158
}
150-
snippet += `${parseBody(request.toJSON(), indentation, options.trimRequestBody)}`;
151-
if (request.body && !request.headers.has('Content-Type')) {
159+
snippet += `${parseBody(request.toJSON(), indentation, options.trimRequestBody, contentType)}`;
160+
if (request.body && !contentType) {
152161
if (request.body.mode === 'file') {
153162
request.addHeader({
154163
key: 'Content-Type',

codegens/python-requests/lib/util/parseBody.js

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
var _ = require('../lodash'),
22
sanitize = require('./sanitize').sanitize,
3+
trueToken = '__PYTHON#%0True__',
4+
falseToken = '__PYTHON#%0False__',
5+
nullToken = '__PYTHON#%0NULL__',
36
contentTypeHeaderMap = {
47
'aac': 'audio/aac',
58
'abw': 'application/x-abiword',
@@ -74,15 +77,47 @@ var _ = require('../lodash'),
7477
'7-zip': 'application/x-7z-compressed'
7578
};
7679

80+
/**
81+
* Convert true, false and null to Python equivalent True, False and None
82+
*
83+
* @param {String} key
84+
* @param {Object} value
85+
*/
86+
function replacer (key, value) {
87+
if (typeof value === 'boolean') {
88+
return value ? trueToken : falseToken;
89+
}
90+
else if (value === null) {
91+
return nullToken;
92+
}
93+
return value;
94+
}
95+
96+
/**
97+
* Convert JSON into a valid Python dict
98+
* The "true", "false" and "null" tokens are not valid in Python
99+
* so we need to convert them to "True", "False" and "None"
100+
*
101+
* @param {Object} jsonBody - JSON object to be converted
102+
* @param {Number} indentCount - Number of spaces to insert at each indentation level
103+
*/
104+
function pythonify (jsonBody, indentCount) {
105+
return JSON.stringify(jsonBody, replacer, indentCount)
106+
.replace(new RegExp(`"${trueToken}"`, 'g'), 'True')
107+
.replace(new RegExp(`"${falseToken}"`, 'g'), 'False')
108+
.replace(new RegExp(`"${nullToken}"`, 'g'), 'None');
109+
}
110+
77111
/**
78112
* Used to parse the body of the postman SDK-request and return in the desired format
79113
*
80114
* @param {Object} request - postman SDK-request object
81115
* @param {String} indentation - used for indenting snippet's structure
82116
* @param {Boolean} bodyTrim - whether to trim request body fields
117+
* @param {String} contentType - content-type of body
83118
* @returns {String} - request body
84119
*/
85-
module.exports = function (request, indentation, bodyTrim) {
120+
module.exports = function (request, indentation, bodyTrim, contentType) {
86121
// used to check whether body is present in the request or not
87122
if (request.body) {
88123
var requestBody = '',
@@ -92,14 +127,25 @@ module.exports = function (request, indentation, bodyTrim) {
92127

93128
switch (request.body.mode) {
94129
case 'raw':
95-
if (!_.isEmpty(request.body[request.body.mode])) {
96-
requestBody += `payload=${sanitize(request.body[request.body.mode],
97-
request.body.mode, bodyTrim)}\n`;
130+
if (_.isEmpty(request.body[request.body.mode])) {
131+
requestBody = 'payload = {}\n';
98132
}
99-
else {
100-
requestBody = 'payload={}\n';
133+
// Match any application type whose underlying structure is json
134+
// For example application/vnd.api+json
135+
// All of them have +json as suffix
136+
if (contentType && (contentType === 'application/json' || contentType.match(/\+json$/))) {
137+
try {
138+
let jsonBody = JSON.parse(request.body[request.body.mode]);
139+
return `payload = json.dumps(${pythonify(jsonBody, indentation.length)})\n`;
140+
141+
}
142+
catch (error) {
143+
// Do nothing
144+
}
101145
}
102-
return requestBody;
146+
return `payload = ${sanitize(request.body[request.body.mode],
147+
request.body.mode, bodyTrim)}\n`;
148+
103149
case 'graphql':
104150
// eslint-disable-next-line no-case-declarations
105151
let query = request.body[request.body.mode].query,

codegens/python-requests/test/unit/converter.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,44 @@ describe('Python- Requests converter', function () {
6363
});
6464
});
6565

66+
it('should convert JSON tokens into appropriate python tokens', function () {
67+
var request = new sdk.Request({
68+
'method': 'POST',
69+
'header': [
70+
{
71+
'key': 'Content-Type',
72+
'value': 'application/json',
73+
'type': 'text'
74+
}
75+
],
76+
'body': {
77+
'mode': 'raw',
78+
'raw': `{
79+
"true": true,
80+
"false": false,
81+
"null": null
82+
}`
83+
},
84+
'url': {
85+
'raw': 'https://example.com',
86+
'protocol': 'https',
87+
'host': [
88+
'example',
89+
'com'
90+
]
91+
}
92+
});
93+
convert(request, {}, function (error, snippet) {
94+
if (error) {
95+
expect.fail(null, null, error);
96+
}
97+
expect(snippet).to.be.a('string');
98+
expect(snippet).to.include('"true": True');
99+
expect(snippet).to.include('"false": False');
100+
expect(snippet).to.include('"null": None');
101+
});
102+
});
103+
66104
it('should generate snippets for no files in form data', function () {
67105
var request = new sdk.Request({
68106
'method': 'POST',

0 commit comments

Comments
 (0)