Skip to content

Commit 48201c2

Browse files
committed
Add new Rust Reqwest target
Taken from upstream
1 parent 5924fcd commit 48201c2

26 files changed

+1109
-213
lines changed

src/helpers/code-builder.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ CodeBuilder.prototype.blank = function () {
9393
return this
9494
}
9595

96+
/**
97+
* Add the line to the end of the last line. Creates a new line
98+
* if no lines exist yet.
99+
*/
100+
CodeBuilder.prototype.pushToLast = function (line) {
101+
if (!this.code) {
102+
this.push(line)
103+
}
104+
const updatedLine = `${this.code[this.code.length - 1]}${line}`
105+
this.code[this.code.length - 1] = updatedLine
106+
}
107+
96108
/**
97109
* Concatenate all current lines using the given lineJoin
98110
* @return {string}

src/targets/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = {
1818
python: require('./python'),
1919
r: require('./r'),
2020
ruby: require('./ruby'),
21+
rust: require('./rust'),
2122
shell: require('./shell'),
2223
swift: require('./swift')
2324
}

src/targets/rust/helpers.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use strict'
2+
3+
const util = require('util')
4+
5+
function concatValues (
6+
concatType,
7+
values,
8+
pretty,
9+
indentation,
10+
indentLevel
11+
) {
12+
const currentIndent = indentation.repeat(indentLevel)
13+
const closingBraceIndent = indentation.repeat(indentLevel - 1)
14+
const join = pretty ? `,\n${currentIndent}` : ', '
15+
const openingBrace = concatType === 'object' ? 'json!({' : '('
16+
const closingBrace = concatType === 'object' ? '})' : ')'
17+
18+
if (pretty) {
19+
return `${openingBrace}\n${currentIndent}${values.join(
20+
join
21+
)}\n${closingBraceIndent}${closingBrace}`
22+
}
23+
24+
return `${openingBrace}${values.join(join)}${closingBrace}`
25+
}
26+
27+
/**
28+
* Create a valid Rust string of a literal value using serde_json according to its type.
29+
*
30+
* @param {*} value Any Javascript literal
31+
* @param {Object} opts Target options
32+
* @return {string}
33+
*/
34+
exports.literalRepresentation = (
35+
value,
36+
opts,
37+
indentLevel
38+
) => {
39+
/*
40+
* Note: this version is almost entirely borrowed from the Python client helper. The
41+
* only real modification involves the braces and the types. The helper
42+
* could potentially be parameterised for reuse.
43+
*/
44+
indentLevel = indentLevel === undefined ? 1 : indentLevel + 1
45+
46+
switch (Object.prototype.toString.call(value)) {
47+
case '[object Number]':
48+
return value
49+
50+
case '[object Array]': {
51+
let pretty = false
52+
const valuesRep = value.map(v => {
53+
// Switch to prettify if the value is a dict with more than one key.
54+
if (Object.prototype.toString.call(v) === '[object Object]') {
55+
pretty = Object.keys(v).length > 1
56+
}
57+
return exports.literalRepresentation(v, opts, indentLevel)
58+
})
59+
return concatValues('array', valuesRep, pretty, opts.indent, indentLevel)
60+
}
61+
62+
case '[object Object]': {
63+
const keyValuePairs = []
64+
for (const k in value) {
65+
keyValuePairs.push(
66+
util.format('%s: %s',
67+
exports.literalRepresentation(k, opts, indentLevel),
68+
exports.literalRepresentation(value[k], opts, indentLevel)
69+
)
70+
)
71+
}
72+
return concatValues(
73+
'object',
74+
keyValuePairs,
75+
opts.pretty && keyValuePairs.length > 1,
76+
opts.indent,
77+
indentLevel
78+
)
79+
}
80+
81+
case '[object Null]':
82+
return 'json!(null)'
83+
84+
case '[object Boolean]':
85+
return value ? 'true' : 'false'
86+
87+
default:
88+
if (value === null || value === undefined) {
89+
return ''
90+
}
91+
return JSON.stringify(value)
92+
}
93+
}

src/targets/rust/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict'
2+
3+
module.exports = {
4+
info: {
5+
key: 'rust',
6+
title: 'Rust',
7+
extname: '.rs',
8+
default: 'reqwest'
9+
},
10+
11+
reqwest: require('./reqwest')
12+
}

src/targets/rust/reqwest.js

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/**
2+
* @description
3+
* HTTP code snippet generator for Rust using reqwest
4+
*
5+
* @author
6+
* @Benjscho
7+
*
8+
* for any questions or issues regarding the generated code snippet, please open an issue mentioning the author.
9+
*/
10+
11+
const CodeBuilder = require('../../helpers/code-builder')
12+
const { escape } = require('../../helpers/format')
13+
const { literalRepresentation } = require('./helpers')
14+
15+
module.exports = ({ queryObj, url, postData, allHeaders, method }, options) => {
16+
const opts = {
17+
indent: ' ',
18+
pretty: true,
19+
...options
20+
}
21+
22+
let indentLevel = 0
23+
24+
// start snippet
25+
const code = new CodeBuilder(opts.indent)
26+
27+
// import reqwest
28+
code.push(indentLevel, 'use reqwest;')
29+
code.blank()
30+
31+
// start async main for tokio
32+
code.push(indentLevel, '#[tokio::main]')
33+
code.push(indentLevel, 'pub async fn main() {')
34+
indentLevel += 1
35+
36+
// add url
37+
code.push(indentLevel, `let url = "${url}";`)
38+
code.blank()
39+
40+
let hasQuery = false
41+
// construct query string
42+
if (Object.keys(queryObj).length) {
43+
hasQuery = true
44+
code.push(indentLevel, 'let querystring = [')
45+
indentLevel += 1
46+
for (const [key, value] of Object.entries(queryObj)) {
47+
code.push(indentLevel, `("${escape(key)}", "${escape(value)}"),`)
48+
}
49+
indentLevel -= 1
50+
code.push(indentLevel, '];')
51+
code.blank()
52+
}
53+
54+
// construct payload
55+
let payload = {}
56+
const files = {}
57+
58+
let hasFiles = false
59+
let hasForm = false
60+
let hasBody = false
61+
let jsonPayload = false
62+
let isMultipart = false
63+
switch (postData.mimeType) {
64+
case 'application/json':
65+
if (postData.jsonObj) {
66+
code.push(
67+
indentLevel,
68+
`let payload = ${literalRepresentation(postData.jsonObj, opts, indentLevel)};`
69+
)
70+
}
71+
jsonPayload = true
72+
break
73+
74+
case 'multipart/form-data':
75+
isMultipart = true
76+
77+
if (!postData.params) {
78+
code.push(indentLevel, 'let form = reqwest::multipart::Form::new()')
79+
code.push(indentLevel + 1, '.text("", "");')
80+
break
81+
}
82+
83+
payload = {}
84+
postData.params.forEach(p => {
85+
if (p.fileName) {
86+
files[p.name] = p.fileName
87+
hasFiles = true
88+
} else {
89+
payload[p.name] = p.value
90+
}
91+
})
92+
93+
if (hasFiles) {
94+
for (const line of fileToPartString) {
95+
code.push(indentLevel, line)
96+
}
97+
code.blank()
98+
}
99+
code.push(indentLevel, 'let form = reqwest::multipart::Form::new()')
100+
101+
for (const [name, fileName] of Object.entries(files)) {
102+
code.push(indentLevel + 1, `.part("${name}", file_to_part("${fileName}").await)`)
103+
}
104+
for (const [name, value] of Object.entries(payload)) {
105+
code.push(indentLevel + 1, `.text("${name}", "${value}")`)
106+
}
107+
code.pushToLast(';')
108+
109+
break
110+
111+
default: {
112+
if (postData.mimeType === 'application/x-www-form-urlencoded' && postData.paramsObj) {
113+
code.push(
114+
indentLevel,
115+
`let payload = ${literalRepresentation(postData.paramsObj, opts, indentLevel)};`
116+
)
117+
hasForm = true
118+
break
119+
}
120+
121+
if (postData.text) {
122+
code.push(
123+
indentLevel,
124+
`let payload = ${literalRepresentation(postData.text, opts, indentLevel)};`
125+
)
126+
hasBody = true
127+
break
128+
}
129+
}
130+
}
131+
132+
if (hasForm || jsonPayload || hasBody) {
133+
code.unshift('use serde_json::json;')
134+
code.blank()
135+
}
136+
137+
let hasHeaders = false
138+
// construct headers
139+
if (Object.keys(allHeaders).length) {
140+
hasHeaders = true
141+
code.push(indentLevel, 'let mut headers = reqwest::header::HeaderMap::new();')
142+
for (const [key, value] of Object.entries(allHeaders)) {
143+
// Skip setting content-type if there is a file, as this header will
144+
// cause the request to hang, and reqwest will set it for us.
145+
if (key.toLowerCase() === 'content-type' && isMultipart) {
146+
continue
147+
}
148+
code.push(
149+
indentLevel,
150+
`headers.insert("${escape(key)}", ${literalRepresentation(value, opts)}.parse().unwrap());`
151+
)
152+
}
153+
code.blank()
154+
}
155+
156+
// construct client
157+
code.push(indentLevel, 'let client = reqwest::Client::new();')
158+
159+
// construct query
160+
switch (method) {
161+
case 'POST':
162+
code.push(indentLevel, 'let response = client.post(url)')
163+
break
164+
165+
case 'GET':
166+
code.push(indentLevel, 'let response = client.get(url)')
167+
break
168+
169+
default: {
170+
code.push(
171+
indentLevel,
172+
`let response = client.request(reqwest::Method::from_str("${method}").unwrap(), url)`
173+
)
174+
code.unshift('use std::str::FromStr;')
175+
break
176+
}
177+
}
178+
179+
if (hasQuery) {
180+
code.push(indentLevel + 1, '.query(&querystring)')
181+
}
182+
183+
if (isMultipart) {
184+
code.push(indentLevel + 1, '.multipart(form)')
185+
}
186+
187+
if (hasHeaders) {
188+
code.push(indentLevel + 1, '.headers(headers)')
189+
}
190+
191+
if (jsonPayload) {
192+
code.push(indentLevel + 1, '.json(&payload)')
193+
}
194+
195+
if (hasForm) {
196+
code.push(indentLevel + 1, '.form(&payload)')
197+
}
198+
199+
if (hasBody) {
200+
code.push(indentLevel + 1, '.body(payload)')
201+
}
202+
203+
// send query
204+
code.push(indentLevel + 1, '.send()')
205+
code.push(indentLevel + 1, '.await;')
206+
code.blank()
207+
208+
// Print response
209+
code.push(indentLevel, 'let results = response.unwrap()')
210+
code.push(indentLevel + 1, '.json::<serde_json::Value>()')
211+
code.push(indentLevel + 1, '.await')
212+
code.push(indentLevel + 1, '.unwrap();')
213+
code.blank()
214+
215+
code.push(indentLevel, 'dbg!(results);')
216+
217+
code.push('}\n')
218+
219+
return code.join()
220+
}
221+
222+
const fileToPartString = [
223+
'async fn file_to_part(file_name: &\'static str) -> reqwest::multipart::Part {',
224+
' let file = tokio::fs::File::open(file_name).await.unwrap();',
225+
' let stream = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new());',
226+
' let body = reqwest::Body::wrap_stream(stream);',
227+
' reqwest::multipart::Part::stream(body)',
228+
' .file_name(file_name)',
229+
' .mime_str("text/plain").unwrap()',
230+
'}'
231+
]
232+
233+
module.exports.info = {
234+
key: 'reqwest',
235+
title: 'reqwest',
236+
link: 'https://docs.rs/reqwest/latest/reqwest/',
237+
description: 'reqwest HTTP library'
238+
}

0 commit comments

Comments
 (0)