{"id":28645966,"url":"https://github.com/boblail/pluck_map","last_synced_at":"2025-07-20T05:33:22.844Z","repository":{"id":56888298,"uuid":"45745083","full_name":"boblail/pluck_map","owner":"boblail","description":"A DSL for presenting ActiveRecord::Relations without instantiating ActiveRecord models","archived":false,"fork":false,"pushed_at":"2020-10-14T03:24:36.000Z","size":103,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-13T01:52:04.849Z","etag":null,"topics":["activerecord","dsl","presenter","ruby"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/boblail.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-11-07T16:49:47.000Z","updated_at":"2020-10-14T03:24:23.000Z","dependencies_parsed_at":"2022-08-21T00:50:52.223Z","dependency_job_id":null,"html_url":"https://github.com/boblail/pluck_map","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/boblail/pluck_map","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boblail%2Fpluck_map","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boblail%2Fpluck_map/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boblail%2Fpluck_map/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boblail%2Fpluck_map/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/boblail","download_url":"https://codeload.github.com/boblail/pluck_map/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boblail%2Fpluck_map/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265224108,"owners_count":23730340,"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","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":["activerecord","dsl","presenter","ruby"],"created_at":"2025-06-13T01:42:47.020Z","updated_at":"2025-07-20T05:33:22.830Z","avatar_url":"https://github.com/boblail.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PluckMap::Presenter\n\n[![Gem Version](https://badge.fury.io/rb/pluck_map.svg)](https://rubygems.org/gems/pluck_map)\n[![Build Status](https://travis-ci.org/boblail/pluck_map.svg)](https://travis-ci.org/boblail/pluck_map)\n\nThis library provides a DSL for presenting ActiveRecord::Relations without instantiating ActiveRecord models. It is useful when a Rails controller action does little more than fetch several records from the database and present them in some other data format (like JSON or CSV).\n\n\n### Table of Contents\n\n- [Why PluckMap?](#why-pluckmap)\n- Usage\n  - [Defining attributes to present](#defining-attributes-to-present)\n  - [Relationships](#relationships)\n  - [Presenting Records](#presenting-records)\n- [Installation](#installation)\n- [Requirements](#requirements)\n- [Development \u0026 Contributing](#development)\n\n\n## Why PluckMap?\n\nSuppose you have an action like this:\n\n```ruby\n  def index\n    messages = Message.created_by(current_user).after(3.weeks.ago)\n\n    render json: messages.map { |message|\n      { id: message.id,\n        postedAt: message.created_at,\n        text: message.text } }\n  end\n```\n\n:point_up: This instantiates a `Message` for every result, gets the attributes out of it, and then immediately discards it.\n\nWe can skip that unnecessary instantiation by using [`pluck`](https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-pluck):\n\n```ruby\n  def index\n    messages = Message.created_by(current_user).after(3.weeks.ago)\n\n    render json: messages.pluck(:id, :created_at, :text)\n      .map { |id, created, text|\n        { id: id,\n          postedAt: created_at,\n          text: text } }\n  end\n```\n\nIn [a simple benchmark](https://github.com/boblail/pluck_map/blob/master/test/benchmarks.rb), the second example is 3× faster than the first and allocates half as much memory. :rocket: (Mileage may vary, of course, but in real applications with more complex models, I've gotten more like a 10× improvement at bottlenecks.)\n\nOne drawback to this technique is its verbosity — we repeat the attribute names at least three times and changes to blocks like this make for noisy diffs:\n\n```diff\n  def index\n    messages = Message.created_by(current_user).after(3.weeks.ago)\n-   render json: messages.pluck(:id, :created_at, :text)\n+   render json: messages.pluck(:id, :created_at, :text, :channel)\n-     .map { |id, created, text|\n+     .map { |id, created, text, channel|\n        { id: id,\n          postedAt: created_at,\n-         text: text } }\n+         text: text,\n+         channel: channel } }\n  end\n```\n\n`PluckMap::Presenter` gives us a shorthand for generating the above pluck-map pattern. Using it, we could write our example like this:\n\n```ruby\n  def index\n    messages = Message.created_by(current_user).after(3.weeks.ago)\n    presenter = PluckMap[Message].define do |q|\n      q.id\n      q.postedAt select: :created_at\n      q.text\n      q.channel\n    end\n    render json: presenter.to_h(messages)\n  end\n```\n\nUsing that definition, `PluckMap::Presenter` dynamically generates a `.to_h` method that is implemented exactly like the example above that uses `.pluck` and `.map`.\n\nThis DSL also makes it easy to make fields optional:\n\n```diff\n  def index\n    messages = Message.created_by(current_user).after(3.weeks.ago)\n    presenter = PluckMap[Message].define do |q|\n      q.id\n      q.postedAt select: :created_at\n      q.text\n-     q.channel\n+     q.channel if params[:fields] =~ /channel/\n    end\n    render json: presenter.to_h(messages)\n  end\n```\n\n### How is this different from [Jbuilder](https://github.com/rails/jbuilder)?\n\nJbuilder gives you a similar DSL for defining JSON to be presented but it operators on instances of ActiveRecord objects rather than producing a query to pluck just the values we need from the database.\n\n\n\n## Usage\n\n### Defining Attributes to Present\n\n#### Syntax\n\nDefine attributes using either of these syntaxes:\n\n 1. Without the block variable\n\n    ```ruby\n    presenter = PluckMap[Book].define do\n      title\n    end\n    ```\n\n 2. With the block variable\n\n    ```ruby\n    presenter = PluckMap[Book].define do |q|\n      q.title\n    end\n    ```\n\nApart from the repetition of the block variable, the difference between the two styles is the value of `self` within the block. In the first case, `self` will be `PluckMap::AttributesBuilder`. In the second, `self` will be the containing object. The former is less repetitious but the latter can be useful if you want to refer to local methods or instance variables in the context.\n\n#### `:as` and `:select`\n\n:point_down: This will construct a query to select `books.title` from the database and present the value of each title with the key (or column name) `\"title\"`:\n\n```ruby\npresenter = PluckMap[Book].define do\n  title\nend\n```\n\nThere are two ways to change the name of the key that is presented. Both of the following examples will select `authors.first_name` from the database and present it as `\"firstName\"`:\n\n\n 1. Using `:as`\n\n    ```ruby\n    presenter = PluckMap[Author].define do\n      first_name as: \"firstName\"\n    end\n    ```\n\n 2. Using `:select`\n\n    ```ruby\n    presenter = PluckMap[Author].define do\n      firstName select: :first_name\n    end\n    ```\n\nYou can also pass raw SQL expressions to `:select`:\n\n```ruby\npresenter = PluckMap[Person].define do\n  name select: Arel.sql(\"CONCAT(first_name, ' ', last_name)\")\nend\n```\n\n#### `:map`\n\nIn the example above, we constructed `name` from `first_name` and `last_name` with a SQL expression. There are many reasons why we might want to process values before presenting them. When possible, it's usually more efficient to do this work in the query itself, but there are times when it's necessary or expedient to do it in Ruby. Use `:map` to process values returned from the query before they are presented.\n\nHere are a couple of examples:\n\n- Constructing `\"name\"` with `:map`:\n    ```ruby\n    presenter = PluckMap[Person].define do\n      name select: %i[ first_name last_name ], map: -\u003e(first, last) { \"#{first} #{last}\" }\n    end\n    ```\n\n- Formatting phone numbers with `:map`:\n    ```ruby\n    presenter = PluckMap[Person].define do\n      phoneNumber select: %i[ phone_number ], map: -\u003e(number) { PhoneNumberFormatter.format(number) }\n    end\n    ```\n\n#### `:value`\n\nYou can also hard-code a value to be used and it won't be queried from the database. There are two ways of expressing this:\n\n```ruby\npresenter = PluckMap[Person].define do\n  id\n  type \"Person\"\nend\n```\n\n```ruby\npresenter = PluckMap[Person].define do\n  id\n  type value: \"Person\"\nend\n```\n\n### Structured attributes\n\nYou can also nest attributes by passing a block to the attribute method:\n\n```ruby\npresenter = PluckMap[Person].define do\n  parent do\n    id select: :parent_id\n    type \"Parent\"\n  end\nend\n```\n\n### Relationships\n\nPluckMap can also describe nested data. There are two special methods in the `define` block that introduce child resources:\n\n 1. `has_one` will treat the resource as a nested object or null\n 2. `has_many` will treat the resource as an array of nested objects (which may be empty)\n\nThe first argument to either of these methods is the name of an association on the presented model.\n\nYou can use either of these methods with any kind of ActiveRecord relation (`belongs_to`, `has_one`, `has_many`, `has_and_belongs_to_many`), although it generally makes more sense to use `has_one` with Rails' singular associations and `has_many` with Rails' plural associations.\n\n#### `has_one`\n\nIn the example below, assume\n\n```ruby\nclass Book \u003c ActiveRecord::Base\n  belongs_to :author\nend\n```\n\nThis presenter :point_down: selects the title of every book as well as its author's name:\n\n```ruby\npresenter = PluckMap[Book].define do\n  title\n  has_one :author do\n    name\n  end\nend\n```\n\n(We can also write it using block variables, if that's easier to read.)\n\n```ruby\npresenter = PluckMap[Book].define do |book|\n  book.title\n  book.has_one :author do |author|\n    author.name\n  end\nend\n```\n\nAttributes defined for a relationship support all the same features as [attributes defined at the root level](#defining-attributes-to-present).\n\n\n#### `has_many`\n\nWe can present the reverse of the above example with `has_many`. This example will select a list of authors and, for each, a list of the books they wrote:\n\n```ruby\npresenter = PluckMap[Author].define do\n  name\n  has_many :books do\n    title\n  end\nend\n```\n\n#### scopes\n\nAn optional second argument to both `has_one` and `has_many` is a scope block that you can use to modify the query that would select the associated records. You can use any of ActiveRecord's standard [querying methods](https://guides.rubyonrails.org/active_record_querying.html) inside the scope block.\n\nIn this example, we've altered our last presenter to ensure that books are listed alphabetically:\n\n```ruby\npresenter = PluckMap[Author].define do\n  name\n  has_many :books, -\u003e { order(title: :asc) } do\n    title\n  end\nend\n```\n\n\n### Presenting Records\n\n#### `:to_h`\n\nOnce you've defined a presenter, pass an `ActiveRecord::Relation` to `to_h` to get an array of hashes:\n\n```ruby\npresenter = PluckMap[Person].define do\n  id\n  type value: \"Person\"\nend\n\npresenter.to_h(Person.where(id: 1)) # =\u003e [{ id: 1, type: \"Person\" }]\n```\n\n#### `:to_json` and `:to_csv`\n\nYou can `.map` that array to construct whatever document you need, but PluckMap implements two methods that are optimized for generating JSON and CSV:\n\n```ruby\npresenter.to_json(Person.where(id: 1)) # =\u003e '[{\"id\":1,\"type\":\"Person\"}]'\npresenter.to_csv(Person.where(id: 1)) # =\u003e \"id,type\\n1,Person\"\n```\n\n#### Custom Presenters\n\nYou can define new (or override existing) presenter methods by mixing modules into `PluckMap::Presenter`. Here's an example of how you might create a presenter that produces an Excel document using an imaginary `Excel::Document` library:\n\n```ruby\nmodule PluckToXlsxPresenter\n  def to_excel(query)\n    # Every presenter method accepts an ActiveRecord::Relation\n    # and passes it to `pluck` which yields the results.\n    pluck(query) do |results|\n\n      # Use an imaginary Excel gem that has an Excel::Document object\n      spreadsheet = Excel::Document.new\n\n      # Fill in a Header row\n      # `attributes` is a method on `PluckMap::Presenter` that describes\n      # the attributes you defined when you constructed the presenter.\n      attributes.each_with_index do |attribute, i|\n        spreadsheet.cell[0, i] = attribute.name\n      end\n\n      # Results is an array of rows (Rows are an array of values)\n      results.each_with_index do |values, row_number|\n        attributes.each_with_index do |attribute, column_number|\n\n          # `attribute.exec` will pick the right values from the row\n          # and perform any required processing.\n          spreadsheet.cell[row_number + 1, column_number] = attribute.exec(values)\n        end\n      end\n\n      spreadsheet.render # `pluck` returns the result of the block\n    end\n  end\nend\n\nPluckMap::Presenter.send :include, PluckToXlsxPresenter\n```\n\n\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"pluck_map\"\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install pluck_map\n\n\n\n## Requirements\n\nThe gem's only runtime requirement is:\n\n - [activerecord](https://rubygems.org/gems/activerecord) 4.2+\n\nIt supports these databases out of the box:\n\n - PostgreSQL 9.4+\n - MySQL 5.7.22+\n - SQLite 3.10.0+\n\n(Note: the versions given above are when certain JSON aggregate functions were introduced in each supported database. `PluckMap`'s core behavior will work with earlier versions of the database above but certain features like optimizations to `to_json` and relationships require the specified versions.)\n\n\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/boblail/pluck_map.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboblail%2Fpluck_map","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fboblail%2Fpluck_map","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboblail%2Fpluck_map/lists"}