Module: RSMP::Convert::Export::JSONSchema

Defined in:
lib/rsmp_schema/convert/export/json_schema.rb

Overview

Converts SXL definitions to JSON Schema files.

Constant Summary collapse

JSON_OPTIONS =
{
  array_nl: "\n",
  object_nl: "\n",
  indent: '  ',
  space_before: ' ',
  space: ' '
}.freeze

Class Method Summary collapse

Class Method Details

.build_default_item(item, arguments, property_key) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 170

def self.build_default_item(item, arguments, property_key)
  rules = arguments.map do |key, argument|
    {
      'if' => { 'properties' => { 'n' => { 'const' => key } } },
      'then' => { 'properties' => { property_key => build_value(argument) } }
    }
  end
  {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    'description' => item['description'],
    'properties' => { 'n' => { 'enum' => arguments.keys.sort } },
    'allOf' => rules
  }
end

.build_item(item, property_key: 'v') ⇒ Object

convert yaml alarm/status/command item to corresponding jsons schema



139
140
141
142
143
144
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 139

def self.build_item(item, property_key: 'v')
  arguments = item['arguments']
  return simple_item(item) unless arguments

  property_key == 's' ? build_status_item(item, arguments) : build_default_item(item, arguments, property_key)
end

.build_json_array(item, out) ⇒ Object

convert an yaml item with type: array to json schema



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 56

def self.build_json_array(item, out)
  required = item.reject { |_k, v| v['optional'] == true }.keys.sort
  out.merge!({
               'type' => 'array',
               'items' => {
                 'type' => 'object',
                 'required' => required,
                 'unevaluatedProperties' => false # Modern alternative to additionalProperties
               }
             })
  out['items']['properties'] = {}
  item.each_pair do |key, v|
    out['items']['properties'][key] = build_value(v)
  end
  out
end

.build_status_item(item, arguments) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 153

def self.build_status_item(item, arguments)
  branches = arguments.map do |key, argument|
    {
      'if' => { 'properties' => { 'n' => { 'const' => key } } },
      'then' => { 'properties' => { 's' => build_value(argument) } }
    }
  end
  {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    'description' => item['description'],
    'properties' => { 'n' => { 'enum' => arguments.keys.sort } },
    'if' => { '$ref' => '../../defs/guards.json#/$defs/q_unknown_or_undefined' },
    'then' => {},
    'else' => { 'allOf' => branches }
  }
end

.build_value(item) ⇒ Object

convert a yaml item to json schema



26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 26

def self.build_value(item)
  out = {}
  out['description'] = item['description'] if item['description']
  if item['type'] =~ /_list$/
    handle_string_list item, out
  else
    handle_types item, out
    handle_enum item, out
    handle_pattern item, out
  end
  wrap_refs out
end

.enum_keys(item) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 107

def self.enum_keys(item)
  case item['values']
  when Hash
    validate_hash_values! item
    item['values'].keys.sort
  when Array
    item['values'].sort
  else
    raise 'Error: Values must be specified as either a Hash or an Array, ' \
          "got #{item['values'].class}"
  end
end

.generate(sxl) ⇒ Object

generate the json schema from a string containing yaml



327
328
329
330
331
332
333
334
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 327

def self.generate(sxl)
  out = {}
  output_root out, sxl[:meta]
  output_alarms out, sxl[:alarms]
  output_statuses out, sxl[:statuses]
  output_commands out, sxl[:commands]
  out
end

.handle_enum(item, out) ⇒ Object

convert yaml values to jsons schema enum



101
102
103
104
105
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 101

def self.handle_enum(item, out)
  return unless item['values']

  out['enum'] = stringify_values(enum_keys(item))
end

.handle_pattern(item, out) ⇒ Object

convert yaml pattern to jsons schema



134
135
136
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 134

def self.handle_pattern(item, out)
  out['pattern'] = item['pattern'] if item['pattern']
end

.handle_string_list(item, out) ⇒ Object

convert a yaml item with list: true to json schema



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 80

def self.handle_string_list(item, out)
  case item['type']
  when 'boolean_list'
    out['$ref'] = '../../../core/3.1.2/definitions.json#/boolean_list'
  when 'integer_list'
    out['$ref'] = '../../../core/3.1.2/definitions.json#/integer_list'
  when 'string_list'
    out['$ref'] = '../../../core/3.1.2/definitions.json#/string_list'
  else
    raise "Error: List of #{item['type']} is not supported: #{item.inspect}"
  end

  if item['values']
    value_list = item['values'].keys.join('|')
    out['pattern'] = /(?-mix:^(#{value_list})(?:,(#{value_list}))*$)/
  end

  puts "Warning: Pattern not support for lists: #{item.inspect}" if item['pattern']
end

.handle_types(item, out) ⇒ Object

convert an item which is not a string-list, to json schema



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 40

def self.handle_types(item, out)
  case item['type']
  when 'boolean'
    out['$ref'] = '../../../core/3.1.2/definitions.json#/boolean'
  when 'timestamp'
    out['$ref'] = '../../../core/3.1.2/definitions.json#/timestamp'
  when 'integer', 'ordinal', 'unit', 'scale', 'long'
    out['$ref'] = '../../../core/3.1.2/definitions.json#/integer'
  when 'array' # a json array
    build_json_array item['items'], out
  else # string, base64, and any unknown types
    out['type'] = 'string'
  end
end

.output_alarm(out, key, item) ⇒ Object

convert an alarm to json schema



205
206
207
208
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 205

def self.output_alarm(out, key, item)
  json = build_item item
  out["alarms/#{key}.json"] = output_json json
end

.output_alarms(out, items) ⇒ Object

convert alarms to json schema



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 186

def self.output_alarms(out, items)
  list = items.keys.sort.map do |key|
    {
      'if' => { 'required' => ['aCId'], 'properties' => { 'aCId' => { 'const' => key } } },
      'then' => { '$ref' => "#{key}.json" }
    }
  end
  json = {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    'properties' => {
      'aCId' => { 'enum' => items.keys.sort },
      'rvs' => { 'items' => { 'allOf' => list } }
    }
  }
  out['alarms/alarms.json'] = output_json json
  items.each_pair { |key, item| output_alarm out, key, item }
end

.output_command(out, key, item) ⇒ Object

convert a command to json schema



278
279
280
281
282
283
284
285
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 278

def self.output_command(out, key, item)
  json = build_item item
  # Always add the command operation (cO) constraint at the top-level properties
  json['properties'] ||= {}
  json['properties']['cO'] = { 'const' => item['command'] }

  out["commands/#{key}.json"] = output_json json
end

.output_commands(out, items) ⇒ Object

convert commands to json schema



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 248

def self.output_commands(out, items)
  list = [{ 'properties' => { 'cCI' => { 'enum' => items.keys.sort } } }]
  items.keys.sort.each do |key|
    list << {
      'if' => { 'required' => ['cCI'], 'properties' => { 'cCI' => { 'const' => key } } },
      'then' => { '$ref' => "#{key}.json" }
    }
  end
  json = {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    'items' => { 'allOf' => list }
  }
  out['commands/commands.json'] = output_json json

  json = {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    'properties' => { 'arg' => { '$ref' => 'commands.json' } }
  }
  out['commands/command_requests.json'] = output_json json

  json = {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    'properties' => { 'rvs' => { '$ref' => 'commands.json' } }
  }
  out['commands/command_responses.json'] = output_json json

  items.each_pair { |key, item| output_command out, key, item }
end

.output_json(item) ⇒ Object



21
22
23
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 21

def self.output_json(item)
  JSON.generate(item, JSON_OPTIONS)
end

.output_root(out, meta) ⇒ Object

output the json schema root



288
289
290
291
292
293
294
295
296
297
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 288

def self.output_root(out, meta)
  json = {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    'name' => meta['name'],
    'description' => meta['description'],
    'version' => meta['version'],
    'allOf' => root_type_rules
  }
  out['rsmp.json'] = output_json json
end

.output_status(out, key, item) ⇒ Object

convert a status to json schema



242
243
244
245
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 242

def self.output_status(out, key, item)
  json = build_item item, property_key: 's'
  out["statuses/#{key}.json"] = output_json json
end

.output_statuses(out, items) ⇒ Object

convert statuses to json schema



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 211

def self.output_statuses(out, items)
  # ensure shared guard is written (relative to version folder)
  out['../defs/guards.json'] ||= output_json({
                                               '$schema' => 'https://json-schema.org/draft/2020-12/schema',
                                               '$defs' => {
                                                 'q_unknown_or_undefined' => {
                                                   'allOf' => [
                                                     { 'required' => ['q'] },
                                                     { 'properties' => { 'q' => { 'enum' => %w[undefined
                                                                                               unknown] } } }
                                                   ]
                                                 }
                                               }
                                             })

  list = [{ 'properties' => { 'sCI' => { 'enum' => items.keys.sort } } }]
  items.keys.sort.each do |key|
    list << {
      'if' => { 'required' => ['sCI'], 'properties' => { 'sCI' => { 'const' => key } } },
      'then' => { '$ref' => "#{key}.json" }
    }
  end
  json = {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    'properties' => { 'sS' => { 'items' => { 'allOf' => list } } }
  }
  out['statuses/statuses.json'] = output_json json
  items.each_pair { |key, item| output_status out, key, item }
end

.root_type_rulesObject



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 299

def self.root_type_rules
  [
    {
      'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'CommandRequest' } } },
      'then' => { '$ref' => 'commands/command_requests.json' }
    },
    {
      'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'CommandResponse' } } },
      'then' => { '$ref' => 'commands/command_responses.json' }
    },
    {
      'if' => {
        'required' => ['type'],
        'properties' => {
          'type' => { 'enum' => %w[StatusRequest StatusResponse StatusSubscribe StatusUnsubscribe
                                   StatusUpdate] }
        }
      },
      'then' => { '$ref' => 'statuses/statuses.json' }
    },
    {
      'if' => { 'required' => ['type'], 'properties' => { 'type' => { 'const' => 'Alarm' } } },
      'then' => { '$ref' => 'alarms/alarms.json' }
    }
  ]
end

.simple_item(item) ⇒ Object



146
147
148
149
150
151
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 146

def self.simple_item(item)
  {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    'description' => item['description']
  }
end

.stringify_values(values) ⇒ Object



129
130
131
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 129

def self.stringify_values(values)
  values.map { |v| v.is_a?(Integer) || v.is_a?(Float) ? v.to_s : v }
end

.validate_hash_values!(item) ⇒ Object



120
121
122
123
124
125
126
127
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 120

def self.validate_hash_values!(item)
  item['values'].each_pair do |k, v|
    next unless ['', nil].include?(v)

    raise "Error: '#{k}' has empty value in #{item}. " \
          '(When using a hash to specify \'values\', the hash values cannot be empty.)'
  end
end

.wrap_refs(out) ⇒ Object

JSON Schema 2020-12 allows combining $ref with other properties directly



74
75
76
77
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 74

def self.wrap_refs(out)
  # No wrapping needed with modern JSON Schema
  out
end

.write(sxl, folder) ⇒ Object

convert yaml to json schema and write files to a folder



337
338
339
340
341
342
343
344
# File 'lib/rsmp_schema/convert/export/json_schema.rb', line 337

def self.write(sxl, folder)
  out = generate sxl
  out.each_pair do |relative_path, str|
    path = File.join(folder, relative_path)
    FileUtils.mkdir_p File.dirname(path)
    File.open(path, 'w+') { |file| file.puts str }
  end
end