Giter Club home page Giter Club logo

hm's Introduction

Hm? Hm!

Gem Version Build Status

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 and OpenStruct (which are dig-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 to dig, 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 just transform_keys(&:to_sym) to deep symbolize keys), and is able to limit processing to only specified pathes, like transform_keys([:order, :items, :*, :*], &:capitalize)
  • transform_values(path, path, path) { |key| ... }
  • update — same as transform, 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 all nil values, including nested collections;
  • cleanup recursively 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, like reduce(%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 selecting weather.sunrise AND weather.sunset path;
    • [:items, {title: 'Potato'}] for selecting whole hashes from :items, which have title: '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

Victor Shepelev aka @zverok

License

MIT

hm's People

Contributors

berniechiu avatar zverok avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

berniechiu

hm's Issues

Feature: Recursive #transform_values

Hi, this gem shows a lot of promise!

I find it would be useful to access values that are not hashes or arrays themselves and apply changes.

For example, I have a hash with various namespaces and generally number values (when they are not nested hashes)

hash = {
  a: 1.25,
  b: {
    c: 2.13
  }
}

It would be great to do something like

proc = Proc.new{ |v|
  if v.is_a?(Hash) || v.is_a?(Array)
    v
  else
    v.to_s
  end
}

Hm.new(hash).recursively_transform_values(:*, &proc).to_h
#=> {
  a: "1.25",
  b: {c: "2.13"}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.