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.
@@ -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
- include Hashie::Extensions::MergeInitializer
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: SmartMessage::Header.new(
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
@@ -25,5 +25,8 @@ module SmartMessage
25
25
  # A received message is of an unknown class
26
26
  class UnknownMessageClass < RuntimeError; end
27
27
 
28
+ # A property validation failed
29
+ class ValidationError < RuntimeError; end
30
+
28
31
  end
29
32
  end
@@ -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
- property :message_class
17
- property :published_at
18
- property :publisher_pid
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
- # Store description if provided
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
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  module SmartMessage
6
- VERSION = '0.0.4'
6
+ VERSION = '0.0.5'
7
7
  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
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