├── 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 | [![TravisCI Build Status](https://travis-ci.org/drapergem/draper.svg?branch=master)](http://travis-ci.org/drapergem/draper) 6 | [![Code Climate](https://codeclimate.com/github/drapergem/draper.png)](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 | --------------------------------------------------------------------------------