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

Implements hooks support #25

Closed
wants to merge 14 commits into from
Closed

Implements hooks support #25

wants to merge 14 commits into from

Conversation

davejlong
Copy link
Member

@davejlong davejlong commented Nov 18, 2016

Starting work for #4. The hook API will enable developers to create exs files in hooks/ which will auto load. A hook would look like the following:

hook :slack do
  %{"text" => message} = conn.params
  broadcast! :slack_message, message
end

The slack hook above will generate a route at /hooks/slack. The underlying API of hook/2 will be leveraging the Plug.Route API so hooks will have access to the normal conn object just like any other route.

  • Remove variable conn is unused compiler warning
  • Fix issue Implements hooks support #25 (comment)
  • Have general hook handle params from form, query or JSON
  • Generate hooks directory when user creates new project

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.4%) to 80.208% when pulling 5a045eb on hooks into 9d3f150 on master.

@davejlong
Copy link
Member Author

Would it make sense to, when building the hook allow the dev to pass a custom route to generate?

hook :slack, "/slash" do
  ...
end

Which would then generate /hooks/slash instead of /hooks/slack.

I can't think of a use case, but it's fairly simple to include.

@zorbash
Copy link
Member

zorbash commented Nov 19, 2016

Concerning #25 (comment) i don't think we need such an option right now.

Webhooks are enabled through the `/hooks` route. Building hooks, similar
to jobs, is as easy as putting a file in the `hooks/` directory in a
Kitto project. The Hook API is very simple and similar to the
`Plug.Route` DSL building routes:

```elixir
use Kitto.Hooks.DSL

hook :github do
  {:ok, body, _} = read_body conn
  commits = GitHub.parse_commits_from_hook(body)
  broadcast! :github_commits, %{commits: commits}
end
```

The hook above generates a route at `/hooks/github` listening on all
HTTP methods. Hooks include both the `conn` object that routes include
as well as `broadcast!/2` to broadcast events to dashboards.
@davejlong davejlong changed the title [WIP] Implements hooks support Implements hooks support Nov 22, 2016
@@ -132,6 +132,27 @@ end
The above will spawn a supervised process which will emit a [server-sent
event](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) with the name `random` every second.

## Hooks

When jobs don't work, whether you want something more realtime than polling or
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When jobs don't work has a quite negative feeling doesn't it?


defp load_hooks, do: hook_files |> Enum.each(&Code.load_file/1)
defp hook_files do
[System.cwd, hook_dir, "**/*.{ex,exs}"] |> Path.join |> Path.wildcard
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should use Kitto.root instead of System.cwd

Hooks act like routes in `Plug.Route` and come complete with the `conn`
object for accessing request information.
"""
defmacro __using__(_opts) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add @doc false and a newline above.

plug :match
plug :dispatch

match "/:hook" do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should hooks be authenticated?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that. I would say no. Instead a lot of APIs when sending webhook requests will add a request signature to the HTTP headers of the request, authenticating that the webhook is coming from where it should be coming from.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add authentication later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String.to_atom hook could be DRYied here.

@@ -1,6 +1,18 @@
defmodule Kitto.TestHelper do
def atomify_map(map) do
for {key, value} <- map, into: %{}, do: {String.to_atom(key), value}
for {key, value} <- map, into: %{} do
{if(is_atom(key), do: key, else: String.to_atom(key)), value}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if is_atom(key), do: ..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't work in this context. If I change that line to:

{if is_atom(key), do: key, else: String.to_atom(key), value}

It generates a compile time SyntextError: test/test_helper.exs:4: syntax error before: value

plug :dispatch

match "/:hook" do
if Kitto.Hooks.hooks[String.to_atom hook] do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better alias both Kitto.Hooks and Kitto.Notifier at the top of the module definition.

@davejlong
Copy link
Member Author

All changes that could be implemented, or are not up for further discussion, are added.

@moduledoc """
DSL for building Webhook handlers. Define a new Webhook like follows:

defmodule MyHook do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have to define a module. It's documented (and tested) differently elsewhere.

@@ -0,0 +1,3 @@
use Kitto.Hooks.DSL

hook :hello, do: broadcast! :hello, %{text: "Hello World"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests print a warning about variable conn is unused. We have to do something about it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I saw that. Unfortunately, I cannot make the conn object available in hooks without getting that warning, as not every hook will necessarily use the conn object.

defmodule Kitto.Hooks.Router do
use Plug.Router

alias Kitto.{Notifier,Hooks}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Leave a space after ,.

defp hook_files do
[hook_dir, "**/*.{ex,exs}"] |> Path.join |> Path.wildcard
end
defp hook_dir, do: Application.get_env(:kitto, :hook_dir, default_hook_dir)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would one want to configure the hooks dir?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the reason was to make the logic easier to test, but I thought that maybe someone might want to change it somewhere in the world at some point... maybe.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have it configurable then. I can see how easier it get to test it this way.

@zorbash
Copy link
Member

zorbash commented Nov 26, 2016

@davejlong concerning the unused variable warning, we should go with something like:

defmacro hook(name, do: block) do
  quote do
    # Append the hook to the list of hooks
    Kitto.Hooks.register unquote(name), fn(var!(conn)) ->
       _ = var!(conn)
       unquote(block) 
     end
  end
end

Just like phoenix does at: https://github.com/phoenixframework/phoenix/blob/v1.2/lib/phoenix/endpoint.ex#L376

@davejlong
Copy link
Member Author

davejlong commented Nov 26, 2016 via email

@coveralls
Copy link

Coverage Status

Coverage increased (+1.4%) to 86.486% when pulling fd8aced on hooks into f129784 on master.

@davejlong
Copy link
Member Author

I'll work up an example of a Slack slash command using the hooks support, but I think we can go ahead and merge this in. Expect another blog post and PR for the slack command hook later this week.

Hooks are stored in the `hooks/` directory and are structured as follows:

```elixir
# File jobs/github.exs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be File: hooks/github.exs.

@@ -0,0 +1,3 @@
defmodule Kitto.Hooks.DSLTest do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go to test/fixtures/hooks/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops. I should actually remove it. It was a test case, but there's nothing in the DSL that isn't being tested elsewhere.

@zorbash
Copy link
Member

zorbash commented Dec 4, 2016

Will review this tomorrow (2016-12-05). It's the last thing keeping 0.3.0 from being released and it looks good.

defmodule Kitto.HooksTest do
use ExUnit.Case, async: true

# setup do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be commented out?

def handle_call({:lookup, hook}, _from, hooks) do
{:reply, Map.fetch(hooks, hook), hooks}
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✂️ Remove newline

defmodule Kitto.Hooks do
@moduledoc """
Kitto Hooks enable an alternative to Jobs for feeding data into dashboards.
Hooks enable remote services to push data into Kitto using webhooks.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hooks enable remote services to push data into Kitto using webhooks.

This sentence is a bit self-referencing.

the root of the application. Hooks can be defined as follows:

use Kitto.Hooks.DSL
hook :hello do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave a newline above the hook definition as we do with jobs.

use Kitto.Hooks.DSL
hook :hello do
{:ok, body, _} = read_body conn
broadcast! :hello, body |> Poison.decode!
Copy link
Member

@zorbash zorbash Dec 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newline above broadcast!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think we should also rewrite broadcast!/2 to broadcast!/1 calls.

## Hooks

If, instead of polling for new data from a data source, you want to act on data
as it changes, hooks are a useful feature for implementing webhooks to feed data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hooks are a useful feature for implementing webhooks

I find this a bit difficult to comprehend. Considering that someone may not be familiar with the concept of hooks, having hooks and webhooks in the same sentence can be a bit confusing.

hook :github do
{:ok, body, _} = read_body conn
commits = GitHub.parse_commits_from_hook(body)
broadcast! :github_commits, %{commits: commits}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add newline above.

```

The hook generates a route using the atom in the `hook/2` method. The hook above
will listen at `/hooks/github` on any HTTP method.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook above will respond to any HTTP method but actually only works with requests having a JSON body.

@@ -5,3 +5,5 @@ config :kitto, templates_dir: "test/fixtures/views"
config :kitto, default_layout: "layout"

config :logger, level: :warn

config :kitto, :hook_dir, "test/fixtures/hooks"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we stub this inside the hooks_test?

@@ -85,6 +86,7 @@ defmodule Kitto do
defp children(_env) do
[supervisor(__MODULE__, [], function: :start_server),
supervisor(Kitto.Notifier, []),
worker(Kitto.Hooks, []),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it doesn't depend on any of the other children, i think we should start it last.

@@ -0,0 +1,30 @@
defmodule Kitto.Hooks.Router do
use Plug.Router
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised credo doesn't nag about the missing @moduledoc here.

defp hook_files do
[hook_dir, "**/*.{ex,exs}"] |> Path.join |> Path.wildcard
end
defp hook_dir, do: Application.get_env(:kitto, :hook_dir, default_hook_dir)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


use GenServer

def start_link do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting it like https://github.com/kittoframework/kitto/blob/master/lib/kitto/runner.ex#L17
with a configurable name allows us to test it independently by spawning a new process (see: https://github.com/kittoframework/kitto/blob/master/test/runner_test.exs#L32) and then disposing it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making the server name dynamic is doable, but will take some time. I would suggest waiting until 0.4 if this is necessary for releasing the PR.

### Callbacks

def handle_call({:lookup, hook}, from, hooks) when is_atom(hook) do
handle_call({:lookup, Atom.to_string(hook)}, from, hooks)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace Atom.to_string(hook) with either "#{hook}" or hook |> to_string

@doc """
Registers a new hook into the registry
"""
def register(name, block) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be like https://github.com/kittoframework/kitto/blob/master/lib/kitto/runner.ex#L31

where the first param is a reference to the process.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that really necessary since the GenServer is always named :hook_registry?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and then I saw your previous comment above.

@zorbash zorbash modified the milestones: 0.4.0, 0.3.0 Dec 5, 2016

hook :slack do
%{"text" => text} = conn.params
broadcast! :slack_message, %{text: text}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newline above

# 3. In the setting for the URL use http://[my.kitto.dashboard]/hooks/slack
#
# As with most webhooks, your Kitto API will need to be publicly accessible
# to Slack. You can build verification using the `token` in the URL parameters
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can build verification using the token in the URL parameters

It's not entirely clear how the user is supposed to build authentication. We should either include it in the example, or mention it elsewhere (wiki).

@@ -0,0 +1,68 @@
defmodule Kitto.Hooks do
Copy link
Member

@zorbash zorbash Dec 5, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this module serves as a hook registry why not just name it Kitto.HookRegistry?


def init(:ok) do
load_hooks
{:ok, %{}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add newline above.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.9%) to 87.459% when pulling 8dd0181 on hooks into 55fe04b on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.5%) to 87.129% when pulling 7d49e9c on hooks into 55fe04b on master.

@davejlong
Copy link
Member Author

I'm going to close this for right now while I rethink the hook system. Will reopen a PR after I revise the concept to be more unified with the job running system.

@davejlong davejlong closed this Dec 31, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants