├── spec
├── dummy
│ ├── log
│ │ └── .gitkeep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── .rspec
│ ├── app
│ │ ├── views
│ │ │ ├── posts
│ │ │ │ ├── show.html.erb
│ │ │ │ └── _post.html.erb
│ │ │ ├── post_mailer
│ │ │ │ └── decorated_email.html.erb
│ │ │ └── layouts
│ │ │ │ └── application.html.erb
│ │ ├── models
│ │ │ ├── post.rb
│ │ │ ├── user.rb
│ │ │ ├── admin.rb
│ │ │ └── mongoid_post.rb
│ │ ├── mailers
│ │ │ ├── application_mailer.rb
│ │ │ └── post_mailer.rb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── decorators
│ │ │ ├── mongoid_post_decorator.rb
│ │ │ └── post_decorator.rb
│ │ └── controllers
│ │ │ ├── application_controller.rb
│ │ │ ├── localized_urls.rb
│ │ │ └── posts_controller.rb
│ ├── db
│ │ ├── seeds.rb
│ │ ├── migrate
│ │ │ └── 20121019115657_create_posts.rb
│ │ └── schema.rb
│ ├── test
│ │ ├── minitest_helper.rb
│ │ ├── test_helper.rb
│ │ └── decorators
│ │ │ ├── minitest
│ │ │ ├── view_context_test.rb
│ │ │ ├── helpers_test.rb
│ │ │ ├── spec_type_test.rb
│ │ │ └── devise_test.rb
│ │ │ └── test_unit
│ │ │ ├── helpers_test.rb
│ │ │ ├── view_context_test.rb
│ │ │ └── devise_test.rb
│ ├── bin
│ │ └── rails
│ ├── spec
│ │ ├── models
│ │ │ ├── post_spec.rb
│ │ │ └── mongoid_post_spec.rb
│ │ ├── decorators
│ │ │ ├── spec_type_spec.rb
│ │ │ ├── active_model_serializers_spec.rb
│ │ │ ├── view_context_spec.rb
│ │ │ ├── helpers_spec.rb
│ │ │ ├── devise_spec.rb
│ │ │ └── post_decorator_spec.rb
│ │ ├── spec_helper.rb
│ │ ├── shared_examples
│ │ │ └── decoratable.rb
│ │ └── mailers
│ │ │ └── post_mailer_spec.rb
│ ├── config.ru
│ ├── config
│ │ ├── environment.rb
│ │ ├── boot.rb
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── initializers
│ │ │ ├── mime_types.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── session_store.rb
│ │ │ ├── inflections.rb
│ │ │ └── secret_token.rb
│ │ ├── routes.rb
│ │ ├── database.yml
│ │ ├── environments
│ │ │ ├── development.rb
│ │ │ ├── test.rb
│ │ │ └── production.rb
│ │ ├── mongoid.yml
│ │ └── application.rb
│ ├── Rakefile
│ ├── script
│ │ └── rails
│ ├── lib
│ │ └── tasks
│ │ │ └── test.rake
│ └── fast_spec
│ │ └── post_decorator_spec.rb
├── performance
│ ├── active_record.rb
│ ├── models.rb
│ ├── decorators.rb
│ └── benchmark.rb
├── draper
│ ├── view_helpers_spec.rb
│ ├── decoratable
│ │ └── equality_spec.rb
│ ├── undecorate_spec.rb
│ ├── lazy_helpers_spec.rb
│ ├── helper_proxy_spec.rb
│ ├── decorates_assigned_spec.rb
│ ├── decorated_association_spec.rb
│ ├── view_context
│ │ └── build_strategy_spec.rb
│ ├── view_context_spec.rb
│ ├── finders_spec.rb
│ ├── decoratable_spec.rb
│ ├── factory_spec.rb
│ └── collection_decorator_spec.rb
├── generators
│ ├── controller
│ │ └── controller_generator_spec.rb
│ └── decorator
│ │ └── decorator_generator_spec.rb
├── support
│ ├── matchers
│ │ └── have_text.rb
│ ├── shared_examples
│ │ ├── decoratable_equality.rb
│ │ └── view_helpers.rb
│ └── dummy_app.rb
├── spec_helper.rb
└── integration
│ └── integration_spec.rb
├── .rspec
├── .yardopts
├── lib
├── draper
│ ├── version.rb
│ ├── helper_support.rb
│ ├── undecorate.rb
│ ├── test
│ │ ├── minitest_integration.rb
│ │ ├── rspec_integration.rb
│ │ └── devise_helper.rb
│ ├── delegation.rb
│ ├── lazy_helpers.rb
│ ├── tasks
│ │ └── test.rake
│ ├── decoratable
│ │ └── equality.rb
│ ├── decorated_association.rb
│ ├── test_case.rb
│ ├── view_helpers.rb
│ ├── finders.rb
│ ├── view_context
│ │ └── build_strategy.rb
│ ├── helper_proxy.rb
│ ├── automatic_delegation.rb
│ ├── decorates_assigned.rb
│ ├── railtie.rb
│ ├── decoratable.rb
│ ├── factory.rb
│ ├── collection_decorator.rb
│ ├── view_context.rb
│ └── decorator.rb
├── generators
│ ├── rspec
│ │ ├── templates
│ │ │ └── decorator_spec.rb
│ │ └── decorator_generator.rb
│ ├── mini_test
│ │ ├── templates
│ │ │ ├── decorator_spec.rb
│ │ │ └── decorator_test.rb
│ │ └── decorator_generator.rb
│ ├── test_unit
│ │ ├── templates
│ │ │ └── decorator_test.rb
│ │ └── decorator_generator.rb
│ ├── controller_override.rb
│ └── rails
│ │ ├── templates
│ │ └── decorator.rb
│ │ └── decorator_generator.rb
└── draper.rb
├── gemfiles
├── 3.0.gemfile
├── 3.1.gemfile
├── 3.2.gemfile
├── 4.1.gemfile
└── 4.0.gemfile
├── .gitignore
├── Gemfile
├── .travis.yml
├── Guardfile
├── LICENSE
├── CONTRIBUTING.md
├── draper.gemspec
├── Rakefile
├── CHANGELOG.md
└── README.md
/spec/dummy/log/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --order rand
3 |
--------------------------------------------------------------------------------
/spec/dummy/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format progress
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/posts/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= render post %>
2 |
--------------------------------------------------------------------------------
/.yardopts:
--------------------------------------------------------------------------------
1 | yardoc 'lib/draper/**/*.rb' -m markdown --no-private
2 |
--------------------------------------------------------------------------------
/spec/dummy/db/seeds.rb:
--------------------------------------------------------------------------------
1 | Post.delete_all
2 | Post.create id: 1
3 |
--------------------------------------------------------------------------------
/lib/draper/version.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | VERSION = "1.4.0"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/post_mailer/decorated_email.html.erb:
--------------------------------------------------------------------------------
1 | <%= render @post %>
2 |
--------------------------------------------------------------------------------
/spec/dummy/test/minitest_helper.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/rails'
3 |
--------------------------------------------------------------------------------
/spec/performance/active_record.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 | class Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/gemfiles/3.0.gemfile:
--------------------------------------------------------------------------------
1 | gem "rails", "~> 3.0.0"
2 | gem "active_model_serializers", "~> 0.7.0"
3 |
--------------------------------------------------------------------------------
/gemfiles/3.1.gemfile:
--------------------------------------------------------------------------------
1 | gem "rails", "~> 3.1.0"
2 | gem "mongoid", "~> 3.0.0"
3 | gem "devise", "~> 2.2"
4 |
--------------------------------------------------------------------------------
/gemfiles/3.2.gemfile:
--------------------------------------------------------------------------------
1 | gem "rails", "~> 3.2.0"
2 | gem "mongoid", "~> 3.1.0"
3 | gem "devise", "~> 2.2"
4 |
--------------------------------------------------------------------------------
/gemfiles/4.1.gemfile:
--------------------------------------------------------------------------------
1 | gem "rails", github: "rails/rails"
2 | gem "devise", github: "plataformatec/devise"
3 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/post.rb:
--------------------------------------------------------------------------------
1 | class Post < ActiveRecord::Base
2 | # attr_accessible :title, :body
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | if defined?(Devise)
2 | class User
3 | extend Devise::Models
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/gemfiles/4.0.gemfile:
--------------------------------------------------------------------------------
1 | gem "rails", "~> 4.0.0"
2 | gem "mongoid", github: "mongoid/mongoid"
3 | gem "devise", "~> 3.0.0"
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/admin.rb:
--------------------------------------------------------------------------------
1 | if defined?(Devise)
2 | class Admin
3 | extend Devise::Models
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/generators/rspec/templates/decorator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe <%= class_name %>Decorator do
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | include LocalizedUrls
3 | end
4 |
--------------------------------------------------------------------------------
/lib/generators/mini_test/templates/decorator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | describe <%= class_name %>Decorator do
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | def hello_world
3 | "Hello, world!"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/mongoid_post.rb:
--------------------------------------------------------------------------------
1 | if defined?(Mongoid)
2 | class MongoidPost
3 | include Mongoid::Document
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/decorators/mongoid_post_decorator.rb:
--------------------------------------------------------------------------------
1 | if defined?(Mongoid)
2 | class MongoidPostDecorator < Draper::Decorator
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/lib/generators/mini_test/templates/decorator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class <%= class_name %>DecoratorTest < Draper::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/lib/generators/test_unit/templates/decorator_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class <%= class_name %>DecoratorTest < Draper::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/lib/draper/helper_support.rb:
--------------------------------------------------------------------------------
1 | module Draper::HelperSupport
2 | def decorate(input, &block)
3 | capture { block.call(input.decorate) }
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include LocalizedUrls
3 | protect_from_forgery
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/spec/dummy/spec/models/post_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'shared_examples/decoratable'
3 |
4 | describe Post do
5 | it_behaves_like "a decoratable model"
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Dummy::Application
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/localized_urls.rb:
--------------------------------------------------------------------------------
1 | module LocalizedUrls
2 | def default_url_options(options = {})
3 | {locale: I18n.locale, host: "www.example.com", port: 12345}
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Dummy::Application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 |
6 |
7 |
8 | <%= yield %>
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
4 |
5 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
6 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20121019115657_create_posts.rb:
--------------------------------------------------------------------------------
1 | class CreatePosts < ActiveRecord::Migration
2 | def change
3 | create_table :posts do |t|
4 |
5 | t.timestamps
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/dummy/spec/decorators/spec_type_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "A spec in this folder" do
4 | it "is a decorator spec" do
5 | expect(example.metadata[:type]).to be :decorator
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/draper/undecorate.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | def self.undecorate(object)
3 | if object.respond_to?(:decorated?) && object.decorated?
4 | object.object
5 | else
6 | object
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/spec/models/mongoid_post_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'shared_examples/decoratable'
3 |
4 | if defined?(Mongoid)
5 | describe MongoidPost do
6 | it_behaves_like "a decoratable model"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rvmrc
3 | .bundle
4 | Gemfile.lock
5 | pkg/*
6 | coverage.data
7 | coverage/*
8 | .yardoc
9 | doc/*
10 | tmp
11 | vendor/bundle
12 | *.swp
13 | *.swo
14 | *.DS_Store
15 | spec/dummy/log/*
16 | spec/dummy/db/*.sqlite3
17 |
--------------------------------------------------------------------------------
/spec/draper/view_helpers_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'support/shared_examples/view_helpers'
3 |
4 | module Draper
5 | describe ViewHelpers do
6 | it_behaves_like "view helpers", Class.new{include ViewHelpers}.new
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/lib/draper/test/minitest_integration.rb:
--------------------------------------------------------------------------------
1 | class Draper::TestCase
2 | register_spec_type(self) do |desc|
3 | desc < Draper::Decorator || desc < Draper::CollectionDecorator if desc.is_a?(Class)
4 | end
5 | register_spec_type(/Decorator( ?Test)?\z/i, self)
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.routes.draw do
2 | scope "(:locale)", locale: /en|zh/ do
3 | resources :posts, only: [:show] do
4 | get "mail", on: :member
5 | end
6 | end
7 |
8 | devise_for :users, :admins if defined?(Devise)
9 | end
10 |
--------------------------------------------------------------------------------
/spec/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | Dummy::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/spec/dummy/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/spec/draper/decoratable/equality_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'support/shared_examples/decoratable_equality'
3 |
4 | module Draper
5 | describe Decoratable::Equality do
6 | describe "#==" do
7 | it_behaves_like "decoration-aware #==", Object.new.extend(Decoratable::Equality)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rspec/rails'
4 |
5 | RSpec.configure do |config|
6 | config.treat_symbols_as_metadata_keys_with_true_values = true
7 | config.expect_with(:rspec) {|c| c.syntax = :expect}
8 | config.order = :random
9 | end
10 |
--------------------------------------------------------------------------------
/lib/generators/rspec/decorator_generator.rb:
--------------------------------------------------------------------------------
1 | module Rspec
2 | class DecoratorGenerator < ::Rails::Generators::NamedBase
3 | source_root File.expand_path('../templates', __FILE__)
4 |
5 | def create_spec_file
6 | template 'decorator_spec.rb', File.join('spec/decorators', class_path, "#{singular_name}_decorator_spec.rb")
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/generators/test_unit/decorator_generator.rb:
--------------------------------------------------------------------------------
1 | module TestUnit
2 | class DecoratorGenerator < ::Rails::Generators::NamedBase
3 | source_root File.expand_path('../templates', __FILE__)
4 |
5 | def create_test_file
6 | template 'decorator_test.rb', File.join('test/decorators', class_path, "#{singular_name}_decorator_test.rb")
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | platforms :ruby do
6 | gem "sqlite3"
7 | end
8 |
9 | platforms :jruby do
10 | gem "minitest", ">= 3.0"
11 | gem "activerecord-jdbcsqlite3-adapter", ">= 1.3.0.beta2"
12 | end
13 |
14 | version = ENV["RAILS_VERSION"] || "4.0"
15 |
16 | eval_gemfile File.expand_path("../gemfiles/#{version}.gemfile", __FILE__)
17 |
--------------------------------------------------------------------------------
/spec/dummy/lib/tasks/test.rake:
--------------------------------------------------------------------------------
1 | require 'rake/testtask'
2 | require 'rspec/core/rake_task'
3 |
4 | Rake::Task[:test].clear
5 | Rake::TestTask.new :test do |t|
6 | t.libs << "test"
7 | t.pattern = "test/**/*_test.rb"
8 | end
9 |
10 | RSpec::Core::RakeTask.new :spec
11 |
12 | RSpec::Core::RakeTask.new :fast_spec do |t|
13 | t.pattern = "fast_spec/**/*_spec.rb"
14 | end
15 |
16 | task :default => [:test, :spec, :fast_spec]
17 |
--------------------------------------------------------------------------------
/spec/dummy/spec/decorators/active_model_serializers_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Draper::CollectionDecorator do
4 | describe "#active_model_serializer" do
5 | it "returns ActiveModel::ArraySerializer" do
6 | collection_decorator = Draper::CollectionDecorator.new([])
7 |
8 | expect(collection_decorator.active_model_serializer).to be ActiveModel::ArraySerializer
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/performance/models.rb:
--------------------------------------------------------------------------------
1 | require "./performance/active_record"
2 | class Product < ActiveRecord::Base
3 | def self.sample_class_method
4 | "sample class method"
5 | end
6 |
7 | def hello_world
8 | "Hello, World"
9 | end
10 | end
11 |
12 | class FastProduct < ActiveRecord::Base
13 | def self.sample_class_method
14 | "sample class method"
15 | end
16 |
17 | def hello_world
18 | "Hello, World"
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 |
3 | services:
4 | - mongodb
5 |
6 | rvm:
7 | - 1.9.3
8 | - 2.1.0
9 | - jruby-19mode
10 | - rbx-2
11 | - ruby-head
12 | - jruby-head
13 |
14 | env:
15 | - "RAILS_VERSION=4.0"
16 | - "RAILS_VERSION=3.2"
17 | - "RAILS_VERSION=3.1"
18 | - "RAILS_VERSION=3.0"
19 | - "RAILS_VERSION=4.1"
20 |
21 | matrix:
22 | allow_failures:
23 | - env: "RAILS_VERSION=4.1"
24 | - rvm: ruby-head
25 | - rvm: jruby-head
26 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # Dummy::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/lib/draper/delegation.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | module Delegation
3 | # @overload delegate(*methods, options = {})
4 | # Overrides {http://api.rubyonrails.org/classes/Module.html#method-i-delegate Module.delegate}
5 | # to make `:object` the default delegation target.
6 | #
7 | # @return [void]
8 | def delegate(*methods)
9 | options = methods.extract_options!
10 | super *methods, options.reverse_merge(to: :object)
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/posts_controller.rb:
--------------------------------------------------------------------------------
1 | class PostsController < ApplicationController
2 | decorates_assigned :post
3 |
4 | def show
5 | @post = Post.find(params[:id])
6 | end
7 |
8 | def mail
9 | post = Post.find(params[:id])
10 | email = PostMailer.decorated_email(post).deliver
11 | render text: email.body
12 | end
13 |
14 | private
15 |
16 | def goodnight_moon
17 | "Goodnight, moon!"
18 | end
19 | helper_method :goodnight_moon
20 | end
21 |
--------------------------------------------------------------------------------
/lib/draper/lazy_helpers.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | # Include this module in your decorators to get direct access to the helpers
3 | # so that you can stop typing `h.` everywhere, at the cost of mixing in a
4 | # bazillion methods.
5 | module LazyHelpers
6 |
7 | # Sends missing methods to the {HelperProxy}.
8 | def method_missing(method, *args, &block)
9 | helpers.send(method, *args, &block)
10 | rescue NoMethodError
11 | super
12 | end
13 |
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/dummy/app/mailers/post_mailer.rb:
--------------------------------------------------------------------------------
1 | class PostMailer < ApplicationMailer
2 | default from: "from@example.com"
3 | layout "application"
4 |
5 | # Mailers don't import app/helpers automatically
6 | helper :application
7 |
8 | def decorated_email(post)
9 | @post = post.decorate
10 | mail to: "to@example.com", subject: "A decorated post"
11 | end
12 |
13 | private
14 |
15 | def goodnight_moon
16 | "Goodnight, moon!"
17 | end
18 | helper_method :goodnight_moon
19 | end
20 |
--------------------------------------------------------------------------------
/lib/generators/controller_override.rb:
--------------------------------------------------------------------------------
1 | require "rails/generators"
2 | require "rails/generators/rails/controller/controller_generator"
3 | require "rails/generators/rails/scaffold_controller/scaffold_controller_generator"
4 |
5 | module Rails
6 | module Generators
7 | class ControllerGenerator
8 | hook_for :decorator, default: true do |generator|
9 | invoke generator, [name.singularize]
10 | end
11 | end
12 |
13 | class ScaffoldControllerGenerator
14 | hook_for :decorator, default: true
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/draper/tasks/test.rake:
--------------------------------------------------------------------------------
1 | require 'rake/testtask'
2 |
3 | test_task = if Rails.version.to_f < 3.2
4 | require 'rails/test_unit/railtie'
5 | Rake::TestTask
6 | else
7 | require 'rails/test_unit/sub_test_task'
8 | Rails::SubTestTask
9 | end
10 |
11 | namespace :test do
12 | test_task.new(:decorators => "test:prepare") do |t|
13 | t.libs << "test"
14 | t.pattern = "test/decorators/**/*_test.rb"
15 | end
16 | end
17 |
18 | if Rake::Task.task_defined?('test:run')
19 | Rake::Task['test:run'].enhance do
20 | Rake::Task['test:decorators'].invoke
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/draper/undecorate_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Draper, '.undecorate' do
4 | it 'undecorates a decorated object' do
5 | object = Model.new
6 | decorator = Draper::Decorator.new(object)
7 | expect(Draper.undecorate(decorator)).to equal object
8 | end
9 |
10 | it 'passes a non-decorated object through' do
11 | object = Model.new
12 | expect(Draper.undecorate(object)).to equal object
13 | end
14 |
15 | it 'passes a non-decorator object through' do
16 | object = Object.new
17 | expect(Draper.undecorate(object)).to equal object
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/generators/rails/templates/decorator.rb:
--------------------------------------------------------------------------------
1 | <%- module_namespacing do -%>
2 | <%- if parent_class_name.present? -%>
3 | class <%= class_name %>Decorator < <%= parent_class_name %>
4 | <%- else -%>
5 | class <%= class_name %>
6 | <%- end -%>
7 | delegate_all
8 |
9 | # Define presentation-specific methods here. Helpers are accessed through
10 | # `helpers` (aka `h`). You can override attributes, for example:
11 | #
12 | # def created_at
13 | # helpers.content_tag :span, class: 'time' do
14 | # object.created_at.strftime("%a %m/%d/%y")
15 | # end
16 | # end
17 |
18 | end
19 | <% end -%>
20 |
--------------------------------------------------------------------------------
/spec/dummy/spec/decorators/view_context_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def it_does_not_leak_view_context
4 | 2.times do
5 | it "has an independent view context" do
6 | expect(Draper::ViewContext.current).not_to be :leaked
7 | Draper::ViewContext.current = :leaked
8 | end
9 | end
10 | end
11 |
12 | describe "A decorator spec", type: :decorator do
13 | it_does_not_leak_view_context
14 | end
15 |
16 | describe "A controller spec", type: :controller do
17 | it_does_not_leak_view_context
18 | end
19 |
20 | describe "A mailer spec", type: :mailer do
21 | it_does_not_leak_view_context
22 | end
23 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 | #
12 | # These inflection rules are supported but not enabled by default:
13 | # ActiveSupport::Inflector.inflections do |inflect|
14 | # inflect.acronym 'RESTful'
15 | # end
16 |
--------------------------------------------------------------------------------
/spec/dummy/test/decorators/minitest/view_context_test.rb:
--------------------------------------------------------------------------------
1 | require 'minitest_helper'
2 |
3 | def it_does_not_leak_view_context
4 | 2.times do
5 | it "has an independent view context" do
6 | refute_equal :leaked, Draper::ViewContext.current
7 | Draper::ViewContext.current = :leaked
8 | end
9 | end
10 | end
11 |
12 | describe "A decorator test" do
13 | it_does_not_leak_view_context
14 | end
15 |
16 | describe "A controller test" do
17 | tests Class.new(ActionController::Base)
18 |
19 | it_does_not_leak_view_context
20 | end
21 |
22 | describe "A mailer test" do
23 | it_does_not_leak_view_context
24 | end
25 |
--------------------------------------------------------------------------------
/spec/draper/lazy_helpers_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Draper
4 | describe LazyHelpers do
5 | describe "#method_missing" do
6 | let(:decorator) do
7 | Struct.new(:helpers){include Draper::LazyHelpers}.new(double)
8 | end
9 |
10 | it "proxies methods to #helpers" do
11 | decorator.helpers.stub(:foo).and_return{|arg| arg}
12 | expect(decorator.foo(:passed)).to be :passed
13 | end
14 |
15 | it "passes blocks" do
16 | decorator.helpers.stub(:foo).and_return{|&block| block.call}
17 | expect(decorator.foo{:yielded}).to be :yielded
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/dummy/test/decorators/test_unit/helpers_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class HelpersTest < Draper::TestCase
4 | def test_access_helpers_through_helper
5 | assert_equal "Help!
", helper.content_tag(:p, "Help!")
6 | end
7 |
8 | def test_access_helpers_through_helpers
9 | assert_equal "Help!
", helpers.content_tag(:p, "Help!")
10 | end
11 |
12 | def test_access_helpers_through_h
13 | assert_equal "Help!
", h.content_tag(:p, "Help!")
14 | end
15 |
16 | def test_same_helper_object_as_decorators
17 | decorator = Draper::Decorator.new(Object.new)
18 |
19 | assert_same decorator.helpers, helpers
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/dummy/spec/decorators/helpers_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "A decorator spec" do
4 | it "can access helpers through `helper`" do
5 | expect(helper.content_tag(:p, "Help!")).to eq "Help!
"
6 | end
7 |
8 | it "can access helpers through `helpers`" do
9 | expect(helpers.content_tag(:p, "Help!")).to eq "Help!
"
10 | end
11 |
12 | it "can access helpers through `h`" do
13 | expect(h.content_tag(:p, "Help!")).to eq "Help!
"
14 | end
15 |
16 | it "gets the same helper object as a decorator" do
17 | decorator = Draper::Decorator.new(Object.new)
18 |
19 | expect(helpers).to be decorator.helpers
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/dummy/test/decorators/minitest/helpers_test.rb:
--------------------------------------------------------------------------------
1 | require 'minitest_helper'
2 |
3 | describe "A decorator test" do
4 | it "can access helpers through `helper`" do
5 | assert_equal "Help!
", helper.content_tag(:p, "Help!")
6 | end
7 |
8 | it "can access helpers through `helpers`" do
9 | assert_equal "Help!
", helpers.content_tag(:p, "Help!")
10 | end
11 |
12 | it "can access helpers through `h`" do
13 | assert_equal "Help!
", h.content_tag(:p, "Help!")
14 | end
15 |
16 | it "gets the same helper object as a decorator" do
17 | decorator = Draper::Decorator.new(Object.new)
18 |
19 | assert_same decorator.helpers, helpers
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/dummy/test/decorators/test_unit/view_context_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | def it_does_not_leak_view_context
4 | 2.times do |n|
5 | define_method("test_has_independent_view_context_#{n}") do
6 | refute_equal :leaked, Draper::ViewContext.current
7 | Draper::ViewContext.current = :leaked
8 | end
9 | end
10 | end
11 |
12 | class DecoratorTest < Draper::TestCase
13 | it_does_not_leak_view_context
14 | end
15 |
16 | class ControllerTest < ActionController::TestCase
17 | tests Class.new(ActionController::Base)
18 |
19 | it_does_not_leak_view_context
20 | end
21 |
22 | class MailerTest < ActionMailer::TestCase
23 | it_does_not_leak_view_context
24 | end
25 |
--------------------------------------------------------------------------------
/spec/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | development:
7 | adapter: sqlite3
8 | database: db/development.sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | # Warning: The database defined as "test" will be erased and
13 | # re-generated from your development database when you run "rake".
14 | # Do not set this db to the same as development or production.
15 | test:
16 | adapter: sqlite3
17 | database: db/test.sqlite3
18 | pool: 5
19 | timeout: 5000
20 |
21 | production:
22 | adapter: sqlite3
23 | database: db/production.sqlite3
24 | pool: 5
25 | timeout: 5000
26 |
--------------------------------------------------------------------------------
/lib/generators/mini_test/decorator_generator.rb:
--------------------------------------------------------------------------------
1 | require 'generators/mini_test'
2 |
3 | module MiniTest
4 | module Generators
5 | class DecoratorGenerator < Base
6 | def self.source_root
7 | File.expand_path('../templates', __FILE__)
8 | end
9 |
10 | class_option :spec, :type => :boolean, :default => false, :desc => "Use MiniTest::Spec DSL"
11 |
12 | check_class_collision suffix: "DecoratorTest"
13 |
14 | def create_test_file
15 | template_type = options[:spec] ? "spec" : "test"
16 | template "decorator_#{template_type}.rb", File.join("test/decorators", class_path, "#{singular_name}_decorator_test.rb")
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/generators/controller/controller_generator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'rails'
3 | require 'ammeter/init'
4 | require 'generators/controller_override'
5 | require 'generators/rails/decorator_generator'
6 |
7 | describe Rails::Generators::ControllerGenerator do
8 | destination File.expand_path("../tmp", __FILE__)
9 |
10 | before { prepare_destination }
11 | after(:all) { FileUtils.rm_rf destination_root }
12 |
13 | describe "the generated decorator" do
14 | subject { file("app/decorators/your_model_decorator.rb") }
15 |
16 | describe "naming" do
17 | before { run_generator %w(YourModels) }
18 |
19 | it { should contain "class YourModelDecorator" }
20 | end
21 | end
22 | end
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | Dummy::Application.config.secret_key_base = 'c2e3474d3816f60bf6dd0f3b983e7283c7ff5373e11a96935340b544a31964dbe5ee077136165ee2975e0005f5e80207c0059e6d5589699031242ba5a06dcb87'
8 | Dummy::Application.config.secret_token = 'c2e3474d3816f60bf6dd0f3b983e7283c7ff5373e11a96935340b544a31964dbe5ee077136165ee2975e0005f5e80207c0059e6d5589699031242ba5a06dcb87'
9 |
--------------------------------------------------------------------------------
/lib/draper/test/rspec_integration.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | module DecoratorExampleGroup
3 | include Draper::TestCase::Behavior
4 | extend ActiveSupport::Concern
5 |
6 | included { metadata[:type] = :decorator }
7 | end
8 |
9 | RSpec.configure do |config|
10 | if RSpec::Core::Version::STRING.starts_with?("3")
11 | config.include DecoratorExampleGroup, file_path: %r{spec/decorators}, type: :decorator
12 | else
13 | config.include DecoratorExampleGroup, example_group: {file_path: %r{spec/decorators}}, type: :decorator
14 | end
15 |
16 | [:decorator, :controller, :mailer].each do |type|
17 | config.before(:each, type: type) { Draper::ViewContext.clear! }
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
We're sorry, but something went wrong.
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/spec/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/draper/decoratable/equality.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | module Decoratable
3 | module Equality
4 | # Compares self with a possibly-decorated object.
5 | #
6 | # @return [Boolean]
7 | def ==(other)
8 | super || Equality.test_for_decorator(self, other)
9 | end
10 |
11 | # Compares an object to a possibly-decorated object.
12 | #
13 | # @return [Boolean]
14 | def self.test(object, other)
15 | return object == other if object.is_a?(Decoratable)
16 | object == other || test_for_decorator(object, other)
17 | end
18 |
19 | # @private
20 | def self.test_for_decorator(object, other)
21 | other.respond_to?(:decorated?) && other.decorated? &&
22 | other.respond_to?(:object) && test(object, other.object)
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/dummy/spec/shared_examples/decoratable.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for "a decoratable model" do
2 | describe ".decorate" do
3 | it "applies a collection decorator to a scope" do
4 | described_class.create
5 | decorated = described_class.limit(1).decorate
6 |
7 | expect(decorated).to have(1).items
8 | expect(decorated).to be_decorated
9 | end
10 | end
11 |
12 | describe "#==" do
13 | it "is true for other instances' decorators" do
14 | pending "Mongoid < 3.1 overrides `#==`" if defined?(Mongoid) && Mongoid::VERSION.to_f < 3.1 && described_class < Mongoid::Document
15 |
16 | described_class.create
17 | one = described_class.first
18 | other = described_class.first
19 |
20 | expect(one).not_to be other
21 | expect(one == other.decorate).to be_true
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | def rspec_guard(options = {}, &block)
2 | options = {
3 | :version => 2,
4 | :notification => false
5 | }.merge(options)
6 |
7 | guard 'rspec', options, &block
8 | end
9 |
10 | rspec_guard :spec_paths => %w{spec/draper spec/generators} do
11 | watch(%r{^spec/.+_spec\.rb$})
12 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
13 | watch('spec/spec_helper.rb') { "spec" }
14 | end
15 |
16 | rspec_guard :spec_paths => 'spec/integration', :env => {'RAILS_ENV' => 'development'} do
17 | watch(%r{^spec/.+_spec\.rb$})
18 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
19 | watch('spec/spec_helper.rb') { "spec" }
20 | end
21 |
22 | rspec_guard :spec_paths => 'spec/integration', :env => {'RAILS_ENV' => 'production'} do
23 | watch(%r{^spec/.+_spec\.rb$})
24 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
25 | watch('spec/spec_helper.rb') { "spec" }
26 | end
27 |
--------------------------------------------------------------------------------
/lib/draper/decorated_association.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | # @private
3 | class DecoratedAssociation
4 |
5 | def initialize(owner, association, options)
6 | options.assert_valid_keys(:with, :scope, :context)
7 |
8 | @owner = owner
9 | @association = association
10 |
11 | @scope = options[:scope]
12 |
13 | decorator_class = options[:with]
14 | context = options.fetch(:context, ->(context){ context })
15 | @factory = Draper::Factory.new(with: decorator_class, context: context)
16 | end
17 |
18 | def call
19 | decorate unless defined?(@decorated)
20 | @decorated
21 | end
22 |
23 | private
24 |
25 | attr_reader :factory, :owner, :association, :scope
26 |
27 | def decorate
28 | associated = owner.object.send(association)
29 | associated = associated.send(scope) if scope
30 |
31 | @decorated = factory.decorate(associated, context_args: owner.context)
32 | end
33 |
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/draper/test/devise_helper.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | module DeviseHelper
3 | def sign_in(resource_or_scope, resource = nil)
4 | scope = begin
5 | Devise::Mapping.find_scope!(resource_or_scope)
6 | rescue RuntimeError => e
7 | # Draper 1.0 didn't require the mapping to exist
8 | ActiveSupport::Deprecation.warn("#{e.message}.\nUse `sign_in :user, mock_user` instead.", caller)
9 | :user
10 | end
11 |
12 | _stub_current_scope scope, resource || resource_or_scope
13 | end
14 |
15 | def sign_out(resource_or_scope)
16 | scope = Devise::Mapping.find_scope!(resource_or_scope)
17 | _stub_current_scope scope, nil
18 | end
19 |
20 | private
21 |
22 | def _stub_current_scope(scope, resource)
23 | Draper::ViewContext.current.controller.singleton_class.class_eval do
24 | define_method "current_#{scope}" do
25 | resource
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended to check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(:version => 20121019115657) do
15 |
16 | create_table "posts", :force => true do |t|
17 | t.datetime "created_at", :null => false
18 | t.datetime "updated_at", :null => false
19 | end
20 |
21 | end
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/spec/performance/decorators.rb:
--------------------------------------------------------------------------------
1 | require "./performance/models"
2 | class ProductDecorator < Draper::Decorator
3 |
4 | def awesome_title
5 | "Awesome Title"
6 | end
7 |
8 | # Original #method_missing
9 | def method_missing(method, *args, &block)
10 | if allow?(method)
11 | begin
12 | model.send(method, *args, &block)
13 | rescue NoMethodError
14 | super
15 | end
16 | else
17 | super
18 | end
19 | end
20 |
21 | end
22 |
23 | class FastProductDecorator < Draper::Decorator
24 |
25 | def awesome_title
26 | "Awesome Title"
27 | end
28 |
29 | # Modified #method_missing
30 | def method_missing(method, *args, &block)
31 | if allow?(method)
32 | begin
33 | self.class.send :define_method, method do |*args, &block|
34 | model.send(method, *args, &block)
35 | end
36 | self.send(method, *args, &block)
37 | rescue NoMethodError
38 | super
39 | end
40 | else
41 | super
42 | end
43 | end
44 |
45 | end
46 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing to Draper
2 | ===================
3 |
4 | First of all, **thank you** for wanting to help and reading this!
5 |
6 | If you have found a problem with Draper, please [check to see](https://github.com/drapergem/draper/issues) if there's already an issue and if there's not, [create a new report](https://github.com/drapergem/draper/issues/new). Please include your versions of Draper and Rails (and any other gems that are relevant to your issue, e.g. RSpec if you're having trouble in your tests).
7 |
8 | ## Sending a pull request
9 |
10 | Thanks again! Here's a quick how-to:
11 |
12 | 1. [Fork the project](https://help.github.com/articles/fork-a-repo).
13 | 2. Create a branch - `git checkout -b adding_magic`
14 | 3. Make your changes, and add some tests!
15 | 4. Check that the tests pass - `bundle exec rake`
16 | 5. Commit your changes - `git commit -am "Added some magic"`
17 | 6. Push the branch to Github - `git push origin adding_magic`
18 | 7. Send us a [pull request](https://help.github.com/articles/using-pull-requests)!
19 |
20 | :heart: :sparkling_heart: :heart:
21 |
--------------------------------------------------------------------------------
/spec/support/matchers/have_text.rb:
--------------------------------------------------------------------------------
1 | require 'capybara'
2 |
3 | module HaveTextMatcher
4 | def have_text(text)
5 | HaveText.new(text)
6 | end
7 |
8 | class HaveText
9 | def initialize(text)
10 | @text = text
11 | end
12 |
13 | def in(css)
14 | @css = css
15 | self
16 | end
17 |
18 | def matches?(subject)
19 | @subject = Capybara.string(subject)
20 |
21 | @subject.has_css?(@css || "*", text: @text)
22 | end
23 |
24 | def failure_message_for_should
25 | "expected to find #{@text.inspect} #{within}"
26 | end
27 |
28 | def failure_message_for_should_not
29 | "expected not to find #{@text.inspect} #{within}"
30 | end
31 |
32 | private
33 |
34 | def within
35 | if @css && @subject.has_css?(@css)
36 | "within\n#{@subject.find(@css).native}"
37 | else
38 | "#{inside} within\n#{@subject.native}"
39 | end
40 | end
41 |
42 | def inside
43 | @css ? "inside #{@css.inspect}" : "anywhere"
44 | end
45 | end
46 | end
47 |
48 | RSpec.configure do |config|
49 | config.include HaveTextMatcher
50 | end
51 |
--------------------------------------------------------------------------------
/lib/draper/test_case.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | require 'active_support/test_case'
3 |
4 | class TestCase < ::ActiveSupport::TestCase
5 | module ViewContextTeardown
6 | def teardown
7 | super
8 | Draper::ViewContext.clear!
9 | end
10 | end
11 |
12 | module Behavior
13 | if defined?(::Devise)
14 | require 'draper/test/devise_helper'
15 | include Draper::DeviseHelper
16 | end
17 |
18 | if defined?(::Capybara) && (defined?(::RSpec) || defined?(::MiniTest::Matchers))
19 | require 'capybara/rspec/matchers'
20 | include ::Capybara::RSpecMatchers
21 | end
22 |
23 | include Draper::ViewHelpers::ClassMethods
24 | alias_method :helper, :helpers
25 | end
26 |
27 | include Behavior
28 | include ViewContextTeardown
29 | end
30 | end
31 |
32 | if defined?(ActionController::TestCase)
33 | class ActionController::TestCase
34 | include Draper::TestCase::ViewContextTeardown
35 | end
36 | end
37 |
38 | if defined?(ActionMailer::TestCase)
39 | class ActionMailer::TestCase
40 | include Draper::TestCase::ViewContextTeardown
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/draper/view_helpers.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | # Provides the {#helpers} method used in {Decorator} and {CollectionDecorator}
3 | # to call the Rails helpers.
4 | module ViewHelpers
5 | extend ActiveSupport::Concern
6 |
7 | module ClassMethods
8 |
9 | # Access the helpers proxy to call built-in and user-defined
10 | # Rails helpers from a class context.
11 | #
12 | # @return [HelperProxy] the helpers proxy
13 | def helpers
14 | Draper::ViewContext.current
15 | end
16 | alias_method :h, :helpers
17 |
18 | end
19 |
20 | # Access the helpers proxy to call built-in and user-defined
21 | # Rails helpers. Aliased to `h` for convenience.
22 | #
23 | # @return [HelperProxy] the helpers proxy
24 | def helpers
25 | Draper::ViewContext.current
26 | end
27 | alias_method :h, :helpers
28 |
29 | # Alias for `helpers.localize`, since localize is something that's used
30 | # quite often. Further aliased to `l` for convenience.
31 | def localize(*args)
32 | helpers.localize(*args)
33 | end
34 | alias_method :l, :localize
35 |
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'draper'
3 | require 'rails/version'
4 | require 'action_controller'
5 | require 'action_controller/test_case'
6 |
7 | RSpec.configure do |config|
8 | config.treat_symbols_as_metadata_keys_with_true_values = true
9 | config.expect_with(:rspec) {|c| c.syntax = :expect}
10 | config.order = :random
11 | end
12 |
13 | class Model; include Draper::Decoratable; end
14 |
15 | class Product < Model; end
16 | class SpecialProduct < Product; end
17 | class ProductDecorator < Draper::Decorator; end
18 | class ProductsDecorator < Draper::CollectionDecorator; end
19 |
20 | class ProductPresenter < Draper::Decorator; end
21 |
22 | class OtherDecorator < Draper::Decorator; end
23 |
24 | module Namespaced
25 | class Product < Model; end
26 | class ProductDecorator < Draper::Decorator; end
27 |
28 | class OtherDecorator < Draper::Decorator; end
29 | end
30 |
31 | # After each example, revert changes made to the class
32 | def protect_class(klass)
33 | before { stub_const klass.name, Class.new(klass) }
34 | end
35 |
36 | def protect_module(mod)
37 | before { stub_const mod.name, mod.dup }
38 | end
39 |
--------------------------------------------------------------------------------
/lib/draper/finders.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | # Provides automatically-decorated finder methods for your decorators. You
3 | # do not have to extend this module directly; it is extended by
4 | # {Decorator.decorates_finders}.
5 | module Finders
6 |
7 | def find(id, options = {})
8 | decorate(object_class.find(id), options)
9 | end
10 |
11 | def all(options = {})
12 | decorate_collection(object_class.all, options)
13 | end
14 |
15 | def first(options = {})
16 | decorate(object_class.first, options)
17 | end
18 |
19 | def last(options = {})
20 | decorate(object_class.last, options)
21 | end
22 |
23 | # Decorates dynamic finder methods (`find_all_by_` and friends).
24 | def method_missing(method, *args, &block)
25 | return super unless method =~ /^find_(all_|last_|or_(initialize_|create_))?by_/
26 |
27 | result = object_class.send(method, *args, &block)
28 | options = args.extract_options!
29 |
30 | if method =~ /^find_all/
31 | decorate_collection(result, options)
32 | else
33 | decorate(result, options)
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/dummy/test/decorators/minitest/spec_type_test.rb:
--------------------------------------------------------------------------------
1 | require 'minitest_helper'
2 |
3 | def it_is_a_decorator_test
4 | it "is a decorator test" do
5 | assert_kind_of Draper::TestCase, self
6 | end
7 | end
8 |
9 | def it_is_not_a_decorator_test
10 | it "is not a decorator test" do
11 | refute_kind_of Draper::TestCase, self
12 | end
13 | end
14 |
15 | ProductDecorator = Class.new(Draper::Decorator)
16 | ProductsDecorator = Class.new(Draper::CollectionDecorator)
17 |
18 | describe ProductDecorator do
19 | it_is_a_decorator_test
20 | end
21 |
22 | describe ProductsDecorator do
23 | it_is_a_decorator_test
24 | end
25 |
26 | describe "ProductDecorator" do
27 | it_is_a_decorator_test
28 | end
29 |
30 | describe "AnyDecorator" do
31 | it_is_a_decorator_test
32 | end
33 |
34 | describe "Any decorator" do
35 | it_is_a_decorator_test
36 | end
37 |
38 | describe "AnyDecoratorTest" do
39 | it_is_a_decorator_test
40 | end
41 |
42 | describe "Any decorator test" do
43 | it_is_a_decorator_test
44 | end
45 |
46 | describe Object do
47 | it_is_not_a_decorator_test
48 | end
49 |
50 | describe "Nope" do
51 | it_is_not_a_decorator_test
52 | end
53 |
--------------------------------------------------------------------------------
/lib/generators/rails/decorator_generator.rb:
--------------------------------------------------------------------------------
1 | module Rails
2 | module Generators
3 | class DecoratorGenerator < NamedBase
4 | source_root File.expand_path("../templates", __FILE__)
5 | check_class_collision suffix: "Decorator"
6 |
7 | class_option :parent, type: :string, desc: "The parent class for the generated decorator"
8 |
9 | def create_decorator_file
10 | template 'decorator.rb', File.join('app/decorators', class_path, "#{file_name}_decorator.rb")
11 | end
12 |
13 | hook_for :test_framework
14 |
15 | private
16 |
17 | def parent_class_name
18 | options.fetch("parent") do
19 | begin
20 | require 'application_decorator'
21 | ApplicationDecorator
22 | rescue LoadError
23 | "Draper::Decorator"
24 | end
25 | end
26 | end
27 |
28 | # Rails 3.0.X compatibility, stolen from https://github.com/jnunemaker/mongomapper/pull/385/files#L1R32
29 | unless methods.include?(:module_namespacing)
30 | def module_namespacing
31 | yield if block_given?
32 | end
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/dummy/fast_spec/post_decorator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'draper'
2 | require 'rspec'
3 |
4 | require 'active_model/naming'
5 | require_relative '../app/decorators/post_decorator'
6 |
7 | Draper::ViewContext.test_strategy :fast
8 |
9 | Post = Struct.new(:id) { extend ActiveModel::Naming }
10 |
11 | describe PostDecorator do
12 | let(:decorator) { PostDecorator.new(object) }
13 | let(:object) { Post.new(42) }
14 |
15 | it "can use built-in helpers" do
16 | expect(decorator.truncated).to eq "Once upon a..."
17 | end
18 |
19 | it "can use built-in private helpers" do
20 | expect(decorator.html_escaped).to eq "<script>danger</script>"
21 | end
22 |
23 | it "can't use user-defined helpers from app/helpers" do
24 | expect{decorator.hello_world}.to raise_error NoMethodError, /hello_world/
25 | end
26 |
27 | it "can't use path helpers" do
28 | expect{decorator.path_with_model}.to raise_error NoMethodError, /post_path/
29 | end
30 |
31 | it "can't use url helpers" do
32 | expect{decorator.url_with_model}.to raise_error NoMethodError, /post_url/
33 | end
34 |
35 | it "can't be passed implicitly to url_for" do
36 | expect{decorator.link}.to raise_error
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/dummy/spec/mailers/post_mailer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PostMailer do
4 | describe "#decorated_email" do
5 | let(:email_body) { Capybara.string(email.body.to_s) }
6 | let(:email) { PostMailer.decorated_email(post).deliver }
7 | let(:post) { Post.create }
8 |
9 | it "decorates" do
10 | expect(email_body).to have_content "Today"
11 | end
12 |
13 | it "can use path helpers with a model" do
14 | expect(email_body).to have_css "#path_with_model", text: "/en/posts/#{post.id}"
15 | end
16 |
17 | it "can use path helpers with an id" do
18 | expect(email_body).to have_css "#path_with_id", text: "/en/posts/#{post.id}"
19 | end
20 |
21 | it "can use url helpers with a model" do
22 | expect(email_body).to have_css "#url_with_model", text: "http://www.example.com:12345/en/posts/#{post.id}"
23 | end
24 |
25 | it "can use url helpers with an id" do
26 | expect(email_body).to have_css "#url_with_id", text: "http://www.example.com:12345/en/posts/#{post.id}"
27 | end
28 |
29 | it "uses the correct view context controller" do
30 | expect(email_body).to have_css "#controller", text: "PostMailer"
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/draper/view_context/build_strategy.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | module ViewContext
3 | # @private
4 | module BuildStrategy
5 |
6 | def self.new(name, &block)
7 | const_get(name.to_s.camelize).new(&block)
8 | end
9 |
10 | class Fast
11 | def initialize(&block)
12 | @view_context_class = Class.new(ActionView::Base, &block)
13 | end
14 |
15 | def call
16 | view_context_class.new
17 | end
18 |
19 | private
20 |
21 | attr_reader :view_context_class
22 | end
23 |
24 | class Full
25 | def initialize(&block)
26 | @block = block
27 | end
28 |
29 | def call
30 | controller.view_context.tap do |context|
31 | context.singleton_class.class_eval(&block) if block
32 | end
33 | end
34 |
35 | private
36 |
37 | attr_reader :block
38 |
39 | def controller
40 | (Draper::ViewContext.controller || ApplicationController.new).tap do |controller|
41 | controller.request ||= ActionController::TestRequest.new if defined?(ActionController::TestRequest)
42 | end
43 | end
44 | end
45 |
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/dummy/app/decorators/post_decorator.rb:
--------------------------------------------------------------------------------
1 | class PostDecorator < Draper::Decorator
2 | # don't delegate_all here because it helps to identify things we
3 | # have to delegate for ActiveModel compatibility
4 |
5 | # need to delegate attribute methods for AM::Serialization
6 | # need to delegate id and new_record? for AR::Base#== (Rails 3.0 only)
7 | delegate :id, :created_at, :new_record?
8 |
9 | def posted_date
10 | if created_at.to_date == DateTime.now.utc.to_date
11 | "Today"
12 | else
13 | "Not Today"
14 | end
15 | end
16 |
17 | def path_with_model
18 | h.post_path(object)
19 | end
20 |
21 | def path_with_id
22 | h.post_path(id: id)
23 | end
24 |
25 | def url_with_model
26 | h.post_url(object)
27 | end
28 |
29 | def url_with_id
30 | h.post_url(id: id)
31 | end
32 |
33 | def link
34 | h.link_to id.to_s, self
35 | end
36 |
37 | def truncated
38 | h.truncate("Once upon a time in a world far far away", length: 17, separator: ' ')
39 | end
40 |
41 | def html_escaped
42 | h.html_escape("")
43 | end
44 |
45 | def hello_world
46 | h.hello_world
47 | end
48 |
49 | def goodnight_moon
50 | h.goodnight_moon
51 | end
52 |
53 | def updated_at
54 | :overridden
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/posts/_post.html.erb:
--------------------------------------------------------------------------------
1 |
2 | - Environment:
3 | - <%= Rails.env %>
4 |
5 | - Draper view context controller:
6 | - <%= Draper::ViewContext.current.controller.class %>
7 |
8 | - Posted:
9 | - <%= post.posted_date %>
10 |
11 | - Built-in helpers:
12 | - <%= post.truncated %>
13 |
14 | - Built-in private helpers:
15 | - <%= post.html_escaped %>
16 |
17 | - Helpers from app/helpers:
18 | - <%= post.hello_world %>
19 |
20 | - Helpers from the controller:
21 | - <%= post.goodnight_moon %>
22 |
23 | - Path with decorator:
24 | - <%= post_path(post) %>
25 |
26 | - Path with model:
27 | - <%= post.path_with_model %>
28 |
29 | - Path with id:
30 | - <%= post.path_with_id %>
31 |
32 | - URL with decorator:
33 | - <%= post_url(post) %>
34 |
35 | - URL with model:
36 | - <%= post.url_with_model %>
37 |
38 | - URL with id:
39 | - <%= post.url_with_id %>
40 |
41 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/decoratable_equality.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for "decoration-aware #==" do |subject|
2 | it "is true for itself" do
3 | expect(subject == subject).to be_true
4 | end
5 |
6 | it "is false for another object" do
7 | expect(subject == Object.new).to be_false
8 | end
9 |
10 | it "is true for a decorated version of itself" do
11 | decorated = double(object: subject, decorated?: true)
12 |
13 | expect(subject == decorated).to be_true
14 | end
15 |
16 | it "is false for a decorated other object" do
17 | decorated = double(object: Object.new, decorated?: true)
18 |
19 | expect(subject == decorated).to be_false
20 | end
21 |
22 | it "is false for a decoratable object with a `object` association" do
23 | decoratable = double(object: subject, decorated?: false)
24 |
25 | expect(subject == decoratable).to be_false
26 | end
27 |
28 | it "is false for an undecoratable object with a `object` association" do
29 | undecoratable = double(object: subject)
30 |
31 | expect(subject == undecoratable).to be_false
32 | end
33 |
34 | it "is true for a multiply-decorated version of itself" do
35 | decorated = double(object: subject, decorated?: true)
36 | redecorated = double(object: decorated, decorated?: true)
37 |
38 | expect(subject == redecorated).to be_true
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Show full error reports and disable caching
10 | config.consider_all_requests_local = true
11 | config.action_controller.perform_caching = false
12 |
13 | # Print deprecation notices to the Rails logger
14 | config.active_support.deprecation = :log
15 |
16 | # Only use best-standards-support built into browsers
17 | config.action_dispatch.best_standards_support = :builtin
18 |
19 | config.eager_load = false
20 |
21 | # Raise exception on mass assignment protection for Active Record models
22 | # config.active_record.mass_assignment_sanitizer = :strict
23 |
24 | # Log the query plan for queries taking more than this (works
25 | # with SQLite, MySQL, and PostgreSQL)
26 | # config.active_record.auto_explain_threshold_in_seconds = 0.5
27 |
28 | # Do not compress assets
29 | # config.assets.compress = false
30 |
31 | # Expands the lines which load the assets
32 | # config.assets.debug = true
33 | end
34 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Configure static asset server for tests with Cache-Control for performance
11 | # config.serve_static_assets = true
12 | # config.static_cache_control = "public, max-age=3600"
13 |
14 | # Show full error reports and disable caching
15 | config.consider_all_requests_local = true
16 | config.action_controller.perform_caching = false
17 |
18 | # Raise exceptions instead of rendering exception templates
19 | config.action_dispatch.show_exceptions = false
20 |
21 | # Disable request forgery protection in test environment
22 | config.action_controller.allow_forgery_protection = false
23 |
24 | # Raise exception on mass assignment protection for Active Record models
25 | # config.active_record.mass_assignment_sanitizer = :strict
26 |
27 | # Print deprecation notices to the stderr
28 | config.active_support.deprecation = :stderr
29 |
30 | config.eager_load = false
31 | end
32 |
--------------------------------------------------------------------------------
/lib/draper/helper_proxy.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | # Provides access to helper methods - both Rails built-in helpers, and those
3 | # defined in your application.
4 | class HelperProxy
5 |
6 | # @overload initialize(view_context)
7 | def initialize(view_context = nil)
8 | view_context ||= current_view_context # backwards compatibility
9 |
10 | @view_context = view_context
11 | end
12 |
13 | # Sends helper methods to the view context.
14 | def method_missing(method, *args, &block)
15 | self.class.define_proxy method
16 | send(method, *args, &block)
17 | end
18 |
19 | # Checks if the context responds to an instance method, or is able to
20 | # proxy it to the view context.
21 | def respond_to_missing?(method, include_private = false)
22 | super || view_context.respond_to?(method)
23 | end
24 |
25 | delegate :capture, to: :view_context
26 |
27 | protected
28 |
29 | attr_reader :view_context
30 |
31 | private
32 |
33 | def self.define_proxy(name)
34 | define_method name do |*args, &block|
35 | view_context.send(name, *args, &block)
36 | end
37 | end
38 |
39 | def current_view_context
40 | ActiveSupport::Deprecation.warn("wrong number of arguments (0 for 1) passed to Draper::HelperProxy.new", caller[1..-1])
41 | Draper::ViewContext.current.view_context
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/support/shared_examples/view_helpers.rb:
--------------------------------------------------------------------------------
1 | shared_examples_for "view helpers" do |subject|
2 | describe "#helpers" do
3 | it "returns the current view context" do
4 | Draper::ViewContext.stub current: :current_view_context
5 | expect(subject.helpers).to be :current_view_context
6 | end
7 |
8 | it "is aliased to #h" do
9 | Draper::ViewContext.stub current: :current_view_context
10 | expect(subject.h).to be :current_view_context
11 | end
12 | end
13 |
14 | describe "#localize" do
15 | it "delegates to #helpers" do
16 | subject.stub helpers: double
17 | subject.helpers.should_receive(:localize).with(:an_object, some: "parameter")
18 | subject.localize(:an_object, some: "parameter")
19 | end
20 |
21 | it "is aliased to #l" do
22 | subject.stub helpers: double
23 | subject.helpers.should_receive(:localize).with(:an_object, some: "parameter")
24 | subject.l(:an_object, some: "parameter")
25 | end
26 | end
27 |
28 | describe ".helpers" do
29 | it "returns the current view context" do
30 | Draper::ViewContext.stub current: :current_view_context
31 | expect(subject.class.helpers).to be :current_view_context
32 | end
33 |
34 | it "is aliased to .h" do
35 | Draper::ViewContext.stub current: :current_view_context
36 | expect(subject.class.h).to be :current_view_context
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/draper.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "draper/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "draper"
7 | s.version = Draper::VERSION
8 | s.authors = ["Jeff Casimir", "Steve Klabnik"]
9 | s.email = ["jeff@casimircreative.com", "steve@steveklabnik.com"]
10 | s.homepage = "http://github.com/drapergem/draper"
11 | s.summary = "View Models for Rails"
12 | s.description = "Draper adds an object-oriented layer of presentation logic to your Rails apps."
13 | s.license = "MIT"
14 |
15 | s.files = `git ls-files`.split("\n")
16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18 | s.require_paths = ["lib"]
19 |
20 | s.add_dependency 'activesupport', '>= 3.0'
21 | s.add_dependency 'actionpack', '>= 3.0'
22 | s.add_dependency 'request_store', '~> 1.0'
23 | s.add_dependency 'activemodel', '>= 3.0'
24 |
25 | s.add_development_dependency 'ammeter'
26 | s.add_development_dependency 'rake', '>= 0.9.2'
27 | s.add_development_dependency 'rspec', '~> 2.12'
28 | s.add_development_dependency 'rspec-mocks', '>= 2.12.1'
29 | s.add_development_dependency 'rspec-rails', '~> 2.12'
30 | s.add_development_dependency 'minitest-rails', '>= 1.0'
31 | s.add_development_dependency 'capybara'
32 | s.add_development_dependency 'active_model_serializers'
33 | end
34 |
--------------------------------------------------------------------------------
/spec/dummy/test/decorators/test_unit/devise_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | if defined?(Devise)
4 | class DeviseTest < Draper::TestCase
5 | def test_sign_in_a_real_user
6 | user = User.new
7 | sign_in user
8 |
9 | assert_same user, helper.current_user
10 | end
11 |
12 | def test_sign_in_a_mock_user
13 | user = Object.new
14 | sign_in :user, user
15 |
16 | assert_same user, helper.current_user
17 | end
18 |
19 | def test_sign_in_a_real_admin
20 | admin = Admin.new
21 | sign_in admin
22 |
23 | assert_same admin, helper.current_admin
24 | end
25 |
26 | def test_sign_in_a_mock_admin
27 | admin = Object.new
28 | sign_in :admin, admin
29 |
30 | assert_same admin, helper.current_admin
31 | end
32 |
33 | def test_sign_out_a_real_user
34 | user = User.new
35 | sign_in user
36 | sign_out user
37 |
38 | assert helper.current_user.nil?
39 | end
40 |
41 | def test_sign_out_a_mock_user
42 | user = Object.new
43 | sign_in :user, user
44 | sign_out :user
45 |
46 | assert helper.current_user.nil?
47 | end
48 |
49 | def test_sign_out_without_a_user
50 | sign_out :user
51 |
52 | assert helper.current_user.nil?
53 | end
54 |
55 | def test_backwards_compatibility
56 | user = Object.new
57 | ActiveSupport::Deprecation.silence do
58 | sign_in user
59 | end
60 |
61 | assert_same user, helper.current_user
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/spec/dummy/test/decorators/minitest/devise_test.rb:
--------------------------------------------------------------------------------
1 | require 'minitest_helper'
2 |
3 | if defined?(Devise)
4 | describe "A decorator test" do
5 | it "can sign in a real user" do
6 | user = User.new
7 | sign_in user
8 |
9 | assert_same user, helper.current_user
10 | end
11 |
12 | it "can sign in a mock user" do
13 | user = Object.new
14 | sign_in :user, user
15 |
16 | assert_same user, helper.current_user
17 | end
18 |
19 | it "can sign in a real admin" do
20 | admin = Admin.new
21 | sign_in admin
22 |
23 | assert_same admin, helper.current_admin
24 | end
25 |
26 | it "can sign in a mock admin" do
27 | admin = Object.new
28 | sign_in :admin, admin
29 |
30 | assert_same admin, helper.current_admin
31 | end
32 |
33 | it "can sign out a real user" do
34 | user = User.new
35 | sign_in user
36 | sign_out user
37 |
38 | assert helper.current_user.nil?
39 | end
40 |
41 | it "can sign out a mock user" do
42 | user = Object.new
43 | sign_in :user, user
44 | sign_out :user
45 |
46 | assert helper.current_user.nil?
47 | end
48 |
49 | it "can sign out without a user" do
50 | sign_out :user
51 |
52 | assert helper.current_user.nil?
53 | end
54 |
55 | it "is backwards-compatible" do
56 | user = Object.new
57 | ActiveSupport::Deprecation.silence do
58 | sign_in user
59 | end
60 |
61 | assert_same user, helper.current_user
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/spec/dummy/spec/decorators/devise_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | if defined?(Devise)
4 | describe "A decorator spec" do
5 | it "can sign in a real user" do
6 | user = User.new
7 | sign_in user
8 |
9 | expect(helper.current_user).to be user
10 | end
11 |
12 | it "can sign in a mock user" do
13 | user = double("User")
14 | sign_in :user, user
15 |
16 | expect(helper.current_user).to be user
17 | end
18 |
19 | it "can sign in a real admin" do
20 | admin = Admin.new
21 | sign_in admin
22 |
23 | expect(helper.current_admin).to be admin
24 | end
25 |
26 | it "can sign in a mock admin" do
27 | admin = double("Admin")
28 | sign_in :admin, admin
29 |
30 | expect(helper.current_admin).to be admin
31 | end
32 |
33 | it "can sign out a real user" do
34 | user = User.new
35 | sign_in user
36 | sign_out user
37 |
38 | expect(helper.current_user).to be_nil
39 | end
40 |
41 | it "can sign out a mock user" do
42 | user = double("User")
43 | sign_in :user, user
44 | sign_out :user
45 |
46 | expect(helper.current_user).to be_nil
47 | end
48 |
49 | it "can sign out without a user" do
50 | sign_out :user
51 |
52 | expect(helper.current_user).to be_nil
53 | end
54 |
55 | it "is backwards-compatible" do
56 | user = double("User")
57 | ActiveSupport::Deprecation.silence do
58 | sign_in user
59 | end
60 |
61 | expect(helper.current_user).to be user
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/spec/performance/benchmark.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
4 | Bundler.require(:default) if defined?(Bundler)
5 |
6 | require "benchmark"
7 | require "draper"
8 | require "./performance/models"
9 | require "./performance/decorators"
10 |
11 | Benchmark.bm do |bm|
12 | puts "\n[ Exclusivelly using #method_missing for model delegation ]"
13 | [ 1_000, 10_000, 100_000 ].each do |i|
14 | puts "\n[ #{i} ]"
15 | bm.report("#new ") do
16 | i.times do |n|
17 | ProductDecorator.decorate(Product.new)
18 | end
19 | end
20 |
21 | bm.report("#hello_world ") do
22 | i.times do |n|
23 | ProductDecorator.decorate(Product.new).hello_world
24 | end
25 | end
26 |
27 | bm.report("#sample_class_method ") do
28 | i.times do |n|
29 | ProductDecorator.decorate(Product.new).class.sample_class_method
30 | end
31 | end
32 | end
33 |
34 | puts "\n[ Defining methods on method_missing first hit ]"
35 | [ 1_000, 10_000, 100_000 ].each do |i|
36 | puts "\n[ #{i} ]"
37 | bm.report("#new ") do
38 | i.times do |n|
39 | FastProductDecorator.decorate(FastProduct.new)
40 | end
41 | end
42 |
43 | bm.report("#hello_world ") do
44 | i.times do |n|
45 | FastProductDecorator.decorate(FastProduct.new).hello_world
46 | end
47 | end
48 |
49 | bm.report("#sample_class_method ") do
50 | i.times do |n|
51 | FastProductDecorator.decorate(FastProduct.new).class.sample_class_method
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/draper/automatic_delegation.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | module AutomaticDelegation
3 | extend ActiveSupport::Concern
4 |
5 | # Delegates missing instance methods to the source object.
6 | def method_missing(method, *args, &block)
7 | return super unless delegatable?(method)
8 |
9 | self.class.delegate method
10 | send(method, *args, &block)
11 | end
12 |
13 | # Checks if the decorator responds to an instance method, or is able to
14 | # proxy it to the source object.
15 | def respond_to_missing?(method, include_private = false)
16 | super || delegatable?(method)
17 | end
18 |
19 | # @private
20 | def delegatable?(method)
21 | object.respond_to?(method)
22 | end
23 |
24 | module ClassMethods
25 | # Proxies missing class methods to the source class.
26 | def method_missing(method, *args, &block)
27 | return super unless delegatable?(method)
28 |
29 | object_class.send(method, *args, &block)
30 | end
31 |
32 | # Checks if the decorator responds to a class method, or is able to proxy
33 | # it to the source class.
34 | def respond_to_missing?(method, include_private = false)
35 | super || delegatable?(method)
36 | end
37 |
38 | # @private
39 | def delegatable?(method)
40 | object_class? && object_class.respond_to?(method)
41 | end
42 |
43 | # @private
44 | # Avoids reloading the model class when ActiveSupport clears autoloaded
45 | # dependencies in development mode.
46 | def before_remove_const
47 | end
48 | end
49 |
50 | included do
51 | private :delegatable?
52 | private_class_method :delegatable?
53 | end
54 |
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/draper/decorates_assigned.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | module DecoratesAssigned
3 | # @overload decorates_assigned(*variables, options = {})
4 | # Defines a helper method to access decorated instance variables.
5 | #
6 | # @example
7 | # # app/controllers/articles_controller.rb
8 | # class ArticlesController < ApplicationController
9 | # decorates_assigned :article
10 | #
11 | # def show
12 | # @article = Article.find(params[:id])
13 | # end
14 | # end
15 | #
16 | # # app/views/articles/show.html.erb
17 | # <%= article.decorated_title %>
18 | #
19 | # @param [Symbols*] variables
20 | # names of the instance variables to decorate (without the `@`).
21 | # @param [Hash] options
22 | # @option options [Decorator, CollectionDecorator] :with (nil)
23 | # decorator class to use. If nil, it is inferred from the instance
24 | # variable.
25 | # @option options [Hash, #call] :context
26 | # extra data to be stored in the decorator. If a Proc is given, it will
27 | # be passed the controller and should return a new context hash.
28 | def decorates_assigned(*variables)
29 | factory = Draper::Factory.new(variables.extract_options!)
30 |
31 | variables.each do |variable|
32 | undecorated = "@#{variable}"
33 | decorated = "@decorated_#{variable}"
34 |
35 | define_method variable do
36 | return instance_variable_get(decorated) if instance_variable_defined?(decorated)
37 | instance_variable_set decorated, factory.decorate(instance_variable_get(undecorated), context_args: self)
38 | end
39 |
40 | helper_method variable
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/draper.rb:
--------------------------------------------------------------------------------
1 | require 'action_view'
2 | require 'active_model/naming'
3 | require 'active_model/serialization'
4 | require 'active_model/serializers/json'
5 | require 'active_model/serializers/xml'
6 | require 'active_support/inflector'
7 | require 'active_support/core_ext/hash/keys'
8 | require 'active_support/core_ext/hash/reverse_merge'
9 | require 'active_support/core_ext/name_error'
10 |
11 | require 'draper/version'
12 | require 'draper/view_helpers'
13 | require 'draper/delegation'
14 | require 'draper/automatic_delegation'
15 | require 'draper/finders'
16 | require 'draper/decorator'
17 | require 'draper/helper_proxy'
18 | require 'draper/lazy_helpers'
19 | require 'draper/decoratable'
20 | require 'draper/factory'
21 | require 'draper/decorated_association'
22 | require 'draper/helper_support'
23 | require 'draper/view_context'
24 | require 'draper/collection_decorator'
25 | require 'draper/undecorate'
26 | require 'draper/decorates_assigned'
27 | require 'draper/railtie' if defined?(Rails)
28 |
29 | module Draper
30 | def self.setup_action_controller(base)
31 | base.class_eval do
32 | include Draper::ViewContext
33 | extend Draper::HelperSupport
34 | extend Draper::DecoratesAssigned
35 |
36 | before_filter :activate_draper
37 | end
38 | end
39 |
40 | def self.setup_action_mailer(base)
41 | base.class_eval do
42 | include Draper::ViewContext
43 | end
44 | end
45 |
46 | def self.setup_orm(base)
47 | base.class_eval do
48 | include Draper::Decoratable
49 | end
50 | end
51 |
52 | class UninferrableDecoratorError < NameError
53 | def initialize(klass)
54 | super("Could not infer a decorator for #{klass}.")
55 | end
56 | end
57 |
58 | class UninferrableSourceError < NameError
59 | def initialize(klass)
60 | super("Could not infer a source for #{klass}.")
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake'
2 | require 'bundler/gem_tasks'
3 | require 'rspec/core/rake_task'
4 |
5 | def run_in_dummy_app(command)
6 | success = system("cd spec/dummy && #{command}")
7 | raise "#{command} failed" unless success
8 | end
9 |
10 | task "default" => "ci"
11 |
12 | desc "Run all tests for CI"
13 | task "ci" => "spec"
14 |
15 | desc "Run all specs"
16 | task "spec" => "spec:all"
17 |
18 | namespace "spec" do
19 | task "all" => ["draper", "generators", "integration"]
20 |
21 | def spec_task(name)
22 | desc "Run #{name} specs"
23 | RSpec::Core::RakeTask.new(name) do |t|
24 | t.pattern = "spec/#{name}/**/*_spec.rb"
25 | end
26 | end
27 |
28 | spec_task "draper"
29 | spec_task "generators"
30 |
31 | desc "Run integration specs"
32 | task "integration" => ["db:setup", "integration:all"]
33 |
34 | namespace "integration" do
35 | task "all" => ["development", "production", "test"]
36 |
37 | ["development", "production"].each do |environment|
38 | task environment do
39 | Rake::Task["spec:integration:run"].execute environment
40 | end
41 | end
42 |
43 | task "run" do |t, environment|
44 | puts "Running integration specs in #{environment}"
45 |
46 | ENV["RAILS_ENV"] = environment
47 | success = system("rspec spec/integration")
48 |
49 | raise "Integration specs failed in #{environment}" unless success
50 | end
51 |
52 | task "test" do
53 | puts "Running rake in dummy app"
54 | ENV["RAILS_ENV"] = "test"
55 | run_in_dummy_app "rake"
56 | end
57 | end
58 | end
59 |
60 | namespace "db" do
61 | desc "Set up databases for integration testing"
62 | task "setup" do
63 | puts "Setting up databases"
64 | run_in_dummy_app "rm -f db/*.sqlite3"
65 | run_in_dummy_app "RAILS_ENV=development rake db:schema:load db:seed"
66 | run_in_dummy_app "RAILS_ENV=production rake db:schema:load db:seed"
67 | run_in_dummy_app "RAILS_ENV=test rake db:schema:load"
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/spec/draper/helper_proxy_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Draper
4 | describe HelperProxy do
5 | describe "#initialize" do
6 | it "sets the view context" do
7 | view_context = double
8 | helper_proxy = HelperProxy.new(view_context)
9 |
10 | expect(helper_proxy.send(:view_context)).to be view_context
11 | end
12 | end
13 |
14 | describe "#method_missing" do
15 | protect_class HelperProxy
16 |
17 | it "proxies methods to the view context" do
18 | view_context = double
19 | helper_proxy = HelperProxy.new(view_context)
20 |
21 | view_context.stub(:foo).and_return{|arg| arg}
22 | expect(helper_proxy.foo(:passed)).to be :passed
23 | end
24 |
25 | it "passes blocks" do
26 | view_context = double
27 | helper_proxy = HelperProxy.new(view_context)
28 |
29 | view_context.stub(:foo).and_return{|&block| block.call}
30 | expect(helper_proxy.foo{:yielded}).to be :yielded
31 | end
32 |
33 | it "defines the method for better performance" do
34 | helper_proxy = HelperProxy.new(double(foo: "bar"))
35 |
36 | expect(HelperProxy.instance_methods).not_to include :foo
37 | helper_proxy.foo
38 | expect(HelperProxy.instance_methods).to include :foo
39 | end
40 | end
41 |
42 | describe "#respond_to_missing?" do
43 | it "allows #method to be called on the view context" do
44 | helper_proxy = HelperProxy.new(double(foo: "bar"))
45 |
46 | expect(helper_proxy.respond_to?(:foo)).to be_true
47 | end
48 | end
49 |
50 | describe "proxying methods which are overriding" do
51 | it "proxies :capture" do
52 | view_context = double
53 | helper_proxy = HelperProxy.new(view_context)
54 |
55 | view_context.stub(:capture).and_return{|*args, &block| [*args, block.call] }
56 | expect(helper_proxy.capture(:first_arg, :second_arg){:yielded}).to \
57 | be_eql [:first_arg, :second_arg, :yielded]
58 | end
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/draper/railtie.rb:
--------------------------------------------------------------------------------
1 | require 'rails/railtie'
2 |
3 | module ActiveModel
4 | class Railtie < Rails::Railtie
5 | generators do |app|
6 | app ||= Rails.application # Rails 3.0.x does not yield `app`
7 |
8 | Rails::Generators.configure! app.config.generators
9 | require_relative '../generators/controller_override'
10 | end
11 | end
12 | end
13 |
14 | module Draper
15 | class Railtie < Rails::Railtie
16 |
17 | config.after_initialize do |app|
18 | app.config.paths.add 'app/decorators', eager_load: true
19 |
20 | if Rails.env.test?
21 | require 'draper/test_case'
22 | require 'draper/test/rspec_integration' if defined?(RSpec) and RSpec.respond_to?(:configure)
23 | end
24 | end
25 |
26 | initializer "draper.setup_action_controller" do |app|
27 | ActiveSupport.on_load :action_controller do
28 | Draper.setup_action_controller self
29 | end
30 | end
31 |
32 | initializer "draper.setup_action_mailer" do |app|
33 | ActiveSupport.on_load :action_mailer do
34 | Draper.setup_action_mailer self
35 | end
36 | end
37 |
38 | initializer "draper.setup_orm" do |app|
39 | [:active_record, :mongoid].each do |orm|
40 | ActiveSupport.on_load orm do
41 | Draper.setup_orm self
42 | end
43 | end
44 | end
45 |
46 | initializer "draper.setup_active_model_serializers" do |app|
47 | ActiveSupport.on_load :active_model_serializers do
48 | Draper::CollectionDecorator.send :include, ActiveModel::ArraySerializerSupport
49 | end
50 | end
51 |
52 | initializer "draper.minitest-rails_integration" do |app|
53 | ActiveSupport.on_load :minitest do
54 | require "draper/test/minitest_integration"
55 | end
56 | end
57 |
58 | console do
59 | require 'action_controller/test_case'
60 | ApplicationController.new.view_context
61 | Draper::ViewContext.build
62 | end
63 |
64 | rake_tasks do
65 | Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f }
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/spec/support/dummy_app.rb:
--------------------------------------------------------------------------------
1 | require 'socket'
2 | require 'net/http'
3 |
4 | # Adapted from code by Jon Leighton
5 | # https://github.com/jonleighton/focused_controller/blob/ec7ccf1/test/acceptance/app_test.rb
6 |
7 | class DummyApp
8 |
9 | def initialize(environment)
10 | raise ArgumentError, "Environment must be development or production" unless ["development", "production"].include?(environment.to_s)
11 | @environment = environment
12 | end
13 |
14 | attr_reader :environment
15 |
16 | def url
17 | "http://#{localhost}:#{port}"
18 | end
19 |
20 | def get(path)
21 | Net::HTTP.get(URI(url + path))
22 | end
23 |
24 | def within_app(&block)
25 | Dir.chdir(root, &block)
26 | end
27 |
28 | def start_server
29 | within_app do
30 | IO.popen("bundle exec rails s -e #{@environment} -p #{port} 2>&1") do |out|
31 | start = Time.now
32 | started = false
33 | output = ""
34 | timeout = 60.0
35 |
36 | while !started && !out.eof? && Time.now - start <= timeout
37 | output << read_output(out)
38 | sleep 0.1
39 |
40 | begin
41 | TCPSocket.new(localhost, port)
42 | rescue Errno::ECONNREFUSED
43 | else
44 | started = true
45 | end
46 | end
47 |
48 | raise "Server failed to start:\n#{output}" unless started
49 |
50 | yield
51 |
52 | Process.kill("KILL", out.pid)
53 | File.delete("tmp/pids/server.pid")
54 | end
55 | end
56 | end
57 |
58 | private
59 |
60 | def root
61 | File.expand_path("../../dummy", __FILE__)
62 | end
63 |
64 | def localhost
65 | "127.0.0.1"
66 | end
67 |
68 | def port
69 | @port ||= begin
70 | server = TCPServer.new(localhost, 0)
71 | server.addr[1]
72 | ensure
73 | server.close if server
74 | end
75 | end
76 |
77 | def read_output(stream)
78 | read = IO.select([stream], [], [stream], 0.1)
79 | output = ""
80 | loop { output << stream.read_nonblock(1024) } if read
81 | output
82 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK, EOFError
83 | output
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/spec/draper/decorates_assigned_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Draper
4 | describe DecoratesAssigned do
5 | let(:controller_class) do
6 | Class.new do
7 | extend DecoratesAssigned
8 |
9 | def self.helper_method(method)
10 | helper_methods << method
11 | end
12 |
13 | def self.helper_methods
14 | @helper_methods ||= []
15 | end
16 | end
17 | end
18 |
19 | describe ".decorates_assigned" do
20 | it "adds helper methods" do
21 | controller_class.decorates_assigned :article, :author
22 |
23 | expect(controller_class.instance_methods).to include :article
24 | expect(controller_class.instance_methods).to include :author
25 |
26 | expect(controller_class.helper_methods).to include :article
27 | expect(controller_class.helper_methods).to include :author
28 | end
29 |
30 | it "creates a factory" do
31 | Factory.should_receive(:new).once
32 | controller_class.decorates_assigned :article, :author
33 | end
34 |
35 | it "passes options to the factory" do
36 | options = {foo: "bar"}
37 |
38 | Factory.should_receive(:new).with(options)
39 | controller_class.decorates_assigned :article, :author, options
40 | end
41 |
42 | describe "the generated method" do
43 | it "decorates the instance variable" do
44 | object = double
45 | factory = double
46 | Factory.stub new: factory
47 |
48 | controller_class.decorates_assigned :article
49 | controller = controller_class.new
50 | controller.instance_variable_set "@article", object
51 |
52 | factory.should_receive(:decorate).with(object, context_args: controller).and_return(:decorated)
53 | expect(controller.article).to be :decorated
54 | end
55 |
56 | it "memoizes" do
57 | factory = double
58 | Factory.stub new: factory
59 |
60 | controller_class.decorates_assigned :article
61 | controller = controller_class.new
62 |
63 | factory.should_receive(:decorate).once
64 | controller.article
65 | controller.article
66 | end
67 | end
68 | end
69 |
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/spec/dummy/spec/decorators/post_decorator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PostDecorator do
4 | let(:decorator) { PostDecorator.new(object) }
5 | let(:object) { Post.create }
6 |
7 | it "can use built-in helpers" do
8 | expect(decorator.truncated).to eq "Once upon a..."
9 | end
10 |
11 | it "can use built-in private helpers" do
12 | expect(decorator.html_escaped).to eq "<script>danger</script>"
13 | end
14 |
15 | it "can use user-defined helpers from app/helpers" do
16 | expect(decorator.hello_world).to eq "Hello, world!"
17 | end
18 |
19 | it "can be passed to path helpers" do
20 | expect(helpers.post_path(decorator)).to eq "/en/posts/#{object.id}"
21 | end
22 |
23 | it "can use path helpers with its model" do
24 | expect(decorator.path_with_model).to eq "/en/posts/#{object.id}"
25 | end
26 |
27 | it "can use path helpers with its id" do
28 | expect(decorator.path_with_id).to eq "/en/posts/#{object.id}"
29 | end
30 |
31 | it "can be passed to url helpers" do
32 | expect(helpers.post_url(decorator)).to eq "http://www.example.com:12345/en/posts/#{object.id}"
33 | end
34 |
35 | it "can use url helpers with its model" do
36 | expect(decorator.url_with_model).to eq "http://www.example.com:12345/en/posts/#{object.id}"
37 | end
38 |
39 | it "can use url helpers with its id" do
40 | expect(decorator.url_with_id).to eq "http://www.example.com:12345/en/posts/#{object.id}"
41 | end
42 |
43 | it "can be passed implicitly to url_for" do
44 | expect(decorator.link).to eq "#{object.id}"
45 | end
46 |
47 | it "serializes overriden attributes" do
48 | expect(decorator.serializable_hash["updated_at"]).to be :overridden
49 | end
50 |
51 | it "serializes to JSON" do
52 | json = decorator.to_json
53 | expect(json).to match /"updated_at":"overridden"/
54 | end
55 |
56 | it "serializes to XML" do
57 | pending("Rails < 3.2 does not use `serializable_hash` in `to_xml`") if Rails.version.to_f < 3.2
58 |
59 | xml = Capybara.string(decorator.to_xml)
60 | expect(xml).to have_css "post > updated-at", text: "overridden"
61 | end
62 |
63 | it "uses a test view context from ApplicationController" do
64 | expect(Draper::ViewContext.current.controller).to be_an ApplicationController
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/integration/integration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'support/dummy_app'
3 | require 'support/matchers/have_text'
4 |
5 | app = DummyApp.new(ENV["RAILS_ENV"])
6 | spec_types = {
7 | view: ["/posts/1", "PostsController"],
8 | mailer: ["/posts/1/mail", "PostMailer"]
9 | }
10 |
11 | app.start_server do
12 | spec_types.each do |type, (path, controller)|
13 | page = app.get(path)
14 |
15 | describe "in a #{type}" do
16 | it "runs in the correct environment" do
17 | expect(page).to have_text(app.environment).in("#environment")
18 | end
19 |
20 | it "uses the correct view context controller" do
21 | expect(page).to have_text(controller).in("#controller")
22 | end
23 |
24 | it "can use built-in helpers" do
25 | expect(page).to have_text("Once upon a...").in("#truncated")
26 | end
27 |
28 | it "can use built-in private helpers" do
29 | # Nokogiri unescapes text!
30 | expect(page).to have_text("").in("#html_escaped")
31 | end
32 |
33 | it "can use user-defined helpers from app/helpers" do
34 | expect(page).to have_text("Hello, world!").in("#hello_world")
35 | end
36 |
37 | it "can use user-defined helpers from the controller" do
38 | expect(page).to have_text("Goodnight, moon!").in("#goodnight_moon")
39 | end
40 |
41 | it "can be passed to path helpers" do
42 | expect(page).to have_text("/en/posts/1").in("#path_with_decorator")
43 | end
44 |
45 | it "can use path helpers with a model" do
46 | expect(page).to have_text("/en/posts/1").in("#path_with_model")
47 | end
48 |
49 | it "can use path helpers with an id" do
50 | expect(page).to have_text("/en/posts/1").in("#path_with_id")
51 | end
52 |
53 | it "can be passed to url helpers" do
54 | expect(page).to have_text("http://www.example.com:12345/en/posts/1").in("#url_with_decorator")
55 | end
56 |
57 | it "can use url helpers with a model" do
58 | expect(page).to have_text("http://www.example.com:12345/en/posts/1").in("#url_with_model")
59 | end
60 |
61 | it "can use url helpers with an id" do
62 | expect(page).to have_text("http://www.example.com:12345/en/posts/1").in("#url_with_id")
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # Code is not reloaded between requests
5 | config.cache_classes = true
6 |
7 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_assets = false
13 |
14 | config.eager_load = true
15 |
16 | # Defaults to nil and saved in location specified by config.assets.prefix
17 | # config.assets.manifest = YOUR_PATH
18 |
19 | # Specifies the header that your server uses for sending files
20 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
21 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
22 |
23 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
24 | # config.force_ssl = true
25 |
26 | # See everything in the log (default is :info)
27 | # config.log_level = :debug
28 |
29 | # Prepend all log lines with the following tags
30 | # config.log_tags = [ :subdomain, :uuid ]
31 |
32 | # Use a different logger for distributed setups
33 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
34 |
35 | # Use a different cache store in production
36 | # config.cache_store = :mem_cache_store
37 |
38 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
39 | # config.action_controller.asset_host = "http://assets.example.com"
40 |
41 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
42 | # config.assets.precompile += %w( search.js )
43 |
44 | # Enable threaded mode
45 | # config.threadsafe!
46 |
47 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
48 | # the I18n.default_locale when a translation can not be found)
49 | config.i18n.fallbacks = true
50 |
51 | # Send deprecation notices to registered listeners
52 | config.active_support.deprecation = :notify
53 |
54 | # Log the query plan for queries taking more than this (works
55 | # with SQLite, MySQL, and PostgreSQL)
56 | # config.active_record.auto_explain_threshold_in_seconds = 0.5
57 | end
58 |
--------------------------------------------------------------------------------
/lib/draper/decoratable.rb:
--------------------------------------------------------------------------------
1 | require 'draper/decoratable/equality'
2 |
3 | module Draper
4 | # Provides shortcuts to decorate objects directly, so you can do
5 | # `@product.decorate` instead of `ProductDecorator.new(@product)`.
6 | #
7 | # This module is included by default into `ActiveRecord::Base` and
8 | # `Mongoid::Document`, but you're using another ORM, or want to decorate
9 | # plain old Ruby objects, you can include it manually.
10 | module Decoratable
11 | extend ActiveSupport::Concern
12 | include Draper::Decoratable::Equality
13 |
14 | # Decorates the object using the inferred {#decorator_class}.
15 | # @param [Hash] options
16 | # see {Decorator#initialize}
17 | def decorate(options = {})
18 | decorator_class.decorate(self, options)
19 | end
20 |
21 | # (see ClassMethods#decorator_class)
22 | def decorator_class
23 | self.class.decorator_class
24 | end
25 |
26 | def decorator_class?
27 | self.class.decorator_class?
28 | end
29 |
30 | # The list of decorators that have been applied to the object.
31 | #
32 | # @return [Array] `[]`
33 | def applied_decorators
34 | []
35 | end
36 |
37 | # (see Decorator#decorated_with?)
38 | # @return [false]
39 | def decorated_with?(decorator_class)
40 | false
41 | end
42 |
43 | # Checks if this object is decorated.
44 | #
45 | # @return [false]
46 | def decorated?
47 | false
48 | end
49 |
50 | module ClassMethods
51 |
52 | # Decorates a collection of objects. Used at the end of a scope chain.
53 | #
54 | # @example
55 | # Product.popular.decorate
56 | # @param [Hash] options
57 | # see {Decorator.decorate_collection}.
58 | def decorate(options = {})
59 | collection = Rails::VERSION::MAJOR >= 4 ? all : scoped
60 | decorator_class.decorate_collection(collection, options.reverse_merge(with: nil))
61 | end
62 |
63 | def decorator_class?
64 | decorator_class
65 | rescue Draper::UninferrableDecoratorError
66 | false
67 | end
68 |
69 | # Infers the decorator class to be used by {Decoratable#decorate} (e.g.
70 | # `Product` maps to `ProductDecorator`).
71 | #
72 | # @return [Class] the inferred decorator class.
73 | def decorator_class
74 | prefix = respond_to?(:model_name) ? model_name : name
75 | decorator_name = "#{prefix}Decorator"
76 | decorator_name.constantize
77 | rescue NameError => error
78 | if superclass.respond_to?(:decorator_class)
79 | superclass.decorator_class
80 | else
81 | raise unless error.missing_name?(decorator_name)
82 | raise Draper::UninferrableDecoratorError.new(self)
83 | end
84 | end
85 |
86 | # Compares with possibly-decorated objects.
87 | #
88 | # @return [Boolean]
89 | def ===(other)
90 | super || (other.respond_to?(:object) && super(other.object))
91 | end
92 |
93 | end
94 |
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/spec/dummy/config/mongoid.yml:
--------------------------------------------------------------------------------
1 | development:
2 | # Configure available database sessions. (required)
3 | sessions:
4 | # Defines the default session. (required)
5 | default:
6 | # Defines the name of the default database that Mongoid can connect to.
7 | # (required).
8 | database: dummy_development
9 | # Provides the hosts the default session can connect to. Must be an array
10 | # of host:port pairs. (required)
11 | hosts:
12 | - localhost:27017
13 | options:
14 | # Change whether the session persists in safe mode by default.
15 | # (default: false)
16 | # safe: false
17 |
18 | # Change the default consistency model to :eventual or :strong.
19 | # :eventual will send reads to secondaries, :strong sends everything
20 | # to master. (default: :eventual)
21 | # consistency: :eventual
22 |
23 | # How many times Moped should attempt to retry an operation after
24 | # failure. (default: 30)
25 | # max_retries: 30
26 |
27 | # The time in seconds that Moped should wait before retrying an
28 | # operation on failure. (default: 1)
29 | # retry_interval: 1
30 | # Configure Mongoid specific options. (optional)
31 | options:
32 | # Configuration for whether or not to allow access to fields that do
33 | # not have a field definition on the model. (default: true)
34 | # allow_dynamic_fields: true
35 |
36 | # Enable the identity map, needed for eager loading. (default: false)
37 | # identity_map_enabled: false
38 |
39 | # Includes the root model name in json serialization. (default: false)
40 | # include_root_in_json: false
41 |
42 | # Include the _type field in serializaion. (default: false)
43 | # include_type_for_serialization: false
44 |
45 | # Preload all models in development, needed when models use
46 | # inheritance. (default: false)
47 | # preload_models: false
48 |
49 | # Protect id and type from mass assignment. (default: true)
50 | # protect_sensitive_fields: true
51 |
52 | # Raise an error when performing a #find and the document is not found.
53 | # (default: true)
54 | # raise_not_found_error: true
55 |
56 | # Raise an error when defining a scope with the same name as an
57 | # existing method. (default: false)
58 | # scope_overwrite_exception: false
59 |
60 | # Skip the database version check, used when connecting to a db without
61 | # admin access. (default: false)
62 | # skip_version_check: false
63 |
64 | # User Active Support's time zone in conversions. (default: true)
65 | # use_activesupport_time_zone: true
66 |
67 | # Ensure all times are UTC in the app side. (default: false)
68 | # use_utc: false
69 | test:
70 | sessions:
71 | default:
72 | database: dummy_test
73 | hosts:
74 | - localhost:27017
75 | options:
76 | # In the test environment we lower the retries and retry interval to
77 | # low amounts for fast failures.
78 | max_retries: 1
79 | retry_interval: 0
80 |
--------------------------------------------------------------------------------
/lib/draper/factory.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | class Factory
3 | # Creates a decorator factory.
4 | #
5 | # @option options [Decorator, CollectionDecorator] :with (nil)
6 | # decorator class to use. If nil, it is inferred from the object
7 | # passed to {#decorate}.
8 | # @option options [Hash, #call] context
9 | # extra data to be stored in created decorators. If a proc is given, it
10 | # will be called each time {#decorate} is called and its return value
11 | # will be used as the context.
12 | def initialize(options = {})
13 | options.assert_valid_keys(:with, :context)
14 | @decorator_class = options.delete(:with)
15 | @default_options = options
16 | end
17 |
18 | # Decorates an object, inferring whether to create a singular or collection
19 | # decorator from the type of object passed.
20 | #
21 | # @param [Object] object
22 | # object to decorate.
23 | # @option options [Hash] context
24 | # extra data to be stored in the decorator. Overrides any context passed
25 | # to the constructor.
26 | # @option options [Object, Array] context_args (nil)
27 | # argument(s) to be passed to the context proc.
28 | # @return [Decorator, CollectionDecorator] the decorated object.
29 | def decorate(object, options = {})
30 | return nil if object.nil?
31 | Worker.new(decorator_class, object).call(options.reverse_merge(default_options))
32 | end
33 |
34 | private
35 |
36 | attr_reader :decorator_class, :default_options
37 |
38 | # @private
39 | class Worker
40 | def initialize(decorator_class, object)
41 | @decorator_class = decorator_class
42 | @object = object
43 | end
44 |
45 | def call(options)
46 | update_context options
47 | decorator.call(object, options)
48 | end
49 |
50 | def decorator
51 | return decorator_method(decorator_class) if decorator_class
52 | return object_decorator if decoratable?
53 | return decorator_method(Draper::CollectionDecorator) if collection?
54 | raise Draper::UninferrableDecoratorError.new(object.class)
55 | end
56 |
57 | private
58 |
59 | attr_reader :decorator_class, :object
60 |
61 | def object_decorator
62 | if collection?
63 | ->(object, options) { object.decorator_class.decorate_collection(object, options.reverse_merge(with: nil))}
64 | else
65 | ->(object, options) { object.decorate(options) }
66 | end
67 | end
68 |
69 | def decorator_method(klass)
70 | if collection? && klass.respond_to?(:decorate_collection)
71 | klass.method(:decorate_collection)
72 | else
73 | klass.method(:decorate)
74 | end
75 | end
76 |
77 | def collection?
78 | object.respond_to?(:first)
79 | end
80 |
81 | def decoratable?
82 | object.respond_to?(:decorate)
83 | end
84 |
85 | def update_context(options)
86 | args = options.delete(:context_args)
87 | options[:context] = options[:context].call(*Array.wrap(args)) if options[:context].respond_to?(:call)
88 | end
89 | end
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | def attempt_require(file)
4 | require file
5 | rescue LoadError
6 | end
7 |
8 | require 'rails/all'
9 | require 'draper'
10 | attempt_require 'mongoid'
11 | attempt_require 'devise'
12 | require 'active_model_serializers'
13 |
14 | module Dummy
15 | class Application < Rails::Application
16 | # Settings in config/environments/* take precedence over those specified here.
17 | # Application configuration should go into files in config/initializers
18 | # -- all .rb files in that directory are automatically loaded.
19 |
20 | # Custom directories with classes and modules you want to be autoloadable.
21 | # config.autoload_paths += %W(#{config.root}/extras)
22 |
23 | # Only load the plugins named here, in the order given (default is alphabetical).
24 | # :all can be used as a placeholder for all plugins not explicitly named.
25 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
26 |
27 | # Activate observers that should always be running.
28 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
29 |
30 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
31 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
32 | # config.time_zone = 'Central Time (US & Canada)'
33 |
34 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
35 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
36 | # config.i18n.default_locale = :de
37 |
38 | # Configure the default encoding used in templates for Ruby 1.9.
39 | config.encoding = "utf-8"
40 |
41 | # Configure sensitive parameters which will be filtered from the log file.
42 | config.filter_parameters += [:password]
43 |
44 | # Enable escaping HTML in JSON.
45 | config.active_support.escape_html_entities_in_json = true
46 |
47 | # Use SQL instead of Active Record's schema dumper when creating the database.
48 | # This is necessary if your schema can't be completely dumped by the schema dumper,
49 | # like if you have constraints or database-specific column types
50 | # config.active_record.schema_format = :sql
51 |
52 | # Enforce whitelist mode for mass assignment.
53 | # This will create an empty whitelist of attributes available for mass-assignment for all models
54 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
55 | # parameters by using an attr_accessible or attr_protected declaration.
56 | # config.active_record.whitelist_attributes = true
57 |
58 | # Enable the asset pipeline
59 | # config.assets.enabled = true
60 |
61 | # Version of your assets, change this if you want to expire all your assets
62 | # config.assets.version = '1.0'
63 |
64 | # Tell Action Mailer not to deliver emails to the real world.
65 | # The :test delivery method accumulates sent emails in the
66 | # ActionMailer::Base.deliveries array.
67 | config.action_mailer.delivery_method = :test
68 | end
69 | end
70 |
71 | ActiveRecord::Migration.verbose = false
72 |
--------------------------------------------------------------------------------
/spec/draper/decorated_association_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Draper
4 | describe DecoratedAssociation do
5 |
6 | describe "#initialize" do
7 | it "accepts valid options" do
8 | valid_options = {with: Decorator, scope: :foo, context: {}}
9 | expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, valid_options)}.not_to raise_error
10 | end
11 |
12 | it "rejects invalid options" do
13 | expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, foo: "bar")}.to raise_error ArgumentError, /Unknown key/
14 | end
15 |
16 | it "creates a factory" do
17 | options = {with: Decorator, context: {foo: "bar"}}
18 |
19 | Factory.should_receive(:new).with(options)
20 | DecoratedAssociation.new(double, :association, options)
21 | end
22 |
23 | describe ":with option" do
24 | it "defaults to nil" do
25 | Factory.should_receive(:new).with(with: nil, context: anything())
26 | DecoratedAssociation.new(double, :association, {})
27 | end
28 | end
29 |
30 | describe ":context option" do
31 | it "defaults to the identity function" do
32 | Factory.should_receive(:new).with do |options|
33 | options[:context].call(:anything) == :anything
34 | end
35 | DecoratedAssociation.new(double, :association, {})
36 | end
37 | end
38 | end
39 |
40 | describe "#call" do
41 | it "calls the factory" do
42 | factory = double
43 | Factory.stub new: factory
44 | associated = double
45 | owner_context = {foo: "bar"}
46 | object = double(association: associated)
47 | owner = double(object: object, context: owner_context)
48 | decorated_association = DecoratedAssociation.new(owner, :association, {})
49 | decorated = double
50 |
51 | factory.should_receive(:decorate).with(associated, context_args: owner_context).and_return(decorated)
52 | expect(decorated_association.call).to be decorated
53 | end
54 |
55 | it "memoizes" do
56 | factory = double
57 | Factory.stub new: factory
58 | owner = double(object: double(association: double), context: {})
59 | decorated_association = DecoratedAssociation.new(owner, :association, {})
60 | decorated = double
61 |
62 | factory.should_receive(:decorate).once.and_return(decorated)
63 | expect(decorated_association.call).to be decorated
64 | expect(decorated_association.call).to be decorated
65 | end
66 |
67 | context "when the :scope option was given" do
68 | it "applies the scope before decoration" do
69 | factory = double
70 | Factory.stub new: factory
71 | scoped = double
72 | object = double(association: double(applied_scope: scoped))
73 | owner = double(object: object, context: {})
74 | decorated_association = DecoratedAssociation.new(owner, :association, scope: :applied_scope)
75 | decorated = double
76 |
77 | factory.should_receive(:decorate).with(scoped, anything()).and_return(decorated)
78 | expect(decorated_association.call).to be decorated
79 | end
80 | end
81 | end
82 |
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/lib/draper/collection_decorator.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | class CollectionDecorator
3 | include Enumerable
4 | include Draper::ViewHelpers
5 | extend Draper::Delegation
6 |
7 | # @return the collection being decorated.
8 | attr_reader :object
9 |
10 | # @return [Class] the decorator class used to decorate each item, as set by
11 | # {#initialize}.
12 | attr_reader :decorator_class
13 |
14 | # @return [Hash] extra data to be used in user-defined methods, and passed
15 | # to each item's decorator.
16 | attr_accessor :context
17 |
18 | array_methods = Array.instance_methods - Object.instance_methods
19 | delegate :==, :as_json, *array_methods, to: :decorated_collection
20 |
21 | # @param [Enumerable] object
22 | # collection to decorate.
23 | # @option options [Class, nil] :with (nil)
24 | # the decorator class used to decorate each item. When `nil`, each item's
25 | # {Decoratable#decorate decorate} method will be used.
26 | # @option options [Hash] :context ({})
27 | # extra data to be stored in the collection decorator and used in
28 | # user-defined methods, and passed to each item's decorator.
29 | def initialize(object, options = {})
30 | options.assert_valid_keys(:with, :context)
31 | @object = object
32 | @decorator_class = options[:with]
33 | @context = options.fetch(:context, {})
34 | end
35 |
36 | class << self
37 | alias_method :decorate, :new
38 | end
39 |
40 | # @return [Array] the decorated items.
41 | def decorated_collection
42 | @decorated_collection ||= object.map{|item| decorate_item(item)}
43 | end
44 |
45 | # Delegated to the decorated collection when using the block form
46 | # (`Enumerable#find`) or to the decorator class if not
47 | # (`ActiveRecord::FinderMethods#find`)
48 | def find(*args, &block)
49 | if block_given?
50 | decorated_collection.find(*args, &block)
51 | else
52 | ActiveSupport::Deprecation.warn("Using ActiveRecord's `find` on a CollectionDecorator is deprecated. Call `find` on a model, and then decorate the result", caller)
53 | decorate_item(object.find(*args))
54 | end
55 | end
56 |
57 | def to_s
58 | "#<#{self.class.name} of #{decorator_class || "inferred decorators"} for #{object.inspect}>"
59 | end
60 |
61 | def context=(value)
62 | @context = value
63 | each {|item| item.context = value } if @decorated_collection
64 | end
65 |
66 | # @return [true]
67 | def decorated?
68 | true
69 | end
70 |
71 | alias_method :decorated_with?, :instance_of?
72 |
73 | def kind_of?(klass)
74 | decorated_collection.kind_of?(klass) || super
75 | end
76 | alias_method :is_a?, :kind_of?
77 |
78 | def replace(other)
79 | decorated_collection.replace(other)
80 | self
81 | end
82 |
83 | protected
84 |
85 | # Decorates the given item.
86 | def decorate_item(item)
87 | item_decorator.call(item, context: context)
88 | end
89 |
90 | private
91 |
92 | def item_decorator
93 | if decorator_class
94 | decorator_class.method(:decorate)
95 | else
96 | ->(item, options) { item.decorate(options) }
97 | end
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/lib/draper/view_context.rb:
--------------------------------------------------------------------------------
1 | require 'draper/view_context/build_strategy'
2 | require 'request_store'
3 |
4 | module Draper
5 | module ViewContext
6 | # Hooks into a controller or mailer to save the view context in {current}.
7 | def view_context
8 | super.tap do |context|
9 | Draper::ViewContext.current = context
10 | end
11 | end
12 |
13 | # Set the current controller
14 | def activate_draper
15 | Draper::ViewContext.controller = self
16 | end
17 |
18 | # Returns the current controller.
19 | def self.controller
20 | RequestStore.store[:current_controller]
21 | end
22 |
23 | # Sets the current controller.
24 | def self.controller=(controller)
25 | RequestStore.store[:current_controller] = controller
26 | end
27 |
28 | # Returns the current view context, or builds one if none is saved.
29 | #
30 | # @return [HelperProxy]
31 | def self.current
32 | RequestStore.store.fetch(:current_view_context) { build! }
33 | end
34 |
35 | # Sets the current view context.
36 | def self.current=(view_context)
37 | RequestStore.store[:current_view_context] = Draper::HelperProxy.new(view_context)
38 | end
39 |
40 | # Clears the saved controller and view context.
41 | def self.clear!
42 | RequestStore.store.delete :current_controller
43 | RequestStore.store.delete :current_view_context
44 | end
45 |
46 | # Builds a new view context for usage in tests. See {test_strategy} for
47 | # details of how the view context is built.
48 | def self.build
49 | build_strategy.call
50 | end
51 |
52 | # Builds a new view context and sets it as the current view context.
53 | #
54 | # @return [HelperProxy]
55 | def self.build!
56 | # send because we want to return the HelperProxy returned from #current=
57 | send :current=, build
58 | end
59 |
60 | # Configures the strategy used to build view contexts in tests, which
61 | # defaults to `:full` if `test_strategy` has not been called. Evaluates
62 | # the block, if given, in the context of the view context's class.
63 | #
64 | # @example Pass a block to add helper methods to the view context:
65 | # Draper::ViewContext.test_strategy :fast do
66 | # include ApplicationHelper
67 | # end
68 | #
69 | # @param [:full, :fast] name
70 | # the strategy to use:
71 | #
72 | # `:full` - build a fully-working view context. Your Rails environment
73 | # must be loaded, including your `ApplicationController`.
74 | #
75 | # `:fast` - build a minimal view context in tests, with no dependencies
76 | # on other components of your application.
77 | def self.test_strategy(name, &block)
78 | @build_strategy = Draper::ViewContext::BuildStrategy.new(name, &block)
79 | end
80 |
81 | # @private
82 | def self.build_strategy
83 | @build_strategy ||= Draper::ViewContext::BuildStrategy.new(:full)
84 | end
85 |
86 | # @deprecated Use {controller} instead.
87 | def self.current_controller
88 | ActiveSupport::Deprecation.warn("Draper::ViewContext.current_controller is deprecated (use controller instead)", caller)
89 | self.controller || ApplicationController.new
90 | end
91 |
92 | # @deprecated Use {controller=} instead.
93 | def self.current_controller=(controller)
94 | ActiveSupport::Deprecation.warn("Draper::ViewContext.current_controller= is deprecated (use controller instead)", caller)
95 | self.controller = controller
96 | end
97 |
98 | # @deprecated Use {build} instead.
99 | def self.build_view_context
100 | ActiveSupport::Deprecation.warn("Draper::ViewContext.build_view_context is deprecated (use build instead)", caller)
101 | build
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/spec/draper/view_context/build_strategy_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def fake_view_context
4 | double("ViewContext")
5 | end
6 |
7 | def fake_controller(view_context = fake_view_context)
8 | double("Controller", view_context: view_context, request: double("Request"))
9 | end
10 |
11 | module Draper
12 | describe ViewContext::BuildStrategy::Full do
13 | describe "#call" do
14 | context "when a current controller is set" do
15 | it "returns the controller's view context" do
16 | view_context = fake_view_context
17 | ViewContext.stub controller: fake_controller(view_context)
18 | strategy = ViewContext::BuildStrategy::Full.new
19 |
20 | expect(strategy.call).to be view_context
21 | end
22 | end
23 |
24 | context "when a current controller is not set" do
25 | it "uses ApplicationController" do
26 | view_context = fake_view_context
27 | stub_const "ApplicationController", double(new: fake_controller(view_context))
28 | strategy = ViewContext::BuildStrategy::Full.new
29 |
30 | expect(strategy.call).to be view_context
31 | end
32 | end
33 |
34 | it "adds a request if one is not defined" do
35 | controller = Class.new(ActionController::Base).new
36 | ViewContext.stub controller: controller
37 | strategy = ViewContext::BuildStrategy::Full.new
38 |
39 | expect(controller.request).to be_nil
40 | strategy.call
41 | expect(controller.request).to be_an ActionController::TestRequest
42 | expect(controller.params).to eq({})
43 |
44 | # sanity checks
45 | expect(controller.view_context.request).to be controller.request
46 | expect(controller.view_context.params).to be controller.params
47 | end
48 |
49 | it "adds methods to the view context from the constructor block" do
50 | ViewContext.stub controller: fake_controller
51 | strategy = ViewContext::BuildStrategy::Full.new do
52 | def a_helper_method; end
53 | end
54 |
55 | expect(strategy.call).to respond_to :a_helper_method
56 | end
57 |
58 | it "includes modules into the view context from the constructor block" do
59 | view_context = Object.new
60 | ViewContext.stub controller: fake_controller(view_context)
61 | helpers = Module.new do
62 | def a_helper_method; end
63 | end
64 | strategy = ViewContext::BuildStrategy::Full.new do
65 | include helpers
66 | end
67 |
68 | expect(strategy.call).to respond_to :a_helper_method
69 | end
70 | end
71 | end
72 |
73 | describe ViewContext::BuildStrategy::Fast do
74 | describe "#call" do
75 | it "returns an instance of a subclass of ActionView::Base" do
76 | strategy = ViewContext::BuildStrategy::Fast.new
77 |
78 | returned = strategy.call
79 |
80 | expect(returned).to be_an ActionView::Base
81 | expect(returned.class).not_to be ActionView::Base
82 | end
83 |
84 | it "returns different instances each time" do
85 | strategy = ViewContext::BuildStrategy::Fast.new
86 |
87 | expect(strategy.call).not_to be strategy.call
88 | end
89 |
90 | it "returns the same subclass each time" do
91 | strategy = ViewContext::BuildStrategy::Fast.new
92 |
93 | expect(strategy.call.class).to be strategy.call.class
94 | end
95 |
96 | it "adds methods to the view context from the constructor block" do
97 | strategy = ViewContext::BuildStrategy::Fast.new do
98 | def a_helper_method; end
99 | end
100 |
101 | expect(strategy.call).to respond_to :a_helper_method
102 | end
103 |
104 | it "includes modules into the view context from the constructor block" do
105 | helpers = Module.new do
106 | def a_helper_method; end
107 | end
108 | strategy = ViewContext::BuildStrategy::Fast.new do
109 | include helpers
110 | end
111 |
112 | expect(strategy.call).to respond_to :a_helper_method
113 | end
114 | end
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/spec/generators/decorator/decorator_generator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'rails'
3 | require 'ammeter/init'
4 | require 'generators/rails/decorator_generator'
5 |
6 | describe Rails::Generators::DecoratorGenerator do
7 | destination File.expand_path("../tmp", __FILE__)
8 |
9 | before { prepare_destination }
10 | after(:all) { FileUtils.rm_rf destination_root }
11 |
12 | describe "the generated decorator" do
13 | subject { file("app/decorators/your_model_decorator.rb") }
14 |
15 | describe "naming" do
16 | before { run_generator %w(YourModel) }
17 |
18 | it { should contain "class YourModelDecorator" }
19 | end
20 |
21 | describe "namespacing" do
22 | subject { file("app/decorators/namespace/your_model_decorator.rb") }
23 | before { run_generator %w(Namespace::YourModel) }
24 |
25 | it { should contain "class Namespace::YourModelDecorator" }
26 | end
27 |
28 | describe "inheritance" do
29 | context "by default" do
30 | before { run_generator %w(YourModel) }
31 |
32 | it { should contain "class YourModelDecorator < Draper::Decorator" }
33 | end
34 |
35 | context "with the --parent option" do
36 | before { run_generator %w(YourModel --parent=FooDecorator) }
37 |
38 | it { should contain "class YourModelDecorator < FooDecorator" }
39 | end
40 |
41 | context "with an ApplicationDecorator" do
42 | before do
43 | Object.any_instance.stub(:require).with("application_decorator").and_return do
44 | stub_const "ApplicationDecorator", Class.new
45 | end
46 | end
47 |
48 | before { run_generator %w(YourModel) }
49 |
50 | it { should contain "class YourModelDecorator < ApplicationDecorator" }
51 | end
52 | end
53 | end
54 |
55 | context "with -t=rspec" do
56 | describe "the generated spec" do
57 | subject { file("spec/decorators/your_model_decorator_spec.rb") }
58 |
59 | describe "naming" do
60 | before { run_generator %w(YourModel -t=rspec) }
61 |
62 | it { should contain "describe YourModelDecorator" }
63 | end
64 |
65 | describe "namespacing" do
66 | subject { file("spec/decorators/namespace/your_model_decorator_spec.rb") }
67 | before { run_generator %w(Namespace::YourModel -t=rspec) }
68 |
69 | it { should contain "describe Namespace::YourModelDecorator" }
70 | end
71 | end
72 | end
73 |
74 | context "with -t=test_unit" do
75 | describe "the generated test" do
76 | subject { file("test/decorators/your_model_decorator_test.rb") }
77 |
78 | describe "naming" do
79 | before { run_generator %w(YourModel -t=test_unit) }
80 |
81 | it { should contain "class YourModelDecoratorTest < Draper::TestCase" }
82 | end
83 |
84 | describe "namespacing" do
85 | subject { file("test/decorators/namespace/your_model_decorator_test.rb") }
86 | before { run_generator %w(Namespace::YourModel -t=test_unit) }
87 |
88 | it { should contain "class Namespace::YourModelDecoratorTest < Draper::TestCase" }
89 | end
90 | end
91 | end
92 |
93 | context "with -t=mini_test" do
94 | describe "the generated test" do
95 | subject { file("test/decorators/your_model_decorator_test.rb") }
96 |
97 | describe "naming" do
98 | before { run_generator %w(YourModel -t=mini_test) }
99 |
100 | it { should contain "class YourModelDecoratorTest < Draper::TestCase" }
101 | end
102 |
103 | describe "namespacing" do
104 | subject { file("test/decorators/namespace/your_model_decorator_test.rb") }
105 | before { run_generator %w(Namespace::YourModel -t=mini_test) }
106 |
107 | it { should contain "class Namespace::YourModelDecoratorTest < Draper::TestCase" }
108 | end
109 | end
110 | end
111 |
112 | context "with -t=mini_test --spec" do
113 | describe "the generated test" do
114 | subject { file("test/decorators/your_model_decorator_test.rb") }
115 |
116 | describe "naming" do
117 | before { run_generator %w(YourModel -t=mini_test --spec) }
118 |
119 | it { should contain "describe YourModelDecorator" }
120 | end
121 |
122 | describe "namespacing" do
123 | subject { file("test/decorators/namespace/your_model_decorator_test.rb") }
124 | before { run_generator %w(Namespace::YourModel -t=mini_test --spec) }
125 |
126 | it { should contain "describe Namespace::YourModelDecorator" }
127 | end
128 | end
129 | end
130 |
131 | end
132 |
--------------------------------------------------------------------------------
/spec/draper/view_context_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Draper
4 | describe ViewContext do
5 | describe "#view_context" do
6 | let(:base) { Class.new { def view_context; :controller_view_context; end } }
7 | let(:controller) { Class.new(base) { include ViewContext } }
8 |
9 | it "saves the superclass's view context" do
10 | ViewContext.should_receive(:current=).with(:controller_view_context)
11 | controller.new.view_context
12 | end
13 |
14 | it "returns the superclass's view context" do
15 | expect(controller.new.view_context).to be :controller_view_context
16 | end
17 | end
18 |
19 | describe ".controller" do
20 | it "returns the stored controller from RequestStore" do
21 | RequestStore.stub store: {current_controller: :stored_controller}
22 |
23 | expect(ViewContext.controller).to be :stored_controller
24 | end
25 | end
26 |
27 | describe ".controller=" do
28 | it "stores a controller in RequestStore" do
29 | store = {}
30 | RequestStore.stub store: store
31 |
32 | ViewContext.controller = :stored_controller
33 | expect(store[:current_controller]).to be :stored_controller
34 | end
35 | end
36 |
37 | describe ".current" do
38 | it "returns the stored view context from RequestStore" do
39 | RequestStore.stub store: {current_view_context: :stored_view_context}
40 |
41 | expect(ViewContext.current).to be :stored_view_context
42 | end
43 |
44 | context "when no view context is stored" do
45 | it "builds a view context" do
46 | RequestStore.stub store: {}
47 | ViewContext.stub build_strategy: ->{ :new_view_context }
48 | HelperProxy.stub(:new).with(:new_view_context).and_return(:new_helper_proxy)
49 |
50 | expect(ViewContext.current).to be :new_helper_proxy
51 | end
52 |
53 | it "stores the built view context" do
54 | store = {}
55 | RequestStore.stub store: store
56 | ViewContext.stub build_strategy: ->{ :new_view_context }
57 | HelperProxy.stub(:new).with(:new_view_context).and_return(:new_helper_proxy)
58 |
59 | ViewContext.current
60 | expect(store[:current_view_context]).to be :new_helper_proxy
61 | end
62 | end
63 | end
64 |
65 | describe ".current=" do
66 | it "stores a helper proxy for the view context in RequestStore" do
67 | store = {}
68 | RequestStore.stub store: store
69 | HelperProxy.stub(:new).with(:stored_view_context).and_return(:stored_helper_proxy)
70 |
71 | ViewContext.current = :stored_view_context
72 | expect(store[:current_view_context]).to be :stored_helper_proxy
73 | end
74 | end
75 |
76 | describe ".clear!" do
77 | it "clears the stored controller and view controller" do
78 | store = {current_controller: :stored_controller, current_view_context: :stored_view_context}
79 | RequestStore.stub store: store
80 |
81 | ViewContext.clear!
82 | expect(store).not_to have_key :current_controller
83 | expect(store).not_to have_key :current_view_context
84 | end
85 | end
86 |
87 | describe ".build" do
88 | it "returns a new view context using the build strategy" do
89 | ViewContext.stub build_strategy: ->{ :new_view_context }
90 |
91 | expect(ViewContext.build).to be :new_view_context
92 | end
93 | end
94 |
95 | describe ".build!" do
96 | it "returns a helper proxy for the new view context" do
97 | ViewContext.stub build_strategy: ->{ :new_view_context }
98 | HelperProxy.stub(:new).with(:new_view_context).and_return(:new_helper_proxy)
99 |
100 | expect(ViewContext.build!).to be :new_helper_proxy
101 | end
102 |
103 | it "stores the helper proxy" do
104 | store = {}
105 | RequestStore.stub store: store
106 | ViewContext.stub build_strategy: ->{ :new_view_context }
107 | HelperProxy.stub(:new).with(:new_view_context).and_return(:new_helper_proxy)
108 |
109 | ViewContext.build!
110 | expect(store[:current_view_context]).to be :new_helper_proxy
111 | end
112 | end
113 |
114 | describe ".build_strategy" do
115 | it "defaults to full" do
116 | expect(ViewContext.build_strategy).to be_a ViewContext::BuildStrategy::Full
117 | end
118 |
119 | it "memoizes" do
120 | expect(ViewContext.build_strategy).to be ViewContext.build_strategy
121 | end
122 | end
123 |
124 | describe ".test_strategy" do
125 | protect_module ViewContext
126 |
127 | context "with :fast" do
128 | it "creates a fast strategy" do
129 | ViewContext.test_strategy :fast
130 | expect(ViewContext.build_strategy).to be_a ViewContext::BuildStrategy::Fast
131 | end
132 |
133 | it "passes a block to the strategy" do
134 | ViewContext::BuildStrategy::Fast.stub(:new).and_return{|&block| block.call}
135 |
136 | expect(ViewContext.test_strategy(:fast){:passed}).to be :passed
137 | end
138 | end
139 |
140 | context "with :full" do
141 | it "creates a full strategy" do
142 | ViewContext.test_strategy :full
143 | expect(ViewContext.build_strategy).to be_a ViewContext::BuildStrategy::Full
144 | end
145 |
146 | it "passes a block to the strategy" do
147 | ViewContext::BuildStrategy::Full.stub(:new).and_return{|&block| block.call}
148 |
149 | expect(ViewContext.test_strategy(:full){:passed}).to be :passed
150 | end
151 | end
152 | end
153 | end
154 | end
155 |
--------------------------------------------------------------------------------
/spec/draper/finders_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Draper
4 | describe Finders do
5 | protect_class ProductDecorator
6 | before { ProductDecorator.decorates_finders }
7 |
8 | describe ".find" do
9 | it "proxies to the model class" do
10 | Product.should_receive(:find).with(1)
11 | ProductDecorator.find(1)
12 | end
13 |
14 | it "decorates the result" do
15 | found = Product.new
16 | Product.stub(:find).and_return(found)
17 | decorator = ProductDecorator.find(1)
18 | expect(decorator).to be_a ProductDecorator
19 | expect(decorator.object).to be found
20 | end
21 |
22 | it "passes context to the decorator" do
23 | Product.stub(:find)
24 | context = {some: "context"}
25 | decorator = ProductDecorator.find(1, context: context)
26 |
27 | expect(decorator.context).to be context
28 | end
29 | end
30 |
31 | describe ".find_by_(x)" do
32 | it "proxies to the model class" do
33 | Product.should_receive(:find_by_name).with("apples")
34 | ProductDecorator.find_by_name("apples")
35 | end
36 |
37 | it "decorates the result" do
38 | found = Product.new
39 | Product.stub(:find_by_name).and_return(found)
40 | decorator = ProductDecorator.find_by_name("apples")
41 | expect(decorator).to be_a ProductDecorator
42 | expect(decorator.object).to be found
43 | end
44 |
45 | it "proxies complex ProductDecorators" do
46 | Product.should_receive(:find_by_name_and_size).with("apples", "large")
47 | ProductDecorator.find_by_name_and_size("apples", "large")
48 | end
49 |
50 | it "proxies find_last_by_(x) ProductDecorators" do
51 | Product.should_receive(:find_last_by_name_and_size).with("apples", "large")
52 | ProductDecorator.find_last_by_name_and_size("apples", "large")
53 | end
54 |
55 | it "proxies find_or_initialize_by_(x) ProductDecorators" do
56 | Product.should_receive(:find_or_initialize_by_name_and_size).with("apples", "large")
57 | ProductDecorator.find_or_initialize_by_name_and_size("apples", "large")
58 | end
59 |
60 | it "proxies find_or_create_by_(x) ProductDecorators" do
61 | Product.should_receive(:find_or_create_by_name_and_size).with("apples", "large")
62 | ProductDecorator.find_or_create_by_name_and_size("apples", "large")
63 | end
64 |
65 | it "passes context to the decorator" do
66 | Product.stub(:find_by_name_and_size)
67 | context = {some: "context"}
68 | decorator = ProductDecorator.find_by_name_and_size("apples", "large", context: context)
69 |
70 | expect(decorator.context).to be context
71 | end
72 | end
73 |
74 | describe ".find_all_by_" do
75 | it "proxies to the model class" do
76 | Product.should_receive(:find_all_by_name_and_size).with("apples", "large").and_return([])
77 | ProductDecorator.find_all_by_name_and_size("apples", "large")
78 | end
79 |
80 | it "decorates the result" do
81 | found = [Product.new, Product.new]
82 | Product.stub(:find_all_by_name).and_return(found)
83 | decorator = ProductDecorator.find_all_by_name("apples")
84 |
85 | expect(decorator).to be_a Draper::CollectionDecorator
86 | expect(decorator.decorator_class).to be ProductDecorator
87 | expect(decorator).to eq found
88 | end
89 |
90 | it "passes context to the decorator" do
91 | Product.stub(:find_all_by_name)
92 | context = {some: "context"}
93 | decorator = ProductDecorator.find_all_by_name("apples", context: context)
94 |
95 | expect(decorator.context).to be context
96 | end
97 | end
98 |
99 | describe ".all" do
100 | it "returns a decorated collection" do
101 | found = [Product.new, Product.new]
102 | Product.stub all: found
103 | decorator = ProductDecorator.all
104 |
105 | expect(decorator).to be_a Draper::CollectionDecorator
106 | expect(decorator.decorator_class).to be ProductDecorator
107 | expect(decorator).to eq found
108 | end
109 |
110 | it "passes context to the decorator" do
111 | Product.stub(:all)
112 | context = {some: "context"}
113 | decorator = ProductDecorator.all(context: context)
114 |
115 | expect(decorator.context).to be context
116 | end
117 | end
118 |
119 | describe ".first" do
120 | it "proxies to the model class" do
121 | Product.should_receive(:first)
122 | ProductDecorator.first
123 | end
124 |
125 | it "decorates the result" do
126 | first = Product.new
127 | Product.stub(:first).and_return(first)
128 | decorator = ProductDecorator.first
129 | expect(decorator).to be_a ProductDecorator
130 | expect(decorator.object).to be first
131 | end
132 |
133 | it "passes context to the decorator" do
134 | Product.stub(:first)
135 | context = {some: "context"}
136 | decorator = ProductDecorator.first(context: context)
137 |
138 | expect(decorator.context).to be context
139 | end
140 | end
141 |
142 | describe ".last" do
143 | it "proxies to the model class" do
144 | Product.should_receive(:last)
145 | ProductDecorator.last
146 | end
147 |
148 | it "decorates the result" do
149 | last = Product.new
150 | Product.stub(:last).and_return(last)
151 | decorator = ProductDecorator.last
152 | expect(decorator).to be_a ProductDecorator
153 | expect(decorator.object).to be last
154 | end
155 |
156 | it "passes context to the decorator" do
157 | Product.stub(:last)
158 | context = {some: "context"}
159 | decorator = ProductDecorator.last(context: context)
160 |
161 | expect(decorator.context).to be context
162 | end
163 | end
164 |
165 | end
166 | end
167 |
--------------------------------------------------------------------------------
/spec/draper/decoratable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'support/shared_examples/decoratable_equality'
3 |
4 | module Draper
5 | describe Decoratable do
6 |
7 | describe "#decorate" do
8 | it "returns a decorator for self" do
9 | product = Product.new
10 | decorator = product.decorate
11 |
12 | expect(decorator).to be_a ProductDecorator
13 | expect(decorator.object).to be product
14 | end
15 |
16 | it "accepts context" do
17 | context = {some: "context"}
18 | decorator = Product.new.decorate(context: context)
19 |
20 | expect(decorator.context).to be context
21 | end
22 |
23 | it "uses the #decorator_class" do
24 | product = Product.new
25 | product.stub decorator_class: OtherDecorator
26 |
27 | expect(product.decorate).to be_an_instance_of OtherDecorator
28 | end
29 | end
30 |
31 | describe "#applied_decorators" do
32 | it "returns an empty list" do
33 | expect(Product.new.applied_decorators).to eq []
34 | end
35 | end
36 |
37 | describe "#decorated_with?" do
38 | it "returns false" do
39 | expect(Product.new).not_to be_decorated_with Decorator
40 | end
41 | end
42 |
43 | describe "#decorated?" do
44 | it "returns false" do
45 | expect(Product.new).not_to be_decorated
46 | end
47 | end
48 |
49 | describe "#decorator_class?" do
50 | it "returns true for decoratable model" do
51 | expect(Product.new.decorator_class?).to be_true
52 | end
53 |
54 | it "returns false for non-decoratable model" do
55 | expect(Model.new.decorator_class?).to be_false
56 | end
57 | end
58 |
59 | describe ".decorator_class?" do
60 | it "returns true for decoratable model" do
61 | expect(Product.decorator_class?).to be_true
62 | end
63 |
64 | it "returns false for non-decoratable model" do
65 | expect(Model.decorator_class?).to be_false
66 | end
67 | end
68 |
69 | describe "#decorator_class" do
70 | it "delegates to .decorator_class" do
71 | product = Product.new
72 |
73 | Product.should_receive(:decorator_class).and_return(:some_decorator)
74 | expect(product.decorator_class).to be :some_decorator
75 | end
76 | end
77 |
78 | describe "#==" do
79 | it_behaves_like "decoration-aware #==", Product.new
80 | end
81 |
82 | describe "#===" do
83 | it "is true when #== is true" do
84 | product = Product.new
85 |
86 | product.should_receive(:==).and_return(true)
87 | expect(product === :anything).to be_true
88 | end
89 |
90 | it "is false when #== is false" do
91 | product = Product.new
92 |
93 | product.should_receive(:==).and_return(false)
94 | expect(product === :anything).to be_false
95 | end
96 | end
97 |
98 | describe ".====" do
99 | it "is true for an instance" do
100 | expect(Product === Product.new).to be_true
101 | end
102 |
103 | it "is true for a derived instance" do
104 | expect(Product === Class.new(Product).new).to be_true
105 | end
106 |
107 | it "is false for an unrelated instance" do
108 | expect(Product === Model.new).to be_false
109 | end
110 |
111 | it "is true for a decorated instance" do
112 | decorator = double(object: Product.new)
113 |
114 | expect(Product === decorator).to be_true
115 | end
116 |
117 | it "is true for a decorated derived instance" do
118 | decorator = double(object: Class.new(Product).new)
119 |
120 | expect(Product === decorator).to be_true
121 | end
122 |
123 | it "is false for a decorated unrelated instance" do
124 | decorator = double(object: Model.new)
125 |
126 | expect(Product === decorator).to be_false
127 | end
128 | end
129 |
130 | describe ".decorate" do
131 | let(:scoping_method) { Rails::VERSION::MAJOR >= 4 ? :all : :scoped }
132 |
133 | it "calls #decorate_collection on .decorator_class" do
134 | scoped = [Product.new]
135 | Product.stub scoping_method => scoped
136 |
137 | Product.decorator_class.should_receive(:decorate_collection).with(scoped, with: nil).and_return(:decorated_collection)
138 | expect(Product.decorate).to be :decorated_collection
139 | end
140 |
141 | it "accepts options" do
142 | options = {with: ProductDecorator, context: {some: "context"}}
143 | Product.stub scoping_method => []
144 |
145 | Product.decorator_class.should_receive(:decorate_collection).with([], options)
146 | Product.decorate(options)
147 | end
148 | end
149 |
150 | describe ".decorator_class" do
151 | context "for classes" do
152 | it "infers the decorator from the class" do
153 | expect(Product.decorator_class).to be ProductDecorator
154 | end
155 |
156 | context "without a decorator on its own" do
157 | it "infers the decorator from a superclass" do
158 | expect(SpecialProduct.decorator_class).to be ProductDecorator
159 | end
160 | end
161 | end
162 |
163 | context "for ActiveModel classes" do
164 | it "infers the decorator from the model name" do
165 | Product.stub(:model_name).and_return("Other")
166 |
167 | expect(Product.decorator_class).to be OtherDecorator
168 | end
169 | end
170 |
171 | context "in a namespace" do
172 | context "for classes" do
173 | it "infers the decorator from the class" do
174 | expect(Namespaced::Product.decorator_class).to be Namespaced::ProductDecorator
175 | end
176 | end
177 |
178 | context "for ActiveModel classes" do
179 | it "infers the decorator from the model name" do
180 | Namespaced::Product.stub(:model_name).and_return("Namespaced::Other")
181 |
182 | expect(Namespaced::Product.decorator_class).to be Namespaced::OtherDecorator
183 | end
184 | end
185 | end
186 |
187 | context "when the decorator can't be inferred" do
188 | it "throws an UninferrableDecoratorError" do
189 | expect{Model.decorator_class}.to raise_error UninferrableDecoratorError
190 | end
191 | end
192 |
193 | context "when an unrelated NameError is thrown" do
194 | it "re-raises that error" do
195 | String.any_instance.stub(:constantize).and_return{Draper::Base}
196 | expect{Product.decorator_class}.to raise_error NameError, /Draper::Base/
197 | end
198 | end
199 | end
200 |
201 | end
202 | end
203 |
--------------------------------------------------------------------------------
/spec/draper/factory_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module Draper
4 | describe Factory do
5 |
6 | describe "#initialize" do
7 | it "accepts valid options" do
8 | valid_options = {with: Decorator, context: {foo: "bar"}}
9 | expect{Factory.new(valid_options)}.not_to raise_error
10 | end
11 |
12 | it "rejects invalid options" do
13 | expect{Factory.new(foo: "bar")}.to raise_error ArgumentError, /Unknown key/
14 | end
15 | end
16 |
17 | describe "#decorate" do
18 | context "when object is nil" do
19 | it "returns nil" do
20 | factory = Factory.new
21 |
22 | expect(factory.decorate(nil)).to be_nil
23 | end
24 | end
25 |
26 | it "calls a worker" do
27 | factory = Factory.new
28 | worker = ->(*){ :decorated }
29 |
30 | Factory::Worker.should_receive(:new).and_return(worker)
31 | expect(factory.decorate(double)).to be :decorated
32 | end
33 |
34 | it "passes the object to the worker" do
35 | factory = Factory.new
36 | object = double
37 |
38 | Factory::Worker.should_receive(:new).with(anything(), object).and_return(->(*){})
39 | factory.decorate(object)
40 | end
41 |
42 | context "when the :with option was given" do
43 | it "passes the decorator class to the worker" do
44 | decorator_class = double
45 | factory = Factory.new(with: decorator_class)
46 |
47 | Factory::Worker.should_receive(:new).with(decorator_class, anything()).and_return(->(*){})
48 | factory.decorate(double)
49 | end
50 | end
51 |
52 | context "when the :with option was omitted" do
53 | it "passes nil to the worker" do
54 | factory = Factory.new
55 |
56 | Factory::Worker.should_receive(:new).with(nil, anything()).and_return(->(*){})
57 | factory.decorate(double)
58 | end
59 | end
60 |
61 | it "passes options to the call" do
62 | factory = Factory.new
63 | worker = ->(*){}
64 | Factory::Worker.stub new: worker
65 | options = {foo: "bar"}
66 |
67 | worker.should_receive(:call).with(options)
68 | factory.decorate(double, options)
69 | end
70 |
71 | context "when the :context option was given" do
72 | it "sets the passed context" do
73 | factory = Factory.new(context: {foo: "bar"})
74 | worker = ->(*){}
75 | Factory::Worker.stub new: worker
76 |
77 | worker.should_receive(:call).with(baz: "qux", context: {foo: "bar"})
78 | factory.decorate(double, {baz: "qux"})
79 | end
80 |
81 | it "is overridden by explicitly-specified context" do
82 | factory = Factory.new(context: {foo: "bar"})
83 | worker = ->(*){}
84 | Factory::Worker.stub new: worker
85 |
86 | worker.should_receive(:call).with(context: {baz: "qux"})
87 | factory.decorate(double, context: {baz: "qux"})
88 | end
89 | end
90 | end
91 |
92 | end
93 |
94 | describe Factory::Worker do
95 |
96 | describe "#call" do
97 | it "calls the decorator method" do
98 | object = double
99 | options = {foo: "bar"}
100 | worker = Factory::Worker.new(double, object)
101 | decorator = ->(*){}
102 | worker.stub decorator: decorator
103 |
104 | decorator.should_receive(:call).with(object, options).and_return(:decorated)
105 | expect(worker.call(options)).to be :decorated
106 | end
107 |
108 | context "when the :context option is callable" do
109 | it "calls it" do
110 | worker = Factory::Worker.new(double, double)
111 | decorator = ->(*){}
112 | worker.stub decorator: decorator
113 | context = {foo: "bar"}
114 |
115 | decorator.should_receive(:call).with(anything(), context: context)
116 | worker.call(context: ->{ context })
117 | end
118 |
119 | it "receives arguments from the :context_args option" do
120 | worker = Factory::Worker.new(double, double)
121 | worker.stub decorator: ->(*){}
122 | context = ->{}
123 |
124 | context.should_receive(:call).with(:foo, :bar)
125 | worker.call(context: context, context_args: [:foo, :bar])
126 | end
127 |
128 | it "wraps non-arrays passed to :context_args" do
129 | worker = Factory::Worker.new(double, double)
130 | worker.stub decorator: ->(*){}
131 | context = ->{}
132 | hash = {foo: "bar"}
133 |
134 | context.should_receive(:call).with(hash)
135 | worker.call(context: context, context_args: hash)
136 | end
137 | end
138 |
139 | context "when the :context option is not callable" do
140 | it "doesn't call it" do
141 | worker = Factory::Worker.new(double, double)
142 | decorator = ->(*){}
143 | worker.stub decorator: decorator
144 | context = {foo: "bar"}
145 |
146 | decorator.should_receive(:call).with(anything(), context: context)
147 | worker.call(context: context)
148 | end
149 | end
150 |
151 | it "does not pass the :context_args option to the decorator" do
152 | worker = Factory::Worker.new(double, double)
153 | decorator = ->(*){}
154 | worker.stub decorator: decorator
155 |
156 | decorator.should_receive(:call).with(anything(), foo: "bar")
157 | worker.call(foo: "bar", context_args: [])
158 | end
159 | end
160 |
161 | describe "#decorator" do
162 | context "for a singular object" do
163 | context "when decorator_class is specified" do
164 | it "returns the .decorate method from the decorator" do
165 | decorator_class = Class.new(Decorator)
166 | worker = Factory::Worker.new(decorator_class, double)
167 |
168 | expect(worker.decorator).to eq decorator_class.method(:decorate)
169 | end
170 | end
171 |
172 | context "when decorator_class is unspecified" do
173 | context "and the object is decoratable" do
174 | it "returns the object's #decorate method" do
175 | object = double
176 | options = {foo: "bar"}
177 | worker = Factory::Worker.new(nil, object)
178 |
179 | object.should_receive(:decorate).with(options).and_return(:decorated)
180 | expect(worker.decorator.call(object, options)).to be :decorated
181 | end
182 | end
183 |
184 | context "and the object is not decoratable" do
185 | it "raises an error" do
186 | object = double
187 | worker = Factory::Worker.new(nil, object)
188 |
189 | expect{worker.decorator}.to raise_error UninferrableDecoratorError
190 | end
191 | end
192 | end
193 | end
194 |
195 | context "for a collection object" do
196 | context "when decorator_class is a CollectionDecorator" do
197 | it "returns the .decorate method from the collection decorator" do
198 | decorator_class = Class.new(CollectionDecorator)
199 | worker = Factory::Worker.new(decorator_class, [])
200 |
201 | expect(worker.decorator).to eq decorator_class.method(:decorate)
202 | end
203 | end
204 |
205 | context "when decorator_class is a Decorator" do
206 | it "returns the .decorate_collection method from the decorator" do
207 | decorator_class = Class.new(Decorator)
208 | worker = Factory::Worker.new(decorator_class, [])
209 |
210 | expect(worker.decorator).to eq decorator_class.method(:decorate_collection)
211 | end
212 | end
213 |
214 | context "when decorator_class is unspecified" do
215 | context "and the object is decoratable" do
216 | it "returns the .decorate_collection method from the object's decorator" do
217 | object = []
218 | decorator_class = Class.new(Decorator)
219 | object.stub decorator_class: decorator_class
220 | object.stub decorate: nil
221 | worker = Factory::Worker.new(nil, object)
222 |
223 | decorator_class.should_receive(:decorate_collection).with(object, foo: "bar", with: nil).and_return(:decorated)
224 | expect(worker.decorator.call(object, foo: "bar")).to be :decorated
225 | end
226 | end
227 |
228 | context "and the object is not decoratable" do
229 | it "returns the .decorate method from CollectionDecorator" do
230 | worker = Factory::Worker.new(nil, [])
231 |
232 | expect(worker.decorator).to eq CollectionDecorator.method(:decorate)
233 | end
234 | end
235 | end
236 | end
237 | end
238 |
239 | end
240 | end
241 |
--------------------------------------------------------------------------------
/lib/draper/decorator.rb:
--------------------------------------------------------------------------------
1 | module Draper
2 | class Decorator
3 | include Draper::ViewHelpers
4 | extend Draper::Delegation
5 |
6 | include ActiveModel::Serialization
7 | include ActiveModel::Serializers::JSON
8 | include ActiveModel::Serializers::Xml
9 |
10 | # @return the object being decorated.
11 | attr_reader :object
12 | alias_method :model, :object
13 | alias_method :source, :object # TODO: deprecate this
14 | alias_method :to_source, :object # TODO: deprecate this
15 |
16 | # @return [Hash] extra data to be used in user-defined methods.
17 | attr_accessor :context
18 |
19 | # Wraps an object in a new instance of the decorator.
20 | #
21 | # Decorators may be applied to other decorators. However, applying a
22 | # decorator to an instance of itself will create a decorator with the same
23 | # source as the original, rather than redecorating the other instance.
24 | #
25 | # @param [Object] object
26 | # object to decorate.
27 | # @option options [Hash] :context ({})
28 | # extra data to be stored in the decorator and used in user-defined
29 | # methods.
30 | def initialize(object, options = {})
31 | options.assert_valid_keys(:context)
32 | @object = object
33 | @context = options.fetch(:context, {})
34 | handle_multiple_decoration(options) if object.instance_of?(self.class)
35 | end
36 |
37 | class << self
38 | alias_method :decorate, :new
39 | end
40 |
41 | # Automatically delegates instance methods to the source object. Class
42 | # methods will be delegated to the {object_class}, if it is set.
43 | #
44 | # @return [void]
45 | def self.delegate_all
46 | include Draper::AutomaticDelegation
47 | end
48 |
49 | # Sets the source class corresponding to the decorator class.
50 | #
51 | # @note This is only necessary if you wish to proxy class methods to the
52 | # source (including when using {decorates_finders}), and the source class
53 | # cannot be inferred from the decorator class (e.g. `ProductDecorator`
54 | # maps to `Product`).
55 | # @param [String, Symbol, Class] object_class
56 | # source class (or class name) that corresponds to this decorator.
57 | # @return [void]
58 | def self.decorates(object_class)
59 | @object_class = object_class.to_s.camelize.constantize
60 | alias_object_to_object_class_name
61 | end
62 |
63 | # Returns the source class corresponding to the decorator class, as set by
64 | # {decorates}, or as inferred from the decorator class name (e.g.
65 | # `ProductDecorator` maps to `Product`).
66 | #
67 | # @return [Class] the source class that corresponds to this decorator.
68 | def self.object_class
69 | @object_class ||= inferred_object_class
70 | end
71 |
72 | # Checks whether this decorator class has a corresponding {object_class}.
73 | def self.object_class?
74 | object_class
75 | rescue Draper::UninferrableSourceError
76 | false
77 | end
78 |
79 | class << self # TODO deprecate this
80 | alias_method :source_class, :object_class
81 | alias_method :source_class?, :object_class?
82 | end
83 |
84 | # Automatically decorates ActiveRecord finder methods, so that you can use
85 | # `ProductDecorator.find(id)` instead of
86 | # `ProductDecorator.decorate(Product.find(id))`.
87 | #
88 | # Finder methods are applied to the {object_class}.
89 | #
90 | # @return [void]
91 | def self.decorates_finders
92 | extend Draper::Finders
93 | end
94 |
95 | # Automatically decorate an association.
96 | #
97 | # @param [Symbol] association
98 | # name of the association to decorate (e.g. `:products`).
99 | # @option options [Class] :with
100 | # the decorator to apply to the association.
101 | # @option options [Symbol] :scope
102 | # a scope to apply when fetching the association.
103 | # @option options [Hash, #call] :context
104 | # extra data to be stored in the associated decorator. If omitted, the
105 | # associated decorator's context will be the same as the parent
106 | # decorator's. If a Proc is given, it will be called with the parent's
107 | # context and should return a new context hash for the association.
108 | # @return [void]
109 | def self.decorates_association(association, options = {})
110 | options.assert_valid_keys(:with, :scope, :context)
111 | define_method(association) do
112 | decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options)
113 | decorated_associations[association].call
114 | end
115 | end
116 |
117 | # @overload decorates_associations(*associations, options = {})
118 | # Automatically decorate multiple associations.
119 | # @param [Symbols*] associations
120 | # names of the associations to decorate.
121 | # @param [Hash] options
122 | # see {decorates_association}.
123 | # @return [void]
124 | def self.decorates_associations(*associations)
125 | options = associations.extract_options!
126 | associations.each do |association|
127 | decorates_association(association, options)
128 | end
129 | end
130 |
131 | # Decorates a collection of objects. The class of the collection decorator
132 | # is inferred from the decorator class if possible (e.g. `ProductDecorator`
133 | # maps to `ProductsDecorator`), but otherwise defaults to
134 | # {Draper::CollectionDecorator}.
135 | #
136 | # @param [Object] object
137 | # collection to decorate.
138 | # @option options [Class, nil] :with (self)
139 | # the decorator class used to decorate each item. When `nil`, it is
140 | # inferred from each item.
141 | # @option options [Hash] :context
142 | # extra data to be stored in the collection decorator.
143 | def self.decorate_collection(object, options = {})
144 | options.assert_valid_keys(:with, :context)
145 | collection_decorator_class.new(object, options.reverse_merge(with: self))
146 | end
147 |
148 | # @return [Array] the list of decorators that have been applied to
149 | # the object.
150 | def applied_decorators
151 | chain = object.respond_to?(:applied_decorators) ? object.applied_decorators : []
152 | chain << self.class
153 | end
154 |
155 | # Checks if a given decorator has been applied to the object.
156 | #
157 | # @param [Class] decorator_class
158 | def decorated_with?(decorator_class)
159 | applied_decorators.include?(decorator_class)
160 | end
161 |
162 | # Checks if this object is decorated.
163 | #
164 | # @return [true]
165 | def decorated?
166 | true
167 | end
168 |
169 | # Compares the source object with a possibly-decorated object.
170 | #
171 | # @return [Boolean]
172 | def ==(other)
173 | Draper::Decoratable::Equality.test(object, other)
174 | end
175 |
176 | # Checks if `self.kind_of?(klass)` or `object.kind_of?(klass)`
177 | #
178 | # @param [Class] klass
179 | def kind_of?(klass)
180 | super || object.kind_of?(klass)
181 | end
182 | alias_method :is_a?, :kind_of?
183 |
184 | # Checks if `self.instance_of?(klass)` or `object.instance_of?(klass)`
185 | #
186 | # @param [Class] klass
187 | def instance_of?(klass)
188 | super || object.instance_of?(klass)
189 | end
190 |
191 | if RUBY_VERSION < "2.0"
192 | # nasty hack to stop 1.9.x using the delegated `to_s` in `inspect`
193 | alias_method :_to_s, :to_s
194 |
195 | def inspect
196 | ivars = instance_variables.map do |name|
197 | "#{name}=#{instance_variable_get(name).inspect}"
198 | end
199 | _to_s.insert(-2, " #{ivars.join(", ")}")
200 | end
201 | end
202 |
203 | delegate :to_s
204 |
205 | # In case object is nil
206 | delegate :present?, :blank?
207 |
208 | # ActiveModel compatibility
209 | # @private
210 | def to_model
211 | self
212 | end
213 |
214 | # @return [Hash] the object's attributes, sliced to only include those
215 | # implemented by the decorator.
216 | def attributes
217 | object.attributes.select {|attribute, _| respond_to?(attribute) }
218 | end
219 |
220 | # ActiveModel compatibility
221 | delegate :to_param, :to_partial_path
222 |
223 | # ActiveModel compatibility
224 | singleton_class.delegate :model_name, to: :object_class
225 |
226 | # @return [Class] the class created by {decorate_collection}.
227 | def self.collection_decorator_class
228 | name = collection_decorator_name
229 | name.constantize
230 | rescue NameError => error
231 | raise if name && !error.missing_name?(name)
232 | Draper::CollectionDecorator
233 | end
234 |
235 | private
236 |
237 | def self.inherited(subclass)
238 | subclass.alias_object_to_object_class_name
239 | super
240 | end
241 |
242 | def self.alias_object_to_object_class_name
243 | alias_method object_class.name.underscore, :object if object_class?
244 | end
245 |
246 | def self.object_class_name
247 | raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/
248 | name.chomp("Decorator")
249 | end
250 |
251 | def self.inferred_object_class
252 | name = object_class_name
253 | name.constantize
254 | rescue NameError => error
255 | raise if name && !error.missing_name?(name)
256 | raise Draper::UninferrableSourceError.new(self)
257 | end
258 |
259 | def self.collection_decorator_name
260 | plural = object_class_name.pluralize
261 | raise NameError if plural == object_class_name
262 | "#{plural}Decorator"
263 | end
264 |
265 | def handle_multiple_decoration(options)
266 | if object.applied_decorators.last == self.class
267 | @context = object.context unless options.has_key?(:context)
268 | @object = object.object
269 | else
270 | warn "Reapplying #{self.class} decorator to target that is already decorated with it. Call stack:\n#{caller(1).join("\n")}"
271 | end
272 | end
273 |
274 | def decorated_associations
275 | @decorated_associations ||= {}
276 | end
277 | end
278 | end
279 |
--------------------------------------------------------------------------------
/spec/draper/collection_decorator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'support/shared_examples/view_helpers'
3 |
4 | module Draper
5 | describe CollectionDecorator do
6 | it_behaves_like "view helpers", CollectionDecorator.new([])
7 |
8 | describe "#initialize" do
9 | describe "options validation" do
10 |
11 | it "does not raise error on valid options" do
12 | valid_options = {with: Decorator, context: {}}
13 | expect{CollectionDecorator.new([], valid_options)}.not_to raise_error
14 | end
15 |
16 | it "raises error on invalid options" do
17 | expect{CollectionDecorator.new([], foo: "bar")}.to raise_error ArgumentError, /Unknown key/
18 | end
19 | end
20 | end
21 |
22 | context "with context" do
23 | it "stores the context itself" do
24 | context = {some: "context"}
25 | decorator = CollectionDecorator.new([], context: context)
26 |
27 | expect(decorator.context).to be context
28 | end
29 |
30 | it "passes context to the individual decorators" do
31 | context = {some: "context"}
32 | decorator = CollectionDecorator.new([Product.new, Product.new], context: context)
33 |
34 | decorator.each do |item|
35 | expect(item.context).to be context
36 | end
37 | end
38 | end
39 |
40 | describe "#context=" do
41 | it "updates the stored context" do
42 | decorator = CollectionDecorator.new([], context: {some: "context"})
43 | new_context = {other: "context"}
44 |
45 | decorator.context = new_context
46 | expect(decorator.context).to be new_context
47 | end
48 |
49 | context "when the collection is already decorated" do
50 | it "updates the items' context" do
51 | decorator = CollectionDecorator.new([Product.new, Product.new], context: {some: "context"})
52 | decorator.decorated_collection # trigger decoration
53 | new_context = {other: "context"}
54 |
55 | decorator.context = new_context
56 | decorator.each do |item|
57 | expect(item.context).to be new_context
58 | end
59 | end
60 | end
61 |
62 | context "when the collection has not yet been decorated" do
63 | it "does not trigger decoration" do
64 | decorator = CollectionDecorator.new([])
65 |
66 | decorator.should_not_receive(:decorated_collection)
67 | decorator.context = {other: "context"}
68 | end
69 |
70 | it "sets context after decoration is triggered" do
71 | decorator = CollectionDecorator.new([Product.new, Product.new], context: {some: "context"})
72 | new_context = {other: "context"}
73 |
74 | decorator.context = new_context
75 | decorator.each do |item|
76 | expect(item.context).to be new_context
77 | end
78 | end
79 | end
80 | end
81 |
82 | describe "item decoration" do
83 | it "sets decorated items' source models" do
84 | collection = [Product.new, Product.new]
85 | decorator = CollectionDecorator.new(collection)
86 |
87 | decorator.zip collection do |item, object|
88 | expect(item.object).to be object
89 | end
90 | end
91 |
92 | context "when the :with option was given" do
93 | it "uses the :with option" do
94 | decorator = CollectionDecorator.new([Product.new], with: OtherDecorator).first
95 |
96 | expect(decorator).to be_decorated_with OtherDecorator
97 | end
98 | end
99 |
100 | context "when the :with option was not given" do
101 | it "infers the item decorator from each item" do
102 | decorator = CollectionDecorator.new([double(decorate: :inferred_decorator)]).first
103 |
104 | expect(decorator).to be :inferred_decorator
105 | end
106 | end
107 | end
108 |
109 | describe ".delegate" do
110 | protect_class ProductsDecorator
111 |
112 | it "defaults the :to option to :object" do
113 | Object.should_receive(:delegate).with(:foo, :bar, to: :object)
114 | ProductsDecorator.delegate :foo, :bar
115 | end
116 |
117 | it "does not overwrite the :to option if supplied" do
118 | Object.should_receive(:delegate).with(:foo, :bar, to: :baz)
119 | ProductsDecorator.delegate :foo, :bar, to: :baz
120 | end
121 | end
122 |
123 | describe "#find" do
124 | context "with a block" do
125 | it "decorates Enumerable#find" do
126 | decorator = CollectionDecorator.new([])
127 |
128 | decorator.decorated_collection.should_receive(:find).and_return(:delegated)
129 | expect(decorator.find{|p| p.title == "title"}).to be :delegated
130 | end
131 | end
132 |
133 | context "without a block" do
134 | it "decorates object.find" do
135 | object = []
136 | found = double(decorate: :decorated)
137 | decorator = CollectionDecorator.new(object)
138 |
139 | object.should_receive(:find).and_return(found)
140 | ActiveSupport::Deprecation.silence do
141 | expect(decorator.find(1)).to be :decorated
142 | end
143 | end
144 | end
145 | end
146 |
147 | describe "#to_ary" do
148 | # required for `render @collection` in Rails
149 | it "delegates to the decorated collection" do
150 | decorator = CollectionDecorator.new([])
151 |
152 | decorator.decorated_collection.should_receive(:to_ary).and_return(:delegated)
153 | expect(decorator.to_ary).to be :delegated
154 | end
155 | end
156 |
157 | it "delegates array methods to the decorated collection" do
158 | decorator = CollectionDecorator.new([])
159 |
160 | decorator.decorated_collection.should_receive(:[]).with(42).and_return(:delegated)
161 | expect(decorator[42]).to be :delegated
162 | end
163 |
164 | describe "#==" do
165 | context "when comparing to a collection decorator with the same object" do
166 | it "returns true" do
167 | object = [Product.new, Product.new]
168 | decorator = CollectionDecorator.new(object)
169 | other = ProductsDecorator.new(object)
170 |
171 | expect(decorator == other).to be_true
172 | end
173 | end
174 |
175 | context "when comparing to a collection decorator with a different object" do
176 | it "returns false" do
177 | decorator = CollectionDecorator.new([Product.new, Product.new])
178 | other = ProductsDecorator.new([Product.new, Product.new])
179 |
180 | expect(decorator == other).to be_false
181 | end
182 | end
183 |
184 | context "when comparing to a collection of the same items" do
185 | it "returns true" do
186 | object = [Product.new, Product.new]
187 | decorator = CollectionDecorator.new(object)
188 | other = object.dup
189 |
190 | expect(decorator == other).to be_true
191 | end
192 | end
193 |
194 | context "when comparing to a collection of different items" do
195 | it "returns false" do
196 | decorator = CollectionDecorator.new([Product.new, Product.new])
197 | other = [Product.new, Product.new]
198 |
199 | expect(decorator == other).to be_false
200 | end
201 | end
202 |
203 | context "when the decorated collection has been modified" do
204 | it "is no longer equal to the object" do
205 | object = [Product.new, Product.new]
206 | decorator = CollectionDecorator.new(object)
207 | other = object.dup
208 |
209 | decorator << Product.new.decorate
210 | expect(decorator == other).to be_false
211 | end
212 | end
213 | end
214 |
215 | describe "#to_s" do
216 | context "when :with option was given" do
217 | it "returns a string representation of the collection decorator" do
218 | decorator = CollectionDecorator.new(["a", "b", "c"], with: ProductDecorator)
219 |
220 | expect(decorator.to_s).to eq '#'
221 | end
222 | end
223 |
224 | context "when :with option was not given" do
225 | it "returns a string representation of the collection decorator" do
226 | decorator = CollectionDecorator.new(["a", "b", "c"])
227 |
228 | expect(decorator.to_s).to eq '#'
229 | end
230 | end
231 |
232 | context "for a custom subclass" do
233 | it "uses the custom class name" do
234 | decorator = ProductsDecorator.new([])
235 |
236 | expect(decorator.to_s).to match /ProductsDecorator/
237 | end
238 | end
239 | end
240 |
241 | describe '#object' do
242 | it 'returns the underlying collection' do
243 | collection = [Product.new]
244 | decorator = ProductsDecorator.new(collection)
245 |
246 | expect(decorator.object).to eq collection
247 | end
248 | end
249 |
250 | describe '#decorated?' do
251 | it 'returns true' do
252 | decorator = ProductsDecorator.new([Product.new])
253 |
254 | expect(decorator).to be_decorated
255 | end
256 | end
257 |
258 | describe '#decorated_with?' do
259 | it "checks if a decorator has been applied to a collection" do
260 | decorator = ProductsDecorator.new([Product.new])
261 |
262 | expect(decorator).to be_decorated_with ProductsDecorator
263 | expect(decorator).not_to be_decorated_with OtherDecorator
264 | end
265 | end
266 |
267 | describe '#kind_of?' do
268 | it 'asks the kind of its decorated collection' do
269 | decorator = ProductsDecorator.new([])
270 | decorator.decorated_collection.should_receive(:kind_of?).with(Array).and_return("true")
271 | expect(decorator.kind_of?(Array)).to eq "true"
272 | end
273 |
274 | context 'when asking the underlying collection returns false' do
275 | it 'asks the CollectionDecorator instance itself' do
276 | decorator = ProductsDecorator.new([])
277 | decorator.decorated_collection.stub(:kind_of?).with(::Draper::CollectionDecorator).and_return(false)
278 | expect(decorator.kind_of?(::Draper::CollectionDecorator)).to be true
279 | end
280 | end
281 | end
282 |
283 | describe '#is_a?' do
284 | it 'aliases to #kind_of?' do
285 | decorator = ProductsDecorator.new([])
286 | expect(decorator.method(:kind_of?)).to eq decorator.method(:is_a?)
287 | end
288 | end
289 |
290 | describe "#replace" do
291 | it "replaces the decorated collection" do
292 | decorator = CollectionDecorator.new([Product.new])
293 | replacement = [:foo, :bar]
294 |
295 | decorator.replace replacement
296 | expect(decorator).to match_array replacement
297 | end
298 |
299 | it "returns itself" do
300 | decorator = CollectionDecorator.new([Product.new])
301 |
302 | expect(decorator.replace([:foo, :bar])).to be decorator
303 | end
304 | end
305 |
306 | end
307 | end
308 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Draper Changelog
2 |
3 | ## 1.3.0
4 |
5 | [30 commits by 11 authors](https://github.com/drapergem/draper/compare/v1.2.1...v1.3.0)
6 |
7 | * [Add `decorator_class?` method](https://github.com/drapergem/draper/commit/53e1df5c3ee169144a2778c6d2ee13cc6af99429)
8 |
9 | * [Clear ViewContext before specs instead of after](https://github.com/drapergem/draper/pull/547)
10 |
11 | * [Add alias method `#{model_name}` to the decorated object ](https://github.com/drapergem/draper/commit/f5f27243c3f0c37bff4872e1e78521e570ff1e4f)
12 |
13 | * [Hook into `controller_generator` and `scaffold_controller_generator` instead of resource generator](https://github.com/drapergem/draper/commit/cd4f298987c3b4ad8046563ad4a087055fc7efe2)
14 |
15 | * [Delegate `to_s` method to the object](https://github.com/drapergem/draper/commit/58b8181050c2a9a86f54660e7bb6bfefa5fd0b64)
16 |
17 | ## 1.2.1
18 |
19 | [28 commits by 4 authors](https://github.com/drapergem/draper/compare/v1.2.0...v1.2.1)
20 |
21 | * [Document stubbing route helpers](https://github.com/drapergem/draper/commit/dbe8a81ca7d4d9ae87b4b62926a0ba6379397fbc)
22 |
23 | * [Rename `source` to `object`. `source` still works, but will be deprecated in a future release.](https://github.com/drapergem/draper/commit/4b933ef39d252ecfe93c573a072633be545c49fb)
24 |
25 | Various bugfixes, as always.
26 |
27 | ## 1.2.0
28 |
29 | [78 commits by 14 authors](https://github.com/drapergem/draper/compare/v1.1.0...v1.2.0)
30 |
31 | * [Added license to the gemspec](https://github.com/drapergem/draper/commit/731fa85f7744a12da1364a3aa94cdf6994efa9e2)
32 |
33 | * [Improvements in serialization](https://github.com/drapergem/draper/pull/448)
34 |
35 | * [Fix support for Guard in development](https://github.com/drapergem/draper/commit/93c95200277fd3922e30e74c7a7e05563747e896)
36 |
37 | * [Improved support for #capture](https://github.com/drapergem/draper/commit/efb934a6f59b9d8afe4a7fe29e9a94aae983b05c)
38 |
39 | * [CollectionDecorator now has #decorated?](https://github.com/drapergem/draper/commit/65e3c4e4573173b510440b7e80f95a87109a1d15)
40 |
41 | * [Added #respond_to_missing?](https://github.com/drapergem/draper/commit/11bb59bcf89f8e0be4ba2851eb78634caf783ecd)
42 |
43 | * [Add proper STI support](https://github.com/drapergem/draper/commit/7802d97446cf6ea130a66c2781f5a7a087d28c0a)
44 |
45 | * [Added before_remove_const callback for ActiveSupport](https://github.com/drapergem/draper/commit/9efda27c6a4f8680df4fad8f67ecb58e3f91703f)
46 |
47 | * [Accept a lambda for context](https://github.com/drapergem/draper/commit/3ab3b35e875b8b2bd99a57e4add388313e765230)
48 |
49 | * [Start testing against Ruby 2.0.0](https://github.com/drapergem/draper/commit/dbd1cbceedb0ddb3f060196cd31eb967db8a370b)
50 |
51 | * [Fix our use of #scoped per Rails 4 deprecation](https://github.com/haines/draper/commit/47ee29c3b739eee3fc28a561432ad2363c14813c)
52 |
53 | * [Properly swallow NameErrors](https://github.com/haines/draper/commit/09ad84fb712a30dd4302b9daa03d11281ac7d169)
54 |
55 | * [Delegate CollectionDecorator#kind_of? to the underlying collection](https://github.com/drapergem/draper/commit/d5975f2c5b810306a96d9485fd39d550896dc2a1)
56 |
57 | * [Make CollectionDecorators respond to #decorated_with?](https://github.com/drapergem/draper/commit/dc430b3e82de0d9cae86f7297f816e5b69d9ca58)
58 |
59 | * [Avoid using #extend in Decorator#== for performance reasons](https://github.com/drapergem/draper/commit/205a0d43b4141f7b1756fe2b44b877545eb37517)
60 |
61 | ## 1.1.0
62 |
63 | [44 commits by 6 authors](https://github.com/drapergem/draper/compare/v1.0.0...v1.1.0)
64 |
65 | * README improvements.
66 | * Rails 4 compatibility.
67 | [b2401c7](https://github.com/drapergem/draper/commit/b2401c71e092470e3b912b5da475115c22b55734)
68 | * Added support for testing decorators without loading `ApplicationController`.
69 | See the [README](https://github.com/drapergem/draper/blob/v1.1.0/README.md#isolated-tests) for details.
70 | [4d0181f](https://github.com/drapergem/draper/commit/4d0181fb9c65dc769b05ed19bfcec2119d6e88f7)
71 | * Improved the `==` method to check for `decorated?` before attempting to
72 | compare with `source`.
73 | [6c31617](https://github.com/drapergem/draper/commit/6c316176f5039a5861491fbcaa81f64ac4b36768)
74 | * Changed the way helpers are accessed, so that helper methods may be stubbed
75 | in tests.
76 | [7a04619](https://github.com/drapergem/draper/commit/7a04619a06f832801bd4aedaaf5985d6e3e5e1af)
77 | * Made the Devise test helper `sign_in` method independent of mocking
78 | framework.
79 | [b0902ab](https://github.com/drapergem/draper/commit/b0902ab0fe01916b7fddb0a3d97aa0c7cca09482)
80 | * Stopped attempting to call `to_a` on decorated objects (a now-redundant lazy
81 | query workaround).
82 | [34c6390](https://github.com/drapergem/draper/commit/34c6390583f7fc7704d04e38bc974b65fc92517c)
83 | * Fixed a minor bug where view contexts could be accidentally shared between
84 | tests.
85 | [3d07cb3](https://github.com/drapergem/draper/commit/3d07cb387b1cae6f97897dfb85512e30f5e888e9)
86 | * Improved helper method performance.
87 | [e6f88a5](https://github.com/drapergem/draper/commit/e6f88a5e7dada3f9db480e13e16d1acc964ba098)
88 | * Our specs now use the new RSpec `expect( ).to` syntax.
89 | [9a3b319](https://github.com/drapergem/draper/commit/9a3b319d6d54cd78fb2654a94bbe893e36359754)
90 |
91 | ## 1.0.0
92 |
93 | [249 commits by 19 authors](https://github.com/drapergem/draper/compare/v0.18.0...v1.0.0)
94 |
95 | Major changes are described [in the upgrade guide](https://github.com/drapergem/draper/wiki/Upgrading-to-1.0).
96 |
97 | * Infer collection decorators.
98 | [e8253df](https://github.com/drapergem/draper/commit/e8253df7dc6c90a542444c0f4ef289909fce4f90)
99 | * Prevent calls to `scoped` on decorated associations.
100 | [5dcc6c3](https://github.com/drapergem/draper/commit/5dcc6c31ecf408753158d15fed9fb23fbfdc3734)
101 | * Add `helper` method to tests.
102 | [551961e](https://github.com/drapergem/draper/commit/551961e72ee92355bc9c848bedfcc573856d12b0)
103 | * Inherit method security.
104 | [1865ed3](https://github.com/drapergem/draper/commit/1865ed3e3b2b34853689a60b59b8ce9145674d1d)
105 | * Test against all versions of Rails 3.
106 | [1865ed3](https://github.com/drapergem/draper/commit/1865ed3e3b2b34853689a60b59b8ce9145674d1d)
107 | * Pretend to be `instance_of?(source.class)`.
108 | [30d209f](https://github.com/drapergem/draper/commit/30d209f990847e84b221ac798e84b976f5775cc0)
109 | * Remove security from `Decorator`. Do manual delegation with `:delegate`.
110 | [c6f8aaa](https://github.com/drapergem/draper/commit/c6f8aaa2b2bd4679738050aede2503aa8e9db130)
111 | * Add generators for MiniTest.
112 | [1fac02b](https://github.com/drapergem/draper/commit/1fac02b65b15e32f06e8292cb858c97cb1c1da2c)
113 | * Test against edge rails.
114 | [e9b71e3](https://github.com/drapergem/draper/commit/e9b71e3cf55a800b48c083ff257a7c1cbe1b601b)
115 |
116 | ### 1.0.0.beta6
117 |
118 | * Fix up README to include changes made.
119 | [5e6e4d1](https://github.com/drapergem/draper/commit/5e6e4d11b1e0c07c12b6b1e87053bc3f50ef2ab6)
120 | * `CollectionDecorator` no longer freezes its collection: direct access is
121 | discouraged by making access private.
122 | [c6d60e6](https://github.com/drapergem/draper/commit/c6d60e6577ed396385f3f1151c3f188fe47e9a57)
123 | * A fix for `Decoratable#==`.
124 | [e4fa239](https://github.com/drapergem/draper/commit/e4fa239d84e8e9d6a490d785abb3953acc28fa65)
125 | * Ensure we coerce to an array in the right place.
126 | [9eb9fc9](https://github.com/drapergem/draper/commit/9eb9fc909c372ea1c2392d05594fa75a5c08b095)
127 |
128 | ### 1.0.0.beta5
129 |
130 | * Change CollectionDecorator to freeze its collection.
131 | [04d7796](https://github.com/drapergem/draper/commit/04d779615c43580409083a71661489e1bbf91ad4)
132 | * Bugfix on `CollectionDecorator#to_s`.
133 | [eefd7d0](https://github.com/drapergem/draper/commit/eefd7d09cac97d531b9235246378c3746d153f08)
134 | * Upgrade `request_store` dependency to take advantage of a bugfix.
135 | [9f17212](https://github.com/drapergem/draper/commit/9f17212fd1fb656ef1314327d60fe45e0acf60a2)
136 |
137 | ### 1.0.0.beta4
138 |
139 | * Fixed a race condition with capybara integration.
140 | [e794649](https://github.com/drapergem/draper/commit/e79464931e7b98c85ed5d78ed9ca38d51f43006e)
141 | * `[]` can be decorated again.
142 | [597fbdf](https://github.com/drapergem/draper/commit/597fbdf0c80583f5ea6df9f7350fefeaa0cca989)
143 | * `model == decorator` as well as `decorator == model`.
144 | [46f8a68](https://github.com/drapergem/draper/commit/46f8a6823c50c13e5c9ab3c07723f335c4e291bc)
145 | * Preliminary Mongoid integration.
146 | [892d195](https://github.com/drapergem/draper/commit/892d1954202c61fd082a07213c8d4a23560687bc)
147 | * Add a helper method `sign_in` for devise in decorator specs.
148 | [66a3009](https://github.com/drapergem/draper/commit/66a30093ed4207d02d8fa60bda4df2da091d85a3)
149 | * Brought back `context`.
150 | [9609156](https://github.com/drapergem/draper/commit/9609156b997b3a469386eef3a5f043b24d8a2fba)
151 | * Fixed issue where classes were incorrectly being looked up.
152 | [ee2a015](https://github.com/drapergem/draper/commit/ee2a015514ff87dfd2158926457e988c2fc3fd79)
153 | * Integrate RequestStore for per-request storage.
154 | [fde1cde](https://github.com/drapergem/draper/commit/fde1cde9adfb856750c1f616d8b62d221ef97fc6)
155 |
156 | ### 1.0.0.beta3
157 |
158 | * Relaxed Rails version requirement to 3.0. Support for < 3.2 should be
159 | considered experimental. Please file bug reports.
160 |
161 | ### 1.0.0.beta2
162 |
163 | * `has_finders` is now `decorates_finders`.
164 | [33f18aa](https://github.com/drapergem/draper/commit/33f18aa062e0d3848443dbd81047f20d5665579f)
165 | * If a finder method is used, and the source class is not set and cannot be
166 | inferred, an `UninferrableSourceError` is raised.
167 | [8ef5bf2](https://github.com/drapergem/draper/commit/8ef5bf2f02f7033e3cd4f1f5de7397b02c984fe3)
168 | * Class methods are now properly delegated again.
169 | [731995a](https://github.com/drapergem/draper/commit/731995a5feac4cd06cf9328d2892c0eca9992db6)
170 | * We no longer `respond_to?` private methods on the source.
171 | [18ebac8](https://github.com/drapergem/draper/commit/18ebac81533a6413aa20a3c26f23e91d0b12b031)
172 | * Rails versioning relaxed to support Rails 4.
173 | [8bfd393](https://github.com/drapergem/draper/commit/8bfd393b5baa7aa1488076a5e2cb88648efaa815)
174 |
175 | ### 1.0.0.beta1
176 |
177 | * Renaming `Draper::Base` to `Draper::Decorator`. This is the most significant
178 | change you'll need to upgrade your application.
179 | [025742c](https://github.com/drapergem/draper/commit/025742cb3b295d259cf0ecf3669c24817d6f2df1)
180 | * Added an internal Rails application for integration tests. This won't affect
181 | your application, but we're now running a set of Cucumber tests inside of a
182 | Rails app in both development and production mode to help ensure that we
183 | don't make changes that break Draper.
184 | [90a4859](https://github.com/drapergem/draper/commit/90a4859085cab158658d23d77cd3108b6037e36f)
185 | * Add `#decorated?` method. This gives us a free RSpec matcher,
186 | `be_decorated`.
187 | [834a6fd](https://github.com/drapergem/draper/commit/834a6fd1f24b5646c333a04a99fe9846a58965d6)
188 | * `#decorates` is no longer needed inside your models, and should be removed.
189 | Decorators automatically infer the class they decorate.
190 | [e1214d9](https://github.com/drapergem/draper/commit/e1214d97b62f2cab45227cc650029734160dcdfe)
191 | * Decorators do not automatically come with 'finders' by default. If you'd like
192 | to use `SomeDecorator.find(1)`, for example, simply add `#has_finders` to
193 | the decorator to include them.
194 | [42b6f78](https://github.com/drapergem/draper/commit/42b6f78fda4f51845dab4d35da68880f1989d178)
195 | * To refer to the object being decorated, `#source` is now the preferred
196 | method.
197 | [1e84fcb](https://github.com/drapergem/draper/commit/1e84fcb4a0eab0d12f5feda6886ce1caa239cb16)
198 | * `ActiveModel::Serialization` is included in Decorators if you've requred
199 | `ActiveModel::Serializers`, so that decorators can be serialized.
200 | [c4b3527](https://github.com/drapergem/draper/commit/c4b352799067506849abcbf14963ea36abda301c)
201 | * Properly support Test::Unit.
202 | [087e134](https://github.com/drapergem/draper/commit/087e134ed0885ec11325ffabe8ab2bebef77a33a)
203 |
204 | And many small bug fixes and refactorings.
205 |
206 | ## 0.x
207 |
208 | See changes prior to version 1.0 [here](https://github.com/drapergem/draper/blob/16140fed55f57d18f8b10a0789dd1fa5b3115a8d/CHANGELOG.markdown).
209 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Draper: View Models for Rails
2 |
3 | **Announcement**: Draper is looking for a new maintainer. If you're interested, please [join the conversation](https://github.com/drapergem/draper/issues/621).
4 |
5 | [](http://travis-ci.org/drapergem/draper)
6 | [](https://codeclimate.com/github/drapergem/draper)
7 |
8 | Draper adds an object-oriented layer of presentation logic to your Rails
9 | application.
10 |
11 | Without Draper, this functionality might have been tangled up in procedural
12 | helpers or adding bulk to your models. With Draper decorators, you can wrap your
13 | models with presentation-related logic to organise - and test - this layer of
14 | your app much more effectively.
15 |
16 | ## Why Use a Decorator?
17 |
18 | Imagine your application has an `Article` model. With Draper, you'd create a
19 | corresponding `ArticleDecorator`. The decorator wraps the model, and deals
20 | *only* with presentational concerns. In the controller, you decorate the article
21 | before handing it off to the view:
22 |
23 | ```ruby
24 | # app/controllers/articles_controller.rb
25 | def show
26 | @article = Article.find(params[:id]).decorate
27 | end
28 | ```
29 |
30 | In the view, you can use the decorator in exactly the same way as you would have
31 | used the model. But whenever you start needing logic in the view or start
32 | thinking about a helper method, you can implement a method on the decorator
33 | instead.
34 |
35 | Let's look at how you could convert an existing Rails helper to a decorator
36 | method. You have this existing helper:
37 |
38 | ```ruby
39 | # app/helpers/articles_helper.rb
40 | def publication_status(article)
41 | if article.published?
42 | "Published at #{article.published_at.strftime('%A, %B %e')}"
43 | else
44 | "Unpublished"
45 | end
46 | end
47 | ```
48 |
49 | But it makes you a little uncomfortable. `publication_status` lives in a
50 | nebulous namespace spread across all controllers and view. Down the road, you
51 | might want to display the publication status of a `Book`. And, of course, your
52 | design calls for a slighly different formatting to the date for a `Book`.
53 |
54 | Now your helper method can either switch based on the input class type (poor
55 | Ruby style), or you break it out into two methods, `book_publication_status` and
56 | `article_publication_status`. And keep adding methods for each publication
57 | type...to the global helper namespace. And you'll have to remember all the names. Ick.
58 |
59 | Ruby thrives when we use Object-Oriented style. If you didn't know Rails'
60 | helpers existed, you'd probably imagine that your view template could feature
61 | something like this:
62 |
63 | ```erb
64 | <%= @article.publication_status %>
65 | ```
66 |
67 | Without a decorator, you'd have to implement the `publication_status` method in
68 | the `Article` model. That method is presentation-centric, and thus does not
69 | belong in a model.
70 |
71 | Instead, you implement a decorator:
72 |
73 | ```ruby
74 | # app/decorators/article_decorator.rb
75 | class ArticleDecorator < Draper::Decorator
76 | delegate_all
77 |
78 | def publication_status
79 | if published?
80 | "Published at #{published_at}"
81 | else
82 | "Unpublished"
83 | end
84 | end
85 |
86 | def published_at
87 | object.published_at.strftime("%A, %B %e")
88 | end
89 | end
90 | ```
91 |
92 | Within the `publication_status` method we use the `published?` method. Where
93 | does that come from? It's a method of the source `Article`, whose methods have
94 | been made available on the decorator by the `delegate_all` call above.
95 |
96 | You might have heard this sort of decorator called a "presenter", an "exhibit",
97 | a "view model", or even just a "view" (in that nomenclature, what Rails calls
98 | "views" are actually "templates"). Whatever you call it, it's a great way to
99 | replace procedural helpers like the one above with "real" object-oriented
100 | programming.
101 |
102 | Decorators are the ideal place to:
103 | * format complex data for user display
104 | * define commonly-used representations of an object, like a `name` method that
105 | combines `first_name` and `last_name` attributes
106 | * mark up attributes with a little semantic HTML, like turning a `url` field
107 | into a hyperlink
108 |
109 | ## Installation
110 |
111 | Add Draper to your Gemfile:
112 |
113 | ```ruby
114 | gem 'draper', '~> 1.3'
115 | ```
116 |
117 | And run `bundle install` within your app's directory.
118 |
119 | If you're upgrading from a 0.x release, the major changes are outlined [in the
120 | wiki](https://github.com/drapergem/draper/wiki/Upgrading-to-1.0).
121 |
122 | ## Writing Decorators
123 |
124 | Decorators inherit from `Draper::Decorator`, live in your `app/decorators`
125 | directory, and are named for the model that they decorate:
126 |
127 | ```ruby
128 | # app/decorators/article_decorator.rb
129 | class ArticleDecorator < Draper::Decorator
130 | # ...
131 | end
132 | ```
133 |
134 | ### Generators
135 |
136 | When you have Draper installed and generate a controller...
137 |
138 | ```
139 | rails generate resource Article
140 | ```
141 |
142 | ...you'll get a decorator for free!
143 |
144 | But if the `Article` model already exists, you can run...
145 |
146 | ```
147 | rails generate decorator Article
148 | ```
149 |
150 | ...to create the `ArticleDecorator`.
151 |
152 | ### Accessing Helpers
153 |
154 | Normal Rails helpers are still useful for lots of tasks. Both Rails' provided
155 | helpers and those defined in your app can be accessed within a decorator via the `h` method:
156 |
157 | ```ruby
158 | class ArticleDecorator < Draper::Decorator
159 | def emphatic
160 | h.content_tag(:strong, "Awesome")
161 | end
162 | end
163 | ```
164 |
165 | If writing `h.` frequently is getting you down, you can add...
166 |
167 | ```
168 | include Draper::LazyHelpers
169 | ```
170 |
171 | ...at the top of your decorator class - you'll mix in a bazillion methods and
172 | never have to type `h.` again.
173 |
174 | (*Note*: the `capture` method is only available through `h` or `helpers`)
175 |
176 | ### Accessing the model
177 |
178 | When writing decorator methods you'll usually need to access the wrapped model.
179 | While you may choose to use delegation ([covered below](#delegating-methods))
180 | for convenience, you can always use the `object` (or its alias `model`):
181 |
182 | ```ruby
183 | class ArticleDecorator < Draper::Decorator
184 | def published_at
185 | object.published_at.strftime("%A, %B %e")
186 | end
187 | end
188 | ```
189 |
190 | ## Decorating Objects
191 |
192 | ### Single Objects
193 |
194 | Ok, so you've written a sweet decorator, now you're going to want to put it into
195 | action! A simple option is to call the `decorate` method on your model:
196 |
197 | ```ruby
198 | @article = Article.first.decorate
199 | ```
200 |
201 | This infers the decorator from the object being decorated. If you want more
202 | control - say you want to decorate a `Widget` with a more general
203 | `ProductDecorator` - then you can instantiate a decorator directly:
204 |
205 | ```ruby
206 | @widget = ProductDecorator.new(Widget.first)
207 | # or, equivalently
208 | @widget = ProductDecorator.decorate(Widget.first)
209 | ```
210 |
211 | ### Collections
212 |
213 | #### Decorating Individual Elements
214 |
215 | If you have a collection of objects, you can decorate them all in one fell
216 | swoop:
217 |
218 | ```ruby
219 | @articles = ArticleDecorator.decorate_collection(Article.all)
220 | ```
221 |
222 | If your collection is an ActiveRecord query, you can use this:
223 |
224 | ```ruby
225 | @articles = Article.popular.decorate
226 | ```
227 |
228 | *Note:* In Rails 3, the `.all` method returns an array and not a query. Thus you
229 | _cannot_ use the technique of `Article.all.decorate` in Rails 3. In Rails 4,
230 | `.all` returns a query so this techique would work fine.
231 |
232 | #### Decorating the Collection Itself
233 |
234 | If you want to add methods to your decorated collection (for example, for
235 | pagination), you can subclass `Draper::CollectionDecorator`:
236 |
237 | ```ruby
238 | # app/decorators/articles_decorator.rb
239 | class ArticlesDecorator < Draper::CollectionDecorator
240 | def page_number
241 | 42
242 | end
243 | end
244 |
245 | # elsewhere...
246 | @articles = ArticlesDecorator.new(Article.all)
247 | # or, equivalently
248 | @articles = ArticlesDecorator.decorate(Article.all)
249 | ```
250 |
251 | Draper decorates each item by calling the `decorate` method. Alternatively, you can
252 | specify a decorator by overriding the collection decorator's `decorator_class`
253 | method, or by passing the `:with` option to the constructor.
254 |
255 | #### Using pagination
256 |
257 | Some pagination gems add methods to `ActiveRecord::Relation`. For example,
258 | [Kaminari](https://github.com/amatsuda/kaminari)'s `paginate` helper method
259 | requires the collection to implement `current_page`, `total_pages`, and
260 | `limit_value`. To expose these on a collection decorator, you can delegate to
261 | the `object`:
262 |
263 | ```ruby
264 | class PaginatingDecorator < Draper::CollectionDecorator
265 | delegate :current_page, :total_pages, :limit_value
266 | end
267 | ```
268 |
269 | The `delegate` method used here is the same as that added by [Active
270 | Support](http://api.rubyonrails.org/classes/Module.html#method-i-delegate),
271 | except that the `:to` option is not required; it defaults to `:object` when
272 | omitted.
273 |
274 | [will_paginate](https://github.com/mislav/will_paginate) needs the following delegations:
275 |
276 | ```ruby
277 | delegate :current_page, :per_page, :offset, :total_entries, :total_pages
278 | ```
279 |
280 | ### Decorating Associated Objects
281 |
282 | You can automatically decorate associated models when the primary model is
283 | decorated. Assuming an `Article` model has an associated `Author` object:
284 |
285 | ```ruby
286 | class ArticleDecorator < Draper::Decorator
287 | decorates_association :author
288 | end
289 | ```
290 |
291 | When `ArticleDecorator` decorates an `Article`, it will also use
292 | `AuthorDecorator` to decorate the associated `Author`.
293 |
294 | ### Decorated Finders
295 |
296 | You can call `decorates_finders` in a decorator...
297 |
298 | ```ruby
299 | class ArticleDecorator < Draper::Decorator
300 | decorates_finders
301 | end
302 | ```
303 |
304 | ...which allows you to then call all the normal ActiveRecord-style finders on
305 | your `ArticleDecorator` and they'll return decorated objects:
306 |
307 | ```ruby
308 | @article = ArticleDecorator.find(params[:id])
309 | ```
310 |
311 | ### When to Decorate Objects
312 |
313 | Decorators are supposed to behave very much like the models they decorate, and
314 | for that reason it is very tempting to just decorate your objects at the start
315 | of your controller action and then use the decorators throughout. *Don't*.
316 |
317 | Because decorators are designed to be consumed by the view, you should only be
318 | accessing them there. Manipulate your models to get things ready, then decorate
319 | at the last minute, right before you render the view. This avoids many of the
320 | common pitfalls that arise from attempting to modify decorators (in particular,
321 | collection decorators) after creating them.
322 |
323 | To help you make your decorators read-only, we have the `decorates_assigned`
324 | method in your controller. It adds a helper method that returns the decorated
325 | version of an instance variable:
326 |
327 | ```ruby
328 | # app/controllers/articles_controller.rb
329 | class ArticlesController < ApplicationController
330 | decorates_assigned :article
331 |
332 | def show
333 | @article = Article.find(params[:id])
334 | end
335 | end
336 | ```
337 |
338 | The `decorates_assigned :article` bit is roughly equivalent to
339 |
340 | ```ruby
341 | def article
342 | @decorated_article ||= @article.decorate
343 | end
344 | helper_method :article
345 | ```
346 |
347 | This means that you can just replace `@article` with `article` in your views and
348 | you'll have access to an ArticleDecorator object instead. In your controller you
349 | can continue to use the `@article` instance variable to manipulate the model -
350 | for example, `@article.comments.build` to add a new blank comment for a form.
351 |
352 | ## Testing
353 |
354 | Draper supports RSpec, MiniTest::Rails, and Test::Unit, and will add the
355 | appropriate tests when you generate a decorator.
356 |
357 | ### RSpec
358 |
359 | Your specs are expected to live in `spec/decorators`. If you use a different
360 | path, you need to tag them with `type: :decorator`.
361 |
362 | In a controller spec, you might want to check whether your instance variables
363 | are being decorated properly. You can use the handy predicate matchers:
364 |
365 | ```ruby
366 | assigns(:article).should be_decorated
367 |
368 | # or, if you want to be more specific
369 | assigns(:article).should be_decorated_with ArticleDecorator
370 | ```
371 |
372 | Note that `model.decorate == model`, so your existing specs shouldn't break when
373 | you add the decoration.
374 |
375 | #### Spork Users
376 |
377 | In your `Spork.prefork` block of `spec_helper.rb`, add this:
378 |
379 | ```ruby
380 | require 'draper/test/rspec_integration'
381 | ```
382 |
383 | ### Isolated Tests
384 |
385 | In tests, Draper needs to build a view context to access helper methods. By
386 | default, it will create an `ApplicationController` and then use its view
387 | context. If you are speeding up your test suite by testing each component in
388 | isolation, you can eliminate this dependency by putting the following in your
389 | `spec_helper` or similar:
390 |
391 | ```ruby
392 | Draper::ViewContext.test_strategy :fast
393 | ```
394 |
395 | In doing so, your decorators will no longer have access to your application's
396 | helpers. If you need to selectively include such helpers, you can pass a block:
397 |
398 | ```ruby
399 | Draper::ViewContext.test_strategy :fast do
400 | include ApplicationHelper
401 | end
402 | ```
403 |
404 | #### Stubbing Route Helper Functions
405 |
406 | If you are writing isolated tests for Draper methods that call route helper
407 | methods, you can stub them instead of needing to require Rails.
408 |
409 | If you are using RSpec, minitest-rails, or the Test::Unit syntax of minitest,
410 | you already have access to the Draper `helpers` in your tests since they
411 | inherit from `Draper::TestCase`. If you are using minitest's spec syntax
412 | without minitest-rails, you can explicitly include the Draper `helpers`:
413 |
414 | ```ruby
415 | describe YourDecorator do
416 | include Draper::ViewHelpers
417 | end
418 | ```
419 |
420 | Then you can stub the specific route helper functions you need using your
421 | preferred stubbing technique (this example uses RSpec's `stub` method):
422 |
423 | ```ruby
424 | helpers.stub(users_path: '/users')
425 | ```
426 |
427 | ## Advanced usage
428 |
429 | ### Shared Decorator Methods
430 |
431 | You might have several decorators that share similar needs. Since decorators are
432 | just Ruby objects, you can use any normal Ruby technique for sharing
433 | functionality.
434 |
435 | In Rails controllers, common functionality is organized by having all
436 | controllers inherit from `ApplicationController`. You can apply this same
437 | pattern to your decorators:
438 |
439 | ```ruby
440 | # app/decorators/application_decorator.rb
441 | class ApplicationDecorator < Draper::Decorator
442 | # ...
443 | end
444 | ```
445 |
446 | Then modify your decorators to inherit from that `ApplicationDecorator` instead
447 | of directly from `Draper::Decorator`:
448 |
449 | ```ruby
450 | class ArticleDecorator < ApplicationDecorator
451 | # decorator methods
452 | end
453 | ```
454 |
455 | ### Delegating Methods
456 |
457 | When your decorator calls `delegate_all`, any method called on the decorator not
458 | defined in the decorator itself will be delegated to the decorated object. This
459 | is a very permissive interface.
460 |
461 | If you want to strictly control which methods are called within views, you can
462 | choose to only delegate certain methods from the decorator to the source model:
463 |
464 | ```ruby
465 | class ArticleDecorator < Draper::Decorator
466 | delegate :title, :body
467 | end
468 | ```
469 |
470 | We omit the `:to` argument here as it defaults to the `object` being decorated.
471 | You could choose to delegate methods to other places like this:
472 |
473 | ```ruby
474 | class ArticleDecorator < Draper::Decorator
475 | delegate :title, :body
476 | delegate :name, :title, to: :author, prefix: true
477 | end
478 | ```
479 |
480 | From your view template, assuming `@article` is decorated, you could do any of
481 | the following:
482 |
483 | ```ruby
484 | @article.title # Returns the article's `.title`
485 | @article.body # Returns the article's `.body`
486 | @article.author_name # Returns the article's `author.name`
487 | @article.author_title # Returns the article's `author.title`
488 | ```
489 |
490 | ### Adding Context
491 |
492 | If you need to pass extra data to your decorators, you can use a `context` hash.
493 | Methods that create decorators take it as an option, for example:
494 |
495 | ```ruby
496 | Article.first.decorate(context: {role: :admin})
497 | ```
498 |
499 | The value passed to the `:context` option is then available in the decorator
500 | through the `context` method.
501 |
502 | If you use `decorates_association`, the context of the parent decorator is
503 | passed to the associated decorators. You can override this with the `:context`
504 | option:
505 |
506 | ```ruby
507 | class ArticleDecorator < Draper::Decorator
508 | decorates_association :author, context: {foo: "bar"}
509 | end
510 | ```
511 |
512 | or, if you want to modify the parent's context, use a lambda that takes a hash
513 | and returns a new hash:
514 |
515 | ```ruby
516 | class ArticleDecorator < Draper::Decorator
517 | decorates_association :author,
518 | context: ->(parent_context){ parent_context.merge(foo: "bar") }
519 | end
520 | ```
521 |
522 | ### Specifying Decorators
523 |
524 | When you're using `decorates_association`, Draper uses the `decorate` method on
525 | the associated record(s) to perform the decoration. If you want use a specific
526 | decorator, you can use the `:with` option:
527 |
528 | ```ruby
529 | class ArticleDecorator < Draper::Decorator
530 | decorates_association :author, with: FancyPersonDecorator
531 | end
532 | ```
533 |
534 | For a collection association, you can specify a `CollectionDecorator` subclass,
535 | which is applied to the whole collection, or a singular `Decorator` subclass,
536 | which is applied to each item individually.
537 |
538 | ### Scoping Associations
539 |
540 | If you want your decorated association to be ordered, limited, or otherwise
541 | scoped, you can pass a `:scope` option to `decorates_association`, which will be
542 | applied to the collection *before* decoration:
543 |
544 | ```ruby
545 | class ArticleDecorator < Draper::Decorator
546 | decorates_association :comments, scope: :recent
547 | end
548 | ```
549 |
550 | ### Proxying Class Methods
551 |
552 | If you want to proxy class methods to the wrapped model class, including when
553 | using `decorates_finders`, Draper needs to know the model class. By default, it
554 | assumes that your decorators are named `SomeModelDecorator`, and then attempts
555 | to proxy unknown class methods to `SomeModel`.
556 |
557 | If your model name can't be inferred from your decorator name in this way, you
558 | need to use the `decorates` method:
559 |
560 | ```ruby
561 | class MySpecialArticleDecorator < Draper::Decorator
562 | decorates :article
563 | end
564 | ```
565 |
566 | This is only necessary when proxying class methods.
567 |
568 | ### Making Models Decoratable
569 |
570 | Models get their `decorate` method from the `Draper::Decoratable` module, which
571 | is included in `ActiveRecord::Base` and `Mongoid::Document` by default. If
572 | you're [using another
573 | ORM](https://github.com/drapergem/draper/wiki/Using-other-ORMs) (including
574 | versions of Mongoid prior to 3.0), or want to decorate plain old Ruby objects,
575 | you can include this module manually.
576 |
577 | ## Contributors
578 |
579 | Draper was conceived by Jeff Casimir and heavily refined by Steve Klabnik and a
580 | great community of open source
581 | [contributors](https://github.com/drapergem/draper/contributors).
582 |
583 | ### Core Team
584 |
585 | * Jeff Casimir (jeff@jumpstartlab.com)
586 | * Steve Klabnik (steve@jumpstartlab.com)
587 | * Vasiliy Ermolovich
588 | * Andrew Haines
589 |
--------------------------------------------------------------------------------