No commit activity in last 3 years
No release in over 3 years
Lightweight framework for adding methods to groups of ActiveRecord objects
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies
 Project Readme

CollectionExtensions

Sometimes an operation just doesn't fit well into a scope, but you don't want to lose your declarative code style by operating on all the objects individually. This gem adds a few lines of code to make it easier to add methods to collections of ActiveRecord objects.

Installation

Add this line to your application's Gemfile:

gem 'collection_extensions'

And then execute:

$ bundle

Or install it yourself as:

$ gem install collection_extensions

Depending on where you put your modules, you may need to require the files explicitly. I put my modules in app/models/collection_extensions and then require them by adding this file:

# in config/initializers/custom_requires.rb
Dir.glob(Rails.root + 'app/models/collection_extensions/*') {|file| require file}

Usage

Syntax

A collections of records will be extended based on the naming convention %sCollectionExtensions, where the %s substitution is the camel-cased name of the class. For example, collections of User objects will be extended with the methods in the UserCollectionExtensions model and collections of BlogPost objects will be extended with the methods in the BlogPostCollectionExtensions module.

module BlogPostCollectionExtensions

  # hash key is user ID, hash value is the number of comments they've posted on this set of blog posts
  def comments_per_user
    comment_counts = {}
    
    # 'self' is the array of blog posts
    each do |blog_post|
      blog_post.comments.each do |comment|
        comment_counts[comment.user_id] ||= 0
        comment_counts[comment.user_id] += 1
      end
    end
    
    comment_counts
  end
end

Examples

Complicated 'scopes'

Not all logical groupings of records can be easily represented by a scope. Say that you wanted to have a feature targeted at people with G+ profiles. You might write psuedocode something like this:

users = User.where("email like '%@gmail.com'").all
google_api_instance = GooglePlusAPI.new(ENV['google_plus_token'], ENV['google_plus_secret'])
users.select! { |u| profile = google_api_instance.get_profile(u.email); profile.confirmed }

Hitting an API isn't something I would normally put in a scope definition, so the first time I needed to get the G+ users I'd probably write code like the above. Then over time, this block might be copied and pasted across my code. Eventually perhaps I'd abstract it into a method:

def google_plus(users)
  google_api_instance = GooglePlusAPI.new(ENV['google_plus_token'], ENV['google_plus_secret'])
  users.select! { |u| profile = google_api_instance.get_profile(u.email); profile.confirmed }
end

# now I can use a single line in the dozens of places I need to access the G+ users
users = google_plus(User.where("email like '%@gmail.com'").all)

Better, but conceptually I want to be able to say User.google_plus. That's where the collection_extension gem comes in. Add the google_plus method to an user collection extension module and it will be available on any group of users:

module UserCollectionExtensions
  def google_plus(users)
    google_api_instance = GooglePlusAPI.new(ENV['google_plus_token'], ENV['google_plus_secret'])
    select { |u| profile = google_api_instance.get_profile(u.email); profile.confirmed }
  end
end

User.where("email like '%@gmail.com'").google_plus

Bulk Actions

Rails provides some methods that operate on collections of objects, such as Comment.destroy_all. The collection extension gem can be used to write similar convenience methods for bulk actions:

class CommentCollectionExtensions
  def moderated!
    each {|comment| comment.update_attributes(moderated: true)}
  end
end

Comment.created_by_trusted_user.moderated!  # let a bunch of comments through at once

Aggregations

Sometimes you want to roll up some data, say the average time between registration and first purchase for a particular subset of users:

class UserCollectionExtensions
  def average_time_to_first_purchase
    times = collect { |u| u.orders.first.created_at - u.created_at }
    times.inject{ |sum, el| sum + el }.to_f / times.length
  end
end

Tag.find("email_blast").users.average_time_to_first_purchase

Tell, Don't Ask

Collections of objects often encourage violation of the "Tell, Don't Ask" principle (Thoughtbot has a great refresher). Most of the methods available on a collection are by their very nature asking for information about the contents of the collection. Collect, each, select, detect, etc - their entire purpose is to expose the contents of the collection. Yes, you can then follow tell-don't-ask when operating on each member, which is a start. But what if the thing you're trying to do is really related to the collection, not the individual objects? There's a fine line and it's hard to find good examples (email me if you think of one!)... but if you do run across a case where what you really want is essentially a unit method on a collection, a collection extension is the place for it.

Naming Convention

If you want a different naming convention, set the config variable using %s string substitution:

CollectionExtensions::Config.naming_convention = "MethodsForCollectionsOf%s"

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request