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.
@@ -0,0 +1,477 @@
1
+ #!/usr/bin/env ruby
2
+ # examples/07_error_handling_scenarios.rb
3
+ #
4
+ # Error Handling Example: SmartMessage Validation and Version Control
5
+ #
6
+ # This example demonstrates how SmartMessage handles various error conditions:
7
+ # 1. Missing required properties
8
+ # 2. Property validation failures
9
+ # 3. Version mismatches between publishers and subscribers
10
+ #
11
+ # These scenarios help developers understand SmartMessage's robust error handling
12
+ # and how to build resilient message-based systems.
13
+
14
+ require_relative '../lib/smart_message'
15
+
16
+ puts "=== SmartMessage Example: Error Handling Scenarios ==="
17
+ puts
18
+
19
+ # Message for testing required property validation
20
+ class UserRegistrationMessage < SmartMessage::Base
21
+ version 1
22
+ description "User registration data with strict validation requirements"
23
+
24
+ property :user_id,
25
+ required: true,
26
+ description: "Unique identifier for the new user account"
27
+ property :email,
28
+ required: true,
29
+ validate: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
30
+ validation_message: "Must be a valid email address",
31
+ description: "User's email address (must be valid format)"
32
+ property :age,
33
+ required: true,
34
+ validate: ->(v) { v.is_a?(Integer) && v.between?(13, 120) },
35
+ validation_message: "Age must be an integer between 13 and 120",
36
+ description: "User's age in years (13-120)"
37
+ property :username,
38
+ required: true,
39
+ validate: ->(v) { v.is_a?(String) && v.match?(/\A[a-zA-Z0-9_]{3,20}\z/) },
40
+ validation_message: "Username must be 3-20 characters, letters/numbers/underscores only",
41
+ description: "Unique username (3-20 chars, alphanumeric + underscore)"
42
+ property :subscription_type,
43
+ required: false,
44
+ validate: ['free', 'premium', 'enterprise'],
45
+ validation_message: "Subscription type must be 'free', 'premium', or 'enterprise'",
46
+ description: "User's subscription tier"
47
+ property :created_at,
48
+ default: -> { Time.now.iso8601 },
49
+ description: "Timestamp when user account was created"
50
+
51
+ config do
52
+ transport SmartMessage::Transport::StdoutTransport.new(loopback: true)
53
+ serializer SmartMessage::Serializer::JSON.new
54
+ end
55
+
56
+ def self.process(message_header, message_payload)
57
+ user_data = JSON.parse(message_payload)
58
+ puts "✅ User registration processed: #{user_data['username']} (#{user_data['email']})"
59
+ end
60
+ end
61
+
62
+ # Version 2 of the same message (for version mismatch testing)
63
+ class UserRegistrationMessageV2 < SmartMessage::Base
64
+ version 2
65
+ description "Version 2 of user registration with additional required fields"
66
+
67
+ property :user_id,
68
+ required: true,
69
+ description: "Unique identifier for the new user account"
70
+ property :email,
71
+ required: true,
72
+ validate: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
73
+ validation_message: "Must be a valid email address",
74
+ description: "User's email address (must be valid format)"
75
+ property :age,
76
+ required: true,
77
+ validate: ->(v) { v.is_a?(Integer) && v.between?(13, 120) },
78
+ validation_message: "Age must be an integer between 13 and 120",
79
+ description: "User's age in years (13-120)"
80
+ property :username,
81
+ required: true,
82
+ validate: ->(v) { v.is_a?(String) && v.match?(/\A[a-zA-Z0-9_]{3,20}\z/) },
83
+ validation_message: "Username must be 3-20 characters, letters/numbers/underscores only",
84
+ description: "Unique username (3-20 chars, alphanumeric + underscore)"
85
+ property :phone_number,
86
+ required: true, # New required field in version 2
87
+ validate: ->(v) { v.is_a?(String) && v.match?(/\A\+?[1-9]\d{1,14}\z/) },
88
+ validation_message: "Phone number must be in international format",
89
+ description: "User's phone number in international format (new in v2)"
90
+ property :subscription_type,
91
+ required: false,
92
+ validate: ['free', 'premium', 'enterprise'],
93
+ validation_message: "Subscription type must be 'free', 'premium', or 'enterprise'",
94
+ description: "User's subscription tier"
95
+ property :created_at,
96
+ default: -> { Time.now.iso8601 },
97
+ description: "Timestamp when user account was created"
98
+
99
+ config do
100
+ transport SmartMessage::Transport::StdoutTransport.new(loopback: true)
101
+ serializer SmartMessage::Serializer::JSON.new
102
+ end
103
+
104
+ def self.process(message_header, message_payload)
105
+ user_data = JSON.parse(message_payload)
106
+ puts "✅ User registration V2 processed: #{user_data['username']} (#{user_data['email']}, #{user_data['phone_number']})"
107
+ end
108
+ end
109
+
110
+ # Message for testing Hashie::Dash limitation with multiple required fields
111
+ class MultiRequiredMessage < SmartMessage::Base
112
+ description "Test message demonstrating Hashie::Dash required property limitation"
113
+
114
+ property :field_a, required: true, description: "First required field"
115
+ property :field_b, required: true, description: "Second required field"
116
+ property :field_c, required: true, description: "Third required field"
117
+ property :field_d, required: true, description: "Fourth required field"
118
+ property :optional_field, description: "Optional field for comparison"
119
+
120
+ config do
121
+ transport SmartMessage::Transport::StdoutTransport.new(loopback: true)
122
+ serializer SmartMessage::Serializer::JSON.new
123
+ end
124
+ end
125
+
126
+ # Error demonstration class
127
+ class ErrorDemonstrator
128
+ def initialize
129
+ puts "🚀 Starting Error Handling Demonstrations\n"
130
+ end
131
+
132
+ def run_all_scenarios
133
+ scenario_1_missing_required_properties
134
+ puts "\n" + "="*60 + "\n"
135
+
136
+ scenario_2_validation_failures
137
+ puts "\n" + "="*60 + "\n"
138
+
139
+ scenario_3_version_mismatches
140
+ puts "\n" + "="*60 + "\n"
141
+
142
+ scenario_4_hashie_limitation
143
+ puts "\n" + "="*60 + "\n"
144
+
145
+ puts "✨ All error scenarios demonstrated!"
146
+ puts "\nKey Takeaways:"
147
+ puts "• SmartMessage validates required properties at creation time"
148
+ puts "• Custom validation rules provide detailed error messages"
149
+ puts "• Version mismatches are detected during message processing"
150
+ puts "• Hashie::Dash limitation: only reports first missing required property"
151
+ puts "• Proper error handling ensures system resilience"
152
+ end
153
+
154
+ private
155
+
156
+ def scenario_1_missing_required_properties
157
+ puts "📋 SCENARIO 1: Missing Required Properties"
158
+ puts "="*50
159
+ puts
160
+
161
+ puts "Attempting to create UserRegistrationMessage with missing required fields..."
162
+ puts
163
+
164
+ # Test Case 1: Missing user_id
165
+ puts "🔴 Test Case 1A: Missing user_id (required field)"
166
+ begin
167
+ missing_user_id = UserRegistrationMessage.new(
168
+ email: "john@example.com",
169
+ age: 25,
170
+ username: "johndoe"
171
+ )
172
+ puts "❌ ERROR: Should have failed but didn't!"
173
+ rescue => e
174
+ puts "✅ Expected error caught: #{e.class.name}"
175
+ puts " Message: #{e.message}"
176
+ end
177
+ puts
178
+
179
+ # Test Case 2: Missing multiple required fields
180
+ puts "🔴 Test Case 1B: Missing multiple required fields (email, age)"
181
+ begin
182
+ missing_multiple = UserRegistrationMessage.new(
183
+ user_id: "USER-123",
184
+ username: "johndoe"
185
+ )
186
+ puts "❌ ERROR: Should have failed but didn't!"
187
+ rescue => e
188
+ puts "✅ Expected error caught: #{e.class.name}"
189
+ puts " Message: #{e.message}"
190
+ end
191
+ puts
192
+
193
+ # Test Case 3: Valid message (should work)
194
+ puts "🟢 Test Case 1C: All required fields provided (should succeed)"
195
+ begin
196
+ valid_user = UserRegistrationMessage.new(
197
+ user_id: "USER-123",
198
+ email: "john@example.com",
199
+ age: 25,
200
+ username: "johndoe"
201
+ )
202
+ puts "✅ Message created successfully"
203
+ puts " User: #{valid_user.username} (#{valid_user.email})"
204
+
205
+ # Subscribe and publish to test processing
206
+ UserRegistrationMessage.subscribe
207
+ valid_user.publish
208
+ rescue => e
209
+ puts "❌ Unexpected error: #{e.class.name}: #{e.message}"
210
+ end
211
+ end
212
+
213
+ def scenario_2_validation_failures
214
+ puts "📋 SCENARIO 2: Property Validation Failures"
215
+ puts "="*50
216
+ puts
217
+
218
+ puts "Testing custom validation rules..."
219
+ puts "Note: SmartMessage validates properties when validate! is called explicitly"
220
+ puts " or automatically during publish operations."
221
+ puts
222
+
223
+ # Test Case 1: Invalid email format
224
+ puts "🔴 Test Case 2A: Invalid email format"
225
+ begin
226
+ invalid_email = UserRegistrationMessage.new(
227
+ user_id: "USER-124",
228
+ email: "not-an-email", # Invalid format
229
+ age: 25,
230
+ username: "janedoe"
231
+ )
232
+ puts " Message created, now validating..."
233
+ invalid_email.validate! # Explicitly trigger validation
234
+ puts "❌ ERROR: Should have failed validation but didn't!"
235
+ rescue => e
236
+ puts "✅ Expected validation error caught: #{e.class.name}"
237
+ puts " Message: #{e.message}"
238
+ end
239
+ puts
240
+
241
+ # Test Case 2: Invalid age (too young)
242
+ puts "🔴 Test Case 2B: Invalid age (too young)"
243
+ begin
244
+ invalid_age = UserRegistrationMessage.new(
245
+ user_id: "USER-125",
246
+ email: "kid@example.com",
247
+ age: 10, # Too young (< 13)
248
+ username: "kiduser"
249
+ )
250
+ puts " Message created, now validating..."
251
+ invalid_age.validate! # Explicitly trigger validation
252
+ puts "❌ ERROR: Should have failed validation but didn't!"
253
+ rescue => e
254
+ puts "✅ Expected validation error caught: #{e.class.name}"
255
+ puts " Message: #{e.message}"
256
+ end
257
+ puts
258
+
259
+ # Test Case 3: Invalid username format
260
+ puts "🔴 Test Case 2C: Invalid username (special characters)"
261
+ begin
262
+ invalid_username = UserRegistrationMessage.new(
263
+ user_id: "USER-126",
264
+ email: "user@example.com",
265
+ age: 30,
266
+ username: "user@123!" # Contains invalid characters
267
+ )
268
+ puts " Message created, now validating..."
269
+ invalid_username.validate! # Explicitly trigger validation
270
+ puts "❌ ERROR: Should have failed validation but didn't!"
271
+ rescue => e
272
+ puts "✅ Expected validation error caught: #{e.class.name}"
273
+ puts " Message: #{e.message}"
274
+ end
275
+ puts
276
+
277
+ # Test Case 4: Invalid subscription type
278
+ puts "🔴 Test Case 2D: Invalid subscription type"
279
+ begin
280
+ invalid_subscription = UserRegistrationMessage.new(
281
+ user_id: "USER-127",
282
+ email: "user@example.com",
283
+ age: 30,
284
+ username: "validuser",
285
+ subscription_type: "platinum" # Not in allowed list
286
+ )
287
+ puts " Message created, now validating..."
288
+ invalid_subscription.validate! # Explicitly trigger validation
289
+ puts "❌ ERROR: Should have failed validation but didn't!"
290
+ rescue => e
291
+ puts "✅ Expected validation error caught: #{e.class.name}"
292
+ puts " Message: #{e.message}"
293
+ end
294
+ puts
295
+
296
+ # Test Case 5: All validations pass
297
+ puts "🟢 Test Case 2E: All validations pass (should succeed)"
298
+ begin
299
+ valid_user = UserRegistrationMessage.new(
300
+ user_id: "USER-128",
301
+ email: "valid@example.com",
302
+ age: 25,
303
+ username: "validuser123",
304
+ subscription_type: "premium"
305
+ )
306
+ puts " Message created, now validating..."
307
+ valid_user.validate! # Explicitly trigger validation
308
+ puts "✅ All validations passed successfully"
309
+ puts " User: #{valid_user.username} (#{valid_user.subscription_type} plan)"
310
+
311
+ valid_user.publish
312
+ rescue => e
313
+ puts "❌ Unexpected error: #{e.class.name}: #{e.message}"
314
+ end
315
+ end
316
+
317
+ def scenario_3_version_mismatches
318
+ puts "📋 SCENARIO 3: Version Mismatches"
319
+ puts "="*50
320
+ puts
321
+
322
+ puts "Testing version compatibility between publishers and subscribers..."
323
+ puts
324
+
325
+ # Subscribe to V1 messages
326
+ puts "🔧 Setting up V1 subscriber..."
327
+ UserRegistrationMessage.subscribe
328
+ puts
329
+
330
+ # Subscribe to V2 messages
331
+ puts "🔧 Setting up V2 subscriber..."
332
+ UserRegistrationMessageV2.subscribe
333
+ puts
334
+
335
+ # Test Case 1: V1 message to V1 subscriber (should work)
336
+ puts "🟢 Test Case 3A: V1 message → V1 subscriber (compatible)"
337
+ begin
338
+ v1_message = UserRegistrationMessage.new(
339
+ user_id: "USER-V1-001",
340
+ email: "v1user@example.com",
341
+ age: 28,
342
+ username: "v1user"
343
+ )
344
+ puts "✅ V1 Message created (version #{v1_message._sm_header.version})"
345
+ v1_message.publish
346
+ rescue => e
347
+ puts "❌ Unexpected error: #{e.class.name}: #{e.message}"
348
+ end
349
+ puts
350
+
351
+ # Test Case 2: V2 message to V2 subscriber (should work)
352
+ puts "🟢 Test Case 3B: V2 message → V2 subscriber (compatible)"
353
+ begin
354
+ v2_message = UserRegistrationMessageV2.new(
355
+ user_id: "USER-V2-001",
356
+ email: "v2user@example.com",
357
+ age: 32,
358
+ username: "v2user",
359
+ phone_number: "+1234567890"
360
+ )
361
+ puts "✅ V2 Message created (version #{v2_message._sm_header.version})"
362
+ v2_message.publish
363
+ rescue => e
364
+ puts "❌ Unexpected error: #{e.class.name}: #{e.message}"
365
+ end
366
+ puts
367
+
368
+ # Test Case 3: Demonstrate version mismatch handling
369
+ puts "🔴 Test Case 3C: Version mismatch demonstration"
370
+ puts " Creating a message with manually modified version header..."
371
+ begin
372
+ # Create a V1 message but manually change its version
373
+ version_mismatch_message = UserRegistrationMessage.new(
374
+ user_id: "USER-MISMATCH-001",
375
+ email: "mismatch@example.com",
376
+ age: 35,
377
+ username: "mismatchuser"
378
+ )
379
+
380
+ puts "✅ Original message created with version #{version_mismatch_message._sm_header.version}"
381
+
382
+ # Manually modify the header version to simulate a mismatch
383
+ version_mismatch_message._sm_header.version = 99
384
+ puts "🔧 Manually changed header version to #{version_mismatch_message._sm_header.version}"
385
+
386
+ # Try to validate - this should catch the version mismatch
387
+ puts "🔍 Attempting to validate message with mismatched version..."
388
+ version_mismatch_message.validate!
389
+
390
+ puts "❌ ERROR: Version mismatch should have been detected!"
391
+ rescue => e
392
+ puts "✅ Expected version mismatch error caught: #{e.class.name}"
393
+ puts " Message: #{e.message}"
394
+ end
395
+ puts
396
+
397
+ # Test Case 4: Show version information
398
+ puts "📊 Test Case 3D: Version information display"
399
+ v1_msg = UserRegistrationMessage.new(user_id: "INFO-V1", email: "info@example.com", age: 25, username: "infouser")
400
+ v2_msg = UserRegistrationMessageV2.new(user_id: "INFO-V2", email: "info@example.com", age: 25, username: "infouser", phone_number: "+1234567890")
401
+
402
+ puts "📋 Version Information:"
403
+ puts " UserRegistrationMessage (V1):"
404
+ puts " Class version: #{UserRegistrationMessage.version}"
405
+ puts " Expected header version: #{UserRegistrationMessage.expected_header_version}"
406
+ puts " Instance header version: #{v1_msg._sm_header.version}"
407
+ puts
408
+ puts " UserRegistrationMessageV2 (V2):"
409
+ puts " Class version: #{UserRegistrationMessageV2.version}"
410
+ puts " Expected header version: #{UserRegistrationMessageV2.expected_header_version}"
411
+ puts " Instance header version: #{v2_msg._sm_header.version}"
412
+ end
413
+
414
+ def scenario_4_hashie_limitation
415
+ puts "📋 SCENARIO 4: Hashie::Dash Limitation with Multiple Missing Required Properties"
416
+ puts "="*80
417
+ puts
418
+
419
+ puts "Demonstrating a limitation in Hashie::Dash (SmartMessage's base class)..."
420
+ puts "When multiple required properties are missing, only the FIRST one is reported."
421
+ puts
422
+
423
+ puts "🔴 Test Case 4A: All 4 required fields missing (field_a, field_b, field_c, field_d)"
424
+ puts "Expected: Ideally should report all missing fields"
425
+ puts "Actual Hashie::Dash behavior:"
426
+ begin
427
+ message = MultiRequiredMessage.new(optional_field: "present")
428
+ puts "❌ ERROR: Should have failed but didn't!"
429
+ rescue => e
430
+ puts "✅ Error caught: #{e.class.name}"
431
+ puts " Message: #{e.message}"
432
+ puts " 📊 Analysis: Only 'field_a' is reported, despite 3 other missing required fields"
433
+ end
434
+ puts
435
+
436
+ puts "🔴 Test Case 4B: Incremental discovery (fixing one field at a time)"
437
+ test_data = [
438
+ { name: "Provide field_a only", data: { field_a: "value_a", optional_field: "test" } },
439
+ { name: "Provide field_a + field_b", data: { field_a: "value_a", field_b: "value_b", optional_field: "test" } },
440
+ { name: "Provide field_a + field_b + field_c", data: { field_a: "value_a", field_b: "value_b", field_c: "value_c", optional_field: "test" } }
441
+ ]
442
+
443
+ test_data.each_with_index do |test_case, index|
444
+ puts " #{index + 1}. #{test_case[:name]}:"
445
+ begin
446
+ message = MultiRequiredMessage.new(test_case[:data])
447
+ puts " ✅ Success: Message created"
448
+ rescue => e
449
+ field_name = e.message.match(/property '([^']+)'/)[1] rescue 'unknown'
450
+ puts " ❌ Still missing: #{field_name}"
451
+ end
452
+ end
453
+
454
+ puts
455
+ puts "📊 LIMITATION SUMMARY:"
456
+ puts "="*50
457
+ puts "🔍 Issue: Hashie::Dash stops validation at the first missing required property"
458
+ puts "📋 Impact: Poor developer experience - must fix errors one at a time"
459
+ puts "⚡ UX Problem: In forms with many required fields, users see only one error at a time"
460
+ puts
461
+ puts "💡 Potential Solutions:"
462
+ puts "• Override Hashie::Dash initialization to collect ALL missing required fields"
463
+ puts "• Add SmartMessage enhancement: validate_all_required! method"
464
+ puts "• Provide aggregate error messages listing all missing properties"
465
+ puts "• Use custom validation that reports multiple missing fields simultaneously"
466
+ puts
467
+ puts "🚀 Example of better error message:"
468
+ puts ' "Missing required properties: field_a, field_b, field_c, field_d"'
469
+ puts " vs current: \"The property 'field_a' is required\""
470
+ end
471
+ end
472
+
473
+ # Run the demo if this file is executed directly
474
+ if __FILE__ == $0
475
+ demo = ErrorDemonstrator.new
476
+ demo.run_all_scenarios
477
+ end
@@ -73,6 +73,9 @@ class BotChatAgent < BaseAgent
73
73
  return unless @active_rooms.include?(chat_data['room_id'])
74
74
  return if chat_data['sender_id'] == @agent_id
75
75
 
76
+ # Don't respond to other bots to avoid infinite loops
77
+ return if chat_data['message_type'] == 'bot'
78
+
76
79
  # Log the message
77
80
  log_display("👁️ [#{chat_data['room_id']}] #{chat_data['sender_name']}: #{chat_data['content']}")
78
81
 
@@ -80,7 +83,7 @@ class BotChatAgent < BaseAgent
80
83
  if chat_data['content'].start_with?('/')
81
84
  handle_inline_command(chat_data)
82
85
  else
83
- # Respond to certain keywords
86
+ # Respond to certain keywords from human users only
84
87
  respond_to_keywords(chat_data)
85
88
  end
86
89
  end
@@ -102,15 +102,26 @@ SmartMessage::Transport.register(:file, FileTransport)
102
102
 
103
103
  # Define the Chat Message
104
104
  class ChatMessage < SmartMessage::Base
105
- property :message_id
106
- property :room_id
107
- property :sender_id
108
- property :sender_name
109
- property :content
110
- property :message_type # 'user', 'bot', 'system'
111
- property :timestamp
112
- property :mentions
113
- property :metadata
105
+ description "Chat messages for tmux-based multi-pane chat demonstration"
106
+
107
+ property :message_id,
108
+ description: "Unique identifier for this chat message"
109
+ property :room_id,
110
+ description: "Chat room identifier for message routing"
111
+ property :sender_id,
112
+ description: "Unique ID of the user or bot sending the message"
113
+ property :sender_name,
114
+ description: "Display name of the message sender"
115
+ property :content,
116
+ description: "The actual text content of the chat message"
117
+ property :message_type,
118
+ description: "Message type: 'user', 'bot', or 'system'"
119
+ property :timestamp,
120
+ description: "ISO8601 timestamp when message was sent"
121
+ property :mentions,
122
+ description: "Array of user IDs mentioned in the message"
123
+ property :metadata,
124
+ description: "Additional message metadata for tmux display"
114
125
 
115
126
  config do
116
127
  transport SmartMessage::Transport.create(:file)
@@ -124,13 +135,22 @@ end
124
135
 
125
136
  # Define Bot Command Message
126
137
  class BotCommandMessage < SmartMessage::Base
127
- property :command_id
128
- property :room_id
129
- property :user_id
130
- property :user_name
131
- property :command
132
- property :parameters
133
- property :timestamp
138
+ description "Commands sent to chat bots in the tmux chat system"
139
+
140
+ property :command_id,
141
+ description: "Unique identifier for this bot command"
142
+ property :room_id,
143
+ description: "Chat room where the command was issued"
144
+ property :user_id,
145
+ description: "User who issued the bot command"
146
+ property :user_name,
147
+ description: "Display name of the user issuing the command"
148
+ property :command,
149
+ description: "Bot command name (help, joke, weather, etc.)"
150
+ property :parameters,
151
+ description: "Array of parameters for the bot command"
152
+ property :timestamp,
153
+ description: "ISO8601 timestamp when command was issued"
134
154
 
135
155
  config do
136
156
  transport SmartMessage::Transport.create(:file)
@@ -144,12 +164,20 @@ end
144
164
 
145
165
  # Define System Notification Message
146
166
  class SystemNotificationMessage < SmartMessage::Base
147
- property :notification_id
148
- property :room_id
149
- property :notification_type
150
- property :content
151
- property :timestamp
152
- property :metadata
167
+ description "System notifications for tmux chat room events and status updates"
168
+
169
+ property :notification_id,
170
+ description: "Unique identifier for this system notification"
171
+ property :room_id,
172
+ description: "Chat room affected by this notification"
173
+ property :notification_type,
174
+ description: "Type of notification (user_joined, user_left, etc.)"
175
+ property :content,
176
+ description: "Human-readable description of the system event"
177
+ property :timestamp,
178
+ description: "ISO8601 timestamp when the event occurred"
179
+ property :metadata,
180
+ description: "Additional system event metadata for tmux display"
153
181
 
154
182
  config do
155
183
  transport SmartMessage::Transport.create(:file)