hotstepper 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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +36 -0
  4. data/Rakefile +8 -0
  5. data/app/validators/unique_items_validator.rb +40 -0
  6. data/config/locales/custom-predicates.en.yml +70 -0
  7. data/config/locales/dry-validation-errors.en.yml +113 -0
  8. data/config/locales/en.yml +14 -0
  9. data/config/routes.rb +4 -0
  10. data/lib/boot/setup.rb +151 -0
  11. data/lib/generators/hotstepper/service/USAGE +17 -0
  12. data/lib/generators/hotstepper/service/service_generator.rb +137 -0
  13. data/lib/generators/hotstepper/service/templates/operation.rb.tt +10 -0
  14. data/lib/generators/hotstepper/service/templates/service.rb.tt +39 -0
  15. data/lib/generators/hotstepper/typespace/USAGE +8 -0
  16. data/lib/generators/hotstepper/typespace/templates/types.rb.tt +5 -0
  17. data/lib/generators/hotstepper/typespace/typespace_generator.rb +25 -0
  18. data/lib/hotstepper/arel_ext/array_encoding.rb +66 -0
  19. data/lib/hotstepper/arel_ext/base_helpers.rb +220 -0
  20. data/lib/hotstepper/arel_ext/helpers.rb +17 -0
  21. data/lib/hotstepper/arel_ext/json_encoding.rb +49 -0
  22. data/lib/hotstepper/arel_ext/ltree_encoding.rb +52 -0
  23. data/lib/hotstepper/arel_ext/postgres_helpers.rb +34 -0
  24. data/lib/hotstepper/arel_ext/types.rb +37 -0
  25. data/lib/hotstepper/arel_ext/value_select_query.rb +142 -0
  26. data/lib/hotstepper/common.rb +52 -0
  27. data/lib/hotstepper/engine.rb +38 -0
  28. data/lib/hotstepper/models/association_helpers.rb +141 -0
  29. data/lib/hotstepper/models/checks_before_destruction.rb +35 -0
  30. data/lib/hotstepper/models/id_quoting.rb +23 -0
  31. data/lib/hotstepper/models/lazy_ordering.rb +57 -0
  32. data/lib/hotstepper/models/limit_to_one.rb +35 -0
  33. data/lib/hotstepper/models/lookup_helpers.rb +21 -0
  34. data/lib/hotstepper/models/materialized_view.rb +39 -0
  35. data/lib/hotstepper/models/model.rb +40 -0
  36. data/lib/hotstepper/models/querying.rb +55 -0
  37. data/lib/hotstepper/models/timestamp_scopes.rb +26 -0
  38. data/lib/hotstepper/models/view.rb +14 -0
  39. data/lib/hotstepper/models/where_matches.rb +67 -0
  40. data/lib/hotstepper/monadic/coercion.rb +111 -0
  41. data/lib/hotstepper/monadic/decorators.rb +61 -0
  42. data/lib/hotstepper/monadic/do_for.rb +22 -0
  43. data/lib/hotstepper/monadic.rb +8 -0
  44. data/lib/hotstepper/operation.rb +41 -0
  45. data/lib/hotstepper/patterns.rb +20 -0
  46. data/lib/hotstepper/query_building.rb +142 -0
  47. data/lib/hotstepper/service.rb +84 -0
  48. data/lib/hotstepper/steps/abstract_module.rb +21 -0
  49. data/lib/hotstepper/steps/implementation.rb +39 -0
  50. data/lib/hotstepper/steps/step.rb +26 -0
  51. data/lib/hotstepper/steps/watcher.rb +23 -0
  52. data/lib/hotstepper/steps/wrapper.rb +16 -0
  53. data/lib/hotstepper/struct.rb +30 -0
  54. data/lib/hotstepper/support/file_wrapper.rb +75 -0
  55. data/lib/hotstepper/support/generators/common.rb +39 -0
  56. data/lib/hotstepper/support/generators/namespaced.rb +23 -0
  57. data/lib/hotstepper/support/generators/service_operations.rb +14 -0
  58. data/lib/hotstepper/support/generators/step_definition.rb +45 -0
  59. data/lib/hotstepper/support/namespace_wrapper.rb +73 -0
  60. data/lib/hotstepper/support/null_service.rb +14 -0
  61. data/lib/hotstepper/support/types.rb +26 -0
  62. data/lib/hotstepper/testing/do_for.rb +23 -0
  63. data/lib/hotstepper/testing/operations.rb +88 -0
  64. data/lib/hotstepper/testing/typing.rb +18 -0
  65. data/lib/hotstepper/testing.rb +24 -0
  66. data/lib/hotstepper/typespace.rb +108 -0
  67. data/lib/hotstepper/typing.rb +69 -0
  68. data/lib/hotstepper/version.rb +5 -0
  69. data/lib/hotstepper.rb +56 -0
  70. data/lib/tasks/hotstepper_tasks.rake +6 -0
  71. data/lib/testing/contexts/operations.rb +13 -0
  72. data/lib/testing/matchers/00_disable_with.rb +22 -0
  73. data/lib/testing/matchers/dry_validation.rb +94 -0
  74. data/lib/testing/matchers/monadic.rb +107 -0
  75. data/lib/testing/matchers/negated.rb +8 -0
  76. metadata +377 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ class Engine < ::Rails::Engine
5
+ initializer "hotstepper.i18n" do
6
+ ActiveSupport.on_load(:i18n) do
7
+ I18n.backend.eager_load!
8
+
9
+ [Dry::Schema, Dry::Validation::Contract].each do |lib|
10
+ lib.config.messages.backend = :i18n
11
+ lib.config.messages.load_paths << Hotstepper::Engine.root.join("config", "locales", "custom-predicates.en.yml").realpath
12
+ lib.config.messages.load_paths << Rails.root.join("config", "locales", "en.yml").realpath
13
+ end
14
+ end
15
+ end
16
+
17
+ initializer "hotstepper.models" do
18
+ ActiveSupport.on_load(:active_record) do
19
+ extend Hotstepper::ArelExt::Helpers
20
+
21
+ include Hotstepper::Models::Model
22
+ end
23
+
24
+ if defined?(ApplicationRecord)
25
+ Hotstepper.config.db.connection_class = ApplicationRecord
26
+ end
27
+ end
28
+
29
+ # Wire up the `Hotstepper::Common` infrastructure.
30
+ initializer "hotstepper.operation_integration" do
31
+ config.to_prepare do
32
+ Hotstepper.configure do |config|
33
+ config.common.container = Dry::Rails::Railtie.container
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ # Helpers for defining certain types of associations.
6
+ module AssociationHelpers
7
+ extend ActiveSupport::Concern
8
+
9
+ include Hotstepper::Models::LookupHelpers
10
+
11
+ module ClassMethods
12
+ # @param [Symbol] name
13
+ # @param [Class] model_klass
14
+ # @return [Boolean] whether the association with the given name
15
+ def associated_against?(name, model_klass)
16
+ has_association_for?(name) { _1.klass == model_klass }
17
+ end
18
+
19
+ # Delegate a scope to an association.
20
+ #
21
+ # * For `has_many` associations, it will use a subquery to filter
22
+ # the parent model by the associated records matching the delegated scope.
23
+ # This avoids the cartesian issue with joining against many child records.
24
+ # * For `has_one` and `belongs_to` associations, it will use an `INNER JOIN`
25
+ # and merge the delegated scope directly.
26
+ # @param [Symbol] name
27
+ # @param [Symbol] association_name
28
+ # @param [Symbol] target_name
29
+ # @return [void]
30
+ def delegate_scope!(name, association_name, target_name: name)
31
+ scope_definition = ->(*args, **kwargs) do
32
+ association = reflect_on_association(association_name)
33
+
34
+ delegated = association.klass.public_send(target_name, *args, **kwargs)
35
+
36
+ if association.has_one? || association.belongs_to?
37
+ joins(association_name).merge(delegated)
38
+ else
39
+ where(id: delegated.select(association.foreign_key))
40
+ end
41
+ end
42
+
43
+ scope name, scope_definition
44
+ end
45
+
46
+ # @see #delegate_scope!
47
+ # @param [<Symbol>] names the names of the scopes to delegate
48
+ # @param [Symbol] to the association to delegate to
49
+ # @return [void]
50
+ def delegate_scopes!(*names, to:)
51
+ # :nocov:
52
+ names.each do |scope|
53
+ delegate_scope! name, to
54
+ end
55
+ # :nocov:
56
+ end
57
+
58
+ # @param [Symbol] name
59
+ # @param [Symbol] association_name
60
+ # @param [Symbol] scope_name
61
+ # @return [void]
62
+ def delegate_lookup!(name, association_name, scope_name: :"lookup_by_#{name}")
63
+ delegate_scope! scope_name, association_name
64
+ end
65
+
66
+ # @see #delegate_lookup!
67
+ # @param [<Symbol>] names the names of the lookups to delegate
68
+ # @param [Symbol] to the association to delegate to
69
+ # @return [void]
70
+ def delegate_lookups!(*names, to:)
71
+ names.each do |name|
72
+ delegate_lookup! name, to
73
+ end
74
+ end
75
+
76
+ # Define a read-only `belongs_to` association
77
+ #
78
+ # @note This is more of a semantic association type in order to avoid the need
79
+ # to declare what to do with dependents or inverse associations when they
80
+ # are not necessary.
81
+ # @param [Symbol] name
82
+ # @param [<Object>] args
83
+ # @param [{ Symbol => Object }] kwargs
84
+ # @yield Set up the belongs_to association
85
+ # @yieldreturn [void]
86
+ # @!macro [attach] belongs_to_readonly
87
+ # @!parse ruby
88
+ # # @note This association is read-only (likely against a database view).
89
+ # belongs_to $1, -> { readonly }, ${2--1}
90
+ def belongs_to_readonly(name, *args, **kwargs, &)
91
+ belongs_to(name, *args, **kwargs, &)
92
+ end
93
+
94
+ # @param [Symbol] association_name
95
+ # @yieldreturn [Boolean]
96
+ # @return [Boolean] whether the model has an association with the given name
97
+ def has_association_for?(association_name)
98
+ assoc = reflect_on_association(association_name)
99
+
100
+ return false if assoc.blank?
101
+
102
+ block_given? ? yield(assoc) : true
103
+ end
104
+
105
+ # Define a read-only `has_many` association.
106
+ #
107
+ # @note This is more of a semantic association type in order to avoid the need
108
+ # to declare what to do with dependents or inverse associations when they
109
+ # are not necessary.
110
+ # @param [Symbol] name
111
+ # @param [<Object>] args
112
+ # @param [{ Symbol => Object }] kwargs
113
+ # @yield Set up the has_many association
114
+ # @yieldreturn [void]
115
+ # @!macro [attach] has_many_readonly
116
+ # @!parse ruby
117
+ # # @note This association is read-only (likely against a database view).
118
+ # has_many $1, -> { readonly }, ${2--1}
119
+ def has_many_readonly(name, *args, **kwargs, &)
120
+ has_many(name, *args, **kwargs, &)
121
+ end
122
+
123
+ # Define a read-only `has_one` association.
124
+ #
125
+ # @note This is more of a semantic association type in order to avoid the need
126
+ # to declare what to do with dependents or inverse associations when they
127
+ # are not necessary.
128
+ # @param [Symbol] name
129
+ # @param [<Object>] args
130
+ # @param [{ Symbol => Object }] kwargs
131
+ # @yield Set up the has_one association
132
+ # @yieldreturn [void]
133
+ # @!macro [attach] has_one_readonly
134
+ # @!parse ruby
135
+ # # @note This association is read-only (likely against a database view).
136
+ # has_one $1, -> { readonly }, ${2--1}
137
+ def has_one_readonly(name, *args, **kwargs, &) = has_one(name, *args, **kwargs, &)
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ # A concern to determine if a given model has any populated associations
6
+ # with a dependent option set to `:restrict_with_error` or `:restrict_with_exception`.
7
+ #
8
+ # It is used for easy validation in destroy mutations.
9
+ module ChecksBeforeDestruction
10
+ extend ActiveSupport::Concern
11
+
12
+ RESTRICTS_WITH = /\Arestrict_with/
13
+
14
+ # @param [Symbol] name
15
+ def has_existing_association?(name) = association(name).scope.exists?
16
+
17
+ def has_protected_dependents? = protected_dependents.any?
18
+
19
+ def protected_dependents
20
+ self.class.protected_associations.each_with_object([]) do |assoc, names|
21
+ names << assoc.name if has_existing_association?(assoc.name)
22
+ end
23
+ end
24
+
25
+ module ClassMethods
26
+ # @return [<ActiveRecord::Reflection::HasManyReflection>]
27
+ def protected_associations
28
+ @protected_associations ||= reflect_on_all_associations.select do |assoc|
29
+ RESTRICTS_WITH.match? assoc.options[:dependent]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ module IdQuoting
6
+ extend ActiveSupport::Concern
7
+
8
+ # Quote the model's primary key if it is persisted and a single string.
9
+ #
10
+ # @return [String, nil]
11
+ def quoted_id
12
+ self.class.connection.quote id if persisted? && (id.kind_of?(String) || id.kind_of?(Integer))
13
+ end
14
+
15
+ module ClassMethods
16
+ # @return [<String>]
17
+ def quoted_ids
18
+ ids.map { |id| connection.quote id }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ module LazyOrdering
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ # @param [Symbol] column
10
+ # @return [ActiveRecord::Relation]
11
+ def lazily_order(column, raw_direction = :asc)
12
+ base = lazy_order_scope_for column
13
+
14
+ expr = lazy_order_expr_for(column, raw_direction:)
15
+
16
+ base.order(expr)
17
+ end
18
+
19
+ def lazy_order_column_for(column)
20
+ method_name = :"#{column}_order_column"
21
+
22
+ if respond_to?(method_name)
23
+ public_send(method_name)
24
+ else
25
+ column
26
+ end.then { |expr| arel_attrify(expr) }
27
+ end
28
+
29
+ def lazy_order_expr_for(column, raw_direction: "asc")
30
+ direction = Hotstepper::Support::Types::SimpleSortDirection[raw_direction]
31
+
32
+ method_name = :"#{column}_order_expression"
33
+
34
+ if respond_to?(method_name)
35
+ public_send(method_name, direction:)
36
+ else
37
+ attr = lazy_order_column_for column
38
+
39
+ attr.public_send(direction)
40
+ end
41
+ end
42
+
43
+ # @param [Symbol] column
44
+ # @return [ActiveRecord::Relation]
45
+ def lazy_order_scope_for(column)
46
+ scope_name = :"prepare_order_for_#{column}"
47
+
48
+ if respond_to?(scope_name)
49
+ all.public_send(scope_name)
50
+ else
51
+ all
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ module LimitToOne
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ # @raise [ActiveRecord::RecordNotFound]
10
+ # @raise [LimitToOne::TooManyMatches]
11
+ # @return [ApplicationRecord]
12
+ def only_one!
13
+ found_count = limit(2).count
14
+
15
+ raise TooManyMatches, "too many matches for scope" if found_count > 1
16
+
17
+ first!
18
+ end
19
+
20
+ # @raise [LimitToOne::TooManyMatches]
21
+ # @return [ApplicationRecord]
22
+ def only_one_or_initialize
23
+ found_count = limit(2).count
24
+
25
+ raise TooManyMatches, "too many matches for scope" if found_count > 1
26
+
27
+ first_or_initialize
28
+ end
29
+ end
30
+
31
+ # @api private
32
+ class TooManyMatches < StandardError; end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ module LookupHelpers
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ def define_simple_lookup!(column_name, scope_name: :"lookup_by_#{column_name}")
10
+ scope scope_name, ->(input) { where(column_name => input) }
11
+ end
12
+
13
+ def define_simple_lookups!(*columns)
14
+ columns.each do |column_name|
15
+ define_simple_lookup! column_name
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ # A concern for a materialized view-backed model.
6
+ #
7
+ # @see Hotstepper::Models::View
8
+ # @note It is intended to integrate with the Scenic gem.
9
+ module MaterializedView
10
+ extend ActiveSupport::Concern
11
+
12
+ include Hotstepper::Models::View
13
+
14
+ included do
15
+ extend Dry::Core::ClassAttributes
16
+
17
+ defines :refreshes_concurrently, type: Hotstepper::Support::Types::Bool
18
+
19
+ refreshes_concurrently true
20
+ end
21
+
22
+ module ClassMethods
23
+ # Checks whether the materialized view has been populated with data.
24
+ def populated? = Scenic.database.populated?(table_name)
25
+
26
+ # @return [void]
27
+ def refresh!(concurrently: refreshes_concurrently, cascade: false)
28
+ Scenic.database.refresh_materialized_view(table_name, concurrently:, cascade:)
29
+ end
30
+
31
+ # @api private
32
+ # @return [void]
33
+ def refreshes_concurrently!
34
+ refreshes_concurrently true
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ # A concern that is included on `ActiveRecord::Base` models.
6
+ module Model
7
+ extend ActiveSupport::Concern
8
+
9
+ include AssociationHelpers
10
+ include ChecksBeforeDestruction
11
+ include IdQuoting
12
+ include LazyOrdering
13
+ include LimitToOne
14
+ include LookupHelpers
15
+ include Querying
16
+ include WhereMatches
17
+ include Hotstepper::Common::CallsOperation
18
+
19
+ included do
20
+ extend Hotstepper::ArelExt::Helpers
21
+ extend Hotstepper::Monadic::Decorators
22
+ end
23
+
24
+ module ClassMethods
25
+ # @api private
26
+ # @note Only used for tests
27
+ def default_factory
28
+ # :nocov:
29
+ model_name.i18n_key
30
+ # :nocov:
31
+ end
32
+
33
+ # @return [void]
34
+ def with_timestamp_scopes!
35
+ include TimestampScopes
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ # Generic querying helpers that don't fit in elsewhere.
6
+ module Querying
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ # Sometimes we need to get a count on something that has aggregations,
11
+ # `DISTINCT ON`, etc. This should handle most cases.
12
+ # @param [Boolean] strip_order
13
+ # @return [Integer]
14
+ def count_from_subquery(strip_order: false)
15
+ subquery = all
16
+
17
+ subquery = subquery.reorder(nil) if strip_order
18
+
19
+ unscoped.from(subquery).count
20
+ end
21
+
22
+ # @param [<ApplicationRecord>] records
23
+ # @return [ActiveRecord::Relation<ApplicationRecord>]
24
+ def limited_to(*records)
25
+ flattened = records.flatten
26
+
27
+ return all if flattened.blank?
28
+
29
+ id = flattened.filter_map do |record|
30
+ case record
31
+ when self then record.id
32
+ when ::Hotstepper::Support::Types::UUID, Hotstepper::Support::Types::Integer then record
33
+ end
34
+ end.uniq
35
+
36
+ id.blank? ? none : where(id:)
37
+ end
38
+
39
+ # @param [Integer, nil] number of records to return, or nil to return a single record
40
+ # @return [ApplicationRecord, ActiveRecord::Relation<ApplicationRecord>, nil]
41
+ def sample(num = nil)
42
+ randomized = reorder(Arel.sql("RANDOM()"))
43
+
44
+ if num.is_a?(Integer) && num >= 1
45
+ randomized.limit(num)
46
+ else
47
+ randomized.first
48
+ end
49
+ end
50
+
51
+ alias scoped_sample sample
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ # @note Not automatically included in {Hotstepper::Models::Model},
6
+ # since not all models will have timestamps.
7
+ module TimestampScopes
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ scope :in_recent_order, -> { order(created_at: :desc) }
12
+ scope :in_oldest_order, -> { order(created_at: :asc) }
13
+ scope :most_recently_updated, -> { order(updated_at: :desc) }
14
+ scope :least_recently_updated, -> { order(updated_at: :asc) }
15
+ end
16
+
17
+ module ClassMethods
18
+ # @return [ApplicationRecord, nil]
19
+ def latest = in_recent_order.first
20
+
21
+ # @return [ApplicationRecord, nil]
22
+ def oldest = in_oldest_order.first
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ # A concern for a view-backed model.
6
+ #
7
+ # @see Hotstepper::Models::MaterializedView
8
+ module View
9
+ extend ActiveSupport::Concern
10
+
11
+ def readonly? = true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hotstepper
4
+ module Models
5
+ # A concern for building `LIKE` / `ILIKE` queries.
6
+ module WhereMatches
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def where_matches(_wildcard: false, _escape: true, _case_sensitive: false, _or: false, **pairs)
11
+ counter = 0
12
+
13
+ pairs.reduce(all) do |scope, (column, value)|
14
+ counter += 1
15
+
16
+ next scope if value.blank?
17
+
18
+ conditions = arel_or_expressions value do |actual_value|
19
+ needle = wrap_like_value_in_wildcard(actual_value, _wildcard, escape: _escape)
20
+
21
+ arel_table[column].matches(needle, nil, _case_sensitive)
22
+ end
23
+
24
+ if _or && counter > 1
25
+ scope.or(scope.unscoped.where(conditions))
26
+ else
27
+ scope.where conditions
28
+ end
29
+ end
30
+ end
31
+
32
+ def where_like(_wildcard: :both, **pairs)
33
+ where_matches(**pairs, _wildcard:)
34
+ end
35
+
36
+ def where_contains(**pairs)
37
+ where_matches _wildcard: :both, **pairs
38
+ end
39
+
40
+ def where_begins_like(**pairs)
41
+ where_matches _wildcard: :end, **pairs
42
+ end
43
+
44
+ def where_ends_like(**pairs)
45
+ where_matches _wildcard: :start, **pairs
46
+ end
47
+
48
+ private
49
+
50
+ def wrap_like_value_in_wildcard(value, wildcard_type = :both, escape: true)
51
+ needle = escape ? value.to_s.gsub(?%, "\\%").gsub(?_, "\\_") : value
52
+
53
+ case wildcard_type
54
+ when :start
55
+ "%#{needle}"
56
+ when :end
57
+ "#{needle}%"
58
+ when :both
59
+ "%#{needle}%"
60
+ else
61
+ needle.to_s
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end