Skip to content

Commit 7725cb9

Browse files
authored
Merge pull request #1138 from code-corps/refactor-github-api-to-use-gateway
Switched the GitHub API to use a simple Gateway module
2 parents dead112 + 8b6d5a4 commit 7725cb9

37 files changed

+460
-301
lines changed

config/config.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ config :code_corps, :corsica_log_level, [rejected: :warn]
6767
{:ok, pem} = (System.get_env("GITHUB_APP_PEM") || "") |> Base.decode64()
6868

6969
config :code_corps,
70-
github: CodeCorps.GitHub.API,
70+
github: CodeCorps.GitHub.API.Gateway,
7171
github_app_id: System.get_env("GITHUB_APP_ID"),
7272
github_app_client_id: System.get_env("GITHUB_APP_CLIENT_ID"),
7373
github_app_client_secret: System.get_env("GITHUB_APP_CLIENT_SECRET"),

lib/code_corps/cloudex/cloudex_test.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ defmodule CloudexTest do
1414
end
1515
end
1616

17-
@spec upload(String.t) :: Cloudex.UploadedImage.t
17+
@spec upload(String.t) :: %Cloudex.UploadedImage{}
1818
def upload(_url) do
1919
[ok: %Cloudex.UploadedImage{public_id: fake_cloudinary_id()}]
2020
end

lib/code_corps/github/api/api.ex

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,37 @@ defmodule CodeCorps.GitHub.API do
22
alias CodeCorps.{
33
GithubAppInstallation,
44
GitHub,
5+
GitHub.API.Pagination,
56
GitHub.APIError,
67
GitHub.HTTPClientError,
78
User
89
}
910

11+
alias HTTPoison.{Error, Response}
12+
13+
def gateway(), do: Application.get_env(:code_corps, :github)
14+
1015
@spec request(GitHub.method, String.t, GitHub.headers, GitHub.body, list) :: GitHub.response
11-
def request(method, url, headers, body, options) do
12-
method
13-
|> :hackney.request(url, headers, body, options)
16+
def request(method, url, body, headers, options) do
17+
gateway().request(method, url, body, headers, options)
1418
|> marshall_response()
1519
end
1620

17-
defdelegate get_all(url, headers, opts), to: CodeCorps.GitHub.EagerAPI
21+
@spec get_all(String.t, GitHub.headers, list) :: GitHub.response
22+
def get_all(url, headers, options) do
23+
{:ok, %Response{request_url: request_url, headers: response_headers}} =
24+
gateway().request(:head, url, "", headers, options)
25+
26+
response_headers
27+
|> Pagination.retrieve_total_pages()
28+
|> Pagination.to_page_numbers()
29+
|> Enum.map(&Pagination.add_page_param(options, &1))
30+
|> Enum.map(&gateway().request(:get, url, "", headers, &1))
31+
|> Enum.map(&marshall_response/1)
32+
|> Enum.map(&Tuple.to_list/1)
33+
|> Enum.map(&List.last/1)
34+
|> List.flatten
35+
end
1836

1937
@doc """
2038
Get access token headers for a given `CodeCorps.User` and
@@ -47,29 +65,32 @@ defmodule CodeCorps.GitHub.API do
4765
end
4866
end
4967

50-
@typep http_success :: {:ok, integer, [{String.t, String.t}], String.t}
68+
@typep http_success :: {:ok, Response.t}
5169
@typep http_failure :: {:error, term}
5270

5371
@spec marshall_response(http_success | http_failure) :: GitHub.response
54-
defp marshall_response({:ok, status, _headers, body}) when status in 200..299 do
72+
defp marshall_response({:ok, %Response{body: body, status_code: status}}) when status in 200..299 do
5573
case body |> Poison.decode do
5674
{:ok, json} ->
5775
{:ok, json}
5876
{:error, _value} ->
5977
{:error, HTTPClientError.new(reason: :body_decoding_error)}
6078
end
6179
end
62-
defp marshall_response({:ok, 404, _headers, body}) do
80+
defp marshall_response({:ok, %Response{body: body, status_code: 404}}) do
6381
{:error, APIError.new({404, %{"message" => body}})}
6482
end
65-
defp marshall_response({:ok, status, _headers, body}) when status in 400..599 do
83+
defp marshall_response({:ok, %Response{body: body, status_code: status}}) when status in 400..599 do
6684
case body |> Poison.decode do
6785
{:ok, json} ->
6886
{:error, APIError.new({status, json})}
6987
{:error, _value} ->
7088
{:error, HTTPClientError.new(reason: :body_decoding_error)}
7189
end
7290
end
91+
defp marshall_response({:error, %Error{reason: reason}}) do
92+
{:error, HTTPClientError.new(reason: reason)}
93+
end
7394
defp marshall_response({:error, reason}) do
7495
{:error, HTTPClientError.new(reason: reason)}
7596
end

lib/code_corps/github/api/comment.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ defmodule CodeCorps.GitHub.API.Comment do
3232
attrs = comment |> GitHub.Adapters.Comment.to_api
3333

3434
with opts when is_list(opts) <- GitHub.API.opts_for(user, installation) do
35-
GitHub.request(:post, endpoint, %{}, attrs, opts)
35+
GitHub.request(:post, endpoint, attrs, %{}, opts)
3636
else
3737
{:error, github_error} -> {:error, github_error}
3838
end
@@ -56,7 +56,7 @@ defmodule CodeCorps.GitHub.API.Comment do
5656
attrs = comment |> GitHub.Adapters.Comment.to_api
5757

5858
with opts when is_list(opts) <- GitHub.API.opts_for(user, installation) do
59-
GitHub.request(:patch, endpoint, %{}, attrs, opts)
59+
GitHub.request(:patch, endpoint, attrs, %{}, opts)
6060
else
6161
{:error, github_error} -> {:error, github_error}
6262
end

lib/code_corps/github/api/eager_api.ex

Lines changed: 0 additions & 83 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule CodeCorps.GitHub.API.Gateway do
2+
@moduledoc ~S"""
3+
The gate through which all communication with the GitHub API must go through.
4+
5+
The purpose of this module is to centralize the most basic GitHub API request,
6+
so the module can be injected into tests easily, giving full control to what
7+
the tested response is.
8+
"""
9+
10+
alias CodeCorps.GitHub
11+
12+
@spec request(GitHub.method, String.t, GitHub.body, GitHub.headers, list) :: GitHub.response
13+
def request(method, url, body, headers, options) do
14+
HTTPoison.request(method, url, body, headers, options)
15+
end
16+
end

lib/code_corps/github/api/issue.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ defmodule CodeCorps.GitHub.API.Issue do
4141
attrs = task |> GitHub.Adapters.Issue.to_api
4242

4343
with opts when is_list(opts) <- GitHub.API.opts_for(user, installation) do
44-
GitHub.request(:post, endpoint, %{}, attrs, opts)
44+
GitHub.request(:post, endpoint, attrs, %{}, opts)
4545
else
4646
{:error, github_error} -> {:error, github_error}
4747
end
@@ -63,7 +63,7 @@ defmodule CodeCorps.GitHub.API.Issue do
6363
attrs = task |> GitHub.Adapters.Issue.to_api
6464

6565
with opts when is_list(opts) <- GitHub.API.opts_for(user, installation) do
66-
GitHub.request(:patch, endpoint, %{}, attrs, opts)
66+
GitHub.request(:patch, endpoint, attrs, %{}, opts)
6767
else
6868
{:error, github_error} -> {:error, github_error}
6969
end
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
defmodule CodeCorps.GitHub.API.Pagination do
2+
@moduledoc ~S"""
3+
Used to parse and build pagination data when fetching multiple pages from the GitHub API
4+
"""
5+
6+
@doc ~S"""
7+
Parses a collection of response headers and determines the record page count for a GitHub endpoint.
8+
9+
The response the headers are retrieved from is usually generated using a
10+
`:head` request.
11+
12+
The value of a "Link" header is used to determine the page count.
13+
14+
If the "Link" header is not present in the collection, the count is assumed 1
15+
16+
If the "Link" header is present, we use regex to parse the pagination info
17+
from its value.
18+
19+
The format of the header is as follows:
20+
21+
```
22+
{"Link", '<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
23+
<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
24+
<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
25+
<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"'
26+
```
27+
28+
The page count is determind by locating the `rel="last"` url and extracting
29+
the `page` query parameter from it.
30+
"""
31+
@spec retrieve_total_pages(list) :: integer
32+
def retrieve_total_pages(headers) do
33+
headers
34+
|> List.keyfind("Link", 0, nil)
35+
|> extract_total_pages
36+
end
37+
38+
@spec extract_total_pages(nil | String.t) :: integer
39+
defp extract_total_pages(nil), do: 1
40+
defp extract_total_pages({"Link", value} = _header) do
41+
value
42+
|> String.split(", ")
43+
|> Enum.map(fn link ->
44+
rel = get_rel(link)
45+
page = get_page(link)
46+
{rel, page}
47+
end)
48+
|> Enum.into(%{})
49+
|> Map.get("last")
50+
end
51+
52+
@spec get_rel(String.t) :: String.t
53+
defp get_rel(link) when is_binary(link) do
54+
# Searches for `rel=`
55+
Regex.run(~r{rel="([a-z]+)"}, link) |> List.last()
56+
end
57+
58+
@spec get_page(String.t) :: integer
59+
defp get_page(link) when is_binary(link) do
60+
# Searches for the following variations:
61+
# ```
62+
# ?page={match}>
63+
# ?page={match}&...
64+
# &page={match}>
65+
# &page={match}&...
66+
# ```
67+
Regex.run(~r{[&/?]page=([^>&]+)}, link) |> List.last |> String.to_integer
68+
end
69+
70+
@doc ~S"""
71+
From the specified page count, generates a list of integers, `1..count`
72+
"""
73+
@spec to_page_numbers(integer) :: list(integer)
74+
def to_page_numbers(total) when is_integer(total), do: 1..total
75+
76+
@doc ~S"""
77+
Adds a `page` query parameter to an `options` `Keyword` list.
78+
79+
For `HTTPPoison`, query parameters go under the `params` key of the `options`
80+
argument, so this function also ensures the `params` key is present.
81+
"""
82+
@spec add_page_param(Keyword.t, integer) :: Keyword.t
83+
def add_page_param(options, page) when is_list(options) when is_integer(page) do
84+
params =
85+
options
86+
|> Keyword.get(:params, [])
87+
|> Keyword.put(:page, page)
88+
89+
options
90+
|> Keyword.put(:params, params)
91+
end
92+
end

lib/code_corps/github/api/user.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule CodeCorps.GitHub.API.User do
44
"""
55

66
alias CodeCorps.{Accounts, GitHub, User}
7-
alias Ecto.{Changeset}
7+
alias Ecto.Changeset
88

99

1010
@single_endpoint "user"

lib/code_corps/github/event/installation/matched_user.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ defmodule CodeCorps.GitHub.Event.Installation.MatchedUser do
1212
User
1313
}
1414

15-
@typep process_outcome :: {:ok, GithubAppInstallation.t} | {:error, Changeset.t}
15+
@typep process_outcome :: {:ok, GithubAppInstallation.t} | {:error, Ecto.Changeset.t}
1616
@type outcome :: process_outcome | {:error, :too_many_unprocessed_installations}
1717

1818
@doc """

0 commit comments

Comments
 (0)