cancancan 3.0.0.rc1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d745990a81dbfa8dfaf8802b76bbe6a09d11291562383b429320c1db96355e20
4
- data.tar.gz: 579e1848a8c8a5c096a747306c7aa08b2a156bcd404f2c6b6d5b418c02026324
3
+ metadata.gz: f912e7e8ba7143a52b467949b374103edb0aae0b8b3dcbea4157400b6a70e7d6
4
+ data.tar.gz: 604a95ca8d9a794386810ad68a98eec86d1b6a8b73c07d64c0b8706ec5c52fa6
5
5
  SHA512:
6
- metadata.gz: 91d38cda2159ce1c061101d599aae4abf8bb1358bfd1fe43b4432e7eba0ce9e4002b52d055a1e693138a05e185b70fbc504b6f696e2d02d6e8086b8a6f9976ac
7
- data.tar.gz: eb23bb3c5b8a3feaa55d27226443fa8e3fd15175a23d7fa6810c4253ca8646fdbd88150138e3bc81e473d7a27302584ec5ebdb5e31e0fd1dfdf54458b52b3fea
6
+ metadata.gz: 5988dd5e020a13b020769f632a4c780f2b3a401c6064f9d4b1da7f4d3570cf18d52be603ed2fd55059b88f5b06e4dd6fb6708e893ff899e2ffbd793eddfec07a
7
+ data.tar.gz: 5c2338a84835d7d828739984925bd2906f044d8b0c0355a0a62a8fe763e2f203e579e4909ba37fdbf8f30bce26e36790a47793ab056fb39e7a6c1e683ce9e486
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cancan/version'
4
+ require 'cancan/config'
4
5
  require 'cancan/parameter_validators'
5
6
  require 'cancan/ability'
6
7
  require 'cancan/rule'
@@ -16,6 +17,7 @@ require 'cancan/rules_compressor'
16
17
  if defined? ActiveRecord
17
18
  require 'cancan/model_adapters/conditions_extractor'
18
19
  require 'cancan/model_adapters/conditions_normalizer'
20
+ require 'cancan/model_adapters/sti_normalizer'
19
21
  require 'cancan/model_adapters/active_record_adapter'
20
22
  require 'cancan/model_adapters/active_record_4_adapter'
21
23
  require 'cancan/model_adapters/active_record_5_adapter'
@@ -302,7 +302,11 @@ module CanCan
302
302
 
303
303
  def alternative_subjects(subject)
304
304
  subject = subject.class unless subject.is_a?(Module)
305
- [:all, *subject.ancestors, subject.class.to_s]
305
+ if subject.respond_to?(:subclasses) && subject < ActiveRecord::Base
306
+ [:all, *(subject.ancestors + subject.subclasses), subject.class.to_s]
307
+ else
308
+ [:all, *subject.ancestors, subject.class.to_s]
309
+ end
306
310
  end
307
311
  end
308
312
  end
@@ -0,0 +1,26 @@
1
+ # This class is responsible for matching classes and their subclasses as well as
2
+ # upmatching classes to their ancestors.
3
+ # This is used to generate sti connections
4
+ class SubjectClassMatcher
5
+ def self.matches_subject_class?(subjects, subject)
6
+ subjects.any? do |sub|
7
+ has_subclasses = subject.respond_to?(:subclasses)
8
+ matching_class_check(subject, sub, has_subclasses)
9
+ end
10
+ end
11
+
12
+ def self.matching_class_check(subject, sub, has_subclasses)
13
+ matches = matches_class_or_is_related(subject, sub)
14
+ if has_subclasses
15
+ matches || subject.subclasses.include?(sub)
16
+ else
17
+ matches
18
+ end
19
+ end
20
+
21
+ def self.matches_class_or_is_related(subject, sub)
22
+ sub.is_a?(Module) && (subject.is_a?(sub) ||
23
+ subject.class.to_s == sub.to_s ||
24
+ (subject.is_a?(Module) && subject.ancestors.include?(sub)))
25
+ end
26
+ end
@@ -21,7 +21,7 @@ module CanCan
21
21
  def matches_block_conditions(subject, *extra_args)
22
22
  return @base_behavior if subject_class?(subject)
23
23
 
24
- @block.call(subject, *extra_args)
24
+ @block.call(subject, *extra_args.compact)
25
25
  end
26
26
 
27
27
  def matches_non_block_conditions(subject)
@@ -78,7 +78,7 @@ module CanCan
78
78
 
79
79
  def hash_condition_match?(attribute, value)
80
80
  if attribute.is_a?(Array) || (defined?(ActiveRecord) && attribute.is_a?(ActiveRecord::Relation))
81
- attribute.any? { |element| matches_conditions_hash?(element, value) }
81
+ attribute.to_a.any? { |element| matches_conditions_hash?(element, value) }
82
82
  else
83
83
  attribute && matches_conditions_hash?(attribute, value)
84
84
  end
@@ -97,7 +97,10 @@ module CanCan
97
97
  end
98
98
 
99
99
  def conditions_empty?
100
- @conditions == {} || @conditions.nil?
100
+ # @conditions might be an ActiveRecord::Associations::CollectionProxy
101
+ # which it's `==` implementation will fetch all records for comparison
102
+
103
+ (@conditions.is_a?(Hash) && @conditions == {}) || @conditions.nil?
101
104
  end
102
105
  end
103
106
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ def self.valid_accessible_by_strategies
5
+ strategies = [:left_join]
6
+ strategies << :subquery unless CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0')
7
+ strategies
8
+ end
9
+
10
+ # Determines how CanCan should build queries when calling accessible_by,
11
+ # if the query will contain a join. The default strategy is `:subquery`.
12
+ #
13
+ # # config/initializers/cancan.rb
14
+ # CanCan.accessible_by_strategy = :subquery
15
+ #
16
+ # Valid strategies are:
17
+ # - :subquery - Creates a nested query with all joins, wrapped by a
18
+ # WHERE IN query.
19
+ # - :left_join - Calls the joins directly using `left_joins`, and
20
+ # ensures records are unique using `distinct`. Note that
21
+ # `distinct` is not reliable in some cases. See
22
+ # https://github.com/CanCanCommunity/cancancan/pull/605
23
+ def self.accessible_by_strategy
24
+ @accessible_by_strategy || default_accessible_by_strategy
25
+ end
26
+
27
+ def self.default_accessible_by_strategy
28
+ if CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0')
29
+ # see https://github.com/CanCanCommunity/cancancan/pull/655 for where this was added
30
+ # the `subquery` strategy (from https://github.com/CanCanCommunity/cancancan/pull/619
31
+ # only works in Rails 5 and higher
32
+ :left_join
33
+ else
34
+ :subquery
35
+ end
36
+ end
37
+
38
+ def self.accessible_by_strategy=(value)
39
+ unless valid_accessible_by_strategies.include?(value)
40
+ raise ArgumentError, "accessible_by_strategy must be one of #{valid_accessible_by_strategies.join(', ')}"
41
+ end
42
+
43
+ if value == :subquery && CanCan::ModelAdapters::ActiveRecordAdapter.version_lower?('5.0.0')
44
+ raise ArgumentError, 'accessible_by_strategy = :subquery requires ActiveRecord 5 or newer'
45
+ end
46
+
47
+ @accessible_by_strategy = value
48
+ end
49
+ end
@@ -58,5 +58,13 @@ module CanCan
58
58
  def to_s
59
59
  @message || @default_message
60
60
  end
61
+
62
+ def inspect
63
+ details = %i[action subject conditions message].map do |attribute|
64
+ value = instance_variable_get "@#{attribute}"
65
+ "#{attribute}: #{value.inspect}" if value.present?
66
+ end.compact.join(', ')
67
+ "#<#{self.class.name} #{details}>"
68
+ end
61
69
  end
62
70
  end
@@ -5,7 +5,7 @@ module CanCan
5
5
  class AbstractAdapter
6
6
  def self.inherited(subclass)
7
7
  @subclasses ||= []
8
- @subclasses << subclass
8
+ @subclasses.insert(0, subclass)
9
9
  end
10
10
 
11
11
  def self.adapter_class(model_class)
@@ -34,10 +34,8 @@ module CanCan
34
34
  # look inside the where clause to decide to outer join tables
35
35
  # you're using in the where. Instead, `references()` is required
36
36
  # in addition to `includes()` to force the outer join.
37
- def build_relation(*where_conditions)
38
- relation = @model_class.where(*where_conditions)
39
- relation = relation.includes(joins).references(joins) if joins.present?
40
- relation
37
+ def build_joins_relation(relation, *_where_conditions)
38
+ relation.includes(joins).references(joins)
41
39
  end
42
40
 
43
41
  # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
@@ -21,13 +21,19 @@ module CanCan
21
21
 
22
22
  private
23
23
 
24
- def build_relation(*where_conditions)
25
- relation = @model_class.where(*where_conditions)
26
- relation = relation.left_joins(joins).distinct if joins.present?
27
- relation
24
+ def build_joins_relation(relation, *where_conditions)
25
+ case CanCan.accessible_by_strategy
26
+ when :subquery
27
+ inner = @model_class.unscoped do
28
+ @model_class.left_joins(joins).where(*where_conditions)
29
+ end
30
+ @model_class.where(@model_class.primary_key => inner)
31
+
32
+ when :left_join
33
+ relation.left_joins(joins).distinct
34
+ end
28
35
  end
29
36
 
30
- # Rails 4.2 deprecates `sanitize_sql_hash_for_conditions`
31
37
  def sanitize_sql(conditions)
32
38
  if conditions.is_a?(Hash)
33
39
  sanitize_sql_activerecord5(conditions)
@@ -41,11 +47,7 @@ module CanCan
41
47
  table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
42
48
  predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
43
49
 
44
- conditions = predicate_builder.resolve_column_aliases(conditions)
45
-
46
- conditions.stringify_keys!
47
-
48
- predicate_builder.build_from_hash(conditions).map { |b| visit_nodes(b) }.join(' AND ')
50
+ predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ')
49
51
  end
50
52
 
51
53
  def visit_nodes(node)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'conditions_extractor.rb'
4
- require 'cancan/rules_compressor'
5
3
  module CanCan
6
4
  module ModelAdapters
7
5
  class ActiveRecordAdapter < AbstractAdapter
@@ -16,6 +14,7 @@ module CanCan
16
14
  def initialize(model_class, rules)
17
15
  super
18
16
  @compressed_rules = RulesCompressor.new(@rules.reverse).rules_collapsed.reverse
17
+ StiNormalizer.normalize(@compressed_rules)
19
18
  ConditionsNormalizer.normalize(model_class, @compressed_rules)
20
19
  end
21
20
 
@@ -60,6 +59,14 @@ module CanCan
60
59
  end
61
60
  end
62
61
 
62
+ def build_relation(*where_conditions)
63
+ relation = @model_class.where(*where_conditions)
64
+ return relation unless joins.present?
65
+
66
+ # subclasses must implement `build_joins_relation`
67
+ build_joins_relation(relation, *where_conditions)
68
+ end
69
+
63
70
  # Returns the associations used in conditions for the :joins option of a search.
64
71
  # See ModelAdditions#accessible_by
65
72
  def joins
@@ -31,7 +31,7 @@ module CanCan
31
31
  raise WrongAssociationName, "Association '#{key}' not defined in model '#{model_class.name}'"
32
32
  end
33
33
 
34
- if reflection.options[:through].present?
34
+ if normalizable_association? reflection
35
35
  key = reflection.options[:through]
36
36
  value = { reflection.source_reflection_name => value }
37
37
  reflection = model_class.reflect_on_association(key)
@@ -39,6 +39,10 @@ module CanCan
39
39
 
40
40
  { key => normalize_conditions(reflection.klass.name.constantize, value) }
41
41
  end
42
+
43
+ def normalizable_association?(reflection)
44
+ reflection.options[:through].present? && !reflection.options[:source_type].present?
45
+ end
42
46
  end
43
47
  end
44
48
  end
@@ -0,0 +1,31 @@
1
+ # this class is responsible for detecting sti classes and creating new rules for the
2
+ # relevant subclasses, using the inheritance_column as a merger
3
+ module CanCan
4
+ module ModelAdapters
5
+ class StiNormalizer
6
+ class << self
7
+ def normalize(rules)
8
+ rules_cache = []
9
+ rules.delete_if.with_index do |rule, _index|
10
+ subjects = rule.subjects.select do |subject|
11
+ next if subject == :all || subject.descends_from_active_record?
12
+
13
+ rules_cache.push(build_rule_for_subclass(rule, subject))
14
+ true
15
+ end
16
+ subjects.length == rule.subjects.length
17
+ end
18
+ rules_cache.each { |rule| rules.push(rule) }
19
+ end
20
+
21
+ private
22
+
23
+ # create a new rule for the subclasses that links on the inheritance_column
24
+ def build_rule_for_subclass(rule, subject)
25
+ CanCan::Rule.new(rule.base_behavior, rule.actions, subject.superclass,
26
+ rule.conditions.merge(subject.inheritance_column => subject.name), rule.block)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CanCan
4
+ module Relevant
5
+ # Matches both the action, subject, and attribute, not necessarily the conditions
6
+ def relevant?(action, subject)
7
+ subject = subject.values.first if subject.class == Hash
8
+ @match_all || (matches_action?(action) && matches_subject?(subject))
9
+ end
10
+
11
+ private
12
+
13
+ def matches_action?(action)
14
+ @expanded_actions.include?(:manage) || @expanded_actions.include?(action)
15
+ end
16
+
17
+ def matches_subject?(subject)
18
+ @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
19
+ end
20
+
21
+ def matches_subject_class?(subject)
22
+ @subjects.any? do |sub|
23
+ sub.is_a?(Module) && (subject.is_a?(sub) ||
24
+ subject.class.to_s == sub.to_s ||
25
+ (subject.is_a?(Module) && subject.ancestors.include?(sub)))
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'conditions_matcher.rb'
4
+ require_relative 'class_matcher.rb'
5
+ require_relative 'relevant.rb'
6
+
4
7
  module CanCan
5
8
  # This class is used internally and should only be called through Ability.
6
9
  # it holds the information about a "can" call made on Ability and provides
7
10
  # helpful methods to determine permission checking and conditions hash generation.
8
11
  class Rule # :nodoc:
9
12
  include ConditionsMatcher
13
+ include Relevant
10
14
  include ParameterValidators
11
- attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes
15
+ attr_reader :base_behavior, :subjects, :actions, :conditions, :attributes, :block
12
16
  attr_writer :expanded_actions, :conditions
13
17
 
14
18
  # The first argument when initializing is the base_behavior which is a true/false
@@ -24,9 +28,9 @@ module CanCan
24
28
  raise Error, "Subject is required for #{action}" if action && subject.nil?
25
29
 
26
30
  @base_behavior = base_behavior
27
- @actions = Array(action)
28
- @subjects = Array(subject)
29
- @attributes = Array(attributes)
31
+ @actions = wrap(action)
32
+ @subjects = wrap(subject)
33
+ @attributes = wrap(attributes)
30
34
  @conditions = extra_args || {}
31
35
  @block = block
32
36
  end
@@ -34,11 +38,13 @@ module CanCan
34
38
  def inspect
35
39
  repr = "#<#{self.class.name}"
36
40
  repr += "#{@base_behavior ? 'can' : 'cannot'} #{@actions.inspect}, #{@subjects.inspect}, #{@attributes.inspect}"
37
- repr += if with_scope?
38
- ", #{@conditions.where_values_hash}"
39
- elsif [Hash, String].include?(@conditions.class)
40
- ", #{@conditions.inspect}"
41
- end
41
+
42
+ if with_scope?
43
+ repr += ", #{@conditions.where_values_hash}"
44
+ elsif [Hash, String].include?(@conditions.class)
45
+ repr += ", #{@conditions.inspect}"
46
+ end
47
+
42
48
  repr + '>'
43
49
  end
44
50
 
@@ -55,12 +61,6 @@ module CanCan
55
61
  (!with_scope? && [nil, false, [], {}, '', ' '].include?(@conditions))
56
62
  end
57
63
 
58
- # Matches both the action, subject, and attribute, not necessarily the conditions
59
- def relevant?(action, subject)
60
- subject = subject.values.first if subject.class == Hash
61
- @match_all || (matches_action?(action) && matches_subject?(subject))
62
- end
63
-
64
64
  def only_block?
65
65
  conditions_empty? && @block
66
66
  end
@@ -111,11 +111,7 @@ module CanCan
111
111
  end
112
112
 
113
113
  def matches_subject_class?(subject)
114
- @subjects.any? do |sub|
115
- sub.is_a?(Module) && (subject.is_a?(sub) ||
116
- subject.class.to_s == sub.to_s ||
117
- (subject.is_a?(Module) && subject.ancestors.include?(sub)))
118
- end
114
+ SubjectClassMatcher.matches_subject_class?(@subjects, subject)
119
115
  end
120
116
 
121
117
  def parse_attributes_from_extra_args(args)
@@ -130,5 +126,15 @@ module CanCan
130
126
  raise BlockAndConditionsError, 'A hash of conditions is mutually exclusive with a block. '\
131
127
  "Check \":#{action} #{subject}\" ability."
132
128
  end
129
+
130
+ def wrap(object)
131
+ if object.nil?
132
+ []
133
+ elsif object.respond_to?(:to_ary)
134
+ object.to_ary || [object]
135
+ else
136
+ [object]
137
+ end
138
+ end
133
139
  end
134
140
  end
@@ -3,10 +3,12 @@
3
3
  module CanCan
4
4
  module UnauthorizedMessageResolver
5
5
  def unauthorized_message(action, subject)
6
+ subject = subject.values.last if subject.is_a?(Hash)
6
7
  keys = unauthorized_message_keys(action, subject)
7
- variables = { action: action.to_s }
8
+ variables = {}
9
+ variables[:action] = I18n.translate("actions.#{action}", default: action.to_s)
8
10
  variables[:subject] = translate_subject(subject)
9
- message = I18n.translate(keys.shift, variables.merge(scope: :unauthorized, default: keys + ['']))
11
+ message = I18n.translate(keys.shift, **variables.merge(scope: :unauthorized, default: keys + ['']))
10
12
  message.blank? ? nil : message
11
13
  end
12
14
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CanCan
4
- VERSION = '3.0.0.rc1'.freeze
4
+ VERSION = '3.2.0'.freeze
5
5
  end
metadata CHANGED
@@ -1,38 +1,38 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cancancan
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.rc1
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi (Renuo AG)
8
8
  - Bryan Rite
9
9
  - Ryan Bates
10
10
  - Richard Wilson
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2019-03-17 00:00:00.000000000 Z
14
+ date: 2020-12-12 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: appraisal
18
18
  requirement: !ruby/object:Gem::Requirement
19
19
  requirements:
20
- - - "~>"
21
- - !ruby/object:Gem::Version
22
- version: '2.0'
23
20
  - - ">="
24
21
  - !ruby/object:Gem::Version
25
22
  version: 2.0.0
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
26
  type: :development
27
27
  prerelease: false
28
28
  version_requirements: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '2.0'
33
30
  - - ">="
34
31
  - !ruby/object:Gem::Version
35
32
  version: 2.0.0
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: '2.0'
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: bundler
38
38
  requirement: !ruby/object:Gem::Requirement
@@ -71,22 +71,22 @@ dependencies:
71
71
  name: rspec
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
- - - "~>"
75
- - !ruby/object:Gem::Version
76
- version: '3.2'
77
74
  - - ">="
78
75
  - !ruby/object:Gem::Version
79
76
  version: 3.2.0
77
+ - - "~>"
78
+ - !ruby/object:Gem::Version
79
+ version: '3.2'
80
80
  type: :development
81
81
  prerelease: false
82
82
  version_requirements: !ruby/object:Gem::Requirement
83
83
  requirements:
84
- - - "~>"
85
- - !ruby/object:Gem::Version
86
- version: '3.2'
87
84
  - - ">="
88
85
  - !ruby/object:Gem::Version
89
86
  version: 3.2.0
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.2'
90
90
  - !ruby/object:Gem::Dependency
91
91
  name: rubocop
92
92
  requirement: !ruby/object:Gem::Requirement
@@ -115,7 +115,9 @@ files:
115
115
  - lib/cancan/ability/actions.rb
116
116
  - lib/cancan/ability/rules.rb
117
117
  - lib/cancan/ability/strong_parameter_support.rb
118
+ - lib/cancan/class_matcher.rb
118
119
  - lib/cancan/conditions_matcher.rb
120
+ - lib/cancan/config.rb
119
121
  - lib/cancan/controller_additions.rb
120
122
  - lib/cancan/controller_resource.rb
121
123
  - lib/cancan/controller_resource_builder.rb
@@ -132,8 +134,10 @@ files:
132
134
  - lib/cancan/model_adapters/conditions_extractor.rb
133
135
  - lib/cancan/model_adapters/conditions_normalizer.rb
134
136
  - lib/cancan/model_adapters/default_adapter.rb
137
+ - lib/cancan/model_adapters/sti_normalizer.rb
135
138
  - lib/cancan/model_additions.rb
136
139
  - lib/cancan/parameter_validators.rb
140
+ - lib/cancan/relevant.rb
137
141
  - lib/cancan/rule.rb
138
142
  - lib/cancan/rules_compressor.rb
139
143
  - lib/cancan/unauthorized_message_resolver.rb
@@ -146,7 +150,7 @@ homepage: https://github.com/CanCanCommunity/cancancan
146
150
  licenses:
147
151
  - MIT
148
152
  metadata: {}
149
- post_install_message:
153
+ post_install_message:
150
154
  rdoc_options: []
151
155
  require_paths:
152
156
  - lib
@@ -157,13 +161,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
157
161
  version: 2.2.0
158
162
  required_rubygems_version: !ruby/object:Gem::Requirement
159
163
  requirements:
160
- - - ">"
164
+ - - ">="
161
165
  - !ruby/object:Gem::Version
162
- version: 1.3.1
166
+ version: '0'
163
167
  requirements: []
164
- rubyforge_project:
165
- rubygems_version: 2.7.6
166
- signing_key:
168
+ rubygems_version: 3.0.6
169
+ signing_key:
167
170
  specification_version: 4
168
171
  summary: Simple authorization solution for Rails.
169
172
  test_files: []