Adds support for ActiveModel::Errors

The goal here is to improve ActiveModel support so that Draper can
work seamlessly with Rails' FormHelpers, proxying the model's #errors
method.

I also added support for ActiveModel::Errors, adding a proxy to the
model's #errors method only if it's a descendant of
ActiveModel::Validations.

Also some refactoring was done. Draper now proxies #to_param and #id
methods only if the model is an ActiveModel descendant.

Other things I did include:

- created Draper::ActiveModelSupport::Proxies, which contains the
  methods for proxying default methods(to_param, id, etc) depending
on the ancestors
- wrote specs for class with ActiveModel as ancestor
- wrote specs for class without ActiveModel as ancestor
This commit is contained in:
Alexandre de Oliveira 2012-05-07 23:24:01 -03:00
parent 5686874496
commit 19496f0c4f
9 changed files with 101 additions and 36 deletions

View File

@ -1,5 +1,6 @@
require "draper/version" require "draper/version"
require 'draper/system' require 'draper/system'
require 'draper/active_model_support'
require 'draper/base' require 'draper/base'
require 'draper/lazy_helpers' require 'draper/lazy_helpers'
require 'draper/model_support' require 'draper/model_support'

View File

@ -0,0 +1,24 @@
module Draper::ActiveModelSupport
module Proxies
def create_proxies
# These methods (as keys) will be created only if the correspondent
# model descends from a specific class (as value)
proxies = {}
proxies[:to_param] = ActiveModel::Conversion if defined?(ActiveModel::Conversion)
proxies[:errors] = ActiveModel::Validations if defined?(ActiveModel::Validations)
proxies[:id] = ActiveRecord::Base if defined?(ActiveRecord::Base)
proxies.each do |method_name, dependency|
if model.kind_of?(dependency) || dependency.nil?
class << self
self
end.class_eval do
self.send(:define_method, method_name) do |*args, &block|
model.send(method_name, *args, &block)
end
end
end
end
end
end
end

View File

@ -8,15 +8,11 @@ module Draper
DEFAULT_DENIED = Object.instance_methods << :method_missing DEFAULT_DENIED = Object.instance_methods << :method_missing
DEFAULT_ALLOWED = [] DEFAULT_ALLOWED = []
FORCED_PROXY = [:to_param, :id]
FORCED_PROXY.each do |method|
define_method method do |*args, &block|
model.send method, *args, &block
end
end
self.denied = DEFAULT_DENIED self.denied = DEFAULT_DENIED
self.allowed = DEFAULT_ALLOWED self.allowed = DEFAULT_ALLOWED
include Draper::ActiveModelSupport::Proxies
# Initialize a new decorator instance by passing in # Initialize a new decorator instance by passing in
# an instance of the source class. Pass in an optional # an instance of the source class. Pass in an optional
# context inside the options hash is stored for later use. # context inside the options hash is stored for later use.
@ -28,6 +24,7 @@ module Draper
self.class.model_class = input.class if model_class.nil? self.class.model_class = input.class if model_class.nil?
@model = input.kind_of?(Draper::Base) ? input.model : input @model = input.kind_of?(Draper::Base) ? input.model : input
self.options = options self.options = options
create_proxies
end end
# Proxies to the class specified by `decorates` to automatically # Proxies to the class specified by `decorates` to automatically
@ -260,7 +257,7 @@ module Draper
private private
def allow?(method) def allow?(method)
(allowed.empty? || allowed.include?(method) || FORCED_PROXY.include?(method)) && !denied.include?(method) (allowed.empty? || allowed.include?(method)) && !denied.include?(method)
end end
def find_association_reflection(association) def find_association_reflection(association)

View File

@ -4,6 +4,7 @@ describe Draper::Base do
before(:each){ ApplicationController.new.set_current_view_context } before(:each){ ApplicationController.new.set_current_view_context }
subject{ Decorator.new(source) } subject{ Decorator.new(source) }
let(:source){ Product.new } let(:source){ Product.new }
let(:non_active_model_source){ NonActiveModelProduct.new }
context("proxying class methods") do context("proxying class methods") do
it "should pass missing class method calls on to the wrapped class" do it "should pass missing class method calls on to the wrapped class" do
@ -195,7 +196,7 @@ describe Draper::Base do
end end
end end
context("selecting methods") do describe "method selection" do
it "echos the methods of the wrapped class except default exclusions" do it "echos the methods of the wrapped class except default exclusions" do
source.methods.each do |method| source.methods.each do |method|
unless Draper::Base::DEFAULT_DENIED.include?(method) unless Draper::Base::DEFAULT_DENIED.include?(method)
@ -208,27 +209,48 @@ describe Draper::Base do
DecoratorWithApplicationHelper.new(source).length.should == "overridden" DecoratorWithApplicationHelper.new(source).length.should == "overridden"
end end
it "should always proxy to_param" do
source.send :class_eval, "def to_param; 1; end"
Draper::Base.new(source).to_param.should == 1
end
it "should always proxy id" do
source.send :class_eval, "def id; 123456789; end"
Draper::Base.new(source).id.should == 123456789
end
it "should not copy the .class, .inspect, or other existing methods" do it "should not copy the .class, .inspect, or other existing methods" do
source.class.should_not == subject.class source.class.should_not == subject.class
source.inspect.should_not == subject.inspect source.inspect.should_not == subject.inspect
source.to_s.should_not == subject.to_s source.to_s.should_not == subject.to_s
end end
context "when an ActiveModel descendant" do
it "should always proxy to_param" do
source.stub(:to_param).and_return(1)
Draper::Base.new(source).to_param.should == 1
end
it "should always proxy id" do
source.stub(:id).and_return(123456789)
Draper::Base.new(source).id.should == 123456789
end
it "should always proxy errors" do
Draper::Base.new(source).errors.should be_an_instance_of ActiveModel::Errors
end
end
context "when not an ActiveModel descendant" do
it "does not proxy to_param" do
non_active_model_source.stub(:to_param).and_return(1)
Draper::Base.new(non_active_model_source).to_param.should_not == 1
end
it "does not proxy errors" do
Draper::Base.new(non_active_model_source).should_not respond_to :errors
end
end
end end
context 'the decorated model' do context 'the decorated model' do
it 'receives the mixin' do it 'receives the mixin' do
source.class.ancestors.include?(Draper::ModelSupport) source.class.ancestors.include?(Draper::ModelSupport)
end end
it 'includes ActiveModel support' do
source.class.ancestors.include?(Draper::ActiveModelSupport)
end
end end
it "should wrap source methods so they still accept blocks" do it "should wrap source methods so they still accept blocks" do

View File

@ -1,6 +1,6 @@
require 'spec_helper' require 'spec_helper'
describe Draper::ModelSupport do describe Draper::ActiveModelSupport do
subject { Product.new } subject { Product.new }
describe '#decorator' do describe '#decorator' do

View File

@ -2,20 +2,22 @@ require 'rubygems'
require 'bundler/setup' require 'bundler/setup'
Bundler.require Bundler.require
require './spec/support/samples/active_record.rb' require './spec/support/samples/active_model'
require './spec/support/samples/application_controller.rb' require './spec/support/samples/active_record'
require './spec/support/samples/application_helper.rb' require './spec/support/samples/application_controller'
require './spec/support/samples/decorator.rb' require './spec/support/samples/application_helper'
require './spec/support/samples/decorator_with_allows.rb' require './spec/support/samples/decorator'
require './spec/support/samples/decorator_with_multiple_allows.rb' require './spec/support/samples/decorator_with_allows'
require './spec/support/samples/decorator_with_application_helper.rb' require './spec/support/samples/decorator_with_multiple_allows'
require './spec/support/samples/decorator_with_denies.rb' require './spec/support/samples/decorator_with_application_helper'
require './spec/support/samples/namespaced_product.rb' require './spec/support/samples/decorator_with_denies'
require './spec/support/samples/namespaced_product_decorator.rb' require './spec/support/samples/namespaced_product'
require './spec/support/samples/product.rb' require './spec/support/samples/namespaced_product_decorator'
require './spec/support/samples/product_decorator.rb' require './spec/support/samples/non_active_model_product'
require './spec/support/samples/specific_product_decorator.rb' require './spec/support/samples/product'
require './spec/support/samples/some_thing.rb' require './spec/support/samples/product_decorator'
require './spec/support/samples/some_thing_decorator.rb' require './spec/support/samples/specific_product_decorator'
require './spec/support/samples/widget.rb' require './spec/support/samples/some_thing'
require './spec/support/samples/widget_decorator.rb' require './spec/support/samples/some_thing_decorator'
require './spec/support/samples/widget'
require './spec/support/samples/widget_decorator'

View File

@ -0,0 +1,9 @@
module ActiveModel
module Conversion; end
module Validations; end
class Errors
def initialize(params = nil)
end
end
end

View File

@ -1,5 +1,13 @@
module ActiveRecord module ActiveRecord
class Base class Base
include ActiveModel::Validations
include ActiveModel::Conversion
attr_reader :errors, :to_model
def initialize
@errors = ActiveModel::Errors.new(self)
end
def self.limit def self.limit
self self

View File

@ -0,0 +1,2 @@
class NonActiveModelProduct
end