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.
@@ -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