smart_message 0.0.4 → 0.0.6
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/CHANGELOG.md +118 -0
- data/Gemfile.lock +1 -1
- data/README.md +299 -10
- data/docs/README.md +1 -0
- data/docs/addressing.md +364 -0
- data/docs/architecture.md +2 -0
- data/docs/examples.md +2 -0
- data/docs/getting-started.md +49 -0
- data/docs/properties.md +213 -7
- data/examples/01_point_to_point_orders.rb +27 -11
- data/examples/02_publish_subscribe_events.rb +16 -7
- data/examples/03_many_to_many_chat.rb +56 -22
- data/examples/04_redis_smart_home_iot.rb +48 -21
- data/examples/05_proc_handlers.rb +12 -5
- data/examples/06_custom_logger_example.rb +34 -13
- data/examples/07_error_handling_scenarios.rb +477 -0
- data/examples/08_entity_addressing_basic.rb +366 -0
- data/examples/08_entity_addressing_with_filtering.rb +418 -0
- data/examples/README.md +68 -0
- data/examples/tmux_chat/bot_agent.rb +4 -1
- data/examples/tmux_chat/shared_chat_system.rb +50 -22
- data/lib/smart_message/base.rb +306 -20
- data/lib/smart_message/dispatcher.rb +53 -5
- data/lib/smart_message/errors.rb +3 -0
- data/lib/smart_message/header.rb +46 -5
- data/lib/smart_message/property_descriptions.rb +5 -4
- data/lib/smart_message/property_validations.rb +141 -0
- data/lib/smart_message/transport/base.rb +3 -2
- data/lib/smart_message/transport/redis_transport.rb +2 -2
- data/lib/smart_message/version.rb +1 -1
- metadata +6 -1
data/lib/smart_message/base.rb
CHANGED
@@ -6,6 +6,7 @@ require 'securerandom' # STDLIB
|
|
6
6
|
|
7
7
|
require_relative './wrapper.rb'
|
8
8
|
require_relative './property_descriptions.rb'
|
9
|
+
require_relative './property_validations.rb'
|
9
10
|
|
10
11
|
module SmartMessage
|
11
12
|
# The foundation class for the smart message
|
@@ -17,18 +18,43 @@ module SmartMessage
|
|
17
18
|
@@serializer = nil
|
18
19
|
@@logger = nil
|
19
20
|
|
21
|
+
# Class-level addressing configuration - use a registry for per-class isolation
|
22
|
+
@@addressing_registry = {}
|
23
|
+
|
20
24
|
# Registry for proc-based message handlers
|
21
25
|
@@proc_handlers = {}
|
26
|
+
|
27
|
+
# Class-level version setting
|
28
|
+
class << self
|
29
|
+
attr_accessor :_version
|
30
|
+
|
31
|
+
def version(v = nil)
|
32
|
+
if v.nil?
|
33
|
+
@_version || 1 # Default to version 1 if not set
|
34
|
+
else
|
35
|
+
@_version = v
|
36
|
+
|
37
|
+
# Set up version validation for the header
|
38
|
+
# This ensures that the header version matches the expected class version
|
39
|
+
@expected_header_version = v
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def expected_header_version
|
44
|
+
@expected_header_version || 1
|
45
|
+
end
|
46
|
+
end
|
22
47
|
|
23
48
|
include Hashie::Extensions::Dash::PropertyTranslation
|
24
49
|
|
25
50
|
include SmartMessage::PropertyDescriptions
|
51
|
+
include SmartMessage::PropertyValidations
|
26
52
|
|
27
53
|
include Hashie::Extensions::Coercion
|
28
54
|
include Hashie::Extensions::DeepMerge
|
29
55
|
include Hashie::Extensions::IgnoreUndeclared
|
30
56
|
include Hashie::Extensions::IndifferentAccess
|
31
|
-
|
57
|
+
# MergeInitializer interferes with required property validation - removed
|
32
58
|
include Hashie::Extensions::MethodAccess
|
33
59
|
|
34
60
|
# Common attrubutes for all messages
|
@@ -46,14 +72,37 @@ module SmartMessage
|
|
46
72
|
@transport = nil
|
47
73
|
@serializer = nil
|
48
74
|
@logger = nil
|
49
|
-
|
75
|
+
|
76
|
+
# instance-level over ride of class addressing
|
77
|
+
@from = nil
|
78
|
+
@to = nil
|
79
|
+
@reply_to = nil
|
80
|
+
|
81
|
+
# Create header with version validation specific to this message class
|
82
|
+
header = SmartMessage::Header.new(
|
83
|
+
uuid: SecureRandom.uuid,
|
84
|
+
message_class: self.class.to_s,
|
85
|
+
published_at: Time.now,
|
86
|
+
publisher_pid: Process.pid,
|
87
|
+
version: self.class.version,
|
88
|
+
from: from,
|
89
|
+
to: to,
|
90
|
+
reply_to: reply_to
|
91
|
+
)
|
92
|
+
|
93
|
+
# Set up version validation to match the expected class version
|
94
|
+
expected_version = self.class.expected_header_version
|
95
|
+
header.singleton_class.class_eval do
|
96
|
+
define_method(:validate_version!) do
|
97
|
+
unless self.version == expected_version
|
98
|
+
raise SmartMessage::Errors::ValidationError,
|
99
|
+
"Header version must be #{expected_version}, got: #{self.version}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
50
104
|
attributes = {
|
51
|
-
_sm_header:
|
52
|
-
uuid: SecureRandom.uuid,
|
53
|
-
message_class: self.class.to_s,
|
54
|
-
published_at: 2,
|
55
|
-
publisher_pid: 3
|
56
|
-
)
|
105
|
+
_sm_header: header
|
57
106
|
}.merge(props)
|
58
107
|
|
59
108
|
super(attributes, &block)
|
@@ -62,6 +111,57 @@ module SmartMessage
|
|
62
111
|
|
63
112
|
###################################################
|
64
113
|
## Common instance methods
|
114
|
+
|
115
|
+
# Validate that the header version matches the expected version for this class
|
116
|
+
def validate_header_version!
|
117
|
+
expected = self.class.expected_header_version
|
118
|
+
actual = _sm_header.version
|
119
|
+
unless actual == expected
|
120
|
+
raise SmartMessage::Errors::ValidationError,
|
121
|
+
"#{self.class.name} expects version #{expected}, but header has version #{actual}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Override PropertyValidations validate! to include header and version validation
|
126
|
+
def validate!
|
127
|
+
# Validate message properties using PropertyValidations
|
128
|
+
super
|
129
|
+
|
130
|
+
# Validate header properties
|
131
|
+
_sm_header.validate!
|
132
|
+
|
133
|
+
# Validate header version matches expected class version
|
134
|
+
validate_header_version!
|
135
|
+
end
|
136
|
+
|
137
|
+
# Override PropertyValidations validation_errors to include header errors
|
138
|
+
def validation_errors
|
139
|
+
errors = []
|
140
|
+
|
141
|
+
# Get message property validation errors using PropertyValidations
|
142
|
+
errors.concat(super.map { |err|
|
143
|
+
err.merge(source: 'message')
|
144
|
+
})
|
145
|
+
|
146
|
+
# Get header validation errors
|
147
|
+
errors.concat(_sm_header.validation_errors.map { |err|
|
148
|
+
err.merge(source: 'header')
|
149
|
+
})
|
150
|
+
|
151
|
+
# Check version mismatch
|
152
|
+
expected = self.class.expected_header_version
|
153
|
+
actual = _sm_header.version
|
154
|
+
unless actual == expected
|
155
|
+
errors << {
|
156
|
+
property: :version,
|
157
|
+
value: actual,
|
158
|
+
message: "Expected version #{expected}, got: #{actual}",
|
159
|
+
source: 'version_mismatch'
|
160
|
+
}
|
161
|
+
end
|
162
|
+
|
163
|
+
errors
|
164
|
+
end
|
65
165
|
|
66
166
|
# SMELL: How does the transport know how to decode a message before
|
67
167
|
# it knows the message class? We need a wrapper around
|
@@ -82,6 +182,9 @@ module SmartMessage
|
|
82
182
|
# NOTE: you publish instances; but, you subscribe/unsubscribe at
|
83
183
|
# the class-level
|
84
184
|
def publish
|
185
|
+
# Validate the complete message before publishing (now uses overridden validate!)
|
186
|
+
validate!
|
187
|
+
|
85
188
|
# TODO: move all of the _sm_ property processes into the wrapper
|
86
189
|
_sm_header.published_at = Time.now
|
87
190
|
_sm_header.publisher_pid = Process.pid
|
@@ -142,6 +245,61 @@ module SmartMessage
|
|
142
245
|
def reset_serializer; @serializer = nil; end
|
143
246
|
|
144
247
|
|
248
|
+
#########################################################
|
249
|
+
## instance-level addressing configuration
|
250
|
+
|
251
|
+
def from(entity_id = nil)
|
252
|
+
if entity_id.nil?
|
253
|
+
@from || self.class.from
|
254
|
+
else
|
255
|
+
@from = entity_id
|
256
|
+
# Update the header with the new value
|
257
|
+
_sm_header.from = entity_id if _sm_header
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def from_configured?; !from.nil?; end
|
262
|
+
def from_missing?; from.nil?; end
|
263
|
+
def reset_from;
|
264
|
+
@from = nil
|
265
|
+
_sm_header.from = nil if _sm_header
|
266
|
+
end
|
267
|
+
|
268
|
+
def to(entity_id = nil)
|
269
|
+
if entity_id.nil?
|
270
|
+
@to || self.class.to
|
271
|
+
else
|
272
|
+
@to = entity_id
|
273
|
+
# Update the header with the new value
|
274
|
+
_sm_header.to = entity_id if _sm_header
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def to_configured?; !to.nil?; end
|
279
|
+
def to_missing?; to.nil?; end
|
280
|
+
def reset_to;
|
281
|
+
@to = nil
|
282
|
+
_sm_header.to = nil if _sm_header
|
283
|
+
end
|
284
|
+
|
285
|
+
def reply_to(entity_id = nil)
|
286
|
+
if entity_id.nil?
|
287
|
+
@reply_to || self.class.reply_to
|
288
|
+
else
|
289
|
+
@reply_to = entity_id
|
290
|
+
# Update the header with the new value
|
291
|
+
_sm_header.reply_to = entity_id if _sm_header
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def reply_to_configured?; !reply_to.nil?; end
|
296
|
+
def reply_to_missing?; reply_to.nil?; end
|
297
|
+
def reset_reply_to;
|
298
|
+
@reply_to = nil
|
299
|
+
_sm_header.reply_to = nil if _sm_header
|
300
|
+
end
|
301
|
+
|
302
|
+
|
145
303
|
#########################################################
|
146
304
|
## instance-level utility methods
|
147
305
|
|
@@ -150,6 +308,11 @@ module SmartMessage
|
|
150
308
|
self.class.to_s
|
151
309
|
end
|
152
310
|
|
311
|
+
# return this class' description
|
312
|
+
def description
|
313
|
+
self.class.description
|
314
|
+
end
|
315
|
+
|
153
316
|
|
154
317
|
# returns a collection of class Set that consists of
|
155
318
|
# the symbolized values of the property names of the message
|
@@ -173,7 +336,7 @@ module SmartMessage
|
|
173
336
|
|
174
337
|
def description(desc = nil)
|
175
338
|
if desc.nil?
|
176
|
-
@description
|
339
|
+
@description || "#{self.name} is a SmartMessage"
|
177
340
|
else
|
178
341
|
@description = desc.to_s
|
179
342
|
end
|
@@ -261,6 +424,113 @@ module SmartMessage
|
|
261
424
|
def reset_serializer; @@serializer = nil; end
|
262
425
|
|
263
426
|
|
427
|
+
#########################################################
|
428
|
+
## class-level addressing configuration
|
429
|
+
|
430
|
+
# Helper method to normalize filter values (string -> array, nil -> nil)
|
431
|
+
private def normalize_filter_value(value)
|
432
|
+
case value
|
433
|
+
when nil
|
434
|
+
nil
|
435
|
+
when String
|
436
|
+
[value]
|
437
|
+
when Array
|
438
|
+
value
|
439
|
+
else
|
440
|
+
raise ArgumentError, "Filter value must be a String, Array, or nil, got: #{value.class}"
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Helper method to find addressing values in the inheritance chain
|
445
|
+
private def find_addressing_value(field)
|
446
|
+
# Start with current class
|
447
|
+
current_class = self
|
448
|
+
|
449
|
+
while current_class && current_class.respond_to?(:name)
|
450
|
+
class_name = current_class.name || current_class.to_s
|
451
|
+
|
452
|
+
# Check registry for this class
|
453
|
+
result = @@addressing_registry.dig(class_name, field)
|
454
|
+
return result if result
|
455
|
+
|
456
|
+
# If we have a proper name but no result, also check the to_s version
|
457
|
+
if current_class.name
|
458
|
+
alternative_key = current_class.to_s
|
459
|
+
result = @@addressing_registry.dig(alternative_key, field)
|
460
|
+
return result if result
|
461
|
+
end
|
462
|
+
|
463
|
+
# Move up the inheritance chain
|
464
|
+
current_class = current_class.superclass
|
465
|
+
|
466
|
+
# Stop if we reach SmartMessage::Base or above
|
467
|
+
break if current_class == SmartMessage::Base || current_class.nil?
|
468
|
+
end
|
469
|
+
|
470
|
+
nil
|
471
|
+
end
|
472
|
+
|
473
|
+
def from(entity_id = nil)
|
474
|
+
class_name = self.name || self.to_s
|
475
|
+
if entity_id.nil?
|
476
|
+
# Try to find the value, checking inheritance chain
|
477
|
+
result = find_addressing_value(:from)
|
478
|
+
result
|
479
|
+
else
|
480
|
+
@@addressing_registry[class_name] ||= {}
|
481
|
+
@@addressing_registry[class_name][:from] = entity_id
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def from_configured?; !from.nil?; end
|
486
|
+
def from_missing?; from.nil?; end
|
487
|
+
def reset_from;
|
488
|
+
class_name = self.name || self.to_s
|
489
|
+
@@addressing_registry[class_name] ||= {}
|
490
|
+
@@addressing_registry[class_name][:from] = nil
|
491
|
+
end
|
492
|
+
|
493
|
+
def to(entity_id = nil)
|
494
|
+
class_name = self.name || self.to_s
|
495
|
+
if entity_id.nil?
|
496
|
+
# Try to find the value, checking inheritance chain
|
497
|
+
result = find_addressing_value(:to)
|
498
|
+
result
|
499
|
+
else
|
500
|
+
@@addressing_registry[class_name] ||= {}
|
501
|
+
@@addressing_registry[class_name][:to] = entity_id
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
def to_configured?; !to.nil?; end
|
506
|
+
def to_missing?; to.nil?; end
|
507
|
+
def reset_to;
|
508
|
+
class_name = self.name || self.to_s
|
509
|
+
@@addressing_registry[class_name] ||= {}
|
510
|
+
@@addressing_registry[class_name][:to] = nil
|
511
|
+
end
|
512
|
+
|
513
|
+
def reply_to(entity_id = nil)
|
514
|
+
class_name = self.name || self.to_s
|
515
|
+
if entity_id.nil?
|
516
|
+
# Try to find the value, checking inheritance chain
|
517
|
+
result = find_addressing_value(:reply_to)
|
518
|
+
result
|
519
|
+
else
|
520
|
+
@@addressing_registry[class_name] ||= {}
|
521
|
+
@@addressing_registry[class_name][:reply_to] = entity_id
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
def reply_to_configured?; !reply_to.nil?; end
|
526
|
+
def reply_to_missing?; reply_to.nil?; end
|
527
|
+
def reset_reply_to;
|
528
|
+
class_name = self.name || self.to_s
|
529
|
+
@@addressing_registry[class_name] ||= {}
|
530
|
+
@@addressing_registry[class_name][:reply_to] = nil
|
531
|
+
end
|
532
|
+
|
533
|
+
|
264
534
|
#########################################################
|
265
535
|
## class-level subscription management via the transport
|
266
536
|
|
@@ -272,25 +542,30 @@ module SmartMessage
|
|
272
542
|
# - String: Method name like "MyService.handle_message"
|
273
543
|
# - Proc: A proc/lambda that accepts (message_header, message_payload)
|
274
544
|
# - nil: Uses default "MessageClass.process" method
|
545
|
+
# @param broadcast [Boolean, nil] Filter for broadcast messages (to: nil)
|
546
|
+
# @param to [String, Array, nil] Filter for messages directed to specific entities
|
547
|
+
# @param from [String, Array, nil] Filter for messages from specific entities
|
275
548
|
# @param block [Proc] Alternative way to pass a processing block
|
276
549
|
# @return [String] The identifier used for this subscription
|
277
550
|
#
|
278
|
-
# @example Using default handler
|
551
|
+
# @example Using default handler (all messages)
|
279
552
|
# MyMessage.subscribe
|
280
553
|
#
|
281
|
-
# @example Using custom method name
|
282
|
-
# MyMessage.subscribe("MyService.handle_message")
|
554
|
+
# @example Using custom method name with filtering
|
555
|
+
# MyMessage.subscribe("MyService.handle_message", to: 'my-service')
|
283
556
|
#
|
284
|
-
# @example Using a block
|
285
|
-
# MyMessage.subscribe do |header, payload|
|
557
|
+
# @example Using a block with broadcast filtering
|
558
|
+
# MyMessage.subscribe(broadcast: true) do |header, payload|
|
286
559
|
# data = JSON.parse(payload)
|
287
|
-
# puts "Received: #{data}"
|
560
|
+
# puts "Received broadcast: #{data}"
|
288
561
|
# end
|
289
562
|
#
|
290
|
-
# @example
|
291
|
-
#
|
292
|
-
#
|
293
|
-
|
563
|
+
# @example Entity-specific filtering
|
564
|
+
# MyMessage.subscribe(to: 'order-service', from: ['payment', 'user'])
|
565
|
+
#
|
566
|
+
# @example Broadcast + directed messages
|
567
|
+
# MyMessage.subscribe(to: 'my-service', broadcast: true)
|
568
|
+
def subscribe(process_method = nil, broadcast: nil, to: nil, from: nil, &block)
|
294
569
|
message_class = whoami
|
295
570
|
|
296
571
|
# Handle different parameter types
|
@@ -308,10 +583,21 @@ module SmartMessage
|
|
308
583
|
end
|
309
584
|
# If process_method is a String, use it as-is
|
310
585
|
|
586
|
+
# Normalize string filters to arrays
|
587
|
+
to_filter = normalize_filter_value(to)
|
588
|
+
from_filter = normalize_filter_value(from)
|
589
|
+
|
590
|
+
# Create filter options
|
591
|
+
filter_options = {
|
592
|
+
broadcast: broadcast,
|
593
|
+
to: to_filter,
|
594
|
+
from: from_filter
|
595
|
+
}
|
596
|
+
|
311
597
|
# TODO: Add proper logging here
|
312
598
|
|
313
599
|
raise Errors::TransportNotConfigured if transport_missing?
|
314
|
-
transport.subscribe(message_class, process_method)
|
600
|
+
transport.subscribe(message_class, process_method, filter_options)
|
315
601
|
|
316
602
|
process_method
|
317
603
|
end
|
@@ -83,17 +83,30 @@ module SmartMessage
|
|
83
83
|
end
|
84
84
|
|
85
85
|
|
86
|
-
def add(message_class, process_method_as_string)
|
86
|
+
def add(message_class, process_method_as_string, filter_options = {})
|
87
87
|
klass = String(message_class)
|
88
|
-
|
89
|
-
|
88
|
+
|
89
|
+
# Create subscription entry with filter options
|
90
|
+
subscription = {
|
91
|
+
process_method: process_method_as_string,
|
92
|
+
filters: filter_options
|
93
|
+
}
|
94
|
+
|
95
|
+
# Check if this exact subscription already exists
|
96
|
+
existing_subscription = @subscribers[klass].find do |sub|
|
97
|
+
sub[:process_method] == process_method_as_string && sub[:filters] == filter_options
|
98
|
+
end
|
99
|
+
|
100
|
+
unless existing_subscription
|
101
|
+
@subscribers[klass] += [subscription]
|
90
102
|
end
|
91
103
|
end
|
92
104
|
|
93
105
|
|
94
106
|
# drop a processer from a subscribed message
|
95
107
|
def drop(message_class, process_method_as_string)
|
96
|
-
|
108
|
+
klass = String(message_class)
|
109
|
+
@subscribers[klass].reject! { |sub| sub[:process_method] == process_method_as_string }
|
97
110
|
end
|
98
111
|
|
99
112
|
|
@@ -115,7 +128,15 @@ module SmartMessage
|
|
115
128
|
def route(message_header, message_payload)
|
116
129
|
message_klass = message_header.message_class
|
117
130
|
return nil if @subscribers[message_klass].empty?
|
118
|
-
|
131
|
+
|
132
|
+
@subscribers[message_klass].each do |subscription|
|
133
|
+
# Extract subscription details
|
134
|
+
message_processor = subscription[:process_method]
|
135
|
+
filters = subscription[:filters]
|
136
|
+
|
137
|
+
# Check if message matches filters
|
138
|
+
next unless message_matches_filters?(message_header, filters)
|
139
|
+
|
119
140
|
SS.add(message_klass, message_processor, 'routed' )
|
120
141
|
@router_pool.post do
|
121
142
|
begin
|
@@ -144,6 +165,33 @@ module SmartMessage
|
|
144
165
|
|
145
166
|
private
|
146
167
|
|
168
|
+
# Check if a message matches the subscription filters
|
169
|
+
# @param message_header [SmartMessage::Header] The message header
|
170
|
+
# @param filters [Hash] The filter criteria
|
171
|
+
# @return [Boolean] True if the message matches all filters
|
172
|
+
def message_matches_filters?(message_header, filters)
|
173
|
+
# If no filters specified, accept all messages (backward compatibility)
|
174
|
+
return true if filters.nil? || filters.empty? || filters.values.all?(&:nil?)
|
175
|
+
|
176
|
+
# Check from filter
|
177
|
+
if filters[:from]
|
178
|
+
from_match = filters[:from].include?(message_header.from)
|
179
|
+
return false unless from_match
|
180
|
+
end
|
181
|
+
|
182
|
+
# Check to/broadcast filters (OR logic between them)
|
183
|
+
if filters[:broadcast] || filters[:to]
|
184
|
+
broadcast_match = filters[:broadcast] && message_header.to.nil?
|
185
|
+
to_match = filters[:to] && filters[:to].include?(message_header.to)
|
186
|
+
|
187
|
+
# If either broadcast or to filter is specified, at least one must match
|
188
|
+
combined_match = (broadcast_match || to_match)
|
189
|
+
return false unless combined_match
|
190
|
+
end
|
191
|
+
|
192
|
+
true
|
193
|
+
end
|
194
|
+
|
147
195
|
# Check if a message processor is a proc handler
|
148
196
|
# @param message_processor [String] The message processor identifier
|
149
197
|
# @return [Boolean] True if this is a proc handler
|
data/lib/smart_message/errors.rb
CHANGED
data/lib/smart_message/header.rb
CHANGED
@@ -2,19 +2,60 @@
|
|
2
2
|
# encoding: utf-8
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
|
+
require_relative './property_descriptions'
|
6
|
+
require_relative './property_validations'
|
7
|
+
|
5
8
|
module SmartMessage
|
6
9
|
# Every smart message has a common header format that contains
|
7
10
|
# information used to support the dispatching of subscribed
|
8
11
|
# messages upon receipt from a transport.
|
9
12
|
class Header < Hashie::Dash
|
10
13
|
include Hashie::Extensions::IndifferentAccess
|
11
|
-
include Hashie::Extensions::MergeInitializer
|
12
14
|
include Hashie::Extensions::MethodAccess
|
15
|
+
include SmartMessage::PropertyDescriptions
|
16
|
+
include SmartMessage::PropertyValidations
|
13
17
|
|
14
18
|
# Common attributes of the smart message standard header
|
15
|
-
property :uuid
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
+
property :uuid,
|
20
|
+
required: true,
|
21
|
+
message: "UUID is required for message tracking and deduplication",
|
22
|
+
description: "Unique identifier for this specific message instance, used for tracking and deduplication"
|
23
|
+
|
24
|
+
property :message_class,
|
25
|
+
required: true,
|
26
|
+
message: "Message class is required to identify the message type",
|
27
|
+
description: "Fully qualified class name of the message type (e.g. 'OrderMessage', 'PaymentNotification')"
|
28
|
+
|
29
|
+
property :published_at,
|
30
|
+
required: true,
|
31
|
+
message: "Published timestamp is required for message ordering",
|
32
|
+
description: "Timestamp when the message was published by the sender, used for ordering and debugging"
|
33
|
+
|
34
|
+
property :publisher_pid,
|
35
|
+
required: true,
|
36
|
+
message: "Publisher process ID is required for debugging and traceability",
|
37
|
+
description: "Process ID of the publishing application, useful for debugging and tracing message origins"
|
38
|
+
|
39
|
+
property :version,
|
40
|
+
required: true,
|
41
|
+
default: 1,
|
42
|
+
message: "Message version is required for schema compatibility",
|
43
|
+
description: "Schema version of the message format, used for schema evolution and compatibility checking",
|
44
|
+
validate: ->(v) { v.is_a?(Integer) && v > 0 },
|
45
|
+
validation_message: "Header version must be a positive integer"
|
46
|
+
|
47
|
+
# Message addressing properties for entity-to-entity communication
|
48
|
+
property :from,
|
49
|
+
required: true,
|
50
|
+
message: "From entity ID is required for message routing and replies",
|
51
|
+
description: "Unique identifier of the entity sending this message, used for routing responses and audit trails"
|
52
|
+
|
53
|
+
property :to,
|
54
|
+
required: false,
|
55
|
+
description: "Optional unique identifier of the intended recipient entity. When nil, message is broadcast to all subscribers"
|
56
|
+
|
57
|
+
property :reply_to,
|
58
|
+
required: false,
|
59
|
+
description: "Optional unique identifier of the entity that should receive replies to this message. Defaults to 'from' entity if not specified"
|
19
60
|
end
|
20
61
|
end
|
@@ -13,16 +13,17 @@ module SmartMessage
|
|
13
13
|
|
14
14
|
module ClassMethods
|
15
15
|
def property(property_name, options = {})
|
16
|
+
# Extract our custom option before passing to parent
|
16
17
|
description = options.delete(:description)
|
17
18
|
|
18
|
-
#
|
19
|
+
# Call original property method first
|
20
|
+
super(property_name, options)
|
21
|
+
|
22
|
+
# Then store description if provided
|
19
23
|
if description
|
20
24
|
@property_descriptions ||= {}
|
21
25
|
@property_descriptions[property_name.to_sym] = description
|
22
26
|
end
|
23
|
-
|
24
|
-
# Call original property method
|
25
|
-
super(property_name, options)
|
26
27
|
end
|
27
28
|
|
28
29
|
def property_description(property_name)
|