{"id":15405679,"url":"https://github.com/zverok/hm","last_synced_at":"2025-07-24T08:11:31.082Z","repository":{"id":56876502,"uuid":"121854475","full_name":"zverok/hm","owner":"zverok","description":"Idiomatic Ruby hash transformations","archived":false,"fork":false,"pushed_at":"2019-06-09T11:25:25.000Z","size":35,"stargazers_count":129,"open_issues_count":0,"forks_count":1,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-06-28T20:54:21.934Z","etag":null,"topics":["functional","hashes","transformations"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zverok.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-02-17T12:42:58.000Z","updated_at":"2024-02-28T14:18:54.000Z","dependencies_parsed_at":"2022-08-20T23:10:27.758Z","dependency_job_id":null,"html_url":"https://github.com/zverok/hm","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/zverok/hm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fhm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fhm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fhm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fhm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zverok","download_url":"https://codeload.github.com/zverok/hm/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zverok%2Fhm/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266813480,"owners_count":23988545,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-24T02:00:09.469Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["functional","hashes","transformations"],"created_at":"2024-10-01T16:18:09.258Z","updated_at":"2025-07-24T08:11:31.060Z","avatar_url":"https://github.com/zverok.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hm? Hm!\n\n[![Gem Version](https://badge.fury.io/rb/hm.svg)](http://badge.fury.io/rb/hm)\n[![Build Status](https://travis-ci.org/zverok/hm.svg?branch=master)](https://travis-ci.org/zverok/hm)\n\n**Hm** is an experimental Ruby gem trying to provide effective, idiomatic, chainable **H**ash\n**m**odifications (transformations) DSL.\n\n## Showcase\n\n```ruby\napi_json = \u003c\u003c-JSON\n{\n  \"coord\": {\"lon\": -0.13, \"lat\": 51.51},\n  \"weather\": [{\"id\": 300, \"main\": \"Drizzle\", \"description\": \"light intensity drizzle\", \"icon\": \"09d\"}],\n  \"base\": \"stations\",\n  \"main\": {\"temp\": 280.32, \"pressure\": 1012, \"humidity\": 81, \"temp_min\": 279.15, \"temp_max\": 281.15},\n  \"visibility\": 10000,\n  \"wind\": {\"speed\": 4.1, \"deg\": 80},\n  \"clouds\": {\"all\": 90},\n  \"dt\": 1485789600,\n  \"sys\": {\"type\": 1, \"id\": 5091, \"message\": 0.0103, \"country\": \"GB\", \"sunrise\": 1485762037, \"sunset\": 1485794875},\n  \"id\": 2643743,\n  \"name\": \"London\",\n  \"cod\": 200\n}\nJSON\n\nweather = JSON.parse(api_json)\npp Hm.new(weather)\n  .transform_keys(\u0026:to_sym)                         # symbolize all keys\n  .except(:id, :cod, %i[sys id], %i[weather * id])  # remove some system values\n  .transform(\n    %i[main *] =\u003e :*,                               # move all {main: {temp: X}} to just {temp: X}\n    %i[sys *] =\u003e :*,                                # same for :sys\n    %i[coord *] =\u003e :coord,                          # gather values for coord.lat, coord.lng into Array in :coord\n    [:weather, 0] =\u003e :weather,                      # move first of :weather Array to just :weather key\n    dt: :timestamp                                  # rename :dt to :timestamp\n  )\n  .cleanup                                          # remove now empty main: {} and sys: {} hashes\n  .transform_values(\n    :timestamp, :sunrise, :sunset,\n    \u0026Time.method(:at))                              # parse timestamps\n  .bury(:weather, :comment, 'BAD')                  # insert some random new key\n  .to_h\n# {\n#  :coord=\u003e[-0.13, 51.51],\n#  :weather=\u003e {:main=\u003e\"Drizzle\", :description=\u003e\"light intensity drizzle\", :icon=\u003e\"09d\", :comment=\u003e\"BAD\"},\n#  :base=\u003e\"stations\",\n#  :visibility=\u003e10000,\n#  :wind=\u003e{:speed=\u003e4.1, :deg=\u003e80},\n#  :clouds=\u003e{:all=\u003e90},\n#  :name=\u003e\"London\",\n#  :temp=\u003e280.32,\n#  :pressure=\u003e1012,\n#  :humidity=\u003e81,\n#  :temp_min=\u003e279.15,\n#  :temp_max=\u003e281.15,\n#  :type=\u003e1,\n#  :message=\u003e0.0103,\n#  :country=\u003e\"GB\",\n#  :sunrise=\u003e2017-01-30 09:40:37 +0200,\n#  :sunset=\u003e2017-01-30 18:47:55 +0200,\n#  :timestamp=\u003e2017-01-30 17:20:00 +0200}\n# }\n```\n\n## Features/problems\n\n* Small, no-dependencies, no-monkey patching, just \"plug and play\";\n* Idiomatic, terse, chainable;\n* Very new and experimental, works on the cases I've extracted from different production problems and\n  invented on the road, but may not work for yours;\n* Most of the methods work on Arrays and Hashes, but not on `Struct` and `OpenStruct` (which are\n  `dig`-able in Ruby), though, base `#dig` and `#dig!` should work on them too;\n* `Hm(hash).dig(...)` works even on versions of Ruby before 2.3 (when native `#dig` was introduced);\n* API is subject to polish and change in future.\n\n## Usage\n\nInstall it with `gem install hm` or adding `gem 'hm'` in your `Gemfile`.\n\nOne of the most important concepts of `Hm` is \"path\" through the structure. It is the same list of\nkeys Ruby's native `#dig()` supports, with one, yet powerful, addition: `:*` stands for `each` (works\nwith any `Enumerable` that is met at the structure at this point):\n\n```ruby\norder = {\n  date: Date.today,\n  items: [\n    {title: 'Beer', price: 10.0},\n    {title: 'Beef', price: 5.0},\n    {title: 'Potato', price: 7.8}\n  ]\n}\nHm(order).dig(:items, :*, :price) # =\u003e [10.0, 5.0, 7.8]\n```\n\nOn top of that, `Hm` provides a set of chainable transformations, which can be used this way:\n\n```ruby\nHm(some_hash)\n  .transformation(...)\n  .transformation(...)\n  .transformation(...)\n  .to_h # =\u003e return the processed hash\n```\n\nList of currently available transformations:\n\n* `bury(:key1, :key2, :key3, value)` — opposite to `dig`, stores value in a nested structure;\n* `transform([:path, :to, :key] =\u003e [:other, :path], [:multiple, :*, :values] =\u003e [:other, :*])` —\n  powerful key renaming, with wildcards support;\n* `transform_keys(path, path, path) { |key| ... }` — works with nested hashes (so you can just\n  `transform_keys(\u0026:to_sym)` to deep symbolize keys), and is able to limit processing to only\n  specified pathes, like `transform_keys([:order, :items, :*, :*], \u0026:capitalize)`\n* `transform_values(path, path, path) { |key| ... }`\n* `update` — same as `transform`, but copies source key to target ones, instead of moving;\n* `slice(:key1, :key2, [:path, :to, :*, :key3])` — extracts only list of specified key pathes;\n* `except(:key1, :key2, [:path, :to, :*, :key3])` — removes list of specified key pathes;\n* `compact` removes all `nil` values, including nested collections;\n* `cleanup` recursively removes all \"empty\" values (empty strings, hashes, arrays, `nil`s);\n* `select(path, path) { |val| ... }` — selects only parts of hash that match specified pathes and\n  specified block;\n* `reject(path, path) { |val| ... }` — drops parts of hash that match specified pathes and\n  specified block;\n* `reduce([:path, :to, :*, :values] =\u003e [:path, :to, :result]) { |memo, val| ... }` — reduce several\n  values into one, like `reduce(%i[items * price] =\u003e :total, \u0026:+)`.\n\nLook at [API docs](http://www.rubydoc.info/gems/hm) for details about each method.\n\n## Further goals\n\nCurrently, I am planning to just use existing one in several projects and see how it will go. The\nideas to where it can be developed further exist, though:\n\n* Just add more useful methods (like `merge` probably), and make their addition modular;\n* There is a temptation for more powerful \"dig path language\", I am looking for a real non-imaginary\n   cases for those, theoretically pretty enchancements:\n  * `:**` for arbitrary depth;\n  * `/foo/` and `(0..1)` for selecting key ranges;\n  * `[:weather, [:sunrise, :sunset]]` for selecting `weather.sunrise` AND `weather.sunset` path;\n  * `[:items, {title: 'Potato'}]` for selecting whole hashes from `:items`, which have `title: 'Potato'`\n    in them.\n* `Hm()` idiom for storing necessary transformations in constants:\n\n```ruby\nWEATHER_TRANSFORM = Hm()\n  .tranform(%w[temp min] =\u003e :temp_min, %w[temp max] =\u003e :temp_max)\n  .transform_values(:dt, \u0026Time.method(:at))\n\n# ...later...\nweathers.map(\u0026WEATHER_TRANSFORM)\n```\n* \"Inline expectations framework\":\n\n```ruby\nHm(api_response)\n  .expect(:results, 0, :id) # raises if api_response[:results][0][:id] is absent\n  .transform(something, something) # continue with our tranformations\n```\n\nIf you find something of the above useful for production use-cases, drop me a note (or GitHub issue;\nor, even better, PR!).\n\n## Prior art\n\nHash transformers:\n\n* https://github.com/solnic/transproc\n* https://github.com/deseretbook/hashformer\n\nHash paths:\n\n* https://github.com/nickcharlton/keypath-ruby\n* https://github.com/maiha/hash-path\n\n## Author\n\n[Victor Shepelev aka @zverok](https://zverok.github.io)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Fhm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzverok%2Fhm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzverok%2Fhm/lists"}