smart_message 0.0.4 → 0.0.5
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 +51 -0
- data/README.md +244 -9
- data/docs/architecture.md +2 -0
- data/docs/examples.md +2 -0
- data/docs/getting-started.md +11 -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/tmux_chat/bot_agent.rb +4 -1
- data/examples/tmux_chat/shared_chat_system.rb +50 -22
- data/lib/smart_message/base.rb +105 -8
- data/lib/smart_message/errors.rb +3 -0
- data/lib/smart_message/header.rb +32 -5
- data/lib/smart_message/property_descriptions.rb +5 -4
- data/lib/smart_message/property_validations.rb +141 -0
- data/lib/smart_message/version.rb +1 -1
- metadata +3 -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
|
@@ -19,16 +20,38 @@ module SmartMessage
|
|
19
20
|
|
20
21
|
# Registry for proc-based message handlers
|
21
22
|
@@proc_handlers = {}
|
23
|
+
|
24
|
+
# Class-level version setting
|
25
|
+
class << self
|
26
|
+
attr_accessor :_version
|
27
|
+
|
28
|
+
def version(v = nil)
|
29
|
+
if v.nil?
|
30
|
+
@_version || 1 # Default to version 1 if not set
|
31
|
+
else
|
32
|
+
@_version = v
|
33
|
+
|
34
|
+
# Set up version validation for the header
|
35
|
+
# This ensures that the header version matches the expected class version
|
36
|
+
@expected_header_version = v
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def expected_header_version
|
41
|
+
@expected_header_version || 1
|
42
|
+
end
|
43
|
+
end
|
22
44
|
|
23
45
|
include Hashie::Extensions::Dash::PropertyTranslation
|
24
46
|
|
25
47
|
include SmartMessage::PropertyDescriptions
|
48
|
+
include SmartMessage::PropertyValidations
|
26
49
|
|
27
50
|
include Hashie::Extensions::Coercion
|
28
51
|
include Hashie::Extensions::DeepMerge
|
29
52
|
include Hashie::Extensions::IgnoreUndeclared
|
30
53
|
include Hashie::Extensions::IndifferentAccess
|
31
|
-
|
54
|
+
# MergeInitializer interferes with required property validation - removed
|
32
55
|
include Hashie::Extensions::MethodAccess
|
33
56
|
|
34
57
|
# Common attrubutes for all messages
|
@@ -47,13 +70,28 @@ module SmartMessage
|
|
47
70
|
@serializer = nil
|
48
71
|
@logger = nil
|
49
72
|
|
73
|
+
# Create header with version validation specific to this message class
|
74
|
+
header = SmartMessage::Header.new(
|
75
|
+
uuid: SecureRandom.uuid,
|
76
|
+
message_class: self.class.to_s,
|
77
|
+
published_at: Time.now,
|
78
|
+
publisher_pid: Process.pid,
|
79
|
+
version: self.class.version
|
80
|
+
)
|
81
|
+
|
82
|
+
# Set up version validation to match the expected class version
|
83
|
+
expected_version = self.class.expected_header_version
|
84
|
+
header.singleton_class.class_eval do
|
85
|
+
define_method(:validate_version!) do
|
86
|
+
unless self.version == expected_version
|
87
|
+
raise SmartMessage::Errors::ValidationError,
|
88
|
+
"Header version must be #{expected_version}, got: #{self.version}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
50
93
|
attributes = {
|
51
|
-
_sm_header:
|
52
|
-
uuid: SecureRandom.uuid,
|
53
|
-
message_class: self.class.to_s,
|
54
|
-
published_at: 2,
|
55
|
-
publisher_pid: 3
|
56
|
-
)
|
94
|
+
_sm_header: header
|
57
95
|
}.merge(props)
|
58
96
|
|
59
97
|
super(attributes, &block)
|
@@ -62,6 +100,57 @@ module SmartMessage
|
|
62
100
|
|
63
101
|
###################################################
|
64
102
|
## Common instance methods
|
103
|
+
|
104
|
+
# Validate that the header version matches the expected version for this class
|
105
|
+
def validate_header_version!
|
106
|
+
expected = self.class.expected_header_version
|
107
|
+
actual = _sm_header.version
|
108
|
+
unless actual == expected
|
109
|
+
raise SmartMessage::Errors::ValidationError,
|
110
|
+
"#{self.class.name} expects version #{expected}, but header has version #{actual}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Override PropertyValidations validate! to include header and version validation
|
115
|
+
def validate!
|
116
|
+
# Validate message properties using PropertyValidations
|
117
|
+
super
|
118
|
+
|
119
|
+
# Validate header properties
|
120
|
+
_sm_header.validate!
|
121
|
+
|
122
|
+
# Validate header version matches expected class version
|
123
|
+
validate_header_version!
|
124
|
+
end
|
125
|
+
|
126
|
+
# Override PropertyValidations validation_errors to include header errors
|
127
|
+
def validation_errors
|
128
|
+
errors = []
|
129
|
+
|
130
|
+
# Get message property validation errors using PropertyValidations
|
131
|
+
errors.concat(super.map { |err|
|
132
|
+
err.merge(source: 'message')
|
133
|
+
})
|
134
|
+
|
135
|
+
# Get header validation errors
|
136
|
+
errors.concat(_sm_header.validation_errors.map { |err|
|
137
|
+
err.merge(source: 'header')
|
138
|
+
})
|
139
|
+
|
140
|
+
# Check version mismatch
|
141
|
+
expected = self.class.expected_header_version
|
142
|
+
actual = _sm_header.version
|
143
|
+
unless actual == expected
|
144
|
+
errors << {
|
145
|
+
property: :version,
|
146
|
+
value: actual,
|
147
|
+
message: "Expected version #{expected}, got: #{actual}",
|
148
|
+
source: 'version_mismatch'
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
errors
|
153
|
+
end
|
65
154
|
|
66
155
|
# SMELL: How does the transport know how to decode a message before
|
67
156
|
# it knows the message class? We need a wrapper around
|
@@ -82,6 +171,9 @@ module SmartMessage
|
|
82
171
|
# NOTE: you publish instances; but, you subscribe/unsubscribe at
|
83
172
|
# the class-level
|
84
173
|
def publish
|
174
|
+
# Validate the complete message before publishing (now uses overridden validate!)
|
175
|
+
validate!
|
176
|
+
|
85
177
|
# TODO: move all of the _sm_ property processes into the wrapper
|
86
178
|
_sm_header.published_at = Time.now
|
87
179
|
_sm_header.publisher_pid = Process.pid
|
@@ -150,6 +242,11 @@ module SmartMessage
|
|
150
242
|
self.class.to_s
|
151
243
|
end
|
152
244
|
|
245
|
+
# return this class' description
|
246
|
+
def description
|
247
|
+
self.class.description
|
248
|
+
end
|
249
|
+
|
153
250
|
|
154
251
|
# returns a collection of class Set that consists of
|
155
252
|
# the symbolized values of the property names of the message
|
@@ -173,7 +270,7 @@ module SmartMessage
|
|
173
270
|
|
174
271
|
def description(desc = nil)
|
175
272
|
if desc.nil?
|
176
|
-
@description
|
273
|
+
@description || "#{self.name} is a SmartMessage"
|
177
274
|
else
|
178
275
|
@description = desc.to_s
|
179
276
|
end
|
data/lib/smart_message/errors.rb
CHANGED
data/lib/smart_message/header.rb
CHANGED
@@ -2,19 +2,46 @@
|
|
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"
|
19
46
|
end
|
20
47
|
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)
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# lib/smart_message/property_validations.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
module PropertyValidations
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
base.class_eval do
|
10
|
+
@property_validators = {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def property(property_name, options = {})
|
16
|
+
# Extract our custom options before passing to Hashie::Dash
|
17
|
+
# Note: Hashie's 'message' option only works with 'required', so we use 'validation_message'
|
18
|
+
validator = options.delete(:validate)
|
19
|
+
validation_message = options.delete(:validation_message)
|
20
|
+
|
21
|
+
# Call original property method first
|
22
|
+
super(property_name, options)
|
23
|
+
|
24
|
+
# Then store validator if provided
|
25
|
+
if validator
|
26
|
+
@property_validators ||= {}
|
27
|
+
@property_validators[property_name.to_sym] = {
|
28
|
+
validator: validator,
|
29
|
+
message: validation_message || "Validation failed for property '#{property_name}'"
|
30
|
+
}
|
31
|
+
|
32
|
+
# Note: We don't override setter methods since they may conflict with Hashie::Dash
|
33
|
+
# Instead, validation happens during validate! calls
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def property_validator(property_name)
|
38
|
+
@property_validators&.[](property_name.to_sym)
|
39
|
+
end
|
40
|
+
|
41
|
+
def property_validators
|
42
|
+
@property_validators&.dup || {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def validated_properties
|
46
|
+
@property_validators&.keys || []
|
47
|
+
end
|
48
|
+
|
49
|
+
# Validate all properties with validators
|
50
|
+
def validate_all(instance)
|
51
|
+
validated_properties.each do |property_name|
|
52
|
+
validator_info = property_validator(property_name)
|
53
|
+
next unless validator_info
|
54
|
+
|
55
|
+
value = instance.send(property_name)
|
56
|
+
validator = validator_info[:validator]
|
57
|
+
error_message = validator_info[:message]
|
58
|
+
|
59
|
+
# Skip validation if value is nil and property is not required
|
60
|
+
next if value.nil? && !instance.class.required_properties.include?(property_name)
|
61
|
+
|
62
|
+
# Perform validation
|
63
|
+
is_valid = case validator
|
64
|
+
when Proc
|
65
|
+
instance.instance_exec(value, &validator)
|
66
|
+
when Symbol
|
67
|
+
instance.send(validator, value)
|
68
|
+
when Regexp
|
69
|
+
!!(value.to_s =~ validator)
|
70
|
+
when Class
|
71
|
+
value.is_a?(validator)
|
72
|
+
when Array
|
73
|
+
validator.include?(value)
|
74
|
+
when Range
|
75
|
+
validator.include?(value)
|
76
|
+
else
|
77
|
+
value == validator
|
78
|
+
end
|
79
|
+
|
80
|
+
unless is_valid
|
81
|
+
raise SmartMessage::Errors::ValidationError, "#{instance.class.name}##{property_name}: #{error_message}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Instance methods
|
89
|
+
def validate!
|
90
|
+
self.class.validate_all(self)
|
91
|
+
end
|
92
|
+
|
93
|
+
def valid?
|
94
|
+
validate!
|
95
|
+
true
|
96
|
+
rescue SmartMessage::Errors::ValidationError
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
def validation_errors
|
101
|
+
errors = []
|
102
|
+
self.class.validated_properties.each do |property_name|
|
103
|
+
validator_info = self.class.property_validator(property_name)
|
104
|
+
next unless validator_info
|
105
|
+
|
106
|
+
value = send(property_name)
|
107
|
+
validator = validator_info[:validator]
|
108
|
+
|
109
|
+
# Skip validation if value is nil and property is not required
|
110
|
+
next if value.nil? && !self.class.required_properties.include?(property_name)
|
111
|
+
|
112
|
+
# Perform validation
|
113
|
+
is_valid = case validator
|
114
|
+
when Proc
|
115
|
+
instance_exec(value, &validator)
|
116
|
+
when Symbol
|
117
|
+
send(validator, value)
|
118
|
+
when Regexp
|
119
|
+
!!(value.to_s =~ validator)
|
120
|
+
when Class
|
121
|
+
value.is_a?(validator)
|
122
|
+
when Array
|
123
|
+
validator.include?(value)
|
124
|
+
when Range
|
125
|
+
validator.include?(value)
|
126
|
+
else
|
127
|
+
value == validator
|
128
|
+
end
|
129
|
+
|
130
|
+
unless is_valid
|
131
|
+
errors << {
|
132
|
+
property: property_name,
|
133
|
+
value: value,
|
134
|
+
message: validator_info[:message]
|
135
|
+
}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
errors
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_message
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dewayne VanHoozer
|
@@ -218,6 +218,7 @@ files:
|
|
218
218
|
- examples/04_redis_smart_home_iot.rb
|
219
219
|
- examples/05_proc_handlers.rb
|
220
220
|
- examples/06_custom_logger_example.rb
|
221
|
+
- examples/07_error_handling_scenarios.rb
|
221
222
|
- examples/README.md
|
222
223
|
- examples/smart_home_iot_dataflow.md
|
223
224
|
- examples/tmux_chat/README.md
|
@@ -238,6 +239,7 @@ files:
|
|
238
239
|
- lib/smart_message/logger/base.rb
|
239
240
|
- lib/smart_message/logger/default.rb
|
240
241
|
- lib/smart_message/property_descriptions.rb
|
242
|
+
- lib/smart_message/property_validations.rb
|
241
243
|
- lib/smart_message/serializer.rb
|
242
244
|
- lib/smart_message/serializer/base.rb
|
243
245
|
- lib/smart_message/serializer/json.rb
|