Merge remote-tracking branch 'origin/master' into feature/rspec3-updates

# Conflicts:
#	.travis.yml
#	Gemfile
#	Guardfile
#	draper.gemspec
#	lib/generators/rspec/templates/decorator_spec.rb
#	spec/draper/collection_decorator_spec.rb
#	spec/draper/decoratable_spec.rb
#	spec/draper/decorated_association_spec.rb
#	spec/draper/decorates_assigned_spec.rb
#	spec/draper/decorator_spec.rb
#	spec/draper/factory_spec.rb
#	spec/draper/finders_spec.rb
#	spec/draper/view_context/build_strategy_spec.rb
#	spec/draper/view_context_spec.rb
#	spec/dummy/fast_spec/post_decorator_spec.rb
#	spec/dummy/spec/models/post_spec.rb
#	spec/generators/controller/controller_generator_spec.rb
#	spec/generators/decorator/decorator_generator_spec.rb
#	spec/support/shared_examples/view_helpers.rb
This commit is contained in:
Cliff Braton 2019-03-20 12:59:34 -05:00
commit cb9af6c9dc
No known key found for this signature in database
GPG Key ID: 27CEBBC155D16E2A
96 changed files with 1153 additions and 531 deletions

16
.codeclimate.yml Normal file
View File

@ -0,0 +1,16 @@
---
engines:
duplication:
enabled: true
config:
languages:
- ruby
fixme:
enabled: true
rubocop:
enabled: true
ratings:
paths:
- "**.rb"
exclude_paths:
- spec/

24
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,24 @@
## Description
Detail your changes here.
A few sentences describing the overall goals of the pull request's commits will suffice.
Some questions you might answer:
* Why was this change required?
* Did you have any tough decisions to make? Which one(s) did you go with and why?
* Are there any deployment impacts to this change?
* Is there something you aren't happy with or that needs extra attention?
## Testing
Outline steps to test your changes.
1. Go here.
1. Click this.
1. See that.
## To-Dos
- [ ] tests
- [ ] documentation
## References
* [GitHub Issue ####](https://github.com/drapergem/draper/issues/####)
* [GitHub Pull Request ####](https://github.com/drapergem/draper/pull/####)

4
.gitignore vendored
View File

@ -1,5 +1,7 @@
*.gem
*.rvmrc
.rvmrc
.ruby-version
.ruby-gemset
.bundle
Gemfile.lock
pkg/*

11
.rubocop.yml Normal file
View File

@ -0,0 +1,11 @@
AllCops:
TargetRubyVersion: 2.4
DisplayCopNames: true
Exclude:
- 'spec/dummy/**/*'
Style/StringLiterals:
Enabled: false
Metrics/LineLength:
Max: 100

View File

@ -1,15 +1,29 @@
env:
global:
- CC_TEST_REPORTER_ID=b7ba588af2a540fa96c267b3655a2afe31ea29976dc25905a668dd28d5e88915
language: ruby
sudo: false
cache: bundler
services:
- mongodb
rvm:
- 2.1.5
- 2.2.1
- 2.2.2
- 2.2.3
- 2.3.5
- 2.4.3
- 2.5.4
- 2.6.2
- ruby-head
env:
- "RAILS_VERSION=4.0"
- "RAILS_VERSION=4.1"
- "RAILS_VERSION=4.2"
matrix:
allow_failures:
- rvm: ruby-head
before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
after_script:
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT

View File

@ -1,5 +1,41 @@
# Draper Changelog
## 3.1.0
* Rails 6 support [#841](https://github.com/drapergem/draper/pull/841)
* Include ORM query methods in `CollectionDecorator` (e.g. `includes`) [#845](https://github.com/drapergem/draper/pull/845)
* Document the fix for view context leaking in specs [#847](https://github.com/drapergem/draper/pull/847)
## 3.0.1
* Let `decorator_class` infer anonymous class decorator from superclass [#820](https://github.com/drapergem/draper/pull/820)
* When inferring decorator fails, show original class instead of `ActiveRecord::Base` [#821](https://github.com/drapergem/draper/pull/821)
* ActiveJob compatibility and documentation [#817](https://github.com/drapergem/draper/pull/817)
## 3.0.0 - 2017
### Breaking Changes
* Rename UninferrableSourceError to UninferrableObjectError [#768](https://github.com/drapergem/draper/pull/768)
* Remove conflicting source aliases: `source`, `to_source`, `source_class` and `source_class?` [#786](https://github.com/drapergem/draper/pull/786)
### New Features
* Generator for creating `ApplicationDecorator` that other decorators inherit from [#796](https://github.com/drapergem/draper/pull/796)
* Draper configuration with ability to customize the controller Draper uses [#788](https://github.com/drapergem/draper/pull/788)
* Added support for Rails 5 API-only applications [#793](https://github.com/drapergem/draper/pull/793)
* Added support for Rails runner [#739](https://github.com/drapergem/draper/pull/739)
### Other Changes
* Clear view context when the controller changes [#799](https://github.com/drapergem/draper/pull/799)
* Removed previously deprecated functionality [#785](https://github.com/drapergem/draper/pull/785)
* Only delegate === if other is an instance of a class that inherits from `Decorator` [#720](https://github.com/drapergem/draper/pull/720)
* Always default to `CollectionDecorator` when `NameError` is raised [#795](https://github.com/drapergem/draper/pull/795)
* Fixed issues in order to support Rails 5.1
* Fixed a bug where helpers were used inside a decorator and this decorator was used outside of controller context
## 3.0.0.pre1 - 2016-07-10
* Added support for Rails 5, dropped 4.0 - 4.2
* Ruby >= 2.2 is required, matching Rails 5
* Dropped support for ActiveModelSerializers 0.8
## 2.1.0 - 2015-03-26
* Cleared most issues and merged a few PRs

17
Gemfile
View File

@ -3,20 +3,13 @@ source "https://rubygems.org"
gemspec
platforms :ruby do
gem "sqlite3"
gem 'sqlite3', '~> 1.3.6'
end
platforms :jruby do
gem "minitest", ">= 3.0"
gem "activerecord-jdbcsqlite3-adapter", ">= 1.3.0.beta2"
gem "minitest"
gem "activerecord-jdbcsqlite3-adapter"
end
group :development, :test do
gem 'guard-rspec', require: false
gem 'ruby_gntp'
gem 'colorize'
end
version = ENV["RAILS_VERSION"] || "4.1"
eval_gemfile File.expand_path("../gemfiles/#{version}.gemfile", __FILE__)
gem "rails", "~> 5.0"
gem "mongoid", github: "mongodb/mongoid"

View File

@ -1,29 +1,26 @@
notification :gntp, host: '127.0.0.1'
def rspec_guard(options = {}, &block)
opts = {
:cmd => 'rspec'
options = {
version: 2,
notification: false
}.merge(options)
guard 'rspec', opts, &block
guard 'rspec', options, &block
end
rspec_guard :spec_paths => %w{spec/draper spec/generators} do
rspec_guard spec_paths: %w{spec/draper spec/generators} do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
end
rspec_guard :spec_paths => ['spec/integration'], cmd: 'RAILS_ENV=development rspec' do
rspec_guard spec_paths: 'spec/integration', env: {'RAILS_ENV' => 'development'} do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
end
rspec_guard :spec_paths => ['spec/integration'], cmd: 'RAILS_ENV=production rspec' do
rspec_guard spec_paths: 'spec/integration', env: {'RAILS_ENV' => 'production'} do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
end
# vim: set ts=8 sw=2 tw=0 ft=ruby et :

107
README.md
View File

@ -1,8 +1,9 @@
# Draper: View Models for Rails
[![TravisCI Build Status](https://travis-ci.org/drapergem/draper.svg?branch=master)](http://travis-ci.org/drapergem/draper)
[![Code Climate](https://codeclimate.com/github/drapergem/draper.png)](https://codeclimate.com/github/drapergem/draper)
[![Inline docs](http://inch-ci.org/github/drapergem/draper.png?branch=master)](http://inch-ci.org/github/drapergem/draper)
[![Code Climate](https://codeclimate.com/github/drapergem/draper.svg)](https://codeclimate.com/github/drapergem/draper)
[![Test Coverage](https://api.codeclimate.com/v1/badges/0d40c43951d516bf6985/test_coverage)](https://codeclimate.com/github/drapergem/draper/test_coverage)
[![Inline docs](http://inch-ci.org/github/drapergem/draper.svg?branch=master)](http://inch-ci.org/github/drapergem/draper)
Draper adds an object-oriented layer of presentation logic to your Rails
application.
@ -48,7 +49,7 @@ end
But it makes you a little uncomfortable. `publication_status` lives in a
nebulous namespace spread across all controllers and view. Down the road, you
might want to display the publication status of a `Book`. And, of course, your
design calls for a slighly different formatting to the date for a `Book`.
design calls for a slightly different formatting to the date for a `Book`.
Now your helper method can either switch based on the input class type (poor
Ruby style), or you break it out into two methods, `book_publication_status` and
@ -107,13 +108,13 @@ Decorators are the ideal place to:
## Installation
Add Draper to your Gemfile:
As of version 3.0.0, Draper is only compatible with Rails 5 / Ruby 2.2 and later. Add Draper to your Gemfile.
```ruby
gem 'draper', '~> 1.3'
gem 'draper'
```
And run `bundle install` within your app's directory.
After that, run `bundle install` within your app's directory.
If you're upgrading from a 0.x release, the major changes are outlined [in the
wiki](https://github.com/drapergem/draper/wiki/Upgrading-to-1.0).
@ -132,6 +133,12 @@ end
### Generators
To create an `ApplicationDecorator` that all generated decorators inherit from, run...
```
rails generate draper:install
```
When you have Draper installed and generate a controller...
```
@ -276,6 +283,19 @@ omitted.
delegate :current_page, :per_page, :offset, :total_entries, :total_pages
```
If needed, you can then set the collection_decorator_class of your CustomDecorator as follows:
```ruby
class ArticleDecorator < Draper::Decorator
def self.collection_decorator_class
PaginatingDecorator
end
end
ArticleDecorator.decorate_collection(@articles.paginate)
# => Collection decorated by PaginatingDecorator
# => Members decorated by ArticleDecorator
```
### Decorating Associated Objects
You can automatically decorate associated models when the primary model is
@ -307,6 +327,17 @@ your `ArticleDecorator` and they'll return decorated objects:
@article = ArticleDecorator.find(params[:id])
```
### Decorated Query Methods
By default, Draper will decorate all [QueryMethods](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html)
of ActiveRecord.
If you're using another ORM, in order to support it, you can tell Draper to use a custom strategy:
```ruby
Draper.configure do |config|
config.default_query_methods_strategy = :mongoid
end
```
### When to Decorate Objects
Decorators are supposed to behave very much like the models they decorate, and
@ -348,6 +379,18 @@ you'll have access to an ArticleDecorator object instead. In your controller you
can continue to use the `@article` instance variable to manipulate the model -
for example, `@article.comments.build` to add a new blank comment for a form.
## Configuration
Draper works out the box well, but also provides a hook for you to configure its
default functionality. For example, Draper assumes you have a base `ApplicationController`.
If your base controller is named something different (e.g. `BaseController`),
you can tell Draper to use it by adding the following to an initializer:
```ruby
Draper.configure do |config|
config.default_controller = BaseController
end
```
## Testing
Draper supports RSpec, MiniTest::Rails, and Test::Unit, and will add the
@ -379,6 +422,15 @@ In your `Spork.prefork` block of `spec_helper.rb`, add this:
require 'draper/test/rspec_integration'
```
#### Custom Draper Controller ViewContext
If running tests in an engine setting with a controller other than "ApplicationController," set a custom controller in `spec_helper.rb`
```ruby
config.before(:each, type: :decorator) do |example|
Draper::ViewContext.controller = ExampleEngine::CustomRootController.new
end
```
### Isolated Tests
In tests, Draper needs to build a view context to access helper methods. By
@ -423,6 +475,20 @@ preferred stubbing technique (this example uses RSpec's `stub` method):
helpers.stub(users_path: '/users')
```
### View context leakage
As mentioned before, Draper needs to build a view context to access helper methods. In MiniTest, the view context is
cleared during `before_setup` preventing any view context leakage. In RSpec, the view context is cleared before each
`decorator`, `controller`, and `mailer` spec. However, if you use decorators in other types of specs
(e.g. `job`), you may still experience the view context leaking from the previous spec. To solve this, add the
following to your `spec_helper` for each type of spec you are experiencing the leakage:
```ruby
config.before(:each, type: :type) { Draper::ViewContext.clear! }
```
_Note_: The `:type` above is just a placeholder. Replace `:type` with the type of spec you are experiencing
the leakage from.
## Advanced usage
### Shared Decorator Methods
@ -455,7 +521,10 @@ end
When your decorator calls `delegate_all`, any method called on the decorator not
defined in the decorator itself will be delegated to the decorated object. This
is a very permissive interface.
includes calling `super` from within the decorator. A call to `super` from within
the decorator will first try to call the method on the parent decorator class. If
the method does not exist on the parent decorator class, it will then try to call
the method on the decorated `object`. This is a very permissive interface.
If you want to strictly control which methods are called within views, you can
choose to only delegate certain methods from the decorator to the source model:
@ -564,24 +633,40 @@ end
This is only necessary when proxying class methods.
Once this association between the decorator and the model is set up, you can call
`SomeModel.decorator_class` to access class methods defined in the decorator.
If necessary, you can check if your model is decorated with `SomeModel.decorator_class?`.
### Making Models Decoratable
Models get their `decorate` method from the `Draper::Decoratable` module, which
is included in `ActiveRecord::Base` and `Mongoid::Document` by default. If
you're [using another
ORM](https://github.com/drapergem/draper/wiki/Using-other-ORMs) (including
versions of Mongoid prior to 3.0), or want to decorate plain old Ruby objects,
you're using another ORM, or want to decorate plain old Ruby objects,
you can include this module manually.
### Active Job Integration
[Active Job](http://edgeguides.rubyonrails.org/active_job_basics.html) allows you to pass ActiveRecord
objects to background tasks directly and performs the necessary serialization and deserialization. In
order to do this, arguments to a background job must implement [Global ID](https://github.com/rails/globalid).
Decorated objects implement Global ID by delegating to the object they are decorating. This means
you can pass decorated objects to background jobs, however, the object won't be decorated when it is
deserialized.
## Contributors
Draper was conceived by Jeff Casimir and heavily refined by Steve Klabnik and a
great community of open source
[contributors](https://github.com/drapergem/draper/contributors).
### Core Team
### Current maintainers
* Cliff Braton (cliff.braton@gmail.com)
### Historical maintainers
* Jeff Casimir (jeff@jumpstartlab.com)
* Steve Klabnik (steve@jumpstartlab.com)
* Vasiliy Ermolovich
* Andrew Haines
* Sean Linsley

View File

@ -64,6 +64,6 @@ namespace "db" do
run_in_dummy_app "rm -f db/*.sqlite3"
run_in_dummy_app "RAILS_ENV=development rake db:schema:load db:seed"
run_in_dummy_app "RAILS_ENV=production rake db:schema:load db:seed"
run_in_dummy_app "RAILS_ENV=test rake db:schema:load"
run_in_dummy_app "RAILS_ENV=test rake db:environment:set db:schema:load"
end
end

View File

@ -1,6 +1,4 @@
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "draper/version"
require File.join(__dir__, "lib", "draper", "version")
Gem::Specification.new do |s|
s.name = "draper"
@ -17,15 +15,20 @@ Gem::Specification.new do |s|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
s.add_dependency 'activesupport', '>= 3.0'
s.add_dependency 'actionpack', '>= 3.0'
s.add_dependency 'request_store', '~> 1.0'
s.add_dependency 'activemodel', '>= 3.0'
s.required_ruby_version = '>= 2.2.2'
s.add_dependency 'activesupport', '>= 5.0'
s.add_dependency 'actionpack', '>= 5.0'
s.add_dependency 'request_store', '>= 1.0'
s.add_dependency 'activemodel', '>= 5.0'
s.add_dependency 'activemodel-serializers-xml', '>= 1.0'
s.add_development_dependency 'ammeter'
s.add_development_dependency 'rake', '>= 0.9.2'
s.add_development_dependency 'rspec-rails', '~> 3.3'
s.add_development_dependency 'minitest-rails', '>= 1.0'
s.add_development_dependency 'rake'
s.add_development_dependency 'rspec-rails'
s.add_development_dependency 'minitest-rails'
s.add_development_dependency 'capybara'
s.add_development_dependency 'active_model_serializers'
s.add_development_dependency 'active_model_serializers', '>= 0.10'
s.add_development_dependency 'rubocop'
s.add_development_dependency 'simplecov'
end

View File

@ -1,3 +0,0 @@
gem "rails", "~> 4.0.0"
gem "mongoid", "~> 4.0"
gem "devise", "~> 3.0.0"

View File

@ -1,3 +0,0 @@
gem "rails", "~> 4.1.0"
gem "mongoid", "~> 4.0"
gem "devise", "~> 3.2"

View File

@ -1,3 +0,0 @@
gem "rails", "~> 4.2.0"
gem "mongoid", "~> 4.0"
gem "devise", "~> 3.4"

View File

@ -9,7 +9,9 @@ require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/core_ext/name_error'
require 'draper/version'
require 'draper/configuration'
require 'draper/view_helpers'
require 'draper/compatibility/api_only'
require 'draper/delegation'
require 'draper/automatic_delegation'
require 'draper/finders'
@ -21,19 +23,23 @@ require 'draper/factory'
require 'draper/decorated_association'
require 'draper/helper_support'
require 'draper/view_context'
require 'draper/query_methods'
require 'draper/collection_decorator'
require 'draper/undecorate'
require 'draper/decorates_assigned'
require 'draper/railtie' if defined?(Rails)
module Draper
extend Draper::Configuration
def self.setup_action_controller(base)
base.class_eval do
include Draper::Compatibility::ApiOnly if base == ActionController::API
include Draper::ViewContext
extend Draper::HelperSupport
extend Draper::DecoratesAssigned
before_filter :activate_draper
before_action :activate_draper
end
end
@ -55,9 +61,9 @@ module Draper
end
end
class UninferrableSourceError < NameError
class UninferrableObjectError < NameError
def initialize(klass)
super("Could not infer a source for #{klass}.")
super("Could not infer an object for #{klass}.")
end
end
end

View File

@ -2,12 +2,14 @@ module Draper
module AutomaticDelegation
extend ActiveSupport::Concern
# Delegates missing instance methods to the source object.
# Delegates missing instance methods to the source object. Note: This will delegate `super`
# method calls to `object` as well. Calling `super` will first try to call the method on
# the parent decorator class. If no method exists on the parent class, it will then try
# to call the method on the `object`.
def method_missing(method, *args, &block)
return super unless delegatable?(method)
self.class.delegate method
send(method, *args, &block)
object.send(method, *args, &block)
end
# Checks if the decorator responds to an instance method, or is able to
@ -18,6 +20,8 @@ module Draper
# @private
def delegatable?(method)
return if private_methods.include?(method)
object.respond_to?(method)
end

View File

@ -2,6 +2,7 @@ module Draper
class CollectionDecorator
include Enumerable
include Draper::ViewHelpers
include Draper::QueryMethods
extend Draper::Delegation
# @return the collection being decorated.
@ -42,17 +43,7 @@ module Draper
@decorated_collection ||= object.map{|item| decorate_item(item)}
end
# Delegated to the decorated collection when using the block form
# (`Enumerable#find`) or to the decorator class if not
# (`ActiveRecord::FinderMethods#find`)
def find(*args, &block)
if block_given?
decorated_collection.find(*args, &block)
else
ActiveSupport::Deprecation.warn("Using ActiveRecord's `find` on a CollectionDecorator is deprecated. Call `find` on a model, and then decorate the result", caller)
decorate_item(object.find(*args))
end
end
delegate :find, to: :decorated_collection
def to_s
"#<#{self.class.name} of #{decorator_class || "inferred decorators"} for #{object.inspect}>"

View File

@ -0,0 +1,23 @@
module Draper
module Compatibility
# Draper expects your `ApplicationController` to include `ActionView::Rendering`. The
# `ApplicationController` generated by Rails 5 API-only applications (created with
# `rails new --api`) don't by default. However, including `ActionView::Rendering` in
# `ApplicatonController` breaks `render :json` due to `render_to_body` being overridden.
#
# This compatibility patch fixes the issue by restoring the original `render_to_body`
# method after including `ActionView::Rendering`. Ultimately, including `ActionView::Rendering`
# in an ActionController::API may not be supported functionality by Rails (see Rails issue
# for more detail: https://github.com/rails/rails/issues/27211). This hack is meant to be a
# temporary solution until we can find a way to not rely on the controller layer.
module ApiOnly
extend ActiveSupport::Concern
included do
alias :previous_render_to_body :render_to_body
include ActionView::Rendering
alias :render_to_body :previous_render_to_body
end
end
end
end

View File

@ -0,0 +1,22 @@
module Draper
module Compatibility
# [Active Job](http://edgeguides.rubyonrails.org/active_job_basics.html) allows you to pass
# ActiveRecord objects to background tasks directly and performs the necessary serialization
# and deserialization. In order to do this, arguments to a background job must implement
# [Global ID](https://github.com/rails/globalid).
#
# This compatibility patch implements Global ID for decorated objects by delegating to the object
# that is decorated. This means you can pass decorated objects to background jobs, but
# the object won't be decorated when it is deserialized. This patch is meant as an intermediate
# fix until we can find a way to deserialize the decorated object correctly.
module GlobalID
extend ActiveSupport::Concern
included do
include ::GlobalID::Identification
delegate :to_global_id, :to_signed_global_id, to: :object
end
end
end
end

View File

@ -0,0 +1,23 @@
module Draper
module Configuration
def configure
yield self
end
def default_controller
@@default_controller ||= ApplicationController
end
def default_controller=(controller)
@@default_controller = controller
end
def default_query_methods_strategy
@@default_query_methods_strategy ||= :active_record
end
def default_query_methods_strategy=(strategy)
@@default_query_methods_strategy = strategy
end
end
end

View File

@ -48,7 +48,6 @@ module Draper
end
module ClassMethods
# Decorates a collection of objects. Used at the end of a scope chain.
#
# @example
@ -56,8 +55,7 @@ module Draper
# @param [Hash] options
# see {Decorator.decorate_collection}.
def decorate(options = {})
collection = Rails::VERSION::MAJOR >= 4 ? all : scoped
decorator_class.decorate_collection(collection, options.reverse_merge(with: nil))
decorator_class.decorate_collection(all, options.reverse_merge(with: nil))
end
def decorator_class?
@ -70,16 +68,16 @@ module Draper
# `Product` maps to `ProductDecorator`).
#
# @return [Class] the inferred decorator class.
def decorator_class
def decorator_class(called_on = self)
prefix = respond_to?(:model_name) ? model_name : name
decorator_name = "#{prefix}Decorator"
decorator_name.constantize
rescue NameError => error
decorator_name_constant = decorator_name.safe_constantize
return decorator_name_constant unless decorator_name_constant.nil?
if superclass.respond_to?(:decorator_class)
superclass.decorator_class
superclass.decorator_class(called_on)
else
raise unless error.missing_name?(decorator_name)
raise Draper::UninferrableDecoratorError.new(self)
raise Draper::UninferrableDecoratorError.new(called_on)
end
end
@ -87,7 +85,7 @@ module Draper
#
# @return [Boolean]
def ===(other)
super || (other.respond_to?(:object) && super(other.object))
super || (other.is_a?(Draper::Decorator) && super(other.object))
end
end

View File

@ -1,7 +1,6 @@
module Draper
# @private
class DecoratedAssociation
def initialize(owner, association, options)
options.assert_valid_keys(:with, :scope, :context)
@ -30,6 +29,5 @@ module Draper
@decorated = factory.decorate(associated, context_args: owner.context)
end
end
end

50
lib/draper/decorator.rb Executable file → Normal file
View File

@ -1,6 +1,9 @@
require 'draper/compatibility/global_id'
module Draper
class Decorator
include Draper::ViewHelpers
include Draper::Compatibility::GlobalID if defined?(GlobalID)
extend Draper::Delegation
include ActiveModel::Serialization
@ -10,8 +13,6 @@ module Draper
# @return the object being decorated.
attr_reader :object
alias_method :model, :object
alias_method :source, :object # TODO: deprecate this
alias_method :to_source, :object # TODO: deprecate this
# @return [Hash] extra data to be used in user-defined methods.
attr_accessor :context
@ -72,15 +73,10 @@ module Draper
# Checks whether this decorator class has a corresponding {object_class}.
def self.object_class?
object_class
rescue Draper::UninferrableSourceError
rescue Draper::UninferrableObjectError
false
end
class << self # TODO deprecate this
alias_method :source_class, :object_class
alias_method :source_class?, :object_class?
end
# Automatically decorates ActiveRecord finder methods, so that you can use
# `ProductDecorator.find(id)` instead of
# `ProductDecorator.decorate(Product.find(id))`.
@ -182,7 +178,7 @@ module Draper
# Returns a unique hash for a decorated object based on
# the decorator class and the object being decorated.
#
#
# @return [Fixnum]
def hash
self.class.hash ^ object.hash
@ -203,18 +199,6 @@ module Draper
super || object.instance_of?(klass)
end
if RUBY_VERSION < "2.0"
# nasty hack to stop 1.9.x using the delegated `to_s` in `inspect`
alias_method :_to_s, :to_s
def inspect
ivars = instance_variables.map do |name|
"#{name}=#{instance_variable_get(name).inspect}"
end
_to_s.insert(-2, " #{ivars.join(", ")}")
end
end
delegate :to_s
# In case object is nil
@ -241,10 +225,9 @@ module Draper
# @return [Class] the class created by {decorate_collection}.
def self.collection_decorator_class
name = collection_decorator_name
name.constantize
rescue NameError => error
raise if name && !error.missing_name?(name)
Draper::CollectionDecorator
name_constant = name && name.safe_constantize
name_constant || Draper::CollectionDecorator
end
private
@ -259,22 +242,23 @@ module Draper
end
def self.object_class_name
raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/
return nil if name.nil? || name.demodulize !~ /.+Decorator$/
name.chomp("Decorator")
end
def self.inferred_object_class
name = object_class_name
name.constantize
rescue NameError => error
raise if name && !error.missing_name?(name)
raise Draper::UninferrableSourceError.new(self)
name_constant = name && name.safe_constantize
return name_constant unless name_constant.nil?
raise Draper::UninferrableObjectError.new(self)
end
def self.collection_decorator_name
plural = object_class_name.pluralize
raise NameError if plural == object_class_name
"#{plural}Decorator"
singular = object_class_name
plural = singular && singular.pluralize
"#{plural}Decorator" unless plural == singular
end
def handle_multiple_decoration(options)

1
lib/draper/finders.rb Executable file → Normal file
View File

@ -3,7 +3,6 @@ module Draper
# do not have to extend this module directly; it is extended by
# {Decorator.decorates_finders}.
module Finders
def find(id, options = {})
decorate(object_class.find(id), options)
end

View File

@ -2,11 +2,8 @@ module Draper
# Provides access to helper methods - both Rails built-in helpers, and those
# defined in your application.
class HelperProxy
# @overload initialize(view_context)
def initialize(view_context = nil)
view_context ||= current_view_context # backwards compatibility
def initialize(view_context)
@view_context = view_context
end
@ -35,10 +32,5 @@ module Draper
view_context.send(name, *args, &block)
end
end
def current_view_context
ActiveSupport::Deprecation.warn("wrong number of arguments (0 for 1) passed to Draper::HelperProxy.new", caller[1..-1])
Draper::ViewContext.current.view_context
end
end
end

View File

@ -3,13 +3,11 @@ module Draper
# so that you can stop typing `h.` everywhere, at the cost of mixing in a
# bazillion methods.
module LazyHelpers
# Sends missing methods to the {HelperProxy}.
def method_missing(method, *args, &block)
helpers.send(method, *args, &block)
rescue NoMethodError
super
end
end
end

View File

@ -0,0 +1,23 @@
require_relative 'query_methods/load_strategy'
module Draper
module QueryMethods
# Proxies missing query methods to the source class if the strategy allows.
def method_missing(method, *args, &block)
return super unless strategy.allowed? method
object.send(method, *args, &block).decorate
end
def respond_to_missing?(method, include_private = false)
strategy.allowed?(method) || super
end
private
# Configures the strategy used to proxy the query methods, which defaults to `:active_record`.
def strategy
@strategy ||= LoadStrategy.new(Draper.default_query_methods_strategy)
end
end
end

View File

@ -0,0 +1,21 @@
module Draper
module QueryMethods
module LoadStrategy
def self.new(name)
const_get(name.to_s.camelize).new
end
class ActiveRecord
def allowed?(method)
::ActiveRecord::Relation::VALUE_METHODS.include? method
end
end
class Mongoid
def allowed?(method)
raise NotImplementedError
end
end
end
end
end

33
lib/draper/railtie.rb Executable file → Normal file
View File

@ -3,8 +3,6 @@ require 'rails/railtie'
module ActiveModel
class Railtie < Rails::Railtie
generators do |app|
app ||= Rails.application # Rails 3.0.x does not yield `app`
Rails::Generators.configure! app.config.generators
require_relative '../generators/controller_override'
end
@ -13,7 +11,6 @@ end
module Draper
class Railtie < Rails::Railtie
config.after_initialize do |app|
app.config.paths.add 'app/decorators', eager_load: true
@ -23,19 +20,19 @@ module Draper
end
end
initializer "draper.setup_action_controller" do |app|
initializer 'draper.setup_action_controller' do
ActiveSupport.on_load :action_controller do
Draper.setup_action_controller self
end
end
initializer "draper.setup_action_mailer" do |app|
initializer 'draper.setup_action_mailer' do
ActiveSupport.on_load :action_mailer do
Draper.setup_action_mailer self
end
end
initializer "draper.setup_orm" do |app|
initializer 'draper.setup_orm' do
[:active_record, :mongoid].each do |orm|
ActiveSupport.on_load orm do
Draper.setup_orm self
@ -43,28 +40,22 @@ module Draper
end
end
initializer "draper.setup_active_model_serializers" do |app|
ActiveSupport.on_load :active_model_serializers do
if defined?(ActiveModel::ArraySerializerSupport)
Draper::CollectionDecorator.send :include, ActiveModel::ArraySerializerSupport
end
end
end
initializer "draper.minitest-rails_integration" do |app|
initializer 'draper.minitest-rails_integration' do
ActiveSupport.on_load :minitest do
require "draper/test/minitest_integration"
require 'draper/test/minitest_integration'
end
end
console do
def initialize_view_context
require 'action_controller/test_case'
ApplicationController.new.view_context
Draper.default_controller.new.view_context
Draper::ViewContext.build
end
rake_tasks do
Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f }
end
console { initialize_view_context }
runner { initialize_view_context }
rake_tasks { Dir[File.join(File.dirname(__FILE__), 'tasks/*.rake')].each { |f| load f } }
end
end

View File

@ -1,22 +1,9 @@
require 'rake/testtask'
test_task = if Rails.version.to_f < 3.2
require 'rails/test_unit/railtie'
Rake::TestTask
else
require 'rails/test_unit/sub_test_task'
Rails::SubTestTask
end
require 'rails/test_unit/railtie'
namespace :test do
test_task.new(:decorators => "test:prepare") do |t|
Rake::TestTask.new(decorators: "test:prepare") do |t|
t.libs << "test"
t.pattern = "test/decorators/**/*_test.rb"
end
end
if Rails.version.to_f < 4.2 && Rake::Task.task_defined?('test:run')
Rake::Task['test:run'].enhance do
Rake::Task['test:decorators'].invoke
end
end

View File

@ -1,14 +1,7 @@
module Draper
module DeviseHelper
def sign_in(resource_or_scope, resource = nil)
scope = begin
Devise::Mapping.find_scope!(resource_or_scope)
rescue RuntimeError => e
# Draper 1.0 didn't require the mapping to exist
ActiveSupport::Deprecation.warn("#{e.message}.\nUse `sign_in :user, mock_user` instead.", caller)
:user
end
scope = Devise::Mapping.find_scope!(resource_or_scope)
_stub_current_scope scope, resource || resource_or_scope
end

0
lib/draper/test/minitest_integration.rb Executable file → Normal file
View File

6
lib/draper/test/rspec_integration.rb Executable file → Normal file
View File

@ -7,11 +7,7 @@ module Draper
end
RSpec.configure do |config|
if RSpec::Core::Version::STRING.starts_with?("3")
config.include DecoratorExampleGroup, file_path: %r{spec/decorators}, type: :decorator
else
config.include DecoratorExampleGroup, example_group: {file_path: %r{spec/decorators}}, type: :decorator
end
config.include DecoratorExampleGroup, file_path: %r{spec/decorators}, type: :decorator
[:decorator, :controller, :mailer].each do |type|
config.before(:each, type: type) { Draper::ViewContext.clear! }

View File

@ -3,9 +3,9 @@ module Draper
class TestCase < ::ActiveSupport::TestCase
module ViewContextTeardown
def teardown
super
def before_setup
Draper::ViewContext.clear!
super
end
end
@ -29,14 +29,10 @@ module Draper
end
end
if defined?(ActionController::TestCase)
class ActionController::TestCase
include Draper::TestCase::ViewContextTeardown
end
if defined? ActionController::TestCase
ActionController::TestCase.include Draper::TestCase::ViewContextTeardown
end
if defined?(ActionMailer::TestCase)
class ActionMailer::TestCase
include Draper::TestCase::ViewContextTeardown
end
if defined? ActionMailer::TestCase
ActionMailer::TestCase.include Draper::TestCase::ViewContextTeardown
end

View File

@ -6,4 +6,12 @@ module Draper
object
end
end
def self.undecorate_chain(object)
if object.respond_to?(:decorated?) && object.decorated?
undecorate_chain(object.object)
else
object
end
end
end

View File

@ -1,3 +1,3 @@
module Draper
VERSION = "2.1.0"
VERSION = '3.1.0'
end

22
lib/draper/view_context.rb Executable file → Normal file
View File

@ -20,8 +20,10 @@ module Draper
RequestStore.store[:current_controller]
end
# Sets the current controller.
# Sets the current controller. Clears view context when we are setting
# different controller.
def self.controller=(controller)
clear! if RequestStore.store[:current_controller] != controller
RequestStore.store[:current_controller] = controller
end
@ -82,23 +84,5 @@ module Draper
def self.build_strategy
@build_strategy ||= Draper::ViewContext::BuildStrategy.new(:full)
end
# @deprecated Use {controller} instead.
def self.current_controller
ActiveSupport::Deprecation.warn("Draper::ViewContext.current_controller is deprecated (use controller instead)", caller)
self.controller || ApplicationController.new
end
# @deprecated Use {controller=} instead.
def self.current_controller=(controller)
ActiveSupport::Deprecation.warn("Draper::ViewContext.current_controller= is deprecated (use controller instead)", caller)
self.controller = controller
end
# @deprecated Use {build} instead.
def self.build_view_context
ActiveSupport::Deprecation.warn("Draper::ViewContext.build_view_context is deprecated (use build instead)", caller)
build
end
end
end

View File

@ -2,7 +2,6 @@ module Draper
module ViewContext
# @private
module BuildStrategy
def self.new(name, &block)
const_get(name.to_s.camelize).new(&block)
end
@ -37,12 +36,20 @@ module Draper
attr_reader :block
def controller
(Draper::ViewContext.controller || ApplicationController.new).tap do |controller|
controller.request ||= ActionController::TestRequest.new if defined?(ActionController::TestRequest)
Draper::ViewContext.controller ||= Draper.default_controller.new
Draper::ViewContext.controller.tap do |controller|
controller.request ||= new_test_request controller if defined?(ActionController::TestRequest)
end
end
end
def new_test_request(controller)
is_above_rails_5_1 ? ActionController::TestRequest.create(controller) : ActionController::TestRequest.create
end
def is_above_rails_5_1
ActionController::TestRequest.method(:create).parameters.first == [:req, :controller_class]
end
end
end
end
end

View File

@ -5,7 +5,6 @@ module Draper
extend ActiveSupport::Concern
module ClassMethods
# Access the helpers proxy to call built-in and user-defined
# Rails helpers from a class context.
#
@ -14,7 +13,6 @@ module Draper
Draper::ViewContext.current
end
alias_method :h, :helpers
end
# Access the helpers proxy to call built-in and user-defined
@ -32,6 +30,5 @@ module Draper
helpers.localize(*args)
end
alias_method :l, :localize
end
end

View File

@ -5,13 +5,13 @@ require "rails/generators/rails/scaffold_controller/scaffold_controller_generato
module Rails
module Generators
class ControllerGenerator
hook_for :decorator, default: true do |generator|
hook_for :decorator, type: :boolean, default: true do |generator|
invoke generator, [name.singularize]
end
end
class ScaffoldControllerGenerator
hook_for :decorator, default: true
hook_for :decorator, type: :boolean, default: true
end
end
end

View File

@ -0,0 +1,14 @@
module Draper
module Generators
class InstallGenerator < Rails::Generators::Base
source_root File.expand_path("templates", __dir__)
desc 'Creates an ApplicationDecorator, if none exists.'
def create_application_decorator
file = 'application_decorator.rb'
copy_file file, "app/decorators/#{file}"
end
end
end
end

View File

@ -0,0 +1,8 @@
class ApplicationDecorator < Draper::Decorator
# Define methods for all decorated objects.
# Helpers are accessed through `helpers` (aka `h`). For example:
#
# def percent_amount
# h.number_to_percentage object.amount, precision: 2
# end
end

View File

@ -4,10 +4,10 @@ module MiniTest
module Generators
class DecoratorGenerator < Base
def self.source_root
File.expand_path('../templates', __FILE__)
File.expand_path("templates", __dir__)
end
class_option :spec, :type => :boolean, :default => false, :desc => "Use MiniTest::Spec DSL"
class_option :spec, type: :boolean, default: false, desc: "Use MiniTest::Spec DSL"
check_class_collision suffix: "DecoratorTest"

View File

@ -1,7 +1,7 @@
module Rails
module Generators
class DecoratorGenerator < NamedBase
source_root File.expand_path("../templates", __FILE__)
class DecoratorGenerator < NamedBase
source_root File.expand_path("templates", __dir__)
check_class_collision suffix: "Decorator"
class_option :parent, type: :string, desc: "The parent class for the generated decorator"
@ -24,13 +24,6 @@ module Rails
end
end
end
# Rails 3.0.X compatibility, stolen from https://github.com/jnunemaker/mongomapper/pull/385/files#L1R32
unless methods.include?(:module_namespacing)
def module_namespacing
yield if block_given?
end
end
end
end
end

View File

@ -1,9 +1,11 @@
module Rspec
class DecoratorGenerator < ::Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)
module Generators
class DecoratorGenerator < ::Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)
def create_spec_file
template 'decorator_spec.rb', File.join('spec/decorators', class_path, "#{singular_name}_decorator_spec.rb")
def create_spec_file
template 'decorator_spec.rb', File.join('spec/decorators', class_path, "#{singular_name}_decorator_spec.rb")
end
end
end
end

View File

@ -1,9 +1,12 @@
module TestUnit
class DecoratorGenerator < ::Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)
module Generators
class DecoratorGenerator < ::Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)
check_class_collision suffix: "DecoratorTest"
def create_test_file
template 'decorator_test.rb', File.join('test/decorators', class_path, "#{singular_name}_decorator_test.rb")
def create_test_file
template 'decorator_test.rb', File.join('test/decorators', class_path, "#{singular_name}_decorator_test.rb")
end
end
end
end

View File

@ -7,7 +7,6 @@ module Draper
describe "#initialize" do
describe "options validation" do
it "does not raise error on valid options" do
valid_options = {with: Decorator, context: {}}
expect{CollectionDecorator.new([], valid_options)}.not_to raise_error
@ -121,26 +120,11 @@ module Draper
end
describe "#find" do
context "with a block" do
it "decorates Enumerable#find" do
decorator = CollectionDecorator.new([])
it "decorates Enumerable#find" do
decorator = CollectionDecorator.new([])
expect(decorator.decorated_collection).to receive(:find).and_return(:delegated)
expect(decorator.find{|p| p.title == "title"}).to be :delegated
end
end
context "without a block" do
it "decorates object.find" do
object = []
found = double(decorate: :decorated)
decorator = CollectionDecorator.new(object)
expect(object).to receive(:find).and_return(found)
ActiveSupport::Deprecation.silence do
expect(decorator.find(1)).to be :decorated
end
end
expect(decorator.decorated_collection).to receive(:find).and_return(:delegated)
expect(decorator.find{|p| p.title == "title"}).to be :delegated
end
end
@ -157,7 +141,7 @@ module Draper
it "delegates array methods to the decorated collection" do
decorator = CollectionDecorator.new([])
expect(decorator.decorated_collection).to receive(:[]).with(42).and_return(:delegated)
allow(decorator.decorated_collection).to receive(:[]).with(42).and_return(:delegated)
expect(decorator[42]).to be :delegated
end
@ -302,6 +286,5 @@ module Draper
expect(decorator.replace([:foo, :bar])).to be decorator
end
end
end
end

View File

@ -0,0 +1,49 @@
require 'spec_helper'
module Draper
RSpec.describe Configuration do
it 'yields Draper on configure' do
Draper.configure { |config| expect(config).to be Draper }
end
describe '#default_controller' do
it 'defaults default_controller to ApplicationController' do
expect(Draper.default_controller).to be ApplicationController
end
it 'allows customizing default_controller through configure' do
default = Draper.default_controller
Draper.configure do |config|
config.default_controller = CustomController
end
expect(Draper.default_controller).to be CustomController
Draper.default_controller = default
end
end
describe '#default_query_methods_strategy' do
let!(:default) { Draper.default_query_methods_strategy }
subject { Draper.default_query_methods_strategy }
context 'when there is no custom strategy' do
it { is_expected.to eq(:active_record) }
end
context 'when using a custom strategy' do
before do
Draper.configure do |config|
config.default_query_methods_strategy = :mongoid
end
end
after { Draper.default_query_methods_strategy = default }
it { is_expected.to eq(:mongoid) }
end
end
end
end

View File

@ -11,7 +11,7 @@ module Draper
expect(decorator).to be_a ProductDecorator
expect(decorator.object).to be product
end
end
it "accepts context" do
context = {some: "context"}
@ -22,7 +22,8 @@ module Draper
it "uses the #decorator_class" do
product = Product.new
allow(product).to receive(:decorator_class) { OtherDecorator }
allow(product).to receive_messages decorator_class: OtherDecorator
expect(product.decorate).to be_an_instance_of OtherDecorator
end
end
@ -72,6 +73,16 @@ module Draper
expect(Product).to receive(:decorator_class).and_return(:some_decorator)
expect(product.decorator_class).to be :some_decorator
end
it "specifies the class that #decorator_class was first called on (superclass)" do
person = Person.new
expect { person.decorator_class }.to raise_error(Draper::UninferrableDecoratorError, 'Could not infer a decorator for Person.')
end
it "specifies the class that #decorator_class was first called on (subclass)" do
child = Child.new
expect { child.decorator_class }.to raise_error(Draper::UninferrableDecoratorError, 'Could not infer a decorator for Child.')
end
end
describe "#==" do
@ -108,37 +119,42 @@ module Draper
end
it "is true for a decorated instance" do
decorator = double(object: Product.new)
decorator = Product.new.decorate
expect(Product === decorator).to be_truthy
end
it "is true for a decorated derived instance" do
decorator = double(object: Class.new(Product).new)
decorator = Class.new(Product).new.decorate
expect(Product === decorator).to be_truthy
end
it "is false for a decorated unrelated instance" do
decorator = double(object: Model.new)
decorator = Other.new.decorate
expect(Product === decorator).to be_falsey
end
it "is false for a non-decorator which happens to respond to object" do
decorator = double(object: Product.new)
expect(Product === decorator).to be_falsey
end
end
describe ".decorate" do
let(:scoping_method) { Rails::VERSION::MAJOR >= 4 ? :all : :scoped }
it "calls #decorate_collection on .decorator_class" do
scoped = [Product.new]
allow(Product).to receive(scoping_method).and_return(scoped)
allow(Product).to receive(:all).and_return(scoped)
expect(Product.decorator_class).to receive(:decorate_collection).with(scoped, with: nil).and_return(:decorated_collection)
expect(Product.decorate).to be :decorated_collection
end
it "accepts options" do
options = {with: ProductDecorator, context: {some: "context"}}
allow(Product).to receive(scoping_method).and_return([])
allow(Product).to receive(:all).and_return([])
expect(Product.decorator_class).to receive(:decorate_collection).with([], options)
Product.decorate(options)
@ -182,6 +198,15 @@ module Draper
end
end
context "when the decorator contains name error" do
it "throws an NameError" do
# We imitate ActiveSupport::Autoload behavior here in order to cause lazy NameError exception raising
allow_any_instance_of(Module).to receive(:const_missing) { Class.new { any_nonexisting_method_name } }
expect{Model.decorator_class}.to raise_error { |error| expect(error).to be_an_instance_of(NameError) }
end
end
context "when the decorator can't be inferred" do
it "throws an UninferrableDecoratorError" do
expect{Model.decorator_class}.to raise_error UninferrableDecoratorError
@ -190,10 +215,22 @@ module Draper
context "when an unrelated NameError is thrown" do
it "re-raises that error" do
allow_any_instance_of(String).to receive(:constantize) { Draper::Base }
# Not related to safe_constantize behavior, we just want to raise a NameError inside the function
allow_any_instance_of(String).to receive(:safe_constantize) { Draper::Base }
expect{Product.decorator_class}.to raise_error NameError, /Draper::Base/
end
end
context "when an anonymous class is given" do
it "infers the decorator from a superclass" do
anonymous_class = Class.new(Product) do
def self.name
to_s
end
end
expect(anonymous_class.decorator_class).to be ProductDecorator
end
end
end
end
end

View File

@ -1,8 +1,7 @@
require 'spec_helper'
module Draper
RSpec.describe DecoratedAssociation do
Rspec.describe DecoratedAssociation do
describe "#initialize" do
it "accepts valid options" do
valid_options = {with: Decorator, scope: :foo, context: {}}
@ -40,7 +39,7 @@ module Draper
describe "#call" do
it "calls the factory" do
factory = double
allow(Factory).to receive(:new).and_return(factory)
allow(Factory).to receive_messages(new: factory)
associated = double
owner_context = {foo: "bar"}
object = double(association: associated)
@ -54,7 +53,7 @@ module Draper
it "memoizes" do
factory = double
allow(Factory).to receive(:new).and_return(factory)
allow(Factory).to receive_messages(new: factory)
owner = double(object: double(association: double), context: {})
decorated_association = DecoratedAssociation.new(owner, :association, {})
decorated = double
@ -67,7 +66,7 @@ module Draper
context "when the :scope option was given" do
it "applies the scope before decoration" do
factory = double
allow(Factory).to receive(:new).and_return(factory)
allow(Factory).to receive_messages(new: factory)
scoped = double
object = double(association: double(applied_scope: scoped))
owner = double(object: object, context: {})
@ -79,6 +78,5 @@ module Draper
end
end
end
end
end

View File

@ -1,7 +1,7 @@
require 'spec_helper'
module Draper
RSpec.describe DecoratesAssigned do
describe DecoratesAssigned do
let(:controller_class) do
Class.new do
extend DecoratesAssigned
@ -28,14 +28,14 @@ module Draper
end
it "creates a factory" do
expect(Factory).to receive(:new).once
allow(Factory).to receive(:new).once
controller_class.decorates_assigned :article, :author
end
it "passes options to the factory" do
options = {foo: "bar"}
expect(Factory).to receive(:new).with(options)
allow(Factory).to receive(:new).with(options)
controller_class.decorates_assigned :article, :author, options
end
@ -43,7 +43,7 @@ module Draper
it "decorates the instance variable" do
object = double
factory = double
allow(Factory).to receive(:new).and_return(factory)
allow(Factory).to receive_messages(new: factory)
controller_class.decorates_assigned :article
controller = controller_class.new
@ -55,7 +55,7 @@ module Draper
it "memoizes" do
factory = double
allow(Factory).to receive(:new).and_return(factory)
allow(Factory).to receive_messages(new: factory)
controller_class.decorates_assigned :article
controller = controller_class.new
@ -66,5 +66,6 @@ module Draper
end
end
end
end
end

179
spec/draper/decorator_spec.rb Executable file → Normal file
View File

@ -145,13 +145,6 @@ module Draper
ProductDecorator.decorate_collection([], options)
end
end
context "when a NameError is thrown" do
it "re-raises that error" do
allow_any_instance_of(String).to receive(:constantize) { Draper::DecoratedEnumerableProxy }
expect{ProductDecorator.decorate_collection([])}.to raise_error NameError, /Draper::DecoratedEnumerableProxy/
end
end
end
describe ".decorates" do
@ -181,42 +174,40 @@ module Draper
protect_class Namespaced::ProductDecorator
context "when not set by .decorates" do
it "raises an UninferrableSourceError for a so-named 'Decorator'" do
expect{Decorator.object_class}.to raise_error UninferrableSourceError
it "raises an UninferrableObjectError for a so-named 'Decorator'" do
expect{Decorator.object_class}.to raise_error UninferrableObjectError
end
it "raises an UninferrableSourceError for anonymous decorators" do
expect{Class.new(Decorator).object_class}.to raise_error UninferrableSourceError
it "raises an UninferrableObjectError for anonymous decorators" do
expect{Class.new(Decorator).object_class}.to raise_error UninferrableObjectError
end
it "raises an UninferrableSourceError for a decorator without a model" do
skip
expect{OtherDecorator.object_class}.to raise_error UninferrableSourceError
it "raises an UninferrableObjectError for a decorator without a model" do
SomeDecorator = Class.new(Draper::Decorator)
expect{SomeDecorator.object_class}.to raise_error UninferrableObjectError
end
it "raises an UninferrableSourceError for other naming conventions" do
expect{ProductPresenter.object_class}.to raise_error UninferrableSourceError
it "raises an UninferrableObjectError for other naming conventions" do
ProductPresenter = Class.new(Draper::Decorator)
expect{ProductPresenter.object_class}.to raise_error UninferrableObjectError
end
it "infers the source for '<Model>Decorator'" do
it "infers the object class for '<Model>Decorator'" do
expect(ProductDecorator.object_class).to be Product
end
it "infers namespaced sources" do
it "infers the object class for namespaced decorators" do
expect(Namespaced::ProductDecorator.object_class).to be Namespaced::Product
end
context "when an unrelated NameError is thrown" do
it "re-raises that error" do
allow_any_instance_of(String).to receive(:constantize) { SomethingThatDoesntExist }
# Not related to safe_constantize behavior, we just want to raise a NameError inside the function
allow_any_instance_of(String).to receive(:safe_constantize) { SomethingThatDoesntExist }
expect{ProductDecorator.object_class}.to raise_error NameError, /SomethingThatDoesntExist/
end
end
end
it "is aliased to .source_class" do
expect(ProductDecorator.source_class).to be Product
end
end
describe ".object_class?" do
@ -227,13 +218,24 @@ module Draper
end
it "returns false when .object_class is not inferrable" do
allow(Decorator).to receive(:object_class).and_raise(UninferrableSourceError.new(Decorator))
allow(Decorator).to receive(:object_class).and_raise(UninferrableObjectError.new(Decorator))
expect(Decorator.object_class?).to be_falsey
end
end
it "is aliased to .source_class?" do
allow(Decorator).to receive(:object_class).and_return(Model)
expect(Decorator.source_class?).to be_truthy
describe '.collection_decorator_class' do
it 'defaults to CollectionDecorator' do
allow_any_instance_of(String).to receive(:safe_constantize) { nil }
expect(ProductDecorator.collection_decorator_class).to be Draper::CollectionDecorator
end
it 'infers collection decorator based on name' do
expect(ProductDecorator.collection_decorator_class).to be ProductsDecorator
end
it 'infers collection decorator base on name for namespeced model' do
expect(Namespaced::ProductDecorator.collection_decorator_class).to be Namespaced::ProductsDecorator
end
end
@ -335,7 +337,6 @@ module Draper
expect(decorator.object).to be object
expect(decorator.model).to be object
expect(decorator.to_source).to be object
end
it "is aliased to #model" do
@ -344,20 +345,6 @@ module Draper
expect(decorator.model).to be object
end
it "is aliased to #source" do
object = Model.new
decorator = Decorator.new(object)
expect(decorator.source).to be object
end
it "is aliased to #to_source" do
object = Model.new
decorator = Decorator.new(object)
expect(decorator.to_source).to be object
end
end
describe "aliasing object to object class name" do
@ -479,13 +466,15 @@ module Draper
it "returns only the object's attributes that are implemented by the decorator" do
decorator = Decorator.new(double(attributes: {foo: "bar", baz: "qux"}))
allow(decorator).to receive(:foo)
expect(decorator.attributes).to eq({foo: "bar"})
end
end
describe ".model_name" do
it "delegates to the source class" do
allow(Decorator).to receive(:object_class) { double(model_name: :delegated) }
it "delegates to the object class" do
allow(Decorator).to receive(:object_class).and_return(double(model_name: :delegated))
expect(Decorator.model_name).to be :delegated
end
end
@ -592,12 +581,51 @@ module Draper
expect(decorator.hello_world).to be :delegated
end
it "adds delegated methods to the decorator when they are used" do
decorator = Decorator.new(double(hello_world: :delegated))
it 'delegates `super` to parent class first' do
parent_decorator_class = Class.new(Decorator) do
def hello_world
"parent#hello_world"
end
end
expect(decorator.methods).not_to include :hello_world
decorator.hello_world
expect(decorator.methods).to include :hello_world
child_decorator_class = Class.new(parent_decorator_class) do
def hello_world
super
end
end
decorator = child_decorator_class.new(double(hello_world: 'object#hello_world'))
expect(decorator.hello_world).to eq 'parent#hello_world'
end
it 'delegates `super` to object if method does not exist on parent class' do
decorator_class = Class.new(Decorator) do
def hello_world
super
end
end
decorator = decorator_class.new(double(hello_world: 'object#hello_world'))
expect(decorator.hello_world).to eq 'object#hello_world'
end
it 'raises `NoMethodError` when `super` is called on for method that does not exist' do
decorator_class = Class.new(Decorator) do
def hello_world
super
end
end
decorator = decorator_class.new(double)
expect{decorator.hello_world}.to raise_error NoMethodError
end
it "allows decorator to decorate different classes of objects" do
decorator_1 = Decorator.new(double)
decorator_2 = Decorator.new(double(hello_world: :delegated))
decorator_2.hello_world
expect(decorator_1.methods).not_to include :hello_world
end
it "passes blocks to delegated methods" do
@ -616,7 +644,7 @@ module Draper
it "delegates already-delegated methods" do
object = Class.new{ delegate :bar, to: :foo }.new
allow(object).to receive(:foo) { double(bar: :delegated) }
allow(object).to receive_messages foo: double(bar: :delegated)
decorator = Decorator.new(object)
expect(decorator.bar).to be :delegated
@ -636,26 +664,47 @@ module Draper
expect{decorator.hello_world}.to raise_error NoMethodError
expect(decorator.methods).not_to include :hello_world
end
context 'when decorator overrides a public method defined on the object with a private' do
let(:decorator_class) do
Class.new(Decorator) do
private
def hello_world
'hello world'
end
end
end
let(:object) { Class.new { def hello_world; end }.new }
it 'does not delegate the public method defined on the object' do
decorator = decorator_class.new(object)
expect{ decorator.hello_world }.to raise_error NoMethodError
end
end
end
context ".method_missing" do
context "without a source class" do
context "without an object class" do
it "raises a NoMethodError on missing methods" do
expect{Decorator.hello_world}.to raise_error NoMethodError
end
end
context "with a source class" do
it "delegates methods that exist on the source class" do
context "with an object class" do
it "delegates methods that exist on the object class" do
object_class = Class.new
allow(object_class).to receive(:hello_world).and_return(:delegated)
allow(Decorator).to receive(:object_class).and_return(object_class)
allow(object_class).to receive_messages hello_world: :delegated
allow(Decorator).to receive_messages object_class: object_class
expect(Decorator.hello_world).to be :delegated
end
it "does not delegate methods that do not exist on the source class" do
allow(Decorator).to receive(:object_class) { Class.new }
it "does not delegate methods that do not exist on the object class" do
allow(Decorator).to receive_messages object_class: Class.new
expect{Decorator.hello_world}.to raise_error NoMethodError
end
end
@ -693,7 +742,7 @@ module Draper
end
describe ".respond_to?" do
context "without a source class" do
context "without a object class" do
it "returns true for its own class methods" do
Decorator.class_eval{def self.hello_world; end}
@ -705,16 +754,16 @@ module Draper
end
end
context "with a source class" do
context "with a object class" do
it "returns true for its own class methods" do
Decorator.class_eval{def self.hello_world; end}
allow(Decorator).to receive(:object_class) { Class.new }
allow(Decorator).to receive_messages object_class: Class.new
expect(Decorator).to respond_to :hello_world
end
it "returns true for the source's class methods" do
allow(Decorator).to receive(:object_class) { double(hello_world: :delegated) }
it "returns true for the object's class methods" do
allow(Decorator).to receive_messages object_class: double(hello_world: :delegated)
expect(Decorator).to respond_to :hello_world
end
@ -732,7 +781,7 @@ module Draper
describe ".respond_to_missing?" do
it "allows .method to be called on delegated class methods" do
allow(Decorator).to receive(:object_class) { double(hello_world: :delegated) }
allow(Decorator).to receive_messages object_class: double(hello_world: :delegated)
expect(Decorator.method(:hello_world)).not_to be_nil
end
@ -740,7 +789,7 @@ module Draper
end
describe "class spoofing" do
it "pretends to be a kind of the source class" do
it "pretends to be a kind of the object class" do
decorator = Decorator.new(Model.new)
expect(decorator.kind_of?(Model)).to be_truthy
@ -754,7 +803,7 @@ module Draper
expect(decorator.is_a?(Decorator)).to be_truthy
end
it "pretends to be an instance of the source class" do
it "pretends to be an instance of the object class" do
decorator = Decorator.new(Model.new)
expect(decorator.instance_of?(Model)).to be_truthy

View File

@ -0,0 +1,25 @@
require 'spec_helper'
require 'support/shared_examples/view_helpers'
SimpleCov.command_name 'test:unit'
module Draper
describe Draper do
describe '.setup_action_controller' do
it 'includes api only compatability if base is ActionController::API' do
base = ActionController::API
Draper.setup_action_controller(base)
expect(base.included_modules).to include(Draper::Compatibility::ApiOnly)
end
it 'does not include api only compatibility if base ActionController::Base' do
base = ActionController::Base
Draper.setup_action_controller(base)
expect(base.included_modules).not_to include(Draper::Compatibility::ApiOnly)
end
end
end
end

View File

@ -1,8 +1,7 @@
require 'spec_helper'
module Draper
RSpec.describe Factory do
Rspec.describe Factory do
describe "#initialize" do
it "accepts valid options" do
valid_options = {with: Decorator, context: {foo: "bar"}}
@ -64,7 +63,7 @@ module Draper
allow(Factory::Worker).to receive(:new).and_return(worker)
options = {foo: "bar"}
expect(worker).to receive(:call).with(options)
allow(worker).to receive(:call).with(options)
factory.decorate(double, options)
end
@ -72,27 +71,25 @@ module Draper
it "sets the passed context" do
factory = Factory.new(context: {foo: "bar"})
worker = ->(*){}
allow(Factory::Worker).to receive(:new).and_return(worker)
allow(Factory::Worker).to receive_messages new: worker
expect(worker).to receive(:call).with(baz: 'qux', context: { foo: 'bar' })
expect(worker).to receive(:call).with(baz: "qux", context: {foo: "bar"})
factory.decorate(double, {baz: "qux"})
end
it "is overridden by explicitly-specified context" do
factory = Factory.new(context: {foo: "bar"})
worker = ->(*){}
allow(Factory::Worker).to receive(:new) { worker }
allow(Factory::Worker).to receive_messages new: worker
expect(worker).to receive(:call).with(context: {baz: "qux"})
factory.decorate(double, context: {baz: "qux"})
end
end
end
end
RSpec.describe Factory::Worker do
Rspec.describe Factory::Worker do
describe "#call" do
it "calls the decorator method" do
object = double
@ -101,7 +98,7 @@ module Draper
decorator = ->(*){}
allow(worker).to receive(:decorator){ decorator }
expect(decorator).to receive(:call).with(object, options).and_return(:decorated)
allow(decorator).to receive(:call).with(object, options).and_return(:decorated)
expect(worker.call(options)).to be :decorated
end
@ -109,7 +106,7 @@ module Draper
it "calls it" do
worker = Factory::Worker.new(double, double)
decorator = ->(*){}
allow(worker).to receive(:decorator) { decorator }
allow(worker).to receive_messages decorator: decorator
context = {foo: "bar"}
expect(decorator).to receive(:call).with(anything(), context: context)
@ -118,7 +115,7 @@ module Draper
it "receives arguments from the :context_args option" do
worker = Factory::Worker.new(double, double)
allow(worker).to receive(:decorator) { ->(*){} }
allow(worker).to receive_messages decorator: ->(*){}
context = ->{}
expect(context).to receive(:call).with(:foo, :bar)
@ -127,7 +124,7 @@ module Draper
it "wraps non-arrays passed to :context_args" do
worker = Factory::Worker.new(double, double)
allow(worker).to receive(:decorator) { ->(*){} }
allow(worker).to receive_messages decorator: ->(*){}
context = ->{}
hash = {foo: "bar"}
@ -140,7 +137,7 @@ module Draper
it "doesn't call it" do
worker = Factory::Worker.new(double, double)
decorator = ->(*){}
allow(worker).to receive(:decorator) { decorator }
allow(worker).to receive_messages decorator: decorator
context = {foo: "bar"}
expect(decorator).to receive(:call).with(anything(), context: context)
@ -151,7 +148,7 @@ module Draper
it "does not pass the :context_args option to the decorator" do
worker = Factory::Worker.new(double, double)
decorator = ->(*){}
allow(worker).to receive(:decorator) { decorator }
allow(worker).to receive_messages decorator: decorator
expect(decorator).to receive(:call).with(anything(), foo: "bar")
worker.call(foo: "bar", context_args: [])

View File

@ -99,7 +99,7 @@ module Draper
describe ".all" do
it "returns a decorated collection" do
found = [Product.new, Product.new]
allow(Product).to receive(:all).and_return(found)
allow(Product).to receive_messages all: found
decorator = ProductDecorator.all
expect(decorator).to be_a Draper::CollectionDecorator

View File

@ -0,0 +1,26 @@
require 'spec_helper'
require 'active_record'
module Draper
module QueryMethods
describe LoadStrategy do
describe '#new' do
subject { described_class.new(:active_record) }
it { is_expected.to be_an_instance_of(LoadStrategy::ActiveRecord) }
end
end
describe LoadStrategy::ActiveRecord do
describe '#allowed?' do
it 'checks whether or not ActiveRecord::Relation::VALUE_METHODS has the given method' do
allow(::ActiveRecord::Relation::VALUE_METHODS).to receive(:include?)
described_class.new.allowed? :foo
expect(::ActiveRecord::Relation::VALUE_METHODS).to have_received(:include?).with(:foo)
end
end
end
end
end

View File

@ -0,0 +1,63 @@
require 'spec_helper'
require_relative '../dummy/app/decorators/post_decorator'
Post = Struct.new(:id) { }
module Draper
describe QueryMethods do
let(:fake_strategy) { instance_double(QueryMethods::LoadStrategy::ActiveRecord) }
before { allow(QueryMethods::LoadStrategy).to receive(:new).and_return(fake_strategy) }
describe '#method_missing' do
let(:collection) { [ Post.new, Post.new ] }
let(:collection_decorator) { PostDecorator.decorate_collection(collection) }
context 'when strategy allows collection to call the method' do
let(:results) { spy(:results) }
before do
allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(true)
allow(collection).to receive(:send).with(:some_query_method).and_return(results)
end
it 'calls the method on the collection and decorate it results' do
collection_decorator.some_query_method
expect(results).to have_received(:decorate)
end
end
context 'when strategy does not allow collection to call the method' do
before { allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(false) }
it 'raises NoMethodError' do
expect { collection_decorator.some_query_method }.to raise_exception(NoMethodError)
end
end
end
describe "#respond_to?" do
let(:collection) { [ Post.new, Post.new ] }
let(:collection_decorator) { PostDecorator.decorate_collection(collection) }
subject { collection_decorator.respond_to?(:some_query_method) }
context 'when strategy allows collection to call the method' do
before do
allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(true)
end
it { is_expected.to eq(true) }
end
context 'when strategy does not allow collection to call the method' do
before do
allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(false)
end
it { is_expected.to eq(false) }
end
end
end
end

View File

@ -0,0 +1,20 @@
require 'spec_helper'
describe Draper, '.undecorate_chain' do
let!(:object) { Model.new }
let!(:decorated_inner) { Class.new(Draper::Decorator).new(object) }
let!(:decorated_outer) { Class.new(Draper::Decorator).new(decorated_inner) }
it 'undecorates full chain of decorated objects' do
expect(Draper.undecorate_chain(decorated_outer)).to equal object
end
it 'passes a non-decorated object through' do
expect(Draper.undecorate_chain(object)).to equal object
end
it 'passes a non-decorator object through' do
object = Object.new
expect(Draper.undecorate_chain(object)).to equal object
end
end

View File

@ -14,7 +14,7 @@ module Draper
context "when a current controller is set" do
it "returns the controller's view context" do
view_context = fake_view_context
allow(ViewContext).to receive(:controller) { fake_controller(view_context) }
allow(ViewContext).to receive_messages controller: fake_controller(view_context)
strategy = ViewContext::BuildStrategy::Full.new
expect(strategy.call).to be view_context
@ -23,31 +23,47 @@ module Draper
context "when a current controller is not set" do
it "uses ApplicationController" do
view_context = fake_view_context
stub_const "ApplicationController", double(new: fake_controller(view_context))
strategy = ViewContext::BuildStrategy::Full.new
expect(strategy.call).to be view_context
expect(Draper::ViewContext.controller).to be_nil
view_context = ViewContext::BuildStrategy::Full.new.call
expect(view_context.controller).to eq Draper::ViewContext.controller
expect(view_context.controller).to be_an ApplicationController
end
end
it "adds a request if one is not defined" do
controller = Class.new(ActionController::Base).new
allow(ViewContext).to receive(:controller) { controller }
allow(ViewContext).to receive_messages controller: controller
strategy = ViewContext::BuildStrategy::Full.new
expect(controller.request).to be_nil
strategy.call
expect(controller.request).to be_an ActionController::TestRequest
expect(controller.params).to eq({})
expect(controller.params).to be_empty
# sanity checks
expect(controller.view_context.request).to be controller.request
expect(controller.view_context.params).to be controller.params
end
it "compatible with rails 5.1 change on ActionController::TestRequest.create method" do
ActionController::TestRequest.class_eval do
if ActionController::TestRequest.method(:create).parameters.first == []
def create controller_class
create
end
end
end
controller = Class.new(ActionController::Base).new
allow(ViewContext).to receive_messages controller: controller
strategy = ViewContext::BuildStrategy::Full.new
expect(controller.request).to be_nil
strategy.call
expect(controller.request).to be_an ActionController::TestRequest
end
it "adds methods to the view context from the constructor block" do
allow(ViewContext).to receive(:controller) { fake_controller }
allow(ViewContext).to receive(:controller).and_return(fake_controller)
strategy = ViewContext::BuildStrategy::Full.new do
def a_helper_method; end
end
@ -57,7 +73,7 @@ module Draper
it "includes modules into the view context from the constructor block" do
view_context = Object.new
allow(ViewContext).to receive(:controller) { fake_controller(view_context) }
allow(ViewContext).to receive(:controller).and_return(fake_controller(view_context))
helpers = Module.new do
def a_helper_method; end
end

View File

@ -18,7 +18,7 @@ module Draper
describe ".controller" do
it "returns the stored controller from RequestStore" do
allow(RequestStore).to receive(:store) { { current_controller: :stored_controller } }
allow(RequestStore).to receive_messages store: {current_controller: :stored_controller}
expect(ViewContext.controller).to be :stored_controller
end
@ -27,24 +27,52 @@ module Draper
describe ".controller=" do
it "stores a controller in RequestStore" do
store = {}
allow(RequestStore).to receive(:store).and_return(store)
allow(RequestStore).to receive_messages store: store
ViewContext.controller = :stored_controller
expect(store[:current_controller]).to be :stored_controller
end
it "cleans context when controller changes" do
store = {
current_controller: :stored_controller,
current_view_context: :stored_view_context
}
allow(RequestStore).to receive_messages store: store
ViewContext.controller = :other_stored_controller
expect(store).to include(current_controller: :other_stored_controller)
expect(store).not_to include(:current_view_context)
end
it "doesn't clean context when controller is the same" do
store = {
current_controller: :stored_controller,
current_view_context: :stored_view_context
}
allow(RequestStore).to receive_messages store: store
ViewContext.controller = :stored_controller
expect(store).to include(current_controller: :stored_controller)
expect(store).to include(current_view_context: :stored_view_context)
end
end
describe ".current" do
it "returns the stored view context from RequestStore" do
allow(RequestStore).to receive(:store) { { current_view_context: :stored_view_context } }
allow(RequestStore).to receive_messages store: {current_view_context: :stored_view_context}
expect(ViewContext.current).to be :stored_view_context
end
context "when no view context is stored" do
it "builds a view context" do
allow(RequestStore).to receive(:store).and_return({})
allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context })
allow(RequestStore).to receive_messages store: {}
allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context }
allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy)
expect(ViewContext.current).to be :new_helper_proxy
@ -52,8 +80,8 @@ module Draper
it "stores the built view context" do
store = {}
allow(RequestStore).to receive(:store).and_return(store)
allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context })
allow(RequestStore).to receive_messages store: store
allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context }
allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy)
ViewContext.current
@ -65,7 +93,7 @@ module Draper
describe ".current=" do
it "stores a helper proxy for the view context in RequestStore" do
store = {}
allow(RequestStore).to receive(:store).and_return(store)
allow(RequestStore).to receive_messages store: store
allow(HelperProxy).to receive(:new).with(:stored_view_context).and_return(:stored_helper_proxy)
ViewContext.current = :stored_view_context
@ -76,7 +104,7 @@ module Draper
describe ".clear!" do
it "clears the stored controller and view controller" do
store = {current_controller: :stored_controller, current_view_context: :stored_view_context}
allow(RequestStore).to receive(:store).and_return(store)
allow(RequestStore).to receive_messages store: store
ViewContext.clear!
expect(store).not_to have_key :current_controller
@ -86,7 +114,7 @@ module Draper
describe ".build" do
it "returns a new view context using the build strategy" do
allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context })
allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context }
expect(ViewContext.build).to be :new_view_context
end
@ -94,7 +122,7 @@ module Draper
describe ".build!" do
it "returns a helper proxy for the new view context" do
allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context })
allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context }
allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy)
expect(ViewContext.build!).to be :new_helper_proxy
@ -102,8 +130,8 @@ module Draper
it "stores the helper proxy" do
store = {}
allow(RequestStore).to receive(:store) { store }
allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context })
allow(RequestStore).to receive_messages store: store
allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context }
allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy)
ViewContext.build!

View File

@ -1,4 +0,0 @@
class ApplicationController < ActionController::Base
include LocalizedUrls
protect_from_forgery
end

View File

@ -0,0 +1,4 @@
class BaseController < ActionController::Base
include LocalizedUrls
protect_from_forgery
end

View File

@ -1,4 +1,4 @@
class PostsController < ApplicationController
class PostsController < BaseController
decorates_assigned :post
def show
@ -8,7 +8,7 @@ class PostsController < ApplicationController
def mail
post = Post.find(params[:id])
email = PostMailer.decorated_email(post).deliver
render text: email.body
render html: email.body.to_s.html_safe
end
private

0
spec/dummy/app/decorators/post_decorator.rb Executable file → Normal file
View File

View File

@ -0,0 +1,7 @@
class PublishPostJob < ActiveJob::Base
queue_as :default
def perform(post)
post.save!
end
end

View File

@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

View File

@ -1,3 +1,3 @@
class Post < ActiveRecord::Base
class Post < ApplicationRecord
# attr_accessible :title, :body
end

View File

@ -20,14 +20,16 @@
<dt>Helpers from the controller:</dt>
<dd id="goodnight_moon"><%= post.goodnight_moon %></dd>
<dt>Path with decorator:</dt>
<dd id="path_with_decorator"><%= post_path(post) %></dd>
<% unless defined? mailer %>
<dt>Path with decorator:</dt>
<dd id="path_with_decorator"><%= post_url(post) %></dd>
<dt>Path with model:</dt>
<dd id="path_with_model"><%= post.path_with_model %></dd>
<dt>Path with model:</dt>
<dd id="path_with_model"><%= post.path_with_model %></dd>
<dt>Path with id:</dt>
<dd id="path_with_id"><%= post.path_with_id %></dd>
<dt>Path with id:</dt>
<dd id="path_with_id"><%= post.path_with_id %></dd>
<% end %>
<dt>URL with decorator:</dt>
<dd id="url_with_decorator"><%= post_url(post) %></dd>

View File

@ -38,9 +38,6 @@ module Dummy
# Configure the default encoding used in templates for Ruby 1.9.
config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters += [:password]
# Enable escaping HTML in JSON.
config.active_support.escape_html_entities_in_json = true

View File

@ -2,4 +2,4 @@ require 'rubygems'
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

View File

@ -28,4 +28,6 @@ Dummy::Application.configure do
config.active_support.deprecation = :stderr
config.eager_load = false
config.active_job.queue_adapter = :test
end

View File

@ -0,0 +1,3 @@
Draper.configure do |config|
config.default_controller = BaseController
end

View File

@ -0,0 +1,4 @@
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
Rails.application.config.filter_parameters += [:password]

View File

@ -1,54 +1,117 @@
development:
# Configure available database sessions. (required)
sessions:
# Defines the default session. (required)
# Configure available database clients. (required)
clients:
# Defines the default client. (required)
default:
# Defines the name of the default database that Mongoid can connect to.
# (required).
database: dummy_development
# Provides the hosts the default session can connect to. Must be an array
# Provides the hosts the default client can connect to. Must be an array
# of host:port pairs. (required)
hosts:
- localhost:27017
options:
# Change whether the session persists in safe mode by default.
# (default: false)
# safe: false
# Change the default write concern. (default = { w: 1 })
# write:
# w: 1
# Change the default consistency model to :eventual or :strong.
# :eventual will send reads to secondaries, :strong sends everything
# to master. (default: :eventual)
# consistency: :eventual
# Change the default read preference. Valid options for mode are: :secondary,
# :secondary_preferred, :primary, :primary_preferred, :nearest
# (default: primary)
# read:
# mode: :secondary_preferred
# tag_sets:
# - use: web
# The name of the user for authentication.
# user: 'user'
# The password of the user for authentication.
# password: 'password'
# The user's database roles.
# roles:
# - 'dbOwner'
# Change the default authentication mechanism. Valid options are: :scram,
# :mongodb_cr, :mongodb_x509, and :plain. (default on 3.0 is :scram, default
# on 2.4 and 2.6 is :plain)
# auth_mech: :scram
# The database or source to authenticate the user against. (default: admin)
# auth_source: admin
# Force a the driver cluster to behave in a certain manner instead of auto-
# discovering. Can be one of: :direct, :replica_set, :sharded. Set to :direct
# when connecting to hidden members of a replica set.
# connect: :direct
# Changes the default time in seconds the server monitors refresh their status
# via ismaster commands. (default: 10)
# heartbeat_frequency: 10
# The time in seconds for selecting servers for a near read preference. (default: 5)
# local_threshold: 5
# The timeout in seconds for selecting a server for an operation. (default: 30)
# server_selection_timeout: 30
# The maximum number of connections in the connection pool. (default: 5)
# max_pool_size: 5
# The minimum number of connections in the connection pool. (default: 1)
# min_pool_size: 1
# The time to wait, in seconds, in the connection pool for a connection
# to be checked in before timing out. (default: 5)
# wait_queue_timeout: 5
# The time to wait to establish a connection before timing out, in seconds.
# (default: 5)
# connect_timeout: 5
# The timeout to wait to execute operations on a socket before raising an error.
# (default: 5)
# socket_timeout: 5
# The name of the replica set to connect to. Servers provided as seeds that do
# not belong to this replica set will be ignored.
# replica_set: name
# Whether to connect to the servers via ssl. (default: false)
# ssl: true
# The certificate file used to identify the connection against MongoDB.
# ssl_cert: /path/to/my.cert
# The private keyfile used to identify the connection against MongoDB.
# Note that even if the key is stored in the same file as the certificate,
# both need to be explicitly specified.
# ssl_key: /path/to/my.key
# A passphrase for the private key.
# ssl_key_pass_phrase: password
# Whether or not to do peer certification validation. (default: true)
# ssl_verify: true
# The file containing a set of concatenated certification authority certifications
# used to validate certs passed from the other end of the connection.
# ssl_ca_cert: /path/to/ca.cert
# How many times Moped should attempt to retry an operation after
# failure. (default: 30)
# max_retries: 30
# The time in seconds that Moped should wait before retrying an
# operation on failure. (default: 1)
# retry_interval: 1
# Configure Mongoid specific options. (optional)
options:
# Configuration for whether or not to allow access to fields that do
# not have a field definition on the model. (default: true)
# allow_dynamic_fields: true
# Enable the identity map, needed for eager loading. (default: false)
# identity_map_enabled: false
# Includes the root model name in json serialization. (default: false)
# include_root_in_json: false
# Include the _type field in serializaion. (default: false)
# Include the _type field in serialization. (default: false)
# include_type_for_serialization: false
# Preload all models in development, needed when models use
# inheritance. (default: false)
# preload_models: false
# Protect id and type from mass assignment. (default: true)
# protect_sensitive_fields: true
# Raise an error when performing a #find and the document is not found.
# (default: true)
# raise_not_found_error: true
@ -57,23 +120,23 @@ development:
# existing method. (default: false)
# scope_overwrite_exception: false
# Skip the database version check, used when connecting to a db without
# admin access. (default: false)
# skip_version_check: false
# User Active Support's time zone in conversions. (default: true)
# Use Active Support's time zone in conversions. (default: true)
# use_activesupport_time_zone: true
# Ensure all times are UTC in the app side. (default: false)
# use_utc: false
# Set the Mongoid and Ruby driver log levels when not in a Rails
# environment. The Mongoid logger will be set to the Rails logger
# otherwise.(default: :info)
# log_level: :info
test:
sessions:
clients:
default:
database: dummy_test
hosts:
- localhost:27017
options:
# In the test environment we lower the retries and retry interval to
# low amounts for fast failures.
max_retries: 1
retry_interval: 0
read:
mode: :primary
max_pool_size: 1

View File

@ -1,4 +1,4 @@
class CreatePosts < ActiveRecord::Migration
class CreatePosts < ActiveRecord::Migration[4.2]
def change
create_table :posts do |t|

View File

@ -11,11 +11,11 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20121019115657) do
ActiveRecord::Schema.define(version: 20121019115657) do
create_table "posts", :force => true do |t|
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
create_table "posts", force: true do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end

View File

@ -13,4 +13,4 @@ RSpec::Core::RakeTask.new :fast_spec do |t|
t.pattern = "fast_spec/**/*_spec.rb"
end
task :default => [:test, :spec, :fast_spec]
task default: [:test, :spec, :fast_spec]

View File

@ -2,15 +2,11 @@ require_relative '../rails_helper'
RSpec.describe Draper::CollectionDecorator do
describe "#active_model_serializer" do
it "returns ActiveModel::ArraySerializer" do
collection_decorator = Draper::CollectionDecorator.new([])
if defined?(ActiveModel::ArraySerializerSupport)
collection_serializer = collection_decorator.active_model_serializer
else
collection_serializer = ActiveModel::Serializer.serializer_for(collection_decorator)
end
it "returns ActiveModel::Serializer::CollectionSerializer" do
collection_decorator = Draper::CollectionDecorator.new([])
collection_serializer = ActiveModel::Serializer.serializer_for(collection_decorator)
expect(collection_serializer).to be ActiveModel::ArraySerializer
expect(collection_serializer).to be ActiveModel::Serializer::CollectionSerializer
end
end
end

View File

@ -51,14 +51,5 @@ if defined?(Devise)
expect(helper.current_user).to be_nil
end
it "is backwards-compatible" do
user = double("User")
ActiveSupport::Deprecation.silence do
sign_in user
end
expect(helper.current_user).to be user
end
end
end

6
spec/dummy/spec/decorators/post_decorator_spec.rb Executable file → Normal file
View File

@ -54,13 +54,11 @@ RSpec.describe PostDecorator do
end
it "serializes to XML" do
pending("Rails < 3.2 does not use `serializable_hash` in `to_xml`") if Rails.version.to_f < 3.2
xml = Capybara.string(decorator.to_xml)
expect(xml).to have_css "post > updated-at", text: "overridden"
end
it "uses a test view context from ApplicationController" do
expect(Draper::ViewContext.current.controller).to be_an ApplicationController
it "uses a test view context from BaseController" do
expect(Draper::ViewContext.current.controller).to be_an BaseController
end
end

View File

@ -0,0 +1,9 @@
RSpec.describe PublishPostJob, type: :job do
let(:post) { Post.create.decorate }
subject(:job) { described_class.perform_later(post) }
it 'queues the job' do
expect { job }.to have_enqueued_job(described_class).with(post.object)
end
end

View File

@ -10,14 +10,6 @@ RSpec.describe PostMailer do
expect(email_body).to have_content "Today"
end
it "can use path helpers with a model" do
expect(email_body).to have_css "#path_with_model", text: "/en/posts/#{post.id}"
end
it "can use path helpers with an id" do
expect(email_body).to have_css "#path_with_id", text: "/en/posts/#{post.id}"
end
it "can use url helpers with a model" do
expect(email_body).to have_css "#url_with_model", text: "http://www.example.com:12345/en/posts/#{post.id}"
end

View File

@ -0,0 +1,7 @@
require 'spec_helper'
describe ApplicationRecord do
it { expect(described_class.superclass).to eq ActiveRecord::Base }
it { expect(described_class.abstract_class).to be_truthy }
end

View File

@ -2,5 +2,14 @@ require_relative '../spec_helper'
require_relative '../shared_examples/decoratable'
RSpec.describe Post do
it_behaves_like "a decoratable model"
it_behaves_like 'a decoratable model'
it { should be_a ApplicationRecord }
describe '#to_global_id' do
let(:post) { Post.create }
subject { post.to_global_id }
it { is_expected.to eq post.decorate.to_global_id }
end
end

View File

@ -11,8 +11,6 @@ RSpec.shared_examples_for "a decoratable model" do
describe "#==" do
it "is true for other instances' decorators" do
pending "Mongoid < 3.1 overrides `#==`" if defined?(Mongoid) && Mongoid::VERSION.to_f < 3.1 && described_class < Mongoid::Document
described_class.create
one = described_class.first
other = described_class.first

View File

@ -51,14 +51,5 @@ if defined?(Devise)
assert helper.current_user.nil?
end
it "is backwards-compatible" do
user = Object.new
ActiveSupport::Deprecation.silence do
sign_in user
end
assert_same user, helper.current_user
end
end
end

View File

@ -13,12 +13,12 @@ describe "A decorator test" do
it_does_not_leak_view_context
end
describe "A controller test" do
tests Class.new(ActionController::Base)
describe "A controller decorator test" do
subject { Class.new(ActionController::Base) }
it_does_not_leak_view_context
end
describe "A mailer test" do
describe "A mailer decorator test" do
it_does_not_leak_view_context
end

View File

@ -51,14 +51,5 @@ if defined?(Devise)
assert helper.current_user.nil?
end
def test_backwards_compatibility
user = Object.new
ActiveSupport::Deprecation.silence do
sign_in user
end
assert_same user, helper.current_user
end
end
end

View File

@ -14,7 +14,7 @@ class DecoratorTest < Draper::TestCase
end
class ControllerTest < ActionController::TestCase
tests Class.new(ActionController::Base)
subject{ Class.new(ActionController::Base) }
it_does_not_leak_view_context
end

View File

@ -1,11 +1,11 @@
require 'spec_helper'
require_relative '../../dummy/spec/rails_helper'
require 'rails'
require 'dummy/config/environment'
require 'ammeter/init'
require 'generators/controller_override'
require 'generators/rails/decorator_generator'
SimpleCov.command_name 'test:generator'
RSpec.describe Rails::Generators::ControllerGenerator do
describe Rails::Generators::ControllerGenerator do
destination File.expand_path("../tmp", __FILE__)
before { prepare_destination }

View File

@ -1,10 +1,9 @@
require 'spec_helper'
require 'rspec/rails'
# require_relative '../../dummy/spec/rails_helper'
require 'dummy/config/environment'
require 'ammeter/init'
require 'generators/rails/decorator_generator'
RSpec.describe Rails::Generators::DecoratorGenerator do
describe Rails::Generators::DecoratorGenerator do
destination File.expand_path("../tmp", __FILE__)
before { prepare_destination }
@ -41,6 +40,7 @@ RSpec.describe Rails::Generators::DecoratorGenerator do
context "with an ApplicationDecorator" do
before do
allow_any_instance_of(Object).to receive(:require)
allow_any_instance_of(Object).to receive(:require).with("application_decorator").and_return(
stub_const "ApplicationDecorator", Class.new
)

View File

@ -0,0 +1,19 @@
require 'spec_helper'
require 'dummy/config/environment'
require 'ammeter/init'
require 'generators/draper/install_generator'
describe Draper::Generators::InstallGenerator do
destination File.expand_path('../tmp', __FILE__)
before { prepare_destination }
after(:all) { FileUtils.rm_rf destination_root }
describe 'the application decorator' do
subject { file('app/decorators/application_decorator.rb') }
before { run_generator }
it { is_expected.to contain 'class ApplicationDecorator' }
end
end

View File

@ -1,6 +1,7 @@
require 'spec_helper'
require 'support/dummy_app'
require 'support/matchers/have_text'
SimpleCov.command_name 'test:integration'
app = DummyApp.new(ENV["RAILS_ENV"])
spec_types = {
@ -38,16 +39,19 @@ app.start_server do
expect(page).to have_text("Goodnight, moon!").in("#goodnight_moon")
end
it "can be passed to path helpers" do
expect(page).to have_text("/en/posts/1").in("#path_with_decorator")
end
# _path helpers aren't available in mailers
if type == :view
it "can be passed to path helpers" do
expect(page).to have_text("/en/posts/1").in("#path_with_decorator")
end
it "can use path helpers with a model" do
expect(page).to have_text("/en/posts/1").in("#path_with_model")
end
it "can use path helpers with a model" do
expect(page).to have_text("/en/posts/1").in("#path_with_model")
end
it "can use path helpers with an id" do
expect(page).to have_text("/en/posts/1").in("#path_with_id")
it "can use path helpers with an id" do
expect(page).to have_text("/en/posts/1").in("#path_with_id")
end
end
it "can be passed to url helpers" do

View File

@ -1,7 +1,7 @@
require 'rubygems'
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
Bundler.require(:default) if defined?(Bundler)
Bundler.require :default
require "benchmark"
require "draper"

17
spec/spec_helper.rb Executable file → Normal file
View File

@ -1,6 +1,12 @@
require 'simplecov'
SimpleCov.start do
add_filter 'spec'
add_group 'Draper', 'lib/draper'
add_group 'Generators', 'lib/generators'
end
require 'bundler/setup'
require 'draper'
require 'rails/version'
require 'action_controller'
require 'action_controller/test_case'
@ -32,20 +38,23 @@ class Model; include Draper::Decoratable; end
class Product < Model; end
class SpecialProduct < Product; end
class Other < Model; end
class Person < Model; end
class Child < Person; end
class ProductDecorator < Draper::Decorator; end
class ProductsDecorator < Draper::CollectionDecorator; end
class ProductPresenter < Draper::Decorator; end
class OtherDecorator < Draper::Decorator; end
module Namespaced
class Product < Model; end
class ProductDecorator < Draper::Decorator; end
ProductsDecorator = Class.new(Draper::CollectionDecorator)
class OtherDecorator < Draper::Decorator; end
end
ApplicationController = Class.new(ActionController::Base)
CustomController = Class.new(ActionController::Base)
# After each example, revert changes made to the class
def protect_class(klass)
before { stub_const klass.name, Class.new(klass) }

View File

@ -3,12 +3,12 @@ require 'spec_helper'
RSpec.shared_examples_for "view helpers" do |subject|
describe "#helpers" do
it "returns the current view context" do
allow(Draper::ViewContext).to receive(:current) { :current_view_context }
allow(Draper::ViewContext).to receive_messages current: :current_view_context
expect(subject.helpers).to be :current_view_context
end
it "is aliased to #h" do
allow(Draper::ViewContext).to receive(:current) { :current_view_context }
allow(Draper::ViewContext).to receive_messages current: :current_view_context
expect(subject.h).to be :current_view_context
end
end
@ -22,24 +22,26 @@ RSpec.shared_examples_for "view helpers" do |subject|
end
it "delegates to #helpers" do
expect(helpers).to receive(:localize).with(:an_object, some: "parameter")
allow(subject).to receive(:helpers).and_return(double)
expect(subject.helpers).to receive(:localize).with(:an_object, some: "parameter")
subject.localize(:an_object, some: "parameter")
end
it "is aliased to #l" do
expect(helpers).to receive(:localize).with(:an_object, some: 'parameter')
allow(subject).to receive_messages helpers: double
expect(subject.helpers).to receive(:localize).with(:an_object, some: "parameter")
subject.l(:an_object, some: "parameter")
end
end
describe ".helpers" do
it "returns the current view context" do
allow(Draper::ViewContext).to receive(:current) { :current_view_context }
allow(Draper::ViewContext).to receive_messages current: :current_view_context
expect(subject.class.helpers).to be :current_view_context
end
it "is aliased to .h" do
allow(Draper::ViewContext).to receive(:current) { :current_view_context }
allow(Draper::ViewContext).to receive(:current).and_return(:current_view_context)
expect(subject.class.h).to be :current_view_context
end
end