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
data/README.md
ADDED
@@ -0,0 +1,985 @@
|
|
1
|
+
# π NatsWave
|
2
|
+
|
3
|
+
A comprehensive NATS-based event messaging system for Rails applications that enables seamless communication between
|
4
|
+
different teams, products, and services in asset management and valuation workflows.
|
5
|
+
|
6
|
+
[](https://badge.fury.io/rb/nats_wave)
|
7
|
+
[](https://github.com/PurpleWave/nats_wave/actions)
|
8
|
+
[](https://codeclimate.com/github/PurpleWave/nats_wave)
|
9
|
+
[](https://codeclimate.com/github/PurpleWave/nats_wave/coverage)
|
10
|
+
|
11
|
+
## π Table of Contents
|
12
|
+
|
13
|
+
- [Features](#-features)
|
14
|
+
- [Installation](#-installation)
|
15
|
+
- [Quick Start](#-quick-start)
|
16
|
+
- [Configuration](#-configuration)
|
17
|
+
- [Model-Based Architecture](#-model-based-architecture)
|
18
|
+
- [Publishing Events](#-publishing-events)
|
19
|
+
- [Subscribing to Events](#-subscribing-to-events)
|
20
|
+
- [Data Mapping & Transformation](#-data-mapping--transformation)
|
21
|
+
- [Monitoring & Metrics](#-monitoring--metrics)
|
22
|
+
- [Error Handling](#-error-handling)
|
23
|
+
- [Production Deployment](#-production-deployment)
|
24
|
+
- [API Reference](#-api-reference)
|
25
|
+
- [Examples](#-examples)
|
26
|
+
- [Contributing](#-contributing)
|
27
|
+
- [License](#-license)
|
28
|
+
|
29
|
+
## β¨ Features
|
30
|
+
|
31
|
+
- **π Model-Based Configuration** - Configure subscriptions and mappings directly in your models
|
32
|
+
- **π Automatic Event Publishing** - ActiveRecord integration publishes model changes automatically
|
33
|
+
- **πΊοΈ Intelligent Data Mapping** - Transform data between different schemas and field names
|
34
|
+
- **π Datadog Integration** - Built-in metrics and monitoring support
|
35
|
+
- **π‘οΈ Error Handling** - Dead letter queues, retries, and graceful failure handling
|
36
|
+
- **π§ Middleware System** - Authentication, validation, and logging middleware
|
37
|
+
- **β‘ High Performance** - Connection pooling, async publishing, and batch operations
|
38
|
+
- **π₯ Health Monitoring** - Built-in health checks and status endpoints
|
39
|
+
- **π Auto-Discovery** - Automatic registration of model subscriptions
|
40
|
+
- **π§ͺ Test-Friendly** - Comprehensive test helpers and mocking support
|
41
|
+
|
42
|
+
## π¦ Installation
|
43
|
+
|
44
|
+
Add this line to your Rails application's Gemfile:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
gem 'nats_wave'
|
48
|
+
|
49
|
+
# Optional: For enhanced monitoring
|
50
|
+
gem 'dogstatsd-ruby' # For Datadog metrics
|
51
|
+
```
|
52
|
+
|
53
|
+
And then execute:
|
54
|
+
|
55
|
+
```bash
|
56
|
+
bundle install
|
57
|
+
```
|
58
|
+
|
59
|
+
Generate the configuration files:
|
60
|
+
|
61
|
+
```bash
|
62
|
+
rails generate nats_wave:install
|
63
|
+
rails db:migrate
|
64
|
+
```
|
65
|
+
|
66
|
+
## π Quick Start
|
67
|
+
|
68
|
+
### 1. Basic Configuration
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# config/initializers/nats_wave.rb
|
72
|
+
NatsWave.configure do |config|
|
73
|
+
config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
|
74
|
+
config.service_name = 'valuation_service'
|
75
|
+
config.default_subject_prefix = "#{config.service_name}.events"
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
### 2. Add to Your Models
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# app/models/asset.rb
|
83
|
+
class Asset < ApplicationRecord
|
84
|
+
include NatsWave::NatsPublishable
|
85
|
+
include NatsWave::Concerns::Mappable
|
86
|
+
|
87
|
+
# Automatically publish asset events
|
88
|
+
nats_publishable(
|
89
|
+
actions: [:create, :update, :destroy],
|
90
|
+
skip_attributes: [:notes, :edge_data],
|
91
|
+
include_associations: [:package, :estimates]
|
92
|
+
)
|
93
|
+
|
94
|
+
# Subscribe to external asset data from IMS
|
95
|
+
nats_wave_maps_from 'IMSAsset',
|
96
|
+
subjects: ['ims.assets.*', 'inventory.assets.*'],
|
97
|
+
field_mappings: {
|
98
|
+
'asset_id' => 'unique_id',
|
99
|
+
'description' => 'description',
|
100
|
+
'manufacturer' => 'make',
|
101
|
+
'model_name' => 'model',
|
102
|
+
'year_manufactured' => 'year',
|
103
|
+
'serial_number' => 'serial',
|
104
|
+
'vin_number' => 'vin'
|
105
|
+
},
|
106
|
+
transformations: {
|
107
|
+
'make' => ->(make) { make&.upcase&.strip },
|
108
|
+
'year' => ->(year) { year.to_i if year.present? }
|
109
|
+
},
|
110
|
+
unique_fields: [:unique_id, :vin, :serial]
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
### 3. Start the Subscriber
|
115
|
+
|
116
|
+
```bash
|
117
|
+
# Development - auto-starts with Rails
|
118
|
+
rails server
|
119
|
+
|
120
|
+
# Production - run dedicated subscriber process
|
121
|
+
rails nats_wave:start
|
122
|
+
```
|
123
|
+
|
124
|
+
### 4. Test the Integration
|
125
|
+
|
126
|
+
```bash
|
127
|
+
# Test connectivity
|
128
|
+
rails nats_wave:health
|
129
|
+
|
130
|
+
# Show model subscriptions
|
131
|
+
rails nats_wave:model_subscriptions
|
132
|
+
|
133
|
+
# Publish a test message
|
134
|
+
rails nats_wave:test
|
135
|
+
```
|
136
|
+
|
137
|
+
## βοΈ Configuration
|
138
|
+
|
139
|
+
### Environment Variables
|
140
|
+
|
141
|
+
```bash
|
142
|
+
# Required
|
143
|
+
NATS_URL=nats://your-nats-server:4222
|
144
|
+
SERVICE_NAME=valuation_service
|
145
|
+
|
146
|
+
# Optional
|
147
|
+
NATS_AUTH_SECRET=your-secret-key
|
148
|
+
DD_AGENT_HOST=localhost # For Datadog metrics
|
149
|
+
DD_AGENT_PORT=8125
|
150
|
+
```
|
151
|
+
|
152
|
+
### Advanced Configuration
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# config/initializers/nats_wave.rb
|
156
|
+
NatsWave.configure do |config|
|
157
|
+
# NATS Connection
|
158
|
+
config.nats_url = ENV.fetch('NATS_URL')
|
159
|
+
config.service_name = ENV.fetch('SERVICE_NAME')
|
160
|
+
config.connection_pool_size = 10
|
161
|
+
config.reconnect_attempts = 3
|
162
|
+
|
163
|
+
# Publishing
|
164
|
+
config.publishing_enabled = !Rails.env.test?
|
165
|
+
config.async_publishing = Rails.env.production?
|
166
|
+
config.default_subject_prefix = "#{config.service_name}.events"
|
167
|
+
|
168
|
+
# Subscription
|
169
|
+
config.subscription_enabled = !Rails.env.test?
|
170
|
+
config.queue_group = "#{config.service_name}_consumers"
|
171
|
+
|
172
|
+
# Middleware
|
173
|
+
config.middleware_authentication_enabled = Rails.env.production?
|
174
|
+
config.auth_secret_key = ENV['NATS_AUTH_SECRET']
|
175
|
+
config.middleware_logging_enabled = true
|
176
|
+
config.log_level = Rails.env.production? ? 'info' : 'debug'
|
177
|
+
|
178
|
+
# Error Handling
|
179
|
+
config.max_retries = 3
|
180
|
+
config.retry_delay = 5
|
181
|
+
end
|
182
|
+
|
183
|
+
# Configure Datadog metrics
|
184
|
+
if defined?(Datadog::Statsd)
|
185
|
+
NatsWave::Metrics.configure_datadog(
|
186
|
+
namespace: 'valuation_app.nats_wave',
|
187
|
+
tags: {
|
188
|
+
service: NatsWave.configuration.service_name,
|
189
|
+
environment: Rails.env
|
190
|
+
}
|
191
|
+
)
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
195
|
+
## ποΈ Model-Based Architecture
|
196
|
+
|
197
|
+
NatsWave uses a model-centric approach where each model defines its own subscriptions and mappings.
|
198
|
+
|
199
|
+
### Publishing Events
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
|
203
|
+
class Package < ApplicationRecord
|
204
|
+
include NatsWave::NatsPublishable
|
205
|
+
|
206
|
+
nats_publishable(
|
207
|
+
actions: [:create, :update], # Which actions to publish
|
208
|
+
skip_attributes: [:notes, :valuation_notes], # Attributes to exclude
|
209
|
+
include_associations: [:organization, :assets, :contact_user], # Include related data
|
210
|
+
if: -> { workflow_status == 'active' }, # Conditional publishing
|
211
|
+
unless: -> { is_shared? }, # Skip conditions
|
212
|
+
subject_prefix: 'packages' # Custom subject prefix
|
213
|
+
)
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
# Add custom metadata to published events
|
218
|
+
def nats_wave_metadata
|
219
|
+
{
|
220
|
+
organization_name: organization&.name,
|
221
|
+
asset_count: assets.count,
|
222
|
+
total_estimated_value: assets.sum(&:estimated_hammer),
|
223
|
+
valuation_complete: valuation_complete?
|
224
|
+
}
|
225
|
+
end
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
229
|
+
### Subscribing to External Events
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
|
233
|
+
class Asset < ApplicationRecord
|
234
|
+
include NatsWave::Concerns::Mappable
|
235
|
+
|
236
|
+
# Map from inventory management system
|
237
|
+
nats_wave_maps_from 'InventoryAsset',
|
238
|
+
subjects: ['inventory.assets.*', 'warehouse.assets.*'],
|
239
|
+
field_mappings: {
|
240
|
+
'inventory_id' => 'unique_id',
|
241
|
+
'asset_description' => 'description',
|
242
|
+
'manufacturer' => 'make',
|
243
|
+
'model_number' => 'model',
|
244
|
+
'year_built' => 'year',
|
245
|
+
'serial_num' => 'serial',
|
246
|
+
'vin_code' => 'vin',
|
247
|
+
'location_code' => 'location'
|
248
|
+
},
|
249
|
+
transformations: {
|
250
|
+
'make' => ->(make) { make&.upcase&.strip },
|
251
|
+
'model' => ->(model) { model&.titleize&.strip },
|
252
|
+
'year' => ->(year) { year.to_i if year.present? },
|
253
|
+
'estimated_hammer' => ->(value) { value.to_f.round(2) }
|
254
|
+
},
|
255
|
+
unique_fields: [:unique_id, :vin, :serial],
|
256
|
+
sync_strategy: :upsert,
|
257
|
+
conditions: {
|
258
|
+
'status' => 'available',
|
259
|
+
'condition' => ['good', 'fair', 'excellent']
|
260
|
+
}
|
261
|
+
|
262
|
+
# Map from auction system
|
263
|
+
nats_wave_maps_from 'AuctionItem',
|
264
|
+
subjects: ['auction.items.*'],
|
265
|
+
field_mappings: {
|
266
|
+
'lot_number' => 'lot_number',
|
267
|
+
'hammer_price' => 'pw_contract_price',
|
268
|
+
'sale_date' => 'pw_contract_date'
|
269
|
+
},
|
270
|
+
conditions: {
|
271
|
+
'auction_status' => 'sold'
|
272
|
+
}
|
273
|
+
end
|
274
|
+
```
|
275
|
+
|
276
|
+
### Custom Event Handlers
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
|
280
|
+
class Estimate < ApplicationRecord
|
281
|
+
include NatsWave::Concerns::Mappable
|
282
|
+
|
283
|
+
# Subscribe to valuation requests
|
284
|
+
nats_wave_subscribes_to 'valuation.requests.*' do |message|
|
285
|
+
ValuationRequestProcessor.process_request(message)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Subscribe to market price updates
|
289
|
+
nats_wave_subscribes_to 'market.prices.*' do |message|
|
290
|
+
MarketPriceService.update_estimates(message)
|
291
|
+
end
|
292
|
+
|
293
|
+
# Subscribe to auction results for comparison
|
294
|
+
nats_wave_subscribes_to 'auction.results.*' do |message|
|
295
|
+
AuctionResultAnalyzer.analyze_accuracy(message)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
## π€ Publishing Events
|
301
|
+
|
302
|
+
### Automatic Publishing
|
303
|
+
|
304
|
+
Events are automatically published when you include `NatsPublishable` in your models:
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
# This automatically publishes to "valuation_service.events.asset.create"
|
308
|
+
asset = Asset.create!(
|
309
|
+
make: "Caterpillar",
|
310
|
+
model: "320D",
|
311
|
+
year: 2018,
|
312
|
+
serial: "ABC123456",
|
313
|
+
description: "Excavator - Track Type"
|
314
|
+
)
|
315
|
+
|
316
|
+
# This publishes to "valuation_service.events.asset.update"
|
317
|
+
asset.update!(estimated_hammer: 125000)
|
318
|
+
|
319
|
+
# This publishes to "valuation_service.events.asset.destroy"
|
320
|
+
asset.destroy!
|
321
|
+
```
|
322
|
+
|
323
|
+
### Manual Publishing
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
# Publish custom valuation events
|
327
|
+
NatsWave.publish(
|
328
|
+
subject: 'valuation.completed',
|
329
|
+
model: 'ValuationSession',
|
330
|
+
action: 'complete',
|
331
|
+
data: {
|
332
|
+
package_id: package.id,
|
333
|
+
asset_count: package.assets.count,
|
334
|
+
total_value: package.total_estimated_value,
|
335
|
+
appraiser_id: current_user.id
|
336
|
+
},
|
337
|
+
metadata: {
|
338
|
+
organization_id: package.organization.id,
|
339
|
+
completion_time: Time.current,
|
340
|
+
valuation_method: 'comparative_analysis'
|
341
|
+
}
|
342
|
+
)
|
343
|
+
|
344
|
+
# Batch publishing for performance
|
345
|
+
events = assets.map do |asset|
|
346
|
+
{
|
347
|
+
subject: 'asset.valued',
|
348
|
+
model: 'Asset',
|
349
|
+
action: 'update',
|
350
|
+
data: asset.attributes.except('notes', 'edge_data')
|
351
|
+
}
|
352
|
+
end
|
353
|
+
|
354
|
+
NatsWave.client.publish_batch(events)
|
355
|
+
```
|
356
|
+
|
357
|
+
### Conditional Publishing
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
|
361
|
+
class Asset < ApplicationRecord
|
362
|
+
include NatsWave::NatsPublishable
|
363
|
+
|
364
|
+
nats_publishable(
|
365
|
+
if: -> { should_publish_to_external_systems? },
|
366
|
+
unless: -> { asset_status == 'draft' }
|
367
|
+
)
|
368
|
+
|
369
|
+
private
|
370
|
+
|
371
|
+
def should_publish_to_external_systems?
|
372
|
+
asset_status == 'completed' &&
|
373
|
+
package.workflow_status == 'finalized' &&
|
374
|
+
!package.is_shared?
|
375
|
+
end
|
376
|
+
end
|
377
|
+
```
|
378
|
+
|
379
|
+
## π₯ Subscribing to Events
|
380
|
+
|
381
|
+
### Automatic Model Syncing
|
382
|
+
|
383
|
+
When you configure `nats_wave_maps_from`, NatsWave automatically:
|
384
|
+
|
385
|
+
1. Subscribes to the specified subjects
|
386
|
+
2. Transforms incoming data using field mappings
|
387
|
+
3. Applies custom transformations
|
388
|
+
4. Syncs data to your local models
|
389
|
+
|
390
|
+
```ruby
|
391
|
+
# Incoming message from inventory system:
|
392
|
+
{
|
393
|
+
"subject": "inventory.assets.update",
|
394
|
+
"model": "InventoryAsset",
|
395
|
+
"action": "update",
|
396
|
+
"data": {
|
397
|
+
"inventory_id": "INV-2024-001",
|
398
|
+
"asset_description": "2020 Ford F-150 Pickup Truck",
|
399
|
+
"manufacturer": "ford",
|
400
|
+
"model_number": "F-150",
|
401
|
+
"year_built": "2020",
|
402
|
+
"serial_num": "1FTFW1ET5LFA12345",
|
403
|
+
"vin_code": "1FTFW1ET5LFA12345",
|
404
|
+
"estimated_value": 35000.00
|
405
|
+
}
|
406
|
+
}
|
407
|
+
|
408
|
+
# Automatically transforms to:
|
409
|
+
{
|
410
|
+
"unique_id": "INV-2024-001",
|
411
|
+
"description": "2020 Ford F-150 Pickup Truck",
|
412
|
+
"make": "FORD",
|
413
|
+
"model": "F-150",
|
414
|
+
"year": 2020,
|
415
|
+
"serial": "1FTFW1ET5LFA12345",
|
416
|
+
"vin": "1FTFW1ET5LFA12345",
|
417
|
+
"estimated_hammer": 35000.00
|
418
|
+
}
|
419
|
+
|
420
|
+
# And updates/creates the local Asset record
|
421
|
+
```
|
422
|
+
|
423
|
+
### Custom Subscription Handlers
|
424
|
+
|
425
|
+
```ruby
|
426
|
+
|
427
|
+
class Organization < ApplicationRecord
|
428
|
+
include NatsWave::Concerns::Mappable
|
429
|
+
|
430
|
+
# Process user activity for analytics
|
431
|
+
nats_wave_subscribes_to 'users.*.login', 'users.*.logout' do |message|
|
432
|
+
UserActivityTracker.track_activity(message)
|
433
|
+
end
|
434
|
+
|
435
|
+
# Process package completions with custom queue group
|
436
|
+
nats_wave_subscribes_to 'packages.*.completed',
|
437
|
+
queue_group: 'reporting_processors' do |message|
|
438
|
+
PackageCompletionReporter.generate_report(message)
|
439
|
+
end
|
440
|
+
end
|
441
|
+
```
|
442
|
+
|
443
|
+
## πΊοΈ Data Mapping & Transformation
|
444
|
+
|
445
|
+
### Field Mappings
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
nats_wave_maps_from 'ExternalAsset',
|
449
|
+
field_mappings: {
|
450
|
+
'external_id' => 'unique_id',
|
451
|
+
'asset_make' => 'make',
|
452
|
+
'asset_model' => 'model',
|
453
|
+
'manufacture_year' => 'year',
|
454
|
+
'serial_number' => 'serial',
|
455
|
+
'vehicle_identification' => 'vin',
|
456
|
+
'asset_location' => 'location'
|
457
|
+
}
|
458
|
+
```
|
459
|
+
|
460
|
+
### Data Transformations
|
461
|
+
|
462
|
+
```ruby
|
463
|
+
nats_wave_maps_from 'ExternalAsset',
|
464
|
+
transformations: {
|
465
|
+
# Lambda transformations
|
466
|
+
'estimated_hammer' => ->(value) { value.to_f.round(2) },
|
467
|
+
'make' => ->(make) { make&.upcase&.strip },
|
468
|
+
'model' => ->(model) { model&.titleize&.strip },
|
469
|
+
|
470
|
+
# Method transformations
|
471
|
+
'vin' => :normalize_vin,
|
472
|
+
'serial' => :normalize_serial,
|
473
|
+
|
474
|
+
# Complex transformations with access to full record
|
475
|
+
'description' => ->(value, record) {
|
476
|
+
"#{record['year']} #{record['make']} #{record['model']} #{value}".strip
|
477
|
+
}
|
478
|
+
}
|
479
|
+
|
480
|
+
private
|
481
|
+
|
482
|
+
def normalize_vin(vin)
|
483
|
+
vin&.upcase&.gsub(/[^A-Z0-9]/, '')
|
484
|
+
end
|
485
|
+
|
486
|
+
def normalize_serial(serial)
|
487
|
+
serial&.upcase&.strip
|
488
|
+
end
|
489
|
+
```
|
490
|
+
|
491
|
+
### Conditional Syncing
|
492
|
+
|
493
|
+
```ruby
|
494
|
+
nats_wave_maps_from 'ExternalAsset',
|
495
|
+
conditions: {
|
496
|
+
'status' => 'available', # Exact match
|
497
|
+
'condition' => ['good', 'fair', 'excellent'], # Array inclusion
|
498
|
+
'value' => ->(value) { value.to_f > 1000 }, # Custom logic
|
499
|
+
'category' => /^(vehicle|equipment|machinery)$/ # Regex match
|
500
|
+
}
|
501
|
+
```
|
502
|
+
|
503
|
+
### Sync Strategies
|
504
|
+
|
505
|
+
```ruby
|
506
|
+
nats_wave_maps_from 'ExternalAsset',
|
507
|
+
sync_strategy: :upsert, # Create or update (default)
|
508
|
+
# sync_strategy: :create_only, # Only create new records
|
509
|
+
# sync_strategy: :update_only, # Only update existing records
|
510
|
+
unique_fields: [:unique_id, :vin, :serial] # Fields to match existing records
|
511
|
+
```
|
512
|
+
|
513
|
+
## π Monitoring & Metrics
|
514
|
+
|
515
|
+
### Datadog Integration
|
516
|
+
|
517
|
+
NatsWave provides comprehensive Datadog metrics out of the box:
|
518
|
+
|
519
|
+
```ruby
|
520
|
+
# Automatic metrics tracked:
|
521
|
+
-nats_wave.messages.published # Messages published by subject/model
|
522
|
+
-nats_wave.messages.received # Messages received by subject/model
|
523
|
+
-nats_wave.processing_time # Message processing duration
|
524
|
+
-nats_wave.errors # Errors by type and subject
|
525
|
+
-nats_wave.connection_status # NATS connection health
|
526
|
+
-nats_wave.sync.operations # Model sync operations
|
527
|
+
-nats_wave.retries # Retry attempts
|
528
|
+
-nats_wave.valuations.completed # Valuation completion events
|
529
|
+
-nats_wave.assets.synced # Asset synchronization events
|
530
|
+
```
|
531
|
+
|
532
|
+
### Health Checks
|
533
|
+
|
534
|
+
```ruby
|
535
|
+
# Built-in health check endpoint
|
536
|
+
GET /health/n ats_wave
|
537
|
+
|
538
|
+
# Response:
|
539
|
+
{
|
540
|
+
"nats_connected": true,
|
541
|
+
"database_connected": true,
|
542
|
+
"timestamp": "2024-01-01T12:00:00Z",
|
543
|
+
"service_name": "valuation_service",
|
544
|
+
"version": "1.1.0",
|
545
|
+
"instance_id": "web-1",
|
546
|
+
"published_subjects": [
|
547
|
+
"valuation_service.events.asset.*",
|
548
|
+
"valuation_service.events.package.*",
|
549
|
+
"valuation_service.events.estimate.*"
|
550
|
+
],
|
551
|
+
"subscribed_subjects": [
|
552
|
+
"inventory.assets.*",
|
553
|
+
"auction.results.*",
|
554
|
+
"market.prices.*"
|
555
|
+
],
|
556
|
+
"nats_url": "nats://your-server:4222"
|
557
|
+
}
|
558
|
+
```
|
559
|
+
|
560
|
+
### Monitoring Commands
|
561
|
+
|
562
|
+
```bash
|
563
|
+
# Check overall health
|
564
|
+
rails nats_wave:health
|
565
|
+
|
566
|
+
# Show all model subscriptions
|
567
|
+
rails nats_wave:model_subscriptions
|
568
|
+
|
569
|
+
# Monitor live NATS activity
|
570
|
+
rails nats_wave:monitor
|
571
|
+
|
572
|
+
# Show failed messages
|
573
|
+
rails nats_wave:show_failed
|
574
|
+
|
575
|
+
# Retry failed messages
|
576
|
+
rails nats_wave:retry_failed
|
577
|
+
```
|
578
|
+
|
579
|
+
## π‘οΈ Error Handling
|
580
|
+
|
581
|
+
### Dead Letter Queue
|
582
|
+
|
583
|
+
Failed messages are automatically stored in a dead letter queue with retry logic:
|
584
|
+
|
585
|
+
```ruby
|
586
|
+
# Automatic retry with exponential backoff
|
587
|
+
# Retry 1: after 5 seconds
|
588
|
+
# Retry 2: after 25 seconds
|
589
|
+
# Retry 3: after 125 seconds
|
590
|
+
# After max retries: moved to permanent failure storage
|
591
|
+
```
|
592
|
+
|
593
|
+
### Error Recovery
|
594
|
+
|
595
|
+
```ruby
|
596
|
+
# Retry failed messages
|
597
|
+
rails nats_wave: retry_failed
|
598
|
+
|
599
|
+
# Clear failed messages (after manual review)
|
600
|
+
rails nats_wave: clear_failed
|
601
|
+
|
602
|
+
# View failed message details
|
603
|
+
failed_messages = NatsWave::DeadLetterQueue.new(NatsWave.configuration)
|
604
|
+
failed_messages.get_failed_messages.each do |msg|
|
605
|
+
puts "Error: #{msg[:error]}"
|
606
|
+
puts "Retry Count: #{msg[:retry_count]}"
|
607
|
+
puts "Message: #{msg[:message]}"
|
608
|
+
end
|
609
|
+
```
|
610
|
+
|
611
|
+
### Custom Error Handling
|
612
|
+
|
613
|
+
```ruby
|
614
|
+
|
615
|
+
class CustomErrorHandler
|
616
|
+
def self.handle_failed_message(message, error, retry_count)
|
617
|
+
# Send to monitoring service
|
618
|
+
ErrorTracker.notify(error, {
|
619
|
+
message: message,
|
620
|
+
retry_count: retry_count,
|
621
|
+
service: 'nats_wave'
|
622
|
+
})
|
623
|
+
|
624
|
+
# Custom retry logic for valuation-specific errors
|
625
|
+
if retry_count < 5 && error.is_a?(ValuationTimeoutError)
|
626
|
+
RetryValuationJob.set(wait: 30.seconds).perform_later(message)
|
627
|
+
end
|
628
|
+
end
|
629
|
+
end
|
630
|
+
```
|
631
|
+
|
632
|
+
## π Production Deployment
|
633
|
+
|
634
|
+
### Docker Setup
|
635
|
+
|
636
|
+
```dockerfile
|
637
|
+
# Dockerfile
|
638
|
+
FROM ruby:3.2-slim
|
639
|
+
|
640
|
+
WORKDIR /app
|
641
|
+
COPY Gemfile Gemfile.lock ./
|
642
|
+
RUN bundle install --deployment
|
643
|
+
|
644
|
+
COPY . .
|
645
|
+
|
646
|
+
# Health check
|
647
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
648
|
+
CMD curl -f http://localhost:3000/health/nats_wave || exit 1
|
649
|
+
|
650
|
+
EXPOSE 3000
|
651
|
+
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
|
652
|
+
```
|
653
|
+
|
654
|
+
### Docker Compose
|
655
|
+
|
656
|
+
```yaml
|
657
|
+
# docker-compose.yml
|
658
|
+
version: '3.8'
|
659
|
+
services:
|
660
|
+
nats:
|
661
|
+
image: nats:latest
|
662
|
+
ports:
|
663
|
+
- "4222:4222"
|
664
|
+
- "8222:8222"
|
665
|
+
command: "--http_port 8222 --js"
|
666
|
+
|
667
|
+
valuation_app:
|
668
|
+
build: .
|
669
|
+
depends_on:
|
670
|
+
- nats
|
671
|
+
- redis
|
672
|
+
- postgres
|
673
|
+
environment:
|
674
|
+
- NATS_URL=nats://nats:4222
|
675
|
+
- SERVICE_NAME=valuation_service
|
676
|
+
ports:
|
677
|
+
- "3000:3000"
|
678
|
+
```
|
679
|
+
|
680
|
+
### Kubernetes Deployment
|
681
|
+
|
682
|
+
```yaml
|
683
|
+
# k8s/deployment.yaml
|
684
|
+
apiVersion: apps/v1
|
685
|
+
kind: Deployment
|
686
|
+
metadata:
|
687
|
+
name: valuation-service
|
688
|
+
spec:
|
689
|
+
replicas: 3
|
690
|
+
selector:
|
691
|
+
matchLabels:
|
692
|
+
app: valuation-service
|
693
|
+
template:
|
694
|
+
metadata:
|
695
|
+
labels:
|
696
|
+
app: valuation-service
|
697
|
+
spec:
|
698
|
+
containers:
|
699
|
+
- name: app
|
700
|
+
image: valuation-service:latest
|
701
|
+
env:
|
702
|
+
- name: NATS_URL
|
703
|
+
value: "nats://nats-cluster:4222"
|
704
|
+
- name: SERVICE_NAME
|
705
|
+
value: "valuation-service"
|
706
|
+
ports:
|
707
|
+
- containerPort: 3000
|
708
|
+
livenessProbe:
|
709
|
+
httpGet:
|
710
|
+
path: /health/nats_wave
|
711
|
+
port: 3000
|
712
|
+
initialDelaySeconds: 30
|
713
|
+
periodSeconds: 10
|
714
|
+
```
|
715
|
+
|
716
|
+
### Separate Subscriber Process
|
717
|
+
|
718
|
+
For high-volume applications, run subscribers in dedicated processes:
|
719
|
+
|
720
|
+
```bash
|
721
|
+
# Procfile
|
722
|
+
web: bundle exec rails server -p $PORT
|
723
|
+
worker: bundle exec rails nats_wave:start
|
724
|
+
```
|
725
|
+
|
726
|
+
## π‘ Examples
|
727
|
+
|
728
|
+
### Asset Management Integration
|
729
|
+
|
730
|
+
```ruby
|
731
|
+
# app/models/asset.rb
|
732
|
+
class Asset < ApplicationRecord
|
733
|
+
include NatsWave::NatsPublishable
|
734
|
+
include NatsWave::Concerns::Mappable
|
735
|
+
|
736
|
+
nats_publishable(
|
737
|
+
actions: [:create, :update],
|
738
|
+
include_associations: [:package, :estimates, :images]
|
739
|
+
)
|
740
|
+
|
741
|
+
# Sync from inventory management system
|
742
|
+
nats_wave_maps_from 'InventoryItem',
|
743
|
+
subjects: ['inventory.items.*'],
|
744
|
+
field_mappings: {
|
745
|
+
'item_id' => 'unique_id',
|
746
|
+
'description' => 'description',
|
747
|
+
'manufacturer' => 'make',
|
748
|
+
'model_name' => 'model',
|
749
|
+
'year_manufactured' => 'year',
|
750
|
+
'serial_number' => 'serial',
|
751
|
+
'vin_number' => 'vin',
|
752
|
+
'location_code' => 'location'
|
753
|
+
},
|
754
|
+
transformations: {
|
755
|
+
'estimated_hammer' => ->(value) { value.to_f.round(2) },
|
756
|
+
'make' => ->(make) { make&.upcase&.strip }
|
757
|
+
}
|
758
|
+
|
759
|
+
# Sync from auction management
|
760
|
+
nats_wave_maps_from 'AuctionAsset',
|
761
|
+
subjects: ['auction.assets.*'],
|
762
|
+
field_mappings: {
|
763
|
+
'lot_number' => 'lot_number',
|
764
|
+
'hammer_price' => 'pw_contract_price',
|
765
|
+
'sale_date' => 'pw_contract_date'
|
766
|
+
},
|
767
|
+
conditions: {
|
768
|
+
'status' => 'sold'
|
769
|
+
}
|
770
|
+
end
|
771
|
+
|
772
|
+
# app/models/package.rb
|
773
|
+
class Package < ApplicationRecord
|
774
|
+
include NatsWave::NatsPublishable
|
775
|
+
|
776
|
+
nats_publishable(
|
777
|
+
actions: [:create, :update],
|
778
|
+
if: -> { workflow_status == 'finalized' }
|
779
|
+
)
|
780
|
+
|
781
|
+
# Listen for IMS submission confirmations
|
782
|
+
nats_wave_subscribes_to 'ims.submissions.confirmed.*' do |message|
|
783
|
+
package = Package.find_by(id: message['data']['package_id'])
|
784
|
+
package&.mark_as_submitted!
|
785
|
+
end
|
786
|
+
end
|
787
|
+
```
|
788
|
+
|
789
|
+
### Valuation Workflow
|
790
|
+
|
791
|
+
```ruby
|
792
|
+
# app/models/estimate.rb
|
793
|
+
class Estimate < ApplicationRecord
|
794
|
+
include NatsWave::NatsPublishable
|
795
|
+
include NatsWave::Concerns::Mappable
|
796
|
+
|
797
|
+
nats_publishable(
|
798
|
+
skip_attributes: [:notes, :source_data]
|
799
|
+
)
|
800
|
+
|
801
|
+
# Sync from external valuation service
|
802
|
+
nats_wave_maps_from 'ExternalValuation',
|
803
|
+
subjects: ['valuations.external.*'],
|
804
|
+
field_mappings: {
|
805
|
+
'valuation_id' => 'key',
|
806
|
+
'asset_identifier' => 'asset_id',
|
807
|
+
'appraiser_id' => 'user_id',
|
808
|
+
'estimated_value' => 'value',
|
809
|
+
'valuation_notes' => 'notes'
|
810
|
+
},
|
811
|
+
transformations: {
|
812
|
+
'value' => ->(value) { (value.to_f * 100).round }, # Convert to cents
|
813
|
+
'notes' => ->(notes) { notes&.strip }
|
814
|
+
},
|
815
|
+
unique_fields: [:key, :asset_id, :user_id]
|
816
|
+
|
817
|
+
# Listen for market price updates
|
818
|
+
nats_wave_subscribes_to 'market.prices.*' do |message|
|
819
|
+
MarketPriceAnalyzer.update_estimates_based_on_market(message)
|
820
|
+
end
|
821
|
+
end
|
822
|
+
|
823
|
+
# app/models/valuation_request.rb
|
824
|
+
class ValuationRequest < ApplicationRecord
|
825
|
+
include NatsWave::NatsPublishable
|
826
|
+
|
827
|
+
nats_publishable(
|
828
|
+
actions: [:create, :update]
|
829
|
+
)
|
830
|
+
|
831
|
+
# Process valuation assignments
|
832
|
+
nats_wave_subscribes_to 'valuations.assignments.*' do |message|
|
833
|
+
ValuationAssignmentProcessor.process_assignment(message)
|
834
|
+
end
|
835
|
+
end
|
836
|
+
```
|
837
|
+
|
838
|
+
### Organization & User Management
|
839
|
+
|
840
|
+
```ruby
|
841
|
+
# app/models/user.rb
|
842
|
+
class User < ApplicationRecord
|
843
|
+
include NatsWave::NatsPublishable
|
844
|
+
include NatsWave::Concerns::Mappable
|
845
|
+
|
846
|
+
nats_publishable(
|
847
|
+
skip_attributes: [:api_auth_key, :source_details]
|
848
|
+
)
|
849
|
+
|
850
|
+
# Sync from authentication service
|
851
|
+
nats_wave_maps_from 'AuthUser',
|
852
|
+
subjects: ['auth.users.*'],
|
853
|
+
field_mappings: {
|
854
|
+
'user_id' => 'key',
|
855
|
+
'email_address' => 'email',
|
856
|
+
'full_name' => 'name',
|
857
|
+
'is_active' => 'active'
|
858
|
+
},
|
859
|
+
transformations: {
|
860
|
+
'email' => ->(email) { email&.downcase },
|
861
|
+
'active' => ->(active) { active == 'true' || active == true }
|
862
|
+
},
|
863
|
+
unique_fields: [:key, :email]
|
864
|
+
end
|
865
|
+
|
866
|
+
# app/models/organization.rb
|
867
|
+
class Organization < ApplicationRecord
|
868
|
+
include NatsWave::NatsPublishable
|
869
|
+
include NatsWave::Concerns::Mappable
|
870
|
+
|
871
|
+
nats_publishable(
|
872
|
+
skip_attributes: [:api_auth_key, :workflow_config]
|
873
|
+
)
|
874
|
+
|
875
|
+
# Sync from CRM system
|
876
|
+
nats_wave_maps_from 'CRMOrganization',
|
877
|
+
subjects: ['crm.organizations.*'],
|
878
|
+
field_mappings: {
|
879
|
+
'organization_id' => 'key',
|
880
|
+
'company_name' => 'name',
|
881
|
+
'primary_contact' => 'contact',
|
882
|
+
'email_address' => 'email',
|
883
|
+
'phone_number' => 'phone',
|
884
|
+
'is_active' => 'active'
|
885
|
+
},
|
886
|
+
unique_fields: [:key, :name]
|
887
|
+
end
|
888
|
+
```
|
889
|
+
|
890
|
+
### Image and Media Processing
|
891
|
+
|
892
|
+
```ruby
|
893
|
+
# app/models/image.rb
|
894
|
+
class Image < ApplicationRecord
|
895
|
+
include NatsWave::NatsPublishable
|
896
|
+
|
897
|
+
nats_publishable(
|
898
|
+
actions: [:create, :update],
|
899
|
+
skip_attributes: [:source_data, :import_url],
|
900
|
+
if: -> { !deleted? && sizes.present? }
|
901
|
+
)
|
902
|
+
|
903
|
+
# Listen for image processing completion
|
904
|
+
nats_wave_subscribes_to 'media.processing.completed.*' do |message|
|
905
|
+
ImageProcessingService.handle_completion(message)
|
906
|
+
end
|
907
|
+
|
908
|
+
# Listen for IMS image submissions
|
909
|
+
nats_wave_subscribes_to 'ims.images.*' do |message|
|
910
|
+
IMSImageProcessor.process_submission(message)
|
911
|
+
end
|
912
|
+
end
|
913
|
+
|
914
|
+
# app/models/video.rb
|
915
|
+
class Video < ApplicationRecord
|
916
|
+
include NatsWave::NatsPublishable
|
917
|
+
|
918
|
+
nats_publishable(
|
919
|
+
actions: [:create, :update],
|
920
|
+
if: -> { !deleted? && url.present? }
|
921
|
+
)
|
922
|
+
|
923
|
+
# Listen for video processing updates
|
924
|
+
nats_wave_subscribes_to 'media.videos.*' do |message|
|
925
|
+
VideoProcessingService.handle_update(message)
|
926
|
+
end
|
927
|
+
end
|
928
|
+
```
|
929
|
+
|
930
|
+
## π€ Contributing
|
931
|
+
|
932
|
+
We welcome contributions! Please see our [Contributing Guide](CODE_OF_CONDUCT.md) for details.
|
933
|
+
|
934
|
+
### Development Setup
|
935
|
+
|
936
|
+
```bash
|
937
|
+
git clone https://github.com/PurpleWave/nats_wave.git
|
938
|
+
cd nats_wave
|
939
|
+
bundle install
|
940
|
+
|
941
|
+
# Start NATS server for testing
|
942
|
+
docker run -p 4222:4222 -p 8222:8222 nats:latest --http_port 8222
|
943
|
+
|
944
|
+
# Run tests
|
945
|
+
bundle exec rspec
|
946
|
+
|
947
|
+
# Run linting
|
948
|
+
bundle exec rubocop
|
949
|
+
```
|
950
|
+
|
951
|
+
### Testing
|
952
|
+
|
953
|
+
```bash
|
954
|
+
# Run all tests
|
955
|
+
bundle exec rspec
|
956
|
+
|
957
|
+
# Run specific test files
|
958
|
+
bundle exec rspec spec/nats_wave/publisher_spec.rb
|
959
|
+
|
960
|
+
# Run with coverage
|
961
|
+
COVERAGE=true bundle exec rspec
|
962
|
+
```
|
963
|
+
|
964
|
+
## π Changelog
|
965
|
+
|
966
|
+
See [CHANGELOG.md](CHANGELOG.md) for details about changes in each version.
|
967
|
+
|
968
|
+
## π License
|
969
|
+
|
970
|
+
This gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
971
|
+
|
972
|
+
## πββοΈ Support
|
973
|
+
|
974
|
+
- **Documentation**: [GitHub Wiki](https://github.com/PurpleWave/nats_wave/wiki)
|
975
|
+
- **Issues**: [GitHub Issues](https://github.com/PurpleWave/nats_wave/issues)
|
976
|
+
- **Discussions**: [GitHub Discussions](https://github.com/PurpleWave/nats_wave/discussions)
|
977
|
+
|
978
|
+
## π’ About Purple Wave
|
979
|
+
|
980
|
+
NatsWave is developed and maintained by [Purple Wave](https://purplewave.com) to enable seamless communication between
|
981
|
+
our microservices and teams in the asset valuation and auction industry.
|
982
|
+
|
983
|
+
---
|
984
|
+
|
985
|
+
**Made with β€οΈ by Jeffrey Dabo**
|