sourced-message 0.5.0 → 0.5.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/lib/sourced/message.rb +45 -1
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8202b720a3a57bf15a429a136797186c2bc8bd2721a7539e2f08495e7078923
4
- data.tar.gz: 6532b0db8f33fb6aa9ba4d585d5b0424fd089be4917e994f4a1abd341db132b4
3
+ metadata.gz: 5f851a99f35af7389e8aeaa9a62e174c9c76fe83c7123ccf245160672250534b
4
+ data.tar.gz: af633b34af3853e032e2b4735394dec70b03bcce4e19b1dacd54f80cf76382f3
5
5
  SHA512:
6
- metadata.gz: ba7bafee643414f03b642074c521257fb731f663dc653d6ddedb3c6ca2d26304e727d7c719fac830d196aee3e1086aed22d126759c4671a3242fcc6c89fff5a8
7
- data.tar.gz: 5f979cf196b3d6e0bc3b37ddc5b85f0a9284b1d8fad4a15a877ca835b18a747e49d9c1e5a5a0ad0b4acaaffe8b7bbb02411ee34f888445a30f6f3835c6eb267b
6
+ metadata.gz: 46637a045f56cd99c1ead9dba880d6d846a58106d7d8200c35606fc21d484f92514f310cad4fc1f3ea20298c9e356a875069a4c076c9a40c73a7cc7ebdc1be85
7
+ data.tar.gz: 83972e23b181caa04596cb6235a5fe0aec375bf6795967a12157c1b104c1b5e380c37a246afff29ce57c3fa909847ade958567b0b90ff1af2831c7c922353846
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ - `Message::Registry` raises `DuplicateTypeError` when two classes with different names
4
+ claim one type string, instead of silently replacing the registered class. Registering
5
+ a type again is still allowed when the names are equal (a code reloader redefining a
6
+ constant) or either class is anonymous (throwaway classes in specs; a constant in an
7
+ anonymous module counts as anonymous until the module is named). `.define`
8
+ registers a class before it's assigned to its constant, so the comparison waits until
9
+ the new class has a name: the next registration of the type, its lookup (`[]`,
10
+ `Message.from`, codec `decode`) or `#all` (codec `compile!`) raises.
11
+
3
12
  ## [0.5.0]
4
13
 
5
14
  - `Message#tags`: an envelope attribute (`Types::Tags`, Symbol keys and String values,
@@ -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.1'
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
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis