curlconverter
Version:
convert curl commands to Python, JavaScript, Go, PHP and more
356 lines (335 loc) • 11.4 kB
text/typescript
import { Word, eq } from "../shell/Word.js";
import { parse, getFirst, COMMON_SUPPORTED_ARGS } from "../parse.js";
import type { Request, Warnings } from "../parse.js";
import { parseQueryString } from "../Query.js";
export const supportedArgs = new Set([
...COMMON_SUPPORTED_ARGS,
"max-time",
"connect-timeout",
"location",
"location-trusted",
"upload-file",
"form",
"form-string",
]);
// The only difference from Java is that in Kotlin the $ needs to be escaped
// https://kotlinlang.org/docs/java-to-kotlin-idioms-strings.html#concatenate-strings
// https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.6
// https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.3
const regexEscape = /\$|"|\\|\p{C}|[^ \P{Z}]/gu;
const regexDigit = /[0-9]/; // it's 0-7 actually but that would generate confusing code
export function reprStr(s: string): string {
return (
'"' +
s.replace(regexEscape, (c: string, index: number, string: string) => {
switch (c) {
case "$":
return "\\$";
case "\\":
return "\\\\";
case "\b":
return "\\b";
case "\f":
return "\\f";
case "\n":
return "\\n";
case "\r":
return "\\r";
case "\t":
return "\\t";
case '"':
return '\\"';
}
if (c.length === 2) {
const first = c.charCodeAt(0);
const second = c.charCodeAt(1);
return (
"\\u" +
first.toString(16).padStart(4, "0") +
"\\u" +
second.toString(16).padStart(4, "0")
);
}
if (c === "\0" && !regexDigit.test(string.charAt(index + 1))) {
return "\\0";
}
return "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0");
}) +
'"'
);
}
// TODO: anything different here?
export function repr(w: Word, imports: Set<string>): string {
const args: string[] = [];
for (const t of w.tokens) {
if (typeof t === "string") {
args.push(reprStr(t));
} else if (t.type === "variable") {
args.push("System.getenv(" + reprStr(t.value) + ') ?: ""');
imports.add("java.lang.System");
} else {
args.push("exec(" + reprStr(t.value) + ")");
imports.add("java.lang.Runtime");
imports.add("java.util.Scanner");
}
}
return args.join(" + ");
}
export function _toKotlin(
requests: Request[],
warnings: Warnings = [],
): string {
const request = getFirst(requests, warnings);
const url = request.urls[0];
const imports = new Set<string>([
"java.io.IOException",
"okhttp3.OkHttpClient",
"okhttp3.Request",
]);
let kotlinCode = "";
kotlinCode += "val client = OkHttpClient()";
const clientLines = [];
if (request.timeout) {
// TODO: floats don't work here
clientLines.push(
" .callTimeout(" + request.timeout.toString() + ", TimeUnit.SECONDS)\n",
);
imports.add("java.util.concurrent.TimeUnit");
}
if (request.connectTimeout) {
clientLines.push(
" .connectTimeout(" +
request.connectTimeout.toString() +
", TimeUnit.SECONDS)\n",
);
imports.add("java.util.concurrent.TimeUnit");
}
if (request.followRedirects === false) {
clientLines.push(" .followRedirects(false)\n");
clientLines.push(" .followSslRedirects(false)\n");
}
// TODO: Proxy
if (clientLines.length) {
kotlinCode += ".newBuilder()\n";
for (const line of clientLines) {
kotlinCode += line;
}
kotlinCode += " .build()";
}
kotlinCode += "\n\n";
if (url.auth) {
const [name, password] = url.auth;
kotlinCode +=
"val credential = Credentials.basic(" +
repr(name, imports) +
", " +
repr(password, imports) +
");\n\n";
imports.add("okhttp3.Credentials");
if (request.authType !== "basic") {
warnings.push([
"okhttp-unsupported-auth-type",
"OkHttp doesn't support auth type " + request.authType,
]);
}
}
const methodCallArgs = [];
const contentType = request.headers.getContentType();
const exactContentType = request.headers.get("content-type");
if (url.uploadFile) {
if (eq(url.uploadFile, "-") || eq(url.uploadFile, ".")) {
warnings.push(["upload-stdin", "uploading from stdin isn't supported"]);
}
if (exactContentType) {
kotlinCode +=
"val MEDIA_TYPE = " +
repr(exactContentType, imports) +
".toMediaType()\n\n";
imports.add("okhttp3.MediaType.Companion.toMediaType");
}
methodCallArgs.push("file.asRequestBody(MEDIA_TYPE)");
kotlinCode += "val file = File(" + repr(url.uploadFile, imports) + ")\n\n";
imports.add("java.io.File");
imports.add("okhttp3.RequestBody.Companion.asRequestBody");
} else if (request.multipartUploads) {
methodCallArgs.push("requestBody");
kotlinCode += "val requestBody = MultipartBody.Builder()\n";
kotlinCode += " .setType(MultipartBody.FORM)\n";
for (const m of request.multipartUploads) {
const args = [repr(m.name, imports)];
if ("content" in m) {
args.push(repr(m.content, imports));
} else {
if ("filename" in m && m.filename) {
args.push(repr(m.filename, imports));
args.push(
"File(" + repr(m.contentFile, imports) + ").asRequestBody()", // TODO: content type here
);
imports.add("java.io.File");
imports.add("okhttp3.RequestBody.Companion.asRequestBody");
} else {
// TODO: import
// TODO: probably doesn't work
args.push("Files.readAllBytes(" + repr(m.contentFile, imports) + ")");
}
}
kotlinCode += " .addFormDataPart(" + args.join(", ") + ")\n";
}
kotlinCode += " .build()\n\n";
imports.add("okhttp3.MultipartBody");
} else if (
request.dataArray &&
request.dataArray.length === 1 &&
!(request.dataArray[0] instanceof Word) &&
!request.dataArray[0].name
) {
// TODO: surely --upload-file and this can't be identical,
// doesn't this ignore url encoding?
if (exactContentType) {
kotlinCode +=
"val MEDIA_TYPE = " +
repr(exactContentType, imports) +
".toMediaType()\n\n";
imports.add("okhttp3.MediaType.Companion.toMediaType");
}
methodCallArgs.push("file.asRequestBody(MEDIA_TYPE)");
imports.add("okhttp3.RequestBody.Companion.asRequestBody");
kotlinCode +=
"val file = File(" +
repr(request.dataArray[0].filename, imports) +
")\n\n";
imports.add("java.io.File");
} else if (request.data) {
if (contentType === "application/x-www-form-urlencoded") {
const [queryList] = parseQueryString(request.data);
if (!queryList) {
if (exactContentType) {
kotlinCode +=
"val MEDIA_TYPE = " +
repr(exactContentType, imports) +
".toMediaType()\n\n";
imports.add("okhttp3.MediaType.Companion.toMediaType");
}
methodCallArgs.push("requestBody.toRequestBody(MEDIA_TYPE)");
imports.add("okhttp3.RequestBody.Companion.toRequestBody");
kotlinCode +=
"val requestBody = " + repr(request.data, imports) + "\n\n";
} else {
methodCallArgs.push("formBody");
kotlinCode += "val formBody = FormBody.Builder()\n";
for (const [name, value] of queryList) {
kotlinCode +=
" .add(" +
repr(name, imports) +
", " +
repr(value, imports) +
")\n";
}
kotlinCode += " .build()\n\n";
imports.add("okhttp3.FormBody");
}
} else {
if (exactContentType) {
kotlinCode +=
"val MEDIA_TYPE = " +
repr(exactContentType, imports) +
".toMediaType()\n\n";
imports.add("okhttp3.MediaType.Companion.toMediaType");
}
methodCallArgs.push("requestBody.toRequestBody(MEDIA_TYPE)");
kotlinCode += "val requestBody = " + repr(request.data, imports) + "\n\n";
imports.add("okhttp3.RequestBody.Companion.toRequestBody");
}
}
kotlinCode += "val request = Request.Builder()\n";
kotlinCode += " .url(" + repr(url.url, imports) + ")\n";
const methods = ["DELETE", "GET", "HEAD", "PATCH", "POST", "PUT"];
const dataMethods = ["DELETE", "PATCH", "POST", "PUT"];
const requiredDataMethods = ["PATCH", "POST", "PUT"];
const method = url.method;
const methodStr = url.method.toString();
let methodCall = "method";
if (
!method.isString() ||
!methods.includes(methodStr) ||
// If we appended something to methodCallArgs it means we need to send data
(methodCallArgs.length && !dataMethods.includes(methodStr)) ||
(!methodCallArgs.length && requiredDataMethods.includes(methodStr))
) {
if (!methodCallArgs.length) {
methodCallArgs.push('"".toRequestBody()');
imports.add("okhttp3.RequestBody.Companion.toRequestBody");
}
methodCallArgs.unshift(repr(method, imports));
} else {
if (methodStr === "DELETE" && !methodCallArgs.length) {
methodCallArgs.push('"".toRequestBody()');
imports.add("okhttp3.RequestBody.Companion.toRequestBody");
}
methodCall = method.toString().toLowerCase();
}
if (methodCall !== "get") {
kotlinCode += " ." + methodCall + "(" + methodCallArgs.join(", ") + ")\n";
}
if (request.headers.length) {
for (const [headerName, headerValue] of request.headers) {
if (headerValue === null) {
continue;
}
kotlinCode +=
" .header(" +
repr(headerName, imports) +
", " +
repr(headerValue, imports) +
")\n";
}
}
if (url.auth) {
const authHeader = request.headers.lowercase
? "authorization"
: "Authorization";
kotlinCode += ' .header("' + authHeader + '", credential)\n';
}
kotlinCode += " .build()\n";
kotlinCode += "\n";
kotlinCode += "client.newCall(request).execute().use { response ->\n";
kotlinCode +=
' if (!response.isSuccessful) throw IOException("Unexpected code $response")\n';
kotlinCode += " response.body!!.string()\n";
kotlinCode += "}\n";
let preambleCode = "";
for (const imp of Array.from(imports).sort()) {
preambleCode += "import " + imp + "\n";
}
if (imports.size) {
preambleCode += "\n";
}
// TODO
if (imports.has("java.lang.Runtime")) {
// Helper function that runs a bash command and always returns a string
preambleCode += "fun exec(cmd: String): String {\n";
preambleCode += " try {\n";
preambleCode += " val p = Runtime.getRuntime().exec(cmd)\n";
preambleCode += " p.waitFor()\n";
preambleCode +=
' val s = Scanner(p.getInputStream()).useDelimiter("\\\\A")\n';
preambleCode += ' return s.hasNext() ? s.next() : ""\n';
preambleCode += " } catch (Exception e) {\n";
preambleCode += ' return ""\n';
preambleCode += " }\n";
preambleCode += "}\n";
preambleCode += "\n";
}
return preambleCode + kotlinCode;
}
export function toKotlinWarn(
curlCommand: string | string[],
warnings: Warnings = [],
): [string, Warnings] {
const requests = parse(curlCommand, supportedArgs, warnings);
const kotlin = _toKotlin(requests, warnings);
return [kotlin, warnings];
}
export function toKotlin(curlCommand: string | string[]): string {
return toKotlinWarn(curlCommand)[0];
}