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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8202b720a3a57bf15a429a136797186c2bc8bd2721a7539e2f08495e7078923
4
- data.tar.gz: 6532b0db8f33fb6aa9ba4d585d5b0424fd089be4917e994f4a1abd341db132b4
3
+ metadata.gz: 80569ac57d96516a6efd3bd8c09ef862a7c48d9e661fb82404476608995c6350
4
+ data.tar.gz: a480b3c5007cab3ad87f242a5828a548e294150ef2ad65b5afdc2dc98d53423c
5
5
  SHA512:
6
- metadata.gz: ba7bafee643414f03b642074c521257fb731f663dc653d6ddedb3c6ca2d26304e727d7c719fac830d196aee3e1086aed22d126759c4671a3242fcc6c89fff5a8
7
- data.tar.gz: 5f979cf196b3d6e0bc3b37ddc5b85f0a9284b1d8fad4a15a877ca835b18a747e49d9c1e5a5a0ad0b4acaaffe8b7bbb02411ee34f888445a30f6f3835c6eb267b
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`
@@ -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.0'
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 +Time.now+.
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
- parsed = Fugit.parse_duration(value) or
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
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.5.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis