class JsonMapping

Stores and applies a mapping to an input ruby Hash

Public Class Methods

new(schema_path, transforms = {}) click to toggle source

@param [String] schema_path The path to the YAML schema @param [Hash] transforms A hash of callable objects (Procs/Lambdas). Keys must match transform names specified in YAML

# File lib/json_mapping.rb, line 21
def initialize(schema_path, transforms = {})
  schema = YAML.safe_load(File.read(schema_path))

  @conditions = (schema['conditions'] || {}).map do |key, value|
    [key, Object.const_get("Conditions::#{value['class']}").new(value['predicate'])]
  end.to_h

  @object_schemas = schema['objects']
  @transforms = transforms || {}
  @logger = Logger.new($stdout)
end

Public Instance Methods

apply(input_hash) click to toggle source

@param [Hash] input_hash A ruby hash onto which the schema should be applied @return [Array] An array of output hashes representing the mapped objects

# File lib/json_mapping.rb, line 36
def apply(input_hash)
  raise FormatError, 'Must define objects under the \'objects\' name' if @object_schemas.nil?

  @object_schemas.map { |schema| parse_object(input_hash, schema) }.reduce(&:merge)
end

Private Instance Methods

apply_conditions(value, conds) click to toggle source

Applies conditions to a value @param [Any] value A value to compare the condition predicates against @param [Array] conds An array of conditions @return [Array] If multiple conditions are satisfied @return [Any] If one condition is satisfied @return [nil] If no conditions are satisfied

# File lib/json_mapping.rb, line 160
def apply_conditions(value, conds)
  output = []
  conds.each do |cond|
    input_val = value
    raise FormatError, "Conditions are a hash: #{cond}" unless cond.is_a? Hash
    raise Conditions::ConditionError, "Unknown condition named #{cond['name']}" unless @conditions.key?(cond['name'])

    condition = @conditions[cond['name']]

    input_val = [input_val] unless input_val.is_a? Array
    input_val = input_val.select do |x|
      x = parse_path(x, cond['field']) if cond.key?('field')
      condition.apply(x)
    end

    next if input_val.empty?

    # Maintain the original data-type of the value (i.e Array or single element)
    input_val = input_val[0] if input_val.length == 1 && !value.is_a?(Array)
    output << (cond['output'] || input_val)
  end

  return (output.length == 1 ? output[0] : output) unless output.empty?
end
map_value(input_hash, schema) click to toggle source

Maps a schema to a single field in the output schema @param [Hash] input_hash The input hash to be mapped @param [Hash] schema The schema which should be applied @return [Hash] A Hash which represents the applied schema

# File lib/json_mapping.rb, line 89
def map_value(input_hash, schema)
  raise FormatError, "Schema should be a hash: #{schema}" unless schema.is_a? Hash

  output = {}
  output[schema['name']] = schema['default']
  return output if schema['path'].nil?

  value = parse_path(input_hash, schema['path'])
  return output if value.nil?

  if schema.key?('conditions')
    value = apply_conditions(value, schema['conditions']) || output[schema['name']]
  end

  if schema.key?('transform') && value != output[schema['name']]
    raise TransformError, "Undefined transform named #{schema['transform']}" unless @transforms.key?(schema['transform'])
    raise TransformError, 'Transforms should respond to the \'call\' method' unless @transforms[schema['transform']].respond_to?(:call)

    value = @transforms[schema['transform']].call(value)
  end

  output[schema['name']] = value
  output
end
parse_object(input_hash, schema) click to toggle source

Maps an object schema to an object in the output @param [Hash] input_hash The hash onto which the schema should be mapped @param [Hash] schema A hash representing the schema which should be applied to the input Raises FormatError if schema is not a Hash or has no key name @return [Hash] The output object

# File lib/json_mapping.rb, line 50
def parse_object(input_hash, schema)
  raise FormatError, "Object should be a hash: #{schema}" unless schema.is_a? Hash
  raise FormatError, "Object needs a name: #{schema}" unless schema.key?('name')

  output = {}
  # Its an object
  if schema.key?('attributes')
    output[schema['name']] = schema['default']

    object_hash = parse_path(input_hash, schema['path'])
    return output if object_hash.nil?

    unless object_hash.is_a? Array
      object_hash = [object_hash]
    end

    attrs = []
    object_hash.each do |obj|
      attributes_hash = {}
      schema['attributes'].each do |attribute|
        attr_hash = parse_object(obj, attribute)
        attributes_hash = attributes_hash.merge(attr_hash)
      end
      attrs << attributes_hash
    end

    output[schema['name']] = attrs.length == 1 && schema['path'][-1] != '*' ? attrs[0] : attrs
  else # Its a value
    output = map_value(input_hash, schema)
  end

  output
end
parse_path(input_hash, path) click to toggle source

@param [Hash] input_hash The input hash @param [String] path The path at which to grab the value @return [Any] The value at the particular path

# File lib/json_mapping.rb, line 118
def parse_path(input_hash, path)
  raise ArgumentError, "path must be string, not #{path.class}" unless path.is_a? String

  parts = path.split('/')
  value = input_hash

  parts.each_with_index do |part, idx|
    if value.nil?
      @logger.warn("Could not find #{path} in #{input_hash}")
      break
    end

    if part == '*'
      raise PathError, "#{parts[0, idx].join('/')} in #{input_hash} is not an array" unless value.is_a? Array

      return value.map { |obj| parse_path(obj, parts[idx + 1..-1].join('/')) }
    else
      next if part.empty?

      if value.is_a? Array
        part = part.to_i

        if part >= value.length
          @logger.warn("Index went out of bounds while parsing #{path} in #{input_hash}")
          value = nil
          break
        end
      end

      value = value[part]
    end
  end
  value
end