babik 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +16 -0
  3. data/README.md +718 -0
  4. data/Rakefile +18 -0
  5. data/lib/babik.rb +122 -0
  6. data/lib/babik/database.rb +16 -0
  7. data/lib/babik/queryset.rb +154 -0
  8. data/lib/babik/queryset/components/aggregation.rb +172 -0
  9. data/lib/babik/queryset/components/limit.rb +22 -0
  10. data/lib/babik/queryset/components/order.rb +161 -0
  11. data/lib/babik/queryset/components/projection.rb +118 -0
  12. data/lib/babik/queryset/components/select_related.rb +78 -0
  13. data/lib/babik/queryset/components/sql_renderer.rb +99 -0
  14. data/lib/babik/queryset/components/where.rb +43 -0
  15. data/lib/babik/queryset/lib/association/foreign_association_chain.rb +97 -0
  16. data/lib/babik/queryset/lib/association/select_related_association_chain.rb +32 -0
  17. data/lib/babik/queryset/lib/condition.rb +103 -0
  18. data/lib/babik/queryset/lib/field.rb +34 -0
  19. data/lib/babik/queryset/lib/join/association_joiner.rb +39 -0
  20. data/lib/babik/queryset/lib/join/join.rb +86 -0
  21. data/lib/babik/queryset/lib/selection/config.rb +19 -0
  22. data/lib/babik/queryset/lib/selection/foreign_selection.rb +39 -0
  23. data/lib/babik/queryset/lib/selection/local_selection.rb +40 -0
  24. data/lib/babik/queryset/lib/selection/operation/base.rb +126 -0
  25. data/lib/babik/queryset/lib/selection/operation/date.rb +178 -0
  26. data/lib/babik/queryset/lib/selection/operation/operations.rb +201 -0
  27. data/lib/babik/queryset/lib/selection/operation/regex.rb +58 -0
  28. data/lib/babik/queryset/lib/selection/path/foreign_path.rb +50 -0
  29. data/lib/babik/queryset/lib/selection/path/local_path.rb +44 -0
  30. data/lib/babik/queryset/lib/selection/path/path.rb +23 -0
  31. data/lib/babik/queryset/lib/selection/select_related_selection.rb +38 -0
  32. data/lib/babik/queryset/lib/selection/selection.rb +19 -0
  33. data/lib/babik/queryset/lib/update/assignment.rb +108 -0
  34. data/lib/babik/queryset/mixins/aggregatable.rb +17 -0
  35. data/lib/babik/queryset/mixins/bounded.rb +38 -0
  36. data/lib/babik/queryset/mixins/clonable.rb +52 -0
  37. data/lib/babik/queryset/mixins/countable.rb +44 -0
  38. data/lib/babik/queryset/mixins/deletable.rb +13 -0
  39. data/lib/babik/queryset/mixins/distinguishable.rb +27 -0
  40. data/lib/babik/queryset/mixins/filterable.rb +51 -0
  41. data/lib/babik/queryset/mixins/limitable.rb +88 -0
  42. data/lib/babik/queryset/mixins/lockable.rb +31 -0
  43. data/lib/babik/queryset/mixins/none.rb +16 -0
  44. data/lib/babik/queryset/mixins/projectable.rb +34 -0
  45. data/lib/babik/queryset/mixins/related_selector.rb +28 -0
  46. data/lib/babik/queryset/mixins/set_operations.rb +32 -0
  47. data/lib/babik/queryset/mixins/sortable.rb +49 -0
  48. data/lib/babik/queryset/mixins/sql_renderizable.rb +17 -0
  49. data/lib/babik/queryset/mixins/updatable.rb +14 -0
  50. data/lib/babik/queryset/templates/default/delete/main.sql.erb +14 -0
  51. data/lib/babik/queryset/templates/default/select/components/aggregation.sql.erb +5 -0
  52. data/lib/babik/queryset/templates/default/select/components/from.sql.erb +16 -0
  53. data/lib/babik/queryset/templates/default/select/components/from_set.sql.erb +3 -0
  54. data/lib/babik/queryset/templates/default/select/components/from_table.sql.erb +2 -0
  55. data/lib/babik/queryset/templates/default/select/components/limit.sql.erb +10 -0
  56. data/lib/babik/queryset/templates/default/select/components/order_by.sql.erb +9 -0
  57. data/lib/babik/queryset/templates/default/select/components/projection.sql.erb +7 -0
  58. data/lib/babik/queryset/templates/default/select/components/select_related.sql.erb +26 -0
  59. data/lib/babik/queryset/templates/default/select/components/where.sql.erb +39 -0
  60. data/lib/babik/queryset/templates/default/select/main.sql.erb +42 -0
  61. data/lib/babik/queryset/templates/default/update/main.sql.erb +15 -0
  62. data/lib/babik/queryset/templates/mssql/select/components/limit.sql.erb +8 -0
  63. data/lib/babik/queryset/templates/mssql/select/components/order_by.sql.erb +21 -0
  64. data/lib/babik/queryset/templates/mysql2/delete/main.sql.erb +15 -0
  65. data/lib/babik/queryset/templates/mysql2/update/main.sql.erb +18 -0
  66. data/lib/babik/queryset/templates/sqlite3/select/components/from_set.sql.erb +5 -0
  67. data/test/config/db/schema.rb +83 -0
  68. data/test/config/models/bad_post.rb +5 -0
  69. data/test/config/models/bad_tag.rb +5 -0
  70. data/test/config/models/category.rb +4 -0
  71. data/test/config/models/geozone.rb +6 -0
  72. data/test/config/models/group.rb +5 -0
  73. data/test/config/models/group_user.rb +5 -0
  74. data/test/config/models/post.rb +24 -0
  75. data/test/config/models/post_tag.rb +5 -0
  76. data/test/config/models/tag.rb +5 -0
  77. data/test/config/models/user.rb +6 -0
  78. data/test/delete/delete_test.rb +60 -0
  79. data/test/delete/foreign_conditions_delete_test.rb +57 -0
  80. data/test/delete/local_conditions_delete_test.rb +20 -0
  81. data/test/enable_coverage.rb +17 -0
  82. data/test/lib/selection/operation/log/test-queries.log +1 -0
  83. data/test/lib/selection/operation/test_date.rb +131 -0
  84. data/test/lib/selection/operation/test_regex.rb +55 -0
  85. data/test/other/clone_test.rb +129 -0
  86. data/test/other/escape_test.rb +21 -0
  87. data/test/other/inverse_of_required_test.rb +33 -0
  88. data/test/select/aggregate_test.rb +151 -0
  89. data/test/select/bounds_test.rb +46 -0
  90. data/test/select/count_test.rb +147 -0
  91. data/test/select/distinct_test.rb +38 -0
  92. data/test/select/exclude_test.rb +72 -0
  93. data/test/select/filter_from_object_test.rb +125 -0
  94. data/test/select/filter_test.rb +207 -0
  95. data/test/select/for_update_test.rb +19 -0
  96. data/test/select/foreign_selection_test.rb +60 -0
  97. data/test/select/get_test.rb +40 -0
  98. data/test/select/limit_test.rb +109 -0
  99. data/test/select/local_selection_test.rb +24 -0
  100. data/test/select/lookup_test.rb +208 -0
  101. data/test/select/none_test.rb +40 -0
  102. data/test/select/order_test.rb +165 -0
  103. data/test/select/project_test.rb +107 -0
  104. data/test/select/select_related_test.rb +124 -0
  105. data/test/select/subquery_test.rb +50 -0
  106. data/test/set_operations/basic_usage_test.rb +121 -0
  107. data/test/test_helper.rb +55 -0
  108. data/test/update/update_test.rb +93 -0
  109. metadata +278 -0
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/selection/operation/base'
4
+
5
+ module Babik
6
+ module Selection
7
+ # SQL operation module
8
+ module Operation
9
+
10
+ # Match by case sensitive regex
11
+ class Regex < Base
12
+ def initialize(field, value)
13
+ value = value.inspect[1..-2] if value.class == Regexp
14
+ value = value[1..-2] if value.class == String && value[0] == '/' && value[-1] == '/'
15
+ value = _mysql2_convert_regex(value) if db_engine == 'mysql2'
16
+ super(field, "?field #{operator} ?value", value)
17
+ end
18
+
19
+ def operator
20
+ dbms_adapter = db_engine
21
+ return 'REGEXP' if dbms_adapter == 'mysql2'
22
+ return '~' if dbms_adapter == 'postgresql'
23
+ return 'REGEXP' if dbms_adapter == 'sqlite3'
24
+ raise NotImplementedError, "Invalid dbms #{dbms_adapter}. Only mysql, postgresql, and sqlite3 are accepted"
25
+ end
26
+
27
+ def _mysql2_convert_regex(value)
28
+ replacements = { '\\d' => '[0-9]', '\\w' => '[a-zA-Z]' }
29
+ replacements.each do |pcre_pattern, mysql_pattern|
30
+ value = value.gsub(pcre_pattern, mysql_pattern)
31
+ end
32
+ value
33
+ end
34
+ end
35
+
36
+ # Match by case insensitive regex
37
+ class IRegex < Regex
38
+
39
+ def initialize(field, value)
40
+ value = value.inspect[1..-2] if value.class == Regexp
41
+ value = value[1..-2] if value.class == String && value[0] == '/' && value[-1] == '/'
42
+ value = "(?i)#{value}" if db_engine == 'sqlite3'
43
+ field = "LOWER(#{field})" if db_engine == 'mysql2'
44
+ super(field, value)
45
+ end
46
+
47
+ def operator
48
+ dbms_adapter = db_engine
49
+ return 'REGEXP' if dbms_adapter == 'mysql2'
50
+ return '~*' if dbms_adapter == 'postgresql'
51
+ return 'REGEXP' if dbms_adapter == 'sqlite3'
52
+ raise NotImplementedError, "Invalid dbms #{dbms_adapter}. Only mysql, postgresql, and sqlite3 are accepted"
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/selection/config'
4
+ require 'babik/queryset/lib/join/association_joiner'
5
+ require 'babik/queryset/lib/association/foreign_association_chain'
6
+
7
+ module Babik
8
+ module Selection
9
+ module Path
10
+ # Foreign path
11
+ # A foreign path is a succession of associations ending optionally in an operator
12
+ # if operator is not present, equal is supposed.
13
+ class ForeignPath
14
+ RELATIONSHIP_SEPARATOR = Babik::Selection::Config::RELATIONSHIP_SEPARATOR
15
+ OPERATOR_SEPARATOR = Babik::Selection::Config::OPERATOR_SEPARATOR
16
+
17
+ attr_reader :model, :selection_path, :selected_field
18
+
19
+ delegate :left_joins_by_alias, to: :@association_joiner
20
+ delegate :target_alias, to: :@association_joiner
21
+ delegate :associations, to: :@association_chain
22
+
23
+ # Construct a foreign path
24
+ # A foreign path will be used with a value as a foreign selection to filter
25
+ # a model with foreign conditions
26
+ # @param model [ActiveRecord::Base] model that is the object of the foreign path.
27
+ # @param selection_path [String, Symbol] Association path with an operator. e.g.:
28
+ # posts::category__in
29
+ # author::posts::tags
30
+ # creation_at__date__gte
31
+ #
32
+ def initialize(model, selection_path)
33
+ @model = model
34
+ @selection_path = selection_path.dup
35
+ @association_path = selection_path.to_s.split(RELATIONSHIP_SEPARATOR)
36
+ selection_path = @association_path.pop
37
+ @selected_field, @operator = selection_path.split(OPERATOR_SEPARATOR)
38
+ @operator ||= 'equal'
39
+ _initialize_associations
40
+ end
41
+
42
+ # Initialize associations
43
+ def _initialize_associations
44
+ @association_chain = Babik::Association::ForeignAssociationChain.new(@model, @association_path, @selection_path)
45
+ @association_joiner = Babik::QuerySet::Join::AssociationJoiner.new(@association_chain.associations)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/selection/config'
4
+
5
+ module Babik
6
+ module Selection
7
+ module Path
8
+
9
+ # Local path
10
+ class LocalPath
11
+ OPERATOR_SEPARATOR = Babik::Selection::Config::OPERATOR_SEPARATOR
12
+
13
+ attr_reader :model, :selection_path, :selected_field, :operator, :secondary_operator
14
+
15
+ # Construct a local field path
16
+ # @param model [ActiveRecord::Base] model whose field will be used.
17
+ # @param selection_path [String] selection path. Of the form <field>__<operator>. e.g. first_name__equal, stars__gt
18
+ # If no operator is given (first_name), 'equal' will be used.
19
+ def initialize(model, selection_path)
20
+ @model = model
21
+ @selection_path = selection_path.dup
22
+ @selected_field, @operator, @secondary_operator = @selection_path.to_s.split(OPERATOR_SEPARATOR)
23
+ # By default, if no operator is given, 'equal' will be used
24
+ @operator ||= 'equal'
25
+ end
26
+
27
+ # Return the target table alias.
28
+ # That is alias of the model table.
29
+ # For the moment, actually, return the name of this model's table.
30
+ # @return [String] alias of the model table.
31
+ def target_alias
32
+ @model.table_name
33
+ end
34
+
35
+ # A local selection has no related left joins
36
+ # @return [Hash] Empty hash.
37
+ def left_joins_by_alias
38
+ {}
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/selection/config'
4
+ require 'babik/queryset/lib/selection/path/foreign_path'
5
+ require 'babik/queryset/lib/selection/path/local_path'
6
+
7
+ module Babik
8
+ module Selection
9
+ module Path
10
+ # Represents a factory class for ForeignPath & LocalPath
11
+ class Factory
12
+
13
+ # Factory Method used to create local and foreign selections
14
+ def self.build(model, selection_path)
15
+ is_foreign = selection_path.match?(Babik::Selection::Config::RELATIONSHIP_SEPARATOR)
16
+ return Babik::Selection::Path::ForeignPath.new(model, selection_path) if is_foreign
17
+ Babik::Selection::Path::LocalPath.new(model, selection_path)
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/join/association_joiner'
4
+ require 'babik/queryset/lib/join/join'
5
+ require 'babik/queryset/lib/selection/selection'
6
+ require 'babik/queryset/lib/selection/config'
7
+ require 'babik/queryset/lib/association/select_related_association_chain'
8
+ require 'babik/queryset/lib/selection/path/foreign_path'
9
+
10
+ module Babik
11
+ module Selection
12
+
13
+ # Abstraction of a selection used in select_related operation
14
+ class SelectRelatedSelection
15
+ RELATIONSHIP_SEPARATOR = Babik::Selection::Config::RELATIONSHIP_SEPARATOR
16
+ attr_reader :model, :selection_path, :association_path, :target_model, :id
17
+
18
+ delegate :left_joins_by_alias, to: :@association_joiner
19
+ delegate :target_alias, to: :@association_joiner
20
+
21
+ def initialize(model, selection_path)
22
+ @model = model
23
+ @selection_path = selection_path.dup
24
+ @association_path = selection_path.to_s.split(RELATIONSHIP_SEPARATOR)
25
+ @id = @association_path.join('__')
26
+
27
+ _initialize_associations
28
+ @target_model = @association_chain.target_model
29
+ end
30
+
31
+ def _initialize_associations
32
+ @association_chain = Babik::Association::SelectRelatedAssociationChain.new(@model, @association_path, @selection_path)
33
+ @association_joiner = Babik::QuerySet::Join::AssociationJoiner.new(@association_chain.associations)
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'babik/queryset/lib/selection/config'
4
+
5
+ module Babik
6
+ module Selection
7
+ # Represents a filter selection (that can be filtered in WHERE)
8
+ class Base
9
+
10
+ # Factory Method used to create local and foreign selections
11
+ def self.factory(model, selection_path, value)
12
+ is_foreign_selection = selection_path.match?(Babik::Selection::Config::RELATIONSHIP_SEPARATOR)
13
+ return Babik::Selection::ForeignSelection.new(model, selection_path, value) if is_foreign_selection
14
+ Babik::Selection::LocalSelection.new(model, selection_path, value)
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Common module for Babik library
4
+ module Babik
5
+ module QuerySet
6
+ # Update operation module
7
+ module Update
8
+ # Field assignment module
9
+ module Assignment
10
+ # Return the field prepared for the UPDATE operation.
11
+ # Used when rendering the SQL template
12
+ # @param model [ActiveRecord::Base] model this field belongs to.
13
+ # @param field [String] field to be updated.
14
+ # @return [String] Field prepared to be inserted in the left part of a SQL UPDATE assignment.
15
+ def self.sql_field(model, field)
16
+ field = Babik::Table::Field.new(model, field)
17
+ field.real_field
18
+ end
19
+
20
+ # Return the value prepared for the UPDATE operation.
21
+ # Used when rendering the SQL template
22
+ # @param update_field_value [Operation, Function, String, ActiveRecord::BASE] field to be updated.
23
+ # if Operation, an arithmetic operation based on other field of the record will be applied (+, -, * ...)
24
+ # if Function, a function will be called.
25
+ # The parameters of the function can ben any other field of the record.
26
+ # if String, a escaped version of the value will be returned.
27
+ # if ActiveRecord::Base, the id of the object will be returned.
28
+ # Otherwise, the value as-is will be returned.
29
+ # @return [String] Field prepared to be inserted in the left part of a SQL UPDATE assignment.
30
+ def self.sql_value(update_field_value)
31
+ return update_field_value.sql_value if update_field_value.is_a?(Operation) || update_field_value.is_a?(Function)
32
+ return _escape(update_field_value) if update_field_value.is_a?(String)
33
+ return update_field_value.id if update_field_value.is_a?(ActiveRecord::Base)
34
+ update_field_value
35
+ end
36
+
37
+ # Escape a value for database
38
+ # @param str [String] original string value.
39
+ # @return [String] escaped string value.
40
+ def self._escape(str)
41
+ Babik::Database.escape(str)
42
+ end
43
+
44
+ # Represents a function operator that can be used in an UPDATE
45
+ # For example:
46
+ # UPDATE SET stars = ABS(stars)
47
+ class Function
48
+ def initialize(field, function_call)
49
+ @field = field
50
+ @function_call = function_call
51
+ end
52
+
53
+ # Return the right part of the assignment of the UPDATE statement.
54
+ # @return [String] right part of the assignment with the format defined by the function_call attribute.
55
+ def sql_value
56
+ @function_call
57
+ end
58
+ end
59
+
60
+ # Represents a table field. It will be used when an update field is based on its value an nothing else.
61
+ # For example:
62
+ # UPDATE SET stars = stars + 1
63
+ class Operation
64
+ def initialize(field, operation, value)
65
+ @field = field
66
+ @operation = operation
67
+ @value = value
68
+ end
69
+
70
+ # Return the right part of the assignment of the UPDATE statement.
71
+ # @return [String] right part of the assignment with the format <field> <operation> <value>.
72
+ def sql_value
73
+ "#{@field} #{@operation} #{@value}"
74
+ end
75
+ end
76
+
77
+ # Decrement operation
78
+ class Decrement < Operation
79
+ def initialize(field, value = 1)
80
+ super(field, '-', value)
81
+ end
82
+ end
83
+
84
+ # Increment operation
85
+ class Increment < Operation
86
+ def initialize(field, value = 1)
87
+ super(field, '+', value)
88
+ end
89
+ end
90
+
91
+ # Multiplication operation
92
+ class Multiply < Operation
93
+ def initialize(field, value)
94
+ super(field, '*', value)
95
+ end
96
+ end
97
+
98
+ # Division operation
99
+ class Divide < Operation
100
+ def initialize(field, value)
101
+ super(field, '/', value)
102
+ end
103
+ end
104
+
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Functionality related to the aggregation selection
6
+ module Aggregatable
7
+ # Aggregate a set of objects.
8
+ # @param agg_functions [Hash{symbol: Babik.agg}] hash with the different aggregations that will be computed.
9
+ # @return [Hash{symbol: float}] Result of computing each one of the aggregations.
10
+ def aggregate(agg_functions)
11
+ @_aggregation = Babik::QuerySet::Aggregation.new(@model, agg_functions)
12
+ select_sql = sql.select
13
+ self.class._execute_sql(select_sql).first.transform_values(&:to_f).symbolize_keys
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Babik
4
+ module QuerySet
5
+ # Every QuerySet is bounded by its first and last items
6
+ module Bounded
7
+
8
+ # Return the first element given some order
9
+ # @param order [Array, String, Hash] ordering that will be applied to the QuerySet.
10
+ # See {Babik::QuerySet::Sortable#order_by}.
11
+ # @return [ActiveRecord::Base] First element according to the order.
12
+ def earliest(*order)
13
+ self.order_by(*order).first
14
+ end
15
+
16
+ # Return the first element of the QuerySet.
17
+ # @return [ActiveRecord::Base] First element of the QuerySet.
18
+ def first
19
+ self.all.first
20
+ end
21
+
22
+ # Return the last element of the QuerySet.
23
+ # @return [ActiveRecord::Base] Last element of the QuerySet.
24
+ def last
25
+ self.invert_order.all.first
26
+ end
27
+
28
+ # Return the last element given some order
29
+ # @param order [Array, String, Hash] ordering that will be applied to the QuerySet.
30
+ # See {Babik::QuerySet::Sortable#order_by}.
31
+ # @return [ActiveRecord::Base] Last element according to the order.
32
+ def latest(*order)
33
+ self.order_by(*order).last
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'deep_clone'
4
+
5
+ module Babik
6
+ module QuerySet
7
+ # Clone operation for the QuerySet
8
+ module Clonable
9
+
10
+ # Clone the queryset using ruby_deep_clone {https://github.com/gmodarelli/ruby-deepclone}.
11
+ # @return [QuerySet] Deep copy of this QuerySet.
12
+ def clone
13
+ DeepClone.clone(self)
14
+ end
15
+
16
+ # Clone this QuerySet and apply the 'mutator_method' to it.
17
+ # @param mutator_method [Symbol] Name of the method.
18
+ # @param parameters [Array] Parameters passed to the method
19
+ # @return [QuerySet] The resultant QuerySet of applying the mutator to the clone of the caller object.
20
+ def mutate_clone(mutator_method, parameters = [])
21
+ clone_ = clone
22
+ if parameters.empty?
23
+ clone_.send(mutator_method)
24
+ else
25
+ clone_.send(mutator_method, *parameters)
26
+ end
27
+ clone_
28
+ end
29
+
30
+ # Check if the called method has a modifying version (a bang method). If that is the case
31
+ # it will be called on a clone of this instance. Otherwise, super will be called.
32
+ # @param name [String] method name
33
+ # @param args [String] method arguments
34
+ # @param _block [Proc] Proc that could be passed to the method. Not used.
35
+ # @return [QuerySet] Clone of this QuerySet (with method 'name' called on ), an empty QuerySet.
36
+ def method_missing(name, *args, &_block)
37
+ modifying_method = "#{name}!"
38
+ return mutate_clone(modifying_method.to_sym, args) if self.respond_to?(modifying_method)
39
+ super
40
+ end
41
+
42
+ # Check if the called method has a modifying version (a bang method).
43
+ # @return [Boolean] True if there is a modifying method with the requested method name
44
+ # in that case, return true, otherwise, return false.
45
+ def respond_to_missing?(name, *_args, &_block)
46
+ modifying_method = "#{name}!"
47
+ self.respond_to?(modifying_method)
48
+ end
49
+
50
+ end
51
+ end
52
+ end