Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 14 additions & 0 deletions src/helpers/__snapshots__/utils.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,20 @@ exports[`availableTargets > returns all available targets 1`] = `
"key": "ruby",
"title": "Ruby",
},
{
"clients": [
{
"description": "reqwest HTTP library",
"extname": ".rs",
"key": "reqwest",
"link": "https://docs.rs/reqwest/latest/reqwest/",
"title": "reqwest",
},
],
"default": "reqwest",
"key": "rust",
"title": "Rust",
},
{
"cli": "%s",
"clients": [
Expand Down
8 changes: 8 additions & 0 deletions src/helpers/code-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ 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
72 changes: 72 additions & 0 deletions src/targets/rust/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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, val] of Object.entries(value)) {
keyValuePairs.push(`"${key}": ${literalRepresentation(val, 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, '\\\\').replace(/"/g, '\\"')}"`;
}
};
225 changes: 225 additions & 0 deletions src/targets/rust/reqwest/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/**
* @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 type { Client } from '../../index.js';

import { CodeBuilder } from '../../../helpers/code-builder';
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) {
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 = "https://httpbin.org/anything";

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