diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..b86b6ea --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,16 @@ +--- +engines: + duplication: + enabled: true + config: + languages: + - ruby + fixme: + enabled: true + rubocop: + enabled: true +ratings: + paths: + - "**.rb" +exclude_paths: +- spec/ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..6ffb093 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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/####) diff --git a/.gitignore b/.gitignore index 010ce16..fba20e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.gem -*.rvmrc +.rvmrc +.ruby-version +.ruby-gemset .bundle Gemfile.lock pkg/* diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..aa61678 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,11 @@ +AllCops: + TargetRubyVersion: 2.4 + DisplayCopNames: true + Exclude: + - 'spec/dummy/**/*' + +Style/StringLiterals: + Enabled: false + +Metrics/LineLength: + Max: 100 diff --git a/.travis.yml b/.travis.yml index ce21326..02f9017 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,29 @@ +env: + global: + - CC_TEST_REPORTER_ID=b7ba588af2a540fa96c267b3655a2afe31ea29976dc25905a668dd28d5e88915 + language: ruby +sudo: false +cache: bundler services: - mongodb rvm: - - 2.1.5 - - 2.2.1 - - 2.2.2 - - 2.2.3 + - 2.3.5 + - 2.4.3 + - 2.5.4 + - 2.6.2 + - ruby-head -env: - - "RAILS_VERSION=4.0" - - "RAILS_VERSION=4.1" - - "RAILS_VERSION=4.2" +matrix: + allow_failures: + - rvm: ruby-head + +before_script: + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + - ./cc-test-reporter before-build + +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe535c..1cc8933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Draper Changelog +## 3.1.0 +* Rails 6 support [#841](https://github.com/drapergem/draper/pull/841) +* Include ORM query methods in `CollectionDecorator` (e.g. `includes`) [#845](https://github.com/drapergem/draper/pull/845) +* Document the fix for view context leaking in specs [#847](https://github.com/drapergem/draper/pull/847) + +## 3.0.1 +* Let `decorator_class` infer anonymous class decorator from superclass [#820](https://github.com/drapergem/draper/pull/820) +* When inferring decorator fails, show original class instead of `ActiveRecord::Base` [#821](https://github.com/drapergem/draper/pull/821) +* ActiveJob compatibility and documentation [#817](https://github.com/drapergem/draper/pull/817) + +## 3.0.0 - 2017 + +### Breaking Changes +* Rename UninferrableSourceError to UninferrableObjectError [#768](https://github.com/drapergem/draper/pull/768) +* Remove conflicting source aliases: `source`, `to_source`, `source_class` and `source_class?` [#786](https://github.com/drapergem/draper/pull/786) + +### New Features +* Generator for creating `ApplicationDecorator` that other decorators inherit from [#796](https://github.com/drapergem/draper/pull/796) +* Draper configuration with ability to customize the controller Draper uses [#788](https://github.com/drapergem/draper/pull/788) +* Added support for Rails 5 API-only applications [#793](https://github.com/drapergem/draper/pull/793) +* Added support for Rails runner [#739](https://github.com/drapergem/draper/pull/739) + +### Other Changes +* Clear view context when the controller changes [#799](https://github.com/drapergem/draper/pull/799) +* Removed previously deprecated functionality [#785](https://github.com/drapergem/draper/pull/785) +* Only delegate === if other is an instance of a class that inherits from `Decorator` [#720](https://github.com/drapergem/draper/pull/720) +* Always default to `CollectionDecorator` when `NameError` is raised [#795](https://github.com/drapergem/draper/pull/795) +* Fixed issues in order to support Rails 5.1 +* Fixed a bug where helpers were used inside a decorator and this decorator was used outside of controller context + +## 3.0.0.pre1 - 2016-07-10 + +* Added support for Rails 5, dropped 4.0 - 4.2 + * Ruby >= 2.2 is required, matching Rails 5 +* Dropped support for ActiveModelSerializers 0.8 + ## 2.1.0 - 2015-03-26 * Cleared most issues and merged a few PRs diff --git a/Gemfile b/Gemfile index d1190ad..788cbd0 100644 --- a/Gemfile +++ b/Gemfile @@ -3,20 +3,13 @@ source "https://rubygems.org" gemspec platforms :ruby do - gem "sqlite3" + gem 'sqlite3', '~> 1.3.6' end platforms :jruby do - gem "minitest", ">= 3.0" - gem "activerecord-jdbcsqlite3-adapter", ">= 1.3.0.beta2" + gem "minitest" + gem "activerecord-jdbcsqlite3-adapter" end -group :development, :test do - gem 'guard-rspec', require: false - gem 'ruby_gntp' - gem 'colorize' -end - -version = ENV["RAILS_VERSION"] || "4.1" - -eval_gemfile File.expand_path("../gemfiles/#{version}.gemfile", __FILE__) +gem "rails", "~> 5.0" +gem "mongoid", github: "mongodb/mongoid" diff --git a/Guardfile b/Guardfile index ac973cd..2c1a566 100644 --- a/Guardfile +++ b/Guardfile @@ -1,29 +1,26 @@ -notification :gntp, host: '127.0.0.1' - def rspec_guard(options = {}, &block) - opts = { - :cmd => 'rspec' + options = { + version: 2, + notification: false }.merge(options) - guard 'rspec', opts, &block + guard 'rspec', options, &block end -rspec_guard :spec_paths => %w{spec/draper spec/generators} do +rspec_guard spec_paths: %w{spec/draper spec/generators} do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { "spec" } end -rspec_guard :spec_paths => ['spec/integration'], cmd: 'RAILS_ENV=development rspec' do +rspec_guard spec_paths: 'spec/integration', env: {'RAILS_ENV' => 'development'} do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { "spec" } end -rspec_guard :spec_paths => ['spec/integration'], cmd: 'RAILS_ENV=production rspec' do +rspec_guard spec_paths: 'spec/integration', env: {'RAILS_ENV' => 'production'} do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { "spec" } end - -# vim: set ts=8 sw=2 tw=0 ft=ruby et : diff --git a/README.md b/README.md index 80009a5..1895852 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Draper: View Models for Rails [![TravisCI Build Status](https://travis-ci.org/drapergem/draper.svg?branch=master)](http://travis-ci.org/drapergem/draper) -[![Code Climate](https://codeclimate.com/github/drapergem/draper.png)](https://codeclimate.com/github/drapergem/draper) -[![Inline docs](http://inch-ci.org/github/drapergem/draper.png?branch=master)](http://inch-ci.org/github/drapergem/draper) +[![Code Climate](https://codeclimate.com/github/drapergem/draper.svg)](https://codeclimate.com/github/drapergem/draper) +[![Test Coverage](https://api.codeclimate.com/v1/badges/0d40c43951d516bf6985/test_coverage)](https://codeclimate.com/github/drapergem/draper/test_coverage) +[![Inline docs](http://inch-ci.org/github/drapergem/draper.svg?branch=master)](http://inch-ci.org/github/drapergem/draper) Draper adds an object-oriented layer of presentation logic to your Rails application. @@ -48,7 +49,7 @@ end But it makes you a little uncomfortable. `publication_status` lives in a nebulous namespace spread across all controllers and view. Down the road, you might want to display the publication status of a `Book`. And, of course, your -design calls for a slighly different formatting to the date for a `Book`. +design calls for a slightly different formatting to the date for a `Book`. Now your helper method can either switch based on the input class type (poor Ruby style), or you break it out into two methods, `book_publication_status` and @@ -107,13 +108,13 @@ Decorators are the ideal place to: ## Installation -Add Draper to your Gemfile: +As of version 3.0.0, Draper is only compatible with Rails 5 / Ruby 2.2 and later. Add Draper to your Gemfile. ```ruby -gem 'draper', '~> 1.3' + gem 'draper' ``` -And run `bundle install` within your app's directory. +After that, run `bundle install` within your app's directory. If you're upgrading from a 0.x release, the major changes are outlined [in the wiki](https://github.com/drapergem/draper/wiki/Upgrading-to-1.0). @@ -132,6 +133,12 @@ end ### Generators +To create an `ApplicationDecorator` that all generated decorators inherit from, run... + +``` +rails generate draper:install +``` + When you have Draper installed and generate a controller... ``` @@ -276,6 +283,19 @@ omitted. delegate :current_page, :per_page, :offset, :total_entries, :total_pages ``` +If needed, you can then set the collection_decorator_class of your CustomDecorator as follows: +```ruby +class ArticleDecorator < Draper::Decorator + def self.collection_decorator_class + PaginatingDecorator + end +end + +ArticleDecorator.decorate_collection(@articles.paginate) +# => Collection decorated by PaginatingDecorator +# => Members decorated by ArticleDecorator +``` + ### Decorating Associated Objects You can automatically decorate associated models when the primary model is @@ -307,6 +327,17 @@ your `ArticleDecorator` and they'll return decorated objects: @article = ArticleDecorator.find(params[:id]) ``` +### Decorated Query Methods +By default, Draper will decorate all [QueryMethods](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html) +of ActiveRecord. +If you're using another ORM, in order to support it, you can tell Draper to use a custom strategy: + +```ruby +Draper.configure do |config| + config.default_query_methods_strategy = :mongoid +end +``` + ### When to Decorate Objects Decorators are supposed to behave very much like the models they decorate, and @@ -348,6 +379,18 @@ you'll have access to an ArticleDecorator object instead. In your controller you can continue to use the `@article` instance variable to manipulate the model - for example, `@article.comments.build` to add a new blank comment for a form. +## Configuration +Draper works out the box well, but also provides a hook for you to configure its +default functionality. For example, Draper assumes you have a base `ApplicationController`. +If your base controller is named something different (e.g. `BaseController`), +you can tell Draper to use it by adding the following to an initializer: + +```ruby +Draper.configure do |config| + config.default_controller = BaseController +end +``` + ## Testing Draper supports RSpec, MiniTest::Rails, and Test::Unit, and will add the @@ -379,6 +422,15 @@ In your `Spork.prefork` block of `spec_helper.rb`, add this: require 'draper/test/rspec_integration' ``` +#### Custom Draper Controller ViewContext +If running tests in an engine setting with a controller other than "ApplicationController," set a custom controller in `spec_helper.rb` + +```ruby +config.before(:each, type: :decorator) do |example| + Draper::ViewContext.controller = ExampleEngine::CustomRootController.new +end +``` + ### Isolated Tests In tests, Draper needs to build a view context to access helper methods. By @@ -423,6 +475,20 @@ preferred stubbing technique (this example uses RSpec's `stub` method): helpers.stub(users_path: '/users') ``` +### View context leakage +As mentioned before, Draper needs to build a view context to access helper methods. In MiniTest, the view context is +cleared during `before_setup` preventing any view context leakage. In RSpec, the view context is cleared before each +`decorator`, `controller`, and `mailer` spec. However, if you use decorators in other types of specs +(e.g. `job`), you may still experience the view context leaking from the previous spec. To solve this, add the +following to your `spec_helper` for each type of spec you are experiencing the leakage: + +```ruby +config.before(:each, type: :type) { Draper::ViewContext.clear! } +``` + +_Note_: The `:type` above is just a placeholder. Replace `:type` with the type of spec you are experiencing +the leakage from. + ## Advanced usage ### Shared Decorator Methods @@ -455,7 +521,10 @@ end When your decorator calls `delegate_all`, any method called on the decorator not defined in the decorator itself will be delegated to the decorated object. This -is a very permissive interface. +includes calling `super` from within the decorator. A call to `super` from within +the decorator will first try to call the method on the parent decorator class. If +the method does not exist on the parent decorator class, it will then try to call +the method on the decorated `object`. This is a very permissive interface. If you want to strictly control which methods are called within views, you can choose to only delegate certain methods from the decorator to the source model: @@ -564,24 +633,40 @@ end This is only necessary when proxying class methods. +Once this association between the decorator and the model is set up, you can call +`SomeModel.decorator_class` to access class methods defined in the decorator. +If necessary, you can check if your model is decorated with `SomeModel.decorator_class?`. + ### Making Models Decoratable Models get their `decorate` method from the `Draper::Decoratable` module, which is included in `ActiveRecord::Base` and `Mongoid::Document` by default. If -you're [using another -ORM](https://github.com/drapergem/draper/wiki/Using-other-ORMs) (including -versions of Mongoid prior to 3.0), or want to decorate plain old Ruby objects, +you're using another ORM, or want to decorate plain old Ruby objects, you can include this module manually. +### Active Job Integration + +[Active Job](http://edgeguides.rubyonrails.org/active_job_basics.html) allows you to pass ActiveRecord +objects to background tasks directly and performs the necessary serialization and deserialization. In +order to do this, arguments to a background job must implement [Global ID](https://github.com/rails/globalid). +Decorated objects implement Global ID by delegating to the object they are decorating. This means +you can pass decorated objects to background jobs, however, the object won't be decorated when it is +deserialized. + ## Contributors Draper was conceived by Jeff Casimir and heavily refined by Steve Klabnik and a great community of open source [contributors](https://github.com/drapergem/draper/contributors). -### Core Team +### Current maintainers + +* Cliff Braton (cliff.braton@gmail.com) + +### Historical maintainers * Jeff Casimir (jeff@jumpstartlab.com) * Steve Klabnik (steve@jumpstartlab.com) * Vasiliy Ermolovich * Andrew Haines +* Sean Linsley diff --git a/Rakefile b/Rakefile index 848fc54..b80230c 100644 --- a/Rakefile +++ b/Rakefile @@ -64,6 +64,6 @@ namespace "db" do run_in_dummy_app "rm -f db/*.sqlite3" run_in_dummy_app "RAILS_ENV=development rake db:schema:load db:seed" run_in_dummy_app "RAILS_ENV=production rake db:schema:load db:seed" - run_in_dummy_app "RAILS_ENV=test rake db:schema:load" + run_in_dummy_app "RAILS_ENV=test rake db:environment:set db:schema:load" end end diff --git a/draper.gemspec b/draper.gemspec index 23b8e62..7134318 100644 --- a/draper.gemspec +++ b/draper.gemspec @@ -1,6 +1,4 @@ -# -*- encoding: utf-8 -*- -$:.push File.expand_path("../lib", __FILE__) -require "draper/version" +require File.join(__dir__, "lib", "draper", "version") Gem::Specification.new do |s| s.name = "draper" @@ -17,15 +15,20 @@ Gem::Specification.new do |s| s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] - s.add_dependency 'activesupport', '>= 3.0' - s.add_dependency 'actionpack', '>= 3.0' - s.add_dependency 'request_store', '~> 1.0' - s.add_dependency 'activemodel', '>= 3.0' + s.required_ruby_version = '>= 2.2.2' + + s.add_dependency 'activesupport', '>= 5.0' + s.add_dependency 'actionpack', '>= 5.0' + s.add_dependency 'request_store', '>= 1.0' + s.add_dependency 'activemodel', '>= 5.0' + s.add_dependency 'activemodel-serializers-xml', '>= 1.0' s.add_development_dependency 'ammeter' - s.add_development_dependency 'rake', '>= 0.9.2' - s.add_development_dependency 'rspec-rails', '~> 3.3' - s.add_development_dependency 'minitest-rails', '>= 1.0' + s.add_development_dependency 'rake' + s.add_development_dependency 'rspec-rails' + s.add_development_dependency 'minitest-rails' s.add_development_dependency 'capybara' - s.add_development_dependency 'active_model_serializers' + s.add_development_dependency 'active_model_serializers', '>= 0.10' + s.add_development_dependency 'rubocop' + s.add_development_dependency 'simplecov' end diff --git a/gemfiles/4.0.gemfile b/gemfiles/4.0.gemfile deleted file mode 100644 index 8e03c65..0000000 --- a/gemfiles/4.0.gemfile +++ /dev/null @@ -1,3 +0,0 @@ -gem "rails", "~> 4.0.0" -gem "mongoid", "~> 4.0" -gem "devise", "~> 3.0.0" diff --git a/gemfiles/4.1.gemfile b/gemfiles/4.1.gemfile deleted file mode 100644 index 34d85f4..0000000 --- a/gemfiles/4.1.gemfile +++ /dev/null @@ -1,3 +0,0 @@ -gem "rails", "~> 4.1.0" -gem "mongoid", "~> 4.0" -gem "devise", "~> 3.2" diff --git a/gemfiles/4.2.gemfile b/gemfiles/4.2.gemfile deleted file mode 100644 index 8bac430..0000000 --- a/gemfiles/4.2.gemfile +++ /dev/null @@ -1,3 +0,0 @@ -gem "rails", "~> 4.2.0" -gem "mongoid", "~> 4.0" -gem "devise", "~> 3.4" diff --git a/lib/draper.rb b/lib/draper.rb index 351a81c..4f8b667 100644 --- a/lib/draper.rb +++ b/lib/draper.rb @@ -9,7 +9,9 @@ require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/name_error' require 'draper/version' +require 'draper/configuration' require 'draper/view_helpers' +require 'draper/compatibility/api_only' require 'draper/delegation' require 'draper/automatic_delegation' require 'draper/finders' @@ -21,19 +23,23 @@ require 'draper/factory' require 'draper/decorated_association' require 'draper/helper_support' require 'draper/view_context' +require 'draper/query_methods' require 'draper/collection_decorator' require 'draper/undecorate' require 'draper/decorates_assigned' require 'draper/railtie' if defined?(Rails) module Draper + extend Draper::Configuration + def self.setup_action_controller(base) base.class_eval do + include Draper::Compatibility::ApiOnly if base == ActionController::API include Draper::ViewContext extend Draper::HelperSupport extend Draper::DecoratesAssigned - before_filter :activate_draper + before_action :activate_draper end end @@ -55,9 +61,9 @@ module Draper end end - class UninferrableSourceError < NameError + class UninferrableObjectError < NameError def initialize(klass) - super("Could not infer a source for #{klass}.") + super("Could not infer an object for #{klass}.") end end end diff --git a/lib/draper/automatic_delegation.rb b/lib/draper/automatic_delegation.rb index 41e0e71..8d62faf 100644 --- a/lib/draper/automatic_delegation.rb +++ b/lib/draper/automatic_delegation.rb @@ -2,12 +2,14 @@ module Draper module AutomaticDelegation extend ActiveSupport::Concern - # Delegates missing instance methods to the source object. + # Delegates missing instance methods to the source object. Note: This will delegate `super` + # method calls to `object` as well. Calling `super` will first try to call the method on + # the parent decorator class. If no method exists on the parent class, it will then try + # to call the method on the `object`. def method_missing(method, *args, &block) return super unless delegatable?(method) - self.class.delegate method - send(method, *args, &block) + object.send(method, *args, &block) end # Checks if the decorator responds to an instance method, or is able to @@ -18,6 +20,8 @@ module Draper # @private def delegatable?(method) + return if private_methods.include?(method) + object.respond_to?(method) end diff --git a/lib/draper/collection_decorator.rb b/lib/draper/collection_decorator.rb index 2f3102d..3bcb5e6 100644 --- a/lib/draper/collection_decorator.rb +++ b/lib/draper/collection_decorator.rb @@ -2,6 +2,7 @@ module Draper class CollectionDecorator include Enumerable include Draper::ViewHelpers + include Draper::QueryMethods extend Draper::Delegation # @return the collection being decorated. @@ -42,17 +43,7 @@ module Draper @decorated_collection ||= object.map{|item| decorate_item(item)} end - # Delegated to the decorated collection when using the block form - # (`Enumerable#find`) or to the decorator class if not - # (`ActiveRecord::FinderMethods#find`) - def find(*args, &block) - if block_given? - decorated_collection.find(*args, &block) - else - ActiveSupport::Deprecation.warn("Using ActiveRecord's `find` on a CollectionDecorator is deprecated. Call `find` on a model, and then decorate the result", caller) - decorate_item(object.find(*args)) - end - end + delegate :find, to: :decorated_collection def to_s "#<#{self.class.name} of #{decorator_class || "inferred decorators"} for #{object.inspect}>" diff --git a/lib/draper/compatibility/api_only.rb b/lib/draper/compatibility/api_only.rb new file mode 100644 index 0000000..44fb041 --- /dev/null +++ b/lib/draper/compatibility/api_only.rb @@ -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 diff --git a/lib/draper/compatibility/global_id.rb b/lib/draper/compatibility/global_id.rb new file mode 100644 index 0000000..271be65 --- /dev/null +++ b/lib/draper/compatibility/global_id.rb @@ -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 diff --git a/lib/draper/configuration.rb b/lib/draper/configuration.rb new file mode 100644 index 0000000..e890d1b --- /dev/null +++ b/lib/draper/configuration.rb @@ -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 diff --git a/lib/draper/decoratable.rb b/lib/draper/decoratable.rb index 3771e57..5a3aee3 100644 --- a/lib/draper/decoratable.rb +++ b/lib/draper/decoratable.rb @@ -48,7 +48,6 @@ module Draper end module ClassMethods - # Decorates a collection of objects. Used at the end of a scope chain. # # @example @@ -56,8 +55,7 @@ module Draper # @param [Hash] options # see {Decorator.decorate_collection}. def decorate(options = {}) - collection = Rails::VERSION::MAJOR >= 4 ? all : scoped - decorator_class.decorate_collection(collection, options.reverse_merge(with: nil)) + decorator_class.decorate_collection(all, options.reverse_merge(with: nil)) end def decorator_class? @@ -70,16 +68,16 @@ module Draper # `Product` maps to `ProductDecorator`). # # @return [Class] the inferred decorator class. - def decorator_class + def decorator_class(called_on = self) prefix = respond_to?(:model_name) ? model_name : name decorator_name = "#{prefix}Decorator" - decorator_name.constantize - rescue NameError => error + decorator_name_constant = decorator_name.safe_constantize + return decorator_name_constant unless decorator_name_constant.nil? + if superclass.respond_to?(:decorator_class) - superclass.decorator_class + superclass.decorator_class(called_on) else - raise unless error.missing_name?(decorator_name) - raise Draper::UninferrableDecoratorError.new(self) + raise Draper::UninferrableDecoratorError.new(called_on) end end @@ -87,7 +85,7 @@ module Draper # # @return [Boolean] def ===(other) - super || (other.respond_to?(:object) && super(other.object)) + super || (other.is_a?(Draper::Decorator) && super(other.object)) end end diff --git a/lib/draper/decorated_association.rb b/lib/draper/decorated_association.rb index 7bdbb76..6bba855 100644 --- a/lib/draper/decorated_association.rb +++ b/lib/draper/decorated_association.rb @@ -1,7 +1,6 @@ module Draper # @private class DecoratedAssociation - def initialize(owner, association, options) options.assert_valid_keys(:with, :scope, :context) @@ -30,6 +29,5 @@ module Draper @decorated = factory.decorate(associated, context_args: owner.context) end - end end diff --git a/lib/draper/decorator.rb b/lib/draper/decorator.rb old mode 100755 new mode 100644 index 3a9d84f..1401595 --- a/lib/draper/decorator.rb +++ b/lib/draper/decorator.rb @@ -1,6 +1,9 @@ +require 'draper/compatibility/global_id' + module Draper class Decorator include Draper::ViewHelpers + include Draper::Compatibility::GlobalID if defined?(GlobalID) extend Draper::Delegation include ActiveModel::Serialization @@ -10,8 +13,6 @@ module Draper # @return the object being decorated. attr_reader :object alias_method :model, :object - alias_method :source, :object # TODO: deprecate this - alias_method :to_source, :object # TODO: deprecate this # @return [Hash] extra data to be used in user-defined methods. attr_accessor :context @@ -72,15 +73,10 @@ module Draper # Checks whether this decorator class has a corresponding {object_class}. def self.object_class? object_class - rescue Draper::UninferrableSourceError + rescue Draper::UninferrableObjectError false end - class << self # TODO deprecate this - alias_method :source_class, :object_class - alias_method :source_class?, :object_class? - end - # Automatically decorates ActiveRecord finder methods, so that you can use # `ProductDecorator.find(id)` instead of # `ProductDecorator.decorate(Product.find(id))`. @@ -182,7 +178,7 @@ module Draper # Returns a unique hash for a decorated object based on # the decorator class and the object being decorated. - # + # # @return [Fixnum] def hash self.class.hash ^ object.hash @@ -203,18 +199,6 @@ module Draper super || object.instance_of?(klass) end - if RUBY_VERSION < "2.0" - # nasty hack to stop 1.9.x using the delegated `to_s` in `inspect` - alias_method :_to_s, :to_s - - def inspect - ivars = instance_variables.map do |name| - "#{name}=#{instance_variable_get(name).inspect}" - end - _to_s.insert(-2, " #{ivars.join(", ")}") - end - end - delegate :to_s # In case object is nil @@ -241,10 +225,9 @@ module Draper # @return [Class] the class created by {decorate_collection}. def self.collection_decorator_class name = collection_decorator_name - name.constantize - rescue NameError => error - raise if name && !error.missing_name?(name) - Draper::CollectionDecorator + name_constant = name && name.safe_constantize + + name_constant || Draper::CollectionDecorator end private @@ -259,22 +242,23 @@ module Draper end def self.object_class_name - raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/ + return nil if name.nil? || name.demodulize !~ /.+Decorator$/ name.chomp("Decorator") end def self.inferred_object_class name = object_class_name - name.constantize - rescue NameError => error - raise if name && !error.missing_name?(name) - raise Draper::UninferrableSourceError.new(self) + name_constant = name && name.safe_constantize + return name_constant unless name_constant.nil? + + raise Draper::UninferrableObjectError.new(self) end def self.collection_decorator_name - plural = object_class_name.pluralize - raise NameError if plural == object_class_name - "#{plural}Decorator" + singular = object_class_name + plural = singular && singular.pluralize + + "#{plural}Decorator" unless plural == singular end def handle_multiple_decoration(options) diff --git a/lib/draper/finders.rb b/lib/draper/finders.rb old mode 100755 new mode 100644 index b0fe0cc..3cb4648 --- a/lib/draper/finders.rb +++ b/lib/draper/finders.rb @@ -3,7 +3,6 @@ module Draper # do not have to extend this module directly; it is extended by # {Decorator.decorates_finders}. module Finders - def find(id, options = {}) decorate(object_class.find(id), options) end diff --git a/lib/draper/helper_proxy.rb b/lib/draper/helper_proxy.rb index 73b147e..8645a13 100644 --- a/lib/draper/helper_proxy.rb +++ b/lib/draper/helper_proxy.rb @@ -2,11 +2,8 @@ module Draper # Provides access to helper methods - both Rails built-in helpers, and those # defined in your application. class HelperProxy - # @overload initialize(view_context) - def initialize(view_context = nil) - view_context ||= current_view_context # backwards compatibility - + def initialize(view_context) @view_context = view_context end @@ -35,10 +32,5 @@ module Draper view_context.send(name, *args, &block) end end - - def current_view_context - ActiveSupport::Deprecation.warn("wrong number of arguments (0 for 1) passed to Draper::HelperProxy.new", caller[1..-1]) - Draper::ViewContext.current.view_context - end end end diff --git a/lib/draper/lazy_helpers.rb b/lib/draper/lazy_helpers.rb index 2e8d9a0..c25761a 100644 --- a/lib/draper/lazy_helpers.rb +++ b/lib/draper/lazy_helpers.rb @@ -3,13 +3,11 @@ module Draper # so that you can stop typing `h.` everywhere, at the cost of mixing in a # bazillion methods. module LazyHelpers - # Sends missing methods to the {HelperProxy}. def method_missing(method, *args, &block) helpers.send(method, *args, &block) rescue NoMethodError super end - end end diff --git a/lib/draper/query_methods.rb b/lib/draper/query_methods.rb new file mode 100644 index 0000000..267ee8a --- /dev/null +++ b/lib/draper/query_methods.rb @@ -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 diff --git a/lib/draper/query_methods/load_strategy.rb b/lib/draper/query_methods/load_strategy.rb new file mode 100644 index 0000000..878468e --- /dev/null +++ b/lib/draper/query_methods/load_strategy.rb @@ -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 diff --git a/lib/draper/railtie.rb b/lib/draper/railtie.rb old mode 100755 new mode 100644 index e75bda3..556c15a --- a/lib/draper/railtie.rb +++ b/lib/draper/railtie.rb @@ -3,8 +3,6 @@ require 'rails/railtie' module ActiveModel class Railtie < Rails::Railtie generators do |app| - app ||= Rails.application # Rails 3.0.x does not yield `app` - Rails::Generators.configure! app.config.generators require_relative '../generators/controller_override' end @@ -13,7 +11,6 @@ end module Draper class Railtie < Rails::Railtie - config.after_initialize do |app| app.config.paths.add 'app/decorators', eager_load: true @@ -23,19 +20,19 @@ module Draper end end - initializer "draper.setup_action_controller" do |app| + initializer 'draper.setup_action_controller' do ActiveSupport.on_load :action_controller do Draper.setup_action_controller self end end - initializer "draper.setup_action_mailer" do |app| + initializer 'draper.setup_action_mailer' do ActiveSupport.on_load :action_mailer do Draper.setup_action_mailer self end end - initializer "draper.setup_orm" do |app| + initializer 'draper.setup_orm' do [:active_record, :mongoid].each do |orm| ActiveSupport.on_load orm do Draper.setup_orm self @@ -43,28 +40,22 @@ module Draper end end - initializer "draper.setup_active_model_serializers" do |app| - ActiveSupport.on_load :active_model_serializers do - if defined?(ActiveModel::ArraySerializerSupport) - Draper::CollectionDecorator.send :include, ActiveModel::ArraySerializerSupport - end - end - end - - initializer "draper.minitest-rails_integration" do |app| + initializer 'draper.minitest-rails_integration' do ActiveSupport.on_load :minitest do - require "draper/test/minitest_integration" + require 'draper/test/minitest_integration' end end - console do + def initialize_view_context require 'action_controller/test_case' - ApplicationController.new.view_context + Draper.default_controller.new.view_context Draper::ViewContext.build end - rake_tasks do - Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f } - end + console { initialize_view_context } + + runner { initialize_view_context } + + rake_tasks { Dir[File.join(File.dirname(__FILE__), 'tasks/*.rake')].each { |f| load f } } end end diff --git a/lib/draper/tasks/test.rake b/lib/draper/tasks/test.rake index 978607b..f7c5318 100644 --- a/lib/draper/tasks/test.rake +++ b/lib/draper/tasks/test.rake @@ -1,22 +1,9 @@ require 'rake/testtask' - -test_task = if Rails.version.to_f < 3.2 - require 'rails/test_unit/railtie' - Rake::TestTask -else - require 'rails/test_unit/sub_test_task' - Rails::SubTestTask -end +require 'rails/test_unit/railtie' namespace :test do - test_task.new(:decorators => "test:prepare") do |t| + Rake::TestTask.new(decorators: "test:prepare") do |t| t.libs << "test" t.pattern = "test/decorators/**/*_test.rb" end end - -if Rails.version.to_f < 4.2 && Rake::Task.task_defined?('test:run') - Rake::Task['test:run'].enhance do - Rake::Task['test:decorators'].invoke - end -end diff --git a/lib/draper/test/devise_helper.rb b/lib/draper/test/devise_helper.rb index 4ca7524..e6b1b38 100644 --- a/lib/draper/test/devise_helper.rb +++ b/lib/draper/test/devise_helper.rb @@ -1,14 +1,7 @@ module Draper module DeviseHelper def sign_in(resource_or_scope, resource = nil) - scope = begin - Devise::Mapping.find_scope!(resource_or_scope) - rescue RuntimeError => e - # Draper 1.0 didn't require the mapping to exist - ActiveSupport::Deprecation.warn("#{e.message}.\nUse `sign_in :user, mock_user` instead.", caller) - :user - end - + scope = Devise::Mapping.find_scope!(resource_or_scope) _stub_current_scope scope, resource || resource_or_scope end diff --git a/lib/draper/test/minitest_integration.rb b/lib/draper/test/minitest_integration.rb old mode 100755 new mode 100644 diff --git a/lib/draper/test/rspec_integration.rb b/lib/draper/test/rspec_integration.rb old mode 100755 new mode 100644 index d18f636..013d8ff --- a/lib/draper/test/rspec_integration.rb +++ b/lib/draper/test/rspec_integration.rb @@ -7,11 +7,7 @@ module Draper end RSpec.configure do |config| - if RSpec::Core::Version::STRING.starts_with?("3") - config.include DecoratorExampleGroup, file_path: %r{spec/decorators}, type: :decorator - else - config.include DecoratorExampleGroup, example_group: {file_path: %r{spec/decorators}}, type: :decorator - end + config.include DecoratorExampleGroup, file_path: %r{spec/decorators}, type: :decorator [:decorator, :controller, :mailer].each do |type| config.before(:each, type: type) { Draper::ViewContext.clear! } diff --git a/lib/draper/test_case.rb b/lib/draper/test_case.rb index 334a8f7..7d16356 100644 --- a/lib/draper/test_case.rb +++ b/lib/draper/test_case.rb @@ -3,9 +3,9 @@ module Draper class TestCase < ::ActiveSupport::TestCase module ViewContextTeardown - def teardown - super + def before_setup Draper::ViewContext.clear! + super end end @@ -29,14 +29,10 @@ module Draper end end -if defined?(ActionController::TestCase) - class ActionController::TestCase - include Draper::TestCase::ViewContextTeardown - end +if defined? ActionController::TestCase + ActionController::TestCase.include Draper::TestCase::ViewContextTeardown end -if defined?(ActionMailer::TestCase) - class ActionMailer::TestCase - include Draper::TestCase::ViewContextTeardown - end +if defined? ActionMailer::TestCase + ActionMailer::TestCase.include Draper::TestCase::ViewContextTeardown end diff --git a/lib/draper/undecorate.rb b/lib/draper/undecorate.rb index b787286..42ada29 100644 --- a/lib/draper/undecorate.rb +++ b/lib/draper/undecorate.rb @@ -6,4 +6,12 @@ module Draper object end end + + def self.undecorate_chain(object) + if object.respond_to?(:decorated?) && object.decorated? + undecorate_chain(object.object) + else + object + end + end end diff --git a/lib/draper/version.rb b/lib/draper/version.rb index b17a538..03b9f5f 100644 --- a/lib/draper/version.rb +++ b/lib/draper/version.rb @@ -1,3 +1,3 @@ module Draper - VERSION = "2.1.0" + VERSION = '3.1.0' end diff --git a/lib/draper/view_context.rb b/lib/draper/view_context.rb old mode 100755 new mode 100644 index 7f5007f..d0d05bb --- a/lib/draper/view_context.rb +++ b/lib/draper/view_context.rb @@ -20,8 +20,10 @@ module Draper RequestStore.store[:current_controller] end - # Sets the current controller. + # Sets the current controller. Clears view context when we are setting + # different controller. def self.controller=(controller) + clear! if RequestStore.store[:current_controller] != controller RequestStore.store[:current_controller] = controller end @@ -82,23 +84,5 @@ module Draper def self.build_strategy @build_strategy ||= Draper::ViewContext::BuildStrategy.new(:full) end - - # @deprecated Use {controller} instead. - def self.current_controller - ActiveSupport::Deprecation.warn("Draper::ViewContext.current_controller is deprecated (use controller instead)", caller) - self.controller || ApplicationController.new - end - - # @deprecated Use {controller=} instead. - def self.current_controller=(controller) - ActiveSupport::Deprecation.warn("Draper::ViewContext.current_controller= is deprecated (use controller instead)", caller) - self.controller = controller - end - - # @deprecated Use {build} instead. - def self.build_view_context - ActiveSupport::Deprecation.warn("Draper::ViewContext.build_view_context is deprecated (use build instead)", caller) - build - end end end diff --git a/lib/draper/view_context/build_strategy.rb b/lib/draper/view_context/build_strategy.rb index 3d4dc11..d1bdd3f 100644 --- a/lib/draper/view_context/build_strategy.rb +++ b/lib/draper/view_context/build_strategy.rb @@ -2,7 +2,6 @@ module Draper module ViewContext # @private module BuildStrategy - def self.new(name, &block) const_get(name.to_s.camelize).new(&block) end @@ -37,12 +36,20 @@ module Draper attr_reader :block def controller - (Draper::ViewContext.controller || ApplicationController.new).tap do |controller| - controller.request ||= ActionController::TestRequest.new if defined?(ActionController::TestRequest) + Draper::ViewContext.controller ||= Draper.default_controller.new + Draper::ViewContext.controller.tap do |controller| + controller.request ||= new_test_request controller if defined?(ActionController::TestRequest) end end - end + def new_test_request(controller) + is_above_rails_5_1 ? ActionController::TestRequest.create(controller) : ActionController::TestRequest.create + end + + def is_above_rails_5_1 + ActionController::TestRequest.method(:create).parameters.first == [:req, :controller_class] + end + end end end end diff --git a/lib/draper/view_helpers.rb b/lib/draper/view_helpers.rb index d6ee9b1..5d2b015 100644 --- a/lib/draper/view_helpers.rb +++ b/lib/draper/view_helpers.rb @@ -5,7 +5,6 @@ module Draper extend ActiveSupport::Concern module ClassMethods - # Access the helpers proxy to call built-in and user-defined # Rails helpers from a class context. # @@ -14,7 +13,6 @@ module Draper Draper::ViewContext.current end alias_method :h, :helpers - end # Access the helpers proxy to call built-in and user-defined @@ -32,6 +30,5 @@ module Draper helpers.localize(*args) end alias_method :l, :localize - end end diff --git a/lib/generators/controller_override.rb b/lib/generators/controller_override.rb index 3965fa3..5e90ded 100644 --- a/lib/generators/controller_override.rb +++ b/lib/generators/controller_override.rb @@ -5,13 +5,13 @@ require "rails/generators/rails/scaffold_controller/scaffold_controller_generato module Rails module Generators class ControllerGenerator - hook_for :decorator, default: true do |generator| + hook_for :decorator, type: :boolean, default: true do |generator| invoke generator, [name.singularize] end end class ScaffoldControllerGenerator - hook_for :decorator, default: true + hook_for :decorator, type: :boolean, default: true end end end diff --git a/lib/generators/draper/install_generator.rb b/lib/generators/draper/install_generator.rb new file mode 100644 index 0000000..cb56d51 --- /dev/null +++ b/lib/generators/draper/install_generator.rb @@ -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 diff --git a/lib/generators/draper/templates/application_decorator.rb b/lib/generators/draper/templates/application_decorator.rb new file mode 100644 index 0000000..5caf08b --- /dev/null +++ b/lib/generators/draper/templates/application_decorator.rb @@ -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 diff --git a/lib/generators/mini_test/decorator_generator.rb b/lib/generators/mini_test/decorator_generator.rb index cedea48..c69d219 100644 --- a/lib/generators/mini_test/decorator_generator.rb +++ b/lib/generators/mini_test/decorator_generator.rb @@ -4,10 +4,10 @@ module MiniTest module Generators class DecoratorGenerator < Base def self.source_root - File.expand_path('../templates', __FILE__) + File.expand_path("templates", __dir__) end - class_option :spec, :type => :boolean, :default => false, :desc => "Use MiniTest::Spec DSL" + class_option :spec, type: :boolean, default: false, desc: "Use MiniTest::Spec DSL" check_class_collision suffix: "DecoratorTest" diff --git a/lib/generators/rails/decorator_generator.rb b/lib/generators/rails/decorator_generator.rb index 6ccd2ad..42c70fd 100644 --- a/lib/generators/rails/decorator_generator.rb +++ b/lib/generators/rails/decorator_generator.rb @@ -1,7 +1,7 @@ module Rails module Generators - class DecoratorGenerator < NamedBase - source_root File.expand_path("../templates", __FILE__) + class DecoratorGenerator < NamedBase + source_root File.expand_path("templates", __dir__) check_class_collision suffix: "Decorator" class_option :parent, type: :string, desc: "The parent class for the generated decorator" @@ -24,13 +24,6 @@ module Rails end end end - - # Rails 3.0.X compatibility, stolen from https://github.com/jnunemaker/mongomapper/pull/385/files#L1R32 - unless methods.include?(:module_namespacing) - def module_namespacing - yield if block_given? - end - end end end end diff --git a/lib/generators/rspec/decorator_generator.rb b/lib/generators/rspec/decorator_generator.rb index 6b97c33..688c422 100644 --- a/lib/generators/rspec/decorator_generator.rb +++ b/lib/generators/rspec/decorator_generator.rb @@ -1,9 +1,11 @@ module Rspec - class DecoratorGenerator < ::Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + module Generators + class DecoratorGenerator < ::Rails::Generators::NamedBase + source_root File.expand_path("templates", __dir__) - def create_spec_file - template 'decorator_spec.rb', File.join('spec/decorators', class_path, "#{singular_name}_decorator_spec.rb") + def create_spec_file + template 'decorator_spec.rb', File.join('spec/decorators', class_path, "#{singular_name}_decorator_spec.rb") + end end end end diff --git a/lib/generators/test_unit/decorator_generator.rb b/lib/generators/test_unit/decorator_generator.rb index b0f3a28..bec2fbd 100644 --- a/lib/generators/test_unit/decorator_generator.rb +++ b/lib/generators/test_unit/decorator_generator.rb @@ -1,9 +1,12 @@ module TestUnit - class DecoratorGenerator < ::Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + module Generators + class DecoratorGenerator < ::Rails::Generators::NamedBase + source_root File.expand_path("templates", __dir__) + check_class_collision suffix: "DecoratorTest" - def create_test_file - template 'decorator_test.rb', File.join('test/decorators', class_path, "#{singular_name}_decorator_test.rb") + def create_test_file + template 'decorator_test.rb', File.join('test/decorators', class_path, "#{singular_name}_decorator_test.rb") + end end end end diff --git a/spec/draper/collection_decorator_spec.rb b/spec/draper/collection_decorator_spec.rb index 173ef26..1e36901 100644 --- a/spec/draper/collection_decorator_spec.rb +++ b/spec/draper/collection_decorator_spec.rb @@ -7,7 +7,6 @@ module Draper describe "#initialize" do describe "options validation" do - it "does not raise error on valid options" do valid_options = {with: Decorator, context: {}} expect{CollectionDecorator.new([], valid_options)}.not_to raise_error @@ -121,26 +120,11 @@ module Draper end describe "#find" do - context "with a block" do - it "decorates Enumerable#find" do - decorator = CollectionDecorator.new([]) + it "decorates Enumerable#find" do + decorator = CollectionDecorator.new([]) - expect(decorator.decorated_collection).to receive(:find).and_return(:delegated) - expect(decorator.find{|p| p.title == "title"}).to be :delegated - end - end - - context "without a block" do - it "decorates object.find" do - object = [] - found = double(decorate: :decorated) - decorator = CollectionDecorator.new(object) - - expect(object).to receive(:find).and_return(found) - ActiveSupport::Deprecation.silence do - expect(decorator.find(1)).to be :decorated - end - end + expect(decorator.decorated_collection).to receive(:find).and_return(:delegated) + expect(decorator.find{|p| p.title == "title"}).to be :delegated end end @@ -157,7 +141,7 @@ module Draper it "delegates array methods to the decorated collection" do decorator = CollectionDecorator.new([]) - expect(decorator.decorated_collection).to receive(:[]).with(42).and_return(:delegated) + allow(decorator.decorated_collection).to receive(:[]).with(42).and_return(:delegated) expect(decorator[42]).to be :delegated end @@ -302,6 +286,5 @@ module Draper expect(decorator.replace([:foo, :bar])).to be decorator end end - end end diff --git a/spec/draper/configuration_spec.rb b/spec/draper/configuration_spec.rb new file mode 100644 index 0000000..695f16d --- /dev/null +++ b/spec/draper/configuration_spec.rb @@ -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 diff --git a/spec/draper/decoratable_spec.rb b/spec/draper/decoratable_spec.rb index 63f9992..0436285 100644 --- a/spec/draper/decoratable_spec.rb +++ b/spec/draper/decoratable_spec.rb @@ -11,7 +11,7 @@ module Draper expect(decorator).to be_a ProductDecorator expect(decorator.object).to be product - end + end it "accepts context" do context = {some: "context"} @@ -22,7 +22,8 @@ module Draper it "uses the #decorator_class" do product = Product.new - allow(product).to receive(:decorator_class) { OtherDecorator } + allow(product).to receive_messages decorator_class: OtherDecorator + expect(product.decorate).to be_an_instance_of OtherDecorator end end @@ -72,6 +73,16 @@ module Draper expect(Product).to receive(:decorator_class).and_return(:some_decorator) expect(product.decorator_class).to be :some_decorator end + + it "specifies the class that #decorator_class was first called on (superclass)" do + person = Person.new + expect { person.decorator_class }.to raise_error(Draper::UninferrableDecoratorError, 'Could not infer a decorator for Person.') + end + + it "specifies the class that #decorator_class was first called on (subclass)" do + child = Child.new + expect { child.decorator_class }.to raise_error(Draper::UninferrableDecoratorError, 'Could not infer a decorator for Child.') + end end describe "#==" do @@ -108,37 +119,42 @@ module Draper end it "is true for a decorated instance" do - decorator = double(object: Product.new) + decorator = Product.new.decorate expect(Product === decorator).to be_truthy end it "is true for a decorated derived instance" do - decorator = double(object: Class.new(Product).new) + decorator = Class.new(Product).new.decorate expect(Product === decorator).to be_truthy end it "is false for a decorated unrelated instance" do - decorator = double(object: Model.new) + decorator = Other.new.decorate + + expect(Product === decorator).to be_falsey + end + + it "is false for a non-decorator which happens to respond to object" do + decorator = double(object: Product.new) expect(Product === decorator).to be_falsey end end describe ".decorate" do - let(:scoping_method) { Rails::VERSION::MAJOR >= 4 ? :all : :scoped } - it "calls #decorate_collection on .decorator_class" do scoped = [Product.new] - allow(Product).to receive(scoping_method).and_return(scoped) + allow(Product).to receive(:all).and_return(scoped) + expect(Product.decorator_class).to receive(:decorate_collection).with(scoped, with: nil).and_return(:decorated_collection) expect(Product.decorate).to be :decorated_collection end it "accepts options" do options = {with: ProductDecorator, context: {some: "context"}} - allow(Product).to receive(scoping_method).and_return([]) + allow(Product).to receive(:all).and_return([]) expect(Product.decorator_class).to receive(:decorate_collection).with([], options) Product.decorate(options) @@ -182,6 +198,15 @@ module Draper end end + context "when the decorator contains name error" do + it "throws an NameError" do + # We imitate ActiveSupport::Autoload behavior here in order to cause lazy NameError exception raising + allow_any_instance_of(Module).to receive(:const_missing) { Class.new { any_nonexisting_method_name } } + + expect{Model.decorator_class}.to raise_error { |error| expect(error).to be_an_instance_of(NameError) } + end + end + context "when the decorator can't be inferred" do it "throws an UninferrableDecoratorError" do expect{Model.decorator_class}.to raise_error UninferrableDecoratorError @@ -190,10 +215,22 @@ module Draper context "when an unrelated NameError is thrown" do it "re-raises that error" do - allow_any_instance_of(String).to receive(:constantize) { Draper::Base } + # Not related to safe_constantize behavior, we just want to raise a NameError inside the function + allow_any_instance_of(String).to receive(:safe_constantize) { Draper::Base } expect{Product.decorator_class}.to raise_error NameError, /Draper::Base/ end end + + context "when an anonymous class is given" do + it "infers the decorator from a superclass" do + anonymous_class = Class.new(Product) do + def self.name + to_s + end + end + expect(anonymous_class.decorator_class).to be ProductDecorator + end + end end end end diff --git a/spec/draper/decorated_association_spec.rb b/spec/draper/decorated_association_spec.rb index 4deeb8a..efe6c91 100644 --- a/spec/draper/decorated_association_spec.rb +++ b/spec/draper/decorated_association_spec.rb @@ -1,8 +1,7 @@ require 'spec_helper' module Draper - RSpec.describe DecoratedAssociation do - + Rspec.describe DecoratedAssociation do describe "#initialize" do it "accepts valid options" do valid_options = {with: Decorator, scope: :foo, context: {}} @@ -40,7 +39,7 @@ module Draper describe "#call" do it "calls the factory" do factory = double - allow(Factory).to receive(:new).and_return(factory) + allow(Factory).to receive_messages(new: factory) associated = double owner_context = {foo: "bar"} object = double(association: associated) @@ -54,7 +53,7 @@ module Draper it "memoizes" do factory = double - allow(Factory).to receive(:new).and_return(factory) + allow(Factory).to receive_messages(new: factory) owner = double(object: double(association: double), context: {}) decorated_association = DecoratedAssociation.new(owner, :association, {}) decorated = double @@ -67,7 +66,7 @@ module Draper context "when the :scope option was given" do it "applies the scope before decoration" do factory = double - allow(Factory).to receive(:new).and_return(factory) + allow(Factory).to receive_messages(new: factory) scoped = double object = double(association: double(applied_scope: scoped)) owner = double(object: object, context: {}) @@ -79,6 +78,5 @@ module Draper end end end - end end diff --git a/spec/draper/decorates_assigned_spec.rb b/spec/draper/decorates_assigned_spec.rb index ff2f7c5..c3b9c31 100644 --- a/spec/draper/decorates_assigned_spec.rb +++ b/spec/draper/decorates_assigned_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' module Draper - RSpec.describe DecoratesAssigned do + describe DecoratesAssigned do let(:controller_class) do Class.new do extend DecoratesAssigned @@ -28,14 +28,14 @@ module Draper end it "creates a factory" do - expect(Factory).to receive(:new).once + allow(Factory).to receive(:new).once controller_class.decorates_assigned :article, :author end it "passes options to the factory" do options = {foo: "bar"} - expect(Factory).to receive(:new).with(options) + allow(Factory).to receive(:new).with(options) controller_class.decorates_assigned :article, :author, options end @@ -43,7 +43,7 @@ module Draper it "decorates the instance variable" do object = double factory = double - allow(Factory).to receive(:new).and_return(factory) + allow(Factory).to receive_messages(new: factory) controller_class.decorates_assigned :article controller = controller_class.new @@ -55,7 +55,7 @@ module Draper it "memoizes" do factory = double - allow(Factory).to receive(:new).and_return(factory) + allow(Factory).to receive_messages(new: factory) controller_class.decorates_assigned :article controller = controller_class.new @@ -66,5 +66,6 @@ module Draper end end end + end end diff --git a/spec/draper/decorator_spec.rb b/spec/draper/decorator_spec.rb old mode 100755 new mode 100644 index 42cc5a9..d1eccf0 --- a/spec/draper/decorator_spec.rb +++ b/spec/draper/decorator_spec.rb @@ -145,13 +145,6 @@ module Draper ProductDecorator.decorate_collection([], options) end end - - context "when a NameError is thrown" do - it "re-raises that error" do - allow_any_instance_of(String).to receive(:constantize) { Draper::DecoratedEnumerableProxy } - expect{ProductDecorator.decorate_collection([])}.to raise_error NameError, /Draper::DecoratedEnumerableProxy/ - end - end end describe ".decorates" do @@ -181,42 +174,40 @@ module Draper protect_class Namespaced::ProductDecorator context "when not set by .decorates" do - it "raises an UninferrableSourceError for a so-named 'Decorator'" do - expect{Decorator.object_class}.to raise_error UninferrableSourceError + it "raises an UninferrableObjectError for a so-named 'Decorator'" do + expect{Decorator.object_class}.to raise_error UninferrableObjectError end - it "raises an UninferrableSourceError for anonymous decorators" do - expect{Class.new(Decorator).object_class}.to raise_error UninferrableSourceError + it "raises an UninferrableObjectError for anonymous decorators" do + expect{Class.new(Decorator).object_class}.to raise_error UninferrableObjectError end - it "raises an UninferrableSourceError for a decorator without a model" do - skip - expect{OtherDecorator.object_class}.to raise_error UninferrableSourceError + it "raises an UninferrableObjectError for a decorator without a model" do + SomeDecorator = Class.new(Draper::Decorator) + expect{SomeDecorator.object_class}.to raise_error UninferrableObjectError end - it "raises an UninferrableSourceError for other naming conventions" do - expect{ProductPresenter.object_class}.to raise_error UninferrableSourceError + it "raises an UninferrableObjectError for other naming conventions" do + ProductPresenter = Class.new(Draper::Decorator) + expect{ProductPresenter.object_class}.to raise_error UninferrableObjectError end - it "infers the source for 'Decorator'" do + it "infers the object class for 'Decorator'" do expect(ProductDecorator.object_class).to be Product end - it "infers namespaced sources" do + it "infers the object class for namespaced decorators" do expect(Namespaced::ProductDecorator.object_class).to be Namespaced::Product end context "when an unrelated NameError is thrown" do it "re-raises that error" do - allow_any_instance_of(String).to receive(:constantize) { SomethingThatDoesntExist } + # Not related to safe_constantize behavior, we just want to raise a NameError inside the function + allow_any_instance_of(String).to receive(:safe_constantize) { SomethingThatDoesntExist } expect{ProductDecorator.object_class}.to raise_error NameError, /SomethingThatDoesntExist/ end end end - - it "is aliased to .source_class" do - expect(ProductDecorator.source_class).to be Product - end end describe ".object_class?" do @@ -227,13 +218,24 @@ module Draper end it "returns false when .object_class is not inferrable" do - allow(Decorator).to receive(:object_class).and_raise(UninferrableSourceError.new(Decorator)) + allow(Decorator).to receive(:object_class).and_raise(UninferrableObjectError.new(Decorator)) + expect(Decorator.object_class?).to be_falsey end + end - it "is aliased to .source_class?" do - allow(Decorator).to receive(:object_class).and_return(Model) - expect(Decorator.source_class?).to be_truthy + describe '.collection_decorator_class' do + it 'defaults to CollectionDecorator' do + allow_any_instance_of(String).to receive(:safe_constantize) { nil } + expect(ProductDecorator.collection_decorator_class).to be Draper::CollectionDecorator + end + + it 'infers collection decorator based on name' do + expect(ProductDecorator.collection_decorator_class).to be ProductsDecorator + end + + it 'infers collection decorator base on name for namespeced model' do + expect(Namespaced::ProductDecorator.collection_decorator_class).to be Namespaced::ProductsDecorator end end @@ -335,7 +337,6 @@ module Draper expect(decorator.object).to be object expect(decorator.model).to be object - expect(decorator.to_source).to be object end it "is aliased to #model" do @@ -344,20 +345,6 @@ module Draper expect(decorator.model).to be object end - - it "is aliased to #source" do - object = Model.new - decorator = Decorator.new(object) - - expect(decorator.source).to be object - end - - it "is aliased to #to_source" do - object = Model.new - decorator = Decorator.new(object) - - expect(decorator.to_source).to be object - end end describe "aliasing object to object class name" do @@ -479,13 +466,15 @@ module Draper it "returns only the object's attributes that are implemented by the decorator" do decorator = Decorator.new(double(attributes: {foo: "bar", baz: "qux"})) allow(decorator).to receive(:foo) + expect(decorator.attributes).to eq({foo: "bar"}) end end describe ".model_name" do - it "delegates to the source class" do - allow(Decorator).to receive(:object_class) { double(model_name: :delegated) } + it "delegates to the object class" do + allow(Decorator).to receive(:object_class).and_return(double(model_name: :delegated)) + expect(Decorator.model_name).to be :delegated end end @@ -592,12 +581,51 @@ module Draper expect(decorator.hello_world).to be :delegated end - it "adds delegated methods to the decorator when they are used" do - decorator = Decorator.new(double(hello_world: :delegated)) + it 'delegates `super` to parent class first' do + parent_decorator_class = Class.new(Decorator) do + def hello_world + "parent#hello_world" + end + end - expect(decorator.methods).not_to include :hello_world - decorator.hello_world - expect(decorator.methods).to include :hello_world + child_decorator_class = Class.new(parent_decorator_class) do + def hello_world + super + end + end + + decorator = child_decorator_class.new(double(hello_world: 'object#hello_world')) + expect(decorator.hello_world).to eq 'parent#hello_world' + end + + it 'delegates `super` to object if method does not exist on parent class' do + decorator_class = Class.new(Decorator) do + def hello_world + super + end + end + + decorator = decorator_class.new(double(hello_world: 'object#hello_world')) + expect(decorator.hello_world).to eq 'object#hello_world' + end + + it 'raises `NoMethodError` when `super` is called on for method that does not exist' do + decorator_class = Class.new(Decorator) do + def hello_world + super + end + end + + decorator = decorator_class.new(double) + expect{decorator.hello_world}.to raise_error NoMethodError + end + + it "allows decorator to decorate different classes of objects" do + decorator_1 = Decorator.new(double) + decorator_2 = Decorator.new(double(hello_world: :delegated)) + + decorator_2.hello_world + expect(decorator_1.methods).not_to include :hello_world end it "passes blocks to delegated methods" do @@ -616,7 +644,7 @@ module Draper it "delegates already-delegated methods" do object = Class.new{ delegate :bar, to: :foo }.new - allow(object).to receive(:foo) { double(bar: :delegated) } + allow(object).to receive_messages foo: double(bar: :delegated) decorator = Decorator.new(object) expect(decorator.bar).to be :delegated @@ -636,26 +664,47 @@ module Draper expect{decorator.hello_world}.to raise_error NoMethodError expect(decorator.methods).not_to include :hello_world end + + context 'when decorator overrides a public method defined on the object with a private' do + let(:decorator_class) do + Class.new(Decorator) do + private + + def hello_world + 'hello world' + end + end + end + + let(:object) { Class.new { def hello_world; end }.new } + + it 'does not delegate the public method defined on the object' do + decorator = decorator_class.new(object) + + expect{ decorator.hello_world }.to raise_error NoMethodError + end + end end context ".method_missing" do - context "without a source class" do + context "without an object class" do it "raises a NoMethodError on missing methods" do expect{Decorator.hello_world}.to raise_error NoMethodError end end - context "with a source class" do - it "delegates methods that exist on the source class" do + context "with an object class" do + it "delegates methods that exist on the object class" do object_class = Class.new - allow(object_class).to receive(:hello_world).and_return(:delegated) - allow(Decorator).to receive(:object_class).and_return(object_class) + allow(object_class).to receive_messages hello_world: :delegated + allow(Decorator).to receive_messages object_class: object_class expect(Decorator.hello_world).to be :delegated end - it "does not delegate methods that do not exist on the source class" do - allow(Decorator).to receive(:object_class) { Class.new } + it "does not delegate methods that do not exist on the object class" do + allow(Decorator).to receive_messages object_class: Class.new + expect{Decorator.hello_world}.to raise_error NoMethodError end end @@ -693,7 +742,7 @@ module Draper end describe ".respond_to?" do - context "without a source class" do + context "without a object class" do it "returns true for its own class methods" do Decorator.class_eval{def self.hello_world; end} @@ -705,16 +754,16 @@ module Draper end end - context "with a source class" do + context "with a object class" do it "returns true for its own class methods" do Decorator.class_eval{def self.hello_world; end} - allow(Decorator).to receive(:object_class) { Class.new } + allow(Decorator).to receive_messages object_class: Class.new expect(Decorator).to respond_to :hello_world end - it "returns true for the source's class methods" do - allow(Decorator).to receive(:object_class) { double(hello_world: :delegated) } + it "returns true for the object's class methods" do + allow(Decorator).to receive_messages object_class: double(hello_world: :delegated) expect(Decorator).to respond_to :hello_world end @@ -732,7 +781,7 @@ module Draper describe ".respond_to_missing?" do it "allows .method to be called on delegated class methods" do - allow(Decorator).to receive(:object_class) { double(hello_world: :delegated) } + allow(Decorator).to receive_messages object_class: double(hello_world: :delegated) expect(Decorator.method(:hello_world)).not_to be_nil end @@ -740,7 +789,7 @@ module Draper end describe "class spoofing" do - it "pretends to be a kind of the source class" do + it "pretends to be a kind of the object class" do decorator = Decorator.new(Model.new) expect(decorator.kind_of?(Model)).to be_truthy @@ -754,7 +803,7 @@ module Draper expect(decorator.is_a?(Decorator)).to be_truthy end - it "pretends to be an instance of the source class" do + it "pretends to be an instance of the object class" do decorator = Decorator.new(Model.new) expect(decorator.instance_of?(Model)).to be_truthy diff --git a/spec/draper/draper_spec.rb b/spec/draper/draper_spec.rb new file mode 100644 index 0000000..9f29515 --- /dev/null +++ b/spec/draper/draper_spec.rb @@ -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 diff --git a/spec/draper/factory_spec.rb b/spec/draper/factory_spec.rb index 6831a54..0085aa7 100644 --- a/spec/draper/factory_spec.rb +++ b/spec/draper/factory_spec.rb @@ -1,8 +1,7 @@ require 'spec_helper' module Draper - RSpec.describe Factory do - + Rspec.describe Factory do describe "#initialize" do it "accepts valid options" do valid_options = {with: Decorator, context: {foo: "bar"}} @@ -64,7 +63,7 @@ module Draper allow(Factory::Worker).to receive(:new).and_return(worker) options = {foo: "bar"} - expect(worker).to receive(:call).with(options) + allow(worker).to receive(:call).with(options) factory.decorate(double, options) end @@ -72,27 +71,25 @@ module Draper it "sets the passed context" do factory = Factory.new(context: {foo: "bar"}) worker = ->(*){} - allow(Factory::Worker).to receive(:new).and_return(worker) + allow(Factory::Worker).to receive_messages new: worker - expect(worker).to receive(:call).with(baz: 'qux', context: { foo: 'bar' }) + expect(worker).to receive(:call).with(baz: "qux", context: {foo: "bar"}) factory.decorate(double, {baz: "qux"}) end it "is overridden by explicitly-specified context" do factory = Factory.new(context: {foo: "bar"}) worker = ->(*){} - allow(Factory::Worker).to receive(:new) { worker } + allow(Factory::Worker).to receive_messages new: worker expect(worker).to receive(:call).with(context: {baz: "qux"}) factory.decorate(double, context: {baz: "qux"}) end end end - end - RSpec.describe Factory::Worker do - + Rspec.describe Factory::Worker do describe "#call" do it "calls the decorator method" do object = double @@ -101,7 +98,7 @@ module Draper decorator = ->(*){} allow(worker).to receive(:decorator){ decorator } - expect(decorator).to receive(:call).with(object, options).and_return(:decorated) + allow(decorator).to receive(:call).with(object, options).and_return(:decorated) expect(worker.call(options)).to be :decorated end @@ -109,7 +106,7 @@ module Draper it "calls it" do worker = Factory::Worker.new(double, double) decorator = ->(*){} - allow(worker).to receive(:decorator) { decorator } + allow(worker).to receive_messages decorator: decorator context = {foo: "bar"} expect(decorator).to receive(:call).with(anything(), context: context) @@ -118,7 +115,7 @@ module Draper it "receives arguments from the :context_args option" do worker = Factory::Worker.new(double, double) - allow(worker).to receive(:decorator) { ->(*){} } + allow(worker).to receive_messages decorator: ->(*){} context = ->{} expect(context).to receive(:call).with(:foo, :bar) @@ -127,7 +124,7 @@ module Draper it "wraps non-arrays passed to :context_args" do worker = Factory::Worker.new(double, double) - allow(worker).to receive(:decorator) { ->(*){} } + allow(worker).to receive_messages decorator: ->(*){} context = ->{} hash = {foo: "bar"} @@ -140,7 +137,7 @@ module Draper it "doesn't call it" do worker = Factory::Worker.new(double, double) decorator = ->(*){} - allow(worker).to receive(:decorator) { decorator } + allow(worker).to receive_messages decorator: decorator context = {foo: "bar"} expect(decorator).to receive(:call).with(anything(), context: context) @@ -151,7 +148,7 @@ module Draper it "does not pass the :context_args option to the decorator" do worker = Factory::Worker.new(double, double) decorator = ->(*){} - allow(worker).to receive(:decorator) { decorator } + allow(worker).to receive_messages decorator: decorator expect(decorator).to receive(:call).with(anything(), foo: "bar") worker.call(foo: "bar", context_args: []) diff --git a/spec/draper/finders_spec.rb b/spec/draper/finders_spec.rb index a483b99..8266c9f 100644 --- a/spec/draper/finders_spec.rb +++ b/spec/draper/finders_spec.rb @@ -99,7 +99,7 @@ module Draper describe ".all" do it "returns a decorated collection" do found = [Product.new, Product.new] - allow(Product).to receive(:all).and_return(found) + allow(Product).to receive_messages all: found decorator = ProductDecorator.all expect(decorator).to be_a Draper::CollectionDecorator diff --git a/spec/draper/query_methods/load_strategy_spec.rb b/spec/draper/query_methods/load_strategy_spec.rb new file mode 100644 index 0000000..816a14f --- /dev/null +++ b/spec/draper/query_methods/load_strategy_spec.rb @@ -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 diff --git a/spec/draper/query_methods_spec.rb b/spec/draper/query_methods_spec.rb new file mode 100644 index 0000000..3884d34 --- /dev/null +++ b/spec/draper/query_methods_spec.rb @@ -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 diff --git a/spec/draper/undecorate_chain_spec.rb b/spec/draper/undecorate_chain_spec.rb new file mode 100644 index 0000000..00e4c9a --- /dev/null +++ b/spec/draper/undecorate_chain_spec.rb @@ -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 diff --git a/spec/draper/view_context/build_strategy_spec.rb b/spec/draper/view_context/build_strategy_spec.rb index cc7520d..10743ab 100644 --- a/spec/draper/view_context/build_strategy_spec.rb +++ b/spec/draper/view_context/build_strategy_spec.rb @@ -14,7 +14,7 @@ module Draper context "when a current controller is set" do it "returns the controller's view context" do view_context = fake_view_context - allow(ViewContext).to receive(:controller) { fake_controller(view_context) } + allow(ViewContext).to receive_messages controller: fake_controller(view_context) strategy = ViewContext::BuildStrategy::Full.new expect(strategy.call).to be view_context @@ -23,31 +23,47 @@ module Draper context "when a current controller is not set" do it "uses ApplicationController" do - view_context = fake_view_context - stub_const "ApplicationController", double(new: fake_controller(view_context)) - strategy = ViewContext::BuildStrategy::Full.new - - expect(strategy.call).to be view_context + expect(Draper::ViewContext.controller).to be_nil + view_context = ViewContext::BuildStrategy::Full.new.call + expect(view_context.controller).to eq Draper::ViewContext.controller + expect(view_context.controller).to be_an ApplicationController end end it "adds a request if one is not defined" do controller = Class.new(ActionController::Base).new - allow(ViewContext).to receive(:controller) { controller } + allow(ViewContext).to receive_messages controller: controller strategy = ViewContext::BuildStrategy::Full.new expect(controller.request).to be_nil strategy.call expect(controller.request).to be_an ActionController::TestRequest - expect(controller.params).to eq({}) + expect(controller.params).to be_empty # sanity checks expect(controller.view_context.request).to be controller.request expect(controller.view_context.params).to be controller.params end + it "compatible with rails 5.1 change on ActionController::TestRequest.create method" do + ActionController::TestRequest.class_eval do + if ActionController::TestRequest.method(:create).parameters.first == [] + def create controller_class + create + end + end + end + controller = Class.new(ActionController::Base).new + allow(ViewContext).to receive_messages controller: controller + strategy = ViewContext::BuildStrategy::Full.new + + expect(controller.request).to be_nil + strategy.call + expect(controller.request).to be_an ActionController::TestRequest + end + it "adds methods to the view context from the constructor block" do - allow(ViewContext).to receive(:controller) { fake_controller } + allow(ViewContext).to receive(:controller).and_return(fake_controller) strategy = ViewContext::BuildStrategy::Full.new do def a_helper_method; end end @@ -57,7 +73,7 @@ module Draper it "includes modules into the view context from the constructor block" do view_context = Object.new - allow(ViewContext).to receive(:controller) { fake_controller(view_context) } + allow(ViewContext).to receive(:controller).and_return(fake_controller(view_context)) helpers = Module.new do def a_helper_method; end end diff --git a/spec/draper/view_context_spec.rb b/spec/draper/view_context_spec.rb index 6f6f2ee..b191d0e 100644 --- a/spec/draper/view_context_spec.rb +++ b/spec/draper/view_context_spec.rb @@ -18,7 +18,7 @@ module Draper describe ".controller" do it "returns the stored controller from RequestStore" do - allow(RequestStore).to receive(:store) { { current_controller: :stored_controller } } + allow(RequestStore).to receive_messages store: {current_controller: :stored_controller} expect(ViewContext.controller).to be :stored_controller end @@ -27,24 +27,52 @@ module Draper describe ".controller=" do it "stores a controller in RequestStore" do store = {} - allow(RequestStore).to receive(:store).and_return(store) + allow(RequestStore).to receive_messages store: store ViewContext.controller = :stored_controller expect(store[:current_controller]).to be :stored_controller end + + it "cleans context when controller changes" do + store = { + current_controller: :stored_controller, + current_view_context: :stored_view_context + } + + allow(RequestStore).to receive_messages store: store + + ViewContext.controller = :other_stored_controller + + expect(store).to include(current_controller: :other_stored_controller) + expect(store).not_to include(:current_view_context) + end + + it "doesn't clean context when controller is the same" do + store = { + current_controller: :stored_controller, + current_view_context: :stored_view_context + } + + allow(RequestStore).to receive_messages store: store + + ViewContext.controller = :stored_controller + + expect(store).to include(current_controller: :stored_controller) + expect(store).to include(current_view_context: :stored_view_context) + end end describe ".current" do it "returns the stored view context from RequestStore" do - allow(RequestStore).to receive(:store) { { current_view_context: :stored_view_context } } + allow(RequestStore).to receive_messages store: {current_view_context: :stored_view_context} expect(ViewContext.current).to be :stored_view_context end context "when no view context is stored" do it "builds a view context" do - allow(RequestStore).to receive(:store).and_return({}) - allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context }) + allow(RequestStore).to receive_messages store: {} + allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy) expect(ViewContext.current).to be :new_helper_proxy @@ -52,8 +80,8 @@ module Draper it "stores the built view context" do store = {} - allow(RequestStore).to receive(:store).and_return(store) - allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context }) + allow(RequestStore).to receive_messages store: store + allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy) ViewContext.current @@ -65,7 +93,7 @@ module Draper describe ".current=" do it "stores a helper proxy for the view context in RequestStore" do store = {} - allow(RequestStore).to receive(:store).and_return(store) + allow(RequestStore).to receive_messages store: store allow(HelperProxy).to receive(:new).with(:stored_view_context).and_return(:stored_helper_proxy) ViewContext.current = :stored_view_context @@ -76,7 +104,7 @@ module Draper describe ".clear!" do it "clears the stored controller and view controller" do store = {current_controller: :stored_controller, current_view_context: :stored_view_context} - allow(RequestStore).to receive(:store).and_return(store) + allow(RequestStore).to receive_messages store: store ViewContext.clear! expect(store).not_to have_key :current_controller @@ -86,7 +114,7 @@ module Draper describe ".build" do it "returns a new view context using the build strategy" do - allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context }) + allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } expect(ViewContext.build).to be :new_view_context end @@ -94,7 +122,7 @@ module Draper describe ".build!" do it "returns a helper proxy for the new view context" do - allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context }) + allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy) expect(ViewContext.build!).to be :new_helper_proxy @@ -102,8 +130,8 @@ module Draper it "stores the helper proxy" do store = {} - allow(RequestStore).to receive(:store) { store } - allow(ViewContext).to receive(:build_strategy).and_return( ->{ :new_view_context }) + allow(RequestStore).to receive_messages store: store + allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy) ViewContext.build! diff --git a/spec/dummy/app/controllers/application_controller.rb b/spec/dummy/app/controllers/application_controller.rb deleted file mode 100644 index bbe1d38..0000000 --- a/spec/dummy/app/controllers/application_controller.rb +++ /dev/null @@ -1,4 +0,0 @@ -class ApplicationController < ActionController::Base - include LocalizedUrls - protect_from_forgery -end diff --git a/spec/dummy/app/controllers/base_controller.rb b/spec/dummy/app/controllers/base_controller.rb new file mode 100644 index 0000000..d9e9218 --- /dev/null +++ b/spec/dummy/app/controllers/base_controller.rb @@ -0,0 +1,4 @@ +class BaseController < ActionController::Base + include LocalizedUrls + protect_from_forgery +end diff --git a/spec/dummy/app/controllers/posts_controller.rb b/spec/dummy/app/controllers/posts_controller.rb index 1993005..af35c93 100644 --- a/spec/dummy/app/controllers/posts_controller.rb +++ b/spec/dummy/app/controllers/posts_controller.rb @@ -1,4 +1,4 @@ -class PostsController < ApplicationController +class PostsController < BaseController decorates_assigned :post def show @@ -8,7 +8,7 @@ class PostsController < ApplicationController def mail post = Post.find(params[:id]) email = PostMailer.decorated_email(post).deliver - render text: email.body + render html: email.body.to_s.html_safe end private diff --git a/spec/dummy/app/decorators/post_decorator.rb b/spec/dummy/app/decorators/post_decorator.rb old mode 100755 new mode 100644 diff --git a/spec/dummy/app/jobs/publish_post_job.rb b/spec/dummy/app/jobs/publish_post_job.rb new file mode 100644 index 0000000..6a71256 --- /dev/null +++ b/spec/dummy/app/jobs/publish_post_job.rb @@ -0,0 +1,7 @@ +class PublishPostJob < ActiveJob::Base + queue_as :default + + def perform(post) + post.save! + end +end diff --git a/spec/dummy/app/models/application_record.rb b/spec/dummy/app/models/application_record.rb new file mode 100644 index 0000000..10a4cba --- /dev/null +++ b/spec/dummy/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/spec/dummy/app/models/post.rb b/spec/dummy/app/models/post.rb index 6cc74b1..59b1f95 100644 --- a/spec/dummy/app/models/post.rb +++ b/spec/dummy/app/models/post.rb @@ -1,3 +1,3 @@ -class Post < ActiveRecord::Base +class Post < ApplicationRecord # attr_accessible :title, :body end diff --git a/spec/dummy/app/views/posts/_post.html.erb b/spec/dummy/app/views/posts/_post.html.erb index 22350a4..4225361 100644 --- a/spec/dummy/app/views/posts/_post.html.erb +++ b/spec/dummy/app/views/posts/_post.html.erb @@ -20,14 +20,16 @@
Helpers from the controller:
<%= post.goodnight_moon %>
-
Path with decorator:
-
<%= post_path(post) %>
+ <% unless defined? mailer %> +
Path with decorator:
+
<%= post_url(post) %>
-
Path with model:
-
<%= post.path_with_model %>
+
Path with model:
+
<%= post.path_with_model %>
-
Path with id:
-
<%= post.path_with_id %>
+
Path with id:
+
<%= post.path_with_id %>
+ <% end %>
URL with decorator:
<%= post_url(post) %>
diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index d7452d9..f0e1fea 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -38,9 +38,6 @@ module Dummy # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" - # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += [:password] - # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true diff --git a/spec/dummy/config/boot.rb b/spec/dummy/config/boot.rb index 4b06b02..dd8365e 100644 --- a/spec/dummy/config/boot.rb +++ b/spec/dummy/config/boot.rb @@ -2,4 +2,4 @@ require 'rubygems' ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/spec/dummy/config/environments/test.rb b/spec/dummy/config/environments/test.rb index 58a8b6b..f2f925c 100644 --- a/spec/dummy/config/environments/test.rb +++ b/spec/dummy/config/environments/test.rb @@ -28,4 +28,6 @@ Dummy::Application.configure do config.active_support.deprecation = :stderr config.eager_load = false + + config.active_job.queue_adapter = :test end diff --git a/spec/dummy/config/initializers/draper.rb b/spec/dummy/config/initializers/draper.rb new file mode 100644 index 0000000..414d9d6 --- /dev/null +++ b/spec/dummy/config/initializers/draper.rb @@ -0,0 +1,3 @@ +Draper.configure do |config| + config.default_controller = BaseController +end diff --git a/spec/dummy/config/initializers/filter_parameter_logging.rb b/spec/dummy/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..4a994e1 --- /dev/null +++ b/spec/dummy/config/initializers/filter_parameter_logging.rb @@ -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] diff --git a/spec/dummy/config/mongoid.yml b/spec/dummy/config/mongoid.yml index 92d1932..ecbfb6d 100644 --- a/spec/dummy/config/mongoid.yml +++ b/spec/dummy/config/mongoid.yml @@ -1,54 +1,117 @@ development: - # Configure available database sessions. (required) - sessions: - # Defines the default session. (required) + # Configure available database clients. (required) + clients: + # Defines the default client. (required) default: # Defines the name of the default database that Mongoid can connect to. # (required). database: dummy_development - # Provides the hosts the default session can connect to. Must be an array + # Provides the hosts the default client can connect to. Must be an array # of host:port pairs. (required) hosts: - localhost:27017 options: - # Change whether the session persists in safe mode by default. - # (default: false) - # safe: false + # Change the default write concern. (default = { w: 1 }) + # write: + # w: 1 - # Change the default consistency model to :eventual or :strong. - # :eventual will send reads to secondaries, :strong sends everything - # to master. (default: :eventual) - # consistency: :eventual + # Change the default read preference. Valid options for mode are: :secondary, + # :secondary_preferred, :primary, :primary_preferred, :nearest + # (default: primary) + # read: + # mode: :secondary_preferred + # tag_sets: + # - use: web + + # The name of the user for authentication. + # user: 'user' + + # The password of the user for authentication. + # password: 'password' + + # The user's database roles. + # roles: + # - 'dbOwner' + + # Change the default authentication mechanism. Valid options are: :scram, + # :mongodb_cr, :mongodb_x509, and :plain. (default on 3.0 is :scram, default + # on 2.4 and 2.6 is :plain) + # auth_mech: :scram + + # The database or source to authenticate the user against. (default: admin) + # auth_source: admin + + # Force a the driver cluster to behave in a certain manner instead of auto- + # discovering. Can be one of: :direct, :replica_set, :sharded. Set to :direct + # when connecting to hidden members of a replica set. + # connect: :direct + + # Changes the default time in seconds the server monitors refresh their status + # via ismaster commands. (default: 10) + # heartbeat_frequency: 10 + + # The time in seconds for selecting servers for a near read preference. (default: 5) + # local_threshold: 5 + + # The timeout in seconds for selecting a server for an operation. (default: 30) + # server_selection_timeout: 30 + + # The maximum number of connections in the connection pool. (default: 5) + # max_pool_size: 5 + + # The minimum number of connections in the connection pool. (default: 1) + # min_pool_size: 1 + + # The time to wait, in seconds, in the connection pool for a connection + # to be checked in before timing out. (default: 5) + # wait_queue_timeout: 5 + + # The time to wait to establish a connection before timing out, in seconds. + # (default: 5) + # connect_timeout: 5 + + # The timeout to wait to execute operations on a socket before raising an error. + # (default: 5) + # socket_timeout: 5 + + # The name of the replica set to connect to. Servers provided as seeds that do + # not belong to this replica set will be ignored. + # replica_set: name + + # Whether to connect to the servers via ssl. (default: false) + # ssl: true + + # The certificate file used to identify the connection against MongoDB. + # ssl_cert: /path/to/my.cert + + # The private keyfile used to identify the connection against MongoDB. + # Note that even if the key is stored in the same file as the certificate, + # both need to be explicitly specified. + # ssl_key: /path/to/my.key + + # A passphrase for the private key. + # ssl_key_pass_phrase: password + + # Whether or not to do peer certification validation. (default: true) + # ssl_verify: true + + # The file containing a set of concatenated certification authority certifications + # used to validate certs passed from the other end of the connection. + # ssl_ca_cert: /path/to/ca.cert - # How many times Moped should attempt to retry an operation after - # failure. (default: 30) - # max_retries: 30 - # The time in seconds that Moped should wait before retrying an - # operation on failure. (default: 1) - # retry_interval: 1 # Configure Mongoid specific options. (optional) options: - # Configuration for whether or not to allow access to fields that do - # not have a field definition on the model. (default: true) - # allow_dynamic_fields: true - - # Enable the identity map, needed for eager loading. (default: false) - # identity_map_enabled: false - # Includes the root model name in json serialization. (default: false) # include_root_in_json: false - # Include the _type field in serializaion. (default: false) + # Include the _type field in serialization. (default: false) # include_type_for_serialization: false # Preload all models in development, needed when models use # inheritance. (default: false) # preload_models: false - # Protect id and type from mass assignment. (default: true) - # protect_sensitive_fields: true - # Raise an error when performing a #find and the document is not found. # (default: true) # raise_not_found_error: true @@ -57,23 +120,23 @@ development: # existing method. (default: false) # scope_overwrite_exception: false - # Skip the database version check, used when connecting to a db without - # admin access. (default: false) - # skip_version_check: false - - # User Active Support's time zone in conversions. (default: true) + # Use Active Support's time zone in conversions. (default: true) # use_activesupport_time_zone: true # Ensure all times are UTC in the app side. (default: false) # use_utc: false + + # Set the Mongoid and Ruby driver log levels when not in a Rails + # environment. The Mongoid logger will be set to the Rails logger + # otherwise.(default: :info) + # log_level: :info test: - sessions: + clients: default: database: dummy_test hosts: - localhost:27017 options: - # In the test environment we lower the retries and retry interval to - # low amounts for fast failures. - max_retries: 1 - retry_interval: 0 + read: + mode: :primary + max_pool_size: 1 diff --git a/spec/dummy/db/migrate/20121019115657_create_posts.rb b/spec/dummy/db/migrate/20121019115657_create_posts.rb index 30d7c04..1ff0db9 100644 --- a/spec/dummy/db/migrate/20121019115657_create_posts.rb +++ b/spec/dummy/db/migrate/20121019115657_create_posts.rb @@ -1,4 +1,4 @@ -class CreatePosts < ActiveRecord::Migration +class CreatePosts < ActiveRecord::Migration[4.2] def change create_table :posts do |t| diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index fe5946e..9aebf2c 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -11,11 +11,11 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20121019115657) do +ActiveRecord::Schema.define(version: 20121019115657) do - create_table "posts", :force => true do |t| - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + create_table "posts", force: true do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end end diff --git a/spec/dummy/lib/tasks/test.rake b/spec/dummy/lib/tasks/test.rake index 56052a8..ac09958 100644 --- a/spec/dummy/lib/tasks/test.rake +++ b/spec/dummy/lib/tasks/test.rake @@ -13,4 +13,4 @@ RSpec::Core::RakeTask.new :fast_spec do |t| t.pattern = "fast_spec/**/*_spec.rb" end -task :default => [:test, :spec, :fast_spec] +task default: [:test, :spec, :fast_spec] diff --git a/spec/dummy/spec/decorators/active_model_serializers_spec.rb b/spec/dummy/spec/decorators/active_model_serializers_spec.rb index e4d00a8..c667f05 100644 --- a/spec/dummy/spec/decorators/active_model_serializers_spec.rb +++ b/spec/dummy/spec/decorators/active_model_serializers_spec.rb @@ -2,15 +2,11 @@ require_relative '../rails_helper' RSpec.describe Draper::CollectionDecorator do describe "#active_model_serializer" do - it "returns ActiveModel::ArraySerializer" do - collection_decorator = Draper::CollectionDecorator.new([]) - if defined?(ActiveModel::ArraySerializerSupport) - collection_serializer = collection_decorator.active_model_serializer - else - collection_serializer = ActiveModel::Serializer.serializer_for(collection_decorator) - end + it "returns ActiveModel::Serializer::CollectionSerializer" do + collection_decorator = Draper::CollectionDecorator.new([]) + collection_serializer = ActiveModel::Serializer.serializer_for(collection_decorator) - expect(collection_serializer).to be ActiveModel::ArraySerializer + expect(collection_serializer).to be ActiveModel::Serializer::CollectionSerializer end end end diff --git a/spec/dummy/spec/decorators/devise_spec.rb b/spec/dummy/spec/decorators/devise_spec.rb index 540a04d..d2399c1 100644 --- a/spec/dummy/spec/decorators/devise_spec.rb +++ b/spec/dummy/spec/decorators/devise_spec.rb @@ -51,14 +51,5 @@ if defined?(Devise) expect(helper.current_user).to be_nil end - - it "is backwards-compatible" do - user = double("User") - ActiveSupport::Deprecation.silence do - sign_in user - end - - expect(helper.current_user).to be user - end end end diff --git a/spec/dummy/spec/decorators/post_decorator_spec.rb b/spec/dummy/spec/decorators/post_decorator_spec.rb old mode 100755 new mode 100644 index a763599..97115f5 --- a/spec/dummy/spec/decorators/post_decorator_spec.rb +++ b/spec/dummy/spec/decorators/post_decorator_spec.rb @@ -54,13 +54,11 @@ RSpec.describe PostDecorator do end it "serializes to XML" do - pending("Rails < 3.2 does not use `serializable_hash` in `to_xml`") if Rails.version.to_f < 3.2 - xml = Capybara.string(decorator.to_xml) expect(xml).to have_css "post > updated-at", text: "overridden" end - it "uses a test view context from ApplicationController" do - expect(Draper::ViewContext.current.controller).to be_an ApplicationController + it "uses a test view context from BaseController" do + expect(Draper::ViewContext.current.controller).to be_an BaseController end end diff --git a/spec/dummy/spec/jobs/publish_post_job_spec.rb b/spec/dummy/spec/jobs/publish_post_job_spec.rb new file mode 100644 index 0000000..e0ffbb1 --- /dev/null +++ b/spec/dummy/spec/jobs/publish_post_job_spec.rb @@ -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 diff --git a/spec/dummy/spec/mailers/post_mailer_spec.rb b/spec/dummy/spec/mailers/post_mailer_spec.rb index 1838fef..b6a4d81 100644 --- a/spec/dummy/spec/mailers/post_mailer_spec.rb +++ b/spec/dummy/spec/mailers/post_mailer_spec.rb @@ -10,14 +10,6 @@ RSpec.describe PostMailer do expect(email_body).to have_content "Today" end - it "can use path helpers with a model" do - expect(email_body).to have_css "#path_with_model", text: "/en/posts/#{post.id}" - end - - it "can use path helpers with an id" do - expect(email_body).to have_css "#path_with_id", text: "/en/posts/#{post.id}" - end - it "can use url helpers with a model" do expect(email_body).to have_css "#url_with_model", text: "http://www.example.com:12345/en/posts/#{post.id}" end diff --git a/spec/dummy/spec/models/application_spec.rb b/spec/dummy/spec/models/application_spec.rb new file mode 100644 index 0000000..5fed74b --- /dev/null +++ b/spec/dummy/spec/models/application_spec.rb @@ -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 diff --git a/spec/dummy/spec/models/post_spec.rb b/spec/dummy/spec/models/post_spec.rb index 4c453fe..7dc6213 100644 --- a/spec/dummy/spec/models/post_spec.rb +++ b/spec/dummy/spec/models/post_spec.rb @@ -2,5 +2,14 @@ require_relative '../spec_helper' require_relative '../shared_examples/decoratable' RSpec.describe Post do - it_behaves_like "a decoratable model" + it_behaves_like 'a decoratable model' + + it { should be_a ApplicationRecord } + + describe '#to_global_id' do + let(:post) { Post.create } + subject { post.to_global_id } + + it { is_expected.to eq post.decorate.to_global_id } + end end diff --git a/spec/dummy/spec/shared_examples/decoratable.rb b/spec/dummy/spec/shared_examples/decoratable.rb index 331b4ad..a23428f 100644 --- a/spec/dummy/spec/shared_examples/decoratable.rb +++ b/spec/dummy/spec/shared_examples/decoratable.rb @@ -11,8 +11,6 @@ RSpec.shared_examples_for "a decoratable model" do describe "#==" do it "is true for other instances' decorators" do - pending "Mongoid < 3.1 overrides `#==`" if defined?(Mongoid) && Mongoid::VERSION.to_f < 3.1 && described_class < Mongoid::Document - described_class.create one = described_class.first other = described_class.first diff --git a/spec/dummy/test/decorators/minitest/devise_test.rb b/spec/dummy/test/decorators/minitest/devise_test.rb index f27af83..8e55dd6 100644 --- a/spec/dummy/test/decorators/minitest/devise_test.rb +++ b/spec/dummy/test/decorators/minitest/devise_test.rb @@ -51,14 +51,5 @@ if defined?(Devise) assert helper.current_user.nil? end - - it "is backwards-compatible" do - user = Object.new - ActiveSupport::Deprecation.silence do - sign_in user - end - - assert_same user, helper.current_user - end end end diff --git a/spec/dummy/test/decorators/minitest/view_context_test.rb b/spec/dummy/test/decorators/minitest/view_context_test.rb index c1df670..4f7482f 100644 --- a/spec/dummy/test/decorators/minitest/view_context_test.rb +++ b/spec/dummy/test/decorators/minitest/view_context_test.rb @@ -13,12 +13,12 @@ describe "A decorator test" do it_does_not_leak_view_context end -describe "A controller test" do - tests Class.new(ActionController::Base) +describe "A controller decorator test" do + subject { Class.new(ActionController::Base) } it_does_not_leak_view_context end -describe "A mailer test" do +describe "A mailer decorator test" do it_does_not_leak_view_context end diff --git a/spec/dummy/test/decorators/test_unit/devise_test.rb b/spec/dummy/test/decorators/test_unit/devise_test.rb index d50adea..36a5c0c 100644 --- a/spec/dummy/test/decorators/test_unit/devise_test.rb +++ b/spec/dummy/test/decorators/test_unit/devise_test.rb @@ -51,14 +51,5 @@ if defined?(Devise) assert helper.current_user.nil? end - - def test_backwards_compatibility - user = Object.new - ActiveSupport::Deprecation.silence do - sign_in user - end - - assert_same user, helper.current_user - end end end diff --git a/spec/dummy/test/decorators/test_unit/view_context_test.rb b/spec/dummy/test/decorators/test_unit/view_context_test.rb index 98b71a7..faa1738 100644 --- a/spec/dummy/test/decorators/test_unit/view_context_test.rb +++ b/spec/dummy/test/decorators/test_unit/view_context_test.rb @@ -14,7 +14,7 @@ class DecoratorTest < Draper::TestCase end class ControllerTest < ActionController::TestCase - tests Class.new(ActionController::Base) + subject{ Class.new(ActionController::Base) } it_does_not_leak_view_context end diff --git a/spec/generators/controller/controller_generator_spec.rb b/spec/generators/controller/controller_generator_spec.rb index a42f270..07be58f 100644 --- a/spec/generators/controller/controller_generator_spec.rb +++ b/spec/generators/controller/controller_generator_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -require_relative '../../dummy/spec/rails_helper' -require 'rails' +require 'dummy/config/environment' require 'ammeter/init' require 'generators/controller_override' require 'generators/rails/decorator_generator' +SimpleCov.command_name 'test:generator' -RSpec.describe Rails::Generators::ControllerGenerator do +describe Rails::Generators::ControllerGenerator do destination File.expand_path("../tmp", __FILE__) before { prepare_destination } diff --git a/spec/generators/decorator/decorator_generator_spec.rb b/spec/generators/decorator/decorator_generator_spec.rb index 00af75d..c4318e8 100644 --- a/spec/generators/decorator/decorator_generator_spec.rb +++ b/spec/generators/decorator/decorator_generator_spec.rb @@ -1,10 +1,9 @@ require 'spec_helper' -require 'rspec/rails' -# require_relative '../../dummy/spec/rails_helper' +require 'dummy/config/environment' require 'ammeter/init' require 'generators/rails/decorator_generator' -RSpec.describe Rails::Generators::DecoratorGenerator do +describe Rails::Generators::DecoratorGenerator do destination File.expand_path("../tmp", __FILE__) before { prepare_destination } @@ -41,6 +40,7 @@ RSpec.describe Rails::Generators::DecoratorGenerator do context "with an ApplicationDecorator" do before do + allow_any_instance_of(Object).to receive(:require) allow_any_instance_of(Object).to receive(:require).with("application_decorator").and_return( stub_const "ApplicationDecorator", Class.new ) diff --git a/spec/generators/install/install_generator_spec.rb b/spec/generators/install/install_generator_spec.rb new file mode 100644 index 0000000..c75297f --- /dev/null +++ b/spec/generators/install/install_generator_spec.rb @@ -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 diff --git a/spec/integration/integration_spec.rb b/spec/integration/integration_spec.rb index 24dc3a3..81a49ab 100644 --- a/spec/integration/integration_spec.rb +++ b/spec/integration/integration_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' require 'support/dummy_app' require 'support/matchers/have_text' +SimpleCov.command_name 'test:integration' app = DummyApp.new(ENV["RAILS_ENV"]) spec_types = { @@ -38,16 +39,19 @@ app.start_server do expect(page).to have_text("Goodnight, moon!").in("#goodnight_moon") end - it "can be passed to path helpers" do - expect(page).to have_text("/en/posts/1").in("#path_with_decorator") - end + # _path helpers aren't available in mailers + if type == :view + it "can be passed to path helpers" do + expect(page).to have_text("/en/posts/1").in("#path_with_decorator") + end - it "can use path helpers with a model" do - expect(page).to have_text("/en/posts/1").in("#path_with_model") - end + it "can use path helpers with a model" do + expect(page).to have_text("/en/posts/1").in("#path_with_model") + end - it "can use path helpers with an id" do - expect(page).to have_text("/en/posts/1").in("#path_with_id") + it "can use path helpers with an id" do + expect(page).to have_text("/en/posts/1").in("#path_with_id") + end end it "can be passed to url helpers" do diff --git a/spec/performance/benchmark.rb b/spec/performance/benchmark.rb index 32c63e4..938131e 100644 --- a/spec/performance/benchmark.rb +++ b/spec/performance/benchmark.rb @@ -1,7 +1,7 @@ require 'rubygems' ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) -Bundler.require(:default) if defined?(Bundler) +Bundler.require :default require "benchmark" require "draper" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb old mode 100755 new mode 100644 index a9c2b35..f2d7011 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,12 @@ +require 'simplecov' +SimpleCov.start do + add_filter 'spec' + add_group 'Draper', 'lib/draper' + add_group 'Generators', 'lib/generators' +end + require 'bundler/setup' require 'draper' -require 'rails/version' require 'action_controller' require 'action_controller/test_case' @@ -32,20 +38,23 @@ class Model; include Draper::Decoratable; end class Product < Model; end class SpecialProduct < Product; end class Other < Model; end +class Person < Model; end +class Child < Person; end class ProductDecorator < Draper::Decorator; end class ProductsDecorator < Draper::CollectionDecorator; end -class ProductPresenter < Draper::Decorator; end - class OtherDecorator < Draper::Decorator; end module Namespaced class Product < Model; end class ProductDecorator < Draper::Decorator; end - + ProductsDecorator = Class.new(Draper::CollectionDecorator) class OtherDecorator < Draper::Decorator; end end +ApplicationController = Class.new(ActionController::Base) +CustomController = Class.new(ActionController::Base) + # After each example, revert changes made to the class def protect_class(klass) before { stub_const klass.name, Class.new(klass) } diff --git a/spec/support/shared_examples/view_helpers.rb b/spec/support/shared_examples/view_helpers.rb index 05cd640..22863bd 100644 --- a/spec/support/shared_examples/view_helpers.rb +++ b/spec/support/shared_examples/view_helpers.rb @@ -3,12 +3,12 @@ require 'spec_helper' RSpec.shared_examples_for "view helpers" do |subject| describe "#helpers" do it "returns the current view context" do - allow(Draper::ViewContext).to receive(:current) { :current_view_context } + allow(Draper::ViewContext).to receive_messages current: :current_view_context expect(subject.helpers).to be :current_view_context end it "is aliased to #h" do - allow(Draper::ViewContext).to receive(:current) { :current_view_context } + allow(Draper::ViewContext).to receive_messages current: :current_view_context expect(subject.h).to be :current_view_context end end @@ -22,24 +22,26 @@ RSpec.shared_examples_for "view helpers" do |subject| end it "delegates to #helpers" do - expect(helpers).to receive(:localize).with(:an_object, some: "parameter") + allow(subject).to receive(:helpers).and_return(double) + expect(subject.helpers).to receive(:localize).with(:an_object, some: "parameter") subject.localize(:an_object, some: "parameter") end it "is aliased to #l" do - expect(helpers).to receive(:localize).with(:an_object, some: 'parameter') + allow(subject).to receive_messages helpers: double + expect(subject.helpers).to receive(:localize).with(:an_object, some: "parameter") subject.l(:an_object, some: "parameter") end end describe ".helpers" do it "returns the current view context" do - allow(Draper::ViewContext).to receive(:current) { :current_view_context } + allow(Draper::ViewContext).to receive_messages current: :current_view_context expect(subject.class.helpers).to be :current_view_context end it "is aliased to .h" do - allow(Draper::ViewContext).to receive(:current) { :current_view_context } + allow(Draper::ViewContext).to receive(:current).and_return(:current_view_context) expect(subject.class.h).to be :current_view_context end end