Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --head, --form, --user and --location flags to parser #5

Merged
merged 10 commits into from
Jun 6, 2024
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 0.98.4
- Add CurlReq.Plugin
- Add new supported flags: `--head`, `--form`, `--user` and `--location`
- Add `CurlReq.from_curl/1`
- Improved docs and added typespecs

## 0.98.3
- Change `ex_doc` to a dev dependency.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ iex> Req.new(url: "/fact", base_url: "https://catfact.ninja/")

```

<!-- MDOC !-->

## Installation

The package can be installed
Expand Down
79 changes: 74 additions & 5 deletions lib/curl_req.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ defmodule CurlReq do
|> Enum.fetch!(1)

@type inspect_opt :: {:label, String.t()}
@type req_request :: %Req.Request{}

@doc """
Inspect a Req struct in curl syntax.
Expand All @@ -19,7 +18,7 @@ defmodule CurlReq do
...> # |> Req.request!()

"""
@spec inspect(req_request(), [inspect_opt()]) :: req_request()
@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))
Expand All @@ -44,14 +43,22 @@ defmodule CurlReq do
@doc """
Transforms a Req request into a curl command.

Supported curl flags are:

* `-b`
* `-H`
* `-X`
* `-I`
* `-d`

## Examples

iex> Req.new(url: URI.parse("https://www.google.com"))
...> |> CurlReq.to_curl()
~S(curl -H "accept-encoding: gzip" -H "user-agent: req/0.4.14" -X GET https://www.google.com)

"""
@spec to_curl(req_request()) :: String.t()
@spec to_curl(Req.Request.t(), Keyword.t()) :: String.t()
def to_curl(req, options \\ []) do
req =
if Keyword.get(options, :run_steps, true) do
Expand Down Expand Up @@ -79,30 +86,92 @@ defmodule CurlReq do
body -> ["-d", body]
end

redirect =
case req.options do
%{redirect: true} -> ["-L"]
derekkraan marked this conversation as resolved.
Show resolved Hide resolved
_ -> []
end

method =
case req.method do
nil -> ["-X", "GET"]
:head -> ["-I"]
m -> ["-X", String.upcase(to_string(m))]
end

url = [to_string(req.url)]

CurlReq.Shell.cmd_to_string("curl", headers ++ cookies ++ body ++ method ++ url)
CurlReq.Shell.cmd_to_string(
"curl",
headers ++ cookies ++ body ++ method ++ redirect ++ url
)
end

@doc """
Transforms a curl command into a Req request.

Supported curl command line flags are supported:

* `-H`/`--header`
* `-X`/`--request`
* `-d`/`--data`
* `-b`/`--cookie`
* `-I`/`--head`
* `-F`/`--form`
* `-L`/`--location`
* `-u`/`--user`

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> CurlReq.from_curl("curl https://www.google.com")
%Req.Request{method: :get, url: URI.parse("https://www.google.com")}

iex> ~S(curl -d "some data" https://example.com) |> CurlReq.from_curl()
%Req.Request{method: :get, body: "some data", url: URI.parse("https://example.com")}

iex> CurlReq.from_curl("curl -I https://example.com")
%Req.Request{method: :head, url: URI.parse("https://example.com")}

iex> CurlReq.from_curl("curl -b cookie_key=cookie_val https://example.com")
%Req.Request{method: :get, headers: %{"cookie" => ["cookie_key=cookie_val"]}, url: URI.parse("https://example.com")}
"""
@doc since: "0.98.4"

@spec from_curl(String.t()) :: Req.Request.t()
def from_curl(curl_command), do: CurlReq.Macro.parse(curl_command)

@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

## Examples

iex> import CurlReq
...> ~CURL(curl "https://www.google.com")
%Req.Request{method: :get, url: URI.parse("https://www.google.com")}

iex> import CurlReq
...> ~CURL(curl -d "some data" "https://example.com")
%Req.Request{method: :get, body: "some data", url: URI.parse("https://example.com")}

iex> import CurlReq
...> ~CURL(curl -I "https://example.com")
%Req.Request{method: :head, url: URI.parse("https://example.com")}

iex> import CurlReq
...> ~CURL(curl -b "cookie_key=cookie_val" "https://example.com")
%Req.Request{method: :get, headers: %{"cookie" => ["cookie_key=cookie_val"]}, url: URI.parse("https://example.com")}
"""
defmacro sigil_CURL(curl_command, modifiers)

defmacro sigil_CURL({:<<>>, _line_info, [command]}, _extra) do
command
|> CurlReq.Macro.parse()
|> CurlReq.Macro.to_req()
|> Macro.escape()
end
end
86 changes: 76 additions & 10 deletions lib/curl_req/macro.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule CurlReq.Macro do

# TODO: handle newlines

@spec parse(String.t()) :: Req.Request.t()
def parse(command) do
command =
command
Expand All @@ -13,22 +14,39 @@ defmodule CurlReq.Macro do
command
|> OptionParser.split()
|> OptionParser.parse(
strict: [header: :keep, request: :string, data: :keep, cookie: :string],
aliases: [H: :header, X: :request, d: :data, b: :cookie]
strict: [
header: :keep,
request: :string,
data: :keep,
cookie: :string,
head: :boolean,
form: :keep,
location: :boolean,
user: :string
],
aliases: [
H: :header,
X: :request,
d: :data,
b: :cookie,
I: :head,
F: :form,
L: :location,
u: :user
]
)

url = String.trim(url)
%{url: url, options: options}
end

@doc false
def to_req(%{url: url, options: options}) do
%Req.Request{}
|> Req.merge(url: url)
|> add_header(options)
|> add_method(options)
|> add_body(options)
|> add_cookie(options)
|> add_form(options)
|> add_auth(options)
|> configure_redirects(options)
end

defp add_header(req, options) do
Expand All @@ -46,10 +64,14 @@ defmodule CurlReq.Macro do

defp add_method(req, options) do
method =
options
|> Keyword.get(:request, "GET")
|> String.downcase()
|> String.to_existing_atom()
if Keyword.get(options, :head, false) do
:head
else
options
|> Keyword.get(:request, "GET")
|> String.downcase()
|> String.to_existing_atom()
end

Req.merge(req, method: method)
end
Expand All @@ -70,4 +92,48 @@ defmodule CurlReq.Macro do
cookie -> Req.Request.put_header(req, "cookie", cookie)
end
end

defp add_form(req, options) do
case Keyword.get_values(options, :form) do
[] ->
req

formdata ->
form =
for fd <- formdata, reduce: %{} do
map ->
[key, value] = String.split(fd, "=", parts: 2)
Map.put(map, key, value)
end

req
|> Req.Request.register_options([:form])
|> Req.Request.prepend_request_steps(encode_body: &Req.Steps.encode_body/1)
|> Req.merge(form: form)
end
end

defp add_auth(req, options) do
case Keyword.get(options, :user) do
nil ->
req

credentials ->
req
|> Req.Request.register_options([:auth])
|> Req.Request.prepend_request_steps(auth: &Req.Steps.auth/1)
|> Req.merge(auth: {:basic, credentials})
end
end

defp configure_redirects(req, options) do
if Keyword.get(options, :location, false) do
req
|> Req.Request.register_options([:redirect])
|> Req.Request.prepend_response_steps(redirect: &Req.Steps.redirect/1)
|> Req.merge(redirect: true)
else
req
end
end
end
Loading