hooks-ruby 0.0.2

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.
data/bin/rdoc ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rdoc' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rdoc", "rdoc")
data/bin/ri ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ri' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rdoc", "ri")
data/bin/rspec ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/rubocop ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rubocop", "rubocop")
data/bin/ruby-parse ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ruby-parse' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("parser", "ruby-parse")
data/bin/ruby-rewrite ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ruby-rewrite' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("parser", "ruby-rewrite")
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubygems-await' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rubygems-await", "rubygems-await")
data/bin/sigstore-cli ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'sigstore-cli' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("sigstore-cli", "sigstore-cli")
data/bin/thor ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'thor' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("thor", "thor")
data/config.ru ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/hooks"
4
+
5
+ app = Hooks.build(config: "./spec/acceptance/config/hooks.yaml")
6
+ run app
data/hooks.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/hooks/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "hooks-ruby"
7
+ spec.version = Hooks::VERSION
8
+ spec.authors = ["github", "GrantBirki"]
9
+ spec.license = "MIT"
10
+
11
+ spec.summary = "A Pluggable Webhook Server Framework written in Ruby"
12
+ spec.description = <<~SPEC_DESC
13
+ A Pluggable Webhook Server Framework written in Ruby
14
+ SPEC_DESC
15
+
16
+ spec.homepage = "https://github.com/github/hooks"
17
+ spec.metadata = {
18
+ "bug_tracker_uri" => "https://github.com/github/hooks/issues"
19
+ }
20
+
21
+ spec.add_dependency "redacting-logger", "~> 1.5"
22
+ spec.add_dependency "retryable", "~> 3.0", ">= 3.0.5"
23
+ spec.add_dependency "dry-schema", "~> 1.14", ">= 1.14.1"
24
+ spec.add_dependency "grape", "~> 2.3"
25
+ spec.add_dependency "grape-swagger", "~> 2.1", ">= 2.1.2"
26
+ spec.add_dependency "puma", "~> 6.6"
27
+
28
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2.2")
29
+
30
+ spec.files = %w[LICENSE README.md hooks.gemspec config.ru]
31
+ spec.files += Dir.glob("lib/**/*.rb")
32
+ spec.files += Dir.glob("bin/*")
33
+ spec.bindir = "bin"
34
+ spec.executables = ["hooks"]
35
+ spec.require_paths = ["lib"]
36
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "grape"
4
+ require "json"
5
+ require "securerandom"
6
+ require_relative "helpers"
7
+ require_relative "auth/auth"
8
+ require_relative "../plugins/handlers/base"
9
+ require_relative "../plugins/handlers/default"
10
+ require_relative "../core/logger_factory"
11
+ require_relative "../core/log"
12
+
13
+ # Import all core endpoint classes dynamically
14
+ Dir[File.join(__dir__, "endpoints/**/*.rb")].sort.each { |file| require file }
15
+
16
+ module Hooks
17
+ module App
18
+ # Factory for creating configured Grape API classes
19
+ class API
20
+ include Hooks::App::Helpers
21
+ include Hooks::App::Auth
22
+
23
+ class << self
24
+ attr_reader :start_time
25
+ end
26
+
27
+ # Create a new configured API class
28
+ def self.create(config:, endpoints:, log:)
29
+ @start_time = Time.now
30
+
31
+ Hooks::Log.instance = log
32
+
33
+ api_class = Class.new(Grape::API) do
34
+ content_type :json, "application/json"
35
+ content_type :txt, "text/plain"
36
+ content_type :xml, "application/xml"
37
+ content_type :any, "*/*"
38
+ format :txt
39
+ default_format :txt
40
+ end
41
+
42
+ api_class.class_eval do
43
+ helpers Helpers, Auth
44
+
45
+ mount Hooks::App::HealthEndpoint => config[:health_path]
46
+ mount Hooks::App::VersionEndpoint => config[:version_path]
47
+
48
+ endpoints.each do |endpoint_config|
49
+ full_path = "#{config[:root_path]}#{endpoint_config[:path]}"
50
+ handler_class_name = endpoint_config[:handler]
51
+
52
+ post(full_path) do
53
+ request_id = uuid
54
+ request_context = {
55
+ request_id:,
56
+ path: full_path,
57
+ handler: handler_class_name
58
+ }
59
+
60
+ Core::LogContext.with(request_context) do
61
+ begin
62
+ enforce_request_limits(config)
63
+ request.body.rewind
64
+ raw_body = request.body.read
65
+
66
+ if endpoint_config[:auth]
67
+ log.info "validating request (id: #{request_id}, handler: #{handler_class_name})"
68
+ validate_auth!(raw_body, headers, endpoint_config, config)
69
+ end
70
+
71
+ payload = parse_payload(raw_body, headers, symbolize: config[:symbolize_payload])
72
+ handler = load_handler(handler_class_name)
73
+ normalized_headers = config[:normalize_headers] ? Hooks::Utils::Normalize.headers(headers) : headers
74
+
75
+ response = handler.call(
76
+ payload:,
77
+ headers: normalized_headers,
78
+ config: endpoint_config
79
+ )
80
+
81
+ log.info "request processed successfully (id: #{request_id}, handler: #{handler_class_name})"
82
+ status 200
83
+ content_type "application/json"
84
+ (response || { status: "ok" }).to_json
85
+ rescue => e
86
+ log.error "request failed: #{e.message} (id: #{request_id}, handler: #{handler_class_name})"
87
+ error_response = {
88
+ error: e.message,
89
+ code: determine_error_code(e),
90
+ request_id: request_id
91
+ }
92
+ error_response[:backtrace] = e.backtrace unless config[:production]
93
+ status error_response[:code]
94
+ content_type "application/json"
95
+ error_response.to_json
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ if config[:use_catchall_route]
102
+ route_path = Hooks::App::CatchallEndpoint.mount_path(config)
103
+ route_block = Hooks::App::CatchallEndpoint.route_block(config, log)
104
+ post(route_path, &route_block)
105
+ end
106
+ end
107
+
108
+ api_class
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../core/plugin_loader"
4
+
5
+ module Hooks
6
+ module App
7
+ # Provides authentication helpers for verifying incoming requests.
8
+ #
9
+ # @example Usage
10
+ # include Hooks::App::Auth
11
+ # validate_auth!(payload, headers, endpoint_config)
12
+ module Auth
13
+ # Verifies the incoming request using the configured authentication method.
14
+ #
15
+ # @param payload [String, Hash] The request payload to authenticate.
16
+ # @param headers [Hash] The request headers.
17
+ # @param endpoint_config [Hash] The endpoint configuration, must include :auth key.
18
+ # @param global_config [Hash] The global configuration (optional, for compatibility).
19
+ # @raise [StandardError] Raises error if authentication fails or is misconfigured.
20
+ # @return [void]
21
+ # @note This method will halt execution with an error if authentication fails.
22
+ def validate_auth!(payload, headers, endpoint_config, global_config = {})
23
+ auth_config = endpoint_config[:auth]
24
+
25
+ # Security: Ensure auth type is present and valid
26
+ auth_type = auth_config&.dig(:type)
27
+ unless auth_type&.is_a?(String) && !auth_type.strip.empty?
28
+ error!("authentication configuration missing or invalid", 500)
29
+ end
30
+
31
+ # Get auth plugin from loaded plugins registry (boot-time loaded only)
32
+ begin
33
+ auth_class = Core::PluginLoader.get_auth_plugin(auth_type)
34
+ rescue => e
35
+ error!("unsupported auth type '#{auth_type}'", 400)
36
+ end
37
+
38
+ unless auth_class.valid?(
39
+ payload:,
40
+ headers:,
41
+ config: endpoint_config
42
+ )
43
+ error!("authentication failed", 401)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "grape"
4
+ require_relative "../../plugins/handlers/default"
5
+ require_relative "../helpers"
6
+
7
+ module Hooks
8
+ module App
9
+ class CatchallEndpoint < Grape::API
10
+ include Hooks::App::Helpers
11
+
12
+ def self.mount_path(config)
13
+ "#{config[:root_path]}/*path"
14
+ end
15
+
16
+ def self.route_block(captured_config, captured_logger)
17
+ proc do
18
+ request_id = uuid
19
+
20
+ # Use captured values
21
+ config = captured_config
22
+ log = captured_logger
23
+
24
+ # Set request context for logging
25
+ request_context = {
26
+ request_id: request_id,
27
+ path: "/#{params[:path]}",
28
+ handler: "DefaultHandler"
29
+ }
30
+
31
+ Hooks::Core::LogContext.with(request_context) do
32
+ begin
33
+ # Enforce request limits
34
+ enforce_request_limits(config)
35
+
36
+ # Get raw body for payload parsing
37
+ request.body.rewind
38
+ raw_body = request.body.read
39
+
40
+ # Parse payload
41
+ payload = parse_payload(raw_body, headers)
42
+
43
+ # Use default handler
44
+ handler = DefaultHandler.new
45
+
46
+ # Call handler
47
+ response = handler.call(
48
+ payload: payload,
49
+ headers: headers,
50
+ config: {}
51
+ )
52
+
53
+ log.info "request processed successfully with default handler (id: #{request_id})"
54
+
55
+ # Return response as JSON string when using txt format
56
+ status 200
57
+ content_type "application/json"
58
+ (response || { status: "ok" }).to_json
59
+
60
+ rescue StandardError => e
61
+ log.error "request failed: #{e.message} (id: #{request_id})"
62
+
63
+ # Return error response
64
+ error_response = {
65
+ error: e.message,
66
+ code: determine_error_code(e),
67
+ request_id: request_id
68
+ }
69
+
70
+ # Add backtrace in all environments except production
71
+ unless config[:production] == true
72
+ error_response[:backtrace] = e.backtrace
73
+ end
74
+
75
+ status error_response[:code]
76
+ content_type "application/json"
77
+ error_response.to_json
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "grape"
4
+ require_relative "../../version"
5
+
6
+ module Hooks
7
+ module App
8
+ class HealthEndpoint < Grape::API
9
+ get do
10
+ content_type "application/json"
11
+ {
12
+ status: "healthy",
13
+ timestamp: Time.now.iso8601,
14
+ version: Hooks::VERSION,
15
+ uptime_seconds: (Time.now - Hooks::App::API.start_time).to_i
16
+ }.to_json
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "grape"
4
+ require_relative "../../version"
5
+
6
+ module Hooks
7
+ module App
8
+ class VersionEndpoint < Grape::API
9
+ get do
10
+ content_type "application/json"
11
+ {
12
+ version: Hooks::VERSION,
13
+ timestamp: Time.now.iso8601
14
+ }.to_json
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "../security"
5
+ require_relative "../core/plugin_loader"
6
+
7
+ module Hooks
8
+ module App
9
+ module Helpers
10
+ # Generate a unique identifier (UUID)
11
+ #
12
+ # @return [String] a new UUID string
13
+ def uuid
14
+ SecureRandom.uuid
15
+ end
16
+
17
+ # Enforce request size and timeout limits
18
+ #
19
+ # @param config [Hash] The configuration hash, must include :request_limit
20
+ # @raise [StandardError] Halts with error if request body is too large
21
+ # @return [void]
22
+ # @note Timeout enforcement should be handled at the server level (e.g., Puma)
23
+ def enforce_request_limits(config)
24
+ # Check content length (handle different header formats and sources)
25
+ content_length = headers["Content-Length"] || headers["CONTENT_LENGTH"] ||
26
+ headers["content-length"] || headers["HTTP_CONTENT_LENGTH"] ||
27
+ env["CONTENT_LENGTH"] || env["HTTP_CONTENT_LENGTH"]
28
+
29
+ # Also try to get from request object directly
30
+ content_length ||= request.content_length if respond_to?(:request) && request.respond_to?(:content_length)
31
+
32
+ content_length = content_length&.to_i
33
+
34
+ if content_length && content_length > config[:request_limit]
35
+ error!("request body too large", 413)
36
+ end
37
+
38
+ # Note: Timeout enforcement would typically be handled at the server level (Puma, etc.)
39
+ end
40
+
41
+ # Parse request payload
42
+ #
43
+ # @param raw_body [String] The raw request body
44
+ # @param headers [Hash] The request headers
45
+ # @param symbolize [Boolean] Whether to symbolize keys in parsed JSON (default: true)
46
+ # @return [Hash, String] Parsed JSON as Hash (optionally symbolized), or raw body if not JSON
47
+ def parse_payload(raw_body, headers, symbolize: true)
48
+ content_type = headers["Content-Type"] || headers["CONTENT_TYPE"] || headers["content-type"] || headers["HTTP_CONTENT_TYPE"]
49
+
50
+ # Try to parse as JSON if content type suggests it or if it looks like JSON
51
+ if content_type&.include?("application/json") || (raw_body.strip.start_with?("{", "[") rescue false)
52
+ begin
53
+ parsed_payload = JSON.parse(raw_body)
54
+ parsed_payload = parsed_payload.transform_keys(&:to_sym) if symbolize && parsed_payload.is_a?(Hash)
55
+ return parsed_payload
56
+ rescue JSON::ParserError
57
+ # If JSON parsing fails, return raw body
58
+ end
59
+ end
60
+
61
+ # Return raw body for all other cases
62
+ raw_body
63
+ end
64
+
65
+ # Load handler class
66
+ #
67
+ # @param handler_class_name [String] The name of the handler class to load
68
+ # @return [Object] An instance of the loaded handler class
69
+ # @raise [StandardError] If handler cannot be found
70
+ def load_handler(handler_class_name)
71
+ # Get handler class from loaded plugins registry (boot-time loaded only)
72
+ begin
73
+ handler_class = Core::PluginLoader.get_handler_plugin(handler_class_name)
74
+ return handler_class.new
75
+ rescue => e
76
+ error!("failed to get handler '#{handler_class_name}': #{e.message}", 500)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # Determine HTTP error code from exception
83
+ #
84
+ # @param exception [Exception] The exception to map to an HTTP status code
85
+ # @return [Integer] The HTTP status code (400, 501, or 500)
86
+ def determine_error_code(exception)
87
+ case exception
88
+ when ArgumentError then 400
89
+ when NotImplementedError then 501
90
+ else 500
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end