active_webhook 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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