Created
April 15, 2012 16:10
-
-
Save vsavkin/2393607 to your computer and use it in GitHub Desktop.
Restrict AR
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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