curlconverter
Version:
convert curl commands to Python, JavaScript, Go, PHP and more
352 lines (322 loc) • 9.72 kB
text/typescript
import { warnIfPartsIgnored } from "../../Warnings.js";
import { Word, eq } from "../../shell/Word.js";
import { parse, COMMON_SUPPORTED_ARGS } from "../../parse.js";
import type { Request, Warnings } from "../../parse.js";
import { parseQueryString } from "../../Query.js";
import { repr, reprSymbol, objToRuby, queryToRubyDict } from "./ruby.js";
export const supportedArgs = new Set([
...COMMON_SUPPORTED_ARGS,
"digest",
"no-digest",
"form",
"form-string",
"http0.9",
"http1.0",
"http1.1",
"insecure",
"output",
"upload-file",
"next",
]);
function getDataString(request: Request): [string, boolean] {
if (!request.data) {
return ["", false];
}
if (
request.dataArray &&
request.dataArray.length === 1 &&
!(request.dataArray[0] instanceof Word) &&
!request.dataArray[0].name
) {
const { filetype, filename } = request.dataArray[0];
if (eq(filename, "-")) {
if (filetype === "binary") {
// TODO: read stdin in binary
// https://ruby-doc.org/core-2.3.0/IO.html#method-i-binmode
// TODO: .delete("\\r\\n") ?
return ['body = STDIN.read.delete("\\n")\n', false];
} else {
return ['body = STDIN.read.delete("\\n")\n', false];
}
}
switch (filetype) {
case "binary":
return [
// TODO: What's the difference between binread() and read()?
// TODO: .delete("\\r\\n") ?
"body = File.binread(" + repr(filename) + ').delete("\\n")\n',
false,
];
case "data":
case "json":
return [
"body = File.read(" + repr(filename) + ').delete("\\n")\n',
false,
];
case "urlencode":
// TODO: urlencode
return [
"body = File.read(" + repr(filename) + ').delete("\\n")\n',
false,
];
}
}
const contentTypeHeader = request.headers.get("content-type");
const isJson =
contentTypeHeader &&
eq(contentTypeHeader.split(";")[0].trim(), "application/json");
if (isJson && request.data.isString()) {
try {
const dataAsStr = request.data.toString();
const dataAsJson = JSON.parse(dataAsStr);
if (typeof dataAsJson === "object" && dataAsJson !== null) {
// TODO: we actually want to know how it's serialized by Ruby's builtin
// JSON formatter but this is hopefully good enough.
const roundtrips = JSON.stringify(dataAsJson) === dataAsStr;
let code = "";
if (!roundtrips) {
code += "# The object won't be serialized exactly like this\n";
code += "# body = " + repr(request.data) + "\n";
}
code += "body = " + objToRuby(dataAsJson) + ".to_json\n";
return [code, true];
}
} catch {}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, queryAsDict] = parseQueryString(request.data);
if (!request.isDataBinary && queryAsDict) {
// If the original request contained %20, Ruby will encode them as "+"
return ["body = " + queryToRubyDict(queryAsDict) + "\n", false];
}
return ["body = " + repr(request.data) + "\n", false];
}
export function getFilesString(
request: Request,
warnings: Warnings,
): [string, boolean] {
if (!request.multipartUploads) {
return ["", false];
}
// If body contains an open file, HTTParty will send the entire request as a
// multipart form
let explicitMultipart = true;
let body = "body = {\n";
for (const m of request.multipartUploads) {
body += " " + reprSymbol(m.name) + ": ";
if ("filename" in m && m.filename && repr(m.filename)) {
warnings.push([
"multipart-filename",
"Ruby's HTTParty does not support custom filenames for multipart uploads: " +
JSON.stringify(m.filename.toString()),
]);
}
if ("contentFile" in m) {
if (eq(m.contentFile, "-")) {
if (request.stdinFile) {
body += "File.open(" + repr(request.stdinFile) + ")";
explicitMultipart = false;
} else if (request.stdin) {
body += repr(request.stdin);
}
// TODO: does this work?
body += "STDIN";
// explicitMultipart = false;
} else if (m.contentFile === m.filename) {
// TODO: curl will look at the file extension to determine each content-type
body += "File.open(" + repr(m.contentFile) + ")";
explicitMultipart = false;
} else {
body += "File.open(" + repr(m.contentFile) + ")";
explicitMultipart = false;
}
} else {
body += repr(m.content);
}
body += "\n";
}
body += "}\n";
return [body, explicitMultipart];
}
function requestToRubyHttparty(
request: Request,
warnings: Warnings,
imports: Set<string>,
): string {
warnIfPartsIgnored(request, warnings, { dataReadsFile: true });
if (
request.dataReadsFile &&
request.dataArray &&
request.dataArray.length &&
(request.dataArray.length > 1 ||
(!(request.dataArray[0] instanceof Word) && request.dataArray[0].name))
) {
warnings.push([
"unsafe-data",
"the generated data content is wrong, " +
// TODO: might not come from "@"
JSON.stringify("@" + request.dataReadsFile) +
" means read the file " +
JSON.stringify(request.dataReadsFile),
]);
}
let code = "";
let partyCode = "";
const methods = [
"GET",
"HEAD",
"POST",
"PATCH",
"PUT",
"PROPPATCH",
"LOCK",
"UNLOCK",
"OPTIONS",
"PROPFIND",
"DELETE",
"MOVE",
"COPY",
"MKCOL",
"TRACE",
];
code += "url = " + repr(request.urls[0].url) + "\n";
const method = request.urls[0].method;
const methodStr = method.toString();
if (method.isString() && methods.includes(methodStr.toUpperCase())) {
partyCode += "res = HTTParty." + methodStr.toLowerCase() + "(url";
if (methodStr !== methodStr.toUpperCase()) {
warnings.push([
"method-case",
"HTTP method will be uppercased: " + methodStr,
]);
}
} else {
warnings.push([
"unsupported-method",
"unsupported HTTP method: " + methodStr,
]);
partyCode += `res = HTTParty.get(url`;
}
if (request.urls[0].auth && ["basic", "digest"].includes(request.authType)) {
if (request.authType === "basic") {
partyCode += ", basic_auth: auth";
} else if (request.authType === "digest") {
partyCode += ", digest_auth: auth";
}
code +=
"auth = { username: " +
repr(request.urls[0].auth[0]) +
", password: " +
repr(request.urls[0].auth[1]) +
"}\n";
}
let reqBody;
let explicitMultipart = false;
if (request.urls[0].uploadFile) {
if (
eq(request.urls[0].uploadFile, "-") ||
eq(request.urls[0].uploadFile, ".")
) {
reqBody = "body = STDIN.read\n";
} else {
reqBody = "body = File.read(" + repr(request.urls[0].uploadFile) + ")\n";
}
} else if (request.data) {
let importJson = false;
[reqBody, importJson] = getDataString(request);
if (importJson) {
imports.add("json");
}
} else if (request.multipartUploads) {
[reqBody, explicitMultipart] = getFilesString(request, warnings);
const contentType = request.headers.get("content-type");
if (
explicitMultipart &&
contentType &&
contentType.isString() &&
contentType.toString() === "multipart/form-data"
) {
request.headers.delete("content-type");
}
}
if (request.headers.length) {
partyCode += ", headers: headers";
code += "headers = {\n";
for (const [headerName, headerValue] of request.headers) {
if (
["accept-encoding", "content-length"].includes(
headerName.toLowerCase().toString(),
)
) {
code += "# ";
}
// TODO: nil?
code +=
" " +
repr(headerName) +
": " +
repr(headerValue ?? new Word("nil")) +
",\n";
}
code += "}\n";
}
if (reqBody) {
code += reqBody;
if (explicitMultipart) {
partyCode += ", multipart: true";
}
if (
request.dataArray &&
request.dataArray.length === 1 &&
!(request.dataArray[0] instanceof Word) &&
!request.dataArray[0].name
) {
const { filetype } = request.dataArray[0];
if (filetype === "binary") {
partyCode += ", body_stream: body";
} else {
partyCode += ", body: body";
}
} else {
partyCode += ", body: body";
}
}
if (request.insecure) {
partyCode += ", verify: false";
}
partyCode += ")\n";
code += partyCode;
if (request.urls[0].output && !eq(request.urls[0].output, "/dev/null")) {
if (eq(request.urls[0].output, "-")) {
code += "\nputs res.body\n";
} else {
code += "\nFile.write(" + repr(request.urls[0].output) + ", res.body)\n";
}
}
return code;
}
export function _toRubyHttparty(
requests: Request[],
warnings: Warnings = [],
): string {
const imports = new Set<string>();
const code = requests
.map((r) => requestToRubyHttparty(r, warnings, imports))
.join("\n\n");
let prelude = "require 'httparty'\n";
for (const imp of Array.from(imports).sort()) {
prelude += "require '" + imp + "'\n";
}
return prelude + "\n" + code;
}
export function toRubyHttpartyWarn(
curlCommand: string | string[],
warnings: Warnings = [],
): [string, Warnings] {
const requests = parse(curlCommand, supportedArgs, warnings);
const ruby = _toRubyHttparty(requests, warnings);
return [ruby, warnings];
}
export function toRubyHttparty(curlCommand: string | string[]): string {
return toRubyHttpartyWarn(curlCommand)[0];
}