Hm? Hm!
Hm is an experimental Ruby gem trying to provide effective, idiomatic, chainable Hash modifications (transformations) DSL.
Showcase
api_json = <<-JSON
{
"coord": {"lon": -0.13, "lat": 51.51},
"weather": [{"id": 300, "main": "Drizzle", "description": "light intensity drizzle", "icon": "09d"}],
"base": "stations",
"main": {"temp": 280.32, "pressure": 1012, "humidity": 81, "temp_min": 279.15, "temp_max": 281.15},
"visibility": 10000,
"wind": {"speed": 4.1, "deg": 80},
"clouds": {"all": 90},
"dt": 1485789600,
"sys": {"type": 1, "id": 5091, "message": 0.0103, "country": "GB", "sunrise": 1485762037, "sunset": 1485794875},
"id": 2643743,
"name": "London",
"cod": 200
}
JSON
weather = JSON.parse(api_json)
pp Hm.new(weather)
.transform_keys(&:to_sym) # symbolize all keys
.except(:id, :cod, %i[sys id], %i[weather * id]) # remove some system values
.transform(
%i[main *] => :*, # move all {main: {temp: X}} to just {temp: X}
%i[sys *] => :*, # same for :sys
%i[coord *] => :coord, # gather values for coord.lat, coord.lng into Array in :coord
[:weather, 0] => :weather, # move first of :weather Array to just :weather key
dt: :timestamp # rename :dt to :timestamp
)
.cleanup # remove now empty main: {} and sys: {} hashes
.transform_values(
:timestamp, :sunrise, :sunset,
&Time.method(:at)) # parse timestamps
.bury(:weather, :comment, 'BAD') # insert some random new key
.to_h
# {
# :coord=>[-0.13, 51.51],
# :weather=> {:main=>"Drizzle", :description=>"light intensity drizzle", :icon=>"09d", :comment=>"BAD"},
# :base=>"stations",
# :visibility=>10000,
# :wind=>{:speed=>4.1, :deg=>80},
# :clouds=>{:all=>90},
# :name=>"London",
# :temp=>280.32,
# :pressure=>1012,
# :humidity=>81,
# :temp_min=>279.15,
# :temp_max=>281.15,
# :type=>1,
# :message=>0.0103,
# :country=>"GB",
# :sunrise=>2017-01-30 09:40:37 +0200,
# :sunset=>2017-01-30 18:47:55 +0200,
# :timestamp=>2017-01-30 17:20:00 +0200}
# }
Features/problems
- Small, no-dependencies, no-monkey patching, just "plug and play";
- Idiomatic, terse, chainable;
- Very new and experimental, works on the cases I've extracted from different production problems and invented on the road, but may not work for yours;
- Most of the methods work on Arrays and Hashes, but not on
Struct
andOpenStruct
(which aredig
-able in Ruby), though, base#dig
and#dig!
should work on them too; Hm(hash).dig(...)
works even on versions of Ruby before 2.3 (when native#dig
was introduced);- API is subject to polish and change in future.
Usage
Install it with gem install hm
or adding gem 'hm'
in your Gemfile
.
One of the most important concepts of Hm
is "path" through the structure. It is the same list of
keys Ruby's native #dig()
supports, with one, yet powerful, addition: :*
stands for each
(works
with any Enumerable
that is met at the structure at this point):
order = {
date: Date.today,
items: [
{title: 'Beer', price: 10.0},
{title: 'Beef', price: 5.0},
{title: 'Potato', price: 7.8}
]
}
Hm(order).dig(:items, :*, :price) # => [10.0, 5.0, 7.8]
On top of that, Hm
provides a set of chainable transformations, which can be used this way:
Hm(some_hash)
.transformation(...)
.transformation(...)
.transformation(...)
.to_h # => return the processed hash
List of currently available transformations:
bury(:key1, :key2, :key3, value)
— opposite todig
, stores value in a nested structure;transform([:path, :to, :key] => [:other, :path], [:multiple, :*, :values] => [:other, :*])
— powerful key renaming, with wildcards support;transform_keys(path, path, path) { |key| ... }
— works with nested hashes (so you can justtransform_keys(&:to_sym)
to deep symbolize keys), and is able to limit processing to only specified pathes, liketransform_keys([:order, :items, :*, :*], &:capitalize)
transform_values(path, path, path) { |key| ... }
update
— same astransform
, but copies source key to target ones, instead of moving;slice(:key1, :key2, [:path, :to, :*, :key3])
— extracts only list of specified key pathes;except(:key1, :key2, [:path, :to, :*, :key3])
— removes list of specified key pathes;compact
removes allnil
values, including nested collections;cleanup
recursively removes all "empty" values (empty strings, hashes, arrays,nil
s);select(path, path) { |val| ... }
— selects only parts of hash that match specified pathes and specified block;reject(path, path) { |val| ... }
— drops parts of hash that match specified pathes and specified block;reduce([:path, :to, :*, :values] => [:path, :to, :result]) { |memo, val| ... }
— reduce several values into one, likereduce(%i[items * price] => :total, &:+)
.
Look at API docs for details about each method.
Further goals
Currently, I am planning to just use existing one in several projects and see how it will go. The ideas to where it can be developed further exist, though:
- Just add more useful methods (like
merge
probably), and make their addition modular; - There is a temptation for more powerful "dig path language", I am looking for a real non-imaginary
cases for those, theoretically pretty enchancements:
:**
for arbitrary depth;/foo/
and(0..1)
for selecting key ranges;[:weather, [:sunrise, :sunset]]
for selectingweather.sunrise
ANDweather.sunset
path;[:items, {title: 'Potato'}]
for selecting whole hashes from:items
, which havetitle: 'Potato'
in them.
Hm()
idiom for storing necessary transformations in constants:
WEATHER_TRANSFORM = Hm()
.tranform(%w[temp min] => :temp_min, %w[temp max] => :temp_max)
.transform_values(:dt, &Time.method(:at))
# ...later...
weathers.map(&WEATHER_TRANSFORM)
- "Inline expectations framework":
Hm(api_response)
.expect(:results, 0, :id) # raises if api_response[:results][0][:id] is absent
.transform(something, something) # continue with our tranformations
If you find something of the above useful for production use-cases, drop me a note (or GitHub issue; or, even better, PR!).
Prior art
Hash transformers:
Hash paths:
Author
License
MIT