The document discusses refactoring code to centralize business logic using pipelines in Elixir. It describes how the code was originally scattered across different modules, with the parsing, expiration checking, and sending of webhooks handled separately. The refactored code defines a WebhookState struct and uses pipelines to chain the parsing, checking, and sending functions together. This leads to more modular, readable code with centralized business logic.
17. Our goal when using a functional
paradigm is to think out our
program as one big function,
transforming its inputs into
outputs
— Dave Thomas, "Elixir for Programmers"
17
18. THE STORY SO FAR
> PagerDuty enables engineers to go on-call
> Webhooks let you send data to different systems
> My teams owns the webhooks service
18
21. defmodule Processor do
def process(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload} ->
HTTP.post(endpoint, payload)
{:error, error_msg} ->
Logger.error("error - #{error_msg}")
end
end
end
21
22. defmodule Processor do
def process(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload} ->
HTTP.post(endpoint, payload)
{:error, error_msg} ->
Logger.error("error - #{error_msg}")
end
end
end
21
23. defmodule Processor do
def process(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload} ->
HTTP.post(endpoint, payload)
{:error, error_msg} ->
Logger.error("error - #{error_msg}")
end
end
end
21
24. defmodule Parser do
@expired_threshold_seconds 5
def parse(raw_webhook) do
decoded_webhook = JSON.decode(raw_webhook)
endpoint = decoded_webhook.endpoint
payload = decoded_webhook.payload
time_since_created = decoded_webhook.created_at - Time.now
is_expired = time_since_created > @expired_threshold_seconds * 1000
case is_expired do
true -> {:error, "webhook expired"}
false -> {:ok, endpoint, payload}
end
end
22
25. defmodule Parser do
@expired_threshold_seconds 5
def parse(raw_webhook) do
decoded_webhook = JSON.decode(raw_webhook)
endpoint = decoded_webhook.endpoint
payload = decoded_webhook.payload
time_since_created = decoded_webhook.created_at - Time.now
is_expired = time_since_created > @expired_threshold_seconds * 1000
case is_expired do
true -> {:error, "webhook expired"}
false -> {:ok, endpoint, payload}
end
end
22
26. defmodule Parser do
@expired_threshold_seconds 5
def parse(raw_webhook) do
decoded_webhook = JSON.decode(raw_webhook)
endpoint = decoded_webhook.endpoint
payload = decoded_webhook.payload
time_since_created = decoded_webhook.created_at - Time.now
is_expired = time_since_created > @expired_threshold_seconds * 1000
case is_expired do
true -> {:error, "webhook expired"}
false -> {:ok, endpoint, payload}
end
end
22
27. defmodule Parser do
@expired_threshold_seconds 5
def parse(raw_webhook) do
decoded_webhook = JSON.decode(raw_webhook)
endpoint = decoded_webhook.endpoint
payload = decoded_webhook.payload
time_since_created = decoded_webhook.created_at - Time.now
is_expired = time_since_created > @expired_threshold_seconds * 1000
case is_expired do
true -> {:error, "webhook expired"}
false -> {:ok, endpoint, payload}
end
end
22
28. defmodule Parser do
@expired_threshold_seconds 5
def parse(raw_webhook) do
decoded_webhook = JSON.decode(raw_webhook)
endpoint = decoded_webhook.endpoint
payload = decoded_webhook.payload
time_since_created = decoded_webhook.created_at - Time.now
is_expired = time_since_created > @expired_threshold_seconds * 1000
case is_expired do
true -> {:error, "webhook expired"}
false -> {:ok, endpoint, payload}
end
end
22
29. defmodule Processor do
def process(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload} ->
HTTP.post(endpoint, payload)
{:error, error_msg} ->
Logger.error("error - #{error_msg}")
end
end
end
23
30. defmodule Processor do
def process(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload} ->
HTTP.post(endpoint, payload)
{:error, error_msg} ->
Logger.error("error - #{error_msg}")
end
end
end
23
31. defmodule Processor do
def process(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload} ->
HTTP.post(endpoint, payload)
{:error, error_msg} ->
Logger.error("error - #{error_msg}")
end
end
end
23
47. def process(raw_webhook) do
with(
{:ok, webhook} <- parse(raw_webhook),
{:ok, webhook} <- check_expired(webhook),
{:ok, webhook} <- send(webhook)
) do
Logger.info("webhook processed successfully :) !")
:ok
else
{:error, msg} ->
Logger.error("error processing webhook :( ! - #{msg}")
:error
end
end
35
48. defp parse(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload, created_at} ->
{:ok, %WebhookState{
endpoint: endpoint,
payload: payload,
created_at: created_at
}}
{:error, error_msg} ->
{:error, "failed to parse webhook"}
end
end
36
49. defp parse(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload, created_at} ->
{:ok, %WebhookState{
endpoint: endpoint,
payload: payload,
created_at: created_at
}}
{:error, error_msg} ->
{:error, "failed to parse webhook"}
end
end
36
50. defmodule Parser do
def parse(raw_webhook) do
decoded_webhook = JSON.decode(raw_webhook)
endpoint = decoded_webhook.event.endpoint
payload = decoded_webhook.event.payload
created_at = decoded_webhook.event.created_at
# no more expired webhook logic :) !
{:ok, endpoint, payload, created_at}
end
end
37
51. defmodule Parser do
def parse(raw_webhook) do
decoded_webhook = JSON.decode(raw_webhook)
endpoint = decoded_webhook.event.endpoint
payload = decoded_webhook.event.payload
created_at = decoded_webhook.event.created_at
# no more expired webhook logic :) !
{:ok, endpoint, payload, created_at}
end
end
37
52. defp parse(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload, created_at} ->
{:ok, %WebhookState{
endpoint: endpoint,
payload: payload,
created_at: created_at
}}
{:error, error_msg} ->
{:error, "failed to parse webhook"}
end
end
38
53. defp parse(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload, created_at} ->
{:ok, %WebhookState{
endpoint: endpoint,
payload: payload,
created_at: created_at
}}
{:error, error_msg} ->
{:error, "failed to parse webhook"}
end
end
38
54. defp parse(raw_webhook) do
parsed_webhook = Parser.parse(raw_webhook)
case parsed_webhook do
{:ok, endpoint, payload, created_at} ->
{:ok, %WebhookState{
endpoint: endpoint,
payload: payload,
created_at: created_at
}}
{:error, error_msg} ->
{:error, "failed to parse webhook"}
end
end
38
55. def process(raw_webhook) do
with(
{:ok, webhook} <- parse(raw_webhook),
{:ok, webhook} <- check_expired(webhook),
{:ok, webhook} <- send(webhook)
) do
Logger.info("webhook processed successfully :) !")
:ok
else
{:error, msg} ->
Logger.error("error processing webhook :( ! - #{msg}")
:error
end
end
39
56. def process(raw_webhook) do
with(
{:ok, webhook} <- parse(raw_webhook),
{:ok, webhook} <- check_expired(webhook),
{:ok, webhook} <- send(webhook)
) do
Logger.info("webhook processed successfully :) !")
:ok
else
{:error, msg} ->
Logger.error("error processing webhook :( ! - #{msg}")
:error
end
end
39
57. @expired_threshold_seconds 5
defp check_expired(webhook = %WebhookState{created_at: created_at}) do
# this was moved from Parser to Processor :) !
time_since_created = created_at - Time.now
is_expired = time_since_created > @expired_threshold_seconds * 1000
case is_expired do
true ->
{:error, "webhook expired"}
false ->
{:ok, webhook}
end
end
40
58. @expired_threshold_seconds 5
defp check_expired(webhook = %WebhookState{created_at: created_at}) do
# this was moved from Parser to Processor :) !
time_since_created = created_at - Time.now
is_expired = time_since_created > @expired_threshold_seconds * 1000
case is_expired do
true ->
{:error, "webhook expired"}
false ->
{:ok, webhook}
end
end
40
59. @expired_threshold_seconds 5
defp check_expired(webhook = %WebhookState{created_at: created_at}) do
# this was moved from Parser to Processor :) !
time_since_created = created_at - Time.now
is_expired = time_since_created > @expired_threshold_seconds * 1000
case is_expired do
true ->
{:error, "webhook expired"}
false ->
{:ok, webhook}
end
end
40
60. def process(raw_webhook) do
with(
{:ok, webhook} <- parse(raw_webhook),
{:ok, webhook} <- check_expired(webhook),
{:ok, webhook} <- send(webhook)
) do
Logger.info("webhook processed successfully :) !")
:ok
else
{:error, msg} ->
Logger.error("error processing webhook :( ! - #{msg}")
:error
end
end
41
61. def process(raw_webhook) do
with(
{:ok, webhook} <- parse(raw_webhook),
{:ok, webhook} <- check_expired(webhook),
{:ok, webhook} <- send(webhook)
) do
Logger.info("webhook processed successfully :) !")
:ok
else
{:error, msg} ->
Logger.error("error processing webhook :( ! - #{msg}")
:error
end
end
41
62. defp send(webhook = %WebhookState{endpoint: endpoint, payload: payload}) do
HTTP.post(endpoint, payload)
end
42
63. defp send(webhook = %WebhookState{endpoint: endpoint, payload: payload}) do
HTTP.post(endpoint, payload)
end
42