defmodule CurlReq do @req_version :application.get_key(:req, :vsn) |> elem(1) @flag_docs CurlReq.Curl.flags() |> Enum.map(fn {long, nil} -> "* `--#{long}`" {long, short} -> "* `--#{long}`/`-#{short}`" end) |> Enum.join("\n") @doc false def req_version(), do: @req_version @type inspect_opt :: {:label, String.t()} @doc """ Inspect a Req struct in curl syntax. Returns the unchanged `req`, just like `IO.inspect/2`. ## Examples Req.new(url: "https://example.com") |> CurlReq.inspect() |> Req.request!() #=> curl --compressed -X GET https://example.com """ @spec inspect(Req.Request.t(), [inspect_opt()]) :: Req.Request.t() def inspect(req, opts \\ []) do case Keyword.get(opts, :label) do nil -> IO.puts(to_curl(req)) label -> IO.puts([label, ": ", to_curl(req)]) end req end @spec step_names(Req.Request.t(), boolean()) :: [atom()] defp step_names(%Req.Request{} = _req, false), do: [] defp step_names(%Req.Request{} = req, true), do: req.request_steps |> Keyword.keys() @spec step_names(Req.Request.t(), [atom()]) :: [atom()] defp step_names(%Req.Request{} = req, except: excludes) do for {name, _} <- req.request_steps, name not in excludes do name end end defp step_names(%Req.Request{} = req, only: includes) do for {name, _} <- req.request_steps, name in includes do name end end @spec run_steps(Req.Request.t(), [atom()]) :: Req.Request.t() defp run_steps(req, steps) do req.request_steps |> Enum.filter(fn {step, _} -> step in steps end) |> Enum.reduce(req, fn {step_name, step}, req -> case step.(req) do {_req, _response_or_error} -> raise "The request was stopped by #{step_name} request_step." next_req -> next_req end end) end @doc """ Transforms a Req request into a curl command. The following flags are supported: #{@flag_docs} Options: - `run_steps`: Run the Req.Steps before generating the curl command to have fine-tuned control over the Req.Request. Default: `true`. * `true`: Run all steps * `false`: Run no steps * `only: [atom()]`: A list of step names as atoms and only they will be executed * `except: [atom()]`: A list of step names as atoms and these steps will be excluded from the executed steps - `flags`: Specify the style the argument flags are constructed. Can either be `:short` or `:long`, Default: `:short` - `flavor` or `flavour`: With the `:curl` flavor (the default) it will try to use native curl representations for compression, auth and will use the native user agent. If flavor is set to `:req` the headers will not be modified and the curl command is constructed to stay as true as possible to the original `Req.Request` ## Examples iex> Req.new(url: URI.parse("https://www.example.com"), compressed: true) ...> |> CurlReq.to_curl() ~S(curl --compressed -X GET https://www.example.com) iex> Req.new(url: URI.parse("https://www.example.com"), compressed: true) ...> |> CurlReq.to_curl(flags: :long, flavor: :req) ~S(curl --header "accept-encoding: gzip" --user-agent "req/#{@req_version}" --request GET https://www.example.com) iex> Req.new(url: "https://www.example.com") ...> |> CurlReq.to_curl(run_steps: [except: [:compressed]]) ~S(curl -X GET https://www.example.com) """ @type flags :: :short | :long @type flavor :: :curl | :req @type to_curl_opts :: [ flags: flags(), flavor: flavor(), flavour: flavor(), run_steps: boolean() | [only: [atom()]] | [except: [atom()]] ] @spec to_curl(Req.Request.t(), to_curl_opts()) :: String.t() def to_curl(req, options \\ []) do options = Keyword.validate!(options, flags: :short, run_steps: true, flavor: nil, flavour: :curl) flavor = options[:flavor] || options[:flavour] flags = options[:flags] run_steps = options[:run_steps] available_steps = step_names(req, run_steps) req = run_steps(req, available_steps) curl_options = [flavor: flavor, flags: flags] CurlReq.Req.decode(req) |> CurlReq.Curl.encode(curl_options) end @doc """ Transforms a curl command into a Req request. The following flags are supported: #{@flag_docs} The `curl` command prefix is optional > #### Info {: .info} > > Only string inputs are supported. That means for example `-d @data.txt` will not load the file or `-d @-` will not read from stdin ## Examples iex> req = CurlReq.from_curl("curl https://www.example.com") iex> req.method :get iex> req.url URI.parse("https://www.example.com") iex> req = CurlReq.from_curl("curl -I https://example.com") iex> req.method :head iex> req.url URI.parse("https://example.com") iex> req = CurlReq.from_curl("curl -b cookie_key=cookie_val https://example.com") iex> req.method :get iex> Req.Request.get_header(req, "cookie") ["cookie_key=cookie_val"] """ @doc since: "0.98.4" @spec from_curl(String.t()) :: Req.Request.t() def from_curl(curl_command) do curl_command |> CurlReq.Curl.decode() |> CurlReq.Req.encode() end @doc """ Same as `from_curl/1` but as a sigil. The benefit here is, that the `Req.Request` struct will be created at compile time and you don't need to escape the string. Remember to ```elixir require CurlReq ``` to use the custom sigil. ## Examples iex> req = ~CURL(curl "https://www.example.com") iex> req.method :get iex> req.url URI.parse("https://www.example.com") iex> req = ~CURL(curl -I "https://example.com") iex> req.method :head iex> req.url URI.parse("https://example.com") iex> req = ~CURL(curl -b "cookie_key=cookie_val" "https://example.com") iex> req.method :get iex> Req.Request.get_header(req, "cookie") ["cookie_key=cookie_val"] """ defmacro sigil_CURL(curl_command, modifiers) defmacro sigil_CURL({:<<>>, _line_info, [command]}, _extra) do command |> from_curl() |> Macro.escape() end end