sourced-message 0.4.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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +10 -3
  4. data/lib/sourced/message.rb +30 -1
  5. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12ddf9298bf827a4f20e9f2db6b9edababd24efa73323326e5cd5ff6bbedebc1
4
- data.tar.gz: a88ab37ddb627868b20cd9a03615334a01d1f957fc3dedae25db4486623b983f
3
+ metadata.gz: b8202b720a3a57bf15a429a136797186c2bc8bd2721a7539e2f08495e7078923
4
+ data.tar.gz: 6532b0db8f33fb6aa9ba4d585d5b0424fd089be4917e994f4a1abd341db132b4
5
5
  SHA512:
6
- metadata.gz: 154e306175767857b9471c5700fe12ec3f73c17292af5c30826e2f61afa0dc217853e56950cddbf3decf8b38c38f5837e3cad76a822c6078cfd8904ef912093b
7
- data.tar.gz: 0640b5e2560fa1dfd331ee8b015dde0b5195bf1ec55efaa1cb0e98cf4b3713ca8544b4bc784495cb09f08d32d12bc2dc4c60977b60df99b021bf6f71a6e63c76
6
+ metadata.gz: ba7bafee643414f03b642074c521257fb731f663dc653d6ddedb3c6ca2d26304e727d7c719fac830d196aee3e1086aed22d126759c4671a3242fcc6c89fff5a8
7
+ data.tar.gz: 5f979cf196b3d6e0bc3b37ddc5b85f0a9284b1d8fad4a15a877ca835b18a747e49d9c1e5a5a0ad0b4acaaffe8b7bbb02411ee34f888445a30f6f3835c6eb267b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## [0.4.0]
4
16
 
5
17
  - Requires plumb 0.4, whose codecs rewrite Hash keys like values: `#encode` now emits
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' })
@@ -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.4.0'
25
+ VERSION = '0.5.0'
26
26
 
27
27
  EMPTY_ARRAY = [].freeze
28
28
 
@@ -43,10 +43,15 @@ module Sourced
43
43
  Array[Any.defer { JSONValue }] | Hash[Symbol, Any.defer { JSONValue }]
44
44
 
45
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]
46
49
  end
47
50
 
48
51
  # Raised by {.from} when a type string isn't registered.
49
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)
50
55
  # Raised by {#at} when a message would be scheduled in the past.
51
56
  PastMessageDateError = Class.new(ArgumentError)
52
57
  LaxTime = Types::Time | Types::String.build(::Time, :parse).policy(:rescue, ::ArgumentError)
@@ -57,6 +62,9 @@ module Sourced
57
62
  attribute? :correlation_id, Types::UUID::V4
58
63
  attribute :created_at, LaxTime.default { Time.now }
59
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)
60
68
  attribute :payload, Types::Static[nil]
61
69
 
62
70
  # Lookup table mapping type strings to message subclasses.
@@ -200,6 +208,27 @@ module Sourced
200
208
  with(metadata: metadata.merge(meta))
201
209
  end
202
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
+
203
232
  def with_payload(attrs = {})
204
233
  hash = to_h
205
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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis