Module: RSMP::Convert::Export::JSONSchema
- Defined in:
- lib/rsmp/convert/export/json_schema.rb,
lib/rsmp/convert/export/json_schema/index.rb,
lib/rsmp/convert/export/json_schema/items.rb,
lib/rsmp/convert/export/json_schema/values.rb,
lib/rsmp/convert/export/json_schema/outputs.rb
Overview
Converts SXL definitions to JSON Schema files.
Constant Summary collapse
- JSON_OPTIONS =
{ array_nl: "\n", object_nl: "\n", indent: ' ', space_before: ' ', space: ' ', ascii_only: true }.freeze
- DIRECT_JSON_TYPES =
{ 'boolean' => 'boolean', 'integer' => 'integer', 'number' => 'number', 'object' => 'object', 'null' => 'null' }.freeze
- DEFINITION_REFS =
{ 'boolean_as_string' => '../defs/definitions.json#/boolean', 'timestamp' => '../defs/definitions.json#/timestamp', 'integer_as_string' => '../defs/definitions.json#/integer', 'long_as_string' => '../defs/definitions.json#/integer' }.freeze
- GUARDS_JSON =
{ '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$defs' => { 'q_unknown_or_undefined' => { 'allOf' => [ { 'required' => ['q'] }, { 'properties' => { 'q' => { 'enum' => %w[undefined unknown] } } } ] }, 'age_unknown_or_undefined' => { 'allOf' => [ { 'required' => ['age'] }, { 'properties' => { 'age' => { 'enum' => %w[undefined unknown] } } } ] } } }.freeze
Class Method Summary collapse
- .argument_array_descriptor(argument) ⇒ Object
- .argument_object_descriptor(argument) ⇒ Object
- .argument_type_descriptor(argument) ⇒ Object
- .build_default_item(item, arguments, property_key) ⇒ Object
-
.build_item(item, property_key: 'v') ⇒ Object
convert yaml alarm/status/command item to corresponding json schema.
-
.build_json_array(item, out) ⇒ Object
convert an yaml item with type: array to json schema.
- .build_json_array_type(item, out) ⇒ Object
- .build_number_as_string_type(out) ⇒ Object
- .build_status_item(item, arguments) ⇒ Object
-
.build_value(item) ⇒ Object
convert a yaml item to json schema.
- .command_argument_contains_rule(command_code, name) ⇒ Object
- .command_request_arg_schema(items) ⇒ Object
- .command_requests_schema(items) ⇒ Object
- .command_required_argument_rules(items) ⇒ Object
- .command_responses_schema ⇒ Object
- .commands_schema(items) ⇒ Object
-
.definitions_source(sxl) ⇒ Object
Path to definitions.json for the fallback bundled core schema version.
- .enum_keys(item) ⇒ Object
-
.generate(sxl) ⇒ Object
generate the json schema from a string containing yaml.
-
.handle_enum(item, out) ⇒ Object
convert yaml values to json schema enum.
-
.handle_pattern(item, out) ⇒ Object
convert yaml pattern to json schema.
-
.handle_string_list(item, out) ⇒ Object
convert a yaml item with list: true to json schema.
-
.handle_types(item, out) ⇒ Object
convert an item which is not a string-list, to json schema.
- .index_item(item) ⇒ Object
- .index_items(items) ⇒ Object
- .minimum_core_version(sxl) ⇒ Object
-
.output_alarm(out, key, item) ⇒ Object
convert an alarm to json schema.
-
.output_alarms(out, items) ⇒ Object
convert alarms to json schema.
-
.output_command(out, key, item) ⇒ Object
convert a command to json schema.
-
.output_commands(out, items) ⇒ Object
convert commands to json schema.
- .output_json(item) ⇒ Object
-
.output_root(out, meta) ⇒ Object
output the json schema root.
-
.output_status(out, key, item) ⇒ Object
convert a status to json schema.
-
.output_statuses(out, items) ⇒ Object
convert statuses to json schema.
- .output_sxl_index(out, sxl) ⇒ Object
- .required_argument_names(item) ⇒ Object
- .root_type_rules ⇒ Object
- .simple_item(item) ⇒ Object
- .string_type?(item) ⇒ Boolean
- .stringify_values(values) ⇒ Object
- .typed_arguments(arguments) ⇒ Object
- .validate_hash_values!(item) ⇒ Object
-
.wrap_refs(out) ⇒ Object
JSON Schema 2020-12 allows combining $ref with other properties directly.
-
.write(sxl, folder) ⇒ Object
convert yaml to json schema and write files to a folder.
Class Method Details
.argument_array_descriptor(argument) ⇒ Object
49 50 51 52 53 |
# File 'lib/rsmp/convert/export/json_schema/index.rb', line 49 def self.argument_array_descriptor(argument) descriptor = { 'type' => argument['type'] } descriptor['items'] = typed_arguments(argument['items']) if argument['items'].is_a?(Hash) descriptor end |
.argument_object_descriptor(argument) ⇒ Object
55 56 57 58 59 60 |
# File 'lib/rsmp/convert/export/json_schema/index.rb', line 55 def self.argument_object_descriptor(argument) descriptor = { 'type' => argument['type'] } properties = argument['properties'] || argument['items'] descriptor['properties'] = typed_arguments(properties) if properties.is_a?(Hash) descriptor end |
.argument_type_descriptor(argument) ⇒ Object
37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/rsmp/convert/export/json_schema/index.rb', line 37 def self.argument_type_descriptor(argument) type = argument['type'] case type when 'array' argument_array_descriptor(argument) when 'object' argument_object_descriptor(argument) else type end end |
.build_default_item(item, arguments, property_key) ⇒ Object
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/rsmp/convert/export/json_schema/items.rb', line 38 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 = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', 'description' => item['description'], 'properties' => { 'n' => { 'enum' => arguments.keys.sort } }, 'allOf' => rules } return schema unless property_key == 'v' schema.delete 'allOf' schema.merge( 'if' => { '$ref' => '../defs/guards.json#/$defs/age_unknown_or_undefined' }, 'then' => {}, 'else' => { 'allOf' => rules } ) end |
.build_item(item, property_key: 'v') ⇒ Object
convert yaml alarm/status/command item to corresponding json schema
7 8 9 10 11 12 |
# File 'lib/rsmp/convert/export/json_schema/items.rb', line 7 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
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 59 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 } }) out['items']['properties'] = {} item.each_pair do |key, v| out['items']['properties'][key] = build_value(v) end out end |
.build_json_array_type(item, out) ⇒ Object
45 46 47 48 49 50 51 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 45 def self.build_json_array_type(item, out) if item['items'] build_json_array item['items'], out else out['type'] = 'array' end end |
.build_number_as_string_type(out) ⇒ Object
53 54 55 56 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 53 def self.build_number_as_string_type(out) out['type'] = 'string' out['pattern'] = '^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?$' end |
.build_status_item(item, arguments) ⇒ Object
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/rsmp/convert/export/json_schema/items.rb', line 21 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
22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 22 def self.build_value(item) out = {} out['description'] = item['description'] if item['description'] if item['type'] =~ /_list(_as_string)?$/ handle_string_list item, out else handle_types item, out handle_enum item, out handle_pattern item, out end wrap_refs out end |
.command_argument_contains_rule(command_code, name) ⇒ Object
139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 139 def self.command_argument_contains_rule(command_code, name) { 'contains' => { 'required' => %w[cCI n], 'properties' => { 'cCI' => { 'const' => command_code }, 'n' => { 'const' => name } } } } end |
.command_request_arg_schema(items) ⇒ Object
112 113 114 115 116 117 118 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 112 def self.command_request_arg_schema(items) schema = { '$ref' => 'commands.json' } required_rules = command_required_argument_rules(items) return schema if required_rules.empty? { 'allOf' => [schema] + required_rules } end |
.command_requests_schema(items) ⇒ Object
98 99 100 101 102 103 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 98 def self.command_requests_schema(items) { '$schema' => 'https://json-schema.org/draft/2020-12/schema', 'properties' => { 'arg' => command_request_arg_schema(items) } } end |
.command_required_argument_rules(items) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 120 def self.command_required_argument_rules(items) items.keys.sort.filter_map do |key| required = required_argument_names(items[key]) next if required.empty? { 'if' => { 'contains' => { 'required' => ['cCI'], 'properties' => { 'cCI' => { 'const' => key } } } }, 'then' => { 'allOf' => required.map { |name| command_argument_contains_rule(key, name) } } } end end |
.command_responses_schema ⇒ Object
105 106 107 108 109 110 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 105 def self.command_responses_schema { '$schema' => 'https://json-schema.org/draft/2020-12/schema', 'properties' => { 'rvs' => { '$ref' => 'commands.json' } } } end |
.commands_schema(items) ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 84 def self.commands_schema(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 { '$schema' => 'https://json-schema.org/draft/2020-12/schema', 'items' => { 'allOf' => list } } end |
.definitions_source(sxl) ⇒ Object
Path to definitions.json for the fallback bundled core schema version
36 37 38 39 40 41 42 |
# File 'lib/rsmp/convert/export/json_schema.rb', line 36 def self.definitions_source(sxl) version = minimum_core_version(sxl) path = File.("../../../../schemas/core/#{version}/definitions.json", __dir__) raise "Missing core definitions for RSMP #{version}" unless File.exist?(path) path end |
.enum_keys(item) ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 118 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
45 46 47 48 49 50 51 52 53 |
# File 'lib/rsmp/convert/export/json_schema.rb', line 45 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] output_sxl_index out, sxl out end |
.handle_enum(item, out) ⇒ Object
convert yaml values to json schema enum
105 106 107 108 109 110 111 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 105 def self.handle_enum(item, out) return unless item['values'] values = enum_keys(item) values = stringify_values(values) if string_type? item out['enum'] = values end |
.handle_pattern(item, out) ⇒ Object
convert yaml pattern to json schema
145 146 147 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 145 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
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 82 def self.handle_string_list(item, out) case item['type'] when 'boolean_list', 'boolean_list_as_string' out['$ref'] = '../defs/definitions.json#/boolean_list' when 'integer_list', 'integer_list_as_string' out['$ref'] = '../defs/definitions.json#/integer_list' when 'number_list', 'number_list_as_string' out['$ref'] = '../defs/definitions.json#/number_list' when 'string_list', 'string_list_as_string' out['$ref'] = '../defs/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 handle_pattern item, out end |
.handle_types(item, out) ⇒ Object
convert an item which is not a string-list, to json schema
36 37 38 39 40 41 42 43 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 36 def self.handle_types(item, out) type = item['type'] return build_json_array_type(item, out) if type == 'array' return build_number_as_string_type(out) if type == 'number_as_string' return out['$ref'] = DEFINITION_REFS[type] if DEFINITION_REFS.key?(type) out['type'] = DIRECT_JSON_TYPES.fetch(type, 'string') end |
.index_item(item) ⇒ Object
21 22 23 24 25 26 27 28 29 |
# File 'lib/rsmp/convert/export/json_schema/index.rb', line 21 def self.index_item(item) arguments = item['arguments'] || {} entry = {} required = typed_arguments(arguments.reject { |_name, argument| argument['optional'] == true }) optional = typed_arguments(arguments.select { |_name, argument| argument['optional'] == true }) entry['required'] = required unless required.empty? entry['optional'] = optional unless optional.empty? entry end |
.index_items(items) ⇒ Object
15 16 17 18 19 |
# File 'lib/rsmp/convert/export/json_schema/index.rb', line 15 def self.index_items(items) items.keys.sort.to_h do |key| [key, index_item(items[key])] end end |
.minimum_core_version(sxl) ⇒ Object
31 32 33 |
# File 'lib/rsmp/convert/export/json_schema.rb', line 31 def self.minimum_core_version(sxl) sxl.dig(:meta, 'minimum_core_version') || RSMP::Schema.latest_core_version end |
.output_alarm(out, key, item) ⇒ Object
convert an alarm to json schema
44 45 46 47 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 44 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
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 25 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
156 157 158 159 160 161 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 156 def self.output_command(out, key, item) json = build_item item 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
75 76 77 78 79 80 81 82 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 75 def self.output_commands(out, items) out['defs/guards.json'] ||= output_json(GUARDS_JSON) out['commands/commands.json'] = output_json commands_schema(items) out['commands/command_requests.json'] = output_json command_requests_schema(items) out['commands/command_responses.json'] = output_json command_responses_schema items.each_pair { |key, item| output_command out, key, item } end |
.output_json(item) ⇒ Object
27 28 29 |
# File 'lib/rsmp/convert/export/json_schema.rb', line 27 def self.output_json(item) JSON.generate(item, JSON_OPTIONS) end |
.output_root(out, meta) ⇒ Object
output the json schema root
164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 164 def self.output_root(out, ) json = { '$schema' => 'https://json-schema.org/draft/2020-12/schema', 'name' => ['name'], 'description' => ['description'], 'version' => ['version'], 'allOf' => root_type_rules } json['prefix'] = ['prefix'] if ['prefix'] json['minimum_core_version'] = ['minimum_core_version'] if ['minimum_core_version'] out['rsmp.json'] = output_json json end |
.output_status(out, key, item) ⇒ Object
convert a status to json schema
69 70 71 72 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 69 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
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 50 def self.output_statuses(out, items) out['defs/guards.json'] ||= output_json(GUARDS_JSON) 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 |
.output_sxl_index(out, sxl) ⇒ Object
6 7 8 9 10 11 12 13 |
# File 'lib/rsmp/convert/export/json_schema/index.rb', line 6 def self.output_sxl_index(out, sxl) out['sxl_index.json'] = output_json({ 'meta' => sxl[:meta], 'statuses' => index_items(sxl[:statuses]), 'commands' => index_items(sxl[:commands]), 'alarms' => index_items(sxl[:alarms]) }) end |
.required_argument_names(item) ⇒ Object
151 152 153 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 151 def self.required_argument_names(item) (item['arguments'] || {}).reject { |_name, argument| argument['optional'] == true }.keys.sort end |
.root_type_rules ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/rsmp/convert/export/json_schema/outputs.rb', line 177 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
14 15 16 17 18 19 |
# File 'lib/rsmp/convert/export/json_schema/items.rb', line 14 def self.simple_item(item) { '$schema' => 'https://json-schema.org/draft/2020-12/schema', 'description' => item['description'] } end |
.string_type?(item) ⇒ Boolean
113 114 115 116 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 113 def self.string_type?(item) type = item['type'].to_s type == 'string' || type.end_with?('_as_string') || %w[base64 timestamp].include?(type) end |
.stringify_values(values) ⇒ Object
140 141 142 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 140 def self.stringify_values(values) values.map { |v| v.is_a?(Integer) || v.is_a?(Float) ? v.to_s : v } end |
.typed_arguments(arguments) ⇒ Object
31 32 33 34 35 |
# File 'lib/rsmp/convert/export/json_schema/index.rb', line 31 def self.typed_arguments(arguments) arguments.keys.sort.to_h do |name| [name, argument_type_descriptor(arguments[name])] end end |
.validate_hash_values!(item) ⇒ Object
131 132 133 134 135 136 137 138 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 131 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
77 78 79 |
# File 'lib/rsmp/convert/export/json_schema/values.rb', line 77 def self.wrap_refs(out) out end |
.write(sxl, folder) ⇒ Object
convert yaml to json schema and write files to a folder
56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/rsmp/convert/export/json_schema.rb', line 56 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 # Copy definitions.json so each version folder is self-contained defs_dest = File.join(folder, 'defs', 'definitions.json') FileUtils.mkdir_p File.dirname(defs_dest) source = definitions_source(sxl) FileUtils.cp source, defs_dest end |