Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 58 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## DESCRIPTION:

Maps values from hashes with different structures and/or key names. Ideal for normalizing arbitrary data to be consumed by your applications, or to prepare your data for different display formats (ie. json).

Tiny module that allows you to easily adapt from one hash structure to another with a simple declarative DSL.

## FEATURES/PROBLEMS:
Expand Down Expand Up @@ -61,7 +61,7 @@ You can use HashMapper in your own little hash-like objects:
class NiceHash
include Enumerable
extend HashMapper

map from('/names/first'), to('/first_name')
map from('/names/last'), to('/last_name')

Expand Down Expand Up @@ -89,7 +89,7 @@ end

#### Coercing values

You want to make sure an incoming value gets converted to a certain type, so
You want to make sure an incoming value gets converted to a certain type, so

```ruby
{'one' => '1', 'two' => '2'}
Expand Down Expand Up @@ -152,6 +152,51 @@ map from('/names'), to('/user') do |names|
end
```

#### Keeping unmapped keys as is

You want unmapped keys from the input to be transmitted to the output, use the `keep_unmapped_keys` option on the `normalize` or `denormalize` methods.

Usage exemple:
```ruby
class ManyLevels
extend HashMapper
map from('/name'), to('/tag_attributes/name')
map from('/properties/type'), to('/tag_attributes/type')
map from('/tagid'), to('/tag_id')
map from('/properties/egg'), to('/chicken')
end

input = {
{
:name => 'ismael',
:tag => ["Ruby"],
:tagid => 1,
:properties => {
:type => 'BLAH',
:egg => 33,
:thing => "thingy"
}
}

ManyLevels.normalize(input, keep_unmapped_keys: true)

# outputs:
{
:tag_id => 1,
:tag => ["Ruby"],
:chicken => 33,
:properties => {
:thing => "thingy"
},
:tag_attributes => {
:name => 'ismael',
:type => 'BLAH'
}
}
```

Keys `:tag` and `:properties/:thing` are kept without even being declared in the Mapper.

### Mapping in reverse

Cool, you can map one hash into another, but what if I want the opposite operation?
Expand All @@ -165,9 +210,9 @@ output = NameMapper.normalize(input) # => {:first_name => 'Mark', :last_name =>

NameMapper.denormalize(output) # => input
```

This will work with your block filters and even nested mappers (see below).

### Advanced usage
#### Array access
You want:
Expand Down Expand Up @@ -252,7 +297,7 @@ end

But HashMapper's nested mappers will actually do that for you if a value is an array, so:

```ruby
```ruby
map from('/employees'), to('employees'), using: UserMapper
```
... Will map each employee using UserMapper.
Expand All @@ -268,12 +313,12 @@ They all yield a block with 2 arguments - the hash you are mapping from and the
```ruby
class EggMapper
map from('/raw'), to('/fried')

before_normalize do |input, output|
input['raw'] ||= 'please' # this will give 'raw' a default value
input['raw'] ||= 'please' # this will give 'raw' a default value
input
end

after_denormalize do |input, output|
output.to_a # the denormalized object will now be an array, not a hash!!
end
Expand All @@ -290,12 +335,12 @@ You can pass one extra argument to before and after filters if you need to:
```ruby
class EggMapper
map from('/raw'), to('/fried')

before_normalize do |input, output, opts|
input['raw'] ||= 'please' unless opts[:no_default] # this will give 'raw' a default value
input['raw'] ||= 'please' unless opts[:no_default] # this will give 'raw' a default value
input
end

after_denormalize do |input, output, opts|
output.to_a # the denormalized object will now be an array, not a hash!!
end
Expand All @@ -306,7 +351,7 @@ EggMapper.normalize({}, no_default: true)
EggMapper.denormalize({fried: 4})
```


## REQUIREMENTS:

## TODO:
Expand Down
8 changes: 5 additions & 3 deletions hash_mapper.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Gem::Specification.new do |s|
s.authors = ['Ismael Celis']
s.description = %q{Tiny module that allows you to easily adapt from one hash structure to another with a simple declarative DSL.}
s.email = %q{ismaelct@gmail.com}

s.files = `git ls-files`.split("\n")
s.homepage = %q{http://github.com/ismasan/hash_mapper}
s.rdoc_options = ['--charset=UTF-8']
Expand All @@ -17,13 +17,15 @@ Gem::Specification.new do |s|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ['lib']

if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency("activesupport", ">= 4")
s.add_runtime_dependency("ruby_dig")
else
s.add_dependency("activesupport", ">= 4")
s.add_dependency("ruby_dig")
end

# specify any dependencies here; for example:
s.add_development_dependency 'rspec'
s.add_development_dependency 'rake'
Expand Down
55 changes: 42 additions & 13 deletions lib/hash_mapper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'hash_mapper/version'
require 'ruby_dig'

$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
Expand All @@ -7,6 +8,9 @@ def require_active_support
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/object/deep_dup'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/class/attribute'
end

Expand Down Expand Up @@ -101,6 +105,7 @@ def after_denormalize(&blk)

def perform_hash_mapping(a_hash, meth, opts)
output = {}
a_hash = a_hash.deep_dup

# Before filters
a_hash = self.send(:"before_#{meth}_filters").inject(a_hash) do |memo, filter|
Expand All @@ -109,13 +114,41 @@ def perform_hash_mapping(a_hash, meth, opts)

# Do the mapping
self.maps.each do |m|
m.process_into(output, a_hash, meth)
m.process_into(output, a_hash, meth, opts)
end

# After filters
self.send(:"after_#{meth}_filters").inject(output) do |memo, filter|
output = self.send(:"after_#{meth}_filters").inject(output) do |memo, filter|
filter.call(a_hash, memo, opts)
end

output = merge_hashes(output, a_hash, meth) if opts[:keep_unmapped_keys]

output
end

def merge_hashes(output, dupped_hash, meth)
clean_dup(dupped_hash, meth).deep_merge(output)
end

def clean_dup(dupped_hash, meth)
self.maps.each do |m|
segments = meth == :normalize ? m.path_from.segments.dup : m.path_to.segments.dup
key = segments.pop
digged_hash(dupped_hash, segments).delete(key)
end
clean(dupped_hash)
end

def clean(hash)
return unless hash.is_a?(Hash)
hash.keys.each { |k| clean(hash[k]) }
hash.delete_if { |_, v| v.blank? }
end

def digged_hash(hash, segments)
return hash if segments.empty?
hash.dig(*segments) || {}
end

# Contains PathMaps
Expand All @@ -132,14 +165,14 @@ def initialize(path_from, path_to, options = {})
@default_value = options.fetch(:default, :hash_mapper_no_default)
end

def process_into(output, input, meth = :normalize)
def process_into(output, input, meth = :normalize, opts)
path_1, path_2 = (meth == :normalize ? [path_from, path_to] : [path_to, path_from])
value = get_value_from_input(output, input, path_1, meth)
value = get_value_from_input(output, input, path_1, meth, opts)
set_value_in_output(output, path_2, value)
end
protected

def get_value_from_input(output, input, path, meth)
def get_value_from_input(output, input, path, meth, opts)
value = path.inject(input) do |h,e|
if h.is_a?(Hash)
v = [h[e.to_sym], h[e.to_s]].compact.first
Expand All @@ -149,7 +182,7 @@ def get_value_from_input(output, input, path, meth)
return :hash_mapper_no_value if v.nil?
v
end
delegated_mapper ? delegate_to_nested_mapper(value, meth) : value
delegated_mapper ? delegate_to_nested_mapper(value, meth, opts) : value
end

def set_value_in_output(output, path, value)
Expand All @@ -163,14 +196,14 @@ def set_value_in_output(output, path, value)
add_value_to_hash!(output, path, value)
end

def delegate_to_nested_mapper(value, meth)
def delegate_to_nested_mapper(value, meth, opts)
case value
when Array
value.map {|h| delegated_mapper.send(meth, h)}
value.map {|h| delegated_mapper.send(meth, h, opts)}
when nil
return :hash_mapper_no_value
else
delegated_mapper.send(meth, value)
delegated_mapper.send(meth, value, opts)
end
end

Expand All @@ -190,9 +223,7 @@ def add_value_to_hash!(hash, path, value)
end
end
end

end

end

# contains array of path segments
Expand Down Expand Up @@ -246,7 +277,5 @@ def parse(path)
end.flatten
segments
end

end

end
Loading