# Hashformer: A declarative data transformation DSL for Ruby # Created June 2014 by Mike Bourgeous, DeseretBook.com # Copyright (C)2016 Deseret Book # See LICENSE and README.md for details. require 'classy_hash' require 'hashformer/version' require 'hashformer/generate' require 'hashformer/date' # This module contains the Hashformer methods for transforming Ruby Hash # objects from one form to another. # # See README.md for examples. module Hashformer # Transforms +data+ according to the specification in +xform+. The # transformation specification in +xform+ is a Hash specifying an input key # name (e.g. a String or Symbol), generator, or transforming lambda for each # output key name. If +validate+ is true, then ClassyHash::validate will be # used to validate the input and output data formats against the # :@__in_schema and :@__out_schema keys within +xform+, if specified. # # Nested transformations can be specified by using a Hash as the # transformation value, or by calling Hashformer.transform again inside of a # lambda. # # If a value in +xform+ is a Proc, the Proc will be called with the input # Hash, and the return value of the Proc used as the output value. # # If a key in +xform+ is a Proc, the Proc will be called with the exact # original input value from +xform+ (before calling a lambda, if applicable) # and the input Hash, and the return value of the Proc used as the name of # the output key. # # Example (see the README for more examples): # Hashformer.transform({old_name: 'Name'}, {new_name: :old_name}) # Returns {new_name: 'Name'} # Hashformer.transform({orig: 5}, {opposite: lambda{|i| -i[:orig]}}) # Returns {opposite: -5} def self.transform(data, xform, validate=true) raise 'Must transform a Hash' unless data.is_a?(Hash) raise 'Transformation must be a Hash' unless xform.is_a?(Hash) validate(data, xform[:__in_schema], 'input') if validate out = {} xform.each do |key, value| next if key == :__in_schema || key == :__out_schema key = key.call(value, data) if key.respond_to?(:call) out[key] = self.get_value(data, value) end validate(out, xform[:__out_schema], 'output') if validate out end # Returns a value for the given +key+, method chain, or callable on the given # +input_hash+. Hash keys will be processed with Hashformer.transform for # supporting nested transformations. def self.get_value(input_hash, key) if Hashformer::Generate::Chain::ReceiverMethods === key # Had to special case chains to allow chaining .call key.__chain.call(input_hash) elsif Hashformer::Generate::Constant === key key.value elsif key.respond_to?(:call) key.call(input_hash) elsif key.is_a?(Hash) transform(input_hash, key) else input_hash[key] end end private # Validates the given data against the given schema, at the given step. def self.validate(data, schema, step) return unless schema.is_a?(Hash) begin ClassyHash.validate(data, schema) rescue => e raise "#{step} data failed validation: #{e}" end end end if !Kernel.const_defined?(:HF) HF = Hashformer end