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 *.gem
*.rvmrc .rvmrc
.ruby-version
.ruby-gemset
.bundle .bundle
Gemfile.lock Gemfile.lock
pkg/* 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 language: ruby
sudo: false
cache: bundler
services: services:
- mongodb - mongodb
rvm: rvm:
- 2.1.5 - 2.3.5
- 2.2.1 - 2.4.3
- 2.2.2 - 2.5.4
- 2.2.3 - 2.6.2
- ruby-head
env: matrix:
- "RAILS_VERSION=4.0" allow_failures:
- "RAILS_VERSION=4.1" - rvm: ruby-head
- "RAILS_VERSION=4.2"
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 # 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 ## 2.1.0 - 2015-03-26
* Cleared most issues and merged a few PRs * Cleared most issues and merged a few PRs

17
Gemfile
View File

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

View File

@ -1,29 +1,26 @@
notification :gntp, host: '127.0.0.1'
def rspec_guard(options = {}, &block) def rspec_guard(options = {}, &block)
opts = { options = {
:cmd => 'rspec' version: 2,
notification: false
}.merge(options) }.merge(options)
guard 'rspec', opts, &block guard 'rspec', options, &block
end 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{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" } watch('spec/spec_helper.rb') { "spec" }
end 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{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" } watch('spec/spec_helper.rb') { "spec" }
end 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{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" } watch('spec/spec_helper.rb') { "spec" }
end 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 # Draper: View Models for Rails
[![TravisCI Build Status](https://travis-ci.org/drapergem/draper.svg?branch=master)](http://travis-ci.org/drapergem/draper) [![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) [![Code Climate](https://codeclimate.com/github/drapergem/draper.svg)](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) [![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 Draper adds an object-oriented layer of presentation logic to your Rails
application. application.
@ -48,7 +49,7 @@ end
But it makes you a little uncomfortable. `publication_status` lives in a But it makes you a little uncomfortable. `publication_status` lives in a
nebulous namespace spread across all controllers and view. Down the road, you 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 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 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 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 ## 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 ```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 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). wiki](https://github.com/drapergem/draper/wiki/Upgrading-to-1.0).
@ -132,6 +133,12 @@ end
### Generators ### 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... When you have Draper installed and generate a controller...
``` ```
@ -276,6 +283,19 @@ omitted.
delegate :current_page, :per_page, :offset, :total_entries, :total_pages 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 ### Decorating Associated Objects
You can automatically decorate associated models when the primary model is 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]) @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 ### When to Decorate Objects
Decorators are supposed to behave very much like the models they decorate, and 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 - 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. 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 ## Testing
Draper supports RSpec, MiniTest::Rails, and Test::Unit, and will add the 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' 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 ### Isolated Tests
In tests, Draper needs to build a view context to access helper methods. By 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') 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 ## Advanced usage
### Shared Decorator Methods ### Shared Decorator Methods
@ -455,7 +521,10 @@ end
When your decorator calls `delegate_all`, any method called on the decorator not 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 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 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: 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. 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 ### Making Models Decoratable
Models get their `decorate` method from the `Draper::Decoratable` module, which Models get their `decorate` method from the `Draper::Decoratable` module, which
is included in `ActiveRecord::Base` and `Mongoid::Document` by default. If is included in `ActiveRecord::Base` and `Mongoid::Document` by default. If
you're [using another you're using another ORM, or want to decorate plain old Ruby objects,
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 can include this module manually. 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 ## Contributors
Draper was conceived by Jeff Casimir and heavily refined by Steve Klabnik and a Draper was conceived by Jeff Casimir and heavily refined by Steve Klabnik and a
great community of open source great community of open source
[contributors](https://github.com/drapergem/draper/contributors). [contributors](https://github.com/drapergem/draper/contributors).
### Core Team ### Current maintainers
* Cliff Braton (cliff.braton@gmail.com)
### Historical maintainers
* Jeff Casimir (jeff@jumpstartlab.com) * Jeff Casimir (jeff@jumpstartlab.com)
* Steve Klabnik (steve@jumpstartlab.com) * Steve Klabnik (steve@jumpstartlab.com)
* Vasiliy Ermolovich * Vasiliy Ermolovich
* Andrew Haines * 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 "rm -f db/*.sqlite3"
run_in_dummy_app "RAILS_ENV=development rake db:schema:load db:seed" 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=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
end end

View File

@ -1,6 +1,4 @@
# -*- encoding: utf-8 -*- require File.join(__dir__, "lib", "draper", "version")
$:.push File.expand_path("../lib", __FILE__)
require "draper/version"
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "draper" 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.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"] s.require_paths = ["lib"]
s.add_dependency 'activesupport', '>= 3.0' s.required_ruby_version = '>= 2.2.2'
s.add_dependency 'actionpack', '>= 3.0'
s.add_dependency 'request_store', '~> 1.0' s.add_dependency 'activesupport', '>= 5.0'
s.add_dependency 'activemodel', '>= 3.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 'ammeter'
s.add_development_dependency 'rake', '>= 0.9.2' s.add_development_dependency 'rake'
s.add_development_dependency 'rspec-rails', '~> 3.3' s.add_development_dependency 'rspec-rails'
s.add_development_dependency 'minitest-rails', '>= 1.0' s.add_development_dependency 'minitest-rails'
s.add_development_dependency 'capybara' 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 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 'active_support/core_ext/name_error'
require 'draper/version' require 'draper/version'
require 'draper/configuration'
require 'draper/view_helpers' require 'draper/view_helpers'
require 'draper/compatibility/api_only'
require 'draper/delegation' require 'draper/delegation'
require 'draper/automatic_delegation' require 'draper/automatic_delegation'
require 'draper/finders' require 'draper/finders'
@ -21,19 +23,23 @@ require 'draper/factory'
require 'draper/decorated_association' require 'draper/decorated_association'
require 'draper/helper_support' require 'draper/helper_support'
require 'draper/view_context' require 'draper/view_context'
require 'draper/query_methods'
require 'draper/collection_decorator' require 'draper/collection_decorator'
require 'draper/undecorate' require 'draper/undecorate'
require 'draper/decorates_assigned' require 'draper/decorates_assigned'
require 'draper/railtie' if defined?(Rails) require 'draper/railtie' if defined?(Rails)
module Draper module Draper
extend Draper::Configuration
def self.setup_action_controller(base) def self.setup_action_controller(base)
base.class_eval do base.class_eval do
include Draper::Compatibility::ApiOnly if base == ActionController::API
include Draper::ViewContext include Draper::ViewContext
extend Draper::HelperSupport extend Draper::HelperSupport
extend Draper::DecoratesAssigned extend Draper::DecoratesAssigned
before_filter :activate_draper before_action :activate_draper
end end
end end
@ -55,9 +61,9 @@ module Draper
end end
end end
class UninferrableSourceError < NameError class UninferrableObjectError < NameError
def initialize(klass) def initialize(klass)
super("Could not infer a source for #{klass}.") super("Could not infer an object for #{klass}.")
end end
end end
end end

View File

@ -2,12 +2,14 @@ module Draper
module AutomaticDelegation module AutomaticDelegation
extend ActiveSupport::Concern 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) def method_missing(method, *args, &block)
return super unless delegatable?(method) return super unless delegatable?(method)
self.class.delegate method object.send(method, *args, &block)
send(method, *args, &block)
end end
# Checks if the decorator responds to an instance method, or is able to # Checks if the decorator responds to an instance method, or is able to
@ -18,6 +20,8 @@ module Draper
# @private # @private
def delegatable?(method) def delegatable?(method)
return if private_methods.include?(method)
object.respond_to?(method) object.respond_to?(method)
end end

View File

@ -2,6 +2,7 @@ module Draper
class CollectionDecorator class CollectionDecorator
include Enumerable include Enumerable
include Draper::ViewHelpers include Draper::ViewHelpers
include Draper::QueryMethods
extend Draper::Delegation extend Draper::Delegation
# @return the collection being decorated. # @return the collection being decorated.
@ -42,17 +43,7 @@ module Draper
@decorated_collection ||= object.map{|item| decorate_item(item)} @decorated_collection ||= object.map{|item| decorate_item(item)}
end end
# Delegated to the decorated collection when using the block form delegate :find, to: :decorated_collection
# (`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
def to_s def to_s
"#<#{self.class.name} of #{decorator_class || "inferred decorators"} for #{object.inspect}>" "#<#{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 end
module ClassMethods module ClassMethods
# Decorates a collection of objects. Used at the end of a scope chain. # Decorates a collection of objects. Used at the end of a scope chain.
# #
# @example # @example
@ -56,8 +55,7 @@ module Draper
# @param [Hash] options # @param [Hash] options
# see {Decorator.decorate_collection}. # see {Decorator.decorate_collection}.
def decorate(options = {}) def decorate(options = {})
collection = Rails::VERSION::MAJOR >= 4 ? all : scoped decorator_class.decorate_collection(all, options.reverse_merge(with: nil))
decorator_class.decorate_collection(collection, options.reverse_merge(with: nil))
end end
def decorator_class? def decorator_class?
@ -70,16 +68,16 @@ module Draper
# `Product` maps to `ProductDecorator`). # `Product` maps to `ProductDecorator`).
# #
# @return [Class] the inferred decorator class. # @return [Class] the inferred decorator class.
def decorator_class def decorator_class(called_on = self)
prefix = respond_to?(:model_name) ? model_name : name prefix = respond_to?(:model_name) ? model_name : name
decorator_name = "#{prefix}Decorator" decorator_name = "#{prefix}Decorator"
decorator_name.constantize decorator_name_constant = decorator_name.safe_constantize
rescue NameError => error return decorator_name_constant unless decorator_name_constant.nil?
if superclass.respond_to?(:decorator_class) if superclass.respond_to?(:decorator_class)
superclass.decorator_class superclass.decorator_class(called_on)
else else
raise unless error.missing_name?(decorator_name) raise Draper::UninferrableDecoratorError.new(called_on)
raise Draper::UninferrableDecoratorError.new(self)
end end
end end
@ -87,7 +85,7 @@ module Draper
# #
# @return [Boolean] # @return [Boolean]
def ===(other) def ===(other)
super || (other.respond_to?(:object) && super(other.object)) super || (other.is_a?(Draper::Decorator) && super(other.object))
end end
end end

View File

@ -1,7 +1,6 @@
module Draper module Draper
# @private # @private
class DecoratedAssociation class DecoratedAssociation
def initialize(owner, association, options) def initialize(owner, association, options)
options.assert_valid_keys(:with, :scope, :context) options.assert_valid_keys(:with, :scope, :context)
@ -30,6 +29,5 @@ module Draper
@decorated = factory.decorate(associated, context_args: owner.context) @decorated = factory.decorate(associated, context_args: owner.context)
end end
end 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 module Draper
class Decorator class Decorator
include Draper::ViewHelpers include Draper::ViewHelpers
include Draper::Compatibility::GlobalID if defined?(GlobalID)
extend Draper::Delegation extend Draper::Delegation
include ActiveModel::Serialization include ActiveModel::Serialization
@ -10,8 +13,6 @@ module Draper
# @return the object being decorated. # @return the object being decorated.
attr_reader :object attr_reader :object
alias_method :model, :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. # @return [Hash] extra data to be used in user-defined methods.
attr_accessor :context attr_accessor :context
@ -72,15 +73,10 @@ module Draper
# Checks whether this decorator class has a corresponding {object_class}. # Checks whether this decorator class has a corresponding {object_class}.
def self.object_class? def self.object_class?
object_class object_class
rescue Draper::UninferrableSourceError rescue Draper::UninferrableObjectError
false false
end 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 # Automatically decorates ActiveRecord finder methods, so that you can use
# `ProductDecorator.find(id)` instead of # `ProductDecorator.find(id)` instead of
# `ProductDecorator.decorate(Product.find(id))`. # `ProductDecorator.decorate(Product.find(id))`.
@ -182,7 +178,7 @@ module Draper
# Returns a unique hash for a decorated object based on # Returns a unique hash for a decorated object based on
# the decorator class and the object being decorated. # the decorator class and the object being decorated.
# #
# @return [Fixnum] # @return [Fixnum]
def hash def hash
self.class.hash ^ object.hash self.class.hash ^ object.hash
@ -203,18 +199,6 @@ module Draper
super || object.instance_of?(klass) super || object.instance_of?(klass)
end 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 delegate :to_s
# In case object is nil # In case object is nil
@ -241,10 +225,9 @@ module Draper
# @return [Class] the class created by {decorate_collection}. # @return [Class] the class created by {decorate_collection}.
def self.collection_decorator_class def self.collection_decorator_class
name = collection_decorator_name name = collection_decorator_name
name.constantize name_constant = name && name.safe_constantize
rescue NameError => error
raise if name && !error.missing_name?(name) name_constant || Draper::CollectionDecorator
Draper::CollectionDecorator
end end
private private
@ -259,22 +242,23 @@ module Draper
end end
def self.object_class_name def self.object_class_name
raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/ return nil if name.nil? || name.demodulize !~ /.+Decorator$/
name.chomp("Decorator") name.chomp("Decorator")
end end
def self.inferred_object_class def self.inferred_object_class
name = object_class_name name = object_class_name
name.constantize name_constant = name && name.safe_constantize
rescue NameError => error return name_constant unless name_constant.nil?
raise if name && !error.missing_name?(name)
raise Draper::UninferrableSourceError.new(self) raise Draper::UninferrableObjectError.new(self)
end end
def self.collection_decorator_name def self.collection_decorator_name
plural = object_class_name.pluralize singular = object_class_name
raise NameError if plural == object_class_name plural = singular && singular.pluralize
"#{plural}Decorator"
"#{plural}Decorator" unless plural == singular
end end
def handle_multiple_decoration(options) 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 # do not have to extend this module directly; it is extended by
# {Decorator.decorates_finders}. # {Decorator.decorates_finders}.
module Finders module Finders
def find(id, options = {}) def find(id, options = {})
decorate(object_class.find(id), options) decorate(object_class.find(id), options)
end end

View File

@ -2,11 +2,8 @@ module Draper
# Provides access to helper methods - both Rails built-in helpers, and those # Provides access to helper methods - both Rails built-in helpers, and those
# defined in your application. # defined in your application.
class HelperProxy class HelperProxy
# @overload initialize(view_context) # @overload initialize(view_context)
def initialize(view_context = nil) def initialize(view_context)
view_context ||= current_view_context # backwards compatibility
@view_context = view_context @view_context = view_context
end end
@ -35,10 +32,5 @@ module Draper
view_context.send(name, *args, &block) view_context.send(name, *args, &block)
end end
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
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 # so that you can stop typing `h.` everywhere, at the cost of mixing in a
# bazillion methods. # bazillion methods.
module LazyHelpers module LazyHelpers
# Sends missing methods to the {HelperProxy}. # Sends missing methods to the {HelperProxy}.
def method_missing(method, *args, &block) def method_missing(method, *args, &block)
helpers.send(method, *args, &block) helpers.send(method, *args, &block)
rescue NoMethodError rescue NoMethodError
super super
end end
end 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 module ActiveModel
class Railtie < Rails::Railtie class Railtie < Rails::Railtie
generators do |app| generators do |app|
app ||= Rails.application # Rails 3.0.x does not yield `app`
Rails::Generators.configure! app.config.generators Rails::Generators.configure! app.config.generators
require_relative '../generators/controller_override' require_relative '../generators/controller_override'
end end
@ -13,7 +11,6 @@ end
module Draper module Draper
class Railtie < Rails::Railtie class Railtie < Rails::Railtie
config.after_initialize do |app| config.after_initialize do |app|
app.config.paths.add 'app/decorators', eager_load: true app.config.paths.add 'app/decorators', eager_load: true
@ -23,19 +20,19 @@ module Draper
end end
end end
initializer "draper.setup_action_controller" do |app| initializer 'draper.setup_action_controller' do
ActiveSupport.on_load :action_controller do ActiveSupport.on_load :action_controller do
Draper.setup_action_controller self Draper.setup_action_controller self
end end
end end
initializer "draper.setup_action_mailer" do |app| initializer 'draper.setup_action_mailer' do
ActiveSupport.on_load :action_mailer do ActiveSupport.on_load :action_mailer do
Draper.setup_action_mailer self Draper.setup_action_mailer self
end end
end end
initializer "draper.setup_orm" do |app| initializer 'draper.setup_orm' do
[:active_record, :mongoid].each do |orm| [:active_record, :mongoid].each do |orm|
ActiveSupport.on_load orm do ActiveSupport.on_load orm do
Draper.setup_orm self Draper.setup_orm self
@ -43,28 +40,22 @@ module Draper
end end
end end
initializer "draper.setup_active_model_serializers" do |app| initializer 'draper.minitest-rails_integration' do
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|
ActiveSupport.on_load :minitest do ActiveSupport.on_load :minitest do
require "draper/test/minitest_integration" require 'draper/test/minitest_integration'
end end
end end
console do def initialize_view_context
require 'action_controller/test_case' require 'action_controller/test_case'
ApplicationController.new.view_context Draper.default_controller.new.view_context
Draper::ViewContext.build Draper::ViewContext.build
end end
rake_tasks do console { initialize_view_context }
Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f }
end runner { initialize_view_context }
rake_tasks { Dir[File.join(File.dirname(__FILE__), 'tasks/*.rake')].each { |f| load f } }
end end
end end

View File

@ -1,22 +1,9 @@
require 'rake/testtask' require 'rake/testtask'
require 'rails/test_unit/railtie'
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
namespace :test do namespace :test do
test_task.new(:decorators => "test:prepare") do |t| Rake::TestTask.new(decorators: "test:prepare") do |t|
t.libs << "test" t.libs << "test"
t.pattern = "test/decorators/**/*_test.rb" t.pattern = "test/decorators/**/*_test.rb"
end end
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 Draper
module DeviseHelper module DeviseHelper
def sign_in(resource_or_scope, resource = nil) def sign_in(resource_or_scope, resource = nil)
scope = begin scope = Devise::Mapping.find_scope!(resource_or_scope)
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
_stub_current_scope scope, resource || resource_or_scope _stub_current_scope scope, resource || resource_or_scope
end 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 end
RSpec.configure do |config| RSpec.configure do |config|
if RSpec::Core::Version::STRING.starts_with?("3") config.include DecoratorExampleGroup, file_path: %r{spec/decorators}, type: :decorator
config.include DecoratorExampleGroup, file_path: %r{spec/decorators}, type: :decorator
else
config.include DecoratorExampleGroup, example_group: {file_path: %r{spec/decorators}}, type: :decorator
end
[:decorator, :controller, :mailer].each do |type| [:decorator, :controller, :mailer].each do |type|
config.before(:each, type: type) { Draper::ViewContext.clear! } config.before(:each, type: type) { Draper::ViewContext.clear! }

View File

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

View File

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

View File

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

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

@ -20,8 +20,10 @@ module Draper
RequestStore.store[:current_controller] RequestStore.store[:current_controller]
end end
# Sets the current controller. # Sets the current controller. Clears view context when we are setting
# different controller.
def self.controller=(controller) def self.controller=(controller)
clear! if RequestStore.store[:current_controller] != controller
RequestStore.store[:current_controller] = controller RequestStore.store[:current_controller] = controller
end end
@ -82,23 +84,5 @@ module Draper
def self.build_strategy def self.build_strategy
@build_strategy ||= Draper::ViewContext::BuildStrategy.new(:full) @build_strategy ||= Draper::ViewContext::BuildStrategy.new(:full)
end 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
end end

View File

@ -2,7 +2,6 @@ module Draper
module ViewContext module ViewContext
# @private # @private
module BuildStrategy module BuildStrategy
def self.new(name, &block) def self.new(name, &block)
const_get(name.to_s.camelize).new(&block) const_get(name.to_s.camelize).new(&block)
end end
@ -37,12 +36,20 @@ module Draper
attr_reader :block attr_reader :block
def controller def controller
(Draper::ViewContext.controller || ApplicationController.new).tap do |controller| Draper::ViewContext.controller ||= Draper.default_controller.new
controller.request ||= ActionController::TestRequest.new if defined?(ActionController::TestRequest) Draper::ViewContext.controller.tap do |controller|
controller.request ||= new_test_request controller if defined?(ActionController::TestRequest)
end end
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 end
end end

View File

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

View File

@ -5,13 +5,13 @@ require "rails/generators/rails/scaffold_controller/scaffold_controller_generato
module Rails module Rails
module Generators module Generators
class ControllerGenerator class ControllerGenerator
hook_for :decorator, default: true do |generator| hook_for :decorator, type: :boolean, default: true do |generator|
invoke generator, [name.singularize] invoke generator, [name.singularize]
end end
end end
class ScaffoldControllerGenerator class ScaffoldControllerGenerator
hook_for :decorator, default: true hook_for :decorator, type: :boolean, default: true
end end
end 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 module Generators
class DecoratorGenerator < Base class DecoratorGenerator < Base
def self.source_root def self.source_root
File.expand_path('../templates', __FILE__) File.expand_path("templates", __dir__)
end 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" check_class_collision suffix: "DecoratorTest"

View File

@ -1,7 +1,7 @@
module Rails module Rails
module Generators module Generators
class DecoratorGenerator < NamedBase class DecoratorGenerator < NamedBase
source_root File.expand_path("../templates", __FILE__) source_root File.expand_path("templates", __dir__)
check_class_collision suffix: "Decorator" check_class_collision suffix: "Decorator"
class_option :parent, type: :string, desc: "The parent class for the generated decorator" class_option :parent, type: :string, desc: "The parent class for the generated decorator"
@ -24,13 +24,6 @@ module Rails
end end
end 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 end
end end

View File

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

View File

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

View File

@ -7,7 +7,6 @@ module Draper
describe "#initialize" do describe "#initialize" do
describe "options validation" do describe "options validation" do
it "does not raise error on valid options" do it "does not raise error on valid options" do
valid_options = {with: Decorator, context: {}} valid_options = {with: Decorator, context: {}}
expect{CollectionDecorator.new([], valid_options)}.not_to raise_error expect{CollectionDecorator.new([], valid_options)}.not_to raise_error
@ -121,26 +120,11 @@ module Draper
end end
describe "#find" do describe "#find" do
context "with a block" do it "decorates Enumerable#find" do
it "decorates Enumerable#find" do decorator = CollectionDecorator.new([])
decorator = CollectionDecorator.new([])
expect(decorator.decorated_collection).to receive(:find).and_return(:delegated) expect(decorator.decorated_collection).to receive(:find).and_return(:delegated)
expect(decorator.find{|p| p.title == "title"}).to be :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
end end
end end
@ -157,7 +141,7 @@ module Draper
it "delegates array methods to the decorated collection" do it "delegates array methods to the decorated collection" do
decorator = CollectionDecorator.new([]) 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 expect(decorator[42]).to be :delegated
end end
@ -302,6 +286,5 @@ module Draper
expect(decorator.replace([:foo, :bar])).to be decorator expect(decorator.replace([:foo, :bar])).to be decorator
end end
end end
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).to be_a ProductDecorator
expect(decorator.object).to be product expect(decorator.object).to be product
end end
it "accepts context" do it "accepts context" do
context = {some: "context"} context = {some: "context"}
@ -22,7 +22,8 @@ module Draper
it "uses the #decorator_class" do it "uses the #decorator_class" do
product = Product.new 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 expect(product.decorate).to be_an_instance_of OtherDecorator
end end
end end
@ -72,6 +73,16 @@ module Draper
expect(Product).to receive(:decorator_class).and_return(:some_decorator) expect(Product).to receive(:decorator_class).and_return(:some_decorator)
expect(product.decorator_class).to be :some_decorator expect(product.decorator_class).to be :some_decorator
end 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 end
describe "#==" do describe "#==" do
@ -108,37 +119,42 @@ module Draper
end end
it "is true for a decorated instance" do it "is true for a decorated instance" do
decorator = double(object: Product.new) decorator = Product.new.decorate
expect(Product === decorator).to be_truthy expect(Product === decorator).to be_truthy
end end
it "is true for a decorated derived instance" do 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 expect(Product === decorator).to be_truthy
end end
it "is false for a decorated unrelated instance" do 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 expect(Product === decorator).to be_falsey
end end
end end
describe ".decorate" do describe ".decorate" do
let(:scoping_method) { Rails::VERSION::MAJOR >= 4 ? :all : :scoped }
it "calls #decorate_collection on .decorator_class" do it "calls #decorate_collection on .decorator_class" do
scoped = [Product.new] 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.decorator_class).to receive(:decorate_collection).with(scoped, with: nil).and_return(:decorated_collection)
expect(Product.decorate).to be :decorated_collection expect(Product.decorate).to be :decorated_collection
end end
it "accepts options" do it "accepts options" do
options = {with: ProductDecorator, context: {some: "context"}} 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) expect(Product.decorator_class).to receive(:decorate_collection).with([], options)
Product.decorate(options) Product.decorate(options)
@ -182,6 +198,15 @@ module Draper
end end
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 context "when the decorator can't be inferred" do
it "throws an UninferrableDecoratorError" do it "throws an UninferrableDecoratorError" do
expect{Model.decorator_class}.to raise_error UninferrableDecoratorError expect{Model.decorator_class}.to raise_error UninferrableDecoratorError
@ -190,10 +215,22 @@ module Draper
context "when an unrelated NameError is thrown" do context "when an unrelated NameError is thrown" do
it "re-raises that error" 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/ expect{Product.decorator_class}.to raise_error NameError, /Draper::Base/
end end
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 end
end end

View File

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

View File

@ -1,7 +1,7 @@
require 'spec_helper' require 'spec_helper'
module Draper module Draper
RSpec.describe DecoratesAssigned do describe DecoratesAssigned do
let(:controller_class) do let(:controller_class) do
Class.new do Class.new do
extend DecoratesAssigned extend DecoratesAssigned
@ -28,14 +28,14 @@ module Draper
end end
it "creates a factory" do it "creates a factory" do
expect(Factory).to receive(:new).once allow(Factory).to receive(:new).once
controller_class.decorates_assigned :article, :author controller_class.decorates_assigned :article, :author
end end
it "passes options to the factory" do it "passes options to the factory" do
options = {foo: "bar"} options = {foo: "bar"}
expect(Factory).to receive(:new).with(options) allow(Factory).to receive(:new).with(options)
controller_class.decorates_assigned :article, :author, options controller_class.decorates_assigned :article, :author, options
end end
@ -43,7 +43,7 @@ module Draper
it "decorates the instance variable" do it "decorates the instance variable" do
object = double object = double
factory = double factory = double
allow(Factory).to receive(:new).and_return(factory) allow(Factory).to receive_messages(new: factory)
controller_class.decorates_assigned :article controller_class.decorates_assigned :article
controller = controller_class.new controller = controller_class.new
@ -55,7 +55,7 @@ module Draper
it "memoizes" do it "memoizes" do
factory = double factory = double
allow(Factory).to receive(:new).and_return(factory) allow(Factory).to receive_messages(new: factory)
controller_class.decorates_assigned :article controller_class.decorates_assigned :article
controller = controller_class.new controller = controller_class.new
@ -66,5 +66,6 @@ module Draper
end end
end end
end 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) ProductDecorator.decorate_collection([], options)
end end
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 end
describe ".decorates" do describe ".decorates" do
@ -181,42 +174,40 @@ module Draper
protect_class Namespaced::ProductDecorator protect_class Namespaced::ProductDecorator
context "when not set by .decorates" do context "when not set by .decorates" do
it "raises an UninferrableSourceError for a so-named 'Decorator'" do it "raises an UninferrableObjectError for a so-named 'Decorator'" do
expect{Decorator.object_class}.to raise_error UninferrableSourceError expect{Decorator.object_class}.to raise_error UninferrableObjectError
end end
it "raises an UninferrableSourceError for anonymous decorators" do it "raises an UninferrableObjectError for anonymous decorators" do
expect{Class.new(Decorator).object_class}.to raise_error UninferrableSourceError expect{Class.new(Decorator).object_class}.to raise_error UninferrableObjectError
end end
it "raises an UninferrableSourceError for a decorator without a model" do it "raises an UninferrableObjectError for a decorator without a model" do
skip SomeDecorator = Class.new(Draper::Decorator)
expect{OtherDecorator.object_class}.to raise_error UninferrableSourceError expect{SomeDecorator.object_class}.to raise_error UninferrableObjectError
end end
it "raises an UninferrableSourceError for other naming conventions" do it "raises an UninferrableObjectError for other naming conventions" do
expect{ProductPresenter.object_class}.to raise_error UninferrableSourceError ProductPresenter = Class.new(Draper::Decorator)
expect{ProductPresenter.object_class}.to raise_error UninferrableObjectError
end 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 expect(ProductDecorator.object_class).to be Product
end end
it "infers namespaced sources" do it "infers the object class for namespaced decorators" do
expect(Namespaced::ProductDecorator.object_class).to be Namespaced::Product expect(Namespaced::ProductDecorator.object_class).to be Namespaced::Product
end end
context "when an unrelated NameError is thrown" do context "when an unrelated NameError is thrown" do
it "re-raises that error" 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/ expect{ProductDecorator.object_class}.to raise_error NameError, /SomethingThatDoesntExist/
end end
end end
end end
it "is aliased to .source_class" do
expect(ProductDecorator.source_class).to be Product
end
end end
describe ".object_class?" do describe ".object_class?" do
@ -227,13 +218,24 @@ module Draper
end end
it "returns false when .object_class is not inferrable" do 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 expect(Decorator.object_class?).to be_falsey
end end
end
it "is aliased to .source_class?" do describe '.collection_decorator_class' do
allow(Decorator).to receive(:object_class).and_return(Model) it 'defaults to CollectionDecorator' do
expect(Decorator.source_class?).to be_truthy 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
end end
@ -335,7 +337,6 @@ module Draper
expect(decorator.object).to be object expect(decorator.object).to be object
expect(decorator.model).to be object expect(decorator.model).to be object
expect(decorator.to_source).to be object
end end
it "is aliased to #model" do it "is aliased to #model" do
@ -344,20 +345,6 @@ module Draper
expect(decorator.model).to be object expect(decorator.model).to be object
end 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 end
describe "aliasing object to object class name" do 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 it "returns only the object's attributes that are implemented by the decorator" do
decorator = Decorator.new(double(attributes: {foo: "bar", baz: "qux"})) decorator = Decorator.new(double(attributes: {foo: "bar", baz: "qux"}))
allow(decorator).to receive(:foo) allow(decorator).to receive(:foo)
expect(decorator.attributes).to eq({foo: "bar"}) expect(decorator.attributes).to eq({foo: "bar"})
end end
end end
describe ".model_name" do describe ".model_name" do
it "delegates to the source class" do it "delegates to the object class" do
allow(Decorator).to receive(:object_class) { double(model_name: :delegated) } allow(Decorator).to receive(:object_class).and_return(double(model_name: :delegated))
expect(Decorator.model_name).to be :delegated expect(Decorator.model_name).to be :delegated
end end
end end
@ -592,12 +581,51 @@ module Draper
expect(decorator.hello_world).to be :delegated expect(decorator.hello_world).to be :delegated
end end
it "adds delegated methods to the decorator when they are used" do it 'delegates `super` to parent class first' do
decorator = Decorator.new(double(hello_world: :delegated)) parent_decorator_class = Class.new(Decorator) do
def hello_world
"parent#hello_world"
end
end
expect(decorator.methods).not_to include :hello_world child_decorator_class = Class.new(parent_decorator_class) do
decorator.hello_world def hello_world
expect(decorator.methods).to include :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 end
it "passes blocks to delegated methods" do it "passes blocks to delegated methods" do
@ -616,7 +644,7 @@ module Draper
it "delegates already-delegated methods" do it "delegates already-delegated methods" do
object = Class.new{ delegate :bar, to: :foo }.new 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) decorator = Decorator.new(object)
expect(decorator.bar).to be :delegated expect(decorator.bar).to be :delegated
@ -636,26 +664,47 @@ module Draper
expect{decorator.hello_world}.to raise_error NoMethodError expect{decorator.hello_world}.to raise_error NoMethodError
expect(decorator.methods).not_to include :hello_world expect(decorator.methods).not_to include :hello_world
end 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 end
context ".method_missing" do context ".method_missing" do
context "without a source class" do context "without an object class" do
it "raises a NoMethodError on missing methods" do it "raises a NoMethodError on missing methods" do
expect{Decorator.hello_world}.to raise_error NoMethodError expect{Decorator.hello_world}.to raise_error NoMethodError
end end
end end
context "with a source class" do context "with an object class" do
it "delegates methods that exist on the source class" do it "delegates methods that exist on the object class" do
object_class = Class.new object_class = Class.new
allow(object_class).to receive(:hello_world).and_return(:delegated) allow(object_class).to receive_messages hello_world: :delegated
allow(Decorator).to receive(:object_class).and_return(object_class) allow(Decorator).to receive_messages object_class: object_class
expect(Decorator.hello_world).to be :delegated expect(Decorator.hello_world).to be :delegated
end end
it "does not delegate methods that do not exist on the source class" do it "does not delegate methods that do not exist on the object class" do
allow(Decorator).to receive(:object_class) { Class.new } allow(Decorator).to receive_messages object_class: Class.new
expect{Decorator.hello_world}.to raise_error NoMethodError expect{Decorator.hello_world}.to raise_error NoMethodError
end end
end end
@ -693,7 +742,7 @@ module Draper
end end
describe ".respond_to?" do 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 it "returns true for its own class methods" do
Decorator.class_eval{def self.hello_world; end} Decorator.class_eval{def self.hello_world; end}
@ -705,16 +754,16 @@ module Draper
end end
end end
context "with a source class" do context "with a object class" do
it "returns true for its own class methods" do it "returns true for its own class methods" do
Decorator.class_eval{def self.hello_world; end} 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 expect(Decorator).to respond_to :hello_world
end end
it "returns true for the source's class methods" do it "returns true for the object's 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).to respond_to :hello_world expect(Decorator).to respond_to :hello_world
end end
@ -732,7 +781,7 @@ module Draper
describe ".respond_to_missing?" do describe ".respond_to_missing?" do
it "allows .method to be called on delegated class methods" 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 expect(Decorator.method(:hello_world)).not_to be_nil
end end
@ -740,7 +789,7 @@ module Draper
end end
describe "class spoofing" do 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) decorator = Decorator.new(Model.new)
expect(decorator.kind_of?(Model)).to be_truthy expect(decorator.kind_of?(Model)).to be_truthy
@ -754,7 +803,7 @@ module Draper
expect(decorator.is_a?(Decorator)).to be_truthy expect(decorator.is_a?(Decorator)).to be_truthy
end 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) decorator = Decorator.new(Model.new)
expect(decorator.instance_of?(Model)).to be_truthy 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' require 'spec_helper'
module Draper module Draper
RSpec.describe Factory do Rspec.describe Factory do
describe "#initialize" do describe "#initialize" do
it "accepts valid options" do it "accepts valid options" do
valid_options = {with: Decorator, context: {foo: "bar"}} valid_options = {with: Decorator, context: {foo: "bar"}}
@ -64,7 +63,7 @@ module Draper
allow(Factory::Worker).to receive(:new).and_return(worker) allow(Factory::Worker).to receive(:new).and_return(worker)
options = {foo: "bar"} options = {foo: "bar"}
expect(worker).to receive(:call).with(options) allow(worker).to receive(:call).with(options)
factory.decorate(double, options) factory.decorate(double, options)
end end
@ -72,27 +71,25 @@ module Draper
it "sets the passed context" do it "sets the passed context" do
factory = Factory.new(context: {foo: "bar"}) factory = Factory.new(context: {foo: "bar"})
worker = ->(*){} 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"}) factory.decorate(double, {baz: "qux"})
end end
it "is overridden by explicitly-specified context" do it "is overridden by explicitly-specified context" do
factory = Factory.new(context: {foo: "bar"}) factory = Factory.new(context: {foo: "bar"})
worker = ->(*){} 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"}) expect(worker).to receive(:call).with(context: {baz: "qux"})
factory.decorate(double, context: {baz: "qux"}) factory.decorate(double, context: {baz: "qux"})
end end
end end
end end
end end
RSpec.describe Factory::Worker do Rspec.describe Factory::Worker do
describe "#call" do describe "#call" do
it "calls the decorator method" do it "calls the decorator method" do
object = double object = double
@ -101,7 +98,7 @@ module Draper
decorator = ->(*){} decorator = ->(*){}
allow(worker).to receive(:decorator){ 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 expect(worker.call(options)).to be :decorated
end end
@ -109,7 +106,7 @@ module Draper
it "calls it" do it "calls it" do
worker = Factory::Worker.new(double, double) worker = Factory::Worker.new(double, double)
decorator = ->(*){} decorator = ->(*){}
allow(worker).to receive(:decorator) { decorator } allow(worker).to receive_messages decorator: decorator
context = {foo: "bar"} context = {foo: "bar"}
expect(decorator).to receive(:call).with(anything(), context: context) expect(decorator).to receive(:call).with(anything(), context: context)
@ -118,7 +115,7 @@ module Draper
it "receives arguments from the :context_args option" do it "receives arguments from the :context_args option" do
worker = Factory::Worker.new(double, double) worker = Factory::Worker.new(double, double)
allow(worker).to receive(:decorator) { ->(*){} } allow(worker).to receive_messages decorator: ->(*){}
context = ->{} context = ->{}
expect(context).to receive(:call).with(:foo, :bar) expect(context).to receive(:call).with(:foo, :bar)
@ -127,7 +124,7 @@ module Draper
it "wraps non-arrays passed to :context_args" do it "wraps non-arrays passed to :context_args" do
worker = Factory::Worker.new(double, double) worker = Factory::Worker.new(double, double)
allow(worker).to receive(:decorator) { ->(*){} } allow(worker).to receive_messages decorator: ->(*){}
context = ->{} context = ->{}
hash = {foo: "bar"} hash = {foo: "bar"}
@ -140,7 +137,7 @@ module Draper
it "doesn't call it" do it "doesn't call it" do
worker = Factory::Worker.new(double, double) worker = Factory::Worker.new(double, double)
decorator = ->(*){} decorator = ->(*){}
allow(worker).to receive(:decorator) { decorator } allow(worker).to receive_messages decorator: decorator
context = {foo: "bar"} context = {foo: "bar"}
expect(decorator).to receive(:call).with(anything(), context: context) 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 it "does not pass the :context_args option to the decorator" do
worker = Factory::Worker.new(double, double) worker = Factory::Worker.new(double, double)
decorator = ->(*){} decorator = ->(*){}
allow(worker).to receive(:decorator) { decorator } allow(worker).to receive_messages decorator: decorator
expect(decorator).to receive(:call).with(anything(), foo: "bar") expect(decorator).to receive(:call).with(anything(), foo: "bar")
worker.call(foo: "bar", context_args: []) worker.call(foo: "bar", context_args: [])

View File

@ -99,7 +99,7 @@ module Draper
describe ".all" do describe ".all" do
it "returns a decorated collection" do it "returns a decorated collection" do
found = [Product.new, Product.new] found = [Product.new, Product.new]
allow(Product).to receive(:all).and_return(found) allow(Product).to receive_messages all: found
decorator = ProductDecorator.all decorator = ProductDecorator.all
expect(decorator).to be_a Draper::CollectionDecorator 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 context "when a current controller is set" do
it "returns the controller's view context" do it "returns the controller's view context" do
view_context = fake_view_context 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 strategy = ViewContext::BuildStrategy::Full.new
expect(strategy.call).to be view_context expect(strategy.call).to be view_context
@ -23,31 +23,47 @@ module Draper
context "when a current controller is not set" do context "when a current controller is not set" do
it "uses ApplicationController" do it "uses ApplicationController" do
view_context = fake_view_context expect(Draper::ViewContext.controller).to be_nil
stub_const "ApplicationController", double(new: fake_controller(view_context)) view_context = ViewContext::BuildStrategy::Full.new.call
strategy = ViewContext::BuildStrategy::Full.new expect(view_context.controller).to eq Draper::ViewContext.controller
expect(view_context.controller).to be_an ApplicationController
expect(strategy.call).to be view_context
end end
end end
it "adds a request if one is not defined" do it "adds a request if one is not defined" do
controller = Class.new(ActionController::Base).new controller = Class.new(ActionController::Base).new
allow(ViewContext).to receive(:controller) { controller } allow(ViewContext).to receive_messages controller: controller
strategy = ViewContext::BuildStrategy::Full.new strategy = ViewContext::BuildStrategy::Full.new
expect(controller.request).to be_nil expect(controller.request).to be_nil
strategy.call strategy.call
expect(controller.request).to be_an ActionController::TestRequest expect(controller.request).to be_an ActionController::TestRequest
expect(controller.params).to eq({}) expect(controller.params).to be_empty
# sanity checks # sanity checks
expect(controller.view_context.request).to be controller.request expect(controller.view_context.request).to be controller.request
expect(controller.view_context.params).to be controller.params expect(controller.view_context.params).to be controller.params
end 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 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 strategy = ViewContext::BuildStrategy::Full.new do
def a_helper_method; end def a_helper_method; end
end end
@ -57,7 +73,7 @@ module Draper
it "includes modules into the view context from the constructor block" do it "includes modules into the view context from the constructor block" do
view_context = Object.new 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 helpers = Module.new do
def a_helper_method; end def a_helper_method; end
end end

View File

@ -18,7 +18,7 @@ module Draper
describe ".controller" do describe ".controller" do
it "returns the stored controller from RequestStore" 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 expect(ViewContext.controller).to be :stored_controller
end end
@ -27,24 +27,52 @@ module Draper
describe ".controller=" do describe ".controller=" do
it "stores a controller in RequestStore" do it "stores a controller in RequestStore" do
store = {} store = {}
allow(RequestStore).to receive(:store).and_return(store) allow(RequestStore).to receive_messages store: store
ViewContext.controller = :stored_controller ViewContext.controller = :stored_controller
expect(store[:current_controller]).to be :stored_controller expect(store[:current_controller]).to be :stored_controller
end 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 end
describe ".current" do describe ".current" do
it "returns the stored view context from RequestStore" 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 expect(ViewContext.current).to be :stored_view_context
end end
context "when no view context is stored" do context "when no view context is stored" do
it "builds a view context" do it "builds a view context" do
allow(RequestStore).to receive(:store).and_return({}) allow(RequestStore).to receive_messages store: {}
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) allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy)
expect(ViewContext.current).to be :new_helper_proxy expect(ViewContext.current).to be :new_helper_proxy
@ -52,8 +80,8 @@ module Draper
it "stores the built view context" do it "stores the built view context" do
store = {} store = {}
allow(RequestStore).to receive(:store).and_return(store) allow(RequestStore).to receive_messages store: store
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) allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy)
ViewContext.current ViewContext.current
@ -65,7 +93,7 @@ module Draper
describe ".current=" do describe ".current=" do
it "stores a helper proxy for the view context in RequestStore" do it "stores a helper proxy for the view context in RequestStore" do
store = {} 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) allow(HelperProxy).to receive(:new).with(:stored_view_context).and_return(:stored_helper_proxy)
ViewContext.current = :stored_view_context ViewContext.current = :stored_view_context
@ -76,7 +104,7 @@ module Draper
describe ".clear!" do describe ".clear!" do
it "clears the stored controller and view controller" do it "clears the stored controller and view controller" do
store = {current_controller: :stored_controller, current_view_context: :stored_view_context} 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! ViewContext.clear!
expect(store).not_to have_key :current_controller expect(store).not_to have_key :current_controller
@ -86,7 +114,7 @@ module Draper
describe ".build" do describe ".build" do
it "returns a new view context using the build strategy" 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 expect(ViewContext.build).to be :new_view_context
end end
@ -94,7 +122,7 @@ module Draper
describe ".build!" do describe ".build!" do
it "returns a helper proxy for the new view context" 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) allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy)
expect(ViewContext.build!).to be :new_helper_proxy expect(ViewContext.build!).to be :new_helper_proxy
@ -102,8 +130,8 @@ module Draper
it "stores the helper proxy" do it "stores the helper proxy" do
store = {} store = {}
allow(RequestStore).to receive(:store) { store } allow(RequestStore).to receive_messages store: store
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) allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy)
ViewContext.build! 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 decorates_assigned :post
def show def show
@ -8,7 +8,7 @@ class PostsController < ApplicationController
def mail def mail
post = Post.find(params[:id]) post = Post.find(params[:id])
email = PostMailer.decorated_email(post).deliver email = PostMailer.decorated_email(post).deliver
render text: email.body render html: email.body.to_s.html_safe
end end
private 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 # attr_accessible :title, :body
end end

View File

@ -20,14 +20,16 @@
<dt>Helpers from the controller:</dt> <dt>Helpers from the controller:</dt>
<dd id="goodnight_moon"><%= post.goodnight_moon %></dd> <dd id="goodnight_moon"><%= post.goodnight_moon %></dd>
<dt>Path with decorator:</dt> <% unless defined? mailer %>
<dd id="path_with_decorator"><%= post_path(post) %></dd> <dt>Path with decorator:</dt>
<dd id="path_with_decorator"><%= post_url(post) %></dd>
<dt>Path with model:</dt> <dt>Path with model:</dt>
<dd id="path_with_model"><%= post.path_with_model %></dd> <dd id="path_with_model"><%= post.path_with_model %></dd>
<dt>Path with id:</dt> <dt>Path with id:</dt>
<dd id="path_with_id"><%= post.path_with_id %></dd> <dd id="path_with_id"><%= post.path_with_id %></dd>
<% end %>
<dt>URL with decorator:</dt> <dt>URL with decorator:</dt>
<dd id="url_with_decorator"><%= post_url(post) %></dd> <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. # Configure the default encoding used in templates for Ruby 1.9.
config.encoding = "utf-8" config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters += [:password]
# Enable escaping HTML in JSON. # Enable escaping HTML in JSON.
config.active_support.escape_html_entities_in_json = true 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__) 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.active_support.deprecation = :stderr
config.eager_load = false config.eager_load = false
config.active_job.queue_adapter = :test
end 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: development:
# Configure available database sessions. (required) # Configure available database clients. (required)
sessions: clients:
# Defines the default session. (required) # Defines the default client. (required)
default: default:
# Defines the name of the default database that Mongoid can connect to. # Defines the name of the default database that Mongoid can connect to.
# (required). # (required).
database: dummy_development 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) # of host:port pairs. (required)
hosts: hosts:
- localhost:27017 - localhost:27017
options: options:
# Change whether the session persists in safe mode by default. # Change the default write concern. (default = { w: 1 })
# (default: false) # write:
# safe: false # w: 1
# Change the default consistency model to :eventual or :strong. # Change the default read preference. Valid options for mode are: :secondary,
# :eventual will send reads to secondaries, :strong sends everything # :secondary_preferred, :primary, :primary_preferred, :nearest
# to master. (default: :eventual) # (default: primary)
# consistency: :eventual # 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) # Configure Mongoid specific options. (optional)
options: 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) # Includes the root model name in json serialization. (default: false)
# include_root_in_json: 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 # include_type_for_serialization: false
# Preload all models in development, needed when models use # Preload all models in development, needed when models use
# inheritance. (default: false) # inheritance. (default: false)
# preload_models: 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. # Raise an error when performing a #find and the document is not found.
# (default: true) # (default: true)
# raise_not_found_error: true # raise_not_found_error: true
@ -57,23 +120,23 @@ development:
# existing method. (default: false) # existing method. (default: false)
# scope_overwrite_exception: false # scope_overwrite_exception: false
# Skip the database version check, used when connecting to a db without # Use Active Support's time zone in conversions. (default: true)
# admin access. (default: false)
# skip_version_check: false
# User Active Support's time zone in conversions. (default: true)
# use_activesupport_time_zone: true # use_activesupport_time_zone: true
# Ensure all times are UTC in the app side. (default: false) # Ensure all times are UTC in the app side. (default: false)
# use_utc: 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: test:
sessions: clients:
default: default:
database: dummy_test database: dummy_test
hosts: hosts:
- localhost:27017 - localhost:27017
options: options:
# In the test environment we lower the retries and retry interval to read:
# low amounts for fast failures. mode: :primary
max_retries: 1 max_pool_size: 1
retry_interval: 0

View File

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

View File

@ -11,11 +11,11 @@
# #
# It's strongly recommended to check this file into your version control system. # 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| create_table "posts", force: true do |t|
t.datetime "created_at", :null => false t.datetime "created_at", null: false
t.datetime "updated_at", :null => false t.datetime "updated_at", null: false
end end
end end

View File

@ -13,4 +13,4 @@ RSpec::Core::RakeTask.new :fast_spec do |t|
t.pattern = "fast_spec/**/*_spec.rb" t.pattern = "fast_spec/**/*_spec.rb"
end 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 RSpec.describe Draper::CollectionDecorator do
describe "#active_model_serializer" do describe "#active_model_serializer" do
it "returns ActiveModel::ArraySerializer" do it "returns ActiveModel::Serializer::CollectionSerializer" do
collection_decorator = Draper::CollectionDecorator.new([]) collection_decorator = Draper::CollectionDecorator.new([])
if defined?(ActiveModel::ArraySerializerSupport) collection_serializer = ActiveModel::Serializer.serializer_for(collection_decorator)
collection_serializer = collection_decorator.active_model_serializer
else
collection_serializer = ActiveModel::Serializer.serializer_for(collection_decorator)
end
expect(collection_serializer).to be ActiveModel::ArraySerializer expect(collection_serializer).to be ActiveModel::Serializer::CollectionSerializer
end end
end end
end end

View File

@ -51,14 +51,5 @@ if defined?(Devise)
expect(helper.current_user).to be_nil expect(helper.current_user).to be_nil
end 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
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 end
it "serializes to XML" do 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) xml = Capybara.string(decorator.to_xml)
expect(xml).to have_css "post > updated-at", text: "overridden" expect(xml).to have_css "post > updated-at", text: "overridden"
end end
it "uses a test view context from ApplicationController" do it "uses a test view context from BaseController" do
expect(Draper::ViewContext.current.controller).to be_an ApplicationController expect(Draper::ViewContext.current.controller).to be_an BaseController
end end
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" expect(email_body).to have_content "Today"
end 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 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}" expect(email_body).to have_css "#url_with_model", text: "http://www.example.com:12345/en/posts/#{post.id}"
end 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' require_relative '../shared_examples/decoratable'
RSpec.describe Post do 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 end

View File

@ -11,8 +11,6 @@ RSpec.shared_examples_for "a decoratable model" do
describe "#==" do describe "#==" do
it "is true for other instances' decorators" 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 described_class.create
one = described_class.first one = described_class.first
other = described_class.first other = described_class.first

View File

@ -51,14 +51,5 @@ if defined?(Devise)
assert helper.current_user.nil? assert helper.current_user.nil?
end 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
end end

View File

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

View File

@ -51,14 +51,5 @@ if defined?(Devise)
assert helper.current_user.nil? assert helper.current_user.nil?
end 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
end end

View File

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

View File

@ -1,11 +1,11 @@
require 'spec_helper' require 'spec_helper'
require_relative '../../dummy/spec/rails_helper' require 'dummy/config/environment'
require 'rails'
require 'ammeter/init' require 'ammeter/init'
require 'generators/controller_override' require 'generators/controller_override'
require 'generators/rails/decorator_generator' 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__) destination File.expand_path("../tmp", __FILE__)
before { prepare_destination } before { prepare_destination }

View File

@ -1,10 +1,9 @@
require 'spec_helper' require 'spec_helper'
require 'rspec/rails' require 'dummy/config/environment'
# require_relative '../../dummy/spec/rails_helper'
require 'ammeter/init' require 'ammeter/init'
require 'generators/rails/decorator_generator' require 'generators/rails/decorator_generator'
RSpec.describe Rails::Generators::DecoratorGenerator do describe Rails::Generators::DecoratorGenerator do
destination File.expand_path("../tmp", __FILE__) destination File.expand_path("../tmp", __FILE__)
before { prepare_destination } before { prepare_destination }
@ -41,6 +40,7 @@ RSpec.describe Rails::Generators::DecoratorGenerator do
context "with an ApplicationDecorator" do context "with an ApplicationDecorator" do
before do before do
allow_any_instance_of(Object).to receive(:require)
allow_any_instance_of(Object).to receive(:require).with("application_decorator").and_return( allow_any_instance_of(Object).to receive(:require).with("application_decorator").and_return(
stub_const "ApplicationDecorator", Class.new 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 'spec_helper'
require 'support/dummy_app' require 'support/dummy_app'
require 'support/matchers/have_text' require 'support/matchers/have_text'
SimpleCov.command_name 'test:integration'
app = DummyApp.new(ENV["RAILS_ENV"]) app = DummyApp.new(ENV["RAILS_ENV"])
spec_types = { spec_types = {
@ -38,16 +39,19 @@ app.start_server do
expect(page).to have_text("Goodnight, moon!").in("#goodnight_moon") expect(page).to have_text("Goodnight, moon!").in("#goodnight_moon")
end end
it "can be passed to path helpers" do # _path helpers aren't available in mailers
expect(page).to have_text("/en/posts/1").in("#path_with_decorator") if type == :view
end 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 it "can use path helpers with a model" do
expect(page).to have_text("/en/posts/1").in("#path_with_model") expect(page).to have_text("/en/posts/1").in("#path_with_model")
end end
it "can use path helpers with an id" do it "can use path helpers with an id" do
expect(page).to have_text("/en/posts/1").in("#path_with_id") expect(page).to have_text("/en/posts/1").in("#path_with_id")
end
end end
it "can be passed to url helpers" do it "can be passed to url helpers" do

View File

@ -1,7 +1,7 @@
require 'rubygems' require 'rubygems'
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
Bundler.require(:default) if defined?(Bundler) Bundler.require :default
require "benchmark" require "benchmark"
require "draper" 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 'bundler/setup'
require 'draper' require 'draper'
require 'rails/version'
require 'action_controller' require 'action_controller'
require 'action_controller/test_case' require 'action_controller/test_case'
@ -32,20 +38,23 @@ class Model; include Draper::Decoratable; end
class Product < Model; end class Product < Model; end
class SpecialProduct < Product; end class SpecialProduct < Product; end
class Other < Model; end class Other < Model; end
class Person < Model; end
class Child < Person; end
class ProductDecorator < Draper::Decorator; end class ProductDecorator < Draper::Decorator; end
class ProductsDecorator < Draper::CollectionDecorator; end class ProductsDecorator < Draper::CollectionDecorator; end
class ProductPresenter < Draper::Decorator; end
class OtherDecorator < Draper::Decorator; end class OtherDecorator < Draper::Decorator; end
module Namespaced module Namespaced
class Product < Model; end class Product < Model; end
class ProductDecorator < Draper::Decorator; end class ProductDecorator < Draper::Decorator; end
ProductsDecorator = Class.new(Draper::CollectionDecorator)
class OtherDecorator < Draper::Decorator; end class OtherDecorator < Draper::Decorator; end
end end
ApplicationController = Class.new(ActionController::Base)
CustomController = Class.new(ActionController::Base)
# After each example, revert changes made to the class # After each example, revert changes made to the class
def protect_class(klass) def protect_class(klass)
before { stub_const klass.name, Class.new(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| RSpec.shared_examples_for "view helpers" do |subject|
describe "#helpers" do describe "#helpers" do
it "returns the current view context" 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 expect(subject.helpers).to be :current_view_context
end end
it "is aliased to #h" do 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 expect(subject.h).to be :current_view_context
end end
end end
@ -22,24 +22,26 @@ RSpec.shared_examples_for "view helpers" do |subject|
end end
it "delegates to #helpers" do 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") subject.localize(:an_object, some: "parameter")
end end
it "is aliased to #l" do 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") subject.l(:an_object, some: "parameter")
end end
end end
describe ".helpers" do describe ".helpers" do
it "returns the current view context" 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 expect(subject.class.helpers).to be :current_view_context
end end
it "is aliased to .h" do 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 expect(subject.class.h).to be :current_view_context
end end
end end