webhook_system 0.1.1 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 845cc8863c319a2a78ff235cbb41256a9bdbe850
4
- data.tar.gz: afa19be64345c961c0989c09d09fc80341200ab4
3
+ metadata.gz: 7949660ffcd7499c9ad2cd90030a700c984ee81f
4
+ data.tar.gz: db0e7b0233716c187c67cd1ec09560eb053ee30b
5
5
  SHA512:
6
- metadata.gz: 0285d5a6e08624c92cdddf4eceed759c55f2839c139d922c09c8d82302114b0dcfba8416dae5ea5cb965ca8bdcc971c323087397ddae26b309217c9c1fb11820
7
- data.tar.gz: de888a90628e3987dc5f4b0416d7eef83e8afc6f1972135c6545ecd8839f4cacb570ef4f1d73900cadc3cd808a10329c6b43c0adb60bd805278942d858b0e3a6
6
+ metadata.gz: 433ab1c14d97195cb9222247266e5feb06e17e26d36bef61ebb313057cade5a8994600bf2730799e0f25e08c3fc026c345c8223a441a4b6383655919b2a25bb0
7
+ data.tar.gz: 2142d2b4256bc7abe4a2a3d3f408b5163ad783b61aafdf851485050c82b6ddb7f4bb38a8f438153fb283f5979ed709750abdf8d11776321fdd2289c1644027f7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Change Log
2
2
 
3
+ ## [v0.1.1](https://github.com/payrollhero/webhook_system/tree/v0.1.1) (2016-02-05)
4
+ [Full Changelog](https://github.com/payrollhero/webhook_system/compare/v0.1.0...v0.1.1)
5
+
6
+ - Fixing hash based attribute definition and a few fixes [\#3](https://github.com/payrollhero/webhook_system/pull/3) ([piotrb](https://github.com/piotrb))
7
+
3
8
  ## [v0.1.0](https://github.com/payrollhero/webhook_system/tree/v0.1.0) (2016-02-05)
4
9
  [Full Changelog](https://github.com/payrollhero/webhook_system/compare/v0.0.1...v0.1.0)
5
10
 
data/README.md CHANGED
@@ -28,20 +28,119 @@ tables first:
28
28
 
29
29
  ```ruby
30
30
  create_table :webhook_subscriptions do |t|
31
- t.string :url
32
- t.boolean :active
31
+ t.string :url, null: false
32
+ t.boolean :active, null: false
33
33
  t.text :secret
34
34
 
35
35
  t.index :active
36
36
  end
37
37
 
38
38
  create_table :webhook_subscription_topics do |t|
39
- t.string :name
40
- t.belongs_to :subscription
39
+ t.string :name, null: false
40
+ t.belongs_to :subscription, null: false
41
41
 
42
42
  t.index :subscription_id
43
43
  t.index :name
44
44
  end
45
+
46
+ create_table :webhook_event_logs do |t|
47
+ t.belongs_to :subscription, null: false
48
+
49
+ t.string :event_name, null: false
50
+ t.string :event_id, null: false
51
+ t.integer :status, null: false
52
+
53
+ t.text :request, limit: 64_000, null: false
54
+ t.text :response, limit: 64_000, null: false
55
+
56
+ t.datetime :created_at, null: false
57
+
58
+ t.index :created_at
59
+ t.index :event_name
60
+ t.index :status
61
+ t.index :subscription_id
62
+ t.index :event_id
63
+ end
64
+ ```
65
+
66
+ ### Migrating from version 0.x
67
+
68
+ First migrate the null constraints in ...
69
+
70
+ ```ruby
71
+ def up
72
+ change_column :webhook_subscriptions, :url, null: false
73
+ change_column :webhook_subscriptions, :active, null: false
74
+ change_column :webhook_subscription_topics, :name, null: false
75
+ change_column :webhook_subscription_topics, :subscription_id, null: false
76
+ end
77
+
78
+ def down
79
+ change_column :webhook_subscription_topics, :subscription_id, null: true
80
+ change_column :webhook_subscription_topics, :name, null: true
81
+ change_column :webhook_subscriptions, :active, null: true
82
+ change_column :webhook_subscriptions, :url, null: true
83
+ end
84
+ ```
85
+
86
+ Then add the new table ...
87
+
88
+ ```ruby
89
+ create_table :webhook_event_logs do |t|
90
+ t.belongs_to :subscription, null: false
91
+
92
+ t.string :event_name, null: false
93
+ t.string :event_id, null: false
94
+ t.integer :status, null: false
95
+
96
+ t.text :request, limit: 64_000, null: false
97
+ t.text :response, limit: 64_000, null: false
98
+
99
+ t.datetime :created_at, null: false
100
+
101
+ t.index :created_at
102
+ t.index :event_name
103
+ t.index :status
104
+ t.index :subscription_id
105
+ t.index :event_id
106
+ end
107
+ ```
108
+
109
+ ## Configuring the ActiveJob Job
110
+
111
+ There is a couple of things you might need to configure in your handling of these jobs
112
+
113
+ ### Queue Name
114
+
115
+ You might need to reopen the class and define the queue:
116
+
117
+ eg:
118
+ ```ruby
119
+ class WebhookSystem::Job
120
+ queue_as :some_queue_name
121
+ end
122
+ ```
123
+
124
+ ### Error Handling
125
+
126
+ By default the job will fail itself when it gets a non 200 response. You can handle these errors by rescuing the
127
+ Request failed exception. eg:
128
+
129
+ ```ruby
130
+ class WebhookSystem::Job
131
+ rescue_from(WebhookSystem::Job::RequestFailed) do |exception|
132
+ case exception.code
133
+ when 200..299
134
+ # this is ok, ignore it
135
+ when 300..399
136
+ # this is kinda ok, but let's log it ..
137
+ Rails.logger.info "weird response"
138
+ else
139
+ # otherwise re-raise it so the job fails
140
+ raise exception
141
+ end
142
+ end
143
+ end
45
144
  ```
46
145
 
47
146
  ## Building Events
@@ -13,6 +13,7 @@ module WebhookSystem
13
13
  autoload :Job
14
14
  autoload :Encoder
15
15
  autoload :BaseEvent
16
+ autoload :EventLog
16
17
 
17
18
  # Error raised when there is an issue with decoding the payload
18
19
  class DecodingError < RuntimeError
@@ -4,6 +4,13 @@ module WebhookSystem
4
4
  class BaseEvent
5
5
  include PhModel
6
6
 
7
+ def initialize(*args, &block)
8
+ super(*args, &block)
9
+ @event_id = SecureRandom.uuid.freeze
10
+ end
11
+
12
+ attr_reader :event_id
13
+
7
14
  def event_name
8
15
  mesg = "class #{self.class.name} must implement abstract method `#{self.class.name}#event_name()'."
9
16
  raise RuntimeError.new(mesg).tap { |err| err.backtrace = caller }
@@ -15,7 +22,10 @@ module WebhookSystem
15
22
  end
16
23
 
17
24
  def as_json
18
- result = { 'event' => event_name }
25
+ result = {
26
+ 'event_name' => event_name,
27
+ 'event_id' => event_id,
28
+ }
19
29
  each_attribute do |attribute_name, attribute_method|
20
30
  validate_attribute_name attribute_name
21
31
  result[attribute_name.to_s] = public_send(attribute_method).as_json
@@ -24,7 +34,7 @@ module WebhookSystem
24
34
  end
25
35
 
26
36
  def self.key_is_reserved?(key)
27
- key.to_s.in? %w(event)
37
+ key.to_s.in? %w(event event_id)
28
38
  end
29
39
 
30
40
  private
@@ -49,7 +49,7 @@ module WebhookSystem
49
49
  # :nodoc:
50
50
  module Payload
51
51
  def self.encode(raw_encrypted_data, iv)
52
- JSON.dump(
52
+ JSON.pretty_generate(
53
53
  'format' => 'base64+aes256',
54
54
  'payload' => Base64.encode64(raw_encrypted_data),
55
55
  'iv' => Base64.encode64(iv)
@@ -0,0 +1,39 @@
1
+ module WebhookSystem
2
+
3
+ # This is the model holding on to all webhook responses
4
+ class EventLog < ActiveRecord::Base
5
+ self.table_name = 'webhook_event_logs'
6
+
7
+ belongs_to :subscription, class_name: 'WebhookSystem::Subscription'
8
+
9
+ validates :event_id, presence: true
10
+ validates :subscription_id, presence: true
11
+ validates :event_name, presence: true
12
+ validates :status, presence: true
13
+
14
+ serialize :request, JSON
15
+ serialize :response, JSON
16
+
17
+ def self.construct(subscription, event, request, response)
18
+ request_info = {
19
+ 'event' => event,
20
+ 'headers' => request.headers.to_hash,
21
+ 'body' => request.body,
22
+ 'url' => request.path,
23
+ }
24
+ response_info = {
25
+ 'headers' => response.headers.to_hash,
26
+ 'body' => response.body,
27
+ }
28
+
29
+ attributes = {
30
+ event_name: event['event_name'],
31
+ event_id: event['event_id'],
32
+ status: response.status,
33
+ request: request_info,
34
+ response: response_info,
35
+ }
36
+ subscription.event_logs.build(attributes)
37
+ end
38
+ end
39
+ end
@@ -3,20 +3,48 @@ module WebhookSystem
3
3
  # This is the ActiveJob in charge of actually sending each event
4
4
  class Job < ActiveJob::Base
5
5
 
6
+ # Exception class around non 200 responses
7
+ class RequestFailed < RuntimeError
8
+ def initialize(message, code)
9
+ super(message)
10
+ @code = code
11
+ end
12
+ end
13
+
6
14
  def perform(subscription, event)
7
- payload = Encoder.encode(subscription.secret, event)
8
- self.class.post(subscription.url, payload)
15
+ self.class.post(subscription, event)
16
+ end
17
+
18
+ def self.post(subscription, event)
19
+ client = build_client
20
+ request = build_request(client, subscription, event)
21
+ response = client.builder.build_response(client, request)
22
+ log_response(subscription, event, request, response)
23
+ ensure_success(response)
9
24
  end
10
25
 
11
- def self.post(endpoint, payload)
12
- client_for(endpoint).post do |req|
26
+ def self.ensure_success(response)
27
+ status = response.status
28
+ unless status == 200
29
+ raise RequestFailed.new("request failed with code: #{status}", status)
30
+ end
31
+ end
32
+
33
+ def self.build_request(client, subscription, event)
34
+ payload = Encoder.encode(subscription.secret, event)
35
+ client.build_request(:post) do |req|
36
+ req.url subscription.url
13
37
  req.headers['Content-Type'] = 'application/json; base64+aes256'
14
38
  req.body = payload.to_s
15
39
  end
16
40
  end
17
41
 
18
- def self.client_for(endpoint)
19
- Faraday.new(url: endpoint) do |faraday|
42
+ def self.log_response(subscription, event, request, response)
43
+ EventLog.construct(subscription, event, request, response).save!
44
+ end
45
+
46
+ def self.build_client
47
+ Faraday.new do |faraday|
20
48
  faraday.response :logger if ENV['WEBHOOK_DEBUG']
21
49
  faraday.adapter Faraday.default_adapter
22
50
  end
@@ -8,6 +8,8 @@ module WebhookSystem
8
8
  validates :secret, presence: true
9
9
 
10
10
  has_many :topics, class_name: 'WebhookSystem::SubscriptionTopic', dependent: :destroy
11
+ has_many :event_logs, class_name: 'WebhookSystem::EventLog', dependent: :destroy
12
+
11
13
  accepts_nested_attributes_for :topics, allow_destroy: true
12
14
 
13
15
  scope :active, -> { where(active: true) }
@@ -1,4 +1,3 @@
1
- # frozen_string_literal: true
2
1
  module WebhookSystem
3
- VERSION = '0.1.1'.freeze
2
+ VERSION = '1.0.0'.freeze
4
3
  end
@@ -36,6 +36,6 @@ Gem::Specification.new do |gem|
36
36
  gem.add_development_dependency 'webmock'
37
37
 
38
38
  # static analysis gems
39
- gem.add_development_dependency 'rubocop', '~> 0.36.0'
39
+ gem.add_development_dependency 'rubocop', '~> 0.37.1'
40
40
  gem.add_development_dependency 'reek', '~> 3.7'
41
41
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webhook_system
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Banasik
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-02-05 00:00:00.000000000 Z
11
+ date: 2016-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -226,14 +226,14 @@ dependencies:
226
226
  requirements:
227
227
  - - "~>"
228
228
  - !ruby/object:Gem::Version
229
- version: 0.36.0
229
+ version: 0.37.1
230
230
  type: :development
231
231
  prerelease: false
232
232
  version_requirements: !ruby/object:Gem::Requirement
233
233
  requirements:
234
234
  - - "~>"
235
235
  - !ruby/object:Gem::Version
236
- version: 0.36.0
236
+ version: 0.37.1
237
237
  - !ruby/object:Gem::Dependency
238
238
  name: reek
239
239
  requirement: !ruby/object:Gem::Requirement
@@ -271,6 +271,7 @@ files:
271
271
  - lib/webhook_system/base_event.rb
272
272
  - lib/webhook_system/dispatcher.rb
273
273
  - lib/webhook_system/encoder.rb
274
+ - lib/webhook_system/event_log.rb
274
275
  - lib/webhook_system/job.rb
275
276
  - lib/webhook_system/subscription.rb
276
277
  - lib/webhook_system/subscription_topic.rb