![build](https://github.com/pioz/plucker/workflows/Ruby/badge.svg)
[![codecov](https://codecov.io/gh/pioz/plucker/graph/badge.svg?token=95G6SJXB47)](https://codecov.io/gh/pioz/plucker)

# Plucker

Plucker allows projecting records extracted from a query into an array of
specifically defined [Ruby structs](https://ruby-doc.org/current/Struct.html) for the occasion. It is an
enchanted [`pluck`](https://www.rubydoc.info/docs/rails/ActiveRecord%2FCalculations:pluck). It
takes a list of values you want to extract and throws them into a custom
array of Ruby struct.

This can make your application more efficient because it avoids loading
ActiveRecord objects and utilizes structs, which are more efficient.

## Installation

Install the gem and add to the application's Gemfile by executing:

    $ bundle add plucker

If bundler is not being used to manage dependencies, install the gem by executing:

    $ gem install plucker

## Usage

```ruby
posts = Post.joins(:author, :comments).group(:id).plucker(:title, 'authors.name', { comments_count: 'COUNT(comments.id)' })
post = posts.first
post.title # 'How to make pizza'
post.authors_name # 'Henry'
post.comments_count # 2
post.id # NoMethodError: undefined method `id' for #<struct title="How to make pizza", authors_name="Henry", comments_count=2>
```

## Purpose

Let's assume we have these classes:

```ruby
class Author < ApplicationRecord
  has_many :post

  validates :name, presence: true
end

class Post < ApplicationRecord
  belongs_to :author

  validates :title, body, presence: true

  def slug
    self.title.parameterize
  end
end
```

and we execute this query:

```ruby
posts = Post.joins(:author).select('id, authors.name AS author_name')
```

The objects in the posts array are ActiveRecord objects of type `Post`. As I
read the code, it feels natural for me to be able to do:

```ruby
post = posts.first
post.id
post.title
post.body
post.slug
```

Now, out of these instructions, only `post.id` works, while all the others
will result in an error because the fields were not selected. This is very
strange to me, and in a complex codebase, it can lead to confusion and
frustration.

Furthermore, I can see in the code `post.author_name` and wonder where that
method or column is defined. Obviously, I won't find the definition of that
method because it is dynamically generated by ActiveRecord. I don't like this
very much; it makes it unclear what data is present in the object.

Therefore, I have decided to write Plucker to have well-defined objects with
clear fields right from the start. I aim for lightweight and efficient
objects without creating ActiveRecord fat objects with methods that I can't
even use.

Keep in mind that you can always continue to perform queries in the standard
ActiveRecord way. With Plucker, you have a new, more efficient, and clearer
option.

## Doc

The arguments of Plucker can be specified in in 3 different ways depending on
the requirements: as a `Symbol`, as a `String`, or as a `Hash`.

When using a symbol, the column with the corresponding name to the symbol is
selected, and the struct field will have that name:

```ruby
post = Post.plucker(:title).last
#<struct title="How to make pizza">

post = Post.joins(:author).plucker(:title, :name).last
#<struct title="How to make pizza", name="Henry">
```

When using the symbol `:*`, it is interpreted as `SELECT *` statement,
selecting all columns from the specified table:

```ruby
post = Post.plucker(:*).last
#<struct id=1, title="How to make pizza", author_id=1, created_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, updated_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00>
```

When using a string value, the name of the struct field will be generated
using the [`parameterize`](https://www.rubydoc.info/gems/activesupport/String#parameterize-instance_method)
function with an underscore as the separator:

```ruby
post = Post.joins(:comments).plucker('posts.title', 'COUNT(comments.id)').last
#<struct posts_title="How to make pizza", count_comments_id=2>
```

When using the string `table_name.*`, it is interpreted as `SELECT
table_name.*` statement, selecting all columns from the specified table:

```ruby
post = Post.joins(:author).plucker(:*, 'authors.*').last
#<struct id=1, title="How to make pizza", author_id=1, created_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, updated_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, authors_id: 1, authors_name: 'Henry', authors_created_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, authors_updated_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00>
```

When using a Hash, it operates similarly to the String case, except that the
name of the struct field will be the same as the key of the Hash:

```ruby
post = Post.joins(:comments).plucker(:title, comments_count: 'COUNT(comments.id)').last
#<struct title="How to make pizza", comments_count=2>
```

Plucker also takes an optional block, which is passed to the struct
definition:

```ruby
posts = Post.plucker(:title) do
  def slug
    self.title.parameterize
  end

  def as_json
    super.tap do |json|
      json['slug'] = self.slug
    end
  end
end.last
#<struct title="How to make pizza">

post.title
# 'How to make pizza'
post.slug
# 'how-to-make-pizza'
post.as_json
# {"title"=>"How to make pizza", "slug"=>"how-to-make-pizza"}
```

## Contributing

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

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
