rsmp 0.48.4 → 0.51.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/Gemfile.lock +19 -20
  4. data/README.md +27 -3
  5. data/Rakefile +3 -1
  6. data/documentation/cli.md +79 -4
  7. data/documentation/collecting_message.md +90 -170
  8. data/documentation/message_distribution.md +17 -3
  9. data/documentation/tasks.md +65 -117
  10. data/lib/rsmp/cli.rb +93 -5
  11. data/lib/rsmp/collect/collector.rb +117 -134
  12. data/lib/rsmp/collect/distributor.rb +16 -3
  13. data/lib/rsmp/collect/queue.rb +14 -6
  14. data/lib/rsmp/collect/receiver.rb +1 -1
  15. data/lib/rsmp/collect/state_collector.rb +11 -2
  16. data/lib/rsmp/collection.rb +12 -0
  17. data/lib/rsmp/completion.rb +49 -0
  18. data/lib/rsmp/component/component_base.rb +3 -3
  19. data/lib/rsmp/convert/export/json_schema/index.rb +8 -3
  20. data/lib/rsmp/convert/import/yaml.rb +12 -8
  21. data/lib/rsmp/delivery.rb +18 -0
  22. data/lib/rsmp/event.rb +19 -0
  23. data/lib/rsmp/event_source.rb +36 -0
  24. data/lib/rsmp/exchange.rb +10 -0
  25. data/lib/rsmp/failure.rb +23 -0
  26. data/lib/rsmp/helpers/error.rb +14 -23
  27. data/lib/rsmp/message.rb +9 -7
  28. data/lib/rsmp/node/node.rb +3 -21
  29. data/lib/rsmp/node/site/connections.rb +4 -9
  30. data/lib/rsmp/node/site/site.rb +31 -16
  31. data/lib/rsmp/node/supervisor/modules/connection.rb +19 -5
  32. data/lib/rsmp/node/supervisor/modules/sites.rb +16 -5
  33. data/lib/rsmp/node/supervisor/supervisor.rb +13 -14
  34. data/lib/rsmp/node/task.rb +59 -53
  35. data/lib/rsmp/proxy/modules/acknowledgements.rb +22 -9
  36. data/lib/rsmp/proxy/modules/lifecycle.rb +134 -0
  37. data/lib/rsmp/proxy/modules/receive.rb +109 -66
  38. data/lib/rsmp/proxy/modules/send.rb +109 -51
  39. data/lib/rsmp/proxy/modules/state.rb +15 -5
  40. data/lib/rsmp/proxy/modules/tasks.rb +96 -70
  41. data/lib/rsmp/proxy/modules/versions.rb +8 -8
  42. data/lib/rsmp/proxy/modules/watchdogs.rb +15 -2
  43. data/lib/rsmp/proxy/proxy.rb +4 -66
  44. data/lib/rsmp/proxy/site/connection.rb +27 -22
  45. data/lib/rsmp/proxy/site/modules/aggregated_status.rb +18 -8
  46. data/lib/rsmp/proxy/site/modules/alarms.rb +45 -38
  47. data/lib/rsmp/proxy/site/modules/commands.rb +18 -8
  48. data/lib/rsmp/proxy/site/modules/status.rb +44 -19
  49. data/lib/rsmp/proxy/site/site_proxy.rb +12 -10
  50. data/lib/rsmp/proxy/supervisor/modules/aggregated_status.rb +1 -1
  51. data/lib/rsmp/proxy/supervisor/modules/alarms.rb +1 -1
  52. data/lib/rsmp/proxy/supervisor/modules/commands.rb +1 -1
  53. data/lib/rsmp/proxy/supervisor/modules/message_buffer.rb +8 -9
  54. data/lib/rsmp/proxy/supervisor/modules/status.rb +1 -1
  55. data/lib/rsmp/proxy/supervisor/modules/status_updates.rb +1 -1
  56. data/lib/rsmp/proxy/supervisor/supervisor_proxy.rb +34 -30
  57. data/lib/rsmp/result.rb +95 -0
  58. data/lib/rsmp/schema/validation.rb +5 -3
  59. data/lib/rsmp/sxl/interface.rb +8 -1
  60. data/lib/rsmp/sxl/processing/catalogue.rb +61 -0
  61. data/lib/rsmp/sxl/processing/conflict_checker.rb +30 -0
  62. data/lib/rsmp/sxl/processing/dependency_graph.rb +37 -0
  63. data/lib/rsmp/sxl/processing/document.rb +158 -0
  64. data/lib/rsmp/sxl/processing/error.rb +8 -0
  65. data/lib/rsmp/sxl/processing/manifest.rb +144 -0
  66. data/lib/rsmp/sxl/processing/natural_sort.rb +22 -0
  67. data/lib/rsmp/sxl/processing/resolver.rb +94 -0
  68. data/lib/rsmp/sxl/processing/version_requirement.rb +86 -0
  69. data/lib/rsmp/sxl/processing.rb +18 -0
  70. data/lib/rsmp/sxl/supervisor_interface.rb +8 -1
  71. data/lib/rsmp/termination.rb +10 -0
  72. data/lib/rsmp/tlc/modules/plans.rb +0 -2
  73. data/lib/rsmp/tlc/proxy/control.rb +50 -16
  74. data/lib/rsmp/tlc/proxy/detectors.rb +15 -4
  75. data/lib/rsmp/tlc/proxy/io.rb +46 -31
  76. data/lib/rsmp/tlc/proxy/plans.rb +36 -19
  77. data/lib/rsmp/tlc/proxy/plans_bang.rb +44 -0
  78. data/lib/rsmp/tlc/proxy/status.rb +47 -23
  79. data/lib/rsmp/tlc/proxy/system.rb +16 -4
  80. data/lib/rsmp/tlc/supervisor_interface.rb +7 -1
  81. data/lib/rsmp/tlc/traffic_controller_site.rb +6 -9
  82. data/lib/rsmp/validation.rb +22 -0
  83. data/lib/rsmp/version.rb +1 -1
  84. data/lib/rsmp.rb +19 -4
  85. data/rsmp.gemspec +1 -1
  86. metadata +25 -4
  87. data/lib/rsmp/collect/collector/status.rb +0 -34
@@ -1,37 +1,25 @@
1
1
  module RSMP
2
- # Collects messages from a distributor.
3
- # Can filter by message type, componet and direction.
4
- # Wakes up the once the desired number of messages has been collected.
2
+ # Collects matching messages and resolves once with an immutable Result.
5
3
  class Collector
6
4
  include Receiver
7
- include Status
8
5
  include Reporting
9
6
  include Logging
10
7
 
11
- attr_reader :condition, :messages, :status, :error, :task, :m_id, :initiator
8
+ attr_reader :messages, :m_id, :initiator
12
9
 
13
10
  def initialize(distributor, options = {})
14
11
  initialize_receiver distributor, filter: options[:filter]
15
12
  @options = {
16
13
  cancel: {
17
- schema_error: true,
18
- disconnect: false
14
+ invalid_message: true,
15
+ disconnect: true
19
16
  }
20
- }.deep_merge options
17
+ }.deep_merge(options)
21
18
  @timeout = options[:timeout]
22
19
  @num = options[:num]
23
20
  @initiator = options[:initiator]
24
21
  @m_id = options[:m_id] || @initiator&.attributes&.dig('mId')
25
- @condition = Async::Notification.new
26
- make_title options[:title]
27
-
28
- if task
29
- @task = task
30
- elsif distributor.respond_to? 'task'
31
- # if distributor is a Proxy, or some other object that implements task(),
32
- # then try to get the task that way
33
- @task = distributor.task
34
- end
22
+ make_title(options[:title])
35
23
  reset
36
24
  end
37
25
 
@@ -45,207 +33,202 @@ module RSMP
45
33
  end
46
34
  end
47
35
 
48
- def use_task(task)
49
- @task = task
50
- end
51
-
52
36
  def reset
53
37
  @messages = []
54
- @error = nil
55
- @status = :ready
38
+ @completion = Completion.new
39
+ @active = false
56
40
  end
57
41
 
58
- # Inspect formatter that shows the message we have collected
59
- def inspect
60
- "#<#{self.class.name}:#{object_id}, message:#{@messages}>"
42
+ def active?
43
+ @active
61
44
  end
62
45
 
63
- # if an errors caused collection to abort, then raise it
64
- # return self, so this can be tucked on to calls that return a collector
65
- def ok!
66
- raise @error if @error
46
+ alias collecting? active?
67
47
 
68
- self
48
+ def inspect
49
+ "#<#{self.class.name}:#{object_id}, message:#{@messages}>"
69
50
  end
70
51
 
71
- # Collect message
72
- # Will return once all messages have been collected, or timeout is reached
73
52
  def collect(&)
74
53
  start(&)
75
54
  wait
76
- @status
77
55
  ensure
78
- @distributor&.remove_receiver self
56
+ stop
79
57
  end
80
58
 
81
- # Collect message
82
- # Returns the collected messages, or raise an exception in case of a time out.
83
59
  def collect!(&)
84
- collect(&)
85
- ok!
86
- @messages
60
+ collect(&).value!.messages
87
61
  end
88
62
 
89
- # If collection is not active, return status immeditatly. Otherwise wait until
90
- # the desired messages have been collected, or timeout is reached.
63
+ # The configured timeout terminates this collection. Completion retains the
64
+ # result, so every current or future waiter observes the same value.
91
65
  def wait
92
- if collecting?
93
- if @timeout
94
- @task.with_timeout(@timeout) { @condition.wait }
95
- else
96
- @condition.wait
97
- end
66
+ raise 'collector has not been started' unless active? || @completion.resolved?
67
+
68
+ observed = @completion.wait(timeout: @timeout)
69
+ if timeout_result?(observed)
70
+ finish_failure(observed.failure.with(message: describe_progress))
71
+ observed = @completion.wait
98
72
  end
99
- @status
100
- rescue Async::TimeoutError
101
- @error = RSMP::TimeoutError.new describe_progress
102
- @status = :timeout
73
+ observed
103
74
  end
104
75
 
105
- # If collection is not active, raise an error. Otherwise wait until
106
- # the desired messages have been collected.
107
- # If timeout is reached or collection was cancelled with an error, an exception is raised.
108
76
  def wait!
109
- wait
110
- raise @error if timeout? || (cancelled? && @error)
111
-
112
- @messages
77
+ wait.value!.messages
113
78
  end
114
79
 
115
- # Start collection and return immediately
116
- # You can later use wait() to wait for completion
117
80
  def start(&block)
118
- raise "Can't start collectimng unless ready (currently #{@status})" unless ready?
81
+ raise 'collector is already active' if active?
119
82
 
120
83
  @block = block
121
- raise ArgumentError, 'Num, timeout or block must be provided' unless @num || @timeout || @block
84
+ raise ArgumentError, 'num, timeout or block must be provided' unless @num || @timeout || @block
122
85
 
123
86
  reset
124
- @status = :collecting
87
+ @active = true
125
88
  log_start
126
- @distributor&.add_receiver self
89
+ @distributor.add_receiver(self)
90
+ self
127
91
  end
128
92
 
129
- # Check if we receive a NotAck related to initiating request, identified by @m_id.
130
- def reject_not_ack(message)
131
- return unless @m_id
93
+ def stop
94
+ return self unless @active
132
95
 
133
- return unless message.is_a?(MessageNotAck)
134
- return unless message.attribute('oMId') == @m_id
96
+ @active = false
97
+ @distributor.remove_receiver(self)
98
+ self
99
+ end
135
100
 
136
- m_id_short = RSMP::Message.shorten_m_id @m_id, 8
137
- cancel RSMP::MessageRejected.new("#{@title} #{m_id_short} was rejected with '#{message.attribute('rea')}'")
138
- @distributor.log "#{identifier}: cancelled due to a NotAck", level: :debug
101
+ # Check for a NotAck related to the initiating request.
102
+ def reject_not_ack?(message)
103
+ return false unless @m_id
104
+ return false unless message.is_a?(MessageNotAck)
105
+ return false unless message.attribute('oMId') == @m_id
106
+
107
+ m_id_short = RSMP::Message.shorten_m_id(@m_id, 8)
108
+ finish_failure(
109
+ Failure.new(
110
+ code: :message_rejected,
111
+ message: "#{@title} #{m_id_short} was rejected with '#{message.attribute('rea')}'",
112
+ source: :peer,
113
+ context: { message: message, original_message_id: @m_id }
114
+ )
115
+ )
116
+ @distributor.log "#{identifier}: rejected by a NotAck", level: :debug
139
117
  true
140
118
  end
141
119
 
142
- # Handle message and return true if we're done collecting
143
120
  def receive(message)
144
- raise ArgumentError unless message
145
- unless ready? || collecting?
146
- raise "can't process message when status is :#{@status}, title: #{@title}, desc: #{describe}"
147
- end
121
+ return unless active?
148
122
 
149
- if perform_match message
150
- if done?
151
- complete
152
- else
153
- incomplete
154
- end
123
+ if perform_match?(message)
124
+ done? ? complete : incomplete
155
125
  end
156
- @status
126
+ active?
157
127
  end
158
128
 
159
129
  def describe; end
160
130
 
161
- # Match message against our collection criteria
162
- def perform_match(message)
163
- return false if reject_not_ack(message)
131
+ def perform_match?(message)
132
+ return false if reject_not_ack?(message)
164
133
  return false unless acceptable?(message)
165
134
 
166
135
  if @block
167
- status = [@block.call(message)].flatten
168
- return unless collecting?
136
+ status = Array(@block.call(message))
137
+ return false unless active?
169
138
 
170
- keep message if status.include?(:keep)
139
+ keep(message) if status.include?(:keep)
171
140
  else
172
- keep message
141
+ keep(message)
173
142
  end
143
+ true
174
144
  end
175
145
 
176
- # Have we collected the required number of messages?
177
146
  def done?
178
147
  @num && @messages.size >= @num
179
148
  end
180
149
 
181
- # Called when we're done collecting. Remove ourself as a receiver,
182
- # se we don't receive message notifications anymore
183
150
  def complete
184
- @status = :ok
185
- do_stop
151
+ finish_success(build_collection)
186
152
  log_complete
187
153
  end
188
154
 
189
- # called when we received a message, but are not done yet
190
155
  def incomplete
191
156
  log_incomplete
192
157
  end
193
158
 
194
- # Remove ourself as a receiver, so we don't receive message notifications anymore,
195
- # and wake up the async condition
196
- def do_stop
197
- @distributor.remove_receiver self
198
- @condition.signal
199
- end
159
+ def receive_event(event)
160
+ return unless active?
200
161
 
201
- # Handle upstream error
202
- def receive_error(error, options = {})
203
- case error
204
- when RSMP::SchemaError
205
- receive_schema_error error, options
206
- when RSMP::DisconnectError
207
- receive_disconnect error, options
162
+ case event.type
163
+ when :invalid_message
164
+ receive_invalid_message(event)
165
+ when :connection_ended
166
+ receive_connection_ended(event)
208
167
  end
209
168
  end
210
169
 
211
- # Cancel if we received e schema error for a message type we're collecting
212
- def receive_schema_error(error, options)
213
- return unless @options.dig(:cancel, :schema_error)
170
+ def receive_invalid_message(event)
171
+ return unless @options.dig(:cancel, :invalid_message)
172
+ return unless event.message
173
+ return unless acceptable?(event.message)
214
174
 
215
- message = options[:message]
216
- return unless message
175
+ finish_failure(event.failure)
176
+ end
217
177
 
218
- klass = message.class.name.split('::').last
219
- return unless @filter&.type.nil? || [@filter&.type].flatten.include?(klass)
178
+ def receive_connection_ended(event)
179
+ return unless @options.dig(:cancel, :disconnect)
220
180
 
221
- @distributor.log "#{identifier}: cancelled due to schema error in #{klass} #{message.m_id_short}", level: :debug
222
- cancel error
181
+ finish_failure(event.failure)
223
182
  end
224
183
 
225
- # Cancel if we received e notifiction about a disconnect
226
- def receive_disconnect(error, _options)
227
- return unless @options.dig(:cancel, :disconnect)
184
+ # Explicit cancellation is an expected caller-controlled result.
185
+ def cancel(reason = 'Collection cancelled')
186
+ finish_failure(
187
+ Failure.new(code: :cancelled, message: reason.to_s, source: :local)
188
+ )
189
+ end
190
+
191
+ def fail(failure)
192
+ raise ArgumentError, 'failure must be an RSMP::Failure' unless failure.is_a?(Failure)
228
193
 
229
- @distributor.log "#{identifier}: cancelled due to a connection error: #{error}", level: :debug
230
- cancel error
194
+ finish_failure(failure)
231
195
  end
232
196
 
233
- # Abort collection
234
- def cancel(error = nil)
235
- @error = error
236
- @status = :cancelled
237
- do_stop
197
+ # Unexpected receiver/callback failures reject the completion so waiters see
198
+ # the original exception and stack trace.
199
+ def crash(error)
200
+ return unless active?
201
+
202
+ @completion.crash(error)
203
+ stop
238
204
  end
239
205
 
240
- # Store a message in the result array
241
206
  def keep(message)
242
207
  @messages << message
243
208
  end
244
209
 
245
- # Check a message against our match criteria
246
- # Return true if there's a match, false if not
247
210
  def acceptable?(message)
248
211
  @filter.nil? || @filter.accept?(message)
249
212
  end
213
+
214
+ def build_collection
215
+ Collection.new(messages: @messages)
216
+ end
217
+
218
+ private
219
+
220
+ def timeout_result?(result)
221
+ result.failure? && result.failure.code == :timeout && !@completion.resolved?
222
+ end
223
+
224
+ def finish_success(collection)
225
+ @completion.succeed(collection)
226
+ stop
227
+ end
228
+
229
+ def finish_failure(failure)
230
+ @completion.fail(failure)
231
+ stop
232
+ end
250
233
  end
251
234
  end
@@ -56,11 +56,24 @@ module RSMP
56
56
  end
57
57
 
58
58
  def distribute_immediately(message)
59
- @receivers.each { |receiver| receiver.receive message }
59
+ @receivers.dup.each { |receiver| deliver(receiver, :receive, message) }
60
60
  end
61
61
 
62
- def distribute_error(error, options = {})
63
- @receivers.each { |receiver| receiver.receive_error error, options }
62
+ def distribute_event(event)
63
+ raise ArgumentError, 'event must be an RSMP::Event' unless event.is_a?(RSMP::Event)
64
+
65
+ @receivers.dup.each { |receiver| deliver(receiver, :receive_event, event) }
66
+ end
67
+
68
+ private
69
+
70
+ def deliver(receiver, method, value)
71
+ receiver.public_send(method, value)
72
+ rescue StandardError => e
73
+ remove_receiver(receiver)
74
+ raise unless receiver.respond_to?(:crash)
75
+
76
+ receiver.crash(e)
64
77
  end
65
78
  end
66
79
  end
@@ -7,10 +7,9 @@ module RSMP
7
7
 
8
8
  attr_reader :messages
9
9
 
10
- def initialize(distributor, task:, filter: nil)
10
+ def initialize(distributor, filter: nil)
11
11
  initialize_receiver distributor, filter: filter
12
12
  @condition = Async::Notification.new
13
- @task = task
14
13
  clear
15
14
  end
16
15
 
@@ -21,14 +20,23 @@ module RSMP
21
20
  def wait_for_message(timeout: nil)
22
21
  if @messages.empty?
23
22
  if timeout
24
- @task.with_timeout(timeout) { @condition.wait }
23
+ Async::Task.current.with_timeout(timeout) { @condition.wait }
25
24
  else
26
25
  @condition.wait
27
26
  end
28
27
  end
29
- @messages.shift
30
- rescue Async::TimeoutError
31
- raise RSMP::TimeoutError
28
+ Result.success(@messages.shift)
29
+ rescue Async::TimeoutError => e
30
+ Result.failure(
31
+ :timeout,
32
+ message: "No message was received within #{timeout}s",
33
+ source: :timeout,
34
+ cause: e
35
+ )
36
+ end
37
+
38
+ def wait_for_message!(...)
39
+ wait_for_message(...).value!
32
40
  end
33
41
 
34
42
  def handle_message(message)
@@ -23,7 +23,7 @@ module RSMP
23
23
  handle_message(message) if accept_message?(message)
24
24
  end
25
25
 
26
- def receive_error(error, options = {}); end
26
+ def receive_event(event); end
27
27
 
28
28
  def accept_message?(message)
29
29
  @filter.nil? || @filter.accept?(message)
@@ -106,9 +106,9 @@ module RSMP
106
106
  end
107
107
  end
108
108
 
109
- def perform_match(message)
109
+ def perform_match?(message)
110
110
  return false if super == false
111
- return unless collecting?
111
+ return false unless collecting?
112
112
 
113
113
  @matchers.each do |matcher|
114
114
  break unless collecting?
@@ -120,6 +120,7 @@ module RSMP
120
120
  handle_match_result(matched, matcher, message, item)
121
121
  end
122
122
  end
123
+ true
123
124
  end
124
125
 
125
126
  # don't collect anything. Matcher will collect them instead
@@ -248,5 +249,13 @@ module RSMP
248
249
  def log_complete
249
250
  @distributor.log "#{identifier}: Completed with #{matcher_got_hash}", level: :collect
250
251
  end
252
+
253
+ def build_collection
254
+ Collection.new(
255
+ messages: messages,
256
+ reached: reached,
257
+ matcher_status: matcher_status
258
+ )
259
+ end
251
260
  end
252
261
  end
@@ -0,0 +1,12 @@
1
+ module RSMP
2
+ # Immutable snapshot returned by a completed collector.
3
+ Collection = Data.define(:messages, :reached, :matcher_status) do
4
+ def initialize(messages:, reached: [], matcher_status: {})
5
+ super(
6
+ messages: messages.dup.freeze,
7
+ reached: reached.dup.freeze,
8
+ matcher_status: matcher_status.dup.freeze
9
+ )
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ module RSMP
2
+ # A resolve-once Async completion. Expected failures resolve to Result::Failure;
3
+ # unexpected exceptions reject the promise and retain their original backtrace.
4
+ class Completion
5
+ def initialize
6
+ @promise = Async::Promise.new
7
+ end
8
+
9
+ def resolved?
10
+ @promise.resolved?
11
+ end
12
+
13
+ def succeed(value = nil)
14
+ @promise.resolve(Result.success(value))
15
+ end
16
+
17
+ def fail(failure = nil, **)
18
+ result = if failure.is_a?(RSMP::Failure)
19
+ Result.failure(failure: failure)
20
+ else
21
+ Result.failure(failure, **)
22
+ end
23
+ @promise.resolve(result)
24
+ end
25
+
26
+ def crash(exception)
27
+ @promise.reject(exception)
28
+ end
29
+
30
+ # A wait timeout bounds this caller's wait; it does not resolve or cancel the
31
+ # underlying operation.
32
+ def wait(timeout: nil)
33
+ @promise.wait(timeout: timeout)
34
+ rescue Async::TimeoutError => e
35
+ return @promise.wait if @promise.resolved?
36
+
37
+ Result.failure(
38
+ :timeout,
39
+ message: "Operation did not complete within #{timeout}s",
40
+ source: :timeout,
41
+ cause: e
42
+ )
43
+ end
44
+
45
+ def wait!(timeout: nil)
46
+ wait(timeout: timeout).value!
47
+ end
48
+ end
49
+ end
@@ -70,7 +70,7 @@ module RSMP
70
70
 
71
71
  def set_aggregated_status(status, options = {})
72
72
  status = [status] if status.is_a? Symbol
73
- raise InvalidArgument unless status.is_a? Array
73
+ raise ArgumentError, 'aggregated status must be an Array' unless status.is_a? Array
74
74
 
75
75
  input = status & AGGREGATED_STATUS_KEYS
76
76
  return unless input != @aggregated_status
@@ -82,8 +82,8 @@ module RSMP
82
82
  end
83
83
 
84
84
  def aggregated_status_bools=(status)
85
- raise InvalidArgument unless status.is_a? Array
86
- raise InvalidArgument unless status.size == 8
85
+ raise ArgumentError, 'aggregated status must be an Array' unless status.is_a? Array
86
+ raise ArgumentError, 'aggregated status must contain 8 elements' unless status.size == 8
87
87
 
88
88
  return unless status != @aggregated_status_bools
89
89
 
@@ -6,8 +6,10 @@ module RSMP
6
6
  def self.output_sxl_index(out, sxl)
7
7
  out['sxl_index.json'] = output_json({
8
8
  'meta' => sxl[:meta],
9
- 'statuses' => index_items(sxl[:statuses], legacy_types: legacy_types?(sxl)),
10
- 'commands' => index_items(sxl[:commands], legacy_types: legacy_types?(sxl)),
9
+ 'statuses' => index_items(sxl[:statuses],
10
+ legacy_types: legacy_types?(sxl)),
11
+ 'commands' => index_items(sxl[:commands],
12
+ legacy_types: legacy_types?(sxl)),
11
13
  'alarms' => index_items(sxl[:alarms], legacy_types: legacy_types?(sxl))
12
14
  })
13
15
  end
@@ -69,7 +71,10 @@ module RSMP
69
71
 
70
72
  def self.argument_array_descriptor(argument, legacy_types:)
71
73
  descriptor = { 'type' => argument['type'] }
72
- descriptor['items'] = typed_arguments(argument['items'], legacy_types: legacy_types) if argument['items'].is_a?(Hash)
74
+ if argument['items'].is_a?(Hash)
75
+ descriptor['items'] =
76
+ typed_arguments(argument['items'], legacy_types: legacy_types)
77
+ end
73
78
  descriptor
74
79
  end
75
80
 
@@ -20,7 +20,7 @@ module RSMP
20
20
  def self.convert(yaml)
21
21
  sxl = build_empty_sxl
22
22
  sxl[:meta] = yaml['meta']
23
- merge_objects(sxl, yaml['objects'])
23
+ merge_components(sxl, component_definitions(yaml))
24
24
  sxl
25
25
  end
26
26
 
@@ -28,16 +28,20 @@ module RSMP
28
28
  { meta: {}, alarms: {}, statuses: {}, commands: {} }
29
29
  end
30
30
 
31
- def self.merge_objects(sxl, objects)
32
- objects.each_pair do |_type, object|
33
- merge_object_items(sxl, object)
31
+ def self.component_definitions(yaml)
32
+ yaml.fetch('components') { yaml.fetch('objects') }
33
+ end
34
+
35
+ def self.merge_components(sxl, components)
36
+ components.each_pair do |_type, component|
37
+ merge_component_items(sxl, component)
34
38
  end
35
39
  end
36
40
 
37
- def self.merge_object_items(sxl, object)
38
- object['alarms']&.each { |id, item| sxl[:alarms][id] = item }
39
- object['statuses']&.each { |id, item| sxl[:statuses][id] = item }
40
- object['commands']&.each { |id, item| sxl[:commands][id] = item }
41
+ def self.merge_component_items(sxl, component)
42
+ component['alarms']&.each { |id, item| sxl[:alarms][id] = item }
43
+ component['statuses']&.each { |id, item| sxl[:statuses][id] = item }
44
+ component['commands']&.each { |id, item| sxl[:commands][id] = item }
41
45
  end
42
46
  end
43
47
  end
@@ -0,0 +1,18 @@
1
+ module RSMP
2
+ # The disposition of a successfully submitted outbound message.
3
+ Delivery = Data.define(:message, :state) do
4
+ def initialize(message:, state:)
5
+ raise ArgumentError, 'delivery state must be :sent or :buffered' unless %i[sent buffered].include?(state)
6
+
7
+ super
8
+ end
9
+
10
+ def sent?
11
+ state == :sent
12
+ end
13
+
14
+ def buffered?
15
+ state == :buffered
16
+ end
17
+ end
18
+ end
data/lib/rsmp/event.rb ADDED
@@ -0,0 +1,19 @@
1
+ module RSMP
2
+ # A typed occurrence produced by a long-running node or connection.
3
+ Event = Data.define(:type, :source, :session_id, :message, :failure, :context, :at) do
4
+ def initialize(type:, source:, session_id: nil, message: nil, failure: nil, context: {}, at: Time.now)
5
+ raise ArgumentError, 'event type must be a Symbol' unless type.is_a?(Symbol)
6
+ raise ArgumentError, 'event context must be a Hash' unless context.is_a?(Hash)
7
+
8
+ super(
9
+ type: type,
10
+ source: source,
11
+ session_id: session_id,
12
+ message: message,
13
+ failure: failure,
14
+ context: context.dup.freeze,
15
+ at: at
16
+ )
17
+ end
18
+ end
19
+ end