UNPKG

curlconverter

Version:

convert curl commands to Python, JavaScript, Go, PHP and more

350 lines (311 loc) 9.43 kB
import { warnIfPartsIgnored } from "../Warnings.js"; import { Word, joinWords } from "../shell/Word.js"; import { parse, 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, "form", "form-string", "insecure", "no-insecure", "next", ]); const regexEscape = /"|\\|\p{C}|[^ \P{Z}]|#\{/gu; export function reprStr(s: string): string { return ( '"' + s.replace(regexEscape, (c: string): string => { switch (c[0]) { case "\x00": return "\\0"; case "\x07": return "\\a"; case "\b": return "\\b"; case "\f": return "\\f"; case "\n": return "\\n"; case "\r": return "\\r"; case "\t": return "\\t"; case "\v": return "\\v"; case "\x1B": return "\\e"; case "\\": return "\\\\"; case '"': return '\\"'; case "#": return "\\" + c; } const hex = (c.codePointAt(0) as number).toString(16); if (hex.length <= 4) { return "\\u" + hex.padStart(4, "0"); } return "\\u{" + hex + "}"; }) + '"' ); } export function repr(w: Word): 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.get_env(" + reprStr(t.value) + ', "")'); } else { // TODO: strip newline? // TODO: use System.cmd(), which needs to be two arguments the command name + list of args args.push("elem(System.shell(" + reprStr(t.value) + "), 0)"); } } return args.join(" <> "); } function addIndent(value: string): string { // split on new lines and add 2 spaces of indentation to each line, except empty lines return value .split("\n") .map((line) => (line ? " " + line : line)) .join("\n"); } function getCookies(request: Request): string { if (!request.cookies || !request.cookies.length) { return ""; } // TODO: this duplicates work, just get it from request.headers const cookies = joinWords( request.cookies.map((c) => joinWords(c, "=")), "; ", ); return `cookie: [${repr(cookies)}]`; } function getOptions(request: Request, params: string): [string, string] { const hackneyOptions: string[] = []; const auth = getBasicAuth(request); if (auth) { hackneyOptions.push(auth); } if (request.insecure) { hackneyOptions.push(":insecure"); } const cookies = getCookies(request); if (cookies) { hackneyOptions.push(cookies); } let hackneyOptionsString = ""; if (hackneyOptions.length > 1) { hackneyOptionsString = `hackney: [\n ${hackneyOptions.join( ",\n ", )}\n ]`; } else if (hackneyOptions.length) { hackneyOptionsString = `hackney: [${hackneyOptions[0]}]`; } const optionsWithoutParams = `[${hackneyOptionsString}]`; let options = optionsWithoutParams; if (params !== "[]") { options = ""; options += "[\n"; options += " params: " + addIndent(params).trim(); if (hackneyOptionsString) { options += ",\n"; options += " " + addIndent(hackneyOptionsString).trim(); } options += "\n"; options += " ]"; } return [options, optionsWithoutParams]; } function getBasicAuth(request: Request): string { if (!request.urls[0].auth) { return ""; } const [user, password] = request.urls[0].auth; return `basic_auth: {${repr(user)}, ${repr(password)}}`; } function getQueryDict(request: Request): string { if (!request.urls[0].queryList || !request.urls[0].queryList.length) { return "[]"; } let queryDict = "[\n"; const queryDictLines: string[] = []; for (const [paramName, rawValue] of request.urls[0].queryList) { queryDictLines.push(` {${repr(paramName)}, ${repr(rawValue)}}`); } queryDict += queryDictLines.join(",\n"); queryDict += "\n ]"; return queryDict; } function getHeadersDict(request: Request): string { if (!request.headers.length) { return "[]"; } let dict = "[\n"; const dictLines: string[] = []; for (const [headerName, headerValue] of request.headers) { dictLines.push( ` {${repr(headerName)}, ${repr(headerValue ?? new Word())}}`, ); } dict += dictLines.join(",\n"); dict += "\n ]"; return dict; } function getBody(request: Request): string { const formData = getFormDataString(request); return formData ? formData : '""'; } function getFormDataString(request: Request): string { if (request.multipartUploads) { if (!request.multipartUploads.length) { return `{:multipart, []}`; } const formParams: string[] = []; for (const m of request.multipartUploads) { if ("contentFile" in m) { formParams.push( ` {:file, ${repr(m.contentFile)}, {"form-data", [{:name, ${repr( m.name, )}}, {:filename, Path.basename(${repr( m.filename ?? m.contentFile, )})}]}, []}`, ); } else { formParams.push(` {${repr(m.name)}, ${repr(m.content)}}`); } } const formStr = formParams.join(",\n"); if (formStr) { return `{:multipart, [ ${formStr} ]}`; } } if (request.data) { return getDataString(request); } return ""; } function getDataString(request: Request): string { if (!request.data) { return ""; } // TODO: JSON with Poison if (!request.isDataRaw && request.data.startsWith("@")) { const filePath = request.data.slice(1); if (request.isDataBinary) { return `File.read!(${repr(filePath)})`; } else { return `{:file, ${repr(filePath)}}`; } } const [parsedQuery] = parseQueryString(request.data); if (parsedQuery && parsedQuery.length) { const data = parsedQuery.map((p) => { const [key, value] = p; return ` {${repr(key)}, ${repr(value)}}`; }); return `{:form, [\n${data.join(",\n")}\n ]}`; } if ( request.data.isString() && !request.data.includes("|") && request.data.split("\n", 4).length > 3 && // No trailing whitespace, except possibly on the last line !request.data.match(/[^\S\r\n]\n/) ) { return "~s|" + request.data.toString() + "|"; } return repr(request.data); } function requestToElixir(request: Request, warnings: Warnings = []): string { warnIfPartsIgnored(request, warnings); if (request.cookies) { request.headers.delete("cookie"); } // delete!(url, headers \\ [], options \\ []) // get!(url, headers \\ [], options \\ []) // head!(url, headers \\ [], options \\ []) // options!(url, headers \\ [], options \\ []) // patch!(url, body, headers \\ [], options \\ []) // post!(url, body, headers \\ [], options \\ []) // put!(url, body \\ "", headers \\ [], options \\ []) const methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]; const bodyMethods = ["PATCH", "POST", "PUT"]; const method = request.urls[0].method; const methodStr = method.toString(); if (!methods.includes(methodStr)) { warnings.push([ "bad-method", "Unsupported method " + JSON.stringify(methodStr), ]); } const isBodyMethod = bodyMethods.includes(methodStr); const body = getBody(request); const headers = getHeadersDict(request); const params = getQueryDict(request); // params can go in the options argument, but if we're using the full // form, put them as a separate argument. const [options, optionsWithoutParams] = getOptions(request, params); if (isBodyMethod || (body === '""' && methods.includes(methodStr))) { // Add args backwards. As soon as we see a non-default value, we have to // add all preceding arguments. let args: string[] = []; let keepArgs = false; keepArgs ||= options !== "[]"; if (keepArgs) { args.push(options); } keepArgs ||= headers !== "[]"; if (keepArgs) { args.push(headers); } keepArgs ||= body !== '""'; if (keepArgs && isBodyMethod) { args.push(body); } args.push(repr(request.urls[0].urlWithoutQueryList)); args = args.reverse(); let s = "response = HTTPoison." + methodStr.toLowerCase() + "!("; if (args.length === 1) { // If we just need the method+URL, keep it all on one line s += args[0]; } else { s += "\n"; s += " " + args.join(",\n "); s += "\n"; } return s + ")\n"; } return `request = %HTTPoison.Request{ method: :${method.toLowerCase().toString()}, url: ${repr(request.urls[0].urlWithoutQueryList)}, body: ${body}, headers: ${headers}, options: ${optionsWithoutParams}, params: ${params} } response = HTTPoison.request(request) `; } export function _toElixir( requests: Request[], warnings: Warnings = [], ): string { return requests.map((r) => requestToElixir(r, warnings)).join("\n"); } export function toElixirWarn( curlCommand: string | string[], warnings: Warnings = [], ): [string, Warnings] { const requests = parse(curlCommand, supportedArgs, warnings); const elixir = _toElixir(requests, warnings); return [elixir, warnings]; } export function toElixir(curlCommand: string | string[]): string { return toElixirWarn(curlCommand)[0]; }