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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +26 -0
- data/README.md +595 -0
- data/lib/complyance/circuit_breaker.rb +99 -0
- data/lib/complyance/persistent_queue_manager.rb +474 -0
- data/lib/complyance/retry_strategy.rb +198 -0
- data/lib/complyance_sdk/config/retry_config.rb +127 -0
- data/lib/complyance_sdk/config/sdk_config.rb +212 -0
- data/lib/complyance_sdk/exceptions/circuit_breaker_open_error.rb +14 -0
- data/lib/complyance_sdk/exceptions/sdk_exception.rb +93 -0
- data/lib/complyance_sdk/generators/config_generator.rb +67 -0
- data/lib/complyance_sdk/generators/install_generator.rb +22 -0
- data/lib/complyance_sdk/generators/templates/complyance_initializer.rb +36 -0
- data/lib/complyance_sdk/http/authentication_middleware.rb +43 -0
- data/lib/complyance_sdk/http/client.rb +223 -0
- data/lib/complyance_sdk/http/logging_middleware.rb +153 -0
- data/lib/complyance_sdk/jobs/base_job.rb +63 -0
- data/lib/complyance_sdk/jobs/process_document_job.rb +92 -0
- data/lib/complyance_sdk/jobs/sidekiq_job.rb +165 -0
- data/lib/complyance_sdk/middleware/rack_middleware.rb +39 -0
- data/lib/complyance_sdk/models/country.rb +205 -0
- data/lib/complyance_sdk/models/country_policy_registry.rb +159 -0
- data/lib/complyance_sdk/models/document_type.rb +52 -0
- data/lib/complyance_sdk/models/environment.rb +144 -0
- data/lib/complyance_sdk/models/logical_doc_type.rb +228 -0
- data/lib/complyance_sdk/models/mode.rb +47 -0
- data/lib/complyance_sdk/models/operation.rb +47 -0
- data/lib/complyance_sdk/models/policy_result.rb +145 -0
- data/lib/complyance_sdk/models/purpose.rb +52 -0
- data/lib/complyance_sdk/models/source.rb +104 -0
- data/lib/complyance_sdk/models/source_ref.rb +130 -0
- data/lib/complyance_sdk/models/unify_request.rb +208 -0
- data/lib/complyance_sdk/models/unify_response.rb +198 -0
- data/lib/complyance_sdk/queue/persistent_queue_manager.rb +609 -0
- data/lib/complyance_sdk/railtie.rb +29 -0
- data/lib/complyance_sdk/retry/circuit_breaker.rb +159 -0
- data/lib/complyance_sdk/retry/retry_manager.rb +108 -0
- data/lib/complyance_sdk/retry/retry_strategy.rb +225 -0
- data/lib/complyance_sdk/version.rb +5 -0
- data/lib/complyance_sdk.rb +935 -0
- 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
|