Skip to content

Instantly share code, notes, and snippets.

@mieko
Last active August 29, 2015 14:27
Show Gist options
  • Save mieko/64cebb43366261b61259 to your computer and use it in GitHub Desktop.
Save mieko/64cebb43366261b61259 to your computer and use it in GitHub Desktop.
Rails Console Finders

Rails Quick Console Finders

I use rails console a lot for poking around at models. Exploratory style. A LOT.

I always have seeded data, and typing User.find_by(slug: 'administrator') or User.friendly.find('administrator') gets really annoying.

This shit gets ridiculous in a REPL environment:

c = Chat.for_two(User.find_by(slug: 'mike-owens'), 
                 User.find_by(slug: 'bob-barker'))

And of course, after you've typed that off-the-cuff, you realize you did need a reference to one of the now-anonymous users:

c.messages.create(author: User.find_by(slug: 'mike-owens'), 
                  body: 'fuck, thats repetitive')

After a while, you get in the habit of:

mike = User.find_by(slug: 'mike-owens')
bob = User.find_by(slug: 'bob-barker')
c = Chat.for_two(mike, bob)
c.messages.create(author: mike, body: 'still repetitive')

So I've written these quick finders that get loaded into console sessions. I get to now use syntax like so:

c = Chat.for_two(U.mike, U.bob)
c.messages.create(author: U.mike, body: 'so refreshing')

If converts method names on the cleverly-short-named finder class to ActiveRecord lookups. Awesome.

It also does some tricks with wildcards: Leading or trailing underscores in the method name mark the position of a wildcard. So if you have a Location record that looks like:

+-----+------+-------+--------------------------------------+
| id  | type | name  | slug                                 |
+-----+------+-------+--------------------------------------+
| 161 | Unit | 1292B | trollingwood-hills-building-29-1292b |
+-----+------+-------+--------------------------------------+

You could have a Location finder named L and get it by: L._1292b. Or use L._1292_ to get the first record where the slug contains 1292.

Adding a ? to the end of a method turns it into an exists? query.

To load a file into only a console session, you'll need something like this in config/application.rb

module YourApp
  class Application < Rails::Application
    # ... normal configuration stuff ...
    console do
      ARGV.push '-r', Rails.application.root.join('lib/console.rb')
    end
  end
end

I think Rails should do that by default, and leave an empty lib/console.rb ready to hack on. Check out my lib/console.rb that defines these finders.

MIT licensed.

# This adds a lot of convenience classes/methods for the console. For example,
# instead of having to type the following in the console:
#
# Tenant.where(slug: 'trollingwood-hills')
#
# You define T as a console finder for Tenant, then use:
#
# T.troll
#
# In the simplest case, these stub classes convert underscores to dashes and
# performs a prefix search, returning the first result. For example, you can
# differentiate between 'mike-owens' and 'mike-snipes' with something like:
# U.mike_o and U.mike_s
#
# U.mike_o? is also intercepted, and turned into an `exists?`-like method.
# U[id] is defined to return Model.find(id)
#
# Sigh, as an added bonus, you can lead or trail a method name with "_" to
# signify [I]LIKE wildcards, e.g., U.mike_ acts as default, a prefix search.
# U._mike finds the first record thats ENDS with the identifier, and U._mike_
# finds the first record that CONTAINS the identifier. These forms are
# composable with the '?' exists modifier at the end of the method name.
module ConsoleFinders
def self.define_finder(class_name = nil, model:, attribute: :slug, replace: '-')
Class.new(BasicObject) do
# We'll delegate these manually, so we can stay BasicObject-simple.
# They're unlikely to clash with dynamic finders, and useful enough
# to have.
delegate_methods = %i(all where find find_by find_each first last)
delegate_methods.each do |method|
define_singleton_method(method) do |*args|
model.send(method, *args)
end
end
# M[1] -> Model.find(1)
define_singleton_method(:[]) do |id|
model.find(id)
end
define_singleton_method(:inspect) do
super() + " (finder for: #{model})"
end
define_singleton_method(:model) do
model
end
# Here we actually do the needful. Convert the method name into a
# [I]LIKE query and return results
define_singleton_method(:method_missing) do |method_name, *args|
fail ArgumentError, "unexpected argument" unless args.empty?
method_name = method_name.to_s
# Get rid of leading and trailing underscores, and trailing ?'s.
# convert underscores into dashes
column_value = method_name.gsub(/\A_|[_\?]*\z/, '').gsub('_', replace)
# Set up the like query based on leading or trailing underscores
like_query = case method_name
when /\A_.*_\??\z/; "%#{column_value}%" # M._mike_ -> %mike%
when /\A_/; "%#{column_value}" # M._mike -> %mike
when /_\??\z/; "#{column_value}%" # M.mike_ -> mike%
else; "#{column_value}%" # M.mike -> mike%
end
# AREL's #matches uses whatever the database adapter requires for a
# case-insensitive search, e.g., ILIKE on Postgres, LIKE on lesser
# database engines.
conditions = model.arel_table[attribute].matches(like_query)
if method_name[-1] == '?'
model.where(conditions).any?
else
model.where(conditions).first
end
end
end.tap do |cls|
# Name the anonymous class
const_set(class_name, cls) if class_name
end
end
# Actually define the short finders we want
define_finder :U, model: User
# Alternatively...
T = define_finder model: Tenant
end
include ConsoleFinders
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment