PluckMap::Presenter

Gem Version Build Status

This 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).

Suppose you have an action like this:

  def index
    messages = Message.created_by(current_user).after(3.weeks.ago)

    render json: messages.map { |message|
      { id: message.id,
        postedAt: message.created_at,
        text: message.text } }
  end

This instantiates a Message for every result, gets the attributes out of it, and then immediately discards it.

We can skip that unnecessary instantiation by using pluck:

  def index
    messages = Message.created_by(current_user).after(3.weeks.ago)

    render json: messages.pluck(:id, :created_at, :text)
      .map { |id, created, text|
        { id: id,
          postedAt: created_at,
          text: text } }
  end

In a simple benchmark, 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.)

One 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:

  def index
    messages = Message.created_by(current_user).after(3.weeks.ago)
-   render json: messages.pluck(:id, :created_at, :text)
+   render json: messages.pluck(:id, :created_at, :text, :channel)
-     .map { |id, created, text|
+     .map { |id, created, text, channel|
        { id: id,
          postedAt: created_at,
-         text: text } }
+         text: text,
+         channel: channel } }
  end

PluckMap::Presenter gives us a shorthand for generating the above pluck-map pattern. Using it, we could write our example like this:

  def index
    messages = Message.created_by(current_user).after(3.weeks.ago)
    presenter = PluckMap[Message].define do |q|
      q.id
      q.postedAt select: :created_at
      q.text
      q.channel
    end
    render json: presenter.to_h(messages)
  end

Using that definition, PluckMap::Presenter dynamically generates a .to_h method that is implemented exactly like the example above that uses .pluck and .map.

This DSL also makes it easy to make fields optional:

  def index
    messages = Message.created_by(current_user).after(3.weeks.ago)
    presenter = PluckMap[Message].define do |q|
      q.id
      q.postedAt select: :created_at
      q.text
-     q.channel
+     q.channel if params[:fields] =~ /channel/
    end
    render json: presenter.to_h(messages)
  end

Installation

Add this line to your application's Gemfile:

gem "pluck_map"

And then execute:

$ bundle

Or install it yourself as:

$ gem install pluck_map

Development

After 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.

To 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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/boblail/pluck_map.