├── .gitignore
├── .rspec
├── .rubocop.yml
├── .travis.yml
├── Appraisals
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
├── console
├── git-hooks
│ └── pre-commit
├── install_git_hooks
└── setup
├── gemfiles
├── rails_4.gemfile
├── rails_5_0.gemfile
└── rails_5_1.gemfile
├── lib
├── assets
│ ├── javascripts
│ │ └── rails_stuff
│ │ │ └── plugin_manager.coffee
│ └── stylesheets
│ │ └── rails_stuff
│ │ └── _media_queries.scss
├── net
│ └── http
│ │ └── debug.rb
├── rails_stuff.rb
└── rails_stuff
│ ├── association_writer.rb
│ ├── engine.rb
│ ├── generators
│ └── concern
│ │ ├── USAGE
│ │ ├── concern_generator.rb
│ │ └── templates
│ │ └── concern.rb
│ ├── helpers.rb
│ ├── helpers
│ ├── all.rb
│ ├── bootstrap.rb
│ ├── forms.rb
│ ├── links.rb
│ ├── resource_form.rb
│ ├── text.rb
│ └── translation.rb
│ ├── nullify_blank_attrs.rb
│ ├── params_parser.rb
│ ├── random_uniq_attr.rb
│ ├── redis_storage.rb
│ ├── require_nested.rb
│ ├── resources_controller.rb
│ ├── resources_controller
│ ├── actions.rb
│ ├── basic_helpers.rb
│ ├── belongs_to.rb
│ ├── has_scope_helpers.rb
│ ├── kaminari_helpers.rb
│ ├── resource_helper.rb
│ └── sti_helpers.rb
│ ├── responders.rb
│ ├── responders
│ └── turbolinks.rb
│ ├── rspec_helpers.rb
│ ├── rspec_helpers
│ ├── concurrency.rb
│ ├── groups
│ │ ├── feature.rb
│ │ └── request.rb
│ ├── matchers
│ │ ├── be_valid_js.rb
│ │ └── redirect_with_turbolinks.rb
│ └── signinable.rb
│ ├── sort_scope.rb
│ ├── statusable.rb
│ ├── statusable
│ ├── builder.rb
│ ├── helper.rb
│ ├── mapped_builder.rb
│ └── mapped_helper.rb
│ ├── strong_parameters.rb
│ ├── test_helpers.rb
│ ├── test_helpers
│ ├── concurrency.rb
│ ├── integration_session.rb
│ └── response.rb
│ ├── transform_attrs.rb
│ ├── types_tracker.rb
│ ├── url_for_keeping_params.rb
│ └── version.rb
├── rails_stuff.gemspec
└── spec
├── integration
└── requests
│ └── site
│ ├── forms_spec.rb
│ ├── projects_spec.rb
│ └── users_spec.rb
├── integration_helper.rb
├── rails_helper.rb
├── rails_stuff
├── association_writer_spec.rb
├── generators
│ └── concern_generator_spec.rb
├── helpers
│ ├── bootstrap_spec.rb
│ ├── forms_spec.rb
│ ├── links_spec.rb
│ ├── resource_form_spec.rb
│ ├── text_spec.rb
│ └── translation_spec.rb
├── nullify_blank_attrs_spec.rb
├── params_parser_spec.rb
├── random_uniq_attr_spec.rb
├── redis_storage_spec.rb
├── resources_controller
│ ├── actions_spec.rb
│ ├── basic_helpers_spec.rb
│ ├── resource_helper_spec.rb
│ └── sti_helpers_spec.rb
├── resources_controller_spec.rb
├── sort_scope_spec.rb
├── statusable
│ ├── builder_spec.rb
│ ├── helper_spec.rb
│ ├── mapped_builder_spec.rb
│ └── mapped_helper_spec.rb
├── statusable_spec.rb
├── strong_parameters_spec.rb
├── test_helpers
│ └── concurrency_spec.rb
├── transform_attrs_spec.rb
├── types_tracker_spec.rb
└── url_for_keeping_params_spec.rb
├── rails_stuff_spec.rb
├── spec_helper.rb
└── support
├── active_record.rb
├── app
└── views
│ └── site
│ ├── forms
│ └── index.html.erb
│ ├── projects
│ ├── edit.html.erb
│ └── index.html.erb
│ └── users
│ ├── edit.html.erb
│ ├── index.html.erb
│ ├── new.html.erb
│ └── show.html.erb
├── controller.rb
├── redis.rb
├── schema.rb
└── shared
└── statusable.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /gemfiles/*.lock
5 | /_yardoc/
6 | /coverage/
7 | /doc/
8 | /pkg/
9 | /spec/reports/
10 | /tmp/
11 | /log/
12 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | Exclude:
3 | - lib/rails_stuff/generators/*/templates/*.rb
4 | - tmp/**/*
5 | Rails: {Enabled: true}
6 |
7 | Layout/AlignParameters:
8 | # Disable, till rubocop supports combination of styles.
9 | # Use one of this styles where appropriate, keep it clean, compact and readable.
10 | Enabled: false
11 | # EnforcedStyle:
12 | # - with_first_parameter
13 | # - with_fixed_indentation
14 |
15 | # Breaks
16 | #
17 | # I18n.t(key,
18 | # param: val,
19 | # # ...
20 | # )
21 | Layout/ClosingParenthesisIndentation: {Enabled: false}
22 | Layout/DotPosition: {EnforcedStyle: trailing}
23 | # Same as Layout/ClosingParenthesisIndentation
24 | Layout/MultilineMethodCallBraceLayout: {Enabled: false}
25 | Layout/MultilineMethodCallIndentation: {EnforcedStyle: indented}
26 | Layout/MultilineOperationIndentation: {EnforcedStyle: indented}
27 | Layout/SpaceInsideHashLiteralBraces: {EnforcedStyle: no_space}
28 |
29 | # Offences named scopes and `expect {}.to change {}`.
30 | Lint/AmbiguousBlockAssociation: {Enabled: false}
31 |
32 | # We use it in describe messages.
33 | Lint/InterpolationCheck:
34 | Exclude:
35 | - spec/**/*
36 |
37 | Naming/PredicateName: {Enabled: false}
38 | Naming/VariableNumber: {EnforcedStyle: snake_case}
39 |
40 | Style/Alias: {Enabled: false}
41 | Style/Documentation: {Enabled: false}
42 | Style/IfUnlessModifier: {Enabled: false}
43 | Style/GuardClause: {Enabled: false}
44 | Style/Lambda: {Enabled: false}
45 | Style/ModuleFunction: {Enabled: false}
46 | Style/NestedParenthesizedCalls: {Enabled: false}
47 | Style/SignalException: {EnforcedStyle: only_raise}
48 | Style/TrailingCommaInArguments: {Enabled: false}
49 | Style/TrailingCommaInLiteral: {EnforcedStyleForMultiline: comma}
50 |
51 | Metrics/AbcSize: {Max: 21}
52 | # Other metrics are just enough.
53 | # This one offences all specs, routes and some initializers.
54 | Metrics/BlockLength: {Enabled: false}
55 | Metrics/LineLength: {Max: 100}
56 | Metrics/MethodLength: {Max: 30}
57 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | cache: bundler
3 | rvm:
4 | - 2.2.3
5 | gemfile:
6 | - gemfiles/rails_4.gemfile
7 | - gemfiles/rails_5_0.gemfile
8 | - gemfiles/rails_5_1.gemfile
9 | services:
10 | - redis
11 | notifications:
12 | email: false
13 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise 'rails-4' do
2 | group :development do
3 | gem 'rails', '~> 4.0'
4 | end
5 | end
6 |
7 | appraise 'rails-5_0' do
8 | group :development do
9 | gem 'rails', '~> 5.0.0'
10 | gem 'rails-controller-testing', '~> 0.1.1'
11 | end
12 | end
13 |
14 | appraise 'rails-5_1' do
15 | group :development do
16 | gem 'rails', '~> 5.1'
17 | gem 'rails-controller-testing', '~> 0.1.1'
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Unreleased
2 |
3 | ### Controllers
4 |
5 | - `resource_params` returns permited hash when resource key is not present in `params`.
6 | - `use_resource_class_for_invalid_type` option for STI helpers,
7 | and `RailsStuff::ResourcesController::StiHelpers::InvalidType` error instead of
8 | `ActiveRecord::NotFound` for invalid type.
9 |
10 | ### Models
11 |
12 | - `TransformAttrs` in favour of `NullifyBlankAttrs` (deprecated).
13 |
14 | ### Misc
15 |
16 | - Parse BigDecimals with ParamsParser.
17 | - ParamsParser parses empty strings to `nil` (except `parse_string`).
18 |
19 |
20 | ## 0.6.0.rc1
21 |
22 | ### Controllers
23 |
24 | - Don't overwrite :location option if present.
25 | - Don't overwrite responder and `respond_with`'s mimes.
26 | - `#current_sort_scope` to access applied sorting rules.
27 | - `.resource_helper` has `source` option, which accepts lambdas and strings.
28 | `class` option is deprecated now in favour of `source`.
29 |
30 | ### Models
31 |
32 | - Improved Statusable:
33 |
34 | - Model are now clear from lot of helper methods.
35 | There is single `statuses` method for statusable field which holds all helpers.
36 | - `.select_options` supports `:only` option.
37 | - Helpers to map/unmap values for mapped statuses from external code.
38 |
39 | ### Tests
40 |
41 | - A lot of new test and rspec helpers and configs (see RSpecHelpers, TestHelpers).
42 | - `RSpecHelpers.setup` & `TestHelpers.setup` to setup helpers instead of requiring
43 | files. This methods accepts `only` & `except` options.
44 |
45 | ### Misc
46 |
47 | - Use AR#update instead of #update_attributes.
48 |
49 | ## 0.5.1
50 |
51 | Rails 5 support.
52 |
53 | ## 0.5.0
54 |
55 | ### Controllers
56 |
57 | - `belongs_to`.
58 | - `resource_helper` generates enquirer method.
59 | - `resources_controller kaminari: false` to skip kaminari for the only controller.
60 | - `has_sort_scope` can use custom order method.
61 | - Fix: removed source_for_collection from action_methods.
62 |
63 | ### Models
64 |
65 | - Statusable supports mappings (store status as integer) and suffixes.
66 | - AssociationWriter to override `#field=` & `#field_id=` in single instruction.
67 | - Limit retries count for RandomUniqAttr (default to 10).
68 |
69 | ### Helpers
70 |
71 | - `Helpers::Translation` methods can raise errors on missing translations.
72 | It respects app's `raise_on_missing_translations`, and can be configured manually.
73 |
74 | ### Tests
75 |
76 | - Concurrency helper.
77 | - RSpec configurator.
78 |
79 | Misc
80 |
81 | - RequireNested to require all files in subdirectory.
82 | - `rails g concern %parent%/%module%` generator for concerns.
83 |
84 | ## 0.4.0
85 |
86 | - TypesTracker defines scopes for every type.
87 |
88 | ## 0.3.0
89 |
90 | - PluginManager & media queries.
91 |
92 | ## 0.2.0
93 |
94 | - Bypass block to `respond_with`.
95 |
96 | - `url_for_keeping_params`
97 |
98 | - `params.require_permitted`
99 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gemspec
3 |
4 | group :development do
5 | gem 'appraisal', '~> 2.1.0'
6 | gem 'rails'
7 |
8 | gem 'pry', '~> 0.10.1'
9 | gem 'pry-byebug', '~> 3.2.0'
10 | gem 'sdoc', '~> 0.4.1'
11 |
12 | gem 'activemodel_translation', '~> 0.1.0'
13 | gem 'database_cleaner', '~> 1.5.0'
14 | gem 'has_scope', '~> 0.7.0'
15 | gem 'hashie', '~> 3.4.0'
16 | gem 'kaminari', '~> 0.16.3'
17 | gem 'pooled_redis', '~> 0.2.1'
18 | gem 'responders', '~> 2.1'
19 | gem 'sqlite3', '~> 1.3.10'
20 |
21 | gem 'rspec-its', '~> 1.2.0'
22 | gem 'rspec-rails', '~> 3.5'
23 |
24 | gem 'rubocop', '~> 0.51.0'
25 |
26 | gem 'coveralls', '~> 0.8.2', require: false
27 | end
28 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Max Melentiev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 | require 'rspec/core/rake_task'
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task default: :spec
7 |
8 | require 'sdoc'
9 | RDoc::Task.new(:doc) do |rdoc|
10 | rdoc.rdoc_dir = 'doc'
11 |
12 | rdoc.title = 'RailsStuff'
13 |
14 | rdoc.options << '--markup' << 'markdown'
15 | rdoc.options << '-e' << 'UTF-8'
16 | rdoc.options << '--format' << 'sdoc'
17 | rdoc.options << '--template' << 'rails'
18 | rdoc.options << '--all'
19 |
20 | rdoc.rdoc_files.include('README.md')
21 | rdoc.rdoc_files.include('lib/**/*.rb')
22 | end
23 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'bundler/setup'
4 | require 'rails_stuff'
5 |
6 | require 'pry'
7 | Pry.start
8 |
--------------------------------------------------------------------------------
/bin/git-hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | pattern=$(echo -n '\.rb
4 | \.gemspec
5 | \.jbuilder
6 | \.rake
7 | config\.ru
8 | Gemfile
9 | Rakefile' | tr "\\n" '|')
10 |
11 | files=`git diff --cached --name-status | grep -E "^[AM].*($pattern)$" | cut -f2-`
12 | if [ -n "$files" ]; then
13 | bundle exec rubocop $files --force-exclusion
14 | fi
15 |
--------------------------------------------------------------------------------
/bin/install_git_hooks:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | root = File.expand_path('../../', __FILE__)
4 | hooks_dir = "#{root}/bin/git-hooks"
5 |
6 | `ls -1 #{hooks_dir}`.each_line.map(&:strip).each do |file|
7 | `ln -sf #{hooks_dir}/#{file} #{root}/.git/hooks/#{file}`
8 | end
9 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 |
5 | bundle install
6 | appraisal install
7 | bin/install_git_hooks
8 |
9 | # Do any other automated setup that you need to do here
10 |
--------------------------------------------------------------------------------
/gemfiles/rails_4.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | group :development do
6 | gem "appraisal", "~> 2.1.0"
7 | gem "rails", "~> 4.0"
8 | gem "pry", "~> 0.10.1"
9 | gem "pry-byebug", "~> 3.2.0"
10 | gem "sdoc", "~> 0.4.1"
11 | gem "activemodel_translation", "~> 0.1.0"
12 | gem "database_cleaner", "~> 1.5.0"
13 | gem "has_scope", "~> 0.7.0"
14 | gem "hashie", "~> 3.4.0"
15 | gem "kaminari", "~> 0.16.3"
16 | gem "pooled_redis", "~> 0.2.1"
17 | gem "responders", "~> 2.1"
18 | gem "sqlite3", "~> 1.3.10"
19 | gem "rspec-its", "~> 1.2.0"
20 | gem "rspec-rails", "~> 3.5"
21 | gem "rubocop", "~> 0.51.0"
22 | gem "coveralls", "~> 0.8.2", :require => false
23 | end
24 |
25 | gemspec :path => "../"
26 |
--------------------------------------------------------------------------------
/gemfiles/rails_5_0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | group :development do
6 | gem "appraisal", "~> 2.1.0"
7 | gem "rails", "~> 5.0.0"
8 | gem "pry", "~> 0.10.1"
9 | gem "pry-byebug", "~> 3.2.0"
10 | gem "sdoc", "~> 0.4.1"
11 | gem "activemodel_translation", "~> 0.1.0"
12 | gem "database_cleaner", "~> 1.5.0"
13 | gem "has_scope", "~> 0.7.0"
14 | gem "hashie", "~> 3.4.0"
15 | gem "kaminari", "~> 0.16.3"
16 | gem "pooled_redis", "~> 0.2.1"
17 | gem "responders", "~> 2.1"
18 | gem "sqlite3", "~> 1.3.10"
19 | gem "rspec-its", "~> 1.2.0"
20 | gem "rspec-rails", "~> 3.5"
21 | gem "rubocop", "~> 0.51.0"
22 | gem "coveralls", "~> 0.8.2", :require => false
23 | gem "rails-controller-testing", "~> 0.1.1"
24 | end
25 |
26 | gemspec :path => "../"
27 |
--------------------------------------------------------------------------------
/gemfiles/rails_5_1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | group :development do
6 | gem "appraisal", "~> 2.1.0"
7 | gem "rails", "~> 5.1"
8 | gem "pry", "~> 0.10.1"
9 | gem "pry-byebug", "~> 3.2.0"
10 | gem "sdoc", "~> 0.4.1"
11 | gem "activemodel_translation", "~> 0.1.0"
12 | gem "database_cleaner", "~> 1.5.0"
13 | gem "has_scope", "~> 0.7.0"
14 | gem "hashie", "~> 3.4.0"
15 | gem "kaminari", "~> 0.16.3"
16 | gem "pooled_redis", "~> 0.2.1"
17 | gem "responders", "~> 2.1"
18 | gem "sqlite3", "~> 1.3.10"
19 | gem "rspec-its", "~> 1.2.0"
20 | gem "rspec-rails", "~> 3.5"
21 | gem "rubocop", "~> 0.51.0"
22 | gem "coveralls", "~> 0.8.2", :require => false
23 | gem "rails-controller-testing", "~> 0.1.1"
24 | end
25 |
26 | gemspec :path => "../"
27 |
--------------------------------------------------------------------------------
/lib/assets/javascripts/rails_stuff/plugin_manager.coffee:
--------------------------------------------------------------------------------
1 | # Provides simple way to create jQuery plugins. Create class and PluginManager
2 | # will create jQuery function for it. It'll create instance of class for each
3 | # jQuery element and prevent calling constructor twice.
4 | #
5 | # PluginManager.add 'myPlugin', class
6 | # constructor: (@$element, @options) ->
7 | # # ...
8 | #
9 | # customAction: (options)->
10 | # # ...
11 | #
12 | # # Add initializers
13 | # $ -> $('[data-my-plugin]').myPlugin()
14 | # # or
15 | # $(document).on 'click', '[data-my-plugin]', (e) ->
16 | # $(@).myPlugin('customAction', event: e)
17 | #
18 | # # Or use it manually
19 | # $('.selector').myPlugin().myPlugin('customAction')
20 | class window.PluginManager
21 | @plugins = {}
22 |
23 | # Takes class and creates jQuery's plugin function for it.
24 | # This function simply creates class instance for each element
25 | # and prevents creating multiple instances on single element.
26 | #
27 | # Name is set explicitly to avoid errors when using uglifier.
28 | @add: (pluginName, klass) ->
29 | data_index = "#{pluginName}.instance"
30 | @plugins[pluginName] = klass
31 | jQuery.fn[pluginName] = (action, options) ->
32 | if typeof action is 'object'
33 | options = action
34 | action = null
35 | @each ->
36 | $this = jQuery @
37 | unless instance = $this.data data_index
38 | instance = new klass $this, options
39 | $this.data data_index, instance
40 | instance[action](options) if action
41 |
--------------------------------------------------------------------------------
/lib/assets/stylesheets/rails_stuff/_media_queries.scss:
--------------------------------------------------------------------------------
1 | // Foundation-style media queries.
2 | // Works with Bootstrap3 out of the box.
3 | // Without bootstrap just define variables:
4 | // screen-xs-max, screen-{sm,md}-{min,max}, screen-lg-min
5 | //
6 | // Usage:
7 | //
8 | // @media #{$sm-up} { ... }
9 | // @media #{$sm-only} and #{$landscape} { ... }
10 | $screen: "only screen" !default;
11 |
12 | $landscape: "(orientation: landscape)" !default;
13 | $portrait: "(orientation: portrait)" !default;
14 |
15 | $xs-up: $screen !default;
16 | $xs-only: "(max-width: #{$screen-xs-max})" !default;
17 |
18 | $sm-up: "(min-width:#{$screen-sm-min})" !default;
19 | $sm-only: "(min-width:#{$screen-sm-min}) and (max-width:#{$screen-sm-max})" !default;
20 |
21 | $md-up: "(min-width:#{$screen-md-min})" !default;
22 | $md-only: "(min-width:#{$screen-md-min}) and (max-width:#{$screen-md-max})" !default;
23 |
24 | $lg-up: "(min-width:#{$screen-lg-min})" !default;
25 | $lg-only: "(min-width:#{$screen-lg-min})" !default;
26 |
--------------------------------------------------------------------------------
/lib/net/http/debug.rb:
--------------------------------------------------------------------------------
1 | require 'net/http'
2 |
3 | class << Net::HTTP
4 | # Redefines `.new` to set debug device for all new instances.
5 | def debug!(out = $stderr)
6 | return if respond_to?(:__new__)
7 | class << self
8 | alias_method :__new__, :new
9 | end
10 |
11 | define_singleton_method :new do |*args, &blk|
12 | instance = __new__(*args, &blk)
13 | instance.set_debug_output(out)
14 | instance
15 | end
16 | end
17 |
18 | # Restores original `.new`.
19 | def disable_debug!
20 | return unless respond_to?(:__new__)
21 | class << self
22 | alias_method :new, :__new__
23 | remove_method :__new__
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/rails_stuff.rb:
--------------------------------------------------------------------------------
1 | require 'rails_stuff/version'
2 | require 'active_support/version'
3 | require 'active_support/dependencies/autoload'
4 |
5 | # Useful stuff for Rails.
6 | module RailsStuff
7 | extend ActiveSupport::Autoload
8 |
9 | autoload :Helpers
10 | autoload :AssociationWriter
11 | autoload :NullifyBlankAttrs
12 | autoload :ParamsParser
13 | autoload :RandomUniqAttr
14 | autoload :RedisStorage
15 | autoload :RequireNested
16 | autoload :ResourcesController
17 | autoload :Responders
18 | autoload :RSpecHelpers, 'rails_stuff/rspec_helpers'
19 | autoload :SortScope
20 | autoload :Statusable
21 | autoload :TestHelpers, 'rails_stuff/test_helpers'
22 | autoload :TransformAttrs
23 | autoload :TypesTracker
24 |
25 | module_function
26 |
27 | def rails_version
28 | @rails_version = ActiveSupport::VERSION
29 | end
30 |
31 | def rails4?
32 | rails_version::MAJOR == 4
33 | end
34 |
35 | def deprecation_07
36 | @deprecation ||= begin
37 | require 'active_support/deprecation'
38 | ActiveSupport::Deprecation.new('0.7', 'RailsStuff')
39 | end
40 | end
41 | end
42 |
43 | require 'rails_stuff/engine' if defined?(Rails::Engine)
44 |
--------------------------------------------------------------------------------
/lib/rails_stuff/association_writer.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | # ActiveRecord's association can be updated with object and by object id.
3 | # Owerwrite this both writers with single instruction:
4 | #
5 | # association_writer :product do |val|
6 | # super(val).tap { update_price if product }
7 | # end
8 | #
9 | module AssociationWriter
10 | def association_writer(name, &block)
11 | define_method("#{name}=", &block)
12 | define_method("#{name}_id=", &block)
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/rails_stuff/engine.rb:
--------------------------------------------------------------------------------
1 | require 'rails/railtie'
2 |
3 | module RailsStuff
4 | MODULES = { # rubocop:disable MutableConstant
5 | require_nested: [:require, -> { RequireNested.setup }],
6 | association_writer: :model,
7 | nullify_blank_attrs: :model,
8 | random_uniq_attr: :model,
9 | statusable: :model,
10 | transform_attrs: :model,
11 | resources_controller: [
12 | -> { defined?(::Responders) && :controller },
13 | -> { ResourcesController.use_kaminari! if defined?(::Kaminari) },
14 | ],
15 | sort_scope: -> { defined?(::HasScope) && :controller },
16 | strong_parameters: -> { defined?(ActionController::Parameters) && :require },
17 | url_for_keeping_params: -> { defined?(ActionDispatch::Routing) && :require },
18 | }
19 |
20 | class << self
21 | # Set it to array of modules to load.
22 | #
23 | # # config/initializers/rails_stuff.rb
24 | # RailsStuff.load_modules = [:statusable, :sort_scope]
25 | attr_accessor :load_modules
26 | # Override default base classes for models & controllers.
27 | attr_writer :base_controller, :base_model
28 |
29 | def base_controller
30 | @base_controller || ActionController::Base
31 | end
32 |
33 | def base_model
34 | @base_model || ActiveRecord::Base
35 | end
36 |
37 | # Extends base controller and model classes with modules.
38 | # By default uses all modules. Use load_modules= to override this list.
39 | def setup_modules!
40 | modules_to_load = load_modules || MODULES.keys
41 | MODULES.slice(*modules_to_load).each do |m, (type, init)|
42 | case type.respond_to?(:call) ? type.call : type
43 | when :controller
44 | RailsStuff.base_controller.extend const_get(m.to_s.camelize)
45 | when :model
46 | RailsStuff.base_model.extend const_get(m.to_s.camelize)
47 | when :require
48 | require "rails_stuff/#{m}"
49 | end
50 | init.try!(:call)
51 | end
52 | end
53 | end
54 |
55 | class Engine < Rails::Engine
56 | initializer :rails_stuff_setup_modules, after: :load_config_initializers do
57 | RailsStuff.setup_modules!
58 | end
59 |
60 | generators do
61 | require 'rails_stuff/generators/concern/concern_generator'
62 | end
63 |
64 | config.action_dispatch.rescue_responses.merge!(
65 | 'RailsStuff::ResourcesController::StiHelpers::InvalidType' => :unprocessable_entity,
66 | )
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/rails_stuff/generators/concern/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Generates concern for the class.
3 |
4 | Example:
5 | `rails generate concern User::Authentication`
6 |
7 | create app/models/user/authentication.rb
8 |
9 | `rails generate concern admin_controller/localized`
10 |
11 | create app/controllers/admin_controller/localized.rb
12 |
--------------------------------------------------------------------------------
/lib/rails_stuff/generators/concern/concern_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators'
2 |
3 | module RailsStuff
4 | module Generators
5 | class ConcernGenerator < Rails::Generators::NamedBase # :nodoc:
6 | argument :base,
7 | type: :string,
8 | required: false,
9 | banner: 'Base dir with the class',
10 | description: 'Use it when class is not inside app/models or app/controllers.'
11 |
12 | namespace 'rails:concern'
13 | source_root File.expand_path('../templates', __FILE__)
14 |
15 | def create_concern_files
16 | template 'concern.rb', File.join(base_path, "#{file_path}.rb")
17 | end
18 |
19 | private
20 |
21 | def base_path
22 | base || file_path.include?('_controller/') ? 'app/controllers' : 'app/models'
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/rails_stuff/generators/concern/templates/concern.rb:
--------------------------------------------------------------------------------
1 | module <%= class_name %>
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | end
6 |
7 | module ClassMethods
8 | def class_method
9 | end
10 | end
11 |
12 | def instance_method
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/rails_stuff/helpers.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Helpers
3 | extend ActiveSupport::Autoload
4 |
5 | autoload :Bootstrap
6 | autoload :Forms
7 | autoload :Links
8 | autoload :ResourceForm
9 | autoload :Text
10 | autoload :Translation
11 |
12 | autoload :All
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/rails_stuff/helpers/all.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Helpers
3 | # Collection of all helper modules.
4 | module All
5 | include Translation
6 | include Links
7 | include Bootstrap
8 | include Text
9 | include Forms
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/rails_stuff/helpers/bootstrap.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/hash/keys'
2 | require 'active_support/core_ext/hash/transform_values'
3 |
4 | module RailsStuff
5 | module Helpers
6 | module Bootstrap
7 | BOOTSTRAP_FLASH_TYPE = {
8 | success: 'alert-success',
9 | error: 'alert-danger',
10 | alert: 'alert-warning',
11 | notice: 'alert-info',
12 | }.stringify_keys.freeze
13 |
14 | CROSS = '×'.html_safe.freeze # rubocop:disable Rails/OutputSafety
15 |
16 | def flash_messages
17 | messages = flash.map do |type, message|
18 | content_tag :div, class: [:alert, BOOTSTRAP_FLASH_TYPE[type] || type] do
19 | content_tag(:button, CROSS, class: :close, data: {dismiss: :alert}) +
20 | simple_format(message)
21 | end
22 | end
23 | safe_join(messages)
24 | end
25 |
26 | ICONS = {
27 | destroy: %(),
28 | edit: %(),
29 | new: %(),
30 | }.tap { |x| x.transform_values!(&:html_safe) if ''.respond_to?(:html_safe) }
31 |
32 | def basic_link_icons
33 | ICONS
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/rails_stuff/helpers/forms.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Helpers
3 | module Forms
4 | # Returns hidden field tags for requested fields when they are present in params.
5 | # Usually used to bypass params in GET-forms.
6 | def hidden_params_fields(*fields)
7 | inputs = fields.flat_map do |field|
8 | next unless params.key?(field)
9 | val = params[field]
10 | if val.is_a?(Array)
11 | name = "#{field}[]"
12 | val.map { |str| [name, str] }
13 | else
14 | [[field, val]]
15 | end
16 | end
17 | safe_join inputs.map { |(name, val)| hidden_field_tag name, val if name }
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/rails_stuff/helpers/links.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Helpers
3 | # Link helpers for basic actions.
4 | module Links
5 | ICONS = { # rubocop:disable MutableConstant
6 | destroy: -> { translate_action(:destroy) },
7 | edit: -> { translate_action(:edit) },
8 | new: -> { translate_action(:new) },
9 | }
10 |
11 | def basic_link_icons
12 | ICONS
13 | end
14 |
15 | def basic_link_icon(action)
16 | val = basic_link_icons[action]
17 | val.is_a?(Proc) ? instance_exec(&val) : val
18 | end
19 |
20 | def link_to_destroy(url, **options)
21 | link_to basic_link_icon(:destroy), url, {
22 | title: translate_action(:delete),
23 | method: :delete,
24 | data: {confirm: translate_confirmation(:delete)},
25 | }.merge!(options)
26 | end
27 |
28 | def link_to_edit(url = nil, **options)
29 | link_to basic_link_icon(:edit), (url || url_for(action: :edit)),
30 | {title: translate_action(:edit)}.merge!(options)
31 | end
32 |
33 | def link_to_new(url = nil, **options)
34 | link_to basic_link_icon(:new), (url || url_for(action: :new)), options
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/rails_stuff/helpers/resource_form.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/array/wrap'
2 | module RailsStuff
3 | module Helpers
4 | # Provides helper for SimpleForm.
5 | module ResourceForm
6 | # Generates `resource_form` helper to display form with basic arguments,
7 | # elements, errors and options. Generated method can work without arguments
8 | # in most of cases: it takes object from `resource` method.
9 | #
10 | # Use `namespace` to add additional path parts to form action:
11 | #
12 | # # this one will use [:site, resource]
13 | # resource_form_for :site
14 | #
15 | # #### Options
16 | #
17 | # - `back_url` - default back url. Can be string with code, or hash for `url_for`.
18 | # - `resource_method` - method to take resource from.
19 | # - `method` - name of generated method.
20 | #
21 | def resource_form_for(namespace = nil, **options)
22 | default_back_url =
23 | case options[:back_url]
24 | when Hash then "url_for(#{options[:back_url]})"
25 | when String then options[:back_url]
26 | else 'url_for(object)'
27 | end
28 | resource_method = options.fetch(:resource_method, :resource)
29 | method_name = options.fetch(:method, :resource_form)
30 | object_arg = (Array.wrap(namespace).map(&:inspect) + [resource_method]).join(', ')
31 |
32 | class_eval <<-RUBY, __FILE__, __LINE__ + 1
33 | def #{method_name}(object = [#{object_arg}], **options)
34 | back_url = options.delete(:back_url) || #{default_back_url}
35 | simple_form_for object, options do |f|
36 | html = ActiveSupport::SafeBuffer.new
37 | msg = f.object.errors[:base].first
38 | html << content_tag(:div, msg, class: 'alert alert-danger') if msg
39 | html << capture { yield(f) }
40 | html << content_tag(:div, class: 'form-group') do
41 | inputs = f.button(:submit, class: 'btn-primary')
42 | inputs << ' '
43 | inputs << link_to(translate_action(:cancel), back_url, class: :btn)
44 | end
45 | end
46 | end
47 | RUBY
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/rails_stuff/helpers/text.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Helpers
3 | module Text
4 | # Replaces blank values with cached placeholder from translations.
5 | # When called with block, it'll check value for blankness, but returns
6 | # block's result if value is present.
7 | #
8 | # replace_blank(description)
9 | # replace_blank(tags) { tags.join(', ') }
10 | # replace_blank(order.paid_at) { |x| l x, format: :long }
11 | #
12 | def replace_blank(value, &block)
13 | if value.blank?
14 | blank_placeholder
15 | else
16 | block_given? ? capture(value, &block) : value
17 | end
18 | end
19 |
20 | # Default placeholder value.
21 | def blank_placeholder
22 | @_blank_placeholder ||= content_tag :small,
23 | "(#{I18n.t(:'helpers.placeholder.blank', default: '-')})",
24 | class: :'text-muted'
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/rails_stuff/helpers/translation.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Helpers
3 | module Translation
4 | class << self
5 | def i18n_raise
6 | @i18n_raise ||= defined?(Rails) && ActionView::Base.raise_on_missing_translations
7 | end
8 |
9 | attr_writer :i18n_raise
10 | end
11 |
12 | # Translates & caches actions within `helpers.actions` scope.
13 | def translate_action(action)
14 | @translate_action ||= Hash.new do |h, key|
15 | h[key] = I18n.t("helpers.actions.#{key}", raise: Translation.i18n_raise)
16 | end
17 | @translate_action[action]
18 | end
19 |
20 | # Translates & caches confirmations within `helpers.confirmations` scope.
21 | def translate_confirmation(action)
22 | @translate_confirmation ||= Hash.new do |h, key|
23 | h[key] = I18n.t("helpers.confirmations.#{key}",
24 | default: [:'helpers.confirm'],
25 | raise: Translation.i18n_raise,
26 | )
27 | end
28 | @translate_confirmation[action]
29 | end
30 |
31 | # Translates boolean values.
32 | def yes_no(val)
33 | @translate_yes_no ||= Hash.new do |h, key|
34 | h[key] = I18n.t("helpers.yes_no.#{key}", raise: Translation.i18n_raise)
35 | end
36 | @translate_yes_no[val.to_s]
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/rails_stuff/nullify_blank_attrs.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/object/blank'
2 |
3 | module RailsStuff
4 | # Changes to `nil` assigned blank attributes.
5 | #
6 | # class App
7 | # nullify_blank_attrs :site_url
8 | # # ...
9 | module NullifyBlankAttrs
10 | def nullify_blank_attrs(*attrs)
11 | RailsStuff.deprecation_07.warn('Use transform_attrs *attrs, with: :nullify')
12 | nullify_blank_attrs_methods.class_eval do
13 | attrs.each do |attr|
14 | define_method("#{attr}=") { |val| super(val.presence) }
15 | end
16 | end
17 | end
18 |
19 | # Module to store generated methods, so they can be overriden in model.
20 | def nullify_blank_attrs_methods
21 | @nullify_blank_attrs_methods ||= Module.new.tap { |x| prepend x }
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/rails_stuff/params_parser.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | # Provides parsing and type-casting functions.
3 | # Reraises all ocured errors with Error class, so you can handle it together:
4 | #
5 | # rescue_from RailsStuff::ParamsParser::Error, with: :render_bad_request
6 | #
7 | # You can define more parsing methods by extending with this module
8 | # and using .parse:
9 | #
10 | # # models/params_parser
11 | # module ParamsParser
12 | # extend RailsStuff::ParamsParser
13 | # extend self
14 | #
15 | # def parse_money(val)
16 | # parse(val) { your_stuff(val) }
17 | # end
18 | # end
19 | #
20 | module ParamsParser
21 | # This exceptions is wrapper for any exception occured in parser.
22 | # Original exception message can be retrieved with `original_message` method.
23 | class Error < ::StandardError
24 | attr_reader :original_message, :value
25 |
26 | def initialize(original_message = nil, value = nil)
27 | message = "Error while parsing: #{value.inspect}"
28 | @original_message = original_message || message
29 | @value = value
30 | super(message)
31 | end
32 |
33 | # Keeps message when passing instance to `raise`.
34 | def exception(*)
35 | self
36 | end
37 |
38 | # Show original messages in tests.
39 | def to_s
40 | "#{super} (#{original_message})"
41 | end
42 | end
43 |
44 | extend self
45 |
46 | # Parses value with specified block. Reraises occured error with Error.
47 | def parse(val, *args, &block)
48 | parse_not_blank(val, *args, &block)
49 | rescue => e # rubocop:disable Lint/RescueWithoutErrorClass
50 | raise Error.new(e.message, val), nil, e.backtrace
51 | end
52 |
53 | # Parses each value in array with specified block.
54 | # Returns `nil` if `val` is not an array.
55 | def parse_array(array, *args, &block)
56 | return unless array.is_a?(Array)
57 | parse(array) { array.map { |val| parse_not_blank(val, *args, &block) } }
58 | end
59 |
60 | # Parses value with given block only when it is not nil.
61 | # Empty string is converted to nil. Pass `allow_blank: true` to return it as is.
62 | def parse_not_blank(val, allow_blank: false)
63 | return if val.nil? || !allow_blank && val.is_a?(String) && val.blank?
64 | yield(val)
65 | end
66 |
67 | # :method: parse_int
68 | # :call-seq: parse_int(val)
69 | #
70 | # Parse int value.
71 |
72 | # :method: parse_int_array
73 | # :call-seq: parse_int_array(val)
74 | #
75 | # Parses array of ints. Returns `nil` if `val` is not an array.
76 |
77 | # :method: parse_float
78 | # :call-seq: parse_float(val)
79 | #
80 | # Parse float value.
81 |
82 | # :method: parse_float_array
83 | # :call-seq: parse_float_array(val)
84 | #
85 | # Parses array of floats. Returns `nil` if `val` is not an array.
86 |
87 | # Parsers for generic types, which are implemented with #to_i, #to_f & #to_s
88 | # methods.
89 | %w[int float].each do |type|
90 | block = :"to_#{type[0]}".to_proc
91 |
92 | define_method "parse_#{type}" do |val|
93 | parse(val, &block)
94 | end
95 |
96 | define_method "parse_#{type}_array" do |val|
97 | parse_array(val, &block)
98 | end
99 | end
100 |
101 | # Parse string value.
102 | def parse_string(val)
103 | parse(val, allow_blank: true, &:to_s)
104 | end
105 |
106 | # Parses array of strings. Returns `nil` if `val` is not an array.
107 | def parse_string_array(val)
108 | parse_array(val, allow_blank: true, &:to_s)
109 | end
110 |
111 | # Parse decimal value.
112 | def parse_decimal(val)
113 | parse(val) { |x| string_to_decimal(x) }
114 | end
115 |
116 | # Parses array of decimals. Returns `nil` if `val` is not an array.
117 | def parse_decimal_array(val)
118 | parse_array(val) { |x| string_to_decimal(x) }
119 | end
120 |
121 | # Parse boolean using ActiveResord's parser.
122 | def parse_boolean(val)
123 | parse(val) do
124 | @boolean_parser ||= boolean_parser
125 | @boolean_parser[val]
126 | end
127 | end
128 |
129 | def boolean_parser
130 | require 'active_record'
131 | ar_parser = ActiveRecord::Type::Boolean.new
132 | if RailsStuff.rails4?
133 | ->(val) { ar_parser.type_cast_from_user(val) }
134 | else
135 | ->(val) { ar_parser.cast(val) }
136 | end
137 | end
138 |
139 | # Parse time in current TZ using `Time.parse`.
140 | def parse_datetime(val)
141 | parse(val) { Time.zone.parse(val) || raise('Invalid datetime') }
142 | end
143 |
144 | # Parse JSON string.
145 | def parse_json(val)
146 | parse(val) { JSON.parse(val) }
147 | end
148 |
149 | private
150 |
151 | # Workaround for https://github.com/ruby/bigdecimal/issues/63
152 | def string_to_decimal(val)
153 | BigDecimal.new(val)
154 | rescue ArgumentError
155 | BigDecimal.new(0)
156 | end
157 | end
158 | end
159 |
--------------------------------------------------------------------------------
/lib/rails_stuff/random_uniq_attr.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | # Provides save way to generate uniq random values for ActiveRecord models.
3 | # You need to make field nullable and add unique index on it.
4 | # The way it works:
5 | #
6 | # - Instance is saved as usual
7 | # - If random fields are not empty, it does nothing
8 | # - Generates random value and tries to update instance
9 | # - If `RecordNotUnique` is occured, it keeps trying to generate new values.
10 | #
11 | module RandomUniqAttr
12 | DEFAULT_GENERATOR = ->(*) { SecureRandom.hex(32) }
13 | MAX_ATTEMPTS = 10
14 |
15 | class << self
16 | # Made from `Devise.firendly_token` with increased length.
17 | def friendly_token(length = 32)
18 | SecureRandom.urlsafe_base64(length).tr('lIO0', 'sxyz')
19 | end
20 | end
21 |
22 | # Generates necessary methods and setups on-create callback for the `field`.
23 | # You can optionally pass custom generator function:
24 | #
25 | # random_uniq_attr(:code) { |instance| my_random(instance) }
26 | #
27 | def random_uniq_attr(field, **options, &block)
28 | set_method = :"set_#{field}"
29 | generate_method = :"generate_#{field}"
30 | max_attempts = options.fetch(:max_attempts) { MAX_ATTEMPTS }
31 |
32 | after_create set_method, unless: :"#{field}?"
33 |
34 | # def self.generate_key
35 | define_singleton_method generate_method, &(block || DEFAULT_GENERATOR)
36 |
37 | # def set_key
38 | define_method(set_method) do
39 | attempt = 0
40 | begin
41 | raise 'Available only for persisted record' unless persisted?
42 | transaction(requires_new: true) do
43 | new_value = self.class.send(generate_method, self)
44 | update_column field, new_value # rubocop:disable Rails/SkipsModelValidations
45 | end
46 | rescue ActiveRecord::RecordNotUnique
47 | attempt += 1
48 | raise if attempt > max_attempts
49 | retry
50 | end
51 | end
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/rails_stuff/redis_storage.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/module/delegation'
2 | require 'active_support/core_ext/module/remove_method'
3 | require 'active_support/core_ext/object/blank'
4 |
5 | module RailsStuff
6 | # Provides methods to store data in redis. Can be easily integrated into
7 | # ActiveRecor or other class.
8 | #
9 | # Redis is accessed via with_redis method which uses redis_pool
10 | # (default to `Rails.redis_pool`, see `pooled_redis` gem) to checkout connection.
11 | # Basic methods are #get, #delete and #set.
12 | #
13 | # Redis keys are generated from requested key and redis_prefix
14 | # (default to underscored class name). You can pass an array as a key and all the
15 | # parts will be concatenated with `:`. #set automalically generates
16 | # sequential keys, if given key is `nil` (last element of array is `nil`).
17 | #
18 | # It uses `dump` and `load` methods to encode values
19 | # (by default delegated to `Marshal`).
20 | module RedisStorage
21 | # Serializers
22 | delegate :dump, :load, to: Marshal
23 |
24 | # Redis connections pool. Default to `Rails.redis_pool`.
25 | # Override this method to change it.
26 | def redis_pool
27 | Rails.redis_pool
28 | end
29 |
30 | # Options to use in SET command. Use to set EX, or smth.
31 | def redis_set_options
32 | {}
33 | end
34 |
35 | # :method: redis_pool=
36 | # :call-seq: redis_pool=
37 | #
38 | # Set redis_pool.
39 |
40 | # :method: redis_set_options=
41 | # :call-seq: redis_set_options=
42 | #
43 | # Set redis_set_options.
44 |
45 | # Setters that overrides methods, so new values are inherited without recursive `super`.
46 | %i[redis_pool redis_set_options].each do |name|
47 | define_method "#{name}=" do |val|
48 | singleton_class.class_eval do
49 | remove_possible_method(name)
50 | define_method(name) { val }
51 | end
52 | end
53 | end
54 |
55 | # Checkout connection & run block with it.
56 | def with_redis(&block)
57 | redis_pool.with(&block)
58 | end
59 |
60 | # Prefix that used in every key for a model. Default to pluralized model name.
61 | def redis_prefix
62 | @redis_prefix ||= name.underscore
63 | end
64 |
65 | # Override default redis_prefix.
66 | attr_writer :redis_prefix
67 |
68 | # Generates key for given `id`(s) prefixed with #redis_prefix.
69 | # Multiple ids are joined with `:`.
70 | def redis_key_for(id)
71 | "#{redis_prefix}:#{Array(id).join(':')}"
72 | end
73 |
74 | # Generates key to store current maximum id. Examples:
75 | #
76 | # users_id_seq
77 | # user_id_seq:eu
78 | def redis_id_seq_key(id = [])
79 | postfix = Array(id).join(':')
80 | "#{redis_prefix}_id_seq#{":#{postfix}" if postfix.present?}"
81 | end
82 |
83 | # Generate next ID. It stores counter separately and uses
84 | # it to retrieve next id.
85 | def next_id(*args)
86 | with_redis { |redis| redis.incr(redis_id_seq_key(*args)) }
87 | end
88 |
89 | # Reset ID counter.
90 | def reset_id_seq(*args)
91 | with_redis { |redis| redis.del(redis_id_seq_key(*args)) }
92 | end
93 |
94 | # Saves value to redis. If `id` is `nil`, it's generated with #next_id.
95 | # Returns last part of id / generated id.
96 | def set(id, value, options = {})
97 | id = Array(id)
98 | id.push(nil) if id.empty?
99 | id[id.size - 1] ||= next_id(id[0..-2])
100 | with_redis do |redis|
101 | redis.set(redis_key_for(id), dump(value), redis_set_options.merge(options))
102 | end
103 | id.last
104 | end
105 |
106 | # Reads value from redis.
107 | def get(id)
108 | return unless id
109 | with_redis { |redis| redis.get(redis_key_for(id)).try { |data| load(data) } }
110 | end
111 |
112 | # Remove record from redis.
113 | def delete(id)
114 | return true unless id
115 | with_redis { |redis| redis.del(redis_key_for(id)) }
116 | true
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/lib/rails_stuff/require_nested.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/dependencies'
2 |
3 | module RailsStuff
4 | module RequireNested
5 | class << self
6 | # Make #require_nested available in module.
7 | def setup
8 | Module.include(self)
9 | end
10 | end
11 |
12 | module_function
13 |
14 | # Requires nested modules with `require_dependency`.
15 | # Pass custom directory to require its content.
16 | # By default uses caller's filename with stripped `.rb` extension from.
17 | def require_nested(dir = 0)
18 | dir = caller_locations(dir + 1, 1)[0].path.sub(/\.rb$/, '') if dir.is_a?(Integer)
19 | Dir["#{dir}/*.rb"].each { |file| require_dependency file }
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/rails_stuff/resources_controller.rb:
--------------------------------------------------------------------------------
1 | require 'responders'
2 |
3 | module RailsStuff
4 | # InheritedResources on diet.
5 | # Tiny and simple implementation. Feel free to change/extend it right in you
6 | # application. Or just use separate modules.
7 | module ResourcesController
8 | extend ActiveSupport::Autoload
9 |
10 | autoload :Actions
11 | autoload :BasicHelpers
12 | autoload :BelongsTo
13 | autoload :HasScopeHelpers
14 | autoload :KaminariHelpers
15 | autoload :ResourceHelper
16 | autoload :Responder
17 | autoload :StiHelpers
18 |
19 | extend KaminariHelpers::ConfigMethods
20 |
21 | class << self
22 | def inject(base, **options)
23 | base.include BasicHelpers
24 | base.include KaminariHelpers if options.fetch(:kaminari) { use_kaminari? }
25 | base.include StiHelpers if options[:sti]
26 | base.include Actions
27 | base.extend HasScopeHelpers
28 | base.extend ResourceHelper
29 | base.extend BelongsTo
30 | end
31 | end
32 |
33 | # Setups basic actions and helpers in resources controller.
34 | #
35 | # #### Options
36 | #
37 | # - `sti` - include STI helpers
38 | # - `kaminari` - include Kaminari helpers
39 | # - `after_save_action` - action to use for `after_save_url`
40 | # - `source_relation` - override `source_relation`
41 | def resources_controller(**options)
42 | ResourcesController.inject(self, **options)
43 |
44 | self.after_save_action = options[:after_save_action] || after_save_action
45 |
46 | resource_belongs_to(*options[:belongs_to]) if options[:belongs_to]
47 | if options[:source_relation]
48 | protected define_method(:source_relation, &options[:source_relation])
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/rails_stuff/resources_controller/actions.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module ResourcesController
3 | # Basic actions for resources controller.
4 | module Actions
5 | def new
6 | build_resource
7 | end
8 |
9 | def create(options = {}, &block)
10 | if create_resource
11 | options[:location] ||= after_save_url
12 | end
13 | respond_with(resource, options, &block)
14 | end
15 |
16 | def update(options = {}, &block)
17 | if update_resource
18 | options[:location] ||= after_save_url
19 | end
20 | respond_with(resource, options, &block)
21 | end
22 |
23 | def destroy(options = {}, &block)
24 | resource.destroy
25 | options[:location] ||= after_destroy_url
26 | flash_errors!
27 | respond_with(resource, options, &block)
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/rails_stuff/resources_controller/basic_helpers.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module ResourcesController
3 | module BasicHelpers
4 | extend ActiveSupport::Concern
5 |
6 | included do
7 | helper_method :resource, :collection
8 | self.after_save_action = :show
9 | end
10 |
11 | module ClassMethods
12 | attr_writer :resource_class,
13 | :resource_param_name,
14 | :permitted_attrs
15 |
16 | # Defines action to redirect after resource was saved. Default to `:show`.
17 | attr_accessor :after_save_action
18 |
19 | # Resource class for controller. Default to class, based on
20 | # demodulized controller name.
21 | def resource_class
22 | @resource_class ||=
23 | Object.const_get(name.to_s.demodulize.sub(/Controller$/, '').singularize)
24 | end
25 |
26 | # Key to lookup for resource attributes in `params`.
27 | # Default to class'es `param_key`.
28 | def resource_param_name
29 | @resource_param_name ||= resource_class.model_name.param_key
30 | end
31 |
32 | # Class-level permitted attributes.
33 | #
34 | # `attr_reader`, default to `[]`.
35 | def permitted_attrs
36 | @permitted_attrs ||= []
37 | end
38 |
39 | # Concats `@permitted_attrs` variable with given attrs.
40 | def permit_attrs(*attrs)
41 | permitted_attrs.concat attrs
42 | end
43 |
44 | # Prevent CanCan's implementation.
45 | def authorize_resource
46 | raise 'use `before_action :authorize_resource!` instead'
47 | end
48 | end
49 |
50 | protected
51 |
52 | # Accesss resources collection.
53 | def collection
54 | @_collection ||= source_for_collection
55 | end
56 |
57 | # End-point relation to be used as source for `collection`.
58 | def source_for_collection
59 | source_relation
60 | end
61 |
62 | # Relation which is used to find and build resources.
63 | def source_relation
64 | self.class.resource_class
65 | end
66 |
67 | # Resource found by `params[:id]`
68 | def resource
69 | @_resource ||= source_relation.find params[:id]
70 | end
71 |
72 | # Instantiate resource with attrs from `resource_params`.
73 | def build_resource(attrs = resource_params)
74 | @_resource = source_relation.new(attrs)
75 | end
76 |
77 | # Builds and saves resource.
78 | def create_resource
79 | build_resource
80 | resource.save
81 | end
82 |
83 | # Updates resource with `resource_params`.
84 | def update_resource(attrs = resource_params)
85 | resource.update(attrs)
86 | end
87 |
88 | # Flashes errors in a safe way. Joins `full_messages` and truncates
89 | # result to avoid cookies overflow.
90 | def flash_errors!(errors = resource.errors, max_length = 100)
91 | flash[:error] = errors.full_messages.join("\n").truncate(max_length) if errors.any?
92 | end
93 |
94 | # URL to be used in `Location` header & to redirect to after
95 | # resource was created/updated. Default uses `self.class.after_save_action`.
96 | def after_save_url
97 | action = self.class.after_save_action
98 | if action == :index
99 | index_url
100 | else
101 | url_for action: action, id: resource
102 | end
103 | end
104 |
105 | # URL to be used in `Location` header & to redirect after
106 | # resource was destroyed. Default to #index_url.
107 | def after_destroy_url
108 | index_url
109 | end
110 |
111 | # Extracted from #after_save_url and #after_destroy_url because they use
112 | # same value. It's easier to override this urls in one place.
113 | def index_url
114 | url_for action: :index
115 | end
116 |
117 | # Override it to return permited params. By default it returns params
118 | # using `self.class.resource_param_name` and `permitted_attrs` methods.
119 | def resource_params
120 | @_resource_params ||= begin
121 | key = self.class.resource_param_name
122 | params.permit(key => permitted_attrs)[key] || params.class.new.permit!
123 | end
124 | end
125 |
126 | # Default permitted attributes are taken from class method. Override it
127 | # to implement request-based permitted attrs.
128 | def permitted_attrs
129 | self.class.permitted_attrs
130 | end
131 |
132 | # Default authorization implementation.
133 | # Uses `#authorize!` method which is not implemented here
134 | # (use CanCan or other implementation).
135 | def authorize_resource!
136 | action = action_name.to_sym
137 | target =
138 | case action
139 | when :index, :create, :new then self.class.resource_class.new
140 | else resource
141 | end
142 | authorize!(action, target)
143 | end
144 | end
145 | end
146 | end
147 |
--------------------------------------------------------------------------------
/lib/rails_stuff/resources_controller/belongs_to.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module ResourcesController
3 | module BelongsTo
4 | class << self
5 | # Builds lambda to use as `#source_relation`.
6 | def source_relation(subject, collection, optional: false, **)
7 | check_subject = :"#{subject}?"
8 | lambda do
9 | if optional && !send(check_subject)
10 | super()
11 | else
12 | send(subject).public_send(collection)
13 | end
14 | end
15 | end
16 |
17 | # Builds lambda to use as `#index_url`
18 | def index_url(subject, *, field: nil, param: nil)
19 | field ||= :"#{subject}_id"
20 | param ||= field
21 | -> { url_for action: :index, param => resource.public_send(field) }
22 | end
23 | end
24 |
25 | # Defines resource helper and source relation
26 | def resource_belongs_to(subject, resource_helper: true, urls: true, **options)
27 | resource_helper(subject) if resource_helper
28 | collection = options[:collection] || resource_class.model_name.plural
29 | source_relation_proc = BelongsTo.source_relation(subject, collection, options)
30 | protected define_method(:source_relation, &source_relation_proc)
31 | protected define_method(:index_url, &BelongsTo.index_url(subject, urls)) if urls
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/rails_stuff/resources_controller/has_scope_helpers.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module ResourcesController
3 | module HasScopeHelpers
4 | # This method overrides default `has_scope` so it include helpers from
5 | # InstanceMethods only when this method is called.
6 | def has_scope(*)
7 | super.tap { include InstanceMethods }
8 | end
9 |
10 | module InstanceMethods
11 | protected
12 |
13 | # Applies `has_scope` scopes to original source.
14 | def source_for_collection
15 | apply_scopes(super)
16 | end
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/rails_stuff/resources_controller/kaminari_helpers.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module ResourcesController
3 | module KaminariHelpers
4 | protected
5 |
6 | # Make source_for_collection use Kaminari-style scopes to paginate relation.
7 | def source_for_collection
8 | super.page(params[:page]).per(params[:per])
9 | end
10 |
11 | module ConfigMethods
12 | def use_kaminari!(val = true)
13 | @use_kaminari = val
14 | end
15 |
16 | def use_kaminari?
17 | @use_kaminari
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/rails_stuff/resources_controller/resource_helper.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module ResourcesController
3 | # Defines resource helper and finder method.
4 | module ResourceHelper
5 | # Defines protected helper method. Ex. for `:user`
6 | #
7 | # helper_method :user
8 | #
9 | # def user
10 | # @user ||= User.find params[:user_id]
11 | # end
12 | #
13 | # #### Options
14 | #
15 | # - `source` - class name or Proc returning relation, default to `resource_name.classify`.
16 | # - `param` - param name, default to `resource_name.foreign_key`
17 | #
18 | # rubocop:disable CyclomaticComplexity, PerceivedComplexity, AbcSize
19 | def resource_helper(name, param: nil, source: nil, **options)
20 | RailsStuff.deprecation_07.warn('use :source instead of :class') if options.key?(:class)
21 |
22 | param ||= name.to_s.foreign_key.to_sym
23 | define_method("#{name}?") { params.key?(param) }
24 |
25 | ivar = :"@#{name}"
26 | source ||= options[:class] || name.to_s.classify
27 | if source.is_a?(Proc)
28 | define_method(name) do
29 | instance_variable_get(ivar) ||
30 | instance_variable_set(ivar, instance_exec(&source).find(params[param]))
31 | end
32 | else
33 | source = Object.const_get(source) unless source.is_a?(Class)
34 | define_method(name) do
35 | instance_variable_get(ivar) ||
36 | instance_variable_set(ivar, source.find(params[param]))
37 | end
38 | end
39 |
40 | helper_method name, :"#{name}?"
41 | protected name, :"#{name}?"
42 | end
43 | # rubocop:enable CyclomaticComplexity, PerceivedComplexity, AbcSize
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/rails_stuff/resources_controller/sti_helpers.rb:
--------------------------------------------------------------------------------
1 | require 'active_record/errors'
2 |
3 | module RailsStuff
4 | module ResourcesController
5 | # Helper methods for controllers which works with STI models.
6 | module StiHelpers
7 | class InvalidType < StandardError; end
8 |
9 | extend ActiveSupport::Concern
10 |
11 | module ClassMethods
12 | # Returns hash which is used to get subclass for requested type.
13 | #
14 | # By default it uses `.types_list` or `.descendants` to get list of
15 | # classes and indexes them by class names.
16 | def resource_class_by_type
17 | @resource_class_by_type ||=
18 | if resource_class.respond_to?(:types_list)
19 | resource_class.types_list
20 | else
21 | resource_class.descendants
22 | end.index_by(&:name)
23 | end
24 |
25 | attr_writer :resource_class_by_type
26 |
27 | def resource_class_for(name)
28 | return resource_class unless name
29 | resource_class_by_type[name] ||
30 | if use_resource_class_for_invalid_type
31 | resource_class
32 | else
33 | raise(InvalidType, "No type mapped for #{name.inspect}")
34 | end
35 | end
36 |
37 | attr_accessor :use_resource_class_for_invalid_type
38 |
39 | # Class-level accessor to permitted attributes for specisic class.
40 | def permitted_attrs_for
41 | @permitted_attrs_for ||= Hash.new { |h, k| h[k] = [] }
42 | end
43 |
44 | # Permits attrs only for specific class.
45 | def permit_attrs_for(klass, *attrs)
46 | permitted_attrs_for[klass].concat attrs
47 | end
48 | end
49 |
50 | protected
51 |
52 | # Returns model class depending on `type` attr in params.
53 | # If resource is requested by id, it returns its class.
54 | def class_from_request
55 | @class_from_request ||=
56 | if params.key?(:id)
57 | resource.class
58 | else
59 | key = self.class.resource_param_name
60 | name = params.permit(key => [:type])[key].try!(:[], :type)
61 | self.class.resource_class_for(name)
62 | end
63 | end
64 |
65 | # Instantiates object using class_from_request.
66 | def build_resource(attrs = resource_params)
67 | @_resource = super.becomes!(class_from_request)
68 | end
69 |
70 | # Merges default attrs with attrs for specific class.
71 | def permitted_attrs
72 | super + self.class.permitted_attrs_for[class_from_request]
73 | end
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/lib/rails_stuff/responders.rb:
--------------------------------------------------------------------------------
1 | require 'rails_stuff/responders/turbolinks'
2 |
3 | module RailsStuff
4 | # Here are some useful responders extensions.
5 | #
6 | # Previous versions had responder with SJR helpers, which has been removed.
7 | # To achieve same result define responder with:
8 | #
9 | # class CustomResponder < ActionController::Responder
10 | # include Responders::HttpCacheResponder
11 | # include RailsStuff::Responders::Turbolinks
12 | #
13 | # # SJR: render action on failures, redirect on success
14 | # alias_method :to_js, :to_html
15 | # end
16 | #
17 | module Responders
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/rails_stuff/responders/turbolinks.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Responders
3 | module Turbolinks
4 | # Bypasses `:turbolinks` from options.
5 | def redirect_to(url, opts = {})
6 | if !opts.key?(:turbolinks) && options.key?(:turbolinks)
7 | opts[:turbolinks] = options[:turbolinks]
8 | end
9 | super
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/rails_stuff/rspec_helpers.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/array/wrap'
2 | require 'active_support/dependencies/autoload'
3 |
4 | module RailsStuff
5 | # Collection of RSpec configurations and helpers for better experience.
6 | module RSpecHelpers
7 | autoload :Signinable, 'rails_stuff/rspec_helpers/signinable'
8 |
9 | extend self
10 |
11 | # Single endpoint for multiple seups. Use `:only` and `:except` options
12 | # to filter actions.
13 | def setup(only: nil, except: nil)
14 | items = instance_methods.map(&:to_s) - %w[setup]
15 | items -= Array.wrap(except).map(&:to_s) if except
16 | if only
17 | only = Array.wrap(only).map(&:to_s)
18 | items &= only
19 | items += only
20 | end
21 | items.uniq.each { |item| public_send(item) }
22 | end
23 |
24 | %w[
25 | concurrency
26 | groups/request
27 | groups/feature
28 | matchers/be_valid_js
29 | matchers/redirect_with_turbolinks
30 | ].each do |file|
31 | define_method(file) { require "rails_stuff/rspec_helpers/#{file}" }
32 | end
33 |
34 | # Setup all TestHelpers.
35 | def test_helpers
36 | TestHelpers.setup
37 | end
38 |
39 | # Setups database cleaner to use strategy depending on metadata.
40 | # By default it uses `:transaction` for all examples and `:truncation`
41 | # for features and examples with `concurrent: true`.
42 | #
43 | # Other types can be tuned with `config.cleaner_strategy` hash &
44 | # `config.cleaner_strategy.default`.
45 | def database_cleaner # rubocop:disable AbcSize
46 | return unless defined?(DatabaseCleaner)
47 | ::RSpec.configure do |config|
48 | if config.respond_to?(:use_transactional_fixtures=)
49 | config.use_transactional_fixtures = false
50 | end
51 | config.add_setting :database_cleaner_strategy
52 | config.database_cleaner_strategy = {feature: :truncation}
53 | config.database_cleaner_strategy.default = :transaction
54 | config.add_setting :database_cleaner_options
55 | config.database_cleaner_options = {truncation: {except: %w[spatial_ref_sys]}}
56 | config.add_setting :database_cleaner_args
57 | config.database_cleaner_args = ->(ex) do
58 | strategy = ex.metadata[:concurrent] && :truncation
59 | strategy ||= config.database_cleaner_strategy[ex.metadata[:type]]
60 | options = config.database_cleaner_options[strategy] || {}
61 | [strategy, options]
62 | end
63 | config.around do |ex|
64 | DatabaseCleaner.strategy = config.database_cleaner_args.call(ex)
65 | DatabaseCleaner.cleaning { ex.run }
66 | end
67 | end
68 | end
69 |
70 | # Setups redis to flush db after suite and before each example with
71 | # `flush_redis: :true`. `Rails.redis` client is used by default.
72 | # Can be tuned with `config.redis`.
73 | def redis
74 | ::RSpec.configure do |config|
75 | config.add_setting :redis
76 | config.redis = Rails.redis if defined?(Rails.redis)
77 | config.add_setting :flush_redis_proc
78 | config.flush_redis_proc = ->(*) { Array.wrap(config.redis).each(&:flushdb) }
79 | config.before(flush_redis: true) { instance_exec(&config.flush_redis_proc) }
80 | config.after(:suite) { instance_exec(&config.flush_redis_proc) }
81 | end
82 | end
83 |
84 | # Runs debugger after each failed example with `:debug` tag.
85 | # Uses `pry` by default, this can be configured `config.debugger=`.
86 | def debug
87 | ::RSpec.configure do |config|
88 | config.add_setting :debugger_proc
89 | config.debugger_proc = ->(ex) do
90 | exception = ex.exception
91 | defined?(Pry) ? binding.pry : debugger # rubocop:disable Debugger
92 | end
93 | config.after(debug: true) do |ex|
94 | instance_exec(ex, &config.debugger_proc) if ex.exception
95 | end
96 | end
97 | end
98 |
99 | # Clear logs `tail -f`-safely.
100 | def clear_logs
101 | ::RSpec.configure do |config|
102 | config.add_setting :clear_log_file
103 | config.clear_log_file = Rails.root.join('log', 'test.log') if defined?(Rails.root)
104 | config.add_setting :clear_log_file_proc
105 | config.clear_log_file_proc = ->(file) do
106 | next unless file && File.exist?(file)
107 | FileUtils.cp(file, "#{file}.last")
108 | File.open(file, 'w').close
109 | end
110 | config.after(:suite) do
111 | instance_exec(config.clear_log_file, &config.clear_log_file_proc) unless ENV['KEEP_LOG']
112 | end
113 | end
114 | end
115 |
116 | # Freeze time for specs with `:frozen_time` metadata.
117 | def frozen_time
118 | ::RSpec.configure do |config|
119 | config.around(frozen_time: true) { |ex| Timecop.freeze { ex.run } }
120 | end
121 | end
122 | end
123 | end
124 |
--------------------------------------------------------------------------------
/lib/rails_stuff/rspec_helpers/concurrency.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 | require 'rails_stuff/test_helpers/concurrency'
3 |
4 | module RailsStuff
5 | module RSpecHelpers
6 | module Concurrency
7 | extend ActiveSupport::Concern
8 | include RailsStuff::TestHelpers::Concurrency
9 |
10 | module ClassMethods
11 | # Defines subject which runs parent's value in multiple threads concurrently.
12 | # Define `thread_args` or `threads_count` with `let` to configure it.
13 | #
14 | # Sets metadata `concurrent: true` so database cleaner uses right strategy.
15 | def concurrent_subject!
16 | metadata[:concurrent] = true
17 | subject do
18 | super_proc = super()
19 | args = defined?(thread_args) && thread_args
20 | args ||= defined?(threads_count) && threads_count
21 | -> { concurrently(args, &super_proc) }
22 | end
23 | end
24 |
25 | # Runs given block in current context and nested context with concurrent subject.
26 | #
27 | # subject { -> { increment_value_once } }
28 | # # This will create 2 examples. One for current contex, and one
29 | # # for current context where subject will run multiple times concurrently.
30 | # check_concurrent do
31 | # it { should change { value }.by(1) }
32 | # end
33 | def check_concurrent(&block)
34 | instance_eval(&block)
35 | context 'running multiple times concurrently' do
36 | concurrent_subject!
37 | instance_eval(&block)
38 | end
39 | end
40 | end
41 |
42 | ::RSpec.configuration.include(self) if defined?(::RSpec)
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/rails_stuff/rspec_helpers/groups/feature.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module RSpecHelpers
3 | module Groups
4 | module Feature
5 | def wait_for_ajax
6 | Timeout.timeout(Capybara.default_max_wait_time) do
7 | loop until finished_all_ajax_requests?
8 | end
9 | end
10 |
11 | # Tuned for jQuery, override it if you don't use jQuery.
12 | def finished_all_ajax_requests?
13 | page.evaluate_script('jQuery.active').zero?
14 | end
15 |
16 | def pause
17 | $stderr.write 'Press enter to continue'
18 | $stdin.gets
19 | end
20 |
21 | ::RSpec.configuration.include self, type: :feature
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/rails_stuff/rspec_helpers/groups/request.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | module RailsStuff
4 | module RSpecHelpers
5 | module Groups
6 | module Request
7 | extend ActiveSupport::Concern
8 |
9 | included do
10 | # Define default params for ResourcesController.
11 | #
12 | # subject { -> { patch(resource_path, params: params) } }
13 | # let(:resource) { create(:user) }
14 | # let(:resource_params) { {name: 'new name'} }
15 | # # params will be {user: {name: 'new name'}}
16 | if described_class.respond_to?(:resource_param_name)
17 | let(:params) { {described_class.resource_param_name => resource_params} }
18 | end
19 | end
20 |
21 | module ClassMethods
22 | # Adds `referer`, `referer_path` and `headers` with `let`.
23 | # Requires `root_url`
24 | def set_referer
25 | let(:referer) { root_url.sub(%r{/$}, referer_path) }
26 | let(:referer_path) { '/test_referer' }
27 | let(:headers) do
28 | headers = {Referer: referer}
29 | defined?(super) ? super().merge(headers) : headers
30 | end
31 | end
32 |
33 | # Perform simple request to initialize session.
34 | # Useful for `change` matchers.
35 | def init_session
36 | before do
37 | path = defined?(init_session_path) ? init_session_path : '/'
38 | get(path)
39 | end
40 | end
41 |
42 | def with_csrf_protection!
43 | around do |ex|
44 | begin
45 | old = ActionController::Base.allow_forgery_protection
46 | ActionController::Base.allow_forgery_protection = true
47 | ex.run
48 | ensure
49 | ActionController::Base.allow_forgery_protection = old
50 | end
51 | end
52 | let(:csrf_response) do
53 | path = defined?(csrf_response_path) ? csrf_response_path : '/'
54 | get(path) && response.body
55 | end
56 | let(:csrf_param) { csrf_response.match(/meta name="csrf-param" content="([^"]*)"/)[1] }
57 | let(:csrf_token) { csrf_response.match(/"
44 | @scope.assert_operator redirect_expected, :===, redirect_is, message
45 | rescue ActiveSupport::TestCase::Assertion => e
46 | raise XhrFailure, e
47 | end
48 |
49 | ::RSpec::Rails::Matchers::RedirectTo::RedirectTo.prepend(self) if defined?(::RSpec::Rails)
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/rails_stuff/rspec_helpers/signinable.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module RSpecHelpers
3 | module Signinable
4 | # Context-level helper to add before filter to sign in user.
5 | # Adds `current_user` with let if not defined yet or with gven block.
6 | #
7 | # Instance-level `sign_in(user_or_nil)` method must be defined, so
8 | # this module can be used in any of feature, request or controller groups.
9 | #
10 | # sign_in { owner }
11 | # sign_in # will call current_user or define it with nil
12 | def sign_in(&block)
13 | if block || !instance_methods.include?(:current_user)
14 | block ||= ->(*) {}
15 | let(:current_user, &block)
16 | end
17 | before { sign_in(instance_eval { current_user }) }
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/rails_stuff/sort_scope.rb:
--------------------------------------------------------------------------------
1 | require 'has_scope'
2 |
3 | module RailsStuff
4 | # Provides safe and flexible way to sort collections by user's input.
5 | # Uses `has_scope` gem.
6 | #
7 | # Supports different input format, and limits requested fields
8 | # to allowed subset.
9 | module SortScope
10 | # Register type for has_scop that accepts stings, hashes & arrays.
11 | HasScope::ALLOWED_TYPES[:any] = [[String, Hash, Array, Symbol, ActionController::Parameters]]
12 |
13 | # Setups has_scope to order collection by allowed columns.
14 | # Sort column is filtered by SortScope.filter_param method.
15 | # Accepts params:
16 | #
17 | # - `sort=name`
18 | # - `sort=name&sort_desc=true`
19 | # - `sort[name]&sort[order]`
20 | # - `sort[name]&sort[order]=desc
21 | #
22 | # #### Options
23 | #
24 | # - `by` - array of available fields to sort by,
25 | # - `default` - default sort expression,
26 | # - `only` - bypassed to `has_scope` to limit actions (default to `:index`),
27 | # - `order_method` - use custom method to sort instead of `.order`.
28 | #
29 | # rubocop:disable ClassVars
30 | def has_sort_scope(config = {})
31 | @@_sort_scope_id ||= 0
32 | default = config[:default] || :id
33 | default = default.is_a?(Hash) ? default.stringify_keys : default.to_s
34 | allowed = Array.wrap(config[:by]).map(&:to_s)
35 | only_actions = config.fetch(:only, :index)
36 | order_method = config.fetch(:order_method, :order)
37 | # Counter added into scope name to allow to define multiple scopes in same controller.
38 | has_scope("sort_#{@@_sort_scope_id += 1}",
39 | as: :sort,
40 | default: nil,
41 | allow_blank: true,
42 | only: only_actions,
43 | type: :any,
44 | ) do |c, scope, val|
45 | sort_args = SortScope.filter_param(val, c.params, allowed, default)
46 | c.instance_variable_set(:@current_sort_scope, sort_args)
47 | scope.public_send(order_method, sort_args)
48 | end
49 | end
50 | # rubocop:enable ClassVars
51 |
52 | # It does not use `ClassMethods` similar to `ActiveSupport::Concern`
53 | # due to backward compatibility (SortScope extends controller class).
54 | module InstanceMethods
55 | protected
56 |
57 | def current_sort_scope
58 | @current_sort_scope ||= {}
59 | end
60 | end
61 |
62 | class << self
63 | def extended(base)
64 | base.class_eval do
65 | include InstanceMethods
66 | helper_method :current_sort_scope if respond_to?(:helper_method)
67 | end
68 | end
69 |
70 | # Filters value with whitelist of allowed fields to sort by.
71 | #
72 | # rubocop:disable CyclomaticComplexity, PerceivedComplexity, BlockNesting
73 | def filter_param(val, params, allowed, default = nil)
74 | val ||= default
75 | unless val == default
76 | val = val.to_unsafe_h if val.is_a?(ActionController::Parameters)
77 | val =
78 | if val.is_a?(Hash)
79 | val.each_with_object({}) do |(key, dir), h|
80 | h[key] = (dir == 'desc' ? :desc : :asc) if allowed.include?(key)
81 | end
82 | else
83 | allowed.include?(val) ? val : default
84 | end
85 | end
86 | if val && !val.is_a?(Hash)
87 | val = {val => ParamsParser.parse_boolean(params[:sort_desc]) ? :desc : :asc}
88 | end
89 | val
90 | end
91 | # rubocop:enable CyclomaticComplexity, PerceivedComplexity, BlockNesting
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/lib/rails_stuff/statusable.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | # Basic helpers to work with `status`-like field.
3 | #
4 | # For every status value it provides:
5 | #
6 | # - scopes with status name (eg. `.rejected`, '.not_rejected')
7 | # - inquiry method to check status (eg. `#rejected?`)
8 | # - bang method to update status (eg. `#rejected!`)
9 | #
10 | # It also provides:
11 | #
12 | # - translation helpers (`acttivemodel_translation` gem required)
13 | # - inclusion validator
14 | # - string/symbol agnostic `#status=`
15 | # - `#status_sym`
16 | # - `status_select_options` helper.
17 | #
18 | # It supports mapped statuses, just provide a hash with
19 | # `{status_name => interna_value}` instead of array of statuses.
20 | module Statusable
21 | autoload :Helper, 'rails_stuff/statusable/helper'
22 | autoload :Builder, 'rails_stuff/statusable/builder'
23 | autoload :MappedHelper, 'rails_stuff/statusable/mapped_helper'
24 | autoload :MappedBuilder, 'rails_stuff/statusable/mapped_builder'
25 |
26 | class << self
27 | # Fetches statuses list from model constants. See #has_status_field.
28 | def fetch_statuses(model, field)
29 | const_name = "#{field.to_s.pluralize.upcase}_MAPPING"
30 | const_name = field.to_s.pluralize.upcase unless model.const_defined?(const_name)
31 | model.const_get(const_name)
32 | end
33 | end
34 |
35 | # Defines all helpers working with `field` (default to `status`).
36 | # List of values can be given as second argument, otherwise it'll
37 | # be read from consts using pluralized name of `field`
38 | # (eg. default to `STATUSES_MAPPING`, `STATUSES`).
39 | #
40 | # #### Options
41 | #
42 | # - `prefix` - used to prefix value-named helpers.
43 | #
44 | # # this defines #shipped?, #shipped! methods
45 | # has_status_field :delivery_status, %i(shipped delivered)
46 | #
47 | # # this defines #delivery_shipped?, #delivery_shipped! methods
48 | # has_status_field :delivery_status, %i(shipped delivered), prefix: :delivery_
49 | #
50 | # - `suffix` - similar to `prefix`.
51 | #
52 | # - `validate` - additional options for validatior. `false` to disable it.
53 | #
54 | # - `helper` - custom helper class.
55 | #
56 | # - `builder` - custom methods builder class.
57 | #
58 | # - `mapping` - shortcut for `statuses` param (see examples).
59 | #
60 | # Pass block to customize methods generation process (see Builder for available methods):
61 | #
62 | # # This will define only scope with status names, but no other methods.
63 | # has_status_field do |builder|
64 | # builder.value_scopes
65 | # end
66 | #
67 | # Examples:
68 | #
69 | # # Setup #status field, take list from STATUSES or STATUSES_MAPPING constant.
70 | # has_status_field
71 | # # Custom field, take kist from KINDS or KINDS_MAPPING:
72 | # has_status_field :kind
73 | # # Inline statuses list and options:
74 | # has_status_field :status, %i(one two), prefix: :my_
75 | # has_status_field :status, {one: 1, two: 2}, prefix: :my_
76 | # has_status_field :status, mapping: {one: 1, two: 2}, prefix: :my_
77 | # # Mapped field without options:
78 | # has_status_field :status, {one: 1, two: 2}, {}
79 | # has_status_field :status, mapping: {one: 1, two: 2}
80 | #
81 | def has_status_field(field = :status, statuses = nil, mapping: nil, **options)
82 | statuses ||= mapping || Statusable.fetch_statuses(self, field)
83 | is_mapped = statuses.is_a?(Hash)
84 | helper_class = options.fetch(:helper) { is_mapped ? MappedHelper : Helper }
85 | helper = helper_class.new(self, field, statuses)
86 | helper.attach
87 | builder_class = options.fetch(:builder) { is_mapped ? MappedBuilder : Builder }
88 | if builder_class
89 | builder = builder_class.new(helper, options)
90 | block_given? ? yield(builder) : builder.generate
91 | end
92 | end
93 |
94 | # Module to hold generated methods. Single for all status fields in model.
95 | def statusable_methods
96 | # Include generated methods with a module, not right in class.
97 | @statusable_methods ||= Module.new.tap do |m|
98 | m.const_set :ClassMethods, Module.new
99 | include m
100 | extend m::ClassMethods
101 | end
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/lib/rails_stuff/statusable/builder.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Statusable
3 | # Basic builder for statuses list. Generates methods and scopes.
4 | class Builder
5 | attr_reader :helper, :options, :prefix, :suffix
6 | delegate :model, :field, :list, to: :helper
7 | delegate :define_scope, :define_method, :define_class_method, to: :helper
8 |
9 | def initialize(helper, **options)
10 | @helper = helper
11 | @options = options
12 | @prefix = options[:prefix]
13 | @suffix = options[:suffix]
14 | end
15 |
16 | def generate
17 | validations if options.fetch(:validate, true)
18 | field_accessor
19 | field_scope
20 | value_scopes
21 | value_accessors
22 | translation_helpers
23 | end
24 |
25 | def validations
26 | model.validates_inclusion_of field,
27 | {in: valid_list}.merge!(options.fetch(:validate, {}))
28 | end
29 |
30 | # Field reader returns string, so we stringify list for validation.
31 | def valid_list
32 | list.map(&:to_s)
33 | end
34 |
35 | # Yields every status with it's database value into block.
36 | def each_status
37 | list.each { |x| yield x, x.to_s }
38 | end
39 |
40 | # Wraps status name with prefix and suffix.
41 | def status_method_name(status)
42 | "#{prefix}#{status}#{suffix}"
43 | end
44 |
45 | # Scope with given status. Useful for has_scope.
46 | def field_scope
47 | field = self.field
48 | define_scope "with_#{field}", ->(status) { where(field => status) }
49 | end
50 |
51 | # Status accessors for every status.
52 | def value_accessors
53 | each_status do |status, value|
54 | value_accessor status, value
55 | end
56 | end
57 |
58 | # Scopes for every status.
59 | def value_scopes
60 | field = self.field
61 | each_status do |status, value|
62 | define_scope status_method_name(status), -> { where(field => value) }
63 | define_scope "not_#{status_method_name(status)}", -> { where.not(field => value) }
64 | end
65 | end
66 |
67 | # Generates methods for specific value.
68 | def value_accessor(status, value)
69 | field = self.field
70 |
71 | # Shortcut to check status.
72 | define_method "#{status_method_name(status)}?" do
73 | # Access raw value, 'cause reader can be overriden.
74 | self[field] == value
75 | end
76 |
77 | # Shortcut to update status.
78 | define_method "#{status_method_name(status)}!" do
79 | update!(field => value)
80 | end
81 | end
82 |
83 | def field_accessor
84 | field_reader
85 | field_writer
86 | end
87 |
88 | # Make field accept sympbols.
89 | def field_writer
90 | define_method "#{field}=" do |val|
91 | val = val.to_s if val.is_a?(Symbol)
92 | super(val)
93 | end
94 | end
95 |
96 | # Status as symbol.
97 | def field_reader
98 | field = self.field
99 | define_method "#{field}_sym" do
100 | val = self[field]
101 | val && val.to_sym
102 | end
103 | end
104 |
105 | def translation_helpers
106 | field = self.field
107 | define_method "#{field}_name" do
108 | val = send(field)
109 | self.class.t(".#{field}_name.#{val}") if val
110 | end
111 | end
112 | end
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/lib/rails_stuff/statusable/helper.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Statusable
3 | # Class to hold helper methods for statusable field.
4 | #
5 | # Order.has_status_field :status, %i(pending complete)
6 | # Order.statuses.list # => %(pending complete)
7 | # # ...
8 | class Helper
9 | attr_reader :model, :field, :list
10 |
11 | def initialize(model, field, statuses)
12 | @model = model
13 | @field = field.freeze
14 | @list = statuses.freeze
15 | end
16 |
17 | def translate(status)
18 | model.t(".#{field}_name.#{status}") if status
19 | end
20 |
21 | alias_method :t, :translate
22 |
23 | # Returns array compatible with select_options helper.
24 | def select_options(only: nil, except: nil)
25 | only ||= list
26 | only -= except if except
27 | only.map { |x| [translate(x), x] }
28 | end
29 |
30 | # Generate class method in model to access helper.
31 | def attach(method_name = field.to_s.pluralize)
32 | helper = self
33 | define_class_method(method_name) { helper }
34 | end
35 |
36 | # Rails 4 doesn't use `instance_exec` for scopes, so we do it manually.
37 | # For Rails 5 it's just use `.scope`.
38 | def define_scope(name, body)
39 | if RailsStuff.rails4?
40 | model.singleton_class.send(:define_method, name) do |*args|
41 | all.scoping { instance_exec(*args, &body) } || all
42 | end
43 | else
44 | model.scope(name, body)
45 | end
46 | end
47 |
48 | def define_method(method, &block)
49 | methods_module.send(:define_method, method, &block)
50 | end
51 |
52 | def define_class_method(method, &block)
53 | methods_module::ClassMethods.send(:define_method, method, &block)
54 | end
55 |
56 | protected
57 |
58 | # Module to hold generated methods.
59 | def methods_module
60 | model.statusable_methods
61 | end
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/lib/rails_stuff/statusable/mapped_builder.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Statusable
3 | # Generates methods and scopes when status names are mapped to internal values.
4 | class MappedBuilder < Statusable::Builder
5 | delegate :mapping, :inverse_mapping, to: :helper
6 |
7 | # Field reader returns mapped value, so we don't need to stringify list.
8 | alias_method :valid_list, :list
9 |
10 | def each_status(&block)
11 | mapping.each(&block)
12 | end
13 |
14 | # Scope with given status. Useful for has_scope.
15 | def field_scope
16 | field = self.field
17 | helper = self.helper
18 | define_scope "with_#{field}", ->(status) { where(field => helper.map(status)) }
19 | end
20 |
21 | def field_reader
22 | field = self.field
23 | helper = self.helper
24 |
25 | # Returns status name.
26 | define_method field do |original = false|
27 | val = super()
28 | original || !val ? val : helper.unmap(val)
29 | end
30 |
31 | # Status as symbol.
32 | define_method "#{field}_sym" do
33 | val = public_send(field)
34 | val && val.to_sym
35 | end
36 | end
37 |
38 | def field_writer
39 | helper = self.helper
40 | # Make field accept sympbols.
41 | define_method "#{field}=" do |val|
42 | val = val.to_s if val.is_a?(Symbol)
43 | super(helper.map(val))
44 | end
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/rails_stuff/statusable/mapped_helper.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module Statusable
3 | # Helper to hold
4 | class MappedHelper < Helper
5 | attr_reader :mapping, :inverse_mapping, :indifferent_mapping
6 |
7 | def initialize(*)
8 | super
9 | @mapping = @list
10 | @indifferent_mapping = mapping.with_indifferent_access
11 | @list = mapping.keys.freeze
12 | @inverse_mapping = mapping.invert.freeze
13 | end
14 |
15 | def select_options(original: false, only: nil, except: nil)
16 | return super(only: only, except: except) unless original
17 | only ||= mapping_values
18 | only -= except if except
19 | only.map { |x| [translate(inverse_mapping.fetch(x)), x] }
20 | end
21 |
22 | def mapping_values
23 | @mapping_values ||= mapping.values
24 | end
25 |
26 | def map(val)
27 | map_with(indifferent_mapping, val)
28 | end
29 |
30 | def unmap(val)
31 | map_with(inverse_mapping, val)
32 | end
33 |
34 | protected
35 |
36 | # Maps single value or array with given map.
37 | def map_with(map, val)
38 | if val.is_a?(Array)
39 | val.map { |x| map.fetch(x, x) }
40 | else
41 | map.fetch(val, val)
42 | end
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/rails_stuff/strong_parameters.rb:
--------------------------------------------------------------------------------
1 | ActionController::Parameters.class_eval do
2 | # Permits and then checks all fields for presence.
3 | def require_permitted(*fields)
4 | permit(*fields).tap { |permitted| fields.each { |f| permitted.require(f) } }
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/rails_stuff/test_helpers.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/array/wrap'
2 |
3 | module RailsStuff
4 | # Collection of RSpec configurations and helpers for better experience.
5 | module TestHelpers
6 | extend self
7 |
8 | def setup(only: nil, except: nil)
9 | items = instance_methods.map(&:to_s) - %w[setup]
10 | items -= Array.wrap(except).map(&:to_s) if except
11 | if only
12 | only = Array.wrap(only).map(&:to_s)
13 | items &= only
14 | items += only
15 | end
16 | items.each { |item| public_send(item) }
17 | end
18 |
19 | %w[
20 | integration_session
21 | response
22 | ].each do |file|
23 | define_method(file.tr('/', '_')) { require "rails_stuff/test_helpers/#{file}" }
24 | end
25 |
26 | # Make BigDecimal`s more readable.
27 | def big_decimal
28 | require 'bigdecimal'
29 | BigDecimal.class_eval do
30 | alias_method :inspect_orig, :inspect
31 | alias_method :inspect, :to_s
32 | end
33 | end
34 |
35 | # Raise errors from failed threads.
36 | def thread
37 | Thread.abort_on_exception = true
38 | end
39 |
40 | # Raise all translation errors, to not miss any of translations.
41 | # Make sure to set `config.action_view.raise_on_missing_translations = true` in
42 | # `config/environments/test.rb` yourself.
43 | def i18n
44 | return unless defined?(I18n)
45 | I18n.config.exception_handler = ->(exception, _locale, _key, _options) do
46 | raise exception.respond_to?(:to_exception) ? exception.to_exception : exception
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/rails_stuff/test_helpers/concurrency.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/array/wrap'
2 |
3 | module RailsStuff
4 | module TestHelpers
5 | module Concurrency
6 | class << self
7 | # Default threads count
8 | attr_accessor :threads_count
9 | end
10 | @threads_count = 3
11 |
12 | extend self
13 |
14 | # Runs block concurrently in separate threads.
15 | # Pass array of args arrays to run each thread with its own arguments.
16 | # Or pass Integer to run specified threads count with same arguments.
17 | # Default is to run Concurrency.threads_count threads.
18 | #
19 | # concurrently { do_something }
20 | # concurrently(5) { do_something }
21 | # concurrently([[1, opt: true], [2, opt: false]]) do |arg, **options|
22 | # do_something(arg, options)
23 | # end
24 | # # It'll automatically wrap single args into Array:
25 | # concurrently(1, 2, {opt: true}, {opt: false}, [1, opt: false]) { ... }
26 | #
27 | def concurrently(thread_args = nil)
28 | thread_args ||= Concurrency.threads_count
29 | threads =
30 | case thread_args
31 | when Integer
32 | Array.new(thread_args) { Thread.new { yield } }
33 | else
34 | thread_args.map { |args| Thread.new { yield(*Array.wrap(args)) } }
35 | end
36 | threads.each(&:join)
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/rails_stuff/test_helpers/integration_session.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | module TestHelpers
3 | module IntegrationSession
4 | # Return smth usable instead of status code.
5 | def process(*)
6 | super
7 | response
8 | end
9 |
10 | # Set host explicitly, because it can change after request.
11 | def default_url_options
12 | super.merge(host: host)
13 | end
14 |
15 | ActionDispatch::Integration::Session.prepend(self) if defined?(ActionDispatch)
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/rails_stuff/test_helpers/response.rb:
--------------------------------------------------------------------------------
1 | require 'hashie'
2 |
3 | module RailsStuff
4 | module TestHelpers
5 | module Response
6 | class << self
7 | # Return `Hashie::Mash` for a given object. When `Array` is given
8 | # it is mapped to mash recursievly.
9 | def prepare_json_object(object)
10 | case object
11 | when Hash then Hashie::Mash.new(object)
12 | when Array then object.map(&method(__callee__))
13 | else object
14 | end
15 | end
16 | end
17 |
18 | # Missing enquirers.
19 | {
20 | unprocessable_entity: 422,
21 | no_content: 204,
22 | }.each do |name, code|
23 | define_method("#{name}?") { status == code }
24 | end
25 |
26 | # Easy access to json bodies. It parses and return `Hashie::Mash`'es, so
27 | # properties can be accessed via method calls:
28 | #
29 | # response.json_body.order.items.id
30 | # # note that hash methods are still present:
31 | # response.json_body.order[:key] # instead of order.key
32 | def json_body
33 | @json_body ||= Response.prepare_json_object(JSON.parse(body))
34 | end
35 |
36 | # Makes it easier to debug failed specs.
37 | def inspect
38 | ""
39 | end
40 |
41 | ActionDispatch::TestResponse.send(:include, self) if defined?(ActionDispatch)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/rails_stuff/transform_attrs.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | # Generates writers which apply given transfromer for values.
3 | module TransformAttrs
4 | class << self
5 | def transformations
6 | @transformations ||= {}
7 | end
8 |
9 | # Register new transformation with given block.
10 | #
11 | # number_regexp = /\A\d+\z/
12 | # RailsStuff::TransformAttrs.register :phone do |val|
13 | # if val && val.to_s =~ number_regexp
14 | # ActiveSupport::NumberHelper.number_to_phone(val)
15 | # else
16 | # val
17 | # end
18 | # end
19 | def register(name, &block)
20 | transformations[name] = block
21 | end
22 |
23 | def fetch_block(ids)
24 | if ids.is_a?(Array)
25 | blocks = ids.map { |x| transformations.fetch(x) }
26 | ->(val) { blocks.reduce(val) { |x, block| block.call(x) } }
27 | else
28 | transformations.fetch(ids)
29 | end
30 | end
31 | end
32 |
33 | register(:strip) { |val| val && val.to_s.strip }
34 | register(:nullify, &:presence)
35 | register(:strip_and_nullify) { |val| val && val.to_s.strip.presence }
36 |
37 | # Options:
38 | #
39 | # - `with` - use built-in transfromers from TransformAttrs.transformations.
40 | # Add new transformations with TransformAttrs.register.
41 | # - `new_module` - create new module for generated methods.
42 | # Accepts `:prepend` or `:include`. By default it uses single module
43 | # which is prepended.
44 | #
45 | # transform_attrs(:attr1, :attr2) { |x| x.to_s.downcase }
46 | # transform_attrs(:attr3, &:presence) # to nullify blanks
47 | # transform_attrs(:attr4, with: :strip)
48 | # transform_attrs(:attr4, with: [:strip, :nullify])
49 | # transform_attrs(:attr5, new_module: :include)
50 | def transform_attrs(*attrs, with: nil, new_module: false, &block)
51 | block ||= TransformAttrs.fetch_block(with)
52 | mod = Module.new.tap { |x| public_send(new_module, x) } if new_module
53 | mod ||= transform_attrs_methods
54 | mod.class_eval do
55 | attrs.each do |attr|
56 | define_method("#{attr}=") { |val| super(block[val]) }
57 | end
58 | end
59 | end
60 |
61 | # Module to store generated methods, so they can be overriden in model.
62 | def transform_attrs_methods
63 | @transform_attrs_methods ||= Module.new.tap { |x| prepend x }
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/rails_stuff/types_tracker.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/class/attribute'
2 |
3 | module RailsStuff
4 | # Adds `types_list` method which tracks all descendants.
5 | # Also allows to remove any of descendants from this list.
6 | # Useful for STI models to track all available types.
7 | #
8 | # Use with RequireNested to preload all nested classes.
9 | module TypesTracker
10 | class << self
11 | def extended(base)
12 | base.class_attribute :types_list, instance_accessor: false
13 | base.types_list = types_list_class.new
14 | base.instance_variable_set(:@_types_tracker_base, base)
15 | end
16 |
17 | # Class for `types_list`. Default to `Array`. You can override it
18 | # for all models, or assign new value to specific model
19 | # via `lypes_list=` right after extending.
20 | attr_accessor :types_list_class
21 | end
22 |
23 | self.types_list_class = Array
24 |
25 | # Add `self` to `types_list`. Defines scope for ActiveRecord models.
26 | def register_type(*args)
27 | if types_list.respond_to?(:add)
28 | types_list.add self, *args
29 | else
30 | types_list << self
31 | end
32 | if types_tracker_base.respond_to?(:scope) &&
33 | !types_tracker_base.respond_to?(model_name.element)
34 | type_name = name
35 | types_tracker_base.scope model_name.element, -> { where(type: type_name) }
36 | end
37 | end
38 |
39 | # Remove `self` from `types_list`. It doesnt remove generated scope
40 | # from ActiveRecord models, 'cause it potentialy can remove other methods.
41 | def unregister_type
42 | types_list.delete self
43 | end
44 |
45 | # Tracks all descendants automatically.
46 | def inherited(base)
47 | super
48 | base.register_type
49 | end
50 |
51 | # Class that was initilly extended with TypesTracker.
52 | def types_tracker_base
53 | @_types_tracker_base || superclass.types_tracker_base
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/rails_stuff/url_for_keeping_params.rb:
--------------------------------------------------------------------------------
1 | ActionDispatch::Routing::UrlFor.class_eval do
2 | # Safe way to generate url keeping params from request.
3 | #
4 | # It requires `request` to be present. Please don't use it in mailers
5 | # or in other places where it's not supposed to.
6 | def url_for_keeping_params(params)
7 | url_for params: request.query_parameters.merge(params)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/rails_stuff/version.rb:
--------------------------------------------------------------------------------
1 | module RailsStuff
2 | def self.gem_version
3 | Gem::Version.new VERSION::STRING
4 | end
5 |
6 | module VERSION #:nodoc:
7 | MAJOR = 0
8 | MINOR = 6
9 | TINY = 0
10 | PRE = nil
11 |
12 | STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
13 |
14 | class << self
15 | def to_s
16 | STRING
17 | end
18 |
19 | def inspect
20 | STRING.inspect
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/rails_stuff.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path('../lib', __FILE__)
2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3 | require 'rails_stuff/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'rails_stuff'
7 | spec.version = RailsStuff::VERSION::STRING
8 | spec.authors = ['Max Melentiev']
9 | spec.email = ['melentievm@gmail.com']
10 |
11 | spec.summary = 'Collection of useful modules for Rails'
12 | spec.homepage = 'https://github.com/printercu/rails_stuff'
13 | spec.license = 'MIT'
14 |
15 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
16 | spec.bindir = 'exe'
17 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18 | spec.require_paths = ['lib']
19 |
20 | spec.add_dependency 'activesupport', '> 4.0', '< 6.0'
21 |
22 | spec.add_development_dependency 'bundler', '~> 1.8'
23 | spec.add_development_dependency 'rake', '~> 10.0'
24 | end
25 |
--------------------------------------------------------------------------------
/spec/integration/requests/site/forms_spec.rb:
--------------------------------------------------------------------------------
1 | require 'integration_helper'
2 | require 'support/shared/statusable'
3 |
4 | RSpec.describe Site::FormsController, type: :request do
5 | describe '#index' do
6 | subject { get controller_path }
7 |
8 | # For rails-4
9 | def document_root_element
10 | html_document.root
11 | end
12 |
13 | include_context 'statusable'
14 | before { add_translations(status: Order.statuses.list + Customer.statuses.list) }
15 |
16 | it 'has forms with statusable selects' do
17 | should be_ok
18 | # puts response.body
19 |
20 | assert_select '#new_order' do
21 | assert_select '[name="order[status]"]' do
22 | assert_select 'option', 2 do
23 | assert_select '[value=pending][selected=selected]', 'pending_en'
24 | assert_select '[value=accepted]', 'accepted_en'
25 | end
26 | end
27 | end
28 |
29 | assert_select '#order_2' do
30 | assert_select '[name="order[status]"]' do
31 | assert_select 'option', 2 do
32 | assert_select '[value="3"][selected=selected]', 'delivered_en'
33 | assert_select '[value="2"]', 'accepted_en'
34 | end
35 | end
36 | end
37 |
38 | assert_select '#new_customer' do
39 | assert_select '[name="customer[status]"]' do
40 | assert_select 'option', 2 do
41 | assert_select 'option[value=banned][selected=selected]', 'banned_en'
42 | assert_select 'option[value=verified]', 'verified_en'
43 | end
44 | end
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/integration/requests/site/projects_spec.rb:
--------------------------------------------------------------------------------
1 | require 'integration_helper'
2 |
3 | RSpec.describe Site::ProjectsController, :db_cleaner, type: :request do
4 | let(:user) { User.create_default! }
5 | let(:user_id) { user.id }
6 | let(:resource) { Project.create_default!(user: user) }
7 | let(:resource_id) { resource.id }
8 | let(:other_user) { User.create_default! }
9 | let(:other_resource) { Project.create_default!(user: other_user) }
10 | let(:controller_resource) { controller.send :resource }
11 |
12 | describe '#index' do
13 | subject { get controller_path(user_id: user.id), params: params }
14 | let(:params) { {} }
15 | before { resource && other_resource }
16 |
17 | it 'renders index, and limits collection to parent`s resources' do
18 | should render_template :index
19 | expect(controller.send(:collection).all.to_a).to eq [resource]
20 | end
21 |
22 | context 'when pagination params are given' do
23 | let(:params) { {user_id: user.id, page: 10, per: 2} }
24 | it 'paginates collection' do
25 | should render_template :index
26 | collection = controller.send(:collection)
27 | expect(collection.offset_value).to eq 18
28 | expect(collection.limit_value).to eq 2
29 | end
30 | end
31 |
32 | context 'when parent is not found' do
33 | before { user.destroy }
34 | it { should be_not_found }
35 | end
36 | end
37 |
38 | describe '#create' do
39 | subject { -> { post controller_path(user_id: user_id), params: params } }
40 | let(:resource_params) do
41 | {
42 | name: 'New project',
43 | user_id: other_user.id,
44 | department: 'D',
45 | company: 'C',
46 | type: 'Project::Internal',
47 | }
48 | end
49 |
50 | context 'when create succeeds' do
51 | it 'redirects to created user path' do
52 | should change { user.projects.count }.by(1)
53 | resource = user.projects.last
54 | expect(resource.attributes).to include(
55 | 'name' => 'New project',
56 | 'department' => 'D',
57 | 'company' => nil,
58 | )
59 | should redirect_to site_project_path(resource)
60 | end
61 |
62 | it 'respects per-type allowed attributes' do
63 | resource_params[:type] = 'Project::External'
64 | should change { user.projects.count }.by(1)
65 | resource = user.projects.last
66 | should redirect_to site_project_path(resource)
67 | expect(resource.attributes).to include(
68 | 'name' => 'New project',
69 | 'department' => nil,
70 | 'company' => 'C',
71 | )
72 | end
73 | end
74 |
75 | context 'when create fails' do
76 | let(:resource_params) { super().except(:name) }
77 |
78 | it 'renders index' do
79 | should_not change(Project, :count)
80 | expect(response).to render_template :index
81 | expect(controller_resource.attributes).to include(
82 | 'user_id' => user.id,
83 | 'name' => nil,
84 | 'department' => 'D',
85 | 'company' => nil,
86 | )
87 | end
88 | end
89 |
90 | context 'when invalid type is requested' do
91 | let(:resource_params) { super().merge(type: 'Project::Hidden') }
92 | its(:call) { should be_unprocessable_entity }
93 | end
94 |
95 | context 'when parent is not found' do
96 | let(:user_id) { -1 }
97 | its(:call) { should be_not_found }
98 | end
99 | end
100 |
101 | describe '#update' do
102 | subject { patch(resource_path, params: params) }
103 | let(:resource_params) do
104 | {
105 | name: 'New project',
106 | user_id: other_user.id,
107 | department: 'D',
108 | company: 'C',
109 | type: 'Project::Hidden',
110 | }
111 | end
112 |
113 | context 'when update succeeds' do
114 | it 'redirects to index' do
115 | expect { should be_redirect }.
116 | to change { resource.reload.attributes.slice('name', 'department', 'company', 'type') }.
117 | to(
118 | 'name' => 'New project',
119 | 'department' => nil,
120 | 'company' => 'C',
121 | 'type' => 'Project::External',
122 | )
123 | should redirect_to site_project_path(user)
124 | end
125 |
126 | context 'when resource is of other type' do
127 | let(:resource) { super().becomes!(Project::Internal).tap(&:save!) }
128 |
129 | it 'respects per-type allowed attributes' do
130 | expect { should redirect_to site_project_path(user) }.
131 | to change { resource.reload.attributes.slice('name', 'department', 'company', 'type') }.
132 | to(
133 | 'name' => 'New project',
134 | 'department' => 'D',
135 | 'company' => nil,
136 | 'type' => 'Project::Internal',
137 | )
138 | end
139 | end
140 | end
141 |
142 | context 'when update fails' do
143 | let(:resource_params) { super().merge(name: '') }
144 |
145 | it 'renders edit' do
146 | expect { should render_template :edit }.
147 | to_not change { resource.reload.attributes }
148 | expect(controller_resource.attributes).to include(
149 | 'user_id' => user.id,
150 | 'name' => '',
151 | 'department' => nil,
152 | 'company' => 'C',
153 | 'type' => 'Project::External',
154 | )
155 | end
156 | end
157 | end
158 |
159 | describe '#destroy' do
160 | subject { -> { delete(resource_path) } }
161 | it { should change { resource.class.exists?(resource.id) }.to false }
162 | its(:call) { should redirect_to site_user_projects_path(user) }
163 | end
164 | end
165 |
--------------------------------------------------------------------------------
/spec/integration/requests/site/users_spec.rb:
--------------------------------------------------------------------------------
1 | require 'integration_helper'
2 |
3 | RSpec.describe Site::UsersController, :db_cleaner, type: :request do
4 | let(:resource) { User.create_default! }
5 | let(:resource_id) { resource.id }
6 | let(:other_resource) { User.create_default!(email: 'other@example.com') }
7 | let(:controller_resource) { controller.send :resource }
8 |
9 | describe '#index' do
10 | subject { get controller_path, params: params }
11 | let(:params) { {} }
12 | let(:collection) do
13 | should render_template :index
14 | controller.send(:collection)
15 | end
16 | let!(:resources) { [resource, other_resource] }
17 |
18 | it 'renders index' do
19 | expect(collection.all.to_a).to eq resources
20 | end
21 |
22 | context 'when pagination params are given' do
23 | let(:params) { {page: 10, per: 2} }
24 | it 'paginates collection' do
25 | expect(collection.offset_value).to eq 18
26 | expect(collection.limit_value).to eq 2
27 | end
28 |
29 | context 'and param for has_scope is given' do
30 | let(:params) { super().merge(by_email: resource.email, page: 1) }
31 | it 'paginates & filters collection' do
32 | expect(collection.all.to_a).to eq [resource]
33 | expect(collection.offset_value).to eq 0
34 | expect(collection.limit_value).to eq 2
35 | end
36 | end
37 | end
38 |
39 | context 'when param for has_scope is given' do
40 | let(:params) { {by_email: resource.email} }
41 | it 'filters collection' do
42 | expect(collection.all.to_a).to eq [resource]
43 | end
44 | end
45 | end
46 |
47 | describe '#show' do
48 | subject { get resource_path }
49 |
50 | it 'finds resource and renders template' do
51 | should render_template :show
52 | expect(controller_resource).to eq resource
53 | end
54 |
55 | context 'when resource is not found' do
56 | before { resource.destroy }
57 | it { should be_not_found }
58 | end
59 | end
60 |
61 | describe '#new' do
62 | subject { get controller_path(action: :new), params: params }
63 | let(:resource_params) { {name: 'New name', admin: true} }
64 |
65 | it 'initializes new resource' do
66 | should render_template :new
67 | expect(controller_resource).to be_instance_of User
68 | expect(controller_resource.attributes).to include(
69 | 'name' => 'New name',
70 | 'admin' => false,
71 | )
72 | end
73 | end
74 |
75 | describe '#create' do
76 | subject { post controller_path, params: params }
77 | let(:resource_params) { {name: 'New name', email: 'test', admin: true} }
78 |
79 | it 'redirects to created user path' do
80 | expect { should be_redirect }.to change(User, :count).by(1)
81 | resource = User.last
82 | expect(resource.name).to eq 'New name'
83 | should redirect_to site_user_path(resource)
84 | end
85 |
86 | context 'when create fails' do
87 | let(:resource_params) { super().except(:email) }
88 |
89 | it 'renders :new' do
90 | expect { should render_template :new }.to_not change(User, :count)
91 | expect(controller_resource.attributes).to include(
92 | 'name' => 'New name',
93 | 'admin' => false,
94 | )
95 | end
96 | end
97 | end
98 |
99 | describe '#edit' do
100 | subject { get resource_path(action: :edit) }
101 |
102 | it 'finds resource and renders template' do
103 | should render_template :edit
104 | expect(controller_resource).to eq resource
105 | end
106 |
107 | context 'when resource is not found' do
108 | before { resource.destroy }
109 | it { should be_not_found }
110 | end
111 | end
112 |
113 | describe '#update' do
114 | subject { patch resource_path, params: params }
115 | let(:resource_params) { {name: 'New name', admin: true} }
116 |
117 | context 'when update succeeds' do
118 | it 'redirects to user path' do
119 | expect { should be_redirect }.
120 | to change { resource.reload.name }.to('New name')
121 | expect(resource.admin).to eq false
122 | should redirect_to site_user_path(resource)
123 | end
124 | end
125 |
126 | context 'when update fails' do
127 | let(:resource_params) { {name: '', admin: true} }
128 |
129 | it 'renders :edit' do
130 | expect { should render_template :edit }.
131 | to_not change { resource.reload.name }
132 | expect(controller_resource.attributes).to include(
133 | 'name' => '',
134 | 'admin' => false,
135 | )
136 | end
137 | end
138 |
139 | context 'when resource is not found' do
140 | before { resource.destroy }
141 | it { should be_not_found }
142 | end
143 | end
144 |
145 | describe '#destroy' do
146 | subject { delete resource_path }
147 |
148 | context 'when destroy succeeds' do
149 | it 'redirects to index' do
150 | expect { should redirect_to site_users_path }.
151 | to change { User.find_by id: resource.id }.to(nil)
152 | end
153 | end
154 |
155 | context 'when destroy fails' do
156 | before do
157 | expect_any_instance_of(User).to receive(:destroy) do |instance|
158 | instance.errors.add :base, :forbidden
159 | end
160 | end
161 |
162 | it 'redirects to index and flashes error' do
163 | expect { should redirect_to site_users_path }.
164 | to_not change { User.find_by id: resource.id }
165 | expect(controller_resource.errors[:base]).to be_present
166 | expect(flash[:error]).to be_present
167 | end
168 | end
169 |
170 | context 'when resource is not found' do
171 | before { resource.destroy }
172 | it { should be_not_found }
173 | end
174 | end
175 |
176 | describe '.action_methods' do
177 | subject { described_class.action_methods }
178 | it { should_not include 'source_for_collection' }
179 | end
180 | end
181 |
--------------------------------------------------------------------------------
/spec/integration_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 | require 'rails/engine'
3 | require 'rails/railtie'
4 | require 'support/active_record'
5 | require 'kaminari'
6 | require 'has_scope'
7 | require 'activemodel_translation/helper'
8 | Kaminari::Hooks.init
9 |
10 | ENV['RAILS_ENV'] = 'test'
11 | class TestApplication < Rails::Application
12 | config.eager_load = false
13 | config.log_level = :debug
14 | secrets[:secret_key_base] = 'test'
15 |
16 | config.action_dispatch.rescue_responses.merge!(
17 | 'RailsStuff::ResourcesController::StiHelpers::InvalidType' => :unprocessable_entity,
18 | )
19 | end
20 | Rails.application.initialize!
21 |
22 | # # Routes
23 | Rails.application.routes.draw do
24 | namespace :site do
25 | resources :users do
26 | resources :projects, shallow: true
27 | end
28 | resources :forms, only: :index
29 | end
30 | end
31 |
32 | RailsStuff::TestHelpers.setup only: %w[integration_session response]
33 | RailsStuff::RSpecHelpers.setup only: %w[groups/request clear_logs]
34 |
35 | # # Models
36 | module BuildDefault
37 | def build_default(**attrs)
38 | new(const_get(:DEFAULT_ATTRS).merge(attrs))
39 | end
40 |
41 | def create_default!(**attrs)
42 | build_default(attrs).tap(&:save)
43 | end
44 | end
45 |
46 | class User < ApplicationRecord
47 | has_many :projects, dependent: :nullify
48 | validates_presence_of :name, :email
49 | scope :by_email, ->(val) { where(email: val) }
50 |
51 | DEFAULT_ATTRS = {
52 | name: 'John',
53 | email: 'john@example.domain',
54 | }.freeze
55 | extend BuildDefault
56 | end
57 |
58 | class Project < ApplicationRecord
59 | belongs_to :user, required: true
60 | validates_presence_of :name
61 | extend RailsStuff::TypesTracker
62 |
63 | DEFAULT_ATTRS = {
64 | name: 'Haps.me',
65 | type: 'Project::External',
66 | }.freeze
67 | extend BuildDefault
68 |
69 | class Internal < self
70 | end
71 |
72 | class External < self
73 | end
74 |
75 | class Hidden < self
76 | unregister_type
77 | end
78 | end
79 |
80 | class Customer < ApplicationRecord
81 | extend ActiveModel::Translation
82 | extend RailsStuff::Statusable
83 | has_status_field :status, %i[verified banned premium]
84 | end
85 |
86 | class Order < ApplicationRecord
87 | extend ActiveModel::Translation
88 | extend RailsStuff::Statusable
89 | has_status_field :status, mapping: {pending: 1, accepted: 2, delivered: 3}
90 | end
91 |
92 | # # Controllers
93 | class ApplicationController < ActionController::Base
94 | extend RailsStuff::ResourcesController
95 | include Rails.application.routes.url_helpers
96 | self.view_paths = GEM_ROOT.join('spec/support/app/views')
97 | end
98 |
99 | class SiteController < ApplicationController
100 | rescue_from ActiveRecord::RecordNotFound, with: -> { head :not_found }
101 | respond_to :html
102 | end
103 |
104 | module Site
105 | class UsersController < SiteController
106 | resources_controller kaminari: true
107 | permit_attrs :name, :email
108 | has_scope :by_email
109 | end
110 |
111 | class ProjectsController < SiteController
112 | resources_controller sti: true,
113 | kaminari: true,
114 | belongs_to: [:user, optional: true]
115 | permit_attrs :name
116 | permit_attrs_for Project::External, :company
117 | permit_attrs_for Project::Internal, :department
118 |
119 | def create
120 | super(action: :index)
121 | end
122 | end
123 |
124 | class FormsController < SiteController
125 | end
126 | end
127 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | require 'action_view'
2 | require 'action_dispatch'
3 | require 'action_controller'
4 | require 'rspec/rails'
5 | require 'support/controller'
6 |
--------------------------------------------------------------------------------
/spec/rails_stuff/association_writer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'integration_helper'
2 |
3 | RSpec.describe RailsStuff::AssociationWriter, :db_cleaner do
4 | let(:klass) do
5 | described_class = self.described_class
6 | build_named_class(:Project, ActiveRecord::Base) do
7 | belongs_to :user
8 | extend described_class
9 | attr_reader :username
10 |
11 | association_writer :user do |val|
12 | super(val).tap { @username = user.try!(:name) }
13 | end
14 | end
15 | end
16 | let(:instance) { klass.new }
17 |
18 | describe 'updating by id' do
19 | subject { -> { instance.user_id = new_val } }
20 | let(:new_val) { user.id }
21 | let(:user) { User.create_default! }
22 | it { should change(instance, :username).to(user.name) }
23 | it { should change(instance, :user).to(user) }
24 | its(:call) { should eq new_val }
25 |
26 | context 'when invalid id is given' do
27 | let(:new_val) { -1 }
28 | it { should_not change(instance, :username).from(nil) }
29 | it { should_not change(instance, :user).from(nil) }
30 | its(:call) { should eq new_val }
31 | end
32 | end
33 |
34 | describe 'updating with object' do
35 | subject { -> { instance.user = new_val } }
36 | let(:new_val) { user }
37 | let(:user) { User.create_default! }
38 | it { should change(instance, :username).to(user.name) }
39 | it { should change(instance, :user).to(user) }
40 | its(:call) { should eq new_val }
41 |
42 | context 'when nil is given' do
43 | let(:new_val) {}
44 | it { should_not change(instance, :username).from(nil) }
45 | it { should_not change(instance, :user).from(nil) }
46 | its(:call) { should eq new_val }
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/rails_stuff/generators/concern_generator_spec.rb:
--------------------------------------------------------------------------------
1 | # Here is Minitest test from Rails, I've made it when proposed this generator
2 | # to rails.
3 | #
4 | # While there is no built-in support for generator specs in RSpec,
5 | # it just create exaples which run Minitest tests.
6 |
7 | require 'rails_stuff/generators/concern/concern_generator'
8 |
9 | class ConcernGeneratorTest < Rails::Generators::TestCase
10 | self.generator_class = RailsStuff::Generators::ConcernGenerator
11 | arguments %w[User::Authentication]
12 |
13 | def test_concern_is_created
14 | run_generator
15 | assert_file 'app/models/user/authentication.rb' do |content|
16 | assert_match(/module User::Authentication/, content)
17 | assert_match(/extend ActiveSupport::Concern/, content)
18 | assert_match(/included do/, content)
19 | end
20 | end
21 |
22 | def test_concern_on_revoke
23 | concern_path = 'app/models/user/authentication.rb'
24 | run_generator
25 | assert_file concern_path
26 | run_generator ['User::Authentication'], behavior: :revoke
27 | assert_no_file concern_path
28 | end
29 | end
30 |
31 | class ControllerConcernGeneratorTest < Rails::Generators::TestCase
32 | self.generator_class = RailsStuff::Generators::ConcernGenerator
33 | arguments %w[admin_controller/localized]
34 |
35 | def test_concern_is_created
36 | run_generator
37 | assert_file 'app/controllers/admin_controller/localized.rb' do |content|
38 | assert_match(/module AdminController::Localized/, content)
39 | assert_match(/extend ActiveSupport::Concern/, content)
40 | assert_match(/included do/, content)
41 | end
42 | end
43 | end
44 |
45 | Rails::Generators::TestCase.destination File.expand_path('tmp', GEM_ROOT)
46 |
47 | RSpec.describe RailsStuff::Generators::ConcernGenerator do
48 | [ConcernGeneratorTest, ControllerConcernGeneratorTest].each do |klass|
49 | klass.test_order = :random
50 | klass.runnable_methods.each do |method_name|
51 | it "[delegation to #{klass.name}##{method_name}]" do
52 | test = klass.new(method_name)
53 | test.run
54 | test.failures.first.try! { |x| raise x }
55 | end
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/spec/rails_stuff/helpers/bootstrap_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe RailsStuff::Helpers::Bootstrap, type: :helper do
2 | describe '#basic_link_icons' do
3 | context 'when used with Helper::Links' do
4 | let(:helper) do
5 | Object.tap { |x| x.extend RailsStuff::Helpers::Links }.
6 | tap { |x| x.extend described_class }
7 | end
8 |
9 | it 'returns glyphicons' do
10 | expect(helper.basic_link_icon(:edit)).to include('glyphicon')
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/rails_stuff/helpers/forms_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe RailsStuff::Helpers::Forms, type: :helper do
4 | describe '#hidden_params_fields' do
5 | before do
6 | {
7 | str_1: 'val_1',
8 | str_2: '',
9 | ar_1: %w[val_1_1 val_1_2],
10 | ar_2: %w[val_2],
11 | ar_3: [],
12 | ar_4: [''],
13 | null: nil,
14 | }.each { |k, v| params[k] = v }
15 | end
16 |
17 | def assert_inputs(fields, expected)
18 | html = helper.hidden_params_fields(*fields)
19 | result = Nokogiri::HTML.fragment(html).children.
20 | map { |x| [x.attr(:name), x.attr(:value)] }
21 | expect(result).to contain_exactly(*expected)
22 | end
23 |
24 | it 'works for scalar values' do
25 | assert_inputs %i[str_1 str_2], [%w[str_1 val_1], ['str_2', '']]
26 | assert_inputs %i[str_1 str_3], [%w[str_1 val_1]]
27 | assert_inputs %i[str_1 null], [%w[str_1 val_1], ['null', nil]]
28 | end
29 |
30 | it 'works for array' do
31 | assert_inputs %i[ar_1 ar_2],
32 | [%w(ar_1[] val_1_1), %w(ar_1[] val_1_2), %w(ar_2[] val_2)]
33 | assert_inputs %i[ar_2 ar_3], [%w(ar_2[] val_2)]
34 | assert_inputs [:ar_4], [['ar_4[]', '']]
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/rails_stuff/helpers/links_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe RailsStuff::Helpers::Links, type: :helper do
4 | before { allow(helper).to receive(:basic_link_icons) { icons } }
5 | let(:icons) { %i[destroy edit new].map { |x| [x, :"#{x}_icon"] }.to_h }
6 |
7 | describe '#basic_link_icon' do
8 | let(:icons) do
9 | {
10 | proc: -> { custom_method },
11 | str: 'string',
12 | }
13 | end
14 |
15 | before do
16 | def helper.custom_method
17 | :custom_result
18 | end
19 | end
20 |
21 | it 'returns string and nil as is' do
22 | expect(helper.basic_link_icon(:str)).to eq 'string'
23 | expect(helper.basic_link_icon(:string)).to eq nil
24 | end
25 |
26 | it 'executes procs' do
27 | expect(helper.basic_link_icon(:proc)).to eq :custom_result
28 | end
29 | end
30 |
31 | describe '#link_to_destroy' do
32 | before { helper.extend RailsStuff::Helpers::Translation }
33 |
34 | it 'calls link_to with specific params' do
35 | expect(helper).to receive(:translate_action).with(:delete) { :delete_title }
36 | expect(helper).to receive(:translate_confirmation).with(:delete) { :confirm_delete }
37 | expect(helper).to receive(:link_to).
38 | with(:destroy_icon, :destroy_url,
39 | title: :delete_title,
40 | method: :delete,
41 | data: {confirm: :confirm_delete},
42 | extra: :arg,
43 | ) { 'destroy_link_html' }
44 | expect(helper.link_to_destroy :destroy_url, extra: :arg).to eq 'destroy_link_html'
45 | end
46 | end
47 |
48 | describe '#link_to_edit' do
49 | before { helper.extend RailsStuff::Helpers::Translation }
50 |
51 | it 'calls link_to with specific params' do
52 | expect(helper).to receive(:translate_action).with(:edit) { :edit_title }
53 | expect(helper).to receive(:link_to).
54 | with(:edit_icon, :edit_url, title: :edit_title, extra: :arg) { 'edit_link_html' }
55 | expect(helper.link_to_edit :edit_url, extra: :arg).to eq 'edit_link_html'
56 | end
57 | end
58 |
59 | describe '#link_to_new' do
60 | it 'calls link_to with specific params' do
61 | expect(helper).to receive(:link_to).
62 | with(:new_icon, :new_url, extra: :arg) { 'new_link_html' }
63 | expect(helper.link_to_new :new_url, extra: :arg).to eq 'new_link_html'
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/rails_stuff/helpers/resource_form_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe RailsStuff::Helpers::ResourceForm do
2 | let(:helper) { Module.new.tap { |x| x.extend described_class } }
3 |
4 | describe '#resource_form_for' do
5 | it 'compiles method without errors' do
6 | expect { helper.resource_form_for }.
7 | to change { helper.instance_methods.include?(:resource_form) }.to(true)
8 | end
9 |
10 | it 'compiles method with specified name' do
11 | expect { helper.resource_form_for method: :fform }.
12 | to change { helper.instance_methods.include?(:fform) }.to(true)
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/rails_stuff/helpers/text_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | RSpec.describe RailsStuff::Helpers::Text, type: :helper do
4 | describe '#replace_blank' do
5 | before { expect(I18n).to receive(:t) { :blank } }
6 | let(:placeholder) { '(blank)' }
7 |
8 | it 'replaces blank values with translated placeholder' do
9 | expect(helper.replace_blank('')).to eq placeholder
10 | expect(helper.replace_blank(nil)).to eq placeholder
11 |
12 | expect(helper.replace_blank('0')).to eq '0'
13 | expect(helper.replace_blank(0)).to eq 0
14 | end
15 |
16 | context 'when block is given' do
17 | it 'tests value & returns block`s result if value is present' do
18 | expect(helper.replace_blank('') { 'block' }).to eq placeholder
19 | expect(helper.replace_blank([]) { 'block' }).to eq placeholder
20 | expect(helper.replace_blank(nil) { 'block' }).to eq placeholder
21 | expect(helper.replace_blank(nil, &:name)).to eq placeholder
22 |
23 | expect(helper.replace_blank('test') { 'block' }).to eq 'block'
24 | expect(helper.replace_blank(1) { 'block' }).to eq 'block'
25 | expect(helper.replace_blank([1]) { 'block' }).to eq 'block'
26 |
27 | expect(helper.replace_blank(2) { |x| 'block' * x }).to eq 'blockblock'
28 | expect(helper.replace_blank(double(name: 'test'), &:name)).to eq 'test'
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/rails_stuff/helpers/translation_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe RailsStuff::Helpers::Translation do
2 | let(:helper) { Object.new.tap { |x| x.extend described_class } }
3 |
4 | before do
5 | I18n.backend = I18n::Backend::Simple.new
6 | I18n.backend.store_translations 'en', helpers: {
7 | actions: {
8 | edit: 'edit_en',
9 | delete: 'delete_en',
10 | },
11 | confirmations: {
12 | destroy: 'destroy_en',
13 | clean: 'clean_en',
14 | },
15 | yes_no: {
16 | 'true' => 'yeah',
17 | 'false' => 'nope',
18 | },
19 | }
20 | end
21 |
22 | def self.with_i18n_raise(val)
23 | around do |ex|
24 | begin
25 | old_val = described_class.i18n_raise
26 | described_class.i18n_raise = val
27 | ex.run
28 | ensure
29 | described_class.i18n_raise = old_val
30 | end
31 | end
32 | end
33 |
34 | shared_examples 'raising on missing translation' do
35 | context 'when translation is missing' do
36 | let(:input) { 'missing_key' }
37 |
38 | context 'when .i18n_raise is true' do
39 | with_i18n_raise(false)
40 | its(:call) { should be_present }
41 | end
42 |
43 | context 'when .i18n_raise is true' do
44 | with_i18n_raise(true)
45 | it { should raise_error(I18n::MissingTranslationData) }
46 | end
47 | end
48 | end
49 |
50 | describe '#translate_action' do
51 | subject { ->(val = input) { helper.translate_action(val) } }
52 | include_examples 'raising on missing translation'
53 |
54 | it 'translates and caches actions' do
55 | expect(I18n).to receive(:t).once.and_call_original
56 | 2.times { expect(subject[:edit]).to eq 'edit_en' }
57 | expect(I18n).to receive(:t).once.and_call_original
58 | 2.times { expect(subject['delete']).to eq 'delete_en' }
59 | end
60 | end
61 |
62 | describe '#translate_confirmation' do
63 | subject { ->(val = input) { helper.translate_confirmation(val) } }
64 | include_examples 'raising on missing translation'
65 |
66 | it 'translates and caches confirmations' do
67 | expect(I18n).to receive(:t).once.and_call_original
68 | 2.times { expect(subject[:destroy]).to eq 'destroy_en' }
69 | expect(I18n).to receive(:t).once.and_call_original
70 | 2.times { expect(subject['clean']).to eq 'clean_en' }
71 | end
72 | end
73 |
74 | describe '#yes_no' do
75 | subject { ->(val = input) { helper.yes_no(val) } }
76 | include_examples 'raising on missing translation'
77 |
78 | it 'translates and caches boolean values' do
79 | expect(I18n).to receive(:t).once.and_call_original
80 | 2.times { expect(subject[true]).to eq 'yeah' }
81 | expect(I18n).to receive(:t).once.and_call_original
82 | 2.times { expect(subject[false]).to eq 'nope' }
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/spec/rails_stuff/nullify_blank_attrs_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe RailsStuff::NullifyBlankAttrs do
2 | let(:klass) do
3 | described_class = self.described_class
4 | Class.new do
5 | extend described_class
6 | nullify_blank_attrs :nested, :own
7 |
8 | include(Module.new do
9 | attr_accessor :nested
10 | end)
11 |
12 | attr_accessor :own
13 | end
14 | end
15 | let(:instance) { klass.new }
16 | around { |ex| RailsStuff.deprecation_07.silence { ex.run } }
17 |
18 | it 'nullifies blank attrs' do
19 | expect { instance.nested = 'test' }.to change { instance.nested }.from(nil).to('test')
20 | expect { instance.nested = ' ' }.to change { instance.nested }.to(nil)
21 | end
22 |
23 | it 'works with attrs defined in class' do
24 | expect { instance.own = 'test' }.to change { instance.own }.from(nil).to('test')
25 | expect { instance.own = ' ' }.to change { instance.own }.to(nil)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/rails_stuff/params_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require 'json'
2 | require 'active_support/core_ext/time'
3 |
4 | Time.zone_default = Time.find_zone('UTC')
5 |
6 | RSpec.describe RailsStuff::ParamsParser do
7 | describe '.parse_string' do
8 | subject { ->(val = input) { described_class.parse_string(val) } }
9 |
10 | it 'casts input to string' do
11 | {
12 | nil => nil,
13 | '' => '',
14 | 'a' => 'a',
15 | 1 => '1',
16 | [] => '[]',
17 | }.each do |input, expected|
18 | expect(subject.call(input)).to eq expected
19 | end
20 | end
21 |
22 | context 'when input is invalid' do
23 | let(:input) { Object.new }
24 | before do
25 | def input.to_s
26 | raise 'test'
27 | end
28 | end
29 | it { should raise_error(described_class::Error) }
30 | end
31 | end
32 |
33 | describe '.parse_string_array' do
34 | subject { ->(val = input) { described_class.parse_string_array(val) } }
35 |
36 | it 'casts input to string' do
37 | {
38 | nil => nil,
39 | '' => nil,
40 | [1, :a, 'b'] => %w[1 a b],
41 | ['str', '', nil] => ['str', '', nil],
42 | }.each do |input, expected|
43 | expect(subject.call(input)).to eq expected
44 | end
45 | end
46 |
47 | context 'when input is invalid' do
48 | let(:input) { [Object.new, '1'] }
49 | before do
50 | val = input[0]
51 | def val.to_s
52 | raise 'test'
53 | end
54 | end
55 | it { should raise_error(described_class::Error) }
56 | end
57 | end
58 |
59 | describe '.parse_int' do
60 | subject { ->(val = input) { described_class.parse_int(val) } }
61 |
62 | it 'casts input to integer' do
63 | {
64 | nil => nil,
65 | '' => nil,
66 | 'a' => 0,
67 | '1' => 1,
68 | '-5.5' => -5,
69 | }.each do |input, expected|
70 | expect(subject.call(input)).to be expected
71 | end
72 | end
73 |
74 | context 'when input is invalid' do
75 | let(:input) { [] }
76 | it { should raise_error(described_class::Error) }
77 | end
78 | end
79 |
80 | describe '.parse_int_array' do
81 | subject { ->(val = input) { described_class.parse_int_array(val) } }
82 |
83 | it 'casts input to integer' do
84 | {
85 | nil => nil,
86 | '' => nil,
87 | ['1', '2', 3, '-5.5'] => [1, 2, 3, -5],
88 | ['1', '2', '', '3', 'a', nil] => [1, 2, nil, 3, 0, nil],
89 | }.each do |input, expected|
90 | expect(subject.call(input)).to eq expected
91 | end
92 | end
93 |
94 | context 'when input is invalid' do
95 | let(:input) { [['1'], '1'] }
96 | it { should raise_error(described_class::Error) }
97 | end
98 | end
99 |
100 | describe '.parse_decimal' do
101 | subject { ->(val = input) { described_class.parse_decimal(val) } }
102 |
103 | it 'casts input to BigDecimal' do
104 | {
105 | nil => nil,
106 | '' => nil,
107 | 'a' => 0,
108 | '1' => 1,
109 | '-5.5' => -5.5,
110 | }.each do |input, expected|
111 | result = subject.call(input)
112 | expect(result).to eq expected
113 | expect(result).to be_instance_of BigDecimal if result
114 | end
115 | end
116 |
117 | context 'when input is invalid' do
118 | let(:input) { [] }
119 | it { should raise_error(described_class::Error) }
120 | end
121 | end
122 |
123 | describe '.parse_decimal_array' do
124 | subject { ->(val = input) { described_class.parse_decimal_array(val) } }
125 |
126 | it 'casts input to BigDecimal' do
127 | {
128 | nil => nil,
129 | '' => nil,
130 | ['1', '2', 3, '-5.5'] => [1, 2, 3, -5.5],
131 | ['1', '2', '', '3', 'a', nil] => [1, 2, nil, 3, 0, nil],
132 | }.each do |input, expected|
133 | result = subject.call(input)
134 | expect(result).to eq expected
135 | result.each { |x| expect(x).to be_instance_of BigDecimal if x } if result
136 | end
137 | end
138 |
139 | context 'when input is invalid' do
140 | let(:input) { [['1'], '1'] }
141 | it { should raise_error(described_class::Error) }
142 | end
143 | end
144 |
145 | describe '.parse_boolean' do
146 | subject { ->(val = input) { described_class.parse_boolean(val) } }
147 | it 'parses boolean values' do
148 | [nil, ''].each { |x| expect(subject.call(x)).to eq nil }
149 | [true, '1', 't', 'true', 'on'].each { |x| expect(subject.call(x)).to eq true }
150 | [false, '0', 'f', 'false', 'off'].each { |x| expect(subject.call(x)).to eq false }
151 | end
152 | end
153 |
154 | describe '.parse_datetime' do
155 | subject { ->(val = input) { described_class.parse_datetime(val) } }
156 |
157 | context 'when argument is a string' do
158 | it 'parses time' do
159 | expect(subject.call('19:00')).to eq(Time.zone.parse('19:00'))
160 | expect(subject.call('01.02.2013 19:00')).to eq(Time.zone.parse('2013-02-01 19:00'))
161 | end
162 |
163 | context 'when argument is empty string' do
164 | let(:input) { '' }
165 | its(:call) { should eq nil }
166 | end
167 |
168 | context 'when argument is not date representation' do
169 | let(:input) { 'smth' }
170 | it { should raise_error(described_class::Error) }
171 | end
172 | end
173 |
174 | context 'when argument is nil' do
175 | let(:input) {}
176 | its(:call) { should be nil }
177 | end
178 |
179 | context 'when argument is not a string' do
180 | it 'raise error' do
181 | expect { subject.call([]) }.to raise_error(described_class::Error)
182 | expect { subject.call({}) }.to raise_error(described_class::Error)
183 | end
184 | end
185 | end
186 |
187 | describe '.parse_json' do
188 | subject { ->(val = input) { described_class.parse_json(val) } }
189 |
190 | context 'when argument is nil' do
191 | let(:input) {}
192 | its(:call) { should be nil }
193 | end
194 |
195 | context 'when argument is a string' do
196 | it 'parses json' do
197 | expect(subject.call('{"a": 1}')).to eq('a' => 1)
198 | expect(subject.call('[1,2]')).to eq([1, 2])
199 | end
200 |
201 | context 'when argument is empty string' do
202 | let(:input) { '' }
203 | its(:call) { should be nil }
204 | end
205 |
206 | context 'when argument is invalid json' do
207 | it 'raise error' do
208 | expect { subject.call('{') }.to raise_error(described_class::Error)
209 | expect { subject.call('[]]') }.to raise_error(described_class::Error)
210 | end
211 | end
212 | end
213 |
214 | context 'when argument is not a string' do
215 | it 'raise error' do
216 | expect { subject.call([1]) }.to raise_error(described_class::Error)
217 | expect { subject.call(a: 1) }.to raise_error(described_class::Error)
218 | end
219 | end
220 | end
221 | end
222 |
--------------------------------------------------------------------------------
/spec/rails_stuff/random_uniq_attr_spec.rb:
--------------------------------------------------------------------------------
1 | require 'support/active_record'
2 |
3 | RSpec.describe RailsStuff::RandomUniqAttr, :db_cleaner do
4 | let(:model) do
5 | described_class = self.described_class
6 | attr = self.attr
7 | Class.new(ApplicationRecord) do
8 | self.table_name = :tokens
9 | extend described_class
10 | random_uniq_attr attr
11 | end
12 | end
13 | let(:attr) { :code }
14 |
15 | def create_instance(attrs = {})
16 | model.create!(attrs)
17 | end
18 |
19 | describe '#set_#{attr}' do
20 | let(:other_instance) { create_instance attr => :test }
21 | let(:instance) { create_instance }
22 |
23 | context 'when instance with same generated attr value exists' do
24 | let(:values) { ([:test_2] + [other_instance[attr]] * 5).map(&:to_s) }
25 | before do
26 | other_instance
27 | allow(other_instance.class).to receive("generate_#{attr}") { values.pop }.times
28 | end
29 |
30 | it 'generates new value until it is unique' do
31 | expect(instance[attr]).to eq('test_2')
32 | expect(instance.reload[attr]).to eq('test_2')
33 | end
34 |
35 | context 'when uniq value can not be generated' do
36 | let(:values) { double(:values, pop: other_instance[attr]) }
37 | it { expect { instance }.to raise_error ActiveRecord::RecordNotUnique }
38 | end
39 | end
40 |
41 | it 'passes instance to generate_ method' do
42 | expect(instance.class).to receive("generate_#{attr}").with(instance) { 'new_val' }
43 | expect { instance.send "set_#{attr}" }.to change { instance[attr] }.to 'new_val'
44 | end
45 | end
46 |
47 | describe '.random_uniq_attr' do
48 | let(:klass) do
49 | described_class = self.described_class
50 | Class.new(ApplicationRecord) { extend described_class }
51 | end
52 |
53 | context 'when block is given' do
54 | let(:values) { 5.times.to_a }
55 | before do
56 | values = self.values
57 | klass.random_uniq_attr(:key) { values.shift }
58 | end
59 |
60 | it 'uses block as generator' do
61 | values.dup.each do |val|
62 | expect(klass.generate_key).to eq(val)
63 | end
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/spec/rails_stuff/redis_storage_spec.rb:
--------------------------------------------------------------------------------
1 | require 'support/redis'
2 |
3 | RSpec.describe RailsStuff::RedisStorage do
4 | let(:model) do
5 | described_class = self.described_class
6 | Class.new do
7 | extend described_class
8 | self.redis_prefix = :rails_stuff_test
9 | end
10 | end
11 | let(:prefix) { model.redis_prefix }
12 |
13 | let(:parent) do
14 | described_class = self.described_class
15 | Class.new { extend described_class }
16 | end
17 | let(:child) { Class.new(parent) }
18 | let(:redis) { parent.redis_pool.simple_connection }
19 |
20 | describe '#redis_set_options' do
21 | it 'inherits value from parent' do
22 | parent_val = {a: 1}
23 | child_val = {b: 2}
24 |
25 | expect(parent.redis_set_options).to eq({})
26 | expect(child.redis_set_options).to eq({})
27 |
28 | parent.redis_set_options = parent_val
29 | expect(parent.redis_set_options).to eq(parent_val)
30 | expect(child.redis_set_options).to eq(parent_val)
31 |
32 | child.redis_set_options = child_val
33 | expect(parent.redis_set_options).to eq(parent_val)
34 | expect(child.redis_set_options).to eq(child_val)
35 | end
36 | end
37 |
38 | describe '#redis_key_for' do
39 | it 'works with scalars' do
40 | expect(model.redis_key_for(1)).to eq "#{prefix}:1"
41 | expect(model.redis_key_for(nil)).to eq "#{prefix}:"
42 | expect(model.redis_key_for('test')).to eq "#{prefix}:test"
43 | model.redis_prefix = :test_2
44 | expect(model.redis_key_for(1)).to eq 'test_2:1'
45 | end
46 |
47 | it 'works with array' do
48 | expect(model.redis_key_for([])).to eq "#{prefix}:"
49 | expect(model.redis_key_for([1, 2])).to eq "#{prefix}:1:2"
50 | expect(model.redis_key_for([1, nil, 2])).to eq "#{prefix}:1::2"
51 | expect(model.redis_key_for(['test', :a])).to eq "#{prefix}:test:a"
52 | model.redis_prefix = :test_2
53 | expect(model.redis_key_for([1, 2])).to eq 'test_2:1:2'
54 | end
55 | end
56 |
57 | describe '#redis_id_seq_key' do
58 | it 'works with scalars' do
59 | expect(model.redis_id_seq_key).to eq "#{prefix}_id_seq"
60 | expect(model.redis_id_seq_key(nil)).to eq "#{prefix}_id_seq"
61 | expect(model.redis_id_seq_key('test')).to eq "#{prefix}_id_seq:test"
62 | model.redis_prefix = :test_2
63 | expect(model.redis_id_seq_key(1)).to eq 'test_2_id_seq:1'
64 | end
65 |
66 | it 'works with array' do
67 | expect(model.redis_id_seq_key([])).to eq "#{prefix}_id_seq"
68 | expect(model.redis_id_seq_key([1, 2])).to eq "#{prefix}_id_seq:1:2"
69 | expect(model.redis_id_seq_key([1, nil, 2])).to eq "#{prefix}_id_seq:1::2"
70 | expect(model.redis_id_seq_key(['test', :a])).to eq "#{prefix}_id_seq:test:a"
71 | model.redis_prefix = :test_2
72 | expect(model.redis_id_seq_key([1, 2])).to eq 'test_2_id_seq:1:2'
73 | end
74 | end
75 |
76 | describe '#reset_id_seq' do
77 | it 'deletes counter' do
78 | redis.set("#{prefix}_id_seq", 1)
79 | redis.set("#{prefix}_id_seq:1:2", 1)
80 |
81 | expect { model.reset_id_seq }.
82 | to change { redis.get("#{prefix}_id_seq") }.to(nil)
83 | expect { model.reset_id_seq([1, 2]) }.
84 | to change { redis.get("#{prefix}_id_seq:1:2") }.to(nil)
85 | end
86 | end
87 |
88 | describe '#next_id' do
89 | it 'returns sequental numbers' do
90 | redis.del("#{prefix}_id_seq")
91 | redis.del("#{prefix}_id_seq:1:2")
92 |
93 | {
94 | 1 => nil,
95 | 2 => 1,
96 | }.each do |next_val, prev_val|
97 | expect { expect(model.next_id).to eq(next_val) }.
98 | to change { redis.get("#{prefix}_id_seq").try(:to_i) }.
99 | from(prev_val).to(next_val)
100 |
101 | expect { expect(model.next_id([1, 2])).to eq(next_val) }.
102 | to change { redis.get("#{prefix}_id_seq:1:2").try(:to_i) }.
103 | from(prev_val).to(next_val)
104 | end
105 | end
106 | end
107 |
108 | describe '#set' do
109 | context 'when id is nil' do
110 | it 'generates new id for the record' do
111 | model.reset_id_seq
112 | redis.del("#{prefix}:1")
113 | redis.del("#{prefix}:2")
114 |
115 | expect do
116 | expect { expect(model.set(nil, :test)).to eq(1) }.
117 | to change { redis.get("#{prefix}_id_seq").try(:to_i) }.from(nil).to(1)
118 | end.to change { redis.get("#{prefix}:1") }.from(nil)
119 | expect do
120 | expect { expect(model.set([], :test)).to eq(2) }.
121 | to change { redis.get("#{prefix}_id_seq").try(:to_i) }.from(1).to(2)
122 | end.to change { redis.get("#{prefix}:2") }.from(nil)
123 | end
124 |
125 | context 'for scoped key' do
126 | it 'generates new id for the record' do
127 | model.reset_id_seq(%i[a b])
128 | redis.del("#{prefix}:a:b:1")
129 |
130 | expect do
131 | expect { expect(model.set([:a, :b, nil], :test)).to eq(1) }.
132 | to change { redis.get("#{prefix}_id_seq:a:b").try(:to_i) }.from(nil).to(1)
133 | end.to change { redis.get("#{prefix}:a:b:1") }.from(nil)
134 | end
135 | end
136 | end
137 |
138 | context 'when id is not nil' do
139 | it 'updates value' do
140 | redis.set("#{prefix}:1", 1)
141 | redis.set("#{prefix}:a:b:1", 1)
142 |
143 | expect { model.set(1, 2) }.to change { redis.get("#{prefix}:1") }.from('1')
144 | expect { model.set([:a, :b, 1], 2) }.
145 | to change { redis.get("#{prefix}:a:b:1") }.from('1')
146 | end
147 | end
148 | end
149 |
150 | describe '#delete' do
151 | it 'removes key' do
152 | redis.set("#{prefix}:1", 1)
153 | redis.set("#{prefix}:a:b:1", 1)
154 |
155 | expect { model.delete(1) }.
156 | to change { redis.get("#{prefix}:1") }.from('1').to(nil)
157 | expect { model.delete([:a, :b, 1]) }.
158 | to change { redis.get("#{prefix}:a:b:1") }.from('1').to(nil)
159 | end
160 | end
161 |
162 | describe '#get' do
163 | it 'returns saved value' do
164 | model.set(1, test: 1)
165 | expect(model.get(1)).to eq(test: 1)
166 | model.set([:a, :b, 2], test: 2)
167 | expect(model.get([:a, :b, 2])).to eq(test: 2)
168 | end
169 | end
170 | end
171 |
--------------------------------------------------------------------------------
/spec/rails_stuff/resources_controller/actions_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe RailsStuff::ResourcesController::Actions do
2 | let(:klass) { build_controller_class }
3 | let(:controller) { klass.new }
4 | let(:resource) { double(:resource) }
5 | let(:block) { -> {} }
6 | let(:options) { {option: :param} }
7 | before do
8 | allow(controller).to receive_messages(
9 | after_save_url: :save_redirect_url,
10 | after_destroy_url: :destroy_redirect_url,
11 | resource: resource,
12 | )
13 | end
14 |
15 | def build_controller_class(class_name = nil)
16 | described_class = self.described_class
17 | build_named_class(class_name, ActionController::Base) do
18 | include RailsStuff::ResourcesController::BasicHelpers
19 | include described_class
20 | end
21 | end
22 |
23 | def expect_respond_with(*args, &block)
24 | expect(controller).to receive(:respond_with).with(*args) do |&blk|
25 | expect(blk).to be block
26 | end
27 | end
28 |
29 | shared_examples 'with custom location' do
30 | context 'and custom location is given' do
31 | let(:options) { super().merge(location: :my_custom_location) }
32 | it 'doesnt override location' do
33 | expect_respond_with(resource, options, &block)
34 | subject.call
35 | end
36 | end
37 | end
38 |
39 | describe '#new' do
40 | it 'calls build_resource' do
41 | expect(controller).to receive(:build_resource)
42 | controller.new
43 | end
44 | end
45 |
46 | describe '#create' do
47 | subject { -> { controller.create options.dup, &block } }
48 | before { expect(controller).to receive_messages(create_resource: create_result) }
49 |
50 | context 'when create succeeded' do
51 | let(:create_result) { true }
52 | it 'sets location and calls respond_with' do
53 | expect_respond_with(resource, **options, location: :save_redirect_url, &block)
54 | subject.call
55 | end
56 |
57 | include_examples 'with custom location'
58 | end
59 |
60 | context 'when create failed' do
61 | let(:create_result) { false }
62 | it 'calls respond_with, but doesnt set location' do
63 | expect_respond_with(resource, options, &block)
64 | subject.call
65 | end
66 | end
67 | end
68 |
69 | describe '#update' do
70 | subject { -> { controller.update options.dup, &block } }
71 | before { expect(controller).to receive_messages(update_resource: update_result) }
72 |
73 | context 'when update succeeded' do
74 | let(:update_result) { true }
75 | it 'sets location and calls respond_with' do
76 | expect_respond_with(resource, **options, location: :save_redirect_url, &block)
77 | subject.call
78 | end
79 |
80 | include_examples 'with custom location'
81 | end
82 |
83 | context 'when update failed' do
84 | let(:update_result) { false }
85 | it 'calls respond_with, but doesnt set location' do
86 | expect_respond_with(resource, options, &block)
87 | subject.call
88 | end
89 | end
90 | end
91 |
92 | describe '#destroy' do
93 | subject { -> { controller.destroy options.dup, &block } }
94 | before do
95 | expect(resource).to receive(:destroy)
96 | expect(controller).to receive_messages(flash_errors!: true)
97 | end
98 |
99 | it 'calls rsource.destroy, flash_errors! and respond_with' do
100 | expect_respond_with(resource, option: :param, location: :destroy_redirect_url, &block)
101 | subject.call
102 | end
103 |
104 | include_examples 'with custom location'
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/spec/rails_stuff/resources_controller/basic_helpers_spec.rb:
--------------------------------------------------------------------------------
1 | require 'active_model'
2 |
3 | RSpec.describe RailsStuff::ResourcesController::BasicHelpers do
4 | let(:klass) { build_controller_class }
5 | let(:controller) { klass.new }
6 |
7 | def build_controller_class(class_name = nil)
8 | described_class = self.described_class
9 | build_named_class(class_name, ActionController::Base) do
10 | include described_class
11 | end
12 | end
13 |
14 | describe '.resource_class' do
15 | subject { ->(klass = self.klass) { klass.resource_class } }
16 |
17 | it 'detects class name from controller`s name' do
18 | stub_const('User', Class.new)
19 | stub_const('Product', Class.new)
20 | expect(subject.call build_controller_class 'UsersController').to be User
21 | expect(subject.call build_controller_class 'UserController').to be User
22 | expect(subject.call build_controller_class 'Admin::UserController').to be User
23 | expect(subject.call build_controller_class 'Admin::ProductsController').to be Product
24 | end
25 |
26 | context 'when class can not be automatically determined' do
27 | let(:klass) { build_controller_class :ProductsController }
28 | it { should raise_error(NameError, /uninitialized constant/) }
29 |
30 | context 'when class is set explicitly' do
31 | before { klass.resource_class = :custom_model }
32 | its(:call) { should eq :custom_model }
33 | end
34 | end
35 |
36 | context 'for anonymous class' do
37 | it { should raise_error(NameError, /wrong constant name/) }
38 | end
39 | end
40 |
41 | describe '.resource_param_name' do
42 | subject { ->(klass = self.klass) { klass.resource_param_name } }
43 |
44 | it 'takes value from class`es param_key' do
45 | stub_const('User', build_named_class(:Admin) { include ActiveModel::Model })
46 | expect(subject.call build_controller_class 'UsersController').to eq 'admin'
47 | end
48 |
49 | context 'when resource_class is set explicitly' do
50 | let(:model) { build_named_class(:Product) { include ActiveModel::Model } }
51 | let(:klass) { super().tap { |x| x.resource_class = model } }
52 | its(:call) { should eq 'product' }
53 | end
54 |
55 | context 'when value is set explicitly' do
56 | before { klass.resource_param_name = :custom_id }
57 | its(:call) { should eq :custom_id }
58 | end
59 | end
60 |
61 | describe '.permit_attrs' do
62 | let(:other_class) { build_controller_class }
63 | around { |ex| expect { ex.run }.to_not change(other_class, :permitted_attrs) }
64 |
65 | it 'adds args to .permitted_attrs' do
66 | expect { klass.permit_attrs :name, :lastname }.
67 | to change(klass, :permitted_attrs).from([]).to(%i[name lastname])
68 | expect { klass.permit_attrs project_attributes: %i[id _destroy] }.
69 | to change(klass, :permitted_attrs).by([project_attributes: %i[id _destroy]])
70 | end
71 | end
72 |
73 | describe '#permitted_attrs' do
74 | it 'returns class-level permitted_attrs by default' do
75 | expect { klass.permit_attrs :name }.
76 | to change(controller, :permitted_attrs).from([]).to([:name])
77 | end
78 | end
79 |
80 | describe '#resource_params' do
81 | subject { -> { klass.new.send(:resource_params) } }
82 | # In Rails4 #to_h returns ::Hash.
83 | let(:subject_to_h) { -> { subject.call.to_h.with_indifferent_access } }
84 | let(:params) do
85 | {
86 | user: {name: 'Name', lastname: 'Lastname', admin: true, id: 1},
87 | project: {title: 'Title', _destroy: true, id: 2},
88 | }
89 | end
90 | let(:params_class) { ActionController::Parameters }
91 |
92 | before do
93 | klass.resource_param_name = :user
94 | allow_any_instance_of(klass).to receive(:params) { params_class.new params.as_json }
95 | end
96 |
97 | it 'uses #permitted_attrs and .resource_param_name to filter params' do
98 | expect { klass.permit_attrs :name, :lastname, :address }.
99 | to change(&subject_to_h).from({}).to(name: 'Name', lastname: 'Lastname')
100 | expect(subject.call).to be_instance_of(params_class)
101 | expect { klass.resource_param_name = :project }.
102 | to change(&subject_to_h).to({})
103 | expect { klass.permit_attrs :title }.
104 | to change(&subject_to_h).to(title: 'Title')
105 | expect { klass.resource_param_name = :missing_key }.
106 | to change(&subject_to_h).to({})
107 | end
108 |
109 | context 'when resource key is not present in params' do
110 | before { klass.resource_param_name = :company }
111 | its(:call) { should be_instance_of(params_class) }
112 | its(:call) { should eq(params_class.new.permit!) }
113 | its('call.permitted?') { should eq true }
114 | end
115 | end
116 |
117 | describe '#after_save_url' do
118 | it 'passes .after_save_action to url_for, and uses resource as id (except :index)' do
119 | allow(controller).to receive(:resource) { :resource_obj }
120 | expect(controller).to receive(:url_for).
121 | with(action: :show, id: :resource_obj) { :url }
122 | expect(controller.send :after_save_url).to eq :url
123 |
124 | controller.class.after_save_action = :index
125 | expect(controller).to receive(:url_for).with(action: :index) { :url }
126 | expect(controller.send :after_save_url).to eq :url
127 |
128 | controller.class.after_save_action = :custom
129 | expect(controller).to receive(:url_for).
130 | with(action: :custom, id: :resource_obj) { :url }
131 | expect(controller.send :after_save_url).to eq :url
132 | end
133 | end
134 |
135 | describe '#after_destroy_url' do
136 | it 'returns url_for :index action' do
137 | expect(controller).to receive(:url_for).with(action: :index) { :url }
138 | expect(controller.send :after_destroy_url).to eq :url
139 | end
140 | end
141 | end
142 |
--------------------------------------------------------------------------------
/spec/rails_stuff/resources_controller/resource_helper_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe RailsStuff::ResourcesController::ResourceHelper do
2 | let(:klass) do
3 | described_class = self.described_class
4 | Class.new(ActionController::Base) { extend described_class }
5 | end
6 | let(:params) { {user_id: 1, secret_id: 2, admin_user_id: 3} }
7 | let(:controller) do
8 | params = ActionController::Parameters.new(self.params)
9 | klass.new.tap { |x| allow(x).to receive(:params) { params } }
10 | end
11 |
12 | describe '#resource_helper' do
13 | subject { -> { klass.resource_helper(method, options) } }
14 | let(:method) { :user }
15 | let(:options) { {} }
16 | let(:model_name) { :User }
17 | let!(:model) { stub_const(model_name.to_s, double(:model)) }
18 |
19 | shared_examples 'helper methods' do
20 | it 'generates helper methods' do
21 | expected_methods = [method, :"#{method}?"]
22 | expect(klass).to receive(:helper_method).with(*expected_methods)
23 | should change { klass.protected_instance_methods.sort }.by(expected_methods)
24 | end
25 | end
26 |
27 | shared_examples 'finder method' do |param|
28 | it 'generates finder method' do
29 | subject.call
30 | expect(model).to receive(:find).with(params[param]) { :found_user }
31 | expect do
32 | expect(controller.send(method)).to eq :found_user
33 | end.to change { controller.instance_variable_get("@#{method}") }.from(nil).to(:found_user)
34 | # assert cache
35 | expect(model).to_not receive(:find)
36 | expect(controller.send(method)).to eq :found_user
37 | end
38 | end
39 |
40 | shared_examples 'enquirer method' do |param|
41 | it 'generates enquirer method' do
42 | subject.call
43 | expect(controller.params).to receive(:key?).with(param) { :key_result }
44 | expect(controller.send("#{method}?")).to eq :key_result
45 | end
46 | end
47 |
48 | include_examples 'helper methods'
49 | include_examples 'finder method', :user_id
50 | include_examples 'enquirer method', :user_id
51 |
52 | context 'when :class is given' do
53 | let(:method) { :admin_user }
54 | let(:options) { {class: :Admin} }
55 | let(:model_name) { :Admin }
56 | around { |ex| RailsStuff.deprecation_07.silence { ex.run } }
57 | include_examples 'finder method', :admin_user_id
58 | end
59 |
60 | context 'when :source is given' do
61 | let(:method) { :admin_user }
62 | let(:options) { {source: :Admin} }
63 | let(:model_name) { :Admin }
64 | include_examples 'helper methods'
65 | include_examples 'finder method', :admin_user_id
66 | include_examples 'enquirer method', :admin_user_id
67 |
68 | context 'with class' do
69 | let(:options) { {source: -> { get_source }} }
70 | let(:options) { {source: model} }
71 | let(:model) { Class.new { def self.find(*); end } }
72 | include_examples 'finder method', :admin_user_id
73 | end
74 |
75 | context 'with block' do
76 | let(:options) { {source: -> { get_source }} }
77 | let(:model) { double(:model).tap { |x| klass.send(:define_method, :get_source) { x } } }
78 | include_examples 'finder method', :admin_user_id
79 | end
80 | end
81 |
82 | context 'when :param is given' do
83 | let(:options) { {param: :secret_id} }
84 | include_examples 'helper methods'
85 | include_examples 'finder method', :secret_id
86 | include_examples 'enquirer method', :secret_id
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/spec/rails_stuff/resources_controller/sti_helpers_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe RailsStuff::ResourcesController::StiHelpers do
2 | let(:klass) do
3 | build_controller_class.tap do |x|
4 | x.resource_class = resource_class
5 | x.resource_param_name = :user
6 | end
7 | end
8 | let(:controller) { klass.new }
9 | let(:resource_class) { Class.new }
10 |
11 | def build_controller_class(class_name = nil)
12 | described_class = self.described_class
13 | build_named_class(class_name, ActionController::Base) do
14 | include RailsStuff::ResourcesController::BasicHelpers
15 | include described_class
16 | end
17 | end
18 |
19 | describe '.resource_class_by_type' do
20 | subject { klass.resource_class_by_type }
21 | let(:child_1) { build_named_class :Child1, resource_class }
22 | let(:child_2) { build_named_class :Child2, resource_class }
23 | let(:grandchild) { build_named_class :Grandchild, child_1 }
24 |
25 | it 'is populated with resource_class descendants' do
26 | should eq 'Child1' => child_1, 'Child2' => child_2, 'Grandchild' => grandchild
27 | end
28 |
29 | context 'when resource_class responds to .types_list' do
30 | let(:resource_class) { super().tap { |x| x.extend RailsStuff::TypesTracker } }
31 | before { child_1.unregister_type }
32 |
33 | it 'is populated from types_list' do
34 | should eq 'Child2' => child_2, 'Grandchild' => grandchild
35 | end
36 | end
37 | end
38 |
39 | describe '#class_from_request' do
40 | subject { -> { controller.send :class_from_request } }
41 | let(:model) { Class.new }
42 | let(:params) { {} }
43 | before do
44 | allow_any_instance_of(klass).
45 | to receive(:params) { ActionController::Parameters.new(params) }
46 | klass.resource_class_by_type['User::Admin'] = model
47 | end
48 |
49 | context 'when valid type is requested' do
50 | let(:params) { {user: {type: 'User::Admin'}} }
51 | it 'returns mapped class for this type' do
52 | expect(subject.call).to eq model
53 | end
54 | end
55 |
56 | context 'when invalid type is requested' do
57 | let(:params) { {user: {type: 'Project::External'}} }
58 | it { should raise_error described_class::InvalidType }
59 |
60 | context 'when use_resource_class_for_invalid_type is true' do
61 | before { klass.use_resource_class_for_invalid_type = true }
62 | its(:call) { should eq klass.resource_class }
63 | end
64 | end
65 |
66 | context 'when params for resource is not given' do
67 | let(:params) { {project: {type: 'Project::External'}} }
68 | its(:call) { should eq klass.resource_class }
69 | end
70 |
71 | context 'when id is present' do
72 | let(:params) { {id: 1} }
73 |
74 | it 'takes returns resource`s class' do
75 | resource = Class.new.new
76 | expect(controller).to receive(:resource) { resource }
77 | expect(subject.call).to be resource.class
78 | end
79 | end
80 | end
81 |
82 | describe '.permit_attrs_for' do
83 | let(:other_class) { build_controller_class }
84 | around { |ex| expect { ex.run }.to_not change(other_class, :permitted_attrs_for) }
85 | around do |ex|
86 | expect { ex.run }.to_not change { klass.permitted_attrs_for[:other_type] }
87 | end
88 |
89 | it 'adds args to .permitted_attrs_for' do
90 | expect { klass.permit_attrs_for :user, :name, :lastname }.
91 | to change { klass.permitted_attrs_for[:user] }.
92 | from([]).to(%i[name lastname])
93 | expect { klass.permit_attrs_for :user, project_attributes: %i[id _destroy] }.
94 | to change { klass.permitted_attrs_for[:user] }.
95 | by([project_attributes: %i[id _destroy]])
96 | end
97 | end
98 |
99 | describe '#permitted_attrs' do
100 | it 'concats permitted_attrs with permitted_attrs_for specific klass' do
101 | model_1 = Class.new
102 | model_2 = Class.new
103 | klass.permit_attrs :name
104 | klass.permit_attrs_for model_1, :rule
105 | klass.permit_attrs_for model_2, :value
106 |
107 | expect(controller).to receive(:class_from_request) { model_1 }
108 | expect(controller.send :permitted_attrs).to contain_exactly :name, :rule
109 |
110 | expect(controller).to receive(:class_from_request) { model_2 }
111 | expect(controller.send :permitted_attrs).to contain_exactly :name, :value
112 | end
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/spec/rails_stuff/resources_controller_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe RailsStuff::ResourcesController do
2 | describe '#resources_controller' do
3 | subject { ->(options = self.options) { klass.resources_controller(**options) } }
4 | let(:options) { {} }
5 | let(:klass) do
6 | described_class = self.described_class
7 | Class.new(ActionController::Base) { extend described_class }
8 | end
9 | let(:instance) { klass.new }
10 | let(:basic_modules) do
11 | [
12 | described_class::Actions,
13 | described_class::BasicHelpers,
14 | ]
15 | end
16 |
17 | it { should change { klass.ancestors }.by basic_modules }
18 | it { should_not change(klass, :responder) }
19 |
20 | context 'when :source_relation is given' do
21 | it 'overrides method' do
22 | subject.call source_relation: -> { :custom_source }
23 | expect(instance.send :source_relation).to eq :custom_source
24 | end
25 | end
26 |
27 | context 'when :after_save_action is given' do
28 | it 'sets it' do
29 | subject.call after_save_action: :custom_action
30 | expect(klass.after_save_action).to eq :custom_action
31 | end
32 | end
33 |
34 | context 'when :sti is true' do
35 | let(:options) { {sti: true} }
36 | let(:expected_ancestors) { basic_modules.insert(1, described_class::StiHelpers) }
37 | it { should change { klass.ancestors }.by expected_ancestors }
38 | end
39 |
40 | context 'when :kaminari is true' do
41 | let(:options) { {kaminari: true} }
42 | let(:expected_ancestors) { basic_modules.insert(1, described_class::KaminariHelpers) }
43 | it { should change { klass.ancestors }.by expected_ancestors }
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/spec/rails_stuff/sort_scope_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 | require 'support/active_record'
3 |
4 | RSpec.describe RailsStuff::SortScope do
5 | describe '.filter_param' do
6 | def assert_filter(expected, val = nil, default = nil, allowed = [], params = {})
7 | result = described_class.filter_param(val, params, allowed, default)
8 | expect(result).to eq expected
9 | end
10 |
11 | def assert_default(val = nil, allowed = [])
12 | assert_filter(nil, val, nil, allowed)
13 | assert_filter({id: :desc}, val, {id: :desc}, allowed)
14 | assert_filter({id: :asc}, val, {id: :asc}, allowed, sort_desc: true)
15 | assert_filter({id: :asc}, val, :id, allowed)
16 | assert_filter({id: :desc}, val, :id, allowed, sort_desc: true)
17 | assert_filter({id: :desc}, val, :id, allowed, sort_desc: 'true')
18 | assert_filter({id: :desc}, val, :id, allowed, sort_desc: '1')
19 | assert_filter({id: :asc}, val, :id, allowed, sort_desc: 'false')
20 | assert_filter({id: :asc}, val, :id, allowed, sort_desc: '0')
21 | end
22 |
23 | let(:allowed) { %i[id name] }
24 |
25 | context 'when val is not set' do
26 | it 'returns default' do
27 | assert_default
28 | end
29 | end
30 |
31 | context 'when val is scalar' do
32 | context 'and is not allowed' do
33 | it 'returns default' do
34 | assert_default(:lastname, allowed)
35 | end
36 | end
37 |
38 | it 'returns hash based on it' do
39 | assert_filter({name: :asc}, :name, nil, allowed)
40 | assert_filter({name: :desc}, :name, nil, allowed, sort_desc: true)
41 | end
42 | end
43 |
44 | context 'when val is a hash' do
45 | it 'slices only allowed keys' do
46 | assert_filter({}, {lastname: 'desc', parent: 'asc'}, nil, allowed)
47 |
48 | assert_filter({name: :desc}, {name: 'desc', parent: 'asc'}, nil, allowed)
49 | assert_filter({name: :asc}, {name: 'asc', parent: 'asc'}, nil, allowed)
50 | assert_filter({name: :asc}, {name: {smt: :else}, parent: 'asc'}, nil, allowed)
51 |
52 | assert_filter(
53 | {name: :desc, id: :desc},
54 | {name: 'desc', id: 'desc', parent: 'asc'},
55 | nil,
56 | allowed
57 | )
58 | assert_filter(
59 | {name: :asc, id: :desc},
60 | {name: 'sc', id: 'desc', parent: 'asc'},
61 | nil,
62 | allowed
63 | )
64 | end
65 | end
66 | end
67 |
68 | describe '.has_sort_scope' do
69 | let(:controller_class) do
70 | described_class = self.described_class
71 | Class.new(ActionController::Base) do
72 | extend described_class
73 |
74 | def action_name
75 | :index
76 | end
77 | end
78 | end
79 | let(:controller) { controller_class.new }
80 | let(:model) { Class.new(ApplicationRecord) { self.table_name = :users } }
81 |
82 | def assert_sort_query(expected, **params)
83 | allow(controller).to receive(:params) do
84 | ActionController::Parameters.new(params.as_json)
85 | end
86 | sql = controller.send(:apply_scopes, model).all.order_values.map(&:to_sql).join("\n")
87 | expect(sql).to eq expected
88 | end
89 |
90 | def assert_current_scope(expected)
91 | expect(controller.send(:current_sort_scope)).to eq expected.stringify_keys
92 | end
93 |
94 | context 'when is not called' do
95 | it 'does not sort at all' do
96 | assert_sort_query '', sort: {package_name: :desc, id: :asc}
97 | end
98 | end
99 |
100 | context 'when sorting by multiple fields is allowed' do
101 | before { controller_class.has_sort_scope by: %i[id package_name] }
102 |
103 | it 'applies all scopes' do
104 | assert_current_scope({})
105 | assert_sort_query %("users"."package_name" ASC\n"users"."id" DESC),
106 | sort: {package_name: :asc, id: :desc, qqq: :desc}
107 | assert_current_scope package_name: :asc, id: :desc
108 | assert_sort_query '"users"."id" ASC' # default `default` is `:id`
109 | assert_current_scope id: :asc
110 | assert_sort_query '', sort: {qqq: :desc}
111 | assert_current_scope({})
112 | end
113 |
114 | context 'and action is not index' do
115 | let(:action) { :some_action }
116 | before { allow(controller).to receive(:action_name) { action } }
117 | it 'does not sort at all' do
118 | assert_sort_query '', sort: {package_name: :desc, id: :asc}
119 | end
120 |
121 | context 'but matches :only option' do
122 | before { controller_class.has_sort_scope by: [:id], only: action }
123 | it 'applies scopes' do
124 | assert_sort_query %("users"."id" ASC), sort: {package_name: :desc, id: :asc}
125 | end
126 | end
127 | end
128 | end
129 |
130 | context 'when called multiple times' do
131 | before { controller_class.has_sort_scope by: :id }
132 | before { controller_class.has_sort_scope by: :package_name }
133 |
134 | it 'applies all scopes' do
135 | assert_sort_query %("users"."id" ASC\n"users"."package_name" DESC),
136 | sort: {package_name: :desc, id: :asc, qqq: :desc}
137 | end
138 | end
139 |
140 | context 'accepts default value' do
141 | before { controller_class.has_sort_scope by: [:id], default: :package_name }
142 |
143 | it 'and uses it' do
144 | assert_sort_query '"users"."package_name" ASC'
145 | assert_current_scope package_name: :asc
146 | assert_sort_query '"users"."package_name" DESC', sort_desc: true
147 | assert_sort_query '"users"."package_name" ASC', sort: :qqq
148 | end
149 | end
150 |
151 | context 'accepts default hash value' do
152 | before { controller_class.has_sort_scope by: [:id], default: {package_name: :desc} }
153 |
154 | it 'and uses it' do
155 | assert_sort_query '"users"."package_name" DESC'
156 | assert_current_scope package_name: :desc
157 | end
158 | end
159 |
160 | context 'when :order_method arg is given' do
161 | before do
162 | controller_class.has_sort_scope default: :id, order_method: :sort_by
163 | model.define_singleton_method(:sort_by) { |*| order(my_id: :desc) }
164 | end
165 |
166 | it 'uses this method instead of sort' do
167 | expect(model).to receive(:sort_by).with('id' => :asc).and_call_original
168 | assert_sort_query '"users"."my_id" DESC'
169 | end
170 | end
171 | end
172 | end
173 |
--------------------------------------------------------------------------------
/spec/rails_stuff/statusable/builder_spec.rb:
--------------------------------------------------------------------------------
1 | require 'support/shared/statusable'
2 |
3 | RSpec.describe RailsStuff::Statusable, :db_cleaner do
4 | include_context 'statusable'
5 |
6 | let(:model) do
7 | described_class = self.described_class
8 | build_named_class :Customer, ActiveRecord::Base do
9 | extend described_class
10 | const_set(:STATUSES, %i[confirmed banned])
11 | has_status_field validate: false
12 | has_status_field :subscription_status, %i[pending expired],
13 | prefix: :subscription_
14 | end
15 | end
16 | before do
17 | add_translations(
18 | status: %w[confirmed banned],
19 | subscription_status: %w[pending expired],
20 | )
21 | end
22 |
23 | describe '##{field}=' do
24 | it 'accepts symbols' do
25 | expect { instance.status = :confirmed }.
26 | to change(instance, :status).from(nil).to('confirmed')
27 | end
28 |
29 | it 'accepts strings' do
30 | expect { instance.status = 'banned' }.
31 | to change(instance, :status).from(nil).to('banned')
32 | end
33 |
34 | context 'for custom field' do
35 | it 'accepts strings and symbols' do
36 | expect { instance.subscription_status = 'expired' }.
37 | to change(instance, :subscription_status).from(nil).to('expired')
38 | expect { instance.subscription_status = :pending }.
39 | to change(instance, :subscription_status).to('pending')
40 | end
41 | end
42 | end
43 |
44 | describe '##{field}_sym' do
45 | it 'returns status as symbol' do
46 | expect { instance.status = 'banned' }.
47 | to change(instance, :status_sym).from(nil).to(:banned)
48 | end
49 |
50 | context 'for custom field' do
51 | it 'returns status as symbol' do
52 | expect { instance.subscription_status = 'expired' }.
53 | to change(instance, :subscription_status_sym).from(nil).to(:expired)
54 | end
55 | end
56 | end
57 |
58 | describe '##{status}?' do
59 | it 'checks status' do
60 | expect { instance.status = :confirmed }.
61 | to change(instance, :confirmed?).from(false).to(true)
62 | end
63 |
64 | context 'for custom field' do
65 | it 'checks status' do
66 | expect { instance.subscription_status = :expired }.
67 | to change(instance, :subscription_expired?).from(false).to(true)
68 | end
69 | end
70 | end
71 |
72 | describe '##{status}!' do
73 | it 'updates field value' do
74 | expect(instance).to receive(:update!).with(status: 'confirmed')
75 | instance.confirmed!
76 | end
77 |
78 | context 'for custom field' do
79 | it 'updates field value' do
80 | expect(instance).to receive(:update!).
81 | with(subscription_status: 'pending')
82 | instance.subscription_pending!
83 | end
84 | end
85 | end
86 |
87 | describe '##{field}_name' do
88 | it 'returns translated status name' do
89 | expect { instance.status = :confirmed }.
90 | to change(instance, :status_name).from(nil).to('confirmed_en')
91 | end
92 |
93 | context 'for custom field' do
94 | it 'returns translated status name' do
95 | expect { instance.subscription_status = :expired }.
96 | to change(instance, :subscription_status_name).from(nil).to('expired_en')
97 | end
98 | end
99 | end
100 |
101 | describe 'validations' do
102 | context 'when validate: false' do
103 | it 'skips validations' do
104 | expect { instance.valid? }.to_not change { instance.errors[:status] }.from []
105 | instance.status = :invalid
106 | expect { instance.valid? }.to_not change { instance.errors[:status] }.from []
107 | end
108 | end
109 |
110 | context 'for custom field' do
111 | it 'checks valid value' do
112 | expect { instance.valid? }.
113 | to change { instance.errors[:subscription_status] }.from []
114 | instance.subscription_status = :invalid
115 | expect { instance.valid? }.
116 | to_not change { instance.errors[:subscription_status] }
117 | instance.subscription_status = :pending
118 | expect { instance.valid? }.
119 | to change { instance.errors[:subscription_status] }.to([])
120 | end
121 | end
122 | end
123 |
124 | describe '.with_#{field}' do
125 | it 'filters records by field values' do
126 | assert_filter(-> { where(status: :test) }) { with_status :test }
127 | assert_filter(-> { where(status: %i[test test2]) }) { with_status %i[test test2] }
128 | end
129 |
130 | context 'for custom field' do
131 | it 'filters records by field values' do
132 | assert_filter(-> { where(subscription_status: :test) }) { with_subscription_status :test }
133 | assert_filter(-> { where(subscription_status: %i[test test2]) }) do
134 | with_subscription_status %i[test test2]
135 | end
136 | end
137 | end
138 | end
139 |
140 | describe '.#{status}' do
141 | it 'filters records by field values' do
142 | assert_filter(-> { where(status: :confirmed) }) { confirmed }
143 | end
144 |
145 | context 'for status with prefix' do
146 | it 'filters records by field value' do
147 | assert_filter(-> { where(subscription_status: :pending) }) { subscription_pending }
148 | end
149 | end
150 | end
151 |
152 | describe '.not_#{status}' do
153 | it 'filters records by field values' do
154 | assert_filter(-> { where.not(status: :confirmed) }) { not_confirmed }
155 | end
156 |
157 | context 'for status with prefix' do
158 | it 'filters records by field value' do
159 | assert_filter(-> { where.not(subscription_status: :pending) }) { not_subscription_pending }
160 | end
161 | end
162 | end
163 | end
164 |
--------------------------------------------------------------------------------
/spec/rails_stuff/statusable/helper_spec.rb:
--------------------------------------------------------------------------------
1 | require 'support/shared/statusable'
2 |
3 | RSpec.describe RailsStuff::Statusable::Helper, :db_cleaner do
4 | include_context 'statusable'
5 |
6 | subject { instance }
7 | let(:instance) { model.statuses }
8 | let(:statuses) { %i[confirmed banned] }
9 | let(:model) do
10 | statuses = self.statuses
11 | build_named_class :Customer, ActiveRecord::Base do
12 | extend RailsStuff::Statusable
13 | has_status_field :status, statuses, validate: false
14 | end
15 | end
16 | before { add_translations(status: %w[confirmed banned]) }
17 |
18 | its(:list) { should eq statuses }
19 | its(:list) { should be_frozen }
20 |
21 | describe '#translate' do
22 | subject { ->(val) { instance.translate(val) } }
23 |
24 | it 'returns translated status name' do
25 | expect(subject[nil]).to eq(nil)
26 | expect(subject[:confirmed]).to eq('confirmed_en')
27 | expect(subject['banned']).to eq('banned_en')
28 | end
29 | end
30 |
31 | describe '#select_options' do
32 | subject { ->(*args) { instance.select_options(*args) } }
33 |
34 | it 'returns array for options_for_select' do
35 | expect(subject.call).to contain_exactly(
36 | ['confirmed_en', :confirmed],
37 | ['banned_en', :banned],
38 | )
39 | expect(subject[except: [:confirmed]]).to contain_exactly ['banned_en', :banned]
40 | expect(subject[only: [:confirmed]]).to contain_exactly ['confirmed_en', :confirmed]
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/spec/rails_stuff/statusable/mapped_builder_spec.rb:
--------------------------------------------------------------------------------
1 | require 'support/shared/statusable'
2 |
3 | RSpec.describe RailsStuff::Statusable, :db_cleaner do
4 | include_context 'statusable'
5 |
6 | let(:model) do
7 | described_class = self.described_class
8 | build_named_class :Order, ActiveRecord::Base do
9 | extend described_class
10 | const_set(:STATUSES_MAPPING, confirmed: 1, rejected: 3)
11 | has_status_field validate: false
12 | has_status_field :delivery_status,
13 | mapping: {sent: 1, complete: 4},
14 | prefix: :delivery_
15 | has_status_field :delivery_method, {pickup: 1, local: 2, international: 3},
16 | suffix: :_delivery
17 | end
18 | end
19 | before do
20 | add_translations(
21 | status: %w[confirmed rejected],
22 | delivery_status: %w[sent complete],
23 | delivery_method: %w[pickup local international],
24 | )
25 | end
26 |
27 | describe '##{field}' do
28 | shared_examples 'two readers' do |mapped, original|
29 | its(:call) { should eq mapped }
30 | context 'when arg is true' do
31 | let(:args) { [true] }
32 | its(:call) { should eq original }
33 | end
34 | end
35 |
36 | shared_examples 'field reader' do |field, value|
37 | subject { -> { instance.public_send(field, *args) } }
38 | let(:args) { [] }
39 | include_examples 'two readers', nil, nil
40 |
41 | context 'when value is set' do
42 | before { instance[field] = 1 }
43 | include_examples 'two readers', value, 1
44 |
45 | context 'when value is invalid' do
46 | before { instance[field] = -1 }
47 | include_examples 'two readers', -1, -1
48 | end
49 | end
50 | end
51 |
52 | include_examples 'field reader', :status, :confirmed
53 |
54 | context 'for custom field' do
55 | include_examples 'field reader', :delivery_status, :sent
56 | end
57 | end
58 |
59 | describe '##{field}=' do
60 | shared_examples 'field writer' do |field, value|
61 | subject { ->(val) { instance.public_send("#{field}=", val) } }
62 |
63 | it 'accepts symbols' do
64 | expect { subject[value] }.to change { instance[field] }.to(1)
65 | expect { subject[:invalid] }.to change { instance[field] }.to(0)
66 | end
67 |
68 | it 'accepts strings' do
69 | expect { subject[value] }.to change { instance[field] }.to(1)
70 | expect { subject['invalid'] }.to change { instance[field] }.to(0)
71 | end
72 |
73 | it 'accepts all types' do
74 | expect { subject[55] }.to change { instance[field] }.to(55)
75 | expect { subject[nil] }.to change { instance[field] }.to(nil)
76 | end
77 | end
78 |
79 | include_examples 'field writer', :status, :confirmed
80 |
81 | context 'for custom field' do
82 | include_examples 'field writer', :delivery_status, :sent
83 | end
84 | end
85 |
86 | describe '##{field}_sym' do
87 | it 'returns status as symbol' do
88 | expect { instance.status = 'rejected' }.
89 | to change(instance, :status_sym).from(nil).to(:rejected)
90 | end
91 |
92 | context 'for custom field' do
93 | it 'returns status as symbol' do
94 | expect { instance.delivery_status = 'complete' }.
95 | to change(instance, :delivery_status_sym).from(nil).to(:complete)
96 | end
97 | end
98 | end
99 |
100 | describe '##{field}_name' do
101 | it 'returns translated status name' do
102 | expect { instance.status = :confirmed }.
103 | to change { instance.status_name }.from(nil).to('confirmed_en')
104 | end
105 |
106 | context 'for custom field' do
107 | it 'returns translated status name' do
108 | expect { instance.delivery_status = :complete }.
109 | to change { instance.delivery_status_name }.from(nil).to('complete_en')
110 | end
111 | end
112 | end
113 |
114 | describe '##{status}?' do
115 | it 'checks status' do
116 | expect { instance.status = :confirmed }.
117 | to change { instance.confirmed? }.from(false).to(true)
118 | end
119 |
120 | context 'for custom field' do
121 | it 'checks status' do
122 | expect { instance.delivery_status = :complete }.
123 | to change { instance.delivery_complete? }.from(false).to(true)
124 | end
125 | end
126 |
127 | context 'with suffix' do
128 | it 'checks status' do
129 | expect { instance.delivery_method = :local }.
130 | to change { instance.local_delivery? }.from(false).to(true)
131 | end
132 | end
133 | end
134 |
135 | describe '##{status}!' do
136 | it 'updates field value' do
137 | expect(instance).to receive(:update!).with(status: 1)
138 | instance.confirmed!
139 | end
140 |
141 | context 'for custom field' do
142 | it 'updates field value' do
143 | expect(instance).to receive(:update!).with(delivery_status: 1)
144 | instance.delivery_sent!
145 | end
146 | end
147 |
148 | context 'with suffix' do
149 | it 'updates field value' do
150 | expect(instance).to receive(:update!).with(delivery_method: 2)
151 | instance.local_delivery!
152 | end
153 | end
154 | end
155 |
156 | describe 'validations' do
157 | context 'when validate: false' do
158 | it 'skips validations' do
159 | expect { instance.valid? }.to_not change { instance.errors[:status] }.from []
160 | instance.status = :invalid
161 | expect { instance.valid? }.to_not change { instance.errors[:status] }.from []
162 | end
163 | end
164 |
165 | context 'for custom field' do
166 | it 'checks valid value' do
167 | expect { instance.valid? }.
168 | to change { instance.errors[:delivery_status] }.from []
169 | instance.delivery_status = :invalid
170 | expect { instance.valid? }.
171 | to_not change { instance.errors[:delivery_status] }
172 | instance.delivery_status = :sent
173 | expect { instance.valid? }.
174 | to change { instance.errors[:delivery_status] }.to([])
175 | end
176 | end
177 | end
178 |
179 | describe '.with_#{field}' do
180 | it 'filters records by field values' do
181 | assert_filter(-> { where(status: 1) }) { with_status :confirmed }
182 | assert_filter(-> { where(status: 3) }) { with_status 'rejected' }
183 | assert_filter(-> { where(status: 5) }) { with_status 5 }
184 | assert_filter(-> { where(status: [1, 5]) }) { with_status [1, 5] }
185 | end
186 |
187 | context 'for custom field' do
188 | it 'filters records by field values' do
189 | assert_filter(-> { where(delivery_status: 1) }) { with_delivery_status :sent }
190 | assert_filter(-> { where(delivery_status: 4) }) { with_delivery_status 'complete' }
191 | assert_filter(-> { where(delivery_status: 5) }) { with_delivery_status 5 }
192 | assert_filter(-> { where(delivery_status: [1, 5]) }) { with_delivery_status [1, 5] }
193 | end
194 | end
195 | end
196 |
197 | describe '.#{status}' do
198 | it 'filters records by field values' do
199 | assert_filter(-> { where(status: 1) }) { confirmed }
200 | assert_filter(-> { where(status: 3) }) { rejected }
201 | end
202 |
203 | context 'for status with prefix' do
204 | it 'filters records by field value' do
205 | assert_filter(-> { where(delivery_status: 1) }) { delivery_sent }
206 | assert_filter(-> { where(delivery_status: 4) }) { delivery_complete }
207 | end
208 | end
209 |
210 | context 'for status with suffix' do
211 | it 'filters records by field value' do
212 | assert_filter(-> { where(delivery_method: 1) }) { pickup_delivery }
213 | assert_filter(-> { where(delivery_method: 3) }) { international_delivery }
214 | end
215 | end
216 | end
217 |
218 | describe '.not_#{status}' do
219 | it 'filters records by field values' do
220 | assert_filter(-> { where.not(status: 1) }) { not_confirmed }
221 | end
222 |
223 | context 'for status with prefix' do
224 | it 'filters records by field value' do
225 | assert_filter(-> { where.not(delivery_status: 1) }) { not_delivery_sent }
226 | end
227 | end
228 |
229 | context 'for status with suffix' do
230 | it 'filters records by field value' do
231 | assert_filter(-> { where.not(delivery_method: 1) }) { not_pickup_delivery }
232 | end
233 | end
234 | end
235 | end
236 |
--------------------------------------------------------------------------------
/spec/rails_stuff/statusable/mapped_helper_spec.rb:
--------------------------------------------------------------------------------
1 | require 'support/shared/statusable'
2 |
3 | RSpec.describe RailsStuff::Statusable::MappedHelper, :db_cleaner do
4 | include_context 'statusable'
5 |
6 | subject { instance }
7 | let(:instance) { model.statuses }
8 | let(:statuses) { {confirmed: 1, rejected: 3} }
9 | let(:model) do
10 | statuses = self.statuses
11 | build_named_class :Customer, ActiveRecord::Base do
12 | extend RailsStuff::Statusable
13 | has_status_field :status, statuses, validate: false
14 | end
15 | end
16 | before { add_translations(status: %w[confirmed rejected]) }
17 |
18 | its(:list) { should eq statuses.keys }
19 | its(:list) { should be_frozen }
20 |
21 | its(:mapping) { should eq statuses }
22 | its(:mapping) { should be_frozen }
23 |
24 | its(:inverse_mapping) { should eq statuses.invert }
25 | its(:inverse_mapping) { should be_frozen }
26 |
27 | describe '#select_options' do
28 | subject { ->(*args) { instance.select_options(*args) } }
29 |
30 | it 'returns array for options_for_select' do
31 | expect(subject.call).to contain_exactly(
32 | ['confirmed_en', :confirmed],
33 | ['rejected_en', :rejected],
34 | )
35 | expect(subject[except: [:confirmed]]).to contain_exactly ['rejected_en', :rejected]
36 | expect(subject[only: [:confirmed]]).to contain_exactly ['confirmed_en', :confirmed]
37 | end
38 |
39 | context 'when :original is true' do
40 | subject { ->(**options) { instance.select_options(**options, original: true) } }
41 | it 'uses db values instead of mapped' do
42 | expect(subject.call).to contain_exactly(
43 | ['confirmed_en', 1],
44 | ['rejected_en', 3],
45 | )
46 | expect(subject[except: [1]]).to contain_exactly ['rejected_en', 3]
47 | expect(subject[only: [1]]).to contain_exactly ['confirmed_en', 1]
48 | end
49 | end
50 | end
51 |
52 | describe '#map' do
53 | subject { ->(val) { instance.map(val) } }
54 |
55 | it 'maps single value' do
56 | expect(subject[:confirmed]).to eq 1
57 | expect(subject[:rejected]).to eq 3
58 | expect(subject['rejected']).to eq 3
59 | expect(subject[:missing]).to eq :missing
60 | expect(subject[nil]).to eq nil
61 | end
62 |
63 | it 'maps array' do
64 | expect(subject[[:confirmed, :rejected, 'rejected', :missing, nil]]).
65 | to eq [1, 3, 3, :missing, nil]
66 | end
67 | end
68 |
69 | describe '#unmap' do
70 | subject { ->(val) { instance.unmap(val) } }
71 |
72 | it 'maps single value' do
73 | expect(subject[1]).to eq :confirmed
74 | expect(subject[3]).to eq :rejected
75 | expect(subject[:missing]).to eq :missing
76 | expect(subject[nil]).to eq nil
77 | end
78 |
79 | it 'maps array' do
80 | expect(subject[[1, 3, :missing, nil]]).to eq [:confirmed, :rejected, :missing, nil]
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/spec/rails_stuff/statusable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'support/shared/statusable'
2 |
3 | RSpec.describe RailsStuff::Statusable, :db_cleaner do
4 | include_context 'statusable'
5 |
6 | let(:model) do
7 | build_named_class :Customer, ActiveRecord::Base do
8 | extend RailsStuff::Statusable
9 | end
10 | end
11 |
12 | describe '.has_status_field' do
13 | subject { -> { model.has_status_field :field, %i[a b], options, &block } }
14 | let(:options) { {} }
15 | let(:block) {}
16 | it { should change { model.ancestors.size }.by(1) }
17 | it { should change(model, :instance_methods) }
18 | it { should change(model, :public_methods) }
19 |
20 | context 'for second field' do
21 | before { model.has_status_field :field_2, %i[c d] }
22 | it { should_not change { model.ancestors.size } }
23 | end
24 |
25 | context 'when block is given' do
26 | let(:block) { ->(x) { x.field_scope } }
27 | it { should_not change(model, :instance_methods) }
28 | it { should change { model.respond_to?(:with_field) }.from(false).to(true) }
29 | end
30 |
31 | context 'when builder is false' do
32 | let(:options) { {builder: false} }
33 | it { should_not change(model, :instance_methods) }
34 | it { should change(model, :public_methods).by([:fields]) }
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/rails_stuff/strong_parameters_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 | require 'rails_stuff/strong_parameters'
3 |
4 | RSpec.describe ActionController::Parameters do
5 | describe 'require_permitted' do
6 | let(:input) do
7 | {
8 | a: 1,
9 | b: 'str',
10 | c: [1, 2],
11 | d: '',
12 | e: [],
13 | f: {},
14 | g: {x: 1},
15 | h: nil,
16 | }.stringify_keys
17 | end
18 | let(:instance) { described_class.new(input) }
19 |
20 | it 'permits params and checks they are present' do
21 | expect(instance.require_permitted('a').to_h).to eq input.slice('a')
22 | expect(instance.require_permitted('a', 'b').to_h).to eq input.slice('a', 'b')
23 |
24 | ('c'..input.keys.max).each do |field|
25 | expect { instance.require_permitted('a', field, 'b') }.
26 | to raise_error(ActionController::ParameterMissing), "failed for #{input[field]}"
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/rails_stuff/test_helpers/concurrency_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_stuff/test_helpers/concurrency'
2 |
3 | RSpec.describe RailsStuff::TestHelpers::Concurrency do
4 | describe '#concurrently' do
5 | subject { ->(*args, &block) { instance.concurrently(*args, &block) } }
6 | let(:instance) { Object.new.tap { |x| x.extend described_class } }
7 | let(:acc) { ThreadSafe::Array.new }
8 |
9 | context 'when no args given' do
10 | it 'runs block .threads_count times' do
11 | expect { subject.call { |i| acc << i } }.
12 | to change(acc, :size).from(0).to(described_class.threads_count)
13 | expect(acc).to eq Array.new(described_class.threads_count)
14 | end
15 | end
16 |
17 | context 'when arg is Integer' do
18 | let(:count) { 5 }
19 | it 'runs block arg times' do
20 | acc = ThreadSafe::Array.new
21 | expect { subject.call(count) { |i| acc << i } }.
22 | to change(acc, :size).from(0).to(count)
23 | expect(acc).to eq Array.new(count)
24 | end
25 | end
26 |
27 | context 'when arg is array' do
28 | it 'runs block for each arg' do
29 | input = [
30 | 1,
31 | 2,
32 | {opt: true},
33 | {opt: false},
34 | [1, opt: false],
35 | ]
36 | expect { subject.call(input) { |arg = nil, **options| acc << [arg, options] } }.
37 | to change(acc, :size).to(input.size)
38 | expect(acc).to contain_exactly [1, {}],
39 | [2, {}],
40 | [nil, opt: true],
41 | [nil, opt: false],
42 | [1, opt: false]
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/spec/rails_stuff/transform_attrs_spec.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/object/blank'
2 |
3 | RSpec.describe RailsStuff::TransformAttrs do
4 | let(:klass) do
5 | described_class = self.described_class
6 | Class.new do
7 | def self.extra_ancestors
8 | ancestors.take_while { |x| x != Object }.
9 | # rails 4 adds ActiveSupport::ToJsonWithActiveSupportEncoder.
10 | # We ignore it:
11 | reject(&:name)
12 | end
13 |
14 | extend described_class
15 | attr_accessor :own
16 | end
17 | end
18 |
19 | describe 'writers' do
20 | let(:instance) { klass.new }
21 | before do
22 | klass.send :include, Module.new { attr_accessor :nested }
23 | klass.transform_attrs :own, with: :strip
24 | klass.transform_attrs :nested, with: :nullify
25 | end
26 |
27 | it 'works for own attrs' do
28 | expect { instance.own = ' test' }.to change(instance, :own).to('test')
29 | expect { instance.own = ' ' }.to change(instance, :own).to('')
30 | expect { instance.own = [1] }.to change(instance, :own).to('[1]')
31 | expect { instance.own = [] }.to change(instance, :own).to('[]')
32 | end
33 |
34 | it 'works for nested attrs' do
35 | expect { instance.nested = ' test' }.to change(instance, :nested).to(' test')
36 | expect { instance.nested = ' ' }.to change(instance, :nested).to(nil)
37 | expect { instance.nested = [1] }.to change(instance, :nested).to([1])
38 | expect { instance.nested = [] }.to change(instance, :nested).to(nil)
39 | end
40 |
41 | context 'for chained translations' do
42 | before do
43 | klass.transform_attrs :own, with: %i[strip nullify]
44 | klass.transform_attrs :nested, with: %i[nullify strip]
45 | end
46 |
47 | it 'applies them all' do
48 | expect { instance.own = ' test' }.to change(instance, :own).to('test')
49 | expect { instance.own = ' ' }.to change(instance, :own).to(nil)
50 | expect { instance.own = [] }.to change(instance, :own).to('[]')
51 |
52 | expect { instance.nested = ' test' }.to change(instance, :nested).to('test')
53 | expect { instance.nested = ' ' }.to change(instance, :nested).to(nil)
54 | expect { instance.nested = [] }.to_not change(instance, :nested).from(nil)
55 | end
56 | end
57 | end
58 |
59 | describe '.transform_attrs' do
60 | subject { ->(*args, &block) { klass.transform_attrs(*args, &block) } }
61 |
62 | it 'adds only single module by default' do
63 | expect do
64 | subject.call :one, &:presence
65 | subject.call :two, &:presence
66 | end.to change(klass, :extra_ancestors).from([klass]).to([instance_of(Module), klass])
67 | end
68 |
69 | it 'includes new modules with `new_module:` option' do
70 | expect { subject.call :two, new_module: :include, &:presence }.
71 | to change(klass, :extra_ancestors).from([klass]).
72 | to([klass, instance_of(Module)])
73 | expect { subject.call :one, &:presence }.
74 | to change(klass, :extra_ancestors).
75 | to([instance_of(Module), klass, instance_of(Module)])
76 | expect { subject.call :two, new_module: :prepend, &:presence }.
77 | to change(klass, :extra_ancestors).
78 | to([instance_of(Module), instance_of(Module), klass, instance_of(Module)])
79 | end
80 |
81 | it 'raises error when invalid translation requested' do
82 | expect { subject.call :one, with: :missing }.to raise_error(KeyError)
83 | expect { subject.call :one, with: %i[nullify missing] }.to raise_error(KeyError)
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/spec/rails_stuff/types_tracker_spec.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 |
3 | RSpec.describe RailsStuff::TypesTracker do
4 | let(:base) do
5 | described_class = self.described_class
6 | Class.new { extend described_class }
7 | end
8 |
9 | let(:child) { Class.new(base) }
10 | let(:grand_child) { Class.new(child) }
11 | let(:child_2) { Class.new(base) }
12 |
13 | def with_list_class(klass)
14 | old_klass = described_class.types_list_class
15 | described_class.types_list_class = klass
16 | yield
17 | ensure
18 | described_class.types_list_class = old_klass
19 | end
20 |
21 | # Can't do this with stubbing, 'cause class name is used before it's stubbed.
22 | module TypesTrackerTest
23 | class Project < ApplicationRecord
24 | extend RailsStuff::TypesTracker
25 |
26 | class Internal < self
27 | class Smth < self; end
28 | end
29 |
30 | class ModelName < self; end
31 | end
32 | end
33 |
34 | describe '#inherited' do
35 | it 'tracks ancestors' do
36 | expect(base.types_list).to contain_exactly child, grand_child, child_2
37 | end
38 |
39 | context 'when list_class is overwritten' do
40 | around { |ex| with_list_class(new_list_class) { ex.run } }
41 | let(:new_list_class) { Class.new(Array) }
42 |
43 | it 'tracks ancestors' do
44 | expect(base.types_list).to contain_exactly child, grand_child, child_2
45 | expect(base.types_list.class).to eq new_list_class
46 | end
47 | end
48 | end
49 |
50 | describe '#register_type' do
51 | it 'adds class to types_list' do
52 | child.unregister_type
53 | expect { child.register_type }.
54 | to change { base.types_list }.from([]).to([child])
55 | end
56 |
57 | context 'for activerecord model' do
58 | it 'adds scope' do
59 | expect(TypesTrackerTest::Project).to respond_to :internal
60 | expect(TypesTrackerTest::Project).to respond_to :smth
61 | end
62 |
63 | it 'doesnt override methods with scope' do
64 | TypesTrackerTest::Project::ModelName.register_type
65 | expect(TypesTrackerTest::Project.model_name).to be_instance_of ActiveModel::Name
66 | end
67 | end
68 |
69 | context 'when types_list has #add method' do
70 | around { |ex| with_list_class(new_list_class) { ex.run } }
71 | let(:new_list_class) do
72 | Class.new(Hash) do
73 | def add(klass, value = :default)
74 | self[klass] = value
75 | end
76 | end
77 | end
78 |
79 | it 'uses #add method' do
80 | expect { child.register_type :best }.
81 | to change { base.types_list }.
82 | from(child => :default).to(child => :best)
83 | end
84 | end
85 | end
86 |
87 | describe '#unregister_type' do
88 | it 'removes class from types_list' do
89 | expect { child.unregister_type }.
90 | to change { base.types_list }.
91 | from([child, grand_child]).to([grand_child])
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/spec/rails_stuff/url_for_keeping_params_spec.rb:
--------------------------------------------------------------------------------
1 | require 'integration_helper'
2 | require 'rails_stuff/url_for_keeping_params'
3 |
4 | RSpec.describe ActionDispatch::Routing::UrlFor, type: :controller do
5 | controller Site::UsersController do
6 | def index
7 | head :ok
8 | end
9 | end
10 |
11 | describe '#url_for_keeping_params' do
12 | it 'keeps query params of url' do
13 | get :index, params: {sort: :date, page: 1}
14 | expect(controller.url_for_keeping_params page: 2, limit: 5).
15 | to eq controller.url_for(sort: :date, page: 2, limit: 5)
16 | end
17 |
18 | it 'removes param when overwritten wit nil' do
19 | get :index, params: {sort: :date, page: 1}
20 | expect(controller.url_for_keeping_params page: nil, limit: 5).
21 | to eq controller.url_for(sort: :date, limit: 5)
22 | end
23 |
24 | context 'when used in a view' do
25 | controller Site::UsersController do
26 | def index
27 | render inline: '<%= raw url_for_keeping_params page: 2, limit: 5 %>'
28 | end
29 | end
30 |
31 | it 'works the same way' do
32 | get :index, params: {sort: :date, page: 1}
33 | expect(response.body).
34 | to eq controller.url_for(sort: :date, page: 2, limit: 5, only_path: true)
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/rails_stuff_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe RailsStuff do
2 | it 'has a version number' do
3 | expect(RailsStuff::VERSION).not_to be nil
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'pathname'
2 | require 'pry'
3 | require 'rspec/its'
4 |
5 | if ENV['CI']
6 | require 'coveralls'
7 | Coveralls.wear!
8 | elsif ENV.key?('COV')
9 | require 'simplecov'
10 | SimpleCov.start
11 | end
12 |
13 | GEM_ROOT = Pathname.new File.expand_path('../..', __FILE__)
14 |
15 | $LOAD_PATH.unshift GEM_ROOT.join('lib')
16 | require 'rails_stuff'
17 | RailsStuff::TestHelpers.big_decimal
18 |
19 | RSpec.configure do |config|
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 | config.mock_with :rspec do |mocks|
32 | # Prevents you from mocking or stubbing a method that does not exist on
33 | # a real object. This is generally recommended, and will default to
34 | # `true` in RSpec 4.
35 | mocks.verify_partial_doubles = true
36 | end
37 |
38 | # These two settings work together to allow you to limit a spec run
39 | # to individual examples or groups you care about by tagging them with
40 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples
41 | # get run.
42 | #
43 | # Use `FULL=true bin/rspec` to disable filters.
44 | config.filter_run :focus
45 | config.run_all_when_everything_filtered = true
46 |
47 | # Limits the available syntax to the non-monkey patched syntax that is recommended.
48 | # For more details, see:
49 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
50 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
51 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
52 | config.disable_monkey_patching!
53 |
54 | # Many RSpec users commonly either run the entire suite or an individual
55 | # file, and it's useful to allow more verbose output when running an
56 | # individual spec file.
57 | config.default_formatter = 'doc' if config.files_to_run.one?
58 |
59 | # Print the 10 slowest examples and example groups at the
60 | # end of the spec run, to help surface which specs are running
61 | # particularly slow.
62 | # config.profile_examples = 3
63 |
64 | # Run specs in random order to surface order dependencies. If you find an
65 | # order dependency and want to debug it, you can fix the order by providing
66 | # the seed, which is printed after each run.
67 | # --seed 1234
68 | config.order = :random
69 |
70 | # Seed global randomization in this process using the `--seed` CLI option.
71 | # Setting this allows you to use `--seed` to deterministically reproduce
72 | # test failures related to randomization by passing the same `--seed` value
73 | # as the one that triggered the failure.
74 | Kernel.srand config.seed
75 | end
76 |
77 | require 'active_support/core_ext/object/try'
78 | # Helper which builds class and defines name for it.
79 | def build_named_class(class_name, superclass = nil, &block)
80 | Class.new(superclass || Object) do
81 | define_singleton_method(:name) { class_name.try!(:to_s) }
82 | class_eval(&block) if block
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/support/active_record.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 | ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
3 | require 'support/schema'
4 |
5 | class ApplicationRecord < ActiveRecord::Base
6 | self.abstract_class = true
7 | end
8 |
9 | require 'database_cleaner'
10 | RSpec.configure do |config|
11 | config.around db_cleaner: true do |ex|
12 | DatabaseCleaner.cleaning { ex.run }
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/support/app/views/site/forms/index.html.erb:
--------------------------------------------------------------------------------
1 | index.html.erb
2 | <%= form_for Order.new(status: :pending), url: '/' do |f| %>
3 | <%= f.select :status, Order.statuses.select_options(only: [:pending, :accepted]) %>
4 | <%- end %>
5 |
6 | <%= form_for Order.new(status: :delivered), url: '/', html: {id: 'order_2'} do |f| %>
7 | <%= f.select :status, Order.statuses.select_options(original: true, except: [1]),
8 | selected: f.object.status(true) %>
9 | <%- end %>
10 |
11 | <%= form_for Customer.new(status: :banned), url: '/' do |f| %>
12 | <%= f.select :status, Customer.statuses.select_options(except: [:premium]) %>
13 | <%- end %>
14 |
--------------------------------------------------------------------------------
/spec/support/app/views/site/projects/edit.html.erb:
--------------------------------------------------------------------------------
1 | edit.html.erb
2 |
--------------------------------------------------------------------------------
/spec/support/app/views/site/projects/index.html.erb:
--------------------------------------------------------------------------------
1 | index.html.erb
2 | <% collection -%>
3 |
--------------------------------------------------------------------------------
/spec/support/app/views/site/users/edit.html.erb:
--------------------------------------------------------------------------------
1 | edit.html.erb
2 | <% resource -%>
3 |
--------------------------------------------------------------------------------
/spec/support/app/views/site/users/index.html.erb:
--------------------------------------------------------------------------------
1 | index.html.erb
2 |
--------------------------------------------------------------------------------
/spec/support/app/views/site/users/new.html.erb:
--------------------------------------------------------------------------------
1 | new.html.erb
2 | <% resource -%>
3 |
--------------------------------------------------------------------------------
/spec/support/app/views/site/users/show.html.erb:
--------------------------------------------------------------------------------
1 | show.html.erb
2 | <% resource -%>
3 |
--------------------------------------------------------------------------------
/spec/support/controller.rb:
--------------------------------------------------------------------------------
1 | module Support
2 | # Override process methods, so we can use rails5 specs within rails4.
3 | module ControllerBackport
4 | %w[get post put patch delete].each do |method|
5 | define_method(method) do |path, **options|
6 | params = options[:params] || {}
7 | super(path, params)
8 | end
9 | end
10 | end
11 | end
12 |
13 | RSpec.configure do |config|
14 | if RailsStuff.rails_version::MAJOR >= 5
15 | require 'rails-controller-testing'
16 | %i[controller view request].each do |type|
17 | config.include ::Rails::Controller::Testing::TestProcess, type: type
18 | config.include ::Rails::Controller::Testing::TemplateAssertions, type: type
19 | config.include ::Rails::Controller::Testing::Integration, type: type
20 | end
21 | else
22 | %i[controller request].each do |type|
23 | config.include Support::ControllerBackport, type: type
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/support/redis.rb:
--------------------------------------------------------------------------------
1 | require 'rails/railtie'
2 | require 'pooled_redis'
3 | require 'redis'
4 | Rails.instance_eval { @redis_config = {} }
5 |
--------------------------------------------------------------------------------
/spec/support/schema.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define do
2 | self.verbose = false
3 |
4 | create_table :users, force: true do |t|
5 | t.string :name, null: false
6 | t.string :email, null: false
7 | t.boolean :admin, null: false, default: false
8 |
9 | t.timestamps null: false
10 | end
11 |
12 | create_table :projects, force: true do |t|
13 | t.string :name, null: false
14 | # it's nullable intentionally for tests:
15 | t.belongs_to :user, null: true, foreign_key: true, index: true
16 | t.string :type, null: false, index: true
17 |
18 | t.string :department
19 | t.string :company
20 |
21 | t.timestamps null: false
22 | end
23 |
24 | create_table :customers, force: true do |t|
25 | t.string :status, null: false
26 | t.string :subscription_status, null: false
27 |
28 | t.timestamps null: false
29 | end
30 |
31 | create_table :orders, force: true do |t|
32 | t.integer :status, null: false
33 | t.integer :delivery_status, null: false
34 | t.integer :delivery_method, null: false
35 |
36 | t.timestamps null: false
37 | end
38 |
39 | create_table :tokens, force: true do |t|
40 | t.string :code
41 | t.index :code, unique: true
42 |
43 | t.timestamps null: false
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/spec/support/shared/statusable.rb:
--------------------------------------------------------------------------------
1 | require 'activemodel_translation/helper'
2 | require 'support/active_record'
3 |
4 | RSpec.shared_context 'statusable' do
5 | let(:instance) { model.new }
6 |
7 | def assert_filter(expected, &block)
8 | expected = relation_values(model.instance_exec(&expected))
9 | expect(relation_values(model.instance_exec(&block))).to eq expected
10 | end
11 |
12 | if RailsStuff.rails4?
13 | def relation_values(relation)
14 | where_sql = relation.where_values.map(&:to_sql)
15 | values_sql = relation.bind_values.to_h.transform_values(&:to_s)
16 | [where_sql, values_sql]
17 | end
18 | else
19 | def relation_values(relation)
20 | where_sql = relation.where_clause.ast.to_sql
21 | values_sql = relation.where_clause.to_h.transform_values(&:to_s)
22 | [where_sql, values_sql]
23 | end
24 | end
25 |
26 | def add_translations(data)
27 | I18n.backend = I18n::Backend::Simple.new
28 | data.each do |field, values|
29 | I18n.backend.store_translations 'en', strings: {
30 | "#{field}_name" => values.map { |x| [x, "#{x}_en"] }.to_h,
31 | }
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------