smart_message 0.0.1 → 0.0.3
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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +31 -1
- data/Gemfile.lock +6 -1
- data/README.md +103 -21
- data/docs/README.md +5 -4
- data/docs/architecture.md +41 -8
- data/docs/dispatcher.md +52 -16
- data/docs/getting-started.md +64 -2
- data/docs/message_processing.md +423 -0
- data/docs/proc_handlers_summary.md +247 -0
- data/docs/properties.md +471 -0
- data/docs/transports.md +202 -8
- data/examples/04_redis_smart_home_iot.rb +649 -0
- data/examples/05_proc_handlers.rb +181 -0
- data/examples/README.md +118 -3
- data/examples/smart_home_iot_dataflow.md +257 -0
- data/lib/smart_message/base.rb +108 -4
- data/lib/smart_message/dispatcher.rb +22 -6
- data/lib/smart_message/property_descriptions.rb +41 -0
- data/lib/smart_message/transport/redis_transport.rb +190 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +1 -0
- data/lib/smart_message/version.rb +1 -1
- data/smart_message.gemspec +1 -0
- metadata +23 -1
data/docs/properties.md
ADDED
@@ -0,0 +1,471 @@
|
|
1
|
+
# SmartMessage Property System
|
2
|
+
|
3
|
+
The SmartMessage property system builds on Hashie::Dash to provide a robust, declarative way to define message attributes. This document covers all available property options and features.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
- [Basic Property Definition](#basic-property-definition)
|
7
|
+
- [Class-Level Description](#class-level-description)
|
8
|
+
- [Property Options](#property-options)
|
9
|
+
- [Accessing Property Information](#accessing-property-information)
|
10
|
+
- [Hashie Extensions](#hashie-extensions)
|
11
|
+
- [Examples](#examples)
|
12
|
+
|
13
|
+
## Basic Property Definition
|
14
|
+
|
15
|
+
Properties are defined using the `property` method in your message class:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
class MyMessage < SmartMessage::Base
|
19
|
+
property :field_name
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
## Class-Level Description
|
24
|
+
|
25
|
+
In addition to property-level descriptions, you can add a description for the entire message class:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class OrderMessage < SmartMessage::Base
|
29
|
+
description "Handles order processing and fulfillment workflow"
|
30
|
+
|
31
|
+
property :order_id, description: "Unique order identifier"
|
32
|
+
property :amount, description: "Total amount in cents"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Access the class description
|
36
|
+
OrderMessage.description # => "Handles order processing and fulfillment workflow"
|
37
|
+
|
38
|
+
# Class descriptions can also be set after class definition
|
39
|
+
class PaymentMessage < SmartMessage::Base
|
40
|
+
property :payment_id
|
41
|
+
end
|
42
|
+
|
43
|
+
PaymentMessage.description "Processes payment transactions"
|
44
|
+
PaymentMessage.description # => "Processes payment transactions"
|
45
|
+
|
46
|
+
# Can be set within config block
|
47
|
+
class NotificationMessage < SmartMessage::Base
|
48
|
+
config do
|
49
|
+
description "Sends notifications to users"
|
50
|
+
transport MyTransport.new
|
51
|
+
serializer MySerializer.new
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
Class descriptions are useful for:
|
57
|
+
- Documenting the overall purpose of a message class
|
58
|
+
- Providing context for code generation tools
|
59
|
+
- Integration with documentation systems
|
60
|
+
- API documentation generation
|
61
|
+
|
62
|
+
Note: Class descriptions are not inherited by subclasses. Each class maintains its own description.
|
63
|
+
|
64
|
+
## Property Options
|
65
|
+
|
66
|
+
SmartMessage supports all Hashie::Dash property options plus additional features:
|
67
|
+
|
68
|
+
### 1. Default Values
|
69
|
+
|
70
|
+
Specify a default value for a property when not provided during initialization:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class OrderMessage < SmartMessage::Base
|
74
|
+
# Static default
|
75
|
+
property :status, default: 'pending'
|
76
|
+
|
77
|
+
# Dynamic default using a Proc
|
78
|
+
property :created_at, default: -> { Time.now }
|
79
|
+
|
80
|
+
# Default array
|
81
|
+
property :items, default: []
|
82
|
+
end
|
83
|
+
|
84
|
+
order = OrderMessage.new
|
85
|
+
order.status # => 'pending'
|
86
|
+
order.created_at # => Current time
|
87
|
+
order.items # => []
|
88
|
+
```
|
89
|
+
|
90
|
+
### 2. Required Properties
|
91
|
+
|
92
|
+
Mark properties as required to ensure they're provided during initialization:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class PaymentMessage < SmartMessage::Base
|
96
|
+
property :payment_id, required: true
|
97
|
+
property :amount, required: true
|
98
|
+
property :note # Optional
|
99
|
+
end
|
100
|
+
|
101
|
+
# This raises ArgumentError: The property 'payment_id' is required
|
102
|
+
PaymentMessage.new(amount: 100)
|
103
|
+
|
104
|
+
# This works
|
105
|
+
PaymentMessage.new(payment_id: 'PAY-123', amount: 100)
|
106
|
+
```
|
107
|
+
|
108
|
+
### 3. Property Transformation
|
109
|
+
|
110
|
+
Transform property values when they're set:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
class UserMessage < SmartMessage::Base
|
114
|
+
property :email, transform_with: ->(v) { v.to_s.downcase }
|
115
|
+
property :name, transform_with: ->(v) { v.to_s.strip.capitalize }
|
116
|
+
property :tags, transform_with: ->(v) { Array(v).map(&:to_s) }
|
117
|
+
end
|
118
|
+
|
119
|
+
user = UserMessage.new(
|
120
|
+
email: 'USER@EXAMPLE.COM',
|
121
|
+
name: ' john ',
|
122
|
+
tags: 'admin'
|
123
|
+
)
|
124
|
+
|
125
|
+
user.email # => 'user@example.com'
|
126
|
+
user.name # => 'John'
|
127
|
+
user.tags # => ['admin']
|
128
|
+
```
|
129
|
+
|
130
|
+
### 4. Property Translation (from Hashie::Extensions::Dash::PropertyTranslation)
|
131
|
+
|
132
|
+
Map external field names to internal property names:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
class ApiMessage < SmartMessage::Base
|
136
|
+
property :user_id, from: :userId
|
137
|
+
property :order_date, from: 'orderDate'
|
138
|
+
property :total_amount, from: [:totalAmount, :total, :amount]
|
139
|
+
end
|
140
|
+
|
141
|
+
# All of these work
|
142
|
+
msg1 = ApiMessage.new(userId: 123)
|
143
|
+
msg2 = ApiMessage.new(user_id: 123)
|
144
|
+
msg3 = ApiMessage.new('orderDate' => '2024-01-01')
|
145
|
+
msg4 = ApiMessage.new(totalAmount: 100) # or total: 100, or amount: 100
|
146
|
+
|
147
|
+
msg1.user_id # => 123
|
148
|
+
```
|
149
|
+
|
150
|
+
### 5. Type Coercion (from Hashie::Extensions::Coercion)
|
151
|
+
|
152
|
+
Automatically coerce property values to specific types:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
class TypedMessage < SmartMessage::Base
|
156
|
+
property :count
|
157
|
+
property :price
|
158
|
+
property :active
|
159
|
+
property :tags
|
160
|
+
property :metadata
|
161
|
+
|
162
|
+
coerce_key :count, Integer
|
163
|
+
coerce_key :price, Float
|
164
|
+
coerce_key :active, ->(v) { v.to_s.downcase == 'true' }
|
165
|
+
coerce_key :tags, Array[String]
|
166
|
+
coerce_key :metadata, Hash
|
167
|
+
end
|
168
|
+
|
169
|
+
msg = TypedMessage.new(
|
170
|
+
count: '42',
|
171
|
+
price: '19.99',
|
172
|
+
active: 'yes',
|
173
|
+
tags: 'important',
|
174
|
+
metadata: nil
|
175
|
+
)
|
176
|
+
|
177
|
+
msg.count # => 42 (Integer)
|
178
|
+
msg.price # => 19.99 (Float)
|
179
|
+
msg.active # => false (Boolean logic)
|
180
|
+
msg.tags # => ['important'] (Array)
|
181
|
+
msg.metadata # => {} (Hash)
|
182
|
+
```
|
183
|
+
|
184
|
+
### 6. Property Descriptions (SmartMessage Enhancement)
|
185
|
+
|
186
|
+
Add human-readable descriptions to document your properties for dynamic LLM integration:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class DocumentedMessage < SmartMessage::Base
|
190
|
+
property :transaction_id,
|
191
|
+
required: true,
|
192
|
+
description: "Unique identifier for the transaction"
|
193
|
+
|
194
|
+
property :amount,
|
195
|
+
transform_with: ->(v) { BigDecimal(v.to_s) },
|
196
|
+
description: "Transaction amount in the smallest currency unit"
|
197
|
+
|
198
|
+
property :currency,
|
199
|
+
default: 'USD',
|
200
|
+
description: "ISO 4217 currency code"
|
201
|
+
|
202
|
+
property :status,
|
203
|
+
default: 'pending',
|
204
|
+
description: "Current transaction status: pending, completed, failed"
|
205
|
+
|
206
|
+
property :metadata,
|
207
|
+
default: {},
|
208
|
+
description: "Additional transaction metadata as key-value pairs"
|
209
|
+
end
|
210
|
+
|
211
|
+
# Access descriptions programmatically
|
212
|
+
DocumentedMessage.property_description(:amount)
|
213
|
+
# => "Transaction amount in the smallest currency unit"
|
214
|
+
|
215
|
+
DocumentedMessage.property_descriptions
|
216
|
+
# => {
|
217
|
+
# transaction_id: "Unique identifier for the transaction",
|
218
|
+
# amount: "Transaction amount in the smallest currency unit",
|
219
|
+
# currency: "ISO 4217 currency code",
|
220
|
+
# status: "Current transaction status: pending, completed, failed",
|
221
|
+
# metadata: "Additional transaction metadata as key-value pairs"
|
222
|
+
# }
|
223
|
+
|
224
|
+
DocumentedMessage.described_properties
|
225
|
+
# => [:transaction_id, :amount, :currency, :status, :metadata]
|
226
|
+
```
|
227
|
+
|
228
|
+
## Accessing Property Information
|
229
|
+
|
230
|
+
SmartMessage provides several methods to introspect properties:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
class IntrospectionExample < SmartMessage::Base
|
234
|
+
property :id, required: true, description: "Unique identifier"
|
235
|
+
property :name, description: "Display name"
|
236
|
+
property :created_at, default: -> { Time.now }
|
237
|
+
property :tags
|
238
|
+
end
|
239
|
+
|
240
|
+
# Instance methods
|
241
|
+
instance = IntrospectionExample.new(id: 1, name: "Test")
|
242
|
+
instance.fields # => Set[:id, :name, :created_at, :tags]
|
243
|
+
instance.to_h # => Hash of all properties and values
|
244
|
+
|
245
|
+
# Class methods
|
246
|
+
IntrospectionExample.fields # => Set[:id, :name, :created_at, :tags]
|
247
|
+
IntrospectionExample.property_descriptions # => Hash of descriptions
|
248
|
+
IntrospectionExample.described_properties # => [:id, :name]
|
249
|
+
```
|
250
|
+
|
251
|
+
## Hashie Extensions
|
252
|
+
|
253
|
+
SmartMessage::Base automatically includes these Hashie extensions:
|
254
|
+
|
255
|
+
### 1. DeepMerge
|
256
|
+
Allows deep merging of nested hash properties:
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
msg = MyMessage.new(config: { a: 1, b: { c: 2 } })
|
260
|
+
msg.deep_merge(config: { b: { d: 3 } })
|
261
|
+
# => config: { a: 1, b: { c: 2, d: 3 } }
|
262
|
+
```
|
263
|
+
|
264
|
+
### 2. IgnoreUndeclared
|
265
|
+
Silently ignores properties that haven't been declared:
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
# Won't raise an error for unknown properties
|
269
|
+
msg = MyMessage.new(known: 'value', unknown: 'ignored')
|
270
|
+
```
|
271
|
+
|
272
|
+
### 3. IndifferentAccess
|
273
|
+
Access properties with strings or symbols:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
msg = MyMessage.new('name' => 'John')
|
277
|
+
msg[:name] # => 'John'
|
278
|
+
msg['name'] # => 'John'
|
279
|
+
msg.name # => 'John'
|
280
|
+
```
|
281
|
+
|
282
|
+
### 4. MethodAccess
|
283
|
+
Access properties as methods:
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
msg = MyMessage.new(name: 'John')
|
287
|
+
msg.name # => 'John'
|
288
|
+
msg.name = 'Jane'
|
289
|
+
msg.name # => 'Jane'
|
290
|
+
```
|
291
|
+
|
292
|
+
### 5. MergeInitializer
|
293
|
+
Allows initializing with merged hash values:
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
defaults = { status: 'active', retries: 3 }
|
297
|
+
msg = MyMessage.new(defaults.merge(status: 'pending'))
|
298
|
+
# => status: 'pending', retries: 3
|
299
|
+
```
|
300
|
+
|
301
|
+
## Examples
|
302
|
+
|
303
|
+
### Complete Example: Order Processing Message
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
class OrderProcessingMessage < SmartMessage::Base
|
307
|
+
description "Manages the complete order lifecycle from placement to delivery"
|
308
|
+
|
309
|
+
# Required fields with descriptions
|
310
|
+
property :order_id,
|
311
|
+
required: true,
|
312
|
+
description: "Unique order identifier from the ordering system"
|
313
|
+
|
314
|
+
property :customer_id,
|
315
|
+
required: true,
|
316
|
+
description: "Customer who placed the order"
|
317
|
+
|
318
|
+
# Amount with transformation and description
|
319
|
+
property :total_amount,
|
320
|
+
transform_with: ->(v) { BigDecimal(v.to_s) },
|
321
|
+
description: "Total order amount including tax and shipping"
|
322
|
+
|
323
|
+
# Status with default and validation description
|
324
|
+
property :status,
|
325
|
+
default: 'pending',
|
326
|
+
description: "Order status: pending, processing, shipped, delivered, cancelled"
|
327
|
+
|
328
|
+
# Items with coercion
|
329
|
+
property :items,
|
330
|
+
default: [],
|
331
|
+
description: "Array of order line items"
|
332
|
+
|
333
|
+
# Timestamps with dynamic defaults
|
334
|
+
property :created_at,
|
335
|
+
default: -> { Time.now },
|
336
|
+
description: "When the order was created"
|
337
|
+
|
338
|
+
property :updated_at,
|
339
|
+
default: -> { Time.now },
|
340
|
+
description: "Last modification timestamp"
|
341
|
+
|
342
|
+
# Optional fields
|
343
|
+
property :notes,
|
344
|
+
description: "Optional order notes or special instructions"
|
345
|
+
|
346
|
+
property :shipping_address,
|
347
|
+
description: "Shipping address as a nested hash"
|
348
|
+
|
349
|
+
# Field translation for external APIs
|
350
|
+
property :external_ref,
|
351
|
+
from: [:externalReference, :ext_ref],
|
352
|
+
description: "Reference ID from external system"
|
353
|
+
end
|
354
|
+
|
355
|
+
# Usage
|
356
|
+
order = OrderProcessingMessage.new(
|
357
|
+
order_id: 'ORD-2024-001',
|
358
|
+
customer_id: 'CUST-123',
|
359
|
+
total_amount: '149.99',
|
360
|
+
items: [
|
361
|
+
{ sku: 'WIDGET-A', quantity: 2, price: 49.99 },
|
362
|
+
{ sku: 'WIDGET-B', quantity: 1, price: 50.01 }
|
363
|
+
],
|
364
|
+
shipping_address: {
|
365
|
+
street: '123 Main St',
|
366
|
+
city: 'Springfield',
|
367
|
+
state: 'IL',
|
368
|
+
zip: '62701'
|
369
|
+
},
|
370
|
+
externalReference: 'EXT-789' # Note: uses translated field name
|
371
|
+
)
|
372
|
+
|
373
|
+
# Access properties
|
374
|
+
order.order_id # => 'ORD-2024-001'
|
375
|
+
order.total_amount # => BigDecimal('149.99')
|
376
|
+
order.status # => 'pending' (default)
|
377
|
+
order.external_ref # => 'EXT-789' (translated)
|
378
|
+
order.created_at # => Time object
|
379
|
+
|
380
|
+
# Get class and property information
|
381
|
+
OrderProcessingMessage.description
|
382
|
+
# => "Manages the complete order lifecycle from placement to delivery"
|
383
|
+
|
384
|
+
OrderProcessingMessage.property_description(:total_amount)
|
385
|
+
# => "Total order amount including tax and shipping"
|
386
|
+
|
387
|
+
OrderProcessingMessage.property_descriptions.keys
|
388
|
+
# => [:order_id, :customer_id, :total_amount, :status, :items, ...]
|
389
|
+
```
|
390
|
+
|
391
|
+
### Example: API Integration Message
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
class ApiWebhookMessage < SmartMessage::Base
|
395
|
+
# Handle different API naming conventions
|
396
|
+
property :event_type,
|
397
|
+
from: [:eventType, :event, :type],
|
398
|
+
required: true,
|
399
|
+
description: "Type of webhook event"
|
400
|
+
|
401
|
+
property :payload,
|
402
|
+
required: true,
|
403
|
+
description: "Event payload data"
|
404
|
+
|
405
|
+
property :timestamp,
|
406
|
+
from: [:timestamp, :created_at, :occurredAt],
|
407
|
+
transform_with: ->(v) { Time.parse(v.to_s) },
|
408
|
+
description: "When the event occurred"
|
409
|
+
|
410
|
+
property :retry_count,
|
411
|
+
from: :retryCount,
|
412
|
+
default: 0,
|
413
|
+
transform_with: ->(v) { v.to_i },
|
414
|
+
description: "Number of delivery attempts"
|
415
|
+
|
416
|
+
property :signature,
|
417
|
+
description: "HMAC signature for webhook validation"
|
418
|
+
end
|
419
|
+
|
420
|
+
# Can initialize with various field names
|
421
|
+
webhook1 = ApiWebhookMessage.new(
|
422
|
+
eventType: 'order.completed',
|
423
|
+
payload: { order_id: 123 },
|
424
|
+
occurredAt: '2024-01-01T10:00:00Z',
|
425
|
+
retryCount: '2'
|
426
|
+
)
|
427
|
+
|
428
|
+
webhook2 = ApiWebhookMessage.new(
|
429
|
+
type: 'order.completed', # Alternative field name
|
430
|
+
payload: { order_id: 123 },
|
431
|
+
timestamp: Time.now, # Alternative field name
|
432
|
+
retry_count: 2 # Internal field name
|
433
|
+
)
|
434
|
+
```
|
435
|
+
|
436
|
+
## Best Practices
|
437
|
+
|
438
|
+
1. **Always add descriptions** to document the purpose and format of each property
|
439
|
+
2. **Use required properties** for fields that must be present for valid messages
|
440
|
+
3. **Provide sensible defaults** for optional fields to reduce boilerplate
|
441
|
+
4. **Use transformations** to ensure data consistency and type safety
|
442
|
+
5. **Leverage field translation** when integrating with external APIs that use different naming conventions
|
443
|
+
6. **Document valid values** in descriptions for enum-like fields (e.g., status fields)
|
444
|
+
7. **Use type coercion** for fields that may come from untrusted sources (like HTTP parameters)
|
445
|
+
|
446
|
+
## Property Option Compatibility
|
447
|
+
|
448
|
+
Multiple options can be combined on a single property:
|
449
|
+
|
450
|
+
```ruby
|
451
|
+
property :amount,
|
452
|
+
required: true,
|
453
|
+
from: [:amount, :total, :value],
|
454
|
+
transform_with: ->(v) { BigDecimal(v.to_s) },
|
455
|
+
description: "Transaction amount in cents"
|
456
|
+
```
|
457
|
+
|
458
|
+
The processing order is:
|
459
|
+
1. Field translation (from)
|
460
|
+
2. Default value (if not provided)
|
461
|
+
3. Required validation
|
462
|
+
4. Type coercion
|
463
|
+
5. Transformation
|
464
|
+
6. Value assignment
|
465
|
+
|
466
|
+
## Limitations
|
467
|
+
|
468
|
+
- Property names must be valid Ruby method names
|
469
|
+
- The `_sm_` prefix is reserved for internal SmartMessage properties
|
470
|
+
- Descriptions are metadata only and don't affect runtime behavior
|
471
|
+
- Some Hashie options may conflict if used incorrectly (e.g., required with default)
|
data/docs/transports.md
CHANGED
@@ -125,6 +125,194 @@ messages.each do |msg|
|
|
125
125
|
end
|
126
126
|
```
|
127
127
|
|
128
|
+
### Redis Transport
|
129
|
+
|
130
|
+
Production-ready Redis pub/sub transport for distributed messaging.
|
131
|
+
|
132
|
+
**Features:**
|
133
|
+
- Redis pub/sub messaging
|
134
|
+
- Automatic channel management using message class names
|
135
|
+
- Thread-safe subscriber management
|
136
|
+
- Connection resilience with automatic reconnection
|
137
|
+
- Configurable connection parameters
|
138
|
+
- Background message subscription threads
|
139
|
+
|
140
|
+
**Usage:**
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
# Basic Redis configuration
|
144
|
+
transport = SmartMessage::Transport.create(:redis,
|
145
|
+
url: 'redis://localhost:6379',
|
146
|
+
db: 0
|
147
|
+
)
|
148
|
+
|
149
|
+
# Production configuration with custom options
|
150
|
+
transport = SmartMessage::Transport.create(:redis,
|
151
|
+
url: 'redis://prod-redis:6379',
|
152
|
+
db: 1,
|
153
|
+
auto_subscribe: true,
|
154
|
+
reconnect_attempts: 5,
|
155
|
+
reconnect_delay: 2
|
156
|
+
)
|
157
|
+
|
158
|
+
# Configure in message class
|
159
|
+
class OrderMessage < SmartMessage::Base
|
160
|
+
property :order_id
|
161
|
+
property :customer_id
|
162
|
+
property :amount
|
163
|
+
|
164
|
+
config do
|
165
|
+
transport SmartMessage::Transport.create(:redis,
|
166
|
+
url: 'redis://localhost:6379',
|
167
|
+
db: 1
|
168
|
+
)
|
169
|
+
serializer SmartMessage::Serializer::JSON.new
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.process(message_header, message_payload)
|
173
|
+
order_data = JSON.parse(message_payload)
|
174
|
+
order = new(order_data)
|
175
|
+
puts "Processing order #{order.order_id} for $#{order.amount}"
|
176
|
+
# Your business logic here
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Subscribe to messages (creates Redis subscription to "OrderMessage" channel)
|
181
|
+
OrderMessage.subscribe
|
182
|
+
|
183
|
+
# Publish messages (publishes to "OrderMessage" Redis channel)
|
184
|
+
order = OrderMessage.new(
|
185
|
+
order_id: "ORD-123",
|
186
|
+
customer_id: "CUST-456",
|
187
|
+
amount: 99.99
|
188
|
+
)
|
189
|
+
order.publish
|
190
|
+
```
|
191
|
+
|
192
|
+
**Options:**
|
193
|
+
- `url` (String): Redis connection URL (default: 'redis://localhost:6379')
|
194
|
+
- `db` (Integer): Redis database number (default: 0)
|
195
|
+
- `auto_subscribe` (Boolean): Automatically start subscriber thread (default: true)
|
196
|
+
- `reconnect_attempts` (Integer): Number of reconnection attempts (default: 5)
|
197
|
+
- `reconnect_delay` (Integer): Delay between reconnection attempts in seconds (default: 1)
|
198
|
+
- `debug` (Boolean): Enable debug output (default: false)
|
199
|
+
|
200
|
+
**Channel Naming:**
|
201
|
+
|
202
|
+
The Redis transport uses the message class name as the Redis channel name. This provides automatic routing:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
class UserMessage < SmartMessage::Base
|
206
|
+
# Messages published to/from Redis channel "UserMessage"
|
207
|
+
end
|
208
|
+
|
209
|
+
class AdminMessage < SmartMessage::Base
|
210
|
+
# Messages published to/from Redis channel "AdminMessage"
|
211
|
+
end
|
212
|
+
|
213
|
+
class OrderProcessing::PaymentMessage < SmartMessage::Base
|
214
|
+
# Messages published to/from Redis channel "OrderProcessing::PaymentMessage"
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
**Connection Management:**
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
transport = SmartMessage::Transport.create(:redis, url: 'redis://localhost:6379')
|
222
|
+
|
223
|
+
# Check connection status
|
224
|
+
puts transport.connected? # => true/false
|
225
|
+
|
226
|
+
# Manual connection management
|
227
|
+
transport.connect
|
228
|
+
transport.disconnect
|
229
|
+
|
230
|
+
# The transport automatically reconnects on connection failures
|
231
|
+
```
|
232
|
+
|
233
|
+
**Multi-Message Type Support:**
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
# Different message types can share the same Redis transport
|
237
|
+
redis_transport = SmartMessage::Transport.create(:redis,
|
238
|
+
url: 'redis://localhost:6379',
|
239
|
+
auto_subscribe: true
|
240
|
+
)
|
241
|
+
|
242
|
+
# Configure multiple message classes to use the same transport
|
243
|
+
[OrderMessage, PaymentMessage, ShippingMessage].each do |msg_class|
|
244
|
+
msg_class.config do
|
245
|
+
transport redis_transport
|
246
|
+
serializer SmartMessage::Serializer::JSON.new
|
247
|
+
end
|
248
|
+
|
249
|
+
# Subscribe to each message type (creates separate Redis subscriptions)
|
250
|
+
msg_class.subscribe
|
251
|
+
end
|
252
|
+
|
253
|
+
# Publishing to any message type routes to its specific Redis channel
|
254
|
+
OrderMessage.new(order_id: "123").publish # -> "OrderMessage" channel
|
255
|
+
PaymentMessage.new(amount: 50.0).publish # -> "PaymentMessage" channel
|
256
|
+
ShippingMessage.new(tracking: "ABC").publish # -> "ShippingMessage" channel
|
257
|
+
```
|
258
|
+
|
259
|
+
**Error Handling and Resilience:**
|
260
|
+
|
261
|
+
The Redis transport includes built-in error handling:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
# Automatic reconnection on connection failures
|
265
|
+
transport = SmartMessage::Transport.create(:redis,
|
266
|
+
url: 'redis://localhost:6379',
|
267
|
+
reconnect_attempts: 5, # Try 5 times to reconnect
|
268
|
+
reconnect_delay: 2 # Wait 2 seconds between attempts
|
269
|
+
)
|
270
|
+
|
271
|
+
# Connection failures during publishing will trigger automatic retry
|
272
|
+
# If all reconnection attempts fail, the original error is raised
|
273
|
+
```
|
274
|
+
|
275
|
+
**Production Deployment:**
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
# Production Redis configuration
|
279
|
+
class ProductionMessage < SmartMessage::Base
|
280
|
+
config do
|
281
|
+
transport SmartMessage::Transport.create(:redis,
|
282
|
+
url: ENV['REDIS_URL'] || 'redis://localhost:6379',
|
283
|
+
db: ENV['REDIS_DB']&.to_i || 0,
|
284
|
+
auto_subscribe: true,
|
285
|
+
reconnect_attempts: 10,
|
286
|
+
reconnect_delay: 5
|
287
|
+
)
|
288
|
+
serializer SmartMessage::Serializer::JSON.new
|
289
|
+
logger Logger.new(STDOUT)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
**Testing with Redis:**
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
# Test configuration (using separate Redis database)
|
298
|
+
class TestMessage < SmartMessage::Base
|
299
|
+
config do
|
300
|
+
transport SmartMessage::Transport.create(:redis,
|
301
|
+
url: 'redis://localhost:6379',
|
302
|
+
db: 15, # Use separate database for tests
|
303
|
+
auto_subscribe: true
|
304
|
+
)
|
305
|
+
serializer SmartMessage::Serializer::JSON.new
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# In your test setup
|
310
|
+
def setup
|
311
|
+
# Clear test database
|
312
|
+
Redis.new(url: 'redis://localhost:6379', db: 15).flushdb
|
313
|
+
end
|
314
|
+
```
|
315
|
+
|
128
316
|
## Transport Interface
|
129
317
|
|
130
318
|
All transports must implement the `SmartMessage::Transport::Base` interface:
|
@@ -188,19 +376,24 @@ transport.receive(message_header, message_payload) # protected method
|
|
188
376
|
Register custom transports for easy creation:
|
189
377
|
|
190
378
|
```ruby
|
191
|
-
# Register
|
192
|
-
SmartMessage::Transport.register(:redis, RedisTransport)
|
379
|
+
# Register custom transport classes
|
193
380
|
SmartMessage::Transport.register(:kafka, KafkaTransport)
|
381
|
+
SmartMessage::Transport.register(:webhook, WebhookTransport)
|
194
382
|
|
195
|
-
# List all registered transports
|
383
|
+
# List all registered transports (includes built-ins)
|
196
384
|
puts SmartMessage::Transport.available
|
197
|
-
# => [:stdout, :memory, :redis, :kafka]
|
385
|
+
# => [:stdout, :memory, :redis, :kafka, :webhook]
|
198
386
|
|
199
|
-
# Create instances
|
387
|
+
# Create instances of built-in transports
|
200
388
|
redis_transport = SmartMessage::Transport.create(:redis,
|
201
389
|
url: "redis://localhost:6379"
|
202
390
|
)
|
203
391
|
|
392
|
+
memory_transport = SmartMessage::Transport.create(:memory,
|
393
|
+
auto_process: true
|
394
|
+
)
|
395
|
+
|
396
|
+
# Create instances of custom transports
|
204
397
|
kafka_transport = SmartMessage::Transport.create(:kafka,
|
205
398
|
servers: ["localhost:9092"]
|
206
399
|
)
|
@@ -311,12 +504,13 @@ SmartMessage::Transport.create(:memory,
|
|
311
504
|
max_messages: 1000
|
312
505
|
)
|
313
506
|
|
314
|
-
#
|
507
|
+
# Redis specific
|
315
508
|
SmartMessage::Transport.create(:redis,
|
316
509
|
url: "redis://localhost:6379",
|
317
510
|
db: 1,
|
318
|
-
|
319
|
-
|
511
|
+
auto_subscribe: true,
|
512
|
+
reconnect_attempts: 5,
|
513
|
+
reconnect_delay: 2
|
320
514
|
)
|
321
515
|
```
|
322
516
|
|