smart_message 0.0.3 → 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/Gemfile.lock +1 -1
- data/README.md +244 -9
- data/docs/README.md +1 -0
- data/docs/architecture.md +2 -0
- data/docs/examples.md +2 -0
- data/docs/getting-started.md +11 -0
- data/docs/logging.md +452 -0
- data/docs/properties.md +213 -7
- data/examples/.gitignore +2 -0
- 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 +641 -0
- 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/logger/default.rb +217 -0
- data/lib/smart_message/logger.rb +9 -1
- 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 +7 -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
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# lib/smart_message/logger/default.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'logger'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'stringio'
|
8
|
+
|
9
|
+
module SmartMessage
|
10
|
+
module Logger
|
11
|
+
# Default logger implementation for SmartMessage
|
12
|
+
#
|
13
|
+
# This logger automatically detects and uses the best available logging option:
|
14
|
+
# - Rails.logger if running in a Rails application
|
15
|
+
# - Standard Ruby Logger writing to log/smart_message.log otherwise
|
16
|
+
#
|
17
|
+
# Usage:
|
18
|
+
# # In your message class
|
19
|
+
# config do
|
20
|
+
# logger SmartMessage::Logger::Default.new
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # Or with custom options
|
24
|
+
# config do
|
25
|
+
# logger SmartMessage::Logger::Default.new(
|
26
|
+
# log_file: 'custom/path.log', # File path
|
27
|
+
# level: Logger::DEBUG
|
28
|
+
# )
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# # To log to STDOUT instead of a file
|
32
|
+
# config do
|
33
|
+
# logger SmartMessage::Logger::Default.new(
|
34
|
+
# log_file: STDOUT, # STDOUT or STDERR
|
35
|
+
# level: Logger::INFO
|
36
|
+
# )
|
37
|
+
# end
|
38
|
+
class Default < Base
|
39
|
+
attr_reader :logger, :log_file, :level
|
40
|
+
|
41
|
+
def initialize(log_file: nil, level: nil)
|
42
|
+
@log_file = log_file || default_log_file
|
43
|
+
@level = level || default_log_level
|
44
|
+
|
45
|
+
@logger = setup_logger
|
46
|
+
end
|
47
|
+
|
48
|
+
# Message lifecycle logging methods
|
49
|
+
|
50
|
+
def log_message_created(message)
|
51
|
+
logger.debug { "[SmartMessage] Created: #{message.class.name} - #{message_summary(message)}" }
|
52
|
+
end
|
53
|
+
|
54
|
+
def log_message_published(message, transport)
|
55
|
+
logger.info { "[SmartMessage] Published: #{message.class.name} via #{transport.class.name.split('::').last}" }
|
56
|
+
end
|
57
|
+
|
58
|
+
def log_message_received(message_class, payload)
|
59
|
+
logger.info { "[SmartMessage] Received: #{message_class.name} (#{payload.bytesize} bytes)" }
|
60
|
+
end
|
61
|
+
|
62
|
+
def log_message_processed(message_class, result)
|
63
|
+
logger.info { "[SmartMessage] Processed: #{message_class.name} - #{truncate(result.to_s, 100)}" }
|
64
|
+
end
|
65
|
+
|
66
|
+
def log_message_subscribe(message_class, handler = nil)
|
67
|
+
handler_desc = handler ? " with handler: #{handler}" : ""
|
68
|
+
logger.info { "[SmartMessage] Subscribed: #{message_class.name}#{handler_desc}" }
|
69
|
+
end
|
70
|
+
|
71
|
+
def log_message_unsubscribe(message_class)
|
72
|
+
logger.info { "[SmartMessage] Unsubscribed: #{message_class.name}" }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Error logging
|
76
|
+
|
77
|
+
def log_error(context, error)
|
78
|
+
logger.error { "[SmartMessage] Error in #{context}: #{error.class.name} - #{error.message}" }
|
79
|
+
logger.debug { "[SmartMessage] Backtrace:\n#{error.backtrace.join("\n")}" } if error.backtrace
|
80
|
+
end
|
81
|
+
|
82
|
+
def log_warning(message)
|
83
|
+
logger.warn { "[SmartMessage] Warning: #{message}" }
|
84
|
+
end
|
85
|
+
|
86
|
+
# General purpose logging methods matching Ruby's Logger interface
|
87
|
+
|
88
|
+
def debug(message = nil, &block)
|
89
|
+
logger.debug(message, &block)
|
90
|
+
end
|
91
|
+
|
92
|
+
def info(message = nil, &block)
|
93
|
+
logger.info(message, &block)
|
94
|
+
end
|
95
|
+
|
96
|
+
def warn(message = nil, &block)
|
97
|
+
logger.warn(message, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def error(message = nil, &block)
|
101
|
+
logger.error(message, &block)
|
102
|
+
end
|
103
|
+
|
104
|
+
def fatal(message = nil, &block)
|
105
|
+
logger.fatal(message, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def setup_logger
|
111
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
112
|
+
# Use Rails logger if available
|
113
|
+
setup_rails_logger
|
114
|
+
else
|
115
|
+
# Use standard Ruby logger
|
116
|
+
setup_ruby_logger
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def setup_rails_logger
|
121
|
+
# Wrap Rails.logger to ensure our messages are properly tagged
|
122
|
+
RailsLoggerWrapper.new(Rails.logger, level: @level)
|
123
|
+
end
|
124
|
+
|
125
|
+
def setup_ruby_logger
|
126
|
+
# Handle IO objects (STDOUT, STDERR) vs file paths
|
127
|
+
if @log_file.is_a?(IO) || @log_file.is_a?(StringIO)
|
128
|
+
# For STDOUT/STDERR, don't use rotation
|
129
|
+
ruby_logger = ::Logger.new(@log_file)
|
130
|
+
else
|
131
|
+
# For file paths, ensure directory exists and use rotation
|
132
|
+
FileUtils.mkdir_p(File.dirname(@log_file))
|
133
|
+
|
134
|
+
ruby_logger = ::Logger.new(
|
135
|
+
@log_file,
|
136
|
+
10, # Keep 10 old log files
|
137
|
+
10_485_760 # Rotate when file reaches 10MB
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
141
|
+
ruby_logger.level = @level
|
142
|
+
|
143
|
+
# Set a clean formatter
|
144
|
+
ruby_logger.formatter = proc do |severity, datetime, progname, msg|
|
145
|
+
timestamp = datetime.strftime('%Y-%m-%d %H:%M:%S.%3N')
|
146
|
+
"[#{timestamp}] #{severity.ljust(5)} -- : #{msg}\n"
|
147
|
+
end
|
148
|
+
|
149
|
+
ruby_logger
|
150
|
+
end
|
151
|
+
|
152
|
+
def default_log_file
|
153
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
154
|
+
Rails.root.join('log', 'smart_message.log').to_s
|
155
|
+
else
|
156
|
+
'log/smart_message.log'
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def default_log_level
|
161
|
+
if defined?(Rails) && Rails.respond_to?(:env)
|
162
|
+
case Rails.env
|
163
|
+
when 'production'
|
164
|
+
::Logger::INFO
|
165
|
+
when 'test'
|
166
|
+
::Logger::ERROR
|
167
|
+
else
|
168
|
+
::Logger::DEBUG
|
169
|
+
end
|
170
|
+
else
|
171
|
+
# Default to INFO for non-Rails environments
|
172
|
+
::Logger::INFO
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def message_summary(message)
|
177
|
+
# Create a brief summary of the message for logging
|
178
|
+
if message.respond_to?(:to_h)
|
179
|
+
data = message.to_h
|
180
|
+
# Remove internal header for cleaner logs
|
181
|
+
data.delete(:_sm_header)
|
182
|
+
data.delete('_sm_header')
|
183
|
+
truncate(data.inspect, 200)
|
184
|
+
else
|
185
|
+
truncate(message.inspect, 200)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def truncate(string, max_length)
|
190
|
+
return string if string.length <= max_length
|
191
|
+
"#{string[0...max_length]}..."
|
192
|
+
end
|
193
|
+
|
194
|
+
# Internal wrapper for Rails.logger to handle tagged logging
|
195
|
+
class RailsLoggerWrapper
|
196
|
+
def initialize(rails_logger, level: nil)
|
197
|
+
@rails_logger = rails_logger
|
198
|
+
@rails_logger.level = level if level
|
199
|
+
end
|
200
|
+
|
201
|
+
def method_missing(method, *args, &block)
|
202
|
+
if @rails_logger.respond_to?(:tagged)
|
203
|
+
@rails_logger.tagged('SmartMessage') do
|
204
|
+
@rails_logger.send(method, *args, &block)
|
205
|
+
end
|
206
|
+
else
|
207
|
+
@rails_logger.send(method, *args, &block)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def respond_to_missing?(method, include_private = false)
|
212
|
+
@rails_logger.respond_to?(method, include_private)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
data/lib/smart_message/logger.rb
CHANGED
@@ -3,5 +3,13 @@
|
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
5
|
module SmartMessage::Logger
|
6
|
-
#
|
6
|
+
# Logger module provides logging capabilities for SmartMessage
|
7
|
+
# The Default logger automatically uses Rails.logger if available,
|
8
|
+
# otherwise falls back to a standard Ruby Logger
|
7
9
|
end # module SmartMessage::Logger
|
10
|
+
|
11
|
+
# Load the base class first
|
12
|
+
require_relative 'logger/base'
|
13
|
+
|
14
|
+
# Load the default logger implementation
|
15
|
+
require_relative 'logger/default'
|
@@ -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
|
@@ -204,17 +204,21 @@ files:
|
|
204
204
|
- docs/examples.md
|
205
205
|
- docs/getting-started.md
|
206
206
|
- docs/ideas_to_think_about.md
|
207
|
+
- docs/logging.md
|
207
208
|
- docs/message_processing.md
|
208
209
|
- docs/proc_handlers_summary.md
|
209
210
|
- docs/properties.md
|
210
211
|
- docs/serializers.md
|
211
212
|
- docs/transports.md
|
212
213
|
- docs/troubleshooting.md
|
214
|
+
- examples/.gitignore
|
213
215
|
- examples/01_point_to_point_orders.rb
|
214
216
|
- examples/02_publish_subscribe_events.rb
|
215
217
|
- examples/03_many_to_many_chat.rb
|
216
218
|
- examples/04_redis_smart_home_iot.rb
|
217
219
|
- examples/05_proc_handlers.rb
|
220
|
+
- examples/06_custom_logger_example.rb
|
221
|
+
- examples/07_error_handling_scenarios.rb
|
218
222
|
- examples/README.md
|
219
223
|
- examples/smart_home_iot_dataflow.md
|
220
224
|
- examples/tmux_chat/README.md
|
@@ -233,7 +237,9 @@ files:
|
|
233
237
|
- lib/smart_message/header.rb
|
234
238
|
- lib/smart_message/logger.rb
|
235
239
|
- lib/smart_message/logger/base.rb
|
240
|
+
- lib/smart_message/logger/default.rb
|
236
241
|
- lib/smart_message/property_descriptions.rb
|
242
|
+
- lib/smart_message/property_validations.rb
|
237
243
|
- lib/smart_message/serializer.rb
|
238
244
|
- lib/smart_message/serializer/base.rb
|
239
245
|
- lib/smart_message/serializer/json.rb
|