sourced-message 0.5.0 → 0.5.2
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 +13 -0
- data/README.md +2 -1
- data/lib/sourced/message.rb +64 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80569ac57d96516a6efd3bd8c09ef862a7c48d9e661fb82404476608995c6350
|
|
4
|
+
data.tar.gz: a480b3c5007cab3ad87f242a5828a548e294150ef2ad65b5afdc2dc98d53423c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bae04d8962a7e19f8f21985b674b29e0ddb90987934a51504e8eeba4e357b7925b983c4e9cee10b719b0598ecae963af0dccbd79efb0679a44ace463184e6c37
|
|
7
|
+
data.tar.gz: df823ccc2eab96eaddba05255f0776b069dfa57004bb58f2337f9de8aeedd29fe3d30ed04ed7e72ac0211c0e6195110cb19bbd735014fefd76013aa975349f24
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- `Message#at` with a duration string (`'1h30m'`) raised `ArgumentError` ("Cannot
|
|
4
|
+
determine timezone from \"BST\"") in the hour the clocks go back. The duration's
|
|
5
|
+
seconds are now added to `Time.now`, exactly. Durations in months or years, which
|
|
6
|
+
aren't a fixed number of seconds, raise `ArgumentError`.
|
|
7
|
+
- `Message::Registry` raises `DuplicateTypeError` when two classes with different names
|
|
8
|
+
claim one type string, instead of silently replacing the registered class. Registering
|
|
9
|
+
a type again is still allowed when the names are equal (a code reloader redefining a
|
|
10
|
+
constant) or either class is anonymous (throwaway classes in specs; a constant in an
|
|
11
|
+
anonymous module counts as anonymous until the module is named). `.define`
|
|
12
|
+
registers a class before it's assigned to its constant, so the comparison waits until
|
|
13
|
+
the new class has a name: the next registration of the type, its lookup (`[]`,
|
|
14
|
+
`Message.from`, codec `decode`) or `#all` (codec `compile!`) raises.
|
|
15
|
+
|
|
3
16
|
## [0.5.0]
|
|
4
17
|
|
|
5
18
|
- `Message#tags`: an envelope attribute (`Types::Tags`, Symbol keys and String values,
|
data/README.md
CHANGED
|
@@ -372,10 +372,11 @@ Scheduling a message into the past raises `Sourced::Message::PastMessageDateErro
|
|
|
372
372
|
msg.at(Time.now - 60) # => raises Sourced::Message::PastMessageDateError
|
|
373
373
|
```
|
|
374
374
|
|
|
375
|
-
Passing a string that isn't a duration (e.g. an absolute date) raises `ArgumentError
|
|
375
|
+
Passing a string that isn't a duration (e.g. an absolute date) raises `ArgumentError`, as does a duration in months or years, which vary in length (give days instead):
|
|
376
376
|
|
|
377
377
|
```ruby
|
|
378
378
|
msg.in('2026-12-31T10:00:00') # => raises ArgumentError
|
|
379
|
+
msg.in('1M') # => raises ArgumentError; msg.in('30d') instead
|
|
379
380
|
```
|
|
380
381
|
|
|
381
382
|
### Pattern matching with `case`/`when`
|
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.5.
|
|
25
|
+
VERSION = '0.5.2'
|
|
26
26
|
|
|
27
27
|
EMPTY_ARRAY = [].freeze
|
|
28
28
|
|
|
@@ -54,6 +54,8 @@ module Sourced
|
|
|
54
54
|
TagConflictError = Class.new(ArgumentError)
|
|
55
55
|
# Raised by {#at} when a message would be scheduled in the past.
|
|
56
56
|
PastMessageDateError = Class.new(ArgumentError)
|
|
57
|
+
# Raised by {Registry} when two classes with different names claim one type string.
|
|
58
|
+
DuplicateTypeError = Class.new(ArgumentError)
|
|
57
59
|
LaxTime = Types::Time | Types::String.build(::Time, :parse).policy(:rescue, ::ArgumentError)
|
|
58
60
|
|
|
59
61
|
attribute :id, Types::AutoUUID
|
|
@@ -68,11 +70,19 @@ module Sourced
|
|
|
68
70
|
attribute :payload, Types::Static[nil]
|
|
69
71
|
|
|
70
72
|
# Lookup table mapping type strings to message subclasses.
|
|
73
|
+
#
|
|
74
|
+
# Registering a type again replaces its class: a code reloader redefines a
|
|
75
|
+
# constant, and specs define throwaway anonymous messages reusing type
|
|
76
|
+
# strings. Two classes with different permanent names claiming one type are
|
|
77
|
+
# a bug, and raise {DuplicateTypeError}. {.define} registers a class before
|
|
78
|
+
# it's assigned to its constant, so the replaced class is kept until its
|
|
79
|
+
# successor has a name, and compared on the next registration or lookup.
|
|
71
80
|
class Registry
|
|
72
81
|
# @param message_class [Class] the root message class for this registry
|
|
73
82
|
def initialize(message_class)
|
|
74
83
|
@message_class = message_class
|
|
75
84
|
@lookup = {}
|
|
85
|
+
@replaced = {}
|
|
76
86
|
end
|
|
77
87
|
|
|
78
88
|
# @return [Array<String>] registered type strings
|
|
@@ -85,7 +95,14 @@ module Sourced
|
|
|
85
95
|
#
|
|
86
96
|
# @param key [String] message type string
|
|
87
97
|
# @param klass [Class] message subclass
|
|
98
|
+
# @raise [DuplicateTypeError] if a class with a different name holds the type
|
|
88
99
|
def []=(key, klass)
|
|
100
|
+
check!(key)
|
|
101
|
+
previous = @lookup[key]
|
|
102
|
+
if previous && named(previous) && !previous.equal?(klass)
|
|
103
|
+
duplicate!(key, previous, klass) if named(klass) && named(klass) != named(previous)
|
|
104
|
+
@replaced[key] = previous unless named(klass)
|
|
105
|
+
end
|
|
89
106
|
@lookup[key] = klass
|
|
90
107
|
end
|
|
91
108
|
|
|
@@ -94,7 +111,9 @@ module Sourced
|
|
|
94
111
|
#
|
|
95
112
|
# @param key [String] message type string
|
|
96
113
|
# @return [Class, nil]
|
|
114
|
+
# @raise [DuplicateTypeError] if a class with a different name holds the type
|
|
97
115
|
def [](key)
|
|
116
|
+
check!(key) unless @replaced.empty?
|
|
98
117
|
klass = lookup[key]
|
|
99
118
|
return klass if klass
|
|
100
119
|
|
|
@@ -109,9 +128,11 @@ module Sourced
|
|
|
109
128
|
#
|
|
110
129
|
# @return [Enumerator<Class>] if no block given
|
|
111
130
|
# @yield [Class] each registered message class
|
|
131
|
+
# @raise [DuplicateTypeError] if a class with a different name holds a type
|
|
112
132
|
def all(&block)
|
|
113
133
|
return enum_for(:all) unless block
|
|
114
134
|
|
|
135
|
+
@replaced.keys.each { |key| check!(key) }
|
|
115
136
|
lookup.each_value(&block)
|
|
116
137
|
subclasses.each { |c| c.registry.all(&block) }
|
|
117
138
|
end
|
|
@@ -119,6 +140,29 @@ module Sourced
|
|
|
119
140
|
private
|
|
120
141
|
|
|
121
142
|
attr_reader :lookup, :message_class
|
|
143
|
+
|
|
144
|
+
# Compare the class a type replaced with the one holding it, once the latter has a name.
|
|
145
|
+
def check!(key)
|
|
146
|
+
replaced = @replaced[key]
|
|
147
|
+
return unless replaced
|
|
148
|
+
|
|
149
|
+
current = @lookup[key]
|
|
150
|
+
return unless named(current)
|
|
151
|
+
|
|
152
|
+
@replaced.delete(key)
|
|
153
|
+
duplicate!(key, replaced, current) unless named(current) == named(replaced)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A class's permanent name. A constant set in an anonymous module has a
|
|
157
|
+
# temporary one (+"#<Class:0x...>::Foo"+) until the module is named.
|
|
158
|
+
def named(klass)
|
|
159
|
+
name = klass.name
|
|
160
|
+
name unless name.nil? || name.start_with?('#<')
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def duplicate!(key, registered, klass)
|
|
164
|
+
raise DuplicateTypeError, "message type #{key.inspect} is claimed by both #{registered.name} and #{klass.name}"
|
|
165
|
+
end
|
|
122
166
|
end
|
|
123
167
|
|
|
124
168
|
# @return [Registry] the message type registry for this class
|
|
@@ -242,7 +286,9 @@ module Sourced
|
|
|
242
286
|
# target.
|
|
243
287
|
# - +Integer+ — interpreted as seconds; added to +Time.now+.
|
|
244
288
|
# - +String+ — parsed via +Fugit.parse_duration+ as a duration (e.g.
|
|
245
|
-
# +'5m'+, +'1h30m'+, +'PT5M'+) and added to
|
|
289
|
+
# +'5m'+, +'1h30m'+, +'PT5M'+), up to weeks, and its seconds added to
|
|
290
|
+
# +Time.now+. Months and years aren't a fixed number of seconds, so
|
|
291
|
+
# they raise +ArgumentError+.
|
|
246
292
|
#
|
|
247
293
|
# Raises {PastMessageDateError} when the resolved target is
|
|
248
294
|
# before +created_at+.
|
|
@@ -251,11 +297,7 @@ module Sourced
|
|
|
251
297
|
when Integer
|
|
252
298
|
Time.now + value
|
|
253
299
|
when String
|
|
254
|
-
|
|
255
|
-
raise ArgumentError,
|
|
256
|
-
"Message#at: String argument must be an ISO8601 / Fugit duration " \
|
|
257
|
-
"(e.g. '5m', 'PT1H30M'); got #{value.inspect}"
|
|
258
|
-
parsed.add_to_time(Time.now).to_local_time
|
|
300
|
+
Time.now + duration_seconds(value)
|
|
259
301
|
else
|
|
260
302
|
value
|
|
261
303
|
end
|
|
@@ -331,6 +373,21 @@ module Sourced
|
|
|
331
373
|
attrs[:causation_id] = attrs[:id] unless attrs[:causation_id]
|
|
332
374
|
attrs
|
|
333
375
|
end
|
|
376
|
+
|
|
377
|
+
# A duration string's seconds, up to weeks.
|
|
378
|
+
def duration_seconds(value)
|
|
379
|
+
duration = Fugit.parse_duration(value) or
|
|
380
|
+
raise ArgumentError,
|
|
381
|
+
"Message#at: String argument must be an ISO8601 / Fugit duration " \
|
|
382
|
+
"(e.g. '5m', 'PT1H30M'); got #{value.inspect}"
|
|
383
|
+
if duration.h.key?(:yea) || duration.h.key?(:mon)
|
|
384
|
+
raise ArgumentError,
|
|
385
|
+
"Message#at: can't delay by months or years, which vary in length; " \
|
|
386
|
+
"give days (e.g. '30d'); got #{value.inspect}"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
duration.to_sec
|
|
390
|
+
end
|
|
334
391
|
end
|
|
335
392
|
|
|
336
393
|
class Command < Message; end
|