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
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Models
5
+ module TopicAdditions
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ self.table_name = "active_webhook_topics"
10
+
11
+ scope :enabled, -> { where(disabled_at: nil) }
12
+ scope :with_key, lambda { |key:, version: nil|
13
+ scope = where(key: key)
14
+ scope = scope.where(version: version) if version.present?
15
+ scope
16
+ }
17
+
18
+ def self.last_with_key(key)
19
+ where(key: key).order(id: :desc).first
20
+ end
21
+
22
+ before_validation :set_valid_version
23
+ validates :key, presence: true
24
+ validates :version, presence: true, uniqueness: { scope: :key }
25
+ end
26
+
27
+ def disable(reason = nil)
28
+ self.disabled_at = Time.current
29
+ self.disabled_reason = reason
30
+ end
31
+
32
+ def disable!(reason = nil)
33
+ disable reason
34
+ save!
35
+ end
36
+
37
+ def enable
38
+ self.disabled_at = nil
39
+ self.disabled_reason = nil
40
+ end
41
+
42
+ def enable!
43
+ enable
44
+ save!
45
+ end
46
+
47
+ def disabled?
48
+ !enabled?
49
+ end
50
+
51
+ def enabled?
52
+ disabled_at.nil?
53
+ end
54
+
55
+ protected
56
+
57
+ def set_valid_version
58
+ return if version.present?
59
+
60
+ last_with_key = self.class.last_with_key key
61
+ versions = last_with_key&.version.to_s.split(".")
62
+ versions = [0] if versions.empty?
63
+ version = versions.pop
64
+ versions << version.to_i + 1
65
+
66
+ self.version = versions.join(".")
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Queueing
5
+ class ActiveJobAdapter < BaseAdapter
6
+ class SubscriptionJob < ApplicationJob
7
+ queue_as :low_priority
8
+
9
+ def perform(subscription, hook, context)
10
+ context.symbolize_keys!
11
+ hook = Hook.from_h(hook.symbolize_keys) unless hook.nil?
12
+
13
+ ActiveWebhook.queueing_adapter.fulfill_subscription(
14
+ subscription: subscription,
15
+ hook: hook,
16
+ job_id: job_id,
17
+ **context
18
+ )
19
+ end
20
+ end
21
+
22
+ class TopicJob < ApplicationJob
23
+ queue_as :low_priority
24
+
25
+ def perform(key, version, context)
26
+ context.symbolize_keys!
27
+
28
+ ActiveWebhook.queueing_adapter.new(key: key, version: version, **context).fulfill_topic
29
+ end
30
+ end
31
+
32
+ protected
33
+
34
+ def promise_subscription(subscription:, hook:)
35
+ SubscriptionJob.perform_later subscription, hook&.to_h, context
36
+ end
37
+
38
+ def promise_topic
39
+ TopicJob.perform_later key, version, context
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Queueing
5
+ class BaseAdapter < Adapter
6
+ attribute :key, :version, :format_first
7
+
8
+ def format_first
9
+ @format_first.nil? ? component_configuration.format_first : @format_first
10
+ end
11
+
12
+ # returns count of jobs enqueued
13
+ def call
14
+ return fulfill_topic if format_first
15
+
16
+ promise_topic
17
+ 1
18
+ end
19
+
20
+ def self.build_hook(subscription, **context)
21
+ ActiveWebhook.formatting_adapter.call(subscription: subscription, **context)
22
+ end
23
+
24
+ def self.fulfill_subscription(subscription:, hook: nil, **context)
25
+ ActiveWebhook.delivery_adapter.call(
26
+ subscription: subscription,
27
+ hook: hook || build_hook(subscription, **context),
28
+ **context
29
+ ) if ActiveWebhook.enabled?
30
+ true
31
+ end
32
+
33
+ def fulfill_topic
34
+ subscriptions.each do |subscription|
35
+ hook = format_first ? self.class.build_hook(subscription, **context) : nil
36
+ byebug if context.key?("key")
37
+ promise_subscription subscription: subscription, hook: hook
38
+ end
39
+ subscriptions.count
40
+ end
41
+
42
+ protected
43
+
44
+ def self.component_name
45
+ "queueing"
46
+ end
47
+
48
+ def promise_subscription(_subscription:, _hook:)
49
+ raise NotImplementedError, "#promise_subscription must be implemented."
50
+ end
51
+
52
+ def promise_topic
53
+ raise NotImplementedError, "#promise_topic must be implemented."
54
+ end
55
+
56
+ def subscriptions
57
+ subscriptions_scope.all
58
+ end
59
+
60
+ def subscriptions_scope
61
+ ActiveWebhook.subscription_model.enabled.joins(:topic).includes(:topic).merge(
62
+ ActiveWebhook.topic_model.enabled.with_key(key: key, version: version)
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Queueing
5
+ class Configuration
6
+ include ActiveWebhook::Configuration::Base
7
+
8
+ define_option :adapter,
9
+ values: %i[syncronous sidekiq delayed_job active_job],
10
+ allow_proc: true
11
+
12
+ define_option :format_first, values: [true, false], default: false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delayed_job"
4
+
5
+ module ActiveWebhook
6
+ module Queueing
7
+ class DelayedJobAdapter < BaseAdapter
8
+ protected
9
+
10
+ def promise_subscription(subscription:, hook:)
11
+ ActiveWebhook.queueing_adapter.fulfill_subscription(
12
+ subscription: subscription,
13
+ hook: hook,
14
+ # NOTE: not implemented yet;
15
+ # SEE: https://stackoverflow.com/questions/21590798/referencing-delayed-job-job-id-from-within-the-job-task
16
+ # job_id: ???
17
+ **context
18
+ )
19
+ end
20
+ handle_asynchronously :promise_subscription
21
+
22
+ def promise_topic
23
+ fulfill_topic
24
+ end
25
+ handle_asynchronously :promise_topic
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ module ActiveWebhook
6
+ module Queueing
7
+ class SidekiqAdapter < BaseAdapter
8
+ class SubscriptionWorker
9
+ include Sidekiq::Worker
10
+
11
+ def perform(subscription, hook, context)
12
+ subscription = ActiveWebhook.subscription_model.find_by(id: subscription)
13
+ hook = Hook.from_h(hook.symbolize_keys) unless hook.nil?
14
+
15
+ ActiveWebhook.queueing_adapter.fulfill_subscription(
16
+ subscription: subscription,
17
+ hook: hook,
18
+ job_id: jid,
19
+ **context.symbolize_keys
20
+ )
21
+ end
22
+ end
23
+
24
+ class TopicWorker
25
+ include Sidekiq::Worker
26
+
27
+ def perform(key, version, context)
28
+ ActiveWebhook.queueing_adapter.new(key: key, version: version, **context.symbolize_keys).fulfill_topic
29
+ end
30
+ end
31
+
32
+ protected
33
+
34
+ def promise_subscription(subscription:, hook:)
35
+ SubscriptionWorker.perform_async subscription.id, hook&.to_h, context
36
+ end
37
+
38
+ def promise_topic
39
+ TopicWorker.perform_async key, version, context
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Queueing
5
+ class SyncronousAdapter < BaseAdapter
6
+ def call
7
+ subscriptions.each do |subscription|
8
+ self.class.fulfill_subscription subscription: subscription, **context
9
+ end
10
+ subscriptions.count
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ class Subscription < ActiveRecord::Base
5
+ include ActiveWebhook::Models::SubscriptionAdditions
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ class Topic < ActiveRecord::Base
5
+ include ActiveWebhook::Models::TopicAdditions
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Verification
5
+ class BaseAdapter < Adapter
6
+ attribute :secret, :data
7
+
8
+ def call
9
+ return {} unless secret.present?
10
+
11
+ {
12
+ strategy => signature
13
+ }
14
+ end
15
+
16
+ protected
17
+
18
+ def self.component_name
19
+ "verification"
20
+ end
21
+
22
+ def signature
23
+ raise NotImplementedError, "#signature must be implemented."
24
+ end
25
+
26
+ def strategy
27
+ self.class.name.delete_suffix("Adapter")
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Verification
5
+ class Configuration
6
+ include ActiveWebhook::Configuration::Base
7
+
8
+ define_option :adapter,
9
+ values: %i[unsigned hmac_sha256],
10
+ allow_proc: true
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "base64"
5
+ require "openssl"
6
+ require "active_support/security_utils"
7
+
8
+ module ActiveWebhook
9
+ module Verification
10
+ class HMACSHA256Adapter < BaseAdapter
11
+ def signature
12
+ Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", secret, data))
13
+ end
14
+
15
+ def strategy
16
+ "Hmac-SHA256"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Verification
5
+ class UnsignedAdapter < BaseAdapter
6
+ def call
7
+ {}
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module ActiveWebhook
6
+ module Generators
7
+ class InstallGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+ desc "Creates all the files needed to use Active Webhook"
10
+
11
+ def copy_config
12
+ template "active_webhook_config.rb", Rails.root.join('config','active_webhook.rb')
13
+ end
14
+
15
+ def run_other_generators
16
+ invoke "active_webhook:migrations"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module ActiveWebhook
6
+ module Generators
7
+ class MigrationsGenerator < ::Rails::Generators::Base
8
+ include ::Rails::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+ desc "Creates the migrations needed to use Active Webhook"
12
+
13
+ def self.next_migration_number(path)
14
+ next_migration_number = current_migration_number(path) + 1
15
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
16
+ end
17
+
18
+ def copy_migrations
19
+ migration_template "20210618023338_create_active_webhook_tables.rb",
20
+ Rails.root.join('db','migrate','create_active_webhook_tables.rb')
21
+ end
22
+ end
23
+ end
24
+ end