Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/helpers/code-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ export class CodeBuilder {
this.code.push(newLine);
};

pushToLast = (line: string): void => {
if (!this.code.length) {
this.push(line);
}
const updatedLine = `${this.code[this.code.length - 1]}${line}`;
this.code[this.code.length - 1] = updatedLine;
};


/**
* Add an empty line at the end of current lines
*/
Expand Down
3 changes: 3 additions & 0 deletions src/targets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { powershell } from './powershell/target.js';
import { python } from './python/target.js';
import { r } from './r/target.js';
import { ruby } from './ruby/target.js';
import { rust } from './rust/target.js';
import { shell } from './shell/target.js';
import { swift } from './swift/target.js';

Expand Down Expand Up @@ -118,6 +119,7 @@ type supportedTargets =
| 'python'
| 'r'
| 'ruby'
| 'rust'
| 'shell'
| 'swift';

Expand All @@ -139,6 +141,7 @@ export const targets: Record<supportedTargets, Target> = {
python,
r,
ruby,
rust,
shell,
swift,
};
Expand Down
84 changes: 84 additions & 0 deletions src/targets/rust/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
function concatValues(
concatType: 'array' | 'object',
values: any,
pretty: boolean,
indentation: string,
indentLevel: number,
): string {
const currentIndent = indentation.repeat(indentLevel);
const closingBraceIndent = indentation.repeat(indentLevel - 1);
const join = pretty ? `,\n${currentIndent}` : ', ';
const openingBrace = concatType === 'object' ? 'json!({' : '(';
const closingBrace = concatType === 'object' ? '})' : ')';

if (pretty) {
return `${openingBrace}\n${currentIndent}${values.join(
join,
)}\n${closingBraceIndent}${closingBrace}`;
}

return `${openingBrace}${values.join(join)}${closingBrace}`;
}

/**
* Create a valid Rust string of a literal value using serde_json according to its type.
*
* @param {*} value Any Javascript literal
* @param {Object} opts Target options
* @return {string}
*/
export const literalRepresentation = (
value: any,
opts: Record<string, any>,
indentLevel?: number,
): any => {
/*
* Note: this version is almost entirely borrowed from the Python client helper. The
* only real modification involves the braces and the types. The helper
* could potentially be parameterised for reuse.
*/
indentLevel = indentLevel === undefined ? 1 : indentLevel + 1;

switch (Object.prototype.toString.call(value)) {
case '[object Number]':
return value;

case '[object Array]': {
let pretty = false;
const valuesRep: any = (value as any[]).map(v => {
// Switch to prettify if the value is a dict with more than one key.
if (Object.prototype.toString.call(v) === '[object Object]') {
pretty = Object.keys(v).length > 1;
}
return literalRepresentation(v, opts, indentLevel);
});
return concatValues('array', valuesRep, pretty, opts.indent, indentLevel);
}

case '[object Object]': {
const keyValuePairs = [];
for (const key in value) {

Check failure on line 60 in src/targets/rust/helpers.ts

View workflow job for this annotation

GitHub Actions / lint

eslint(guard-for-in)

Require `for-in` loops to include an `if` statement
keyValuePairs.push(`"${key}": ${literalRepresentation(value[key], opts, indentLevel)}`);
}
return concatValues(
'object',
keyValuePairs,
opts.pretty && keyValuePairs.length > 1,
opts.indent,
indentLevel,
);
}

case '[object Null]':
return 'json!(null)';

case '[object Boolean]':
return value ? 'true' : 'false';

default:
if (value === null || value === undefined) {
return '';
}
return `"${value.toString().replace(/"/g, '\\"')}"`;
Comment thread
erunion marked this conversation as resolved.
Fixed
}
};
240 changes: 240 additions & 0 deletions src/targets/rust/reqwest/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/**
* @description
* HTTP code snippet generator for Rust using reqwest
*
* @author
* @Benjscho
*
* for any questions or issues regarding the generated code snippet, please open an issue mentioning the author.
*/

import { CodeBuilder } from '../../../helpers/code-builder';
import type { Client } from '../../index.js';
import { literalRepresentation } from '../helpers';

export const reqwest: Client = {
info: {
key: 'reqwest',
title: 'reqwest',
link: 'https://docs.rs/reqwest/latest/reqwest/',
description: 'reqwest HTTP library',
extname: '.rs',
},
convert: ({ queryObj, url, postData, allHeaders, method }, options) => {
const opts = {
indent: ' ',
pretty: true,
...options,
};

let indentLevel = 0;

// start snippet
const { push, blank, join, pushToLast, unshift } = new CodeBuilder({ indent: opts.indent });

// import reqwest
push('use reqwest;', indentLevel);
blank();

// start async main for tokio
push('#[tokio::main]', indentLevel);
push('pub async fn main() {', indentLevel);
indentLevel += 1;

// add url
push(`let url = "${url}";`, indentLevel);
blank();

let hasQuery = false;
// construct query string
if (Object.keys(queryObj).length) {
hasQuery = true;
push('let querystring = [', indentLevel);
indentLevel += 1;
for (const [key, value] of Object.entries(queryObj)) {
push(`("${key}", "${value}"),`, indentLevel);
}
indentLevel -= 1;
push('];', indentLevel);
blank();
}

// construct payload
let payload: Record<string, any> = {};
const files: Record<string, string> = {};

let hasFiles = false;
let hasForm = false;
let hasBody = false;
let jsonPayload = false;
let isMultipart = false;
switch (postData.mimeType) {
case 'application/json':
if (postData.jsonObj) {
push(
`let payload = ${literalRepresentation(postData.jsonObj, opts, indentLevel)};`,
indentLevel,
);
}
jsonPayload = true;
break;

case 'multipart/form-data':
isMultipart = true;

if (!postData.params) {
push(`let form = reqwest::multipart::Form::new()`, indentLevel);
push(`.text("", "");`, indentLevel + 1);
break;
}

payload = {};
postData.params.forEach(p => {
if (p.fileName) {
files[p.name] = p.fileName;
hasFiles = true;
} else {
payload[p.name] = p.value;
}
});

if (hasFiles) {
for (const line of fileToPartString) {
push(line, indentLevel);
}
blank();
}
push(`let form = reqwest::multipart::Form::new()`, indentLevel);

for (const [name, fileName] of Object.entries(files)) {
push(`.part("${name}", file_to_part("${fileName}").await)`, indentLevel + 1);
}
for (const [name, value] of Object.entries(payload)) {
push(`.text("${name}", "${value}")`, indentLevel + 1);
}
pushToLast(';');

break;

default: {
if (postData.mimeType === 'application/x-www-form-urlencoded' && postData.paramsObj) {
push(
`let payload = ${literalRepresentation(postData.paramsObj, opts, indentLevel)};`,
indentLevel,
);
hasForm = true;
break;
}

if (postData.text) {
push(
`let payload = ${literalRepresentation(postData.text, opts, indentLevel)};`,
indentLevel,
);
hasBody = true;
break;
}
}
}

if (hasForm || jsonPayload || hasBody) {
unshift(`use serde_json::json;`);
blank();
}

let hasHeaders = false;
// construct headers
if (Object.keys(allHeaders).length) {
hasHeaders = true;
push('let mut headers = reqwest::header::HeaderMap::new();', indentLevel);
for (const [key, value] of Object.entries(allHeaders)) {
// Skip setting content-type if there is a file, as this header will
// cause the request to hang, and reqwest will set it for us.
if (key.toLowerCase() === 'content-type' && isMultipart) {
continue;

Check failure on line 154 in src/targets/rust/reqwest/client.ts

View workflow job for this annotation

GitHub Actions / lint

eslint(no-continue)

Unexpected use of `continue` statement.
}
push(
`headers.insert("${key}", ${literalRepresentation(value, opts)}.parse().unwrap());`,
indentLevel,
);
}
blank();
}

// construct client
push('let client = reqwest::Client::new();', indentLevel);

// construct query
switch (method) {
case 'POST':
push(`let response = client.post(url)`, indentLevel);
break;

case 'GET':
push(`let response = client.get(url)`, indentLevel);
break;

default: {
push(
`let response = client.request(reqwest::Method::from_str("${method}").unwrap(), url)`,
indentLevel,
);
unshift(`use std::str::FromStr;`);
break;
}
}

if (hasQuery) {
push(`.query(&querystring)`, indentLevel + 1);
}

if (isMultipart) {
push(`.multipart(form)`, indentLevel + 1);
}

if (hasHeaders) {
push(`.headers(headers)`, indentLevel + 1);
}

if (jsonPayload) {
push(`.json(&payload)`, indentLevel + 1);
}

if (hasForm) {
push(`.form(&payload)`, indentLevel + 1);
}

if (hasBody) {
push(`.body(payload)`, indentLevel + 1);
}

// send query
push('.send()', indentLevel + 1);
push('.await;', indentLevel + 1);
blank();

// Print response
push('let results = response.unwrap()', indentLevel);
push('.json::<serde_json::Value>()', indentLevel + 1);
push('.await', indentLevel + 1);
push('.unwrap();', indentLevel + 1);
blank();

push('dbg!(results);', indentLevel);

push('}\n');

return join();
},
};

const fileToPartString = [
`async fn file_to_part(file_name: &'static str) -> reqwest::multipart::Part {`,
` let file = tokio::fs::File::open(file_name).await.unwrap();`,
` let stream = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new());`,
` let body = reqwest::Body::wrap_stream(stream);`,
` reqwest::multipart::Part::stream(body)`,
` .file_name(file_name)`,
` .mime_str("text/plain").unwrap()`,
`}`,
];
29 changes: 29 additions & 0 deletions src/targets/rust/reqwest/fixtures/application-form-encoded.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use serde_json::json;
use reqwest;

#[tokio::main]
pub async fn main() {
let url = "http://mockbin.com/har";

let payload = json!({
"foo": "bar",
"hello": "world"
});

let mut headers = reqwest::header::HeaderMap::new();
headers.insert("content-type", "application/x-www-form-urlencoded".parse().unwrap());

let client = reqwest::Client::new();
let response = client.post(url)
.headers(headers)
.form(&payload)
.send()
.await;

let results = response.unwrap()
.json::<serde_json::Value>()
.await
.unwrap();

dbg!(results);
}
Loading
Loading