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.
- checksums.yaml +7 -0
- data/.env.sample +1 -0
- data/.gitignore +63 -0
- data/.rspec +4 -0
- data/.rubocop.yml +86 -0
- data/.todo +48 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +5 -0
- data/DEV_README.md +199 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +483 -0
- data/Rakefile +23 -0
- data/active_webhook.gemspec +75 -0
- data/config/environment.rb +33 -0
- data/lib/active_webhook.rb +125 -0
- data/lib/active_webhook/adapter.rb +117 -0
- data/lib/active_webhook/callbacks.rb +53 -0
- data/lib/active_webhook/configuration.rb +105 -0
- data/lib/active_webhook/delivery/base_adapter.rb +96 -0
- data/lib/active_webhook/delivery/configuration.rb +16 -0
- data/lib/active_webhook/delivery/faraday_adapter.rb +19 -0
- data/lib/active_webhook/delivery/net_http_adapter.rb +28 -0
- data/lib/active_webhook/error_log.rb +7 -0
- data/lib/active_webhook/formatting/base_adapter.rb +109 -0
- data/lib/active_webhook/formatting/configuration.rb +18 -0
- data/lib/active_webhook/formatting/json_adapter.rb +19 -0
- data/lib/active_webhook/formatting/url_encoded_adapter.rb +28 -0
- data/lib/active_webhook/hook.rb +9 -0
- data/lib/active_webhook/logger.rb +21 -0
- data/lib/active_webhook/models/configuration.rb +18 -0
- data/lib/active_webhook/models/error_log_additions.rb +15 -0
- data/lib/active_webhook/models/subscription_additions.rb +72 -0
- data/lib/active_webhook/models/topic_additions.rb +70 -0
- data/lib/active_webhook/queueing/active_job_adapter.rb +43 -0
- data/lib/active_webhook/queueing/base_adapter.rb +67 -0
- data/lib/active_webhook/queueing/configuration.rb +15 -0
- data/lib/active_webhook/queueing/delayed_job_adapter.rb +28 -0
- data/lib/active_webhook/queueing/sidekiq_adapter.rb +43 -0
- data/lib/active_webhook/queueing/syncronous_adapter.rb +14 -0
- data/lib/active_webhook/subscription.rb +7 -0
- data/lib/active_webhook/topic.rb +7 -0
- data/lib/active_webhook/verification/base_adapter.rb +31 -0
- data/lib/active_webhook/verification/configuration.rb +13 -0
- data/lib/active_webhook/verification/hmac_sha256_adapter.rb +20 -0
- data/lib/active_webhook/verification/unsigned_adapter.rb +11 -0
- data/lib/active_webhook/version.rb +5 -0
- data/lib/generators/install_generator.rb +20 -0
- data/lib/generators/migrations_generator.rb +24 -0
- data/lib/generators/templates/20210618023338_create_active_webhook_tables.rb +31 -0
- data/lib/generators/templates/active_webhook_config.rb +87 -0
- metadata +447 -0
    
        data/Rakefile
    ADDED
    
    | @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            #!/usr/bin/env rake
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            def initialize_rake_environment
         | 
| 4 | 
            +
              return unless require_relative "config/environment"
         | 
| 5 | 
            +
            rescue LoadError
         | 
| 6 | 
            +
              puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
         | 
| 7 | 
            +
            end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Bundler::GemHelper.install_tasks
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
         | 
| 12 | 
            +
            load 'rails/tasks/engine.rake'
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            task default: :spec
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            require "rspec/core/rake_task"
         | 
| 17 | 
            +
            RSpec::Core::RakeTask.new(:spec)
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            require "rubocop/rake_task"
         | 
| 20 | 
            +
            RuboCop::RakeTask.new do |task|
         | 
| 21 | 
            +
              task.requires << "rubocop-performance"
         | 
| 22 | 
            +
              task.requires << "rubocop-rspec"
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,75 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            lib = File.expand_path("lib", __dir__)
         | 
| 4 | 
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         | 
| 5 | 
            +
            require "active_webhook/version"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Gem::Specification.new do |spec|
         | 
| 8 | 
            +
              spec.name          = "active_webhook"
         | 
| 9 | 
            +
              spec.version       = ActiveWebhook::VERSION
         | 
| 10 | 
            +
              spec.authors       = ["Jay Crouch"]
         | 
| 11 | 
            +
              spec.email         = ["i.jaycrouch@gmail.com"]
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              spec.summary       = "Simple, efficient, and extensible webhooks for Ruby."
         | 
| 14 | 
            +
              spec.description   = "Simple, efficient, and extensible webhooks for Ruby, including: Rate Limits, Cryptographic " \
         | 
| 15 | 
            +
                                   "Signatures, Asynchronous Delivery, Buffered Delivery, Versioning."
         | 
| 16 | 
            +
              spec.homepage      = "https://github.com/amazing-jay/active_webhook"
         | 
| 17 | 
            +
              spec.license       = "MIT"
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
         | 
| 20 | 
            +
              # to allow pushing to a single host or delete this section to allow pushing to any host.
         | 
| 21 | 
            +
              if spec.respond_to?(:metadata)
         | 
| 22 | 
            +
                # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                spec.metadata["homepage_uri"] = spec.homepage
         | 
| 25 | 
            +
                spec.metadata["source_code_uri"] = "https://github.com/amazing-jay/active_webhook"
         | 
| 26 | 
            +
                spec.metadata["changelog_uri"] = "https://github.com/amazing-jay/active_webhook/master/tree/CHANGELOG.md."
         | 
| 27 | 
            +
              else
         | 
| 28 | 
            +
                raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              # Specify which files should be added to the gem when it is released.
         | 
| 32 | 
            +
              # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
         | 
| 33 | 
            +
              spec.files = Dir.chdir(File.expand_path(__dir__)) do
         | 
| 34 | 
            +
                `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|bin)/}) }
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              spec.bindir                      = "exe"
         | 
| 38 | 
            +
              spec.executables                 = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
         | 
| 39 | 
            +
              spec.require_paths               = ["lib"]
         | 
| 40 | 
            +
              spec.required_ruby_version       = ">= 2.5"
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              spec.add_dependency "activerecord", ">= 5.0.0"
         | 
| 43 | 
            +
              spec.add_dependency "memoist"
         | 
| 44 | 
            +
             | 
| 45 | 
            +
              spec.add_development_dependency "awesome_print", "~> 1.9.2"
         | 
| 46 | 
            +
              spec.add_development_dependency "bundler", "~> 1.17"
         | 
| 47 | 
            +
              spec.add_development_dependency "database_cleaner", "~> 2.0.1"
         | 
| 48 | 
            +
              spec.add_development_dependency "delayed_job_active_record", "~> 4.1.6"
         | 
| 49 | 
            +
              spec.add_development_dependency "dotenv", "~> 2.5"
         | 
| 50 | 
            +
              spec.add_development_dependency "factory_bot", "~> 6.2.0"
         | 
| 51 | 
            +
              spec.add_development_dependency "faker", "~> 2.18"
         | 
| 52 | 
            +
              spec.add_development_dependency "faraday", "~> 1.4.2"
         | 
| 53 | 
            +
              spec.add_development_dependency "listen", "~> 3.5.1"
         | 
| 54 | 
            +
              spec.add_development_dependency "pry-byebug", "~> 3.9"
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              # must come before those below
         | 
| 57 | 
            +
              if ENV['TEST_RAILS_VERSION'].nil?
         | 
| 58 | 
            +
                spec.add_development_dependency 'rails', '~> 6.1.3.2'
         | 
| 59 | 
            +
              else
         | 
| 60 | 
            +
                spec.add_development_dependency 'rails', ENV['TEST_RAILS_VERSION'].to_s
         | 
| 61 | 
            +
              end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              spec.add_development_dependency "rake", "~> 10.0"
         | 
| 64 | 
            +
              spec.add_development_dependency "rspec", "~> 3.0"
         | 
| 65 | 
            +
              spec.add_development_dependency "rspec-rails", "~> 5.0.1"
         | 
| 66 | 
            +
              spec.add_development_dependency "rubocop", "~> 0.60"
         | 
| 67 | 
            +
              spec.add_development_dependency "rubocop-performance", "~> 1.5"
         | 
| 68 | 
            +
              spec.add_development_dependency "rubocop-rspec", "~> 1.37"
         | 
| 69 | 
            +
              spec.add_development_dependency "codecov", "~> 0.5.2"
         | 
| 70 | 
            +
              spec.add_development_dependency "simplecov", "~> 0.16"
         | 
| 71 | 
            +
              spec.add_development_dependency "sqlite3", "~> 1.4.2"
         | 
| 72 | 
            +
              spec.add_development_dependency "sidekiq", "~> 6.2.1"
         | 
| 73 | 
            +
              spec.add_development_dependency "rspec-sidekiq", "~> 3.1.0"
         | 
| 74 | 
            +
              spec.add_development_dependency "webmock", "~> 3.13"
         | 
| 75 | 
            +
            end
         | 
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "dotenv/load"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ENV["RACK_ENV"] ||= ENV["RAILS_ENV"] ||= "development"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require "bundler/setup"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            if RUBY_ENGINE == "jruby"
         | 
| 10 | 
            +
              # Workaround for issue in I18n/JRuby combo.
         | 
| 11 | 
            +
              # See https://github.com/jruby/jruby/issues/6547 and
         | 
| 12 | 
            +
              # https://github.com/ruby-i18n/i18n/issues/555
         | 
| 13 | 
            +
              require "i18n/backend"
         | 
| 14 | 
            +
              require "i18n/backend/simple"
         | 
| 15 | 
            +
            end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            environments = [ENV["RAILS_ENV"].to_sym]
         | 
| 18 | 
            +
            environments << :development if environments.last == :test
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            Bundler.require(*environments)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            SimpleCov.start if ENV['RAILS_ENV'].to_s == "test"
         | 
| 23 | 
            +
            SimpleCov.formatter = SimpleCov::Formatter::Codecov if ENV["CI"] == "true"
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            require "active_webhook"
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            db_config = File.read([__dir__.delete_suffix('/config'), "spec/dummy/config/database.yml"].join("/"))
         | 
| 28 | 
            +
            db_config = ERB.new(db_config).result
         | 
| 29 | 
            +
            db_config = YAML.safe_load(db_config, [], [], true)
         | 
| 30 | 
            +
            DB_CONFIG = db_config[ENV["RAILS_ENV"]]
         | 
| 31 | 
            +
            ActiveRecord::Base.establish_connection(DB_CONFIG)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            FactoryBot.find_definitions if %w(test development).include? ENV["RAILS_ENV"]
         | 
| @@ -0,0 +1,125 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "active_record"
         | 
| 4 | 
            +
            require "memoist"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require "active_webhook/adapter"
         | 
| 7 | 
            +
            require "active_webhook/delivery/base_adapter"
         | 
| 8 | 
            +
            require "active_webhook/formatting/base_adapter"
         | 
| 9 | 
            +
            require "active_webhook/queueing/base_adapter"
         | 
| 10 | 
            +
            require "active_webhook/verification/base_adapter"
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            require "active_webhook/models/error_log_additions"
         | 
| 13 | 
            +
            require "active_webhook/models/subscription_additions"
         | 
| 14 | 
            +
            require "active_webhook/models/topic_additions"
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            require "active_webhook/callbacks"
         | 
| 17 | 
            +
            require "active_webhook/hook"
         | 
| 18 | 
            +
            require "active_webhook/error_log"
         | 
| 19 | 
            +
            require "active_webhook/logger"
         | 
| 20 | 
            +
            require "active_webhook/subscription"
         | 
| 21 | 
            +
            require "active_webhook/topic"
         | 
| 22 | 
            +
            require "active_webhook/version"
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            module ActiveWebhook
         | 
| 25 | 
            +
              class InvalidAdapterError < StandardError; end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              IDENTIFIER = "Active Webhook v#{VERSION}"
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              # IDENTIFIER must be defined first
         | 
| 30 | 
            +
              require "active_webhook/configuration"
         | 
| 31 | 
            +
             | 
| 32 | 
            +
              class << self
         | 
| 33 | 
            +
                attr_writer :enabled
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def configuration
         | 
| 36 | 
            +
                  @configuration ||= Configuration.new
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def configure
         | 
| 40 | 
            +
                  yield(configuration)
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  configuration.after_configure
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def logger
         | 
| 46 | 
            +
                  defined?(Rails) ? Rails.logger : (@logger ||= Logger.new($stdout))
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def subscription_model
         | 
| 50 | 
            +
                  configuration.models.subscription
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def topic_model
         | 
| 54 | 
            +
                  configuration.models.topic
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def origin
         | 
| 58 | 
            +
                  return @@origin if defined? @@origin
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  @@origin = (Rails.application.config.action_mailer.default_url_options[:host] if defined?(Rails))
         | 
| 61 | 
            +
                rescue StandardError
         | 
| 62 | 
            +
                  @@origin = ""
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                # TODO: change the next 4 methods to use memoized thread safe class var rather than configuration.enabled
         | 
| 66 | 
            +
                def enabled?
         | 
| 67 | 
            +
                  configuration.enabled
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def disabled?
         | 
| 71 | 
            +
                  !enabled?
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def enable
         | 
| 75 | 
            +
                  state = enabled?
         | 
| 76 | 
            +
                  configuration.enabled = true
         | 
| 77 | 
            +
                  value = yield
         | 
| 78 | 
            +
                ensure
         | 
| 79 | 
            +
                  configuration.enabled = state
         | 
| 80 | 
            +
                  value
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def disable
         | 
| 84 | 
            +
                  state = enabled?
         | 
| 85 | 
            +
                  configuration.enabled = false
         | 
| 86 | 
            +
                  value = yield
         | 
| 87 | 
            +
                ensure
         | 
| 88 | 
            +
                  configuration.enabled = state
         | 
| 89 | 
            +
                  value
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                Configuration::ADAPTERS.each do |type|
         | 
| 93 | 
            +
                  define_method "#{type}_adapter" do
         | 
| 94 | 
            +
                    fetch_adapter type, configuration.send(type).send("adapter")
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                def trigger(key:, version: nil, **context)
         | 
| 99 | 
            +
                  queueing_adapter.call(key: key, version: version, **context) if enabled?
         | 
| 100 | 
            +
                  true
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                protected
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def fetch_adapter(type, adapter)
         | 
| 106 | 
            +
                  if adapter.is_a?(Symbol) || adapter.is_a?(String)
         | 
| 107 | 
            +
                    adapter = begin
         | 
| 108 | 
            +
                      @adapters ||= {}
         | 
| 109 | 
            +
                      @adapters[type] ||= {}
         | 
| 110 | 
            +
                      @adapters[type][adapter.to_sym] = begin
         | 
| 111 | 
            +
                        path = "active_webhook/#{type}/#{adapter}_adapter"
         | 
| 112 | 
            +
                        require path
         | 
| 113 | 
            +
                        const_name = path.camelize
         | 
| 114 | 
            +
                        ["http", "sha", "hmac", "json", "url"].each { |acronym| const_name.gsub!(acronym.camelize, acronym.upcase) }
         | 
| 115 | 
            +
                        const_name.constantize
         | 
| 116 | 
            +
                      end
         | 
| 117 | 
            +
                    end
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  raise InvalidAdapterError unless adapter.respond_to?(:call)
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                  adapter
         | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
              end
         | 
| 125 | 
            +
            end
         | 
| @@ -0,0 +1,117 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module ActiveWebhook
         | 
| 4 | 
            +
              class Adapter
         | 
| 5 | 
            +
                extend Memoist
         | 
| 6 | 
            +
                # TODO: memoize everything in all adapters
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                class << self
         | 
| 9 | 
            +
                  def attributes
         | 
| 10 | 
            +
                    @attributes ||= if self == ActiveWebhook::Adapter
         | 
| 11 | 
            +
                                      []
         | 
| 12 | 
            +
                                    else
         | 
| 13 | 
            +
                                      ancestors.each_with_object([]) do |ancestor, attrs|
         | 
| 14 | 
            +
                                        break attrs += ancestor.attributes if ancestor != self && ancestor.respond_to?(:attributes)
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                                        attrs
         | 
| 17 | 
            +
                                      end
         | 
| 18 | 
            +
                                    end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    # # byebug
         | 
| 21 | 
            +
                    # # puts ['start', self, @attributes].join(', ')
         | 
| 22 | 
            +
                    # @attributes ||= ancestors.each_with_object([]) do |ancestor, attrs|
         | 
| 23 | 
            +
                    #   # break attrs if ancestor == ActiveWebhook::Adapter
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    #   if ancestor != self
         | 
| 26 | 
            +
                    #     if ancestor.respond_to?(:attributes)
         | 
| 27 | 
            +
                    #       break attrs += ancestor.attributes
         | 
| 28 | 
            +
                    #     end
         | 
| 29 | 
            +
                    #   end
         | 
| 30 | 
            +
                    #   attrs
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    #   # puts ['searching', ancestor, attrs].join(', ')
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    #   # byebug
         | 
| 35 | 
            +
                    #   # break attrs if ancestor == ActiveWebhook::Adapter
         | 
| 36 | 
            +
                    #   # break attrs if ancestor != self && ancestor.respond_to?(:attributes))
         | 
| 37 | 
            +
                    #   # byebug
         | 
| 38 | 
            +
                    #   # attrs
         | 
| 39 | 
            +
                    # end
         | 
| 40 | 
            +
                    # puts ['finished', self, @attributes].join(' ,')
         | 
| 41 | 
            +
                    # # byebug
         | 
| 42 | 
            +
                    # # @attributes ||= []
         | 
| 43 | 
            +
                    # @attributes
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def attribute(*attrs)
         | 
| 47 | 
            +
                    # Module.new.tap do |m| # Using anonymous modules so that super can be used to extend accessor methods
         | 
| 48 | 
            +
                    # include m
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    attrs = attrs.map(&:to_sym)
         | 
| 51 | 
            +
                    (attrs - attributes).each do |attr_name|
         | 
| 52 | 
            +
                      attributes << attr_name.to_sym
         | 
| 53 | 
            +
                      # m.attr_accessor attr_name
         | 
| 54 | 
            +
                      attr_accessor attr_name
         | 
| 55 | 
            +
                      # end
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  # def inherited(subclass)
         | 
| 60 | 
            +
                  #   byebug
         | 
| 61 | 
            +
                  #   # super
         | 
| 62 | 
            +
                  #   subclass.instance_variable_set(:@attributes, ancestors.each_with_object([]) do |ancestor, attrs|
         | 
| 63 | 
            +
                  #                                                  unless ancestor == self || !ancestor.respond_to?(:attributes)
         | 
| 64 | 
            +
                  #                                                    break attrs += ancestor.attributes
         | 
| 65 | 
            +
                  #                                                  end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  #                                                  attrs
         | 
| 68 | 
            +
                  #                                                end)
         | 
| 69 | 
            +
                  #   # byebug
         | 
| 70 | 
            +
                  #   # x = 1
         | 
| 71 | 
            +
                  # end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  def call(*args, **kwargs, &block)
         | 
| 74 | 
            +
                    new(*args, **kwargs).call(&block)
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def component_name
         | 
| 78 | 
            +
                    raise NotImplementedError, ".component_name must be implemented."
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def configuration
         | 
| 82 | 
            +
                    ActiveWebhook.configuration
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  def component_configuration
         | 
| 86 | 
            +
                    configuration.send(component_name)
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                attribute :context
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def initialize(**kwargs)
         | 
| 93 | 
            +
                  self.class.attributes.each do |attr_name|
         | 
| 94 | 
            +
                    send("#{attr_name}=", kwargs[attr_name]) unless attr_name == :context
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
                  self.context = kwargs #.symbolize_keys! #with_indifferent_access
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                def call
         | 
| 100 | 
            +
                  raise NotImplementedError, "#call must be implemented."
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def attributes
         | 
| 104 | 
            +
                  self.class.attributes.each_with_object({}) do |attr_name, h|
         | 
| 105 | 
            +
                    h[attr_name] = send(attr_name)
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def configuration
         | 
| 110 | 
            +
                  self.class.configuration
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                def component_configuration
         | 
| 114 | 
            +
                  self.class.component_configuration
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
              end
         | 
| 117 | 
            +
            end
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module ActiveWebhook
         | 
| 4 | 
            +
              module Callbacks
         | 
| 5 | 
            +
                class InvalidCallbackError < StandardError; end
         | 
| 6 | 
            +
                extend ActiveSupport::Concern
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                SUPPORTED_CALLBACKS = %i(created updated deleted).freeze
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                class_methods do
         | 
| 11 | 
            +
                  def trigger_webhooks(version: nil, only: nil, except: [], **_options)
         | 
| 12 | 
            +
                    callbacks = if only.nil?
         | 
| 13 | 
            +
                      SUPPORTED_CALLBACKS
         | 
| 14 | 
            +
                    else
         | 
| 15 | 
            +
                      Array.wrap(only).map(&:to_sym)
         | 
| 16 | 
            +
                    end - Array.wrap(except).map(&:to_sym)
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    callbacks.each do |callback|
         | 
| 19 | 
            +
                      unless SUPPORTED_CALLBACKS.include? callback
         | 
| 20 | 
            +
                        raise InvalidCallbackError, "Invalid callback: #{callback}. Must be one of #{SUPPORTED_CALLBACKS}."
         | 
| 21 | 
            +
                      end
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    after_commit :trigger_created_webhook, on: :create if callbacks.include?(:created)
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    after_commit :trigger_updated_webhook, on: :update if callbacks.include?(:updated)
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    after_commit :trigger_deleted_webhook, on: :destroy if callbacks.include?(:deleted)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def trigger_created_webhook
         | 
| 33 | 
            +
                  trigger_webhook(:created)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def trigger_updated_webhook
         | 
| 37 | 
            +
                  trigger_webhook(:updated) unless previous_changes.empty?
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def trigger_deleted_webhook
         | 
| 41 | 
            +
                  trigger_webhook(:deleted)
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def trigger_webhook(key, version: nil, type: 'resource', **context)
         | 
| 45 | 
            +
                  key = [self.class.name.underscore, key].join("/") unless key.is_a?(String)
         | 
| 46 | 
            +
                  context[:resource_id] ||= id
         | 
| 47 | 
            +
                  context[:resource_type] ||= self.class.name
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  ActiveWebhook.trigger(key: key, version: version, type: type, **context)
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
              ActiveRecord::Base.include ActiveWebhook::Callbacks
         | 
| 53 | 
            +
            end
         | 
| @@ -0,0 +1,105 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module ActiveWebhook
         | 
| 4 | 
            +
              class Configuration
         | 
| 5 | 
            +
                class InvalidOptionError < StandardError
         | 
| 6 | 
            +
                  def initialize(option, value, values)
         | 
| 7 | 
            +
                    @option = option
         | 
| 8 | 
            +
                    @values = values
         | 
| 9 | 
            +
                    msg = "Invalid option for #{option}: #{value}. Must be one of #{values}."
         | 
| 10 | 
            +
                    super(msg)
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                module Base
         | 
| 15 | 
            +
                  extend ActiveSupport::Concern
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def initialize
         | 
| 18 | 
            +
                    (self.class.instance_variable_get(:@components) || []).each do |component_name|
         | 
| 19 | 
            +
                      component = "#{self.class.name.deconstantize}::#{component_name.to_s.camelize}::Configuration"
         | 
| 20 | 
            +
                      component = component.constantize.new
         | 
| 21 | 
            +
                      instance_variable_set "@#{component_name}", component
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    (self.class.instance_variable_get(:@options) || []).each do |option, option_definition|
         | 
| 25 | 
            +
                      send "#{option}=", option_definition[:default]
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  class_methods do
         | 
| 30 | 
            +
                    protected
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    # TODO: consider changing so that all options accept a proc
         | 
| 33 | 
            +
                    #  q: does the proc run every time the option is asked? seems inefficent
         | 
| 34 | 
            +
                    def define_option(option, values: [], default: nil, allow_nil: false, allow_proc: false, prefixes: nil)
         | 
| 35 | 
            +
                      attr_reader option
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      default = values&.first if default.nil? && !allow_nil
         | 
| 38 | 
            +
                      prefixes ||= name.deconstantize.underscore.delete_prefix("active_webhook").split("/")
         | 
| 39 | 
            +
                      prefixes.shift if prefixes.first.blank?
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                      @options ||= {}
         | 
| 42 | 
            +
                      @options[option] = {
         | 
| 43 | 
            +
                        values: values,
         | 
| 44 | 
            +
                        default: default,
         | 
| 45 | 
            +
                        allow_proc: allow_proc,
         | 
| 46 | 
            +
                        prefixes: prefixes
         | 
| 47 | 
            +
                      }
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                      const_set option.to_s.pluralize.upcase, values
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                      define_method "#{option}=" do |value|
         | 
| 52 | 
            +
                        unless (allow_proc && value.respond_to?(:call)) ||
         | 
| 53 | 
            +
                               (allow_nil && value.nil?) ||
         | 
| 54 | 
            +
                               values.empty? ||
         | 
| 55 | 
            +
                               values.include?(value)
         | 
| 56 | 
            +
                          raise Configuration::InvalidOptionError.new (prefixes + [option]).compact.join("."), value,
         | 
| 57 | 
            +
                                                                      values
         | 
| 58 | 
            +
                        end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                        instance_variable_set "@#{option}", value
         | 
| 61 | 
            +
                      end
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    def define_component(component_name)
         | 
| 65 | 
            +
                      @components ||= []
         | 
| 66 | 
            +
                      @components << component_name
         | 
| 67 | 
            +
                      require "#{name.deconstantize.to_s.underscore}/#{component_name}/configuration"
         | 
| 68 | 
            +
                      attr_reader component_name
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                include Base
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                define_component :models
         | 
| 76 | 
            +
                ADAPTERS = %i[delivery formatting queueing verification].freeze
         | 
| 77 | 
            +
                ADAPTERS.each { |component| define_component component }
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                define_option :origin
         | 
| 80 | 
            +
                define_option :enabled, values: [true, false]
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def origin=(value)
         | 
| 83 | 
            +
                  if (@origin = value).nil?
         | 
| 84 | 
            +
                    ActiveWebhook.remove_class_variable(:@@origin) if ActiveWebhook.class_variable_defined?(:@@origin)
         | 
| 85 | 
            +
                  else
         | 
| 86 | 
            +
                    ActiveWebhook.class_variable_set(:@@origin, value)
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                def after_configure
         | 
| 91 | 
            +
                  # reset logger,
         | 
| 92 | 
            +
                  # ActiveWebhook.class_variable_set(:@logger, nil)
         | 
| 93 | 
            +
                  # cause all adapter files specified to be loaded
         | 
| 94 | 
            +
                  ADAPTERS.each { |type| ActiveWebhook.send "#{type}_adapter" }
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  # (re)set relationships for all models
         | 
| 97 | 
            +
                  models.error_log.belongs_to :subscription, class_name: models.subscription.name, foreign_key: :subscription_id
         | 
| 98 | 
            +
                  models.topic.has_many :subscriptions, class_name: models.subscription.name, foreign_key: :topic_id
         | 
| 99 | 
            +
                  models.subscription.belongs_to :topic, class_name: models.topic.name, foreign_key: :topic_id
         | 
| 100 | 
            +
                  models.subscription.has_many :error_logs, class_name: models.error_log.name, foreign_key: :subscription_id
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  self
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
              end
         | 
| 105 | 
            +
            end
         |