rsmp 0.48.3 → 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 (108) 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. data/schemas/tlc/1.0.10/commands/M0001.json +3 -3
  87. data/schemas/tlc/1.0.10/source/sxl.yaml +3 -3
  88. data/schemas/tlc/1.0.10/sxl_index.json +3 -3
  89. data/schemas/tlc/1.0.13/commands/M0001.json +3 -3
  90. data/schemas/tlc/1.0.13/source/sxl.yaml +3 -3
  91. data/schemas/tlc/1.0.13/sxl_index.json +3 -3
  92. data/schemas/tlc/1.0.14/commands/M0001.json +3 -3
  93. data/schemas/tlc/1.0.14/source/sxl.yaml +3 -3
  94. data/schemas/tlc/1.0.14/sxl_index.json +3 -3
  95. data/schemas/tlc/1.0.15/commands/M0001.json +3 -3
  96. data/schemas/tlc/1.0.15/source/sxl.yaml +3 -3
  97. data/schemas/tlc/1.0.15/sxl_index.json +3 -3
  98. data/schemas/tlc/1.0.7/commands/M0001.json +3 -3
  99. data/schemas/tlc/1.0.7/source/sxl.yaml +3 -3
  100. data/schemas/tlc/1.0.7/sxl_index.json +3 -3
  101. data/schemas/tlc/1.0.8/commands/M0001.json +3 -3
  102. data/schemas/tlc/1.0.8/source/sxl.yaml +3 -3
  103. data/schemas/tlc/1.0.8/sxl_index.json +3 -3
  104. data/schemas/tlc/1.0.9/commands/M0001.json +3 -3
  105. data/schemas/tlc/1.0.9/source/sxl.yaml +3 -3
  106. data/schemas/tlc/1.0.9/sxl_index.json +3 -3
  107. metadata +25 -4
  108. data/lib/rsmp/collect/collector/status.rb +0 -34
@@ -3,9 +3,11 @@ module RSMP
3
3
  module Modules
4
4
  # Handles status requests, responses, subscriptions and updates
5
5
  module Status
6
- # Build and send a StatusRequest. Returns { sent: message }.
6
+ # Build and send a StatusRequest. Returns Result<StatusRequest>.
7
7
  def request_status(status_list, component: nil, m_id: nil, validate: true)
8
- validate_ready 'request status'
8
+ readiness = validate_ready 'request status'
9
+ return readiness if readiness.failure?
10
+
9
11
  component ||= main.c_id
10
12
  m_id ||= RSMP::Message.make_m_id
11
13
 
@@ -21,14 +23,18 @@ module RSMP
21
23
  'mId' => m_id
22
24
  })
23
25
  apply_nts_message_attributes message
24
- send_message message, validate: validate
25
- { sent: message }
26
+ send_message(message, validate: validate).map(&:message)
27
+ end
28
+
29
+ def request_status!(...)
30
+ request_status(...).value!
26
31
  end
27
32
 
28
- # Build, send a StatusRequest and collect the StatusResponse. Returns the collector.
29
- # Call .ok! on the result to raise on NotAck or timeout.
33
+ # Build, send a StatusRequest and collect the StatusResponse. Returns Result<Exchange>.
30
34
  def request_status_and_collect(status_list, within:, component: nil, m_id: nil, validate: true)
31
- validate_ready 'request status'
35
+ readiness = validate_ready 'request status'
36
+ return readiness if readiness.failure?
37
+
32
38
  component ||= main.c_id
33
39
  m_id ||= RSMP::Message.make_m_id
34
40
 
@@ -45,7 +51,11 @@ module RSMP
45
51
  })
46
52
  apply_nts_message_attributes message
47
53
  collector = StatusCollector.new(self, list.to_a, timeout: within, m_id: m_id)
48
- send_message_and_collect(message, collector, validate: validate)[:collector]
54
+ send_message_and_collect(message, collector, validate: validate)
55
+ end
56
+
57
+ def request_status_and_collect!(...)
58
+ request_status_and_collect(...).value!
49
59
  end
50
60
 
51
61
  def process_status_response(message)
@@ -71,9 +81,11 @@ module RSMP
71
81
  end
72
82
  end
73
83
 
74
- # Build and send a StatusSubscribe. Returns { sent: message }.
84
+ # Build and send a StatusSubscribe. Returns Result<StatusSubscribe>.
75
85
  def subscribe_to_status(status_list, component: nil, m_id: nil, validate: true)
76
- validate_ready 'subscribe to status'
86
+ readiness = validate_ready 'subscribe to status'
87
+ return readiness if readiness.failure?
88
+
77
89
  component ||= main.c_id
78
90
  m_id ||= RSMP::Message.make_m_id
79
91
 
@@ -89,14 +101,18 @@ module RSMP
89
101
  'mId' => m_id
90
102
  })
91
103
  apply_nts_message_attributes message
92
- send_message message, validate: validate
93
- { sent: message }
104
+ send_message(message, validate: validate).map(&:message)
94
105
  end
95
106
 
96
- # Build, send a StatusSubscribe and collect the first matching status update. Returns the collector.
97
- # Call .ok! on the result to raise on NotAck or timeout.
107
+ def subscribe_to_status!(...)
108
+ subscribe_to_status(...).value!
109
+ end
110
+
111
+ # Build, send a StatusSubscribe and collect the first matching status update. Returns Result<Exchange>.
98
112
  def subscribe_to_status_and_collect(status_list, within:, component: nil, m_id: nil, validate: true)
99
- validate_ready 'subscribe to status'
113
+ readiness = validate_ready 'subscribe to status'
114
+ return readiness if readiness.failure?
115
+
100
116
  component ||= main.c_id
101
117
  m_id ||= RSMP::Message.make_m_id
102
118
 
@@ -113,7 +129,11 @@ module RSMP
113
129
  })
114
130
  apply_nts_message_attributes message
115
131
  collector = StatusCollector.new(self, list.to_a, timeout: within, m_id: m_id)
116
- send_message_and_collect(message, collector, validate: validate)[:collector]
132
+ send_message_and_collect(message, collector, validate: validate)
133
+ end
134
+
135
+ def subscribe_to_status_and_collect!(...)
136
+ subscribe_to_status_and_collect(...).value!
117
137
  end
118
138
 
119
139
  def remove_subscription_item(component_id, code, name)
@@ -131,15 +151,20 @@ module RSMP
131
151
  remove_subscription_item(component, item['sCI'], item['n'])
132
152
  end
133
153
 
134
- return unless ready? # if the connection is don't we skip sending
154
+ # Local subscription state must be cleaned up even when the peer has
155
+ # already disconnected. In that case there is nothing left to send.
156
+ return Result.success(nil) unless ready?
135
157
 
136
158
  message = RSMP::StatusUnsubscribe.new({
137
159
  'cId' => component,
138
160
  'sS' => status_list
139
161
  })
140
162
  apply_nts_message_attributes message
141
- send_message message, validate: validate
142
- message
163
+ send_message(message, validate: validate).map(&:message)
164
+ end
165
+
166
+ def unsubscribe_to_status!(...)
167
+ unsubscribe_to_status(...).value!
143
168
  end
144
169
 
145
170
  # unsubscribes to all statuses (with all attributes) defined in the used SXL
@@ -56,10 +56,6 @@ module RSMP
56
56
  else
57
57
  super
58
58
  end
59
- rescue RSMP::RepeatedAlarmError, RSMP::RepeatedStatusError, RSMP::TimestampError => e
60
- str = "Rejected #{message.type} message,"
61
- dont_acknowledge message, str, e.to_s
62
- distribute_error e.exception("#{str}#{e.message} #{message.json}")
63
59
  end
64
60
 
65
61
  def handled_by_parent?(message)
@@ -115,7 +111,18 @@ module RSMP
115
111
  end
116
112
 
117
113
  def validate_ready(action)
118
- raise NotReady, "Can't #{action} because connection is not ready. (Currently #{@state})" unless ready?
114
+ return Result.success(self) if ready?
115
+
116
+ Result.failure(
117
+ :not_ready,
118
+ message: "Can't #{action} because connection is not ready. (Currently #{@state})",
119
+ source: :connection,
120
+ context: { action: action, state: @state, session_id: session_id }
121
+ )
122
+ end
123
+
124
+ def validate_ready!(action)
125
+ validate_ready(action).value!
119
126
  end
120
127
 
121
128
  def version_acknowledged; end
@@ -204,11 +211,6 @@ module RSMP
204
211
  end
205
212
  end
206
213
 
207
- def receive_error(error, options = {})
208
- @supervisor&.receive_error error, options
209
- distribute_error error, options
210
- end
211
-
212
214
  def build_component(id:, type:, settings: {})
213
215
  settings ||= {}
214
216
  if type == 'main'
@@ -29,7 +29,7 @@ module RSMP
29
29
  })
30
30
 
31
31
  apply_nts_message_attributes message
32
- send_message message
32
+ send_generated_message message
33
33
  end
34
34
 
35
35
  def process_aggregated_status_request(message)
@@ -8,7 +8,7 @@ module RSMP
8
8
  component.alarms.each_pair do |_alarm_code, alarm_state|
9
9
  if alarm_state.active
10
10
  alarm = AlarmIssue.new(alarm_state.to_hash.merge('aSp' => 'Issue'))
11
- send_message alarm
11
+ send_generated_message alarm
12
12
  end
13
13
  end
14
14
  end
@@ -107,7 +107,7 @@ module RSMP
107
107
  })
108
108
  apply_nts_message_attributes response
109
109
  acknowledge message
110
- send_message response
110
+ send_generated_message response
111
111
  end
112
112
  end
113
113
  end
@@ -88,7 +88,7 @@ module RSMP
88
88
  end
89
89
  end
90
90
 
91
- def buffer_message(message, error = nil)
91
+ def buffer_message(message)
92
92
  prepared = prepare_message_for_buffer message, core_version: @core_version
93
93
  if prepared
94
94
  enqueue_buffered_message prepared
@@ -97,8 +97,6 @@ module RSMP
97
97
  else
98
98
  super
99
99
  end
100
- rescue NotReady, IOError
101
- raise error if error
102
100
  end
103
101
 
104
102
  def enqueue_buffered_message(message)
@@ -118,19 +116,20 @@ module RSMP
118
116
  @message_buffer = []
119
117
  log "Sending #{queued.size} buffered message(s)", level: :info
120
118
  queued.each_with_index do |message, index|
121
- break unless flush_buffered_message(message, queued, index)
119
+ break unless flush_buffered_message?(message, queued, index)
122
120
  end
123
121
  end
124
122
 
125
- def flush_buffered_message(message, queued, index)
123
+ def flush_buffered_message?(message, queued, index)
126
124
  prepared = prepare_message_for_buffer message, core_version: @core_version, for_send: true
127
125
  return true unless prepared
128
126
 
129
- send_message prepared, 'from buffer', buffer: false
130
- true
131
- rescue NotReady, IOError
127
+ result = send_message(prepared, 'from buffer', buffer: false)
128
+ return true if result.success?
129
+
132
130
  @message_buffer = queued[index..] + @message_buffer
133
- log "Stopped sending buffered messages; #{message_buffer.size} message(s) remain queued", level: :warning
131
+ log "Stopped sending buffered messages (#{result.failure.code}); " \
132
+ "#{message_buffer.size} message(s) remain queued", level: :warning
134
133
  false
135
134
  end
136
135
  end
@@ -54,7 +54,7 @@ module RSMP
54
54
 
55
55
  apply_nts_message_attributes response
56
56
  acknowledge message
57
- send_message response
57
+ send_generated_message response
58
58
  end
59
59
 
60
60
  def add_status_subscription(component_id, subs, update_list, arg, now)
@@ -138,7 +138,7 @@ module RSMP
138
138
  'sS' => build_status_list(component, by_code)
139
139
  })
140
140
  apply_nts_message_attributes update
141
- send_message update
141
+ send_generated_message update
142
142
  store_last_sent_status update
143
143
  component.status_updates_sent
144
144
  end
@@ -34,45 +34,39 @@ module RSMP
34
34
  end
35
35
 
36
36
  loop do
37
- connect
37
+ connected = connect
38
+ unless connected.success?
39
+ publish_connection_attempt_failure(connected.failure)
40
+ break unless reconnect_delay?
41
+
42
+ next
43
+ end
38
44
  start_reader
39
45
  start_handshake
40
- wait_for_reader # run until disconnected
41
- break unless reconnect_delay?
42
- rescue Restart
43
- @logger.mute @ip, @port
44
- raise
45
- rescue RSMP::ConnectionError => e
46
- log e, level: :error
47
- break unless reconnect_delay?
48
- rescue StandardError => e
49
- distribute_error e, level: :internal
46
+ close_from_result(wait_for_reader)
50
47
  break unless reconnect_delay?
51
48
  ensure
49
+ close(reason: :internal_failure) if $ERROR_INFO
52
50
  close
53
- stop_subtasks
54
51
  end
55
52
  end
56
53
 
57
54
  def run_accepted_connection
55
+ begin_session
58
56
  self.state = :connected
59
57
  start_reader
60
58
  start_handshake
61
- wait_for_reader
62
- rescue RSMP::ConnectionError => e
63
- log e, level: :error
64
- rescue StandardError => e
65
- distribute_error e, level: :internal
59
+ close_from_result(wait_for_reader)
66
60
  ensure
61
+ close(reason: :internal_failure) if $ERROR_INFO
67
62
  close
68
- stop_subtasks
69
63
  end
70
64
 
71
65
  def start_handshake
72
66
  send_version_request @site_settings['site_id'], core_versions
73
67
  end
74
68
 
75
- def close
69
+ def close(...)
76
70
  prune_unbuffered_status_subscriptions
77
71
  super
78
72
  end
@@ -80,14 +74,15 @@ module RSMP
80
74
  # connect to the supervisor and initiate handshake supervisor
81
75
  def connect
82
76
  log "Connecting to supervisor at #{@ip}:#{@port}", level: :info
77
+ begin_session
83
78
  self.state = :connecting
84
- connect_tcp
79
+ opened = connect_tcp
80
+ return opened if opened.failure?
81
+
82
+ self.state = :connected
85
83
  @logger.unmute @ip, @port
86
84
  log "Connected to supervisor at #{@ip}:#{@port}", level: :info
87
- rescue SystemCallError => e
88
- raise ConnectionError, "Could not connect to supervisor at #{@ip}:#{@port}: Errno #{e.errno} #{e}"
89
- rescue StandardError => e
90
- raise ConnectionError, "Error while connecting to supervisor at #{@ip}:#{@port}: #{e}"
85
+ Result.success(self)
91
86
  end
92
87
 
93
88
  def stop_task
@@ -108,10 +103,22 @@ module RSMP
108
103
 
109
104
  @stream = IO::Stream::Buffered.new(@socket)
110
105
  @protocol = RSMP::Protocol.new(@stream) # rsmp messages are json terminated with a form-feed
111
- self.state = :connected
106
+ Result.success(self)
112
107
  rescue Errno::ECONNREFUSED => e # rescue to avoid log output
113
108
  log 'Connection refused', level: :warning
114
- raise e
109
+ failed_connection_result(e)
110
+ rescue SystemCallError, SocketError, IOError, Async::TimeoutError => e
111
+ failed_connection_result(e)
112
+ end
113
+
114
+ def failed_connection_result(error)
115
+ Result.failure(
116
+ :connection_failed,
117
+ message: "Could not connect to supervisor at #{@ip}:#{@port}: #{error.message}",
118
+ source: :transport,
119
+ context: { ip: @ip, port: @port, session_id: @session_id },
120
+ cause: error
121
+ )
115
122
  end
116
123
 
117
124
  def handshake_complete
@@ -142,9 +149,6 @@ module RSMP
142
149
  else
143
150
  super
144
151
  end
145
- rescue UnknownComponent, UnknownCommand, UnknownStatus,
146
- MessageRejected, MissingAttribute => e
147
- dont_acknowledge message, '', e.to_s
148
152
  end
149
153
 
150
154
  def handle_interface_request(message)
@@ -221,7 +225,7 @@ module RSMP
221
225
  end
222
226
 
223
227
  def send_component_list
224
- send_message ComponentList.new('components' => @site.component_list)
228
+ send_generated_message ComponentList.new('components' => @site.component_list)
225
229
  end
226
230
 
227
231
  def component_list_acknowledged
@@ -0,0 +1,95 @@
1
+ module RSMP
2
+ # Result values represent the expected outcome of finite operations.
3
+ module Result
4
+ # A successfully completed operation.
5
+ Success = Data.define(:value) do
6
+ def success?
7
+ true
8
+ end
9
+
10
+ def failure?
11
+ false
12
+ end
13
+
14
+ def failure
15
+ nil
16
+ end
17
+
18
+ def value!
19
+ value
20
+ end
21
+
22
+ def map
23
+ Result.success(yield(value))
24
+ end
25
+
26
+ def and_then
27
+ yield(value)
28
+ end
29
+ end
30
+
31
+ # An expected operational failure.
32
+ Failure = Data.define(:failure) do
33
+ def initialize(failure:)
34
+ raise ArgumentError, 'failure must be an RSMP::Failure' unless failure.is_a?(RSMP::Failure)
35
+
36
+ super
37
+ end
38
+
39
+ def success?
40
+ false
41
+ end
42
+
43
+ def failure?
44
+ true
45
+ end
46
+
47
+ def value
48
+ nil
49
+ end
50
+
51
+ def value!
52
+ exception = OperationError.new(failure)
53
+ raise exception, cause: failure.cause if failure.cause
54
+
55
+ raise exception
56
+ end
57
+
58
+ def map
59
+ self
60
+ end
61
+
62
+ def and_then
63
+ self
64
+ end
65
+ end
66
+
67
+ module_function
68
+
69
+ def success(value = nil)
70
+ Success.new(value)
71
+ end
72
+
73
+ def failure(code = nil, failure: nil, **details)
74
+ Failure.new(
75
+ failure: failure || RSMP::Failure.new(
76
+ code: code,
77
+ message: details.fetch(:message, code),
78
+ source: details.fetch(:source, :operation),
79
+ context: details.fetch(:context, {}),
80
+ cause: details[:cause]
81
+ )
82
+ )
83
+ end
84
+ end
85
+
86
+ # Raised by bang APIs when an expected failure result is explicitly unwrapped.
87
+ class OperationError < Error
88
+ attr_reader :failure
89
+
90
+ def initialize(failure)
91
+ @failure = failure
92
+ super(failure.message)
93
+ end
94
+ end
95
+ end
@@ -41,6 +41,8 @@ module RSMP
41
41
  end
42
42
 
43
43
  # Core must pass. SXL-defined messages pass if at least one SXL schema passes.
44
+ # A message which does not satisfy its schemas is an ordinary validation
45
+ # outcome. Invalid API arguments and unavailable schemas still raise.
44
46
  def self.validate(message, schemas, options = {})
45
47
  raise ArgumentError, 'message missing' unless message
46
48
  raise ArgumentError, 'schemas missing' unless schemas
@@ -49,9 +51,9 @@ module RSMP
49
51
 
50
52
  errors = validate_core(message, schemas, options)
51
53
  errors.concat validate_sxls(message, schemas, options) if errors.empty?
52
- return nil if errors.empty?
53
-
54
- errors
54
+ Validation.new(violations: errors)
55
+ rescue UnknownMessageCodeError => e
56
+ Validation.new(violations: [['', 'unknown_message_code', e.message]])
55
57
  end
56
58
  end
57
59
  end
@@ -6,7 +6,14 @@ module RSMP
6
6
  class Interface
7
7
  extend Forwardable
8
8
 
9
- def_delegators :proxy, :send_message, :send_message_and_collect, :validate_ready, :log
9
+ def_delegators :proxy,
10
+ :send_message,
11
+ :send_message!,
12
+ :send_message_and_collect,
13
+ :send_message_and_collect!,
14
+ :validate_ready,
15
+ :validate_ready!,
16
+ :log
10
17
 
11
18
  attr_reader :proxy, :name, :version
12
19
 
@@ -0,0 +1,61 @@
1
+ module RSMP
2
+ module SXL
3
+ module Processing
4
+ # Indexes locally available SXL definitions by name and exact version.
5
+ class Catalogue
6
+ def initialize(paths = [])
7
+ @documents = Hash.new { |hash, key| hash[key] = {} }
8
+ Array(paths).each { |path| add_path(path) }
9
+ end
10
+
11
+ def add_path(path)
12
+ expanded_path = File.expand_path(path)
13
+ raise Error, "SXL source #{path} not found" unless File.exist?(expanded_path)
14
+
15
+ if File.directory?(expanded_path)
16
+ pattern = File.join(expanded_path, '**', '{sxl.yaml,sxl.yml}')
17
+ files = Dir.glob(pattern)
18
+ raise Error, "No sxl.yaml or sxl.yml files found under #{path}" if files.empty?
19
+
20
+ files.each { |file| add_file(file) }
21
+ else
22
+ add_file(expanded_path)
23
+ end
24
+ self
25
+ end
26
+
27
+ def add_document(document)
28
+ existing = @documents[document.name][document.version_string]
29
+ if existing && existing.path != document.path
30
+ raise Error,
31
+ "Duplicate SXL #{document.name} #{document.version_string} in #{existing.path} and #{document.path}"
32
+ end
33
+
34
+ @documents[document.name][document.version_string] = document
35
+ document
36
+ end
37
+
38
+ def candidates(name, requirements = [])
39
+ versions = @documents.fetch(name, {}).values.sort_by(&:version).reverse
40
+ versions.select do |document|
41
+ requirements.all? { |requirement| requirement.satisfied_by?(document.version) }
42
+ end
43
+ end
44
+
45
+ def versions(name)
46
+ @documents.fetch(name, {}).values.map(&:version_string).sort_by { |version| Gem::Version.new(version) }
47
+ end
48
+
49
+ def fetch(name, version)
50
+ @documents.fetch(name, {})[version.to_s]
51
+ end
52
+
53
+ private
54
+
55
+ def add_file(path)
56
+ add_document(Document.load(path))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,30 @@
1
+ module RSMP
2
+ module SXL
3
+ module Processing
4
+ # Rejects component types or message codes owned by more than one SXL.
5
+ module ConflictChecker
6
+ def self.check!(documents)
7
+ check_definitions!(documents, :component_types, 'component type')
8
+ check_definitions!(documents, :message_codes, 'message code')
9
+ end
10
+
11
+ def self.check_definitions!(documents, collection, label)
12
+ owners = {}
13
+ documents.each do |document|
14
+ document.public_send(collection).each do |definition|
15
+ previous = owners[definition.id]
16
+ if previous
17
+ raise Error,
18
+ "Conflicting #{label} #{definition.id.inspect} in " \
19
+ "#{previous[:document].name} and #{document.name}"
20
+ end
21
+ owners[definition.id] = { document: document, definition: definition }
22
+ end
23
+ end
24
+ end
25
+
26
+ private_class_method :check_definitions!
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ module RSMP
2
+ module SXL
3
+ module Processing
4
+ # Dependency graph validation shared by resolution and manifest verification.
5
+ module DependencyGraph
6
+ def self.cycle(documents)
7
+ by_name = documents.to_h { |document| [document.name, document] }
8
+ visited = {}
9
+ stack = []
10
+ by_name.each_key do |name|
11
+ found = visit(name, by_name, visited, stack)
12
+ return found if found
13
+ end
14
+ nil
15
+ end
16
+
17
+ def self.visit(name, documents, visited, stack)
18
+ return stack[stack.index(name)..] + [name] if stack.include?(name)
19
+ return nil if visited[name]
20
+
21
+ visited[name] = true
22
+ stack << name
23
+ documents.fetch(name).dependencies.each_key do |dependency|
24
+ next unless documents.key?(dependency)
25
+
26
+ found = visit(dependency, documents, visited, stack)
27
+ return found if found
28
+ end
29
+ stack.pop
30
+ nil
31
+ end
32
+
33
+ private_class_method :visit
34
+ end
35
+ end
36
+ end
37
+ end