Skip to content

Commit

Permalink
Refactor to use multiple handler modules
Browse files Browse the repository at this point in the history
  • Loading branch information
lizkenyon committed Jan 24, 2024
1 parent 5a9436a commit 0735948
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 52 deletions.
29 changes: 11 additions & 18 deletions docs/usage/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,34 @@ If using in the Rails framework, we highly recommend you use the [shopify_app](h

## Create a Webhook Handler

### New webhook handler as of Version 13.5.0
If you want to register for an http webhook you need to implement a webhook handler which the `shopify_api` gem can use to determine how to process your webhook. You can make multiple implementations (one per topic) or you can make one implementation capable of handling all the topics you want to subscribe to. To do this simply make a module or class that includes or extends `ShopifyAPI::Webhooks::WebhookHandler` and implement the `handle_webhook` method which accepts the following named parameters: data: `Hash[Symbol, untyped]`. An example implementation is shown below:

You will also have to define a method called `use_handle_webhook?` which returns a boolean. If this method returns `true` then the `handle_webhook` method will be called when a webhook is received. If this method returns `false` then the `handle` method will be called when a webhook is received. The `handle` method is the old way of handling webhooks and will be deprecated in a future version of the gem. When using this method you will not have access to the `webhook_id` or `api_version` of the webhook.
If you want to register for an http webhook you need to implement a webhook handler which the `shopify_api` gem can use to determine how to process your webhook. You can make multiple implementations (one per topic) or you can make one implementation capable of handling all the topics you want to subscribe to. To do this simply make a module or class that includes or extends `ShopifyAPI::Webhooks::WebhookHandler` and implement the `handle` method which accepts the following named parameters: data: `WebhookMetadata`. An example implementation is shown below:

`data` will have the following keys
- `:topic` - The topic of the webhook
- `:shop` - The shop domain of the webhook
- `:body` - The body of the webhook
- `:webhook_id` - The id of the webhook event to [avoid duplicates](https://shopify.dev/docs/apps/webhooks/best-practices#ignore-duplicates)
- `:api_version` - The api version of the webhook
- `topic`, `String` - The topic of the webhook
- `shop`, `String` - The shop domain of the webhook
- `body`, `T::Hash[String, T.untyped]`- The body of the webhook
- `webhook_id`, `String` - The id of the webhook event to [avoid duplicates](https://shopify.dev/docs/apps/webhooks/best-practices#ignore-duplicates)
- `api_version`, `String` - The api version of the webhook

```ruby
module WebhookHandler
extend ShopifyAPI::Webhooks::Handler

class << self
def handle_webhook(data)
puts "Received webhook! topic: #{data[:topic]} shop: #{data[:shop]} body: #{data[:body]} webhook_id: #{data[:webhook_id]} api_version: #{data[:api_version]"
end
def use_handle_webhook?
true
puts "Received webhook! topic: #{data.topic} shop: #{data.shop} body: #{data.body} webhook_id: #{data.webhook_id} api_version: #{data.api_version"
end
end
end
```
**Note:** As of version 13.5.0 the `handle` method is still available to be used but will be removed in a future version of the gem. It is recommended that you use the `handle_webhook` method instead.
**Note:** As of version 13.5.0 the `ShopifyAPI::Webhooks::Handler` class is still available to be used but will be removed in a future version of the gem.
### Best Practices
It is recommended that in order to respond quickly to the Shopify webhook request that the handler not do any heavy logic or network calls, rather it should simply enqueue the work in some job queue in order to be executed later.
### Webhook Handler for versions 13.4.0 and prior
If you want to register for an http webhook you need to implement a webhook handler which the `shopify_api` gem can use to determine how to process your webhook. You can make multiple implementations (one per topic) or you can make one implementation capable of handling all the topics you want to subscribe to. To do this simply make a module or class that includes or extends `ShopifyAPI::Webhooks::WebhookHandler` and implement the handle method which accepts the following named parameters: topic: `String`, shop: `String`, and body: `Hash[String, untyped]`. An example implementation is shown below:
If you want to register for an http webhook you need to implement a webhook handler which the `shopify_api` gem can use to determine how to process your webhook. You can make multiple implementations (one per topic) or you can make one implementation capable of handling all the topics you want to subscribe to. To do this simply make a module or class that includes or extends `ShopifyAPI::Webhooks::Handler` and implement the handle method which accepts the following named parameters: topic: `String`, shop: `String`, and body: `Hash[String, untyped]`. An example implementation is shown below:
```ruby
module WebhookHandler
Expand All @@ -53,8 +48,6 @@ module WebhookHandler
end
```
**Note:** It is recommended that in order to respond quickly to the Shopify webhook request that the handler not do any heavy logic or network calls, rather it should simply enqueue the work in some job queue in order to be executed later.
## Add to Webhook Registry
The next step is to add all the webhooks you would like to subscribe to for any shop to the webhook registry. To do this you can call `ShopifyAPI::Webhooks::Registry.add_registration` for each webhook you would like to handle. `add_registration` accepts a topic string, a delivery_method symbol (currently supporting `:http`, `:event_bridge`, and `:pub_sub`), a webhook path (the relative path for an http webhook) and a handler. This only needs to be done once when the app is started and we recommend doing this at the same time that you setup `ShopifyAPI::Context`. An example is shown below to register an http webhook:
Expand Down
32 changes: 19 additions & 13 deletions lib/shopify_api/webhooks/handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,36 @@

module ShopifyAPI
module Webhooks
class WebhookMetadata < T::Struct
const :topic, String
const :shop, String
const :body, T::Hash[String, T.untyped]
const :api_version, String
const :webhook_id, String
end

module Handler
include Kernel
extend T::Sig
extend T::Helpers
abstract!

sig do
params(data: T::Hash[Symbol, T.untyped]).void
end
def handle_webhook(data)
if use_handle_webhook?
raise NotImplementedError, "You must implement the `handle_webhook` method in your webhook handler class."
end
end
interface!

sig do
abstract.params(topic: String, shop: String, body: T::Hash[String, T.untyped]).void
end
def handle(topic:, shop:, body:); end
end

module WebhookHandler
include Kernel
extend T::Sig
extend T::Helpers
interface!

sig { returns(T::Boolean) }
def use_handle_webhook?
false
sig do
abstract.params(data: WebhookMetadata).void
end
def handle(data:); end
end
end
end
4 changes: 2 additions & 2 deletions lib/shopify_api/webhooks/registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Registration
sig { returns(String) }
attr_reader :topic

sig { returns(T.nilable(Handler)) }
sig { returns(T.nilable(T.any(Handler, WebhookHandler))) }
attr_reader :handler

sig { returns(T.nilable(T::Array[String])) }
Expand All @@ -23,7 +23,7 @@ class Registration
attr_reader :metafield_namespaces

sig do
params(topic: String, path: String, handler: T.nilable(Handler),
params(topic: String, path: String, handler: T.nilable(T.any(Handler, WebhookHandler)),
fields: T.nilable(T.any(String, T::Array[String])),
metafield_namespaces: T.nilable(T::Array[String])).void
end
Expand Down
8 changes: 4 additions & 4 deletions lib/shopify_api/webhooks/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class << self
params(topic: String,
delivery_method: Symbol,
path: String,
handler: T.nilable(Handler),
handler: T.nilable(T.any(Handler, WebhookHandler)),
fields: T.nilable(T.any(String, T::Array[String])),
metafield_namespaces: T.nilable(T::Array[String])).void
end
Expand Down Expand Up @@ -193,9 +193,9 @@ def process(request)
raise Errors::NoWebhookHandler, "No webhook handler found for topic: #{request.topic}."
end

if handler.use_handle_webhook?
handler.handle_webhook({ topic: request.topic, shop: request.shop, body: request.parsed_body,
webhook_id: request.webhook_id, api_version: request.api_version, })
if handler.is_a?(WebhookHandler)
handler.handle(data: WebhookMetadata.new(topic: request.topic, shop: request.shop,
body: request.parsed_body, api_version: request.api_version, webhook_id: request.webhook_id))
else
handler.handle(topic: request.topic, shop: request.shop, body: request.parsed_body)
ShopifyAPI::Logger.warn(<<~WARNING)
Expand Down
12 changes: 2 additions & 10 deletions test/test_helpers/new_fake_webhook_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,14 @@

module TestHelpers
class NewFakeWebhookHandler
include ShopifyAPI::Webhooks::Handler
include ShopifyAPI::Webhooks::WebhookHandler

def initialize(handler)
@handler = handler
end

def handle(topic:, shop:, body:)
@handler.call(topic, shop, body)
end

def handle_webhook(data)
def handle(data:)
@handler.call(data)
end

def use_handle_webhook?
true
end
end
end
10 changes: 5 additions & 5 deletions test/webhooks/registry_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ def test_process_new_handler

handler = TestHelpers::NewFakeWebhookHandler.new(
lambda do |data|
assert_equal(@topic, data[:topic])
assert_equal(@shop, data[:shop])
assert_equal({}, data[:body])
assert_equal("b1234-eefd-4c9e-9520-049845a02082", data[:webhook_id])
assert_equal("2024-01", data[:api_version])
assert_equal(@topic, data.topic)
assert_equal(@shop, data.shop)
assert_equal({}, data.body)
assert_equal("b1234-eefd-4c9e-9520-049845a02082", data.webhook_id)
assert_equal("2024-01", data.api_version)
handler_called = true
end,
)
Expand Down

0 comments on commit 0735948

Please sign in to comment.