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.
@@ -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