Module: RSMP::Schema
- Defined in:
- lib/rsmp/schema.rb,
lib/rsmp/schema_error.rb,
lib/rsmp/schema/message_resolution.rb
Overview
Resolves SXL schemas from message code ownership.
Defined Under Namespace
Classes: AmbiguousMessageCodeError, Error, UnknownMessageCodeError, UnknownSchemaError, UnknownSchemaTypeError, UnknownSchemaVersionError
Constant Summary collapse
- MESSAGE_CODE_EXTRACTORS =
{ 'StatusRequest' => ->() { status_codes() }, 'StatusSubscribe' => ->() { status_codes() }, 'StatusUnsubscribe' => ->() { status_codes() }, 'StatusResponse' => ->() { status_codes() }, 'StatusUpdate' => ->() { status_codes() }, 'CommandRequest' => ->() { request_command_codes() }, 'CommandResponse' => ->() { response_command_codes() }, 'Alarm' => ->() { alarm_codes() } }.freeze
- MESSAGE_CODE_KINDS =
{ 'StatusRequest' => :statuses, 'StatusSubscribe' => :statuses, 'StatusUnsubscribe' => :statuses, 'StatusResponse' => :statuses, 'StatusUpdate' => :statuses, 'CommandRequest' => :commands, 'CommandResponse' => :commands, 'Alarm' => :alarms }.freeze
Class Method Summary collapse
- .alarm_codes(message) ⇒ Object
- .core_message_type?(message) ⇒ Boolean
-
.core_versions ⇒ Object
get array of core schema versions.
-
.earliest_core_version ⇒ Object
get earliest core schema version.
-
.earliest_version(type) ⇒ Object
get earliest schema version for a particular schema type.
- .ensure_schema_type_available(type, force) ⇒ Object
-
.find_schema(type, version, options = {}) ⇒ Object
find schema for a particular schema and version return nil if not found.
-
.find_schema!(type, version, options = {}) ⇒ Object
find schema for a particular schema and version raise error if not found.
-
.find_schemas(type) ⇒ Object
find schemas versions for particular schema type return nil if type not found.
-
.find_schemas!(type) ⇒ Object
find schemas versions for particular schema type raise error if not found.
-
.latest_core_version ⇒ Object
get latesty core schema version.
-
.latest_version(type) ⇒ Object
get latest schema version for a particular schema type.
-
.load_schema_type(type, type_path, force: false) ⇒ Object
load an schema from a folder.
- .load_schema_version(type, schema_path) ⇒ Object
- .matching_sxl_schemas(schemas, kind, codes, options) ⇒ Object
- .message_code_kind(message) ⇒ Object
- .message_code_kind_name(kind) ⇒ Object
- .message_codes(message) ⇒ Object
- .raise_if_ambiguous_sxl_match(codes, matches) ⇒ Object
- .raise_if_no_sxl_match(kind, codes) ⇒ Object
-
.remove_schema_type(type) ⇒ Object
remove a schema type.
- .request_command_codes(message) ⇒ Object
- .resolve_sxl(message, schemas:, **options) ⇒ Object
- .response_command_codes(message) ⇒ Object
-
.sanitize_version(version) ⇒ Object
get major.minor.patch part of a version string, where patch is optional ignore trailing characters, e.g.
-
.schema?(type, version, options = {}) ⇒ Boolean
true if a particular schema type and version found.
-
.schema_types ⇒ Object
get schemas types.
- .schema_version_paths(type_path) ⇒ Object
-
.schemas ⇒ Object
get all schemas, oganized by type and version.
- .setup ⇒ Object
-
.sort_versions(versions) ⇒ Object
sort version strings.
-
.status_catalogue(type, version) ⇒ Object
return a catalogue of statuses for a particular schema type and version returns a hash of { status_code_id_sym => [arg_name_sym, …] } raises an error if the schema type/version is not found, or has no sxl.yaml.
- .status_codes(message) ⇒ Object
- .sxl_catalogue(type, version, kind) ⇒ Object
- .sxl_defines_codes?(type, version, kind, codes, options) ⇒ Boolean
- .sxl_metadata(type, version, options = {}) ⇒ Object
- .sxl_prefix(type, version, options = {}) ⇒ Object
- .sxl_schemas(schemas) ⇒ Object
-
.validate(message, schemas, options = {}) ⇒ Object
validate using core and optional SXL schemas.
- .validate_core(message, schemas, options) ⇒ Object
- .validate_sxls(message, schemas, options) ⇒ Object
-
.validate_using_schema(message, schema) ⇒ Object
validate an rsmp messages using a schema object.
-
.versions(type) ⇒ Object
get array of schema versions for a particular schema type.
Class Method Details
.alarm_codes(message) ⇒ Object
43 44 45 |
# File 'lib/rsmp/schema/message_resolution.rb', line 43 def self.alarm_codes() [['aCId']].compact end |
.core_message_type?(message) ⇒ Boolean
231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/rsmp/schema.rb', line 231 def self.() type = ['type'] %w[ MessageAck MessageNotAck Version ComponentList AggregatedStatus AggregatedStatusRequest Watchdog ].include?(type) end |
.core_versions ⇒ Object
get array of core schema versions
78 79 80 |
# File 'lib/rsmp/schema.rb', line 78 def self.core_versions versions :core end |
.earliest_core_version ⇒ Object
get earliest core schema version
83 84 85 |
# File 'lib/rsmp/schema.rb', line 83 def self.earliest_core_version earliest_version :core end |
.earliest_version(type) ⇒ Object
get earliest schema version for a particular schema type
99 100 101 102 |
# File 'lib/rsmp/schema.rb', line 99 def self.earliest_version(type) schemas = find_schemas!(type).keys sort_versions(schemas).first end |
.ensure_schema_type_available(type, force) ⇒ Object
41 42 43 |
# File 'lib/rsmp/schema.rb', line 41 def self.ensure_schema_type_available(type, force) raise "Schema type #{type} already loaded" if @schemas[type] && force != true end |
.find_schema(type, version, options = {}) ⇒ Object
find schema for a particular schema and version return nil if not found
148 149 150 151 152 153 154 155 156 157 |
# File 'lib/rsmp/schema.rb', line 148 def self.find_schema(type, version, = {}) raise ArgumentError, 'version missing' unless version version = sanitize_version version if [:lenient] if version schemas = find_schemas type return schemas[version] if schemas end nil end |
.find_schema!(type, version, options = {}) ⇒ Object
find schema for a particular schema and version raise error if not found
176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/rsmp/schema.rb', line 176 def self.find_schema!(type, version, = {}) schema = find_schema type, version, raise ArgumentError, 'version missing' unless version version = sanitize_version version if [:lenient] if version schemas = find_schemas! type schema = schemas[version] return schema if schema end raise UnknownSchemaVersionError, "Unknown schema version #{type} #{version}" end |
.find_schemas(type) ⇒ Object
find schemas versions for particular schema type return nil if type not found
131 132 133 134 135 |
# File 'lib/rsmp/schema.rb', line 131 def self.find_schemas(type) raise ArgumentError, 'type missing' unless type @schemas[type.to_sym] end |
.find_schemas!(type) ⇒ Object
find schemas versions for particular schema type raise error if not found
139 140 141 142 143 144 |
# File 'lib/rsmp/schema.rb', line 139 def self.find_schemas!(type) schemas = find_schemas type raise UnknownSchemaTypeError, "Unknown schema type #{type}" unless schemas schemas end |
.latest_core_version ⇒ Object
get latesty core schema version
88 89 90 |
# File 'lib/rsmp/schema.rb', line 88 def self.latest_core_version latest_version :core end |
.latest_version(type) ⇒ Object
get latest schema version for a particular schema type
105 106 107 108 |
# File 'lib/rsmp/schema.rb', line 105 def self.latest_version(type) schemas = find_schemas!(type).keys sort_versions(schemas).last end |
.load_schema_type(type, type_path, force: false) ⇒ Object
load an schema from a folder. schemas are organized by version, and contain json schema files, with the entry point being rsmp.jspon, eg: tlc
1.0.7
rsmp.json
other jon schema files...
1.0.8
...
an error is raised if the schema type already exists, and force is not set to true
31 32 33 34 35 36 37 38 39 |
# File 'lib/rsmp/schema.rb', line 31 def self.load_schema_type(type, type_path, force: false) type = type.to_sym ensure_schema_type_available(type, force) @schemas[type] = {} @schema_paths ||= {} @schema_paths[type] = {} schema_version_paths(type_path).each { |schema_path| load_schema_version(type, schema_path) } end |
.load_schema_version(type, schema_path) ⇒ Object
49 50 51 52 53 54 55 56 |
# File 'lib/rsmp/schema.rb', line 49 def self.load_schema_version(type, schema_path) version = File.basename(schema_path) file_path = File.join(schema_path, 'rsmp.json') return unless File.exist? file_path @schemas[type][version] = JSONSchemer.schema(Pathname.new(file_path)) @schema_paths[type][version] = schema_path end |
.matching_sxl_schemas(schemas, kind, codes, options) ⇒ Object
82 83 84 85 86 87 88 |
# File 'lib/rsmp/schema/message_resolution.rb', line 82 def self.matching_sxl_schemas(schemas, kind, codes, ) sxl_schemas(schemas).select do |type, version| sxl_defines_codes?(type, version, kind, codes, ) rescue UnknownSchemaError false end end |
.message_code_kind(message) ⇒ Object
47 48 49 |
# File 'lib/rsmp/schema/message_resolution.rb', line 47 def self.() MESSAGE_CODE_KINDS[['type']] end |
.message_code_kind_name(kind) ⇒ Object
62 63 64 65 66 67 68 |
# File 'lib/rsmp/schema/message_resolution.rb', line 62 def self.(kind) { statuses: 'status', commands: 'command', alarms: 'alarm' }.fetch(kind) { kind.to_s.delete_suffix('s') } end |
.message_codes(message) ⇒ Object
26 27 28 29 |
# File 'lib/rsmp/schema/message_resolution.rb', line 26 def self.() extractor = MESSAGE_CODE_EXTRACTORS[['type']] extractor ? extractor.call().uniq : [] end |
.raise_if_ambiguous_sxl_match(codes, matches) ⇒ Object
99 100 101 102 |
# File 'lib/rsmp/schema/message_resolution.rb', line 99 def self.raise_if_ambiguous_sxl_match(codes, matches) names = matches.map { |type, version| "#{type} #{version}" }.join(', ') raise AmbiguousMessageCodeError, "Message code(s) #{codes.join(', ')} match multiple accepted SXLs: #{names}" end |
.raise_if_no_sxl_match(kind, codes) ⇒ Object
94 95 96 97 |
# File 'lib/rsmp/schema/message_resolution.rb', line 94 def self.raise_if_no_sxl_match(kind, codes) raise UnknownMessageCodeError, "No accepted SXL defines #{(kind)} code(s) #{codes.join(', ')}" end |
.remove_schema_type(type) ⇒ Object
remove a schema type
59 60 61 62 63 |
# File 'lib/rsmp/schema.rb', line 59 def self.remove_schema_type(type) type = type.to_sym schemas.delete type @schema_paths&.delete type end |
.request_command_codes(message) ⇒ Object
35 36 37 |
# File 'lib/rsmp/schema/message_resolution.rb', line 35 def self.request_command_codes() (['arg'] || []).map { |item| item['cCI'] }.compact end |
.resolve_sxl(message, schemas:, **options) ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/rsmp/schema/message_resolution.rb', line 70 def self.resolve_sxl(, schemas:, **) kind = () codes = () return nil unless kind && codes.any? matches = matching_sxl_schemas(schemas, kind, codes, ) raise_if_no_sxl_match(kind, codes) if matches.empty? raise_if_ambiguous_sxl_match(codes, matches) if matches.size > 1 matches.first end |
.response_command_codes(message) ⇒ Object
39 40 41 |
# File 'lib/rsmp/schema/message_resolution.rb', line 39 def self.response_command_codes() (['rvs'] || []).map { |item| item['cCI'] }.compact end |
.sanitize_version(version) ⇒ Object
get major.minor.patch part of a version string, where patch is optional ignore trailing characters, e.g.
3.1.3.32A => 3.1.3
3.1A3r3 >= 3.1
return nil if string doesn’t match
164 165 166 167 168 169 170 171 172 |
# File 'lib/rsmp/schema.rb', line 164 def self.sanitize_version(version) # match normal semver z.y.z format if (matched = /^\d+\.\d+\.\d+/.match(version)) matched.to_s # match x.y format, and add patch version zero to get z.y.0 elsif (matched = /^\d+\.\d+/.match(version)) "#{matched}.0" end end |
.schema?(type, version, options = {}) ⇒ Boolean
true if a particular schema type and version found
190 191 192 |
# File 'lib/rsmp/schema.rb', line 190 def self.schema?(type, version, = {}) find_schema(type, version, ) != nil end |
.schema_types ⇒ Object
get schemas types
66 67 68 |
# File 'lib/rsmp/schema.rb', line 66 def self.schema_types schemas.keys end |
.schema_version_paths(type_path) ⇒ Object
45 46 47 |
# File 'lib/rsmp/schema.rb', line 45 def self.schema_version_paths(type_path) Dir.glob("#{type_path}/*").select { |path| File.directory? path } end |
.schemas ⇒ Object
get all schemas, oganized by type and version
71 72 73 74 75 |
# File 'lib/rsmp/schema.rb', line 71 def self.schemas raise 'No schemas available, perhaps Schema.setup was never called?' unless @schemas @schemas end |
.setup ⇒ Object
11 12 13 14 15 16 17 18 19 |
# File 'lib/rsmp/schema.rb', line 11 def self.setup @schemas = {} @schema_paths = {} schemas_path = File.(File.join(__dir__, '..', '..', 'schemas')) Dir.glob("#{schemas_path}/*").select { |f| File.directory? f }.each do |type_path| type = File.basename(type_path).to_sym load_schema_type type, type_path end end |
.sort_versions(versions) ⇒ Object
sort version strings
125 126 127 |
# File 'lib/rsmp/schema.rb', line 125 def self.sort_versions(versions) versions.sort_by { |k| Gem::Version.new(k) } end |
.status_catalogue(type, version) ⇒ Object
return a catalogue of statuses for a particular schema type and version returns a hash of { status_code_id_sym => [arg_name_sym, …] } raises an error if the schema type/version is not found, or has no sxl.yaml
215 216 217 218 219 |
# File 'lib/rsmp/schema.rb', line 215 def self.status_catalogue(type, version) sxl_catalogue(type, version, :statuses).transform_keys(&:to_sym).transform_values do |status| (status['arguments'] || {}).keys.map(&:to_sym) end end |
.status_codes(message) ⇒ Object
31 32 33 |
# File 'lib/rsmp/schema/message_resolution.rb', line 31 def self.status_codes() (['sS'] || []).map { |item| item['sCI'] }.compact end |
.sxl_catalogue(type, version, kind) ⇒ Object
221 222 223 224 225 226 227 228 229 |
# File 'lib/rsmp/schema.rb', line 221 def self.sxl_catalogue(type, version, kind) find_schema! type, version schema_path = @schema_paths&.dig(type.to_sym, version) yaml_path = File.join(schema_path, 'sxl.yaml') if schema_path raise "No sxl.yaml for #{type} #{version}" unless yaml_path && File.exist?(yaml_path) sxl = RSMP::Convert::Import::YAML.read(yaml_path) sxl.fetch(kind) end |
.sxl_defines_codes?(type, version, kind, codes, options) ⇒ Boolean
51 52 53 54 55 56 57 58 59 60 |
# File 'lib/rsmp/schema/message_resolution.rb', line 51 def self.sxl_defines_codes?(type, version, kind, codes, ) version = sanitize_version(version.to_s) if [:lenient] catalogue = sxl_catalogue(type, version, kind) prefix = sxl_prefix(type, version, ) codes.all? do |code| unprefixed = prefix && code.start_with?(prefix) ? code[prefix.length..] : code catalogue.key?(code) || catalogue.key?(code.to_sym) || catalogue.key?(unprefixed) || catalogue.key?(unprefixed.to_sym) end end |
.sxl_metadata(type, version, options = {}) ⇒ Object
194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/rsmp/schema.rb', line 194 def self.(type, version, = {}) version = sanitize_version version if [:lenient] find_schema! type, version path = @schema_paths&.dig(type.to_sym, version) return {} unless path yaml_path = File.join(path, 'sxl.yaml') return YAML.load_file(yaml_path).fetch('meta', {}) if File.exist?(yaml_path) json_path = File.join(path, 'rsmp.json') File.exist?(json_path) ? JSON.parse(File.read(json_path)) : {} end |
.sxl_prefix(type, version, options = {}) ⇒ Object
208 209 210 |
# File 'lib/rsmp/schema.rb', line 208 def self.sxl_prefix(type, version, = {}) (type, version, )['prefix'] end |
.sxl_schemas(schemas) ⇒ Object
90 91 92 |
# File 'lib/rsmp/schema/message_resolution.rb', line 90 def self.sxl_schemas(schemas) schemas.reject { |type, _version| type.to_sym == :core } end |
.validate(message, schemas, options = {}) ⇒ Object
validate using core and optional SXL schemas. Core must pass. SXL-defined messages pass if at least one SXL schema passes. returns nil if validation succeeds, otherwise returns an array of errors.
277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/rsmp/schema.rb', line 277 def self.validate(, schemas, = {}) raise ArgumentError, 'message missing' unless raise ArgumentError, 'schemas missing' unless schemas raise ArgumentError, 'schemas must be a Hash' unless schemas.is_a?(Hash) raise ArgumentError, 'schemas cannot be empty' unless schemas.any? errors = validate_core(, schemas, ) errors.concat validate_sxls(, schemas, ) if errors.empty? return nil if errors.empty? errors end |
.validate_core(message, schemas, options) ⇒ Object
244 245 246 247 248 249 250 |
# File 'lib/rsmp/schema.rb', line 244 def self.validate_core(, schemas, ) core_version = schemas[:core] || schemas['core'] raise ArgumentError, 'schemas must include core' unless core_version schema = find_schema! :core, core_version, validate_using_schema(, schema) end |
.validate_sxls(message, schemas, options) ⇒ Object
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/rsmp/schema.rb', line 252 def self.validate_sxls(, schemas, ) sxl_schemas = schemas.reject { |type, _version| type.to_sym == :core } return [] if sxl_schemas.empty? || () resolved = resolve_sxl(, schemas: schemas, **) if resolved type, version = resolved schema = find_schema! type, version, return validate_using_schema(, schema) end all_errors = [] sxl_schemas.each do |type, version| schema = find_schema! type, version, errors = validate_using_schema(, schema) return [] if errors.empty? all_errors.concat errors end all_errors end |
.validate_using_schema(message, schema) ⇒ Object
validate an rsmp messages using a schema object
111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/rsmp/schema.rb', line 111 def self.validate_using_schema(, schema) raise ArgumentError, 'message missing' unless raise ArgumentError, 'schema missing' unless schema if schema.valid? [] else schema.validate().map do |item| [item['data_pointer'], item['type'], item['details']] end end end |
.versions(type) ⇒ Object
get array of schema versions for a particular schema type
93 94 95 96 |
# File 'lib/rsmp/schema.rb', line 93 def self.versions(type) schemas = find_schemas!(type).keys sort_versions(schemas) end |