-
Notifications
You must be signed in to change notification settings - Fork 84
Expand file tree
/
Copy pathgithub.ex
More file actions
196 lines (167 loc) · 5.74 KB
/
github.ex
File metadata and controls
196 lines (167 loc) · 5.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
defmodule CodeCorps.GitHub do
alias CodeCorps.GitHub.{
API,
API.Headers
}
defmodule APIErrorObject do
@moduledoc """
Represents an error object from the GitHub API.
Used in some `APIError`s when the API's JSON response contains an
`errors` key.
The full details of error objects can be found in the
[GitHub API documentation](https://developer.github.com/v3/#client-errors).
"""
@type t :: %__MODULE__{}
defstruct [:code, :field, :resource]
def new(opts) do
struct(__MODULE__, opts)
end
end
defmodule APIError do
@moduledoc """
Represents a client error from the GitHub API.
You can read more about client errors in the
[GitHub API documentation](https://developer.github.com/v3/#client-errors).
"""
defstruct [:documentation_url, :errors, :message, :status_code]
@type t :: %__MODULE__{
documentation_url: String.t | nil,
errors: list | nil,
message: String.t | nil,
status_code: pos_integer | nil
}
@spec new({integer, map}) :: t
def new({status_code, %{"message" => message, "errors" => errors}}) do
errors = Enum.into(errors, [], fn error -> convert_error(error) end)
%__MODULE__{
errors: errors,
message: message,
status_code: status_code
}
end
def new({status_code, %{"message" => message, "documentation_url" => documentation_url}}) do
%__MODULE__{
documentation_url: documentation_url,
message: message,
status_code: status_code
}
end
def new({status_code, %{"message" => message}}) do
%__MODULE__{
message: message,
status_code: status_code
}
end
@spec convert_error(map) :: APIErrorObject.t
defp convert_error(%{"code" => code, "field" => field, "resource" => resource}) do
APIErrorObject.new([code: code, field: field, resource: resource])
end
end
defmodule HTTPClientError do
defstruct [:reason, message: """
The GitHub HTTP client encountered an error while communicating with
the GitHub API.
"""]
@type t :: %__MODULE__{}
def new(opts) do
struct(__MODULE__, opts)
end
end
@type method :: :get | :post | :put | :delete | :patch | :head
@type body :: {:multipart, list} | map
@type headers :: %{String.t => String.t} | %{}
@type response :: {:ok, map} | {:error, api_error_struct}
@type api_error_struct :: APIError.t | HTTPClientError.t
@typedoc ~S"""
Potential errors which can happen when retrieving data from a paginated
endpoint.
If a new access token is required, then it is regenerated and stored into an
installation, which can result in any of
- `Ecto.Changeset.t`
- `CodeCorps.GitHub.APIError.t`
- `CodeCorps.GitHub.HTTPClientError.t`
Once that is done, the actual request is made, which can error out with
- `CodeCorps.GitHub.Errors.PaginationError.t`
"""
@type paginated_endpoint_error :: Ecto.Changeset.t | APIError.t | HTTPClientError.t | API.Errors.PaginationError.t
@doc """
A low level utility function to make a direct request to the GitHub API.
"""
@spec request(method, String.t, body, headers, list) :: response
def request(method, endpoint, body, headers, options) do
with {:ok, encoded_body} <- body |> Poison.encode do
API.request(
method,
api_url_for(endpoint),
encoded_body,
headers |> Headers.user_request(options),
options
)
else
_ -> {:error, HTTPClientError.new(reason: :body_encoding_error)}
end
end
@doc ~S"""
A low level utility function to make an authenticated request to a GitHub API
endpoint which supports pagination, and fetch all the pages from that endpoint
at once, by making parallel requests to each page and aggregating the results.
"""
@spec get_all(String.t, headers, list) :: {:ok, list(map)} | {:error, API.Errors.PaginationError.t} | {:error, api_error_struct}
def get_all(endpoint, headers, options) do
API.get_all(
api_url_for(endpoint),
headers |> Headers.user_request(options),
options
)
end
@doc """
A low level utility function to make an authenticated request to the
GitHub API on behalf of a GitHub App or integration
"""
@spec integration_request(method, String.t, body, headers, list) :: response
def integration_request(method, endpoint, body, headers, options) do
with {:ok, encoded_body} <- body |> Poison.encode do
API.request(
method,
api_url_for(endpoint),
encoded_body,
headers |> Headers.integration_request,
options
)
else
_ -> {:error, HTTPClientError.new(reason: :body_encoding_error)}
end
end
@token_url "https://github.com/login/oauth/access_token"
@doc """
A low level utility function to fetch a GitHub user's OAuth access token
"""
@spec user_access_token_request(String.t, String.t) :: response
def user_access_token_request(code, state) do
with {:ok, encoded_body} <- code |> build_access_token_params(state) |> Poison.encode do
API.request(
:post,
@token_url,
encoded_body,
Headers.access_token_request,
[]
)
else
_ -> {:error, HTTPClientError.new(reason: :body_encoding_error)}
end
end
@api_url "https://api.github.com/"
@spec api_url_for(String.t) :: String.t
defp api_url_for(endpoint) when is_binary(endpoint) do
@api_url |> URI.merge(endpoint) |> URI.to_string
end
@spec build_access_token_params(String.t, String.t) :: map
defp build_access_token_params(code, state) do
%{
client_id: Application.get_env(:code_corps, :github_app_client_id),
client_secret: Application.get_env(:code_corps, :github_app_client_secret),
code: code,
state: state
}
end
end