HashMap
HashMap is a small library that allow you to map hashes with style :). It will remove from your code many of the ugly navigation inside hashes to get your needed hash structure.
Installation
Add this line to your application's Gemfile:
gem 'hash_map'
And then execute:
$ bundle
Or install it yourself as:
$ gem install hash_map
Usage
Your hash:
{
name: 'Artur',
first_surname: 'hello',
second_surname: 'world',
address: {
postal_code: 12345,
country: {
name: 'Spain',
language: 'ES'
}
},
email: 'asdf@sdfs.com',
phone: nil
}
Your beautiful Mapper:
class ProfileMapper < HashMap::Base
property :first_name, from: :name
property :last_name do |input|
"#{input[:first_surname]} #{input[:second_surname]}"
end
property :language, from: [:address, :country, :language]
from_child :address do
property :code, from: :postal_code
from_child :country do
property :country_name, from: :name
end
end
to_child :email do
property :address, from: :email
property :type, default: :work
end
property :telephone, from: :phone
end
Your wanted hash:
ProfileMapper.map(original)
=> {
first_name: "Artur",
last_name: "hello world",
language: "ES",
code: 12345,
country_name: "Spain",
email: {
address: "asdf@sdfs.com",
type: :work
},
telephone: nil
}
IMPORTANT:
- The output is a Fusu::HashWithIndifferentAccess you can access the values with strings or symbols.
- The input is transformed as well, that's why you do not need to use strings.
Enjoy!
Examples:
form_child
:
{
passenger_data: {
traveller_information: {
passenger: {
first_name: 'Juanito'
},
traveller: {
surname: 'Perez'
}
}
}
}
class DeepExtraction < HashMap::Base
from_child :passenger_data, :traveller_information do
property :firstname, from: [:passenger, :first_name]
property :lastname, from: [:traveller, :surname]
end
end
No 'from' key needed:
class Clever < HashMap::Base
property :name # will get value from the key 'name'
property :address
end
Properties:
class Properties < HashMap::Base
properties :name, :address, :house
end
Collections:
You can map collections passing the mapper option, can be another mapper a proc or anything responding to .call
with one argument.
class Thing < HashMap::Base
properties :name, :age
end
class Collections < HashMap::Base
collection :things, mapper: Thing
collection :numbers, mapper: proc { |n| n.to_i }
end
The collection method always treats the value as an Array
and it always returns an Array
. If the value is not an Array
it will be wrapped in a new one. If the value is nil
it always returns []
.
Collections.map({ things: nil})
=> {
things: []
numbers: []
}
Collections.map({ numbers: '1'})
=> {
things: []
numbers: [1]
}
Options
Adding a second argument will make it available with the name options
class UserMapper < HashMap::Base
properties :name, :lastname
property :company_name do
options[:company_name]
end
end
user = {name: :name, lastname: :lastname}
UserMapper.map(user, company_name: :foo)
#=> {"name"=>:name, "lastname"=>:lastname, "company_name"=>:foo}
Inheritance When inheriting from a Mapper child will inherit the properties
class UserMapper < HashMap::Base
properties :name, :lastname
end
class AdminMapper < UserMapper
properties :role, :company
end
original = {
name: 'John',
lastname: 'Doe',
role: 'Admin',
company: 'ACME'
}
UserMapper.map(original)
#=> { name: 'John', lastname: 'Doe' }
AdminMapper.map(original)
#=> { name: 'John', lastname: 'Doe', role: 'Admin', company: 'ACME' }
Methods:
You can create your helpers in the mapper and call them inside the block
class Methods < HashMap::Base
property(:common_names) { names }
property(:date) { |original| parse_date original[:date] }
property(:class_name) { self.class.name } #=> "Methods"
def names
%w(John Morty)
end
def parse_date(date)
date.strftime('%H:%M')
end
end
Blocks:
In from_child block when you want to get the value with a block the value of the child and original will be yielded in this order: child, original
class Blocks < HashMap::Base
from_child :address do
property :street do |address|
address[:street].upcase
end
property :owner do |address, original|
original[:name]
end
from_child :country do
property :country do |country|
country[:code].upcase
end
end
end
property :name do |original|
original[:name]
end
end
hash = {
name: 'name',
address:{
street: 'street',
country:{
code: 'es'
}
}
}
Blocks.map(hash)
# => {"street"=>"STREET", "owner"=>"name", "country"=>"ES", "name"=>"name"}
Middlewares
transforms_output
original = {
"StatusCode" => 200,
"ErrorDescription" => nil,
"Messages" => nil,
"CompanySettings" => {
"CompanyIdentity" => {
"CompanyGuid" => "0A6005FA-161D-4290-BB7D-B21B14313807",
"PseudoCity" => {
"Code" => "PARTQ2447"
}
},
"IsCertifyEnabled" => false,
"IsProfileEnabled" => true,
"PathMobileConfig" => nil
}
}
class TransformsOutput < HashMap::Base
transforms_output HashMap::UnderscoreKeys
from_child 'CompanySettings' do
from_child 'CompanyIdentity' do
property 'CompanyGuid'
end
properties 'IsCertifyEnabled', 'IsProfileEnabled', 'PathMobileConfig'
end
end
TransformsOutput.call(original)
# => {:company_guid=>"0A6005FA-161D-4290-BB7D-B21B14313807", :is_certify_enabled=>false, :is_profile_enabled=>true, :path_mobile_config=>nil}
Transforms input
original = {
"StatusCode" => 200,
"ErrorDescription" => nil,
"Messages" => nil,
"CompanySettings" => {
"CompanyIdentity" => {
"CompanyGuid" => "0A6005FA-161D-4290-BB7D-B21B14313807",
"PseudoCity" => {
"Code" => "PARTQ2447"
}
},
"IsCertifyEnabled" => false,
"IsProfileEnabled" => true,
"PathMobileConfig" => nil
}
}
class TransformsInput < HashMap::Base
transforms_input HashMap::UnderscoreKeys
from_child :company_settings do
from_child :company_identity do
property :company_guid
end
properties :is_certify_enabled, :is_profile_enabled, :path_mobile_config
end
end
TransformsInput.call(original)
# => {:company_guid=>"0A6005FA-161D-4290-BB7D-B21B14313807", :is_certify_enabled=>false, :is_profile_enabled=>true, :path_mobile_config=>nil}
After each
class AfterEach < HashMap::Base
properties :name, :age
after_each HashMap::BlankToNil, HashMap::StringToBoolean
end
blanks = {
name: '',
age: ''
}
booleans = {
name: 'true',
age: 'false'
}
AfterEach.call(blanks)
#=> {"name"=>nil, "age"=>nil}
AfterEach.call(booleans)
#=> {"name"=>true, "age"=>false}
only_provided_keys
, only_provided_call
class RegularMapper < HashMap::Base
properties :name, :lastname, :phone
from_child :address do
to_child :address do
properties :street, :number
end
end
end
class OnlyProvidedKeysMapper < RegularMapper
only_provided_keys
end
input = { name: "john", address: {street: "Batu Mejan" }, phone: nil }
RegularMapper.call(input) # => {"name"=>"john", "lastname"=>nil, "phone"=>nil, "address"=>{"street"=>"Batu Mejan", "number"=>nil}}
OnlyProvidedKeysMapper.call(input) # => {"name"=>"john", phone: nil, "address"=>{"street"=>"Batu Mejan"}}
You can use a only_provided_call
instead of call if you want to achieve the same result:
RegularMapper.only_provided_call(input) # => {"name"=>"john", phone: nil, "address"=>{"street"=>"Batu Mejan"}}
JSON Adapter
class UserMapper < HashMap::Base
from_child :user do
properties :name, :surname
end
end
json = %Q[{"user":{"name":"John","surname":"Doe"}}]
UserMapper.map(json)
# => {"name"=>"John", "surname"=>"Doe"}
Testing
RSpec
hash_mapped
it do
output = { name: :hello }
expect(output).to hash_mapped(:name)
end
from
it do
original = { first_name: :hello }
output = { name: :hello }
expect(output).to hash_mapped(:name).from(original, :first_name)
end
it do
original = { user: { first_name: :hello } }
output = { name: :hello }
expect(output).to hash_mapped(:name).from(original, :user, :first_name)
end
it do
original = { user: { first_name: :hello } }
output = { user: { name: :hello } }
expect(output).to hash_mapped(:user, :name).from(original, :user, :first_name)
end
and_eq
it do
output = { user: { name: :hello } }
expect(output).to hash_mapped(:user, :name).and_eq(:hello)
end
Motivation
I got bored of doing this:
# this is a hash from an API
hash = JSON.parse(response, :symbolize_names => true)
# hash = {
# user: {
# name: 'John',
# last_name: 'Doe',
# telephone: '989898',
# country: {
# code: 'es'
# }
# }
# }
user_hash = hash[:user]
user = User.new
user.name = user_hash[:name]
user.lastname = user_hash[:last_name]
user.phone = Phone.parse(user_hash[:telephone])
user.country = Country.find_by(code: user_hash[:country][:code])
# boring!!!
# and that's a tiny response
solution:
User.create(MyMapper.map(api_response)) # done
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake rspec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hash_map. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.