{"id":16382459,"url":"https://github.com/andyobtiva/to_collection","last_synced_at":"2026-02-08T08:10:08.374Z","repository":{"id":17772728,"uuid":"82645978","full_name":"AndyObtiva/to_collection","owner":"AndyObtiva","description":"Treat an array of objects and a singular object uniformly as a collection of objects. Especially useful in processing REST Web Service API JSON responses in a functional approach.","archived":false,"fork":false,"pushed_at":"2023-01-18T20:44:24.000Z","size":41,"stargazers_count":0,"open_issues_count":4,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-08-09T21:34:48.982Z","etag":null,"topics":["extension","ruby"],"latest_commit_sha":null,"homepage":"","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/AndyObtiva.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-02-21T06:42:38.000Z","updated_at":"2020-12-09T17:25:20.000Z","dependencies_parsed_at":"2023-02-10T18:00:28.172Z","dependency_job_id":null,"html_url":"https://github.com/AndyObtiva/to_collection","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/AndyObtiva/to_collection","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndyObtiva%2Fto_collection","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndyObtiva%2Fto_collection/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndyObtiva%2Fto_collection/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndyObtiva%2Fto_collection/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AndyObtiva","download_url":"https://codeload.github.com/AndyObtiva/to_collection/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndyObtiva%2Fto_collection/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270390065,"owners_count":24575534,"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-08-14T02:00:10.309Z","response_time":75,"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":["extension","ruby"],"created_at":"2024-10-11T04:05:29.490Z","updated_at":"2026-02-08T08:10:08.315Z","avatar_url":"https://github.com/AndyObtiva.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ToCollection 2.0.1 Ruby Refinement\n[![Gem Version](https://badge.fury.io/rb/to_collection.svg)](http://badge.fury.io/rb/to_collection)\n[![Build Status](https://travis-ci.org/AndyObtiva/to_collection.svg?branch=master)](https://travis-ci.org/AndyObtiva/to_collection)\n[![Coverage Status](https://coveralls.io/repos/github/AndyObtiva/to_collection/badge.svg?branch=master)](https://coveralls.io/github/AndyObtiva/to_collection?branch=master)\n\nTreat an array of objects and a singular object uniformly as a collection of objects.\n\nEspecially useful in processing REST Web Service API JSON responses in a uniform functional approach.\n\n`ToCollection` is a Ruby Refinement, so it may be safely enabled via `using ToCollection` where needed only.\n\nNow also supports [Opal Ruby](https://opalrb.com) by degrading gracefully to monkey-patching.\n\n## Introduction\n\nCanonicalize data to treat uniformly whether it comes in as a single object or an array of objects, dropping `nils` out automatically.\n\nAPI: `object#to_collection(compact=true)` where `compact` is a boolean for whether to compact collection or not. It is true by default.\n\nExample:\n\n```ruby\ncity_counts = {}\npeople_http_request.to_collection.each do |person|\n  city_counts[person[\"city\"]] ||= 0\n  city_counts[person[\"city\"]] += 1\nend\n```\n\nWanna keep `nil` values? No problem! Just pass `false` as an argument:\n\n```ruby\nbad_people_count = 0\ncity_counts = {}\npeople_http_request.to_collection(false).each do |person|\n  if person.nil?\n    bad_people_count += 1\n  else\n    city_counts[person[\"city\"]] ||= 0\n    city_counts[person[\"city\"]] += 1\n  end\nend\n```\n\n## Instructions\n\n### Bundler\n\n- Add `gem 'to_collection', '~\u003e 2.0.1'` to Gemfile\n- Run `bundle`\n- Require `to_collection` ruby gem in code (e.g. via `Bundler.require(:default)` or `require 'bundler/setup'` \u0026 `require 'to_collection'`)\n- Add `using ToCollection` to the top of the Ruby file you would like to refine `Object` in with `#to_collection` method.\n\n### Manual Gem Install\n\n- Run `gem install to_collection -v2.0.1`\n- Add `require 'to_collection'` to code\n- Add `using ToCollection` to the top of the Ruby file you would like to refine `Object` in with `#to_collection` method.\n\n### Opal Ruby\n\n- Follow the first step in [Bundler](#bundler) or [Manual Gem Install](#manual-gem-install) instructions to add the gem.\n- Add Opal.use_gem 'to_collection' to \"config/initializers/assets.rb\" in Rails unless using as part of another gem.\n- Add `require 'to_collection'` to code (this adds the functionality via monkey-patching not refinements in [Opal](https://opalrb.com))\n\n### Note\n\nIf '#to_collection' was already defined on `Object` in a project, requiring the `to_collection` library will print a warning.\n\nIt is still safe to require as it does not overwrite `Object#to_collection` except in Ruby files where `using ToCollection` is added.\n\n## Background\n\nI'm sure you've encountered REST Web Service APIs that operate as follows:\n\nHTTP Request: =\u003e\n```\nGET /people\n```\n\n\u003c= 1 person\nJSON Response:\n```JSON\n{\"first_name\":\"John\",\"last_name\":\"Barber\",\"city\":\"Chicago\"}\n```\n\nHTTP Request: =\u003e\n```\nGET /people\n```\n\n\u003c= 3 people\nJSON Response:\n\n```JSON\n[{\"first_name\":\"John\",\"last_name\":\"Barber\",\"city\":\"Chicago\"}, {\"first_name\":\"Mark\",\"last_name\":\"Jones\",\"city\":\"New York\"}, {\"first_name\":\"Josh\",\"last_name\":\"Beeswax\",\"city\":\"Denver\"}]\n```\n\nHow do you work with the varied JSON responses in Ruby?\n\nOne approach for an app that needs to count people in cities:\n\n```ruby\ncity_counts = {}\njson_response = people_http_request\nif json_response.is_a?(Hash)\n  city_counts[json_response[\"city\"]] ||= 0\n  city_counts[json_response[\"city\"] += 1\nelsif json_response.is_a?(Array)\n  json_response.each do |person|\n    city_counts[person[\"city\"]] ||= 0\n    city_counts[person[\"city\"]] += 1\n  end\nend\n```\n\nNot only is the code above repetitive (unDRY) and complicated, but it also breaks common Ruby and object oriented development standards by relying on explicit type checking instead of duck-typing, polymorphism, or design patterns.\n\nA slightly better version relying on duck-typing would be:\n\n```ruby\ncity_counts = {}\njson_response = people_http_request\nif json_response.respond_to?(:each_pair)\n  city_counts[json_response[\"city\"]] ||= 0\n  city_counts[json_response[\"city\"] += 1\nelsif json_response.respond_to?(:each_index)\n  json_response.each do |person|\n    city_counts[person[\"city\"]] ||= 0\n    city_counts[person[\"city\"]] += 1\n  end\nend\n```\n\nA slightly clearer version relying on design patterns (Strategy) and parametric polymorphism (functional) would be:\n\n```ruby\ncity_counts = {}\ncity_counting_strategies = {\n  Hash: -\u003e { |json_response|\n    city_counts[json_response[\"city\"]] ||= 0\n    city_counts[json_response[\"city\"] += 1\n  },\n  Array: -\u003e { |json_response|\n    json_response.each do |person|\n      city_counts[person[\"city\"]] ||= 0\n      city_counts[person[\"city\"]] += 1\n    end\n  }\n}\njson_response = people_http_request\ncity_counting_strategies[json_response.class].call(json_response)\n```\n\nA more radical version relying on object-oriented polymorphism and Ruby open-classes would be:\n\n```ruby\nHash.class_eval do\n  def process_json_response(\u0026processor)\n    processor.call(self)\n  end\nend\n\nArray.class_eval do\n  def process_json_response(\u0026processor)\n    each(\u0026processor)\n  end\nend\n\ncity_counts = {}\njson_response = people_http_request\njson_response.process_json_response do |person|\n  city_counts[person[\"city\"]] ||= 0\n  city_counts[person[\"city\"]] += 1\nend\n```\n\nThis version is quite elegant, clear, and Ruby idiomatic, but aren't we using a Nuclear device against a fly that sometimes comes as a swarm of flies? I'm sure we can have a much simpler solution, especially in a language like Ruby.\n\nWell, how about this functional solution?\n\n```ruby\ncity_counts = {}\n[people_http_request].flatten.each do |person|\n  city_counts[person[\"city\"]] ||= 0\n  city_counts[person[\"city\"]] += 1\nend\n```\n\nYes, hybrid functional/object-oriented programming to the rescue.\n\nOne may wonder what to do if the response comes in as nil or includes nil values in an array. Well, this approach can scale to handle that too should ignoring nil be the requirement.\n\n```ruby\ncity_counts = {}\n[people_http_request].flatten.compact.each do |person|\n  city_counts[person[\"city\"]] ||= 0\n  city_counts[person[\"city\"]] += 1\nend\n```\n\nCan we generalize this elegant solution beyond counting cities? After all, the key problem with the code on top is it gets quite expensive to maintain in a real-world production app containing many integrations with REST Web Service APIs.\n\nThis functional generalization should work by allowing you to switch json_response variable and process_json_response proc anyway you want:\n\n```ruby\n[json_response].flatten.compact.each(\u0026:process_json_response)\n```\n\nExample:\n\n```ruby\n[cities_json_response].flatten.compact.each(\u0026:group_by_country)\n```\n\nHow about go one step further and bake this into all objects using our previous approach of object-oriented polymorphism and Ruby open-classes? That way, we don't just collapse the difference between dealing with arrays of hashes vs hashes but also arrays of objects vs singular objects by adding. Note the use of flatten(1) below to prevent arrays or arrays from collapsing more than one level.\n\n```ruby\nclass Object\n  def to_collection\n    [self].flatten(1).compact\n  end\nend\n```\n\nExample usage (notice how more readable this is than the explicit version above by hiding flatten and compact):\n\n```ruby\ncity_counts = {}\npeople_http_request.to_collection.each do |person|\n  city_counts[person[\"city\"]] ||= 0\n  city_counts[person[\"city\"]] += 1\nend\n```\n\nA refactored version including optional compacting would be:\n\n```ruby\nclass Object\n  def to_collection(compact=true)\n    collection = [self].flatten(1)\n    compact ? collection.compact : collection\n  end\nend\n```\n\nExample usage of `to_collection(compact)` to count bad person hashes coming as nil:\n\n```ruby\nbad_people_count = 0\ncity_counts = {}\npeople_http_request.to_collection(false).each do |person|\n  if person.nil?\n    bad_people_count += 1\n  else\n    city_counts[person[\"city\"]] ||= 0\n    city_counts[person[\"city\"]] += 1\n  end\nend\n```\n\nOf course, in Ruby 2+, you may use Ruby Refinements, so simply include `Object#to_collection` via this line instead:\n\n```ruby\nusing ToCollection\n```\n\nYou asked for \"Elegant\" didn't you? I hope that was what you were looking for.\n\n## How It Works\n\nA Ruby Refinement is activated via `using ToCollection` adding/overwriting the `#to_collection` method in `Object`, which\nis the ancestor of all Ruby objects.\n\n## Change Log\n\n[CHANGELOG.md](CHANGELOG.md)\n\n## Contributing\n\n* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.\n* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.\n* Fork the project.\n* Start a feature/bugfix branch.\n* `gem install bundler`\n* `bundle`\n* Commit and push until you are happy with your contribution.\n* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. Also, do not upgrade `jeweler`. It is intentionally at an old version that is compatible with running tests in Travis with older verison of Ruby as well as supporting Coveralls, Simplecov, and Code Climate.\n* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.\n\n## Copyright\n\n[MIT](LICENSE.txt)\n\nCopyright (c) 2017-2020 Andy Maleh.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyobtiva%2Fto_collection","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandyobtiva%2Fto_collection","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyobtiva%2Fto_collection/lists"}