ActiveRecord Scopes can be nice to use and a useful way to think about data. Chaining methods together in logical filters to get the desired results without having to think about the structure of the data. ActiveRecord is tided to doing database queries, but to do that you must have a database. Have you ever wanted use this kind of syntax on a set of custom in-memory objects or an array of hashes? Now let’s not let ActiveRecord have all the fun we can do the same thing in plain old Ruby and I’ll show you how.

Here I’ve got an ActiveRecord Person, notice that it is singular because in ActiveRecord a Class represent a collection of people and a single person. We can’t very well just create new collections because the source is a single database instance.

class Person < ActiveRecord::Base
  scope :minors, -> {...}

  scope :adults, -> {...}

  scope :tall, -> {...}

  scope :short, -> {...}
end

Person.tall.minors
# => <Relations ...>

In our Ruby world why not have many different collections instance we are not limited by a single database. So in this example I have made a distinction between a collection of People and a single person.

The People class, defined below, is going to represent the definition of collection of persons. Here I stub out some methods that I will define later just to get the shape of what the classes interface is going to look like.

class People
  def minors; end
  def adults; end
  def tall;   end
  def short;  end
end

Here is a person with the attributes of age and height. I set the #to_s method so that when the objects are sent to #puts it displays more than just the object id, but lists out the attributes.

class Person
  attr_reader :height, :age

  def initialize(age:, height:)
    @age    = age
    @height = height
  end

  def to_s
    "<#Person age: #{age}, height: #{height}>"
  end
end

I create the initializer so that it sets the collection array to an attr_reader :to_a. This will allow conversion of this object into a simple array if need. First let’s start with the create method. For this to have chainable methods ie. People#adults#tall each method call needs to return a new instance of a People collection. Here I’ve filled out the operation needed to return the desired result using #reject and #select on the Array of Persons.

class People
  attr_reader :to_a

  def initialize(collection = [])
    @to_a = collection
  end

  def minors
    create to_a.select { |person| person.age < 19 }
  end

  def adults
    create to_a.reject { |person| minors.to_a.include?(person) }
  end

  def tall
    create to_a.select { |person| person.height >= 5 }
  end

  private

  def create(collection)
    self.class.new(collection)
  end
end

Here I’ve created an array that will be used as the input into our new People class.

person_array = [
  Person.new(
    age: 10, height: 4
  ),
  Person.new(
    age: 19, height: 4
  ),
  Person.new(
    age: 45, height: 6
  ),
  Person.new(
    age: 16, height: 5
  ),
  Person.new(
    age: 32, height: 5
  ),
]
tall_people = People.new(person_array).tall
puts tall_people.to_a
# => [<#Person age: 45, height: 6>
#     <#Person age: 16, height: 5>
#     <#Person age: 32, height: 5>]
puts tall_people.minors.to_a
#=> [<#Person age: 16, height: 5>]

Check out the ActiveEnumerable Gem for a DSL to do the same thing and a whole lot more ActiveRecord query like methods on custom Ruby Collections.