io-complyance-unify-sdk 3.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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +595 -0
  4. data/lib/complyance/circuit_breaker.rb +99 -0
  5. data/lib/complyance/persistent_queue_manager.rb +474 -0
  6. data/lib/complyance/retry_strategy.rb +198 -0
  7. data/lib/complyance_sdk/config/retry_config.rb +127 -0
  8. data/lib/complyance_sdk/config/sdk_config.rb +212 -0
  9. data/lib/complyance_sdk/exceptions/circuit_breaker_open_error.rb +14 -0
  10. data/lib/complyance_sdk/exceptions/sdk_exception.rb +93 -0
  11. data/lib/complyance_sdk/generators/config_generator.rb +67 -0
  12. data/lib/complyance_sdk/generators/install_generator.rb +22 -0
  13. data/lib/complyance_sdk/generators/templates/complyance_initializer.rb +36 -0
  14. data/lib/complyance_sdk/http/authentication_middleware.rb +43 -0
  15. data/lib/complyance_sdk/http/client.rb +223 -0
  16. data/lib/complyance_sdk/http/logging_middleware.rb +153 -0
  17. data/lib/complyance_sdk/jobs/base_job.rb +63 -0
  18. data/lib/complyance_sdk/jobs/process_document_job.rb +92 -0
  19. data/lib/complyance_sdk/jobs/sidekiq_job.rb +165 -0
  20. data/lib/complyance_sdk/middleware/rack_middleware.rb +39 -0
  21. data/lib/complyance_sdk/models/country.rb +205 -0
  22. data/lib/complyance_sdk/models/country_policy_registry.rb +159 -0
  23. data/lib/complyance_sdk/models/document_type.rb +52 -0
  24. data/lib/complyance_sdk/models/environment.rb +144 -0
  25. data/lib/complyance_sdk/models/logical_doc_type.rb +228 -0
  26. data/lib/complyance_sdk/models/mode.rb +47 -0
  27. data/lib/complyance_sdk/models/operation.rb +47 -0
  28. data/lib/complyance_sdk/models/policy_result.rb +145 -0
  29. data/lib/complyance_sdk/models/purpose.rb +52 -0
  30. data/lib/complyance_sdk/models/source.rb +104 -0
  31. data/lib/complyance_sdk/models/source_ref.rb +130 -0
  32. data/lib/complyance_sdk/models/unify_request.rb +208 -0
  33. data/lib/complyance_sdk/models/unify_response.rb +198 -0
  34. data/lib/complyance_sdk/queue/persistent_queue_manager.rb +609 -0
  35. data/lib/complyance_sdk/railtie.rb +29 -0
  36. data/lib/complyance_sdk/retry/circuit_breaker.rb +159 -0
  37. data/lib/complyance_sdk/retry/retry_manager.rb +108 -0
  38. data/lib/complyance_sdk/retry/retry_strategy.rb +225 -0
  39. data/lib/complyance_sdk/version.rb +5 -0
  40. data/lib/complyance_sdk.rb +935 -0
  41. metadata +322 -0
@@ -0,0 +1,198 @@
1
+ require 'logger'
2
+ require_relative 'circuit_breaker'
3
+ require_relative 'error_detail'
4
+ require_relative 'sdk_exception'
5
+
6
+ module Complyance
7
+ # Advanced retry strategy with exponential backoff, jitter, and circuit breaker
8
+ class RetryStrategy
9
+ # Initialize retry strategy
10
+ # @param config [Hash] Retry configuration
11
+ # @option config [Integer] :max_attempts Maximum number of retry attempts
12
+ # @option config [Integer] :base_delay Base delay in milliseconds
13
+ # @option config [Integer] :max_delay Maximum delay in milliseconds
14
+ # @option config [Float] :backoff_multiplier Multiplier for exponential backoff
15
+ # @option config [Float] :jitter_factor Random jitter factor (0-1)
16
+ # @option config [Array<String>] :retryable_errors List of error codes to retry
17
+ # @option config [Array<Integer>] :retryable_http_codes List of HTTP status codes to retry
18
+ # @option config [Boolean] :circuit_breaker_enabled Whether to use circuit breaker
19
+ # @option config [Hash] :circuit_breaker_config Circuit breaker configuration
20
+ def initialize(config = {})
21
+ @config = {
22
+ max_attempts: 3,
23
+ base_delay: 1000,
24
+ max_delay: 30000,
25
+ backoff_multiplier: 2.0,
26
+ jitter_factor: 0.2,
27
+ retryable_errors: ['RATE_LIMIT_EXCEEDED', 'SERVICE_UNAVAILABLE'],
28
+ retryable_http_codes: [408, 429, 500, 502, 503, 504],
29
+ circuit_breaker_enabled: true,
30
+ circuit_breaker_config: {
31
+ failure_threshold: 3,
32
+ reset_timeout: 60
33
+ }
34
+ }.merge(config)
35
+
36
+ @logger = Logger.new(STDOUT)
37
+ @logger.level = Logger::INFO
38
+
39
+ if @config[:circuit_breaker_enabled]
40
+ @circuit_breaker = CircuitBreaker.new(@config[:circuit_breaker_config])
41
+ end
42
+ end
43
+
44
+ # Execute a function with retry logic
45
+ # @param operation [Proc] Function to execute
46
+ # @param operation_name [String] Name of operation for logging
47
+ # @return [Object] Result of operation
48
+ # @raise [SDKException] If all retries fail
49
+ def execute(operation_name, &operation)
50
+ attempt = 1
51
+ last_exception = nil
52
+
53
+ while attempt <= @config[:max_attempts]
54
+ begin
55
+ @logger.debug "Attempting operation '#{operation_name}' (attempt #{attempt}/#{@config[:max_attempts]})"
56
+
57
+ # Use circuit breaker if enabled
58
+ result = if @circuit_breaker
59
+ begin
60
+ @circuit_breaker.execute(&operation)
61
+ rescue RuntimeError => e
62
+ if e.message.include?('Circuit breaker is open')
63
+ raise SDKException.new(ErrorDetail.new(
64
+ 'CIRCUIT_BREAKER_OPEN',
65
+ e.message
66
+ ))
67
+ end
68
+ raise e
69
+ end
70
+ else
71
+ operation.call
72
+ end
73
+
74
+ if attempt > 1
75
+ @logger.info "Operation '#{operation_name}' succeeded on attempt #{attempt}"
76
+ end
77
+
78
+ return result
79
+
80
+ rescue SDKException => e
81
+ last_exception = e
82
+
83
+ # Check if this error should be retried
84
+ unless should_retry?(e, attempt)
85
+ @logger.debug "Operation '#{operation_name}' failed with non-retryable error: #{e.message}"
86
+ raise e
87
+ end
88
+
89
+ # If this was the last attempt, don't wait
90
+ break if attempt >= @config[:max_attempts]
91
+
92
+ # Calculate delay and wait
93
+ delay = calculate_delay(attempt)
94
+ @logger.warn "Operation '#{operation_name}' failed on attempt #{attempt} (#{e.message}), retrying in #{delay}ms"
95
+
96
+ sleep(delay / 1000.0) # Convert ms to seconds
97
+
98
+ rescue StandardError => e
99
+ @logger.error "Unexpected error in operation '#{operation_name}': #{e.message}"
100
+
101
+ error = ErrorDetail.new(
102
+ 'PROCESSING_ERROR',
103
+ "Unexpected error: #{e.message}"
104
+ )
105
+ error.suggestion = "This appears to be an unexpected error. Please contact support if it persists"
106
+ error.add_context_value('originalException', e.class.name)
107
+ raise SDKException.new(error)
108
+ end
109
+
110
+ attempt += 1
111
+ end
112
+
113
+ # All retries exhausted
114
+ @logger.error "Operation '#{operation_name}' failed after #{@config[:max_attempts]} attempts"
115
+ raise last_exception || SDKException.new(ErrorDetail.new(
116
+ 'MAX_RETRIES_EXCEEDED',
117
+ "Maximum retry attempts (#{@config[:max_attempts]}) exceeded"
118
+ ))
119
+ end
120
+
121
+ # Get the current circuit breaker state (for monitoring)
122
+ # @return [String, nil] Current circuit breaker state
123
+ def circuit_breaker_state
124
+ @circuit_breaker&.state
125
+ end
126
+
127
+ # Get circuit breaker statistics (for monitoring)
128
+ # @return [String] Circuit breaker stats
129
+ def circuit_breaker_stats
130
+ if @circuit_breaker
131
+ "CircuitBreaker{state=#{@circuit_breaker.state}, failures=#{@circuit_breaker.failure_count}, " \
132
+ "lastFailure=#{@circuit_breaker.last_failure_time}}"
133
+ else
134
+ "Circuit breaker disabled"
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ # Determine if an error should be retried
141
+ # @param e [SDKException] Exception to check
142
+ # @param attempt [Integer] Current attempt number
143
+ # @return [Boolean] True if error should be retried
144
+ def should_retry?(e, attempt)
145
+ return false if attempt >= @config[:max_attempts]
146
+
147
+ detail = e.error_detail
148
+ return false unless detail
149
+
150
+ # If circuit breaker is open, check if we should wait for timeout
151
+ if detail.code == 'CIRCUIT_BREAKER_OPEN'
152
+ if e.message =~ /(\d+) seconds remaining/
153
+ remaining_seconds = $1.to_i
154
+ delay = remaining_seconds * 1000
155
+ @logger.info "Circuit breaker is open - waiting for #{remaining_seconds} seconds before retrying"
156
+ sleep(delay / 1000.0) # Convert ms to seconds
157
+ return true
158
+ end
159
+ return false
160
+ end
161
+
162
+ # Check if explicitly marked as retryable
163
+ return true if detail.retryable?
164
+
165
+ # Check if error code is in retryable list
166
+ return true if @config[:retryable_errors].include?(detail.code)
167
+
168
+ # Check if HTTP status is retryable
169
+ http_status = detail.context_value('httpStatus')
170
+ if http_status
171
+ status_code = http_status.to_i
172
+ return true if @config[:retryable_http_codes].include?(status_code)
173
+ end
174
+
175
+ false
176
+ end
177
+
178
+ # Calculate delay for next retry with exponential backoff and jitter
179
+ # @param attempt [Integer] Current attempt number
180
+ # @return [Integer] Delay in milliseconds
181
+ def calculate_delay(attempt)
182
+ # Start with base delay and apply exponential backoff
183
+ delay_ms = @config[:base_delay] * (@config[:backoff_multiplier] ** (attempt - 1))
184
+
185
+ # Apply jitter to avoid thundering herd
186
+ if @config[:jitter_factor] > 0
187
+ jitter = (rand * 2 - 1) * @config[:jitter_factor]
188
+ delay_ms = delay_ms * (1 + jitter)
189
+ end
190
+
191
+ # Cap at max delay
192
+ delay_ms = [delay_ms, @config[:max_delay]].min
193
+
194
+ # Ensure minimum delay
195
+ [delay_ms, 0].max.to_i
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module Config
5
+ # Configuration for retry behavior
6
+ class RetryConfig
7
+ # Maximum number of retry attempts
8
+ attr_accessor :max_attempts
9
+
10
+ # Base delay between retries in seconds
11
+ attr_accessor :base_delay
12
+
13
+ # Maximum delay between retries in seconds
14
+ attr_accessor :max_delay
15
+
16
+ # Backoff multiplier for exponential backoff
17
+ attr_accessor :backoff_multiplier
18
+
19
+ # Jitter factor to add randomness to retry delays
20
+ attr_accessor :jitter_factor
21
+
22
+ # Whether circuit breaker is enabled
23
+ attr_accessor :circuit_breaker_enabled
24
+
25
+ # Failure threshold for circuit breaker
26
+ attr_accessor :failure_threshold
27
+
28
+ # Circuit breaker timeout in seconds
29
+ attr_accessor :circuit_breaker_timeout
30
+
31
+ # HTTP status codes that should trigger a retry
32
+ attr_accessor :retryable_http_codes
33
+
34
+ # Error codes that should trigger a retry
35
+ attr_accessor :retryable_error_codes
36
+
37
+ # Initialize a new retry configuration
38
+ #
39
+ # @param options [Hash] Configuration options
40
+ # @option options [Integer] :max_attempts Maximum number of retry attempts
41
+ # @option options [Float] :base_delay Base delay between retries in seconds
42
+ # @option options [Float] :max_delay Maximum delay between retries in seconds
43
+ # @option options [Float] :backoff_multiplier Backoff multiplier for exponential backoff
44
+ # @option options [Float] :jitter_factor Jitter factor to add randomness to retry delays
45
+ # @option options [Boolean] :circuit_breaker_enabled Whether circuit breaker is enabled
46
+ # @option options [Integer] :failure_threshold Failure threshold for circuit breaker
47
+ # @option options [Float] :circuit_breaker_timeout Circuit breaker timeout in seconds
48
+ # @option options [Array<Integer>] :retryable_http_codes HTTP status codes that should trigger a retry
49
+ def initialize(options = {})
50
+ @max_attempts = options.fetch(:max_attempts, 3)
51
+ @base_delay = options.fetch(:base_delay, 1000) # Base delay in milliseconds to match Java
52
+ @max_delay = options.fetch(:max_delay, 30000) # Max delay in milliseconds to match Java
53
+ @backoff_multiplier = options.fetch(:backoff_multiplier, 2.0)
54
+ @jitter_factor = options.fetch(:jitter_factor, 0.2)
55
+ @circuit_breaker_enabled = options.fetch(:circuit_breaker_enabled, true)
56
+ @failure_threshold = options.fetch(:failure_threshold, 5)
57
+ @circuit_breaker_timeout = options.fetch(:circuit_breaker_timeout, 60.0)
58
+ @retryable_http_codes = options.fetch(:retryable_http_codes, [408, 429, 500, 502, 503, 504])
59
+ @retryable_error_codes = options.fetch(:retryable_error_codes, [:network_error, :timeout_error, :rate_limited, :temporary_error])
60
+ end
61
+
62
+ # Create a default retry configuration
63
+ #
64
+ # @return [RetryConfig] The default retry configuration
65
+ def self.default
66
+ new
67
+ end
68
+
69
+ # Create an aggressive retry configuration
70
+ #
71
+ # @return [RetryConfig] An aggressive retry configuration
72
+ def self.aggressive
73
+ new(
74
+ max_attempts: 7,
75
+ base_delay: 0.2,
76
+ max_delay: 10.0,
77
+ backoff_multiplier: 1.5,
78
+ jitter_factor: 0.1,
79
+ circuit_breaker_timeout: 30.0
80
+ )
81
+ end
82
+
83
+ # Create a conservative retry configuration
84
+ #
85
+ # @return [RetryConfig] A conservative retry configuration
86
+ def self.conservative
87
+ new(
88
+ max_attempts: 3,
89
+ base_delay: 2.0,
90
+ max_delay: 60.0,
91
+ backoff_multiplier: 3.0,
92
+ jitter_factor: 0.3,
93
+ failure_threshold: 3,
94
+ circuit_breaker_timeout: 120.0
95
+ )
96
+ end
97
+
98
+ # Create a retry configuration with no retries
99
+ #
100
+ # @return [RetryConfig] A retry configuration with no retries
101
+ def self.none
102
+ new(
103
+ max_attempts: 1,
104
+ circuit_breaker_enabled: false
105
+ )
106
+ end
107
+
108
+ # Get circuit breaker configuration
109
+ #
110
+ # @return [Hash] Circuit breaker configuration hash
111
+ def circuit_breaker_config
112
+ {
113
+ failure_threshold: @failure_threshold,
114
+ timeout_seconds: @circuit_breaker_timeout.to_i,
115
+ success_threshold: 1
116
+ }
117
+ end
118
+
119
+ # Check if circuit breaker is enabled
120
+ #
121
+ # @return [Boolean] True if circuit breaker is enabled
122
+ def circuit_breaker_enabled?
123
+ @circuit_breaker_enabled
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module ComplyanceSDK
6
+ module Config
7
+ # Configuration class for the Complyance SDK
8
+ class SDKConfig
9
+
10
+ # API key for authentication
11
+ attr_accessor :api_key
12
+
13
+ # Environment (sandbox, production)
14
+ attr_accessor :environment
15
+
16
+ # Array of sources
17
+ attr_accessor :sources
18
+
19
+ # Retry configuration
20
+ attr_accessor :retry_config
21
+
22
+ # Logging configuration
23
+ attr_accessor :logging_enabled
24
+
25
+ # Log level
26
+ attr_accessor :log_level
27
+
28
+ # Auto-generate tax destination flag
29
+ attr_accessor :auto_generate_tax_destination
30
+
31
+ # Correlation ID for request tracking
32
+ attr_accessor :correlation_id
33
+
34
+ # Initialize a new configuration
35
+ #
36
+ # @param options [Hash] Configuration options
37
+ # @option options [String] :api_key API key for authentication
38
+ # @option options [String, Symbol] :environment Environment (sandbox, production)
39
+ # @option options [Array<ComplyanceSDK::Models::Source>] :sources Array of sources
40
+ # @option options [ComplyanceSDK::Config::RetryConfig] :retry_config Retry configuration
41
+ # @option options [Boolean] :logging_enabled Whether logging is enabled
42
+ # @option options [Symbol] :log_level Log level (:debug, :info, :warn, :error)
43
+ def initialize(options = {})
44
+ @api_key = options[:api_key]
45
+ @environment = parse_environment(options[:environment])
46
+ @sources = options[:sources] || []
47
+ @retry_config = options[:retry_config] || RetryConfig.default
48
+ @logging_enabled = options.fetch(:logging_enabled, true)
49
+ @log_level = options.fetch(:log_level, :info)
50
+ @auto_generate_tax_destination = options.fetch(:auto_generate_tax_destination, true)
51
+ @correlation_id = options[:correlation_id]
52
+ @errors = {}
53
+ end
54
+
55
+ # Check if the configuration is valid
56
+ #
57
+ # @return [Boolean] True if valid, false otherwise
58
+ def valid?
59
+ @errors = {}
60
+ validate
61
+ @errors.empty?
62
+ end
63
+
64
+ # Get validation errors
65
+ #
66
+ # @return [Hash] Hash of validation errors
67
+ def errors
68
+ @errors ||= {}
69
+ end
70
+
71
+ # Create a configuration from environment variables
72
+ #
73
+ # @return [SDKConfig] The configuration object
74
+ def self.from_env
75
+ new(
76
+ api_key: ENV["COMPLYANCE_API_KEY"],
77
+ environment: ENV["COMPLYANCE_ENVIRONMENT"],
78
+ logging_enabled: ENV.fetch("COMPLYANCE_LOGGING_ENABLED", "true") == "true",
79
+ log_level: ENV.fetch("COMPLYANCE_LOG_LEVEL", "info").to_sym
80
+ )
81
+ end
82
+
83
+ # Create a configuration from Rails credentials
84
+ #
85
+ # @param environment [Symbol] The Rails environment (:development, :production, etc.)
86
+ # @return [SDKConfig] The configuration object
87
+ def self.from_rails(environment = nil)
88
+ return unless defined?(Rails)
89
+
90
+ env = environment || Rails.env.to_sym
91
+ credentials = Rails.application.credentials
92
+
93
+ config = if credentials.dig(:complyance, env)
94
+ credentials.dig(:complyance, env)
95
+ else
96
+ credentials.dig(:complyance) || {}
97
+ end
98
+
99
+ new(
100
+ api_key: config[:api_key],
101
+ environment: config[:environment],
102
+ logging_enabled: config.fetch(:logging_enabled, true),
103
+ log_level: config.fetch(:log_level, :info).to_sym
104
+ )
105
+ end
106
+
107
+ # Create a configuration from a YAML file
108
+ #
109
+ # @param path [String] Path to the YAML file
110
+ # @return [SDKConfig] The configuration object
111
+ def self.from_file(path)
112
+ require "erb"
113
+
114
+ # Process ERB templates in YAML files
115
+ erb_content = ERB.new(File.read(path)).result
116
+ config = YAML.safe_load(erb_content, symbolize_names: true)
117
+
118
+ # Handle sources array
119
+ if config[:sources].is_a?(Array)
120
+ config[:sources] = config[:sources].map do |source_config|
121
+ ComplyanceSDK::Models::Source.new(source_config)
122
+ end
123
+ end
124
+
125
+ # Handle retry_config hash
126
+ if config[:retry_config].is_a?(Hash)
127
+ config[:retry_config] = RetryConfig.new(config[:retry_config])
128
+ end
129
+
130
+ # Convert log_level to symbol if it's a string
131
+ if config[:log_level].is_a?(String)
132
+ config[:log_level] = config[:log_level].to_sym
133
+ end
134
+
135
+ new(config)
136
+ end
137
+
138
+ # Add a source to the configuration
139
+ #
140
+ # @param source [ComplyanceSDK::Models::Source] The source to add
141
+ # @return [Array<ComplyanceSDK::Models::Source>] The updated sources array
142
+ def add_source(source)
143
+ @sources << source
144
+ @sources
145
+ end
146
+
147
+ # Check if auto-generate tax destination is enabled
148
+ #
149
+ # @return [Boolean] True if auto-generate is enabled
150
+ def auto_generate_tax_destination?
151
+ @auto_generate_tax_destination
152
+ end
153
+
154
+ private
155
+
156
+ def parse_environment(env)
157
+ return nil if env.nil?
158
+
159
+ case env.to_s.downcase
160
+ when "production", "prod"
161
+ ComplyanceSDK::Models::Environment::PRODUCTION
162
+ when "sandbox", "test"
163
+ ComplyanceSDK::Models::Environment::SANDBOX
164
+ when "local", "development", "dev"
165
+ ComplyanceSDK::Models::Environment::LOCAL
166
+ else
167
+ # Return the original value for validation error
168
+ env.is_a?(Symbol) ? env : env.to_sym
169
+ end
170
+ end
171
+
172
+ def validate
173
+ validate_api_key
174
+ validate_environment
175
+ validate_sources
176
+ end
177
+
178
+ def validate_api_key
179
+ if @api_key.nil? || @api_key.empty?
180
+ add_error(:api_key, "can't be blank")
181
+ end
182
+ end
183
+
184
+ def validate_environment
185
+ if @environment.nil?
186
+ add_error(:environment, "can't be blank")
187
+ elsif ![
188
+ ComplyanceSDK::Models::Environment::PRODUCTION,
189
+ ComplyanceSDK::Models::Environment::SANDBOX,
190
+ ComplyanceSDK::Models::Environment::LOCAL
191
+ ].include?(@environment)
192
+ add_error(:environment, "must be a valid environment")
193
+ end
194
+ end
195
+
196
+ def validate_sources
197
+ return if @sources.nil? || @sources.empty?
198
+
199
+ @sources.each_with_index do |source, index|
200
+ unless source.is_a?(ComplyanceSDK::Models::Source)
201
+ add_error(:sources, "item at index #{index} is not a valid Source")
202
+ end
203
+ end
204
+ end
205
+
206
+ def add_error(field, message)
207
+ @errors[field] ||= []
208
+ @errors[field] << message
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sdk_exception'
4
+
5
+ module ComplyanceSDK
6
+ module Exceptions
7
+ # Exception raised when circuit breaker is open
8
+ class CircuitBreakerOpenError < SDKException
9
+ def initialize(message = "Circuit breaker is open", **kwargs)
10
+ super(message, context: { code: :circuit_breaker_open, **kwargs })
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ComplyanceSDK
4
+ module Exceptions
5
+ # Base exception class for all SDK exceptions
6
+ class SDKException < StandardError
7
+ # Error code
8
+ attr_reader :code
9
+
10
+ # Error context
11
+ attr_reader :context
12
+
13
+ # Suggested action to resolve the error
14
+ attr_reader :suggestion
15
+
16
+ # Initialize a new SDK exception
17
+ #
18
+ # @param message [String] Error message
19
+ # @param code [Symbol] Error code
20
+ # @param context [Hash] Error context
21
+ # @param suggestion [String] Suggested action to resolve the error
22
+ def initialize(message, code: nil, context: {}, suggestion: nil)
23
+ super(message)
24
+ @code = code
25
+ @context = context
26
+ @suggestion = suggestion
27
+ end
28
+
29
+ # Convert the exception to a hash
30
+ #
31
+ # @return [Hash] The exception as a hash
32
+ def to_h
33
+ {
34
+ code: @code,
35
+ message: message,
36
+ context: @context,
37
+ suggestion: @suggestion
38
+ }
39
+ end
40
+ end
41
+
42
+ # Exception raised when the SDK is not configured
43
+ class ConfigurationError < SDKException
44
+ def initialize(message = "SDK is not configured", **kwargs)
45
+ super(message, code: :configuration_error, **kwargs)
46
+ end
47
+ end
48
+
49
+ # Exception raised when validation fails
50
+ class ValidationError < SDKException
51
+ def initialize(message = "Validation failed", **kwargs)
52
+ super(message, code: :validation_error, **kwargs)
53
+ end
54
+ end
55
+
56
+ # Exception raised when a network error occurs
57
+ class NetworkError < SDKException
58
+ def initialize(message = "Network error", **kwargs)
59
+ super(message, code: :network_error, **kwargs)
60
+ end
61
+ end
62
+
63
+ # Exception raised when an API error occurs
64
+ class APIError < SDKException
65
+ # HTTP status code
66
+ attr_reader :status_code
67
+
68
+ # Initialize a new API error
69
+ #
70
+ # @param message [String] Error message
71
+ # @param status_code [Integer] HTTP status code
72
+ # @param kwargs [Hash] Additional arguments
73
+ def initialize(message = "API error", status_code: nil, **kwargs)
74
+ super(message, code: :api_error, **kwargs)
75
+ @status_code = status_code
76
+ end
77
+
78
+ # Get error detail (alias for context)
79
+ #
80
+ # @return [Hash] Error detail hash
81
+ def error_detail
82
+ @context
83
+ end
84
+
85
+ # Convert the exception to a hash
86
+ #
87
+ # @return [Hash] The exception as a hash
88
+ def to_h
89
+ super.merge(status_code: @status_code)
90
+ end
91
+ end
92
+ end
93
+ end