module JSONSchemaUtils

Constants

SCHEMA_PARSE_RULES

Public Class Methods

apply_schema_defaults(hash, schema) click to toggle source
# 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) click to toggle source
# 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) click to toggle source

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.

# 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) click to toggle source

For a given error, find its list of sub errors.

# 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) click to toggle source
# 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) click to toggle source
# 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 = []) click to toggle source

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.

# 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) click to toggle source

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 => "(attr2 not quite right either)"}

}

# File lib/aspace_client/json_schema_utils.rb, line 198
def self.parse_schema_messages(messages, validator)

  messages = self.extract_suberrors(messages)

  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
  }

  messages.each do |message|

    SCHEMA_PARSE_RULES.each do |rule|
      if (rule[:failed_attribute].nil? || rule[:failed_attribute].include?(message[:failed_attribute])) and
          message[:message] =~ rule[:pattern]
        rule[:do].call(msgs, message, message[:fragment],
                       *message[:message].scan(rule[:pattern]).flatten)

        break
      end
    end

  end

  msgs.delete(:state)
  msgs
end
schema_path_lookup(schema, path) click to toggle source
# 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

Private Class Methods

determine_schema_for(elt, possible_schemas) click to toggle source
# File lib/aspace_client/json_schema_utils.rb, line 390
def self.determine_schema_for(elt, possible_schemas)
  # A number of different types.  Match them up based on the value of the 'jsonmodel_type' property
  schema_types = possible_schemas.map {|schema| schema.is_a?(Hash) ? schema["type"] : schema}

  jsonmodel_type = elt["jsonmodel_type"]

  if !jsonmodel_type
    raise JSONModel::ValidationException.new(:errors => {"record" => ["Can't unambiguously match #{elt.inspect} against schema types: #{schema_types.inspect}. " +
                                                           "Resolve this by adding a 'jsonmodel_type' property to #{elt.inspect}"]})
  end

  next_schema = schema_types.find {|type|
    (type.is_a?(String) && type.include?("JSONModel(:#{jsonmodel_type})")) ||
    (type.is_a?(Hash) && type["jsonmodel_type"] === jsonmodel_type)
  }

  if next_schema.nil?
    raise "Couldn't determine type of '#{elt.inspect}'.  Must be one of: #{schema_types.inspect}"
  end

  next_schema
end
resolve_schema_reference(schema_reference) click to toggle source
# File lib/aspace_client/json_schema_utils.rb, line 380
def self.resolve_schema_reference(schema_reference)
  # This should be a reference to a different JSONModel type.  Resolve it
  # and return its schema.
  ref = JSONModel.parse_jsonmodel_ref(schema_reference)
  raise "Invalid schema given: #{schema_reference}" if !ref

  JSONModel.JSONModel(ref[0]).schema
end