smart_message 0.0.1
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 +7 -0
- data/.envrc +3 -0
- data/.gitignore +8 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +100 -0
- data/COMMITS.md +196 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +71 -0
- data/README.md +303 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/README.md +52 -0
- data/docs/architecture.md +370 -0
- data/docs/dispatcher.md +593 -0
- data/docs/examples.md +808 -0
- data/docs/getting-started.md +235 -0
- data/docs/ideas_to_think_about.md +329 -0
- data/docs/serializers.md +575 -0
- data/docs/transports.md +501 -0
- data/docs/troubleshooting.md +582 -0
- data/examples/01_point_to_point_orders.rb +200 -0
- data/examples/02_publish_subscribe_events.rb +364 -0
- data/examples/03_many_to_many_chat.rb +608 -0
- data/examples/README.md +335 -0
- data/examples/tmux_chat/README.md +283 -0
- data/examples/tmux_chat/bot_agent.rb +272 -0
- data/examples/tmux_chat/human_agent.rb +197 -0
- data/examples/tmux_chat/room_monitor.rb +158 -0
- data/examples/tmux_chat/shared_chat_system.rb +295 -0
- data/examples/tmux_chat/start_chat_demo.sh +190 -0
- data/examples/tmux_chat/stop_chat_demo.sh +22 -0
- data/lib/simple_stats.rb +57 -0
- data/lib/smart_message/base.rb +284 -0
- data/lib/smart_message/dispatcher/.keep +0 -0
- data/lib/smart_message/dispatcher.rb +146 -0
- data/lib/smart_message/errors.rb +29 -0
- data/lib/smart_message/header.rb +20 -0
- data/lib/smart_message/logger/base.rb +8 -0
- data/lib/smart_message/logger.rb +7 -0
- data/lib/smart_message/serializer/base.rb +23 -0
- data/lib/smart_message/serializer/json.rb +22 -0
- data/lib/smart_message/serializer.rb +10 -0
- data/lib/smart_message/transport/base.rb +85 -0
- data/lib/smart_message/transport/memory_transport.rb +69 -0
- data/lib/smart_message/transport/registry.rb +59 -0
- data/lib/smart_message/transport/stdout_transport.rb +62 -0
- data/lib/smart_message/transport.rb +41 -0
- data/lib/smart_message/version.rb +7 -0
- data/lib/smart_message/wrapper.rb +43 -0
- data/lib/smart_message.rb +54 -0
- data/smart_message.gemspec +53 -0
- metadata +252 -0
data/docs/serializers.md
ADDED
@@ -0,0 +1,575 @@
|
|
1
|
+
# Serializers
|
2
|
+
|
3
|
+
Serializers handle the encoding and decoding of message content, transforming Ruby objects into wire formats suitable for transmission and storage.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Serializers are responsible for:
|
8
|
+
- **Encoding**: Converting SmartMessage instances to transmittable formats
|
9
|
+
- **Decoding**: Converting received data back to Ruby objects
|
10
|
+
- **Format Support**: Handling different data formats (JSON, XML, MessagePack, etc.)
|
11
|
+
- **Type Safety**: Ensuring data integrity during conversion
|
12
|
+
|
13
|
+
## Built-in Serializers
|
14
|
+
|
15
|
+
### JSON Serializer
|
16
|
+
|
17
|
+
The default serializer that converts messages to/from JSON format.
|
18
|
+
|
19
|
+
**Features:**
|
20
|
+
- Human-readable output
|
21
|
+
- Wide compatibility
|
22
|
+
- Built on Ruby's standard JSON library
|
23
|
+
- Automatic property serialization
|
24
|
+
|
25
|
+
**Usage:**
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# Basic usage
|
29
|
+
serializer = SmartMessage::Serializer::JSON.new
|
30
|
+
|
31
|
+
# Configure in message class
|
32
|
+
class UserMessage < SmartMessage::Base
|
33
|
+
property :user_id
|
34
|
+
property :email
|
35
|
+
property :preferences
|
36
|
+
|
37
|
+
config do
|
38
|
+
serializer SmartMessage::Serializer::JSON.new
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Manual encoding/decoding
|
43
|
+
message = UserMessage.new(user_id: 123, email: "user@example.com")
|
44
|
+
encoded = serializer.encode(message)
|
45
|
+
# => '{"user_id":123,"email":"user@example.com","preferences":null}'
|
46
|
+
```
|
47
|
+
|
48
|
+
**Encoding Behavior:**
|
49
|
+
- All defined properties are included
|
50
|
+
- Nil values are preserved
|
51
|
+
- Internal `_sm_` properties are included in serialization
|
52
|
+
- Uses Ruby's `#to_json` method under the hood
|
53
|
+
|
54
|
+
## Serializer Interface
|
55
|
+
|
56
|
+
All serializers must implement the `SmartMessage::Serializer::Base` interface:
|
57
|
+
|
58
|
+
### Required Methods
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class CustomSerializer < SmartMessage::Serializer::Base
|
62
|
+
def initialize(options = {})
|
63
|
+
@options = options
|
64
|
+
# Custom initialization
|
65
|
+
end
|
66
|
+
|
67
|
+
# Convert SmartMessage instance to wire format
|
68
|
+
def encode(message_instance)
|
69
|
+
# Transform message_instance to your format
|
70
|
+
# Return string or binary data
|
71
|
+
end
|
72
|
+
|
73
|
+
# Convert wire format back to hash
|
74
|
+
def decode(payload)
|
75
|
+
# Transform payload string back to hash
|
76
|
+
# Return hash suitable for SmartMessage.new(hash)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
### Example: MessagePack Serializer
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
require 'msgpack'
|
85
|
+
|
86
|
+
class MessagePackSerializer < SmartMessage::Serializer::Base
|
87
|
+
def encode(message_instance)
|
88
|
+
message_instance.to_h.to_msgpack
|
89
|
+
end
|
90
|
+
|
91
|
+
def decode(payload)
|
92
|
+
MessagePack.unpack(payload)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Usage
|
97
|
+
class BinaryMessage < SmartMessage::Base
|
98
|
+
property :data
|
99
|
+
property :timestamp
|
100
|
+
|
101
|
+
config do
|
102
|
+
serializer MessagePackSerializer.new
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
### Example: XML Serializer
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
require 'nokogiri'
|
111
|
+
|
112
|
+
class XMLSerializer < SmartMessage::Serializer::Base
|
113
|
+
def encode(message_instance)
|
114
|
+
data = message_instance.to_h
|
115
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
116
|
+
xml.message do
|
117
|
+
data.each do |key, value|
|
118
|
+
xml.send(key, value)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
builder.to_xml
|
123
|
+
end
|
124
|
+
|
125
|
+
def decode(payload)
|
126
|
+
doc = Nokogiri::XML(payload)
|
127
|
+
hash = {}
|
128
|
+
doc.xpath('//message/*').each do |node|
|
129
|
+
hash[node.name] = node.text
|
130
|
+
end
|
131
|
+
hash
|
132
|
+
end
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
## Serialization Patterns
|
137
|
+
|
138
|
+
### Type Coercion
|
139
|
+
|
140
|
+
Handle type conversions during serialization:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
class TypedSerializer < SmartMessage::Serializer::Base
|
144
|
+
def encode(message_instance)
|
145
|
+
data = message_instance.to_h
|
146
|
+
|
147
|
+
# Convert specific types
|
148
|
+
data.transform_values do |value|
|
149
|
+
case value
|
150
|
+
when Time
|
151
|
+
value.iso8601
|
152
|
+
when Date
|
153
|
+
value.to_s
|
154
|
+
when BigDecimal
|
155
|
+
value.to_f
|
156
|
+
else
|
157
|
+
value
|
158
|
+
end
|
159
|
+
end.to_json
|
160
|
+
end
|
161
|
+
|
162
|
+
def decode(payload)
|
163
|
+
data = JSON.parse(payload)
|
164
|
+
|
165
|
+
# Convert back from strings
|
166
|
+
data.transform_values do |value|
|
167
|
+
case value
|
168
|
+
when /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
|
169
|
+
Time.parse(value)
|
170
|
+
else
|
171
|
+
value
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
### Nested Object Serialization
|
179
|
+
|
180
|
+
Handle complex nested structures:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
class NestedSerializer < SmartMessage::Serializer::Base
|
184
|
+
def encode(message_instance)
|
185
|
+
data = deep_serialize(message_instance.to_h)
|
186
|
+
JSON.generate(data)
|
187
|
+
end
|
188
|
+
|
189
|
+
def decode(payload)
|
190
|
+
data = JSON.parse(payload)
|
191
|
+
deep_deserialize(data)
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def deep_serialize(obj)
|
197
|
+
case obj
|
198
|
+
when Hash
|
199
|
+
obj.transform_values { |v| deep_serialize(v) }
|
200
|
+
when Array
|
201
|
+
obj.map { |v| deep_serialize(v) }
|
202
|
+
when SmartMessage::Base
|
203
|
+
# Serialize nested messages
|
204
|
+
obj.to_h
|
205
|
+
else
|
206
|
+
obj
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def deep_deserialize(obj)
|
211
|
+
case obj
|
212
|
+
when Hash
|
213
|
+
obj.transform_values { |v| deep_deserialize(v) }
|
214
|
+
when Array
|
215
|
+
obj.map { |v| deep_deserialize(v) }
|
216
|
+
else
|
217
|
+
obj
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
## Serialization Options
|
224
|
+
|
225
|
+
### Configurable Serializers
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
class ConfigurableJSONSerializer < SmartMessage::Serializer::Base
|
229
|
+
def initialize(options = {})
|
230
|
+
@pretty = options[:pretty] || false
|
231
|
+
@exclude_nil = options[:exclude_nil] || false
|
232
|
+
@date_format = options[:date_format] || :iso8601
|
233
|
+
end
|
234
|
+
|
235
|
+
def encode(message_instance)
|
236
|
+
data = message_instance.to_h
|
237
|
+
|
238
|
+
# Remove nil values if requested
|
239
|
+
data = data.compact if @exclude_nil
|
240
|
+
|
241
|
+
# Format dates
|
242
|
+
data = format_dates(data)
|
243
|
+
|
244
|
+
# Generate JSON
|
245
|
+
if @pretty
|
246
|
+
JSON.pretty_generate(data)
|
247
|
+
else
|
248
|
+
JSON.generate(data)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
private
|
253
|
+
|
254
|
+
def format_dates(data)
|
255
|
+
data.transform_values do |value|
|
256
|
+
case value
|
257
|
+
when Time, Date
|
258
|
+
case @date_format
|
259
|
+
when :iso8601
|
260
|
+
value.iso8601
|
261
|
+
when :unix
|
262
|
+
value.to_i
|
263
|
+
when :rfc2822
|
264
|
+
value.rfc2822
|
265
|
+
else
|
266
|
+
value.to_s
|
267
|
+
end
|
268
|
+
else
|
269
|
+
value
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Usage with options
|
276
|
+
class TimestampMessage < SmartMessage::Base
|
277
|
+
property :event
|
278
|
+
property :timestamp
|
279
|
+
|
280
|
+
config do
|
281
|
+
serializer ConfigurableJSONSerializer.new(
|
282
|
+
pretty: true,
|
283
|
+
exclude_nil: true,
|
284
|
+
date_format: :unix
|
285
|
+
)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
```
|
289
|
+
|
290
|
+
## Error Handling
|
291
|
+
|
292
|
+
### Serialization Errors
|
293
|
+
|
294
|
+
Handle encoding/decoding failures gracefully:
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
class SafeSerializer < SmartMessage::Serializer::Base
|
298
|
+
def encode(message_instance)
|
299
|
+
JSON.generate(message_instance.to_h)
|
300
|
+
rescue JSON::GeneratorError => e
|
301
|
+
# Log the error
|
302
|
+
puts "Serialization failed: #{e.message}"
|
303
|
+
|
304
|
+
# Fallback to simple string representation
|
305
|
+
message_instance.to_h.to_s
|
306
|
+
end
|
307
|
+
|
308
|
+
def decode(payload)
|
309
|
+
JSON.parse(payload)
|
310
|
+
rescue JSON::ParserError => e
|
311
|
+
# Log the error
|
312
|
+
puts "Deserialization failed: #{e.message}"
|
313
|
+
|
314
|
+
# Return error indicator or empty hash
|
315
|
+
{ "_error" => "Failed to deserialize: #{e.message}" }
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
### Validation During Serialization
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
class ValidatingSerializer < SmartMessage::Serializer::Base
|
324
|
+
def encode(message_instance)
|
325
|
+
validate_before_encoding(message_instance)
|
326
|
+
JSON.generate(message_instance.to_h)
|
327
|
+
end
|
328
|
+
|
329
|
+
def decode(payload)
|
330
|
+
data = JSON.parse(payload)
|
331
|
+
validate_after_decoding(data)
|
332
|
+
data
|
333
|
+
end
|
334
|
+
|
335
|
+
private
|
336
|
+
|
337
|
+
def validate_before_encoding(message)
|
338
|
+
required_fields = message.class.properties.select do |prop|
|
339
|
+
message.class.required?(prop)
|
340
|
+
end
|
341
|
+
|
342
|
+
missing = required_fields.select { |field| message[field].nil? }
|
343
|
+
|
344
|
+
if missing.any?
|
345
|
+
raise "Missing required fields: #{missing.join(', ')}"
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def validate_after_decoding(data)
|
350
|
+
unless data.is_a?(Hash)
|
351
|
+
raise "Expected hash, got #{data.class}"
|
352
|
+
end
|
353
|
+
|
354
|
+
# Additional validation logic
|
355
|
+
end
|
356
|
+
end
|
357
|
+
```
|
358
|
+
|
359
|
+
## Performance Considerations
|
360
|
+
|
361
|
+
### Binary Serialization
|
362
|
+
|
363
|
+
For high-performance scenarios, consider binary formats:
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
class ProtobufSerializer < SmartMessage::Serializer::Base
|
367
|
+
def initialize(proto_class)
|
368
|
+
@proto_class = proto_class
|
369
|
+
end
|
370
|
+
|
371
|
+
def encode(message_instance)
|
372
|
+
proto_obj = @proto_class.new(message_instance.to_h)
|
373
|
+
proto_obj.serialize_to_string
|
374
|
+
end
|
375
|
+
|
376
|
+
def decode(payload)
|
377
|
+
proto_obj = @proto_class.parse(payload)
|
378
|
+
proto_obj.to_h
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# Usage
|
383
|
+
UserProto = Google::Protobuf::DescriptorPool.generated_pool.lookup("User").msgclass
|
384
|
+
|
385
|
+
class UserMessage < SmartMessage::Base
|
386
|
+
property :user_id
|
387
|
+
property :name
|
388
|
+
|
389
|
+
config do
|
390
|
+
serializer ProtobufSerializer.new(UserProto)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
```
|
394
|
+
|
395
|
+
### Streaming Serialization
|
396
|
+
|
397
|
+
For large messages, consider streaming:
|
398
|
+
|
399
|
+
```ruby
|
400
|
+
class StreamingSerializer < SmartMessage::Serializer::Base
|
401
|
+
def encode(message_instance)
|
402
|
+
StringIO.new.tap do |io|
|
403
|
+
JSON.dump(message_instance.to_h, io)
|
404
|
+
end.string
|
405
|
+
end
|
406
|
+
|
407
|
+
def decode(payload)
|
408
|
+
StringIO.new(payload).tap do |io|
|
409
|
+
JSON.load(io)
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
```
|
414
|
+
|
415
|
+
## Compression Support
|
416
|
+
|
417
|
+
### Compressed Serialization
|
418
|
+
|
419
|
+
```ruby
|
420
|
+
class CompressedJSONSerializer < SmartMessage::Serializer::Base
|
421
|
+
def encode(message_instance)
|
422
|
+
json_data = JSON.generate(message_instance.to_h)
|
423
|
+
Zlib::Deflate.deflate(json_data)
|
424
|
+
end
|
425
|
+
|
426
|
+
def decode(payload)
|
427
|
+
json_data = Zlib::Inflate.inflate(payload)
|
428
|
+
JSON.parse(json_data)
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
# Usage for large messages
|
433
|
+
class LargeDataMessage < SmartMessage::Base
|
434
|
+
property :dataset
|
435
|
+
property :metadata
|
436
|
+
|
437
|
+
config do
|
438
|
+
serializer CompressedJSONSerializer.new
|
439
|
+
end
|
440
|
+
end
|
441
|
+
```
|
442
|
+
|
443
|
+
## Testing Serializers
|
444
|
+
|
445
|
+
### Serializer Testing Patterns
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
RSpec.describe CustomSerializer do
|
449
|
+
let(:serializer) { CustomSerializer.new }
|
450
|
+
let(:message) do
|
451
|
+
TestMessage.new(
|
452
|
+
user_id: 123,
|
453
|
+
email: "test@example.com",
|
454
|
+
created_at: Time.parse("2025-08-17T10:30:00Z")
|
455
|
+
)
|
456
|
+
end
|
457
|
+
|
458
|
+
describe "#encode" do
|
459
|
+
it "produces valid output" do
|
460
|
+
result = serializer.encode(message)
|
461
|
+
expect(result).to be_a(String)
|
462
|
+
expect(result).not_to be_empty
|
463
|
+
end
|
464
|
+
|
465
|
+
it "includes all properties" do
|
466
|
+
result = serializer.encode(message)
|
467
|
+
# Format-specific assertions
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
describe "#decode" do
|
472
|
+
it "roundtrips correctly" do
|
473
|
+
encoded = serializer.encode(message)
|
474
|
+
decoded = serializer.decode(encoded)
|
475
|
+
|
476
|
+
expect(decoded["user_id"]).to eq(123)
|
477
|
+
expect(decoded["email"]).to eq("test@example.com")
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
describe "error handling" do
|
482
|
+
it "handles invalid input gracefully" do
|
483
|
+
expect { serializer.decode("invalid") }.not_to raise_error
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
```
|
488
|
+
|
489
|
+
### Mock Serializer for Testing
|
490
|
+
|
491
|
+
```ruby
|
492
|
+
class MockSerializer < SmartMessage::Serializer::Base
|
493
|
+
attr_reader :encoded_messages, :decoded_payloads
|
494
|
+
|
495
|
+
def initialize
|
496
|
+
@encoded_messages = []
|
497
|
+
@decoded_payloads = []
|
498
|
+
end
|
499
|
+
|
500
|
+
def encode(message_instance)
|
501
|
+
@encoded_messages << message_instance
|
502
|
+
"mock_encoded_#{message_instance.object_id}"
|
503
|
+
end
|
504
|
+
|
505
|
+
def decode(payload)
|
506
|
+
@decoded_payloads << payload
|
507
|
+
{ "mock" => "decoded", "payload" => payload }
|
508
|
+
end
|
509
|
+
|
510
|
+
def clear
|
511
|
+
@encoded_messages.clear
|
512
|
+
@decoded_payloads.clear
|
513
|
+
end
|
514
|
+
end
|
515
|
+
```
|
516
|
+
|
517
|
+
## Common Serialization Issues
|
518
|
+
|
519
|
+
### Handling Special Values
|
520
|
+
|
521
|
+
```ruby
|
522
|
+
class RobustJSONSerializer < SmartMessage::Serializer::Base
|
523
|
+
def encode(message_instance)
|
524
|
+
data = sanitize_for_json(message_instance.to_h)
|
525
|
+
JSON.generate(data)
|
526
|
+
end
|
527
|
+
|
528
|
+
private
|
529
|
+
|
530
|
+
def sanitize_for_json(obj)
|
531
|
+
case obj
|
532
|
+
when Hash
|
533
|
+
obj.transform_values { |v| sanitize_for_json(v) }
|
534
|
+
when Array
|
535
|
+
obj.map { |v| sanitize_for_json(v) }
|
536
|
+
when Float
|
537
|
+
return nil if obj.nan? || obj.infinite?
|
538
|
+
obj
|
539
|
+
when BigDecimal
|
540
|
+
obj.to_f
|
541
|
+
when Symbol
|
542
|
+
obj.to_s
|
543
|
+
when Complex, Rational
|
544
|
+
obj.to_f
|
545
|
+
else
|
546
|
+
obj
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
```
|
551
|
+
|
552
|
+
### Character Encoding
|
553
|
+
|
554
|
+
```ruby
|
555
|
+
class EncodingAwareSerializer < SmartMessage::Serializer::Base
|
556
|
+
def encode(message_instance)
|
557
|
+
data = message_instance.to_h
|
558
|
+
json = JSON.generate(data)
|
559
|
+
json.force_encoding('UTF-8')
|
560
|
+
end
|
561
|
+
|
562
|
+
def decode(payload)
|
563
|
+
# Ensure proper encoding
|
564
|
+
payload = payload.force_encoding('UTF-8')
|
565
|
+
JSON.parse(payload)
|
566
|
+
end
|
567
|
+
end
|
568
|
+
```
|
569
|
+
|
570
|
+
## Next Steps
|
571
|
+
|
572
|
+
- [Custom Serializers](custom-serializers.md) - Build your own serializer
|
573
|
+
- [Transports](transports.md) - How serializers work with transports
|
574
|
+
- [Message Headers](headers.md) - Understanding message metadata
|
575
|
+
- [Examples](examples.md) - Real-world serialization patterns
|