Let’s do something terrible by hand. First, here’s our data. It comes from a database.

db_results = [
  { id: 1, login: 'mjay', roles: ['user'], projects: ['muffins'] },
  { id: 2, login: 'rroke', roles: ['admin', 'user'], projects: ['security'] },
  { id: 3, login: 'tpain', roles: ['user'], projects: ['muffins'] },
  { id: 4, login: 'ghaz', roles: ['admin', 'user'], projects: ['muffins', 'cakes'] },
  { id: 5, login: 'bbarker', roles: ['user'], projects: ['pies'] }
]

Now when working with these people, we probably could get away with doing something like this for a while:

# find all admins
admins = db_results.select {|user| user[:roles].include? 'admin' }

Which is fine. Until you want to find out what people are on the Muffin Project:

# find all people working on the muffins project
people_on_muffins = db_results.select {|user| user[:projects].include? 'muffins' }

But as you keep working, you might be getting a feeling of deja-vu. The two methods above are very similar. You might be inspired by other Ruby libraries which give you a tiny DSL or at least allow you to pass blocks into methods to be more expressive.

The Smell

Here’s the complete code smelly example.

db_results = [
  { id: 1, login: 'mjay', roles: ['user'], projects: ['muffins'] },
  { id: 2, login: 'rroke', roles: ['admin', 'user'], projects: ['security'] },
  { id: 3, login: 'tpain', roles: ['user'], projects: ['muffins'] },
  { id: 4, login: 'ghaz', roles: ['admin', 'user'], projects: ['muffins', 'cakes'] },
  { id: 5, login: 'bbarker', roles: ['user'], projects: ['pies'] },
]

admins = db_results.select {|user| user[:roles].include? 'admin' }
people_on_muffins = db_results.select {|user| user[:projects].include? 'muffins' }
meeting = admins + people_on_muffins
meeting_ids = meeting.collect {|user| user[:login] }.uniq

puts meeting_ids
# => rroke ghaz mjay tpain

We’re having a meeting between the admins and people who are on the Muffin Project. The only person not matching these rules in this case is Bob Barker (bbarker). He must be busy enjoying retirement eating pie, who knows.

Inspiration

Let’s take a look at Faraday. Faraday uses blocks to great effect to communicate intent just like most libraries in Ruby. In Faraday, this is how a HTTP POST is done using Faraday:

conn.post do |req|
  req.url '/nigiri'
  req.headers['Content-Type'] = 'application/json'
  req.body = '{ "name": "Unagi" }'
end

This is kind of nice! You can get more than one thing done at a time and it doesn’t require a lot of temporary variables. Let’s see if we can use blocks like this. We’ll get to blocks in a miniute. Let’s first refactor a little bit first.

The Fix

There’s a certain similarity between the two selects. We really want to get “admins” and “project people” all together, so let’s just do that. We’ll create two methods that essentially replace the instance methods but can be used in the future for other rules. We’ll call them .with_roles and .with_projects.

def with_roles(results, role)
  results.select {|user| user[:roles].include? role }
end

def with_projects(results, project)
  results.select {|user| user[:projects].include? project }
end

Next, we’ll create a method that takes a block.

def user_ids(results, &block)
  rows = yield block
  ids = rows.collect {|user| user[:login] } if rows
  ids.uniq
end

The &block argument and yield block is optional. You could write this as:

 def user_ids(results)
   rows = results.dup
   rows = yield if block_given?
   ids = rows.collect {|user| user[:login] }
   ids.uniq
 end

But in that case, the block is optional, so you’ll want to check for block_given?. For this example, it’s easier for us to require a block to make this a shorter post … err, well I guess it’s longer now.

In any event, this method’s job is to filter results (users) with whatever code is passed in. Then it uniques the collected array because user IDs are assumed here to be unique. Finally, it returns just user_ids like it’s name implies.

The usage of this user_ids method that takes a block ends up reading very well.

admins = user_ids(db_results) do
  with_roles(db_results, 'admin') +
  with_projects(db_results, 'muffins')
end

puts admins
# => rroke ghaz mjay tpain

Here’s the completed, less smelly example.

db_results = [
  { id: 1, login: 'mjay', roles: ['user'], projects: ['muffins'] },
  { id: 2, login: 'rroke', roles: ['admin', 'user'], projects: ['security'] },
  { id: 3, login: 'tpain', roles: ['user'], projects: ['muffins'] },
  { id: 4, login: 'ghaz', roles: ['admin', 'user'], projects: ['muffins', 'cakes'] },
  { id: 5, login: 'bbarker', roles: ['user'], projects: ['pies'] }
]

def with_roles(results, role)
  results.select {|user| user[:roles].include? role }
end

def with_projects(results, project)
  results.select {|user| user[:projects].include? project }
end

def user_ids(results)
  rows = results.dup
  rows = yield if block_given?
  ids = rows.collect {|user| user[:login] }
  ids.uniq
end

admins = user_ids(db_results) do
  with_roles(db_results, 'admin') +
  with_projects(db_results, 'muffins')
end

puts admins
# => rroke ghaz mjay tpain

# usage without a block, showing that it's a little more flexible
# puts user_ids(db_results)
# => returns everyone because no filtering block was passed

Wrap Up

This is pretty procedural. I’ll leave it to you to put it into a class, maybe add something better than a “plus” operator to combine the user list together. Maybe a UserList abstraction class could help get away from hashes too.

I like going down these paths because you end up with more expressive code that is flexible to change. At the same time, little hints of DSLs come out when using blocks to this effect. This is starting down the path of a Ruby DSL. I’ll be posting about that pretty soon.