Module: JSONSchemaUtils
- Defined in:
- common/json_schema_utils.rb
Constant Summary
- 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)
-
+ (Object) apply_schema_defaults(hash, schema)
-
+ (Object) drop_empty_elements(obj)
-
+ (Object) drop_unknown_properties(hash, schema, drop_readonly = false)
Drop any keys from ‘hash’ that aren’t defined in the JSON schema.
-
+ (Object) extract_suberrors(errors)
For a given error, find its list of sub errors.
-
+ (Object) fragment_join(fragment, property = nil)
-
+ (Boolean) is_blank?(obj)
-
+ (Object) map_hash_with_schema(record, schema, transformations = [])
Given a hash representing a record tree, map across the hash and this model’s schema in lockstep.
-
+ (Object) parse_schema_messages(messages, validator)
Given a list of error messages produced by JSON schema validation, parse them into a structured format like:.
-
+ (Object) schema_path_lookup(schema, path)
Class Method Details
+ (Object) apply_schema_defaults(hash, schema)
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 |
# File 'common/json_schema_utils.rb', line 360 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 |
+ (Object) drop_empty_elements(obj)
322 323 324 325 326 327 328 329 330 331 332 333 |
# File 'common/json_schema_utils.rb', line 322 def self.drop_empty_elements(obj) if obj.is_a?(Hash) Hash[obj.map do |k, v| v = drop_empty_elements(v) [k, v] if !is_blank?(v) end] elsif obj.is_a?(Array) obj.map {|elt| drop_empty_elements(elt)}.reject {|elt| is_blank?(elt)} else obj end end |
+ (Object) drop_unknown_properties(hash, schema, drop_readonly = false)
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.
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 |
# File 'common/json_schema_utils.rb', line 342 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 |
+ (Object) extract_suberrors(errors)
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 |
+ (Object) fragment_join(fragment, property = nil)
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 |
+ (Boolean) is_blank?(obj)
317 318 319 |
# File 'common/json_schema_utils.rb', line 317 def self.is_blank?(obj) obj.nil? || obj == "" || obj == {} end |
+ (Object) map_hash_with_schema(record, schema, transformations = [])
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.
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 313 |
# File 'common/json_schema_utils.rb', line 241 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 |
+ (Object) parse_schema_messages(messages, validator)
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)” }
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 230 |
# File 'common/json_schema_utils.rb', line 202 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 |
+ (Object) schema_path_lookup(schema, path)
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 |