Hm is an experimental Ruby gem trying to provide effective, idiomatic, chainable Hash modifications (transformations) DSL.
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}
# }- 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
StructandOpenStruct(which aredig-able in Ruby), though, base#digand#dig!should work on them too; Hm(hash).dig(...)works even on versions of Ruby before 2.3 (when native#digwas introduced);- API is subject to polish and change in future.
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 hashList 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;compactremoves allnilvalues, including nested collections;cleanuprecursively removes all "empty" values (empty strings, hashes, arrays,nils);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.
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
mergeprobably), 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.sunriseANDweather.sunsetpath;[: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 tranformationsIf you find something of the above useful for production use-cases, drop me a note (or GitHub issue; or, even better, PR!).
Hash transformers:
Hash paths:
MIT