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
@@ -0,0 +1,158 @@
1
+ require 'psych'
2
+
3
+ module RSMP
4
+ module SXL
5
+ module Processing
6
+ # A validated SXL YAML document with a normalized component structure.
7
+ class Document
8
+ NAME = %r{\A[a-z0-9][a-z0-9_/-]*\z}
9
+ PREFIX = %r{\A[a-zA-Z0-9_/-]+/\z}
10
+ MESSAGE_KINDS = %w[alarms statuses commands].freeze
11
+
12
+ SymbolDefinition = Struct.new(:id, :kind, :location, keyword_init: true)
13
+
14
+ attr_reader :path, :data, :name, :version, :version_string, :minimum_core_version,
15
+ :dependencies, :component_types, :message_codes
16
+
17
+ def self.load(path)
18
+ expanded_path = File.expand_path(path)
19
+ raise Error, "SXL source #{path} not found" unless File.exist?(expanded_path)
20
+ raise Error, "SXL source #{path} is not a file" unless File.file?(expanded_path)
21
+
22
+ parse(File.read(expanded_path, encoding: 'UTF-8'), path: expanded_path)
23
+ rescue EncodingError => e
24
+ raise Error, "Cannot read SXL source #{path}: #{e.message}"
25
+ end
26
+
27
+ def self.parse(source, path: '(SXL source)')
28
+ data = Psych.safe_load(source, permitted_classes: [], permitted_symbols: [], aliases: false)
29
+ new(path: path, data: data)
30
+ rescue Psych::Exception => e
31
+ raise Error, "Cannot read SXL source #{path}: #{e.message}"
32
+ end
33
+
34
+ def self.validate_name!(value, label: 'SXL name')
35
+ return value if value.is_a?(String) && NAME.match?(value)
36
+
37
+ raise Error, "Invalid #{label} #{value.inspect}"
38
+ end
39
+
40
+ def initialize(path:, data:)
41
+ @path = path
42
+ @data = normalize_document(require_hash(data, 'document'))
43
+ read_metadata
44
+ read_dependencies
45
+ read_symbols
46
+ rescue Error => e
47
+ raise Error, "#{path}: #{e.message}"
48
+ end
49
+
50
+ private
51
+
52
+ def normalize_document(document)
53
+ components = component_definitions(document)
54
+ document.except('objects').merge('components' => components)
55
+ end
56
+
57
+ def component_definitions(document)
58
+ if document.key?('components') && document.key?('objects')
59
+ raise Error, 'Invalid document; use either components or objects, not both'
60
+ end
61
+
62
+ key = document.key?('components') ? 'components' : 'objects'
63
+ require_hash(document[key], key)
64
+ end
65
+
66
+ def read_metadata
67
+ meta = require_hash(data['meta'], 'meta')
68
+ @name = self.class.validate_name!(meta['name'])
69
+ @version_string = meta['version'].to_s
70
+ @version = VersionRequirement.exact_version(meta['version'], label: 'SXL version')
71
+ @minimum_core_version = read_minimum_core_version(meta)
72
+ @prefix = data['prefix']
73
+ validate_prefix!
74
+ end
75
+
76
+ def read_minimum_core_version(meta)
77
+ minimum = meta['minimum_core_version']
78
+ return unless minimum
79
+
80
+ VersionRequirement.exact_version(minimum, label: 'minimum Core version')
81
+ end
82
+
83
+ def validate_prefix!
84
+ return if @prefix.nil? || (@prefix.is_a?(String) && PREFIX.match?(@prefix))
85
+
86
+ raise Error, "Invalid SXL prefix #{@prefix.inspect}; it must end with /"
87
+ end
88
+
89
+ def read_dependencies
90
+ dependency_data = data['dependencies'] || {}
91
+ require_hash(dependency_data, 'dependencies')
92
+ @dependencies = dependency_data.to_h do |dependency_name, requirement|
93
+ self.class.validate_name!(dependency_name, label: 'dependency name')
94
+ unless requirement.is_a?(String)
95
+ raise Error, "Invalid requirement for #{dependency_name.inspect}; expected a string"
96
+ end
97
+
98
+ [dependency_name, VersionRequirement.new(requirement)]
99
+ end
100
+ end
101
+
102
+ def read_symbols
103
+ components = data['components']
104
+ @component_types = []
105
+ @message_codes = []
106
+ seen_components = {}
107
+ seen_messages = {}
108
+
109
+ components.each_pair do |component_id, component|
110
+ component_id = require_identifier(component_id, 'component type')
111
+ full_component_id = prefixed(component_id)
112
+ ensure_unique!(seen_components, full_component_id, 'component type')
113
+ @component_types << SymbolDefinition.new(id: full_component_id, kind: :component,
114
+ location: component_id)
115
+ read_component_messages(component_id, component, seen_messages)
116
+ end
117
+ end
118
+
119
+ def read_component_messages(component_id, component, seen_messages)
120
+ component = require_hash(component, "component #{component_id.inspect}")
121
+ MESSAGE_KINDS.each do |kind|
122
+ messages = component[kind] || {}
123
+ require_hash(messages, "#{kind} for component #{component_id.inspect}")
124
+ messages.each_key do |code|
125
+ code = require_identifier(code, 'message code')
126
+ full_code = prefixed(code)
127
+ ensure_unique!(seen_messages, full_code, 'message code')
128
+ @message_codes << SymbolDefinition.new(id: full_code, kind: kind.delete_suffix('s').to_sym,
129
+ location: "#{component_id}/#{kind}/#{code}")
130
+ end
131
+ end
132
+ end
133
+
134
+ def require_hash(value, label)
135
+ return value if value.is_a?(Hash)
136
+
137
+ raise Error, "Invalid #{label}; expected a mapping"
138
+ end
139
+
140
+ def require_identifier(value, label)
141
+ return value if value.is_a?(String) && !value.empty?
142
+
143
+ raise Error, "Invalid #{label} #{value.inspect}; expected a non-empty string"
144
+ end
145
+
146
+ def prefixed(identifier)
147
+ "#{@prefix}#{identifier}"
148
+ end
149
+
150
+ def ensure_unique!(seen, identifier, label)
151
+ raise Error, "Duplicate #{label} #{identifier.inspect}" if seen[identifier]
152
+
153
+ seen[identifier] = true
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,8 @@
1
+ module RSMP
2
+ module SXL
3
+ module Processing
4
+ # Base error for SXL dependency and manifest processing.
5
+ class Error < StandardError; end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,144 @@
1
+ require 'date'
2
+ require 'psych'
3
+ require 'time'
4
+
5
+ module RSMP
6
+ module SXL
7
+ module Processing
8
+ # Creates and verifies deterministic SXL manifests.
9
+ module Manifest
10
+ TOP_LEVEL_KEYS = %w[meta sxls].freeze
11
+ META_KEYS = %w[created_at created_by format].freeze
12
+
13
+ def self.create(documents, format:, now: Time.now)
14
+ validate_documents!(documents, format)
15
+ {
16
+ 'meta' => {
17
+ 'created_at' => now.utc.iso8601,
18
+ 'created_by' => "rsmp v#{RSMP::VERSION}",
19
+ 'format' => format.to_s
20
+ },
21
+ 'sxls' => NaturalSort.sort(documents.map(&:name)).to_h do |name|
22
+ document = documents.find { |candidate| candidate.name == name }
23
+ [name, document.version_string]
24
+ end
25
+ }
26
+ end
27
+
28
+ def self.dump(manifest)
29
+ Psych.safe_dump(manifest, permitted_classes: [], aliases: false, line_width: -1)
30
+ end
31
+
32
+ def self.load(path)
33
+ raise Error, "Manifest #{path} not found" unless File.exist?(path)
34
+ raise Error, "Manifest #{path} is not a file" unless File.file?(path)
35
+
36
+ parse(File.read(path, encoding: 'UTF-8'), path: path)
37
+ rescue EncodingError => e
38
+ raise Error, "Cannot read manifest #{path}: #{e.message}"
39
+ end
40
+
41
+ def self.parse(source, path: '(manifest)')
42
+ Psych.safe_load(source, permitted_classes: [Date, Time], permitted_symbols: [], aliases: false)
43
+ rescue Psych::Exception => e
44
+ raise Error, "Cannot read manifest #{path}: #{e.message}"
45
+ end
46
+
47
+ def self.verify!(manifest, catalogue)
48
+ meta, sxls = validate_shape!(manifest)
49
+ documents = load_documents!(sxls, catalogue)
50
+ validate_dependency_closure!(documents, sxls)
51
+ validate_documents!(documents, meta['format'])
52
+ documents
53
+ end
54
+
55
+ def self.validate_documents!(documents, format)
56
+ format_version = VersionRequirement.exact_version(format, label: 'manifest format')
57
+ documents.each do |document|
58
+ next unless document.minimum_core_version && document.minimum_core_version > format_version
59
+
60
+ raise Error,
61
+ "SXL #{document.name} #{document.version_string} requires Core " \
62
+ "#{document.minimum_core_version}, newer than manifest format #{format}"
63
+ end
64
+ cycle = DependencyGraph.cycle(documents)
65
+ raise Error, "Cyclic dependency #{cycle.join(' -> ')}" if cycle
66
+
67
+ ConflictChecker.check!(documents)
68
+ end
69
+
70
+ def self.validate_shape!(manifest)
71
+ raise Error, 'Invalid manifest; expected a mapping' unless manifest.is_a?(Hash)
72
+
73
+ reject_unknown_keys!(manifest, TOP_LEVEL_KEYS, 'manifest')
74
+
75
+ meta = manifest['meta']
76
+ sxls = manifest['sxls']
77
+ raise Error, 'Invalid manifest meta; expected a mapping' unless meta.is_a?(Hash)
78
+ raise Error, 'Invalid manifest sxls; expected a non-empty mapping' unless sxls.is_a?(Hash) && !sxls.empty?
79
+
80
+ reject_unknown_keys!(meta, META_KEYS, 'manifest meta')
81
+ validate_metadata!(meta)
82
+ validate_sxls!(sxls)
83
+ [meta, sxls]
84
+ end
85
+
86
+ def self.validate_metadata!(meta)
87
+ missing = META_KEYS.reject { |key| meta.key?(key) }
88
+ raise Error, "Manifest meta is missing #{missing.join(', ')}" unless missing.empty?
89
+
90
+ Time.iso8601(meta['created_at'].to_s)
91
+ unless meta['created_by'].is_a?(String) && !meta['created_by'].empty?
92
+ raise Error, 'Manifest created_by must be a non-empty string'
93
+ end
94
+
95
+ VersionRequirement.exact_version(meta['format'], label: 'manifest format')
96
+ rescue ArgumentError
97
+ raise Error, 'Manifest created_at must be an ISO 8601 timestamp'
98
+ end
99
+
100
+ def self.validate_sxls!(sxls)
101
+ sxls.each_pair do |name, version|
102
+ Document.validate_name!(name)
103
+ VersionRequirement.exact_version(version, label: "version for #{name}")
104
+ end
105
+ expected_order = NaturalSort.sort(sxls.keys)
106
+ return if sxls.keys == expected_order
107
+
108
+ raise Error, 'Manifest SXLs are not in natural name order'
109
+ end
110
+
111
+ def self.load_documents!(sxls, catalogue)
112
+ sxls.map do |name, version|
113
+ document = catalogue.fetch(name, version)
114
+ raise Error, "SXL #{name} #{version} is not available from the supplied sources" unless document
115
+
116
+ document
117
+ end
118
+ end
119
+
120
+ def self.validate_dependency_closure!(documents, sxls)
121
+ documents.each do |document|
122
+ document.dependencies.each_pair do |name, requirement|
123
+ version = sxls[name]
124
+ raise Error, "Manifest is missing dependency #{name} required by #{document.name}" unless version
125
+ next if requirement.satisfied_by?(version)
126
+
127
+ raise Error,
128
+ "Manifest dependency #{name} #{version} does not satisfy #{requirement.source} " \
129
+ "required by #{document.name}"
130
+ end
131
+ end
132
+ end
133
+
134
+ def self.reject_unknown_keys!(hash, expected, label)
135
+ unknown = hash.keys - expected
136
+ raise Error, "Unknown #{label} key(s): #{unknown.join(', ')}" unless unknown.empty?
137
+ end
138
+
139
+ private_class_method :validate_shape!, :validate_metadata!, :validate_sxls!, :load_documents!,
140
+ :validate_dependency_closure!, :reject_unknown_keys!
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,22 @@
1
+ module RSMP
2
+ module SXL
3
+ module Processing
4
+ # Natural sorting for identifiers containing decimal digit sequences.
5
+ module NaturalSort
6
+ def self.key(value)
7
+ value.scan(/\d+|\D+/).map do |part|
8
+ if part.match?(/\A\d+\z/)
9
+ [0, part.to_i, part.length]
10
+ else
11
+ [1, part]
12
+ end
13
+ end
14
+ end
15
+
16
+ def self.sort(values)
17
+ values.sort_by { |value| key(value) }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,94 @@
1
+ module RSMP
2
+ module SXL
3
+ module Processing
4
+ # Resolves root requirements to a deterministic set of exact SXL versions.
5
+ class Resolver
6
+ def initialize(catalogue)
7
+ @catalogue = catalogue
8
+ @diagnostics = []
9
+ end
10
+
11
+ def resolve(root_requirements)
12
+ constraints = Hash.new { |hash, key| hash[key] = [] }
13
+ root_requirements.each do |name, requirement|
14
+ Document.validate_name!(name)
15
+ constraints[name] << requirement
16
+ end
17
+ raise Error, 'At least one root SXL is required' if constraints.empty?
18
+
19
+ solution = search({}, constraints)
20
+ return sorted(solution.values) if solution
21
+
22
+ detail = @diagnostics.uniq.first(3).join('; ')
23
+ raise Error, "Cannot resolve SXL dependencies#{": #{detail}" unless detail.empty?}"
24
+ end
25
+
26
+ private
27
+
28
+ def search(selected, constraints)
29
+ return nil unless selected_versions_valid?(selected, constraints)
30
+
31
+ unresolved = NaturalSort.sort(constraints.keys.reject { |name| selected.key?(name) })
32
+ return selected if unresolved.empty?
33
+
34
+ name = unresolved.first
35
+ candidates = @catalogue.candidates(name, constraints[name])
36
+ record_missing_candidate(name, constraints[name]) if candidates.empty?
37
+
38
+ candidates.each do |document|
39
+ solution = search_candidate(name, document, selected, constraints)
40
+ return solution if solution
41
+ end
42
+ nil
43
+ end
44
+
45
+ def search_candidate(name, document, selected, constraints)
46
+ next_selected = selected.merge(name => document)
47
+ next_constraints = copy_constraints(constraints)
48
+ document.dependencies.each_pair do |dependency_name, requirement|
49
+ next_constraints[dependency_name] << requirement
50
+ end
51
+
52
+ cycle = DependencyGraph.cycle(next_selected.values)
53
+ if cycle
54
+ @diagnostics << "cyclic dependency #{cycle.join(' -> ')}"
55
+ return nil
56
+ end
57
+
58
+ search(next_selected, next_constraints)
59
+ end
60
+
61
+ def selected_versions_valid?(selected, constraints)
62
+ selected.each_pair do |name, document|
63
+ next if constraints[name].all? { |requirement| requirement.satisfied_by?(document.version) }
64
+
65
+ requirements = constraints[name].map(&:source).join(', ')
66
+ @diagnostics << "#{name} #{document.version_string} does not satisfy #{requirements}"
67
+ return false
68
+ end
69
+ true
70
+ end
71
+
72
+ def record_missing_candidate(name, requirements)
73
+ available = @catalogue.versions(name)
74
+ if available.empty?
75
+ @diagnostics << "no versions available for #{name}"
76
+ else
77
+ expected = requirements.map(&:source).join(', ')
78
+ @diagnostics << "no version of #{name} satisfies #{expected} (available: #{available.join(', ')})"
79
+ end
80
+ end
81
+
82
+ def copy_constraints(constraints)
83
+ constraints.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(name, requirements), copy|
84
+ copy[name] = requirements.dup
85
+ end
86
+ end
87
+
88
+ def sorted(documents)
89
+ documents.sort_by { |document| NaturalSort.key(document.name) }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,86 @@
1
+ require 'rubygems'
2
+
3
+ module RSMP
4
+ module SXL
5
+ module Processing
6
+ # Parses the version requirement syntax defined by RSMP Core.
7
+ class VersionRequirement
8
+ EXACT_VERSION = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\z/
9
+ PARTIAL_VERSION = /\A(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?\z/
10
+ CLAUSE = /\A(~|>=|<=|>|<)?\s*(\d+(?:\.\d+){1,2})\z/
11
+
12
+ attr_reader :source
13
+
14
+ def self.exact_version(value, label: 'version')
15
+ string = value.to_s
16
+ unless EXACT_VERSION.match?(string)
17
+ raise Error, "Invalid #{label} #{value.inspect}; expected MAJOR.MINOR.PATCH"
18
+ end
19
+
20
+ Gem::Version.new(string)
21
+ end
22
+
23
+ def initialize(source)
24
+ @source = source.to_s
25
+ @predicates = parse(@source)
26
+ end
27
+
28
+ def satisfied_by?(version)
29
+ version = self.class.exact_version(version) unless version.is_a?(Gem::Version)
30
+ @predicates.all? { |predicate| predicate.call(version) }
31
+ end
32
+
33
+ private
34
+
35
+ def parse(source)
36
+ raise Error, 'Version requirement cannot be empty' if source.strip.empty?
37
+
38
+ source.split(/\s+and\s+/).map { |clause| parse_clause(clause.strip) }
39
+ end
40
+
41
+ def parse_clause(clause)
42
+ match = CLAUSE.match(clause)
43
+ raise Error, "Invalid version requirement #{source.inspect}" unless match
44
+
45
+ operator = match[1]
46
+ version_string = match[2]
47
+ return compatible_predicate(version_string) if operator == '~'
48
+
49
+ version = self.class.exact_version(version_string, label: 'requirement version')
50
+ comparison_predicate(operator || '=', version)
51
+ end
52
+
53
+ def compatible_predicate(version_string)
54
+ match = PARTIAL_VERSION.match(version_string)
55
+ raise Error, "Invalid compatibility requirement #{source.inspect}" unless match
56
+
57
+ major, minor, patch = match.captures.map { |part| part&.to_i }
58
+ return major_zero_predicate(major, minor, patch) if major.zero?
59
+
60
+ lower = Gem::Version.new("#{major}.#{minor}.#{patch || 0}")
61
+ upper = Gem::Version.new("#{major + 1}.0.0")
62
+ ->(version) { version >= lower && version < upper }
63
+ end
64
+
65
+ def major_zero_predicate(major, minor, patch)
66
+ unless patch
67
+ raise Error, "Major-zero compatibility requirement #{source.inspect} must include an exact patch version"
68
+ end
69
+
70
+ exact = Gem::Version.new("#{major}.#{minor}.#{patch}")
71
+ ->(version) { version == exact }
72
+ end
73
+
74
+ def comparison_predicate(operator, expected)
75
+ case operator
76
+ when '=' then ->(version) { version == expected }
77
+ when '>' then ->(version) { version > expected }
78
+ when '>=' then ->(version) { version >= expected }
79
+ when '<' then ->(version) { version < expected }
80
+ when '<=' then ->(version) { version <= expected }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../version'
2
+ require_relative 'processing/error'
3
+ require_relative 'processing/natural_sort'
4
+ require_relative 'processing/version_requirement'
5
+ require_relative 'processing/document'
6
+ require_relative 'processing/catalogue'
7
+ require_relative 'processing/dependency_graph'
8
+ require_relative 'processing/resolver'
9
+ require_relative 'processing/conflict_checker'
10
+ require_relative 'processing/manifest'
11
+
12
+ module RSMP
13
+ module SXL
14
+ # Dependency resolution, conflict checks, and manifests for SXL definitions.
15
+ module Processing
16
+ end
17
+ end
18
+ end
@@ -8,12 +8,19 @@ module RSMP
8
8
 
9
9
  def_delegators :proxy,
10
10
  :request_status,
11
+ :request_status!,
11
12
  :request_status_and_collect,
13
+ :request_status_and_collect!,
12
14
  :subscribe_to_status,
15
+ :subscribe_to_status!,
13
16
  :subscribe_to_status_and_collect,
17
+ :subscribe_to_status_and_collect!,
14
18
  :unsubscribe_to_status,
19
+ :unsubscribe_to_status!,
15
20
  :send_command,
16
- :send_command_and_collect
21
+ :send_command!,
22
+ :send_command_and_collect,
23
+ :send_command_and_collect!
17
24
 
18
25
  def process_message(_message); end
19
26
  end
@@ -0,0 +1,10 @@
1
+ module RSMP
2
+ # An explicit reason for a long-running service task to finish normally.
3
+ Termination = Data.define(:reason, :source) do
4
+ def initialize(reason:, source:)
5
+ raise ArgumentError, 'termination reason must be a Symbol' unless reason.is_a?(Symbol)
6
+
7
+ super
8
+ end
9
+ end
10
+ end
@@ -213,8 +213,6 @@ module RSMP
213
213
  end.join(',')
214
214
  TrafficControllerSite.make_status times
215
215
  end
216
- rescue StandardError => e
217
- puts e
218
216
  end
219
217
  end
220
218
  end