rsmp 0.45.0 → 0.45.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yaml +1 -1
  3. data/.github/workflows/sus.yaml +1 -1
  4. data/.gitignore +0 -1
  5. data/CHANGELOG.md +12 -1
  6. data/Gemfile +0 -2
  7. data/Gemfile.lock +1 -79
  8. data/README.md +14 -35
  9. data/documentation/configuration.md +12 -11
  10. data/documentation/message_distribution.md +1 -2
  11. data/documentation/tasks.md +1 -2
  12. data/exe/rsmp +1 -2
  13. data/lib/rsmp/cli.rb +33 -3
  14. data/lib/rsmp/convert/export/json_schema/outputs.rb +18 -0
  15. data/lib/rsmp/convert/export/json_schema/values.rb +1 -1
  16. data/lib/rsmp/convert/export/json_schema.rb +14 -6
  17. data/lib/rsmp/schema/core_sxl_resolution.rb +69 -0
  18. data/lib/rsmp/schema/validation.rb +57 -0
  19. data/lib/rsmp/schema.rb +40 -67
  20. data/lib/rsmp/version.rb +1 -1
  21. data/schemas/tlc/1.0.10/rsmp.json +2 -1
  22. data/schemas/tlc/1.0.10/sxl.yaml +1 -0
  23. data/schemas/tlc/1.0.10/sxl_index.json +356 -0
  24. data/schemas/tlc/1.0.13/rsmp.json +2 -1
  25. data/schemas/tlc/1.0.13/sxl.yaml +1 -0
  26. data/schemas/tlc/1.0.13/sxl_index.json +436 -0
  27. data/schemas/tlc/1.0.14/rsmp.json +2 -1
  28. data/schemas/tlc/1.0.14/sxl.yaml +1 -0
  29. data/schemas/tlc/1.0.14/sxl_index.json +468 -0
  30. data/schemas/tlc/1.0.15/rsmp.json +2 -1
  31. data/schemas/tlc/1.0.15/sxl.yaml +1 -0
  32. data/schemas/tlc/1.0.15/sxl_index.json +508 -0
  33. data/schemas/tlc/1.0.7/rsmp.json +2 -1
  34. data/schemas/tlc/1.0.7/sxl.yaml +1 -0
  35. data/schemas/tlc/1.0.7/sxl_index.json +356 -0
  36. data/schemas/tlc/1.0.8/rsmp.json +2 -1
  37. data/schemas/tlc/1.0.8/sxl.yaml +1 -0
  38. data/schemas/tlc/1.0.8/sxl_index.json +356 -0
  39. data/schemas/tlc/1.0.9/rsmp.json +2 -1
  40. data/schemas/tlc/1.0.9/sxl.yaml +1 -0
  41. data/schemas/tlc/1.0.9/sxl_index.json +356 -0
  42. data/schemas/tlc/1.1.0/rsmp.json +2 -1
  43. data/schemas/tlc/1.1.0/sxl.yaml +1 -0
  44. data/schemas/tlc/1.1.0/sxl_index.json +572 -0
  45. data/schemas/tlc/1.2.0/rsmp.json +2 -1
  46. data/schemas/tlc/1.2.0/sxl.yaml +1 -0
  47. data/schemas/tlc/1.2.0/sxl_index.json +571 -0
  48. data/schemas/tlc/1.2.1/rsmp.json +2 -1
  49. data/schemas/tlc/1.2.1/sxl.yaml +1 -0
  50. data/schemas/tlc/1.2.1/sxl_index.json +571 -0
  51. data/schemas/tlc/1.3.0/defs/definitions.json +86 -25
  52. data/schemas/tlc/1.3.0/rsmp.json +2 -1
  53. data/schemas/tlc/1.3.0/statuses/S0024.json +2 -1
  54. data/schemas/tlc/1.3.0/sxl.yaml +1 -0
  55. data/schemas/tlc/1.3.0/sxl_index.json +578 -0
  56. metadata +14 -4
  57. data/.github/copilot-instructions.md +0 -33
  58. data/.rspec +0 -1
  59. data/cucumber.yml +0 -1
@@ -0,0 +1,57 @@
1
+ module RSMP
2
+ # Provides JSON Schema validation for RSMP messages across core and SXL versions.
3
+ module Schema
4
+ def self.core_message_type?(message)
5
+ type = message['type']
6
+ %w[
7
+ MessageAck
8
+ MessageNotAck
9
+ Version
10
+ ComponentList
11
+ AggregatedStatus
12
+ AggregatedStatusRequest
13
+ Watchdog
14
+ ].include?(type)
15
+ end
16
+
17
+ def self.validate_core(message, schemas, options)
18
+ core_version = schemas[:core] || schemas['core']
19
+ raise ArgumentError, 'schemas must include core' unless core_version
20
+
21
+ schema = find_schema! :core, core_version, options
22
+ validate_using_schema(message, schema)
23
+ end
24
+
25
+ def self.validate_sxls(message, schemas, options)
26
+ sxl_schemas = schemas.reject { |type, _version| type.to_sym == :core }
27
+ return [] if sxl_schemas.empty? || core_message_type?(message)
28
+
29
+ resolved = resolve_sxl(message, schemas: schemas, **options)
30
+ return validate_resolved_sxl(message, resolved, schemas, options) if resolved
31
+
32
+ all_errors = []
33
+ sxl_schemas.each do |type, version|
34
+ schema = find_core_sxl_schema! type, version, schema_core_version(schemas), options
35
+ errors = validate_using_schema(message, schema)
36
+ return [] if errors.empty?
37
+
38
+ all_errors.concat errors
39
+ end
40
+ all_errors
41
+ end
42
+
43
+ # Core must pass. SXL-defined messages pass if at least one SXL schema passes.
44
+ def self.validate(message, schemas, options = {})
45
+ raise ArgumentError, 'message missing' unless message
46
+ raise ArgumentError, 'schemas missing' unless schemas
47
+ raise ArgumentError, 'schemas must be a Hash' unless schemas.is_a?(Hash)
48
+ raise ArgumentError, 'schemas cannot be empty' unless schemas.any?
49
+
50
+ errors = validate_core(message, schemas, options)
51
+ errors.concat validate_sxls(message, schemas, options) if errors.empty?
52
+ return nil if errors.empty?
53
+
54
+ errors
55
+ end
56
+ end
57
+ end
data/lib/rsmp/schema.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'json_schemer'
2
2
  require 'json'
3
- require 'yaml'
4
3
 
5
4
  # RSMP (Road Side Message Protocol) schema validation library.
6
5
  module RSMP
@@ -11,13 +10,19 @@ module RSMP
11
10
  def self.setup
12
11
  @schemas = {}
13
12
  @schema_paths = {}
14
- schemas_path = File.expand_path(File.join(__dir__, '..', '..', 'schemas'))
13
+ @sxl_indexes = {}
14
+ @core_sxl_schemas = {}
15
+ schemas_path = schema_root_path
15
16
  Dir.glob("#{schemas_path}/*").select { |f| File.directory? f }.each do |type_path|
16
17
  type = File.basename(type_path).to_sym
17
18
  load_schema_type type, type_path
18
19
  end
19
20
  end
20
21
 
22
+ def self.schema_root_path
23
+ File.expand_path(File.join(__dir__, '..', '..', 'schemas'))
24
+ end
25
+
21
26
  # load an schema from a folder. schemas are organized by version, and contain
22
27
  # json schema files, with the entry point being rsmp.jspon, eg:
23
28
  # tlc
@@ -35,6 +40,8 @@ module RSMP
35
40
  @schemas[type] = {}
36
41
  @schema_paths ||= {}
37
42
  @schema_paths[type] = {}
43
+ clear_sxl_index(type)
44
+ clear_core_sxl_schemas(type)
38
45
  schema_version_paths(type_path).each { |schema_path| load_schema_version(type, schema_path) }
39
46
  end
40
47
 
@@ -53,6 +60,8 @@ module RSMP
53
60
 
54
61
  @schemas[type][version] = JSONSchemer.schema(Pathname.new(file_path))
55
62
  @schema_paths[type][version] = schema_path
63
+ clear_sxl_index(type, version)
64
+ clear_core_sxl_schemas(type, version)
56
65
  end
57
66
 
58
67
  # remove a schema type
@@ -60,6 +69,8 @@ module RSMP
60
69
  type = type.to_sym
61
70
  schemas.delete type
62
71
  @schema_paths&.delete type
72
+ clear_sxl_index(type)
73
+ clear_core_sxl_schemas(type)
63
74
  end
64
75
 
65
76
  # get schemas types
@@ -195,14 +206,7 @@ module RSMP
195
206
  version = sanitize_version version if options[:lenient]
196
207
  find_schema! type, version
197
208
 
198
- path = @schema_paths&.dig(type.to_sym, version)
199
- return {} unless path
200
-
201
- yaml_path = File.join(path, 'sxl.yaml')
202
- return YAML.load_file(yaml_path).fetch('meta', {}) if File.exist?(yaml_path)
203
-
204
- json_path = File.join(path, 'rsmp.json')
205
- File.exist?(json_path) ? JSON.parse(File.read(json_path)) : {}
209
+ sxl_index(type, version).fetch('meta', {})
206
210
  end
207
211
 
208
212
  def self.sxl_prefix(type, version, options = {})
@@ -211,82 +215,51 @@ module RSMP
211
215
 
212
216
  # return a catalogue of statuses for a particular schema type and version
213
217
  # returns a hash of { status_code_id_sym => [arg_name_sym, ...] }
214
- # raises an error if the schema type/version is not found, or has no sxl.yaml
218
+ # raises an error if the schema type/version is not found, or has no status catalogue
215
219
  def self.status_catalogue(type, version)
216
220
  sxl_catalogue(type, version, :statuses).transform_keys(&:to_sym).transform_values do |status|
217
- (status['arguments'] || {}).keys.map(&:to_sym)
221
+ status.fetch('arguments', []).map(&:to_sym)
218
222
  end
219
223
  end
220
224
 
221
225
  def self.sxl_catalogue(type, version, kind)
222
226
  find_schema! type, version
223
- schema_path = @schema_paths&.dig(type.to_sym, version)
224
- yaml_path = File.join(schema_path, 'sxl.yaml') if schema_path
225
- raise "No sxl.yaml for #{type} #{version}" unless yaml_path && File.exist?(yaml_path)
226
-
227
- sxl = RSMP::Convert::Import::YAML.read(yaml_path)
228
- sxl.fetch(kind)
229
- end
230
-
231
- def self.core_message_type?(message)
232
- type = message['type']
233
- %w[
234
- MessageAck
235
- MessageNotAck
236
- Version
237
- ComponentList
238
- AggregatedStatus
239
- AggregatedStatusRequest
240
- Watchdog
241
- ].include?(type)
242
- end
227
+ catalogue = sxl_index(type, version)[kind.to_s]
228
+ raise "No #{kind} catalogue for #{type} #{version}" unless catalogue
243
229
 
244
- def self.validate_core(message, schemas, options)
245
- core_version = schemas[:core] || schemas['core']
246
- raise ArgumentError, 'schemas must include core' unless core_version
247
-
248
- schema = find_schema! :core, core_version, options
249
- validate_using_schema(message, schema)
230
+ catalogue
250
231
  end
251
232
 
252
- def self.validate_sxls(message, schemas, options)
253
- sxl_schemas = schemas.reject { |type, _version| type.to_sym == :core }
254
- return [] if sxl_schemas.empty? || core_message_type?(message)
233
+ def self.clear_sxl_index(type = nil, version = nil)
234
+ @sxl_indexes ||= {}
235
+ return @sxl_indexes.clear unless type
255
236
 
256
- resolved = resolve_sxl(message, schemas: schemas, **options)
257
- if resolved
258
- type, version = resolved
259
- schema = find_schema! type, version, options
260
- return validate_using_schema(message, schema)
237
+ type = type.to_sym
238
+ if version
239
+ @sxl_indexes.delete([type, version.to_s])
240
+ else
241
+ @sxl_indexes.delete_if { |key, _value| key.first == type }
261
242
  end
243
+ end
262
244
 
263
- all_errors = []
264
- sxl_schemas.each do |type, version|
265
- schema = find_schema! type, version, options
266
- errors = validate_using_schema(message, schema)
267
- return [] if errors.empty?
268
-
269
- all_errors.concat errors
270
- end
271
- all_errors
245
+ def self.sxl_index(type, version)
246
+ key = [type.to_sym, version.to_s]
247
+ @sxl_indexes ||= {}
248
+ @sxl_indexes[key] ||= load_sxl_index(type, version)
272
249
  end
273
250
 
274
- # validate using core and optional SXL schemas.
275
- # Core must pass. SXL-defined messages pass if at least one SXL schema passes.
276
- # returns nil if validation succeeds, otherwise returns an array of errors.
277
- def self.validate(message, schemas, options = {})
278
- raise ArgumentError, 'message missing' unless message
279
- raise ArgumentError, 'schemas missing' unless schemas
280
- raise ArgumentError, 'schemas must be a Hash' unless schemas.is_a?(Hash)
281
- raise ArgumentError, 'schemas cannot be empty' unless schemas.any?
251
+ def self.load_sxl_index(type, version)
252
+ schema_path = @schema_paths&.dig(type.to_sym, version.to_s)
253
+ raise UnknownSchemaVersionError, "Unknown schema version #{type} #{version}" unless schema_path
282
254
 
283
- errors = validate_core(message, schemas, options)
284
- errors.concat validate_sxls(message, schemas, options) if errors.empty?
285
- return nil if errors.empty?
255
+ index_path = File.join(schema_path, 'sxl_index.json')
256
+ raise Error, "Missing SXL index #{index_path}" unless File.exist?(index_path)
286
257
 
287
- errors
258
+ JSON.parse(File.read(index_path, encoding: 'UTF-8'))
288
259
  end
289
260
  end
290
261
  end
291
262
 
263
+ require_relative 'schema/core_sxl_resolution'
292
264
  require_relative 'schema/message_resolution'
265
+ require_relative 'schema/validation'
data/lib/rsmp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RSMP
2
- VERSION = '0.45.0'.freeze
2
+ VERSION = '0.45.1'.freeze
3
3
  end
@@ -70,5 +70,6 @@
70
70
  "$ref" : "alarms/alarms.json"
71
71
  }
72
72
  }
73
- ]
73
+ ],
74
+ "minimum_core_version" : "3.1.2"
74
75
  }
@@ -3,6 +3,7 @@ meta:
3
3
  name: tlc
4
4
  description: Traffic Light Controllers
5
5
  version: 1.0.10
6
+ minimum_core_version: 3.1.2
6
7
  objects:
7
8
  Traffic Light Controller:
8
9
  description:
@@ -0,0 +1,356 @@
1
+ {
2
+ "meta" : {
3
+ "name" : "tlc",
4
+ "description" : "Traffic Light Controllers",
5
+ "version" : "1.0.10",
6
+ "minimum_core_version" : "3.1.2"
7
+ },
8
+ "statuses" : {
9
+ "S0001" : {
10
+ "arguments" : [
11
+ "basecyclecounter",
12
+ "cyclecounter",
13
+ "signalgroupstatus",
14
+ "stage"
15
+ ]
16
+ },
17
+ "S0002" : {
18
+ "arguments" : [
19
+ "detectorlogicstatus"
20
+ ]
21
+ },
22
+ "S0003" : {
23
+ "arguments" : [
24
+ "extendedinputstatus",
25
+ "inputstatus"
26
+ ]
27
+ },
28
+ "S0004" : {
29
+ "arguments" : [
30
+ "extendedoutputstatus",
31
+ "outputstatus"
32
+ ]
33
+ },
34
+ "S0005" : {
35
+ "arguments" : [
36
+ "status"
37
+ ]
38
+ },
39
+ "S0006" : {
40
+ "arguments" : [
41
+ "emergencystage",
42
+ "status"
43
+ ]
44
+ },
45
+ "S0007" : {
46
+ "arguments" : [
47
+ "intersection",
48
+ "status"
49
+ ]
50
+ },
51
+ "S0008" : {
52
+ "arguments" : [
53
+ "intersection",
54
+ "status"
55
+ ]
56
+ },
57
+ "S0009" : {
58
+ "arguments" : [
59
+ "intersection",
60
+ "status"
61
+ ]
62
+ },
63
+ "S0010" : {
64
+ "arguments" : [
65
+ "intersection",
66
+ "status"
67
+ ]
68
+ },
69
+ "S0011" : {
70
+ "arguments" : [
71
+ "intersection",
72
+ "status"
73
+ ]
74
+ },
75
+ "S0012" : {
76
+ "arguments" : [
77
+ "intersection",
78
+ "status"
79
+ ]
80
+ },
81
+ "S0013" : {
82
+ "arguments" : [
83
+ "intersection",
84
+ "status"
85
+ ]
86
+ },
87
+ "S0014" : {
88
+ "arguments" : [
89
+ "status"
90
+ ]
91
+ },
92
+ "S0015" : {
93
+ "arguments" : [
94
+ "status"
95
+ ]
96
+ },
97
+ "S0016" : {
98
+ "arguments" : [
99
+ "number"
100
+ ]
101
+ },
102
+ "S0017" : {
103
+ "arguments" : [
104
+ "number"
105
+ ]
106
+ },
107
+ "S0018" : {
108
+ "arguments" : [
109
+ "number"
110
+ ]
111
+ },
112
+ "S0019" : {
113
+ "arguments" : [
114
+ "number"
115
+ ]
116
+ },
117
+ "S0020" : {
118
+ "arguments" : [
119
+ "controlmode",
120
+ "intersection"
121
+ ]
122
+ },
123
+ "S0021" : {
124
+ "arguments" : [
125
+ "detectorlogics"
126
+ ]
127
+ },
128
+ "S0091" : {
129
+ "arguments" : [
130
+ "status",
131
+ "user"
132
+ ]
133
+ },
134
+ "S0092" : {
135
+ "arguments" : [
136
+ "status",
137
+ "user"
138
+ ]
139
+ },
140
+ "S0095" : {
141
+ "arguments" : [
142
+ "status"
143
+ ]
144
+ },
145
+ "S0096" : {
146
+ "arguments" : [
147
+ "day",
148
+ "hour",
149
+ "minute",
150
+ "month",
151
+ "second",
152
+ "year"
153
+ ]
154
+ },
155
+ "S0201" : {
156
+ "arguments" : [
157
+ "starttime",
158
+ "vehicles"
159
+ ]
160
+ },
161
+ "S0202" : {
162
+ "arguments" : [
163
+ "speed",
164
+ "starttime"
165
+ ]
166
+ },
167
+ "S0203" : {
168
+ "arguments" : [
169
+ "occupancy",
170
+ "starttime"
171
+ ]
172
+ },
173
+ "S0204" : {
174
+ "arguments" : [
175
+ "B",
176
+ "C",
177
+ "F",
178
+ "L",
179
+ "LS",
180
+ "MC",
181
+ "P",
182
+ "PS",
183
+ "SP",
184
+ "starttime"
185
+ ]
186
+ }
187
+ },
188
+ "commands" : {
189
+ "M0001" : {
190
+ "arguments" : [
191
+ "intersection",
192
+ "securityCode",
193
+ "status",
194
+ "timeout"
195
+ ]
196
+ },
197
+ "M0002" : {
198
+ "arguments" : [
199
+ "securityCode",
200
+ "status",
201
+ "timeplan"
202
+ ]
203
+ },
204
+ "M0003" : {
205
+ "arguments" : [
206
+ "securityCode",
207
+ "status",
208
+ "traficsituation"
209
+ ]
210
+ },
211
+ "M0004" : {
212
+ "arguments" : [
213
+ "securityCode",
214
+ "status"
215
+ ]
216
+ },
217
+ "M0005" : {
218
+ "arguments" : [
219
+ "emergencyroute",
220
+ "securityCode",
221
+ "status"
222
+ ]
223
+ },
224
+ "M0006" : {
225
+ "arguments" : [
226
+ "input",
227
+ "securityCode",
228
+ "status"
229
+ ]
230
+ },
231
+ "M0007" : {
232
+ "arguments" : [
233
+ "securityCode",
234
+ "status"
235
+ ]
236
+ },
237
+ "M0008" : {
238
+ "arguments" : [
239
+ "mode",
240
+ "securityCode",
241
+ "status"
242
+ ]
243
+ },
244
+ "M0010" : {
245
+ "arguments" : [
246
+ "securityCode",
247
+ "status"
248
+ ]
249
+ },
250
+ "M0011" : {
251
+ "arguments" : [
252
+ "securityCode",
253
+ "status"
254
+ ]
255
+ },
256
+ "M0012" : {
257
+ "arguments" : [
258
+ "securityCode",
259
+ "status"
260
+ ]
261
+ },
262
+ "M0013" : {
263
+ "arguments" : [
264
+ "securityCode",
265
+ "status"
266
+ ]
267
+ },
268
+ "M0019" : {
269
+ "arguments" : [
270
+ "input",
271
+ "inputValue",
272
+ "securityCode",
273
+ "status"
274
+ ]
275
+ },
276
+ "M0103" : {
277
+ "arguments" : [
278
+ "newSecurityCode",
279
+ "oldSecurityCode",
280
+ "status"
281
+ ]
282
+ },
283
+ "M0104" : {
284
+ "arguments" : [
285
+ "day",
286
+ "hour",
287
+ "minute",
288
+ "month",
289
+ "second",
290
+ "securityCode",
291
+ "year"
292
+ ]
293
+ }
294
+ },
295
+ "alarms" : {
296
+ "A0001" : {
297
+ "arguments" : []
298
+ },
299
+ "A0002" : {
300
+ "arguments" : []
301
+ },
302
+ "A0003" : {
303
+ "arguments" : []
304
+ },
305
+ "A0004" : {
306
+ "arguments" : []
307
+ },
308
+ "A0005" : {
309
+ "arguments" : []
310
+ },
311
+ "A0006" : {
312
+ "arguments" : []
313
+ },
314
+ "A0007" : {
315
+ "arguments" : []
316
+ },
317
+ "A0008" : {
318
+ "arguments" : [
319
+ "timeplan"
320
+ ]
321
+ },
322
+ "A0009" : {
323
+ "arguments" : []
324
+ },
325
+ "A0101" : {
326
+ "arguments" : []
327
+ },
328
+ "A0201" : {
329
+ "arguments" : [
330
+ "color"
331
+ ]
332
+ },
333
+ "A0202" : {
334
+ "arguments" : [
335
+ "color"
336
+ ]
337
+ },
338
+ "A0301" : {
339
+ "arguments" : [
340
+ "detector",
341
+ "errormode",
342
+ "manual",
343
+ "type"
344
+ ]
345
+ },
346
+ "A0302" : {
347
+ "arguments" : [
348
+ "detector",
349
+ "errormode",
350
+ "logicerror",
351
+ "manual",
352
+ "type"
353
+ ]
354
+ }
355
+ }
356
+ }
@@ -70,5 +70,6 @@
70
70
  "$ref" : "alarms/alarms.json"
71
71
  }
72
72
  }
73
- ]
73
+ ],
74
+ "minimum_core_version" : "3.1.2"
74
75
  }
@@ -3,6 +3,7 @@ meta:
3
3
  name: tlc
4
4
  description: Traffic Light Controllers
5
5
  version: 1.0.13
6
+ minimum_core_version: 3.1.2
6
7
  objects:
7
8
  Traffic Light Controller:
8
9
  description: