|
1 | 1 | defmodule CodeCorps.GitHub.Event.Installation do |
2 | 2 | @moduledoc """ |
3 | | - In charge of dealing with "Installation" GitHub Webhook events |
| 3 | + In charge of handling a GitHub Webhook payload for the Installation event type |
| 4 | + [https://developer.github.com/v3/activity/events/types/#installationevent](https://developer.github.com/v3/activity/events/types/#installationevent) |
4 | 5 | """ |
5 | 6 |
|
| 7 | + @behaviour CodeCorps.GitHub.Event.Handler |
| 8 | + |
6 | 9 | alias CodeCorps.{ |
7 | 10 | GithubAppInstallation, |
8 | 11 | GithubEvent, |
9 | | - GitHub.Event.Installation.UnmatchedUser, |
10 | | - GitHub.Event.Installation.MatchedUser, |
11 | | - GitHub.Event.Installation.Repos, |
12 | | - GitHub.Event.Installation.Validator, |
| 12 | + GitHub.Event.Installation, |
13 | 13 | Repo, |
14 | 14 | User |
15 | 15 | } |
16 | 16 |
|
17 | | - @typep outcome :: {:ok, GithubAppInstallation.t, Task.t} | {:error, any} |
18 | | - |
19 | | - @doc """ |
20 | | - Handles an "Installation" GitHub Webhook event. The event could be |
21 | | - of subtype "created" or "deleted". Only the "created" variant is handled at |
22 | | - the moment. |
| 17 | + alias Ecto.{Changeset, Multi} |
23 | 18 |
|
24 | | - `Installation::created` will first try to find the `User` using information |
25 | | - from the payload. |
| 19 | + @type outcome :: {:ok, GithubAppInstallation.t} | |
| 20 | + {:error, :not_yet_implemented} | |
| 21 | + {:error, :unexpected_action} | |
| 22 | + {:error, :unexpected_payload} | |
| 23 | + {:error, :validation_error_on_syncing_installation} | |
| 24 | + {:error, :multiple_unprocessed_installations_found} | |
| 25 | + {:error, :github_api_error_on_syncing_repos} | |
| 26 | + {:error, :validation_error_on_deleting_removed_repos} | |
| 27 | + {:error, :validation_error_on_syncing_existing_repos} | |
| 28 | + {:error, :validation_error_on_marking_installation_processed} |
26 | 29 |
|
27 | | - Depending on the outcame of that operation, it will either call one of |
| 30 | + @doc """ |
| 31 | + Handles the "Installation" GitHub Webhook event. |
28 | 32 |
|
29 | | - - `CodeCorps.GitHub.Event.Installation.UnmatchedUser.handle/2` |
30 | | - - `CodeCorps.GitHub.Event.Installation.MatchedUser.handle/2` |
| 33 | + The event could be of subtype `created` or `deleted`. Only the `created` |
| 34 | + variant is handled at the moment. |
31 | 35 |
|
32 | | - These helper modules will create or update the `GithubAppInstallation` with |
33 | | - proper data. |
| 36 | + The process of handling the "created" event subtype is as follows |
34 | 37 |
|
35 | | - Once that is done, the outcome will be returned. |
| 38 | + - try to match the sender with an existing `CodeCorps.User` |
| 39 | + - call specific matching module depending on the user being matched or not |
| 40 | + - `CodeCorps.GitHub.Event.Installation.MatchedUser.handle/2` |
| 41 | + - `CodeCorps.GitHub.Event.Installation.UnmatchedUser.handle/1` |
| 42 | + - sync installation repositories using a third modules |
| 43 | + - `CodeCorps.GitHub.Event.Installation.Repos.process/1` |
36 | 44 |
|
37 | | - Additionally, a background task will launch |
38 | | - `CodeCorps.GitHub.Event.Installation.Repos.process_async` to asynchronously |
39 | | - fetch and process repositories for the installation. |
| 45 | + If everything goes as expected, an `:ok` tuple will be returned, with a |
| 46 | + `CodeCorps.GithubAppInstallation`, marked as "processed". |
40 | 47 |
|
41 | | - The installation will initially be returned with the state "processing", but |
42 | | - in the background, it will eventually switch to either "processed" or |
43 | | - "errored". |
| 48 | + If a step in the process failes, an `:error` tuple will be returned, where the |
| 49 | + second element is an atom indicating which step of the process failed. |
44 | 50 | """ |
45 | 51 | @spec handle(GithubEvent.t, map) :: outcome |
46 | | - def handle(%GithubEvent{action: "created"}, payload) do |
47 | | - case payload |> Validator.valid? do |
48 | | - true -> payload |> do_handle() |> postprocess() |
49 | | - false -> {:error, :unexpected_payload} |
50 | | - end |
51 | | - end |
52 | | - def handle(%GithubEvent{action: "deleted"}, _) do |
53 | | - {:error, :not_fully_implemented} |
| 52 | + def handle(%GithubEvent{}, payload) do |
| 53 | + Multi.new |
| 54 | + |> Multi.run(:payload, fn _ -> payload |> validate_payload() end) |
| 55 | + |> Multi.run(:action, fn _ -> payload |> validate_action() end) |
| 56 | + |> Multi.run(:user, fn _ -> payload |> find_user() end) |
| 57 | + |> Multi.run(:installation, fn %{user: user} -> install_for_user(user, payload) end) |
| 58 | + |> Multi.merge(&process_repos/1) |
| 59 | + |> Repo.transaction |
| 60 | + |> marshall_result() |
54 | 61 | end |
55 | | - def handle(%GithubEvent{action: _action}, _payload) do |
56 | | - {:error, :unexpected_action} |
| 62 | + |
| 63 | + @spec find_user(map) :: {:ok, User.t} | {:ok, nil} | {:error, :unexpected_user_payload} |
| 64 | + defp find_user(%{"sender" => %{"id" => github_id}}) do |
| 65 | + user = Repo.get_by(User, github_id: github_id) |
| 66 | + {:ok, user} |
57 | 67 | end |
| 68 | + defp find_user(_), do: {:error, :unexpected_user_payload} |
58 | 69 |
|
59 | | - @spec do_handle(map) :: outcome |
60 | | - defp do_handle(%{"sender" => sender_attrs} = payload) do |
61 | | - case sender_attrs |> find_user() do |
62 | | - # No user was found with the specified github_id |
63 | | - nil -> UnmatchedUser.handle(payload) |
64 | | - # A user was found, matching the sspecified github_id |
65 | | - %User{} = user -> MatchedUser.handle(user, payload) |
66 | | - end |
| 70 | + @spec install_for_user(User.t, map) :: outcome |
| 71 | + defp install_for_user(%User{} = user, payload) do |
| 72 | + Installation.MatchedUser.handle(user, payload) |
| 73 | + end |
| 74 | + defp install_for_user(nil, payload) do |
| 75 | + Installation.UnmatchedUser.handle(payload) |
67 | 76 | end |
68 | 77 |
|
69 | | - @spec postprocess({:ok, GithubAppInstallation.t} | {:error, any}) :: {:ok, GithubAppInstallation.t} | {:error, any} |
70 | | - defp postprocess({:ok, %GithubAppInstallation{} = installation}) do |
| 78 | + @spec process_repos(%{installation: GithubAppInstallation.t}) :: Multi.t |
| 79 | + defp process_repos(%{installation: %GithubAppInstallation{} = installation}) do |
71 | 80 | installation |
72 | 81 | |> Repo.preload(:github_repos) |
73 | | - |> Repos.process_async |
| 82 | + |> Installation.Repos.process |
| 83 | + end |
| 84 | + |
| 85 | + @spec marshall_result(tuple) :: tuple |
| 86 | + defp marshall_result({:ok, %{processed_installation: installation}}), do: {:ok, installation} |
| 87 | + defp marshall_result({:error, :payload, :invalid, _steps}), do: {:error, :unexpected_payload} |
| 88 | + defp marshall_result({:error, :action, :unexpected_action, _steps}), do: {:error, :unexpected_action} |
| 89 | + defp marshall_result({:error, :action, :not_yet_implemented, _steps}), do: {:error, :not_yet_implemented} |
| 90 | + defp marshall_result({:error, :user, :unexpected_user_payload, _steps}), do: {:error, :unexpected_payload} |
| 91 | + defp marshall_result({:error, :installation, :unexpected_installation_payload, _steps}), do: {:error, :unexpected_payload} |
| 92 | + defp marshall_result({:error, :installation, %Changeset{}, _steps}), do: {:error, :validation_error_on_syncing_installation} |
| 93 | + defp marshall_result({:error, :installation, :too_many_unprocessed_installations, _steps}), do: {:error, :multiple_unprocessed_installations_found} |
| 94 | + defp marshall_result({:error, :api_response, %CodeCorps.GitHub.APIError{}, _steps}), do: {:error, :github_api_error_on_syncing_repos} |
| 95 | + defp marshall_result({:error, :deleted_repos, {_results, _changesets}, _steps}), do: {:error, :validation_error_on_deleting_removed_repos} |
| 96 | + defp marshall_result({:error, :synced_repos, {_results, _changesets}, _steps}), do: {:error, :validation_error_on_syncing_existing_repos} |
| 97 | + defp marshall_result({:error, :processed_installation, %Changeset{}, _steps}), do: {:error, :validation_error_on_marking_installation_processed} |
| 98 | + defp marshall_result({:error, _errored_step, _error_response, _steps}), do: {:error, :unexpected_transaction_outcome} |
| 99 | + |
| 100 | + @spec validate_payload(map) :: {:ok, :valid} | {:error, :invalid} |
| 101 | + defp validate_payload(%{} = payload) do |
| 102 | + case payload |> Installation.Validator.valid? do |
| 103 | + true -> {:ok, :valid} |
| 104 | + false -> {:error, :invalid} |
| 105 | + end |
74 | 106 | end |
75 | | - defp postprocess({:error, error}), do: {:error, error} |
76 | 107 |
|
77 | | - @spec find_user(any) :: User.t | nil |
78 | | - defp find_user(%{"id" => github_id}), do: User |> Repo.get_by(github_id: github_id) |
79 | | - defp find_user(_), do: :unexpected_user_payload |
| 108 | + @spec validate_action(map) :: {:ok, :implemented} | |
| 109 | + {:error, :not_yet_implemented} | |
| 110 | + {:error, :unexpected_action} |
| 111 | + defp validate_action(%{"action" => "created"}), do: {:ok, :implemented} |
| 112 | + defp validate_action(%{"action" => "deleted"}), do: {:error, :not_yet_implemented} |
| 113 | + defp validate_action(%{}), do: {:error, :unexpected_action} |
80 | 114 | end |
0 commit comments