Module: JSONSchemaUtils
- Defined in:
- common/json_schema_utils.rb
Constant Summary collapse
- SCHEMA_PARSE_RULES =
[ { :failed_attribute => ['Properties', 'IfMissing', 'ArchivesSpaceSubType'], :pattern => /([A-Z]+: )?The property '.*?' did not contain a required property of '(.*?)'.*/, :do => ->(msgs, , path, type, property) { if type && type =~ /ERROR/ msgs[:errors][fragment_join(path, property)] = ["Property is required but was missing"] else msgs[:warnings][fragment_join(path, property)] = ["Property was missing"] end } }, { :failed_attribute => ['ArchivesSpaceType'], :pattern => /The property '#(.*?)' was not a well-formed date/, :do => ->(msgs, , path, property) { msgs[:errors][fragment_join(path)] = ["Not a valid date"] } }, { :failed_attribute => ['Pattern'], :pattern => /The property '#\/.*?' did not match the regex '(.*?)' in schema/, :do => ->(msgs, , path, regexp) { msgs[:errors][fragment_join(path)] = ["Did not match regular expression: #{regexp}"] } }, { :failed_attribute => ['MinLength'], :pattern => /The property '#\/.*?' was not of a minimum string length of ([0-9]+) in schema/, :do => ->(msgs, , path, length) { msgs[:errors][fragment_join(path)] = ["Must be at least #{length} characters"] } }, { :failed_attribute => ['MaxLength'], :pattern => /The property '#\/.*?' was not of a maximum string length of ([0-9]+) in schema/, :do => ->(msgs, , path, length) { msgs[:errors][fragment_join(path)] = ["Must be #{length} characters or fewer"] } }, { :failed_attribute => ['MinItems'], :pattern => /The property '#\/.*?' did not contain a minimum number of items ([0-9]+) in schema/, :do => ->(msgs, , path, items) { msgs[:errors][fragment_join(path)] = ["At least #{items} item(s) is required"] } }, { :failed_attribute => ['Enum'], :pattern => /The property '#\/.*?' value "(.*?)" .*values: (.*) in schema/, :do => ->(msgs, , path, invalid, valid_set) { msgs[:errors][fragment_join(path)] = ["Invalid value '#{invalid}'. Must be one of: #{valid_set}"] } }, { :failed_attribute => ['ArchivesSpaceDynamicEnum'], :pattern => /The property '#\/.*?' value "(.*?)" .*values: (.*) in schema/, :do => ->(msgs, , path, invalid, valid_set) { msgs[:attribute_types][fragment_join(path)] = 'ArchivesSpaceDynamicEnum' msgs[:errors][fragment_join(path)] = ["Invalid value '#{invalid}'. Must be one of: #{valid_set}"] } }, { :failed_attribute => ['ArchivesSpaceReadOnlyDynamicEnum'], :pattern => /The property '#\/.*?' value "(.*?)" .*values: (.*) in schema/, :do => ->(msgs, , path, invalid, valid_set) { msgs[:attribute_types][fragment_join(path)] = 'ArchivesSpaceReadOnlyDynamicEnum' msgs[:errors][fragment_join(path)] = ["Protected read-only list #{path}. Invalid value '#{invalid}'. Must be one of: #{valid_set}"] } }, { :failed_attribute => ['Type', 'ArchivesSpaceType'], :pattern => /The property '#\/.*?' of type (.*?) did not match the following type: (.*?) in schema/, :do => ->(msgs, , path, actual_type, desired_type) { if actual_type !~ /JSONModel/ || [:failed_attribute] == 'ArchivesSpaceType' # We'll skip JSONModels because the specific problem with the # document will have already been listed separately. msgs[:state][fragment_join(path)] ||= [] msgs[:state][fragment_join(path)] << desired_type if msgs[:state][fragment_join(path)].length == 1 msgs[:errors][fragment_join(path)] = ["Must be a #{desired_type} (you provided a #{actual_type})"] # a little better messages for malformed uri if desired_type =~ /uri$/ msgs[:errors][fragment_join(path)].first << " (malformed or invalid uri? check if referenced object exists.)" end else msgs[:errors][fragment_join(path)] = ["Must be one of: #{msgs[:state][fragment_join(path)].join (", ")} (you provided a #{actual_type})"] end end } }, { :failed_attribute => ['custom_validation'], :pattern => /Validation failed for '(.*?)': (.*?) in schema /, :do => ->(msgs, , path, property, msg) { property = (property && !property.empty?) ? property : nil msgs[:errors][fragment_join(path, property)] = [msg] } }, { :failed_attribute => ['custom_validation'], :pattern => /Warning generated for '(.*?)': (.*?) in schema /, :do => ->(msgs, , path, property, msg) { msgs[:warnings][fragment_join(path, property)] = [msg] } }, { :failed_attribute => ['custom_validation'], :pattern => /Validation error code: (.*?) in schema /, :do => ->(msgs, , path, error_code) { msgs[:errors]['coded_errors'] = [error_code] } }, # Catch all { :failed_attribute => nil, :pattern => /^(.*)$/, :do => ->(msgs, , path, msg) { msgs[:errors]['unknown'] = [msg] } } ]
Class Method Summary collapse
-
.apply_schema_defaults(hash, schema) ⇒ Object
-
.blank?(obj) ⇒ Boolean
-
.drop_empty_elements(obj) ⇒ Object
Recursively walk a map and remove any empty strings, empty maps and nils.
-
.drop_unknown_properties(hash, schema, drop_readonly = false) ⇒ Object
Drop any keys from ‘hash’ that aren’t defined in the JSON schema.
-
.extract_suberrors(errors) ⇒ Object
For a given error, find its list of sub errors.
-
.fragment_join(fragment, property = nil) ⇒ Object
-
.map_hash_with_schema(record, schema, transformations = []) ⇒ Object
Given a hash representing a record tree, map across the hash and this model’s schema in lockstep.
-
.parse_schema_messages(messages, validator) ⇒ Object
Given a list of error messages produced by JSON schema validation, parse them into a structured format like:.
-
.schema_path_lookup(schema, path) ⇒ Object
Class Method Details
.apply_schema_defaults(hash, schema) ⇒ Object
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 |
# File 'common/json_schema_utils.rb', line 380 def self.apply_schema_defaults(hash, schema) fn = proc do |hash, schema| result = hash.clone schema["properties"].each do |property, definition| if definition.has_key?("default") && !hash.has_key?(property.to_s) && !hash.has_key?(property.intern) result[property] = definition["default"] elsif definition['type'] == 'array' && !hash.has_key?(property.to_s) && !hash.has_key?(property.intern) # Array values that weren't provided default to empty result[property] = [] end end result end map_hash_with_schema(hash, schema, [fn]) end |
.blank?(obj) ⇒ Boolean
314 315 316 |
# File 'common/json_schema_utils.rb', line 314 def self.blank?(obj) obj.nil? || obj == '' || obj == {} end |
.drop_empty_elements(obj) ⇒ Object
Recursively walk a map and remove any empty strings, empty maps and nils. Recursively collapses elements so that if, for example, an map becomes empty after having its own empty elements removed, it gets removed as well.
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
# File 'common/json_schema_utils.rb', line 321 def self.drop_empty_elements(obj) queue = [obj] to_visit = [] while !queue.empty? obj = queue.shift if obj.is_a?(Hash) || obj.is_a?(Array) (obj.is_a?(Hash) ? obj.values : obj).each do |v| if v.is_a?(Hash) || v.is_a?(Array) queue.push(v) end end end to_visit.unshift(obj) end while !to_visit.empty? obj = to_visit.shift if obj.is_a?(Array) obj.reject! {|elt| blank?(elt)} elsif obj.is_a?(Hash) obj.keys.each do |k| if blank?(obj[k]) obj.delete(k) end end end end obj end |
.drop_unknown_properties(hash, schema, drop_readonly = false) ⇒ Object
Drop any keys from ‘hash’ that aren’t defined in the JSON schema.
If drop_readonly is true, also drop any values where the schema has ‘readonly’ set to true. These values are produced by the system for the client, but are not part of the data model.
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 |
# File 'common/json_schema_utils.rb', line 362 def self.drop_unknown_properties(hash, schema, drop_readonly = false) fn = proc do |hash, schema| result = {} hash.each do |k, v| if schema["properties"].has_key?(k.to_s) && (!drop_readonly || !schema["properties"][k.to_s]["readonly"]) result[k] = v end end result end hash = drop_empty_elements(hash) map_hash_with_schema(hash, schema, [fn]) end |
.extract_suberrors(errors) ⇒ Object
For a given error, find its list of sub errors.
179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'common/json_schema_utils.rb', line 179 def self.extract_suberrors(errors) errors = Array[errors].flatten result = errors.map do |error| if !error[:errors] error else self.extract_suberrors(error[:errors]) end end result.flatten end |
.fragment_join(fragment, property = nil) ⇒ Object
3 4 5 6 7 8 9 10 11 12 |
# File 'common/json_schema_utils.rb', line 3 def self.fragment_join(fragment, property = nil) fragment = fragment.gsub(/^#\//, "") property = property.gsub(/^#\//, "") if property if property && fragment != "" && fragment !~ /\/$/ fragment = "#{fragment}/" end "#{fragment}#{property}" end |
.map_hash_with_schema(record, schema, transformations = []) ⇒ Object
Given a hash representing a record tree, map across the hash and this model’s schema in lockstep.
Each proc in the ‘transformations’ array is called with the current node in the record tree as its first argument, and the part of the schema that corresponds to it. Whatever the proc returns is used to replace the node in the record tree.
240 241 242 243 244 245 246 247 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 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'common/json_schema_utils.rb', line 240 def self.map_hash_with_schema(record, schema, transformations = []) return record if not record.is_a?(Hash) if schema.is_a?(String) schema = resolve_schema_reference(schema) end # Sometimes a schema won't specify anything other than the required type # (like {'type' => 'object'}). If there's nothing more to check, we're # done. return record if !schema.has_key?("properties") # Apply transformations to the current level of the tree transformations.each do |transform| record = transform.call(record, schema) end # Now figure out how to traverse the remainder of the tree... result = {} record.each do |k, v| k = k.to_s properties = schema['properties'] if properties.has_key?(k) && (properties[k]["type"] == "object") result[k] = self.map_hash_with_schema(v, properties[k], transformations) elsif v.is_a?(Array) && properties.has_key?(k) && (properties[k]["type"] == "array") # Arrays are tricky because they can either consist of a single type, or # a number of different types. if properties[k]["items"]["type"].is_a?(Array) result[k] = v.map {|elt| if elt.is_a?(Hash) next_schema = determine_schema_for(elt, properties[k]["items"]["type"]) self.map_hash_with_schema(elt, next_schema, transformations) elsif elt.is_a?(Array) raise "Nested arrays aren't supported here (yet)" else elt end } # The array contains a single type of object elsif properties[k]["items"]["type"] === "object" result[k] = v.map {|elt| self.map_hash_with_schema(elt, properties[k]["items"], transformations)} else # Just one valid type result[k] = v.map {|elt| self.map_hash_with_schema(elt, properties[k]["items"]["type"], transformations)} end elsif (v.is_a?(Hash) || v.is_a?(Array)) && (properties.has_key?(k) && properties[k]["type"].is_a?(Array)) # Multiple possible types for this single value results = (v.is_a?(Array) ? v : [v]).map {|elt| next_schema = determine_schema_for(elt, properties[k]["type"]) self.map_hash_with_schema(elt, next_schema, transformations) } result[k] = v.is_a?(Array) ? results : results[0] elsif properties.has_key?(k) && JSONModel.parse_jsonmodel_ref(properties[k]["type"]) result[k] = self.map_hash_with_schema(v, properties[k]["type"], transformations) else result[k] = v end end result end |
.parse_schema_messages(messages, validator) ⇒ Object
Given a list of error messages produced by JSON schema validation, parse them into a structured format like:
{ :errors => => “(What was wrong with attr1)”, :warnings => => “(attr2 not quite right either)” }
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'common/json_schema_utils.rb', line 201 def self.(, validator) = self.extract_suberrors() msgs = { :errors => {}, :warnings => {}, # to lookup e.g., msgs[:attribute_types]['extents/0/extent_type'] => 'ArchivesSpaceDynamicEnum' :attribute_types => {}, :state => {} # give the parse rules somewhere to store useful state for a run } .each do || SCHEMA_PARSE_RULES.each do |rule| if (rule[:failed_attribute].nil? || rule[:failed_attribute].include?([:failed_attribute])) and [:message] =~ rule[:pattern] rule[:do].call(msgs, , [:fragment], *[:message].scan(rule[:pattern]).flatten) break end end end msgs.delete(:state) msgs end |
.schema_path_lookup(schema, path) ⇒ Object
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'common/json_schema_utils.rb', line 15 def self.schema_path_lookup(schema, path) if path.is_a? String return self.schema_path_lookup(schema, path.split("/")) end if schema.has_key?('properties') schema = schema['properties'] end if path.length == 1 schema[path.first] else if schema[path.first] self.schema_path_lookup(schema[path.first], path.drop(1)) else nil end end end |