Skip to content

Instantly share code, notes, and snippets.

@vsavkin
Created April 15, 2012 16:10
Show Gist options
  • Save vsavkin/2393607 to your computer and use it in GitHub Desktop.
Save vsavkin/2393607 to your computer and use it in GitHub Desktop.
Restrict AR
# The User class has two attributes: name and age.
# STEP 1: Call restrict_ar! inside your model definition. It will make most AR methods private.
class User < ActiveRecord::Base
restrict_ar!
end
# All AR class methods will become private. As a result, the following calls will raise an exception:
User.create! # BOOM
User.find_by_* # BOOM
User.where # BOOM
# Most AR instance methods will become private. No more nasty update_attributes.
# We'll get an exception if you try to call it:
user.destroy # BOOM
user.update_attributes! # BOOM
# The exceptions will be raised ONLY if we call these methods outside of the User class.
# STEP 2: Therefore, to use them we'll have to define domain specific methods inside the User class:
class User < ActiveRecord::Base
...
def self.create_user params
create! params
end
def update_name name
update_attributes! :name => name
end
def self.by_name name
where(:name => name).first
end
end
# STEP 3. We can go a little bit further and extract all persistence related methods into a separate module.
module UserRepository
def by_name name
where(:name => name).first
end
def persist user
user.save!
end
end
class User < ActiveRecord::Base
restrict_ar!
extend UserRepository
end
# The described approach makes writing database free tests easy.
user = User.new :name => "Victor", :age => 25 #no database hits here
stub(User).find_by_name("Victor"){user} #no database
mock(User).persist(user) #no database is involved
# Why have we defined the persist method? First of all, it's a nice separation of concerns (domain/persistence).
# In addition, it gives us more flexibility in terms of how we persist objects.
# Lastly, we can do more in terms of testing. The following line says, "Persist whatever you want, my tests don't care"
stub(User).persist
# ------------------------
# ------------------------
# ------------------------
# ------------------------
# Implementation:
require_relative '../../lib/fig_leaf'
module RestrictAR
def restrict_ar!
include FigLeaf
hide ActiveRecord::Base, :ancestors => true,
:except => [Object, :init_with, :new_record?, :errors,
:valid?, :save, :save!, :record_timestamps,
:"[]", :persisted?, :destroyed?, :[]=,
:skip_time_zone_conversion_for_attributes]
hide_singletons ActiveRecord::Calculations,
ActiveRecord::FinderMethods,
ActiveRecord::Relation,
ActiveRecord::QueryMethods,
:except => [Class, Object, :primary_key]
def self.method_missing name, *args
if name.to_s =~ /^find_by_(.*)$/
raise NoMethodError.new("#{name}", *args)
else
super
end
end
end
end
ActiveRecord::Base.extend RestrictAR
# Tools for making inherited interfaces private to a class.
module FigLeaf
module Macros
# Given a list of classes, modules, strings, and symbols, compile
# a combined list of methods. Classes and modules will be queried
# for their instance methods; strings and symbols will be treated
# as method names.
#
# Once the list is compiled, make all of the methods private.
#
# Takes an optional options hash, which can include the following options: #
# - :ancestors is a boolean determining whether to consider
# ancestors classes and modules.
#
# - :except is a list of classes, modules, and method names which
# will be excluded from treatment.
def hide(*stuff)
hide_methods(self, [Object], *stuff)
end
# Like #hide, only hides methods at the class/module level.
def hide_singletons(*stuff)
hide_methods(singleton_class, [Class, Object], *stuff)
end
# The shared bits of #hide and #hide_singletons
def hide_methods(mod, except_defaults, *stuff)
options = stuff.last.is_a?(Hash) ? stuff.pop : {}
include_ancestors = options.fetch(:ancestors){false}
except = Array(options.fetch(:except){except_defaults})
protect = Array(options[:protect])
except_methods = collect_methods(true, *except)
protect_methods = collect_methods(true, *protect)
methods_to_hide = collect_methods(include_ancestors, *stuff)
(methods_to_hide - except_methods).each do |method_name|
mod.module_eval do
next unless method_defined?(method_name)
if protect_methods.include?(method_name)
protected method_name
else
private method_name
end
end
end
end
# Given a list of classes, modules, strings, and symbols, compile
# a combined list of methods. Classes and modules will be queried
# for their instance methods; strings and symbols will be treated
# as methods names. +include_ancestors+ determines whether to
# include methods defined by class/module ancestors.
def collect_methods(include_ancestors, *methods_or_modules)
methods_or_modules.inject([]) do |methods, method_or_module|
case method_or_module
when Symbol, String
methods << method_or_module.to_sym
when Module # also includes classes
methods.concat(method_or_module.instance_methods(include_ancestors))
when Array
methods.concat(method_or_module)
else
raise ArgumentError, "Bad argument: #{method_or_module.inspect}"
end
end
end
end
def self.clothe(other)
other.extend(Macros)
end
def self.included(other)
clothe(other)
other.singleton_class.extend(Macros)
end
def self.extended(object)
clothe(object.singleton_class)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment