Project

hash_map

0.01
No commit activity in last 3 years
No release in over 3 years
There's a lot of open issues
Nice DSL to convert hash structure to different one.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 0
>= 0

Runtime

~> 0.2.1
 Project Readme

HashMap

Build Status Gem Version Code Climate Coverage Status Issue Count

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.