lucid-shopify 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +156 -0
- data/lib/lucid/shopify/activate_charge.rb +26 -0
- data/lib/lucid/shopify/authorise.rb +46 -0
- data/lib/lucid/shopify/client.rb +74 -0
- data/lib/lucid/shopify/config.rb +51 -0
- data/lib/lucid/shopify/container.rb +36 -0
- data/lib/lucid/shopify/create_all_webhooks.rb +27 -0
- data/lib/lucid/shopify/create_charge.rb +37 -0
- data/lib/lucid/shopify/create_webhook.rb +24 -0
- data/lib/lucid/shopify/credentials.rb +16 -0
- data/lib/lucid/shopify/delete_all_webhooks.rb +30 -0
- data/lib/lucid/shopify/delete_request.rb +17 -0
- data/lib/lucid/shopify/delete_webhook.rb +22 -0
- data/lib/lucid/shopify/error.rb +11 -0
- data/lib/lucid/shopify/get_request.rb +18 -0
- data/lib/lucid/shopify/post_request.rb +18 -0
- data/lib/lucid/shopify/put_request.rb +18 -0
- data/lib/lucid/shopify/redis_throttled_strategy.rb +75 -0
- data/lib/lucid/shopify/request.rb +55 -0
- data/lib/lucid/shopify/response.rb +127 -0
- data/lib/lucid/shopify/send_request.rb +101 -0
- data/lib/lucid/shopify/throttled_strategy.rb +51 -0
- data/lib/lucid/shopify/types.rb +11 -0
- data/lib/lucid/shopify/verify_callback.rb +55 -0
- data/lib/lucid/shopify/verify_webhook.rb +28 -0
- data/lib/lucid/shopify/version.rb +7 -0
- data/lib/lucid/shopify/webhook.rb +51 -0
- data/lib/lucid/shopify/webhook_handler_list.rb +39 -0
- data/lib/lucid/shopify/webhook_list.rb +29 -0
- data/lib/lucid/shopify.rb +71 -0
- data/lib/lucid-shopify.rb +3 -0
- metadata +199 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid/shopify'
|
4
|
+
|
5
|
+
if defined?(Redis)
|
6
|
+
module Lucid
|
7
|
+
module Shopify
|
8
|
+
# Use Redis to maintain API call limit throttling across threads/processes.
|
9
|
+
#
|
10
|
+
# No delay for requests up to half of the call limit.
|
11
|
+
class RedisThrottledStrategy < ThrottledStrategy
|
12
|
+
LEAK_RATE = 500
|
13
|
+
|
14
|
+
# @param redis_client [Redis]
|
15
|
+
def initialize(redis_client: Redis.current)
|
16
|
+
@redis_client = redis_client
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param request [Request]
|
20
|
+
#
|
21
|
+
# @yieldreturn [Response]
|
22
|
+
#
|
23
|
+
# @return [Response]
|
24
|
+
def call(request, &send_request)
|
25
|
+
interval_key = build_interval_key(request)
|
26
|
+
|
27
|
+
interval(interval_key)
|
28
|
+
|
29
|
+
send_request.().tap do |res|
|
30
|
+
header = res.headers['X-Shopify-Shop-Api-Call-Limit']
|
31
|
+
|
32
|
+
next if header.nil?
|
33
|
+
|
34
|
+
cur, max = header.split('/')
|
35
|
+
|
36
|
+
@redis_client.mapped_hmset(interval_key,
|
37
|
+
cur: cur,
|
38
|
+
max: max,
|
39
|
+
at: timestamp
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# If over half the call limit, sleep until requests leak back to the
|
45
|
+
# threshold.
|
46
|
+
#
|
47
|
+
# @param interval_key [String]
|
48
|
+
private def interval(interval_key)
|
49
|
+
cur, max, at = @redis_client.hmget(interval_key, :cur, :max, :at).map(&:to_i)
|
50
|
+
|
51
|
+
cur = leak(cur, at)
|
52
|
+
|
53
|
+
delay_threshold = max / 2 # no delay
|
54
|
+
|
55
|
+
if cur > delay_threshold
|
56
|
+
sleep(Rational((cur - delay_threshold) * LEAK_RATE, 1000))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Find the actual value of {cur}, by subtracting requests leaked by the
|
61
|
+
# leaky bucket algorithm since the value was set.
|
62
|
+
#
|
63
|
+
# @param cur [Integer]
|
64
|
+
# @param at [Integer]
|
65
|
+
#
|
66
|
+
# @return [Integer]
|
67
|
+
private def leak(cur, at)
|
68
|
+
n = Rational(timestamp - at, LEAK_RATE).floor
|
69
|
+
|
70
|
+
n > cur ? 0 : cur - n
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid/shopify'
|
4
|
+
|
5
|
+
module Lucid
|
6
|
+
module Shopify
|
7
|
+
# @abstract
|
8
|
+
class Request
|
9
|
+
extend Dry::Initializer
|
10
|
+
|
11
|
+
# @return [Credentials]
|
12
|
+
param :credentials
|
13
|
+
# @return [Symbol]
|
14
|
+
param :http_method
|
15
|
+
# @return [String] the endpoint relative to the base URL
|
16
|
+
param :path, reader: :private
|
17
|
+
# @return [Hash]
|
18
|
+
param :options, default: -> { {} }
|
19
|
+
|
20
|
+
# @return [Hash]
|
21
|
+
param :http_headers, default: -> { build_headers }
|
22
|
+
# @return [String]
|
23
|
+
param :url, default: -> { build_url }
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
private def build_url
|
27
|
+
unless path.match?(/oauth/)
|
28
|
+
admin_url = "https://#{credentials.myshopify_domain}/admin/api/#{api_version}"
|
29
|
+
else
|
30
|
+
admin_url = "https://#{credentials.myshopify_domain}/admin"
|
31
|
+
end
|
32
|
+
|
33
|
+
normalised_path = path.sub(/^\//, '')
|
34
|
+
normalised_path = path.sub(/\.json$/, '')
|
35
|
+
|
36
|
+
admin_url + '/' + normalised_path + '.json'
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Hash]
|
40
|
+
private def build_headers
|
41
|
+
access_token = credentials.access_token
|
42
|
+
|
43
|
+
{}.tap do |headers|
|
44
|
+
headers['Accept'] = 'application/json'
|
45
|
+
headers['X-Shopify-Access-Token'] = access_token if access_token
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [String]
|
50
|
+
private def api_version
|
51
|
+
ENV.fetch('SHOPIFY_API_VERSION', Shopify.config.api_version)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require 'lucid/shopify'
|
6
|
+
|
7
|
+
module Lucid
|
8
|
+
module Shopify
|
9
|
+
class Response
|
10
|
+
# @abstract
|
11
|
+
class Error < Error
|
12
|
+
extend Dry::Initializer
|
13
|
+
|
14
|
+
# @return [Request]
|
15
|
+
param :request
|
16
|
+
# @return [Response]
|
17
|
+
param :response
|
18
|
+
|
19
|
+
# @return [String]
|
20
|
+
def message
|
21
|
+
"bad response (#{response.status_code})"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
ClientError = Class.new(Error)
|
26
|
+
ServerError = Class.new(Error)
|
27
|
+
ShopError = Class.new(Error)
|
28
|
+
|
29
|
+
extend Dry::Initializer
|
30
|
+
|
31
|
+
include Enumerable
|
32
|
+
|
33
|
+
# @return [Request] the original request
|
34
|
+
param :request
|
35
|
+
# @return [Integer]
|
36
|
+
param :status_code
|
37
|
+
# @return [Hash]
|
38
|
+
param :headers
|
39
|
+
# @return [String]
|
40
|
+
param :data
|
41
|
+
|
42
|
+
# The parsed response body.
|
43
|
+
#
|
44
|
+
# @return [Hash]
|
45
|
+
def data_hash
|
46
|
+
return {} unless json?
|
47
|
+
|
48
|
+
@data_hash ||= JSON.parse(data)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Boolean]
|
52
|
+
private def json?
|
53
|
+
headers['Content-Type'] =~ /application\/json/ && !data.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [self]
|
57
|
+
#
|
58
|
+
# @raise [ClientError] for status 4xx
|
59
|
+
# @raise [ServerError] for status 5xx
|
60
|
+
#
|
61
|
+
# @note https://help.shopify.com/en/api/getting-started/response-status-codes
|
62
|
+
def assert!
|
63
|
+
case status_code
|
64
|
+
when 402
|
65
|
+
raise ShopError.new(request, self), 'Shop is frozen, awaiting payment'
|
66
|
+
when 423
|
67
|
+
raise ShopError.new(request, self), 'Shop is locked'
|
68
|
+
when 400..499
|
69
|
+
raise ClientError.new(request, self)
|
70
|
+
when 500..599
|
71
|
+
raise ServerError.new(request, self)
|
72
|
+
end
|
73
|
+
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Boolean]
|
78
|
+
def success?
|
79
|
+
status_code.between?(200, 299)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Boolean]
|
83
|
+
def failure?
|
84
|
+
!success?
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Boolean]
|
88
|
+
def errors?
|
89
|
+
data_hash.has_key?('errors')
|
90
|
+
end
|
91
|
+
|
92
|
+
# A string rather than an object is returned by Shopify in the case of,
|
93
|
+
# e.g., 'Not found'. In this case, we return it under the 'resource' key.
|
94
|
+
#
|
95
|
+
# @return [Hash, nil]
|
96
|
+
def errors
|
97
|
+
errors = data_hash['errors']
|
98
|
+
return {'resource' => errors} if errors.is_a?(String)
|
99
|
+
errors
|
100
|
+
end
|
101
|
+
|
102
|
+
# @see Hash#each
|
103
|
+
def each(&block)
|
104
|
+
data_hash.each(&block)
|
105
|
+
end
|
106
|
+
|
107
|
+
# @param key [String]
|
108
|
+
#
|
109
|
+
# @return [Object]
|
110
|
+
def [](key)
|
111
|
+
data_hash[key]
|
112
|
+
end
|
113
|
+
|
114
|
+
alias_method :to_h, :data_hash
|
115
|
+
|
116
|
+
# @return [Hash]
|
117
|
+
def as_json(*)
|
118
|
+
to_h
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [String]
|
122
|
+
def to_json(*args)
|
123
|
+
as_json.to_json(*args)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid/shopify'
|
4
|
+
|
5
|
+
module Lucid
|
6
|
+
module Shopify
|
7
|
+
class SendRequest
|
8
|
+
class NetworkError < Error
|
9
|
+
extend Dry::Initializer
|
10
|
+
|
11
|
+
# @return [HTTP::Error]
|
12
|
+
param :original_exception
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param http [HTTP::Client]
|
16
|
+
# @param strategy [#call, nil] unthrottled by default
|
17
|
+
def initialize(http: Container[:http],
|
18
|
+
strategy: ->(*, &block) { block.() })
|
19
|
+
@http = http
|
20
|
+
@strategy = strategy
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param request [Request]
|
24
|
+
# @param attempts [Integer] additional request attempts on client error
|
25
|
+
#
|
26
|
+
# @return [Hash] the parsed response body
|
27
|
+
#
|
28
|
+
# @raise [NetworkError] if the request failed all attempts
|
29
|
+
# @raise [Response::ClientError] for status 4xx
|
30
|
+
# @raise [Response::ServerError] for status 5xx
|
31
|
+
def call(request, attempts: default_attempts)
|
32
|
+
req = request
|
33
|
+
|
34
|
+
log_request(req)
|
35
|
+
|
36
|
+
res = @strategy.(req) do
|
37
|
+
res = send(req)
|
38
|
+
|
39
|
+
Response.new(req, res.code, res.headers.to_h, res.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
log_response(req, res)
|
43
|
+
|
44
|
+
res.assert!
|
45
|
+
rescue HTTP::ConnectionError,
|
46
|
+
HTTP::ResponseError,
|
47
|
+
HTTP::TimeoutError => e
|
48
|
+
raise NetworkError.new(e), e.message if attempts.zero?
|
49
|
+
|
50
|
+
call(req, attempts: attempts - 1)
|
51
|
+
rescue Response::ClientError => e
|
52
|
+
raise e unless e.response.status_code == 429
|
53
|
+
|
54
|
+
sleep(e.response.headers['Retry-After']&.to_f || 0)
|
55
|
+
|
56
|
+
call(req, attempts: attempts)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param request [Request]
|
60
|
+
#
|
61
|
+
# @return [HTTP::Response]
|
62
|
+
private def send(request)
|
63
|
+
req = request
|
64
|
+
|
65
|
+
@http.headers(req.http_headers).__send__(req.http_method, req.url, req.options)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param request [Request]
|
69
|
+
def log_request(request)
|
70
|
+
req = request
|
71
|
+
|
72
|
+
Shopify.config.logger.info('<%s> [%i] %s %s %s' % [
|
73
|
+
self.class.to_s,
|
74
|
+
req.object_id,
|
75
|
+
req.http_method.to_s.upcase,
|
76
|
+
req.url,
|
77
|
+
req.options[:params]&.to_json || '{}',
|
78
|
+
])
|
79
|
+
end
|
80
|
+
|
81
|
+
# @param request [Request]
|
82
|
+
# @param response [Response]
|
83
|
+
def log_response(request, response)
|
84
|
+
req = request
|
85
|
+
res = response
|
86
|
+
|
87
|
+
Shopify.config.logger.info('<%s> [%i] %i (%s)' % [
|
88
|
+
self.class.to_s,
|
89
|
+
req.object_id,
|
90
|
+
res.status_code,
|
91
|
+
res.headers['X-Shopify-Shop-Api-Call-Limit'],
|
92
|
+
])
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Integer]
|
96
|
+
private def default_attempts
|
97
|
+
3
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lucid/shopify'
|
4
|
+
|
5
|
+
module Lucid
|
6
|
+
module Shopify
|
7
|
+
# Maintain API call limit throttling across a single thread.
|
8
|
+
class ThrottledStrategy
|
9
|
+
MINIMUM_INTERVAL = 500 # ms
|
10
|
+
|
11
|
+
# @param request [Request]
|
12
|
+
#
|
13
|
+
# @yieldreturn [Response]
|
14
|
+
#
|
15
|
+
# @return [Response]
|
16
|
+
def call(request, &send_request)
|
17
|
+
interval(build_interval_key(request))
|
18
|
+
|
19
|
+
send_request.()
|
20
|
+
end
|
21
|
+
|
22
|
+
# If time since the last request < {MINIMUM_INTERVAL}, then sleep for the
|
23
|
+
# difference.
|
24
|
+
#
|
25
|
+
# @param interval_key [String]
|
26
|
+
private def interval(interval_key)
|
27
|
+
if Thread.current[interval_key]
|
28
|
+
(timestamp - Thread.current[interval_key]).tap do |n|
|
29
|
+
sleep(Rational(MINIMUM_INTERVAL - n, 1000)) if n < MINIMUM_INTERVAL
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Thread.current[interval_key] = timestamp
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param request [Request]
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
private def build_interval_key(request)
|
40
|
+
'%s[%s].timestamp' % [self.class, request.credentials.myshopify_domain]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Time in milliseconds since the UNIX epoch.
|
44
|
+
#
|
45
|
+
# @return [Integer]
|
46
|
+
private def timestamp
|
47
|
+
(Time.now.to_f * 1000).to_i
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
require 'lucid/shopify'
|
6
|
+
|
7
|
+
module Lucid
|
8
|
+
module Shopify
|
9
|
+
class VerifyCallback
|
10
|
+
Error = Class.new(Error)
|
11
|
+
|
12
|
+
# Verify that the callback request originated from Shopify.
|
13
|
+
#
|
14
|
+
# @param params [Hash] the request params
|
15
|
+
#
|
16
|
+
# @raise [Error] if signature is invalid
|
17
|
+
def call(params)
|
18
|
+
params = params.to_h.transform_keys(&:to_s)
|
19
|
+
digest = OpenSSL::Digest::SHA256.new
|
20
|
+
digest = OpenSSL::HMAC.hexdigest(digest, Shopify.config.shared_secret, encoded_params(params))
|
21
|
+
|
22
|
+
raise Error, 'invalid signature' unless digest == params['hmac']
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param params [Hash]
|
26
|
+
#
|
27
|
+
# @return [String]
|
28
|
+
private def encoded_params(params)
|
29
|
+
params.reject do |k, _|
|
30
|
+
k == 'hmac'
|
31
|
+
end.map do |k, v|
|
32
|
+
[].tap do |param|
|
33
|
+
param << k.gsub(/./) { |c| encode_key(c) }
|
34
|
+
param << '='
|
35
|
+
param << v.gsub(/./) { |c| encode_val(c) }
|
36
|
+
end.join
|
37
|
+
end.join('&')
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param chr [String]
|
41
|
+
#
|
42
|
+
# @return [String]
|
43
|
+
private def encode_key(chr)
|
44
|
+
{'%' => '%25', '&' => '%26', '=' => '%3D'}[chr] || chr
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param chr [String]
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
private def encode_val(chr)
|
51
|
+
{'%' => '%25', '&' => '%26'}[chr] || chr
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
require 'lucid/shopify'
|
7
|
+
|
8
|
+
module Lucid
|
9
|
+
module Shopify
|
10
|
+
class VerifyWebhook
|
11
|
+
Error = Class.new(Error)
|
12
|
+
|
13
|
+
# Verify that the webhook request originated from Shopify.
|
14
|
+
#
|
15
|
+
# @param data [String] the signed request data
|
16
|
+
# @param hmac [String] the signature
|
17
|
+
#
|
18
|
+
# @raise [Error] if signature is invalid
|
19
|
+
def call(data, hmac)
|
20
|
+
digest = OpenSSL::Digest::SHA256.new
|
21
|
+
digest = OpenSSL::HMAC.digest(digest, Shopify.config.shared_secret, data)
|
22
|
+
digest = Base64.encode64(digest).strip
|
23
|
+
|
24
|
+
raise Error, 'invalid signature' unless digest == hmac
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require 'lucid/shopify'
|
6
|
+
|
7
|
+
module Lucid
|
8
|
+
module Shopify
|
9
|
+
class Webhook
|
10
|
+
extend Dry::Initializer
|
11
|
+
|
12
|
+
# @return [String]
|
13
|
+
param :myshopify_domain
|
14
|
+
# @return [String]
|
15
|
+
param :topic
|
16
|
+
# @return [String]
|
17
|
+
param :data
|
18
|
+
|
19
|
+
# @return [Hash]
|
20
|
+
def data_hash
|
21
|
+
@data_hash ||= JSON.parse(data)
|
22
|
+
rescue JSON::ParserError
|
23
|
+
{}
|
24
|
+
end
|
25
|
+
|
26
|
+
# @see Hash#each
|
27
|
+
def each(&block)
|
28
|
+
data_hash.each(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param key [String]
|
32
|
+
#
|
33
|
+
# @return [Object]
|
34
|
+
def [](key)
|
35
|
+
data_hash[key]
|
36
|
+
end
|
37
|
+
|
38
|
+
alias_method :to_h, :data_hash
|
39
|
+
|
40
|
+
# @return [Hash]
|
41
|
+
def as_json(*)
|
42
|
+
to_h
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [String]
|
46
|
+
def to_json(*args)
|
47
|
+
as_json.to_json(*args)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lucid
|
4
|
+
module Shopify
|
5
|
+
class WebhookHandlerList
|
6
|
+
def initialize
|
7
|
+
@handlers = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Register a handler for a webhook topic. The callable handler should
|
11
|
+
# receive a single {Webhook} argument.
|
12
|
+
#
|
13
|
+
# @param topic [String]
|
14
|
+
# @param handler [#call]
|
15
|
+
def register(topic, handler = nil, &block)
|
16
|
+
raise ArgumentError unless nil ^ handler ^ block
|
17
|
+
|
18
|
+
handler = block if block
|
19
|
+
|
20
|
+
@handlers[topic] ||= []
|
21
|
+
@handlers[topic] << handler
|
22
|
+
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param topic [String]
|
27
|
+
def [](topic)
|
28
|
+
@handlers[topic] || []
|
29
|
+
end
|
30
|
+
|
31
|
+
# Call each of the handlers registered for the given topic in turn.
|
32
|
+
#
|
33
|
+
# @param webhook [Webhook]
|
34
|
+
def delegate(webhook)
|
35
|
+
self[webhook.topic].each { |handler| handler.(webhook) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lucid
|
4
|
+
module Shopify
|
5
|
+
class WebhookList
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@webhooks = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# @yield [Hash]
|
13
|
+
def each(&block)
|
14
|
+
@webhooks.each(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param topic [String]
|
18
|
+
# @param fields [String] e.g. 'id,tags'
|
19
|
+
def register(topic, fields: nil)
|
20
|
+
@webhooks << {}.tap do |webhook|
|
21
|
+
webhook[:topic] = topic
|
22
|
+
webhook[:fields] = fields if fields
|
23
|
+
end
|
24
|
+
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|