Module: JSONSchemaUtils
- Defined in:
- lib/aspace_client/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})"] 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
- .drop_empty_elements(obj) ⇒ Object
-
.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
- .is_blank?(obj) ⇒ Boolean
-
.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
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/aspace_client/json_schema_utils.rb', line 356 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 |
.drop_empty_elements(obj) ⇒ Object
318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'lib/aspace_client/json_schema_utils.rb', line 318 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 |
.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.
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/aspace_client/json_schema_utils.rb', line 338 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.
175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/aspace_client/json_schema_utils.rb', line 175 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 'lib/aspace_client/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 |
.is_blank?(obj) ⇒ Boolean
313 314 315 |
# File 'lib/aspace_client/json_schema_utils.rb', line 313 def self.is_blank?(obj) obj.nil? || obj == "" || obj == {} 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.
237 238 239 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 |
# File 'lib/aspace_client/json_schema_utils.rb', line 237 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 => {:attr1 => "(What was wrong with attr1)",
:warnings => => "(attr2 not quite right either)"
}
198 199 200 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 |
# File 'lib/aspace_client/json_schema_utils.rb', line 198 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 'lib/aspace_client/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 |