Skip to content

Commit

Permalink
Merge pull request #5 from kevinschweikert/feature/more-flags
Browse files Browse the repository at this point in the history
Add --head, --form, --user and --location flags to parser
  • Loading branch information
derekkraan authored Jun 6, 2024
2 parents d46d05a + 41297ae commit 724bcdc
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 72 deletions.
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"]
_ -> []
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

0 comments on commit 724bcdc

Please sign in to comment.