Skip to content

Commit 3324252

Browse files
committed
feat: add rust support
1 parent 649b40b commit 3324252

22 files changed

Lines changed: 790 additions & 0 deletions

src/helpers/code-builder.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ export class CodeBuilder {
5959
this.code.push(newLine);
6060
};
6161

62+
pushToLast = (line: string): void => {
63+
if (!this.code.length) {
64+
this.push(line);
65+
}
66+
const updatedLine = `${this.code[this.code.length - 1]}${line}`;
67+
this.code[this.code.length - 1] = updatedLine;
68+
};
69+
70+
6271
/**
6372
* Add an empty line at the end of current lines
6473
*/

src/targets/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { powershell } from './powershell/target.js';
1919
import { python } from './python/target.js';
2020
import { r } from './r/target.js';
2121
import { ruby } from './ruby/target.js';
22+
import { rust } from './rust/target.js';
2223
import { shell } from './shell/target.js';
2324
import { swift } from './swift/target.js';
2425

@@ -118,6 +119,7 @@ type supportedTargets =
118119
| 'python'
119120
| 'r'
120121
| 'ruby'
122+
| 'rust'
121123
| 'shell'
122124
| 'swift';
123125

@@ -139,6 +141,7 @@ export const targets: Record<supportedTargets, Target> = {
139141
python,
140142
r,
141143
ruby,
144+
rust,
142145
shell,
143146
swift,
144147
};

src/targets/rust/helpers.ts

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

src/targets/rust/reqwest/client.ts

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

0 commit comments

Comments
 (0)