poodle-ruby 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/.rspec +1 -0
- data/.rubocop.yml +65 -0
- data/.yardopts +12 -0
- data/CODE_OF_CONDUCT.md +78 -0
- data/CONTRIBUTING.md +151 -0
- data/LICENSE +21 -0
- data/README.md +442 -0
- data/Rakefile +4 -0
- data/examples/advanced_usage.rb +255 -0
- data/examples/basic_usage.rb +64 -0
- data/lib/poodle/client.rb +222 -0
- data/lib/poodle/configuration.rb +145 -0
- data/lib/poodle/email.rb +190 -0
- data/lib/poodle/email_response.rb +101 -0
- data/lib/poodle/errors/authentication_error.rb +54 -0
- data/lib/poodle/errors/base_error.rb +49 -0
- data/lib/poodle/errors/forbidden_error.rb +56 -0
- data/lib/poodle/errors/network_error.rb +104 -0
- data/lib/poodle/errors/payment_error.rb +73 -0
- data/lib/poodle/errors/rate_limit_error.rb +146 -0
- data/lib/poodle/errors/server_error.rb +57 -0
- data/lib/poodle/errors/validation_error.rb +93 -0
- data/lib/poodle/http_client.rb +327 -0
- data/lib/poodle/rails/railtie.rb +50 -0
- data/lib/poodle/rails/tasks.rake +113 -0
- data/lib/poodle/rails.rb +158 -0
- data/lib/poodle/test_helpers.rb +244 -0
- data/lib/poodle/version.rb +6 -0
- data/lib/poodle.rb +80 -0
- data/sig/poodle.rbs +4 -0
- metadata +107 -0
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Poodle
|
4
|
+
# Email response model representing the API response for email operations
|
5
|
+
#
|
6
|
+
# @example Checking response status
|
7
|
+
# response = client.send_email(email)
|
8
|
+
# if response.success?
|
9
|
+
# puts "Email sent: #{response.message}"
|
10
|
+
# else
|
11
|
+
# puts "Failed: #{response.message}"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @example Converting to hash
|
15
|
+
# response_data = response.to_h
|
16
|
+
# puts response_data[:success]
|
17
|
+
# puts response_data[:message]
|
18
|
+
class EmailResponse
|
19
|
+
# @return [Boolean] whether the email was successfully queued
|
20
|
+
attr_reader :success
|
21
|
+
|
22
|
+
# @return [String] response message from the API
|
23
|
+
attr_reader :message
|
24
|
+
|
25
|
+
# @return [Hash] additional response data
|
26
|
+
attr_reader :data
|
27
|
+
|
28
|
+
# Initialize a new EmailResponse
|
29
|
+
#
|
30
|
+
# @param success [Boolean] whether the operation was successful
|
31
|
+
# @param message [String] response message
|
32
|
+
# @param data [Hash] additional response data
|
33
|
+
def initialize(success:, message:, data: {})
|
34
|
+
@success = success
|
35
|
+
@message = message
|
36
|
+
@data = data
|
37
|
+
freeze # Make the object immutable
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create an EmailResponse from API response data
|
41
|
+
#
|
42
|
+
# @param response_data [Hash] the API response data
|
43
|
+
# @return [EmailResponse] the email response object
|
44
|
+
def self.from_api_response(response_data)
|
45
|
+
new(
|
46
|
+
success: response_data[:success] || response_data["success"] || false,
|
47
|
+
message: response_data[:message] || response_data["message"] || "",
|
48
|
+
data: response_data
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check if email was successfully queued
|
53
|
+
#
|
54
|
+
# @return [Boolean] true if successful
|
55
|
+
def success?
|
56
|
+
@success
|
57
|
+
end
|
58
|
+
|
59
|
+
# Check if email sending failed
|
60
|
+
#
|
61
|
+
# @return [Boolean] true if failed
|
62
|
+
def failed?
|
63
|
+
!@success
|
64
|
+
end
|
65
|
+
|
66
|
+
# Convert response to hash
|
67
|
+
#
|
68
|
+
# @return [Hash] response data as hash
|
69
|
+
def to_h
|
70
|
+
{
|
71
|
+
success: @success,
|
72
|
+
message: @message,
|
73
|
+
data: @data
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
# Convert response to JSON string
|
78
|
+
#
|
79
|
+
# @return [String] response data as JSON
|
80
|
+
def to_json(*args)
|
81
|
+
require "json"
|
82
|
+
to_h.to_json(*args)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get a string representation of the response
|
86
|
+
#
|
87
|
+
# @return [String] formatted response information
|
88
|
+
def to_s
|
89
|
+
status = @success ? "SUCCESS" : "FAILED"
|
90
|
+
"EmailResponse[#{status}]: #{@message}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# Get detailed string representation for debugging
|
94
|
+
#
|
95
|
+
# @return [String] detailed response information
|
96
|
+
def inspect
|
97
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} " \
|
98
|
+
"success=#{@success} message=#{@message.inspect} data=#{@data.inspect}>"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_error"
|
4
|
+
|
5
|
+
module Poodle
|
6
|
+
# Exception raised when API authentication fails (401 Unauthorized)
|
7
|
+
#
|
8
|
+
# @example Handling authentication errors
|
9
|
+
# begin
|
10
|
+
# client.send_email(email)
|
11
|
+
# rescue Poodle::AuthenticationError => e
|
12
|
+
# puts "Authentication failed: #{e.message}"
|
13
|
+
# puts "Please check your API key"
|
14
|
+
# end
|
15
|
+
class AuthenticationError < Error
|
16
|
+
# Initialize a new AuthenticationError
|
17
|
+
#
|
18
|
+
# @param message [String] the error message
|
19
|
+
# @param context [Hash] additional context information
|
20
|
+
def initialize(message = "Authentication failed", context: {})
|
21
|
+
super(message, context: context, status_code: 401)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create an AuthenticationError for invalid API key
|
25
|
+
#
|
26
|
+
# @return [AuthenticationError] the authentication error
|
27
|
+
def self.invalid_api_key
|
28
|
+
new(
|
29
|
+
"Invalid API key provided. Please check your API key and try again.",
|
30
|
+
context: { error_type: "invalid_api_key" }
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create an AuthenticationError for missing API key
|
35
|
+
#
|
36
|
+
# @return [AuthenticationError] the authentication error
|
37
|
+
def self.missing_api_key
|
38
|
+
new(
|
39
|
+
"API key is required. Please provide a valid API key.",
|
40
|
+
context: { error_type: "missing_api_key" }
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create an AuthenticationError for expired API key
|
45
|
+
#
|
46
|
+
# @return [AuthenticationError] the authentication error
|
47
|
+
def self.expired_api_key
|
48
|
+
new(
|
49
|
+
"API key has expired. Please generate a new API key.",
|
50
|
+
context: { error_type: "expired_api_key" }
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Poodle
|
4
|
+
# Base exception class for all Poodle SDK errors
|
5
|
+
#
|
6
|
+
# @example Catching all Poodle errors
|
7
|
+
# begin
|
8
|
+
# client.send_email(email)
|
9
|
+
# rescue Poodle::Error => e
|
10
|
+
# puts "Poodle error: #{e.message}"
|
11
|
+
# puts "Context: #{e.context}"
|
12
|
+
# end
|
13
|
+
class Error < StandardError
|
14
|
+
# @return [Hash] additional context information about the error
|
15
|
+
attr_reader :context
|
16
|
+
|
17
|
+
# @return [Integer, nil] HTTP status code if available
|
18
|
+
attr_reader :status_code
|
19
|
+
|
20
|
+
# Initialize a new error
|
21
|
+
#
|
22
|
+
# @param message [String] the error message
|
23
|
+
# @param context [Hash] additional context information
|
24
|
+
# @param status_code [Integer, nil] HTTP status code
|
25
|
+
def initialize(message = "", context: {}, status_code: nil)
|
26
|
+
@original_message = message
|
27
|
+
super(message)
|
28
|
+
@context = context
|
29
|
+
@status_code = status_code
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get the original error message without formatting
|
33
|
+
#
|
34
|
+
# @return [String] the original error message
|
35
|
+
def message
|
36
|
+
@original_message
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get a string representation of the error with context
|
40
|
+
#
|
41
|
+
# @return [String] formatted error information
|
42
|
+
def to_s
|
43
|
+
result = @original_message
|
44
|
+
result += " (Status: #{@status_code})" if @status_code
|
45
|
+
result += " Context: #{@context}" unless @context.empty?
|
46
|
+
result
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_error"
|
4
|
+
|
5
|
+
module Poodle
|
6
|
+
# Exception raised when access is forbidden (403 Forbidden)
|
7
|
+
#
|
8
|
+
# @example Handling forbidden errors
|
9
|
+
# begin
|
10
|
+
# client.send_email(email)
|
11
|
+
# rescue Poodle::ForbiddenError => e
|
12
|
+
# puts "Access forbidden: #{e.message}"
|
13
|
+
# puts "Reason: #{e.reason}" if e.reason
|
14
|
+
# end
|
15
|
+
class ForbiddenError < Error
|
16
|
+
# @return [String, nil] reason for the forbidden access
|
17
|
+
attr_reader :reason
|
18
|
+
|
19
|
+
# Initialize a new ForbiddenError
|
20
|
+
#
|
21
|
+
# @param message [String] the error message
|
22
|
+
# @param reason [String, nil] reason for the forbidden access
|
23
|
+
# @param context [Hash] additional context information
|
24
|
+
def initialize(message = "Access forbidden", reason: nil, context: {})
|
25
|
+
@reason = reason
|
26
|
+
super(message, context: context.merge(reason: reason), status_code: 403)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create a ForbiddenError for account suspended
|
30
|
+
#
|
31
|
+
# @param reason [String] the suspension reason
|
32
|
+
# @param rate [Float, nil] the suspension rate if applicable
|
33
|
+
# @return [ForbiddenError] the forbidden error
|
34
|
+
def self.account_suspended(reason, rate = nil)
|
35
|
+
message = "Account suspended: #{reason}"
|
36
|
+
message += " (Rate: #{rate})" if rate
|
37
|
+
|
38
|
+
new(
|
39
|
+
message,
|
40
|
+
reason: reason,
|
41
|
+
context: { error_type: "account_suspended", suspension_rate: rate }
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create a ForbiddenError for insufficient permissions
|
46
|
+
#
|
47
|
+
# @return [ForbiddenError] the forbidden error
|
48
|
+
def self.insufficient_permissions
|
49
|
+
new(
|
50
|
+
"API key does not have sufficient permissions for this operation.",
|
51
|
+
reason: "insufficient_permissions",
|
52
|
+
context: { error_type: "insufficient_permissions" }
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_error"
|
4
|
+
|
5
|
+
module Poodle
|
6
|
+
# Exception raised when network or HTTP errors occur
|
7
|
+
#
|
8
|
+
# @example Handling network errors
|
9
|
+
# begin
|
10
|
+
# client.send_email(email)
|
11
|
+
# rescue Poodle::NetworkError => e
|
12
|
+
# puts "Network error: #{e.message}"
|
13
|
+
# puts "Original error: #{e.original_error}" if e.original_error
|
14
|
+
# end
|
15
|
+
class NetworkError < Error
|
16
|
+
# @return [Exception, nil] the original exception that caused this error
|
17
|
+
attr_reader :original_error
|
18
|
+
|
19
|
+
# Initialize a new NetworkError
|
20
|
+
#
|
21
|
+
# @param message [String] the error message
|
22
|
+
# @param original_error [Exception, nil] the original exception
|
23
|
+
# @param context [Hash] additional context information
|
24
|
+
# @param status_code [Integer, nil] HTTP status code
|
25
|
+
def initialize(message = "Network error occurred", original_error: nil, context: {}, status_code: nil)
|
26
|
+
@original_error = original_error
|
27
|
+
super(message, context: context, status_code: status_code)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a NetworkError for connection timeout
|
31
|
+
#
|
32
|
+
# @param timeout [Integer] the timeout duration
|
33
|
+
# @return [NetworkError] the network error
|
34
|
+
def self.connection_timeout(timeout)
|
35
|
+
new(
|
36
|
+
"Connection timeout after #{timeout} seconds",
|
37
|
+
context: { timeout: timeout, error_type: "connection_timeout" },
|
38
|
+
status_code: 408
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create a NetworkError for connection failure
|
43
|
+
#
|
44
|
+
# @param url [String] the URL that failed to connect
|
45
|
+
# @param original_error [Exception, nil] the original exception
|
46
|
+
# @return [NetworkError] the network error
|
47
|
+
def self.connection_failed(url, original_error: nil)
|
48
|
+
new(
|
49
|
+
"Failed to connect to #{url}",
|
50
|
+
original_error: original_error,
|
51
|
+
context: { url: url, error_type: "connection_failed" }
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Create a NetworkError for DNS resolution failure
|
56
|
+
#
|
57
|
+
# @param host [String] the host that failed to resolve
|
58
|
+
# @return [NetworkError] the network error
|
59
|
+
def self.dns_resolution_failed(host)
|
60
|
+
new(
|
61
|
+
"DNS resolution failed for host: #{host}",
|
62
|
+
context: { host: host, error_type: "dns_resolution_failed" }
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Create a NetworkError for SSL/TLS errors
|
67
|
+
#
|
68
|
+
# @param message [String] the SSL error message
|
69
|
+
# @return [NetworkError] the network error
|
70
|
+
def self.ssl_error(message)
|
71
|
+
new(
|
72
|
+
"SSL/TLS error: #{message}",
|
73
|
+
context: { error_type: "ssl_error" }
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create a NetworkError for HTTP errors
|
78
|
+
#
|
79
|
+
# @param status_code [Integer] the HTTP status code
|
80
|
+
# @param message [String] the error message
|
81
|
+
# @return [NetworkError] the network error
|
82
|
+
def self.http_error(status_code, message = "")
|
83
|
+
default_message = "HTTP error occurred with status code: #{status_code}"
|
84
|
+
final_message = message.empty? ? default_message : message
|
85
|
+
|
86
|
+
new(
|
87
|
+
final_message,
|
88
|
+
context: { error_type: "http_error" },
|
89
|
+
status_code: status_code
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Create a NetworkError for malformed response
|
94
|
+
#
|
95
|
+
# @param response [String] the malformed response
|
96
|
+
# @return [NetworkError] the network error
|
97
|
+
def self.malformed_response(response = "")
|
98
|
+
new(
|
99
|
+
"Received malformed response from server",
|
100
|
+
context: { response: response, error_type: "malformed_response" }
|
101
|
+
)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_error"
|
4
|
+
|
5
|
+
module Poodle
|
6
|
+
# Exception raised when payment is required (402 Payment Required)
|
7
|
+
#
|
8
|
+
# @example Handling payment errors
|
9
|
+
# begin
|
10
|
+
# client.send_email(email)
|
11
|
+
# rescue Poodle::PaymentError => e
|
12
|
+
# puts "Payment required: #{e.message}"
|
13
|
+
# puts "Upgrade URL: #{e.upgrade_url}" if e.upgrade_url
|
14
|
+
# end
|
15
|
+
class PaymentError < Error
|
16
|
+
# @return [String, nil] URL to upgrade subscription
|
17
|
+
attr_reader :upgrade_url
|
18
|
+
|
19
|
+
# Initialize a new PaymentError
|
20
|
+
#
|
21
|
+
# @param message [String] the error message
|
22
|
+
# @param upgrade_url [String, nil] URL to upgrade subscription
|
23
|
+
# @param context [Hash] additional context information
|
24
|
+
def initialize(message = "Payment required", upgrade_url: nil, context: {})
|
25
|
+
@upgrade_url = upgrade_url
|
26
|
+
super(message, context: context.merge(upgrade_url: upgrade_url), status_code: 402)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create a PaymentError for subscription expired
|
30
|
+
#
|
31
|
+
# @return [PaymentError] the payment error
|
32
|
+
def self.subscription_expired
|
33
|
+
new(
|
34
|
+
"Subscription expired. Please renew your subscription to continue sending emails.",
|
35
|
+
upgrade_url: "https://app.usepoodle.com/upgrade",
|
36
|
+
context: { error_type: "subscription_expired" }
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a PaymentError for trial limit reached
|
41
|
+
#
|
42
|
+
# @return [PaymentError] the payment error
|
43
|
+
def self.trial_limit_reached
|
44
|
+
new(
|
45
|
+
"Trial limit reached. Please upgrade to a paid plan to continue sending emails.",
|
46
|
+
upgrade_url: "https://app.usepoodle.com/upgrade",
|
47
|
+
context: { error_type: "trial_limit_reached" }
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create a PaymentError for monthly limit reached
|
52
|
+
#
|
53
|
+
# @return [PaymentError] the payment error
|
54
|
+
def self.monthly_limit_reached
|
55
|
+
new(
|
56
|
+
"Monthly email limit reached. Please upgrade your plan to send more emails.",
|
57
|
+
upgrade_url: "https://app.usepoodle.com/upgrade",
|
58
|
+
context: { error_type: "monthly_limit_reached" }
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Create a PaymentError for monthly limit exceeded (alias for monthly_limit_reached)
|
63
|
+
#
|
64
|
+
# @return [PaymentError] the payment error
|
65
|
+
def self.monthly_limit_exceeded
|
66
|
+
new(
|
67
|
+
"Monthly email limit exceeded. Please upgrade your plan to send more emails.",
|
68
|
+
upgrade_url: "https://app.usepoodle.com/upgrade",
|
69
|
+
context: { error_type: "monthly_limit_exceeded" }
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_error"
|
4
|
+
|
5
|
+
module Poodle
|
6
|
+
# Exception raised when API rate limits are exceeded (429 Too Many Requests)
|
7
|
+
#
|
8
|
+
# @example Handling rate limit errors
|
9
|
+
# begin
|
10
|
+
# client.send_email(email)
|
11
|
+
# rescue Poodle::RateLimitError => e
|
12
|
+
# puts "Rate limit exceeded: #{e.message}"
|
13
|
+
# puts "Retry after: #{e.retry_after} seconds" if e.retry_after
|
14
|
+
# puts "Limit: #{e.limit}, Remaining: #{e.remaining}"
|
15
|
+
# end
|
16
|
+
class RateLimitError < Error
|
17
|
+
# @return [Integer, nil] seconds to wait before retrying
|
18
|
+
attr_reader :retry_after
|
19
|
+
|
20
|
+
# @return [Integer, nil] the rate limit
|
21
|
+
attr_reader :limit
|
22
|
+
|
23
|
+
# @return [Integer, nil] remaining requests
|
24
|
+
attr_reader :remaining
|
25
|
+
|
26
|
+
# @return [Integer, nil] time when the rate limit resets
|
27
|
+
attr_reader :reset_time
|
28
|
+
|
29
|
+
# Initialize a new RateLimitError
|
30
|
+
#
|
31
|
+
# @param message [String] the error message
|
32
|
+
# @param retry_after [Integer, nil] seconds to wait before retrying
|
33
|
+
# @param limit [Integer, nil] the rate limit
|
34
|
+
# @param remaining [Integer, nil] remaining requests
|
35
|
+
# @param reset_time [Integer, nil] time when the rate limit resets
|
36
|
+
# @param context [Hash] additional context information
|
37
|
+
def initialize(message = "Rate limit exceeded", **options)
|
38
|
+
@retry_after = options[:retry_after]
|
39
|
+
@limit = options[:limit]
|
40
|
+
@remaining = options[:remaining]
|
41
|
+
@reset_time = options[:reset_time]
|
42
|
+
context = options.fetch(:context, {})
|
43
|
+
|
44
|
+
rate_context = {
|
45
|
+
error_type: "rate_limit_exceeded",
|
46
|
+
retry_after: @retry_after,
|
47
|
+
limit: @limit,
|
48
|
+
remaining: @remaining,
|
49
|
+
reset_time: @reset_time
|
50
|
+
}.compact
|
51
|
+
|
52
|
+
super(message, context: context.merge(rate_context), status_code: 429)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Create a RateLimitError from response headers
|
56
|
+
#
|
57
|
+
# @param headers [Hash] HTTP response headers
|
58
|
+
# @return [RateLimitError] the rate limit error
|
59
|
+
def self.from_headers(headers)
|
60
|
+
retry_after = extract_retry_after(headers)
|
61
|
+
limit = extract_limit(headers)
|
62
|
+
remaining = extract_remaining(headers)
|
63
|
+
reset_time = extract_reset_time(headers)
|
64
|
+
|
65
|
+
message = build_message(retry_after)
|
66
|
+
context = build_context(limit, remaining, reset_time)
|
67
|
+
|
68
|
+
new(
|
69
|
+
message,
|
70
|
+
retry_after: retry_after,
|
71
|
+
limit: limit&.to_i,
|
72
|
+
remaining: remaining&.to_i,
|
73
|
+
reset_time: reset_time&.to_i,
|
74
|
+
context: context
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Extract retry-after value from headers
|
79
|
+
#
|
80
|
+
# @param headers [Hash] HTTP response headers
|
81
|
+
# @return [Integer, nil] retry after seconds
|
82
|
+
def self.extract_retry_after(headers)
|
83
|
+
headers["retry-after"]&.to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
# Extract rate limit from headers
|
87
|
+
#
|
88
|
+
# @param headers [Hash] HTTP response headers
|
89
|
+
# @return [String, nil] rate limit value
|
90
|
+
def self.extract_limit(headers)
|
91
|
+
headers["X-RateLimit-Limit"] || headers["ratelimit-limit"]
|
92
|
+
end
|
93
|
+
|
94
|
+
# Extract remaining requests from headers
|
95
|
+
#
|
96
|
+
# @param headers [Hash] HTTP response headers
|
97
|
+
# @return [String, nil] remaining requests value
|
98
|
+
def self.extract_remaining(headers)
|
99
|
+
headers["X-RateLimit-Remaining"] || headers["ratelimit-remaining"]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Extract reset time from headers
|
103
|
+
#
|
104
|
+
# @param headers [Hash] HTTP response headers
|
105
|
+
# @return [String, nil] reset time value
|
106
|
+
def self.extract_reset_time(headers)
|
107
|
+
headers["X-RateLimit-Reset"] || headers["ratelimit-reset"]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Build error message
|
111
|
+
#
|
112
|
+
# @param retry_after [Integer, nil] retry after seconds
|
113
|
+
# @return [String] error message
|
114
|
+
def self.build_message(retry_after)
|
115
|
+
message = "Rate limit exceeded."
|
116
|
+
message += " Retry after #{retry_after} seconds." if retry_after
|
117
|
+
message
|
118
|
+
end
|
119
|
+
|
120
|
+
# Build context hash
|
121
|
+
#
|
122
|
+
# @param limit [String, nil] rate limit
|
123
|
+
# @param remaining [String, nil] remaining requests
|
124
|
+
# @param reset_time [String, nil] reset time
|
125
|
+
# @return [Hash] context hash
|
126
|
+
def self.build_context(limit, remaining, reset_time)
|
127
|
+
{
|
128
|
+
limit: limit,
|
129
|
+
remaining: remaining,
|
130
|
+
reset_at: reset_time
|
131
|
+
}.compact
|
132
|
+
end
|
133
|
+
|
134
|
+
private_class_method :extract_retry_after, :extract_limit, :extract_remaining,
|
135
|
+
:extract_reset_time, :build_message, :build_context
|
136
|
+
|
137
|
+
# Get the time when the rate limit resets as a Time object
|
138
|
+
#
|
139
|
+
# @return [Time, nil] the reset time
|
140
|
+
def reset_at
|
141
|
+
return nil unless @reset_time
|
142
|
+
|
143
|
+
Time.at(@reset_time)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_error"
|
4
|
+
|
5
|
+
module Poodle
|
6
|
+
# Exception raised when server errors occur (5xx status codes)
|
7
|
+
#
|
8
|
+
# @example Handling server errors
|
9
|
+
# begin
|
10
|
+
# client.send_email(email)
|
11
|
+
# rescue Poodle::ServerError => e
|
12
|
+
# puts "Server error: #{e.message}"
|
13
|
+
# puts "Status code: #{e.status_code}"
|
14
|
+
# end
|
15
|
+
class ServerError < Error
|
16
|
+
# Initialize a new ServerError
|
17
|
+
#
|
18
|
+
# @param message [String] the error message
|
19
|
+
# @param context [Hash] additional context information
|
20
|
+
# @param status_code [Integer] HTTP status code (5xx)
|
21
|
+
def initialize(message = "Server error occurred", context: {}, status_code: 500)
|
22
|
+
super(message, context: context.merge(error_type: "server_error"), status_code: status_code)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create a ServerError for internal server error
|
26
|
+
#
|
27
|
+
# @param message [String] the error message
|
28
|
+
# @return [ServerError] the server error
|
29
|
+
def self.internal_server_error(message = "Internal server error")
|
30
|
+
new(message, status_code: 500)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create a ServerError for bad gateway
|
34
|
+
#
|
35
|
+
# @param message [String] the error message
|
36
|
+
# @return [ServerError] the server error
|
37
|
+
def self.bad_gateway(message = "Bad gateway")
|
38
|
+
new(message, status_code: 502)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create a ServerError for service unavailable
|
42
|
+
#
|
43
|
+
# @param message [String] the error message
|
44
|
+
# @return [ServerError] the server error
|
45
|
+
def self.service_unavailable(message = "Service unavailable")
|
46
|
+
new(message, status_code: 503)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Create a ServerError for gateway timeout
|
50
|
+
#
|
51
|
+
# @param message [String] the error message
|
52
|
+
# @return [ServerError] the server error
|
53
|
+
def self.gateway_timeout(message = "Gateway timeout")
|
54
|
+
new(message, status_code: 504)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|