├── .codeclimate.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .yardopts ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── bundle └── rake ├── draper.gemspec ├── lib ├── draper.rb ├── draper │ ├── automatic_delegation.rb │ ├── collection_decorator.rb │ ├── compatibility │ │ ├── api_only.rb │ │ ├── broadcastable.rb │ │ └── global_id.rb │ ├── configuration.rb │ ├── decoratable.rb │ ├── decoratable │ │ ├── collection_proxy.rb │ │ └── equality.rb │ ├── decorated_association.rb │ ├── decorates_assigned.rb │ ├── decorator.rb │ ├── delegation.rb │ ├── factory.rb │ ├── finders.rb │ ├── helper_proxy.rb │ ├── helper_support.rb │ ├── lazy_helpers.rb │ ├── query_methods.rb │ ├── query_methods │ │ └── load_strategy.rb │ ├── railtie.rb │ ├── tasks │ │ └── test.rake │ ├── test │ │ ├── devise_helper.rb │ │ ├── minitest_integration.rb │ │ └── rspec_integration.rb │ ├── test_case.rb │ ├── undecorate.rb │ ├── version.rb │ ├── view_context.rb │ ├── view_context │ │ └── build_strategy.rb │ └── view_helpers.rb └── generators │ ├── controller_override.rb │ ├── draper │ ├── install_generator.rb │ └── templates │ │ └── application_decorator.rb │ ├── mini_test │ ├── decorator_generator.rb │ └── templates │ │ ├── decorator_spec.rb │ │ └── decorator_test.rb │ ├── rails │ ├── decorator_generator.rb │ └── templates │ │ └── decorator.rb │ ├── rspec │ ├── decorator_generator.rb │ └── templates │ │ └── decorator_spec.rb │ └── test_unit │ ├── decorator_generator.rb │ └── templates │ └── decorator_test.rb └── spec ├── draper ├── collection_decorator_spec.rb ├── configuration_spec.rb ├── decoratable │ └── equality_spec.rb ├── decoratable_spec.rb ├── decorated_association_spec.rb ├── decorates_assigned_spec.rb ├── decorator_spec.rb ├── draper_spec.rb ├── factory_spec.rb ├── finders_spec.rb ├── helper_proxy_spec.rb ├── lazy_helpers_spec.rb ├── query_methods │ └── load_strategy_spec.rb ├── query_methods_spec.rb ├── undecorate_chain_spec.rb ├── undecorate_spec.rb ├── view_context │ └── build_strategy_spec.rb ├── view_context_spec.rb └── view_helpers_spec.rb ├── dummy ├── .rspec ├── Rakefile ├── app │ ├── assets │ │ └── config │ │ │ └── manifest.js │ ├── controllers │ │ ├── application_controller.rb │ │ ├── base_controller.rb │ │ ├── localized_urls.rb │ │ └── posts_controller.rb │ ├── decorators │ │ ├── comment_decorator.rb │ │ ├── mongoid_post_decorator.rb │ │ └── post_decorator.rb │ ├── helpers │ │ └── application_helper.rb │ ├── jobs │ │ └── publish_post_job.rb │ ├── mailers │ │ ├── application_mailer.rb │ │ └── post_mailer.rb │ ├── models │ │ ├── admin.rb │ │ ├── application_record.rb │ │ ├── comment.rb │ │ ├── mongoid_post.rb │ │ ├── post.rb │ │ └── user.rb │ └── views │ │ ├── layouts │ │ └── application.html.erb │ │ ├── post_mailer │ │ └── decorated_email.html.erb │ │ └── posts │ │ ├── _post.html.erb │ │ └── show.html.erb ├── bin │ └── rails ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── draper.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ └── session_store.rb │ ├── locales │ │ └── en.yml │ ├── mongoid.yml │ ├── routes.rb │ └── storage.yml ├── db │ ├── migrate │ │ ├── 20121019115657_create_posts.rb │ │ └── 20240907041839_create_comments.rb │ ├── schema.rb │ └── seeds.rb ├── fast_spec │ └── post_decorator_spec.rb ├── lib │ └── tasks │ │ └── test.rake ├── log │ └── .gitkeep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── favicon.ico ├── script │ └── rails ├── spec │ ├── decorators │ │ ├── active_model_serializers_spec.rb │ │ ├── devise_spec.rb │ │ ├── helpers_spec.rb │ │ ├── post_decorator_spec.rb │ │ ├── spec_type_spec.rb │ │ └── view_context_spec.rb │ ├── jobs │ │ └── publish_post_job_spec.rb │ ├── mailers │ │ └── post_mailer_spec.rb │ ├── models │ │ ├── application_spec.rb │ │ ├── mongoid_post_spec.rb │ │ └── post_spec.rb │ ├── rails_helper.rb │ ├── shared_examples │ │ └── decoratable.rb │ └── spec_helper.rb └── test │ ├── decorators │ ├── minitest │ │ ├── devise_test.rb │ │ ├── helpers_test.rb │ │ ├── spec_type_test.rb │ │ └── view_context_test.rb │ └── test_unit │ │ ├── devise_test.rb │ │ ├── helpers_test.rb │ │ └── view_context_test.rb │ ├── minitest_helper.rb │ └── test_helper.rb ├── generators ├── controller │ └── controller_generator_spec.rb ├── decorator │ └── decorator_generator_spec.rb └── install │ └── install_generator_spec.rb ├── integration └── integration_spec.rb ├── performance ├── active_record.rb ├── benchmark.rb ├── decorators.rb └── models.rb ├── spec_helper.rb └── support ├── dummy_app.rb ├── matchers └── have_text.rb └── shared_examples ├── decoratable_equality.rb └── view_helpers.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | fixme: 9 | enabled: true 10 | rubocop: 11 | enabled: true 12 | ratings: 13 | paths: 14 | - "**.rb" 15 | exclude_paths: 16 | - spec/ 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Detail your changes here. 3 | A few sentences describing the overall goals of the pull request's commits will suffice. 4 | Some questions you might answer: 5 | 6 | * Why was this change required? 7 | * Did you have any tough decisions to make? Which one(s) did you go with and why? 8 | * Are there any deployment impacts to this change? 9 | * Is there something you aren't happy with or that needs extra attention? 10 | 11 | ## Testing 12 | Outline steps to test your changes. 13 | 14 | 1. Go here. 15 | 1. Click this. 16 | 1. See that. 17 | 18 | ## To-Dos 19 | - [ ] tests 20 | - [ ] documentation 21 | 22 | ## References 23 | * [GitHub Issue ####](https://github.com/drapergem/draper/issues/####) 24 | * [GitHub Pull Request ####](https://github.com/drapergem/draper/pull/####) 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | rspec: 15 | name: >- 16 | rspec (${{ matrix.ruby }}) 17 | 18 | runs-on: ubuntu-22.04 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | ruby: 24 | - 3.4 25 | - 3.3 26 | - 3.2 27 | - 3.1 28 | rails: 29 | - 8.0 30 | - 7.2 31 | include: 32 | # Edge 33 | - { ruby: 'head', rails: 'edge', allow-fail: true } 34 | # Outdated 35 | - { ruby: '3.0', rails: '7.1' } 36 | - { ruby: '2.7', rails: '6' } # RSpec AR Expectations support Rails 7.1 since Ruby 3.0 37 | - { ruby: '2.6', rails: '6' } 38 | - { ruby: '2.5', rails: '6' } 39 | - { ruby: '2.4', rails: '5' } 40 | exclude: 41 | - { ruby: '3.1', rails: '8.0' } 42 | 43 | env: 44 | RAILS_VERSION: "${{ matrix.rails }}" 45 | 46 | services: 47 | mongodb: 48 | image: mongo 49 | ports: 50 | - 27017:27017 51 | 52 | steps: 53 | - uses: actions/checkout@v4 54 | 55 | - name: Setup Ruby 56 | uses: ruby/setup-ruby@v1 57 | with: 58 | ruby-version: ${{ matrix.ruby }} 59 | rubygems: latest 60 | bundler-cache: true 61 | 62 | - name: RSpec & publish code coverage 63 | uses: paambaati/codeclimate-action@v8 64 | env: 65 | CC_TEST_REPORTER_ID: b7ba588af2a540fa96c267b3655a2afe31ea29976dc25905a668dd28d5e88915 66 | with: 67 | coverageCommand: bin/rake 68 | continue-on-error: ${{ matrix.allow-fail }} 69 | id: test 70 | 71 | - name: >- 72 | Test ${{ steps.test.outcome }} 73 | run: | 74 | echo Ruby ${{ matrix.ruby }} 75 | echo Rails ${{ matrix.rails }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .rvmrc 3 | .ruby-version 4 | .ruby-gemset 5 | .bundle 6 | Gemfile.lock 7 | pkg/* 8 | coverage.data 9 | coverage/* 10 | .yardoc 11 | doc/* 12 | tmp 13 | vendor/bundle 14 | *.swp 15 | *.swo 16 | *.DS_Store 17 | spec/dummy/log/* 18 | spec/dummy/db/*.sqlite3 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | DisplayCopNames: true 4 | Exclude: 5 | - 'spec/dummy/**/*' 6 | 7 | Style/StringLiterals: 8 | Enabled: false 9 | 10 | Metrics/LineLength: 11 | Max: 100 12 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | yardoc 'lib/draper/**/*.rb' -m markdown --no-private 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Draper 2 | =================== 3 | 4 | First of all, **thank you** for wanting to help and reading this! 5 | 6 | If you have found a problem with Draper, please [check to see](https://github.com/drapergem/draper/issues) if there's already an issue and if there's not, [create a new report](https://github.com/drapergem/draper/issues/new). Please include your versions of Draper and Rails (and any other gems that are relevant to your issue, e.g. RSpec if you're having trouble in your tests). 7 | 8 | ## Sending a pull request 9 | 10 | Thanks again! Here's a quick how-to: 11 | 12 | 1. [Fork the project](https://help.github.com/articles/fork-a-repo). 13 | 2. Create a branch - `git checkout -b adding_magic` 14 | 3. Make your changes, and add some tests! 15 | 4. Check that the tests pass - `bundle exec rake` 16 | 5. Commit your changes - `git commit -am "Added some magic"` 17 | 6. Push the branch to Github - `git push origin adding_magic` 18 | 7. Send us a [pull request](https://help.github.com/articles/using-pull-requests)! 19 | 20 | :heart: :sparkling_heart: :heart: 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'puma' 6 | 7 | platforms :ruby do 8 | if RUBY_VERSION >= "3.0.0" 9 | gem 'sqlite3' 10 | elsif RUBY_VERSION >= "2.5.0" 11 | gem 'sqlite3', '~> 1.4.0' 12 | else 13 | gem 'sqlite3', '~> 1.3.6' 14 | end 15 | end 16 | 17 | platforms :jruby do 18 | gem "minitest" 19 | gem "activerecord-jdbcsqlite3-adapter" 20 | end 21 | 22 | case rails_version = ENV['RAILS_VERSION'] 23 | when nil 24 | gem 'rails' 25 | when 'edge' 26 | gem 'rails', github: 'rails/rails' 27 | else 28 | gem 'rails', "~> #{rails_version}.0" 29 | end 30 | 31 | gem 'mongoid' unless 32 | rails_version == 'edge' 33 | gem 'active_model_serializers' 34 | 35 | case RUBY_VERSION 36 | when '2.6'...'3.0' 37 | gem "turbo-rails", "<= 2.0.7" 38 | gem "redis", "~> 4.0" 39 | when '3.0'...'4' 40 | gem 'turbo-rails' 41 | gem 'redis', '~> 4.0' 42 | end 43 | 44 | if RUBY_VERSION < "2.5.0" 45 | gem 'rspec-activerecord-expectations', '~> 1.2.0' 46 | gem 'simplecov', '0.17.1' 47 | gem "loofah", "< 2.21.0" # Workaround for `uninitialized constant Nokogiri::HTML4` 48 | end 49 | 50 | if RUBY_VERSION < "3.0.0" 51 | gem "concurrent-ruby", "< 1.3.5" 52 | end -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | def rspec_guard(options = {}, &block) 2 | options = { 3 | version: 2, 4 | notification: false 5 | }.merge(options) 6 | 7 | guard 'rspec', options, &block 8 | end 9 | 10 | rspec_guard spec_paths: %w{spec/draper spec/generators} do 11 | watch(%r{^spec/.+_spec\.rb$}) 12 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 13 | watch('spec/spec_helper.rb') { "spec" } 14 | end 15 | 16 | rspec_guard spec_paths: 'spec/integration', env: {'RAILS_ENV' => 'development'} do 17 | watch(%r{^spec/.+_spec\.rb$}) 18 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 19 | watch('spec/spec_helper.rb') { "spec" } 20 | end 21 | 22 | rspec_guard spec_paths: 'spec/integration', env: {'RAILS_ENV' => 'production'} do 23 | watch(%r{^spec/.+_spec\.rb$}) 24 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 25 | watch('spec/spec_helper.rb') { "spec" } 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'bundler/gem_tasks' 3 | require 'rspec/core/rake_task' 4 | 5 | def run_in_dummy_app(command) 6 | success = system("cd spec/dummy && #{command}") 7 | raise "#{command} failed" unless success 8 | end 9 | 10 | task "default" => "ci" 11 | 12 | desc "Run all tests for CI" 13 | task "ci" => "spec" 14 | 15 | desc "Run all specs" 16 | task "spec" => "spec:all" 17 | 18 | namespace "spec" do 19 | task "all" => ["draper", "generators", "integration"] 20 | 21 | def spec_task(name) 22 | desc "Run #{name} specs" 23 | RSpec::Core::RakeTask.new(name) do |t| 24 | t.pattern = "spec/#{name}/**/*_spec.rb" 25 | end 26 | end 27 | 28 | spec_task "draper" 29 | spec_task "generators" 30 | 31 | desc "Run integration specs" 32 | task "integration" => ["db:setup", "integration:all"] 33 | 34 | namespace "integration" do 35 | task "all" => ["development", "production", "test"] 36 | 37 | ["development", "production"].each do |environment| 38 | task environment do 39 | Rake::Task["spec:integration:run"].execute environment 40 | end 41 | end 42 | 43 | task "run" do |t, environment| 44 | puts "Running integration specs in #{environment}" 45 | 46 | ENV["RAILS_ENV"] = environment 47 | success = system("rspec spec/integration") 48 | 49 | raise "Integration specs failed in #{environment}" unless success 50 | end 51 | 52 | task "test" do 53 | puts "Running rake in dummy app" 54 | ENV["RAILS_ENV"] = "test" 55 | run_in_dummy_app "rake" 56 | end 57 | end 58 | end 59 | 60 | namespace "db" do 61 | desc "Set up databases for integration testing" 62 | task "setup" do 63 | puts "Setting up databases" 64 | run_in_dummy_app "rm -f db/*.sqlite3" 65 | run_in_dummy_app "RAILS_ENV=development rake db:schema:load db:seed" 66 | run_in_dummy_app "RAILS_ENV=production rake db:schema:load db:seed" 67 | run_in_dummy_app "RAILS_ENV=test rake db:environment:set db:schema:load" 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= 65 | env_var_version || cli_arg_version || 66 | lockfile_version 67 | end 68 | 69 | def bundler_requirement 70 | return "#{Gem::Requirement.default}.a" unless bundler_version 71 | 72 | bundler_gem_version = Gem::Version.new(bundler_version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rake", "rake") 30 | -------------------------------------------------------------------------------- /draper.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/draper/version' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "draper" 5 | s.version = Draper::VERSION 6 | s.authors = ["Jeff Casimir", "Steve Klabnik"] 7 | s.email = ["jeff@casimircreative.com", "steve@steveklabnik.com"] 8 | s.homepage = "https://github.com/drapergem/draper" 9 | s.summary = "View Models for Rails" 10 | s.description = "Draper adds an object-oriented layer of presentation logic to your Rails apps." 11 | s.license = "MIT" 12 | 13 | # Specify which files should be added to the gem when it is released. 14 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 15 | gemspec = File.basename(__FILE__) 16 | s.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| 17 | ls.readlines("\x0", chomp: true).reject do |f| 18 | (f == gemspec) || 19 | f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile]) 20 | end 21 | end 22 | s.require_paths = ["lib"] 23 | 24 | s.required_ruby_version = '>= 2.2.2' 25 | 26 | s.add_dependency 'activesupport', '>= 5.0' 27 | s.add_dependency 'actionpack', '>= 5.0' 28 | s.add_dependency 'request_store', '>= 1.0' 29 | s.add_dependency 'activemodel', '>= 5.0' 30 | s.add_dependency 'activemodel-serializers-xml', '>= 1.0' 31 | s.add_dependency 'ruby2_keywords' 32 | 33 | s.add_development_dependency 'ammeter' 34 | s.add_development_dependency 'rake' 35 | s.add_development_dependency 'rspec-rails' 36 | s.add_development_dependency 'rspec-activerecord-expectations' 37 | s.add_development_dependency 'minitest-rails' 38 | s.add_development_dependency 'capybara' 39 | s.add_development_dependency 'rubocop' 40 | s.add_development_dependency 'simplecov' 41 | end 42 | -------------------------------------------------------------------------------- /lib/draper.rb: -------------------------------------------------------------------------------- 1 | require 'action_view' 2 | require 'active_model/naming' 3 | require 'active_model/serialization' 4 | require 'active_model/serializers/json' 5 | require 'active_model/serializers/xml' 6 | require 'active_support/inflector' 7 | require 'active_support/core_ext/hash/keys' 8 | require 'active_support/core_ext/hash/reverse_merge' 9 | require 'active_support/core_ext/name_error' 10 | 11 | require 'ruby2_keywords' 12 | 13 | require 'draper/version' 14 | require 'draper/configuration' 15 | require 'draper/view_helpers' 16 | require 'draper/compatibility/api_only' 17 | require 'draper/delegation' 18 | require 'draper/automatic_delegation' 19 | require 'draper/finders' 20 | require 'draper/decorator' 21 | require 'draper/helper_proxy' 22 | require 'draper/lazy_helpers' 23 | require 'draper/decoratable' 24 | require 'draper/factory' 25 | require 'draper/decorated_association' 26 | require 'draper/helper_support' 27 | require 'draper/view_context' 28 | require 'draper/query_methods' 29 | require 'draper/collection_decorator' 30 | require 'draper/undecorate' 31 | require 'draper/decorates_assigned' 32 | require 'draper/railtie' if defined?(Rails) 33 | 34 | module Draper 35 | extend Draper::Configuration 36 | 37 | def self.setup_action_controller(base) 38 | base.class_eval do 39 | include Draper::Compatibility::ApiOnly if base == ActionController::API 40 | include Draper::ViewContext 41 | extend Draper::HelperSupport 42 | extend Draper::DecoratesAssigned 43 | 44 | before_action :activate_draper 45 | end 46 | end 47 | 48 | def self.setup_action_mailer(base) 49 | base.class_eval do 50 | include Draper::ViewContext 51 | end 52 | end 53 | 54 | class UninferrableDecoratorError < NameError 55 | def initialize(klass) 56 | super("Could not infer a decorator for #{klass}.") 57 | end 58 | end 59 | 60 | class UninferrableObjectError < NameError 61 | def initialize(klass) 62 | super("Could not infer an object for #{klass}.") 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/draper/automatic_delegation.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module AutomaticDelegation 3 | extend ActiveSupport::Concern 4 | 5 | # Delegates missing instance methods to the source object. Note: This will delegate `super` 6 | # method calls to `object` as well. Calling `super` will first try to call the method on 7 | # the parent decorator class. If no method exists on the parent class, it will then try 8 | # to call the method on the `object`. 9 | ruby2_keywords def method_missing(method, *args, &block) 10 | return super unless delegatable?(method) 11 | 12 | object.send(method, *args, &block) 13 | end 14 | 15 | # Checks if the decorator responds to an instance method, or is able to 16 | # proxy it to the source object. 17 | def respond_to_missing?(method, include_private = false) 18 | super || delegatable?(method) 19 | end 20 | 21 | # The inherit argument for `private_method_defined?` is supported since Ruby 2.6. 22 | if RUBY_VERSION >= "2.6" 23 | # @private 24 | def delegatable?(method) 25 | return if self.class.private_method_defined?(method, false) 26 | 27 | object.respond_to?(method) 28 | end 29 | else 30 | # @private 31 | def delegatable?(method) 32 | return if private_methods(false).include?(method) 33 | 34 | object.respond_to?(method) 35 | end 36 | end 37 | 38 | module ClassMethods 39 | # Proxies missing class methods to the source class. 40 | ruby2_keywords def method_missing(method, *args, &block) 41 | return super unless delegatable?(method) 42 | 43 | object_class.send(method, *args, &block) 44 | end 45 | 46 | # Checks if the decorator responds to a class method, or is able to proxy 47 | # it to the source class. 48 | def respond_to_missing?(method, include_private = false) 49 | super || delegatable?(method) 50 | end 51 | 52 | # @private 53 | def delegatable?(method) 54 | object_class? && object_class.respond_to?(method) 55 | end 56 | 57 | # @private 58 | # Avoids reloading the model class when ActiveSupport clears autoloaded 59 | # dependencies in development mode. 60 | def before_remove_const 61 | end 62 | end 63 | 64 | included do 65 | private :delegatable? 66 | private_class_method :delegatable? 67 | end 68 | 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/draper/collection_decorator.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | class CollectionDecorator 3 | include Enumerable 4 | include Draper::ViewHelpers 5 | include Draper::QueryMethods 6 | extend Draper::Delegation 7 | 8 | # @return the collection being decorated. 9 | attr_reader :object 10 | 11 | # @return [Class] the decorator class used to decorate each item, as set by 12 | # {#initialize}. 13 | attr_reader :decorator_class 14 | 15 | # @return [Hash] extra data to be used in user-defined methods, and passed 16 | # to each item's decorator. 17 | attr_accessor :context 18 | 19 | array_methods = Array.instance_methods - Object.instance_methods 20 | delegate :==, :as_json, *array_methods, to: :decorated_collection 21 | 22 | # @param [Enumerable] object 23 | # collection to decorate. 24 | # @option options [Class, nil] :with (nil) 25 | # the decorator class used to decorate each item. When `nil`, each item's 26 | # {Decoratable#decorate decorate} method will be used. 27 | # @option options [Hash] :context ({}) 28 | # extra data to be stored in the collection decorator and used in 29 | # user-defined methods, and passed to each item's decorator. 30 | def initialize(object, options = {}) 31 | options.assert_valid_keys(:with, :context) 32 | @object = object 33 | @decorator_class = options[:with] 34 | @context = options.fetch(:context, {}) 35 | end 36 | 37 | class << self 38 | alias :decorate :new 39 | end 40 | 41 | # @return [Array] the decorated items. 42 | def decorated_collection 43 | @decorated_collection ||= object.map{|item| decorate_item(item)} 44 | end 45 | 46 | delegate :find, to: :decorated_collection 47 | 48 | def to_s 49 | "#<#{self.class.name} of #{decorator_class || "inferred decorators"} for #{object.inspect}>" 50 | end 51 | 52 | def context=(value) 53 | @context = value 54 | each {|item| item.context = value } if @decorated_collection 55 | end 56 | 57 | # @return [true] 58 | def decorated? 59 | true 60 | end 61 | 62 | alias :decorated_with? :instance_of? 63 | 64 | def replace(other) 65 | decorated_collection.replace(other) 66 | self 67 | end 68 | 69 | protected 70 | 71 | # Decorates the given item. 72 | def decorate_item(item) 73 | item_decorator.call(item, context: context) 74 | end 75 | 76 | private 77 | 78 | def item_decorator 79 | if decorator_class 80 | decorator_class.method(:decorate) 81 | else 82 | ->(item, options) { item.decorate(options) } 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/draper/compatibility/api_only.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module Compatibility 3 | # Draper expects your `ApplicationController` to include `ActionView::Rendering`. The 4 | # `ApplicationController` generated by Rails 5 API-only applications (created with 5 | # `rails new --api`) don't by default. However, including `ActionView::Rendering` in 6 | # `ApplicatonController` breaks `render :json` due to `render_to_body` being overridden. 7 | # 8 | # This compatibility patch fixes the issue by restoring the original `render_to_body` 9 | # method after including `ActionView::Rendering`. Ultimately, including `ActionView::Rendering` 10 | # in an ActionController::API may not be supported functionality by Rails (see Rails issue 11 | # for more detail: https://github.com/rails/rails/issues/27211). This hack is meant to be a 12 | # temporary solution until we can find a way to not rely on the controller layer. 13 | module ApiOnly 14 | extend ActiveSupport::Concern 15 | 16 | included do 17 | alias :previous_render_to_body :render_to_body 18 | include ActionView::Rendering 19 | alias :render_to_body :previous_render_to_body 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/draper/compatibility/broadcastable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Draper 4 | module Compatibility 5 | # It would look consistent to use decorated objects inside templates broadcasted with 6 | # Turbo::Broadcastable. 7 | # 8 | # This compatibility patch fixes the issue by overriding the original defaults to decorate the 9 | # object, that's passed to the partial in a local variable. 10 | module Broadcastable 11 | private 12 | 13 | def broadcast_rendering_with_defaults(options) 14 | return super unless decorator_class? 15 | 16 | # Add the decorated current instance into the locals (see original method for details). 17 | options[:locals] = 18 | (options[:locals] || {}).reverse_merge!(model_name.element.to_sym => decorate) 19 | 20 | super 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/draper/compatibility/global_id.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module Compatibility 3 | # [Active Job](http://edgeguides.rubyonrails.org/active_job_basics.html) allows you to pass 4 | # ActiveRecord objects to background tasks directly and performs the necessary serialization 5 | # and deserialization. In order to do this, arguments to a background job must implement 6 | # [Global ID](https://github.com/rails/globalid). 7 | # 8 | # This compatibility patch implements Global ID for decorated objects by defining `.find(id)` 9 | # class method that uses the original one and decorates the result. 10 | # This means you can pass decorated objects to background jobs and they will be decorated when 11 | # deserialized. 12 | module GlobalID 13 | extend ActiveSupport::Concern 14 | 15 | included do 16 | include ::GlobalID::Identification 17 | end 18 | 19 | class_methods do 20 | def find(*args) 21 | object_class.find(*args).decorate 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/draper/configuration.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module Configuration 3 | def configure 4 | yield self 5 | end 6 | 7 | def default_controller 8 | @@default_controller ||= ApplicationController 9 | end 10 | 11 | def default_controller=(controller) 12 | @@default_controller = controller 13 | end 14 | 15 | def default_query_methods_strategy 16 | @@default_query_methods_strategy ||= :active_record 17 | end 18 | 19 | def default_query_methods_strategy=(strategy) 20 | @@default_query_methods_strategy = strategy 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/draper/decoratable.rb: -------------------------------------------------------------------------------- 1 | require 'draper/decoratable/equality' 2 | require 'draper/compatibility/broadcastable' 3 | 4 | module Draper 5 | # Provides shortcuts to decorate objects directly, so you can do 6 | # `@product.decorate` instead of `ProductDecorator.new(@product)`. 7 | # 8 | # This module is included by default into `ActiveRecord::Base` and 9 | # `Mongoid::Document`, but you're using another ORM, or want to decorate 10 | # plain old Ruby objects, you can include it manually. 11 | module Decoratable 12 | extend ActiveSupport::Concern 13 | include Draper::Decoratable::Equality 14 | 15 | autoload :CollectionProxy, 'draper/decoratable/collection_proxy' 16 | 17 | included do 18 | prepend Draper::Compatibility::Broadcastable if defined? Turbo::Broadcastable 19 | end 20 | 21 | # Decorates the object using the inferred {#decorator_class}. 22 | # @param [Hash] options 23 | # see {Decorator#initialize} 24 | def decorate(options = {}) 25 | decorator_class.decorate(self, options) 26 | end 27 | 28 | # (see ClassMethods#decorator_class) 29 | def decorator_class 30 | self.class.decorator_class 31 | end 32 | 33 | def decorator_class? 34 | self.class.decorator_class? 35 | end 36 | 37 | # The list of decorators that have been applied to the object. 38 | # 39 | # @return [Array] `[]` 40 | def applied_decorators 41 | [] 42 | end 43 | 44 | # (see Decorator#decorated_with?) 45 | # @return [false] 46 | def decorated_with?(decorator_class) 47 | false 48 | end 49 | 50 | # Checks if this object is decorated. 51 | # 52 | # @return [false] 53 | def decorated? 54 | false 55 | end 56 | 57 | module ClassMethods 58 | # Decorates a collection of objects. Used at the end of a scope chain. 59 | # 60 | # @example 61 | # Product.popular.decorate 62 | # @param [Hash] options 63 | # see {Decorator.decorate_collection}. 64 | def decorate(options = {}) 65 | decorator_class.decorate_collection(all, options.reverse_merge(with: nil)) 66 | end 67 | 68 | def decorator_class? 69 | decorator_class 70 | rescue Draper::UninferrableDecoratorError 71 | false 72 | end 73 | 74 | # Infers the decorator class to be used by {Decoratable#decorate} (e.g. 75 | # `Product` maps to `ProductDecorator`). 76 | # 77 | # @return [Class] the inferred decorator class. 78 | def decorator_class(called_on = self) 79 | prefix = respond_to?(:model_name) ? model_name : name 80 | decorator_name = "#{prefix}Decorator" 81 | decorator_name_constant = decorator_name.safe_constantize 82 | return decorator_name_constant unless decorator_name_constant.nil? 83 | 84 | if superclass.respond_to?(:decorator_class) 85 | superclass.decorator_class(called_on) 86 | else 87 | raise Draper::UninferrableDecoratorError.new(called_on) 88 | end 89 | end 90 | 91 | # Compares with possibly-decorated objects. 92 | # 93 | # @return [Boolean] 94 | def ===(other) 95 | super || (other.is_a?(Draper::Decorator) && super(other.object)) 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/draper/decoratable/collection_proxy.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module Decoratable 3 | module CollectionProxy 4 | # Decorates a collection of objects. Used at the end of a scope chain. 5 | # 6 | # @example 7 | # company.products.popular.decorate 8 | # @param [Hash] options 9 | # see {Decorator.decorate_collection}. 10 | def decorate(options = {}) 11 | decorator_class.decorate_collection(load_target, options.reverse_merge(with: nil)) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/draper/decoratable/equality.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module Decoratable 3 | module Equality 4 | # Compares self with a possibly-decorated object. 5 | # 6 | # @return [Boolean] 7 | def ==(other) 8 | super || Equality.test_for_decorator(self, other) 9 | end 10 | 11 | # Compares an object to a possibly-decorated object. 12 | # 13 | # @return [Boolean] 14 | def self.test(object, other) 15 | return object == other if object.is_a?(Decoratable) 16 | object == other || test_for_decorator(object, other) 17 | end 18 | 19 | # @private 20 | def self.test_for_decorator(object, other) 21 | other.respond_to?(:decorated?) && other.decorated? && 22 | other.respond_to?(:object) && test(object, other.object) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/draper/decorated_association.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | # @private 3 | class DecoratedAssociation 4 | def initialize(owner, association, options) 5 | options.assert_valid_keys(:with, :scope, :context) 6 | 7 | @owner = owner 8 | @association = association 9 | 10 | @scope = options[:scope] 11 | 12 | decorator_class = options[:with] 13 | context = options.fetch(:context, ->(context){ context }) 14 | @factory = Draper::Factory.new(with: decorator_class, context: context) 15 | end 16 | 17 | def call 18 | decorate unless defined?(@decorated) 19 | @decorated 20 | end 21 | 22 | private 23 | 24 | attr_reader :factory, :owner, :association, :scope 25 | 26 | def decorate 27 | associated = owner.object.send(association) 28 | associated = associated.send(scope) if scope 29 | 30 | @decorated = factory.decorate(associated, context_args: owner.context) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/draper/decorates_assigned.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module DecoratesAssigned 3 | # @overload decorates_assigned(*variables, options = {}) 4 | # Defines a helper method to access decorated instance variables. 5 | # 6 | # @example 7 | # # app/controllers/articles_controller.rb 8 | # class ArticlesController < ApplicationController 9 | # decorates_assigned :article 10 | # 11 | # def show 12 | # @article = Article.find(params[:id]) 13 | # end 14 | # end 15 | # 16 | # # app/views/articles/show.html.erb 17 | # <%= article.decorated_title %> 18 | # 19 | # @param [Symbols*] variables 20 | # names of the instance variables to decorate (without the `@`). 21 | # @param [Hash] options 22 | # @option options [Decorator, CollectionDecorator] :with (nil) 23 | # decorator class to use. If nil, it is inferred from the instance 24 | # variable. 25 | # @option options [Hash, #call] :context 26 | # extra data to be stored in the decorator. If a Proc is given, it will 27 | # be passed the controller and should return a new context hash. 28 | def decorates_assigned(*variables) 29 | factory = Draper::Factory.new(variables.extract_options!) 30 | 31 | variables.each do |variable| 32 | undecorated = "@#{variable}" 33 | decorated = "@decorated_#{variable}" 34 | 35 | define_method variable do 36 | return instance_variable_get(decorated) if instance_variable_defined?(decorated) 37 | instance_variable_set decorated, factory.decorate(instance_variable_get(undecorated), context_args: self) 38 | end 39 | 40 | helper_method variable 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/draper/decorator.rb: -------------------------------------------------------------------------------- 1 | require 'draper/compatibility/global_id' 2 | 3 | module Draper 4 | class Decorator 5 | include Draper::ViewHelpers 6 | include Draper::Compatibility::GlobalID if defined?(GlobalID) 7 | extend Draper::Delegation 8 | 9 | include ActiveModel::Serialization 10 | include ActiveModel::Serializers::JSON 11 | include ActiveModel::Serializers::Xml 12 | 13 | # @return the object being decorated. 14 | attr_reader :object 15 | 16 | alias :model :object 17 | 18 | # @return [Hash] extra data to be used in user-defined methods. 19 | attr_accessor :context 20 | 21 | # Wraps an object in a new instance of the decorator. 22 | # 23 | # Decorators may be applied to other decorators. However, applying a 24 | # decorator to an instance of itself will create a decorator with the same 25 | # source as the original, rather than redecorating the other instance. 26 | # 27 | # @param [Object] object 28 | # object to decorate. 29 | # @option options [Hash] :context ({}) 30 | # extra data to be stored in the decorator and used in user-defined 31 | # methods. 32 | def initialize(object, options = {}) 33 | options.assert_valid_keys(:context) 34 | @object = object 35 | @context = options.fetch(:context, {}) 36 | handle_multiple_decoration(options) if object.is_a?(Draper::Decorator) 37 | end 38 | 39 | class << self 40 | alias :decorate :new 41 | end 42 | 43 | # Automatically delegates instance methods to the source object. Class 44 | # methods will be delegated to the {object_class}, if it is set. 45 | # 46 | # @return [void] 47 | def self.delegate_all 48 | include Draper::AutomaticDelegation 49 | end 50 | 51 | # Sets the source class corresponding to the decorator class. 52 | # 53 | # @note This is only necessary if you wish to proxy class methods to the 54 | # source (including when using {decorates_finders}), and the source class 55 | # cannot be inferred from the decorator class (e.g. `ProductDecorator` 56 | # maps to `Product`). 57 | # @param [String, Symbol, Class] object_class 58 | # source class (or class name) that corresponds to this decorator. 59 | # @return [void] 60 | def self.decorates(object_class) 61 | @object_class = object_class.to_s.camelize.constantize 62 | alias_object_to_object_class_name 63 | end 64 | 65 | # Returns the source class corresponding to the decorator class, as set by 66 | # {decorates}, or as inferred from the decorator class name (e.g. 67 | # `ProductDecorator` maps to `Product`). 68 | # 69 | # @return [Class] the source class that corresponds to this decorator. 70 | def self.object_class 71 | @object_class ||= inferred_object_class 72 | end 73 | 74 | # Checks whether this decorator class has a corresponding {object_class}. 75 | def self.object_class? 76 | object_class 77 | rescue Draper::UninferrableObjectError 78 | false 79 | end 80 | 81 | # Automatically decorates ActiveRecord finder methods, so that you can use 82 | # `ProductDecorator.find(id)` instead of 83 | # `ProductDecorator.decorate(Product.find(id))`. 84 | # 85 | # Finder methods are applied to the {object_class}. 86 | # 87 | # @return [void] 88 | def self.decorates_finders 89 | extend Draper::Finders 90 | end 91 | 92 | # Automatically decorate an association. 93 | # 94 | # @param [Symbol] association 95 | # name of the association to decorate (e.g. `:products`). 96 | # @option options [Class] :with 97 | # the decorator to apply to the association. 98 | # @option options [Symbol] :scope 99 | # a scope to apply when fetching the association. 100 | # @option options [Hash, #call] :context 101 | # extra data to be stored in the associated decorator. If omitted, the 102 | # associated decorator's context will be the same as the parent 103 | # decorator's. If a Proc is given, it will be called with the parent's 104 | # context and should return a new context hash for the association. 105 | # @return [void] 106 | def self.decorates_association(association, options = {}) 107 | options.assert_valid_keys(:with, :scope, :context) 108 | define_method(association) do 109 | decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options) 110 | decorated_associations[association].call 111 | end 112 | end 113 | 114 | # @overload decorates_associations(*associations, options = {}) 115 | # Automatically decorate multiple associations. 116 | # @param [Symbols*] associations 117 | # names of the associations to decorate. 118 | # @param [Hash] options 119 | # see {decorates_association}. 120 | # @return [void] 121 | def self.decorates_associations(*associations) 122 | options = associations.extract_options! 123 | associations.each do |association| 124 | decorates_association(association, options) 125 | end 126 | end 127 | 128 | # Decorates a collection of objects. The class of the collection decorator 129 | # is inferred from the decorator class if possible (e.g. `ProductDecorator` 130 | # maps to `ProductsDecorator`), but otherwise defaults to 131 | # {Draper::CollectionDecorator}. 132 | # 133 | # @param [Object] object 134 | # collection to decorate. 135 | # @option options [Class, nil] :with (self) 136 | # the decorator class used to decorate each item. When `nil`, it is 137 | # inferred from each item. 138 | # @option options [Hash] :context 139 | # extra data to be stored in the collection decorator. 140 | def self.decorate_collection(object, options = {}) 141 | options.assert_valid_keys(:with, :context) 142 | collection_decorator_class.new(object, options.reverse_merge(with: self)) 143 | end 144 | 145 | # @return [Array] the list of decorators that have been applied to 146 | # the object. 147 | def applied_decorators 148 | chain = object.respond_to?(:applied_decorators) ? object.applied_decorators : [] 149 | chain << self.class 150 | end 151 | 152 | # Checks if a given decorator has been applied to the object. 153 | # 154 | # @param [Class] decorator_class 155 | def decorated_with?(decorator_class) 156 | applied_decorators.include?(decorator_class) 157 | end 158 | 159 | # Checks if this object is decorated. 160 | # 161 | # @return [true] 162 | def decorated? 163 | true 164 | end 165 | 166 | # Compares the source object with a possibly-decorated object. 167 | # 168 | # @return [Boolean] 169 | def ==(other) 170 | Draper::Decoratable::Equality.test(object, other) 171 | end 172 | 173 | # Delegates equality to :== as expected 174 | # 175 | # @return [Boolean] 176 | def eql?(other) 177 | self == other 178 | end 179 | 180 | # Returns a unique hash for a decorated object based on 181 | # the decorator class and the object being decorated. 182 | # 183 | # @return [Fixnum] 184 | def hash 185 | self.class.hash ^ object.hash 186 | end 187 | 188 | delegate :to_s 189 | 190 | # In case object is nil 191 | delegate :present?, :blank? 192 | 193 | # ActiveModel compatibility 194 | # @private 195 | def to_model 196 | self 197 | end 198 | 199 | # @return [Hash] the object's attributes, sliced to only include those 200 | # implemented by the decorator. 201 | def attributes 202 | object.attributes.select {|attribute, _| respond_to?(attribute) } 203 | end 204 | 205 | # ActiveModel compatibility 206 | delegate :to_param, :to_partial_path 207 | 208 | # ActiveModel compatibility 209 | singleton_class.delegate :model_name, to: :object_class 210 | 211 | # @return [Class] the class created by {decorate_collection}. 212 | def self.collection_decorator_class 213 | name = collection_decorator_name 214 | name_constant = name&.safe_constantize 215 | 216 | name_constant || Draper::CollectionDecorator 217 | end 218 | 219 | private 220 | 221 | def self.inherited(subclass) 222 | subclass.alias_object_to_object_class_name 223 | super 224 | end 225 | 226 | def self.alias_object_to_object_class_name 227 | alias_method object_class.name.underscore, :object if object_class? 228 | end 229 | 230 | def self.object_class_name 231 | return nil if name.nil? || name.demodulize !~ /.+Decorator$/ 232 | name.chomp("Decorator") 233 | end 234 | 235 | def self.inferred_object_class 236 | name = object_class_name 237 | name_constant = name&.safe_constantize 238 | return name_constant unless name_constant.nil? 239 | 240 | raise Draper::UninferrableObjectError.new(self) 241 | end 242 | 243 | def self.collection_decorator_name 244 | singular = object_class_name 245 | plural = singular&.pluralize 246 | 247 | "#{plural}Decorator" unless plural == singular 248 | end 249 | 250 | def handle_multiple_decoration(options) 251 | if object.applied_decorators.last == self.class 252 | @context = object.context unless options.has_key?(:context) 253 | @object = object.object 254 | elsif object.applied_decorators.include?(self.class) 255 | warn "Reapplying #{self.class} decorator to target that is already decorated with it. Call stack:\n#{caller(1).join("\n")}" 256 | end 257 | end 258 | 259 | def decorated_associations 260 | @decorated_associations ||= {} 261 | end 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /lib/draper/delegation.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module Delegation 3 | # @overload delegate(*methods, options = {}) 4 | # Overrides {http://api.rubyonrails.org/classes/Module.html#method-i-delegate Module.delegate} 5 | # to make `:object` the default delegation target. 6 | # 7 | # @return [void] 8 | def delegate(*methods) 9 | options = methods.extract_options! 10 | super(*methods, **options.reverse_merge(to: :object)) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/draper/factory.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | class Factory 3 | # Creates a decorator factory. 4 | # 5 | # @option options [Decorator, CollectionDecorator] :with (nil) 6 | # decorator class to use. If nil, it is inferred from the object 7 | # passed to {#decorate}. 8 | # @option options [Hash, #call] context 9 | # extra data to be stored in created decorators. If a proc is given, it 10 | # will be called each time {#decorate} is called and its return value 11 | # will be used as the context. 12 | def initialize(options = {}) 13 | options.assert_valid_keys(:with, :context) 14 | @decorator_class = options.delete(:with) 15 | @default_options = options 16 | end 17 | 18 | # Decorates an object, inferring whether to create a singular or collection 19 | # decorator from the type of object passed. 20 | # 21 | # @param [Object] object 22 | # object to decorate. 23 | # @option options [Hash] context 24 | # extra data to be stored in the decorator. Overrides any context passed 25 | # to the constructor. 26 | # @option options [Object, Array] context_args (nil) 27 | # argument(s) to be passed to the context proc. 28 | # @return [Decorator, CollectionDecorator] the decorated object. 29 | def decorate(object, options = {}) 30 | return nil if object.nil? 31 | Worker.new(decorator_class, object).call(options.reverse_merge(default_options)) 32 | end 33 | 34 | private 35 | 36 | attr_reader :decorator_class, :default_options 37 | 38 | # @private 39 | class Worker 40 | def initialize(decorator_class, object) 41 | @decorator_class = decorator_class 42 | @object = object 43 | end 44 | 45 | def call(options) 46 | update_context options 47 | decorator.call(object, options) 48 | end 49 | 50 | def decorator 51 | return decorator_method(decorator_class) if decorator_class 52 | return object_decorator if decoratable? 53 | return decorator_method(Draper::CollectionDecorator) if collection? 54 | raise Draper::UninferrableDecoratorError.new(object.class) 55 | end 56 | 57 | private 58 | 59 | attr_reader :decorator_class, :object 60 | 61 | def object_decorator 62 | if collection? 63 | ->(object, options) { object.decorator_class.decorate_collection(object, options.reverse_merge(with: nil))} 64 | else 65 | ->(object, options) { object.decorate(options) } 66 | end 67 | end 68 | 69 | def decorator_method(klass) 70 | if collection? && klass.respond_to?(:decorate_collection) 71 | klass.method(:decorate_collection) 72 | else 73 | klass.method(:decorate) 74 | end 75 | end 76 | 77 | def collection? 78 | object.respond_to?(:first) && !object.is_a?(Struct) 79 | end 80 | 81 | def decoratable? 82 | object.respond_to?(:decorate) 83 | end 84 | 85 | def update_context(options) 86 | args = options.delete(:context_args) 87 | options[:context] = options[:context].call(*Array.wrap(args)) if options[:context].respond_to?(:call) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/draper/finders.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | # Provides automatically-decorated finder methods for your decorators. You 3 | # do not have to extend this module directly; it is extended by 4 | # {Decorator.decorates_finders}. 5 | module Finders 6 | def find(id, options = {}) 7 | decorate(object_class.find(id), options) 8 | end 9 | 10 | def all(options = {}) 11 | decorate_collection(object_class.all, options) 12 | end 13 | 14 | def first(options = {}) 15 | decorate(object_class.first, options) 16 | end 17 | 18 | def last(options = {}) 19 | decorate(object_class.last, options) 20 | end 21 | 22 | # Decorates dynamic finder methods (`find_all_by_` and friends). 23 | def method_missing(method, *args, &block) 24 | return super unless method =~ /^find_(all_|last_|or_(initialize_|create_))?by_/ 25 | 26 | result = object_class.send(method, *args, &block) 27 | options = args.extract_options! 28 | 29 | if method =~ /^find_all/ 30 | decorate_collection(result, options) 31 | else 32 | decorate(result, options) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/draper/helper_proxy.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | # Provides access to helper methods - both Rails built-in helpers, and those 3 | # defined in your application. 4 | class HelperProxy 5 | # @overload initialize(view_context) 6 | def initialize(view_context) 7 | @view_context = view_context 8 | end 9 | 10 | # Sends helper methods to the view context. 11 | ruby2_keywords def method_missing(method, *args, &block) 12 | self.class.define_proxy method 13 | send(method, *args, &block) 14 | end 15 | 16 | # Checks if the context responds to an instance method, or is able to 17 | # proxy it to the view context. 18 | def respond_to_missing?(method, include_private = false) 19 | super || view_context.respond_to?(method) 20 | end 21 | 22 | delegate :capture, to: :view_context 23 | 24 | protected 25 | 26 | attr_reader :view_context 27 | 28 | private 29 | 30 | def self.define_proxy(name) 31 | define_method name do |*args, &block| 32 | view_context.send(name, *args, &block) 33 | end 34 | ruby2_keywords name 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/draper/helper_support.rb: -------------------------------------------------------------------------------- 1 | module Draper::HelperSupport 2 | def decorate(input, &block) 3 | capture { block.call(input.decorate) } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/draper/lazy_helpers.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | # Include this module in your decorators to get direct access to the helpers 3 | # so that you can stop typing `h.` everywhere, at the cost of mixing in a 4 | # bazillion methods. 5 | module LazyHelpers 6 | # Sends missing methods to the {HelperProxy}. 7 | ruby2_keywords def method_missing(method, *args, &block) 8 | helpers.send(method, *args, &block) 9 | rescue NoMethodError 10 | super 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/draper/query_methods.rb: -------------------------------------------------------------------------------- 1 | require_relative 'query_methods/load_strategy' 2 | 3 | module Draper 4 | module QueryMethods 5 | # Proxies missing query methods to the source class if the strategy allows. 6 | ruby2_keywords def method_missing(method, *args, &block) 7 | return super unless strategy.allowed? method 8 | 9 | object.send(method, *args, &block).decorate(with: decorator_class, context: context) 10 | end 11 | 12 | def respond_to_missing?(method, include_private = false) 13 | object.respond_to?(method) && strategy.allowed?(method) || super 14 | end 15 | 16 | private 17 | 18 | # Configures the strategy used to proxy the query methods, which defaults to `:active_record`. 19 | def strategy 20 | @strategy ||= LoadStrategy.new(Draper.default_query_methods_strategy) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/draper/query_methods/load_strategy.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module QueryMethods 3 | module LoadStrategy 4 | def self.new(name) 5 | const_get(name.to_s.camelize).new 6 | end 7 | 8 | class ActiveRecord 9 | def allowed?(method) 10 | ::ActiveRecord::Relation::VALUE_METHODS.include? method 11 | end 12 | end 13 | 14 | class Mongoid 15 | def allowed?(method) 16 | raise NotImplementedError 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/draper/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails/railtie' 2 | 3 | module ActiveModel 4 | class Railtie < Rails::Railtie 5 | generators do |app| 6 | Rails::Generators.configure! app.config.generators 7 | require_relative '../generators/controller_override' 8 | end 9 | end 10 | end 11 | 12 | module Draper 13 | class Railtie < Rails::Railtie 14 | config.after_initialize do |app| 15 | app.config.paths.add 'app/decorators', eager_load: true 16 | 17 | if Rails.env.test? 18 | require 'draper/test_case' 19 | require 'draper/test/rspec_integration' if defined?(RSpec) and RSpec.respond_to?(:configure) 20 | end 21 | end 22 | 23 | initializer 'draper.setup_action_controller' do 24 | ActiveSupport.on_load :action_controller do 25 | Draper.setup_action_controller self 26 | end 27 | end 28 | 29 | initializer 'draper.setup_action_mailer' do 30 | ActiveSupport.on_load :action_mailer do 31 | Draper.setup_action_mailer self 32 | end 33 | end 34 | 35 | initializer 'draper.setup_orm' do 36 | ActiveSupport.on_load :active_record do 37 | include Draper::Decoratable 38 | 39 | ActiveRecord::Associations::CollectionProxy.include Draper::Decoratable::CollectionProxy 40 | end 41 | 42 | ActiveSupport.on_load :mongoid do 43 | include Draper::Decoratable 44 | end 45 | end 46 | 47 | initializer 'draper.minitest-rails_integration' do 48 | ActiveSupport.on_load :minitest do 49 | require 'draper/test/minitest_integration' 50 | end 51 | end 52 | 53 | def initialize_view_context 54 | require 'action_controller/test_case' 55 | Draper.default_controller.new.view_context 56 | Draper::ViewContext.build 57 | end 58 | 59 | console { initialize_view_context } 60 | 61 | runner { initialize_view_context } 62 | 63 | rake_tasks { Dir[File.join(File.dirname(__FILE__), 'tasks/*.rake')].each { |f| load f } } 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/draper/tasks/test.rake: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | require 'rails/test_unit/railtie' 3 | 4 | namespace :test do 5 | Rake::TestTask.new(decorators: "test:prepare") do |t| 6 | t.libs << "test" 7 | t.pattern = "test/decorators/**/*_test.rb" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/draper/test/devise_helper.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module DeviseHelper 3 | def sign_in(resource_or_scope, resource = nil) 4 | scope = Devise::Mapping.find_scope!(resource_or_scope) 5 | _stub_current_scope scope, resource || resource_or_scope 6 | end 7 | 8 | def sign_out(resource_or_scope) 9 | scope = Devise::Mapping.find_scope!(resource_or_scope) 10 | _stub_current_scope scope, nil 11 | end 12 | 13 | private 14 | 15 | def _stub_current_scope(scope, resource) 16 | Draper::ViewContext.current.controller.singleton_class.class_eval do 17 | define_method "current_#{scope}" do 18 | resource 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/draper/test/minitest_integration.rb: -------------------------------------------------------------------------------- 1 | class Draper::TestCase 2 | register_spec_type(self) do |desc| 3 | desc < Draper::Decorator || desc < Draper::CollectionDecorator if desc.is_a?(Class) 4 | end 5 | register_spec_type(/Decorator( ?Test)?\z/i, self) 6 | end 7 | -------------------------------------------------------------------------------- /lib/draper/test/rspec_integration.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module DecoratorExampleGroup 3 | include Draper::TestCase::Behavior 4 | extend ActiveSupport::Concern 5 | 6 | included { metadata[:type] = :decorator } 7 | end 8 | 9 | RSpec.configure do |config| 10 | config.include DecoratorExampleGroup, file_path: %r{spec/decorators}, type: :decorator 11 | 12 | [:decorator, :controller, :mailer].each do |type| 13 | config.before(:each, type: type) { Draper::ViewContext.clear! } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/draper/test_case.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | require 'active_support/test_case' 3 | 4 | class TestCase < ::ActiveSupport::TestCase 5 | module ViewContextTeardown 6 | def before_setup 7 | Draper::ViewContext.clear! 8 | super 9 | end 10 | end 11 | 12 | module Behavior 13 | if defined?(::Devise) 14 | require 'draper/test/devise_helper' 15 | include Draper::DeviseHelper 16 | end 17 | 18 | if defined?(::Capybara) && (defined?(::RSpec) || defined?(::MiniTest::Matchers)) 19 | require 'capybara/rspec/matchers' 20 | include ::Capybara::RSpecMatchers 21 | end 22 | 23 | include Draper::ViewHelpers::ClassMethods 24 | alias :helper :helpers 25 | end 26 | 27 | include Behavior 28 | include ViewContextTeardown 29 | end 30 | end 31 | 32 | if defined? ActionController::TestCase 33 | ActionController::TestCase.include Draper::TestCase::ViewContextTeardown 34 | end 35 | 36 | if defined? ActionMailer::TestCase 37 | ActionMailer::TestCase.include Draper::TestCase::ViewContextTeardown 38 | end 39 | -------------------------------------------------------------------------------- /lib/draper/undecorate.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | def self.undecorate(object) 3 | if object.respond_to?(:decorated?) && object.decorated? 4 | object.object 5 | else 6 | object 7 | end 8 | end 9 | 10 | def self.undecorate_chain(object) 11 | if object.respond_to?(:decorated?) && object.decorated? 12 | undecorate_chain(object.object) 13 | else 14 | object 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/draper/version.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | VERSION = '4.1.0-pre' 3 | end 4 | -------------------------------------------------------------------------------- /lib/draper/view_context.rb: -------------------------------------------------------------------------------- 1 | require 'draper/view_context/build_strategy' 2 | require 'request_store' 3 | 4 | module Draper 5 | module ViewContext 6 | # Hooks into a controller or mailer to save the view context in {current}. 7 | def view_context 8 | super.tap do |context| 9 | Draper::ViewContext.current = context 10 | end 11 | end 12 | 13 | # Set the current controller 14 | def activate_draper 15 | Draper::ViewContext.controller = self 16 | end 17 | 18 | # Returns the current controller. 19 | def self.controller 20 | RequestStore.store[:current_controller] 21 | end 22 | 23 | # Sets the current controller. Clears view context when we are setting 24 | # different controller. 25 | def self.controller=(controller) 26 | clear! if RequestStore.store[:current_controller] != controller 27 | RequestStore.store[:current_controller] = controller 28 | end 29 | 30 | # Returns the current view context, or builds one if none is saved. 31 | # 32 | # @return [HelperProxy] 33 | def self.current 34 | RequestStore.store.fetch(:current_view_context) { build! } 35 | end 36 | 37 | # Sets the current view context. 38 | def self.current=(view_context) 39 | RequestStore.store[:current_view_context] = Draper::HelperProxy.new(view_context) 40 | end 41 | 42 | # Clears the saved controller and view context. 43 | def self.clear! 44 | RequestStore.store.delete :current_controller 45 | RequestStore.store.delete :current_view_context 46 | end 47 | 48 | # Builds a new view context for usage in tests. See {test_strategy} for 49 | # details of how the view context is built. 50 | def self.build 51 | build_strategy.call 52 | end 53 | 54 | # Builds a new view context and sets it as the current view context. 55 | # 56 | # @return [HelperProxy] 57 | def self.build! 58 | # send because we want to return the HelperProxy returned from #current= 59 | send :current=, build 60 | end 61 | 62 | # Configures the strategy used to build view contexts in tests, which 63 | # defaults to `:full` if `test_strategy` has not been called. Evaluates 64 | # the block, if given, in the context of the view context's class. 65 | # 66 | # @example Pass a block to add helper methods to the view context: 67 | # Draper::ViewContext.test_strategy :fast do 68 | # include ApplicationHelper 69 | # end 70 | # 71 | # @param [:full, :fast] name 72 | # the strategy to use: 73 | # 74 | # `:full` - build a fully-working view context. Your Rails environment 75 | # must be loaded, including your `ApplicationController`. 76 | # 77 | # `:fast` - build a minimal view context in tests, with no dependencies 78 | # on other components of your application. 79 | def self.test_strategy(name, &block) 80 | @build_strategy = Draper::ViewContext::BuildStrategy.new(name, &block) 81 | end 82 | 83 | # @private 84 | def self.build_strategy 85 | @build_strategy ||= Draper::ViewContext::BuildStrategy.new(:full) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/draper/view_context/build_strategy.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module ViewContext 3 | # @private 4 | module BuildStrategy 5 | def self.new(name, &block) 6 | const_get(name.to_s.camelize).new(&block) 7 | end 8 | 9 | class Fast 10 | def initialize(&block) 11 | @view_context_class = Class.new(ActionView::Base, &block) 12 | end 13 | 14 | def call 15 | view_context_class.respond_to?(:empty) ? view_context_class.empty : view_context_class.new 16 | end 17 | 18 | private 19 | 20 | attr_reader :view_context_class 21 | end 22 | 23 | class Full 24 | def initialize(&block) 25 | @block = block 26 | end 27 | 28 | def call 29 | controller.view_context.tap do |context| 30 | context.singleton_class.class_eval(&block) if block 31 | end 32 | end 33 | 34 | private 35 | 36 | attr_reader :block 37 | 38 | def controller 39 | Draper::ViewContext.controller ||= Draper.default_controller.new 40 | Draper::ViewContext.controller.tap do |controller| 41 | controller.request ||= ActionDispatch::TestRequest.create 42 | end 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/draper/view_helpers.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | # Provides the {#helpers} method used in {Decorator} and {CollectionDecorator} 3 | # to call the Rails helpers. 4 | module ViewHelpers 5 | extend ActiveSupport::Concern 6 | 7 | module ClassMethods 8 | # Access the helpers proxy to call built-in and user-defined 9 | # Rails helpers from a class context. 10 | # 11 | # @return [HelperProxy] the helpers proxy 12 | def helpers 13 | Draper::ViewContext.current 14 | end 15 | 16 | alias :h :helpers 17 | end 18 | 19 | # Access the helpers proxy to call built-in and user-defined 20 | # Rails helpers. Aliased to `h` for convenience. 21 | # 22 | # @return [HelperProxy] the helpers proxy 23 | def helpers 24 | Draper::ViewContext.current 25 | end 26 | 27 | alias :h :helpers 28 | 29 | # Alias for `helpers.localize`, since localize is something that's used 30 | # quite often. Further aliased to `l` for convenience. 31 | ruby2_keywords def localize(*args) 32 | helpers.localize(*args) 33 | end 34 | 35 | alias :l :localize 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/generators/controller_override.rb: -------------------------------------------------------------------------------- 1 | require "rails/generators" 2 | require "rails/generators/rails/controller/controller_generator" 3 | require "rails/generators/rails/scaffold_controller/scaffold_controller_generator" 4 | 5 | module Rails 6 | module Generators 7 | class ControllerGenerator 8 | hook_for :decorator, type: :boolean, default: true do |generator| 9 | invoke generator, [name.singularize] 10 | end 11 | end 12 | 13 | class ScaffoldControllerGenerator 14 | hook_for :decorator, type: :boolean, default: true 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/draper/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Draper 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | source_root File.expand_path("templates", __dir__) 5 | 6 | desc 'Creates an ApplicationDecorator, if none exists.' 7 | 8 | def create_application_decorator 9 | file = 'application_decorator.rb' 10 | copy_file file, "app/decorators/#{file}" 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/generators/draper/templates/application_decorator.rb: -------------------------------------------------------------------------------- 1 | class ApplicationDecorator < Draper::Decorator 2 | # Define methods for all decorated objects. 3 | # Helpers are accessed through `helpers` (aka `h`). For example: 4 | # 5 | # def percent_amount 6 | # h.number_to_percentage object.amount, precision: 2 7 | # end 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/mini_test/decorator_generator.rb: -------------------------------------------------------------------------------- 1 | require 'generators/mini_test' 2 | 3 | module MiniTest 4 | module Generators 5 | class DecoratorGenerator < Base 6 | def self.source_root 7 | File.expand_path("templates", __dir__) 8 | end 9 | 10 | class_option :spec, type: :boolean, default: false, desc: "Use MiniTest::Spec DSL" 11 | 12 | check_class_collision suffix: "DecoratorTest" 13 | 14 | def create_test_file 15 | template_type = options[:spec] ? "spec" : "test" 16 | template "decorator_#{template_type}.rb", File.join("test/decorators", class_path, "#{singular_name}_decorator_test.rb") 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/generators/mini_test/templates/decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe <%= class_name %>Decorator do 4 | end 5 | -------------------------------------------------------------------------------- /lib/generators/mini_test/templates/decorator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class <%= class_name %>DecoratorTest < Draper::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /lib/generators/rails/decorator_generator.rb: -------------------------------------------------------------------------------- 1 | module Rails 2 | module Generators 3 | class DecoratorGenerator < NamedBase 4 | source_root File.expand_path("templates", __dir__) 5 | check_class_collision suffix: "Decorator" 6 | 7 | class_option :parent, type: :string, desc: "The parent class for the generated decorator" 8 | 9 | def create_decorator_file 10 | template 'decorator.rb', File.join('app/decorators', class_path, "#{file_name}_decorator.rb") 11 | end 12 | 13 | hook_for :test_framework 14 | 15 | private 16 | 17 | def parent_class_name 18 | options.fetch("parent") do 19 | begin 20 | require 'application_decorator' 21 | ApplicationDecorator 22 | rescue LoadError 23 | "Draper::Decorator" 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/generators/rails/templates/decorator.rb: -------------------------------------------------------------------------------- 1 | <%- module_namespacing do -%> 2 | <%- if parent_class_name.present? -%> 3 | class <%= class_name %>Decorator < <%= parent_class_name %> 4 | <%- else -%> 5 | class <%= class_name %> 6 | <%- end -%> 7 | delegate_all 8 | 9 | # Define presentation-specific methods here. Helpers are accessed through 10 | # `helpers` (aka `h`). You can override attributes, for example: 11 | # 12 | # def created_at 13 | # helpers.content_tag :span, class: 'time' do 14 | # object.created_at.strftime("%a %m/%d/%y") 15 | # end 16 | # end 17 | 18 | end 19 | <% end -%> 20 | -------------------------------------------------------------------------------- /lib/generators/rspec/decorator_generator.rb: -------------------------------------------------------------------------------- 1 | module Rspec 2 | module Generators 3 | class DecoratorGenerator < ::Rails::Generators::NamedBase 4 | source_root File.expand_path("templates", __dir__) 5 | 6 | def create_spec_file 7 | template 'decorator_spec.rb', File.join('spec/decorators', class_path, "#{singular_name}_decorator_spec.rb") 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/rspec/templates/decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe <%= class_name %>Decorator do 4 | end 5 | -------------------------------------------------------------------------------- /lib/generators/test_unit/decorator_generator.rb: -------------------------------------------------------------------------------- 1 | module TestUnit 2 | module Generators 3 | class DecoratorGenerator < ::Rails::Generators::NamedBase 4 | source_root File.expand_path("templates", __dir__) 5 | check_class_collision suffix: "DecoratorTest" 6 | 7 | def create_test_file 8 | template 'decorator_test.rb', File.join('test/decorators', class_path, "#{singular_name}_decorator_test.rb") 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/test_unit/templates/decorator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class <%= class_name %>DecoratorTest < Draper::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /spec/draper/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Draper 4 | RSpec.describe Configuration do 5 | it 'yields Draper on configure' do 6 | Draper.configure { |config| expect(config).to be Draper } 7 | end 8 | 9 | describe '#default_controller' do 10 | it 'defaults default_controller to ApplicationController' do 11 | expect(Draper.default_controller).to be ApplicationController 12 | end 13 | 14 | it 'allows customizing default_controller through configure' do 15 | default = Draper.default_controller 16 | 17 | Draper.configure do |config| 18 | config.default_controller = CustomController 19 | end 20 | 21 | expect(Draper.default_controller).to be CustomController 22 | 23 | Draper.default_controller = default 24 | end 25 | end 26 | 27 | describe '#default_query_methods_strategy' do 28 | let!(:default) { Draper.default_query_methods_strategy } 29 | 30 | subject { Draper.default_query_methods_strategy } 31 | 32 | context 'when there is no custom strategy' do 33 | it { is_expected.to eq(:active_record) } 34 | end 35 | 36 | context 'when using a custom strategy' do 37 | before do 38 | Draper.configure do |config| 39 | config.default_query_methods_strategy = :mongoid 40 | end 41 | end 42 | 43 | after { Draper.default_query_methods_strategy = default } 44 | 45 | it { is_expected.to eq(:mongoid) } 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/draper/decoratable/equality_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'support/shared_examples/decoratable_equality' 3 | 4 | module Draper 5 | describe Decoratable::Equality do 6 | describe "#==" do 7 | it_behaves_like "decoration-aware #==", Object.new.extend(Decoratable::Equality) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/draper/decoratable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'support/shared_examples/decoratable_equality' 3 | 4 | module Draper 5 | describe Decoratable do 6 | describe "#decorate" do 7 | it "returns a decorator for self" do 8 | product = Product.new 9 | decorator = product.decorate 10 | 11 | expect(decorator).to be_a ProductDecorator 12 | expect(decorator.object).to be product 13 | end 14 | 15 | it "accepts context" do 16 | context = {some: "context"} 17 | decorator = Product.new.decorate(context: context) 18 | 19 | expect(decorator.context).to be context 20 | end 21 | 22 | it "uses the #decorator_class" do 23 | product = Product.new 24 | allow(product).to receive_messages decorator_class: OtherDecorator 25 | 26 | expect(product.decorate).to be_an_instance_of OtherDecorator 27 | end 28 | end 29 | 30 | describe "#applied_decorators" do 31 | it "returns an empty list" do 32 | expect(Product.new.applied_decorators).to eq [] 33 | end 34 | end 35 | 36 | describe "#decorated_with?" do 37 | it "returns false" do 38 | expect(Product.new).not_to be_decorated_with Decorator 39 | end 40 | end 41 | 42 | describe "#decorated?" do 43 | it "returns false" do 44 | expect(Product.new).not_to be_decorated 45 | end 46 | end 47 | 48 | describe "#decorator_class?" do 49 | it "returns true for decoratable model" do 50 | expect(Product.new.decorator_class?).to be_truthy 51 | end 52 | 53 | it "returns false for non-decoratable model" do 54 | expect(Model.new.decorator_class?).to be_falsey 55 | end 56 | end 57 | 58 | describe ".decorator_class?" do 59 | it "returns true for decoratable model" do 60 | expect(Product.decorator_class?).to be_truthy 61 | end 62 | 63 | it "returns false for non-decoratable model" do 64 | expect(Model.decorator_class?).to be_falsey 65 | end 66 | end 67 | 68 | describe "#decorator_class" do 69 | it "delegates to .decorator_class" do 70 | product = Product.new 71 | 72 | expect(Product).to receive(:decorator_class).and_return(:some_decorator) 73 | expect(product.decorator_class).to be :some_decorator 74 | end 75 | 76 | it "specifies the class that #decorator_class was first called on (superclass)" do 77 | person = Person.new 78 | expect { person.decorator_class }.to raise_error(Draper::UninferrableDecoratorError, 'Could not infer a decorator for Person.') 79 | end 80 | 81 | it "specifies the class that #decorator_class was first called on (subclass)" do 82 | child = Child.new 83 | expect { child.decorator_class }.to raise_error(Draper::UninferrableDecoratorError, 'Could not infer a decorator for Child.') 84 | end 85 | end 86 | 87 | describe "#==" do 88 | it_behaves_like "decoration-aware #==", Product.new 89 | end 90 | 91 | describe "#===" do 92 | it "is true when #== is true" do 93 | product = Product.new 94 | 95 | expect(product).to receive(:==).and_return(true) 96 | expect(product === :anything).to be_truthy 97 | end 98 | 99 | it "is false when #== is false" do 100 | product = Product.new 101 | 102 | expect(product).to receive(:==).and_return(false) 103 | expect(product === :anything).to be_falsey 104 | end 105 | end 106 | 107 | describe ".====" do 108 | it "is true for an instance" do 109 | expect(Product === Product.new).to be_truthy 110 | end 111 | 112 | it "is true for a derived instance" do 113 | expect(Product === Class.new(Product).new).to be_truthy 114 | end 115 | 116 | it "is false for an unrelated instance" do 117 | expect(Product === Model.new).to be_falsey 118 | end 119 | 120 | it "is true for a decorated instance" do 121 | decorator = Product.new.decorate 122 | 123 | expect(Product === decorator).to be_truthy 124 | end 125 | 126 | it "is true for a decorated derived instance" do 127 | decorator = Class.new(Product).new.decorate 128 | 129 | expect(Product === decorator).to be_truthy 130 | end 131 | 132 | it "is false for a decorated unrelated instance" do 133 | decorator = Other.new.decorate 134 | 135 | expect(Product === decorator).to be_falsey 136 | end 137 | 138 | it "is false for a non-decorator which happens to respond to object" do 139 | decorator = double(object: Product.new) 140 | 141 | expect(Product === decorator).to be_falsey 142 | end 143 | end 144 | 145 | describe ".decorate" do 146 | it "calls #decorate_collection on .decorator_class" do 147 | scoped = [Product.new] 148 | allow(Product).to receive(:all).and_return(scoped) 149 | 150 | expect(Product.decorator_class).to receive(:decorate_collection).with(scoped, {with: nil}).and_return(:decorated_collection) 151 | expect(Product.decorate).to be :decorated_collection 152 | end 153 | 154 | it "accepts options" do 155 | options = {with: ProductDecorator, context: {some: "context"}} 156 | allow(Product).to receive(:all).and_return([]) 157 | 158 | expect(Product.decorator_class).to receive(:decorate_collection).with([], options) 159 | Product.decorate(options) 160 | end 161 | end 162 | 163 | describe ".decorator_class" do 164 | context "for classes" do 165 | it "infers the decorator from the class" do 166 | expect(Product.decorator_class).to be ProductDecorator 167 | end 168 | 169 | context "without a decorator on its own" do 170 | it "infers the decorator from a superclass" do 171 | expect(SpecialProduct.decorator_class).to be ProductDecorator 172 | end 173 | end 174 | end 175 | 176 | context "for ActiveModel classes" do 177 | it "infers the decorator from the model name" do 178 | allow(Product).to receive(:model_name){"Other"} 179 | 180 | expect(Product.decorator_class).to be OtherDecorator 181 | end 182 | end 183 | 184 | context "in a namespace" do 185 | context "for classes" do 186 | it "infers the decorator from the class" do 187 | expect(Namespaced::Product.decorator_class).to be Namespaced::ProductDecorator 188 | end 189 | end 190 | 191 | context "for ActiveModel classes" do 192 | it "infers the decorator from the model name" do 193 | allow(Namespaced::Product).to receive(:model_name).and_return("Namespaced::Other") 194 | 195 | expect(Namespaced::Product.decorator_class).to be Namespaced::OtherDecorator 196 | end 197 | end 198 | end 199 | 200 | context "when the decorator contains name error" do 201 | it "throws an NameError" do 202 | # We imitate ActiveSupport::Autoload behavior here in order to cause lazy NameError exception raising 203 | allow_any_instance_of(Module).to receive(:const_missing) { Class.new { any_nonexisting_method_name } } 204 | 205 | expect{Model.decorator_class}.to raise_error { |error| expect(error).to be_an_instance_of(NameError) } 206 | end 207 | end 208 | 209 | context "when the decorator can't be inferred" do 210 | it "throws an UninferrableDecoratorError" do 211 | expect{Model.decorator_class}.to raise_error UninferrableDecoratorError 212 | end 213 | end 214 | 215 | context "when an unrelated NameError is thrown" do 216 | it "re-raises that error" do 217 | # Not related to safe_constantize behavior, we just want to raise a NameError inside the function 218 | allow_any_instance_of(String).to receive(:safe_constantize) { Draper::Base } 219 | expect{Product.decorator_class}.to raise_error NameError, /Draper::Base/ 220 | end 221 | end 222 | 223 | context "when an anonymous class is given" do 224 | it "infers the decorator from a superclass" do 225 | anonymous_class = Class.new(Product) do 226 | def self.name 227 | to_s 228 | end 229 | end 230 | expect(anonymous_class.decorator_class).to be ProductDecorator 231 | end 232 | end 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /spec/draper/decorated_association_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Draper 4 | describe DecoratedAssociation do 5 | describe "#initialize" do 6 | it "accepts valid options" do 7 | valid_options = {with: Decorator, scope: :foo, context: {}} 8 | expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, valid_options)}.not_to raise_error 9 | end 10 | 11 | it "rejects invalid options" do 12 | expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, foo: "bar")}.to raise_error ArgumentError, /Unknown key/ 13 | end 14 | 15 | it "creates a factory" do 16 | options = {with: Decorator, context: {foo: "bar"}} 17 | 18 | expect(Factory).to receive(:new).with(options) 19 | DecoratedAssociation.new(double, :association, options) 20 | end 21 | 22 | describe ":with option" do 23 | it "defaults to nil" do 24 | expect(Factory).to receive(:new).with(with: nil, context: anything()) 25 | DecoratedAssociation.new(double, :association, {}) 26 | end 27 | end 28 | 29 | describe ":context option" do 30 | it "defaults to the identity function" do 31 | expect(Factory).to receive(:new) do |options| 32 | options[:context].call(:anything) == :anything 33 | end 34 | DecoratedAssociation.new(double, :association, {}) 35 | end 36 | end 37 | end 38 | 39 | describe "#call" do 40 | it "calls the factory" do 41 | factory = double 42 | allow(Factory).to receive_messages(new: factory) 43 | associated = double 44 | owner_context = {foo: "bar"} 45 | object = double(association: associated) 46 | owner = double(object: object, context: owner_context) 47 | decorated_association = DecoratedAssociation.new(owner, :association, {}) 48 | decorated = double 49 | 50 | expect(factory).to receive(:decorate).with(associated, context_args: owner_context).and_return(decorated) 51 | expect(decorated_association.call).to be decorated 52 | end 53 | 54 | it "memoizes" do 55 | factory = double 56 | allow(Factory).to receive_messages(new: factory) 57 | owner = double(object: double(association: double), context: {}) 58 | decorated_association = DecoratedAssociation.new(owner, :association, {}) 59 | decorated = double 60 | 61 | expect(factory).to receive(:decorate).once.and_return(decorated) 62 | expect(decorated_association.call).to be decorated 63 | expect(decorated_association.call).to be decorated 64 | end 65 | 66 | context "when the :scope option was given" do 67 | it "applies the scope before decoration" do 68 | factory = double 69 | allow(Factory).to receive_messages(new: factory) 70 | scoped = double 71 | object = double(association: double(applied_scope: scoped)) 72 | owner = double(object: object, context: {}) 73 | decorated_association = DecoratedAssociation.new(owner, :association, scope: :applied_scope) 74 | decorated = double 75 | 76 | expect(factory).to receive(:decorate).with(scoped, anything()).and_return(decorated) 77 | expect(decorated_association.call).to be decorated 78 | end 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/draper/decorates_assigned_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Draper 4 | describe DecoratesAssigned do 5 | let(:controller_class) do 6 | Class.new do 7 | extend DecoratesAssigned 8 | 9 | def self.helper_method(method) 10 | helper_methods << method 11 | end 12 | 13 | def self.helper_methods 14 | @helper_methods ||= [] 15 | end 16 | end 17 | end 18 | 19 | describe ".decorates_assigned" do 20 | it "adds helper methods" do 21 | controller_class.decorates_assigned :article, :author 22 | 23 | expect(controller_class.instance_methods).to include :article 24 | expect(controller_class.instance_methods).to include :author 25 | 26 | expect(controller_class.helper_methods).to include :article 27 | expect(controller_class.helper_methods).to include :author 28 | end 29 | 30 | it "creates a factory" do 31 | allow(Factory).to receive(:new).once 32 | controller_class.decorates_assigned :article, :author 33 | end 34 | 35 | it "passes options to the factory" do 36 | options = {foo: "bar"} 37 | 38 | allow(Factory).to receive(:new).with(options) 39 | controller_class.decorates_assigned :article, :author, options 40 | end 41 | 42 | describe "the generated method" do 43 | it "decorates the instance variable" do 44 | object = double 45 | factory = double 46 | allow(Factory).to receive_messages(new: factory) 47 | 48 | controller_class.decorates_assigned :article 49 | controller = controller_class.new 50 | controller.instance_variable_set "@article", object 51 | 52 | expect(factory).to receive(:decorate).with(object, context_args: controller).and_return(:decorated) 53 | expect(controller.article).to be :decorated 54 | end 55 | 56 | it "memoizes" do 57 | factory = double 58 | allow(Factory).to receive_messages(new: factory) 59 | 60 | controller_class.decorates_assigned :article 61 | controller = controller_class.new 62 | 63 | expect(factory).to receive(:decorate).once 64 | controller.article 65 | controller.article 66 | end 67 | end 68 | end 69 | 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/draper/draper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'support/shared_examples/view_helpers' 3 | SimpleCov.command_name 'test:unit' 4 | 5 | module Draper 6 | describe Draper do 7 | describe '.setup_action_controller' do 8 | it 'includes api only compatability if base is ActionController::API' do 9 | base = ActionController::API 10 | 11 | Draper.setup_action_controller(base) 12 | 13 | expect(base.included_modules).to include(Draper::Compatibility::ApiOnly) 14 | end 15 | 16 | it 'does not include api only compatibility if base ActionController::Base' do 17 | base = ActionController::Base 18 | 19 | Draper.setup_action_controller(base) 20 | 21 | expect(base.included_modules).not_to include(Draper::Compatibility::ApiOnly) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/draper/factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Draper 4 | describe Factory do 5 | describe "#initialize" do 6 | it "accepts valid options" do 7 | valid_options = {with: Decorator, context: {foo: "bar"}} 8 | expect{Factory.new(valid_options)}.not_to raise_error 9 | end 10 | 11 | it "rejects invalid options" do 12 | expect{Factory.new(foo: "bar")}.to raise_error ArgumentError, /Unknown key/ 13 | end 14 | end 15 | 16 | describe "#decorate" do 17 | context "when object is nil" do 18 | it "returns nil" do 19 | factory = Factory.new 20 | 21 | expect(factory.decorate(nil)).to be_nil 22 | end 23 | end 24 | 25 | it "calls a worker" do 26 | factory = Factory.new 27 | worker = ->(*){ :decorated } 28 | 29 | expect(Factory::Worker).to receive(:new).and_return(worker) 30 | expect(factory.decorate(double)).to be :decorated 31 | end 32 | 33 | it "passes the object to the worker" do 34 | factory = Factory.new 35 | object = double 36 | 37 | expect(Factory::Worker).to receive(:new).with(anything(), object).and_return(->(*){}) 38 | factory.decorate(object) 39 | end 40 | 41 | context "when the :with option was given" do 42 | it "passes the decorator class to the worker" do 43 | decorator_class = double 44 | factory = Factory.new(with: decorator_class) 45 | 46 | expect(Factory::Worker).to receive(:new).with(decorator_class, anything()).and_return(->(*){}) 47 | factory.decorate(double) 48 | end 49 | end 50 | 51 | context "when the :with option was omitted" do 52 | it "passes nil to the worker" do 53 | factory = Factory.new 54 | 55 | expect(Factory::Worker).to receive(:new).with(nil, anything()).and_return(->(*){}) 56 | factory.decorate(double) 57 | end 58 | end 59 | 60 | it "passes options to the call" do 61 | factory = Factory.new 62 | worker = ->(*){} 63 | allow(Factory::Worker).to receive(:new).and_return(worker) 64 | options = {foo: "bar"} 65 | 66 | allow(worker).to receive(:call).with(options) 67 | factory.decorate(double, options) 68 | end 69 | 70 | context "when the :context option was given" do 71 | it "sets the passed context" do 72 | factory = Factory.new(context: {foo: "bar"}) 73 | worker = ->(*){} 74 | allow(Factory::Worker).to receive_messages new: worker 75 | 76 | expect(worker).to receive(:call).with({baz: "qux", context: {foo: "bar"}}) 77 | factory.decorate(double, {baz: "qux"}) 78 | end 79 | 80 | it "is overridden by explicitly-specified context" do 81 | factory = Factory.new({context: {foo: "bar"}}) 82 | worker = ->(*){} 83 | allow(Factory::Worker).to receive_messages new: worker 84 | 85 | expect(worker).to receive(:call).with({context: {baz: "qux"}}) 86 | factory.decorate(double, {context: {baz: "qux"}}) 87 | end 88 | end 89 | end 90 | end 91 | 92 | describe Factory::Worker do 93 | describe "#call" do 94 | it "calls the decorator method" do 95 | object = double 96 | options = {foo: "bar"} 97 | worker = Factory::Worker.new(double, object) 98 | decorator = ->(*){} 99 | allow(worker).to receive(:decorator){ decorator } 100 | 101 | allow(decorator).to receive(:call).with(object, options).and_return(:decorated) 102 | expect(worker.call(options)).to be :decorated 103 | end 104 | 105 | context "when the :context option is callable" do 106 | it "calls it" do 107 | worker = Factory::Worker.new(double, double) 108 | decorator = ->(*){} 109 | allow(worker).to receive_messages decorator: decorator 110 | context = {foo: "bar"} 111 | 112 | expect(decorator).to receive(:call).with(anything(), {context: context}) 113 | worker.call(context: ->{ context }) 114 | end 115 | 116 | it "receives arguments from the :context_args option" do 117 | worker = Factory::Worker.new(double, double) 118 | allow(worker).to receive_messages decorator: ->(*){} 119 | context = ->{} 120 | 121 | expect(context).to receive(:call).with(:foo, :bar) 122 | worker.call(context: context, context_args: [:foo, :bar]) 123 | end 124 | 125 | it "wraps non-arrays passed to :context_args" do 126 | worker = Factory::Worker.new(double, double) 127 | allow(worker).to receive_messages decorator: ->(*){} 128 | context = ->{} 129 | hash = {foo: "bar"} 130 | 131 | expect(context).to receive(:call).with(hash) 132 | worker.call(context: context, context_args: hash) 133 | end 134 | end 135 | 136 | context "when the :context option is not callable" do 137 | it "doesn't call it" do 138 | worker = Factory::Worker.new(double, double) 139 | decorator = ->(*){} 140 | allow(worker).to receive_messages decorator: decorator 141 | context = {foo: "bar"} 142 | 143 | expect(decorator).to receive(:call).with(anything(), {context: context}) 144 | worker.call(context: context) 145 | end 146 | end 147 | 148 | it "does not pass the :context_args option to the decorator" do 149 | worker = Factory::Worker.new(double, double) 150 | decorator = ->(*){} 151 | allow(worker).to receive_messages decorator: decorator 152 | 153 | expect(decorator).to receive(:call).with(anything(), {foo: "bar"}) 154 | worker.call(foo: "bar", context_args: []) 155 | end 156 | end 157 | 158 | describe "#decorator" do 159 | context "for a singular object" do 160 | context "when decorator_class is specified" do 161 | it "returns the .decorate method from the decorator" do 162 | decorator_class = Class.new(Decorator) 163 | worker = Factory::Worker.new(decorator_class, double) 164 | 165 | expect(worker.decorator).to eq decorator_class.method(:decorate) 166 | end 167 | end 168 | 169 | context "when decorator_class is unspecified" do 170 | context "and the object is decoratable" do 171 | it "returns the object's #decorate method" do 172 | object = double 173 | options = {foo: "bar"} 174 | worker = Factory::Worker.new(nil, object) 175 | 176 | expect(object).to receive(:decorate).with(options).and_return(:decorated) 177 | expect(worker.decorator.call(object, options)).to be :decorated 178 | end 179 | end 180 | 181 | context "and the object is not decoratable" do 182 | it "raises an error" do 183 | object = double 184 | worker = Factory::Worker.new(nil, object) 185 | 186 | expect{worker.decorator}.to raise_error UninferrableDecoratorError 187 | end 188 | end 189 | end 190 | 191 | context "when the object is a struct" do 192 | it "returns a singular decorator" do 193 | object = Struct.new(:stuff).new("things") 194 | 195 | decorator_class = Class.new(Decorator) 196 | worker = Factory::Worker.new(decorator_class, object) 197 | 198 | expect(worker.decorator).to eq decorator_class.method(:decorate) 199 | end 200 | end 201 | end 202 | 203 | context "for a collection object" do 204 | context "when decorator_class is a CollectionDecorator" do 205 | it "returns the .decorate method from the collection decorator" do 206 | decorator_class = Class.new(CollectionDecorator) 207 | worker = Factory::Worker.new(decorator_class, []) 208 | 209 | expect(worker.decorator).to eq decorator_class.method(:decorate) 210 | end 211 | end 212 | 213 | context "when decorator_class is a Decorator" do 214 | it "returns the .decorate_collection method from the decorator" do 215 | decorator_class = Class.new(Decorator) 216 | worker = Factory::Worker.new(decorator_class, []) 217 | 218 | expect(worker.decorator).to eq decorator_class.method(:decorate_collection) 219 | end 220 | end 221 | 222 | context "when decorator_class is unspecified" do 223 | context "and the object is decoratable" do 224 | it "returns the .decorate_collection method from the object's decorator" do 225 | object = [] 226 | decorator_class = Class.new(Decorator) 227 | allow(object).to receive(:decorator_class){ decorator_class } 228 | allow(object).to receive(:decorate){ nil } 229 | worker = Factory::Worker.new(nil, object) 230 | 231 | expect(decorator_class).to receive(:decorate_collection).with(object, {foo: "bar", with: nil}).and_return(:decorated) 232 | expect(worker.decorator.call(object, foo: "bar")).to be :decorated 233 | end 234 | end 235 | 236 | context "and the object is not decoratable" do 237 | it "returns the .decorate method from CollectionDecorator" do 238 | worker = Factory::Worker.new(nil, []) 239 | 240 | expect(worker.decorator).to eq CollectionDecorator.method(:decorate) 241 | end 242 | end 243 | end 244 | end 245 | end 246 | end 247 | end 248 | -------------------------------------------------------------------------------- /spec/draper/finders_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Draper 4 | describe Finders do 5 | protect_class ProductDecorator 6 | before { ProductDecorator.decorates_finders } 7 | 8 | describe ".find" do 9 | it "proxies to the model class" do 10 | expect(Product).to receive(:find).with(1) 11 | ProductDecorator.find(1) 12 | end 13 | 14 | it "decorates the result" do 15 | found = Product.new 16 | allow(Product).to receive(:find).and_return(found) 17 | decorator = ProductDecorator.find(1) 18 | expect(decorator).to be_a ProductDecorator 19 | expect(decorator.object).to be found 20 | end 21 | 22 | it "passes context to the decorator" do 23 | allow(Product).to receive(:find) 24 | context = {some: "context"} 25 | decorator = ProductDecorator.find(1, context: context) 26 | 27 | expect(decorator.context).to be context 28 | end 29 | end 30 | 31 | describe ".find_by_(x)" do 32 | it "proxies to the model class" do 33 | expect(Product).to receive(:find_by_name).with("apples") 34 | ProductDecorator.find_by_name("apples") 35 | end 36 | 37 | it "decorates the result" do 38 | found = Product.new 39 | allow(Product).to receive(:find_by_name).and_return(found) 40 | decorator = ProductDecorator.find_by_name("apples") 41 | expect(decorator).to be_a ProductDecorator 42 | expect(decorator.object).to be found 43 | end 44 | 45 | it "proxies complex ProductDecorators" do 46 | expect(Product).to receive(:find_by_name_and_size).with("apples", "large") 47 | ProductDecorator.find_by_name_and_size("apples", "large") 48 | end 49 | 50 | it "proxies find_last_by_(x) ProductDecorators" do 51 | expect(Product).to receive(:find_last_by_name_and_size).with("apples", "large") 52 | ProductDecorator.find_last_by_name_and_size("apples", "large") 53 | end 54 | 55 | it "proxies find_or_initialize_by_(x) ProductDecorators" do 56 | expect(Product).to receive(:find_or_initialize_by_name_and_size).with("apples", "large") 57 | ProductDecorator.find_or_initialize_by_name_and_size("apples", "large") 58 | end 59 | 60 | it "proxies find_or_create_by_(x) ProductDecorators" do 61 | expect(Product).to receive(:find_or_create_by_name_and_size).with("apples", "large") 62 | ProductDecorator.find_or_create_by_name_and_size("apples", "large") 63 | end 64 | 65 | it "passes context to the decorator" do 66 | allow(Product).to receive(:find_by_name_and_size) 67 | context = {some: "context"} 68 | decorator = ProductDecorator.find_by_name_and_size("apples", "large", context: context) 69 | 70 | expect(decorator.context).to be context 71 | end 72 | end 73 | 74 | describe ".find_all_by_" do 75 | it "proxies to the model class" do 76 | expect(Product).to receive(:find_all_by_name_and_size).with("apples", "large").and_return([]) 77 | ProductDecorator.find_all_by_name_and_size("apples", "large") 78 | end 79 | 80 | it "decorates the result" do 81 | found = [Product.new, Product.new] 82 | allow(Product).to receive(:find_all_by_name).and_return(found) 83 | decorator = ProductDecorator.find_all_by_name("apples") 84 | 85 | expect(decorator).to be_a Draper::CollectionDecorator 86 | expect(decorator.decorator_class).to be ProductDecorator 87 | expect(decorator).to eq found 88 | end 89 | 90 | it "passes context to the decorator" do 91 | allow(Product).to receive(:find_all_by_name) 92 | context = {some: "context"} 93 | decorator = ProductDecorator.find_all_by_name("apples", context: context) 94 | 95 | expect(decorator.context).to be context 96 | end 97 | end 98 | 99 | describe ".all" do 100 | it "returns a decorated collection" do 101 | found = [Product.new, Product.new] 102 | allow(Product).to receive_messages all: found 103 | decorator = ProductDecorator.all 104 | 105 | expect(decorator).to be_a Draper::CollectionDecorator 106 | expect(decorator.decorator_class).to be ProductDecorator 107 | expect(decorator).to eq found 108 | end 109 | 110 | it "passes context to the decorator" do 111 | allow(Product).to receive(:all) 112 | context = {some: "context"} 113 | decorator = ProductDecorator.all(context: context) 114 | 115 | expect(decorator.context).to be context 116 | end 117 | end 118 | 119 | describe ".first" do 120 | it "proxies to the model class" do 121 | expect(Product).to receive(:first) 122 | ProductDecorator.first 123 | end 124 | 125 | it "decorates the result" do 126 | first = Product.new 127 | allow(Product).to receive(:first).and_return(first) 128 | decorator = ProductDecorator.first 129 | expect(decorator).to be_a ProductDecorator 130 | expect(decorator.object).to be first 131 | end 132 | 133 | it "passes context to the decorator" do 134 | allow(Product).to receive(:first) 135 | context = {some: "context"} 136 | decorator = ProductDecorator.first(context: context) 137 | 138 | expect(decorator.context).to be context 139 | end 140 | end 141 | 142 | describe ".last" do 143 | it "proxies to the model class" do 144 | expect(Product).to receive(:last) 145 | ProductDecorator.last 146 | end 147 | 148 | it "decorates the result" do 149 | last = Product.new 150 | allow(Product).to receive(:last).and_return(last) 151 | decorator = ProductDecorator.last 152 | expect(decorator).to be_a ProductDecorator 153 | expect(decorator.object).to be last 154 | end 155 | 156 | it "passes context to the decorator" do 157 | allow(Product).to receive(:last) 158 | context = {some: "context"} 159 | decorator = ProductDecorator.last(context: context) 160 | 161 | expect(decorator.context).to be context 162 | end 163 | end 164 | 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /spec/draper/helper_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Draper 4 | describe HelperProxy do 5 | describe "#initialize" do 6 | it "sets the view context" do 7 | view_context = double 8 | helper_proxy = HelperProxy.new(view_context) 9 | 10 | expect(helper_proxy.send(:view_context)).to be view_context 11 | end 12 | end 13 | 14 | describe "#method_missing" do 15 | protect_class HelperProxy 16 | 17 | it "proxies methods to the view context" do 18 | view_context = double 19 | helper_proxy = HelperProxy.new(view_context) 20 | 21 | allow(view_context).to receive(:foo) { |arg| arg } 22 | expect(helper_proxy.foo(:passed)).to be :passed 23 | end 24 | 25 | it "passes blocks" do 26 | view_context = double 27 | helper_proxy = HelperProxy.new(view_context) 28 | 29 | allow(view_context).to receive(:foo) { |&block| block.call } 30 | expect(helper_proxy.foo{:yielded}).to be :yielded 31 | end 32 | 33 | it "defines the method for better performance" do 34 | helper_proxy = HelperProxy.new(double(foo: "bar")) 35 | 36 | expect(HelperProxy.instance_methods).not_to include :foo 37 | helper_proxy.foo 38 | expect(HelperProxy.instance_methods).to include :foo 39 | end 40 | end 41 | 42 | describe "#respond_to_missing?" do 43 | it "allows #method to be called on the view context" do 44 | helper_proxy = HelperProxy.new(double(foo: "bar")) 45 | 46 | expect(helper_proxy.respond_to?(:foo)).to be_truthy 47 | end 48 | end 49 | 50 | describe "proxying methods which are overriding" do 51 | it "proxies :capture" do 52 | view_context = double 53 | helper_proxy = HelperProxy.new(view_context) 54 | 55 | allow(view_context).to receive(:capture) { |*args, &block| [*args, block.call] } 56 | expect(helper_proxy.capture(:first_arg, :second_arg){:yielded}).to \ 57 | be_eql [:first_arg, :second_arg, :yielded] 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/draper/lazy_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Draper 4 | describe LazyHelpers do 5 | describe "#method_missing" do 6 | let(:decorator) do 7 | Struct.new(:helpers){include Draper::LazyHelpers}.new(double) 8 | end 9 | 10 | it "proxies methods to #helpers" do 11 | allow(decorator.helpers).to receive(:foo) { |arg| arg } 12 | expect(decorator.foo(:passed)).to be :passed 13 | end 14 | 15 | it "passes blocks" do 16 | allow(decorator.helpers).to receive(:foo) { |&block| block.call } 17 | expect(decorator.foo{:yielded}).to be :yielded 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/draper/query_methods/load_strategy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'active_record' 3 | 4 | module Draper 5 | module QueryMethods 6 | describe LoadStrategy do 7 | describe '#new' do 8 | subject { described_class.new(:active_record) } 9 | 10 | it { is_expected.to be_an_instance_of(LoadStrategy::ActiveRecord) } 11 | end 12 | end 13 | 14 | describe LoadStrategy::ActiveRecord do 15 | describe '#allowed?' do 16 | it 'checks whether or not ActiveRecord::Relation::VALUE_METHODS has the given method' do 17 | allow(::ActiveRecord::Relation::VALUE_METHODS).to receive(:include?) 18 | 19 | described_class.new.allowed? :foo 20 | 21 | expect(::ActiveRecord::Relation::VALUE_METHODS).to have_received(:include?).with(:foo) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/draper/query_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../dummy/app/decorators/post_decorator' 3 | 4 | Post = Struct.new(:id) { } 5 | 6 | module Draper 7 | describe QueryMethods do 8 | let(:fake_strategy) { instance_double(QueryMethods::LoadStrategy::ActiveRecord) } 9 | 10 | before { allow(QueryMethods::LoadStrategy).to receive(:new).and_return(fake_strategy) } 11 | 12 | describe '#method_missing' do 13 | let(:collection) { [ Post.new, Post.new ] } 14 | let(:collection_context) { { user: 'foo' } } 15 | let(:collection_decorator) { PostDecorator.decorate_collection(collection, context: collection_context) } 16 | 17 | context 'when strategy allows collection to call the method' do 18 | let(:results) { spy(:results) } 19 | 20 | before do 21 | allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(true) 22 | allow(collection).to receive(:send).with(:some_query_method).and_return(results) 23 | end 24 | 25 | it 'calls the method on the collection and decorate it results' do 26 | collection_decorator.some_query_method 27 | 28 | expect(results).to have_received(:decorate) 29 | end 30 | 31 | it 'calls the method on the collection and keeps the decoration options' do 32 | collection_decorator.some_query_method 33 | 34 | expect(results).to have_received(:decorate).with({ context: collection_context, with: PostDecorator }) 35 | end 36 | end 37 | 38 | context 'when strategy does not allow collection to call the method' do 39 | before { allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(false) } 40 | 41 | it 'raises NoMethodError' do 42 | expect { collection_decorator.some_query_method }.to raise_exception(NoMethodError) 43 | end 44 | end 45 | end 46 | 47 | describe "#respond_to?" do 48 | let(:collection) { [ Post.new, Post.new ] } 49 | let(:collection_decorator) { PostDecorator.decorate_collection(collection) } 50 | 51 | subject { collection_decorator.respond_to?(:some_query_method) } 52 | 53 | context 'when strategy allows collection to call the method' do 54 | before do 55 | allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(true) 56 | allow(collection).to receive(:respond_to?).with(:some_query_method).and_return(true) 57 | end 58 | 59 | it { is_expected.to eq(true) } 60 | 61 | context 'and collection does not implement the method' do 62 | before do 63 | allow(collection).to receive(:respond_to?).with(:some_query_method).and_return(false) 64 | end 65 | 66 | it { is_expected.to eq(false) } 67 | end 68 | end 69 | 70 | context 'when strategy does not allow collection to call the method' do 71 | before do 72 | allow(fake_strategy).to receive(:allowed?).with(:some_query_method).and_return(false) 73 | allow(collection).to receive(:respond_to?).with(:some_query_method).and_return(true) 74 | end 75 | 76 | it { is_expected.to eq(false) } 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/draper/undecorate_chain_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Draper, '.undecorate_chain' do 4 | let!(:object) { Model.new } 5 | let!(:decorated_inner) { Class.new(Draper::Decorator).new(object) } 6 | let!(:decorated_outer) { Class.new(Draper::Decorator).new(decorated_inner) } 7 | 8 | it 'undecorates full chain of decorated objects' do 9 | expect(Draper.undecorate_chain(decorated_outer)).to equal object 10 | end 11 | 12 | it 'passes a non-decorated object through' do 13 | expect(Draper.undecorate_chain(object)).to equal object 14 | end 15 | 16 | it 'passes a non-decorator object through' do 17 | object = Object.new 18 | expect(Draper.undecorate_chain(object)).to equal object 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/draper/undecorate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Draper, '.undecorate' do 4 | it 'undecorates a decorated object' do 5 | object = Model.new 6 | decorator = Draper::Decorator.new(object) 7 | expect(Draper.undecorate(decorator)).to equal object 8 | end 9 | 10 | it 'passes a non-decorated object through' do 11 | object = Model.new 12 | expect(Draper.undecorate(object)).to equal object 13 | end 14 | 15 | it 'passes a non-decorator object through' do 16 | object = Object.new 17 | expect(Draper.undecorate(object)).to equal object 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/draper/view_context/build_strategy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def fake_view_context 4 | double("ViewContext") 5 | end 6 | 7 | def fake_controller(view_context = fake_view_context) 8 | double("Controller", view_context: view_context, request: double("Request")) 9 | end 10 | 11 | module Draper 12 | describe ViewContext::BuildStrategy::Full do 13 | describe "#call" do 14 | context "when a current controller is set" do 15 | it "returns the controller's view context" do 16 | view_context = fake_view_context 17 | allow(ViewContext).to receive_messages controller: fake_controller(view_context) 18 | strategy = ViewContext::BuildStrategy::Full.new 19 | 20 | expect(strategy.call).to be view_context 21 | end 22 | end 23 | 24 | context "when a current controller is not set" do 25 | it "uses ApplicationController" do 26 | expect(Draper::ViewContext.controller).to be_nil 27 | view_context = ViewContext::BuildStrategy::Full.new.call 28 | expect(view_context.controller).to eq Draper::ViewContext.controller 29 | expect(view_context.controller).to be_an ApplicationController 30 | end 31 | end 32 | 33 | it "adds a request if one is not defined" do 34 | controller = Class.new(ActionController::Base).new 35 | allow(ViewContext).to receive_messages controller: controller 36 | strategy = ViewContext::BuildStrategy::Full.new 37 | 38 | expect(controller.request).to be_nil 39 | strategy.call 40 | expect(controller.request).to be_an ActionDispatch::TestRequest 41 | expect(controller.params).to be_empty 42 | 43 | # sanity checks 44 | expect(controller.view_context.request).to be controller.request 45 | expect(controller.view_context.params).to be controller.params 46 | end 47 | 48 | it "adds methods to the view context from the constructor block" do 49 | allow(ViewContext).to receive(:controller).and_return(fake_controller) 50 | strategy = ViewContext::BuildStrategy::Full.new do 51 | def a_helper_method; end 52 | end 53 | 54 | expect(strategy.call).to respond_to :a_helper_method 55 | end 56 | 57 | it "includes modules into the view context from the constructor block" do 58 | view_context = Object.new 59 | allow(ViewContext).to receive(:controller).and_return(fake_controller(view_context)) 60 | helpers = Module.new do 61 | def a_helper_method; end 62 | end 63 | strategy = ViewContext::BuildStrategy::Full.new do 64 | include helpers 65 | end 66 | 67 | expect(strategy.call).to respond_to :a_helper_method 68 | end 69 | end 70 | end 71 | 72 | describe ViewContext::BuildStrategy::Fast do 73 | describe "#call" do 74 | it "returns an instance of a subclass of ActionView::Base" do 75 | strategy = ViewContext::BuildStrategy::Fast.new 76 | 77 | returned = strategy.call 78 | 79 | expect(returned).to be_an ActionView::Base 80 | expect(returned.class).not_to be ActionView::Base 81 | end 82 | 83 | it "returns different instances each time" do 84 | strategy = ViewContext::BuildStrategy::Fast.new 85 | 86 | expect(strategy.call).not_to be strategy.call 87 | end 88 | 89 | it "returns the same subclass each time" do 90 | strategy = ViewContext::BuildStrategy::Fast.new 91 | 92 | expect(strategy.call.class).to be strategy.call.class 93 | end 94 | 95 | it "adds methods to the view context from the constructor block" do 96 | strategy = ViewContext::BuildStrategy::Fast.new do 97 | def a_helper_method; end 98 | end 99 | 100 | expect(strategy.call).to respond_to :a_helper_method 101 | end 102 | 103 | it "includes modules into the view context from the constructor block" do 104 | helpers = Module.new do 105 | def a_helper_method; end 106 | end 107 | strategy = ViewContext::BuildStrategy::Fast.new do 108 | include helpers 109 | end 110 | 111 | expect(strategy.call).to respond_to :a_helper_method 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /spec/draper/view_context_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Draper 4 | describe ViewContext do 5 | describe "#view_context" do 6 | let(:base) { Class.new { def view_context; :controller_view_context; end } } 7 | let(:controller) { Class.new(base) { include ViewContext } } 8 | 9 | it "saves the superclass's view context" do 10 | expect(ViewContext).to receive(:current=).with(:controller_view_context) 11 | controller.new.view_context 12 | end 13 | 14 | it "returns the superclass's view context" do 15 | expect(controller.new.view_context).to be :controller_view_context 16 | end 17 | end 18 | 19 | describe ".controller" do 20 | it "returns the stored controller from RequestStore" do 21 | allow(RequestStore).to receive_messages store: {current_controller: :stored_controller} 22 | 23 | expect(ViewContext.controller).to be :stored_controller 24 | end 25 | end 26 | 27 | describe ".controller=" do 28 | it "stores a controller in RequestStore" do 29 | store = {} 30 | allow(RequestStore).to receive_messages store: store 31 | 32 | ViewContext.controller = :stored_controller 33 | expect(store[:current_controller]).to be :stored_controller 34 | end 35 | 36 | it "cleans context when controller changes" do 37 | store = { 38 | current_controller: :stored_controller, 39 | current_view_context: :stored_view_context 40 | } 41 | 42 | allow(RequestStore).to receive_messages store: store 43 | 44 | ViewContext.controller = :other_stored_controller 45 | 46 | expect(store).to include(current_controller: :other_stored_controller) 47 | expect(store).not_to include(:current_view_context) 48 | end 49 | 50 | it "doesn't clean context when controller is the same" do 51 | store = { 52 | current_controller: :stored_controller, 53 | current_view_context: :stored_view_context 54 | } 55 | 56 | allow(RequestStore).to receive_messages store: store 57 | 58 | ViewContext.controller = :stored_controller 59 | 60 | expect(store).to include(current_controller: :stored_controller) 61 | expect(store).to include(current_view_context: :stored_view_context) 62 | end 63 | end 64 | 65 | describe ".current" do 66 | it "returns the stored view context from RequestStore" do 67 | allow(RequestStore).to receive_messages store: {current_view_context: :stored_view_context} 68 | 69 | expect(ViewContext.current).to be :stored_view_context 70 | end 71 | 72 | context "when no view context is stored" do 73 | it "builds a view context" do 74 | allow(RequestStore).to receive_messages store: {} 75 | allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } 76 | allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy) 77 | 78 | expect(ViewContext.current).to be :new_helper_proxy 79 | end 80 | 81 | it "stores the built view context" do 82 | store = {} 83 | allow(RequestStore).to receive_messages store: store 84 | allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } 85 | allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy) 86 | 87 | ViewContext.current 88 | expect(store[:current_view_context]).to be :new_helper_proxy 89 | end 90 | end 91 | end 92 | 93 | describe ".current=" do 94 | it "stores a helper proxy for the view context in RequestStore" do 95 | store = {} 96 | allow(RequestStore).to receive_messages store: store 97 | allow(HelperProxy).to receive(:new).with(:stored_view_context).and_return(:stored_helper_proxy) 98 | 99 | ViewContext.current = :stored_view_context 100 | expect(store[:current_view_context]).to be :stored_helper_proxy 101 | end 102 | end 103 | 104 | describe ".clear!" do 105 | it "clears the stored controller and view controller" do 106 | store = {current_controller: :stored_controller, current_view_context: :stored_view_context} 107 | allow(RequestStore).to receive_messages store: store 108 | 109 | ViewContext.clear! 110 | expect(store).not_to have_key :current_controller 111 | expect(store).not_to have_key :current_view_context 112 | end 113 | end 114 | 115 | describe ".build" do 116 | it "returns a new view context using the build strategy" do 117 | allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } 118 | 119 | expect(ViewContext.build).to be :new_view_context 120 | end 121 | end 122 | 123 | describe ".build!" do 124 | it "returns a helper proxy for the new view context" do 125 | allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } 126 | allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy) 127 | 128 | expect(ViewContext.build!).to be :new_helper_proxy 129 | end 130 | 131 | it "stores the helper proxy" do 132 | store = {} 133 | allow(RequestStore).to receive_messages store: store 134 | allow(ViewContext).to receive_messages build_strategy: ->{ :new_view_context } 135 | allow(HelperProxy).to receive(:new).with(:new_view_context).and_return(:new_helper_proxy) 136 | 137 | ViewContext.build! 138 | expect(store[:current_view_context]).to be :new_helper_proxy 139 | end 140 | end 141 | 142 | describe ".build_strategy" do 143 | it "defaults to full" do 144 | expect(ViewContext.build_strategy).to be_a ViewContext::BuildStrategy::Full 145 | end 146 | 147 | it "memoizes" do 148 | expect(ViewContext.build_strategy).to be ViewContext.build_strategy 149 | end 150 | end 151 | 152 | describe ".test_strategy" do 153 | protect_module ViewContext 154 | 155 | context "with :fast" do 156 | it "creates a fast strategy" do 157 | ViewContext.test_strategy :fast 158 | expect(ViewContext.build_strategy).to be_a ViewContext::BuildStrategy::Fast 159 | end 160 | 161 | it "passes a block to the strategy" do 162 | allow(ViewContext::BuildStrategy::Fast).to receive(:new) { |&block| block.call } 163 | 164 | expect(ViewContext.test_strategy(:fast){:passed}).to be :passed 165 | end 166 | end 167 | 168 | context "with :full" do 169 | it "creates a full strategy" do 170 | ViewContext.test_strategy :full 171 | expect(ViewContext.build_strategy).to be_a ViewContext::BuildStrategy::Full 172 | end 173 | 174 | it "passes a block to the strategy" do 175 | allow(ViewContext::BuildStrategy::Full).to receive(:new) { |&block| block.call } 176 | 177 | expect(ViewContext.test_strategy(:full){:passed}).to be :passed 178 | end 179 | end 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /spec/draper/view_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'support/shared_examples/view_helpers' 3 | 4 | module Draper 5 | describe ViewHelpers do 6 | it_behaves_like "view helpers", Class.new{include ViewHelpers}.new 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/.rspec: -------------------------------------------------------------------------------- 1 | --format progress 2 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/base_controller.rb: -------------------------------------------------------------------------------- 1 | class BaseController < ActionController::Base 2 | include LocalizedUrls 3 | protect_from_forgery 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/localized_urls.rb: -------------------------------------------------------------------------------- 1 | module LocalizedUrls 2 | def default_url_options(options = {}) 3 | {locale: I18n.locale, host: "www.example.com", port: 12345} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class PostsController < BaseController 2 | decorates_assigned :post 3 | 4 | def show 5 | @post = Post.find(params[:id]) 6 | end 7 | 8 | def mail 9 | post = Post.find(params[:id]) 10 | email = PostMailer.decorated_email(post).deliver 11 | render html: email.body.to_s.html_safe 12 | end 13 | 14 | private 15 | 16 | def goodnight_moon 17 | "Goodnight, moon!" 18 | end 19 | helper_method :goodnight_moon 20 | end 21 | -------------------------------------------------------------------------------- /spec/dummy/app/decorators/comment_decorator.rb: -------------------------------------------------------------------------------- 1 | class CommentDecorator < Draper::Decorator 2 | delegate_all 3 | 4 | # Define presentation-specific methods here. Helpers are accessed through 5 | # `helpers` (aka `h`). You can override attributes, for example: 6 | # 7 | # def created_at 8 | # helpers.content_tag :span, class: 'time' do 9 | # object.created_at.strftime("%a %m/%d/%y") 10 | # end 11 | # end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/app/decorators/mongoid_post_decorator.rb: -------------------------------------------------------------------------------- 1 | class MongoidPostDecorator < Draper::Decorator 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/decorators/post_decorator.rb: -------------------------------------------------------------------------------- 1 | class PostDecorator < Draper::Decorator 2 | # don't delegate_all here because it helps to identify things we 3 | # have to delegate for ActiveModel compatibility 4 | 5 | # need to delegate attribute methods for AM::Serialization 6 | # need to delegate id and new_record? for AR::Base#== (Rails 3.0 only) 7 | delegate :id, :created_at, :new_record? 8 | 9 | def posted_date 10 | if created_at.to_date == DateTime.now.utc.to_date 11 | "Today" 12 | else 13 | "Not Today" 14 | end 15 | end 16 | 17 | def path_with_model 18 | h.post_path(object) 19 | end 20 | 21 | def path_with_id 22 | h.post_path(id: id) 23 | end 24 | 25 | def url_with_model 26 | h.post_url(object) 27 | end 28 | 29 | def url_with_id 30 | h.post_url(id: id) 31 | end 32 | 33 | def link 34 | h.link_to id.to_s, self 35 | end 36 | 37 | def truncated 38 | h.truncate("Once upon a time in a world far far away", length: 17, separator: ' ') 39 | end 40 | 41 | def html_escaped 42 | h.html_escape("") 43 | end 44 | 45 | def hello_world 46 | h.hello_world 47 | end 48 | 49 | def goodnight_moon 50 | h.goodnight_moon 51 | end 52 | 53 | def updated_at 54 | :overridden 55 | end 56 | 57 | def persisted? 58 | true 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def hello_world 3 | "Hello, world!" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/jobs/publish_post_job.rb: -------------------------------------------------------------------------------- 1 | class PublishPostJob < ActiveJob::Base 2 | queue_as :default 3 | 4 | def perform(post) 5 | post.save! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | include LocalizedUrls 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/post_mailer.rb: -------------------------------------------------------------------------------- 1 | class PostMailer < ApplicationMailer 2 | default from: "from@example.com" 3 | layout "application" 4 | 5 | # Mailers don't import app/helpers automatically 6 | helper :application 7 | 8 | def decorated_email(post) 9 | @post = post.decorate 10 | mail to: "to@example.com", subject: "A decorated post" 11 | end 12 | 13 | private 14 | 15 | def goodnight_moon 16 | "Goodnight, moon!" 17 | end 18 | helper_method :goodnight_moon 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/app/models/admin.rb: -------------------------------------------------------------------------------- 1 | class Admin 2 | extend Devise::Models if defined? Devise 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ApplicationRecord 2 | belongs_to :post 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/mongoid_post.rb: -------------------------------------------------------------------------------- 1 | class MongoidPost 2 | include Mongoid::Document if defined? Mongoid 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ApplicationRecord 2 | # attr_accessible :title, :body 3 | 4 | has_many :comments 5 | 6 | broadcasts if defined? Turbo::Broadcastable 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User 2 | extend Devise::Models if defined? Devise 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | 7 | 8 | <%= yield %> 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/dummy/app/views/post_mailer/decorated_email.html.erb: -------------------------------------------------------------------------------- 1 | <%= render @post %> 2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/posts/_post.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Environment:
3 |
<%= Rails.env %>
4 | 5 |
Draper view context controller:
6 |
<%= Draper::ViewContext.current.controller.class %>
7 | 8 |
Posted:
9 |
<%= post.posted_date %>
10 | 11 |
Built-in helpers:
12 |
<%= post.truncated %>
13 | 14 |
Built-in private helpers:
15 |
<%= post.html_escaped %>
16 | 17 |
Helpers from app/helpers:
18 |
<%= post.hello_world %>
19 | 20 |
Helpers from the controller:
21 |
<%= post.goodnight_moon %>
22 | 23 | <% unless defined? mailer %> 24 |
Path with decorator:
25 |
<%= post_url(post) %>
26 | 27 |
Path with model:
28 |
<%= post.path_with_model %>
29 | 30 |
Path with id:
31 |
<%= post.path_with_id %>
32 | <% end %> 33 | 34 |
URL with decorator:
35 |
<%= post_url(post) %>
36 | 37 |
URL with model:
38 |
<%= post.url_with_model %>
39 | 40 |
URL with id:
41 |
<%= post.url_with_id %>
42 |
43 | -------------------------------------------------------------------------------- /spec/dummy/app/views/posts/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= render post %> 2 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Kernel.silence_warnings do 8 | Bundler.require(*Rails.groups) 9 | end 10 | 11 | module Dummy 12 | class Application < Rails::Application 13 | config.load_defaults Rails::VERSION::STRING.to_f 14 | 15 | # For compatibility with applications that use this config 16 | # config.action_controller.include_all_helpers = false # FIXME 17 | 18 | # Please, add to the `ignore` list any other `lib` subdirectories that do 19 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 20 | # Common ones are `templates`, `generators`, or `middleware`, for example. 21 | config.try :autoload_lib, ignore: %w[assets tasks] # HACK for Rails below 7 22 | 23 | # Configuration for the application, engines, and railties goes here. 24 | # 25 | # These settings can be overridden in specific environments using the files 26 | # in config/environments, which are processed later. 27 | # 28 | # config.time_zone = "Central Time (US & Canada)" 29 | # config.eager_load_paths << Rails.root.join("extras") 30 | 31 | # HACK: allows testing in production & development environments 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | end 37 | end 38 | 39 | ActiveRecord::Migration.verbose = false 40 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 4 | 5 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 6 | -------------------------------------------------------------------------------- /spec/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | # production: 2 | # url: redis://redis.example.com:6379 3 | 4 | local: &local 5 | url: redis://localhost:6379 6 | 7 | development: *local 8 | test: *local 9 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.enable_reloading = true 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable server timing. 18 | config.server_timing = true 19 | 20 | # Enable/disable caching. By default caching is disabled. 21 | # Run rails dev:cache to toggle caching. 22 | if Rails.root.join("tmp/caching-dev.txt").exist? 23 | config.action_controller.perform_caching = true 24 | config.action_controller.enable_fragment_cache_logging = true 25 | 26 | config.cache_store = :memory_store 27 | config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } 28 | else 29 | config.action_controller.perform_caching = false 30 | 31 | config.cache_store = :null_store 32 | end 33 | 34 | # Store uploaded files on the local file system (see config/storage.yml for options). 35 | config.active_storage.service = :local 36 | 37 | # Don't care if the mailer can't send. 38 | config.action_mailer.raise_delivery_errors = false 39 | 40 | # Disable caching for Action Mailer templates even if Action Controller 41 | # caching is enabled. 42 | config.action_mailer.perform_caching = false 43 | 44 | config.action_mailer.default_url_options = { host: "localhost", port: 3000 } 45 | 46 | # Print deprecation notices to the Rails logger. 47 | config.active_support.deprecation = :log 48 | 49 | # Raise exceptions for disallowed deprecations. 50 | config.active_support.disallowed_deprecation = :raise 51 | 52 | # Tell Active Support which deprecation messages to disallow. 53 | config.active_support.disallowed_deprecation_warnings = [] 54 | 55 | # Raise an error on page load if there are pending migrations. 56 | config.active_record.migration_error = :page_load 57 | 58 | # Highlight code that triggered database queries in logs. 59 | config.active_record.verbose_query_logs = true 60 | 61 | # Highlight code that enqueued background job in logs. 62 | # config.active_job.verbose_enqueue_logs = true 63 | 64 | # Suppress logger output for asset requests. 65 | # config.assets.quiet = true 66 | 67 | # Raises error for missing translations. 68 | # config.i18n.raise_on_missing_translations = true 69 | 70 | # Annotate rendered view with file names. 71 | # config.action_view.annotate_rendered_view_with_filenames = true 72 | 73 | # Uncomment if you wish to allow Action Cable access from any origin. 74 | # config.action_cable.disable_request_forgery_protection = true 75 | 76 | # Raise error when a before_action's only/except options reference missing actions. 77 | # config.action_controller.raise_on_missing_callback_actions = true 78 | end 79 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.enable_reloading = false 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment 20 | # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. 24 | # config.public_file_server.enabled = false 25 | 26 | # Compress CSS using a preprocessor. 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fall back to assets pipeline if a precompiled asset is missed. 30 | # config.assets.compile = false 31 | 32 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 33 | # config.asset_host = "http://assets.example.com" 34 | 35 | # Specifies the header that your server uses for sending files. 36 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 37 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 38 | 39 | # Store uploaded files on the local file system (see config/storage.yml for options). 40 | config.active_storage.service = :local 41 | 42 | # Mount Action Cable outside main process or domain. 43 | # config.action_cable.mount_path = nil 44 | # config.action_cable.url = "wss://example.com/cable" 45 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 46 | 47 | # Assume all access to the app is happening through a SSL-terminating reverse proxy. 48 | # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. 49 | # config.assume_ssl = true 50 | 51 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 52 | # config.force_ssl = true 53 | 54 | # Skip http-to-https redirect for the default health check endpoint. 55 | # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } 56 | 57 | # Log to STDOUT by default 58 | config.logger = ActiveSupport::Logger.new(STDOUT) 59 | .tap { |logger| logger.formatter = ::Logger::Formatter.new } 60 | .tap { |logger| break ActiveSupport::TaggedLogging.new logger } 61 | 62 | # Prepend all log lines with the following tags. 63 | config.log_tags = [:request_id] 64 | 65 | # "info" includes generic and useful information about system operation, but avoids logging too much 66 | # information to avoid inadvertent exposure of personally identifiable information (PII). If you 67 | # want to log everything, set the level to "debug". 68 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") 69 | 70 | # Use a different cache store in production. 71 | # config.cache_store = :mem_cache_store 72 | 73 | # Use a real queuing backend for Active Job (and separate queues per environment). 74 | # config.active_job.queue_adapter = :resque 75 | # config.active_job.queue_name_prefix = "dummy_production" 76 | 77 | # Disable caching for Action Mailer templates even if Action Controller 78 | # caching is enabled. 79 | config.action_mailer.perform_caching = false 80 | 81 | # Ignore bad email addresses and do not raise email delivery errors. 82 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 83 | # config.action_mailer.raise_delivery_errors = false 84 | 85 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 86 | # the I18n.default_locale when a translation cannot be found). 87 | config.i18n.fallbacks = true 88 | 89 | # Don't log any deprecations. 90 | config.active_support.report_deprecations = false 91 | 92 | # Do not dump schema after migrations. 93 | config.active_record.dump_schema_after_migration = false 94 | 95 | # Enable DNS rebinding protection and other `Host` header attacks. 96 | # config.hosts = [ 97 | # "example.com", # Allow requests from example.com 98 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` 99 | # ] 100 | # Skip DNS rebinding protection for the default health check endpoint. 101 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } 102 | end 103 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # While tests run files are not watched, reloading is not necessary. 12 | config.enable_reloading = false 13 | 14 | # Eager loading loads your entire application. When running a single test locally, 15 | # this is usually not necessary, and can slow down your test suite. However, it's 16 | # recommended that you enable it in continuous integration systems to ensure eager 17 | # loading is working properly before deploying your code. 18 | config.eager_load = ENV["CI"].present? 19 | 20 | # Configure public file server for tests with Cache-Control for performance. 21 | config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } 22 | 23 | # Show full error reports and disable caching. 24 | config.consider_all_requests_local = true 25 | config.action_controller.perform_caching = false 26 | config.cache_store = :null_store 27 | 28 | # Render exception templates for rescuable exceptions and raise for other exceptions. 29 | config.action_dispatch.show_exceptions = :rescuable 30 | 31 | # Disable request forgery protection in test environment. 32 | config.action_controller.allow_forgery_protection = false 33 | 34 | # Store uploaded files on the local file system in a temporary directory. 35 | config.active_storage.service = :test 36 | 37 | config.active_job.queue_adapter = :test # TODO: remove alongside support for Rails below 7.2 38 | 39 | # Disable caching for Action Mailer templates even if Action Controller 40 | # caching is enabled. 41 | config.action_mailer.perform_caching = false 42 | 43 | # Tell Action Mailer not to deliver emails to the real world. 44 | # The :test delivery method accumulates sent emails in the 45 | # ActionMailer::Base.deliveries array. 46 | config.action_mailer.delivery_method = :test 47 | 48 | # Unlike controllers, the mailer instance doesn't have any context about the 49 | # incoming request so you'll need to provide the :host parameter yourself. 50 | config.action_mailer.default_url_options = { host: "www.example.com" } 51 | 52 | # Print deprecation notices to the stderr. 53 | config.active_support.deprecation = :stderr 54 | 55 | # Raise exceptions for disallowed deprecations. 56 | config.active_support.disallowed_deprecation = :raise 57 | 58 | # Tell Active Support which deprecation messages to disallow. 59 | config.active_support.disallowed_deprecation_warnings = [] 60 | 61 | # Raises error for missing translations. 62 | # config.i18n.raise_on_missing_translations = true 63 | 64 | # Annotate rendered view with file names. 65 | # config.action_view.annotate_rendered_view_with_filenames = true 66 | 67 | # Raise error when a before_action's only/except options reference missing actions. 68 | # config.action_controller.raise_on_missing_callback_actions = true 69 | end 70 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/draper.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.to_prepare do 2 | Draper.configure do |config| 3 | config.default_controller = BaseController 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_key_base = 'c2e3474d3816f60bf6dd0f3b983e7283c7ff5373e11a96935340b544a31964dbe5ee077136165ee2975e0005f5e80207c0059e6d5589699031242ba5a06dcb87' 8 | Dummy::Application.config.secret_token = 'c2e3474d3816f60bf6dd0f3b983e7283c7ff5373e11a96935340b544a31964dbe5ee077136165ee2975e0005f5e80207c0059e6d5589699031242ba5a06dcb87' 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/dummy/config/mongoid.yml: -------------------------------------------------------------------------------- 1 | development: 2 | # Configure available database clients. (required) 3 | clients: 4 | # Defines the default client. (required) 5 | default: 6 | # Defines the name of the default database that Mongoid can connect to. 7 | # (required). 8 | database: dummy_development 9 | # Provides the hosts the default client can connect to. Must be an array 10 | # of host:port pairs. (required) 11 | hosts: 12 | - localhost:27017 13 | options: 14 | # Change the default write concern. (default = { w: 1 }) 15 | # write: 16 | # w: 1 17 | 18 | # Change the default read preference. Valid options for mode are: :secondary, 19 | # :secondary_preferred, :primary, :primary_preferred, :nearest 20 | # (default: primary) 21 | # read: 22 | # mode: :secondary_preferred 23 | # tag_sets: 24 | # - use: web 25 | 26 | # The name of the user for authentication. 27 | # user: 'user' 28 | 29 | # The password of the user for authentication. 30 | # password: 'password' 31 | 32 | # The user's database roles. 33 | # roles: 34 | # - 'dbOwner' 35 | 36 | # Change the default authentication mechanism. Valid options are: :scram, 37 | # :mongodb_cr, :mongodb_x509, and :plain. (default on 3.0 is :scram, default 38 | # on 2.4 and 2.6 is :plain) 39 | # auth_mech: :scram 40 | 41 | # The database or source to authenticate the user against. (default: admin) 42 | # auth_source: admin 43 | 44 | # Force a the driver cluster to behave in a certain manner instead of auto- 45 | # discovering. Can be one of: :direct, :replica_set, :sharded. Set to :direct 46 | # when connecting to hidden members of a replica set. 47 | # connect: :direct 48 | 49 | # Changes the default time in seconds the server monitors refresh their status 50 | # via ismaster commands. (default: 10) 51 | # heartbeat_frequency: 10 52 | 53 | # The time in seconds for selecting servers for a near read preference. (default: 5) 54 | # local_threshold: 5 55 | 56 | # The timeout in seconds for selecting a server for an operation. (default: 30) 57 | # server_selection_timeout: 30 58 | 59 | # The maximum number of connections in the connection pool. (default: 5) 60 | # max_pool_size: 5 61 | 62 | # The minimum number of connections in the connection pool. (default: 1) 63 | # min_pool_size: 1 64 | 65 | # The time to wait, in seconds, in the connection pool for a connection 66 | # to be checked in before timing out. (default: 5) 67 | # wait_queue_timeout: 5 68 | 69 | # The time to wait to establish a connection before timing out, in seconds. 70 | # (default: 5) 71 | # connect_timeout: 5 72 | 73 | # The timeout to wait to execute operations on a socket before raising an error. 74 | # (default: 5) 75 | # socket_timeout: 5 76 | 77 | # The name of the replica set to connect to. Servers provided as seeds that do 78 | # not belong to this replica set will be ignored. 79 | # replica_set: name 80 | 81 | # Whether to connect to the servers via ssl. (default: false) 82 | # ssl: true 83 | 84 | # The certificate file used to identify the connection against MongoDB. 85 | # ssl_cert: /path/to/my.cert 86 | 87 | # The private keyfile used to identify the connection against MongoDB. 88 | # Note that even if the key is stored in the same file as the certificate, 89 | # both need to be explicitly specified. 90 | # ssl_key: /path/to/my.key 91 | 92 | # A passphrase for the private key. 93 | # ssl_key_pass_phrase: password 94 | 95 | # Whether or not to do peer certification validation. (default: true) 96 | # ssl_verify: true 97 | 98 | # The file containing a set of concatenated certification authority certifications 99 | # used to validate certs passed from the other end of the connection. 100 | # ssl_ca_cert: /path/to/ca.cert 101 | 102 | 103 | # Configure Mongoid specific options. (optional) 104 | options: 105 | # Includes the root model name in json serialization. (default: false) 106 | # include_root_in_json: false 107 | 108 | # Include the _type field in serialization. (default: false) 109 | # include_type_for_serialization: false 110 | 111 | # Preload all models in development, needed when models use 112 | # inheritance. (default: false) 113 | # preload_models: false 114 | 115 | # Raise an error when performing a #find and the document is not found. 116 | # (default: true) 117 | # raise_not_found_error: true 118 | 119 | # Raise an error when defining a scope with the same name as an 120 | # existing method. (default: false) 121 | # scope_overwrite_exception: false 122 | 123 | # Use Active Support's time zone in conversions. (default: true) 124 | # use_activesupport_time_zone: true 125 | 126 | # Ensure all times are UTC in the app side. (default: false) 127 | # use_utc: false 128 | 129 | # Set the Mongoid and Ruby driver log levels when not in a Rails 130 | # environment. The Mongoid logger will be set to the Rails logger 131 | # otherwise.(default: :info) 132 | # log_level: :info 133 | test: 134 | clients: 135 | default: 136 | database: dummy_test 137 | hosts: 138 | - localhost:27017 139 | options: 140 | read: 141 | mode: :primary 142 | max_pool_size: 1 143 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | scope "(:locale)", locale: /en|zh/ do 3 | resources :posts, only: [:show] do 4 | get "mail", on: :member 5 | end 6 | end 7 | 8 | devise_for :users, :admins if defined?(Devise) 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20121019115657_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :posts do |t| 4 | 5 | t.timestamps 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20240907041839_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :comments do |t| 4 | t.references :post, foreign_key: true 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2024_09_07_041839) do 14 | 15 | create_table "comments", force: :cascade do |t| 16 | t.integer "post_id" 17 | t.datetime "created_at", precision: 6, null: false 18 | t.datetime "updated_at", precision: 6, null: false 19 | t.index ["post_id"], name: "index_comments_on_post_id" 20 | end 21 | 22 | create_table "posts", force: :cascade do |t| 23 | t.datetime "created_at", null: false 24 | t.datetime "updated_at", null: false 25 | end 26 | 27 | add_foreign_key "comments", "posts" 28 | end 29 | -------------------------------------------------------------------------------- /spec/dummy/db/seeds.rb: -------------------------------------------------------------------------------- 1 | Post.delete_all 2 | Post.create id: 1 3 | -------------------------------------------------------------------------------- /spec/dummy/fast_spec/post_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'draper' 2 | 3 | require 'active_model/naming' 4 | require_relative '../app/decorators/post_decorator' 5 | 6 | Draper::ViewContext.test_strategy :fast 7 | 8 | Post = Struct.new(:id) { extend ActiveModel::Naming } 9 | 10 | describe PostDecorator do 11 | let(:decorator) { PostDecorator.new(object) } 12 | let(:object) { Post.new(42) } 13 | 14 | it "can use built-in helpers" do 15 | expect(decorator.truncated).to eq "Once upon a..." 16 | end 17 | 18 | it "can use built-in private helpers" do 19 | expect(decorator.html_escaped).to eq "<script>danger</script>" 20 | end 21 | 22 | it "can't use user-defined helpers from app/helpers" do 23 | expect{decorator.hello_world}.to raise_error NoMethodError, /hello_world/ 24 | end 25 | 26 | it "can't use path helpers" do 27 | expect{decorator.path_with_model}.to raise_error NoMethodError, /post_path/ 28 | end 29 | 30 | it "can't use url helpers" do 31 | expect{decorator.url_with_model}.to raise_error NoMethodError, /post_url/ 32 | end 33 | 34 | it "can't be passed implicitly to url_for" do 35 | expect{decorator.link}.to raise_error ArgumentError 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/dummy/lib/tasks/test.rake: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | require 'rspec/core/rake_task' 3 | 4 | Rake::Task[:test].clear 5 | Rake::TestTask.new :test do |t| 6 | t.libs << "test" 7 | t.pattern = "test/**/*_test.rb" 8 | end 9 | 10 | RSpec::Core::RakeTask.new :spec 11 | 12 | RSpec::Core::RakeTask.new :fast_spec do |t| 13 | t.pattern = "fast_spec/**/*_spec.rb" 14 | end 15 | 16 | task default: [:test, :spec, :fast_spec] 17 | -------------------------------------------------------------------------------- /spec/dummy/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drapergem/draper/2f95bbbae870fbb0acbc2d1f9575ab845baecc60/spec/dummy/log/.gitkeep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drapergem/draper/2f95bbbae870fbb0acbc2d1f9575ab845baecc60/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/dummy/spec/decorators/active_model_serializers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Draper::CollectionDecorator do 4 | describe "#active_model_serializer" do 5 | it "returns ActiveModel::Serializer::CollectionSerializer" do 6 | collection_decorator = Draper::CollectionDecorator.new([]) 7 | collection_serializer = ActiveModel::Serializer.serializer_for(collection_decorator) 8 | 9 | expect(collection_serializer).to be ActiveModel::Serializer::CollectionSerializer 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/dummy/spec/decorators/devise_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined?(Devise) 4 | describe "A decorator spec" do 5 | it "can sign in a real user" do 6 | user = User.new 7 | sign_in user 8 | 9 | expect(helper.current_user).to be user 10 | end 11 | 12 | it "can sign in a mock user" do 13 | user = double("User") 14 | sign_in :user, user 15 | 16 | expect(helper.current_user).to be user 17 | end 18 | 19 | it "can sign in a real admin" do 20 | admin = Admin.new 21 | sign_in admin 22 | 23 | expect(helper.current_admin).to be admin 24 | end 25 | 26 | it "can sign in a mock admin" do 27 | admin = double("Admin") 28 | sign_in :admin, admin 29 | 30 | expect(helper.current_admin).to be admin 31 | end 32 | 33 | it "can sign out a real user" do 34 | user = User.new 35 | sign_in user 36 | sign_out user 37 | 38 | expect(helper.current_user).to be_nil 39 | end 40 | 41 | it "can sign out a mock user" do 42 | user = double("User") 43 | sign_in :user, user 44 | sign_out :user 45 | 46 | expect(helper.current_user).to be_nil 47 | end 48 | 49 | it "can sign out without a user" do 50 | sign_out :user 51 | 52 | expect(helper.current_user).to be_nil 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/dummy/spec/decorators/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "A decorator spec" do 4 | it "can access helpers through `helper`" do 5 | expect(helper.content_tag(:p, "Help!")).to eq "

Help!

" 6 | end 7 | 8 | it "can access helpers through `helpers`" do 9 | expect(helpers.content_tag(:p, "Help!")).to eq "

Help!

" 10 | end 11 | 12 | it "can access helpers through `h`" do 13 | expect(h.content_tag(:p, "Help!")).to eq "

Help!

" 14 | end 15 | 16 | it "gets the same helper object as a decorator" do 17 | decorator = Draper::Decorator.new(Object.new) 18 | 19 | expect(helpers).to be decorator.helpers 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy/spec/decorators/post_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe PostDecorator do 4 | let(:decorator) { PostDecorator.new(object) } 5 | let(:object) { Post.create } 6 | 7 | it "can use built-in helpers" do 8 | expect(decorator.truncated).to eq "Once upon a..." 9 | end 10 | 11 | it "can use built-in private helpers" do 12 | expect(decorator.html_escaped).to eq "<script>danger</script>" 13 | end 14 | 15 | it "can use user-defined helpers from app/helpers" do 16 | expect(decorator.hello_world).to eq "Hello, world!" 17 | end 18 | 19 | it "can be passed to path helpers" do 20 | expect(helpers.post_path(decorator)).to eq "/en/posts/#{object.id}" 21 | end 22 | 23 | it "can use path helpers with its model" do 24 | expect(decorator.path_with_model).to eq "/en/posts/#{object.id}" 25 | end 26 | 27 | it "can use path helpers with its id" do 28 | expect(decorator.path_with_id).to eq "/en/posts/#{object.id}" 29 | end 30 | 31 | it "can be passed to url helpers" do 32 | expect(helpers.post_url(decorator)).to eq "http://www.example.com:12345/en/posts/#{object.id}" 33 | end 34 | 35 | it "can use url helpers with its model" do 36 | expect(decorator.url_with_model).to eq "http://www.example.com:12345/en/posts/#{object.id}" 37 | end 38 | 39 | it "can use url helpers with its id" do 40 | expect(decorator.url_with_id).to eq "http://www.example.com:12345/en/posts/#{object.id}" 41 | end 42 | 43 | it "can be passed implicitly to url_for" do 44 | expect(decorator.link).to eq "#{object.id}" 45 | end 46 | 47 | it "serializes overriden attributes" do 48 | expect(decorator.serializable_hash["updated_at"]).to be :overridden 49 | end 50 | 51 | it "serializes to JSON" do 52 | json = decorator.to_json 53 | expect(json).to match /"updated_at":"overridden"/ 54 | end 55 | 56 | it "serializes to XML" do 57 | xml = Capybara.string(decorator.to_xml) 58 | expect(xml).to have_css "post > updated-at", text: "overridden" 59 | end 60 | 61 | it "uses a test view context from BaseController" do 62 | expect(Draper::ViewContext.current.controller).to be_an BaseController 63 | end 64 | 65 | describe 'Global ID' do 66 | it { expect(GlobalID::Locator.locate decorator.to_gid).to eq decorator } 67 | it { expect(GlobalID::Locator.locate decorator.to_gid).to be_decorated } 68 | it { expect(GlobalID::Locator.locate object.to_gid).not_to be_decorated } 69 | it { expect(GlobalID::Locator.locate_signed decorator.to_sgid).to eq decorator } 70 | it { expect(GlobalID::Locator.locate_signed decorator.to_sgid).to be_decorated } 71 | it { expect(GlobalID::Locator.locate_signed object.to_sgid).not_to be_decorated } 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/dummy/spec/decorators/spec_type_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "A spec in this folder" do 4 | it "is a decorator spec" do 5 | expect(RSpec.current_example.metadata[:type]).to be :decorator 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/spec/decorators/view_context_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | def it_does_not_leak_view_context 4 | 2.times do 5 | it "has an independent view context" do 6 | expect(Draper::ViewContext.current).not_to be :leaked 7 | Draper::ViewContext.current = :leaked 8 | end 9 | end 10 | end 11 | 12 | describe "A decorator spec", type: :decorator do 13 | it_does_not_leak_view_context 14 | end 15 | 16 | describe "A controller spec", type: :controller do 17 | it_does_not_leak_view_context 18 | end 19 | 20 | describe "A mailer spec", type: :mailer do 21 | it_does_not_leak_view_context 22 | end 23 | -------------------------------------------------------------------------------- /spec/dummy/spec/jobs/publish_post_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe PublishPostJob, type: :job do 4 | let(:post) { Post.create.decorate } 5 | 6 | subject(:job) { described_class.perform_later(post) } 7 | 8 | it 'queues the job' do 9 | expect { job }.to have_enqueued_job(described_class).with { |post| 10 | expect(post).to be_decorated 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/spec/mailers/post_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe PostMailer do 4 | describe "#decorated_email" do 5 | let(:email_body) { Capybara.string(email.body.to_s) } 6 | let(:email) { PostMailer.decorated_email(post).deliver } 7 | let(:post) { Post.create } 8 | 9 | it "decorates" do 10 | expect(email_body).to have_content "Today" 11 | end 12 | 13 | it "can use url helpers with a model" do 14 | expect(email_body).to have_css "#url_with_model", text: "http://www.example.com:12345/en/posts/#{post.id}" 15 | end 16 | 17 | it "can use url helpers with an id" do 18 | expect(email_body).to have_css "#url_with_id", text: "http://www.example.com:12345/en/posts/#{post.id}" 19 | end 20 | 21 | it "uses the correct view context controller" do 22 | expect(email_body).to have_css "#controller", text: "PostMailer" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/dummy/spec/models/application_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe ApplicationRecord do 4 | it { expect(described_class.superclass).to eq ActiveRecord::Base } 5 | 6 | it { expect(described_class.abstract_class).to be_truthy } 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/spec/models/mongoid_post_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shared_examples/decoratable' 3 | 4 | if defined?(Mongoid) 5 | describe MongoidPost do 6 | it_behaves_like "a decoratable model" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/spec/models/post_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'shared_examples/decoratable' 3 | 4 | RSpec.describe Post do 5 | let(:record) { described_class.create! } 6 | 7 | it_behaves_like 'a decoratable model' 8 | 9 | it { should be_a ApplicationRecord } 10 | 11 | describe 'broadcasts' do 12 | let(:modification) { described_class.create! } 13 | 14 | it 'passes a decorated object for rendering' do 15 | expect do 16 | modification 17 | end.to have_enqueued_job(Turbo::Streams::ActionBroadcastJob).with { |stream, action:, target:, **rendering| 18 | expect(rendering[:locals]).to include :post 19 | expect(rendering[:locals][:post]).to be_decorated 20 | } 21 | end 22 | end if defined? Turbo::Broadcastable 23 | 24 | describe 'associations' do 25 | context 'when decorated' do 26 | subject { associated.decorate } 27 | 28 | let(:associated) { record.comments } 29 | let(:persisted) { associated.create! [{}] * rand(0..2) } 30 | let(:unsaved) { associated.build [{}] * rand(1..2) } 31 | 32 | before { persisted } # should exist 33 | 34 | it 'returns a decorated collection' do 35 | is_expected.to match_array persisted 36 | is_expected.to be_all &:decorated? 37 | end 38 | 39 | it 'uses cached records' do 40 | expect(associated).not_to be_loaded 41 | 42 | associated.load 43 | 44 | expect { subject.to_a }.to execute.exactly(0).queries 45 | end 46 | 47 | it 'caches records' do 48 | expect(associated).not_to be_loaded 49 | 50 | associated.decorate 51 | 52 | expect { subject.to_a; associated.load }.to execute.exactly(0).queries 53 | end 54 | 55 | context 'with unsaved records' do 56 | before { unsaved } # should exist 57 | 58 | it 'respects unsaved records' do 59 | is_expected.to match_array persisted + unsaved 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/dummy/spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'spec_helper' 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require_relative '../config/environment' 5 | # Prevent database truncation if the environment is production 6 | abort("The Rails environment is running in production mode!") if Rails.env.production? 7 | require 'rspec/rails' 8 | require 'rspec/activerecord/expectations' 9 | # Add additional requires below this line. Rails is not loaded until this point! 10 | 11 | # Requires supporting ruby files with custom matchers and macros, etc, in 12 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 13 | # run as spec files by default. This means that files in spec/support that end 14 | # in _spec.rb will both be required and run as specs, causing the specs to be 15 | # run twice. It is recommended that you do not name files matching this glob to 16 | # end with _spec.rb. You can configure this pattern with the --pattern 17 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 18 | # 19 | # The following line is provided for convenience purposes. It has the downside 20 | # of increasing the boot-up time by auto-requiring all files in the support 21 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 22 | # require only the support files necessary. 23 | # 24 | # Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f } 25 | 26 | # Checks for pending migrations and applies them before tests are run. 27 | # If you are not using ActiveRecord, you can remove these lines. 28 | begin 29 | ActiveRecord::Migration.maintain_test_schema! 30 | rescue ActiveRecord::PendingMigrationError => e 31 | abort e.to_s.strip 32 | end 33 | RSpec.configure do |config| 34 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 35 | # config.fixture_paths = [ 36 | # Rails.root.join('spec/fixtures') 37 | # ] 38 | 39 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 40 | # examples within a transaction, remove the following line or assign false 41 | # instead of true. 42 | config.use_transactional_fixtures = true 43 | 44 | # You can uncomment this line to turn off ActiveRecord support entirely. 45 | # config.use_active_record = false 46 | 47 | # RSpec Rails can automatically mix in different behaviours to your tests 48 | # based on their file location, for example enabling you to call `get` and 49 | # `post` in specs under `spec/controllers`. 50 | # 51 | # You can disable this behaviour by removing the line below, and instead 52 | # explicitly tag your specs with their type, e.g.: 53 | # 54 | # RSpec.describe UsersController, type: :controller do 55 | # # ... 56 | # end 57 | # 58 | # The different available types are documented in the features, such as in 59 | # https://rspec.info/features/6-0/rspec-rails 60 | config.infer_spec_type_from_file_location! 61 | 62 | # Filter lines from Rails gems in backtraces. 63 | config.filter_rails_from_backtrace! 64 | # arbitrary gems may also be filtered via: 65 | # config.filter_gems_from_backtrace("gem name") 66 | 67 | # Extra matchers 68 | config.include RSpec::ActiveRecord::Expectations 69 | end 70 | -------------------------------------------------------------------------------- /spec/dummy/spec/shared_examples/decoratable.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for "a decoratable model" do 2 | describe ".decorate" do 3 | it "applies a collection decorator to a scope" do 4 | described_class.create 5 | decorated = described_class.limit(1).decorate 6 | 7 | expect(decorated.size).to eq(1) 8 | expect(decorated).to be_decorated 9 | end 10 | end 11 | 12 | describe "#==" do 13 | it "is true for other instances' decorators" do 14 | described_class.create 15 | one = described_class.first 16 | other = described_class.first 17 | 18 | expect(one).not_to be other 19 | expect(one == other.decorate).to be_truthy 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/dummy/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | 50 | # This allows you to limit a spec run to individual examples or groups 51 | # you care about by tagging them with `:focus` metadata. When nothing 52 | # is tagged with `:focus`, all examples get run. RSpec also provides 53 | # aliases for `it`, `describe`, and `context` that include `:focus` 54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 55 | config.filter_run_when_matching :focus 56 | 57 | # Allows RSpec to persist some state between runs in order to support 58 | # the `--only-failures` and `--next-failure` CLI options. We recommend 59 | # you configure your source control system to ignore this file. 60 | config.example_status_persistence_file_path = "spec/examples.txt" 61 | 62 | # Limits the available syntax to the non-monkey patched syntax that is 63 | # recommended. For more details, see: 64 | # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ 65 | # config.disable_monkey_patching! 66 | 67 | # Many RSpec users commonly either run the entire suite or an individual 68 | # file, and it's useful to allow more verbose output when running an 69 | # individual spec file. 70 | if config.files_to_run.one? 71 | # Use the documentation formatter for detailed output, 72 | # unless a formatter has already been configured 73 | # (e.g. via a command-line flag). 74 | config.default_formatter = "doc" 75 | end 76 | 77 | # Print the 10 slowest examples and example groups at the 78 | # end of the spec run, to help surface which specs are running 79 | # particularly slow. 80 | config.profile_examples = 10 81 | 82 | # Run specs in random order to surface order dependencies. If you find an 83 | # order dependency and want to debug it, you can fix the order by providing 84 | # the seed, which is printed after each run. 85 | # --seed 1234 86 | config.order = :random 87 | 88 | # Seed global randomization in this process using the `--seed` CLI option. 89 | # Setting this allows you to use `--seed` to deterministically reproduce 90 | # test failures related to randomization by passing the same `--seed` value 91 | # as the one that triggered the failure. 92 | Kernel.srand config.seed 93 | end 94 | -------------------------------------------------------------------------------- /spec/dummy/test/decorators/minitest/devise_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | if defined?(Devise) 4 | describe "A decorator test" do 5 | it "can sign in a real user" do 6 | user = User.new 7 | sign_in user 8 | 9 | assert_same user, helper.current_user 10 | end 11 | 12 | it "can sign in a mock user" do 13 | user = Object.new 14 | sign_in :user, user 15 | 16 | assert_same user, helper.current_user 17 | end 18 | 19 | it "can sign in a real admin" do 20 | admin = Admin.new 21 | sign_in admin 22 | 23 | assert_same admin, helper.current_admin 24 | end 25 | 26 | it "can sign in a mock admin" do 27 | admin = Object.new 28 | sign_in :admin, admin 29 | 30 | assert_same admin, helper.current_admin 31 | end 32 | 33 | it "can sign out a real user" do 34 | user = User.new 35 | sign_in user 36 | sign_out user 37 | 38 | assert helper.current_user.nil? 39 | end 40 | 41 | it "can sign out a mock user" do 42 | user = Object.new 43 | sign_in :user, user 44 | sign_out :user 45 | 46 | assert helper.current_user.nil? 47 | end 48 | 49 | it "can sign out without a user" do 50 | sign_out :user 51 | 52 | assert helper.current_user.nil? 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/dummy/test/decorators/minitest/helpers_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | describe "A decorator test" do 4 | it "can access helpers through `helper`" do 5 | assert_equal "

Help!

", helper.content_tag(:p, "Help!") 6 | end 7 | 8 | it "can access helpers through `helpers`" do 9 | assert_equal "

Help!

", helpers.content_tag(:p, "Help!") 10 | end 11 | 12 | it "can access helpers through `h`" do 13 | assert_equal "

Help!

", h.content_tag(:p, "Help!") 14 | end 15 | 16 | it "gets the same helper object as a decorator" do 17 | decorator = Draper::Decorator.new(Object.new) 18 | 19 | assert_same decorator.helpers, helpers 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy/test/decorators/minitest/spec_type_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | def it_is_a_decorator_test 4 | it "is a decorator test" do 5 | assert_kind_of Draper::TestCase, self 6 | end 7 | end 8 | 9 | def it_is_not_a_decorator_test 10 | it "is not a decorator test" do 11 | refute_kind_of Draper::TestCase, self 12 | end 13 | end 14 | 15 | ProductDecorator = Class.new(Draper::Decorator) 16 | ProductsDecorator = Class.new(Draper::CollectionDecorator) 17 | 18 | describe ProductDecorator do 19 | it_is_a_decorator_test 20 | end 21 | 22 | describe ProductsDecorator do 23 | it_is_a_decorator_test 24 | end 25 | 26 | describe "ProductDecorator" do 27 | it_is_a_decorator_test 28 | end 29 | 30 | describe "AnyDecorator" do 31 | it_is_a_decorator_test 32 | end 33 | 34 | describe "Any decorator" do 35 | it_is_a_decorator_test 36 | end 37 | 38 | describe "AnyDecoratorTest" do 39 | it_is_a_decorator_test 40 | end 41 | 42 | describe "Any decorator test" do 43 | it_is_a_decorator_test 44 | end 45 | 46 | describe Object do 47 | it_is_not_a_decorator_test 48 | end 49 | 50 | describe "Nope" do 51 | it_is_not_a_decorator_test 52 | end 53 | -------------------------------------------------------------------------------- /spec/dummy/test/decorators/minitest/view_context_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest_helper' 2 | 3 | def it_does_not_leak_view_context 4 | 2.times do 5 | it "has an independent view context" do 6 | refute_equal :leaked, Draper::ViewContext.current 7 | Draper::ViewContext.current = :leaked 8 | end 9 | end 10 | end 11 | 12 | describe "A decorator test" do 13 | it_does_not_leak_view_context 14 | end 15 | 16 | describe "A controller decorator test" do 17 | subject { Class.new(ActionController::Base) } 18 | 19 | it_does_not_leak_view_context 20 | end 21 | 22 | describe "A mailer decorator test" do 23 | it_does_not_leak_view_context 24 | end 25 | -------------------------------------------------------------------------------- /spec/dummy/test/decorators/test_unit/devise_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if defined?(Devise) 4 | class DeviseTest < Draper::TestCase 5 | def test_sign_in_a_real_user 6 | user = User.new 7 | sign_in user 8 | 9 | assert_same user, helper.current_user 10 | end 11 | 12 | def test_sign_in_a_mock_user 13 | user = Object.new 14 | sign_in :user, user 15 | 16 | assert_same user, helper.current_user 17 | end 18 | 19 | def test_sign_in_a_real_admin 20 | admin = Admin.new 21 | sign_in admin 22 | 23 | assert_same admin, helper.current_admin 24 | end 25 | 26 | def test_sign_in_a_mock_admin 27 | admin = Object.new 28 | sign_in :admin, admin 29 | 30 | assert_same admin, helper.current_admin 31 | end 32 | 33 | def test_sign_out_a_real_user 34 | user = User.new 35 | sign_in user 36 | sign_out user 37 | 38 | assert helper.current_user.nil? 39 | end 40 | 41 | def test_sign_out_a_mock_user 42 | user = Object.new 43 | sign_in :user, user 44 | sign_out :user 45 | 46 | assert helper.current_user.nil? 47 | end 48 | 49 | def test_sign_out_without_a_user 50 | sign_out :user 51 | 52 | assert helper.current_user.nil? 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/dummy/test/decorators/test_unit/helpers_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class HelpersTest < Draper::TestCase 4 | def test_access_helpers_through_helper 5 | assert_equal "

Help!

", helper.content_tag(:p, "Help!") 6 | end 7 | 8 | def test_access_helpers_through_helpers 9 | assert_equal "

Help!

", helpers.content_tag(:p, "Help!") 10 | end 11 | 12 | def test_access_helpers_through_h 13 | assert_equal "

Help!

", h.content_tag(:p, "Help!") 14 | end 15 | 16 | def test_same_helper_object_as_decorators 17 | decorator = Draper::Decorator.new(Object.new) 18 | 19 | assert_same decorator.helpers, helpers 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy/test/decorators/test_unit/view_context_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | def it_does_not_leak_view_context 4 | 2.times do |n| 5 | define_method("test_has_independent_view_context_#{n}") do 6 | refute_equal :leaked, Draper::ViewContext.current 7 | Draper::ViewContext.current = :leaked 8 | end 9 | end 10 | end 11 | 12 | class DecoratorTest < Draper::TestCase 13 | it_does_not_leak_view_context 14 | end 15 | 16 | class ControllerTest < ActionController::TestCase 17 | subject{ Class.new(ActionController::Base) } 18 | 19 | it_does_not_leak_view_context 20 | end 21 | 22 | class MailerTest < ActionMailer::TestCase 23 | it_does_not_leak_view_context 24 | end 25 | -------------------------------------------------------------------------------- /spec/dummy/test/minitest_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'minitest/rails' 3 | -------------------------------------------------------------------------------- /spec/dummy/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | -------------------------------------------------------------------------------- /spec/generators/controller/controller_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dummy/config/environment' 3 | require 'ammeter/init' 4 | require 'generators/controller_override' 5 | require 'generators/rails/decorator_generator' 6 | SimpleCov.command_name 'test:generator' 7 | 8 | describe Rails::Generators::ControllerGenerator do 9 | destination File.expand_path("../tmp", __FILE__) 10 | 11 | before { prepare_destination } 12 | after(:all) { FileUtils.rm_rf destination_root } 13 | 14 | describe "the generated decorator" do 15 | subject { file("app/decorators/your_model_decorator.rb") } 16 | 17 | describe "naming" do 18 | before { run_generator %w(YourModels) } 19 | 20 | it { is_expected.to contain "class YourModelDecorator" } 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/generators/decorator/decorator_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dummy/config/environment' 3 | require 'ammeter/init' 4 | require 'generators/rails/decorator_generator' 5 | 6 | describe Rails::Generators::DecoratorGenerator do 7 | destination File.expand_path("../tmp", __FILE__) 8 | 9 | before { prepare_destination } 10 | after(:all) { FileUtils.rm_rf destination_root } 11 | 12 | describe "the generated decorator" do 13 | subject { file("app/decorators/your_model_decorator.rb") } 14 | 15 | describe "naming" do 16 | before { run_generator %w(YourModel) } 17 | 18 | it { is_expected.to contain "class YourModelDecorator" } 19 | end 20 | 21 | describe "namespacing" do 22 | subject { file("app/decorators/namespace/your_model_decorator.rb") } 23 | before { run_generator %w(Namespace::YourModel) } 24 | 25 | it { is_expected.to contain "class Namespace::YourModelDecorator" } 26 | end 27 | 28 | describe "inheritance" do 29 | context "by default" do 30 | before { run_generator %w(YourModel) } 31 | 32 | it { is_expected.to contain "class YourModelDecorator < Draper::Decorator" } 33 | end 34 | 35 | context "with the --parent option" do 36 | before { run_generator %w(YourModel --parent=FooDecorator) } 37 | 38 | it { is_expected.to contain "class YourModelDecorator < FooDecorator" } 39 | end 40 | 41 | context "with an ApplicationDecorator" do 42 | before do 43 | allow_any_instance_of(Object).to receive(:require).and_call_original 44 | allow_any_instance_of(Object).to receive(:require).with("application_decorator").and_return( 45 | stub_const "ApplicationDecorator", Class.new 46 | ) 47 | end 48 | 49 | before { run_generator %w(YourModel) } 50 | 51 | it { is_expected.to contain "class YourModelDecorator < ApplicationDecorator" } 52 | end 53 | end 54 | end 55 | 56 | context "with -t=rspec" do 57 | describe "the generated spec" do 58 | subject { file("spec/decorators/your_model_decorator_spec.rb") } 59 | 60 | describe "naming" do 61 | before { run_generator %w(YourModel -t=rspec) } 62 | 63 | it { is_expected.to contain "describe YourModelDecorator" } 64 | end 65 | 66 | describe "namespacing" do 67 | subject { file("spec/decorators/namespace/your_model_decorator_spec.rb") } 68 | before { run_generator %w(Namespace::YourModel -t=rspec) } 69 | 70 | it { is_expected.to contain "describe Namespace::YourModelDecorator" } 71 | end 72 | end 73 | end 74 | 75 | context "with -t=test_unit" do 76 | describe "the generated test" do 77 | subject { file("test/decorators/your_model_decorator_test.rb") } 78 | 79 | describe "naming" do 80 | before { run_generator %w(YourModel -t=test_unit) } 81 | 82 | it { is_expected.to contain "class YourModelDecoratorTest < Draper::TestCase" } 83 | end 84 | 85 | describe "namespacing" do 86 | subject { file("test/decorators/namespace/your_model_decorator_test.rb") } 87 | before { run_generator %w(Namespace::YourModel -t=test_unit) } 88 | 89 | it { is_expected.to contain "class Namespace::YourModelDecoratorTest < Draper::TestCase" } 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/generators/install/install_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'dummy/config/environment' 3 | require 'ammeter/init' 4 | require 'generators/draper/install_generator' 5 | 6 | describe Draper::Generators::InstallGenerator do 7 | destination File.expand_path('../tmp', __FILE__) 8 | 9 | before { prepare_destination } 10 | after(:all) { FileUtils.rm_rf destination_root } 11 | 12 | describe 'the application decorator' do 13 | subject { file('app/decorators/application_decorator.rb') } 14 | 15 | before { run_generator } 16 | 17 | it { is_expected.to contain 'class ApplicationDecorator' } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/integration/integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'support/dummy_app' 3 | require 'support/matchers/have_text' 4 | SimpleCov.command_name 'test:integration' 5 | 6 | app = DummyApp.new(ENV["RAILS_ENV"]) 7 | spec_types = { 8 | view: ["/posts/1", "PostsController"], 9 | mailer: ["/posts/1/mail", "PostMailer"] 10 | } 11 | 12 | app.start_server do 13 | spec_types.each do |type, (path, controller)| 14 | page = app.get(path) 15 | 16 | describe "in a #{type}" do 17 | it "runs in the correct environment" do 18 | expect(page).to have_text(app.environment).in("#environment") 19 | end 20 | 21 | it "uses the correct view context controller" do 22 | expect(page).to have_text(controller).in("#controller") 23 | end 24 | 25 | it "can use built-in helpers" do 26 | expect(page).to have_text("Once upon a...").in("#truncated") 27 | end 28 | 29 | it "can use built-in private helpers" do 30 | # Nokogiri unescapes text! 31 | expect(page).to have_text("").in("#html_escaped") 32 | end 33 | 34 | it "can use user-defined helpers from app/helpers" do 35 | expect(page).to have_text("Hello, world!").in("#hello_world") 36 | end 37 | 38 | it "can use user-defined helpers from the controller" do 39 | expect(page).to have_text("Goodnight, moon!").in("#goodnight_moon") 40 | end 41 | 42 | # _path helpers aren't available in mailers 43 | if type == :view 44 | it "can be passed to path helpers" do 45 | expect(page).to have_text("/en/posts/1").in("#path_with_decorator") 46 | end 47 | 48 | it "can use path helpers with a model" do 49 | expect(page).to have_text("/en/posts/1").in("#path_with_model") 50 | end 51 | 52 | it "can use path helpers with an id" do 53 | expect(page).to have_text("/en/posts/1").in("#path_with_id") 54 | end 55 | end 56 | 57 | it "can be passed to url helpers" do 58 | expect(page).to have_text("http://www.example.com:12345/en/posts/1").in("#url_with_decorator") 59 | end 60 | 61 | it "can use url helpers with a model" do 62 | expect(page).to have_text("http://www.example.com:12345/en/posts/1").in("#url_with_model") 63 | end 64 | 65 | it "can use url helpers with an id" do 66 | expect(page).to have_text("http://www.example.com:12345/en/posts/1").in("#url_with_id") 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/performance/active_record.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | class Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/performance/benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 4 | Bundler.require :default 5 | 6 | require "benchmark" 7 | require "draper" 8 | require "./performance/models" 9 | require "./performance/decorators" 10 | 11 | Benchmark.bm do |bm| 12 | puts "\n[ Exclusivelly using #method_missing for model delegation ]" 13 | [ 1_000, 10_000, 100_000 ].each do |i| 14 | puts "\n[ #{i} ]" 15 | bm.report("#new ") do 16 | i.times do |n| 17 | ProductDecorator.decorate(Product.new) 18 | end 19 | end 20 | 21 | bm.report("#hello_world ") do 22 | i.times do |n| 23 | ProductDecorator.decorate(Product.new).hello_world 24 | end 25 | end 26 | 27 | bm.report("#sample_class_method ") do 28 | i.times do |n| 29 | ProductDecorator.decorate(Product.new).class.sample_class_method 30 | end 31 | end 32 | end 33 | 34 | puts "\n[ Defining methods on method_missing first hit ]" 35 | [ 1_000, 10_000, 100_000 ].each do |i| 36 | puts "\n[ #{i} ]" 37 | bm.report("#new ") do 38 | i.times do |n| 39 | FastProductDecorator.decorate(FastProduct.new) 40 | end 41 | end 42 | 43 | bm.report("#hello_world ") do 44 | i.times do |n| 45 | FastProductDecorator.decorate(FastProduct.new).hello_world 46 | end 47 | end 48 | 49 | bm.report("#sample_class_method ") do 50 | i.times do |n| 51 | FastProductDecorator.decorate(FastProduct.new).class.sample_class_method 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/performance/decorators.rb: -------------------------------------------------------------------------------- 1 | require "./performance/models" 2 | class ProductDecorator < Draper::Decorator 3 | 4 | def awesome_title 5 | "Awesome Title" 6 | end 7 | 8 | # Original #method_missing 9 | def method_missing(method, *args, &block) 10 | if allow?(method) 11 | begin 12 | model.send(method, *args, &block) 13 | rescue NoMethodError 14 | super 15 | end 16 | else 17 | super 18 | end 19 | end 20 | 21 | end 22 | 23 | class FastProductDecorator < Draper::Decorator 24 | 25 | def awesome_title 26 | "Awesome Title" 27 | end 28 | 29 | # Modified #method_missing 30 | def method_missing(method, *args, &block) 31 | if allow?(method) 32 | begin 33 | self.class.send :define_method, method do |*args, &block| 34 | model.send(method, *args, &block) 35 | end 36 | self.send(method, *args, &block) 37 | rescue NoMethodError 38 | super 39 | end 40 | else 41 | super 42 | end 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /spec/performance/models.rb: -------------------------------------------------------------------------------- 1 | require "./performance/active_record" 2 | class Product < ActiveRecord::Base 3 | def self.sample_class_method 4 | "sample class method" 5 | end 6 | 7 | def hello_world 8 | "Hello, World" 9 | end 10 | end 11 | 12 | class FastProduct < ActiveRecord::Base 13 | def self.sample_class_method 14 | "sample class method" 15 | end 16 | 17 | def hello_world 18 | "Hello, World" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start do 3 | add_filter 'spec' 4 | add_group 'Draper', 'lib/draper' 5 | add_group 'Generators', 'lib/generators' 6 | end 7 | 8 | require 'bundler/setup' 9 | require 'draper' 10 | require 'action_controller' 11 | require 'action_controller/test_case' 12 | 13 | RSpec.configure do |config| 14 | config.expect_with(:rspec) {|c| c.syntax = :expect} 15 | config.order = :random 16 | config.mock_with :rspec do |mocks| 17 | mocks.yield_receiver_to_any_instance_implementation_blocks = true 18 | end 19 | end 20 | 21 | class Model; include Draper::Decoratable; end 22 | 23 | class Product < Model; end 24 | class SpecialProduct < Product; end 25 | class Other < Model; end 26 | class Person < Model; end 27 | class Child < Person; end 28 | class ProductDecorator < Draper::Decorator; end 29 | class ProductsDecorator < Draper::CollectionDecorator; end 30 | 31 | class OtherDecorator < Draper::Decorator; end 32 | 33 | module Namespaced 34 | class Product < Model; end 35 | class ProductDecorator < Draper::Decorator; end 36 | ProductsDecorator = Class.new(Draper::CollectionDecorator) 37 | class OtherDecorator < Draper::Decorator; end 38 | end 39 | 40 | ApplicationController = Class.new(ActionController::Base) 41 | CustomController = Class.new(ActionController::Base) 42 | 43 | # After each example, revert changes made to the class 44 | def protect_class(klass) 45 | before { stub_const klass.name, Class.new(klass) } 46 | end 47 | 48 | def protect_module(mod) 49 | before { stub_const mod.name, mod.dup } 50 | end 51 | -------------------------------------------------------------------------------- /spec/support/dummy_app.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'net/http' 3 | 4 | # Adapted from code by Jon Leighton 5 | # https://github.com/jonleighton/focused_controller/blob/ec7ccf1/test/acceptance/app_test.rb 6 | 7 | class DummyApp 8 | 9 | def initialize(environment) 10 | raise ArgumentError, "Environment must be development or production" unless ["development", "production"].include?(environment.to_s) 11 | @environment = environment 12 | end 13 | 14 | attr_reader :environment 15 | 16 | def url 17 | "http://#{localhost}:#{port}" 18 | end 19 | 20 | def get(path) 21 | Net::HTTP.get(URI(url + path)) 22 | end 23 | 24 | def within_app(&block) 25 | Dir.chdir(root, &block) 26 | end 27 | 28 | def start_server 29 | within_app do 30 | IO.popen("bundle exec rails s -e #{@environment} -p #{port} 2>&1") do |out| 31 | start = Time.now 32 | started = false 33 | output = "" 34 | timeout = 60.0 35 | 36 | while !started && !out.eof? && Time.now - start <= timeout 37 | output << read_output(out) 38 | sleep 0.1 39 | 40 | begin 41 | TCPSocket.new(localhost, port) 42 | rescue Errno::ECONNREFUSED 43 | else 44 | started = true 45 | end 46 | end 47 | 48 | raise "Server failed to start:\n#{output}" unless started 49 | 50 | yield 51 | 52 | Process.kill("KILL", out.pid) 53 | File.delete("tmp/pids/server.pid") if File.exist?("tmp/pids/server.pid") 54 | end 55 | end 56 | end 57 | 58 | private 59 | 60 | def root 61 | File.expand_path("../../dummy", __FILE__) 62 | end 63 | 64 | def localhost 65 | "127.0.0.1" 66 | end 67 | 68 | def port 69 | @port ||= begin 70 | server = TCPServer.new(localhost, 0) 71 | server.addr[1] 72 | ensure 73 | server.close if server 74 | end 75 | end 76 | 77 | def read_output(stream) 78 | read = IO.select([stream], [], [stream], 0.1) 79 | output = "" 80 | loop { output << stream.read_nonblock(1024) } if read 81 | output 82 | rescue Errno::EAGAIN, Errno::EWOULDBLOCK, EOFError 83 | output 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/support/matchers/have_text.rb: -------------------------------------------------------------------------------- 1 | require 'capybara' 2 | 3 | module HaveTextMatcher 4 | def have_text(text) 5 | HaveText.new(text) 6 | end 7 | 8 | class HaveText 9 | def initialize(text) 10 | @text = text 11 | end 12 | 13 | def in(css) 14 | @css = css 15 | self 16 | end 17 | 18 | def matches?(subject) 19 | @subject = Capybara.string(subject) 20 | 21 | @subject.has_css?(@css || "*", text: @text) 22 | end 23 | 24 | def failure_message 25 | "expected to find #{@text.inspect} #{within}" 26 | end 27 | 28 | def failure_message_when_negated 29 | "expected not to find #{@text.inspect} #{within}" 30 | end 31 | 32 | private 33 | 34 | def within 35 | if @css && @subject.has_css?(@css) 36 | "within\n#{@subject.find(@css).native}" 37 | else 38 | "#{inside} within\n#{@subject.native}" 39 | end 40 | end 41 | 42 | def inside 43 | @css ? "inside #{@css.inspect}" : "anywhere" 44 | end 45 | end 46 | end 47 | 48 | RSpec.configure do |config| 49 | config.include HaveTextMatcher 50 | end 51 | -------------------------------------------------------------------------------- /spec/support/shared_examples/decoratable_equality.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for "decoration-aware #==" do |subject| 2 | it "is true for itself" do 3 | expect(subject == subject).to be_truthy 4 | end 5 | 6 | it "is false for another object" do 7 | expect(subject == Object.new).to be_falsey 8 | end 9 | 10 | it "is true for a decorated version of itself" do 11 | decorated = double(object: subject, decorated?: true) 12 | 13 | expect(subject == decorated).to be_truthy 14 | end 15 | 16 | it "is false for a decorated other object" do 17 | decorated = double(object: Object.new, decorated?: true) 18 | 19 | expect(subject == decorated).to be_falsey 20 | end 21 | 22 | it "is false for a decoratable object with a `object` association" do 23 | decoratable = double(object: subject, decorated?: false) 24 | 25 | expect(subject == decoratable).to be_falsey 26 | end 27 | 28 | it "is false for an undecoratable object with a `object` association" do 29 | undecoratable = double(object: subject) 30 | 31 | expect(subject == undecoratable).to be_falsey 32 | end 33 | 34 | it "is true for a multiply-decorated version of itself" do 35 | decorated = double(object: subject, decorated?: true) 36 | redecorated = double(object: decorated, decorated?: true) 37 | 38 | expect(subject == redecorated).to be_truthy 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/support/shared_examples/view_helpers.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for "view helpers" do |subject| 2 | describe "#helpers" do 3 | it "returns the current view context" do 4 | allow(Draper::ViewContext).to receive_messages current: :current_view_context 5 | expect(subject.helpers).to be :current_view_context 6 | end 7 | 8 | it "is aliased to #h" do 9 | allow(Draper::ViewContext).to receive_messages current: :current_view_context 10 | expect(subject.h).to be :current_view_context 11 | end 12 | end 13 | 14 | describe "#localize" do 15 | it "delegates to #helpers" do 16 | allow(subject).to receive(:helpers).and_return(double) 17 | expect(subject.helpers).to receive(:localize).with(:an_object, some: "parameter") 18 | subject.localize(:an_object, some: "parameter") 19 | end 20 | 21 | it "is aliased to #l" do 22 | allow(subject).to receive_messages helpers: double 23 | expect(subject.helpers).to receive(:localize).with(:an_object, some: "parameter") 24 | subject.l(:an_object, some: "parameter") 25 | end 26 | end 27 | 28 | describe ".helpers" do 29 | it "returns the current view context" do 30 | allow(Draper::ViewContext).to receive_messages current: :current_view_context 31 | expect(subject.class.helpers).to be :current_view_context 32 | end 33 | 34 | it "is aliased to .h" do 35 | allow(Draper::ViewContext).to receive(:current).and_return(:current_view_context) 36 | expect(subject.class.h).to be :current_view_context 37 | end 38 | end 39 | end 40 | --------------------------------------------------------------------------------