cuprum-collections 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +59 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/DEVELOPMENT.md +25 -0
  5. data/LICENSE +22 -0
  6. data/README.md +950 -0
  7. data/lib/cuprum/collections/base.rb +11 -0
  8. data/lib/cuprum/collections/basic/collection.rb +135 -0
  9. data/lib/cuprum/collections/basic/command.rb +112 -0
  10. data/lib/cuprum/collections/basic/commands/assign_one.rb +54 -0
  11. data/lib/cuprum/collections/basic/commands/build_one.rb +45 -0
  12. data/lib/cuprum/collections/basic/commands/destroy_one.rb +48 -0
  13. data/lib/cuprum/collections/basic/commands/find_many.rb +65 -0
  14. data/lib/cuprum/collections/basic/commands/find_matching.rb +126 -0
  15. data/lib/cuprum/collections/basic/commands/find_one.rb +49 -0
  16. data/lib/cuprum/collections/basic/commands/insert_one.rb +50 -0
  17. data/lib/cuprum/collections/basic/commands/update_one.rb +52 -0
  18. data/lib/cuprum/collections/basic/commands/validate_one.rb +69 -0
  19. data/lib/cuprum/collections/basic/commands.rb +18 -0
  20. data/lib/cuprum/collections/basic/query.rb +160 -0
  21. data/lib/cuprum/collections/basic/query_builder.rb +69 -0
  22. data/lib/cuprum/collections/basic/rspec/command_contract.rb +392 -0
  23. data/lib/cuprum/collections/basic/rspec.rb +8 -0
  24. data/lib/cuprum/collections/basic.rb +22 -0
  25. data/lib/cuprum/collections/command.rb +26 -0
  26. data/lib/cuprum/collections/commands/abstract_find_many.rb +77 -0
  27. data/lib/cuprum/collections/commands/abstract_find_matching.rb +64 -0
  28. data/lib/cuprum/collections/commands/abstract_find_one.rb +44 -0
  29. data/lib/cuprum/collections/commands.rb +8 -0
  30. data/lib/cuprum/collections/constraints/attribute_name.rb +22 -0
  31. data/lib/cuprum/collections/constraints/order/attributes_array.rb +26 -0
  32. data/lib/cuprum/collections/constraints/order/attributes_hash.rb +27 -0
  33. data/lib/cuprum/collections/constraints/order/complex_ordering.rb +46 -0
  34. data/lib/cuprum/collections/constraints/order/sort_direction.rb +32 -0
  35. data/lib/cuprum/collections/constraints/order.rb +8 -0
  36. data/lib/cuprum/collections/constraints/ordering.rb +114 -0
  37. data/lib/cuprum/collections/constraints/query_hash.rb +25 -0
  38. data/lib/cuprum/collections/constraints.rb +8 -0
  39. data/lib/cuprum/collections/errors/already_exists.rb +86 -0
  40. data/lib/cuprum/collections/errors/extra_attributes.rb +66 -0
  41. data/lib/cuprum/collections/errors/failed_validation.rb +66 -0
  42. data/lib/cuprum/collections/errors/invalid_parameters.rb +50 -0
  43. data/lib/cuprum/collections/errors/invalid_query.rb +55 -0
  44. data/lib/cuprum/collections/errors/missing_default_contract.rb +49 -0
  45. data/lib/cuprum/collections/errors/not_found.rb +81 -0
  46. data/lib/cuprum/collections/errors/unknown_operator.rb +71 -0
  47. data/lib/cuprum/collections/errors.rb +8 -0
  48. data/lib/cuprum/collections/queries/ordering.rb +74 -0
  49. data/lib/cuprum/collections/queries/parse.rb +22 -0
  50. data/lib/cuprum/collections/queries/parse_block.rb +206 -0
  51. data/lib/cuprum/collections/queries/parse_strategy.rb +91 -0
  52. data/lib/cuprum/collections/queries.rb +25 -0
  53. data/lib/cuprum/collections/query.rb +247 -0
  54. data/lib/cuprum/collections/query_builder.rb +61 -0
  55. data/lib/cuprum/collections/rspec/assign_one_command_contract.rb +168 -0
  56. data/lib/cuprum/collections/rspec/build_one_command_contract.rb +93 -0
  57. data/lib/cuprum/collections/rspec/collection_contract.rb +153 -0
  58. data/lib/cuprum/collections/rspec/destroy_one_command_contract.rb +106 -0
  59. data/lib/cuprum/collections/rspec/find_many_command_contract.rb +327 -0
  60. data/lib/cuprum/collections/rspec/find_matching_command_contract.rb +194 -0
  61. data/lib/cuprum/collections/rspec/find_one_command_contract.rb +154 -0
  62. data/lib/cuprum/collections/rspec/fixtures.rb +89 -0
  63. data/lib/cuprum/collections/rspec/insert_one_command_contract.rb +83 -0
  64. data/lib/cuprum/collections/rspec/query_builder_contract.rb +92 -0
  65. data/lib/cuprum/collections/rspec/query_contract.rb +650 -0
  66. data/lib/cuprum/collections/rspec/querying_contract.rb +298 -0
  67. data/lib/cuprum/collections/rspec/update_one_command_contract.rb +79 -0
  68. data/lib/cuprum/collections/rspec/validate_one_command_contract.rb +96 -0
  69. data/lib/cuprum/collections/rspec.rb +8 -0
  70. data/lib/cuprum/collections/version.rb +59 -0
  71. data/lib/cuprum/collections.rb +26 -0
  72. metadata +219 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/error'
4
+ require 'sleeping_king_studios/tools/toolbelt'
5
+
6
+ require 'cuprum/collections/errors'
7
+
8
+ module Cuprum::Collections::Errors
9
+ # Returned when a find command does not find the requested items.
10
+ class NotFound < Cuprum::Error
11
+ # Short string used to identify the type of error.
12
+ TYPE = 'cuprum.collections.errors.not_found'
13
+
14
+ # @param collection_name [String, Symbol] The name of the collection.
15
+ # @param primary_key_name [String, Symbol] The name of the primary key
16
+ # attribute.
17
+ # @param primary_key_values [Object, Array] The expected values of the
18
+ # primary key attribute.
19
+ def initialize(collection_name:, primary_key_name:, primary_key_values:)
20
+ @collection_name = collection_name
21
+ @primary_key_name = primary_key_name
22
+ @primary_key_values = Array(primary_key_values)
23
+
24
+ super(message: default_message)
25
+ end
26
+
27
+ # @return [String, Symbol] the name of the collection.
28
+ attr_reader :collection_name
29
+
30
+ # @return [String, Symbol] the name of the primary key attribute.
31
+ attr_reader :primary_key_name
32
+
33
+ # @return [Array] The expected values of the primary key attribute.
34
+ attr_reader :primary_key_values
35
+
36
+ # @return [Hash] a serializable hash representation of the error.
37
+ def as_json
38
+ {
39
+ 'data' => {
40
+ 'collection_name' => collection_name,
41
+ 'primary_key_name' => primary_key_name,
42
+ 'primary_key_values' => primary_key_values
43
+ },
44
+ 'message' => message,
45
+ 'type' => type
46
+ }
47
+ end
48
+
49
+ # @return [String] short string used to identify the type of error.
50
+ def type
51
+ TYPE
52
+ end
53
+
54
+ private
55
+
56
+ def default_message
57
+ primary_keys = primary_key_values.map(&:inspect).join(', ')
58
+
59
+ "#{entity_name} not found with #{primary_key_name} #{primary_keys}"
60
+ end
61
+
62
+ def entity_name
63
+ entity_name = collection_name
64
+ entity_name = tools.str.singularize(entity_name) if singular?
65
+
66
+ titleize(entity_name)
67
+ end
68
+
69
+ def singular?
70
+ primary_key_values.size == 1
71
+ end
72
+
73
+ def titleize(string)
74
+ tools.str.underscore(string).split('_').map(&:capitalize).join(' ')
75
+ end
76
+
77
+ def tools
78
+ SleepingKingStudios::Tools::Toolbelt.instance
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/errors'
4
+ require 'cuprum/collections/queries'
5
+
6
+ module Cuprum::Collections::Errors
7
+ # An error returned when a query attempts to filter by an unknown operator.
8
+ class UnknownOperator < Cuprum::Error
9
+ # Short string used to identify the type of error.
10
+ TYPE = 'cuprum.collections.errors.unknown_operator'
11
+
12
+ # @param operator [String, Symbol] The unknown operator.
13
+ def initialize(operator:)
14
+ @operator = operator
15
+
16
+ super(
17
+ message: generate_message,
18
+ operator: operator
19
+ )
20
+ end
21
+
22
+ # @return [String, Symbol] the unknown operator.
23
+ attr_reader :operator
24
+
25
+ # @return [Hash] a serializable hash representation of the error.
26
+ def as_json
27
+ {
28
+ 'data' => {
29
+ 'corrections' => corrections,
30
+ 'operator' => operator
31
+ },
32
+ 'message' => message,
33
+ 'type' => type
34
+ }
35
+ end
36
+
37
+ # @return [Array<String>] Suggested possible values for the operator.
38
+ def corrections
39
+ @corrections ||=
40
+ DidYouMean::SpellChecker
41
+ .new(dictionary: Cuprum::Collections::Queries::VALID_OPERATORS.to_a)
42
+ .correct(operator)
43
+ end
44
+
45
+ # @return [String] short string used to identify the type of error.
46
+ def type
47
+ TYPE
48
+ end
49
+
50
+ private
51
+
52
+ def generate_message
53
+ message = "unknown operator #{operator.inspect}"
54
+
55
+ return message if corrections.empty?
56
+
57
+ "#{message} - did you mean #{suggestion}?"
58
+ end
59
+
60
+ def suggestion
61
+ tools.ary.humanize_list(
62
+ corrections.map(&:inspect),
63
+ last_separator: ', or '
64
+ )
65
+ end
66
+
67
+ def tools
68
+ SleepingKingStudios::Tools::Toolbelt.instance
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections'
4
+
5
+ module Cuprum::Collections
6
+ # Namespace for errors, which represent failure states of commands.
7
+ module Errors; end
8
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/constraints/ordering'
4
+ require 'cuprum/collections/queries'
5
+
6
+ module Cuprum::Collections::Queries
7
+ # Functionality around validating and normalizing query sort orderings.
8
+ module Ordering
9
+ # Exception class for handling invalid order keywords.
10
+ class InvalidOrderError < ArgumentError; end
11
+
12
+ ORDER_HASH_VALUES = {
13
+ asc: :asc,
14
+ ascending: :asc,
15
+ desc: :desc,
16
+ descending: :desc
17
+ }.freeze
18
+ private_constant :ORDER_HASH_VALUES
19
+
20
+ class << self
21
+ # @overload normalize(*attributes, ordering_hash = nil)
22
+ # Converts the given sort order into a hash with standard values.
23
+ #
24
+ # @param attributes [Array<String, Symbol>] The attribute names to sort
25
+ # by, in ascending direction, and in order of importance.
26
+ # @param ordering_hash [Hash] An optional ordering hash, with keys that
27
+ # are valid attribute names and values that are valid sort directions.
28
+ #
29
+ # @return [Hash] the normalized sort ordering.
30
+ #
31
+ # @raise InvalidOrderError if any of the attributes are invalid
32
+ # attribute names.
33
+ # @raise InvalidOrderError if any of the hash keys are invalid attribute
34
+ # names, or any of the hash values are invalid sort directions.
35
+ def normalize(*attributes)
36
+ validate_ordering!(attributes)
37
+
38
+ qualified = attributes.last.is_a?(Hash) ? attributes.pop : {}
39
+ qualified = normalize_order_hash(qualified)
40
+
41
+ attributes
42
+ .each
43
+ .with_object({}) { |attribute, hsh| hsh[attribute.intern] = :asc }
44
+ .merge(qualified)
45
+ end
46
+
47
+ private
48
+
49
+ def normalize_order_hash(hsh)
50
+ hsh.each.with_object({}) do |(key, value), normalized|
51
+ normalized[key.intern] = normalize_order_hash_value(value)
52
+ end
53
+ end
54
+
55
+ def normalize_order_hash_value(value)
56
+ value = value.downcase if value.is_a?(String)
57
+
58
+ ORDER_HASH_VALUES.fetch(value.is_a?(String) ? value.intern : value)
59
+ end
60
+
61
+ def ordering_constraint
62
+ Cuprum::Collections::Constraints::Ordering.instance
63
+ end
64
+
65
+ def validate_ordering!(attributes)
66
+ return if ordering_constraint.matches?(attributes)
67
+
68
+ raise InvalidOrderError,
69
+ 'order must be a list of attribute names and/or a hash of attribute' \
70
+ ' names with values :asc or :desc'
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/queries'
4
+ require 'cuprum/collections/queries/parse_strategy'
5
+
6
+ module Cuprum::Collections::Queries
7
+ # Command to parse parameters passed to Query#where into criteria.
8
+ class Parse < Cuprum::Command
9
+ private
10
+
11
+ def process(where:, strategy: nil)
12
+ command = step do
13
+ Cuprum::Collections::Queries::ParseStrategy.new.call(
14
+ strategy: strategy,
15
+ where: where
16
+ )
17
+ end
18
+
19
+ command.call(where: where)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require 'cuprum/errors/uncaught_exception'
6
+ require 'stannum/contracts/parameters_contract'
7
+
8
+ require 'cuprum/collections/command'
9
+ require 'cuprum/collections/constraints/query_hash'
10
+ require 'cuprum/collections/errors/invalid_query'
11
+ require 'cuprum/collections/errors/unknown_operator'
12
+ require 'cuprum/collections/queries'
13
+
14
+ module Cuprum::Collections::Queries
15
+ # Command for parsing a Query#where block into criteria.
16
+ #
17
+ # @example An Empty Query
18
+ # command = Cuprum::Collections::Queries::ParseBlock.new
19
+ # result = command.call { {} }
20
+ # result.value #=> []
21
+ #
22
+ # @example A Value Query
23
+ # command = Cuprum::Collections::Queries::ParseBlock.new
24
+ # result = command.call do
25
+ # {
26
+ # author: 'Nnedi Okorafor',
27
+ # series: 'Binti',
28
+ # genre: 'Africanfuturism'
29
+ # }
30
+ # end
31
+ # result.value #=>
32
+ # # [
33
+ # # ['author', :eq, 'Nnedi Okorafor'],
34
+ # # ['series', :eq, 'Binti'],
35
+ # # ['genre', :eq, 'Africanfuturism']
36
+ # # ]
37
+ #
38
+ # @example A Query With Operators
39
+ # command = Cuprum::Collections::Queries::ParseBlock.new
40
+ # result = command.call do
41
+ # {
42
+ # author: equal('Nnedi Okorafor'),
43
+ # series: not_equal('Binti')
44
+ # }
45
+ # end
46
+ # result.value #=>
47
+ # # [
48
+ # # ['author', :eq, 'Nnedi Okorafor'],
49
+ # # ['series', :ne, 'Binti']
50
+ # # ]
51
+ class ParseBlock < Cuprum::Collections::Command
52
+ # Evaluation context for query blocks.
53
+ class Builder < BasicObject
54
+ # Generates an equality criterion.
55
+ #
56
+ # @return [Array] the equality criterion.
57
+ def equals(value)
58
+ [nil, Operators::EQUAL, value]
59
+ end
60
+ alias equal equals
61
+ alias eq equals
62
+
63
+ # Generates a greater than comparison criterion.
64
+ #
65
+ # @return [Array] the greater than criterion.
66
+ def greater_than(value)
67
+ [nil, Operators::GREATER_THAN, value]
68
+ end
69
+ alias gt greater_than
70
+
71
+ # Generates a greater than or equal to comparison criterion.
72
+ #
73
+ # @return [Array] the greater than or equal to criterion.
74
+ def greater_than_or_equal_to(value)
75
+ [nil, Operators::GREATER_THAN_OR_EQUAL_TO, value]
76
+ end
77
+ alias gte greater_than_or_equal_to
78
+
79
+ # Generates a less than comparison criterion.
80
+ #
81
+ # @return [Array] the less than criterion.
82
+ def less_than(value)
83
+ [nil, Operators::LESS_THAN, value]
84
+ end
85
+ alias lt less_than
86
+
87
+ # Generates a less than or equal to comparison criterion.
88
+ #
89
+ # @return [Array] the less than or equal to criterion.
90
+ def less_than_or_equal_to(value)
91
+ [nil, Operators::LESS_THAN_OR_EQUAL_TO, value]
92
+ end
93
+ alias lte less_than_or_equal_to
94
+
95
+ # Generates a negated equality criterion.
96
+ #
97
+ # @return [Array] the negated equality criterion.
98
+ def not_equal(value)
99
+ [nil, Operators::NOT_EQUAL, value]
100
+ end
101
+ alias ne not_equal
102
+
103
+ # Generates a negated inclusion criterion.
104
+ #
105
+ # @return [Array] the negated inclusion criterion.
106
+ def not_one_of(value)
107
+ [nil, Operators::NOT_ONE_OF, value]
108
+ end
109
+
110
+ # Generates an inclusion criterion.
111
+ #
112
+ # @return [Array] the inclusion criterion.
113
+ def one_of(value)
114
+ [nil, Operators::ONE_OF, value]
115
+ end
116
+ end
117
+
118
+ class << self
119
+ extend Forwardable
120
+
121
+ def_delegators :validation_contract,
122
+ :errors_for,
123
+ :match,
124
+ :matches?
125
+
126
+ private
127
+
128
+ def validation_contract
129
+ self::MethodValidations.contracts.fetch(:call)
130
+ end
131
+ end
132
+
133
+ validate_parameters :call do
134
+ keyword :where, Proc
135
+ end
136
+
137
+ private
138
+
139
+ def call_block(&block)
140
+ handle_unknown_operator { Builder.new.instance_exec(&block) }
141
+ rescue StandardError => exception
142
+ error = Cuprum::Errors::UncaughtException.new(
143
+ exception: exception,
144
+ message: 'uncaught exception when parsing query block'
145
+ )
146
+
147
+ failure(error)
148
+ end
149
+
150
+ def generate_criteria(hsh)
151
+ hsh.map do |key, value|
152
+ unless partial_criterion?(value)
153
+ next [key.to_s, Cuprum::Collections::Queries::Operators::EQUAL, value]
154
+ end
155
+
156
+ value.tap { |ary| ary[0] = key.to_s }
157
+ end
158
+ end
159
+
160
+ def handle_unknown_operator
161
+ yield
162
+ rescue NoMethodError => exception
163
+ error = Cuprum::Collections::Errors::UnknownOperator.new(
164
+ operator: exception.name
165
+ )
166
+
167
+ failure(error)
168
+ end
169
+
170
+ def invalid_query_error(errors:, message: nil)
171
+ Cuprum::Collections::Errors::InvalidQuery.new(
172
+ errors: errors,
173
+ message: message,
174
+ strategy: :block
175
+ )
176
+ end
177
+
178
+ def partial_criterion?(obj)
179
+ return false unless obj.is_a?(Array) && obj.size == 3
180
+
181
+ attribute, operator, _value = obj
182
+
183
+ return false unless attribute.nil?
184
+
185
+ Cuprum::Collections::Queries::VALID_OPERATORS.include?(operator)
186
+ end
187
+
188
+ def process(where:)
189
+ hsh = step { call_block(&where) }
190
+
191
+ step { validate_hash(hsh) }
192
+
193
+ generate_criteria(hsh)
194
+ end
195
+
196
+ def validate_hash(obj)
197
+ constraint = Cuprum::Collections::Constraints::QueryHash.new
198
+ match, errors = constraint.match(obj)
199
+
200
+ return if match
201
+
202
+ message = 'query block returned invalid value'
203
+ failure(invalid_query_error(errors: errors, message: message))
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/collections/queries'
4
+ require 'cuprum/collections/queries/parse_block'
5
+ require 'cuprum/collections/errors/invalid_query'
6
+
7
+ module Cuprum::Collections::Queries
8
+ # Command to select the parsing strategy for parsing Query#where parameters.
9
+ class ParseStrategy < Cuprum::Command
10
+ STRATEGIES = {
11
+ block: Cuprum::Collections::Queries::ParseBlock
12
+ }.freeze
13
+ private_constant :STRATEGIES
14
+
15
+ # The :type of the error generated for an unknown parsing strategy.
16
+ UNKNOWN_STRATEGY_ERROR =
17
+ 'cuprum.collections.errors.queries.unknown_strategy'
18
+
19
+ private
20
+
21
+ def find_and_validate_strategy(strategy:, where:)
22
+ command_class = step { find_strategy_by_key(strategy: strategy) }
23
+ parameters = {
24
+ arguments: [],
25
+ block: nil,
26
+ keywords: { where: where }
27
+ }
28
+
29
+ return command_class if command_class.matches?(parameters)
30
+
31
+ errors = command_class.errors_for(parameters)
32
+
33
+ failure(invalid_parameters_error(errors: errors, strategy: strategy))
34
+ end
35
+
36
+ def find_strategy(strategy:, where:)
37
+ if strategy
38
+ return find_and_validate_strategy(strategy: strategy, where: where)
39
+ end
40
+
41
+ command_class = find_strategy_by_parameters(where: where)
42
+
43
+ return command_class if command_class
44
+
45
+ failure(unknown_strategy_error(strategy: strategy))
46
+ end
47
+
48
+ def find_strategy_by_key(strategy:)
49
+ STRATEGIES.fetch(strategy) do
50
+ failure(unknown_strategy_error(strategy: strategy))
51
+ end
52
+ end
53
+
54
+ def find_strategy_by_parameters(where:)
55
+ STRATEGIES
56
+ .values
57
+ .find do |command_class|
58
+ command_class.matches?(
59
+ arguments: [],
60
+ block: nil,
61
+ keywords: { where: where }
62
+ )
63
+ end
64
+ end
65
+
66
+ def invalid_parameters_error(errors:, strategy:)
67
+ Cuprum::Collections::Errors::InvalidQuery.new(
68
+ errors: errors,
69
+ strategy: strategy
70
+ )
71
+ end
72
+
73
+ def process(strategy: nil, where: nil)
74
+ command_class = step do
75
+ find_strategy(strategy: strategy, where: where)
76
+ end
77
+
78
+ command_class.new
79
+ end
80
+
81
+ def unknown_strategy_error(strategy:)
82
+ errors = Stannum::Errors.new
83
+ errors[:strategy].add(UNKNOWN_STRATEGY_ERROR, strategy: strategy)
84
+
85
+ Cuprum::Collections::Errors::InvalidQuery.new(
86
+ errors: errors,
87
+ strategy: strategy
88
+ )
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbox/constant_map'
4
+
5
+ require 'cuprum/collections'
6
+
7
+ module Cuprum::Collections
8
+ # Namespace for internal functionality for implementing collection queries.
9
+ module Queries
10
+ # Defines the supported operators for a Query.
11
+ Operators = SleepingKingStudios::Tools::Toolbox::ConstantMap.new(
12
+ EQUAL: :equal,
13
+ GREATER_THAN: :greater_than,
14
+ GREATER_THAN_OR_EQUAL_TO: :greater_than_or_equal_to,
15
+ LESS_THAN: :less_than,
16
+ LESS_THAN_OR_EQUAL_TO: :less_than_or_equal_to,
17
+ NOT_EQUAL: :not_equal,
18
+ NOT_ONE_OF: :not_one_of,
19
+ ONE_OF: :one_of
20
+ ).freeze
21
+
22
+ # Enumerates the valid operators as a Set for performant lookup.
23
+ VALID_OPERATORS = Set.new(Operators.values).freeze
24
+ end
25
+ end