diff --git a/lib/hooks.rb b/lib/hooks.rb index c5a479d..32b62c3 100644 --- a/lib/hooks.rb +++ b/lib/hooks.rb @@ -7,6 +7,7 @@ require_relative "hooks/core/logger_factory" require_relative "hooks/core/plugin_loader" require_relative "hooks/core/global_components" +require_relative "hooks/core/component_access" require_relative "hooks/core/log" require_relative "hooks/core/failbot" require_relative "hooks/core/stats" diff --git a/lib/hooks/core/component_access.rb b/lib/hooks/core/component_access.rb new file mode 100644 index 0000000..3bee516 --- /dev/null +++ b/lib/hooks/core/component_access.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Hooks + module Core + # Shared module providing access to global components (logger, stats, failbot) + # + # This module provides a consistent interface for accessing global components + # across all plugin types, eliminating code duplication and ensuring consistent + # behavior throughout the application. + # + # @example Usage in a class that needs instance methods + # class MyHandler + # include Hooks::Core::ComponentAccess + # + # def process + # log.info("Processing request") + # stats.increment("requests.processed") + # failbot.report("Error occurred") if error? + # end + # end + # + # @example Usage in a class that needs class methods + # class MyValidator + # extend Hooks::Core::ComponentAccess + # + # def self.validate + # log.info("Validating request") + # stats.increment("requests.validated") + # end + # end + module ComponentAccess + # Short logger accessor + # @return [Hooks::Log] Logger instance for logging messages + # + # Provides a convenient way to log messages without needing + # to reference the full Hooks::Log namespace. + # + # @example Logging an error + # log.error("Something went wrong") + def log + Hooks::Log.instance + end + + # Global stats component accessor + # @return [Hooks::Plugins::Instruments::Stats] Stats instance for metrics reporting + # + # Provides access to the global stats component for reporting metrics + # to services like DataDog, New Relic, etc. + # + # @example Recording a metric + # stats.increment("webhook.processed", { handler: "MyHandler" }) + def stats + Hooks::Core::GlobalComponents.stats + end + + # Global failbot component accessor + # @return [Hooks::Plugins::Instruments::Failbot] Failbot instance for error reporting + # + # Provides access to the global failbot component for reporting errors + # to services like Sentry, Rollbar, etc. + # + # @example Reporting an error + # failbot.report("Something went wrong", { context: "additional info" }) + def failbot + Hooks::Core::GlobalComponents.failbot + end + end + end +end diff --git a/lib/hooks/core/log.rb b/lib/hooks/core/log.rb index 97b30a5..40bcd3d 100644 --- a/lib/hooks/core/log.rb +++ b/lib/hooks/core/log.rb @@ -1,8 +1,23 @@ # frozen_string_literal: true module Hooks + # Global logger accessor module + # + # Provides a singleton-like access pattern for the application logger. + # The logger instance is set during application initialization and can + # be accessed throughout the application lifecycle. + # + # @example Setting the logger instance + # Hooks::Log.instance = Logger.new(STDOUT) + # + # @example Accessing the logger + # Hooks::Log.instance.info("Application started") module Log class << self + # Get or set the global logger instance + # @return [Logger] The global logger instance + # @attr_reader instance [Logger] Current logger instance + # @attr_writer instance [Logger] Set the logger instance attr_accessor :instance end end diff --git a/lib/hooks/plugins/auth/base.rb b/lib/hooks/plugins/auth/base.rb index a97d38d..fc41e35 100644 --- a/lib/hooks/plugins/auth/base.rb +++ b/lib/hooks/plugins/auth/base.rb @@ -3,6 +3,7 @@ require "rack/utils" require_relative "../../core/log" require_relative "../../core/global_components" +require_relative "../../core/component_access" module Hooks module Plugins @@ -11,6 +12,8 @@ module Auth # # All custom Auth plugins must inherit from this class class Base + extend Hooks::Core::ComponentAccess + # Validate request # # @param payload [String] Raw request body @@ -22,42 +25,6 @@ def self.valid?(payload:, headers:, config:) raise NotImplementedError, "Validator must implement .valid? class method" end - # Short logger accessor for all subclasses - # @return [Hooks::Log] Logger instance for request validation - # - # Provides a convenient way for validators to log messages without needing - # to reference the full Hooks::Log namespace. - # - # @example Logging an error in an inherited class - # log.error("oh no an error occured") - def self.log - Hooks::Log.instance - end - - # Global stats component accessor - # @return [Hooks::Core::Stats] Stats instance for metrics reporting - # - # Provides access to the global stats component for reporting metrics - # to services like DataDog, New Relic, etc. - # - # @example Recording a metric in an inherited class - # stats.increment("auth.validation", { plugin: "hmac" }) - def self.stats - Hooks::Core::GlobalComponents.stats - end - - # Global failbot component accessor - # @return [Hooks::Core::Failbot] Failbot instance for error reporting - # - # Provides access to the global failbot component for reporting errors - # to services like Sentry, Rollbar, etc. - # - # @example Reporting an error in an inherited class - # failbot.report("Auth validation failed", { plugin: "hmac" }) - def self.failbot - Hooks::Core::GlobalComponents.failbot - end - # Retrieve the secret from the environment variable based on the key set in the configuration # # Note: This method is intended to be used by subclasses diff --git a/lib/hooks/plugins/auth/hmac.rb b/lib/hooks/plugins/auth/hmac.rb index c420bb1..cae23b9 100644 --- a/lib/hooks/plugins/auth/hmac.rb +++ b/lib/hooks/plugins/auth/hmac.rb @@ -40,6 +40,7 @@ class HMAC < Base DEFAULT_CONFIG = { algorithm: "sha256", format: "algorithm=signature", # Format: algorithm=hash + header: "X-Signature", # Default header containing the signature timestamp_tolerance: 300, # 5 minutes tolerance for timestamp validation version_prefix: "v0" # Default version prefix for versioned signatures }.freeze @@ -157,7 +158,7 @@ def self.build_config(config) tolerance = validator_config[:timestamp_tolerance] || DEFAULT_CONFIG[:timestamp_tolerance] DEFAULT_CONFIG.merge({ - header: validator_config[:header] || "X-Signature", + header: validator_config[:header] || DEFAULT_CONFIG[:header], timestamp_header: validator_config[:timestamp_header], timestamp_tolerance: tolerance, algorithm: algorithm, diff --git a/lib/hooks/plugins/handlers/base.rb b/lib/hooks/plugins/handlers/base.rb index 6dac147..69f2d6b 100644 --- a/lib/hooks/plugins/handlers/base.rb +++ b/lib/hooks/plugins/handlers/base.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "../../core/global_components" +require_relative "../../core/component_access" module Hooks module Plugins @@ -9,6 +10,8 @@ module Handlers # # All custom handlers must inherit from this class and implement the #call method class Base + include Hooks::Core::ComponentAccess + # Process a webhook request # # @param payload [Hash, String] Parsed request body (JSON Hash) or raw string @@ -19,42 +22,6 @@ class Base def call(payload:, headers:, config:) raise NotImplementedError, "Handler must implement #call method" end - - # Short logger accessor for all subclasses - # @return [Hooks::Log] Logger instance - # - # Provides a convenient way for handlers to log messages without needing - # to reference the full Hooks::Log namespace. - # - # @example Logging an error in an inherited class - # log.error("oh no an error occured") - def log - Hooks::Log.instance - end - - # Global stats component accessor - # @return [Hooks::Core::Stats] Stats instance for metrics reporting - # - # Provides access to the global stats component for reporting metrics - # to services like DataDog, New Relic, etc. - # - # @example Recording a metric in an inherited class - # stats.increment("webhook.processed", { handler: "MyHandler" }) - def stats - Hooks::Core::GlobalComponents.stats - end - - # Global failbot component accessor - # @return [Hooks::Core::Failbot] Failbot instance for error reporting - # - # Provides access to the global failbot component for reporting errors - # to services like Sentry, Rollbar, etc. - # - # @example Reporting an error in an inherited class - # failbot.report("Something went wrong", { handler: "MyHandler" }) - def failbot - Hooks::Core::GlobalComponents.failbot - end end end end diff --git a/lib/hooks/plugins/handlers/default.rb b/lib/hooks/plugins/handlers/default.rb index 614c1d6..6161aaa 100644 --- a/lib/hooks/plugins/handlers/default.rb +++ b/lib/hooks/plugins/handlers/default.rb @@ -1,8 +1,37 @@ # frozen_string_literal: true -# Default handler when no custom handler is found -# This handler simply acknowledges receipt of the webhook and shows a few of the built-in features +# Default webhook handler implementation +# +# This handler provides a basic webhook processing implementation that can be used +# as a fallback when no custom handler is configured for an endpoint. It demonstrates +# the standard handler interface and provides basic logging functionality. +# +# @example Usage in endpoint configuration +# handler: +# type: DefaultHandler +# +# @see Hooks::Plugins::Handlers::Base class DefaultHandler < Hooks::Plugins::Handlers::Base + # Process a webhook request with basic acknowledgment + # + # Provides a simple webhook processing implementation that logs the request + # and returns a standard acknowledgment response. This is useful for testing + # webhook endpoints or as a placeholder during development. + # + # @param payload [Hash, String] The webhook payload (parsed JSON or raw string) + # @param headers [Hash] HTTP headers from the webhook request + # @param config [Hash] Endpoint configuration containing handler options + # @return [Hash] Response indicating successful processing + # @option config [Hash] :opts Additional handler-specific configuration options + # + # @example Basic usage + # handler = DefaultHandler.new + # response = handler.call( + # payload: { "event" => "push" }, + # headers: { "Content-Type" => "application/json" }, + # config: { opts: {} } + # ) + # # => { message: "webhook processed successfully", handler: "DefaultHandler", timestamp: "..." } def call(payload:, headers:, config:) log.info("🔔 Default handler invoked for webhook 🔔") diff --git a/lib/hooks/plugins/instruments/failbot.rb b/lib/hooks/plugins/instruments/failbot.rb index bf32442..71960dc 100644 --- a/lib/hooks/plugins/instruments/failbot.rb +++ b/lib/hooks/plugins/instruments/failbot.rb @@ -7,11 +7,25 @@ module Plugins module Instruments # Default failbot instrument implementation # - # This is a stub implementation that does nothing by default. - # Users can replace this with their own implementation for services - # like Sentry, Rollbar, etc. + # This is a no-op implementation that provides the error reporting interface + # without actually sending errors anywhere. It serves as a safe default when + # no custom error reporting implementation is configured. + # + # Users should replace this with their own implementation for services + # like Sentry, Rollbar, Honeybadger, etc. + # + # @example Replacing with a custom implementation + # # In your application initialization: + # custom_failbot = MySentryFailbotImplementation.new + # Hooks::Core::GlobalComponents.failbot = custom_failbot + # + # @see Hooks::Plugins::Instruments::FailbotBase + # @see Hooks::Core::GlobalComponents class Failbot < FailbotBase - # Inherit from FailbotBase to provide a default implementation of the failbot instrument. + # Inherit from FailbotBase to provide a default no-op implementation + # of the error reporting instrument interface. + # + # All methods from FailbotBase are inherited and provide safe no-op behavior. end end end diff --git a/lib/hooks/plugins/instruments/failbot_base.rb b/lib/hooks/plugins/instruments/failbot_base.rb index 986d3e8..ec11ccb 100644 --- a/lib/hooks/plugins/instruments/failbot_base.rb +++ b/lib/hooks/plugins/instruments/failbot_base.rb @@ -1,23 +1,70 @@ # frozen_string_literal: true +require_relative "../../core/component_access" + module Hooks module Plugins module Instruments # Base class for all failbot instrument plugins # - # All custom failbot implementations must inherit from this class and implement - # the required methods for error reporting. + # This class provides the foundation for implementing custom error reporting + # instruments. Subclasses should implement specific methods for their target + # error reporting service (Sentry, Rollbar, Honeybadger, etc.). + # + # @abstract Subclass and implement service-specific error reporting methods + # @example Implementing a custom failbot instrument + # class MySentryFailbot < Hooks::Plugins::Instruments::FailbotBase + # def report(error_or_message, context = {}) + # case error_or_message + # when Exception + # Sentry.capture_exception(error_or_message, extra: context) + # else + # Sentry.capture_message(error_or_message.to_s, extra: context) + # end + # log.debug("Reported error to Sentry") + # end + # end + # + # @see Hooks::Plugins::Instruments::Failbot class FailbotBase - # Short logger accessor for all subclasses - # @return [Hooks::Log] Logger instance + include Hooks::Core::ComponentAccess + + # Report an error or message to the error tracking service + # + # This is a no-op implementation that subclasses should override + # to provide actual error reporting functionality. + # + # @param error_or_message [Exception, String] The error to report or message string + # @param context [Hash] Additional context information about the error + # @return [void] + # @note Subclasses should implement this method for their specific service + # @example Override in subclass + # def report(error_or_message, context = {}) + # if error_or_message.is_a?(Exception) + # ErrorService.report_exception(error_or_message, context) + # else + # ErrorService.report_message(error_or_message, context) + # end + # end + def report(error_or_message, context = {}) + # No-op implementation for base class + end + + # Report a warning-level message # - # Provides a convenient way for instruments to log messages without needing - # to reference the full Hooks::Log namespace. + # This is a no-op implementation that subclasses should override + # to provide actual warning reporting functionality. # - # @example Logging debug info in an inherited class - # log.debug("Sending error to external service") - def log - Hooks::Log.instance + # @param message [String] Warning message to report + # @param context [Hash] Additional context information + # @return [void] + # @note Subclasses should implement this method for their specific service + # @example Override in subclass + # def warn(message, context = {}) + # ErrorService.report_warning(message, context) + # end + def warn(message, context = {}) + # No-op implementation for base class end end end diff --git a/lib/hooks/plugins/instruments/stats.rb b/lib/hooks/plugins/instruments/stats.rb index 058c891..c4bf1ce 100644 --- a/lib/hooks/plugins/instruments/stats.rb +++ b/lib/hooks/plugins/instruments/stats.rb @@ -7,11 +7,25 @@ module Plugins module Instruments # Default stats instrument implementation # - # This is a stub implementation that does nothing by default. - # Users can replace this with their own implementation for services - # like DataDog, New Relic, etc. + # This is a no-op implementation that provides the stats interface without + # actually sending metrics anywhere. It serves as a safe default when no + # custom stats implementation is configured. + # + # Users should replace this with their own implementation for services + # like DataDog, New Relic, StatsD, etc. + # + # @example Replacing with a custom implementation + # # In your application initialization: + # custom_stats = MyCustomStatsImplementation.new + # Hooks::Core::GlobalComponents.stats = custom_stats + # + # @see Hooks::Plugins::Instruments::StatsBase + # @see Hooks::Core::GlobalComponents class Stats < StatsBase - # Inherit from StatsBase to provide a default implementation of the stats instrument. + # Inherit from StatsBase to provide a default no-op implementation + # of the stats instrument interface. + # + # All methods from StatsBase are inherited and provide safe no-op behavior. end end end diff --git a/lib/hooks/plugins/instruments/stats_base.rb b/lib/hooks/plugins/instruments/stats_base.rb index 8f83cc7..ae4704d 100644 --- a/lib/hooks/plugins/instruments/stats_base.rb +++ b/lib/hooks/plugins/instruments/stats_base.rb @@ -1,23 +1,86 @@ # frozen_string_literal: true +require_relative "../../core/component_access" + module Hooks module Plugins module Instruments # Base class for all stats instrument plugins # - # All custom stats implementations must inherit from this class and implement - # the required methods for metrics reporting. + # This class provides the foundation for implementing custom metrics reporting + # instruments. Subclasses should implement specific methods for their target + # metrics service (DataDog, New Relic, StatsD, etc.). + # + # @abstract Subclass and implement service-specific metrics methods + # @example Implementing a custom stats instrument + # class MyStatsImplementation < Hooks::Plugins::Instruments::StatsBase + # def increment(metric_name, tags = {}) + # # Send increment metric to your service + # MyMetricsService.increment(metric_name, tags) + # log.debug("Sent increment metric: #{metric_name}") + # end + # + # def timing(metric_name, duration, tags = {}) + # # Send timing metric to your service + # MyMetricsService.timing(metric_name, duration, tags) + # end + # end + # + # @see Hooks::Plugins::Instruments::Stats class StatsBase - # Short logger accessor for all subclasses - # @return [Hooks::Log] Logger instance + include Hooks::Core::ComponentAccess + + # Record an increment metric + # + # This is a no-op implementation that subclasses should override + # to provide actual metrics reporting functionality. + # + # @param metric_name [String] Name of the metric to increment + # @param tags [Hash] Optional tags/labels for the metric + # @return [void] + # @note Subclasses should implement this method for their specific service + # @example Override in subclass + # def increment(metric_name, tags = {}) + # statsd.increment(metric_name, tags: tags) + # end + def increment(metric_name, tags = {}) + # No-op implementation for base class + end + + # Record a timing/duration metric + # + # This is a no-op implementation that subclasses should override + # to provide actual metrics reporting functionality. + # + # @param metric_name [String] Name of the timing metric + # @param duration [Numeric] Duration value (typically in milliseconds) + # @param tags [Hash] Optional tags/labels for the metric + # @return [void] + # @note Subclasses should implement this method for their specific service + # @example Override in subclass + # def timing(metric_name, duration, tags = {}) + # statsd.timing(metric_name, duration, tags: tags) + # end + def timing(metric_name, duration, tags = {}) + # No-op implementation for base class + end + + # Record a gauge metric # - # Provides a convenient way for instruments to log messages without needing - # to reference the full Hooks::Log namespace. + # This is a no-op implementation that subclasses should override + # to provide actual metrics reporting functionality. # - # @example Logging an error in an inherited class - # log.error("Failed to send metric to external service") - def log - Hooks::Log.instance + # @param metric_name [String] Name of the gauge metric + # @param value [Numeric] Current value for the gauge + # @param tags [Hash] Optional tags/labels for the metric + # @return [void] + # @note Subclasses should implement this method for their specific service + # @example Override in subclass + # def gauge(metric_name, value, tags = {}) + # statsd.gauge(metric_name, value, tags: tags) + # end + def gauge(metric_name, value, tags = {}) + # No-op implementation for base class end end end diff --git a/lib/hooks/plugins/lifecycle.rb b/lib/hooks/plugins/lifecycle.rb index 86778e2..b77afed 100644 --- a/lib/hooks/plugins/lifecycle.rb +++ b/lib/hooks/plugins/lifecycle.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "../core/global_components" +require_relative "../core/component_access" module Hooks module Plugins @@ -8,6 +9,8 @@ module Plugins # # Plugins can hook into request/response/error lifecycle events class Lifecycle + include Hooks::Core::ComponentAccess + # Called before handler execution # # @param env [Hash] Rack environment @@ -30,42 +33,6 @@ def on_response(env, response) def on_error(exception, env) # Override in subclass for error handling logic end - - # Short logger accessor for all subclasses - # @return [Hooks::Log] Logger instance - # - # Provides a convenient way for lifecycle plugins to log messages without needing - # to reference the full Hooks::Log namespace. - # - # @example Logging an error in an inherited class - # log.error("oh no an error occured") - def log - Hooks::Log.instance - end - - # Global stats component accessor - # @return [Hooks::Core::Stats] Stats instance for metrics reporting - # - # Provides access to the global stats component for reporting metrics - # to services like DataDog, New Relic, etc. - # - # @example Recording a metric in an inherited class - # stats.increment("lifecycle.request_processed") - def stats - Hooks::Core::GlobalComponents.stats - end - - # Global failbot component accessor - # @return [Hooks::Core::Failbot] Failbot instance for error reporting - # - # Provides access to the global failbot component for reporting errors - # to services like Sentry, Rollbar, etc. - # - # @example Reporting an error in an inherited class - # failbot.report("Lifecycle hook failed") - def failbot - Hooks::Core::GlobalComponents.failbot - end end end end diff --git a/lib/hooks/version.rb b/lib/hooks/version.rb index f02ec50..5835701 100644 --- a/lib/hooks/version.rb +++ b/lib/hooks/version.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +# Main Hooks module containing version information module Hooks + # Current version of the Hooks webhook framework + # @return [String] The version string following semantic versioning VERSION = "0.0.3" end diff --git a/spec/unit/lib/hooks/core/component_access_spec.rb b/spec/unit/lib/hooks/core/component_access_spec.rb new file mode 100644 index 0000000..7e28c9c --- /dev/null +++ b/spec/unit/lib/hooks/core/component_access_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +describe Hooks::Core::ComponentAccess do + let(:test_class_with_include) do + Class.new do + include Hooks::Core::ComponentAccess + end + end + + let(:test_class_with_extend) do + Class.new do + extend Hooks::Core::ComponentAccess + end + end + + describe "when included" do + let(:instance) { test_class_with_include.new } + + describe "#log" do + it "provides access to global logger" do + expect(instance.log).to be(Hooks::Log.instance) + end + end + + describe "#stats" do + it "provides access to global stats" do + expect(instance.stats).to be_a(Hooks::Plugins::Instruments::Stats) + expect(instance.stats).to eq(Hooks::Core::GlobalComponents.stats) + end + end + + describe "#failbot" do + it "provides access to global failbot" do + expect(instance.failbot).to be_a(Hooks::Plugins::Instruments::Failbot) + expect(instance.failbot).to eq(Hooks::Core::GlobalComponents.failbot) + end + end + end + + describe "when extended" do + describe ".log" do + it "provides access to global logger" do + expect(test_class_with_extend.log).to be(Hooks::Log.instance) + end + end + + describe ".stats" do + it "provides access to global stats" do + expect(test_class_with_extend.stats).to be_a(Hooks::Plugins::Instruments::Stats) + expect(test_class_with_extend.stats).to eq(Hooks::Core::GlobalComponents.stats) + end + end + + describe ".failbot" do + it "provides access to global failbot" do + expect(test_class_with_extend.failbot).to be_a(Hooks::Plugins::Instruments::Failbot) + expect(test_class_with_extend.failbot).to eq(Hooks::Core::GlobalComponents.failbot) + end + end + end +end diff --git a/spec/unit/lib/hooks/handlers/base_spec.rb b/spec/unit/lib/hooks/handlers/base_spec.rb index a433f40..1ce807c 100644 --- a/spec/unit/lib/hooks/handlers/base_spec.rb +++ b/spec/unit/lib/hooks/handlers/base_spec.rb @@ -145,7 +145,7 @@ def call(payload:, headers:, config:) describe "documentation compliance" do it "has the expected public interface" do - expect(described_class.instance_methods(false)).to include(:call, :log, :stats, :failbot) + expect(described_class.instance_methods).to include(:call, :log, :stats, :failbot) end it "call method accepts the documented parameters" do diff --git a/spec/unit/lib/hooks/plugins/auth/base_spec.rb b/spec/unit/lib/hooks/plugins/auth/base_spec.rb index a9a38ea..df241ef 100644 --- a/spec/unit/lib/hooks/plugins/auth/base_spec.rb +++ b/spec/unit/lib/hooks/plugins/auth/base_spec.rb @@ -206,7 +206,7 @@ def self.valid?(payload:, headers:, config:) describe "documentation compliance" do it "has the expected public interface" do - expect(described_class.methods(false)).to include(:valid?, :log, :stats, :failbot, :fetch_secret) + expect(described_class.methods).to include(:valid?, :log, :stats, :failbot, :fetch_secret) end it "valid? method accepts the documented parameters" do