nats_wave 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.idea/.gitignore +8 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/nats_wave.iml +169 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +136 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +332 -0
- data/LICENSE.txt +21 -0
- data/README.md +985 -0
- data/Rakefile +12 -0
- data/config/nats_wave.yml +65 -0
- data/examples/catalog_model.rb +36 -0
- data/examples/configuration_examples.rb +68 -0
- data/examples/user_model.rb +58 -0
- data/lib/generators/nats_wave/install_generator.rb +40 -0
- data/lib/generators/nats_wave/templates/README +31 -0
- data/lib/generators/nats_wave/templates/create_nats_wave_failed_messages.rb +20 -0
- data/lib/generators/nats_wave/templates/create_nats_wave_failed_subscriptions.rb +20 -0
- data/lib/generators/nats_wave/templates/initializer.rb +64 -0
- data/lib/generators/nats_wave/templates/nats_wave.yml +65 -0
- data/lib/nats_wave/adapters/active_record.rb +206 -0
- data/lib/nats_wave/adapters/datadog_metrics.rb +93 -0
- data/lib/nats_wave/auto_registration.rb +109 -0
- data/lib/nats_wave/client.rb +142 -0
- data/lib/nats_wave/concerns/mappable.rb +172 -0
- data/lib/nats_wave/concerns/publishable.rb +216 -0
- data/lib/nats_wave/configuration.rb +105 -0
- data/lib/nats_wave/database_connector.rb +50 -0
- data/lib/nats_wave/dead_letter_queue.rb +146 -0
- data/lib/nats_wave/errors.rb +27 -0
- data/lib/nats_wave/message_transformer.rb +95 -0
- data/lib/nats_wave/metrics.rb +220 -0
- data/lib/nats_wave/middleware/authentication.rb +65 -0
- data/lib/nats_wave/middleware/base.rb +19 -0
- data/lib/nats_wave/middleware/logging.rb +58 -0
- data/lib/nats_wave/middleware/validation.rb +74 -0
- data/lib/nats_wave/model_mapper.rb +125 -0
- data/lib/nats_wave/model_registry.rb +150 -0
- data/lib/nats_wave/publisher.rb +151 -0
- data/lib/nats_wave/railtie.rb +111 -0
- data/lib/nats_wave/schema_registry.rb +77 -0
- data/lib/nats_wave/subscriber.rb +161 -0
- data/lib/nats_wave/version.rb +5 -0
- data/lib/nats_wave.rb +97 -0
- data/lib/tasks/nats_wave.rake +360 -0
- data/sig/nats_wave.rbs +5 -0
- metadata +385 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NatsWave
|
4
|
+
class Metrics
|
5
|
+
@metrics_backend = nil
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :metrics_backend
|
9
|
+
|
10
|
+
# Auto-configure Datadog if available
|
11
|
+
def configure_datadog(config = {})
|
12
|
+
return unless defined?(Datadog::Statsd)
|
13
|
+
|
14
|
+
@metrics_backend = Adapters::DatadogMetrics.new({
|
15
|
+
service_name: NatsWave.configuration&.service_name || 'nats_wave',
|
16
|
+
namespace: 'nats_wave',
|
17
|
+
tags: {
|
18
|
+
service: NatsWave.configuration&.service_name || 'unknown',
|
19
|
+
environment: ENV['RAILS_ENV'] || 'unknown'
|
20
|
+
}
|
21
|
+
}.merge(config))
|
22
|
+
|
23
|
+
Rails.logger.info "🐕 NatsWave configured with Datadog metrics" if defined?(Rails)
|
24
|
+
end
|
25
|
+
|
26
|
+
def increment_published_messages(subject, model: nil, action: nil)
|
27
|
+
return unless metrics_backend
|
28
|
+
|
29
|
+
tags = build_tags(subject: subject, model: model, action: action, direction: 'outbound')
|
30
|
+
metrics_backend.increment('nats_wave.messages.published', tags: tags)
|
31
|
+
|
32
|
+
# Also track by model and action separately
|
33
|
+
if model && action
|
34
|
+
metrics_backend.increment('nats_wave.model.published', tags: ["model:#{model}", "action:#{action}"])
|
35
|
+
end
|
36
|
+
|
37
|
+
log_metric('published', subject, model, action)
|
38
|
+
end
|
39
|
+
|
40
|
+
def increment_received_messages(subject, model: nil, action: nil, source: nil)
|
41
|
+
return unless metrics_backend
|
42
|
+
|
43
|
+
tags = build_tags(subject: subject, model: model, action: action, direction: 'inbound', source: source)
|
44
|
+
metrics_backend.increment('nats_wave.messages.received', tags: tags)
|
45
|
+
|
46
|
+
log_metric('received', subject, model, action)
|
47
|
+
end
|
48
|
+
|
49
|
+
def track_processing_time(subject, model: nil, action: nil, &block)
|
50
|
+
return yield unless metrics_backend
|
51
|
+
|
52
|
+
start_time = Time.current
|
53
|
+
result = nil
|
54
|
+
success = false
|
55
|
+
|
56
|
+
begin
|
57
|
+
result = yield
|
58
|
+
success = true
|
59
|
+
result
|
60
|
+
rescue => e
|
61
|
+
success = false
|
62
|
+
increment_errors(e.class.name, subject: subject, model: model)
|
63
|
+
raise
|
64
|
+
ensure
|
65
|
+
duration = Time.current - start_time
|
66
|
+
|
67
|
+
tags = build_tags(
|
68
|
+
subject: subject,
|
69
|
+
model: model,
|
70
|
+
action: action,
|
71
|
+
status: success ? 'success' : 'error'
|
72
|
+
)
|
73
|
+
|
74
|
+
metrics_backend.histogram('nats_wave.processing_time', duration, tags: tags)
|
75
|
+
|
76
|
+
log_processing_time(subject, duration, success)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def increment_errors(error_type, subject: nil, model: nil, action: nil)
|
81
|
+
return unless metrics_backend
|
82
|
+
|
83
|
+
tags = build_tags(
|
84
|
+
error_type: error_type,
|
85
|
+
subject: subject,
|
86
|
+
model: model,
|
87
|
+
action: action
|
88
|
+
)
|
89
|
+
|
90
|
+
metrics_backend.increment('nats_wave.errors', tags: tags)
|
91
|
+
|
92
|
+
# Track specific error types
|
93
|
+
metrics_backend.increment('nats_wave.error_types', tags: ["error_type:#{error_type}"])
|
94
|
+
end
|
95
|
+
|
96
|
+
def increment_failed_messages(error_type, subject: nil, retry_count: 0)
|
97
|
+
return unless metrics_backend
|
98
|
+
|
99
|
+
tags = build_tags(error_type: error_type, subject: subject, retry_count: retry_count)
|
100
|
+
metrics_backend.increment('nats_wave.messages.failed', tags: tags)
|
101
|
+
end
|
102
|
+
|
103
|
+
def increment_retries(subject: nil, retry_count: 0, max_retries: 3)
|
104
|
+
return unless metrics_backend
|
105
|
+
|
106
|
+
tags = build_tags(
|
107
|
+
subject: subject,
|
108
|
+
retry_count: retry_count,
|
109
|
+
retry_stage: retry_count >= max_retries ? 'final' : 'intermediate'
|
110
|
+
)
|
111
|
+
|
112
|
+
metrics_backend.increment('nats_wave.retries', tags: tags)
|
113
|
+
end
|
114
|
+
|
115
|
+
def gauge_connection_status(connected, nats_url: nil)
|
116
|
+
return unless metrics_backend
|
117
|
+
|
118
|
+
tags = ["status:#{connected ? 'connected' : 'disconnected'}"]
|
119
|
+
tags << "server:#{extract_host(nats_url)}" if nats_url
|
120
|
+
|
121
|
+
metrics_backend.gauge('nats_wave.connection_status', connected ? 1 : 0, tags: tags)
|
122
|
+
end
|
123
|
+
|
124
|
+
def gauge_failed_messages_count(count, queue_type: 'default')
|
125
|
+
return unless metrics_backend
|
126
|
+
|
127
|
+
metrics_backend.gauge('nats_wave.failed_messages_count', count, tags: ["queue:#{queue_type}"])
|
128
|
+
end
|
129
|
+
|
130
|
+
def gauge_subscriber_count(count)
|
131
|
+
return unless metrics_backend
|
132
|
+
|
133
|
+
metrics_backend.gauge('nats_wave.subscriber_count', count)
|
134
|
+
end
|
135
|
+
|
136
|
+
def track_message_size(size_bytes, subject: nil, direction: 'outbound')
|
137
|
+
return unless metrics_backend
|
138
|
+
|
139
|
+
tags = build_tags(subject: subject, direction: direction)
|
140
|
+
metrics_backend.histogram('nats_wave.message_size', size_bytes, tags: tags)
|
141
|
+
end
|
142
|
+
|
143
|
+
def track_batch_size(count, operation: 'publish')
|
144
|
+
return unless metrics_backend
|
145
|
+
|
146
|
+
metrics_backend.histogram('nats_wave.batch_size', count, tags: ["operation:#{operation}"])
|
147
|
+
end
|
148
|
+
|
149
|
+
# Custom business metrics
|
150
|
+
def track_sync_operation(model, operation, duration, success: true, record_count: 1)
|
151
|
+
return unless metrics_backend
|
152
|
+
|
153
|
+
status = success ? 'success' : 'error'
|
154
|
+
tags = ["model:#{model}", "operation:#{operation}", "status:#{status}"]
|
155
|
+
|
156
|
+
metrics_backend.histogram('nats_wave.sync.duration', duration, tags: tags)
|
157
|
+
metrics_backend.histogram('nats_wave.sync.record_count', record_count, tags: tags)
|
158
|
+
metrics_backend.increment('nats_wave.sync.operations', tags: tags)
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def build_tags(**params)
|
164
|
+
tags = []
|
165
|
+
|
166
|
+
params.each do |key, value|
|
167
|
+
next if value.nil? || value.to_s.empty?
|
168
|
+
|
169
|
+
# Clean up subject names for better grouping
|
170
|
+
if key == :subject && value.is_a?(String)
|
171
|
+
# Convert "app.events.user.create" to "user.create"
|
172
|
+
cleaned_subject = value.split('.').last(2).join('.')
|
173
|
+
tags << "subject:#{cleaned_subject}"
|
174
|
+
|
175
|
+
# Also add subject category
|
176
|
+
category = value.split('.').first
|
177
|
+
tags << "subject_category:#{category}" if category
|
178
|
+
else
|
179
|
+
tags << "#{key}:#{value}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
tags
|
184
|
+
end
|
185
|
+
|
186
|
+
def extract_host(nats_url)
|
187
|
+
return 'unknown' unless nats_url
|
188
|
+
|
189
|
+
URI.parse(nats_url).host || 'unknown'
|
190
|
+
rescue
|
191
|
+
'unknown'
|
192
|
+
end
|
193
|
+
|
194
|
+
def log_metric(type, subject, model = nil, action = nil)
|
195
|
+
return unless defined?(Rails)
|
196
|
+
|
197
|
+
Rails.logger.debug({
|
198
|
+
metric: "nats_wave.message.#{type}",
|
199
|
+
subject: subject,
|
200
|
+
model: model,
|
201
|
+
action: action,
|
202
|
+
timestamp: Time.current.iso8601
|
203
|
+
}.compact.to_json)
|
204
|
+
end
|
205
|
+
|
206
|
+
def log_processing_time(subject, duration, success)
|
207
|
+
return unless defined?(Rails)
|
208
|
+
|
209
|
+
level = success ? :debug : :warn
|
210
|
+
Rails.logger.send(level, {
|
211
|
+
metric: 'nats_wave.processing_time',
|
212
|
+
subject: subject,
|
213
|
+
duration_seconds: duration.round(3),
|
214
|
+
success: success,
|
215
|
+
timestamp: Time.current.iso8601
|
216
|
+
}.to_json)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt'
|
4
|
+
|
5
|
+
module NatsWave
|
6
|
+
module Middleware
|
7
|
+
class Authentication < Base
|
8
|
+
def initialize(config)
|
9
|
+
super
|
10
|
+
@secret_key = config.auth_secret_key
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(message)
|
14
|
+
return message unless @config.middleware_authentication_enabled
|
15
|
+
|
16
|
+
# For outgoing messages, add authentication token
|
17
|
+
if outgoing_message?(message)
|
18
|
+
add_auth_token(message)
|
19
|
+
else
|
20
|
+
# For incoming messages, validate token
|
21
|
+
validate_auth_token(message)
|
22
|
+
end
|
23
|
+
|
24
|
+
message
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def outgoing_message?(message)
|
30
|
+
message.key?(:subject) && message.key?(:data)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_auth_token(message)
|
34
|
+
return message unless @secret_key
|
35
|
+
|
36
|
+
payload = {
|
37
|
+
service: @config.service_name,
|
38
|
+
instance_id: @config.instance_id,
|
39
|
+
timestamp: Time.current.to_i,
|
40
|
+
exp: Time.current.to_i + 3600 # 1 hour expiry
|
41
|
+
}
|
42
|
+
|
43
|
+
token = JWT.encode(payload, @secret_key, 'HS256')
|
44
|
+
message[:metadata] ||= {}
|
45
|
+
message[:metadata][:auth_token] = token
|
46
|
+
|
47
|
+
message
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_auth_token(message)
|
51
|
+
token = message.dig('metadata', 'auth_token')
|
52
|
+
|
53
|
+
raise UnauthorizedError, 'Missing authentication token' unless token
|
54
|
+
|
55
|
+
begin
|
56
|
+
JWT.decode(token, @secret_key, true, { algorithm: 'HS256' })
|
57
|
+
rescue JWT::DecodeError => e
|
58
|
+
raise UnauthorizedError, "Invalid authentication token: #{e.message}"
|
59
|
+
end
|
60
|
+
|
61
|
+
message
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NatsWave
|
4
|
+
module Middleware
|
5
|
+
class Base
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(message)
|
11
|
+
raise NotImplementedError, 'Subclasses must implement #call'
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
attr_reader :config
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NatsWave
|
4
|
+
module Middleware
|
5
|
+
class Logging < Base
|
6
|
+
def call(message)
|
7
|
+
return message unless logging_enabled?
|
8
|
+
|
9
|
+
if outgoing_message?(message)
|
10
|
+
log_outgoing_message(message)
|
11
|
+
else
|
12
|
+
log_incoming_message(message)
|
13
|
+
end
|
14
|
+
|
15
|
+
message
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def logging_enabled?
|
21
|
+
@config.middleware_logging_enabled
|
22
|
+
end
|
23
|
+
|
24
|
+
def outgoing_message?(message)
|
25
|
+
message.key?(:subject) && message.key?(:data)
|
26
|
+
end
|
27
|
+
|
28
|
+
def log_outgoing_message(message)
|
29
|
+
NatsWave.logger.send(log_level, {
|
30
|
+
type: 'nats_wave_publish',
|
31
|
+
event_id: message[:event_id],
|
32
|
+
subject: message[:subject],
|
33
|
+
model: message[:model],
|
34
|
+
action: message[:action],
|
35
|
+
service: @config.service_name,
|
36
|
+
timestamp: message[:timestamp]
|
37
|
+
}.to_json)
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_incoming_message(message)
|
41
|
+
NatsWave.logger.send(log_level, {
|
42
|
+
type: 'nats_wave_receive',
|
43
|
+
event_id: message['event_id'],
|
44
|
+
subject: message['subject'],
|
45
|
+
model: message['model'],
|
46
|
+
action: message['action'],
|
47
|
+
source_service: message.dig('source', 'service'),
|
48
|
+
timestamp: message['timestamp']
|
49
|
+
}.to_json)
|
50
|
+
end
|
51
|
+
|
52
|
+
def log_level
|
53
|
+
level = @config.log_level || 'info'
|
54
|
+
level.to_sym
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json-schema'
|
4
|
+
|
5
|
+
module NatsWave
|
6
|
+
module Middleware
|
7
|
+
class Validation < Base
|
8
|
+
MESSAGE_SCHEMA = {
|
9
|
+
type: 'object',
|
10
|
+
required: %w[event_id timestamp source subject model action data],
|
11
|
+
properties: {
|
12
|
+
event_id: {
|
13
|
+
type: 'string',
|
14
|
+
pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
15
|
+
},
|
16
|
+
timestamp: { type: 'string', format: 'date-time' },
|
17
|
+
source: {
|
18
|
+
type: 'object',
|
19
|
+
required: %w[service version instance_id],
|
20
|
+
properties: {
|
21
|
+
service: { type: 'string' },
|
22
|
+
version: { type: 'string' },
|
23
|
+
instance_id: { type: 'string' }
|
24
|
+
}
|
25
|
+
},
|
26
|
+
subject: { type: 'string', minLength: 1 },
|
27
|
+
model: { type: 'string', minLength: 1 },
|
28
|
+
action: { type: 'string', enum: %w[create update delete destroy] },
|
29
|
+
data: { type: 'object' },
|
30
|
+
metadata: { type: 'object' },
|
31
|
+
schema_version: { type: 'string' }
|
32
|
+
}
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
def initialize(config)
|
36
|
+
super
|
37
|
+
@schema_registry = SchemaRegistry.new(config.schema_registry_url) if validation_enabled?
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(message)
|
41
|
+
return message unless validation_enabled?
|
42
|
+
|
43
|
+
validate_message_structure(message)
|
44
|
+
validate_against_schema(message) if @schema_registry
|
45
|
+
|
46
|
+
message
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def validation_enabled?
|
52
|
+
@config.middleware_validation_enabled
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_message_structure(message)
|
56
|
+
errors = JSON::Validator.fully_validate(MESSAGE_SCHEMA, message)
|
57
|
+
|
58
|
+
return if errors.empty?
|
59
|
+
|
60
|
+
error_message = "Message validation failed: #{errors.join(', ')}"
|
61
|
+
NatsWave.logger.error(error_message)
|
62
|
+
raise ValidationError, error_message
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_against_schema(message)
|
66
|
+
schema_version = message['schema_version'] || '1.0'
|
67
|
+
|
68
|
+
return if @schema_registry.validate_message(message, schema_version)
|
69
|
+
|
70
|
+
raise ValidationError, "Message does not conform to schema version #{schema_version}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NatsWave
|
4
|
+
class ModelMapper
|
5
|
+
def initialize(config = nil)
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def map_external_to_local(external_model, external_data)
|
10
|
+
# First check the registry
|
11
|
+
local_model = ModelRegistry.best_local_model_for(external_model, external_data)
|
12
|
+
return nil unless local_model
|
13
|
+
|
14
|
+
local_mappings = ModelRegistry.local_models_for(external_model)
|
15
|
+
mapping_config = local_mappings[local_model]
|
16
|
+
|
17
|
+
# Transform the data
|
18
|
+
transformed_data = transform_data(
|
19
|
+
external_data,
|
20
|
+
mapping_config[:field_mappings] || {},
|
21
|
+
mapping_config[:transformations] || {}
|
22
|
+
)
|
23
|
+
|
24
|
+
{
|
25
|
+
model: local_model,
|
26
|
+
data: transformed_data,
|
27
|
+
config: mapping_config,
|
28
|
+
unique_fields: mapping_config[:unique_fields] || [:id],
|
29
|
+
sync_strategy: mapping_config[:sync_strategy] || :upsert
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def map_local_to_external(local_model_instance, target_external_model)
|
34
|
+
return nil unless local_model_instance.class.respond_to?(:nats_wave_can_sync_from?)
|
35
|
+
return nil unless local_model_instance.class.nats_wave_can_sync_from?(target_external_model)
|
36
|
+
|
37
|
+
local_model_instance.nats_wave_mapped_attributes_for(target_external_model)
|
38
|
+
end
|
39
|
+
|
40
|
+
def transform_data(data, field_mappings, transformations)
|
41
|
+
result = {}
|
42
|
+
|
43
|
+
# Apply field mappings
|
44
|
+
data.each do |key, value|
|
45
|
+
mapped_key = field_mappings[key] || field_mappings[key.to_sym] || key
|
46
|
+
result[mapped_key] = value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Apply transformations
|
50
|
+
transformations.each do |field, transformer|
|
51
|
+
if result.key?(field) && transformer.respond_to?(:call)
|
52
|
+
begin
|
53
|
+
result[field] = transformer.call(result[field])
|
54
|
+
rescue => e
|
55
|
+
NatsWave.logger.error("Transformation failed for field #{field}: #{e.message}")
|
56
|
+
raise TransformationError, "Failed to transform field #{field}: #{e.message}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
result
|
62
|
+
end
|
63
|
+
|
64
|
+
# Legacy compatibility with old configuration-based mappings
|
65
|
+
def map_model(external_model, external_data)
|
66
|
+
# Try registry first
|
67
|
+
registry_mapping = map_external_to_local(external_model, external_data)
|
68
|
+
return registry_mapping if registry_mapping
|
69
|
+
|
70
|
+
# Fall back to configuration-based mapping
|
71
|
+
return nil unless @config
|
72
|
+
|
73
|
+
mapping_config = @config.model_mappings[external_model]
|
74
|
+
return nil unless mapping_config
|
75
|
+
|
76
|
+
local_model = mapping_config[:target_model]
|
77
|
+
field_mappings = mapping_config[:field_mappings] || {}
|
78
|
+
|
79
|
+
mapped_attributes = map_attributes(external_data, field_mappings)
|
80
|
+
|
81
|
+
{
|
82
|
+
model: local_model,
|
83
|
+
id: extract_id(external_data, field_mappings),
|
84
|
+
attributes: mapped_attributes
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def map_attributes(external_data, field_mappings)
|
91
|
+
mapped = {}
|
92
|
+
|
93
|
+
external_data.each do |external_field, value|
|
94
|
+
local_field = field_mappings[external_field] || external_field
|
95
|
+
|
96
|
+
# Apply transformations if configured
|
97
|
+
if @config&.transformation_rules && @config.transformation_rules[local_field]
|
98
|
+
value = apply_transformation(local_field, value)
|
99
|
+
end
|
100
|
+
|
101
|
+
mapped[local_field] = value
|
102
|
+
end
|
103
|
+
|
104
|
+
mapped
|
105
|
+
end
|
106
|
+
|
107
|
+
def extract_id(external_data, field_mappings)
|
108
|
+
id_field = field_mappings['id'] || 'id'
|
109
|
+
external_data[id_field]
|
110
|
+
end
|
111
|
+
|
112
|
+
def apply_transformation(field, value)
|
113
|
+
transformation = @config.transformation_rules[field]
|
114
|
+
|
115
|
+
case transformation
|
116
|
+
when Proc
|
117
|
+
transformation.call(value)
|
118
|
+
when Symbol
|
119
|
+
send(transformation, value) if respond_to?(transformation, true)
|
120
|
+
else
|
121
|
+
value
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|