hookdeck 0.1.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/.rubocop.yml +42 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/Makefile +22 -0
- data/README.md +249 -0
- data/Rakefile +10 -0
- data/lib/hookdeck/client.rb +168 -0
- data/lib/hookdeck/configuration.rb +31 -0
- data/lib/hookdeck/errors.rb +123 -0
- data/lib/hookdeck/http_client.rb +39 -0
- data/lib/hookdeck/middlewares/error_handler.rb +68 -0
- data/lib/hookdeck/middlewares/logging.rb +144 -0
- data/lib/hookdeck/middlewares/request_id.rb +39 -0
- data/lib/hookdeck/resources/attempt.rb +24 -0
- data/lib/hookdeck/resources/base.rb +174 -0
- data/lib/hookdeck/resources/bookmark.rb +70 -0
- data/lib/hookdeck/resources/bulk_event_retry.rb +40 -0
- data/lib/hookdeck/resources/bulk_ignored_event_retry.rb +36 -0
- data/lib/hookdeck/resources/bulk_request_retry.rb +31 -0
- data/lib/hookdeck/resources/connection.rb +62 -0
- data/lib/hookdeck/resources/custom_domain.rb +17 -0
- data/lib/hookdeck/resources/destination.rb +49 -0
- data/lib/hookdeck/resources/event.rb +29 -0
- data/lib/hookdeck/resources/issue.rb +32 -0
- data/lib/hookdeck/resources/issue_trigger.rb +56 -0
- data/lib/hookdeck/resources/notification.rb +30 -0
- data/lib/hookdeck/resources/request.rb +33 -0
- data/lib/hookdeck/resources/source.rb +49 -0
- data/lib/hookdeck/resources/transformation.rb +54 -0
- data/lib/hookdeck/resources.rb +20 -0
- data/lib/hookdeck/version.rb +5 -0
- data/lib/hookdeck.rb +39 -0
- data/sig/hookdeck.rbs +4 -0
- metadata +253 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module Hookdeck
|
2
|
+
class Configuration
|
3
|
+
DEFAULTS = {
|
4
|
+
api_base: 'https://api.hookdeck.com',
|
5
|
+
api_version: '2024-09-01'
|
6
|
+
}.freeze
|
7
|
+
|
8
|
+
attr_accessor :api_key, :api_base, :api_version
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
DEFAULTS.each do |key, value|
|
12
|
+
instance_variable_set("@#{key}", value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Validates the configuration.
|
17
|
+
#
|
18
|
+
# @raise [Hookdeck::ValidationError] If the API key is not provided.
|
19
|
+
def validate!
|
20
|
+
validate_api_version!
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_api_version!
|
26
|
+
return if api_version.match?(/^\d{4}-\d{2}-\d{2}$/)
|
27
|
+
|
28
|
+
raise ValidationError, "Invalid API version format. Expected YYYY-MM-DD, got #{api_version}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Hookdeck
|
2
|
+
class Error < StandardError
|
3
|
+
attr_reader :message, :request_id
|
4
|
+
|
5
|
+
def initialize(message = nil, request_id = nil)
|
6
|
+
@message = message
|
7
|
+
@request_id = request_id
|
8
|
+
super(error_message)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def error_message
|
14
|
+
request_id ? "#{message} (Request ID: #{request_id})" : message
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class ValidationError < Error; end
|
19
|
+
|
20
|
+
class ApiError < Error
|
21
|
+
attr_reader :response,
|
22
|
+
:handled,
|
23
|
+
:status,
|
24
|
+
:message,
|
25
|
+
:data,
|
26
|
+
:request_id
|
27
|
+
|
28
|
+
def initialize(response = nil)
|
29
|
+
@response = response
|
30
|
+
parse_error_response(response&.dig(:body))
|
31
|
+
@request_id = response&.dig(:headers, 'x-request-id')
|
32
|
+
super(message, request_id)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def parse_error_response(body)
|
38
|
+
error_data = parse_body(body)
|
39
|
+
@handled = error_data['handled']
|
40
|
+
@status = error_data['status']
|
41
|
+
@message = error_data['message']
|
42
|
+
@data = error_data['data']
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_body(body)
|
46
|
+
return {} unless body
|
47
|
+
|
48
|
+
body.is_a?(String) ? JSON.parse(body) : body
|
49
|
+
rescue JSON::ParserError
|
50
|
+
{
|
51
|
+
'handled' => true,
|
52
|
+
'status' => response&.dig(:status),
|
53
|
+
'message' => body.to_s,
|
54
|
+
'data' => {}
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class BadRequestError < ApiError
|
60
|
+
def initialize(response)
|
61
|
+
super
|
62
|
+
@status = 400
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class UnauthorizedError < ApiError
|
67
|
+
def initialize(response)
|
68
|
+
super
|
69
|
+
@status = 401
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class ForbiddenError < ApiError
|
74
|
+
def initialize(response)
|
75
|
+
super
|
76
|
+
@status = 403
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class NotFoundError < ApiError
|
81
|
+
def initialize(response)
|
82
|
+
super
|
83
|
+
@status = 404
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class UnprocessableEntityError < ApiError
|
88
|
+
def initialize(response)
|
89
|
+
super
|
90
|
+
@status = 422
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class RateLimitError < ApiError
|
95
|
+
attr_reader :retry_after
|
96
|
+
|
97
|
+
def initialize(response)
|
98
|
+
super
|
99
|
+
@status = 429
|
100
|
+
@retry_after = response&.dig(:headers, 'retry-after')&.to_i
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class ServerError < ApiError
|
105
|
+
def initialize(response)
|
106
|
+
super
|
107
|
+
@status = response&.dig(:status) || 500
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class ConnectionError < Error
|
112
|
+
attr_reader :original_error
|
113
|
+
|
114
|
+
def initialize(error)
|
115
|
+
@original_error = error
|
116
|
+
super(error.message)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class TimeoutError < ConnectionError; end
|
121
|
+
|
122
|
+
class SSLError < ConnectionError; end
|
123
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class HttpClient
|
2
|
+
attr_reader :config
|
3
|
+
|
4
|
+
def initialize(config)
|
5
|
+
@config = config
|
6
|
+
@connection_pools = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def connection
|
10
|
+
Thread.current[:hookdeck_client] ||= build_connection
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_connection
|
14
|
+
Faraday.new(url: config.api_base) do |faraday|
|
15
|
+
setup_request_middleware(faraday)
|
16
|
+
setup_response_middleware(faraday)
|
17
|
+
setup_error_handling(faraday)
|
18
|
+
faraday.adapter :net_http
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup_request_middleware(faraday)
|
23
|
+
faraday.request :json
|
24
|
+
# faraday.request :retry, max: config.max_retries, interval: config.retry_interval
|
25
|
+
faraday.request :authorization, 'Bearer', config.api_key
|
26
|
+
|
27
|
+
# faraday.use Middleware::RequestId, prefix: config.request_id_prefix
|
28
|
+
# faraday.use Middleware::Logging, logger: config.logger
|
29
|
+
end
|
30
|
+
|
31
|
+
def setup_response_middleware(faraday)
|
32
|
+
faraday.response :json, content_type: /\bjson$/
|
33
|
+
# faraday.response :logger, config.logger, bodies: true if config.logger
|
34
|
+
end
|
35
|
+
|
36
|
+
def setup_error_handling(faraday)
|
37
|
+
# faraday.use Middleware::ErrorHandler
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HookDeck
|
4
|
+
module Middleware
|
5
|
+
# ErrorHandler is a Faraday middleware that provides standardized error handling
|
6
|
+
# for HTTP requests. It catches Faraday errors and converts them into specific
|
7
|
+
# HookDeck error types based on the response status and error type
|
8
|
+
#
|
9
|
+
class ErrorHandler < Faraday::Middleware
|
10
|
+
# Executes the middleware
|
11
|
+
# @param env [Hash] The Faraday environment hash
|
12
|
+
# @raise [HookDeck::Error] Various error types based on the response
|
13
|
+
def call(env)
|
14
|
+
@app.call(env)
|
15
|
+
rescue Faraday::Error => e
|
16
|
+
handle_error(e, env)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Maps Faraday errors to HookDeck-specific errors
|
22
|
+
# @param error [Faraday::Error] The original Faraday error
|
23
|
+
# @param _env [Hash] The Faraday environment hash
|
24
|
+
# @raise [HookDeck::Error] Converted error type
|
25
|
+
def handle_error(error, _env)
|
26
|
+
case error
|
27
|
+
when Faraday::ClientError
|
28
|
+
handle_client_error(error.response)
|
29
|
+
when Faraday::ServerError
|
30
|
+
handle_server_error(error.response)
|
31
|
+
when Faraday::ConnectionFailed
|
32
|
+
raise ConnectionError, error
|
33
|
+
when Faraday::TimeoutError
|
34
|
+
raise TimeoutError, error
|
35
|
+
when Faraday::SSLError
|
36
|
+
raise SSLError, error
|
37
|
+
else
|
38
|
+
raise Error, error.message
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Maps HTTP status codes to specific client errors
|
43
|
+
# @param response [Hash] The error response hash
|
44
|
+
# @raise [HookDeck::Error] Specific client error type
|
45
|
+
def handle_client_error(response)
|
46
|
+
error_class = case response[:status]
|
47
|
+
when 400 then BadRequestError
|
48
|
+
when 401 then UnauthorizedError
|
49
|
+
when 403 then ForbiddenError
|
50
|
+
when 404 then NotFoundError
|
51
|
+
when 409 then ConflictError
|
52
|
+
when 422 then UnprocessableEntityError
|
53
|
+
when 429 then RateLimitError
|
54
|
+
else ApiError
|
55
|
+
end
|
56
|
+
|
57
|
+
raise error_class, response
|
58
|
+
end
|
59
|
+
|
60
|
+
# Handles server-side errors
|
61
|
+
# @param response [Hash] The error response hash
|
62
|
+
# @raise [HookDeck::ServerError] Server error
|
63
|
+
def handle_server_error(response)
|
64
|
+
raise ServerError, response
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HookDeck
|
4
|
+
module Middleware
|
5
|
+
# Logging middleware that provides detailed request/response logging with security-aware filtering.
|
6
|
+
# Automatically redacts sensitive information from headers and request/response bodies.
|
7
|
+
#
|
8
|
+
class Logging < Faraday::Middleware
|
9
|
+
# Initialize the logging middleware
|
10
|
+
# @param app [#call] The Faraday app
|
11
|
+
# @param logger [Logger, nil] Optional custom logger (defaults to STDOUT logger)
|
12
|
+
def initialize(app, logger: nil)
|
13
|
+
super(app)
|
14
|
+
@logger = logger || default_logger
|
15
|
+
end
|
16
|
+
|
17
|
+
# Executes the middleware, logging request and response details
|
18
|
+
# @param env [Hash] The Faraday environment hash
|
19
|
+
def call(env)
|
20
|
+
start_time = Time.now
|
21
|
+
log_request(env)
|
22
|
+
|
23
|
+
@app.call(env).on_complete do |response_env|
|
24
|
+
duration = Time.now - start_time
|
25
|
+
log_response(response_env, duration)
|
26
|
+
end
|
27
|
+
rescue StandardError => e
|
28
|
+
log_error(e)
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Logs request details with sensitive data filtered
|
35
|
+
def log_request(env)
|
36
|
+
@logger.info do
|
37
|
+
{
|
38
|
+
method: env.method.upcase,
|
39
|
+
url: env.url.to_s,
|
40
|
+
body: sanitize_body(env.body),
|
41
|
+
headers: sanitize_headers(env.request_headers),
|
42
|
+
request_id: env.request_headers['X-Request-Id']
|
43
|
+
}.to_json
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Logs response details with timing information
|
48
|
+
def log_response(env, duration)
|
49
|
+
@logger.info do
|
50
|
+
{
|
51
|
+
method: env.method.upcase,
|
52
|
+
url: env.url.to_s,
|
53
|
+
status: env.status,
|
54
|
+
duration: duration.round(3),
|
55
|
+
headers: sanitize_headers(env.response_headers),
|
56
|
+
request_id: env.request_headers['X-Request-Id']
|
57
|
+
}.to_json
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Logs error details with backtrace
|
62
|
+
# @param error [StandardError] The caught error
|
63
|
+
def log_error(error)
|
64
|
+
@logger.error do
|
65
|
+
{
|
66
|
+
error: error.class.name,
|
67
|
+
message: error.message,
|
68
|
+
backtrace: error.backtrace&.first(5)
|
69
|
+
}.to_json
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Sanitizes request/response body by filtering sensitive data
|
74
|
+
# @param body [String, nil] The raw body
|
75
|
+
# @return [Hash, String, nil] Sanitized body
|
76
|
+
def sanitize_body(body)
|
77
|
+
return nil if body.nil?
|
78
|
+
return body unless body.is_a?(String)
|
79
|
+
|
80
|
+
begin
|
81
|
+
parsed = JSON.parse(body)
|
82
|
+
sanitize_hash(parsed)
|
83
|
+
rescue JSON::ParserError
|
84
|
+
'Unparseable body'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Recursively sanitizes hash values
|
89
|
+
# @param hash [Hash] Hash to sanitize
|
90
|
+
# @return [Hash] Sanitized hash
|
91
|
+
def sanitize_hash(hash)
|
92
|
+
hash.each_with_object({}) do |(key, value), sanitized|
|
93
|
+
sanitized[key] = if sensitive_key?(key)
|
94
|
+
'[FILTERED]'
|
95
|
+
elsif value.is_a?(Hash)
|
96
|
+
sanitize_hash(value)
|
97
|
+
elsif value.is_a?(Array)
|
98
|
+
value.map { |v| v.is_a?(Hash) ? sanitize_hash(v) : v }
|
99
|
+
else
|
100
|
+
value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Sanitizes headers by filtering sensitive values
|
106
|
+
# @param headers [Hash] Headers to sanitize
|
107
|
+
# @return [Hash] Sanitized headers
|
108
|
+
def sanitize_headers(headers)
|
109
|
+
headers.each_with_object({}) do |(key, value), sanitized|
|
110
|
+
sanitized[key] = if sensitive_header?(key)
|
111
|
+
'[FILTERED]'
|
112
|
+
else
|
113
|
+
value
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Checks if a key contains sensitive information
|
119
|
+
# @param key [String, Symbol] Key to check
|
120
|
+
# @return [Boolean] true if sensitive
|
121
|
+
def sensitive_key?(key)
|
122
|
+
key.to_s.downcase.match?(/password|token|key|secret|credential|auth/)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Checks if a header contains sensitive information
|
126
|
+
# @param header [String, Symbol] Header to check
|
127
|
+
# @return [Boolean] true if sensitive
|
128
|
+
def sensitive_header?(header)
|
129
|
+
header.to_s.downcase.match?(/authorization|x-api-key|cookie/)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Creates a default logger if none provided
|
133
|
+
# @return [Logger] Configured logger
|
134
|
+
def default_logger
|
135
|
+
logger = Logger.new($stdout)
|
136
|
+
logger.progname = 'hookdeck'
|
137
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
138
|
+
"#{datetime} [#{progname}] #{severity}: #{msg}\n"
|
139
|
+
end
|
140
|
+
logger
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HookDeck
|
4
|
+
module Middleware
|
5
|
+
# Middleware that automatically generates and manages request IDs for HTTP requests.
|
6
|
+
# Adds both server and client request IDs to facilitate request tracking and debugging.
|
7
|
+
#
|
8
|
+
class RequestId < Faraday::Middleware
|
9
|
+
# Initialize the request ID middleware
|
10
|
+
# @param app [#call] The Faraday app
|
11
|
+
# @param options [Hash] Configuration options
|
12
|
+
# @option options [String] :prefix ('req') Prefix for generated request IDs
|
13
|
+
def initialize(app, options = {})
|
14
|
+
super(app)
|
15
|
+
@prefix = options.fetch(:prefix, 'req')
|
16
|
+
end
|
17
|
+
|
18
|
+
# Executes the middleware, adding and tracking request IDs
|
19
|
+
def call(env)
|
20
|
+
env.request_headers['X-Request-Id'] ||= generate_request_id
|
21
|
+
env.request_headers['X-Client-Request-Id'] ||= generate_request_id
|
22
|
+
|
23
|
+
@app.call(env).on_complete do |response_env|
|
24
|
+
# Store request IDs for potential error handling
|
25
|
+
response_env[:hookdeck_request_id] = response_env.response_headers['x-request-id']
|
26
|
+
response_env[:hookdeck_client_request_id] = env.request_headers['X-Client-Request-Id']
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Generates a unique request ID with configured prefix
|
33
|
+
# @return [String] Request ID in format "prefix_hexstring"
|
34
|
+
def generate_request_id
|
35
|
+
"#{@prefix}_#{SecureRandom.hex(16)}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hookdeck
|
2
|
+
module Resources
|
3
|
+
# @example List recent attempts
|
4
|
+
# client.attempts.list(limit: 10)
|
5
|
+
#
|
6
|
+
# @example Retrieve specific attempt
|
7
|
+
# attempt = client.attempts.retrieve('atm_123')
|
8
|
+
#
|
9
|
+
class Attempt < Base
|
10
|
+
def list(params = {})
|
11
|
+
get('attempts', params)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Retrieves an attempt by ID.
|
15
|
+
#
|
16
|
+
# @param id [String] The ID of the attempt.
|
17
|
+
# @return [Hash] The attempt.
|
18
|
+
def retrieve(id)
|
19
|
+
validate_id!(id, 'atm_')
|
20
|
+
get("attempts/#{id}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module Hookdeck
|
2
|
+
module Resources
|
3
|
+
class Base
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
def initialize(client)
|
7
|
+
@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def get(path, params = {})
|
13
|
+
request(:get, path, params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def post(path, params = {})
|
17
|
+
request(:post, path, params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def put(path, params = {})
|
21
|
+
request(:put, path, params)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(path, params = {})
|
25
|
+
request(:delete, path, params)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def request(method, path, params = {}, opts = {})
|
31
|
+
path = ensure_api_version(path)
|
32
|
+
handle_response(
|
33
|
+
client.request(
|
34
|
+
method,
|
35
|
+
path,
|
36
|
+
params,
|
37
|
+
opts
|
38
|
+
)
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def ensure_api_version(path)
|
43
|
+
"/#{client.config.api_version}/#{path.gsub(%r{^/}, '')}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def handle_response(response)
|
47
|
+
return nil if response.nil?
|
48
|
+
|
49
|
+
response
|
50
|
+
rescue StandardError => e
|
51
|
+
handle_error(e)
|
52
|
+
end
|
53
|
+
|
54
|
+
def handle_error(error)
|
55
|
+
case error
|
56
|
+
when Faraday::ClientError
|
57
|
+
case error.response[:status]
|
58
|
+
when 400
|
59
|
+
raise BadRequestError, error.response
|
60
|
+
when 401
|
61
|
+
raise UnauthorizedError, error.response
|
62
|
+
when 403
|
63
|
+
raise ForbiddenError, error.response
|
64
|
+
when 404
|
65
|
+
raise NotFoundError, error.response
|
66
|
+
when 409
|
67
|
+
raise ConflictError, error.response
|
68
|
+
when 422
|
69
|
+
raise UnprocessableEntityError, error.response
|
70
|
+
when 429
|
71
|
+
raise RateLimitError, error.response
|
72
|
+
else
|
73
|
+
raise ApiError, error.response
|
74
|
+
end
|
75
|
+
when Faraday::ServerError
|
76
|
+
raise ServerError, error.response
|
77
|
+
when Faraday::ConnectionFailed
|
78
|
+
raise ConnectionError, error
|
79
|
+
when Faraday::TimeoutError
|
80
|
+
raise TimeoutError, error
|
81
|
+
when Faraday::SSLError
|
82
|
+
raise SSLError, error
|
83
|
+
else
|
84
|
+
raise Error, error.message
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate_id!(id, prefix)
|
89
|
+
return if id.is_a?(String) && id.start_with?(prefix)
|
90
|
+
|
91
|
+
raise ValidationError, "Invalid ID format. Expected #{prefix}*, got: #{id}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate_params!(params, required_keys = [], optional_keys = [])
|
95
|
+
# Check for required keys
|
96
|
+
missing_keys = required_keys - params.keys
|
97
|
+
raise ValidationError, "Missing required parameters: #{missing_keys.join(', ')}" if missing_keys.any?
|
98
|
+
|
99
|
+
# Check for unexpected keys
|
100
|
+
allowed_keys = required_keys + optional_keys
|
101
|
+
unexpected_keys = params.keys - allowed_keys
|
102
|
+
return unless unexpected_keys.any?
|
103
|
+
|
104
|
+
raise ValidationError, "Unexpected parameters: #{unexpected_keys.join(', ')}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def validate_enum!(value, allowed_values, field_name)
|
108
|
+
return if allowed_values.include?(value)
|
109
|
+
|
110
|
+
raise ValidationError, "Invalid #{field_name}. Expected one of: #{allowed_values.join(', ')}, got: #{value}"
|
111
|
+
end
|
112
|
+
|
113
|
+
def validate_array!(value, field_name, allowed_values = nil)
|
114
|
+
raise ValidationError, "#{field_name} must be an array, got: #{value.class}" unless value.is_a?(Array)
|
115
|
+
|
116
|
+
return unless allowed_values && (invalid_values = value - allowed_values).any?
|
117
|
+
|
118
|
+
raise ValidationError, "Invalid #{field_name} values: #{invalid_values.join(', ')}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def validate_timestamp!(value, field_name)
|
122
|
+
Time.parse(value.to_s)
|
123
|
+
rescue ArgumentError
|
124
|
+
raise ValidationError, "Invalid timestamp format for #{field_name}: #{value}"
|
125
|
+
end
|
126
|
+
|
127
|
+
def validate_boolean!(value, field_name)
|
128
|
+
return if [true, false].include?(value)
|
129
|
+
|
130
|
+
raise ValidationError, "#{field_name} must be a boolean, got: #{value}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate_integer!(value, field_name, min: nil, max: nil)
|
134
|
+
raise ValidationError, "#{field_name} must be an integer, got: #{value.class}" unless value.is_a?(Integer)
|
135
|
+
|
136
|
+
raise ValidationError, "#{field_name} must be greater than or equal to #{min}" if min && value < min
|
137
|
+
|
138
|
+
return unless max && value > max
|
139
|
+
|
140
|
+
raise ValidationError, "#{field_name} must be less than or equal to #{max}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def validate_string!(value, field_name, pattern: nil, min_length: nil, max_length: nil)
|
144
|
+
raise ValidationError, "#{field_name} must be a string, got: #{value.class}" unless value.is_a?(String)
|
145
|
+
|
146
|
+
raise ValidationError, "#{field_name} format is invalid" if pattern && !value.match?(pattern)
|
147
|
+
|
148
|
+
if min_length && value.length < min_length
|
149
|
+
raise ValidationError, "#{field_name} must be at least #{min_length} characters"
|
150
|
+
end
|
151
|
+
|
152
|
+
return unless max_length && value.length > max_length
|
153
|
+
|
154
|
+
raise ValidationError, "#{field_name} must be at most #{max_length} characters"
|
155
|
+
end
|
156
|
+
|
157
|
+
def validate_url!(value, field_name)
|
158
|
+
uri = URI.parse(value)
|
159
|
+
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
160
|
+
raise ValidationError,
|
161
|
+
"Invalid URL format for #{field_name}"
|
162
|
+
end
|
163
|
+
rescue URI::InvalidURIError
|
164
|
+
raise ValidationError, "Invalid URL format for #{field_name}"
|
165
|
+
end
|
166
|
+
|
167
|
+
def format_timestamp(value)
|
168
|
+
return nil if value.nil?
|
169
|
+
|
170
|
+
value.is_a?(Time) ? value.iso8601 : value
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|