rsmp_schema 0.9.1 → 0.10.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.
@@ -6,26 +6,27 @@ require 'fileutils'
6
6
 
7
7
  module RSMP
8
8
  module Convert
9
+ # Handles exporting SXL definitions.
9
10
  module Export
11
+ # Converts SXL definitions to JSON Schema files.
10
12
  module JSONSchema
11
-
12
- @@json_options = {
13
+ JSON_OPTIONS = {
13
14
  array_nl: "\n",
14
15
  object_nl: "\n",
15
16
  indent: ' ',
16
17
  space_before: ' ',
17
18
  space: ' '
18
- }
19
+ }.freeze
19
20
 
20
- def self.output_json item
21
- JSON.generate(item,@@json_options)
21
+ def self.output_json(item)
22
+ JSON.generate(item, JSON_OPTIONS)
22
23
  end
23
24
 
24
25
  # convert a yaml item to json schema
25
- def self.build_value item
26
+ def self.build_value(item)
26
27
  out = {}
27
28
  out['description'] = item['description'] if item['description']
28
- if item['type'] =~/_list$/
29
+ if item['type'] =~ /_list$/
29
30
  handle_string_list item, out
30
31
  else
31
32
  handle_types item, out
@@ -36,283 +37,294 @@ module RSMP
36
37
  end
37
38
 
38
39
  # convert an item which is not a string-list, to json schema
39
- def self.handle_types item, out
40
+ def self.handle_types(item, out)
40
41
  case item['type']
41
- when "string", "base64"
42
- out["type"] = "string"
43
- when "boolean"
44
- out["$ref"] = "../../../core/3.1.2/definitions.json#/boolean"
45
- when "timestamp"
46
- out["$ref"] = "../../../core/3.1.2/definitions.json#/timestamp"
47
- when "integer", "ordinal", "unit", "scale", "long"
48
- out["$ref"] = "../../../core/3.1.2/definitions.json#/integer"
49
- when 'array' # a json array
42
+ when 'boolean'
43
+ out['$ref'] = '../../../core/3.1.2/definitions.json#/boolean'
44
+ when 'timestamp'
45
+ out['$ref'] = '../../../core/3.1.2/definitions.json#/timestamp'
46
+ when 'integer', 'ordinal', 'unit', 'scale', 'long'
47
+ out['$ref'] = '../../../core/3.1.2/definitions.json#/integer'
48
+ when 'array' # a json array
50
49
  build_json_array item['items'], out
51
- else
52
- out["type"] = "string"
50
+ else # string, base64, and any unknown types
51
+ out['type'] = 'string'
53
52
  end
54
53
  end
55
54
 
56
55
  # convert an yaml item with type: array to json schema
57
- def self.build_json_array item, out
58
- required = item.select { |k,v| v['optional'] != true }.keys.sort
56
+ def self.build_json_array(item, out)
57
+ required = item.reject { |_k, v| v['optional'] == true }.keys.sort
59
58
  out.merge!({
60
- "type" => "array",
61
- "items" => {
62
- "type" => "object",
63
- "required" => required,
64
- "unevaluatedProperties" => false # Modern alternative to additionalProperties
65
- }
66
- })
67
- out["items"]["properties"] = {}
68
- item.each_pair do |key,v|
69
- out["items"]["properties"][key] = build_value(v)
59
+ 'type' => 'array',
60
+ 'items' => {
61
+ 'type' => 'object',
62
+ 'required' => required,
63
+ 'unevaluatedProperties' => false # Modern alternative to additionalProperties
64
+ }
65
+ })
66
+ out['items']['properties'] = {}
67
+ item.each_pair do |key, v|
68
+ out['items']['properties'][key] = build_value(v)
70
69
  end
71
70
  out
72
71
  end
73
72
 
74
73
  # JSON Schema 2020-12 allows combining $ref with other properties directly
75
- def self.wrap_refs out
74
+ def self.wrap_refs(out)
76
75
  # No wrapping needed with modern JSON Schema
77
76
  out
78
77
  end
79
78
 
80
79
  # convert a yaml item with list: true to json schema
81
- def self.handle_string_list item, out
80
+ def self.handle_string_list(item, out)
82
81
  case item['type']
83
- when "boolean_list"
84
- out["$ref"] = "../../../core/3.1.2/definitions.json#/boolean_list"
85
- when "integer_list"
86
- out["$ref"] = "../../../core/3.1.2/definitions.json#/integer_list"
87
- when "string_list"
88
- out["$ref"] = "../../../core/3.1.2/definitions.json#/string_list"
82
+ when 'boolean_list'
83
+ out['$ref'] = '../../../core/3.1.2/definitions.json#/boolean_list'
84
+ when 'integer_list'
85
+ out['$ref'] = '../../../core/3.1.2/definitions.json#/integer_list'
86
+ when 'string_list'
87
+ out['$ref'] = '../../../core/3.1.2/definitions.json#/string_list'
89
88
  else
90
89
  raise "Error: List of #{item['type']} is not supported: #{item.inspect}"
91
90
  end
92
91
 
93
- if item["values"]
94
- value_list = item["values"].keys.join('|')
92
+ if item['values']
93
+ value_list = item['values'].keys.join('|')
95
94
  out['pattern'] = /(?-mix:^(#{value_list})(?:,(#{value_list}))*$)/
96
95
  end
97
96
 
98
- puts "Warning: Pattern not support for lists: #{item.inspect}" if item["pattern"]
97
+ puts "Warning: Pattern not support for lists: #{item.inspect}" if item['pattern']
99
98
  end
100
99
 
101
100
  # convert yaml values to jsons schema enum
102
- def self.handle_enum item, out
103
- if item["values"]
104
- out["enum"] = case item["values"]
105
- when Hash
106
- item["values"].each_pair do |k,v|
107
- if v=='' or v==nil
108
- raise "Error: '#{k}' has empty value in #{item}. (When using a hash to specify 'values', the hash values cannot be empty.)"
109
- end
110
- end
111
- item["values"].keys.sort
112
- when Array
113
- item["values"].sort
114
- else
115
- raise "Error: Values must be specified as either a Hash or an Array, got #{item["values"].class}"
116
- end.map do |v|
117
- if v.is_a?(Integer) || v.is_a?(Float)
118
- v.to_s
119
- else
120
- v
121
- end
122
- end
101
+ def self.handle_enum(item, out)
102
+ return unless item['values']
103
+
104
+ out['enum'] = stringify_values(enum_keys(item))
105
+ end
106
+
107
+ def self.enum_keys(item)
108
+ case item['values']
109
+ when Hash
110
+ validate_hash_values! item
111
+ item['values'].keys.sort
112
+ when Array
113
+ item['values'].sort
114
+ else
115
+ raise 'Error: Values must be specified as either a Hash or an Array, ' \
116
+ "got #{item['values'].class}"
123
117
  end
124
118
  end
125
119
 
120
+ def self.validate_hash_values!(item)
121
+ item['values'].each_pair do |k, v|
122
+ next unless ['', nil].include?(v)
123
+
124
+ raise "Error: '#{k}' has empty value in #{item}. " \
125
+ '(When using a hash to specify \'values\', the hash values cannot be empty.)'
126
+ end
127
+ end
128
+
129
+ def self.stringify_values(values)
130
+ values.map { |v| v.is_a?(Integer) || v.is_a?(Float) ? v.to_s : v }
131
+ end
132
+
126
133
  # convert yaml pattern to jsons schema
127
- def self.handle_pattern item, out
128
- out["pattern"] = item["pattern"] if item["pattern"]
134
+ def self.handle_pattern(item, out)
135
+ out['pattern'] = item['pattern'] if item['pattern']
129
136
  end
130
137
 
131
138
  # convert yaml alarm/status/command item to corresponding jsons schema
132
- def self.build_item item, property_key: 'v'
133
- unless item['arguments']
134
- json = {
135
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
136
- "description" => item['description'],
137
- }
138
- return json
139
- end
140
-
139
+ def self.build_item(item, property_key: 'v')
141
140
  arguments = item['arguments']
141
+ return simple_item(item) unless arguments
142
142
 
143
- # For statuses (property_key == 's'), generate a single top-level gate:
144
- # if q is undefined/unknown -> no constraints; else -> per-n branches.
145
- if property_key == 's'
146
- branches = arguments.map do |key, argument|
147
- {
148
- "if" => { "properties" => { "n" => { "const" => key } } },
149
- "then" => { "properties" => { property_key => build_value(argument) } }
150
- }
151
- end
152
-
153
- return {
154
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
155
- "description" => item['description'],
156
- "properties" => {
157
- "n" => { "enum" => arguments.keys.sort }
158
- },
159
- # reference shared guard (relative from statuses folder to tlc/defs)
160
- "if" => { "$ref" => "../../defs/guards.json#/$defs/q_unknown_or_undefined" },
161
- "then" => {},
162
- "else" => { "allOf" => branches }
143
+ property_key == 's' ? build_status_item(item, arguments) : build_default_item(item, arguments, property_key)
144
+ end
145
+
146
+ def self.simple_item(item)
147
+ {
148
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
149
+ 'description' => item['description']
150
+ }
151
+ end
152
+
153
+ def self.build_status_item(item, arguments)
154
+ branches = arguments.map do |key, argument|
155
+ {
156
+ 'if' => { 'properties' => { 'n' => { 'const' => key } } },
157
+ 'then' => { 'properties' => { 's' => build_value(argument) } }
163
158
  }
164
159
  end
160
+ {
161
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
162
+ 'description' => item['description'],
163
+ 'properties' => { 'n' => { 'enum' => arguments.keys.sort } },
164
+ 'if' => { '$ref' => '../../defs/guards.json#/$defs/q_unknown_or_undefined' },
165
+ 'then' => {},
166
+ 'else' => { 'allOf' => branches }
167
+ }
168
+ end
165
169
 
166
- # Default behavior (alarms/commands): keep simple per-n if/then rules without q gating
170
+ def self.build_default_item(item, arguments, property_key)
167
171
  rules = arguments.map do |key, argument|
168
172
  {
169
- "if" => { "properties" => { "n" => { "const" => key } } },
170
- "then" => { "properties" => { property_key => build_value(argument) } }
173
+ 'if' => { 'properties' => { 'n' => { 'const' => key } } },
174
+ 'then' => { 'properties' => { property_key => build_value(argument) } }
171
175
  }
172
176
  end
173
-
174
177
  {
175
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
176
- "description" => item['description'],
177
- "properties" => {
178
- "n" => { "enum" => arguments.keys.sort }
179
- },
180
- "allOf" => rules
178
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
179
+ 'description' => item['description'],
180
+ 'properties' => { 'n' => { 'enum' => arguments.keys.sort } },
181
+ 'allOf' => rules
181
182
  }
182
183
  end
183
184
 
184
185
  # convert alarms to json schema
185
- def self.output_alarms out, items
186
+ def self.output_alarms(out, items)
186
187
  list = items.keys.sort.map do |key|
187
188
  {
188
- "if" => { "required" => ["aCId"], "properties" => { "aCId" => { "const" => key }}},
189
- "then" => { "$ref" => "#{key}.json" }
189
+ 'if' => { 'required' => ['aCId'], 'properties' => { 'aCId' => { 'const' => key } } },
190
+ 'then' => { '$ref' => "#{key}.json" }
190
191
  }
191
192
  end
192
193
  json = {
193
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
194
- "properties" => {
195
- "aCId" => { "enum" => items.keys.sort },
196
- "rvs" => { "items" => { "allOf" => list } }
194
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
195
+ 'properties' => {
196
+ 'aCId' => { 'enum' => items.keys.sort },
197
+ 'rvs' => { 'items' => { 'allOf' => list } }
197
198
  }
198
199
  }
199
200
  out['alarms/alarms.json'] = output_json json
200
- items.each_pair { |key,item| output_alarm out, key, item }
201
+ items.each_pair { |key, item| output_alarm out, key, item }
201
202
  end
202
203
 
203
204
  # convert an alarm to json schema
204
- def self.output_alarm out, key, item
205
+ def self.output_alarm(out, key, item)
205
206
  json = build_item item
206
207
  out["alarms/#{key}.json"] = output_json json
207
208
  end
208
209
 
209
210
  # convert statuses to json schema
210
- def self.output_statuses out, items
211
+ def self.output_statuses(out, items)
211
212
  # ensure shared guard is written (relative to version folder)
212
213
  out['../defs/guards.json'] ||= output_json({
213
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
214
- "$defs" => {
215
- "q_unknown_or_undefined" => {
216
- "allOf" => [
217
- { "required" => ["q"] },
218
- { "properties" => { "q" => { "enum" => ["undefined", "unknown"] } } }
219
- ]
220
- }
221
- }
222
- })
223
-
224
- list = [ { "properties" => { "sCI" => { "enum"=> items.keys.sort }}} ]
214
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
215
+ '$defs' => {
216
+ 'q_unknown_or_undefined' => {
217
+ 'allOf' => [
218
+ { 'required' => ['q'] },
219
+ { 'properties' => { 'q' => { 'enum' => %w[undefined
220
+ unknown] } } }
221
+ ]
222
+ }
223
+ }
224
+ })
225
+
226
+ list = [{ 'properties' => { 'sCI' => { 'enum' => items.keys.sort } } }]
225
227
  items.keys.sort.each do |key|
226
228
  list << {
227
- "if"=> { "required" => ["sCI"], "properties" => { "sCI"=> { "const"=> key }}},
228
- "then" => { "$ref" => "#{key}.json" }
229
+ 'if' => { 'required' => ['sCI'], 'properties' => { 'sCI' => { 'const' => key } } },
230
+ 'then' => { '$ref' => "#{key}.json" }
229
231
  }
230
232
  end
231
- json = {
232
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
233
- "properties" => { "sS" => { "items" => { "allOf" => list }}}
233
+ json = {
234
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
235
+ 'properties' => { 'sS' => { 'items' => { 'allOf' => list } } }
234
236
  }
235
237
  out['statuses/statuses.json'] = output_json json
236
- items.each_pair { |key,item| output_status out, key, item }
238
+ items.each_pair { |key, item| output_status out, key, item }
237
239
  end
238
240
 
239
241
  # convert a status to json schema
240
- def self.output_status out, key, item
242
+ def self.output_status(out, key, item)
241
243
  json = build_item item, property_key: 's'
242
244
  out["statuses/#{key}.json"] = output_json json
243
245
  end
244
246
 
245
247
  # convert commands to json schema
246
- def self.output_commands out, items
247
- list = [ { "properties" => { "cCI" => { "enum"=> items.keys.sort }}} ]
248
+ def self.output_commands(out, items)
249
+ list = [{ 'properties' => { 'cCI' => { 'enum' => items.keys.sort } } }]
248
250
  items.keys.sort.each do |key|
249
251
  list << {
250
- "if" => { "required" => ["cCI"], "properties" => { "cCI"=> { "const"=> key }}},
251
- "then" => { "$ref" => "#{key}.json" }
252
+ 'if' => { 'required' => ['cCI'], 'properties' => { 'cCI' => { 'const' => key } } },
253
+ 'then' => { '$ref' => "#{key}.json" }
252
254
  }
253
255
  end
254
- json = {
255
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
256
- "items" => { "allOf" => list }
256
+ json = {
257
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
258
+ 'items' => { 'allOf' => list }
257
259
  }
258
260
  out['commands/commands.json'] = output_json json
259
261
 
260
- json = {
261
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
262
- "properties" => { "arg" => { "$ref" => "commands.json" }}
262
+ json = {
263
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
264
+ 'properties' => { 'arg' => { '$ref' => 'commands.json' } }
263
265
  }
264
266
  out['commands/command_requests.json'] = output_json json
265
267
 
266
- json = {
267
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
268
- "properties" => { "rvs" => { "$ref" => "commands.json" }}
268
+ json = {
269
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
270
+ 'properties' => { 'rvs' => { '$ref' => 'commands.json' } }
269
271
  }
270
272
  out['commands/command_responses.json'] = output_json json
271
273
 
272
- items.each_pair { |key,item| output_command out, key, item }
274
+ items.each_pair { |key, item| output_command out, key, item }
273
275
  end
274
276
 
275
277
  # convert a command to json schema
276
- def self.output_command out, key, item
278
+ def self.output_command(out, key, item)
277
279
  json = build_item item
278
280
  # Always add the command operation (cO) constraint at the top-level properties
279
- json["properties"] ||= {}
280
- json["properties"]["cO"] = { "const" => item['command'] }
281
-
281
+ json['properties'] ||= {}
282
+ json['properties']['cO'] = { 'const' => item['command'] }
283
+
282
284
  out["commands/#{key}.json"] = output_json json
283
285
  end
284
286
 
285
287
  # output the json schema root
286
- def self.output_root out, meta
288
+ def self.output_root(out, meta)
287
289
  json = {
288
- "$schema" => "https://json-schema.org/draft/2020-12/schema",
289
- "name"=> meta['name'],
290
- "description"=> meta['description'],
291
- "version"=> meta['version'],
292
- "allOf" => [
293
- {
294
- "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "CommandRequest" }}},
295
- "then" => { "$ref" => "commands/command_requests.json" }
296
- },
297
- {
298
- "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "CommandResponse" }}},
299
- "then" => { "$ref" => "commands/command_responses.json" }
300
- },
301
- {
302
- "if" => { "required" => ["type"], "properties" => { "type" => { "enum" => ["StatusRequest","StatusResponse","StatusSubscribe","StatusUnsubscribe","StatusUpdate"] }}},
303
- "then" => { "$ref" => "statuses/statuses.json" }
304
- },
305
- {
306
- "if" => { "required" => ["type"], "properties" => { "type" => { "const" => "Alarm" }}},
307
- "then" => { "$ref" => "alarms/alarms.json" }
308
- }
309
- ]
290
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
291
+ 'name' => meta['name'],
292
+ 'description' => meta['description'],
293
+ 'version' => meta['version'],
294
+ 'allOf' => root_type_rules
310
295
  }
311
- out["rsmp.json"] = output_json json
296
+ out['rsmp.json'] = output_json json
297
+ end
298
+
299
+ def self.root_type_rules
300
+ [
301
+ {
302
+ 'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'CommandRequest' } } },
303
+ 'then' => { '$ref' => 'commands/command_requests.json' }
304
+ },
305
+ {
306
+ 'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'CommandResponse' } } },
307
+ 'then' => { '$ref' => 'commands/command_responses.json' }
308
+ },
309
+ {
310
+ 'if' => {
311
+ 'required' => ['type'],
312
+ 'properties' => {
313
+ 'type' => { 'enum' => %w[StatusRequest StatusResponse StatusSubscribe StatusUnsubscribe
314
+ StatusUpdate] }
315
+ }
316
+ },
317
+ 'then' => { '$ref' => 'statuses/statuses.json' }
318
+ },
319
+ {
320
+ 'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'Alarm' } } },
321
+ 'then' => { '$ref' => 'alarms/alarms.json' }
322
+ }
323
+ ]
312
324
  end
313
325
 
314
326
  # generate the json schema from a string containing yaml
315
- def self.generate sxl
327
+ def self.generate(sxl)
316
328
  out = {}
317
329
  output_root out, sxl[:meta]
318
330
  output_alarms out, sxl[:alarms]
@@ -322,16 +334,15 @@ module RSMP
322
334
  end
323
335
 
324
336
  # convert yaml to json schema and write files to a folder
325
- def self.write sxl, folder
337
+ def self.write(sxl, folder)
326
338
  out = generate sxl
327
- out.each_pair do |relative_path,str|
339
+ out.each_pair do |relative_path, str|
328
340
  path = File.join(folder, relative_path)
329
- FileUtils.mkdir_p File.dirname(path) # create folders if needed
330
- file = File.open(path, 'w+') # w+ means truncate or create new file
331
- file.puts str
341
+ FileUtils.mkdir_p File.dirname(path)
342
+ File.open(path, 'w+') { |file| file.puts str }
332
343
  end
333
344
  end
334
345
  end
335
346
  end
336
347
  end
337
- end
348
+ end
@@ -5,37 +5,35 @@ require 'json'
5
5
  require 'fileutils'
6
6
 
7
7
  module RSMP
8
+ # Namespace for SXL format conversion tools.
8
9
  module Convert
10
+ # Handles importing SXL definitions.
9
11
  module Import
12
+ # Reads SXL definitions from YAML files.
10
13
  module YAML
11
-
12
- def self.read path
14
+ def self.read(path)
13
15
  convert ::YAML.load_file(path)
14
16
  end
15
17
 
16
- def self.parse str
17
- convert ::YAML.load(str)
18
+ def self.parse(str)
19
+ convert ::YAML.safe_load(str)
18
20
  end
19
21
 
20
- def self.convert yaml
21
- sxl = {
22
- meta: {},
23
- alarms: {},
24
- statuses: {},
25
- commands: {}
26
- }
27
-
22
+ def self.convert(yaml)
23
+ sxl = { meta: {}, alarms: {}, statuses: {}, commands: {} }
28
24
  sxl[:meta] = yaml['meta']
25
+ collect_objects yaml['objects'], sxl
26
+ sxl
27
+ end
29
28
 
30
- yaml['objects'].each_pair do |type,object|
31
- object["alarms"].each { |id,item| sxl[:alarms][id] = item } if object["alarms"]
32
- object["statuses"].each { |id,item| sxl[:statuses][id] = item } if object["statuses"]
33
- object["commands"].each { |id,item| sxl[:commands][id] = item } if object["commands"]
29
+ def self.collect_objects(objects, sxl)
30
+ objects.each_pair do |_type, object|
31
+ object['alarms']&.each { |id, item| sxl[:alarms][id] = item }
32
+ object['statuses']&.each { |id, item| sxl[:statuses][id] = item }
33
+ object['commands']&.each { |id, item| sxl[:commands][id] = item }
34
34
  end
35
- sxl
36
35
  end
37
36
  end
38
-
39
37
  end
40
38
  end
41
- end
39
+ end
@@ -1,13 +1,19 @@
1
- module RSMP::Schema
2
- class Error < StandardError
3
- end
1
+ module RSMP
2
+ module Schema
3
+ # Base error class for rsmp_schema.
4
+ class Error < StandardError
5
+ end
4
6
 
5
- class UnknownSchemaError < Error
6
- end
7
+ # Raised when an unknown schema type or version is requested.
8
+ class UnknownSchemaError < Error
9
+ end
7
10
 
8
- class UnknownSchemaTypeError < UnknownSchemaError
9
- end
11
+ # Raised when the requested schema type does not exist.
12
+ class UnknownSchemaTypeError < UnknownSchemaError
13
+ end
10
14
 
11
- class UnknownSchemaVersionError < UnknownSchemaError
15
+ # Raised when the requested schema version does not exist.
16
+ class UnknownSchemaVersionError < UnknownSchemaError
17
+ end
12
18
  end
13
19
  end