0

One of the things that I really like about Active Record is its named scopes and being able to chain scopes together to build expressive queries.

What would be a similar way to achieve this with plain Ruby Enumerables/Arrays, ideally without monkey-patching Enumerable or Array in any dangerous way?

For example:

### ActiveRecord Model
class User < ActiveRecord::Base
  scope :customers, -> { where(:role => 'customer') }
  scope :speaking, ->(lang) { where(:language => lang) }
end

# querying
User.customers.language('English')  # all English customers

### Plain-Ruby Array
module User
  class << self
    def customers
      users.select { |u| u[:role] == 'customer' }
    end

    def speaking(lang)
      users.select { |u| u[:language] == lang }
    end

    private

    def users
      [
        {:name => 'John', :language => 'English', :role => 'customer'},
        {:name => 'Jean', :language => 'French', :role => 'customer'},
        {:name => 'Hans', :language => 'German', :role => 'user'},
        {:name => 'Max', :language => 'English', :role => 'user'}
      ]
    end
  end
end

User.customers  # all customers
User.language('English')  # all English speakers
# how do I achieve something similar to User.customers.language('English') ...?

I know I can build a method customers_with_language inside the module, but I'm looking for a general way to solve this with any number of "scopes".

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
gingerlime
  • 5,206
  • 4
  • 37
  • 66

1 Answers1

1

Here is a crude implementation of a ScopableArray, which inherits an Array:

class ScopableArray < Array
  def method_missing(method_sym, *args)
    ScopableArray.new(select { |u| u[method_sym] == args[0] } )
  end
end

When this class receives a method it does not identify, it assumes you want to filter it according to a field of the method's name with the argument's value:

users = ScopableArray.new([
    {:name => 'John', :language => 'English', :role => 'customer'},
    {:name => 'Jean', :language => 'French', :role => 'customer'},
    {:name => 'Hans', :language => 'German', :role => 'user'},
    {:name => 'Max', :language => 'English', :role => 'user'}
])

users.role('customer')
# => [{:name=>"John", :language=>"English", :role=>"customer"}, {:name=>"Jean", :language=>"French", :role=>"customer"}]
users.role('customer').language('English')
# => [{:name=>"John", :language=>"English", :role=>"customer"}]

You can also look at ActiveRecord's implementation pattern for a more elaborate scheme where you can define scopes by passing a name and a callable block, something like this:

class ScopableArray2 < Array
  class << self
    def scope(name, body)
      unless body.respond_to?(:call)
        raise ArgumentError, 'The scope body needs to be callable.'
      end

      define_method(name) do |*args|
        dup.select! { |x| body.call(x, *args) }
      end
    end
  end
end

Then you can do something like this:

class Users < ScopableArray2
  scope :customers, ->(x) { x[:role] == 'customer' }
  scope :speaking, ->(x, lang) { x[:language] == lang }
end

users = Users.new([
        {:name => 'John', :language => 'English', :role => 'customer'},
        {:name => 'Jean', :language => 'French', :role => 'customer'},
        {:name => 'Hans', :language => 'German', :role => 'user'},
        {:name => 'Max', :language => 'English', :role => 'user'}
    ])

users.customers.speaking('English')
# => [{:name=>"John", :language=>"English", :role=>"customer"}]
Uri Agassi
  • 36,848
  • 14
  • 76
  • 93
  • Thanks Uri. It looks neat, but only handles property selection. I'm looking for something more general-purpose / composable / functional / expressive hopefully. So I can do things like all customers with a "j" in their name who do not speak English... (ideally anything I can do with an ActiveRecord scope, just operate on arrays). Does it make sense? – gingerlime Mar 06 '16 at 19:32
  • @gingerlime - added a more elaborate solution. You can use this pattern to make your solution even more elaborate. – Uri Agassi Mar 07 '16 at 07:12
  • Looks awesome, Uri! Thanks so much. – gingerlime Mar 07 '16 at 08:26