sourced-message 0.3.0 → 0.5.0
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 +26 -0
- data/README.md +10 -3
- data/lib/sourced/message/codec.rb +9 -5
- data/lib/sourced/message.rb +40 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8202b720a3a57bf15a429a136797186c2bc8bd2721a7539e2f08495e7078923
|
|
4
|
+
data.tar.gz: 6532b0db8f33fb6aa9ba4d585d5b0424fd089be4917e994f4a1abd341db132b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba7bafee643414f03b642074c521257fb731f663dc653d6ddedb3c6ca2d26304e727d7c719fac830d196aee3e1086aed22d126759c4671a3242fcc6c89fff5a8
|
|
7
|
+
data.tar.gz: 5f979cf196b3d6e0bc3b37ddc5b85f0a9284b1d8fad4a15a877ca835b18a747e49d9c1e5a5a0ad0b4acaaffe8b7bbb02411ee34f888445a30f6f3835c6eb267b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.0]
|
|
4
|
+
|
|
5
|
+
- `Message#tags`: an envelope attribute (`Types::Tags`, Symbol keys and String values,
|
|
6
|
+
empty by default) holding key/value pairs a store can index alongside the payload's,
|
|
7
|
+
without the message class declaring them. Codecs encode and decode it with the rest
|
|
8
|
+
of the envelope.
|
|
9
|
+
- `#with_tags(**tags)` returns a copy with tags merged in, values stringified. It raises
|
|
10
|
+
`TagConflictError` when a tag names a payload attribute with a different value.
|
|
11
|
+
- `#correlate` leaves the target's tags alone: tags never pass on to messages produced
|
|
12
|
+
in reaction, unlike metadata.
|
|
13
|
+
- `Sourced::Message::VERSION` is now `0.5.0`.
|
|
14
|
+
|
|
15
|
+
## [0.4.0]
|
|
16
|
+
|
|
17
|
+
- Requires plumb 0.4, whose codecs rewrite Hash keys like values: `#encode` now emits
|
|
18
|
+
String keys (`{'type' => ..., 'payload' => {'name' => ...}}`), and decoders accept
|
|
19
|
+
String or Symbol keys. `JSON.dump` output is unchanged.
|
|
20
|
+
- `Codec#decode` reads the envelope's `type` and `id` under either key form, so
|
|
21
|
+
`decode(encode(message))` works without a JSON round trip.
|
|
22
|
+
- `metadata` is typed as `Types::Metadata`: Symbol keys and `Types::JSONValue` values
|
|
23
|
+
(String, Numeric, boolean, nil, and Arrays/Symbol-keyed Hashes of those, at any depth).
|
|
24
|
+
Codecs now restore nested metadata keys as Symbols. A non-JSON value (a Time, a
|
|
25
|
+
Symbol) makes the message invalid and fails `#encode`, where `JSON.dump` used to
|
|
26
|
+
stringify it silently.
|
|
27
|
+
- `Sourced::Message::VERSION` is now `0.4.0`.
|
|
28
|
+
|
|
3
29
|
## [0.3.0]
|
|
4
30
|
|
|
5
31
|
- Add `Sourced::Message#correlation_type`, the type counterpart of `correlation_id`:
|
data/README.md
CHANGED
|
@@ -9,11 +9,12 @@ A message is a [Plumb](https://github.com/ismasan/plumb)-typed value object with
|
|
|
9
9
|
- an auto-generated `id` and `created_at` timestamp
|
|
10
10
|
- `causation_id` / `correlation_id` for tracing causal chains across processes
|
|
11
11
|
- arbitrary `metadata`
|
|
12
|
+
- `tags`: key/value pairs for a store to index, outside the payload
|
|
12
13
|
- a global **type registry** that can reconstruct any message from a plain hash — handy for transports, queues and event stores
|
|
13
14
|
- **codecs** that serialize messages to JSON or to form params and back, preserving the types each payload declares
|
|
14
15
|
- scheduling helpers (`#at` / `#in`) for delayed messages
|
|
15
16
|
|
|
16
|
-
Messages are immutable: every "mutating" method (`#with_payload`, `#with_metadata`, `#at`, `#correlate`) returns a copy.
|
|
17
|
+
Messages are immutable: every "mutating" method (`#with_payload`, `#with_metadata`, `#with_tags`, `#at`, `#correlate`) returns a copy.
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
@@ -69,6 +70,7 @@ msg.id # => "5f6e..." (auto-generated UUID)
|
|
|
69
70
|
msg.type # => "course.created"
|
|
70
71
|
msg.created_at # => 2026-06-06 12:00:00 ... (defaults to Time.now)
|
|
71
72
|
msg.metadata # => {}
|
|
73
|
+
msg.tags # => {}
|
|
72
74
|
msg.causation_id # => same as msg.id by default
|
|
73
75
|
msg.correlation_id # => same as msg.id by default
|
|
74
76
|
```
|
|
@@ -165,7 +167,7 @@ codec = Sourced::Message::JSONCodec.default.compile!
|
|
|
165
167
|
|
|
166
168
|
encoded = codec.encode(msg)
|
|
167
169
|
# => { id: "8f1c…", causation_id: "8f1c…", correlation_id: "8f1c…",
|
|
168
|
-
# created_at: "2026-09-01T10:00:00.000000+01:00", metadata: {}, type: "course.created",
|
|
170
|
+
# created_at: "2026-09-01T10:00:00.000000+01:00", metadata: {}, tags: {}, type: "course.created",
|
|
169
171
|
# payload: { course_name: "Ruby 101", starts_on: "2026-09-01", level: "beginner" } }
|
|
170
172
|
|
|
171
173
|
decoded = codec.decode(JSON.parse(JSON.dump(encoded), symbolize_names: true))
|
|
@@ -321,6 +323,11 @@ Messages are immutable. Use the `#with_*` helpers to derive new copies:
|
|
|
321
323
|
tagged = msg.with_metadata(channel: 'web', user_id: '42')
|
|
322
324
|
tagged.metadata # => { channel: 'web', user_id: '42' }
|
|
323
325
|
|
|
326
|
+
# Merge tags: String values, outside the payload (keeps the same id).
|
|
327
|
+
# A tag naming a payload attribute must match its value, or it raises TagConflictError.
|
|
328
|
+
scoped = msg.with_tags(school_id: 's1')
|
|
329
|
+
scoped.tags # => { school_id: 's1' }
|
|
330
|
+
|
|
324
331
|
# Override payload attributes
|
|
325
332
|
updated = msg.with_payload(seats: 25)
|
|
326
333
|
updated.payload.seats # => 25
|
|
@@ -329,7 +336,7 @@ updated.payload.course_name # => "Ruby 101" (unchanged)
|
|
|
329
336
|
|
|
330
337
|
### Correlation: tracing causal chains
|
|
331
338
|
|
|
332
|
-
`#correlate` links one message as the cause of another. It returns a copy of the target with `causation_id` set to the source's `id` and `correlation_id` propagated from the source. Metadata from both messages is merged.
|
|
339
|
+
`#correlate` links one message as the cause of another. It returns a copy of the target with `causation_id` set to the source's `id` and `correlation_id` propagated from the source. Metadata from both messages is merged. Tags are not: the copy keeps the target's own.
|
|
333
340
|
|
|
334
341
|
```ruby
|
|
335
342
|
trigger = EnrollStudent.new(payload: { student_id: '42' })
|
|
@@ -182,23 +182,27 @@ module Sourced
|
|
|
182
182
|
# Rebuild a message from decoded attributes. An unknown type raises: a process
|
|
183
183
|
# reading types it doesn't know about is missing the class.
|
|
184
184
|
#
|
|
185
|
-
# @param attrs [Hash]
|
|
185
|
+
# @param attrs [Hash] message attributes, String- (as #encode emits them) or Symbol-keyed
|
|
186
186
|
# @return [Sourced::Message]
|
|
187
187
|
# @raise [UnknownMessageError] if the type isn't in the registry
|
|
188
188
|
# @raise [UnregisteredTypeError] if the type was not compiled
|
|
189
189
|
# @raise [DecodeError] if the attributes don't satisfy the schema
|
|
190
190
|
def decode(attrs)
|
|
191
|
-
type = attrs
|
|
191
|
+
type = envelope(attrs, :type)
|
|
192
|
+
id = envelope(attrs, :id)
|
|
192
193
|
klass = @registry[type]
|
|
193
|
-
raise UnknownMessageError, "Unknown message type: #{label(type,
|
|
194
|
+
raise UnknownMessageError, "Unknown message type: #{label(type, id)}" unless klass
|
|
194
195
|
|
|
195
|
-
build(klass, attrs, pair(type,
|
|
196
|
+
build(klass, attrs, pair(type, id).decoder)
|
|
196
197
|
rescue Plumb::ParseError => e
|
|
197
|
-
raise DecodeError, "cannot decode #{label(type,
|
|
198
|
+
raise DecodeError, "cannot decode #{label(type, id)}: #{e.message}"
|
|
198
199
|
end
|
|
199
200
|
|
|
200
201
|
private
|
|
201
202
|
|
|
203
|
+
# Read before the decoder runs, so it must accept both key forms the decoder does.
|
|
204
|
+
def envelope(attrs, key) = attrs.fetch(key) { attrs[key.to_s] }
|
|
205
|
+
|
|
202
206
|
# --- subclass seams -------------------------------------------------------
|
|
203
207
|
|
|
204
208
|
# @param klass [Class<Sourced::Message>]
|
data/lib/sourced/message.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Sourced
|
|
|
22
22
|
# +Sourced::Message.registry[type]+ resolves a type registered under any
|
|
23
23
|
# subclass. Resolve from this root to see the whole tree.
|
|
24
24
|
class Message < Plumb::Types::Data
|
|
25
|
-
VERSION = '0.
|
|
25
|
+
VERSION = '0.5.0'
|
|
26
26
|
|
|
27
27
|
EMPTY_ARRAY = [].freeze
|
|
28
28
|
|
|
@@ -34,10 +34,24 @@ module Sourced
|
|
|
34
34
|
|
|
35
35
|
# Accepts a UUID string or generates a new one when none is provided.
|
|
36
36
|
AutoUUID = UUID::V4.default { SecureRandom.uuid }
|
|
37
|
+
|
|
38
|
+
# A JSON value, with Symbol-keyed objects at every depth. Typed all the way
|
|
39
|
+
# down so a codec decodes nested keys too. The self-references are deferred
|
|
40
|
+
# inside the union, not the constant itself: naming a type here freezes it,
|
|
41
|
+
# and a frozen Deferred can't materialize.
|
|
42
|
+
JSONValue = String | Numeric | Boolean | Nil |
|
|
43
|
+
Array[Any.defer { JSONValue }] | Hash[Symbol, Any.defer { JSONValue }]
|
|
44
|
+
|
|
45
|
+
Metadata = Hash[Symbol, JSONValue]
|
|
46
|
+
|
|
47
|
+
# Key/value pairs a store can index alongside the payload's. Values are Strings.
|
|
48
|
+
Tags = Hash[Symbol, String]
|
|
37
49
|
end
|
|
38
50
|
|
|
39
51
|
# Raised by {.from} when a type string isn't registered.
|
|
40
52
|
UnknownMessageError = Class.new(ArgumentError)
|
|
53
|
+
# Raised by {#with_tags} when a tag names a payload attribute with a different value.
|
|
54
|
+
TagConflictError = Class.new(ArgumentError)
|
|
41
55
|
# Raised by {#at} when a message would be scheduled in the past.
|
|
42
56
|
PastMessageDateError = Class.new(ArgumentError)
|
|
43
57
|
LaxTime = Types::Time | Types::String.build(::Time, :parse).policy(:rescue, ::ArgumentError)
|
|
@@ -47,7 +61,10 @@ module Sourced
|
|
|
47
61
|
attribute? :causation_id, Types::UUID::V4
|
|
48
62
|
attribute? :correlation_id, Types::UUID::V4
|
|
49
63
|
attribute :created_at, LaxTime.default { Time.now }
|
|
50
|
-
attribute :metadata, Types::
|
|
64
|
+
attribute :metadata, Types::Metadata.default(Plumb::BLANK_HASH)
|
|
65
|
+
# Envelope, not payload: {#correlate} leaves the target's tags alone, so they
|
|
66
|
+
# never pass on to messages produced in reaction.
|
|
67
|
+
attribute :tags, Types::Tags.default(Plumb::BLANK_HASH)
|
|
51
68
|
attribute :payload, Types::Static[nil]
|
|
52
69
|
|
|
53
70
|
# Lookup table mapping type strings to message subclasses.
|
|
@@ -191,6 +208,27 @@ module Sourced
|
|
|
191
208
|
with(metadata: metadata.merge(meta))
|
|
192
209
|
end
|
|
193
210
|
|
|
211
|
+
# A copy with +tags+ merged in, values stringified.
|
|
212
|
+
#
|
|
213
|
+
# @param tags [Hash{Symbol => #to_s}]
|
|
214
|
+
# @return [Message]
|
|
215
|
+
# @raise [TagConflictError] if a tag names a payload attribute with a different value
|
|
216
|
+
def with_tags(**tags)
|
|
217
|
+
return self if tags.empty?
|
|
218
|
+
|
|
219
|
+
tags = tags.transform_values(&:to_s)
|
|
220
|
+
declared = self.class.payload_attribute_names
|
|
221
|
+
tags.each do |key, value|
|
|
222
|
+
next unless declared.include?(key)
|
|
223
|
+
next if payload[key].to_s == value
|
|
224
|
+
|
|
225
|
+
raise TagConflictError,
|
|
226
|
+
"#{type} tag #{key}: #{value.inspect} conflicts with its payload value #{payload[key].inspect}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
with(tags: self.tags.merge(tags))
|
|
230
|
+
end
|
|
231
|
+
|
|
194
232
|
def with_payload(attrs = {})
|
|
195
233
|
hash = to_h
|
|
196
234
|
(hash[:payload] ||= {}).merge!(attrs)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sourced-message
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ismael Celis
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0.
|
|
18
|
+
version: '0.4'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0.
|
|
25
|
+
version: '0.4'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: fugit
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -74,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
75
|
version: '0'
|
|
76
76
|
requirements: []
|
|
77
|
-
rubygems_version: 4.0.
|
|
77
|
+
rubygems_version: 4.0.21
|
|
78
78
|
specification_version: 4
|
|
79
79
|
summary: Canonical typed Message class and registry shared by Sourced and Sidereal.
|
|
80
80
|
test_files: []
|