active_webhook 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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.env.sample +1 -0
  3. data/.gitignore +63 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +86 -0
  6. data/.todo +48 -0
  7. data/.travis.yml +14 -0
  8. data/CHANGELOG.md +5 -0
  9. data/DEV_README.md +199 -0
  10. data/Gemfile +8 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +483 -0
  13. data/Rakefile +23 -0
  14. data/active_webhook.gemspec +75 -0
  15. data/config/environment.rb +33 -0
  16. data/lib/active_webhook.rb +125 -0
  17. data/lib/active_webhook/adapter.rb +117 -0
  18. data/lib/active_webhook/callbacks.rb +53 -0
  19. data/lib/active_webhook/configuration.rb +105 -0
  20. data/lib/active_webhook/delivery/base_adapter.rb +96 -0
  21. data/lib/active_webhook/delivery/configuration.rb +16 -0
  22. data/lib/active_webhook/delivery/faraday_adapter.rb +19 -0
  23. data/lib/active_webhook/delivery/net_http_adapter.rb +28 -0
  24. data/lib/active_webhook/error_log.rb +7 -0
  25. data/lib/active_webhook/formatting/base_adapter.rb +109 -0
  26. data/lib/active_webhook/formatting/configuration.rb +18 -0
  27. data/lib/active_webhook/formatting/json_adapter.rb +19 -0
  28. data/lib/active_webhook/formatting/url_encoded_adapter.rb +28 -0
  29. data/lib/active_webhook/hook.rb +9 -0
  30. data/lib/active_webhook/logger.rb +21 -0
  31. data/lib/active_webhook/models/configuration.rb +18 -0
  32. data/lib/active_webhook/models/error_log_additions.rb +15 -0
  33. data/lib/active_webhook/models/subscription_additions.rb +72 -0
  34. data/lib/active_webhook/models/topic_additions.rb +70 -0
  35. data/lib/active_webhook/queueing/active_job_adapter.rb +43 -0
  36. data/lib/active_webhook/queueing/base_adapter.rb +67 -0
  37. data/lib/active_webhook/queueing/configuration.rb +15 -0
  38. data/lib/active_webhook/queueing/delayed_job_adapter.rb +28 -0
  39. data/lib/active_webhook/queueing/sidekiq_adapter.rb +43 -0
  40. data/lib/active_webhook/queueing/syncronous_adapter.rb +14 -0
  41. data/lib/active_webhook/subscription.rb +7 -0
  42. data/lib/active_webhook/topic.rb +7 -0
  43. data/lib/active_webhook/verification/base_adapter.rb +31 -0
  44. data/lib/active_webhook/verification/configuration.rb +13 -0
  45. data/lib/active_webhook/verification/hmac_sha256_adapter.rb +20 -0
  46. data/lib/active_webhook/verification/unsigned_adapter.rb +11 -0
  47. data/lib/active_webhook/version.rb +5 -0
  48. data/lib/generators/install_generator.rb +20 -0
  49. data/lib/generators/migrations_generator.rb +24 -0
  50. data/lib/generators/templates/20210618023338_create_active_webhook_tables.rb +31 -0
  51. data/lib/generators/templates/active_webhook_config.rb +87 -0
  52. metadata +447 -0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Jay Crouch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,483 @@
1
+ # Active Webhook
2
+
3
+ [![Build Status](https://travis-ci.com/amazing-jay/active_webhook.svg?branch=master)](https://travis-ci.com/amazing-jay/active_webhook)
4
+ [![Test Coverage](https://codecov.io/gh/amazing-jay/active_webhook/graph/badge.svg)](https://codecov.io/gh/amazing-jay/active_webhook)
5
+
6
+ Simple, efficient, extensible webhooks for Ruby.
7
+
8
+ Features include:
9
+
10
+ - Rate Limits
11
+ - Cryptographic Signatures
12
+ - Asynchronous Delivery
13
+ - Buffered Delivery
14
+ - Versioning
15
+
16
+ ## What does an Active Webhook look like?
17
+
18
+ By default, ActiveWebhook delivers HTTP POST requests with the following:
19
+
20
+ #### HEADERS
21
+
22
+ ```json
23
+ {
24
+ "Content-Type": "application/json",
25
+ "User-Agent": "Active Webhook v0.1.0",
26
+ "Origin": "http://my-custom-domain.com",
27
+ "X-Hmac-SHA256": "iDCMPCGuPaq3F9hhEYdcBmIBU6aVOEZakS8GmJbLzoU=",
28
+ "X-Time": "2021-06-29 06:20:26 UTC",
29
+ "X-Topic": "abcdef",
30
+ "X-Topic-Version": "3.73",
31
+ "X-Webhook-Type": "event",
32
+ "X-Webhook-Id": "6f35615cb30a6c51a29bedeb"
33
+ },
34
+ }
35
+ ```
36
+
37
+ #### BODY
38
+ ```json
39
+ {
40
+ "data": {}
41
+ }
42
+ ```
43
+
44
+ _See the [Configuration](https://github.com/amazing-jay/active_webhook#configuration) and [Customization](https://github.com/amazing-jay/active_webhook#customization) sections to learn more._
45
+
46
+ ## Requirements
47
+
48
+ Active Webhook supports (_but does not require_) Rails 5+ and various queuing
49
+ and delivery technologies (e.g. Sidekiq, Delayed Job, Active Job, Net HTTP, Faraday, etc.).
50
+
51
+ ## Download and Installation
52
+
53
+ Add this line to your application's Gemfile:
54
+
55
+ ```ruby
56
+ gem 'active_webhook'
57
+ ```
58
+
59
+ And then execute:
60
+
61
+ $ bundle
62
+
63
+ Or install it yourself as:
64
+
65
+ $ gem install active_webhook
66
+
67
+ Source code can be downloaded on GitHub
68
+ [github.com/amazing-jay/active_webhook/tree/master](https://github.com/amazing-jay/active_webhook/tree/master)
69
+
70
+ ## Setup
71
+
72
+ ### Generate required files
73
+
74
+ $ rails g active_webhook:install
75
+ $ rails db:migrate
76
+
77
+ ### Define topics that you want to make available for your application
78
+
79
+ #### Via Migration _(recommended)_
80
+
81
+ $ rails g migration create_active_webhook_topics
82
+
83
+ Then edit the migration file:
84
+
85
+ ```ruby
86
+ # in db/migrate/20210618023338_create_active_webhook_topics.rb
87
+
88
+ # This is just an example, you can define any topic keys that you want to define
89
+ class CreateActiveWebhookTopics < ActiveRecord::Migration[4.2]
90
+ def change
91
+ # If you do omit a value for`version` when creating a Topic, ActiveWebhook will autoincrement one for you.
92
+ ActiveWebhook::Topic.create(key: "user/created")
93
+ end
94
+ end
95
+ ```
96
+
97
+ And migrate:
98
+
99
+ $ rails db:migrate
100
+ $ rails db:test:prepare
101
+
102
+ #### Or Console
103
+
104
+ $ rails c
105
+ > ActiveWebhook::Topic.create(key: 'user/created': version: '1.1')
106
+
107
+
108
+ ## Usage
109
+
110
+ ### Triggering webhooks
111
+
112
+ To trigger the delivery of a topic, simply execute `ActiveWebhook.trigger(key: key)`, where `key` is a required string that identifies a Topic for delivery (must
113
+ match the `key` of at least one previously defined Topic).
114
+
115
+ ### Options
116
+
117
+ The `trigger` method accepts any number of optional kwarg arguments, some of which have special meaning:
118
+
119
+ - `version` is a string that scopes delivery of Topics by version (if omitted, all topics with matching key will be triggered during the queuing phase).
120
+ - `format_first` is a boolean that overrides the default configuration value during the queueing phase.
121
+ - `data` is a hash that will become the payload body during the build phase
122
+ - `type` is a string that will become the value of the 'X-Webhook-Type' header during the build phase.
123
+ - `max_errors_per_hour` is a integer that overrides the default configuration value during the delivery phase.
124
+
125
+ All other keyword arguments supplied will be passed forward to each adapter for later use by any customizations that you implement.
126
+
127
+ Examples::
128
+
129
+ ```ruby
130
+ ActiveWebhook.trigger(key: 'user/created')
131
+ ActiveWebhook.trigger(key: 'user/deleted', version: '1.1')
132
+ ActiveWebhook.trigger(key: 'user/deleted', data: { id: 1 }, my_option: 'random_value')
133
+ ```
134
+
135
+ ### ActiveRecord Callbacks
136
+
137
+ The following convenience methods are available when working with ActiveRecord objects:
138
+
139
+ ```ruby
140
+ # app/models/application_record.rb
141
+
142
+ # note: trigger_webhooks is defined in ActiveWebhook::Callbacks, which is mixed into ActiveRecord::Base
143
+ class ApplicationRecord < ActiveRecord::Base
144
+ # enable after_commit callbacks for created and deleted topic
145
+ trigger_webhooks except: :updated
146
+
147
+ # conditionally trigger the updated topic
148
+ after_commit on: :updated do
149
+ trigger_webhook :updated if state_changed?
150
+ end
151
+ end
152
+ ```
153
+
154
+ ```ruby
155
+ # app/models/invoice.rb
156
+
157
+ class Invoice < ApplicationRecord
158
+ # override the default behavior to trigger an additional topic
159
+ def trigger_updated_webhook
160
+ trigger_webhook(:sent) if previous_changes.key?("sent_at")
161
+
162
+ super
163
+ end
164
+ ```
165
+
166
+ #### Special payload for webhooks triggered by ActiveRecord callbacks
167
+
168
+ When using ActiveRecord callbacks, the default payload will be set to `resource.as_json`, and the default type option will be set to "resource".
169
+
170
+ By way of example:
171
+
172
+ ```ruby
173
+ # app/models/application_record.rb
174
+
175
+ class User < ApplicationRecord
176
+ def send_reminder
177
+ # this:
178
+ trigger_webhook(:reminded)
179
+ # is more-or-less equivalent to an optomized version of this:
180
+ # ActiveWebhook.trigger(key: 'user/reminded', data: self.as_json, type: "resource")
181
+ end
182
+ ```
183
+
184
+ #### Defining topics for ActiveRecord callbacks
185
+
186
+ Don't forget to create topics for each of the models that you want to enable ActiveRecord callbacks for.
187
+
188
+ ```ruby
189
+ # in db/migrate/20210618023338_create_active_webhook_topics.rb
190
+
191
+ # This is just an example, you can define any topics that you want to define
192
+ class CreateActiveWebhookTopics < ActiveRecord::Migration[4.2]
193
+ def change
194
+ # define default callback topics for all models + a special topic
195
+ ActiveRecord::Base.connection.tables.each do |table|
196
+ %W(created updated deleted special).each do |event|
197
+ ActiveWebhook::Topic.create(key: "#{table.singularize}/#{event}", version: "1.0")
198
+ end
199
+ end
200
+
201
+ # define a second version of the user/created topic so we can conditionally deliver a different payload to subscribers
202
+ ActiveWebhook::Topic.create(key: "user/created", version: "1.1")
203
+
204
+ # define a custom topic
205
+ ActiveWebhook::Topic.create(key: "invoice/sent")
206
+ end
207
+ end
208
+ ```
209
+
210
+ ## Subscribing to topics
211
+
212
+ When a Topic is triggered, Active Webhook will attempt delivery for each
213
+ Subscription registered with the specified topic & version.
214
+
215
+ You can create multiple subscriptions for the same topic.
216
+
217
+ To register a Subscription, simply execute
218
+ `ActiveWebhook::Subscription.create(callback_url: url, topic: topic)`, where:
219
+
220
+ - `callback_url` is a required string that must be a valid URL
221
+ - `topic` is a previously defined Topic
222
+
223
+ e.g.
224
+
225
+ ```ruby
226
+ ActiveWebhook::Subscription.create(
227
+ callback_url: 'http://myappdomain.com/webhooks',
228
+ topic: ActiveWebhook::Topic.find_by_key('user/created')
229
+ )
230
+ # or
231
+ ActiveWebhook::Subscription.create(
232
+ callback_url: 'http://myappdomain.com/webhooks',
233
+ topic: ActiveWebhook::Topic.where(key: 'user/deleted', version: '1.1').first
234
+ )
235
+ ```
236
+
237
+ _NOTE: See the [Self Subscription](https://github.com/amazing-jay/active_webhook#example-2--enable-users-to-self-subscribe-for-the-topics-that-they-care-about) section to learn how to setup self-registration for your users._
238
+
239
+ ## Configuration
240
+
241
+ _NOTE: See `config/active_webhook.rb` for details about all available configuration options._
242
+
243
+ ### Adapters
244
+
245
+ Active Webhook ships with queuing and delivery adapters for:
246
+
247
+ - Sidekiq
248
+ - BackgroundJob
249
+ - ActiveJob
250
+ - Net::HTTP
251
+ - Faraday
252
+
253
+ To activate any adapter, simply uncomment the relevant declaration in the generated
254
+ Active Webhook configuration file, and then install relevant dependencies (if any).
255
+
256
+ For example, to activate the [sidekiq](https://github.com/mperham/sidekiq)
257
+ queuing adapter:
258
+
259
+ ```ruby
260
+ # in config/active_webhook.rb
261
+
262
+ require "active_webhook"
263
+
264
+ ActiveWebhook.configure do |config|
265
+ config.adapters.queueing = :sidekiq
266
+ end
267
+ ```
268
+
269
+ ```ruby
270
+ # in Gemfile
271
+
272
+ gem "sidekiq"
273
+ ```
274
+
275
+ _NOTE: Active Webhook does not register official dependencies for any of the
276
+ gems required by the various adapters so as to not bloat your application with
277
+ unused/incompatible gems. This means that you will have to manually install and
278
+ configure all gems required by the adapters that you use (via command line or
279
+ Bundler)._
280
+
281
+ ## Customization
282
+
283
+ This section illustrates the extensibility of Active Webhook.
284
+
285
+ The following examples will help you:
286
+
287
+ - Scope Subscription delivery by tenant (aka `Company`)
288
+ - Enable users to self-subscribe for the topics that they care about
289
+ - Conditionally customize the payload structure
290
+
291
+ ### Example #1 :: Scope Subscription delivery by tenant (aka Company)
292
+
293
+ $ rails g migration add_company_to_active_webhook_subscriptions
294
+
295
+ ```ruby
296
+ # db/migrate/20210618023339_add_company_to_active_webhook_subscriptions.rb
297
+
298
+ class AddCompanyToActiveWebhookSubscriptions < ActiveRecord::Migration[5.2]
299
+ def change
300
+ add_reference :active_webhook_subscriptions, :company
301
+ end
302
+ end
303
+ ```
304
+
305
+ ```ruby
306
+ # app/models/application_record.rb
307
+
308
+ # note: trigger_webhooks is defined in ActiveWebhook::Callbacks, which is mixed into ActiveRecord::Base
309
+ class ApplicationRecord < ActiveRecord::Base
310
+ def trigger_webhook(key, version: nil, **context)
311
+ context[:company_id] ||= company_id if respond_to?(:company_id)
312
+ context[:company_id] ||= company&.id if respond_to?(:company)
313
+ super
314
+ end
315
+ end
316
+ ```
317
+
318
+ ```ruby
319
+ # app/lib/webhook/queueing_adapter.rb
320
+
321
+ require "active_webhook/queueing/sidekiq_adapter"
322
+
323
+ module Webhook
324
+ class QueueingAdapter < ActiveWebhook::Queueing::SidekiqAdapter
325
+ # qualify subscriptions by tenant
326
+ def subscriptions_scope
327
+ scope = super
328
+ scope = scope.where(company_id: company_id) if company_id.present?
329
+ scope
330
+ end
331
+
332
+ def company_id
333
+ context[:company_id]
334
+ end
335
+ end
336
+ end
337
+ ```
338
+
339
+ ```ruby
340
+ # app/models/active_webhook/subscription.rb
341
+
342
+ # reopen class and add relation
343
+ class ActiveWebhook::Subscription
344
+ belongs_to :company
345
+ end
346
+ ```
347
+
348
+
349
+ ### Example #2 :: Enable users to self-subscribe for the topics that they care about
350
+
351
+ $ rails g migration add_user_to_active_webhook_subscriptions
352
+
353
+ ```ruby
354
+ # db/migrate/20210618023339_add_user_to_active_webhook_subscriptions.rb
355
+
356
+ class AddUserToActiveWebhookSubscriptions < ActiveRecord::Migration[4.2]
357
+ def change
358
+ add_reference :active_webhook_subscriptions, :user
359
+ end
360
+ end
361
+ ```
362
+
363
+ ```ruby
364
+ # app/models/company.rb
365
+
366
+ # note: trigger_webhooks is defined in ActiveWebhook::Callbacks, which is mixed into ActiveRecord::Base
367
+ class User < ApplicationRecord
368
+ has_many :webhook_subscriptions
369
+ end
370
+ ```
371
+
372
+ ```ruby
373
+ # app/models/webhook_subscription.rb
374
+
375
+ class WebhookSubscription < ApplicationRecord
376
+ include ActiveWebhook::Models::SubscriptionAdditions
377
+ belongs_to :user
378
+ end
379
+ ```
380
+
381
+ ```ruby
382
+ # in config/active_webhook.rb
383
+
384
+ require "active_webhook"
385
+
386
+ ActiveWebhook.configure do |config|
387
+ # use our custom Subscription class rather than the default
388
+ config.models.subscription = WebhookSubscription
389
+ end
390
+ ```
391
+
392
+ ```ruby
393
+ # in app/controllers/webhook_subscriptions
394
+ module Webhooks
395
+ class SubscriptionsController < ApplicationController
396
+ def create
397
+ @user = User.find params.permit(:user_id)
398
+ @user.webhook_subscriptions.build_webhook_subscription params.require(:webhook_subscription).permit(
399
+ :callback_url,
400
+ :topic_id
401
+ )
402
+ @user.save!
403
+ end
404
+ end
405
+ end
406
+ ```
407
+
408
+ ### Example #3 :: Conditionally customize the payload
409
+
410
+ ```ruby
411
+ # in config/active_webhook.rb
412
+ ActiveWebhook.configure do |c|
413
+ c.adapters.formatting = MySpecialFormatter
414
+ end
415
+ ```
416
+
417
+ ```ruby
418
+ # in lib/webhooks/my_special_formatter.rb
419
+ # note: This is just an example. Custom formatters do not need to inherit from ActiveWebhook::Formatting::SimpleFormatting.
420
+ class Webhooks::MySpecialFormatter < ActiveWebhook::Formatting::JsonAdapter
421
+ def self.call(subscription, **context)
422
+ payload = super
423
+ payload["type"] = "special" if subscription.topic_key.ends_with? "/special"
424
+ payload.delete("something other key")
425
+ payload
426
+ end
427
+ end
428
+ ```
429
+
430
+ ...or, alternatively
431
+
432
+ ```ruby
433
+ # in config/active_webhook.rb
434
+ ActiveWebhook.configure do |c|
435
+ c.adapters.formatting = Class.new(ActiveWebhook::Formatting::JsonAdapter) do
436
+ def self.call(subscription, **context)
437
+ payload = super
438
+ payload["type"] = "special" if subscription.topic_key.ends_with? "/special"
439
+ payload.delete("something other key")
440
+ payload
441
+ end
442
+ end
443
+ end
444
+ ```
445
+
446
+ For more information, see the files located at:
447
+
448
+ - [lib/active_webhook/queueing](https://github.com/amazing-jay/active_webhook/tree/master/lib/active_webhook/queueing)
449
+ - [lib/active_webhook/delivery](https://github.com/amazing-jay/active_webhook/tree/master/lib/active_webhook/delivery)
450
+ - [lib/active_webhook/verification](https://github.com/amazing-jay/active_webhook/tree/master/lib/active_webhook/verification)
451
+ - [lib/active_webhook/formatting](https://github.com/amazing-jay/active_webhook/tree/master/lib/active_webhook/formatting)
452
+
453
+ ## Development
454
+
455
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
456
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
457
+ prompt that will allow you to experiment.
458
+
459
+ To install this gem onto your local machine, run `bundle exec rake install`. To
460
+ release a new version, update the version number in `version.rb`, and then run
461
+ `bundle exec rake release`, which will create a git tag for the version, push
462
+ git commits and tags, and push the `.gem` file to
463
+ [rubygems.org](https://rubygems.org).
464
+
465
+ ## Contributing
466
+
467
+ Bug reports and pull requests are welcome on GitHub at
468
+ https://github.com/amazing-jay/active_webhook.
469
+
470
+ ## License
471
+
472
+ The gem is available as open source under the terms of the
473
+ [MIT License](https://opensource.org/licenses/MIT).
474
+
475
+ ## ROADMAP
476
+
477
+ * Figure out flakey. specs
478
+ * Add XML format
479
+ * Dummy app; create a local subscription class
480
+ * Upgrade callbacks_spec to use a real model and table defined in a migration
481
+ * Upgrade logger spec to expect stubbed logger to receive and call original (and drop the rest)
482
+ * Disable subscriptions when jobs stop
483
+ * Consolidate formatting adapters and configuration options into a single builder